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 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
@@ -350,6 +102,254 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
@@ -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);