Skip to content

Commit b1e5115

Browse files
CopilotPureWeen
andcommitted
Integrate all changes from PR #32148 into Developer Balance template
Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
1 parent 032560e commit b1e5115

File tree

16 files changed

+220
-113
lines changed

16 files changed

+220
-113
lines changed

src/Templates/src/templates/maui-mobile/AppShell.xaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@
4747
SegmentWidth="40" SegmentHeight="40">
4848
<sf:SfSegmentedControl.ItemsSource>
4949
<x:Array Type="{x:Type sf:SfSegmentItem}">
50-
<sf:SfSegmentItem ImageSource="{StaticResource IconLight}"/>
51-
<sf:SfSegmentItem ImageSource="{StaticResource IconDark}"/>
50+
<sf:SfSegmentItem ImageSource="{StaticResource IconLight}" SemanticProperties.Description="Light mode"/>
51+
<sf:SfSegmentItem ImageSource="{StaticResource IconDark}" SemanticProperties.Description="Dark mode"/>
5252
</x:Array>
5353
</sf:SfSegmentedControl.ItemsSource>
5454
</sf:SfSegmentedControl>

src/Templates/src/templates/maui-mobile/AppShell.xaml.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ public AppShell()
1313
#if (IncludeSampleContent)
1414
var currentTheme = Application.Current!.RequestedTheme;
1515
ThemeSegmentedControl.SelectedIndex = currentTheme == AppTheme.Light ? 0 : 1;
16+
#endif
17+
#if ANDROID || WINDOWS
18+
SemanticProperties.SetDescription(ThemeSegmentedControl, "Theme selection");
1619
#endif
1720
}
1821
#if (IncludeSampleContent)

src/Templates/src/templates/maui-mobile/Data/TagRepository.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,12 @@ public async Task<int> SaveItemAsync(Tag item, int projectID)
200200
await Init();
201201
await SaveItemAsync(item);
202202

203+
var isAssociated = await IsAssociated(item, projectID);
204+
if (isAssociated)
205+
{
206+
return 0; // No need to save again if already associated
207+
}
208+
203209
await using var connection = new SqliteConnection(Constants.DatabasePath);
204210
await connection.OpenAsync();
205211

@@ -212,6 +218,33 @@ public async Task<int> SaveItemAsync(Tag item, int projectID)
212218
return await saveCmd.ExecuteNonQueryAsync();
213219
}
214220

221+
/// <summary>
222+
/// Checks if a tag is already associated with a specific project.
223+
/// </summary>
224+
/// <param name="item">The tag to save.</param>
225+
/// <param name="projectID">The ID of the project.</param>
226+
/// <returns>If tag is already associated with this project</returns>
227+
async Task<bool> IsAssociated(Tag item, int projectID)
228+
{
229+
await Init();
230+
await SaveItemAsync(item);
231+
232+
await using var connection = new SqliteConnection(Constants.DatabasePath);
233+
await connection.OpenAsync();
234+
235+
// First check if the association already exists
236+
var checkCmd = connection.CreateCommand();
237+
checkCmd.CommandText = @"
238+
SELECT COUNT(*) FROM ProjectsTags
239+
WHERE ProjectID = @projectID AND TagID = @tagID";
240+
checkCmd.Parameters.AddWithValue("@projectID", projectID);
241+
checkCmd.Parameters.AddWithValue("@tagID", item.ID);
242+
243+
int existingCount = Convert.ToInt32(await checkCmd.ExecuteScalarAsync());
244+
245+
return existingCount != 0;
246+
}
247+
215248
/// <summary>
216249
/// Deletes a tag from the database.
217250
/// </summary>

