diff --git a/9.0/Apps/DeveloperBalance/AppShell.xaml b/9.0/Apps/DeveloperBalance/AppShell.xaml index 31d32a47a..21a61ca02 100644 --- a/9.0/Apps/DeveloperBalance/AppShell.xaml +++ b/9.0/Apps/DeveloperBalance/AppShell.xaml @@ -33,8 +33,8 @@ SegmentWidth="40" SegmentHeight="40"> - - + + diff --git a/9.0/Apps/DeveloperBalance/AppShell.xaml.cs b/9.0/Apps/DeveloperBalance/AppShell.xaml.cs index b21d99eda..67235f79e 100644 --- a/9.0/Apps/DeveloperBalance/AppShell.xaml.cs +++ b/9.0/Apps/DeveloperBalance/AppShell.xaml.cs @@ -10,6 +10,9 @@ public AppShell() InitializeComponent(); var currentTheme = Application.Current!.RequestedTheme; ThemeSegmentedControl.SelectedIndex = currentTheme == AppTheme.Light ? 0 : 1; +#if ANDROID || WINDOWS + SemanticProperties.SetDescription(ThemeSegmentedControl, "Theme selection"); +#endif } public static async Task DisplaySnackbarAsync(string message) { diff --git a/9.0/Apps/DeveloperBalance/Data/TagRepository.cs b/9.0/Apps/DeveloperBalance/Data/TagRepository.cs index 1e5a92ef4..c97d2b3f4 100644 --- a/9.0/Apps/DeveloperBalance/Data/TagRepository.cs +++ b/9.0/Apps/DeveloperBalance/Data/TagRepository.cs @@ -200,6 +200,12 @@ public async Task SaveItemAsync(Tag item, int projectID) await Init(); await SaveItemAsync(item); + var isAssociated = await IsAssociated(item, projectID); + if (isAssociated) + { + return 0; // No need to save again if already associated + } + await using var connection = new SqliteConnection(Constants.DatabasePath); await connection.OpenAsync(); @@ -212,6 +218,33 @@ public async Task SaveItemAsync(Tag item, int projectID) return await saveCmd.ExecuteNonQueryAsync(); } + /// + /// Checks if a tag is already associated with a specific project. + /// + /// The tag to save. + /// The ID of the project. + /// If tag is already associated with this project + async Task IsAssociated(Tag item, int projectID) + { + await Init(); + await SaveItemAsync(item); + + await using var connection = new SqliteConnection(Constants.DatabasePath); + await connection.OpenAsync(); + + // First check if the association already exists + var checkCmd = connection.CreateCommand(); + checkCmd.CommandText = @" + SELECT COUNT(*) FROM ProjectsTags + WHERE ProjectID = @projectID AND TagID = @tagID"; + checkCmd.Parameters.AddWithValue("@projectID", projectID); + checkCmd.Parameters.AddWithValue("@tagID", item.ID); + + int existingCount = Convert.ToInt32(await checkCmd.ExecuteScalarAsync()); + + return existingCount != 0; + } + /// /// Deletes a tag from the database. /// diff --git a/9.0/Apps/DeveloperBalance/DeveloperBalance.csproj b/9.0/Apps/DeveloperBalance/DeveloperBalance.csproj index 464592409..1b615f42e 100644 --- a/9.0/Apps/DeveloperBalance/DeveloperBalance.csproj +++ b/9.0/Apps/DeveloperBalance/DeveloperBalance.csproj @@ -1,4 +1,4 @@ - + net9.0-android;net9.0-ios;net9.0-maccatalyst @@ -21,13 +21,14 @@ enable XC0103 + 9.0.60 true DeveloperBalance - com.companyname.developerbalance + com.companyname.developerbalance 1.0 @@ -63,7 +64,7 @@ - + @@ -72,7 +73,7 @@ - + diff --git a/9.0/Apps/DeveloperBalance/MauiProgram.cs b/9.0/Apps/DeveloperBalance/MauiProgram.cs index 901929564..50dabb401 100644 --- a/9.0/Apps/DeveloperBalance/MauiProgram.cs +++ b/9.0/Apps/DeveloperBalance/MauiProgram.cs @@ -1,5 +1,7 @@ using CommunityToolkit.Maui; using Microsoft.Extensions.Logging; +using Microsoft.Maui.Handlers; +using Microsoft.Maui.Platform; using Syncfusion.Maui.Toolkit.Hosting; namespace DeveloperBalance; @@ -16,9 +18,23 @@ public static MauiApp CreateMauiApp() .ConfigureMauiHandlers(handlers => { #if IOS || MACCATALYST - handlers.AddHandler(); + handlers.AddHandler(); #endif - }) +#if WINDOWS + Microsoft.Maui.Controls.Handlers.Items.CollectionViewHandler.Mapper.AppendToMapping("KeyboardAccessibleCollectionView", (handler, view) => + { + handler.PlatformView.SingleSelectionFollowsFocus = false; + }); + + Microsoft.Maui.Handlers.ContentViewHandler.Mapper.AppendToMapping(nameof(Pages.Controls.CategoryChart), (handler, view) => + { + if (view is Pages.Controls.CategoryChart && handler.PlatformView is ContentPanel contentPanel) + { + contentPanel.IsTabStop = true; + } + }); +#endif + }) .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); diff --git a/9.0/Apps/DeveloperBalance/PageModels/MainPageModel.cs b/9.0/Apps/DeveloperBalance/PageModels/MainPageModel.cs index 986840a50..462807802 100644 --- a/9.0/Apps/DeveloperBalance/PageModels/MainPageModel.cs +++ b/9.0/Apps/DeveloperBalance/PageModels/MainPageModel.cs @@ -35,6 +35,9 @@ public partial class MainPageModel : ObservableObject, IProjectTaskPageModel [ObservableProperty] private string _today = DateTime.Now.ToString("dddd, MMM d"); + [ObservableProperty] + private Project? selectedProject; + public bool HasCompletedTasks => Tasks?.Any(t => t.IsCompleted) ?? false; @@ -149,8 +152,8 @@ private Task AddTask() => Shell.Current.GoToAsync($"task"); [RelayCommand] - private Task NavigateToProject(Project project) - => Shell.Current.GoToAsync($"project?id={project.ID}"); + private Task? NavigateToProject(Project project) + => project is null ? null : Shell.Current.GoToAsync($"project?id={project.ID}"); [RelayCommand] private Task NavigateToTask(ProjectTask task) diff --git a/9.0/Apps/DeveloperBalance/PageModels/ProjectDetailPageModel.cs b/9.0/Apps/DeveloperBalance/PageModels/ProjectDetailPageModel.cs index a2399d031..da795f956 100644 --- a/9.0/Apps/DeveloperBalance/PageModels/ProjectDetailPageModel.cs +++ b/9.0/Apps/DeveloperBalance/PageModels/ProjectDetailPageModel.cs @@ -1,6 +1,8 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DeveloperBalance.Models; +using System.Collections.ObjectModel; +using System.Windows.Input; namespace DeveloperBalance.PageModels; @@ -34,6 +36,8 @@ public partial class ProjectDetailPageModel : ObservableObject, IQueryAttributab [ObservableProperty] private List _allTags = []; + public IList SelectedTags { get; set; } = new List(); + [ObservableProperty] private IconData _icon; @@ -41,7 +45,7 @@ public partial class ProjectDetailPageModel : ObservableObject, IQueryAttributab bool _isBusy; [ObservableProperty] - private List _icons = new List + private List _icons = new List { new IconData { Icon = FluentUI.ribbon_24_regular, Description = "Ribbon Icon" }, new IconData { Icon = FluentUI.ribbon_star_24_regular, Description = "Ribbon Star Icon" }, @@ -135,6 +139,10 @@ private async Task LoadData(int id) foreach (var tag in allTags) { tag.IsSelected = _project.Tags.Any(t => t.ID == tag.ID); + if (tag.IsSelected) + { + SelectedTags.Add(tag); + } } AllTags = new(allTags); } @@ -173,14 +181,11 @@ private async Task Save() _project.Icon = Icon.Icon ?? FluentUI.ribbon_24_regular; await _projectRepository.SaveItemAsync(_project); - if (_project.IsNullOrNew()) + foreach (var tag in AllTags) { - foreach (var tag in AllTags) + if (tag.IsSelected) { - if (tag.IsSelected) - { - await _tagRepository.SaveItemAsync(tag, _project.ID); - } + await _tagRepository.SaveItemAsync(tag, _project.ID); } } @@ -235,7 +240,7 @@ private Task NavigateToTask(ProjectTask task) => Shell.Current.GoToAsync($"task?id={task.ID}"); [RelayCommand] - private async Task ToggleTag(Tag tag) + internal async Task ToggleTag(Tag tag) { tag.IsSelected = !tag.IsSelected; @@ -244,20 +249,15 @@ private async Task ToggleTag(Tag tag) if (tag.IsSelected) { await _tagRepository.SaveItemAsync(tag, _project.ID); - AllTags = new(AllTags); - await AnnouncementHelper.Announce($"{tag.Title} selected"); } else { await _tagRepository.DeleteItemAsync(tag, _project.ID); - AllTags = new(AllTags); - await AnnouncementHelper.Announce($"{tag.Title} unselected"); } } - else - { - AllTags = new(AllTags); - } + + AllTags = new(AllTags); + await AnnouncementHelper.Announce($"{tag.Title} {(tag.IsSelected ? "selected" : "unselected")}"); } [RelayCommand] @@ -280,4 +280,19 @@ private async Task CleanTasks() OnPropertyChanged(nameof(HasCompletedTasks)); await AppShell.DisplayToastAsync("All cleaned up!"); } -} + + [RelayCommand] + private async Task SelectionChanged(object parameter) + { + if (parameter is IEnumerable enumerableParameter) + { + var changed = enumerableParameter.OfType().ToList(); + + if (changed.Count == 0 && SelectedTags is not null) + changed = SelectedTags.OfType().Except(enumerableParameter.OfType()).ToList(); + + if (changed.Count == 1) + await ToggleTag(changed[0]); + } + } +} \ No newline at end of file diff --git a/9.0/Apps/DeveloperBalance/PageModels/ProjectListPageModel.cs b/9.0/Apps/DeveloperBalance/PageModels/ProjectListPageModel.cs index 97ef69067..d5f0802ce 100644 --- a/9.0/Apps/DeveloperBalance/PageModels/ProjectListPageModel.cs +++ b/9.0/Apps/DeveloperBalance/PageModels/ProjectListPageModel.cs @@ -1,4 +1,3 @@ -#nullable disable using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DeveloperBalance.Data; @@ -14,6 +13,9 @@ public partial class ProjectListPageModel : ObservableObject [ObservableProperty] private List _projects = []; + [ObservableProperty] + private Project? selectedProject; + public ProjectListPageModel(ProjectRepository projectRepository) { _projectRepository = projectRepository; @@ -26,8 +28,8 @@ private async Task Appearing() } [RelayCommand] - Task NavigateToProject(Project project) - => Shell.Current.GoToAsync($"project?id={project.ID}"); + Task? NavigateToProject(Project project) + => project is null ? null : Shell.Current.GoToAsync($"project?id={project.ID}"); [RelayCommand] async Task AddProject() diff --git a/9.0/Apps/DeveloperBalance/Pages/Controls/CategoryChart.xaml b/9.0/Apps/DeveloperBalance/Pages/Controls/CategoryChart.xaml index 4d91e47ea..866b95a92 100644 --- a/9.0/Apps/DeveloperBalance/Pages/Controls/CategoryChart.xaml +++ b/9.0/Apps/DeveloperBalance/Pages/Controls/CategoryChart.xaml @@ -9,14 +9,15 @@ HeightRequest="{OnIdiom 300, Phone=200}" Style="{StaticResource CardStyle}"> @@ -38,7 +39,7 @@