General account details settings and some marking mail issues

This commit is contained in:
Burak Kaan Köse
2026-04-01 01:41:17 +02:00
parent 6f61605c12
commit 7b369201b0
11 changed files with 536 additions and 435 deletions
-160
View File
@@ -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 `<UseWinUI>true</UseWinUI>` 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: 48 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.
-6
View File
@@ -1,6 +0,0 @@
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
version = 1
name = "Wino-Mail"
[setup]
script = ""
+1
View File
@@ -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`.
@@ -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<MailUpdatedMessage>
{
public List<MailUpdatedMessage> Updated { get; } = [];
public void Receive(MailUpdatedMessage message) => Updated.Add(message);
}
}
@@ -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<IOutlookChangeProcessor>(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<RequestInformation>(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<IOutlookChangeProcessor>(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<RequestInformation>(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<IAuthenticator>(MockBehavior.Loose);
var errorFactory = new Mock<IOutlookSynchronizerErrorHandlerFactory>(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<RequestInformation> 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!;
}
}
+4 -2
View File
@@ -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<Guid> 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.
/// </summary>
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));
}
+4 -2
View File
@@ -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<Guid> 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.
/// </summary>
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));
}
@@ -1884,6 +1884,18 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
{
try
{
if (bundle?.UIChangeRequest is MarkReadRequest markReadRequest)
{
await _outlookChangeProcessor.ChangeMailReadStatusAsync(markReadRequest.Item.Id, markReadRequest.IsRead).ConfigureAwait(false);
return;
}
if (bundle?.UIChangeRequest is ChangeFlagRequest changeFlagRequest)
{
await _outlookChangeProcessor.ChangeFlagStatusAsync(changeFlagRequest.Item.Id, changeFlagRequest.IsFlagged).ConfigureAwait(false);
return;
}
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(content))
return;
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Wino.Core.Misc;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Shared;
@@ -263,7 +264,7 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
SelectedOutgoingServerConnectionSecurityIndex = AvailableConnectionSecurities.FindIndex(a => 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<AccountCalendarShowAsOption> ShowAsOptions { get; }
public List<AppColorViewModel> 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<AccountCalendarShowAsOption> showAsOptions)
[ObservableProperty]
public partial AppColorViewModel SelectedColor { get; set; }
public AccountCalendarSettingsItemViewModel(AccountCalendar calendar, ObservableCollection<AccountCalendarShowAsOption> showAsOptions, List<AppColorViewModel> 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));
}
}
@@ -76,11 +76,36 @@
<ScrollViewer>
<Grid RowSpacing="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<controls:Segmented
x:Name="TabSelector"
Grid.Row="0"
HorizontalAlignment="Center"
SelectionChanged="OnTabSelectionChanged">
<controls:SegmentedItem Content="{x:Bind domain:Translator.AccountDetailsPage_TabGeneral, Mode=OneTime}">
<controls:SegmentedItem.Icon>
<SymbolIcon Symbol="ContactInfo" />
</controls:SegmentedItem.Icon>
</controls:SegmentedItem>
<controls:SegmentedItem Content="{x:Bind domain:Translator.AccountDetailsPage_TabMail, Mode=OneTime}">
<controls:SegmentedItem.Icon>
<SymbolIcon Symbol="Mail" />
</controls:SegmentedItem.Icon>
</controls:SegmentedItem>
<controls:SegmentedItem Content="{x:Bind domain:Translator.AccountDetailsPage_TabCalendar, Mode=OneTime}">
<controls:SegmentedItem.Icon>
<FontIcon Glyph="&#xE163;" />
</controls:SegmentedItem.Icon>
</controls:SegmentedItem>
</controls:Segmented>
<Grid
x:Name="GeneralSettingsPanel"
Grid.Row="2"
Visibility="Collapsed">
<StackPanel>
<controls:SettingsCard Description="{x:Bind domain:Translator.AccountEditDialog_Message}" Header="{x:Bind domain:Translator.AccountDetailsPage_Title}">
<controls:SettingsCard.HeaderIcon>
@@ -322,33 +347,8 @@
</controls:SettingsCard>
</StackPanel>
<!--<controls:SettingsExpander
Description="{x:Bind ViewModel.Address, Mode=OneWay}"
Header="{x:Bind ViewModel.AccountName, Mode=OneWay}"
IsExpanded="True">
<controls:SettingsExpander.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False" UriSource="{x:Bind ViewModel.ProviderIconPath, Mode=OneWay}" />
</controls:SettingsExpander.HeaderIcon>
<controls:SettingsExpander.Items>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>-->
<controls:Segmented
x:Name="TabSelector"
Grid.Row="1"
HorizontalAlignment="Center"
SelectionChanged="OnTabSelectionChanged">
<controls:SegmentedItem Content="{x:Bind domain:Translator.AccountDetailsPage_TabMail, Mode=OneTime}">
<controls:SegmentedItem.Icon>
<SymbolIcon Symbol="Mail" />
</controls:SegmentedItem.Icon>
</controls:SegmentedItem>
<controls:SegmentedItem Content="{x:Bind domain:Translator.AccountDetailsPage_TabCalendar, Mode=OneTime}">
<controls:SegmentedItem.Icon>
<FontIcon Glyph="&#xE163;" />
</controls:SegmentedItem.Icon>
</controls:SegmentedItem>
</controls:Segmented>
</Grid>
<!-- Mail Settings Panel -->
<Grid
@@ -551,11 +551,30 @@
<controls:SettingsCard.HeaderIcon>
<FontIcon Glyph="&#xE790;" />
</controls:SettingsCard.HeaderIcon>
<Button
MinWidth="40"
MinHeight="40"
Padding="4"
Tag="{x:Bind}">
<Button.Flyout>
<Flyout Placement="BottomEdgeAlignedLeft">
<GridView
Width="220"
IsItemClickEnabled="True"
ItemClick="CalendarColorItemClick"
ItemTemplate="{StaticResource AccountColorTemplate}"
ItemsSource="{x:Bind AvailableColors, Mode=OneWay}"
SelectedItem="{x:Bind SelectedColor, Mode=TwoWay}"
SelectionMode="Single"
Tag="{x:Bind}" />
</Flyout>
</Button.Flyout>
<Border
Width="32"
Height="32"
Width="24"
Height="24"
Background="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(BackgroundColorHex), Mode=OneWay}"
CornerRadius="4" />
</Button>
</controls:SettingsCard>
<controls:SettingsCard Description="{x:Bind domain:Translator.CalendarAccountSettings_SyncEnabledDescription, Mode=OneTime}" Header="{x:Bind domain:Translator.CalendarAccountSettings_SyncEnabled, Mode=OneTime}">
@@ -592,25 +611,36 @@
</StackPanel>
</Grid>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="TabStates">
<VisualState x:Name="MailTab">
<VisualState x:Name="GeneralTab">
<VisualState.StateTriggers>
<StateTrigger IsActive="{x:Bind helpers:XamlHelpers.AreEqual(ViewModel.SelectedTabIndex, 0), Mode=OneWay}" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="GeneralSettingsPanel.Visibility" Value="Visible" />
<Setter Target="MailSettingsPanel.Visibility" Value="Collapsed" />
<Setter Target="CalendarSettingsPanel.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="MailTab">
<VisualState.StateTriggers>
<StateTrigger IsActive="{x:Bind helpers:XamlHelpers.AreEqual(ViewModel.SelectedTabIndex, 1), Mode=OneWay}" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="GeneralSettingsPanel.Visibility" Value="Collapsed" />
<Setter Target="MailSettingsPanel.Visibility" Value="Visible" />
<Setter Target="CalendarSettingsPanel.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="CalendarTab">
<VisualState.StateTriggers>
<StateTrigger IsActive="{x:Bind helpers:XamlHelpers.AreEqual(ViewModel.SelectedTabIndex, 1), Mode=OneWay}" />
<StateTrigger IsActive="{x:Bind helpers:XamlHelpers.AreEqual(ViewModel.SelectedTabIndex, 2), Mode=OneWay}" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="GeneralSettingsPanel.Visibility" Value="Collapsed" />
<Setter Target="MailSettingsPanel.Visibility" Value="Collapsed" />
<Setter Target="CalendarSettingsPanel.Visibility" Value="Visible" />
</VisualState.Setters>
@@ -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);