src/Templates/src/templates/maui-mobile/MauiProgram.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,30 @@ public static MauiApp CreateMauiApp()
1818
#if (IncludeSampleContent)
1919
.UseMauiCommunityToolkit()
2020
.ConfigureSyncfusionToolkit()
21+
#endif
2122
.ConfigureMauiHandlers(handlers =>
2223
{
24+
#if WINDOWS
25+
Microsoft.Maui.Controls.Handlers.Items.CollectionViewHandler.Mapper.AppendToMapping("KeyboardAccessibleCollectionView", (handler, view) =>
26+
{
27+
handler.PlatformView.SingleSelectionFollowsFocus = false;
28+
});
29+
30+
Microsoft.Maui.Handlers.ContentViewHandler.Mapper.AppendToMapping(nameof(Pages.Controls.CategoryChart), (handler, view) =>
31+
{
32+
if (view is Pages.Controls.CategoryChart && handler.PlatformView is ContentPanel contentPanel)
33+
{
34+
contentPanel.IsTabStop = true;
35+
}
36+
});
37+
#endif
2338
//-:cnd:noEmit
2439
#if IOS || MACCATALYST
2540
handlers.AddHandler<Microsoft.Maui.Controls.CollectionView, Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2>();
2641
#endif
2742
//+:cnd:noEmit
28-
})
43+
})
44+
#if (IncludeSampleContent)
2945
#endif
3046
.ConfigureFonts(fonts =>
3147
{

src/Templates/src/templates/maui-mobile/PageModels/MainPageModel.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ public partial class MainPageModel : ObservableObject, IProjectTaskPageModel
3535
[ObservableProperty]
3636
private string _today = DateTime.Now.ToString("dddd, MMM d");
3737

38+
[ObservableProperty]
39+
private Project? selectedProject;
40+
3841
public bool HasCompletedTasks
3942
=> Tasks?.Any(t => t.IsCompleted) ?? false;
4043

src/Templates/src/templates/maui-mobile/PageModels/ProjectDetailPageModel.cs

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using CommunityToolkit.Mvvm.ComponentModel;
22
using CommunityToolkit.Mvvm.Input;
33
using MauiApp._1.Models;
4+
using System.Collections.ObjectModel;
5+
using System.Windows.Input;
46

57
namespace MauiApp._1.PageModels;
68

@@ -34,6 +36,8 @@ public partial class ProjectDetailPageModel : ObservableObject, IQueryAttributab
3436
[ObservableProperty]
3537
private List<Tag> _allTags = [];
3638

39+
public IList<object> SelectedTags { get; set; } = new List<object>();
40+
3741
[ObservableProperty]
3842
private IconData _icon;
3943

@@ -135,6 +139,10 @@ private async Task LoadData(int id)
135139
foreach (var tag in allTags)
136140
{
137141
tag.IsSelected = _project.Tags.Any(t => t.ID == tag.ID);
142+
if (tag.IsSelected)
143+
{
144+
SelectedTags.Add(tag);
145+
}
138146
}
139147
AllTags = new(allTags);
140148
}
@@ -156,7 +164,6 @@ private async Task TaskCompleted(ProjectTask task)
156164
OnPropertyChanged(nameof(HasCompletedTasks));
157165
}
158166

159-
160167
[RelayCommand]
161168
private async Task Save()
162169
{
@@ -174,14 +181,11 @@ private async Task Save()
174181
_project.Icon = Icon.Icon ?? FluentUI.ribbon_24_regular;
175182
await _projectRepository.SaveItemAsync(_project);
176183

177-
if (_project.IsNullOrNew())
184+
foreach (var tag in AllTags)
178185
{
179-
foreach (var tag in AllTags)
186+
if (tag.IsSelected)
180187
{
181-
if (tag.IsSelected)
182-
{
183-
await _tagRepository.SaveItemAsync(tag, _project.ID);
184-
}
188+
await _tagRepository.SaveItemAsync(tag, _project.ID);
185189
}
186190
}
187191

@@ -236,7 +240,7 @@ private Task NavigateToTask(ProjectTask task) =>
236240
Shell.Current.GoToAsync($"task?id={task.ID}");
237241

238242
[RelayCommand]
239-
private async Task ToggleTag(Tag tag)
243+
internal async Task ToggleTag(Tag tag)
240244
{
241245
tag.IsSelected = !tag.IsSelected;
242246

@@ -253,6 +257,7 @@ private async Task ToggleTag(Tag tag)
253257
}
254258

255259
AllTags = new(AllTags);
260+
SemanticScreenReader.Announce($"{tag.Title} {(tag.IsSelected ? "selected" : "unselected")}");
256261
}
257262

258263
[RelayCommand]
@@ -269,4 +274,34 @@ private async Task CleanTasks()
269274
OnPropertyChanged(nameof(HasCompletedTasks));
270275
await AppShell.DisplayToastAsync("All cleaned up!");
271276
}
277+
278+
[RelayCommand]
279+
private async Task SelectionChanged(object parameter)
280+
{
281+
if (parameter is IEnumerable<object> enumerableParameter)
282+
{
283+
var currentSelection = enumerableParameter.OfType<Tag>().ToList();
284+
var previousSelection = AllTags.Where(t => t.IsSelected).ToList();
285+
286+
// Handle newly selected tags
287+
foreach (var tag in currentSelection.Except(previousSelection))
288+
{
289+
tag.IsSelected = true;
290+
if (!_project.IsNullOrNew())
291+
{
292+
await _tagRepository.SaveItemAsync(tag, _project.ID);
293+
}
294+
}
295+
296+
// Handle deselected tags
297+
foreach (var tag in previousSelection.Except(currentSelection))
298+
{
299+
tag.IsSelected = false;
300+
if (!_project.IsNullOrNew())
301+
{
302+
await _tagRepository.DeleteItemAsync(tag, _project.ID);
303+
}
304+
}
305+
}
306+
}
272307
}

