Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Developer Balance Sample app #544

Merged
merged 3 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions 9.0/Apps/DeveloperBalance/DeveloperBalance.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.35525.253 main
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeveloperBalance", "DeveloperBalance\DeveloperBalance.csproj", "{02E9826F-36BF-5316-B6F5-69DDC6CA1793}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{02E9826F-36BF-5316-B6F5-69DDC6CA1793}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{02E9826F-36BF-5316-B6F5-69DDC6CA1793}.Debug|Any CPU.Build.0 = Debug|Any CPU
{02E9826F-36BF-5316-B6F5-69DDC6CA1793}.Release|Any CPU.ActiveCfg = Release|Any CPU
{02E9826F-36BF-5316-B6F5-69DDC6CA1793}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BF39D4F9-C00C-4751-A4B9-A31E47B152C3}
EndGlobalSection
EndGlobal
15 changes: 15 additions & 0 deletions 9.0/Apps/DeveloperBalance/DeveloperBalance/App.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version = "1.0" encoding = "UTF-8" ?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:DeveloperBalance"
x:Class="DeveloperBalance.App">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
<ResourceDictionary Source="Resources/Styles/AppStyles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
14 changes: 14 additions & 0 deletions 9.0/Apps/DeveloperBalance/DeveloperBalance/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace DeveloperBalance;

public partial class App : Application
{
public App()
{
InitializeComponent();
}

protected override Window CreateWindow(IActivationState? activationState)
{
return new Window(new AppShell());
}
}
44 changes: 44 additions & 0 deletions 9.0/Apps/DeveloperBalance/DeveloperBalance/AppShell.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Shell
x:Class="DeveloperBalance.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:sf="clr-namespace:Syncfusion.Maui.Toolkit.SegmentedControl;assembly=Syncfusion.Maui.Toolkit"
xmlns:pages="clr-namespace:DeveloperBalance.Pages"
Shell.FlyoutBehavior="Flyout"
Title="DeveloperBalance">

<ShellContent
Title="Dashboard"
Icon="{StaticResource IconDashboard}"
ContentTemplate="{DataTemplate pages:MainPage}"
Route="main" />

<ShellContent
Title="Projects"
Icon="{StaticResource IconProjects}"
ContentTemplate="{DataTemplate pages:ProjectListPage}"
Route="projects" />

<ShellContent
Title="Manage Meta"
Icon="{StaticResource IconMeta}"
ContentTemplate="{DataTemplate pages:ManageMetaPage}"
Route="manage" />

<Shell.FlyoutFooter>
<Grid Padding="15">
<sf:SfSegmentedControl x:Name="ThemeSegmentedControl"
VerticalOptions="Center" HorizontalOptions="Center" SelectionChanged="SfSegmentedControl_SelectionChanged"
SegmentWidth="40" SegmentHeight="40">
<sf:SfSegmentedControl.ItemsSource>
<x:Array Type="{x:Type sf:SfSegmentItem}">
<sf:SfSegmentItem ImageSource="{StaticResource IconLight}"/>
<sf:SfSegmentItem ImageSource="{StaticResource IconDark}"/>
</x:Array>
</sf:SfSegmentedControl.ItemsSource>
</sf:SfSegmentedControl>
</Grid>
</Shell.FlyoutFooter>

</Shell>
49 changes: 49 additions & 0 deletions 9.0/Apps/DeveloperBalance/DeveloperBalance/AppShell.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using CommunityToolkit.Maui.Alerts;
using CommunityToolkit.Maui.Core;
using Font = Microsoft.Maui.Font;
namespace DeveloperBalance;

public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
var currentTheme = Application.Current!.UserAppTheme;
ThemeSegmentedControl.SelectedIndex = currentTheme == AppTheme.Light ? 0 : 1;
}
public static async Task DisplaySnackbarAsync(string message)
{
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();

var snackbarOptions = new SnackbarOptions
{
BackgroundColor = Color.FromArgb("#FF3300"),
TextColor = Colors.White,
ActionButtonTextColor = Colors.Yellow,
CornerRadius = new CornerRadius(0),
Font = Font.SystemFontOfSize(18),
ActionButtonFont = Font.SystemFontOfSize(14)
};

var snackbar = Snackbar.Make(message, visualOptions: snackbarOptions);

await snackbar.Show(cancellationTokenSource.Token);
}

public static async Task DisplayToastAsync(string message)
{
// Toast is currently not working in MCT on Windows
if (OperatingSystem.IsWindows())
return;

var toast = Toast.Make(message, textSize: 18);

var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await toast.Show(cts.Token);
}

private void SfSegmentedControl_SelectionChanged(object sender, Syncfusion.Maui.Toolkit.SegmentedControl.SelectionChangedEventArgs e)
{
Application.Current!.UserAppTheme = e.NewIndex == 0 ? AppTheme.Light : AppTheme.Dark;
}
}
184 changes: 184 additions & 0 deletions 9.0/Apps/DeveloperBalance/DeveloperBalance/Data/CategoryRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
using DeveloperBalance.Models;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;

namespace DeveloperBalance.Data;

