Skip to content

Commit

Permalink
Merge pull request #544 from dotnet/add-developerbalance
Browse files Browse the repository at this point in the history
Add Developer Balance Sample app
  • Loading branch information
jfversluis authored Dec 5, 2024
2 parents fe2bf49 + 958ab93 commit 6a0d23f
Show file tree
Hide file tree
Showing 88 changed files with 12,289 additions and 0 deletions.
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

0 comments on commit 6a0d23f

Please sign in to comment.