src/Templates/src/templates/maui-mobile/PageModels/ProjectListPageModel.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#nullable disable
21
using CommunityToolkit.Mvvm.ComponentModel;
32
using CommunityToolkit.Mvvm.Input;
43
using MauiApp._1.Data;
@@ -14,6 +13,9 @@ public partial class ProjectListPageModel : ObservableObject
1413
[ObservableProperty]
1514
private List<Project> _projects = [];
1615

16+
[ObservableProperty]
17+
private Project? selectedProject;
18+
1719
public ProjectListPageModel(ProjectRepository projectRepository)
1820
{
1921
_projectRepository = projectRepository;
@@ -26,8 +28,8 @@ private async Task Appearing()
2628
}
2729

2830
[RelayCommand]
29-
Task NavigateToProject(Project project)
30-
=> Shell.Current.GoToAsync($"project?id={project.ID}");
31+
Task? NavigateToProject(Project project)
32+
=> project is null ? null : Shell.Current.GoToAsync($"project?id={project.ID}");
3133

3234
[RelayCommand]
3335
async Task AddProject()

src/Templates/src/templates/maui-mobile/Pages/Controls/CategoryChart.xaml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
xmlns:controls="clr-namespace:MauiApp._1.Pages.Controls"
66
xmlns:shimmer="clr-namespace:Syncfusion.Maui.Toolkit.Shimmer;assembly=Syncfusion.Maui.Toolkit"
77
xmlns:pageModels="clr-namespace:MauiApp._1.PageModels"
8+
xmlns:models="clr-namespace:MauiApp._1.Models"
89
x:Class="MauiApp._1.Pages.Controls.CategoryChart"
10+
x:DataType="pageModels:MainPageModel"
911
HeightRequest="{OnIdiom 300, Phone=200}"
1012
Margin="0, 12"
11-
x:DataType="pageModels:MainPageModel"
1213
Style="{StaticResource CardStyle}">
1314
<shimmer:SfShimmer
1415
AutomationProperties.IsInAccessibleTree="False"
@@ -29,7 +30,7 @@
2930
<chart:SfCircularChart.Resources>
3031
<controls:ChartDataLabelConverter x:Key="ChartDataLabelConverter"/>
3132
</chart:SfCircularChart.Resources>
32-
<chart:DoughnutSeries
33+
<chart:DoughnutSeries
3334
ItemsSource="{Binding TodoCategoryData}"
3435
PaletteBrushes="{Binding TodoCategoryColors}"
3536
XBindingPath="Title"
@@ -43,7 +44,7 @@
4344
<DataTemplate>
4445
<HorizontalStackLayout x:DataType="chart:ChartDataLabel">
4546
<Label Text="{Binding Item, Converter={StaticResource ChartDataLabelConverter}, ConverterParameter='title'}"
46-
TextColor="{AppThemeBinding
47+
TextColor="{AppThemeBinding
4748
Light={StaticResource DarkOnLightBackground},
4849
Dark={StaticResource LightOnDarkBackground}}"
4950
FontSize="{OnIdiom 18, Phone=14}"/>

src/Templates/src/templates/maui-mobile/Pages/Controls/ProjectCardView.xaml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
xmlns:pageModels="clr-namespace:MauiApp._1.PageModels"
88
xmlns:shimmer="clr-namespace:Syncfusion.Maui.Toolkit.Shimmer;assembly=Syncfusion.Maui.Toolkit"
99
x:Class="MauiApp._1.Pages.Controls.ProjectCardView"
10+
SemanticProperties.Description="{Binding AccessibilityDescription}"
1011
Style="{StaticResource CardStyle}"
12+
MinimumHeightRequest="250"
1113
x:DataType="models:Project">
1214
<shimmer:SfShimmer
1315
BackgroundColor="Transparent"
14-
VerticalOptions="FillAndExpand"
1516
IsActive="{Binding IsBusy, Source={RelativeSource AncestorType={x:Type pageModels:MainPageModel}}, x:DataType=pageModels:IProjectTaskPageModel}">
1617
<shimmer:SfShimmer.CustomView>
1718
<VerticalStackLayout Spacing="15">
@@ -45,15 +46,15 @@
4546
Size="{StaticResource IconSize}"/>
4647
</Image.Source>
4748
</Image>
48-
<Label Text="{Binding Name}" TextColor="{StaticResource Gray400}" FontSize="14" TextTransform="Uppercase"/>
49+
<Label Text="{Binding Name}" TextColor="{AppThemeBinding Light={StaticResource Gray600}, Dark={StaticResource Gray400}}" FontSize="14" TextTransform="Uppercase"/>
4950
<Label Text="{Binding Description}" LineBreakMode="WordWrap"/>
50-
<HorizontalStackLayout Spacing="15" BindableLayout.ItemsSource="{Binding Tags}">
51+
<FlexLayout Wrap="Wrap" Direction="Row" AlignItems="Start" JustifyContent="Start" BindableLayout.ItemsSource="{Binding Tags}">
5152
<BindableLayout.ItemTemplate>
5253
<DataTemplate x:DataType="models:Tag">
5354
<controls:TagView />
5455
</DataTemplate>
5556
</BindableLayout.ItemTemplate>
56-
</HorizontalStackLayout>
57+
</FlexLayout>
5758
</VerticalStackLayout>
5859
</shimmer:SfShimmer.Content>
5960
</shimmer:SfShimmer>

