From 7b369201b0cf4a154fdf3236a78728530c20c5b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Wed, 1 Apr 2026 01:41:17 +0200 Subject: [PATCH] General account details settings and some marking mail issues --- .claude/instructions/winui3.instructions.md | 160 ----- .codex/environments/environment.toml | 6 - AGENTS.md | 1 + .../Services/MailRequestStateTests.cs | 85 +++ .../OutlookSynchronizerRequestSuccessTests.cs | 102 ++++ Wino.Core/Requests/Mail/ChangeFlagRequest.cs | 6 +- Wino.Core/Requests/Mail/MarkReadRequest.cs | 6 +- .../Synchronizers/OutlookSynchronizer.cs | 12 + .../AccountDetailsPageViewModel.cs | 30 +- .../Views/Account/AccountDetailsPage.xaml | 554 +++++++++--------- .../Views/Account/AccountDetailsPage.xaml.cs | 9 + 11 files changed, 536 insertions(+), 435 deletions(-) delete mode 100644 .claude/instructions/winui3.instructions.md delete mode 100644 .codex/environments/environment.toml create mode 100644 Wino.Core.Tests/Services/MailRequestStateTests.cs create mode 100644 Wino.Core.Tests/Synchronizers/OutlookSynchronizerRequestSuccessTests.cs diff --git a/.claude/instructions/winui3.instructions.md b/.claude/instructions/winui3.instructions.md deleted file mode 100644 index 811306c6..00000000 --- a/.claude/instructions/winui3.instructions.md +++ /dev/null @@ -1,160 +0,0 @@ ---- -description: 'WinUI 3 and Windows App SDK coding guidelines. Prevents common UWP API misuse, enforces correct XAML namespaces, threading, windowing, and MVVM patterns for desktop Windows apps.' -applyTo: '**/*.xaml, **/*.cs, **/*.csproj' ---- - -# WinUI 3 / Windows App SDK - -## Critical Rules — NEVER Use Legacy UWP APIs - -These UWP patterns are **wrong** for WinUI 3 desktop apps. Always use the Windows App SDK equivalent. - -- **NEVER** use `Windows.UI.Popups.MessageDialog`. Use `ContentDialog` with `XamlRoot` set. -- **NEVER** show a `ContentDialog` without setting `dialog.XamlRoot = this.Content.XamlRoot` first. -- **NEVER** use `CoreDispatcher.RunAsync` or `Dispatcher.RunAsync`. Use `DispatcherQueue.TryEnqueue`. -- **NEVER** use `Window.Current`. Track the main window via a static `App.MainWindow` property. -- **NEVER** use `Windows.UI.Xaml.*` namespaces. Use `Microsoft.UI.Xaml.*`. -- **NEVER** use `Windows.UI.Composition`. Use `Microsoft.UI.Composition`. -- **NEVER** use `Windows.UI.Colors`. Use `Microsoft.UI.Colors`. -- **NEVER** use `ApplicationView` or `CoreWindow` for window management. Use `Microsoft.UI.Windowing.AppWindow`. -- **NEVER** use `CoreApplicationViewTitleBar`. Use `AppWindowTitleBar`. -- **NEVER** use `GetForCurrentView()` patterns (e.g., `UIViewSettings.GetForCurrentView()`). These do not exist in desktop WinUI 3. Use `AppWindow` APIs instead. -- **NEVER** use UWP `PrintManager` directly. Use `IPrintManagerInterop` with a window handle. -- **NEVER** use `DataTransferManager` directly for sharing. Use `IDataTransferManagerInterop` with a window handle. -- **NEVER** use UWP `IBackgroundTask`. Use `Microsoft.Windows.AppLifecycle` activation. -- **NEVER** use `WebAuthenticationBroker`. Use `OAuth2Manager` (Windows App SDK 1.7+). - -## XAML Patterns - -- The default XAML namespace maps to `Microsoft.UI.Xaml`, not `Windows.UI.Xaml`. -- Prefer `{x:Bind}` over `{Binding}` for compiled, type-safe, higher-performance bindings. -- Set `x:DataType` on `DataTemplate` elements when using `{x:Bind}` — this is required for compiled bindings in templates. On Page/UserControl, `x:DataType` enables compile-time binding validation but is not strictly required if the DataContext does not change. -- Use `Mode=OneWay` for dynamic values, `Mode=OneTime` for static, `Mode=TwoWay` only for editable inputs. -- Do not bind static constants — set them directly in XAML. - -## Threading - -- Use `DispatcherQueue.TryEnqueue(() => { ... })` to update UI from background threads. -- `TryEnqueue` returns `bool`, not a `Task` — it is fire-and-forget. -- Check thread access with `DispatcherQueue.HasThreadAccess` before dispatching. -- WinUI 3 uses standard STA (not ASTA). No built-in reentrancy protection — be cautious with async code that pumps messages. - -## Windowing - -- Get the `AppWindow` from a WinUI 3 `Window` via `WindowNative.GetWindowHandle` → `Win32Interop.GetWindowIdFromWindow` → `AppWindow.GetFromWindowId`. -- Use `AppWindow` for resize, move, title, and presenter operations. -- Custom title bar: use `AppWindow.TitleBar` properties, not `CoreApplicationViewTitleBar`. -- Track the main window as `App.MainWindow` (a static property set in `OnLaunched`). - -## Dialogs and Pickers - -- **ContentDialog**: Always set `dialog.XamlRoot = this.Content.XamlRoot` before calling `ShowAsync()`. -- **File/Folder Pickers**: Initialize with `WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd)` where `hwnd` comes from `WindowNative.GetWindowHandle(App.MainWindow)`. -- **Share/Print**: Use COM interop interfaces (`IDataTransferManagerInterop`, `IPrintManagerInterop`) with window handles. - -## MVVM and Data Binding - -- Prefer `CommunityToolkit.Mvvm` (`[ObservableProperty]`, `[RelayCommand]`) for MVVM infrastructure. -- Use `Microsoft.Extensions.DependencyInjection` for service registration and injection. -- Keep UI (Views) focused on layout and bindings; keep logic in ViewModels and services. -- Use `async`/`await` for I/O and long-running work to keep the UI responsive. - -## Project Setup - -- Target `net10.0-windows10.0.22621.0` (or appropriate TFM for the project's target SDK). -- Set `true` in the project file. -- Reference the latest stable `Microsoft.WindowsAppSDK` NuGet package. -- Use `System.Text.Json` with source generators for JSON serialization. - -## C# Code Style - -- Use file-scoped namespaces. -- Enable nullable reference types. Use `is null` / `is not null` instead of `== null`. -- Prefer pattern matching over `as`/`is` with null checks. -- PascalCase for types, methods, properties. camelCase for private fields. -- Allman brace style (opening brace on its own line). -- Prefer explicit types for built-in types; use `var` only when the type is obvious. - -## Accessibility - -- Set `AutomationProperties.Name` on all interactive controls. -- Use `AutomationProperties.HeadingLevel` on section headers. -- Hide decorative elements with `AutomationProperties.AccessibilityView="Raw"`. -- Ensure full keyboard navigation (Tab, Enter, Space, arrow keys). -- Meet WCAG color contrast requirements. - -## Performance - -- Prefer `{x:Bind}` (compiled) over `{Binding}` (reflection-based). -- **NativeAOT:** Under Native AOT compilation, `{Binding}` (reflection-based) does not work at all. Only `{x:Bind}` (compiled bindings) is supported. If the project uses NativeAOT, use `{x:Bind}` exclusively. -- Use `x:Load` or `x:DeferLoadStrategy` for UI elements that are not immediately needed. -- Use `ItemsRepeater` with virtualization for large lists. -- Avoid deep layout nesting — prefer `Grid` over nested `StackPanel` chains. -- Use `async`/`await` for all I/O; never block the UI thread. - -## App Settings (Packaged vs Unpackaged) - -- **Packaged apps**: `ApplicationData.Current.LocalSettings` works as expected. -- **Unpackaged apps**: Use a custom settings file (e.g., JSON in `Environment.GetFolderPath(SpecialFolder.LocalApplicationData)`). -- Do not assume `ApplicationData` is always available — check packaging status first. - -## Typography - -- **Always** use built-in TextBlock styles (`CaptionTextBlockStyle`, `BodyTextBlockStyle`, `BodyStrongTextBlockStyle`, `SubtitleTextBlockStyle`, `TitleTextBlockStyle`, `TitleLargeTextBlockStyle`, `DisplayTextBlockStyle`). -- Prefer using the built-in TextBlock styles over hardcoding `FontSize`, `FontWeight`, or `FontFamily`. -- Font: Segoe UI Variable is the default — do not change it. -- Use sentence casing for all UI text. - - -## Theming & Colors - -- **Always** use `{ThemeResource}` for brushes and colors to support Light, Dark, and High Contrast themes automatically. -- **Never** hardcode color values (`#FFFFFF`, `Colors.White`, etc.) for UI elements. Use theme resources like `TextFillColorPrimaryBrush`, `CardBackgroundFillColorDefaultBrush`, `CardStrokeColorDefaultBrush`. -- Use `SystemAccentColor` (and `Light1`–`Light3`, `Dark1`–`Dark3` variants) for the user's accent color palette. -- For borders: use `CardStrokeColorDefaultBrush` or `ControlStrokeColorDefaultBrush`. - -## Spacing & Layout - -- Use a **4px grid system**: all margins, padding, and spacing values must be multiples of 4px. -- Standard spacing: 4 (compact), 8 (controls), 12 (small gutters), 16 (content padding), 24 (large gutters). -- Prefer `Grid` over deeply nested `StackPanel` chains for performance. -- Use `Auto` for content-sized rows/columns, `*` for proportional sizing. Avoid fixed pixel sizes. -- Use `VisualStateManager` with `AdaptiveTrigger` for responsive layouts at breakpoints (640px, 1008px). -- Use `ControlCornerRadius` (4px) for small controls and `OverlayCornerRadius` (8px) for cards, dialogs, flyouts. - -## Materials & Elevation - -- Use **Mica** (`MicaBackdrop`) for the app window backdrop. Requires transparent layers above to show through. -- Use **Acrylic** for transient surfaces only (flyouts, menus, navigation panes). -- Use `LayerFillColorDefaultBrush` for content layers above Mica. -- Use `ThemeShadow` with Z-axis `Translation` for elevation. Cards: 4–8 px, Flyouts: 32 px, Dialogs: 128 px. - -## Motion & Transitions - -- Use built-in theme transitions (`EntranceThemeTransition`, `RepositionThemeTransition`, `ContentThemeTransition`, `AddDeleteThemeTransition`). -- Avoid custom storyboard animations when a built-in transition exists. - -## Control Selection - -- Use `NavigationView` for primary app navigation (not custom sidebars). -- Use `InfoBar` for persistent in-app notifications (not custom banners). -- Use `TeachingTip` for contextual guidance (not custom popups). -- Use `NumberBox` for numeric input (not TextBox with manual validation). -- Use `ToggleSwitch` for on/off settings (not CheckBox). -- Use `ItemsView` as the modern collection control for displaying data with built-in selection, virtualization, and layout flexibility. -- Use `ListView`/`GridView` for standard virtualized lists and grids, especially when built-in selection support is needed. -- Use `ItemsRepeater` only for fully custom virtualizing layouts where you need complete control over rendering and do not need built-in selection or interaction handling. -- Use `Expander` for collapsible sections (not custom visibility toggling). - -## Error Handling - -- Always wrap `async void` event handlers in try/catch to prevent unhandled crashes. -- Use `InfoBar` (with `Severity = Error`) for user-facing error messages, not `ContentDialog` for routine errors. -- Handle `App.UnhandledException` for logging and graceful recovery. - -## Testing - -- **NEVER** use a plain MSTest or xUnit project for tests that instantiate WinUI 3 XAML types. Use a **Unit Test App (WinUI in Desktop)** project, which provides the Xaml runtime and UI thread. -- Use `[TestMethod]` for pure logic tests. Use `[UITestMethod]` for any test that creates or interacts with `Microsoft.UI.Xaml` types (controls, pages, user controls). -- Place testable business logic in a **Class Library (WinUI in Desktop)** project, separate from the main app. -- Build the solution before running tests to enable Visual Studio test discovery. diff --git a/.codex/environments/environment.toml b/.codex/environments/environment.toml deleted file mode 100644 index ff363268..00000000 --- a/.codex/environments/environment.toml +++ /dev/null @@ -1,6 +0,0 @@ -# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY -version = 1 -name = "Wino-Mail" - -[setup] -script = "" diff --git a/AGENTS.md b/AGENTS.md index 5d2615c0..08771976 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -152,6 +152,7 @@ private string searchQuery = string.Empty; - ViewModels should only handle UI interaction/state and delegate business logic to services; account-management work belongs in `WinoAccountProfileService`, and preferences import/export/apply logic belongs in `PreferencesService`. - In `EventDetailsPageViewModel.LoadAttendeesAsync`, never mutate `CurrentEvent.Attendees` outside `ExecuteUIThread(...)`. - Never create pure C# controls or controls that heavily manipulate UI structure from `.cs` files. Define controls in XAML and keep UI composition in XAML. +- Never add XAML-backed UI controls to `.xaml.cs`. If a view has XAML, all control declarations, flyouts, templates, and visual composition belong in the `.xaml` file; keep `.xaml.cs` limited to event handling and view glue. - Never subscribe to framework events like `Loaded`, `Unloaded`, or input events from constructors in `.xaml.cs` for XAML-backed controls and pages; wire them directly in XAML instead. - If you use `x:Load` in XAML, always give that `UIElement` an `x:Name`. diff --git a/Wino.Core.Tests/Services/MailRequestStateTests.cs b/Wino.Core.Tests/Services/MailRequestStateTests.cs new file mode 100644 index 00000000..b06c4487 --- /dev/null +++ b/Wino.Core.Tests/Services/MailRequestStateTests.cs @@ -0,0 +1,85 @@ +using CommunityToolkit.Mvvm.Messaging; +using FluentAssertions; +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Enums; +using Wino.Core.Requests.Mail; +using Wino.Messaging.UI; +using Xunit; + +namespace Wino.Core.Tests.Services; + +public sealed class MailRequestStateTests +{ + [Fact] + public void MarkReadRequest_RevertUiChanges_RestoresOriginalReadState() + { + var mailCopy = CreateMailCopy(isRead: false, isFlagged: false); + var request = new MarkReadRequest(mailCopy, IsRead: true); + var recipient = new MailRequestRecipient(); + + WeakReferenceMessenger.Default.RegisterAll(recipient); + + try + { + request.IsNoOp.Should().BeFalse(); + + request.ApplyUIChanges(); + request.RevertUIChanges(); + + mailCopy.IsRead.Should().BeFalse(); + recipient.Updated.Should().HaveCount(2); + recipient.Updated[0].Source.Should().Be(MailUpdateSource.ClientUpdated); + recipient.Updated[1].Source.Should().Be(MailUpdateSource.ClientReverted); + recipient.Updated[1].UpdatedMail.IsRead.Should().BeFalse(); + } + finally + { + WeakReferenceMessenger.Default.UnregisterAll(recipient); + } + } + + [Fact] + public void ChangeFlagRequest_RevertUiChanges_RestoresOriginalFlagState() + { + var mailCopy = CreateMailCopy(isRead: true, isFlagged: false); + var request = new ChangeFlagRequest(mailCopy, IsFlagged: true); + var recipient = new MailRequestRecipient(); + + WeakReferenceMessenger.Default.RegisterAll(recipient); + + try + { + request.IsNoOp.Should().BeFalse(); + + request.ApplyUIChanges(); + request.RevertUIChanges(); + + mailCopy.IsFlagged.Should().BeFalse(); + recipient.Updated.Should().HaveCount(2); + recipient.Updated[0].Source.Should().Be(MailUpdateSource.ClientUpdated); + recipient.Updated[1].Source.Should().Be(MailUpdateSource.ClientReverted); + recipient.Updated[1].UpdatedMail.IsFlagged.Should().BeFalse(); + } + finally + { + WeakReferenceMessenger.Default.UnregisterAll(recipient); + } + } + + private static MailCopy CreateMailCopy(bool isRead, bool isFlagged) => + new() + { + UniqueId = Guid.NewGuid(), + Id = Guid.NewGuid().ToString(), + FolderId = Guid.NewGuid(), + IsRead = isRead, + IsFlagged = isFlagged + }; + + internal sealed class MailRequestRecipient : IRecipient + { + public List Updated { get; } = []; + + public void Receive(MailUpdatedMessage message) => Updated.Add(message); + } +} diff --git a/Wino.Core.Tests/Synchronizers/OutlookSynchronizerRequestSuccessTests.cs b/Wino.Core.Tests/Synchronizers/OutlookSynchronizerRequestSuccessTests.cs new file mode 100644 index 00000000..c723e79d --- /dev/null +++ b/Wino.Core.Tests/Synchronizers/OutlookSynchronizerRequestSuccessTests.cs @@ -0,0 +1,102 @@ +using System.Net; +using System.Net.Http; +using System.Reflection; +using FluentAssertions; +using Microsoft.Kiota.Abstractions; +using Moq; +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Integration.Processors; +using Wino.Core.Requests.Bundles; +using Wino.Core.Requests.Mail; +using Wino.Core.Synchronizers.Mail; +using Xunit; + +namespace Wino.Core.Tests.Synchronizers; + +public sealed class OutlookSynchronizerRequestSuccessTests +{ + [Fact] + public async Task HandleSuccessfulResponseAsync_MarkReadRequest_PersistsLocalReadStateEvenWithoutResponseBody() + { + var changeProcessor = new Mock(MockBehavior.Strict); + changeProcessor + .Setup(x => x.ChangeMailReadStatusAsync("mail-id", true)) + .Returns(Task.CompletedTask); + + var synchronizer = CreateSynchronizer(changeProcessor.Object); + var request = new MarkReadRequest(CreateMailCopy(), IsRead: true); + var bundle = new HttpRequestBundle(new RequestInformation(), request, request); + using var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(string.Empty) + }; + + await InvokeHandleSuccessfulResponseAsync(synchronizer, bundle, response); + + changeProcessor.Verify(x => x.ChangeMailReadStatusAsync("mail-id", true), Times.Once); + } + + [Fact] + public async Task HandleSuccessfulResponseAsync_ChangeFlagRequest_PersistsLocalFlagStateEvenWithoutResponseBody() + { + var changeProcessor = new Mock(MockBehavior.Strict); + changeProcessor + .Setup(x => x.ChangeFlagStatusAsync("mail-id", true)) + .Returns(Task.CompletedTask); + + var synchronizer = CreateSynchronizer(changeProcessor.Object); + var request = new ChangeFlagRequest(CreateMailCopy(), IsFlagged: true); + var bundle = new HttpRequestBundle(new RequestInformation(), request, request); + using var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(string.Empty) + }; + + await InvokeHandleSuccessfulResponseAsync(synchronizer, bundle, response); + + changeProcessor.Verify(x => x.ChangeFlagStatusAsync("mail-id", true), Times.Once); + } + + private static OutlookSynchronizer CreateSynchronizer(IOutlookChangeProcessor changeProcessor) + { + var account = new MailAccount + { + Id = Guid.NewGuid(), + Name = "Outlook", + Address = "user@example.com" + }; + + var authenticator = new Mock(MockBehavior.Loose); + var errorFactory = new Mock(MockBehavior.Loose); + + return new OutlookSynchronizer(account, authenticator.Object, changeProcessor, errorFactory.Object); + } + + private static MailCopy CreateMailCopy() => + new() + { + UniqueId = Guid.NewGuid(), + Id = "mail-id", + FolderId = Guid.NewGuid(), + IsRead = false, + IsFlagged = false + }; + + private static async Task InvokeHandleSuccessfulResponseAsync( + OutlookSynchronizer synchronizer, + HttpRequestBundle bundle, + HttpResponseMessage response) + { + var method = typeof(OutlookSynchronizer).GetMethod( + "HandleSuccessfulResponseAsync", + BindingFlags.Instance | BindingFlags.NonPublic); + + method.Should().NotBeNull(); + + var task = method!.Invoke(synchronizer, [bundle, response]) as Task; + task.Should().NotBeNull(); + await task!; + } +} diff --git a/Wino.Core/Requests/Mail/ChangeFlagRequest.cs b/Wino.Core/Requests/Mail/ChangeFlagRequest.cs index 54f33a19..231435f4 100644 --- a/Wino.Core/Requests/Mail/ChangeFlagRequest.cs +++ b/Wino.Core/Requests/Mail/ChangeFlagRequest.cs @@ -12,6 +12,8 @@ namespace Wino.Core.Requests.Mail; public record ChangeFlagRequest(MailCopy Item, bool IsFlagged) : MailRequestBase(Item), ICustomFolderSynchronizationRequest { + private readonly bool _originalIsFlagged = Item.IsFlagged; + public List SynchronizationFolderIds => [Item.FolderId]; public bool ExcludeMustHaveFolders => true; @@ -22,7 +24,7 @@ public record ChangeFlagRequest(MailCopy Item, bool IsFlagged) : MailRequestBase /// Gets whether this request represents an actual state change. /// If the mail is already in the desired flagged state, no change is needed. /// - public bool IsNoOp => Item.IsFlagged == IsFlagged; + public bool IsNoOp { get; } = Item.IsFlagged == IsFlagged; public override void ApplyUIChanges() { @@ -39,7 +41,7 @@ public record ChangeFlagRequest(MailCopy Item, bool IsFlagged) : MailRequestBase // Skip UI revert if this was a no-op request if (IsNoOp) return; - Item.IsFlagged = !IsFlagged; + Item.IsFlagged = _originalIsFlagged; WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.ClientReverted, MailCopyChangeFlags.IsFlagged)); } diff --git a/Wino.Core/Requests/Mail/MarkReadRequest.cs b/Wino.Core/Requests/Mail/MarkReadRequest.cs index 656df721..28c4fd3b 100644 --- a/Wino.Core/Requests/Mail/MarkReadRequest.cs +++ b/Wino.Core/Requests/Mail/MarkReadRequest.cs @@ -11,6 +11,8 @@ namespace Wino.Core.Requests.Mail; public record MarkReadRequest(MailCopy Item, bool IsRead) : MailRequestBase(Item), ICustomFolderSynchronizationRequest { + private readonly bool _originalIsRead = Item.IsRead; + public List SynchronizationFolderIds => [Item.FolderId]; public override MailSynchronizerOperation Operation => MailSynchronizerOperation.MarkRead; @@ -21,7 +23,7 @@ public record MarkReadRequest(MailCopy Item, bool IsRead) : MailRequestBase(Item /// Gets whether this request represents an actual state change. /// If the mail is already in the desired read state, no change is needed. /// - public bool IsNoOp => Item.IsRead == IsRead; + public bool IsNoOp { get; } = Item.IsRead == IsRead; public override void ApplyUIChanges() { @@ -38,7 +40,7 @@ public record MarkReadRequest(MailCopy Item, bool IsRead) : MailRequestBase(Item // Skip UI revert if this was a no-op request if (IsNoOp) return; - Item.IsRead = !IsRead; + Item.IsRead = _originalIsRead; WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.ClientReverted, MailCopyChangeFlags.IsRead)); } diff --git a/Wino.Core/Synchronizers/OutlookSynchronizer.cs b/Wino.Core/Synchronizers/OutlookSynchronizer.cs index dba0b298..621953ec 100644 --- a/Wino.Core/Synchronizers/OutlookSynchronizer.cs +++ b/Wino.Core/Synchronizers/OutlookSynchronizer.cs @@ -1884,6 +1884,18 @@ public class OutlookSynchronizer : WinoSynchronizer a.ImapConnectionSecurity == ServerInformation.OutgoingServerSocketOption); } - SelectedTabIndex = _statePersistanceService.ApplicationMode == WinoApplicationMode.Calendar ? 1 : 0; + SelectedTabIndex = _statePersistanceService.ApplicationMode == WinoApplicationMode.Calendar ? 2 : 1; var folderStructures = (await _folderService.GetFolderStructureForAccountAsync(Account.Id, true)).Folders; @@ -300,7 +301,7 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel foreach (var calendar in calendars) { AccountCalendars.Add(calendar); - AccountCalendarSettingsItems.Add(new AccountCalendarSettingsItemViewModel(calendar, ShowAsOptions)); + AccountCalendarSettingsItems.Add(new AccountCalendarSettingsItemViewModel(calendar, ShowAsOptions, AvailableColors)); } }); @@ -328,6 +329,15 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel await _calendarService.UpdateAccountCalendarAsync(calendar); } + public async Task UpdateCalendarColorAsync(AccountCalendarSettingsItemViewModel calendarItem, AppColorViewModel color) + { + if (calendarItem?.Calendar == null || color == null || calendarItem.Calendar.BackgroundColorHex == color.Hex) + return; + + calendarItem.SetBackgroundColor(color); + await _calendarService.UpdateAccountCalendarAsync(calendarItem.Calendar); + } + [RelayCommand] private void ResetColor() => SelectedColor = null; @@ -403,6 +413,7 @@ public partial class AccountCalendarSettingsItemViewModel : ObservableObject { public AccountCalendar Calendar { get; } public ObservableCollection ShowAsOptions { get; } + public List AvailableColors { get; } public string Name => Calendar.Name; public string TimeZone => Calendar.TimeZone; @@ -414,11 +425,24 @@ public partial class AccountCalendarSettingsItemViewModel : ObservableObject [ObservableProperty] public partial AccountCalendarShowAsOption SelectedShowAsOption { get; set; } - public AccountCalendarSettingsItemViewModel(AccountCalendar calendar, ObservableCollection showAsOptions) + [ObservableProperty] + public partial AppColorViewModel SelectedColor { get; set; } + + public AccountCalendarSettingsItemViewModel(AccountCalendar calendar, ObservableCollection showAsOptions, List availableColors) { Calendar = calendar; ShowAsOptions = showAsOptions; + AvailableColors = availableColors; IsSynchronizationEnabled = calendar.IsSynchronizationEnabled; SelectedShowAsOption = showAsOptions.FirstOrDefault(option => option.ShowAs == calendar.DefaultShowAs) ?? showAsOptions.FirstOrDefault(); + SelectedColor = availableColors.FirstOrDefault(color => string.Equals(color.Hex, calendar.BackgroundColorHex, StringComparison.OrdinalIgnoreCase)) + ?? new AppColorViewModel(calendar.BackgroundColorHex ?? ColorHelpers.GenerateFlatColorHex()); + } + + public void SetBackgroundColor(AppColorViewModel color) + { + SelectedColor = color; + Calendar.BackgroundColorHex = color.Hex; + OnPropertyChanged(nameof(BackgroundColorHex)); } } diff --git a/Wino.Mail.WinUI/Views/Account/AccountDetailsPage.xaml b/Wino.Mail.WinUI/Views/Account/AccountDetailsPage.xaml index ddf3895b..a68b0c80 100644 --- a/Wino.Mail.WinUI/Views/Account/AccountDetailsPage.xaml +++ b/Wino.Mail.WinUI/Views/Account/AccountDetailsPage.xaml @@ -76,268 +76,20 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -592,25 +611,36 @@ - - + + + + + + + + + + + + - + + diff --git a/Wino.Mail.WinUI/Views/Account/AccountDetailsPage.xaml.cs b/Wino.Mail.WinUI/Views/Account/AccountDetailsPage.xaml.cs index 0047c825..76f64bfd 100644 --- a/Wino.Mail.WinUI/Views/Account/AccountDetailsPage.xaml.cs +++ b/Wino.Mail.WinUI/Views/Account/AccountDetailsPage.xaml.cs @@ -1,6 +1,7 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Navigation; +using Wino.Core.ViewModels.Data; using Wino.Core.Domain.Models.Folders; using Wino.Mail.ViewModels; using Wino.Views.Abstract; @@ -56,6 +57,14 @@ public sealed partial class AccountDetailsPage : AccountDetailsPageAbstract } } + private async void CalendarColorItemClick(object sender, ItemClickEventArgs e) + { + if (sender is GridView { Tag: AccountCalendarSettingsItemViewModel calendarItem } && e.ClickedItem is AppColorViewModel color) + { + await ViewModel.UpdateCalendarColorAsync(calendarItem, color); + } + } + protected override void OnNavigatedTo(NavigationEventArgs e) { base.OnNavigatedTo(e);