diff --git a/src/Templates/src/templates/maui-mobile/AppShell.xaml b/src/Templates/src/templates/maui-mobile/AppShell.xaml index 738a02e3c786..f25afd290c43 100644 --- a/src/Templates/src/templates/maui-mobile/AppShell.xaml +++ b/src/Templates/src/templates/maui-mobile/AppShell.xaml @@ -47,8 +47,8 @@ SegmentWidth="40" SegmentHeight="40"> - - + + diff --git a/src/Templates/src/templates/maui-mobile/AppShell.xaml.cs b/src/Templates/src/templates/maui-mobile/AppShell.xaml.cs index 58fb90dc18ef..e88d6057f64a 100644 --- a/src/Templates/src/templates/maui-mobile/AppShell.xaml.cs +++ b/src/Templates/src/templates/maui-mobile/AppShell.xaml.cs @@ -13,6 +13,9 @@ public AppShell() #if (IncludeSampleContent) var currentTheme = Application.Current!.RequestedTheme; ThemeSegmentedControl.SelectedIndex = currentTheme == AppTheme.Light ? 0 : 1; +#endif +#if ANDROID || WINDOWS + SemanticProperties.SetDescription(ThemeSegmentedControl, "Theme selection"); #endif } #if (IncludeSampleContent) diff --git a/src/Templates/src/templates/maui-mobile/Converter/DataLabelValueConverter.cs b/src/Templates/src/templates/maui-mobile/Converter/DataLabelValueConverter.cs deleted file mode 100644 index 3578fec9097d..000000000000 --- a/src/Templates/src/templates/maui-mobile/Converter/DataLabelValueConverter.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Globalization; -using Microsoft.Maui.Controls; -using MauiApp._1.Models; - -namespace MauiApp._1.Converter; - -public class DataLabelValueConverter : IValueConverter -{ - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is CategoryChartData categoryChartData) - { - switch (parameter?.ToString()) - { - case "Title": - return categoryChartData.Title; - - case "Count": - return categoryChartData.Count; - } - } - - return value; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - return value; - } -} diff --git a/src/Templates/src/templates/maui-mobile/Data/TagRepository.cs b/src/Templates/src/templates/maui-mobile/Data/TagRepository.cs index 7f2a5ed4a479..a2997c475c74 100644 --- a/src/Templates/src/templates/maui-mobile/Data/TagRepository.cs +++ b/src/Templates/src/templates/maui-mobile/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,32 @@ 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 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/src/Templates/src/templates/maui-mobile/MauiProgram.cs b/src/Templates/src/templates/maui-mobile/MauiProgram.cs index 32ebbe27ed96..506153c5bfbe 100644 --- a/src/Templates/src/templates/maui-mobile/MauiProgram.cs +++ b/src/Templates/src/templates/maui-mobile/MauiProgram.cs @@ -18,6 +18,25 @@ public static MauiApp CreateMauiApp() #if (IncludeSampleContent) .UseMauiCommunityToolkit() .ConfigureSyncfusionToolkit() +//-:cnd:noEmit + .ConfigureMauiHandlers(handlers => + { +#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 + }) +//+:cnd:noEmit #endif .ConfigureFonts(fonts => { diff --git a/src/Templates/src/templates/maui-mobile/PageModels/MainPageModel.cs b/src/Templates/src/templates/maui-mobile/PageModels/MainPageModel.cs index c0c3d3a10156..d4c6f1ec07cf 100644 --- a/src/Templates/src/templates/maui-mobile/PageModels/MainPageModel.cs +++ b/src/Templates/src/templates/maui-mobile/PageModels/MainPageModel.cs @@ -37,7 +37,7 @@ public partial class MainPageModel : ObservableObject, IProjectTaskPageModel [ObservableProperty] private Project? selectedProject; - + public bool HasCompletedTasks => Tasks?.Any(t => t.IsCompleted) ?? false; diff --git a/src/Templates/src/templates/maui-mobile/PageModels/ProjectDetailPageModel.cs b/src/Templates/src/templates/maui-mobile/PageModels/ProjectDetailPageModel.cs index 0d51bd25204a..f87c2acde70a 100644 --- a/src/Templates/src/templates/maui-mobile/PageModels/ProjectDetailPageModel.cs +++ b/src/Templates/src/templates/maui-mobile/PageModels/ProjectDetailPageModel.cs @@ -1,6 +1,8 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using MauiApp._1.Models; +using System.Collections.ObjectModel; +using System.Windows.Input; namespace MauiApp._1.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; @@ -147,6 +151,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); } @@ -169,7 +177,6 @@ private async Task TaskCompleted(ProjectTask task) OnPropertyChanged(nameof(HasCompletedTasks)); } - [RelayCommand] private async Task Save() { @@ -187,14 +194,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); } } @@ -249,7 +253,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; @@ -258,20 +262,15 @@ private async Task ToggleTag(Tag tag) if (tag.IsSelected) { await _tagRepository.SaveItemAsync(tag, _project.ID); - AllTags = new(AllTags); - SemanticScreenReader.Announce($"{tag.Title} selected"); } else { await _tagRepository.DeleteItemAsync(tag, _project.ID); - AllTags = new(AllTags); - SemanticScreenReader.Announce($"{tag.Title} unselected"); } } - else - { - AllTags = new(AllTags); - } + + AllTags = new(AllTags); + SemanticScreenReader.Announce($"{tag.Title} {(tag.IsSelected ? "selected" : "unselected")}"); } [RelayCommand] @@ -294,4 +293,34 @@ 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 currentSelection = enumerableParameter.OfType().ToList(); + var previousSelection = AllTags.Where(t => t.IsSelected).ToList(); + + // Handle newly selected tags + foreach (var tag in currentSelection.Except(previousSelection)) + { + tag.IsSelected = true; + if (!_project.IsNullOrNew()) + { + await _tagRepository.SaveItemAsync(tag, _project.ID); + } + } + + // Handle deselected tags + foreach (var tag in previousSelection.Except(currentSelection)) + { + tag.IsSelected = false; + if (!_project.IsNullOrNew()) + { + await _tagRepository.DeleteItemAsync(tag, _project.ID); + } + } + } + } } diff --git a/src/Templates/src/templates/maui-mobile/PageModels/ProjectListPageModel.cs b/src/Templates/src/templates/maui-mobile/PageModels/ProjectListPageModel.cs index b0299b28e958..e582822477c4 100644 --- a/src/Templates/src/templates/maui-mobile/PageModels/ProjectListPageModel.cs +++ b/src/Templates/src/templates/maui-mobile/PageModels/ProjectListPageModel.cs @@ -1,4 +1,3 @@ -#nullable disable using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using MauiApp._1.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 ? Task.CompletedTask : Shell.Current.GoToAsync($"project?id={project.ID}"); [RelayCommand] async Task AddProject() diff --git a/src/Templates/src/templates/maui-mobile/Pages/Controls/CategoryChart.xaml b/src/Templates/src/templates/maui-mobile/Pages/Controls/CategoryChart.xaml index df15a8dd91f9..4eddbc736421 100644 --- a/src/Templates/src/templates/maui-mobile/Pages/Controls/CategoryChart.xaml +++ b/src/Templates/src/templates/maui-mobile/Pages/Controls/CategoryChart.xaml @@ -6,7 +6,6 @@ xmlns:shimmer="clr-namespace:Syncfusion.Maui.Toolkit.Shimmer;assembly=Syncfusion.Maui.Toolkit" xmlns:pageModels="clr-namespace:MauiApp._1.PageModels" xmlns:models="clr-namespace:MauiApp._1.Models" - xmlns:converter="clr-namespace:MauiApp._1.Converter" x:Class="MauiApp._1.Pages.Controls.CategoryChart" x:DataType="pageModels:MainPageModel" HeightRequest="{OnIdiom 300, Phone=200}" @@ -16,7 +15,6 @@ AutomationProperties.IsInAccessibleTree="False" BackgroundColor="Transparent" VerticalOptions="Fill" - x:DataType="pageModels:MainPageModel" IsActive="{Binding IsBusy}"> @@ -28,12 +26,11 @@ + SemanticProperties.Description="Task Categories Chart"> - + - - + SmartLabelAlignment="Shift"> + StrokeWidth="3"> diff --git a/src/Templates/src/templates/maui-mobile/Pages/Controls/ChartDataLabelConverter.cs b/src/Templates/src/templates/maui-mobile/Pages/Controls/ChartDataLabelConverter.cs new file mode 100644 index 000000000000..1fbd0585c0c0 --- /dev/null +++ b/src/Templates/src/templates/maui-mobile/Pages/Controls/ChartDataLabelConverter.cs @@ -0,0 +1,33 @@ +using System; +using System.Globalization; +using MauiApp._1.Models; + +namespace MauiApp._1.Pages.Controls; + +public class ChartDataLabelConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is CategoryChartData categoryData && parameter is string parameterValue) + { + if (string.Equals(parameterValue, "title", StringComparison.OrdinalIgnoreCase)) + { + return categoryData.Title; + } + + if (string.Equals(parameterValue, "count", StringComparison.OrdinalIgnoreCase)) + { + return categoryData.Count.ToString(); + } + + return value?.ToString(); + } + + return value?.ToString(); + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/src/Templates/src/templates/maui-mobile/Pages/Controls/ProjectCardView.xaml b/src/Templates/src/templates/maui-mobile/Pages/Controls/ProjectCardView.xaml index f38e5bf8a644..e4738f1887cd 100644 --- a/src/Templates/src/templates/maui-mobile/Pages/Controls/ProjectCardView.xaml +++ b/src/Templates/src/templates/maui-mobile/Pages/Controls/ProjectCardView.xaml @@ -9,10 +9,10 @@ x:Class="MauiApp._1.Pages.Controls.ProjectCardView" SemanticProperties.Description="{Binding AccessibilityDescription}" Style="{StaticResource CardStyle}" + MinimumHeightRequest="250" x:DataType="models:Project"> @@ -48,13 +48,13 @@ diff --git a/src/Templates/src/templates/maui-mobile/Pages/Controls/TaskView.xaml b/src/Templates/src/templates/maui-mobile/Pages/Controls/TaskView.xaml index 02c260051436..1890f42b2edd 100644 --- a/src/Templates/src/templates/maui-mobile/Pages/Controls/TaskView.xaml +++ b/src/Templates/src/templates/maui-mobile/Pages/Controls/TaskView.xaml @@ -11,12 +11,12 @@ Background="{AppThemeBinding Light={StaticResource LightSecondaryBackground}, Dark={StaticResource DarkSecondaryBackground}}" x:DataType="models:ProjectTask"> - - - - - - + + + + + - - diff --git a/src/Templates/src/templates/maui-mobile/Pages/MainPage.xaml b/src/Templates/src/templates/maui-mobile/Pages/MainPage.xaml index 484677096300..baaa071ce932 100644 --- a/src/Templates/src/templates/maui-mobile/Pages/MainPage.xaml +++ b/src/Templates/src/templates/maui-mobile/Pages/MainPage.xaml @@ -38,7 +38,7 @@ RefreshCommand="{Binding RefreshCommand}"> - + @@ -47,7 +47,7 @@ - +