src/Templates/src/templates/maui-mobile/Pages/Controls/TaskView.xaml

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@
1111
Background="{AppThemeBinding Light={StaticResource LightSecondaryBackground}, Dark={StaticResource DarkSecondaryBackground}}"
1212
x:DataType="models:ProjectTask">
1313

14-
<effectsView:SfEffectsView
14+
<effectsView:SfEffectsView
1515
TouchDownEffects="Highlight"
1616
HighlightBackground="{AppThemeBinding Light={StaticResource DarkOnLightBackground}, Dark={StaticResource LightOnDarkBackground}}">
1717
<shimmer:SfShimmer
1818
BackgroundColor="Transparent"
19-
VerticalOptions="FillAndExpand"
19+
VerticalOptions="Fill"
2020
IsActive="{Binding IsBusy, Source={RelativeSource AncestorType={x:Type pageModels:IProjectTaskPageModel}}, x:DataType=pageModels:IProjectTaskPageModel}">
2121
<shimmer:SfShimmer.CustomView>
2222
<Grid
@@ -35,24 +35,31 @@
3535
</Grid>
3636
</shimmer:SfShimmer.CustomView>
3737
<shimmer:SfShimmer.Content>
38-
<Grid ColumnDefinitions="Auto,*" ColumnSpacing="15" Padding="{OnIdiom 15, Desktop=20}">
39-
<Grid.GestureRecognizers>
40-
<TapGestureRecognizer
41-
Command="{Binding NavigateToTaskCommand, Source={RelativeSource AncestorType={x:Type pageModels:IProjectTaskPageModel}}, x:DataType=pageModels:IProjectTaskPageModel}"
42-
CommandParameter="{Binding .}"/>
43-
</Grid.GestureRecognizers>
44-
38+
<Grid ColumnDefinitions="*" ColumnSpacing="15" Padding="{OnIdiom 15, Desktop=20}">
39+
40+
<Grid SemanticProperties.Description="{Binding Title}" Margin="{OnIdiom -15, Desktop=-20}">
41+
<Label
42+
Margin="{OnIdiom '65,0,0,0', Desktop= '70,0,0,0'}"
43+
Text="{Binding Title}"
44+
HorizontalOptions="Start"
45+
VerticalOptions="Center"
46+
LineBreakMode="WordWrap"/>
47+
48+
<Grid.GestureRecognizers>
49+
<TapGestureRecognizer
50+
Command="{Binding NavigateToTaskCommand, Source={RelativeSource AncestorType={x:Type pageModels:IProjectTaskPageModel}}, x:DataType=pageModels:IProjectTaskPageModel}"
51+
CommandParameter="{Binding .}"/>
52+
</Grid.GestureRecognizers>
53+
</Grid>
54+
4555
<CheckBox Grid.Column="0"
56+
WidthRequest="50"
57+
HorizontalOptions="Start"
4658
IsChecked="{Binding IsCompleted, Mode=OneTime}"
4759
VerticalOptions="Center"
4860
CheckedChanged="CheckBox_CheckedChanged"
61+
AutomationProperties.IsInAccessibleTree="True"
4962
SemanticProperties.Description="{Binding Title}"/>
50-
51-
<Label Grid.Column="1"
52-
Text="{Binding Title}"
53-
VerticalOptions="Center"
54-
LineBreakMode="TailTruncation"
55-
AutomationProperties.IsInAccessibleTree="False" />
5663
</Grid>
5764
</shimmer:SfShimmer.Content>
5865
</shimmer:SfShimmer>

0 commit comments

Comments
 (0)