/// <summary>
/// Repository class for managing categories in the database.
/// </summary>
public class CategoryRepository
{
private bool _hasBeenInitialized = false;
private readonly ILogger _logger;

/// <summary>
/// Initializes a new instance of the <see cref="CategoryRepository"/> class.
/// </summary>
/// <param name="logger">The logger instance.</param>
public CategoryRepository(ILogger<CategoryRepository> logger)
{
_logger = logger;
}

/// <summary>
/// Initializes the database connection and creates the Category table if it does not exist.
/// </summary>
private async Task Init()
{
if (_hasBeenInitialized)
return;

await using var connection = new SqliteConnection(Constants.DatabasePath);
await connection.OpenAsync();

try
{
var createTableCmd = connection.CreateCommand();
createTableCmd.CommandText = @"
CREATE TABLE IF NOT EXISTS Category (
ID INTEGER PRIMARY KEY AUTOINCREMENT,
Title TEXT NOT NULL,
Color TEXT NOT NULL
);";
await createTableCmd.ExecuteNonQueryAsync();
}
catch (Exception e)
{
_logger.LogError(e, "Error creating Category table");
throw;
}

_hasBeenInitialized = true;
}

/// <summary>
/// Retrieves a list of all categories from the database.
/// </summary>
/// <returns>A list of <see cref="Category"/> objects.</returns>
public async Task<List<Category>> ListAsync()
{
await Init();
await using var connection = new SqliteConnection(Constants.DatabasePath);
await connection.OpenAsync();

var selectCmd = connection.CreateCommand();
selectCmd.CommandText = "SELECT * FROM Category";
var categories = new List<Category>();

await using var reader = await selectCmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
categories.Add(new Category
{
ID = reader.GetInt32(0),
Title = reader.GetString(1),
Color = reader.GetString(2)
});
}

return categories;
}

/// <summary>
/// Retrieves a specific category by its ID.
/// </summary>
/// <param name="id">The ID of the category.</param>
/// <returns>A <see cref="Category"/> object if found; otherwise, null.</returns>
public async Task<Category?> GetAsync(int id)
{
await Init();
await using var connection = new SqliteConnection(Constants.DatabasePath);
await connection.OpenAsync();

var selectCmd = connection.CreateCommand();
selectCmd.CommandText = "SELECT * FROM Category WHERE ID = @id";
selectCmd.Parameters.AddWithValue("@id", id);

await using var reader = await selectCmd.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
return new Category
{
ID = reader.GetInt32(0),
Title = reader.GetString(1),
Color = reader.GetString(2)
};
}

return null;
}

/// <summary>
/// Saves a category to the database. If the category ID is 0, a new category is created; otherwise, the existing category is updated.
/// </summary>
/// <param name="item">The category to save.</param>
/// <returns>The ID of the saved category.</returns>
public async Task<int> SaveItemAsync(Category item)
{
await Init();
await using var connection = new SqliteConnection(Constants.DatabasePath);
await connection.OpenAsync();

var saveCmd = connection.CreateCommand();
if (item.ID == 0)
{
saveCmd.CommandText = @"
INSERT INTO Category (Title, Color)
VALUES (@Title, @Color);
SELECT last_insert_rowid();";
}
else
{
saveCmd.CommandText = @"
UPDATE Category SET Title = @Title, Color = @Color
WHERE ID = @ID";
saveCmd.Parameters.AddWithValue("@ID", item.ID);
}

saveCmd.Parameters.AddWithValue("@Title", item.Title);
saveCmd.Parameters.AddWithValue("@Color", item.Color);

var result = await saveCmd.ExecuteScalarAsync();
if (item.ID == 0)
{
item.ID = Convert.ToInt32(result);
}

return item.ID;
}

/// <summary>
/// Deletes a category from the database.
/// </summary>
/// <param name="item">The category to delete.</param>
/// <returns>The number of rows affected.</returns>
public async Task<int> DeleteItemAsync(Category item)
{
await Init();
await using var connection = new SqliteConnection(Constants.DatabasePath);
await connection.OpenAsync();

var deleteCmd = connection.CreateCommand();
deleteCmd.CommandText = "DELETE FROM Category WHERE ID = @id";
deleteCmd.Parameters.AddWithValue("@id", item.ID);

return await deleteCmd.ExecuteNonQueryAsync();
}

/// <summary>
/// Drops the Category table from the database.
/// </summary>
public async Task DropTableAsync()
{
await Init();
await using var connection = new SqliteConnection(Constants.DatabasePath);
await connection.OpenAsync();

var dropTableCmd = connection.CreateCommand();
dropTableCmd.CommandText = "DROP TABLE IF EXISTS Category";

await dropTableCmd.ExecuteNonQueryAsync();
_hasBeenInitialized = false;
}
}
9 changes: 9 additions & 0 deletions 9.0/Apps/DeveloperBalance/DeveloperBalance/Data/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace DeveloperBalance.Data;

public static class Constants
{
public const string DatabaseFilename = "AppSQLite.db3";

public static string DatabasePath =>
$"Data Source={Path.Combine(FileSystem.AppDataDirectory, DatabaseFilename)}";
}
11 changes: 11 additions & 0 deletions 9.0/Apps/DeveloperBalance/DeveloperBalance/Data/JsonContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Text.Json.Serialization;
using DeveloperBalance.Models;

[JsonSerializable(typeof(Project))]
[JsonSerializable(typeof(ProjectTask))]
[JsonSerializable(typeof(ProjectsJson))]
[JsonSerializable(typeof(Category))]
[JsonSerializable(typeof(Tag))]
public partial class JsonContext : JsonSerializerContext
{
}
Loading
Loading