diff --git a/.editorconfig b/.editorconfig
index b06e6570..ba6b725a 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -8,6 +8,9 @@ dotnet_style_operator_placement_when_wrapping = beginning_of_line
tab_width = 4
indent_size = 4
end_of_line = crlf
+[XamlTypeInfo.g.cs]
+dotnet_diagnostic.CS0612.severity = none
+dotnet_diagnostic.CS0618.severity = none
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
@@ -289,4 +292,4 @@ csharp_style_prefer_readonly_struct = true:suggestion
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent
csharp_style_prefer_primary_constructors = true:silent
-csharp_prefer_system_threading_lock = true:suggestion
\ No newline at end of file
+csharp_prefer_system_threading_lock = true:suggestion
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 00000000..91271c54
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,191 @@
+# Copilot Instructions for Wino-Mail Project
+
+## Project Overview
+
+Wino Mail is a native Windows mail client targeting Windows 10 1809+ and Windows 11. The project is **transitioning from UWP to WinUI 3** - always work with WinUI projects (Wino.Mail.WinUI, Wino.Core.WinUI), never edit the old Wino.Mail UWP project.
+
+### Key Technologies
+- **WinUI 3** for UI (previously UWP/WinUI 2)
+- **MVVM Toolkit** (CommunityToolkit.Mvvm) for ViewModels with source generators
+- **Messenger** pattern (WeakReferenceMessenger.Default) for event pub-sub throughout the codebase
+- **SQLite** database stored in publisher cache folder (not local storage)
+- **WebView2** for mail rendering/composition with custom HTML/JavaScript editors
+- **MimeKit/MailKit** for IMAP/SMTP operations
+- **Microsoft Graph SDK** for Outlook synchronization
+- **Gmail API** for Gmail synchronization
+
+### Solution Structure
+```
+Wino.Core.Domain → Entities, interfaces, translations, enums (shared contracts)
+Wino.Core → Synchronization engine, authenticators, request processing
+Wino.Services → Database, mail, folder, account services
+Wino.Mail.ViewModels → Mail-specific ViewModels
+Wino.Core.ViewModels → Shared ViewModels (settings, personalization)
+Wino.Mail.WinUI → **Active WinUI 3 UI project** (use this)
+Wino.Mail → **Deprecated UWP project** (DO NOT EDIT)
+```
+
+## Architecture Patterns
+
+### Mail Synchronization Flow
+1. **WinoRequestDelegator** → Validates and delegates user actions (mark read, delete, move)
+2. **WinoRequestProcessor** → Batches requests using RequestComparer, queues to synchronizers
+3. **Synchronizers** (OutlookSynchronizer, GmailSynchronizer, ImapSynchronizer) → Execute batched operations
+4. **ChangeProcessors** (OutlookChangeProcessor, etc.) → Apply changes to local database
+5. Database updates trigger **Messenger** events (MailAddedMessage, MailUpdatedMessage, etc.)
+
+### Queue-Based Sync (New Pattern - See QUEUE_SYNC_IMPLEMENTATION.md)
+- Initial sync now queues mail IDs first (MailItemQueue table), downloads metadata only (no MIME)
+- MIME content downloaded on-demand when user opens mail
+- Synchronizers override `QueueMailIdsForInitialSyncAsync()`, `DownloadMailsFromQueueAsync()`, `CreateMinimalMailCopyAsync()`
+- Check `MailItemFolder.IsInitialSyncCompleted` to determine sync state
+
+### Dependency Injection Setup
+Services registered in extension methods across projects:
+- `RegisterCoreServices()` in Wino.Core/CoreContainerSetup.cs
+- `RegisterSharedServices()` in Wino.Services/ServicesContainerSetup.cs
+- `RegisterCoreUWPServices()` in CoreUWPContainerSetup.cs
+- ViewModels registered in App.xaml.cs with AddTransient/AddSingleton
+
+### Messenger Pattern (Event Pub-Sub)
+- All ViewModels inherit from CoreBaseViewModel or MailBaseViewModel which implement IRecipient
+- Register/unregister message handlers in `RegisterRecipients()` / `UnregisterRecipients()`
+- Send messages via `WeakReferenceMessenger.Default.Send(new MessageType(...))`
+- Common messages: MailAddedMessage, MailUpdatedMessage, NavigationRequested, ThemeChanged
+
+## ViewModels Development Guidelines
+
+### Observable Properties - Critical Pattern
+- **ALWAYS** use `public partial` observable properties with MVVM Toolkit source generators
+- **NEVER** use private fields with `[ObservableProperty]` attribute
+- **Correct:**
+ ```csharp
+ [ObservableProperty]
+ public partial string SearchQuery { get; set; } = string.Empty;
+ ```
+- **Incorrect:**
+ ```csharp
+ [ObservableProperty]
+ private string searchQuery = string.Empty; // WRONG - will not work
+ ```
+
+### ViewModels Structure
+- Inherit from MailBaseViewModel (for mail features) or CoreBaseViewModel (for shared features)
+- Use `[RelayCommand]` for command methods - source generator creates Command properties
+- Implement IRecipient for message handlers
+- Use `IMailDialogService` for Mail-related dialogs, `IDialogServiceBase` for core dialogs
+- Call `RegisterRecipients()` in constructor/OnNavigatedTo, `UnregisterRecipients()` in OnNavigatedFrom
+
+## Localization System
+
+### Translation Workflow (Custom T4-based System)
+1. Add English strings ONLY to `Wino.Core.Domain/Translations/en_US/resources.json`
+2. Build the project - source generators automatically create Translator properties
+3. Use `Translator.{PropertyName}` in ViewModels, XAML (with x:Bind, OneTime mode)
+4. **NEVER** edit other language files - Crowdin manages translations automatically
+5. **NEVER** hardcode user-facing strings
+
+### Usage Examples
+```csharp
+// ViewModel
+_dialogService.InfoBarMessage(Translator.Info_MissingFolderTitle, message);
+
+// XAML
+
+```
+
+## UI Data Binding and Converters
+
+### WinUI 3 Automatic Conversions
+- **NEVER** create IValueConverter classes or add them to Converters.xaml
+- **NEVER** use BoolToVisibilityConverter - WinUI 3 SDK automatically converts bool to Visibility
+- Direct binding: `Visibility="{x:Bind IsVisible, Mode=OneWay}"`
+- Register control events (for example `Loaded`, `Unloaded`, `SizeChanged`, `PointerEntered`) in XAML markup, not with `+=` in `.xaml.cs`.
+
+### XamlHelpers for Complex Conversions
+- **ALWAYS** use XamlHelpers static methods instead of converters
+- Add xmlns: `xmlns:helpers="using:Wino.Helpers"`
+- Usage: `{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(PropertyName), Mode=OneWay}`
+- Available methods: ReverseBoolToVisibilityConverter, CountToBooleanConverter, BoolToSelectionMode, Base64ToBitmapImage
+- Add new methods to XamlHelpers.cs when needed, don't create converters
+
+## WebView2 Mail Rendering
+
+### Architecture
+- **reader.html** (Wino.Mail.WinUI/JS/) for reading mails
+- **editor.html** for composing mails (uses Jodit editor, not Quill as originally planned)
+- WebView2 uses virtual host mapping: `https://wino.mail/reader.html`
+- JavaScript interop via `ExecuteScriptFunctionAsync()` to call functions like `RenderHTML()`
+- MIME content downloaded on-demand, not during sync
+
+### Key Patterns
+- Set environment variables for WebView2 before initialization (overlay scrollbars, cache)
+- Wait for DOMContentLoaded event before script execution
+- Handle theme changes by updating editor CSS dynamically
+- Cancel external navigation, open in browser via Launcher.LaunchUriAsync()
+
+## File Structure and Project Organization
+
+### Critical Rules
+- **NEVER** edit files in Wino.Mail (UWP) project - it's deprecated
+- **ALWAYS** work with Wino.Mail.WinUI for UI components
+- Place ViewModels in Wino.Mail.ViewModels (mail-specific) or Wino.Core.ViewModels (shared)
+- Create abstract base classes in Views/Abstract folders
+- Mail-specific dialog services go in Wino.Mail.WinUI/Services
+
+### Database and Storage
+- SQLite database in publisher cache folder (not app local storage)
+- EML files stored in app local storage, referenced by MailCopy.FileId
+- Paths resolved via MimeFileService.GetMimeMessagePath()
+- Database entities in Wino.Core.Domain/Entities
+
+## Error Handling and User Feedback
+
+### Exception Handling Patterns
+```csharp
+try {
+ await operation();
+} catch (UnavailableSpecialFolderException ex) {
+ _dialogService.InfoBarMessage(title, message, InfoBarMessageType.Warning, buttonText, action);
+} catch (NotImplementedException) {
+ _dialogService.ShowNotSupportedMessage();
+}
+```
+
+### Dialog Service Methods
+- `InfoBarMessage()` - simple notifications with optional action button
+- `ShowConfirmationDialogAsync()` - yes/no dialogs
+- `PickFilesAsync()` - file selection
+- Always check for null/empty results from dialog operations
+
+## Code Style and Best Practices
+
+- Use `var` where type is obvious from right side
+- String interpolation over string.Format for simple cases
+- Keep methods focused and single-responsibility
+- Add XML documentation for public APIs
+- Avoid introducing new NuGet packages - maximize use of existing libraries
+- Wrap async operations in try-catch blocks
+- Log errors via IWinoLogger but don't expose technical details to users
+
+## Development Workflow
+
+### Building and Running
+- Open WinoMail.slnx in Visual Studio 2022+
+- Target platforms: x86, x64, ARM64 (ARM32 being phased out)
+- Minimum: Windows 10 1809, Target: Windows 11 22H2
+- Set Wino.Mail.WinUI as startup project
+
+### Testing
+- Test suite in Wino.Core.Tests
+- Manual testing required for UI/WebView2 interactions
+- Test synchronization with real accounts when modifying synchronizers
+
+### Common Pitfalls
+- Forgetting to register ViewModels in App.xaml.cs RegisterViewModels()
+- Not calling RegisterRecipients() for message handlers
+- Using private fields with [ObservableProperty] (won't work - must be public partial)
+- Creating IValueConverter classes instead of using XamlHelpers
+- Editing UWP project files instead of WinUI equivalents
+- Hardcoding strings instead of using Translator
+- Forgetting to unregister Messenger recipients (memory leaks)
diff --git a/.github/workflows/pr-winui-build.yml b/.github/workflows/pr-winui-build.yml
new file mode 100644
index 00000000..93be3bc4
--- /dev/null
+++ b/.github/workflows/pr-winui-build.yml
@@ -0,0 +1,125 @@
+name: PR WinUI Build
+
+on:
+ pull_request:
+ types:
+ - opened
+ - synchronize
+ - reopened
+ - ready_for_review
+
+permissions:
+ contents: read
+ packages: read
+
+jobs:
+ build-winui:
+ name: Build project (${{ matrix.platform }})
+ if: github.event.pull_request.draft == false
+ runs-on: windows-latest
+ continue-on-error: ${{ contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association) }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - platform: x86
+ rid: win-x86
+ - platform: x64
+ rid: win-x64
+ - platform: ARM64
+ rid: win-arm64
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup .NET SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 10.0.x
+ source-url: https://nuget.pkg.github.com/bkaankose/index.json
+ env:
+ NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Restore WinUI project dependencies
+ run: dotnet restore Wino.Mail.WinUI/Wino.Mail.WinUI.csproj --configfile nuget.config -p:Platform=${{ matrix.platform }} -p:RuntimeIdentifier=${{ matrix.rid }}
+
+ - name: Build WinUI project
+ run: dotnet build Wino.Mail.WinUI/Wino.Mail.WinUI.csproj --configuration Release --no-restore -p:Platform=${{ matrix.platform }} -p:RuntimeIdentifier=${{ matrix.rid }} -p:GenerateAppxPackageOnBuild=false -p:AppxPackageSigningEnabled=false
+
+ core-tests:
+ name: Run Core tests
+ if: github.event.pull_request.draft == false
+ runs-on: windows-latest
+ continue-on-error: ${{ contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association) }}
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup .NET SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 10.0.x
+ source-url: https://nuget.pkg.github.com/bkaankose/index.json
+ env:
+ NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Restore Core test projects
+ shell: pwsh
+ run: |
+ $coreTests = Get-ChildItem -Path . -Recurse -Filter "*Core*.Tests.csproj" | ForEach-Object { $_.FullName }
+ if (-not $coreTests) {
+ throw "No Core test projects were found."
+ }
+
+ foreach ($project in $coreTests) {
+ dotnet restore $project --configfile nuget.config
+ }
+
+ - name: Run Core test projects
+ shell: pwsh
+ run: |
+ New-Item -ItemType Directory -Path TestResults -Force | Out-Null
+ $coreTests = Get-ChildItem -Path . -Recurse -Filter "*Core*.Tests.csproj"
+ if (-not $coreTests) {
+ throw "No Core test projects were found."
+ }
+
+ foreach ($project in $coreTests) {
+ $name = $project.BaseName
+ dotnet test $project.FullName --configuration Release --no-restore --verbosity normal --logger "trx;LogFileName=$name.trx" --results-directory TestResults
+ }
+
+ - name: Upload Core test result artifacts
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: core-test-results
+ path: TestResults/*.trx
+ if-no-files-found: warn
+
+ - name: Publish Core test report
+ if: always()
+ uses: EnricoMi/publish-unit-test-result-action/windows@v2
+ with:
+ trx_files: TestResults/*.trx
+ check_name: Core test results
+
+ enforce-for-non-maintainers:
+ name: Enforce required checks (non-maintainers)
+ if: github.event.pull_request.draft == false && !contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association)
+ runs-on: ubuntu-latest
+ needs:
+ - build-winui
+ - core-tests
+
+ steps:
+ - name: Fail when build or tests fail for non-maintainers
+ if: needs.build-winui.result != 'success' || needs.core-tests.result != 'success'
+ run: |
+ echo "WinUI build and Core tests must pass for non-maintainer pull requests."
+ exit 1
+
+ - name: Confirm build and test success for non-maintainers
+ run: echo "WinUI build and Core tests passed."
diff --git a/.gitignore b/.gitignore
index cfa63f40..b19b3eb0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -399,3 +399,4 @@ Wino/obj/x86/Debug/XamlSaveStateFile.xml
Wino/obj/x86/Debug/XamlSaveStateFile.xml
*.cache
.vs/Wino/v16/.suo
+/.claude/settings.local.json
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 00000000..08771976
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,160 @@
+# AGENTS.md
+
+This file provides guidance to AI agent when working with code in this repository.
+
+## Project Overview
+
+Wino Mail is a native Windows mail client (Windows 10 1809+ / Windows 11) replacing the deprecated Windows Mail & Calendar. It's **transitioning from UWP to WinUI 3** - always work with WinUI projects (Wino.Mail.WinUI), never edit the deprecated Wino.Mail UWP project.
+
+## Build and Development Commands
+
+```bash
+# Open solution
+# WinoMail.slnx is the main solution file (VS 2022+)
+
+# Build WinUI project (Debug x64)
+dotnet restore Wino.Mail.WinUI/Wino.Mail.WinUI.csproj --configfile nuget.config -p:Platform=x64 -p:RuntimeIdentifier=win-x64 && dotnet build Wino.Mail.WinUI/Wino.Mail.WinUI.csproj -c Debug --no-restore /p:Platform=x64 /p:RuntimeIdentifier=win-x64 /p:GenerateAppxPackageOnBuild=false /p:AppxPackageSigningEnabled=false
+
+# Build WinUI project with diagnostic XAML/compiler logging (use when plain build only shows "XamlCompiler.exe exited with code 1")
+dotnet build Wino.Mail.WinUI/Wino.Mail.WinUI.csproj -c Debug --no-restore /p:Platform=x64 /p:RuntimeIdentifier=win-x64 /p:GenerateAppxPackageOnBuild=false /p:AppxPackageSigningEnabled=false "/flp:logfile=winui-build.log;verbosity=diagnostic" /bl:winui-build.binlog
+
+# Run tests (Debug x64)
+dotnet test Wino.Core.Tests/Wino.Core.Tests.csproj -c Debug /p:Platform=x64
+
+# Copilot CLI build command (Debug x64)
+dotnet restore Wino.Mail.WinUI/Wino.Mail.WinUI.csproj --configfile nuget.config -p:Platform=x64 -p:RuntimeIdentifier=win-x64 && dotnet build Wino.Mail.WinUI/Wino.Mail.WinUI.csproj -c Debug --no-restore /p:Platform=x64 /p:RuntimeIdentifier=win-x64 /p:GenerateAppxPackageOnBuild=false /p:AppxPackageSigningEnabled=false
+```
+
+**Prerequisites:** Visual Studio 2022+ with ".NET desktop development" workload, .NET SDK 10+
+
+**Startup project:** Wino.Mail.WinUI
+
+**Platforms:** x86, x64, ARM64
+
+## Efficient Workflow
+
+- Start with targeted symbol or file search before reading full files
+- Prefer one focused task per thread; use a new thread for unrelated follow-up work
+- Keep verification narrow: build only the affected project, not the full solution, unless cross-project changes require it
+- After the first restore, prefer `--no-restore` builds unless package or project references changed
+- Summarize long build logs and inspect only the files named in diagnostics instead of loading large logs into context
+- When the prompt already names likely files, types, or symbols, start there instead of re-mapping the repository
+- If a WinUI build only reports `XamlCompiler.exe exited with code 1`, rerun with the diagnostic logging command above and inspect the terminal output plus `winui-build.log` for real `WMC`/`WMC1121`/binding diagnostics before guessing
+
+## Architecture
+
+### Solution Structure
+```
+Wino.Core.Domain → Entities, interfaces, translations, enums (shared contracts)
+Wino.Core → Synchronization engine, authenticators, request processing
+Wino.Services → Database, mail, folder, account services
+Wino.Authentication → OAuth2 authenticators (Outlook, Gmail)
+Wino.Mail.ViewModels → Mail-specific ViewModels
+Wino.Core.ViewModels → Shared ViewModels (settings, personalization)
+Wino.Messaging → Pub-sub message definitions
+Wino.Mail.WinUI → **Active WinUI 3 UI project** (use this)
+Wino.Mail → **Deprecated UWP project** (DO NOT EDIT)
+```
+
+### Mail Synchronization Flow
+1. **WinoRequestDelegator** → Validates and delegates user actions (mark read, delete, move)
+2. **WinoRequestProcessor** → Batches requests using RequestComparer, queues to synchronizers
+3. **Synchronizers** (OutlookSynchronizer, GmailSynchronizer, ImapSynchronizer) → Execute batched operations
+4. **ChangeProcessors** → Apply changes to local SQLite database
+5. Database updates trigger **Messenger** events (MailAddedMessage, MailUpdatedMessage, etc.)
+
+### Synchronizer Types
+- **OutlookSynchronizer** - Microsoft Graph SDK for Office 365
+- **GmailSynchronizer** - Gmail API
+- **ImapSynchronizer** - MimeKit/MailKit for IMAP/SMTP
+
+### Queue-Based Sync Pattern
+- Initial sync queues mail IDs first (MailItemQueue table), downloads metadata only
+- MIME content downloaded on-demand when user opens mail
+- Check `MailItemFolder.IsInitialSyncCompleted` for sync state
+- See QUEUE_SYNC_IMPLEMENTATION.md for details
+
+### Dependency Injection
+- `RegisterCoreServices()` in Wino.Core/CoreContainerSetup.cs
+- `RegisterSharedServices()` in Wino.Services/ServicesContainerSetup.cs
+- ViewModels registered in App.xaml.cs
+
+## Key Patterns
+
+### MVVM with Source Generators
+**CORRECT - use public partial properties:**
+```csharp
+[ObservableProperty]
+public partial string SearchQuery { get; set; } = string.Empty;
+```
+
+**WRONG - will not work:**
+```csharp
+[ObservableProperty]
+private string searchQuery = string.Empty;
+```
+
+### Messenger Pattern
+- ViewModels inherit from CoreBaseViewModel or MailBaseViewModel
+- Register handlers in `RegisterRecipients()`, unregister in `UnregisterRecipients()`
+- Send via `WeakReferenceMessenger.Default.Send(new MessageType(...))`
+
+### Data Binding - No Converters
+- **NEVER** create IValueConverter classes
+- WinUI 3 auto-converts bool to Visibility: `Visibility="{x:Bind IsVisible, Mode=OneWay}"`
+- Use XamlHelpers for complex conversions: `{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(Prop)}`
+- `x:Bind` does not implicitly convert `double` to `GridLength`; when binding `RowDefinition.Height` or `ColumnDefinition.Width`, use a `XamlHelpers` method such as `DoubleToGridLength(...)`
+- For `ComboBox` controls in XAML, never use `DisplayMemberPath` or `SelectedValuePath`; use a typed `ItemTemplate` and bind `SelectedItem` explicitly, preferably with `x:Bind`
+
+## Localization
+
+1. Add English strings ONLY to Wino.Core.Domain/Translations/en_US/resources.json
+2. Build project - source generators create Translator properties
+3. Use Translator.{PropertyName} in code/XAML
+4. NEVER edit any resources.json file outside Wino.Core.Domain/Translations/en_US/resources.json
+5. Treat all non-en_US translation files as managed externally and leave them untouched, even when adding new localization keys
+
+## Storage
+
+- **SQLite database** in publisher cache folder (shared with future Wino Calendar)
+- **EML files** in app local storage, referenced by `MailCopy.FileId`
+- Paths resolved via `MimeFileService.GetMimeMessagePath()`
+
+## WebView2 Mail Rendering
+
+- `reader.html` for reading mails, `editor.html` for composing (Jodit editor)
+- Virtual host mapping: `https://wino.mail/reader.html`
+- JavaScript interop via `ExecuteScriptFunctionAsync()`
+- MIME content downloaded on-demand, not during sync
+
+## Common Pitfalls
+
+- Forgetting to register ViewModels in App.xaml.cs `RegisterViewModels()`
+- Not calling `RegisterRecipients()` for message handlers
+- Using private fields with `[ObservableProperty]` instead of public partial
+- Creating IValueConverter classes instead of using XamlHelpers
+- Editing UWP project files instead of WinUI equivalents
+- Hardcoding strings instead of using Translator
+- Forgetting to unregister Messenger recipients (memory leaks)
+- Putting authentication validation, token refresh, account API calls, settings serialization/deserialization, or preference-application logic into ViewModels instead of the corresponding service
+
+## Code Style
+
+- Avoid introducing new NuGet packages when possible
+- Use existing libraries (MimeKit, MailKit, Microsoft Graph, Gmail API)
+- Use `var` where type is obvious
+- String interpolation over string.Format
+- Wrap async operations in try-catch
+- Log errors via IWinoLogger
+- For dependency properties in WinUI code, always prefer `[GeneratedDependencyProperty]` from CommunityToolkit over manual `DependencyProperty.Register(...)` declarations.
+- When a `[RelayCommand]` needs enable/disable logic, prefer the command's `CanExecute` over binding `Button.IsEnabled` in XAML; use `[NotifyCanExecuteChangedFor]` on dependent properties and call `NotifyCanExecuteChanged()` explicitly when non-generated state affects the command.
+- In ViewModels, update all UI-bound properties/collections via `ExecuteUIThread(...)` (especially after awaited calls and any use of `ConfigureAwait(false)`).
+- 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/Directory.Packages.props b/Directory.Packages.props
index c6262929..e0dee889 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,66 +3,78 @@
true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
+
-
-
-
+
+
+
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
+
-
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 93f88c5c..a4dd1da0 100644
--- a/README.md
+++ b/README.md
@@ -1,45 +1,54 @@
-
-
-
+
+
Wino Mail
- Native mail client for Windows device families.
+ Native mail and calendar client for Windows.
-
+
## Motivation
-I'm a big fan of Windows Mail & Calendars due to its simplicity. Personally, I find it more intuitive for daily use cases compared to Outlook desktop and the new WebView2 powered Outlook version. Seeing [Microsoft deprecating it](https://support.microsoft.com/en-us/office/outlook-for-windows-the-future-of-mail-calendar-and-people-on-windows-11-715fc27c-e0f4-4652-9174-47faa751b199#:~:text=The%20Mail%20and%20Calendar%20applications,will%20no%20longer%20be%20supported.) dragged me into starting to work on Wino a couple of years ago. Wino's main motivation is to bring all the existing functionality from Mail & Calendars over time without changing the user experience that millions have loved since the Windows 8 days in Mail & Calendars
+I'm a big fan of Windows Mail & Calendars due to its simplicity. Personally, I find it more intuitive for daily use cases compared to Outlook desktop and the new WebView2 powered Outlook version. Seeing [Microsoft deprecating it](https://support.microsoft.com/en-us/office/outlook-for-windows-the-future-of-mail-calendar-and-people-on-windows-11-715fc27c-e0f4-4652-9174-47faa751b199#:~:text=The%20Mail%20and%20Calendar%20applications,will%20no%20longer%20be%20supported.) dragged me into starting to work on Wino a couple of years ago. Wino's main motivation is to bring all the existing functionality from Mail & Calendars over time without changing the user experience that millions have loved since the Windows 8 days in Mail & Calendars.
+
+## vNext Release Highlights
+
+Wino vNext focuses on making Mail, Calendar, and Contacts feel like one cohesive native Windows experience while improving sync reliability and startup responsiveness.
+
+- 📅 **Calendar management:** Event compose/create flow, calendar-mail mapping, reminder snooze support, occurrence and detail-page improvements, and CalDAV correctness fixes.
+- 👥 **Contact management:** Improved contact workflows, account/settings integration, and contact data-model cleanup.
+- 🔄 **Synchronization reliability:** Refactored synchronizers, better state handling, 404 + 429 error handling, and duplicate-operation prevention.
+- ✉️ **Compose and drafts:** Refined editor/toolbar architecture, better rendering pipeline, Gmail draft support, and large Outlook attachment upload sessions.
+- ⚡ **Performance and quality:** Faster mail fetching with batched DB queries and caching, SQLite indexing/foreign key enforcement, and broader test + CI coverage.
+- 🎨 **WinUI polish:** Improved onboarding/startup, settings and dialogs refresh, notification routing fixes, and keyboard/navigation quality-of-life improvements.
## Features
-- API integration for Outlook and Gmail
-- IMAP/SMTP support for custom mail servers
-- Send, receive, mark as (read,important,spam etc), move mails.
-- Linked/Merged Accounts
-- Toast notifications with background sync.
-- Instant startup performance
-- Offline use / search.
-- Modern and responsive UI
-- Lots of personalization options
-- Dark / Light mode for mail reader
+- 📨 Outlook and Gmail API integration
+- 🌐 IMAP/SMTP support for custom mail servers
+- 📅 Calendar support with event creation/compose and reminders
+- 👥 Contact management and people-centric account experience
+- ✅ Core mail actions: send, receive, read/unread, move, spam, and more
+- 🔗 Linked/Merged accounts
+- 🔔 Toast notifications with background sync
+- ⚡ Instant startup-oriented architecture
+- 🔎 Offline-capable workflows and search improvements
+- 🎛️ Modern responsive WinUI interface with personalization options
+- 🌗 Dark/Light mode for mail reader and app surfaces
## Download
Download latest version of Wino Mail from Microsoft Store for free.
-
-
+
+
## Beta Releases
@@ -48,7 +57,6 @@ Stable releases will always be distributed on Microsoft Store. However, beta rel
These releases are distributed as side-loaded packages. To install them, download the **.msixbundle** file in GitHub releases and [follow the steps explained here.](https://learn.microsoft.com/en-us/windows/application-management/sideload-apps-in-windows)
-
## Contributing
Check out the [contribution guidelines](/CONTRIBUTING.md) before diving into the source code or opening an issue. There are multiple ways to contribute and all of them are explained in detail there.
@@ -59,3 +67,4 @@ Your donations will motivate me more to work on Wino in my spare time and cover
- You can [donate via Paypal by clicking here](https://www.paypal.com/donate/?hosted_button_id=LGPERGGXFMQ7U)
- You can buy Unlimited Accounts add-on in the application. It's a one-time payment for lifetime, not a monthly recurring payment.
+
diff --git a/RELEASE_NOTES_vNext.md b/RELEASE_NOTES_vNext.md
new file mode 100644
index 00000000..3625fe02
--- /dev/null
+++ b/RELEASE_NOTES_vNext.md
@@ -0,0 +1,158 @@
+# Wino Mail vNext Improvements
+
+This document summarizes the major improvements on `feature/vNext` compared to `main`, based on the commit history between the current branch and the merge-base with `main`.
+
+## Wino Calendar
+
+Calendar has grown from an early implementation into a much more complete product area on this branch.
+
+### A full Wino Calendar experience
+
+- Added a dedicated Wino Calendar app entry, making calendar a first-class experience instead of a secondary add-on.
+- Built out the calendar rendering experience with multiple rounds of rendering improvements, updated calendar view styling, calendar buttons, and better event visuals.
+- Added event creation and full event compose flows, including follow-up improvements for attachments, attendees, recurrence summaries, RSVP actions, reminders, and event details.
+- Improved support for all-day events, better display dates, occurrence handling, and mail-to-calendar mapping so calendar actions connect more naturally with messages and invitations.
+
+### Local calendar support
+
+- Added local calendar operation coverage and supporting behavior for IMAP-backed/local calendar scenarios.
+- Prevented duplicate operations by ignoring local calendar apply-changes in the wrong paths.
+- Added busy-state support and metadata fetch flows so newly created accounts can initialize calendar data more reliably.
+
+### CalDAV sync
+
+- Introduced a dedicated CalDAV synchronizer and supporting service/client work.
+- Fixed CalDAV delta sync issues.
+- Fixed CalDAV timezone issues.
+- Added manual live CalDAV workflow tests to validate real-world sync behavior.
+
+This means local and self-hosted calendar scenarios are much better represented on this branch than on `main`.
+
+### API calendar sync for Outlook and Gmail
+
+- Expanded Outlook calendar sync behavior, including broader sync windows and fixes around date/time handling.
+- Improved Gmail drafting and mail/calendar integration so event-related actions work better across providers.
+- Added mail and calendar synchronizer state tracking to make sync progress and error handling more reliable.
+- Added auto calendar sync on account creation and broader auto-sync trigger and cancellation support.
+
+### Calendar polish and reliability
+
+- Fixed calendar crashes and null-handling issues in calendar view date range updates.
+- Fixed double initialization in calendar day views.
+- Improved reaction to calendar changes and calendar item update-source handling.
+- Added reminder snooze support across toast UI, services, and database storage.
+
+Overall, Wino Calendar is one of the biggest themes of this branch: richer UI, more complete event workflows, and real sync support across local, CalDAV, Outlook, and Gmail-backed scenarios.
+
+## Wino Accounts
+
+Wino Accounts was significantly expanded and polished on this branch.
+
+### Account flows and identity
+
+- Added sign in, sign out, and registration flows.
+- Redesigned login and registration dialogs.
+- Added privacy policy presentation during registration.
+- Added forgot password and email confirmation flows.
+- Pointed the app to the real API and improved profile caching.
+
+### Account management and settings
+
+- Added Wino account settings and a dedicated management page.
+- Added a special navigation item for Wino Accounts.
+- Added import functionality for Wino Accounts.
+- Added a preference to hide the title bar Wino account button.
+- Improved the top-shell account icon and signed-out identity visuals.
+
+### Purchases and add-ons
+
+- Added handling for Paddle purchases and add-ons.
+- Added purchase-success deep linking.
+- Added support for AI pack handling through the Microsoft Store.
+
+### User-facing polish
+
+- Redesigned the Wino Account flyout and menu with a more polished Fluent-style presentation.
+- Improved account cleanup behavior when an account is deleted.
+- Added account attention handling and better account details/settings behavior.
+
+Compared to `main`, this branch turns Wino Accounts into a much more complete platform feature rather than a minimal sign-in surface.
+
+## Improved Stability and Reliability
+
+A large part of this branch is about making the app more dependable in everyday use.
+
+### Synchronization stability
+
+- Refactored synchronizers to address long-standing reliability issues.
+- Improved thread mapping across synchronizers.
+- Added generic 404 handling for synchronizers.
+- Added specific Outlook 429 handling for rate-limit scenarios.
+- Improved Outlook authentication and Outlook sync reliability.
+- Improved Gmail synchronizer behavior.
+- Added explicit mail and calendar synchronizer state support.
+
+### Mail and data reliability
+
+- Optimized mail fetching with batched database queries and in-memory caching.
+- Added SQLite indexes and enabled foreign key enforcement.
+- Switched away from the old mail item queue approach and returned to a simpler initial sync strategy.
+- Improved local draft resend behavior and added grace-period handling for local drafts.
+- Added better handling for large Outlook attachments via upload sessions.
+- Fixed issues with sent/draft placement, loading mails with infinite scroll, selection cleanup, and deleted-object scenarios.
+
+### UI and lifecycle stability
+
+- Fixed mail rendering page disposal issues.
+- Fixed WebView2 runtime toast dispatching on the UI thread.
+- Fixed startup mode issues, single-instancing problems, and shell/navigation regressions.
+- Fixed multiple thread selection, container, flicker, and context-menu issues.
+- Fixed crashes and null-reference style issues in several calendar and shell flows.
+
+### Engineering quality
+
+- Added more tests across calendar, CalDAV, IMAP, view-model, sanitization, and account sync scenarios.
+- Added a GitHub Actions workflow to build WinUI and run Core tests on pull requests.
+- Resolved warnings and moved the WinUI project toward warnings-as-errors discipline.
+- Added AOT compatibility work and related cleanup across the app.
+
+The branch is not just adding features; it is also clearly reducing failure points throughout sync, rendering, navigation, and storage.
+
+## Contacts, Settings, and General UX
+
+This branch also improves the everyday product experience outside mail and calendar core flows.
+
+### Contacts
+
+- Added contacts management.
+- Improved contacts UI and related thread/image preview behavior.
+- Removed legacy SQLite base64 contact storage from `AccountContact`.
+- Added contact picture handling support and supporting contact service improvements.
+
+### Settings
+
+- Added a dedicated settings shell and refactored settings home/navigation.
+- Expanded settings UI and introduced new setting options.
+- Added calendar settings into the settings experience.
+- Improved account details/settings pages and storage settings navigation.
+- Refined settings visuals, shell integration, and menu behavior.
+
+### Onboarding and app experience
+
+- Added a new startup window and a more guided onboarding flow with wizard-like steps.
+- Added a "What's New" implementation for feature communication.
+- Improved dialogs, title bar behavior, shell content, navigation, and shell polish across multiple iterations.
+- Added live store update notifications.
+- Improved keyboard shortcuts and related dialogs.
+- Added tray icon support and better toast routing between mail and calendar app entries.
+
+## Summary
+
+Compared to `main`, `feature/vNext` delivers four major leaps:
+
+1. Wino Calendar becomes a substantially more complete feature set, including local calendar support, CalDAV sync, and stronger Outlook and Gmail calendar integration.
+2. Wino Accounts becomes a real product surface with better authentication flows, management, imports, purchases, and polish.
+3. The app is more stable thanks to synchronization refactors, storage improvements, test expansion, and many crash and lifecycle fixes.
+4. Contacts, settings, onboarding, and shell/navigation experience all feel more mature and more consistent.
+
+In short, this branch is a broad product maturation release rather than a narrow feature drop.
diff --git a/Wino.Authentication/OutlookAuthenticator.cs b/Wino.Authentication/OutlookAuthenticator.cs
index 10bc5f26..156cc321 100644
--- a/Wino.Authentication/OutlookAuthenticator.cs
+++ b/Wino.Authentication/OutlookAuthenticator.cs
@@ -24,12 +24,14 @@ public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator
public override MailProviderType ProviderType => MailProviderType.Outlook;
private readonly IPublicClientApplication _publicClientApplication;
+ private readonly INativeAppService _nativeAppService;
private readonly IApplicationConfiguration _applicationConfiguration;
public OutlookAuthenticator(INativeAppService nativeAppService,
IApplicationConfiguration applicationConfiguration,
IAuthenticatorConfig authenticatorConfig) : base(authenticatorConfig)
{
+ _nativeAppService = nativeAppService;
_applicationConfiguration = applicationConfiguration;
var authenticationRedirectUri = nativeAppService.GetWebAuthenticationBrokerUri();
@@ -40,11 +42,25 @@ public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator
ListOperatingSystemAccounts = true,
};
- var outlookAppBuilder = PublicClientApplicationBuilder.Create(AuthenticatorConfig.OutlookAuthenticatorClientId)
- .WithParentActivityOrWindow(nativeAppService.GetCoreWindowHwnd)
- .WithBroker(options)
- .WithDefaultRedirectUri()
- .WithAuthority(Authority);
+ PublicClientApplicationBuilder outlookAppBuilder = null;
+
+ // Being created from an app notification.
+ // This is where we avoid all interactive shit for authentication.
+ if (nativeAppService.GetCoreWindowHwnd == null)
+ {
+ outlookAppBuilder = PublicClientApplicationBuilder.Create(AuthenticatorConfig.OutlookAuthenticatorClientId)
+ .WithDefaultRedirectUri()
+ .WithBroker(options)
+ .WithAuthority(Authority);
+ }
+ else
+ {
+ outlookAppBuilder = PublicClientApplicationBuilder.Create(AuthenticatorConfig.OutlookAuthenticatorClientId)
+ .WithBroker(options)
+ .WithParentActivityOrWindow(_nativeAppService.GetCoreWindowHwnd)
+ .WithDefaultRedirectUri()
+ .WithAuthority(Authority);
+ }
_publicClientApplication = outlookAppBuilder.Build();
}
@@ -82,7 +98,8 @@ public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator
catch (MsalUiRequiredException)
{
// Somehow MSAL is not able to refresh the token silently.
- // Force interactive login.
+ // Force interactive login which will include calendar scopes.
+ // The calling code should update account.IsCalendarAccessGranted = true after successful authentication.
return await GenerateTokenInformationAsync(account);
}
@@ -98,7 +115,13 @@ public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator
{
await EnsureTokenCacheAttachedAsync();
- var authResult = await _publicClientApplication
+ // Interactive authentication required but window doesn't exist.
+ // This can happen when being called from a notification background task and the token is expired.
+ // Force account attention;
+
+ if (_nativeAppService.GetCoreWindowHwnd == null) throw new AuthenticationAttentionException(account);
+
+ AuthenticationResult authResult = await _publicClientApplication
.AcquireTokenInteractive(Scope)
.ExecuteAsync();
diff --git a/Wino.Authentication/Wino.Authentication.csproj b/Wino.Authentication/Wino.Authentication.csproj
index 3c7c77ce..86d3b5e3 100644
--- a/Wino.Authentication/Wino.Authentication.csproj
+++ b/Wino.Authentication/Wino.Authentication.csproj
@@ -1,11 +1,14 @@
- net9.0
+ net10.0
win-x86;win-x64;win-arm64
Wino.Authentication
x86;x64;arm64
true
true
+ true
+ true
+ true
diff --git a/Wino.BackgroundTasks/AppUpdatedTask.cs b/Wino.BackgroundTasks/AppUpdatedTask.cs
deleted file mode 100644
index 1a70899f..00000000
--- a/Wino.BackgroundTasks/AppUpdatedTask.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using Microsoft.Toolkit.Uwp.Notifications;
-using Windows.ApplicationModel;
-using Windows.ApplicationModel.Background;
-
-namespace Wino.BackgroundTasks
-{
- ///
- /// Creates a toast notification to notify user when the Store update happens.
- ///
- public sealed class AppUpdatedTask : IBackgroundTask
- {
- public void Run(IBackgroundTaskInstance taskInstance)
- {
- var def = taskInstance.GetDeferral();
-
- var builder = new ToastContentBuilder();
- builder.SetToastScenario(ToastScenario.Default);
-
- Package package = Package.Current;
- PackageId packageId = package.Id;
- PackageVersion version = packageId.Version;
-
- var versionText = string.Format("{0}.{1}.{2}.{3}", version.Major, version.Minor, version.Build, version.Revision);
-
- // TODO: Handle with Translator, but it's not initialized here yet.
- builder.AddText("Wino Mail is updated!");
- builder.AddText(string.Format("New version {0} is ready.", versionText));
-
- builder.Show();
-
- def.Complete();
- }
- }
-}
diff --git a/Wino.BackgroundTasks/Properties/AssemblyInfo.cs b/Wino.BackgroundTasks/Properties/AssemblyInfo.cs
deleted file mode 100644
index 9887885d..00000000
--- a/Wino.BackgroundTasks/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("Wino.BackgroundTasks")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("Wino.BackgroundTasks")]
-[assembly: AssemblyCopyright("Copyright © 2023")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// Version information for an assembly consists of the following four values:
-//
-// Major Version
-// Minor Version
-// Build Number
-// Revision
-//
-// You can specify all the values or you can default the Build and Revision Numbers
-// by using the '*' as shown below:
-// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.0.0.0")]
-[assembly: AssemblyFileVersion("1.0.0.0")]
-[assembly: ComVisible(false)]
\ No newline at end of file
diff --git a/Wino.BackgroundTasks/Wino.BackgroundTasks.csproj b/Wino.BackgroundTasks/Wino.BackgroundTasks.csproj
deleted file mode 100644
index 67f9296c..00000000
--- a/Wino.BackgroundTasks/Wino.BackgroundTasks.csproj
+++ /dev/null
@@ -1,129 +0,0 @@
-
-
-
-
- Debug
- AnyCPU
- {D9EF0F59-F5F2-4D6C-A5BA-84043D8F3E08}
- winmdobj
- Properties
- Wino.BackgroundTasks
- Wino.BackgroundTasks
- en-US
- UAP
- 10.0.22621.0
- 10.0.17763.0
- 14
- 512
- {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
- false
-
-
- x86
- true
- bin\x86\Debug\
- DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP
- ;2008
- full
- false
- prompt
-
-
- x86
- bin\x86\Release\
- TRACE;NETFX_CORE;WINDOWS_UWP
- true
- ;2008
- pdbonly
- false
- prompt
-
-
- ARM64
- true
- bin\ARM64\Debug\
- DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP
- ;2008
- full
- false
- prompt
-
-
- ARM64
- bin\ARM64\Release\
- TRACE;NETFX_CORE;WINDOWS_UWP
- true
- ;2008
- pdbonly
- false
- prompt
-
-
- x64
- true
- bin\x64\Debug\
- DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP
- ;2008
- full
- false
- prompt
-
-
- x64
- bin\x64\Release\
- TRACE;NETFX_CORE;WINDOWS_UWP
- true
- ;2008
- pdbonly
- false
- prompt
-
-
- PackageReference
-
-
-
-
-
-
-
- 4.66.2
-
-
- 6.2.14
-
-
- 7.1.3
-
-
-
-
- {CF3312E5-5DA0-4867-9945-49EA7598AF1F}
- Wino.Core.Domain
-
-
- {395f19ba-1e42-495c-9db5-1a6f537fccb8}
- Wino.Core.UWP
-
-
- {e6b1632a-8901-41e8-9ddf-6793c7698b0b}
- Wino.Core
-
-
-
-
- Windows Desktop Extensions for the UWP
-
-
-
- 14.0
-
-
-
-
\ No newline at end of file
diff --git a/Wino.Calendar.ViewModels/AccountDetailsPageViewModel.cs b/Wino.Calendar.ViewModels/AccountDetailsPageViewModel.cs
deleted file mode 100644
index a110d386..00000000
--- a/Wino.Calendar.ViewModels/AccountDetailsPageViewModel.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using CommunityToolkit.Mvvm.Input;
-using CommunityToolkit.Mvvm.Messaging;
-using Wino.Calendar.ViewModels.Interfaces;
-using Wino.Core.Domain;
-using Wino.Core.Domain.Enums;
-using Wino.Core.Domain.Interfaces;
-using Wino.Core.Domain.Models.Navigation;
-using Wino.Core.ViewModels;
-using Wino.Mail.ViewModels.Data;
-using Wino.Messaging.Client.Navigation;
-
-namespace Wino.Calendar.ViewModels;
-
-public partial class AccountDetailsPageViewModel : CalendarBaseViewModel
-{
- private readonly IAccountService _accountService;
-
- public AccountProviderDetailViewModel Account { get; private set; }
- public ICalendarDialogService CalendarDialogService { get; }
- public IAccountCalendarStateService AccountCalendarStateService { get; }
-
- public AccountDetailsPageViewModel(ICalendarDialogService calendarDialogService, IAccountService accountService, IAccountCalendarStateService accountCalendarStateService)
- {
- CalendarDialogService = calendarDialogService;
- _accountService = accountService;
- AccountCalendarStateService = accountCalendarStateService;
- }
-
- [RelayCommand]
- private void EditAccountDetails()
- => Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsEditAccountDetails_Title, WinoPage.EditAccountDetailsPage, Account));
-
- public override void OnNavigatedTo(NavigationMode mode, object parameters)
- {
- base.OnNavigatedTo(mode, parameters);
- }
-}
diff --git a/Wino.Calendar.ViewModels/AccountManagementViewModel.cs b/Wino.Calendar.ViewModels/AccountManagementViewModel.cs
deleted file mode 100644
index f7eb9563..00000000
--- a/Wino.Calendar.ViewModels/AccountManagementViewModel.cs
+++ /dev/null
@@ -1,146 +0,0 @@
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-using CommunityToolkit.Mvvm.Input;
-using Wino.Core.Domain;
-using Wino.Core.Domain.Entities.Shared;
-using Wino.Core.Domain.Enums;
-using Wino.Core.Domain.Exceptions;
-using Wino.Core.Domain.Interfaces;
-using Wino.Core.Domain.Models.Authentication;
-using Wino.Core.Domain.Models.Navigation;
-using Wino.Core.Domain.Models.Synchronization;
-using Wino.Core.ViewModels;
-using Wino.Messaging.Server;
-
-namespace Wino.Calendar.ViewModels;
-
-public partial class AccountManagementViewModel : AccountManagementPageViewModelBase
-{
- private readonly IProviderService _providerService;
-
- public AccountManagementViewModel(ICalendarDialogService dialogService,
- IWinoServerConnectionManager winoServerConnectionManager,
- INavigationService navigationService,
- IAccountService accountService,
- IProviderService providerService,
- IStoreManagementService storeManagementService,
- IAuthenticationProvider authenticationProvider,
- IPreferencesService preferencesService) : base(dialogService, winoServerConnectionManager, navigationService, accountService, providerService, storeManagementService, authenticationProvider, preferencesService)
- {
- CalendarDialogService = dialogService;
- _providerService = providerService;
- }
-
- public ICalendarDialogService CalendarDialogService { get; }
-
- public override async void OnNavigatedTo(NavigationMode mode, object parameters)
- {
- base.OnNavigatedTo(mode, parameters);
-
- await InitializeAccountsAsync();
- }
-
- public override async Task InitializeAccountsAsync()
- {
- Accounts.Clear();
-
- var accounts = await AccountService.GetAccountsAsync().ConfigureAwait(false);
-
- await ExecuteUIThread(() =>
- {
- foreach (var account in accounts)
- {
- var accountDetails = GetAccountProviderDetails(account);
-
- Accounts.Add(accountDetails);
- }
- });
-
- await ManageStorePurchasesAsync().ConfigureAwait(false);
- }
-
- [RelayCommand]
- private async Task AddNewAccountAsync()
- {
- if (IsAccountCreationBlocked)
- {
- var isPurchaseClicked = await DialogService.ShowConfirmationDialogAsync(Translator.DialogMessage_AccountLimitMessage, Translator.DialogMessage_AccountLimitTitle, Translator.Buttons_Purchase);
-
- if (!isPurchaseClicked) return;
-
- await PurchaseUnlimitedAccountAsync();
-
- return;
- }
-
- var availableProviders = _providerService.GetAvailableProviders();
-
- var accountCreationDialogResult = await DialogService.ShowAccountProviderSelectionDialogAsync(availableProviders);
-
- if (accountCreationDialogResult == null) return;
-
- var accountCreationCancellationTokenSource = new CancellationTokenSource();
- var accountCreationDialog = CalendarDialogService.GetAccountCreationDialog(accountCreationDialogResult);
-
- await accountCreationDialog.ShowDialogAsync(accountCreationCancellationTokenSource);
- await Task.Delay(500);
-
- // For OAuth authentications, we just generate token and assign it to the MailAccount.
-
- var createdAccount = new MailAccount()
- {
- ProviderType = accountCreationDialogResult.ProviderType,
- Name = accountCreationDialogResult.AccountName,
- Id = Guid.NewGuid()
- };
-
- var tokenInformationResponse = await WinoServerConnectionManager
- .GetResponseAsync(new AuthorizationRequested(accountCreationDialogResult.ProviderType,
- createdAccount,
- createdAccount.ProviderType == MailProviderType.Gmail), accountCreationCancellationTokenSource.Token);
-
- if (accountCreationDialog.State == AccountCreationDialogState.Canceled)
- throw new AccountSetupCanceledException();
-
- tokenInformationResponse.ThrowIfFailed();
-
- await AccountService.CreateAccountAsync(createdAccount, null);
-
- // Sync profile information if supported.
- if (createdAccount.IsProfileInfoSyncSupported)
- {
- // Start profile information synchronization.
- // It's only available for Outlook and Gmail synchronizers.
-
- var profileSyncOptions = new MailSynchronizationOptions()
- {
- AccountId = createdAccount.Id,
- Type = MailSynchronizationType.UpdateProfile
- };
-
- var profileSynchronizationResponse = await WinoServerConnectionManager.GetResponseAsync(new NewMailSynchronizationRequested(profileSyncOptions, SynchronizationSource.Client));
-
- var profileSynchronizationResult = profileSynchronizationResponse.Data;
-
- if (profileSynchronizationResult.CompletedState != SynchronizationCompletedState.Success)
- throw new Exception(Translator.Exception_FailedToSynchronizeProfileInformation);
-
- createdAccount.SenderName = profileSynchronizationResult.ProfileInformation.SenderName;
- createdAccount.Base64ProfilePictureData = profileSynchronizationResult.ProfileInformation.Base64ProfilePictureData;
-
- await AccountService.UpdateProfileInformationAsync(createdAccount.Id, profileSynchronizationResult.ProfileInformation);
- }
-
- accountCreationDialog.State = AccountCreationDialogState.FetchingEvents;
-
- // Start synchronizing events.
- var synchronizationOptions = new CalendarSynchronizationOptions()
- {
- AccountId = createdAccount.Id,
- Type = CalendarSynchronizationType.CalendarMetadata
- };
-
- var synchronizationResponse = await WinoServerConnectionManager.GetResponseAsync(new NewCalendarSynchronizationRequested(synchronizationOptions, SynchronizationSource.Client));
- }
-}
diff --git a/Wino.Calendar.ViewModels/AppShellViewModel.cs b/Wino.Calendar.ViewModels/AppShellViewModel.cs
deleted file mode 100644
index a7454eb5..00000000
--- a/Wino.Calendar.ViewModels/AppShellViewModel.cs
+++ /dev/null
@@ -1,366 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Threading;
-using System.Threading.Tasks;
-using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
-using CommunityToolkit.Mvvm.Messaging;
-using Serilog;
-using Wino.Calendar.ViewModels.Data;
-using Wino.Calendar.ViewModels.Interfaces;
-using Wino.Core.Domain.Collections;
-using Wino.Core.Domain.Enums;
-using Wino.Core.Domain.Extensions;
-using Wino.Core.Domain.Interfaces;
-using Wino.Core.Domain.Models.Calendar;
-using Wino.Core.Domain.Models.Navigation;
-using Wino.Core.Domain.Models.Synchronization;
-using Wino.Core.ViewModels;
-using Wino.Messaging.Client.Calendar;
-using Wino.Messaging.Client.Navigation;
-using Wino.Messaging.Server;
-
-namespace Wino.Calendar.ViewModels;
-
-public partial class AppShellViewModel : CalendarBaseViewModel,
- IRecipient,
- IRecipient,
- IRecipient,
- IRecipient,
- IRecipient
-{
- public IPreferencesService PreferencesService { get; }
- public IStatePersistanceService StatePersistenceService { get; }
- public IAccountCalendarStateService AccountCalendarStateService { get; }
- public INavigationService NavigationService { get; }
- public IWinoServerConnectionManager ServerConnectionManager { get; }
-
- [ObservableProperty]
- private bool _isEventDetailsPageActive;
-
- [ObservableProperty]
- private int _selectedMenuItemIndex = -1;
-
- [ObservableProperty]
- private bool isCalendarEnabled;
-
- ///
- /// Gets or sets the active connection status of the Wino server.
- ///
- [ObservableProperty]
- private WinoServerConnectionStatus activeConnectionStatus;
-
- ///
- /// Gets or sets the display date of the calendar.
- ///
- [ObservableProperty]
- private DateTimeOffset _displayDate;
-
- ///
- /// Gets or sets the highlighted range in the CalendarView and displayed date range in FlipView.
- ///
- [ObservableProperty]
- private DateRange highlightedDateRange;
-
- [ObservableProperty]
- private ObservableRangeCollection dateNavigationHeaderItems = [];
-
- [ObservableProperty]
- private int _selectedDateNavigationHeaderIndex;
-
- public bool IsVerticalCalendar => StatePersistenceService.CalendarDisplayType == CalendarDisplayType.Month;
-
- // For updating account calendars asynchronously.
- private SemaphoreSlim _accountCalendarUpdateSemaphoreSlim = new(1);
-
- public AppShellViewModel(IPreferencesService preferencesService,
- IStatePersistanceService statePersistanceService,
- IAccountService accountService,
- ICalendarService calendarService,
- IAccountCalendarStateService accountCalendarStateService,
- INavigationService navigationService,
- IWinoServerConnectionManager serverConnectionManager)
- {
- _accountService = accountService;
- _calendarService = calendarService;
-
- AccountCalendarStateService = accountCalendarStateService;
- AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested;
- AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged;
-
- NavigationService = navigationService;
- ServerConnectionManager = serverConnectionManager;
- PreferencesService = preferencesService;
-
- StatePersistenceService = statePersistanceService;
- StatePersistenceService.StatePropertyChanged += PrefefencesChanged;
- }
-
- private void SelectedCalendarItemsChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
- {
- throw new NotImplementedException();
- }
-
- private void PrefefencesChanged(object sender, string e)
- {
- if (e == nameof(StatePersistenceService.CalendarDisplayType))
- {
- Messenger.Send(new CalendarDisplayTypeChangedMessage(StatePersistenceService.CalendarDisplayType));
-
-
- // Change the calendar.
- DateClicked(new CalendarViewDayClickedEventArgs(GetDisplayTypeSwitchDate()));
- }
- }
-
- public override async void OnNavigatedTo(NavigationMode mode, object parameters)
- {
- base.OnNavigatedTo(mode, parameters);
-
- UpdateDateNavigationHeaderItems();
-
- await InitializeAccountCalendarsAsync();
-
- TodayClicked();
- }
-
- private async void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e)
- {
- // When using three-state checkbox, multiple accounts will be selected/unselected at the same time.
- // Reporting all these changes one by one to the UI is not efficient and may cause problems in the future.
-
- // Update all calendar states at once.
- try
- {
- await _accountCalendarUpdateSemaphoreSlim.WaitAsync();
-
- foreach (var calendar in e.AccountCalendars)
- {
- await _calendarService.UpdateAccountCalendarAsync(calendar.AccountCalendar).ConfigureAwait(false);
- }
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Error while waiting for account calendar update semaphore.");
- }
- finally
- {
- _accountCalendarUpdateSemaphoreSlim.Release();
- }
- }
-
- private async void UpdateAccountCalendarRequested(object sender, AccountCalendarViewModel e)
- => await _calendarService.UpdateAccountCalendarAsync(e.AccountCalendar).ConfigureAwait(false);
-
- private async Task InitializeAccountCalendarsAsync()
- {
- await Dispatcher.ExecuteOnUIThread(() => AccountCalendarStateService.ClearGroupedAccountCalendar());
-
- var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
-
- foreach (var account in accounts)
- {
- var accountCalendars = await _calendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false);
- var calendarViewModels = new List();
-
- foreach (var calendar in accountCalendars)
- {
- var calendarViewModel = new AccountCalendarViewModel(account, calendar);
-
- calendarViewModels.Add(calendarViewModel);
- }
-
- var groupedAccountCalendarViewModel = new GroupedAccountCalendarViewModel(account, calendarViewModels);
-
- await Dispatcher.ExecuteOnUIThread(() =>
- {
- AccountCalendarStateService.AddGroupedAccountCalendar(groupedAccountCalendarViewModel);
- });
- }
- }
-
- private void ForceNavigateCalendarDate()
- {
- if (SelectedMenuItemIndex == -1)
- {
- var args = new CalendarPageNavigationArgs()
- {
- NavigationDate = _navigationDate ?? DateTime.Now.Date
- };
-
- // Already on calendar. Just navigate.
- NavigationService.Navigate(WinoPage.CalendarPage, args);
-
- _navigationDate = null;
- }
- else
- {
- SelectedMenuItemIndex = -1;
- }
- }
-
- partial void OnSelectedMenuItemIndexChanged(int oldValue, int newValue)
- {
- switch (newValue)
- {
- case -1:
- ForceNavigateCalendarDate();
- break;
- case 0:
- NavigationService.Navigate(WinoPage.ManageAccountsPage);
- break;
- case 1:
- NavigationService.Navigate(WinoPage.SettingsPage);
- break;
- default:
- break;
- }
- }
-
- [RelayCommand]
- private async Task Sync()
- {
- // Sync all calendars.
- var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
-
- foreach (var account in accounts)
- {
- var t = new NewCalendarSynchronizationRequested(new CalendarSynchronizationOptions()
- {
- AccountId = account.Id,
- Type = CalendarSynchronizationType.CalendarMetadata
- }, SynchronizationSource.Client);
-
- Messenger.Send(t);
- }
- }
-
- ///
- /// When calendar type switches, we need to navigate to the most ideal date.
- /// This method returns that date.
- ///
- private DateTime GetDisplayTypeSwitchDate()
- {
- var settings = PreferencesService.GetCurrentCalendarSettings();
- switch (StatePersistenceService.CalendarDisplayType)
- {
- case CalendarDisplayType.Day:
- if (HighlightedDateRange.IsInRange(DateTime.Now)) return DateTime.Now.Date;
-
- return HighlightedDateRange.StartDate;
- case CalendarDisplayType.Week:
- if (HighlightedDateRange == null || HighlightedDateRange.IsInRange(DateTime.Now))
- {
- return DateTime.Now.Date.GetWeekStartDateForDate(settings.FirstDayOfWeek);
- }
-
- return HighlightedDateRange.StartDate.GetWeekStartDateForDate(settings.FirstDayOfWeek);
- case CalendarDisplayType.WorkWeek:
- break;
- case CalendarDisplayType.Month:
- break;
- case CalendarDisplayType.Year:
- break;
- default:
- break;
- }
-
- return DateTime.Today.Date;
- }
-
- private DateTime? _navigationDate;
- private readonly IAccountService _accountService;
- private readonly ICalendarService _calendarService;
-
- #region Commands
-
- [RelayCommand]
- private void TodayClicked()
- {
- _navigationDate = DateTime.Now.Date;
-
- ForceNavigateCalendarDate();
- }
-
- [RelayCommand]
- public void ManageAccounts() => NavigationService.Navigate(WinoPage.AccountManagementPage);
-
- [RelayCommand]
- private Task ReconnectServerAsync() => ServerConnectionManager.ConnectAsync();
-
- [RelayCommand]
- private void DateClicked(CalendarViewDayClickedEventArgs clickedDateArgs)
- {
- _navigationDate = clickedDateArgs.ClickedDate;
-
- ForceNavigateCalendarDate();
- }
-
- #endregion
-
- public void Receive(VisibleDateRangeChangedMessage message) => HighlightedDateRange = message.DateRange;
-
- ///
- /// Sets the header navigation items based on visible date range and calendar type.
- ///
- private void UpdateDateNavigationHeaderItems()
- {
- DateNavigationHeaderItems.Clear();
-
- // TODO: From settings
- var testInfo = new CultureInfo("en-US");
-
- switch (StatePersistenceService.CalendarDisplayType)
- {
- case CalendarDisplayType.Day:
- case CalendarDisplayType.Week:
- case CalendarDisplayType.WorkWeek:
- case CalendarDisplayType.Month:
- DateNavigationHeaderItems.ReplaceRange(testInfo.DateTimeFormat.MonthNames);
- break;
- case CalendarDisplayType.Year:
- break;
- default:
- break;
- }
-
- SetDateNavigationHeaderItems();
- }
-
- partial void OnHighlightedDateRangeChanged(DateRange value) => SetDateNavigationHeaderItems();
-
- private void SetDateNavigationHeaderItems()
- {
- if (HighlightedDateRange == null) return;
-
- if (DateNavigationHeaderItems.Count == 0)
- {
- UpdateDateNavigationHeaderItems();
- }
-
- // TODO: Year view
- var monthIndex = HighlightedDateRange.GetMostVisibleMonthIndex();
-
- SelectedDateNavigationHeaderIndex = Math.Max(monthIndex - 1, -1);
- }
-
- public async void Receive(CalendarEnableStatusChangedMessage message)
- => await ExecuteUIThread(() => IsCalendarEnabled = message.IsEnabled);
-
- public void Receive(NavigateManageAccountsRequested message) => SelectedMenuItemIndex = 1;
-
- public void Receive(CalendarDisplayTypeChangedMessage message) => OnPropertyChanged(nameof(IsVerticalCalendar));
-
- public async void Receive(DetailsPageStateChangedMessage message)
- {
- await ExecuteUIThread(() =>
- {
- IsEventDetailsPageActive = message.IsActivated;
-
- // TODO: This is for Wino Mail. Generalize this later on.
- StatePersistenceService.IsReaderNarrowed = message.IsActivated;
- StatePersistenceService.IsReadingMail = message.IsActivated;
- });
- }
-}
diff --git a/Wino.Calendar.ViewModels/CalendarAccountSettingsPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarAccountSettingsPageViewModel.cs
new file mode 100644
index 00000000..79400c7d
--- /dev/null
+++ b/Wino.Calendar.ViewModels/CalendarAccountSettingsPageViewModel.cs
@@ -0,0 +1,122 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Messaging;
+using Wino.Core.Domain;
+using Wino.Core.Domain.Entities.Calendar;
+using Wino.Core.Domain.Entities.Shared;
+using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Interfaces;
+using Wino.Core.Domain.Models.Navigation;
+using Wino.Core.ViewModels;
+using Wino.Messaging.Client.Calendar;
+
+namespace Wino.Calendar.ViewModels;
+
+///
+/// ViewModel for managing calendar account settings.
+///
+public partial class CalendarAccountSettingsPageViewModel : CalendarBaseViewModel
+{
+ private readonly ICalendarService _calendarService;
+ private readonly IAccountService _accountService;
+
+ [ObservableProperty]
+ public partial MailAccount Account { get; set; }
+
+ [ObservableProperty]
+ public partial AccountCalendar AccountCalendar { get; set; }
+
+ [ObservableProperty]
+ public partial string AccountColorHex { get; set; } = "#0078D4";
+
+ [ObservableProperty]
+ public partial bool IsSyncEnabled { get; set; }
+
+ public ObservableCollection ShowAsOptions { get; } = new ObservableCollection();
+
+ [ObservableProperty]
+ public partial ShowAsOption SelectedDefaultShowAsOption { get; set; }
+
+ public CalendarAccountSettingsPageViewModel(ICalendarService calendarService, IAccountService accountService)
+ {
+ _calendarService = calendarService;
+ _accountService = accountService;
+
+ // Initialize ShowAs options
+ ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Free));
+ ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Tentative));
+ ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Busy));
+ ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.OutOfOffice));
+ ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.WorkingElsewhere));
+ }
+
+ public override async void OnNavigatedTo(NavigationMode mode, object parameters)
+ {
+ base.OnNavigatedTo(mode, parameters);
+
+ if (parameters is AccountCalendar selectedCalendar)
+ {
+ Account = await _accountService.GetAccountAsync(selectedCalendar.AccountId);
+ AccountCalendar = await _calendarService.GetAccountCalendarAsync(selectedCalendar.Id) ?? selectedCalendar;
+ }
+ else if (parameters is Guid accountId)
+ {
+ Account = await _accountService.GetAccountAsync(accountId);
+ var calendars = await _calendarService.GetAccountCalendarsAsync(accountId);
+ AccountCalendar = calendars.FirstOrDefault(c => c.IsPrimary) ?? calendars.FirstOrDefault();
+ }
+ else
+ {
+ return;
+ }
+
+ if (Account == null || AccountCalendar == null)
+ return;
+
+ // Initialize properties from AccountCalendar
+ AccountColorHex = AccountCalendar.BackgroundColorHex ?? "#0078D4";
+ IsSyncEnabled = AccountCalendar.IsSynchronizationEnabled;
+ SelectedDefaultShowAsOption = ShowAsOptions.FirstOrDefault(o => o.ShowAs == AccountCalendar.DefaultShowAs) ?? ShowAsOptions[2];
+ }
+
+ partial void OnAccountColorHexChanged(string value)
+ {
+ if (AccountCalendar != null && !string.IsNullOrEmpty(value))
+ {
+ AccountCalendar.BackgroundColorHex = value;
+ SaveChangesAsync();
+ }
+ }
+
+ partial void OnIsSyncEnabledChanged(bool value)
+ {
+ if (AccountCalendar != null)
+ {
+ AccountCalendar.IsSynchronizationEnabled = value;
+ SaveChangesAsync();
+ }
+ }
+
+ partial void OnSelectedDefaultShowAsOptionChanged(ShowAsOption value)
+ {
+ if (AccountCalendar != null && value != null)
+ {
+ AccountCalendar.DefaultShowAs = value.ShowAs;
+ SaveChangesAsync();
+ }
+ }
+
+ private async void SaveChangesAsync()
+ {
+ if (AccountCalendar == null)
+ return;
+
+ await _calendarService.UpdateAccountCalendarAsync(AccountCalendar);
+
+ // Send message to update UI
+ Messenger.Send(new CalendarListUpdated(AccountCalendar));
+ }
+}
diff --git a/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs b/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs
new file mode 100644
index 00000000..43cf07b7
--- /dev/null
+++ b/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs
@@ -0,0 +1,551 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using CommunityToolkit.Mvvm.Messaging;
+using Serilog;
+using Wino.Calendar.ViewModels.Data;
+using Wino.Calendar.ViewModels.Interfaces;
+using Wino.Core.Domain;
+using Wino.Core.Domain.Collections;
+using Wino.Core.Domain.Entities.Calendar;
+using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Interfaces;
+using Wino.Core.Domain.MenuItems;
+using Wino.Core.Domain.Models;
+using Wino.Core.Domain.Models.Calendar;
+using Wino.Core.Domain.Models.Navigation;
+using Wino.Core.Domain.Models.Synchronization;
+using Wino.Core.ViewModels;
+using Wino.Messaging.Client.Calendar;
+using Wino.Messaging.Server;
+using Wino.Messaging.UI;
+
+namespace Wino.Calendar.ViewModels;
+
+public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
+ ICalendarShellClient,
+ IRecipient,
+ IRecipient
+{
+ public IPreferencesService PreferencesService { get; }
+ public IStatePersistanceService StatePersistenceService { get; }
+ public IAccountCalendarStateService AccountCalendarStateService { get; }
+ public INavigationService NavigationService { get; }
+ public WinoApplicationMode Mode => WinoApplicationMode.Calendar;
+ public bool HandlesNavigationSelection => false;
+ public VisibleDateRange CurrentVisibleRange => _calendarPageViewModel.CurrentVisibleRange;
+ public string VisibleDateRangeText => _calendarPageViewModel.VisibleDateRangeText;
+ System.Collections.IEnumerable ICalendarShellClient.GroupedAccountCalendars => AccountCalendarStateService.GroupedAccountCalendars;
+ System.Collections.IEnumerable ICalendarShellClient.DateNavigationHeaderItems => DateNavigationHeaderItems;
+ object IShellClient.SelectedMenuItem
+ {
+ get => null;
+ set { }
+ }
+ System.Windows.Input.ICommand ICalendarShellClient.TodayClickedCommand => TodayClickedCommand;
+ System.Windows.Input.ICommand ICalendarShellClient.DateClickedCommand => DateClickedCommand;
+ System.Windows.Input.ICommand ICalendarShellClient.PreviousDateRangeCommand => PreviousDateRangeCommand;
+ System.Windows.Input.ICommand ICalendarShellClient.NextDateRangeCommand => NextDateRangeCommand;
+ System.Windows.Input.ICommand ICalendarShellClient.SyncCommand => SyncCommand;
+
+ public bool CanSynchronizeCalendars => !AccountCalendarStateService.IsAnySynchronizationInProgress;
+
+ public MenuItemCollection MenuItems { get; private set; }
+ public MenuItemCollection FooterItems { get; private set; }
+
+ [ObservableProperty]
+ private int _selectedMenuItemIndex = -1;
+
+ [ObservableProperty]
+ private ObservableRangeCollection dateNavigationHeaderItems = [];
+
+ [ObservableProperty]
+ private int _selectedDateNavigationHeaderIndex;
+
+ public bool IsVerticalCalendar => StatePersistenceService.CalendarDisplayType == CalendarDisplayType.Month;
+
+ [ObservableProperty]
+ private bool isStoreUpdateItemVisible;
+
+ private readonly SettingsItem _settingsItem = new();
+ private readonly StoreUpdateMenuItem _storeUpdateMenuItem = new();
+ private readonly SemaphoreSlim _accountCalendarUpdateSemaphoreSlim = new(1);
+ private readonly CalendarPageViewModel _calendarPageViewModel;
+ private readonly IMailDialogService _dialogService;
+ private readonly IStoreUpdateService _storeUpdateService;
+ private readonly IAccountService _accountService;
+ private readonly ICalendarService _calendarService;
+ private readonly IDateContextProvider _dateContextProvider;
+ private bool _runtimeSubscriptionsAttached;
+ private bool _hasRegisteredPersistentRecipients;
+ private DateTime? _navigationDate;
+
+ public CalendarAppShellViewModel(
+ IPreferencesService preferencesService,
+ IStatePersistanceService statePersistanceService,
+ IAccountService accountService,
+ ICalendarService calendarService,
+ IAccountCalendarStateService accountCalendarStateService,
+ INavigationService navigationService,
+ CalendarPageViewModel calendarPageViewModel,
+ IMailDialogService dialogService,
+ IStoreUpdateService storeUpdateService,
+ IDateContextProvider dateContextProvider)
+ {
+ PreferencesService = preferencesService;
+ StatePersistenceService = statePersistanceService;
+ AccountCalendarStateService = accountCalendarStateService;
+ NavigationService = navigationService;
+ _accountService = accountService;
+ _calendarService = calendarService;
+ _calendarPageViewModel = calendarPageViewModel;
+ _dialogService = dialogService;
+ _storeUpdateService = storeUpdateService;
+ _dateContextProvider = dateContextProvider;
+
+ _calendarPageViewModel.PropertyChanged += CalendarPageViewModelPropertyChanged;
+ AccountCalendarStateService.PropertyChanged += AccountCalendarStateServicePropertyChanged;
+ }
+
+ protected override void OnDispatcherAssigned()
+ {
+ base.OnDispatcherAssigned();
+
+ AccountCalendarStateService.Dispatcher = Dispatcher;
+ MenuItems = new MenuItemCollection(Dispatcher);
+ FooterItems = new MenuItemCollection(Dispatcher);
+ _ = RefreshFooterItemsAsync(false);
+ }
+
+ private void CalendarPageViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == nameof(CalendarPageViewModel.CurrentVisibleRange))
+ {
+ OnPropertyChanged(nameof(CurrentVisibleRange));
+ }
+
+ if (e.PropertyName == nameof(CalendarPageViewModel.CurrentVisibleRange) ||
+ e.PropertyName == nameof(CalendarPageViewModel.VisibleDateRangeText))
+ {
+ OnPropertyChanged(nameof(VisibleDateRangeText));
+ UpdateDateNavigationHeaderItems();
+ }
+ }
+
+ private void PrefefencesChanged(object sender, string e)
+ {
+ if (e != nameof(StatePersistenceService.CalendarDisplayType))
+ return;
+
+ Messenger.Send(new CalendarDisplayTypeChangedMessage(StatePersistenceService.CalendarDisplayType));
+ OnPropertyChanged(nameof(IsVerticalCalendar));
+ UpdateDateNavigationHeaderItems();
+ NavigateCalendarDate(GetDisplayTypeSwitchDate());
+ }
+
+ private async void PreferencesServiceChanged(object sender, string e)
+ {
+ if (e == nameof(IPreferencesService.IsStoreUpdateNotificationsEnabled))
+ {
+ await RefreshFooterItemsAsync(false);
+ }
+ }
+
+ public override async void OnNavigatedTo(NavigationMode mode, object parameters)
+ {
+ if (!_hasRegisteredPersistentRecipients)
+ {
+ RegisterRecipients();
+ _hasRegisteredPersistentRecipients = true;
+ }
+
+ AttachRuntimeSubscriptions();
+
+ var activationContext = parameters as ShellModeActivationContext;
+ var shouldRunStartupFlows = activationContext?.IsInitialActivation ?? true;
+
+ PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
+ PreferencesService.PreferenceChanged += PreferencesServiceChanged;
+
+ await RefreshFooterItemsAsync(mode == NavigationMode.New);
+ UpdateDateNavigationHeaderItems();
+ await InitializeAccountCalendarsAsync();
+ ValidateConfiguredNewEventCalendar();
+
+ TodayClicked();
+ }
+
+ public override void OnNavigatedFrom(NavigationMode mode, object parameters)
+ {
+ DetachRuntimeSubscriptions();
+ PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
+ _ = ExecuteUIThread(() =>
+ {
+ DateNavigationHeaderItems.Clear();
+ AccountCalendarStateService.ClearGroupedAccountCalendars();
+ SelectedDateNavigationHeaderIndex = -1;
+ });
+ _calendarPageViewModel.CleanupForShellDeactivation();
+ }
+
+ public void PrepareForShellShutdown()
+ {
+ DetachRuntimeSubscriptions();
+ PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
+
+ if (_hasRegisteredPersistentRecipients)
+ {
+ UnregisterRecipients();
+ _hasRegisteredPersistentRecipients = false;
+ }
+
+ DateNavigationHeaderItems.Clear();
+ SelectedDateNavigationHeaderIndex = -1;
+ SelectedMenuItemIndex = -1;
+ MenuItems?.Clear();
+ FooterItems?.Clear();
+ AccountCalendarStateService.ClearGroupedAccountCalendars();
+ _calendarPageViewModel.CleanupForShellDeactivation();
+ }
+
+ private void AccountCalendarStateServicePropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName != nameof(IAccountCalendarStateService.IsAnySynchronizationInProgress))
+ return;
+
+ OnPropertyChanged(nameof(CanSynchronizeCalendars));
+ SyncCommand.NotifyCanExecuteChanged();
+ }
+
+ private void AttachRuntimeSubscriptions()
+ {
+ if (_runtimeSubscriptionsAttached)
+ return;
+
+ AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested;
+ AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged;
+ StatePersistenceService.StatePropertyChanged += PrefefencesChanged;
+ _runtimeSubscriptionsAttached = true;
+ }
+
+ private void DetachRuntimeSubscriptions()
+ {
+ if (!_runtimeSubscriptionsAttached)
+ return;
+
+ AccountCalendarStateService.AccountCalendarSelectionStateChanged -= UpdateAccountCalendarRequested;
+ AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged -= AccountCalendarStateCollectivelyChanged;
+ StatePersistenceService.StatePropertyChanged -= PrefefencesChanged;
+ _runtimeSubscriptionsAttached = false;
+ }
+
+ private async Task RefreshFooterItemsAsync(bool showNotification)
+ {
+ await ExecuteUIThread(() =>
+ {
+ FooterItems.Clear();
+ });
+ }
+
+ private async Task StartStoreUpdateAsync()
+ {
+ await _storeUpdateService.StartUpdateAsync().ConfigureAwait(false);
+ await RefreshFooterItemsAsync(false).ConfigureAwait(false);
+ }
+
+ private async void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e)
+ {
+ try
+ {
+ await _accountCalendarUpdateSemaphoreSlim.WaitAsync();
+
+ foreach (var calendar in e.AccountCalendars)
+ {
+ await _calendarService.UpdateAccountCalendarAsync(calendar.AccountCalendar).ConfigureAwait(false);
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Error while waiting for account calendar update semaphore.");
+ }
+ finally
+ {
+ _accountCalendarUpdateSemaphoreSlim.Release();
+ }
+ }
+
+ private async void UpdateAccountCalendarRequested(object sender, AccountCalendarViewModel e)
+ => await _calendarService.UpdateAccountCalendarAsync(e.AccountCalendar).ConfigureAwait(false);
+
+ private async Task InitializeAccountCalendarsAsync()
+ {
+ await Dispatcher.ExecuteOnUIThread(() => AccountCalendarStateService.ClearGroupedAccountCalendars());
+
+ var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
+
+ foreach (var account in accounts)
+ {
+ var accountCalendars = await _calendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false);
+ var calendarViewModels = accountCalendars.Select(calendar => new AccountCalendarViewModel(account, calendar)).ToList();
+ var groupedAccountCalendarViewModel = new GroupedAccountCalendarViewModel(account, calendarViewModels);
+
+ await Dispatcher.ExecuteOnUIThread(() =>
+ {
+ AccountCalendarStateService.AddGroupedAccountCalendar(groupedAccountCalendarViewModel);
+ });
+ }
+ }
+
+ private void NavigateCalendarDate(DateTime date)
+ {
+ _navigationDate = date.Date;
+ ForceNavigateCalendarDate();
+ }
+
+ private void ForceNavigateCalendarDate()
+ {
+ var args = new CalendarPageNavigationArgs
+ {
+ NavigationDate = _navigationDate ?? DateTime.Now.Date
+ };
+
+ NavigationService.Navigate(WinoPage.CalendarPage, args);
+ _navigationDate = null;
+ }
+
+ [RelayCommand(CanExecute = nameof(CanSynchronizeCalendars))]
+ private async Task Sync()
+ {
+ var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
+ foreach (var account in accounts)
+ {
+ Messenger.Send(new NewCalendarSynchronizationRequested(new CalendarSynchronizationOptions
+ {
+ AccountId = account.Id,
+ Type = CalendarSynchronizationType.Strict
+ }));
+ }
+ }
+
+ private DateTime GetDisplayTypeSwitchDate()
+ {
+ var today = _dateContextProvider.GetToday();
+ var settings = PreferencesService.GetCurrentCalendarSettings();
+ var referenceRange = CurrentVisibleRange
+ ?? CalendarRangeResolver.Resolve(new CalendarDisplayRequest(StatePersistenceService.CalendarDisplayType, today), settings, today);
+ var targetRange = CalendarRangeResolver.ChangeDisplayType(referenceRange, StatePersistenceService.CalendarDisplayType, settings, today);
+ return targetRange.AnchorDate.ToDateTime(TimeOnly.MinValue);
+ }
+
+ [RelayCommand]
+ private void TodayClicked()
+ {
+ NavigateCalendarDate(_dateContextProvider.GetToday().ToDateTime(TimeOnly.MinValue));
+ }
+
+ [RelayCommand]
+ private void PreviousDateRange()
+ {
+ NavigateRelativePeriod(-1);
+ }
+
+ [RelayCommand]
+ private void NextDateRange()
+ {
+ NavigateRelativePeriod(1);
+ }
+
+ private void NavigateRelativePeriod(int direction)
+ {
+ var today = _dateContextProvider.GetToday();
+ var settings = PreferencesService.GetCurrentCalendarSettings();
+ var referenceRange = CurrentVisibleRange
+ ?? CalendarRangeResolver.Resolve(new CalendarDisplayRequest(StatePersistenceService.CalendarDisplayType, today), settings, today);
+ var targetRange = CalendarRangeResolver.Navigate(referenceRange, direction, settings, today);
+ NavigateCalendarDate(targetRange.AnchorDate.ToDateTime(TimeOnly.MinValue));
+ }
+
+ public async Task HandleNavigationItemInvokedAsync(IMenuItem menuItem)
+ {
+ switch (menuItem)
+ {
+ case NewMailMenuItem:
+ await NewEventAsync().ConfigureAwait(false);
+ break;
+ case SettingsItem:
+ NavigationService.Navigate(WinoPage.SettingsPage);
+ break;
+ case StoreUpdateMenuItem:
+ await StartStoreUpdateAsync().ConfigureAwait(false);
+ break;
+ }
+ }
+
+ [RelayCommand]
+ private async Task NewEventAsync()
+ {
+ var pickedCalendar = TryResolveConfiguredNewEventCalendar();
+
+ if (pickedCalendar == null)
+ {
+ var availableGroups = AccountCalendarStateService.GroupedAccountCalendars
+ .Where(group => group.AccountCalendars.Count > 0)
+ .Select(group => new CalendarPickerAccountGroup
+ {
+ Account = group.Account,
+ Calendars = group.AccountCalendars.Select(calendar => calendar.AccountCalendar).ToList()
+ })
+ .ToList();
+
+ if (availableGroups.Count == 0)
+ {
+ _dialogService.InfoBarMessage(
+ Translator.CalendarEventCompose_NoCalendarsTitle,
+ Translator.CalendarEventCompose_NoCalendarsMessage,
+ InfoBarMessageType.Warning);
+ return;
+ }
+
+ var pickingResult = await _dialogService.ShowSingleCalendarPickerDialogAsync(availableGroups);
+ if (pickingResult.ShouldNavigateToCalendarSettings)
+ {
+ NavigationService.Navigate(WinoPage.CalendarPreferenceSettingsPage);
+ return;
+ }
+
+ pickedCalendar = pickingResult.PickedCalendar;
+ }
+
+ if (pickedCalendar == null)
+ return;
+
+ var (startDate, endDate) = GetDefaultComposeDateRange();
+
+ NavigationService.Navigate(WinoPage.CalendarEventComposePage, new CalendarEventComposeNavigationArgs
+ {
+ SelectedCalendarId = pickedCalendar.Id,
+ StartDate = startDate,
+ EndDate = endDate
+ });
+ }
+
+ public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args)
+ {
+ if (args.Handled || args.Mode != WinoApplicationMode.Calendar)
+ return;
+
+ if (args.Action == KeyboardShortcutAction.NewEvent)
+ {
+ await NewEventAsync();
+ args.Handled = true;
+ }
+ }
+
+ [RelayCommand]
+ private void DateClicked(CalendarViewDayClickedEventArgs clickedDateArgs)
+ => NavigateCalendarDate(clickedDateArgs.ClickedDate);
+
+ protected override void RegisterRecipients()
+ {
+ base.RegisterRecipients();
+
+ UnregisterRecipients();
+
+ Messenger.Register(this);
+ Messenger.Register(this);
+ }
+
+ protected override void UnregisterRecipients()
+ {
+ base.UnregisterRecipients();
+
+ Messenger.Unregister(this);
+ Messenger.Unregister(this);
+ }
+
+ private void UpdateDateNavigationHeaderItems()
+ {
+ var headerText = VisibleDateRangeText;
+ DateNavigationHeaderItems.ReplaceRange(string.IsNullOrWhiteSpace(headerText) ? [] : [headerText]);
+ SelectedDateNavigationHeaderIndex = DateNavigationHeaderItems.Count > 0 ? 0 : -1;
+ }
+
+ public void Receive(CalendarDisplayTypeChangedMessage message)
+ {
+ OnPropertyChanged(nameof(IsVerticalCalendar));
+ UpdateDateNavigationHeaderItems();
+ }
+
+ public async void Receive(AccountRemovedMessage message)
+ {
+ await InitializeAccountCalendarsAsync();
+ ValidateConfiguredNewEventCalendar();
+ }
+
+ private AccountCalendar TryResolveConfiguredNewEventCalendar()
+ {
+ ValidateConfiguredNewEventCalendar();
+
+ if (PreferencesService.NewEventButtonBehavior != NewEventButtonBehavior.AlwaysUseSpecificCalendar
+ || !PreferencesService.DefaultNewEventCalendarId.HasValue)
+ {
+ return null;
+ }
+
+ return AccountCalendarStateService.AllCalendars
+ .FirstOrDefault(calendar => calendar.Id == PreferencesService.DefaultNewEventCalendarId.Value)?
+ .AccountCalendar;
+ }
+
+ private void ValidateConfiguredNewEventCalendar()
+ {
+ if (PreferencesService.NewEventButtonBehavior != NewEventButtonBehavior.AlwaysUseSpecificCalendar
+ || !PreferencesService.DefaultNewEventCalendarId.HasValue)
+ {
+ return;
+ }
+
+ var exists = AccountCalendarStateService.AllCalendars
+ .Any(calendar => calendar.Id == PreferencesService.DefaultNewEventCalendarId.Value);
+
+ if (!exists)
+ {
+ PreferencesService.NewEventButtonBehavior = NewEventButtonBehavior.AskEachTime;
+ PreferencesService.DefaultNewEventCalendarId = null;
+ }
+ }
+
+ private static (DateTime StartDate, DateTime EndDate) GetDefaultComposeDateRange()
+ {
+ var localNow = DateTime.Now;
+ var roundedMinutes = localNow.Minute switch
+ {
+ < 30 => 30,
+ 30 when localNow.Second == 0 && localNow.Millisecond == 0 => 30,
+ _ => 60
+ };
+
+ var startDate = new DateTime(localNow.Year, localNow.Month, localNow.Day, localNow.Hour, 0, 0);
+ startDate = roundedMinutes == 60 ? startDate.AddHours(1) : startDate.AddMinutes(roundedMinutes);
+
+ return (startDate, startDate.AddMinutes(30));
+ }
+
+ void IShellClient.Activate(ShellModeActivationContext activationContext)
+ => OnNavigatedTo(NavigationMode.New, activationContext);
+
+ void IShellClient.Deactivate()
+ => OnNavigatedFrom(NavigationMode.New, null!);
+
+ Task IShellClient.HandleNavigationItemInvokedAsync(IMenuItem menuItem)
+ => menuItem == null ? Task.CompletedTask : HandleNavigationItemInvokedAsync(menuItem);
+
+ Task IShellClient.HandleNavigationSelectionChangedAsync(IMenuItem menuItem)
+ => Task.CompletedTask;
+}
diff --git a/Wino.Calendar.ViewModels/CalendarEventComposePageViewModel.cs b/Wino.Calendar.ViewModels/CalendarEventComposePageViewModel.cs
new file mode 100644
index 00000000..951493be
--- /dev/null
+++ b/Wino.Calendar.ViewModels/CalendarEventComposePageViewModel.cs
@@ -0,0 +1,760 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using EmailValidation;
+using Wino.Calendar.ViewModels.Data;
+using Wino.Core.Domain;
+using Wino.Core.Domain.Entities.Calendar;
+using Wino.Core.Domain.Entities.Shared;
+using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Exceptions;
+using Wino.Core.Domain.Interfaces;
+using Wino.Core.Domain.Models.Calendar;
+using Wino.Core.Domain.Models.Navigation;
+using Wino.Core.Domain.Validation;
+using Wino.Core.ViewModels;
+
+namespace Wino.Calendar.ViewModels;
+
+public partial class CalendarEventComposePageViewModel : CalendarBaseViewModel
+{
+ private readonly IAccountService _accountService;
+ private readonly ICalendarService _calendarService;
+ private readonly INavigationService _navigationService;
+ private readonly IMailDialogService _dialogService;
+ private readonly IContactService _contactService;
+ private readonly IPreferencesService _preferencesService;
+ private readonly IUnderlyingThemeService _underlyingThemeService;
+ private readonly IWinoRequestDelegator _winoRequestDelegator;
+ private readonly CalendarEventComposeResultValidator _composeResultValidator = new();
+
+ public Func> GetHtmlNotesAsync { get; set; }
+
+ public ObservableCollection AvailableCalendars { get; } = [];
+ public ObservableCollection AvailableCalendarGroups { get; } = [];
+ public ObservableCollection Attendees { get; } = [];
+ public ObservableCollection Attachments { get; } = [];
+ public ObservableCollection ShowAsOptions { get; } = [];
+ public ObservableCollection ReminderOptions { get; } = [];
+ public ObservableCollection RecurrenceIntervalOptions { get; } = [];
+ public ObservableCollection RecurrenceFrequencyOptions { get; } = [];
+ public ObservableCollection WeekdayOptions { get; } = [];
+
+ [ObservableProperty]
+ public partial AccountCalendarViewModel SelectedCalendar { get; set; }
+
+ [ObservableProperty]
+ public partial string Title { get; set; } = string.Empty;
+
+ [ObservableProperty]
+ public partial string Location { get; set; } = string.Empty;
+
+ [ObservableProperty]
+ public partial bool IsAllDay { get; set; }
+
+ [ObservableProperty]
+ public partial DateTimeOffset StartDate { get; set; }
+
+ [ObservableProperty]
+ public partial TimeSpan StartTime { get; set; }
+
+ [ObservableProperty]
+ public partial TimeSpan EndTime { get; set; }
+
+ [ObservableProperty]
+ public partial DateTimeOffset AllDayEndDate { get; set; }
+
+ [ObservableProperty]
+ public partial bool IsRecurring { get; set; }
+
+ [ObservableProperty]
+ public partial int SelectedRecurrenceInterval { get; set; } = 1;
+
+ [ObservableProperty]
+ public partial CalendarComposeFrequencyOption SelectedRecurrenceFrequencyOption { get; set; }
+
+ [ObservableProperty]
+ public partial DateTimeOffset? RecurrenceEndDate { get; set; }
+
+ [ObservableProperty]
+ public partial string RecurrenceSummary { get; set; } = string.Empty;
+
+ [ObservableProperty]
+ public partial ReminderOption SelectedReminderOption { get; set; }
+
+ [ObservableProperty]
+ public partial ShowAsOption SelectedShowAsOption { get; set; }
+
+ [ObservableProperty]
+ public partial bool IsDarkWebviewRenderer { get; set; }
+
+ [ObservableProperty]
+ public partial CalendarEventComposeResult LastCreatedResult { get; set; }
+
+ public CalendarSettings CurrentSettings { get; }
+ public string TimePickerClockIdentifier => CurrentSettings.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour ? "24HourClock" : "12HourClock";
+ public bool HasAttachments => Attachments.Count > 0;
+ public bool IsSelectedCalendarCalDav => SelectedCalendar?.Account?.ProviderType == MailProviderType.IMAP4 &&
+ SelectedCalendar.Account.ServerInformation?.CalendarSupportMode == ImapCalendarSupportMode.CalDav;
+ public bool CanAddAttachments => !IsSelectedCalendarCalDav;
+ public string AttachmentsDisabledTooltipText => IsSelectedCalendarCalDav
+ ? Translator.CalendarEventCompose_AttachmentsNotSupportedForCalDav
+ : string.Empty;
+ public string SelectedCalendarDisplayText => SelectedCalendar?.Name ?? Translator.CalendarEventCompose_SelectCalendar;
+ public string SelectedCalendarAccountText => SelectedCalendar?.Account?.Address ?? string.Empty;
+ public bool IsDailyRecurrenceSelected => SelectedRecurrenceFrequencyOption?.Frequency == CalendarItemRecurrenceFrequency.Daily;
+
+ public CalendarEventComposePageViewModel(IAccountService accountService,
+ ICalendarService calendarService,
+ INavigationService navigationService,
+ IMailDialogService dialogService,
+ IContactService contactService,
+ IPreferencesService preferencesService,
+ IUnderlyingThemeService underlyingThemeService,
+ IWinoRequestDelegator winoRequestDelegator)
+ {
+ _accountService = accountService;
+ _calendarService = calendarService;
+ _navigationService = navigationService;
+ _dialogService = dialogService;
+ _contactService = contactService;
+ _preferencesService = preferencesService;
+ _underlyingThemeService = underlyingThemeService;
+ _winoRequestDelegator = winoRequestDelegator;
+
+ CurrentSettings = _preferencesService.GetCurrentCalendarSettings();
+ IsDarkWebviewRenderer = _underlyingThemeService.IsUnderlyingThemeDark();
+
+ Attachments.CollectionChanged += AttachmentsCollectionChanged;
+
+ ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Free));
+ ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Tentative));
+ ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Busy));
+ ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.OutOfOffice));
+ ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.WorkingElsewhere));
+
+ foreach (var reminderMinutes in _calendarService.GetPredefinedReminderMinutes().OrderByDescending(x => x))
+ {
+ ReminderOptions.Add(new ReminderOption(reminderMinutes));
+ }
+
+ foreach (var interval in Enumerable.Range(1, 99))
+ {
+ RecurrenceIntervalOptions.Add(interval);
+ }
+
+ RecurrenceFrequencyOptions.Add(new CalendarComposeFrequencyOption(CalendarItemRecurrenceFrequency.Daily, Translator.CalendarEventCompose_FrequencyDay));
+ RecurrenceFrequencyOptions.Add(new CalendarComposeFrequencyOption(CalendarItemRecurrenceFrequency.Weekly, Translator.CalendarEventCompose_FrequencyWeek));
+ RecurrenceFrequencyOptions.Add(new CalendarComposeFrequencyOption(CalendarItemRecurrenceFrequency.Monthly, Translator.CalendarEventCompose_FrequencyMonth));
+ RecurrenceFrequencyOptions.Add(new CalendarComposeFrequencyOption(CalendarItemRecurrenceFrequency.Yearly, Translator.CalendarEventCompose_FrequencyYear));
+ SelectedRecurrenceFrequencyOption = RecurrenceFrequencyOptions.FirstOrDefault();
+
+ WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Monday, "MO", Translator.CalendarEventCompose_Weekday_Monday));
+ WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Tuesday, "TU", Translator.CalendarEventCompose_Weekday_Tuesday));
+ WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Wednesday, "WE", Translator.CalendarEventCompose_Weekday_Wednesday));
+ WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Thursday, "TH", Translator.CalendarEventCompose_Weekday_Thursday));
+ WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Friday, "FR", Translator.CalendarEventCompose_Weekday_Friday));
+ WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Saturday, "SA", Translator.CalendarEventCompose_Weekday_Saturday));
+ WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Sunday, "SU", Translator.CalendarEventCompose_Weekday_Sunday));
+
+ SelectedReminderOption = GetDefaultReminderOption();
+ SelectedShowAsOption = ShowAsOptions.FirstOrDefault(option => option.ShowAs == CalendarItemShowAs.Busy);
+
+ var (defaultStart, defaultEnd) = GetDefaultComposeDateRange();
+ ApplyDateRange(defaultStart, defaultEnd, false);
+ }
+
+ public override async void OnNavigatedTo(NavigationMode mode, object parameters)
+ {
+ base.OnNavigatedTo(mode, parameters);
+
+ await LoadAvailableCalendarsAsync();
+
+ var args = parameters as CalendarEventComposeNavigationArgs;
+ ApplyNavigationArgs(args);
+ UpdateRecurrenceSummary();
+ }
+
+ partial void OnSelectedCalendarChanged(AccountCalendarViewModel value)
+ {
+ if (value == null)
+ return;
+
+ SelectedShowAsOption = ShowAsOptions.FirstOrDefault(option => option.ShowAs == value.DefaultShowAs)
+ ?? ShowAsOptions.FirstOrDefault();
+
+ if (IsSelectedCalendarCalDav && Attachments.Count > 0)
+ {
+ Attachments.Clear();
+ }
+
+ OnPropertyChanged(nameof(IsSelectedCalendarCalDav));
+ OnPropertyChanged(nameof(CanAddAttachments));
+ OnPropertyChanged(nameof(AttachmentsDisabledTooltipText));
+ OnPropertyChanged(nameof(SelectedCalendarDisplayText));
+ OnPropertyChanged(nameof(SelectedCalendarAccountText));
+ }
+
+ partial void OnIsAllDayChanged(bool value)
+ {
+ if (value)
+ {
+ if (AllDayEndDate.Date <= StartDate.Date)
+ {
+ AllDayEndDate = StartDate.AddDays(1);
+ }
+ }
+
+ UpdateRecurrenceSummary();
+ }
+
+ partial void OnStartDateChanged(DateTimeOffset value)
+ {
+ if (IsAllDay && AllDayEndDate.Date <= value.Date)
+ {
+ AllDayEndDate = value.AddDays(1);
+ }
+
+ if (IsRecurring && WeekdayOptions.All(option => !option.IsSelected))
+ {
+ SelectSingleWeekday(value.DayOfWeek);
+ }
+
+ UpdateRecurrenceSummary();
+ }
+
+ partial void OnStartTimeChanged(TimeSpan value) => UpdateRecurrenceSummary();
+ partial void OnEndTimeChanged(TimeSpan value) => UpdateRecurrenceSummary();
+ partial void OnAllDayEndDateChanged(DateTimeOffset value) => UpdateRecurrenceSummary();
+ partial void OnIsRecurringChanged(bool value)
+ {
+ if (value && WeekdayOptions.All(option => !option.IsSelected))
+ {
+ SelectSingleWeekday(StartDate.DayOfWeek);
+ }
+
+ UpdateRecurrenceSummary();
+ }
+ partial void OnSelectedRecurrenceIntervalChanged(int value) => UpdateRecurrenceSummary();
+ partial void OnSelectedRecurrenceFrequencyOptionChanged(CalendarComposeFrequencyOption value)
+ {
+ OnPropertyChanged(nameof(IsDailyRecurrenceSelected));
+ UpdateRecurrenceSummary();
+ }
+ partial void OnRecurrenceEndDateChanged(DateTimeOffset? value) => UpdateRecurrenceSummary();
+
+ [RelayCommand]
+ private async Task AddAttachmentsAsync()
+ {
+ if (!CanAddAttachments)
+ return;
+
+ var pickedFiles = await _dialogService.PickFilesMetadataAsync("*");
+ if (pickedFiles.Count == 0)
+ return;
+
+ await ExecuteUIThread(() =>
+ {
+ foreach (var file in pickedFiles)
+ {
+ TryAddAttachment(file.FileName, file.FullFilePath, file.FileExtension, file.Size);
+ }
+ });
+ }
+
+ public bool TryAddAttachment(string filePath, long size)
+ {
+ if (string.IsNullOrWhiteSpace(filePath))
+ return false;
+
+ var fileName = Path.GetFileName(filePath);
+ var fileExtension = Path.GetExtension(filePath);
+ return TryAddAttachment(fileName, filePath, fileExtension, size);
+ }
+
+ [RelayCommand]
+ private void RemoveAttachment(CalendarComposeAttachmentViewModel attachment)
+ {
+ if (attachment == null)
+ return;
+
+ Attachments.Remove(attachment);
+ }
+
+ [RelayCommand]
+ private void ClearRecurrenceEndDate()
+ {
+ RecurrenceEndDate = null;
+ }
+
+ [RelayCommand]
+ private void Cancel()
+ {
+ _navigationService.GoBack();
+ }
+
+ [RelayCommand]
+ private async Task CreateAsync()
+ {
+ var uniqueAttendees = Attendees
+ .GroupBy(attendee => attendee.Email, StringComparer.OrdinalIgnoreCase)
+ .Select(group => group.First())
+ .ToList();
+
+ var createdResult = await BuildResultAsync(uniqueAttendees);
+
+ try
+ {
+ _composeResultValidator.Validate(createdResult);
+ }
+ catch (CalendarEventComposeValidationException ex)
+ {
+ ShowValidationMessage(ex.Message);
+ return;
+ }
+
+ LastCreatedResult = createdResult;
+
+ await _winoRequestDelegator.ExecuteAsync(new CalendarOperationPreparationRequest(
+ CalendarSynchronizerOperation.CreateEvent,
+ ComposeResult: createdResult));
+
+ NavigateBackToCalendar(createdResult.StartDate);
+ }
+
+ private void NavigateBackToCalendar(DateTime targetDate)
+ {
+ _navigationService.Navigate(
+ WinoPage.CalendarPage,
+ new CalendarPageNavigationArgs
+ {
+ NavigationDate = targetDate,
+ ForceReload = true
+ });
+ }
+
+ public async Task> SearchContactsAsync(string queryText)
+ {
+ if (string.IsNullOrWhiteSpace(queryText) || queryText.Length < 2)
+ return [];
+
+ return await _contactService.GetAddressInformationAsync(queryText).ConfigureAwait(false);
+ }
+
+ public async Task GetAttendeeAsync(string tokenText)
+ {
+ if (!EmailValidator.Validate(tokenText))
+ return null;
+
+ var existing = Attendees.Any(attendee => attendee.Email.Equals(tokenText, StringComparison.OrdinalIgnoreCase));
+ if (existing)
+ return null;
+
+ var info = await _contactService.GetAddressInformationByAddressAsync(tokenText).ConfigureAwait(false);
+ if (info != null)
+ {
+ return CalendarComposeAttendeeViewModel.FromContact(info);
+ }
+
+ return new CalendarComposeAttendeeViewModel(string.Empty, tokenText);
+ }
+
+ public void AddAttendee(CalendarComposeAttendeeViewModel attendee)
+ {
+ if (Attendees.Any(existing => existing.Email.Equals(attendee.Email, StringComparison.OrdinalIgnoreCase)))
+ return;
+
+ Attendees.Add(attendee);
+ }
+
+ [RelayCommand]
+ private void RemoveAttendee(CalendarComposeAttendeeViewModel attendee)
+ {
+ if (attendee == null)
+ return;
+
+ Attendees.Remove(attendee);
+ }
+
+ public void NotifyAddressExists()
+ {
+ _dialogService.InfoBarMessage(
+ Translator.Info_ContactExistsTitle,
+ Translator.Info_ContactExistsMessage,
+ InfoBarMessageType.Warning);
+ }
+
+ public void NotifyInvalidEmail(string address)
+ {
+ _dialogService.InfoBarMessage(
+ Translator.Info_InvalidAddressTitle,
+ string.Format(Translator.Info_InvalidAddressMessage, address),
+ InfoBarMessageType.Warning);
+ }
+
+ private async Task LoadAvailableCalendarsAsync()
+ {
+ var accountCalendars = new List();
+ var groupedCalendars = new List();
+ var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
+
+ foreach (var account in accounts)
+ {
+ var calendars = await _calendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false);
+ var viewModels = calendars
+ .Select(calendar => new AccountCalendarViewModel(account, calendar))
+ .ToList();
+
+ accountCalendars.AddRange(viewModels);
+
+ if (viewModels.Count > 0)
+ {
+ groupedCalendars.Add(new GroupedAccountCalendarViewModel(account, viewModels));
+ }
+ }
+
+ await ExecuteUIThread(() =>
+ {
+ AvailableCalendars.Clear();
+ AvailableCalendarGroups.Clear();
+
+ foreach (var calendar in accountCalendars.OrderBy(calendar => calendar.Account.Name).ThenBy(calendar => calendar.Name))
+ {
+ AvailableCalendars.Add(calendar);
+ }
+
+ foreach (var group in groupedCalendars.OrderBy(group => group.Account.Name))
+ {
+ AvailableCalendarGroups.Add(group);
+ }
+ });
+ }
+
+ private void ApplyNavigationArgs(CalendarEventComposeNavigationArgs args)
+ {
+ var (defaultStart, defaultEnd) = GetDefaultComposeDateRange();
+ var startDate = args?.StartDate != default ? args!.StartDate : defaultStart;
+ var endDate = args?.EndDate != default ? args!.EndDate : defaultEnd;
+ var isAllDay = args?.IsAllDay ?? false;
+
+ Title = args?.Title ?? string.Empty;
+ Location = args?.Location ?? string.Empty;
+
+ ApplyDateRange(startDate, endDate, isAllDay);
+
+ SelectedCalendar = ResolveSelectedCalendar(args?.SelectedCalendarId);
+ if (SelectedCalendar != null)
+ {
+ SelectedShowAsOption = ShowAsOptions.FirstOrDefault(option => option.ShowAs == SelectedCalendar.DefaultShowAs)
+ ?? SelectedShowAsOption
+ ?? ShowAsOptions.FirstOrDefault();
+ }
+ }
+
+ private AccountCalendarViewModel ResolveSelectedCalendar(Guid? selectedCalendarId)
+ {
+ if (selectedCalendarId.HasValue)
+ {
+ var selectedCalendar = AvailableCalendars.FirstOrDefault(calendar => calendar.Id == selectedCalendarId.Value);
+ if (selectedCalendar != null)
+ return selectedCalendar;
+ }
+
+ return AvailableCalendars.FirstOrDefault(calendar => calendar.IsPrimary) ?? AvailableCalendars.FirstOrDefault();
+ }
+
+ private void ApplyDateRange(DateTime startDate, DateTime endDate, bool isAllDay)
+ {
+ IsAllDay = isAllDay;
+ StartDate = new DateTimeOffset(startDate.Date);
+ StartTime = startDate.TimeOfDay;
+ EndTime = endDate.TimeOfDay;
+ AllDayEndDate = new DateTimeOffset((isAllDay ? endDate.Date : startDate.Date.AddDays(1)));
+ }
+
+ private async Task BuildResultAsync(List uniqueAttendees)
+ {
+ if (RecurrenceEndDate.HasValue && RecurrenceEndDate.Value.Date < StartDate.Date)
+ {
+ throw new CalendarEventComposeValidationException(Translator.CalendarEventCompose_ValidationInvalidRecurrenceEnd);
+ }
+
+ var htmlNotes = GetHtmlNotesAsync == null ? string.Empty : await GetHtmlNotesAsync();
+ var effectiveStart = GetEffectiveStartDateTime();
+ var effectiveEnd = GetEffectiveEndDateTime();
+
+ return new CalendarEventComposeResult
+ {
+ CalendarId = SelectedCalendar?.Id ?? Guid.Empty,
+ AccountId = SelectedCalendar?.Account.Id ?? Guid.Empty,
+ Title = Title.Trim(),
+ Location = Location?.Trim() ?? string.Empty,
+ HtmlNotes = htmlNotes,
+ StartDate = effectiveStart,
+ EndDate = effectiveEnd,
+ IsAllDay = IsAllDay,
+ TimeZoneId = TimeZoneInfo.Local.Id,
+ ShowAs = SelectedShowAsOption?.ShowAs ?? SelectedCalendar?.DefaultShowAs ?? CalendarItemShowAs.Busy,
+ SelectedReminders = BuildSelectedReminders(),
+ Attendees = BuildAttendees(uniqueAttendees),
+ Attachments = CanAddAttachments
+ ? Attachments.Select(attachment => attachment.ToDraftModel()).ToList()
+ : [],
+ Recurrence = BuildRecurrenceRule(),
+ RecurrenceSummary = RecurrenceSummary
+ };
+ }
+
+ private List BuildSelectedReminders()
+ {
+ if (SelectedReminderOption == null)
+ return [];
+
+ return
+ [
+ new Reminder
+ {
+ Id = Guid.NewGuid(),
+ CalendarItemId = Guid.Empty,
+ DurationInSeconds = SelectedReminderOption.Minutes * 60L,
+ ReminderType = CalendarItemReminderType.Popup
+ }
+ ];
+ }
+
+ private static List BuildAttendees(IEnumerable attendees)
+ {
+ return attendees
+ .Select(attendee => new CalendarEventAttendee
+ {
+ Id = Guid.NewGuid(),
+ CalendarItemId = Guid.Empty,
+ Name = attendee.HasDistinctDisplayName ? attendee.DisplayName : string.Empty,
+ Email = attendee.Email,
+ AttendenceStatus = AttendeeStatus.NeedsAction,
+ IsOrganizer = false,
+ ResolvedContact = attendee.ResolvedContact
+ })
+ .ToList();
+ }
+
+ private ReminderOption GetDefaultReminderOption()
+ {
+ var reminderMinutes = Math.Max(1, _preferencesService.DefaultReminderDurationInSeconds / 60);
+ return ReminderOptions.FirstOrDefault(option => option.Minutes == reminderMinutes)
+ ?? ReminderOptions.FirstOrDefault();
+ }
+
+ private void UpdateRecurrenceSummary()
+ {
+ if (!HasInitializedComposeDateRange())
+ {
+ RecurrenceSummary = string.Empty;
+ return;
+ }
+
+ var effectiveStart = GetEffectiveStartDateTime();
+ var effectiveEnd = GetEffectiveEndDateTime();
+ var selectedDays = IsDailyRecurrenceSelected
+ ? WeekdayOptions
+ .Where(option => option.IsSelected)
+ .Select(option => option.DayOfWeek)
+ .ToList()
+ : [];
+
+ RecurrenceSummary = CalendarRecurrenceSummaryFormatter.BuildSummary(
+ IsRecurring,
+ effectiveStart,
+ effectiveEnd,
+ IsAllDay,
+ CurrentSettings,
+ SelectedRecurrenceInterval,
+ SelectedRecurrenceFrequencyOption?.Frequency ?? CalendarItemRecurrenceFrequency.Weekly,
+ selectedDays,
+ RecurrenceEndDate);
+ }
+
+ private bool HasInitializedComposeDateRange()
+ {
+ if (StartDate == default)
+ {
+ return false;
+ }
+
+ return !IsAllDay || AllDayEndDate != default;
+ }
+
+ private string BuildRecurrenceRule()
+ {
+ if (!IsRecurring || SelectedRecurrenceFrequencyOption == null)
+ return string.Empty;
+
+ var parts = new List
+ {
+ $"FREQ={SelectedRecurrenceFrequencyOption.Frequency.ToString().ToUpperInvariant()}",
+ $"INTERVAL={SelectedRecurrenceInterval}"
+ };
+
+ var selectedDays = IsDailyRecurrenceSelected
+ ? WeekdayOptions
+ .Where(option => option.IsSelected)
+ .Select(option => option.RuleValue)
+ .ToList()
+ : [];
+
+ if (selectedDays.Count > 0)
+ {
+ parts.Add($"BYDAY={string.Join(",", selectedDays)}");
+ }
+
+ if (RecurrenceEndDate.HasValue)
+ {
+ var untilValue = IsAllDay
+ ? RecurrenceEndDate.Value.ToString("yyyyMMdd", CultureInfo.InvariantCulture)
+ : RecurrenceEndDate.Value.Date.AddDays(1).AddSeconds(-1).ToString("yyyyMMdd'T'HHmmss", CultureInfo.InvariantCulture);
+
+ parts.Add($"UNTIL={untilValue}");
+ }
+
+ return $"RRULE:{string.Join(";", parts)}";
+ }
+
+ private DateTime GetEffectiveStartDateTime()
+ => StartDate.Date.Add(IsAllDay ? TimeSpan.Zero : StartTime);
+
+ private DateTime GetEffectiveEndDateTime()
+ => IsAllDay
+ ? AllDayEndDate.Date
+ : StartDate.Date.Add(EndTime);
+
+ private static (DateTime StartDate, DateTime EndDate) GetDefaultComposeDateRange()
+ {
+ var localNow = DateTime.Now;
+ var roundedMinutes = localNow.Minute switch
+ {
+ < 30 => 30,
+ 30 when localNow.Second == 0 && localNow.Millisecond == 0 => 30,
+ _ => 60
+ };
+
+ var startDate = new DateTime(localNow.Year, localNow.Month, localNow.Day, localNow.Hour, 0, 0);
+ startDate = roundedMinutes == 60 ? startDate.AddHours(1) : startDate.AddMinutes(roundedMinutes);
+
+ return (startDate, startDate.AddMinutes(30));
+ }
+
+ private CalendarComposeWeekdayOption CreateWeekdayOption(DayOfWeek dayOfWeek, string ruleValue, string label)
+ {
+ var option = new CalendarComposeWeekdayOption(dayOfWeek, ruleValue, label);
+ option.PropertyChanged += WeekdayOptionPropertyChanged;
+ return option;
+ }
+
+ private void WeekdayOptionPropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == nameof(CalendarComposeWeekdayOption.IsSelected))
+ {
+ UpdateRecurrenceSummary();
+ }
+ }
+
+ private void SelectSingleWeekday(DayOfWeek dayOfWeek)
+ {
+ foreach (var option in WeekdayOptions)
+ {
+ option.IsSelected = option.DayOfWeek == dayOfWeek;
+ }
+ }
+
+ private void ShowValidationMessage(string message)
+ {
+ _dialogService.InfoBarMessage(
+ Translator.CalendarEventCompose_ValidationTitle,
+ message,
+ InfoBarMessageType.Warning);
+ }
+
+ private void AttachmentsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ OnPropertyChanged(nameof(HasAttachments));
+ }
+
+ private bool TryAddAttachment(string fileName, string filePath, string fileExtension, long size)
+ {
+ if (!CanAddAttachments ||
+ string.IsNullOrWhiteSpace(filePath) ||
+ Attachments.Any(existing => existing.FilePath.Equals(filePath, StringComparison.OrdinalIgnoreCase)))
+ {
+ return false;
+ }
+
+ Attachments.Add(new CalendarComposeAttachmentViewModel(fileName, filePath, fileExtension, size));
+ return true;
+ }
+
+}
+
+public partial class CalendarComposeFrequencyOption : ObservableObject
+{
+ public CalendarItemRecurrenceFrequency Frequency { get; }
+ public string DisplayText { get; }
+
+ public CalendarComposeFrequencyOption(CalendarItemRecurrenceFrequency frequency, string displayText)
+ {
+ Frequency = frequency;
+ DisplayText = displayText;
+ }
+
+ public string PluralLabel(int interval)
+ {
+ if (interval == 1)
+ return DisplayText;
+
+ return Frequency switch
+ {
+ CalendarItemRecurrenceFrequency.Daily => Translator.CalendarEventCompose_FrequencyDayPlural,
+ CalendarItemRecurrenceFrequency.Weekly => Translator.CalendarEventCompose_FrequencyWeekPlural,
+ CalendarItemRecurrenceFrequency.Monthly => Translator.CalendarEventCompose_FrequencyMonthPlural,
+ CalendarItemRecurrenceFrequency.Yearly => Translator.CalendarEventCompose_FrequencyYearPlural,
+ _ => DisplayText
+ };
+ }
+}
+
+public partial class CalendarComposeWeekdayOption : ObservableObject
+{
+ public DayOfWeek DayOfWeek { get; }
+ public string RuleValue { get; }
+ public string Label { get; }
+ public string FullDayName => DayOfWeek switch
+ {
+ DayOfWeek.Monday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[1],
+ DayOfWeek.Tuesday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[2],
+ DayOfWeek.Wednesday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[3],
+ DayOfWeek.Thursday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[4],
+ DayOfWeek.Friday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[5],
+ DayOfWeek.Saturday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[6],
+ DayOfWeek.Sunday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[0],
+ _ => string.Empty
+ };
+
+ [ObservableProperty]
+ public partial bool IsSelected { get; set; }
+
+ public CalendarComposeWeekdayOption(DayOfWeek dayOfWeek, string ruleValue, string label)
+ {
+ DayOfWeek = dayOfWeek;
+ RuleValue = ruleValue;
+ Label = label;
+ }
+}
+
+
diff --git a/Wino.Calendar.ViewModels/CalendarNotificationSettingsPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarNotificationSettingsPageViewModel.cs
new file mode 100644
index 00000000..df4af29f
--- /dev/null
+++ b/Wino.Calendar.ViewModels/CalendarNotificationSettingsPageViewModel.cs
@@ -0,0 +1,44 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using Wino.Core.Domain.Interfaces;
+
+namespace Wino.Calendar.ViewModels;
+
+public partial class CalendarNotificationSettingsPageViewModel : CalendarSettingsSectionViewModelBase
+{
+ [ObservableProperty]
+ public partial int SelectedDefaultReminderIndex { get; set; }
+
+ [ObservableProperty]
+ public partial int SelectedDefaultSnoozeIndex { get; set; }
+
+ public CalendarNotificationSettingsPageViewModel(
+ IPreferencesService preferencesService,
+ ICalendarService calendarService,
+ IAccountService accountService)
+ : base(preferencesService, calendarService, accountService)
+ {
+ LoadReminderOptions();
+ LoadSnoozeOptions();
+
+ SelectedDefaultReminderIndex = GetSelectedReminderIndex();
+ SelectedDefaultSnoozeIndex = GetSelectedSnoozeIndex();
+
+ IsLoaded = true;
+ }
+
+ partial void OnSelectedDefaultReminderIndexChanged(int value)
+ {
+ if (!IsLoaded)
+ return;
+
+ SaveReminderIndex(value);
+ }
+
+ partial void OnSelectedDefaultSnoozeIndexChanged(int value)
+ {
+ if (!IsLoaded)
+ return;
+
+ SaveSnoozeIndex(value);
+ }
+}
diff --git a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs
index 99cd7a94..fedfce72 100644
--- a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs
+++ b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs
@@ -1,27 +1,27 @@
-using System;
+using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Linq;
+using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
-using CommunityToolkit.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
-using MoreLinq;
+using Itenso.TimePeriod;
using Serilog;
using Wino.Calendar.ViewModels.Data;
using Wino.Calendar.ViewModels.Interfaces;
using Wino.Calendar.ViewModels.Messages;
-using Wino.Core.Domain.Collections;
+using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
+using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.Calendar;
-using Wino.Core.Domain.Models.Calendar.CalendarTypeStrategies;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.ViewModels;
using Wino.Messaging.Client.Calendar;
+using Wino.Messaging.UI;
namespace Wino.Calendar.ViewModels;
@@ -31,81 +31,95 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
IRecipient,
IRecipient,
IRecipient,
- IRecipient
+ IRecipient,
+ IRecipient,
+ IDisposable
{
#region Quick Event Creation
- [ObservableProperty]
- private bool _isQuickEventDialogOpen;
-
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(SelectedQuickEventAccountCalendarName))]
[NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
- private AccountCalendarViewModel _selectedQuickEventAccountCalendar;
+ public partial AccountCalendarViewModel SelectedQuickEventAccountCalendar { get; set; }
public string SelectedQuickEventAccountCalendarName
- {
- get
- {
- return SelectedQuickEventAccountCalendar == null ? "Pick a calendar" : SelectedQuickEventAccountCalendar.Name;
- }
- }
+ => SelectedQuickEventAccountCalendar == null ? "Pick a calendar" : SelectedQuickEventAccountCalendar.Name;
[ObservableProperty]
- private List _hourSelectionStrings;
+ public partial List HourSelectionStrings { get; set; } = [];
- // To be able to revert the values when the user enters an invalid time.
- private string _previousSelectedStartTimeString;
- private string _previousSelectedEndTimeString;
-
- [ObservableProperty]
- private DateTime? _selectedQuickEventDate;
-
- [ObservableProperty]
- private bool _isAllDay;
+ private string _previousSelectedStartTimeString = string.Empty;
+ private string _previousSelectedEndTimeString = string.Empty;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
- private string _selectedStartTimeString;
+ public partial DateTime? SelectedQuickEventDate { get; set; }
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
- private string _selectedEndTimeString;
-
- [ObservableProperty]
- private string _location;
+ public partial bool IsAllDay { get; set; }
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
- private string _eventName;
+ public partial string SelectedStartTimeString { get; set; } = string.Empty;
+
+ [ObservableProperty]
+ [NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
+ public partial string SelectedEndTimeString { get; set; } = string.Empty;
+
+ [ObservableProperty]
+ public partial string Location { get; set; } = string.Empty;
+
+ [ObservableProperty]
+ [NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
+ public partial string EventName { get; set; } = string.Empty;
public DateTime QuickEventStartTime => SelectedQuickEventDate.Value.Date.Add(CurrentSettings.GetTimeSpan(SelectedStartTimeString).Value);
public DateTime QuickEventEndTime => SelectedQuickEventDate.Value.Date.Add(CurrentSettings.GetTimeSpan(SelectedEndTimeString).Value);
- public bool CanSaveQuickEvent => SelectedQuickEventAccountCalendar != null &&
- !string.IsNullOrWhiteSpace(EventName) &&
- !string.IsNullOrWhiteSpace(SelectedStartTimeString) &&
- !string.IsNullOrWhiteSpace(SelectedEndTimeString) &&
- QuickEventEndTime > QuickEventStartTime;
+ public bool CanSaveQuickEvent
+ {
+ get
+ {
+ if (SelectedQuickEventAccountCalendar == null ||
+ SelectedQuickEventDate == null ||
+ string.IsNullOrWhiteSpace(EventName) ||
+ string.IsNullOrWhiteSpace(SelectedStartTimeString) ||
+ string.IsNullOrWhiteSpace(SelectedEndTimeString))
+ {
+ return false;
+ }
+
+ var startTime = CurrentSettings.GetTimeSpan(SelectedStartTimeString);
+ var endTime = CurrentSettings.GetTimeSpan(SelectedEndTimeString);
+
+ if (!startTime.HasValue || !endTime.HasValue)
+ {
+ return false;
+ }
+
+ return IsAllDay || endTime > startTime;
+ }
+ }
#endregion
- #region Data Initialization
+ #region Visible Range
[ObservableProperty]
- private CalendarOrientation _calendarOrientation = CalendarOrientation.Horizontal;
+ public partial VisibleDateRange CurrentVisibleRange { get; set; }
[ObservableProperty]
- private DayRangeCollection _dayRanges = [];
+ public partial string VisibleDateRangeText { get; set; } = string.Empty;
[ObservableProperty]
- private int _selectedDateRangeIndex;
+ public partial DateRange LoadedDateWindow { get; set; }
[ObservableProperty]
- private DayRangeRenderModel _selectedDayRange;
+ public partial bool IsCalendarEnabled { get; set; } = true;
[ObservableProperty]
- private bool _isCalendarEnabled = true;
+ public partial IReadOnlyList CalendarItems { get; set; } = [];
#endregion
@@ -113,107 +127,305 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
public event EventHandler DetailsShowCalendarItemChanged;
+ public bool CanJoinOnline => DisplayDetailsCalendarItemViewModel != null &&
+ !string.IsNullOrEmpty(DisplayDetailsCalendarItemViewModel.CalendarItem.HtmlLink);
+
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsEventDetailsVisible))]
- private CalendarItemViewModel _displayDetailsCalendarItemViewModel;
+ [NotifyCanExecuteChangedFor(nameof(JoinOnlineCommand))]
+ [NotifyPropertyChangedFor(nameof(CanJoinOnline))]
+ public partial CalendarItemViewModel DisplayDetailsCalendarItemViewModel { get; set; }
public bool IsEventDetailsVisible => DisplayDetailsCalendarItemViewModel != null;
#endregion
- // TODO: Get rid of some of the items if we have too many.
- private const int maxDayRangeSize = 10;
-
private readonly ICalendarService _calendarService;
private readonly INavigationService _navigationService;
- private readonly IKeyPressService _keyPressService;
+ private readonly INativeAppService _nativeAppService;
private readonly IPreferencesService _preferencesService;
+ private readonly IWinoRequestDelegator _winoRequestDelegator;
+ private readonly IMailDialogService _dialogService;
+ private readonly IDateContextProvider _dateContextProvider;
+ private readonly ICalendarRangeTextFormatter _calendarRangeTextFormatter;
- // Store latest rendered options.
- private CalendarDisplayType _currentDisplayType;
- private int _displayDayCount;
-
- private SemaphoreSlim _calendarLoadingSemaphore = new(1);
- private bool isLoadMoreBlocked = false;
+ private readonly SemaphoreSlim _calendarLoadingSemaphore = new(1);
+ private bool _subscriptionsAttached;
+ private CancellationTokenSource _pageLifetimeCts = new();
+ private long _pageLifetimeVersion;
+ private List _loadedCalendarItems = [];
[ObservableProperty]
- private CalendarSettings _currentSettings;
+ public partial CalendarSettings CurrentSettings { get; set; }
public IStatePersistanceService StatePersistanceService { get; }
public IAccountCalendarStateService AccountCalendarStateService { get; }
- public CalendarPageViewModel(IStatePersistanceService statePersistanceService,
- ICalendarService calendarService,
- INavigationService navigationService,
- IKeyPressService keyPressService,
- IAccountCalendarStateService accountCalendarStateService,
- IPreferencesService preferencesService)
+ public CalendarPageViewModel(
+ IStatePersistanceService statePersistanceService,
+ ICalendarService calendarService,
+ INavigationService navigationService,
+ IKeyPressService keyPressService,
+ INativeAppService nativeAppService,
+ IAccountCalendarStateService accountCalendarStateService,
+ IPreferencesService preferencesService,
+ IWinoRequestDelegator winoRequestDelegator,
+ IMailDialogService dialogService,
+ IDateContextProvider dateContextProvider,
+ ICalendarRangeTextFormatter calendarRangeTextFormatter)
{
StatePersistanceService = statePersistanceService;
AccountCalendarStateService = accountCalendarStateService;
-
_calendarService = calendarService;
_navigationService = navigationService;
- _keyPressService = keyPressService;
+ _nativeAppService = nativeAppService;
_preferencesService = preferencesService;
+ _winoRequestDelegator = winoRequestDelegator;
+ _dialogService = dialogService;
+ _dateContextProvider = dateContextProvider;
+ _calendarRangeTextFormatter = calendarRangeTextFormatter;
- AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested;
- AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged;
+ RefreshSettings();
+ }
+
+ public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args)
+ {
+ if (args.Handled || args.Mode != WinoApplicationMode.Calendar || args.Action != KeyboardShortcutAction.Delete)
+ return;
+
+ if (DisplayDetailsCalendarItemViewModel?.CalendarItem == null)
+ return;
+
+ if (DisplayDetailsCalendarItemViewModel.CalendarItem.IsRecurringParent)
+ {
+ var confirmed = await _dialogService.ShowConfirmationDialogAsync(
+ Translator.DialogMessage_DeleteRecurringSeriesMessage,
+ Translator.DialogMessage_DeleteRecurringSeriesTitle,
+ Translator.Buttons_Delete);
+
+ if (!confirmed)
+ return;
+ }
+
+ var preparationRequest = new CalendarOperationPreparationRequest(
+ CalendarSynchronizerOperation.DeleteEvent,
+ DisplayDetailsCalendarItemViewModel.CalendarItem,
+ null);
+
+ await _winoRequestDelegator.ExecuteAsync(preparationRequest);
+ DisplayDetailsCalendarItemViewModel = null;
+ args.Handled = true;
+ }
+
+ protected override void RegisterRecipients()
+ {
+ base.RegisterRecipients();
+
+ Messenger.Unregister(this);
+ Messenger.Unregister(this);
+ Messenger.Unregister(this);
+ Messenger.Unregister(this);
+ Messenger.Unregister(this);
+ Messenger.Unregister(this);
+
+ Messenger.Register(this);
+ Messenger.Register(this);
+ Messenger.Register(this);
+ Messenger.Register(this);
+ Messenger.Register(this);
+ Messenger.Register(this);
+ }
+
+ protected override void UnregisterRecipients()
+ {
+ base.UnregisterRecipients();
+
+ Messenger.Unregister(this);
+ Messenger.Unregister(this);
+ Messenger.Unregister(this);
+ Messenger.Unregister(this);
+ Messenger.Unregister(this);
+ Messenger.Unregister(this);
}
private void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e)
- => FilterActiveCalendars(DayRanges);
+ {
+ EnsureSelectedQuickEventAccountCalendar();
+ _ = ReloadCurrentVisibleRangeAsync();
+ }
private void UpdateAccountCalendarRequested(object sender, AccountCalendarViewModel e)
- => FilterActiveCalendars(DayRanges);
-
- private async void FilterActiveCalendars(IEnumerable dayRangeRenderModels)
{
- await ExecuteUIThread(() =>
- {
- var days = dayRangeRenderModels.SelectMany(a => a.CalendarDays);
-
- days.ForEach(a => a.EventsCollection.FilterByCalendars(AccountCalendarStateService.ActiveCalendars.Select(a => a.Id)));
-
- DisplayDetailsCalendarItemViewModel = null;
- });
+ EnsureSelectedQuickEventAccountCalendar();
+ _ = ReloadCurrentVisibleRangeAsync();
}
- // TODO: Replace when calendar settings are updated.
- // Should be a field ideally.
- private BaseCalendarTypeDrawingStrategy GetDrawingStrategy(CalendarDisplayType displayType)
+ [RelayCommand(CanExecute = nameof(CanJoinOnline))]
+ private async Task JoinOnlineAsync()
{
- return displayType switch
- {
- CalendarDisplayType.Day => new DayCalendarDrawingStrategy(CurrentSettings),
- CalendarDisplayType.Week => new WeekCalendarDrawingStrategy(CurrentSettings),
- CalendarDisplayType.Month => new MonthCalendarDrawingStrategy(CurrentSettings),
- _ => throw new NotImplementedException(),
- };
- }
+ if (DisplayDetailsCalendarItemViewModel == null || string.IsNullOrEmpty(DisplayDetailsCalendarItemViewModel.CalendarItem.HtmlLink))
+ return;
- public override void OnNavigatedFrom(NavigationMode mode, object parameters)
- {
- // Do not call base method because that will unregister messenger recipient.
- // This is a singleton view model and should not be unregistered.
+ await _nativeAppService.LaunchUriAsync(new Uri(DisplayDetailsCalendarItemViewModel.CalendarItem.HtmlLink));
}
public override void OnNavigatedTo(NavigationMode mode, object parameters)
{
+ ResetPageLifetime();
base.OnNavigatedTo(mode, parameters);
-
- if (mode == NavigationMode.Back) return;
-
+ AttachSubscriptions();
RefreshSettings();
+ IsCalendarEnabled = true;
+ EnsureSelectedQuickEventAccountCalendar();
+ }
- // Automatically select the first primary calendar for quick event dialog.
- SelectedQuickEventAccountCalendar = AccountCalendarStateService.ActiveCalendars.FirstOrDefault(a => a.IsPrimary);
+ public override void OnNavigatedFrom(NavigationMode mode, object parameters)
+ {
+ base.OnNavigatedFrom(mode, parameters);
+
+ if (StatePersistanceService.ApplicationMode == WinoApplicationMode.Calendar)
+ {
+ CancelPendingOperations();
+ DetachSubscriptions();
+ return;
+ }
+
+ CleanupForShellDeactivation();
+ }
+
+ private void AttachSubscriptions()
+ {
+ if (_subscriptionsAttached)
+ return;
+
+ AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested;
+ AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged;
+ _subscriptionsAttached = true;
+ }
+
+ private void DetachSubscriptions()
+ {
+ if (!_subscriptionsAttached)
+ return;
+
+ AccountCalendarStateService.AccountCalendarSelectionStateChanged -= UpdateAccountCalendarRequested;
+ AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged -= AccountCalendarStateCollectivelyChanged;
+ _subscriptionsAttached = false;
+ }
+
+ private void ReleasePageState()
+ {
+ DetachSubscriptions();
+ DisplayDetailsCalendarItemViewModel = null;
+ SelectedQuickEventAccountCalendar = null;
+ SelectedQuickEventDate = null;
+ HourSelectionStrings = [];
+ CurrentVisibleRange = null;
+ VisibleDateRangeText = string.Empty;
+ LoadedDateWindow = null;
+ _loadedCalendarItems = [];
+ CalendarItems = [];
+ }
+
+ public void Dispose()
+ {
+ CleanupForShellDeactivation();
+ }
+
+ public void CleanupForShellDeactivation()
+ {
+ CancelPendingOperations();
+ ReleasePageState();
+ GC.SuppressFinalize(this);
+ }
+
+ public bool RestoreVisibleState() => CurrentVisibleRange != null;
+
+ public DateTime GetRestoreDate()
+ => CurrentVisibleRange?.AnchorDate.ToDateTime(TimeOnly.MinValue) ?? DateTime.Now.Date;
+
+ private long CurrentPageLifetimeVersion => Interlocked.Read(ref _pageLifetimeVersion);
+
+ private bool IsPageActive(long lifetimeVersion)
+ => lifetimeVersion == CurrentPageLifetimeVersion && !_pageLifetimeCts.IsCancellationRequested;
+
+ private void ResetPageLifetime()
+ {
+ CancelPendingOperations();
+ _pageLifetimeCts = new CancellationTokenSource();
+ Interlocked.Increment(ref _pageLifetimeVersion);
+ }
+
+ private void CancelPendingOperations()
+ {
+ if (!_pageLifetimeCts.IsCancellationRequested)
+ {
+ _pageLifetimeCts.Cancel();
+ }
+ }
+
+ private async Task WaitForCalendarLoadingLockAsync(long lifetimeVersion)
+ {
+ if (!IsPageActive(lifetimeVersion))
+ return false;
+
+ try
+ {
+ await _calendarLoadingSemaphore.WaitAsync(_pageLifetimeCts.Token).ConfigureAwait(false);
+ return IsPageActive(lifetimeVersion);
+ }
+ catch (OperationCanceledException)
+ {
+ return false;
+ }
+ catch (ObjectDisposedException)
+ {
+ return false;
+ }
+ }
+
+ private void ReleaseCalendarLoadingLock()
+ {
+ try
+ {
+ _calendarLoadingSemaphore.Release();
+ }
+ catch (ObjectDisposedException)
+ {
+ }
+ catch (SemaphoreFullException)
+ {
+ }
+ }
+
+ private async Task ExecuteUIThreadIfActiveAsync(long lifetimeVersion, Action action)
+ {
+ if (action == null || !IsPageActive(lifetimeVersion))
+ return;
+
+ try
+ {
+ await ExecuteUIThread(() =>
+ {
+ if (IsPageActive(lifetimeVersion))
+ {
+ action();
+ }
+ }).ConfigureAwait(false);
+ }
+ catch (COMException) when (!IsPageActive(lifetimeVersion))
+ {
+ }
+ catch (ObjectDisposedException) when (!IsPageActive(lifetimeVersion))
+ {
+ }
}
[RelayCommand]
private void NavigateSeries()
{
- if (DisplayDetailsCalendarItemViewModel == null) return;
+ if (DisplayDetailsCalendarItemViewModel == null)
+ return;
NavigateEvent(DisplayDetailsCalendarItemViewModel, CalendarEventTargetType.Series);
}
@@ -221,7 +433,8 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
[RelayCommand]
private void NavigateEventDetails()
{
- if (DisplayDetailsCalendarItemViewModel == null) return;
+ if (DisplayDetailsCalendarItemViewModel == null)
+ return;
NavigateEvent(DisplayDetailsCalendarItemViewModel, CalendarEventTargetType.Single);
}
@@ -235,57 +448,81 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
[RelayCommand(AllowConcurrentExecutions = false, CanExecute = nameof(CanSaveQuickEvent))]
private async Task SaveQuickEventAsync()
{
- var durationSeconds = (QuickEventEndTime - QuickEventStartTime).TotalSeconds;
-
- var testCalendarItem = new CalendarItem
+ var startDate = IsAllDay ? SelectedQuickEventDate.Value.Date : QuickEventStartTime;
+ var endDate = IsAllDay ? SelectedQuickEventDate.Value.Date.AddDays(1) : QuickEventEndTime;
+ var composeResult = new CalendarEventComposeResult
{
CalendarId = SelectedQuickEventAccountCalendar.Id,
- StartDate = QuickEventStartTime,
- DurationInSeconds = durationSeconds,
- CreatedAt = DateTime.UtcNow,
- Description = string.Empty,
- Location = Location,
+ AccountId = SelectedQuickEventAccountCalendar.Account.Id,
Title = EventName,
- Id = Guid.NewGuid()
+ Location = Location ?? string.Empty,
+ HtmlNotes = string.Empty,
+ StartDate = startDate,
+ EndDate = endDate,
+ IsAllDay = IsAllDay,
+ TimeZoneId = TimeZoneInfo.Local.Id,
+ ShowAs = SelectedQuickEventAccountCalendar.DefaultShowAs,
+ SelectedReminders = [],
+ Attendees = [],
+ Attachments = [],
+ Recurrence = string.Empty,
+ RecurrenceSummary = string.Empty
};
- IsQuickEventDialogOpen = false;
- await _calendarService.CreateNewCalendarItemAsync(testCalendarItem, null);
-
- // TODO: Create the request with the synchronizer.
+ var preparationRequest = new CalendarOperationPreparationRequest(
+ CalendarSynchronizerOperation.CreateEvent,
+ ComposeResult: composeResult);
+ await _winoRequestDelegator.ExecuteAsync(preparationRequest);
}
[RelayCommand]
- private void MoreDetails()
+ private void GoToEventComposePage()
{
- // TODO: Navigate to advanced event creation page with existing parameters.
+ if (SelectedQuickEventDate == null)
+ return;
+
+ var startDate = SelectedQuickEventDate.Value;
+ var endDate = SelectedQuickEventDate.Value.AddMinutes(30);
+
+ if (!IsAllDay)
+ {
+ var selectedStartTime = CurrentSettings.GetTimeSpan(SelectedStartTimeString);
+ var selectedEndTime = CurrentSettings.GetTimeSpan(SelectedEndTimeString);
+
+ if (selectedStartTime.HasValue)
+ {
+ startDate = SelectedQuickEventDate.Value.Date.Add(selectedStartTime.Value);
+ }
+
+ if (selectedEndTime.HasValue)
+ {
+ endDate = SelectedQuickEventDate.Value.Date.Add(selectedEndTime.Value);
+ }
+ }
+ else
+ {
+ startDate = SelectedQuickEventDate.Value.Date;
+ endDate = SelectedQuickEventDate.Value.Date.AddDays(1);
+ }
+
+ _navigationService.Navigate(WinoPage.CalendarEventComposePage, new CalendarEventComposeNavigationArgs
+ {
+ SelectedCalendarId = SelectedQuickEventAccountCalendar?.Id,
+ Title = EventName ?? string.Empty,
+ Location = Location ?? string.Empty,
+ IsAllDay = IsAllDay,
+ StartDate = startDate,
+ EndDate = endDate
+ });
}
public void SelectQuickEventTimeRange(TimeSpan startTime, TimeSpan endTime)
{
IsAllDay = false;
-
SelectedStartTimeString = CurrentSettings.GetTimeString(startTime);
SelectedEndTimeString = CurrentSettings.GetTimeString(endTime);
}
- // Manage event detail popup context and select-unselect the proper items.
- // Item selection rules are defined in the selection method.
- partial void OnDisplayDetailsCalendarItemViewModelChanging(CalendarItemViewModel oldValue, CalendarItemViewModel newValue)
- {
- if (oldValue != null)
- {
- UnselectCalendarItem(oldValue);
- }
-
- if (newValue != null)
- {
- SelectCalendarItem(newValue);
- }
- }
-
- // Notify view that the detail context changed.
- // This will align the event detail popup to the selected event.
partial void OnDisplayDetailsCalendarItemViewModelChanged(CalendarItemViewModel value)
=> DetailsShowCalendarItemChanged?.Invoke(this, EventArgs.Empty);
@@ -293,414 +530,293 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
{
CurrentSettings = _preferencesService.GetCurrentCalendarSettings();
- // Populate the hour selection strings.
var timeStrings = new List();
-
for (int hour = 0; hour < 24; hour++)
{
for (int minute = 0; minute < 60; minute += 30)
{
var time = new DateTime(1, 1, 1, hour, minute, 0);
-
- if (CurrentSettings.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour)
- {
- timeStrings.Add(time.ToString("HH:mm"));
- }
- else
- {
- timeStrings.Add(time.ToString("h:mm tt"));
- }
+ timeStrings.Add(CurrentSettings.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour
+ ? time.ToString("HH:mm")
+ : time.ToString("h:mm tt"));
}
}
HourSelectionStrings = timeStrings;
- }
- partial void OnIsCalendarEnabledChanging(bool oldValue, bool newValue) => Messenger.Send(new CalendarEnableStatusChangedMessage(newValue));
-
- private bool ShouldResetDayRanges(LoadCalendarMessage message)
- {
- if (message.ForceRedraw) return true;
-
- // Never reset if the initiative is from the app.
- if (message.CalendarInitInitiative == CalendarInitInitiative.App) return false;
-
- // 1. Display type is different.
- // 2. Day display count is different.
- // 3. Display date is not in the visible range.
-
- if (DayRanges.DisplayRange == null) return false;
-
- return
- (_currentDisplayType != StatePersistanceService.CalendarDisplayType ||
- _displayDayCount != StatePersistanceService.DayDisplayCount ||
- !(message.DisplayDate >= DayRanges.DisplayRange.StartDate && message.DisplayDate <= DayRanges.DisplayRange.EndDate));
- }
-
- private void AdjustCalendarOrientation()
- {
- // Orientation only changes when we should reset.
- // Handle the FlipView orientation here.
- // We don't want to change the orientation while the item manipulation is going on.
- // That causes a glitch in the UI.
-
- bool isRequestedVerticalCalendar = StatePersistanceService.CalendarDisplayType == CalendarDisplayType.Month;
- bool isLastRenderedVerticalCalendar = _currentDisplayType == CalendarDisplayType.Month;
-
- if (isRequestedVerticalCalendar && !isLastRenderedVerticalCalendar)
+ if (CurrentVisibleRange != null)
{
- CalendarOrientation = CalendarOrientation.Vertical;
- }
- else
- {
- CalendarOrientation = CalendarOrientation.Horizontal;
+ VisibleDateRangeText = _calendarRangeTextFormatter.Format(CurrentVisibleRange, _dateContextProvider);
}
}
- public async void Receive(LoadCalendarMessage message)
+ public async Task ApplyDisplayRequestAsync(CalendarDisplayRequest request, bool forceReload = false)
{
- await _calendarLoadingSemaphore.WaitAsync();
+ var lifetimeVersion = CurrentPageLifetimeVersion;
+ var hasLoadingLock = await WaitForCalendarLoadingLockAsync(lifetimeVersion).ConfigureAwait(false);
+
+ if (!hasLoadingLock)
+ return;
try
{
- await ExecuteUIThread(() => IsCalendarEnabled = false);
+ await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () => IsCalendarEnabled = false).ConfigureAwait(false);
- if (ShouldResetDayRanges(message))
- {
- Debug.WriteLine("Will reset day ranges.");
- await ClearDayRangeModelsAsync();
- }
- else if (ShouldScrollToItem(message))
- {
- // Scroll to the selected date.
- Messenger.Send(new ScrollToDateMessage(message.DisplayDate));
- Debug.WriteLine("Scrolling to selected date.");
+ if (!IsPageActive(lifetimeVersion))
return;
+
+ var currentSettings = CurrentSettings;
+ if (currentSettings == null)
+ {
+ RefreshSettings();
+ currentSettings = CurrentSettings;
}
- AdjustCalendarOrientation();
+ var today = _dateContextProvider.GetToday();
+ var visibleRange = CalendarRangeResolver.Resolve(request, currentSettings, today);
+ var previousRange = CalendarRangeResolver.Navigate(visibleRange, -1, currentSettings, today);
+ var nextRange = CalendarRangeResolver.Navigate(visibleRange, 1, currentSettings, today);
+ var loadedDateWindow = new DateRange(
+ previousRange.StartDate.ToDateTime(TimeOnly.MinValue),
+ nextRange.EndDate.AddDays(1).ToDateTime(TimeOnly.MinValue));
- // This will replace the whole collection because the user initiated a new render.
- await RenderDatesAsync(message.CalendarInitInitiative,
- message.DisplayDate,
- CalendarLoadDirection.Replace);
+ var shouldReload = forceReload || !IsSameVisibleRange(CurrentVisibleRange, visibleRange) || !IsSameDateRange(LoadedDateWindow, loadedDateWindow);
+ List loadedItems = null;
- // Scroll to the current hour.
- Messenger.Send(new ScrollToHourMessage(TimeSpan.FromHours(DateTime.Now.Hour)));
+ if (shouldReload)
+ {
+ loadedItems = await LoadCalendarItemsAsync(loadedDateWindow, lifetimeVersion).ConfigureAwait(false);
+ if (!IsPageActive(lifetimeVersion))
+ return;
+ }
+
+ await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () =>
+ {
+ if (loadedItems != null)
+ {
+ _loadedCalendarItems = loadedItems;
+ CalendarItems = loadedItems;
+ }
+
+ EnsureSelectedQuickEventAccountCalendar();
+ CurrentVisibleRange = visibleRange;
+ LoadedDateWindow = loadedDateWindow;
+ VisibleDateRangeText = _calendarRangeTextFormatter.Format(visibleRange, _dateContextProvider);
+ if (DisplayDetailsCalendarItemViewModel != null && !IsCalendarActive(DisplayDetailsCalendarItemViewModel.AssignedCalendar?.Id))
+ {
+ DisplayDetailsCalendarItemViewModel = null;
+ }
+ }).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ }
+ catch (COMException) when (!IsPageActive(lifetimeVersion))
+ {
+ }
+ catch (ObjectDisposedException) when (!IsPageActive(lifetimeVersion))
+ {
}
catch (Exception ex)
{
- Log.Error(ex, "Error while loading calendar.");
- Debugger.Break();
+ Log.Error(ex, "Error while loading visible calendar range.");
}
finally
{
- _calendarLoadingSemaphore.Release();
-
- await ExecuteUIThread(() => IsCalendarEnabled = true);
+ ReleaseCalendarLoadingLock();
+ await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () => IsCalendarEnabled = true).ConfigureAwait(false);
}
}
-
- private async Task AddDayRangeModelAsync(DayRangeRenderModel dayRangeRenderModel)
+ public Task ReloadCurrentVisibleRangeAsync()
{
- if (dayRangeRenderModel == null) return;
+ if (CurrentVisibleRange == null)
+ return Task.CompletedTask;
- await ExecuteUIThread(() =>
- {
- DayRanges.Add(dayRangeRenderModel);
- });
+ return ApplyDisplayRequestAsync(new CalendarDisplayRequest(CurrentVisibleRange.DisplayType, CurrentVisibleRange.AnchorDate), forceReload: true);
}
- private async Task InsertDayRangeModelAsync(DayRangeRenderModel dayRangeRenderModel, int index)
+ public async Task> SearchCalendarItemsAsync(string queryText, int limit, CancellationToken cancellationToken)
{
- if (dayRangeRenderModel == null) return;
+ var results = await _calendarService.SearchCalendarItemsAsync(queryText, limit, cancellationToken).ConfigureAwait(false);
+ var activeCalendarIds = AccountCalendarStateService.ActiveCalendars.Select(calendar => calendar.Id).ToHashSet();
- await ExecuteUIThread(() =>
- {
- DayRanges.Insert(index, dayRangeRenderModel);
- });
+ return results
+ .Where(result => activeCalendarIds.Contains(result.CalendarId))
+ .ToList();
}
- private async Task RemoveDayRangeModelAsync(DayRangeRenderModel dayRangeRenderModel)
+ public void OpenCalendarSearchResult(CalendarItem calendarItem)
{
- if (dayRangeRenderModel == null) return;
-
- await ExecuteUIThread(() =>
- {
- DayRanges.Remove(dayRangeRenderModel);
- });
+ ArgumentNullException.ThrowIfNull(calendarItem);
+ NavigateEvent(new CalendarItemViewModel(calendarItem), CalendarEventTargetType.Single);
}
- private async Task ClearDayRangeModelsAsync()
+ private async Task> LoadCalendarItemsAsync(DateRange loadedDateWindow, long lifetimeVersion)
{
- await ExecuteUIThread(() =>
+ var loadedItems = new Dictionary();
+ var loadPeriod = new TimeRange(loadedDateWindow.StartDate, loadedDateWindow.EndDate);
+
+ foreach (var calendarViewModel in AccountCalendarStateService.ActiveCalendars)
{
- DayRanges.Clear();
- });
- }
+ if (!IsPageActive(lifetimeVersion))
+ return [];
- private async Task RenderDatesAsync(CalendarInitInitiative calendarInitInitiative,
- DateTime? loadingDisplayDate = null,
- CalendarLoadDirection calendarLoadDirection = CalendarLoadDirection.Replace)
- {
- isLoadMoreBlocked = calendarLoadDirection == CalendarLoadDirection.Replace;
-
- // This is the part we arrange the flip view calendar logic.
-
- /* Loading for a month of the selected date is fine.
- * If the selected date is in the loaded range, we'll just change the selected flip index to scroll.
- * If the selected date is not in the loaded range:
- * 1. Detect the direction of the scroll.
- * 2. Load the next month.
- * 3. Replace existing month with the new month.
- */
-
- // 2 things are important: How many items should 1 flip have, and, where we should start loading?
-
- // User initiated renders must always have a date to start with.
- if (calendarInitInitiative == CalendarInitInitiative.User) Guard.IsNotNull(loadingDisplayDate, nameof(loadingDisplayDate));
-
- var strategy = GetDrawingStrategy(StatePersistanceService.CalendarDisplayType);
- var displayDate = loadingDisplayDate.GetValueOrDefault();
-
- // How many days should be placed in 1 flip view item?
- int eachFlipItemCount = strategy.GetRenderDayCount(displayDate, StatePersistanceService.DayDisplayCount);
-
- DateRange flipLoadRange = null;
-
-
- if (calendarInitInitiative == CalendarInitInitiative.User || DayRanges.DisplayRange == null)
- {
- flipLoadRange = strategy.GetRenderDateRange(displayDate, StatePersistanceService.DayDisplayCount);
- }
- else
- {
- // App is trying to load.
- // This should be based on direction. We'll load the next or previous range.
- // DisplayDate is either the start or end date of the current visible range.
-
- if (calendarLoadDirection == CalendarLoadDirection.Previous)
+ var events = await _calendarService.GetCalendarEventsAsync(calendarViewModel, loadPeriod).ConfigureAwait(false);
+ foreach (var calendarItem in events)
{
- flipLoadRange = strategy.GetPreviousDateRange(DayRanges.DisplayRange, StatePersistanceService.DayDisplayCount);
- }
- else
- {
- flipLoadRange = strategy.GetNextDateRange(DayRanges.DisplayRange, StatePersistanceService.DayDisplayCount);
- }
- }
+ if (calendarItem.IsRecurringParent || calendarItem.IsHidden)
+ continue;
- // Create day ranges for each flip item until we reach the total days to load.
- int totalFlipItemCount = (int)Math.Ceiling((double)flipLoadRange.TotalDays / eachFlipItemCount);
+ calendarItem.AssignedCalendar ??= calendarViewModel;
- List renderModels = new();
-
- for (int i = 0; i < totalFlipItemCount; i++)
- {
- var startDate = flipLoadRange.StartDate.AddDays(i * eachFlipItemCount);
- var endDate = startDate.AddDays(eachFlipItemCount);
-
- var range = new DateRange(startDate, endDate);
- var renderOptions = new CalendarRenderOptions(range, CurrentSettings);
-
- var dayRangeHeaderModel = new DayRangeRenderModel(renderOptions);
- renderModels.Add(dayRangeHeaderModel);
- }
-
- // Dates are loaded. Now load the events for them.
- foreach (var renderModel in renderModels)
- {
- await InitializeCalendarEventsForDayRangeAsync(renderModel).ConfigureAwait(false);
- }
-
- // Filter by active calendars. This is a quick operation, and things are not on the UI yet.
- FilterActiveCalendars(renderModels);
-
- CalendarLoadDirection animationDirection = calendarLoadDirection;
-
- //bool removeCurrent = calendarLoadDirection == CalendarLoadDirection.Replace;
-
- if (calendarLoadDirection == CalendarLoadDirection.Replace)
- {
- // New date ranges are being replaced.
- // We must preserve existing selection if any, add the items before/after the current one, remove the current one.
- // This will make sure the new dates are animated in the correct direction.
-
- isLoadMoreBlocked = true;
-
- // Remove all other dates except this one.
- var rangesToRemove = DayRanges.Where(a => a != SelectedDayRange).ToList();
-
- foreach (var range in rangesToRemove)
- {
- await RemoveDayRangeModelAsync(range);
- }
-
- animationDirection = displayDate <= SelectedDayRange?.CalendarRenderOptions.DateRange.StartDate ?
- CalendarLoadDirection.Previous : CalendarLoadDirection.Next;
- }
-
- if (animationDirection == CalendarLoadDirection.Next)
- {
- foreach (var item in renderModels)
- {
- await AddDayRangeModelAsync(item);
- }
- }
- else if (animationDirection == CalendarLoadDirection.Previous)
- {
- // Wait for the animation to finish.
- // Otherwise it somehow shutters a little, which is annoying.
-
- // if (!removeCurrent) await Task.Delay(350);
-
- // Insert each render model in reverse order.
- for (int i = renderModels.Count - 1; i >= 0; i--)
- {
- await InsertDayRangeModelAsync(renderModels[i], 0);
- }
- }
-
- Debug.WriteLine($"Flip count: ({DayRanges.Count})");
-
- foreach (var item in DayRanges)
- {
- Debug.WriteLine($"- {item.CalendarRenderOptions.DateRange.ToString()}");
- }
-
- //if (removeCurrent)
- //{
- // await RemoveDayRangeModelAsync(SelectedDayRange);
- //}
-
- // TODO...
- // await TryConsolidateItemsAsync();
-
- isLoadMoreBlocked = false;
-
- // Only scroll if the render is initiated by user.
- // Otherwise we'll scroll to the app rendered invisible date range.
- if (calendarInitInitiative == CalendarInitInitiative.User)
- {
- // Save the current settings for the page for later comparison.
- _currentDisplayType = StatePersistanceService.CalendarDisplayType;
- _displayDayCount = StatePersistanceService.DayDisplayCount;
-
- Messenger.Send(new ScrollToDateMessage(displayDate));
- }
- }
-
- protected override async void OnCalendarItemAdded(CalendarItem calendarItem)
- {
- base.OnCalendarItemAdded(calendarItem);
-
- // Check if event falls into the current date range.
-
-
- if (DayRanges.DisplayRange == null) return;
-
- // Check whether this event falls into any of the loaded date ranges.
- var allDaysForEvent = DayRanges.SelectMany(a => a.CalendarDays).Where(a => a.Period.OverlapsWith(calendarItem.Period));
-
- foreach (var calendarDay in allDaysForEvent)
- {
- var calendarItemViewModel = new CalendarItemViewModel(calendarItem);
-
- await ExecuteUIThread(() =>
- {
- calendarDay.EventsCollection.AddCalendarItem(calendarItemViewModel);
- });
- }
-
- FilterActiveCalendars(DayRanges);
- }
-
- private async Task InitializeCalendarEventsForDayRangeAsync(DayRangeRenderModel dayRangeRenderModel)
- {
- // Clear all events first for all days.
- foreach (var day in dayRangeRenderModel.CalendarDays)
- {
- await ExecuteUIThread(() =>
- {
- day.EventsCollection.Clear();
- });
- }
-
- // Initialization is done for all calendars, regardless whether they are actively selected or not.
- // This is because the filtering is cached internally of the calendar items in CalendarEventCollection.
- var allCalendars = AccountCalendarStateService.GroupedAccountCalendars.SelectMany(a => a.AccountCalendars);
-
- foreach (var calendarViewModel in allCalendars)
- {
- // Check all the events for the given date range and calendar.
- // Then find the day representation for all the events returned, and add to the collection.
-
- var events = await _calendarService.GetCalendarEventsAsync(calendarViewModel, dayRangeRenderModel).ConfigureAwait(false);
-
- foreach (var @event in events)
- {
- // Find the days that the event falls into.
- var allDaysForEvent = dayRangeRenderModel.CalendarDays.Where(a => a.Period.OverlapsWith(@event.Period));
-
- foreach (var calendarDay in allDaysForEvent)
+ if (!loadedItems.ContainsKey(calendarItem.Id))
{
- var calendarItemViewModel = new CalendarItemViewModel(@event);
- await ExecuteUIThread(() =>
- {
- calendarDay.EventsCollection.AddCalendarItem(calendarItemViewModel);
- });
+ loadedItems.Add(calendarItem.Id, new CalendarItemViewModel(calendarItem));
}
}
}
+
+ return loadedItems.Values.ToList();
}
- private async Task TryConsolidateItemsAsync()
+ private static bool IsSameVisibleRange(VisibleDateRange current, VisibleDateRange next)
{
- // Check if trimming is necessary
- if (DayRanges.Count > maxDayRangeSize)
+ if (current == null && next == null)
+ return true;
+
+ if (current == null || next == null)
+ return false;
+
+ return current.DisplayType == next.DisplayType &&
+ current.AnchorDate == next.AnchorDate &&
+ current.StartDate == next.StartDate &&
+ current.EndDate == next.EndDate;
+ }
+
+ private static bool IsSameDateRange(DateRange current, DateRange next)
+ {
+ if (current == null && next == null)
+ return true;
+
+ if (current == null || next == null)
+ return false;
+
+ return current.StartDate == next.StartDate && current.EndDate == next.EndDate;
+ }
+
+ private bool IsCalendarActive(Guid? calendarId)
+ => calendarId.HasValue && AccountCalendarStateService.ActiveCalendars.Any(calendar => calendar.Id == calendarId.Value);
+
+ private void EnsureSelectedQuickEventAccountCalendar()
+ {
+ if (SelectedQuickEventAccountCalendar != null && IsCalendarActive(SelectedQuickEventAccountCalendar.Id))
{
- Debug.WriteLine("Trimming items.");
+ return;
+ }
- isLoadMoreBlocked = true;
+ SelectedQuickEventAccountCalendar = AccountCalendarStateService.ActiveCalendars.FirstOrDefault(a => a.IsPrimary)
+ ?? AccountCalendarStateService.ActiveCalendars.FirstOrDefault();
+ }
- var removeCount = DayRanges.Count - maxDayRangeSize;
+ public async void Receive(LoadCalendarMessage message)
+ => await ApplyDisplayRequestAsync(message.DisplayRequest, message.ForceReload);
- await Task.Delay(500);
+ public void Receive(CalendarSettingsUpdatedMessage message)
+ {
+ RefreshSettings();
+ _ = ReloadCurrentVisibleRangeAsync();
+ }
- // Right shifted, remove from the start.
- if (SelectedDateRangeIndex > DayRanges.Count / 2)
- {
- DayRanges.RemoveRange(DayRanges.Take(removeCount).ToList());
- }
- else
- {
- // Left shifted, remove from the end.
- DayRanges.RemoveRange(DayRanges.Skip(DayRanges.Count - removeCount).Take(removeCount));
- }
+ public void Receive(CalendarItemTappedMessage message)
+ {
+ if (message.CalendarItemViewModel == null)
+ return;
- SelectedDateRangeIndex = DayRanges.IndexOf(SelectedDayRange);
+ DisplayDetailsCalendarItemViewModel = message.CalendarItemViewModel;
+ }
+
+ public void Receive(CalendarItemDoubleTappedMessage message)
+ => NavigateEvent(message.CalendarItemViewModel, CalendarEventTargetType.Single);
+
+ public void Receive(CalendarItemRightTappedMessage message)
+ {
+ }
+
+ public async void Receive(AccountRemovedMessage message)
+ {
+ if (DisplayDetailsCalendarItemViewModel?.AssignedCalendar?.AccountId == message.Account.Id)
+ {
+ DisplayDetailsCalendarItemViewModel = null;
+ }
+
+ EnsureSelectedQuickEventAccountCalendar();
+ await ReloadCurrentVisibleRangeAsync().ConfigureAwait(false);
+ }
+
+ protected override void OnCalendarItemDeleted(CalendarItem calendarItem)
+ {
+ base.OnCalendarItemDeleted(calendarItem);
+
+ if (DisplayDetailsCalendarItemViewModel?.Id == calendarItem.Id ||
+ DisplayDetailsCalendarItemViewModel?.CalendarItem?.RecurringCalendarItemId == calendarItem.Id)
+ {
+ DisplayDetailsCalendarItemViewModel = null;
+ }
+
+ if (ShouldReloadFor(calendarItem))
+ {
+ _ = ReloadCurrentVisibleRangeAsync();
}
}
- private bool ShouldScrollToItem(LoadCalendarMessage message)
+ protected override void OnCalendarItemUpdated(CalendarItem calendarItem, CalendarItemUpdateSource source)
{
- // Never scroll if the initiative is from the app.
- if (message.CalendarInitInitiative == CalendarInitInitiative.App) return false;
+ base.OnCalendarItemUpdated(calendarItem, source);
- // Nothing to scroll.
- if (DayRanges.Count == 0) return false;
+ if (DisplayDetailsCalendarItemViewModel?.Id == calendarItem.Id)
+ {
+ calendarItem.AssignedCalendar ??= DisplayDetailsCalendarItemViewModel.AssignedCalendar;
+ DisplayDetailsCalendarItemViewModel = new CalendarItemViewModel(calendarItem);
+ }
- if (DayRanges.DisplayRange == null) return false;
+ if (ShouldReloadFor(calendarItem))
+ {
+ _ = ReloadCurrentVisibleRangeAsync();
+ }
+ }
- var selectedDate = message.DisplayDate;
+ protected override void OnCalendarItemAdded(CalendarItem calendarItem)
+ {
+ base.OnCalendarItemAdded(calendarItem);
- return selectedDate >= DayRanges.DisplayRange.StartDate && selectedDate <= DayRanges.DisplayRange.EndDate;
+ if (calendarItem.IsRecurringParent)
+ {
+ _ = ReloadCurrentVisibleRangeAsync();
+ return;
+ }
+
+ if (ShouldReloadFor(calendarItem))
+ {
+ _ = ReloadCurrentVisibleRangeAsync();
+ }
+ }
+
+ private bool ShouldReloadFor(CalendarItem calendarItem)
+ {
+ if (calendarItem == null || LoadedDateWindow == null)
+ return false;
+
+ var loadedWindow = new TimeRange(LoadedDateWindow.StartDate, LoadedDateWindow.EndDate);
+ return loadedWindow.OverlapsWith(calendarItem.Period);
}
partial void OnIsAllDayChanged(bool value)
{
if (value)
{
+ _previousSelectedStartTimeString = SelectedStartTimeString;
+ _previousSelectedEndTimeString = SelectedEndTimeString;
SelectedStartTimeString = HourSelectionStrings.FirstOrDefault();
SelectedEndTimeString = HourSelectionStrings.FirstOrDefault();
}
@@ -711,7 +827,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
}
}
- partial void OnSelectedStartTimeStringChanged(string newValue)
+ partial void OnSelectedStartTimeStringChanged(string oldValue, string newValue)
{
var parsedTime = CurrentSettings.GetTimeSpan(newValue);
@@ -719,152 +835,23 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
{
SelectedStartTimeString = _previousSelectedStartTimeString;
}
- else if (IsAllDay)
+ else if (!IsAllDay)
{
_previousSelectedStartTimeString = newValue;
}
}
- partial void OnSelectedEndTimeStringChanged(string newValue)
+ partial void OnSelectedEndTimeStringChanged(string oldValue, string newValue)
{
var parsedTime = CurrentSettings.GetTimeSpan(newValue);
if (parsedTime == null)
{
- SelectedEndTimeString = _previousSelectedStartTimeString;
+ SelectedEndTimeString = _previousSelectedEndTimeString;
}
- else if (IsAllDay)
+ else if (!IsAllDay)
{
_previousSelectedEndTimeString = newValue;
}
}
-
- partial void OnSelectedDayRangeChanged(DayRangeRenderModel value)
- {
- DisplayDetailsCalendarItemViewModel = null;
-
- if (DayRanges.Count == 0 || SelectedDateRangeIndex < 0) return;
-
- var selectedRange = DayRanges[SelectedDateRangeIndex];
-
- Messenger.Send(new VisibleDateRangeChangedMessage(new DateRange(selectedRange.Period.Start, selectedRange.Period.End)));
-
- if (isLoadMoreBlocked) return;
-
- _ = LoadMoreAsync();
- }
-
- private async Task LoadMoreAsync()
- {
- try
- {
- await _calendarLoadingSemaphore.WaitAsync();
-
- // Depending on the selected index, we'll load more dates.
- // Day ranges may change while the async update is in progress.
- // Therefore we wait for semaphore to be released before we continue.
- // There is no need to load more if the current index is not in ideal position.
-
- if (SelectedDateRangeIndex == 0)
- {
- await RenderDatesAsync(CalendarInitInitiative.App, calendarLoadDirection: CalendarLoadDirection.Previous);
- }
- else if (SelectedDateRangeIndex == DayRanges.Count - 1)
- {
- await RenderDatesAsync(CalendarInitInitiative.App, calendarLoadDirection: CalendarLoadDirection.Next);
- }
- }
- catch (Exception)
- {
- Debugger.Break();
- }
- finally
- {
- _calendarLoadingSemaphore.Release();
- }
- }
-
- public void Receive(CalendarSettingsUpdatedMessage message)
- {
- RefreshSettings();
-
- // TODO: This might need throttling due to slider in the settings page for hour height.
- // or make sure the slider does not update on each tick but on focus lost.
-
- // Messenger.Send(new LoadCalendarMessage(DateTime.UtcNow.Date, CalendarInitInitiative.App, true));
- }
-
- private IEnumerable GetCalendarItems(CalendarItemViewModel calendarItemViewModel, CalendarDayModel selectedDay)
- {
- // All-day and multi-day events are selected collectively.
- // Recurring events must be selected as a single instance.
- // We need to find the day that the event is in, and then select the event.
-
- if (!calendarItemViewModel.IsRecurringEvent)
- {
- return [calendarItemViewModel];
- }
- else
- {
- return DayRanges
- .SelectMany(a => a.CalendarDays)
- .Select(b => b.EventsCollection.GetCalendarItem(calendarItemViewModel.Id))
- .Where(c => c != null)
- .Cast()
- .Distinct();
- }
- }
-
- private void UnselectCalendarItem(CalendarItemViewModel calendarItemViewModel, CalendarDayModel calendarDay = null)
- {
- if (calendarItemViewModel == null) return;
-
- var itemsToUnselect = GetCalendarItems(calendarItemViewModel, calendarDay);
-
- foreach (var item in itemsToUnselect)
- {
- item.IsSelected = false;
- }
- }
-
- private void SelectCalendarItem(CalendarItemViewModel calendarItemViewModel, CalendarDayModel calendarDay = null)
- {
- if (calendarItemViewModel == null) return;
-
- var itemsToSelect = GetCalendarItems(calendarItemViewModel, calendarDay);
-
- foreach (var item in itemsToSelect)
- {
- item.IsSelected = true;
- }
- }
-
- public void Receive(CalendarItemTappedMessage message)
- {
- if (message.CalendarItemViewModel == null) return;
-
- DisplayDetailsCalendarItemViewModel = message.CalendarItemViewModel;
- }
-
- public void Receive(CalendarItemDoubleTappedMessage message) => NavigateEvent(message.CalendarItemViewModel, CalendarEventTargetType.Single);
-
- public void Receive(CalendarItemRightTappedMessage message)
- {
-
- }
-
- public async void Receive(CalendarItemDeleted message)
- {
- // Each deleted recurrence will report for it's own.
-
- await ExecuteUIThread(() =>
- {
- var deletedItem = message.CalendarItem;
-
- // Event might be spreaded into multiple days.
- // Remove from all.
-
- // var calendarItems = GetCalendarItems(deletedItem.Id);
- });
- }
}
diff --git a/Wino.Calendar.ViewModels/CalendarPreferenceSettingsPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarPreferenceSettingsPageViewModel.cs
new file mode 100644
index 00000000..c66b1d1d
--- /dev/null
+++ b/Wino.Calendar.ViewModels/CalendarPreferenceSettingsPageViewModel.cs
@@ -0,0 +1,62 @@
+using System.Linq;
+using CommunityToolkit.Mvvm.ComponentModel;
+using Wino.Calendar.ViewModels.Data;
+using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Interfaces;
+
+namespace Wino.Calendar.ViewModels;
+
+public partial class CalendarPreferenceSettingsPageViewModel : CalendarSettingsSectionViewModelBase
+{
+ [ObservableProperty]
+ public partial CalendarNewEventBehaviorOption SelectedNewEventBehaviorOption { get; set; }
+
+ [ObservableProperty]
+ public partial AccountCalendarViewModel SelectedNewEventCalendar { get; set; }
+
+ public bool ShouldShowSpecificNewEventCalendar
+ => SelectedNewEventBehaviorOption?.Behavior == NewEventButtonBehavior.AlwaysUseSpecificCalendar;
+
+ public CalendarPreferenceSettingsPageViewModel(
+ IPreferencesService preferencesService,
+ ICalendarService calendarService,
+ IAccountService accountService)
+ : base(preferencesService, calendarService, accountService)
+ {
+ LoadNewEventBehaviorOptions();
+ SelectedNewEventBehaviorOption = GetSelectedNewEventBehaviorOption();
+
+ IsLoaded = true;
+ LoadCalendarsAsync(ApplyStoredNewEventCalendarPreference);
+ }
+
+ partial void OnSelectedNewEventBehaviorOptionChanged(CalendarNewEventBehaviorOption value)
+ {
+ if (!IsLoaded)
+ return;
+
+ OnPropertyChanged(nameof(ShouldShowSpecificNewEventCalendar));
+ SaveNewEventBehavior(SelectedNewEventBehaviorOption, SelectedNewEventCalendar);
+ }
+
+ partial void OnSelectedNewEventCalendarChanged(AccountCalendarViewModel value)
+ {
+ if (!IsLoaded)
+ return;
+
+ SaveNewEventBehavior(SelectedNewEventBehaviorOption, value);
+ }
+
+ private void ApplyStoredNewEventCalendarPreference()
+ {
+ var configuredCalendar = ResolveSelectedNewEventCalendar();
+ if (PreferencesService.NewEventButtonBehavior == NewEventButtonBehavior.AlwaysUseSpecificCalendar && configuredCalendar == null)
+ {
+ SelectedNewEventBehaviorOption = NewEventBehaviorOptions.First(option => option.Behavior == NewEventButtonBehavior.AskEachTime);
+ SelectedNewEventCalendar = null;
+ return;
+ }
+
+ SelectedNewEventCalendar = configuredCalendar ?? ResolveFallbackNewEventCalendar();
+ }
+}
diff --git a/Wino.Calendar.ViewModels/CalendarRenderingSettingsPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarRenderingSettingsPageViewModel.cs
new file mode 100644
index 00000000..5a7fe685
--- /dev/null
+++ b/Wino.Calendar.ViewModels/CalendarRenderingSettingsPageViewModel.cs
@@ -0,0 +1,191 @@
+using System;
+using System.Linq;
+using CommunityToolkit.Mvvm.ComponentModel;
+using Wino.Core.Domain.Interfaces;
+
+namespace Wino.Calendar.ViewModels;
+
+public partial class CalendarRenderingSettingsPageViewModel : CalendarSettingsSectionViewModelBase
+{
+ [ObservableProperty]
+ public partial double CellHourHeight { get; set; }
+
+ [ObservableProperty]
+ public partial int SelectedFirstDayOfWeekIndex { get; set; }
+
+ [ObservableProperty]
+ public partial bool Is24HourHeaders { get; set; }
+
+ [ObservableProperty]
+ public partial bool IsWorkingHoursEnabled { get; set; }
+
+ [ObservableProperty]
+ public partial TimeSpan WorkingHourStart { get; set; }
+
+ [ObservableProperty]
+ public partial TimeSpan WorkingHourEnd { get; set; }
+
+ [ObservableProperty]
+ public partial int WorkingDayStartIndex { get; set; }
+
+ [ObservableProperty]
+ public partial int WorkingDayEndIndex { get; set; }
+
+ [ObservableProperty]
+ public partial string TimedDayHeaderDateFormat { get; set; } = "ddd dd";
+
+ [ObservableProperty]
+ public partial int SelectedTimedDayHeaderFormatPresetIndex { get; set; } = -1;
+
+ public CalendarRenderingSettingsPageViewModel(
+ IPreferencesService preferencesService,
+ ICalendarService calendarService,
+ IAccountService accountService)
+ : base(preferencesService, calendarService, accountService)
+ {
+ SelectedFirstDayOfWeekIndex = DayNames.IndexOf(CalendarCulture.DateTimeFormat.GetDayName(preferencesService.FirstDayOfWeek));
+ Is24HourHeaders = preferencesService.Prefer24HourTimeFormat;
+ IsWorkingHoursEnabled = preferencesService.IsWorkingHoursEnabled;
+ WorkingHourStart = preferencesService.WorkingHourStart;
+ WorkingHourEnd = preferencesService.WorkingHourEnd;
+ CellHourHeight = preferencesService.HourHeight;
+ WorkingDayStartIndex = DayNames.IndexOf(CalendarCulture.DateTimeFormat.GetDayName(preferencesService.WorkingDayStart));
+ WorkingDayEndIndex = DayNames.IndexOf(CalendarCulture.DateTimeFormat.GetDayName(preferencesService.WorkingDayEnd));
+ TimedDayHeaderDateFormat = preferencesService.CalendarTimedDayHeaderDateFormat;
+ SelectedTimedDayHeaderFormatPresetIndex = TimedDayHeaderFormatPresets.IndexOf(TimedDayHeaderDateFormat);
+
+ IsLoaded = true;
+ }
+
+ partial void OnCellHourHeightChanged(double oldValue, double newValue) => SaveSettings();
+
+ partial void OnIs24HourHeadersChanged(bool value)
+ {
+ OnPropertyChanged(nameof(TimedHourLabelPreview));
+ SaveSettings();
+ }
+
+ partial void OnSelectedFirstDayOfWeekIndexChanged(int value) => SaveSettings();
+ partial void OnIsWorkingHoursEnabledChanged(bool value) => SaveSettings();
+ partial void OnWorkingHourStartChanged(TimeSpan value) => SaveSettings();
+ partial void OnWorkingHourEndChanged(TimeSpan value) => SaveSettings();
+ partial void OnWorkingDayStartIndexChanged(int value) => SaveSettings();
+ partial void OnWorkingDayEndIndexChanged(int value) => SaveSettings();
+
+ partial void OnTimedDayHeaderDateFormatChanged(string value)
+ {
+ OnPropertyChanged(nameof(TimedDayHeaderFormatPreview));
+ OnPropertyChanged(nameof(TimedHourLabelPreview));
+
+ var normalizedFormat = string.IsNullOrWhiteSpace(value) ? "ddd dd" : value.Trim();
+ var matchingPresetIndex = TimedDayHeaderFormatPresets
+ .Select((format, index) => new { format, index })
+ .Where(item => string.Equals(item.format, normalizedFormat, StringComparison.Ordinal))
+ .Select(item => item.index)
+ .DefaultIfEmpty(-1)
+ .First();
+
+ if (SelectedTimedDayHeaderFormatPresetIndex != matchingPresetIndex)
+ {
+ SelectedTimedDayHeaderFormatPresetIndex = matchingPresetIndex;
+ }
+
+ SaveSettings();
+ }
+
+ partial void OnSelectedTimedDayHeaderFormatPresetIndexChanged(int value)
+ {
+ if (value < 0 || value >= TimedDayHeaderFormatPresets.Count)
+ return;
+
+ var selectedPreset = TimedDayHeaderFormatPresets[value];
+ if (string.Equals(TimedDayHeaderDateFormat, selectedPreset, StringComparison.Ordinal))
+ return;
+
+ TimedDayHeaderDateFormat = selectedPreset;
+ }
+
+ public string TimedDayHeaderFormatPreview
+ {
+ get
+ {
+ var format = string.IsNullOrWhiteSpace(TimedDayHeaderDateFormat) ? "ddd dd" : TimedDayHeaderDateFormat.Trim();
+ var previewDates = new[]
+ {
+ new DateTime(2026, 3, 23),
+ new DateTime(2026, 3, 24),
+ new DateTime(2026, 3, 25)
+ };
+
+ try
+ {
+ return string.Join(" · ", previewDates.Select(date => date.ToString(format, CalendarCulture)));
+ }
+ catch (FormatException)
+ {
+ return string.Join(" · ", previewDates.Select(date => date.ToString("ddd dd", CalendarCulture)));
+ }
+ }
+ }
+
+ public string TimedHourLabelPreview
+ => string.Join(" · ", new[] { 0, 9, 14, 24 }.Select(CurrentSettingsPreviewLabel));
+
+ private string CurrentSettingsPreviewLabel(int hour)
+ {
+ if (Is24HourHeaders)
+ return hour.ToString(CalendarCulture);
+
+ var displayHour = hour % 24;
+ return DateTime.Today.AddHours(displayHour).ToString("h tt", CalendarCulture);
+ }
+
+ private void SaveSettings()
+ {
+ if (!IsLoaded)
+ return;
+
+ PreferencesService.FirstDayOfWeek = SelectedFirstDayOfWeekIndex switch
+ {
+ 0 => DayOfWeek.Sunday,
+ 1 => DayOfWeek.Monday,
+ 2 => DayOfWeek.Tuesday,
+ 3 => DayOfWeek.Wednesday,
+ 4 => DayOfWeek.Thursday,
+ 5 => DayOfWeek.Friday,
+ 6 => DayOfWeek.Saturday,
+ _ => throw new ArgumentOutOfRangeException()
+ };
+
+ PreferencesService.WorkingDayStart = WorkingDayStartIndex switch
+ {
+ 0 => DayOfWeek.Sunday,
+ 1 => DayOfWeek.Monday,
+ 2 => DayOfWeek.Tuesday,
+ 3 => DayOfWeek.Wednesday,
+ 4 => DayOfWeek.Thursday,
+ 5 => DayOfWeek.Friday,
+ 6 => DayOfWeek.Saturday,
+ _ => throw new ArgumentOutOfRangeException()
+ };
+
+ PreferencesService.WorkingDayEnd = WorkingDayEndIndex switch
+ {
+ 0 => DayOfWeek.Sunday,
+ 1 => DayOfWeek.Monday,
+ 2 => DayOfWeek.Tuesday,
+ 3 => DayOfWeek.Wednesday,
+ 4 => DayOfWeek.Thursday,
+ 5 => DayOfWeek.Friday,
+ 6 => DayOfWeek.Saturday,
+ _ => throw new ArgumentOutOfRangeException()
+ };
+
+ PreferencesService.Prefer24HourTimeFormat = Is24HourHeaders;
+ PreferencesService.IsWorkingHoursEnabled = IsWorkingHoursEnabled;
+ PreferencesService.WorkingHourStart = WorkingHourStart;
+ PreferencesService.WorkingHourEnd = WorkingHourEnd;
+ PreferencesService.HourHeight = CellHourHeight;
+ PreferencesService.CalendarTimedDayHeaderDateFormat = TimedDayHeaderDateFormat;
+ }
+}
diff --git a/Wino.Calendar.ViewModels/CalendarSettingsPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarSettingsPageViewModel.cs
deleted file mode 100644
index 4d0075c7..00000000
--- a/Wino.Calendar.ViewModels/CalendarSettingsPageViewModel.cs
+++ /dev/null
@@ -1,126 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Messaging;
-using Wino.Core.Domain.Interfaces;
-using Wino.Core.Domain.Translations;
-using Wino.Core.ViewModels;
-using Wino.Messaging.Client.Calendar;
-
-namespace Wino.Calendar.ViewModels;
-
-public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
-{
- [ObservableProperty]
- private double _cellHourHeight;
-
- [ObservableProperty]
- private int _selectedFirstDayOfWeekIndex;
-
- [ObservableProperty]
- private bool _is24HourHeaders;
-
- [ObservableProperty]
- private TimeSpan _workingHourStart;
-
- [ObservableProperty]
- private TimeSpan _workingHourEnd;
-
- [ObservableProperty]
- private List _dayNames = [];
-
- [ObservableProperty]
- private int _workingDayStartIndex;
-
- [ObservableProperty]
- private int _workingDayEndIndex;
-
- public IPreferencesService PreferencesService { get; }
-
- private readonly bool _isLoaded = false;
-
- public CalendarSettingsPageViewModel(IPreferencesService preferencesService)
- {
- PreferencesService = preferencesService;
-
- var currentLanguageLanguageCode = WinoTranslationDictionary.GetLanguageFileNameRelativePath(preferencesService.CurrentLanguage);
-
- var cultureInfo = new CultureInfo(currentLanguageLanguageCode);
-
- // Populate the day names list
- for (var i = 0; i < 7; i++)
- {
- _dayNames.Add(cultureInfo.DateTimeFormat.DayNames[i]);
- }
-
- var cultureFirstDayName = cultureInfo.DateTimeFormat.GetDayName(preferencesService.FirstDayOfWeek);
-
- _selectedFirstDayOfWeekIndex = _dayNames.IndexOf(cultureFirstDayName);
- _is24HourHeaders = preferencesService.Prefer24HourTimeFormat;
- _workingHourStart = preferencesService.WorkingHourStart;
- _workingHourEnd = preferencesService.WorkingHourEnd;
- _cellHourHeight = preferencesService.HourHeight;
-
- _workingDayStartIndex = _dayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayStart));
- _workingDayEndIndex = _dayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayEnd));
-
- _isLoaded = true;
- }
-
- partial void OnCellHourHeightChanged(double oldValue, double newValue) => SaveSettings();
- partial void OnIs24HourHeadersChanged(bool value) => SaveSettings();
- partial void OnSelectedFirstDayOfWeekIndexChanged(int value) => SaveSettings();
- partial void OnWorkingHourStartChanged(TimeSpan value) => SaveSettings();
- partial void OnWorkingHourEndChanged(TimeSpan value) => SaveSettings();
- partial void OnWorkingDayStartIndexChanged(int value) => SaveSettings();
- partial void OnWorkingDayEndIndexChanged(int value) => SaveSettings();
-
- public void SaveSettings()
- {
- if (!_isLoaded) return;
-
- PreferencesService.FirstDayOfWeek = SelectedFirstDayOfWeekIndex switch
- {
- 0 => DayOfWeek.Sunday,
- 1 => DayOfWeek.Monday,
- 2 => DayOfWeek.Tuesday,
- 3 => DayOfWeek.Wednesday,
- 4 => DayOfWeek.Thursday,
- 5 => DayOfWeek.Friday,
- 6 => DayOfWeek.Saturday,
- _ => throw new ArgumentOutOfRangeException()
- };
-
- PreferencesService.WorkingDayStart = WorkingDayStartIndex switch
- {
- 0 => DayOfWeek.Sunday,
- 1 => DayOfWeek.Monday,
- 2 => DayOfWeek.Tuesday,
- 3 => DayOfWeek.Wednesday,
- 4 => DayOfWeek.Thursday,
- 5 => DayOfWeek.Friday,
- 6 => DayOfWeek.Saturday,
- _ => throw new ArgumentOutOfRangeException()
- };
-
- PreferencesService.WorkingDayEnd = WorkingDayEndIndex switch
- {
- 0 => DayOfWeek.Sunday,
- 1 => DayOfWeek.Monday,
- 2 => DayOfWeek.Tuesday,
- 3 => DayOfWeek.Wednesday,
- 4 => DayOfWeek.Thursday,
- 5 => DayOfWeek.Friday,
- 6 => DayOfWeek.Saturday,
- _ => throw new ArgumentOutOfRangeException()
- };
-
- PreferencesService.Prefer24HourTimeFormat = Is24HourHeaders;
- PreferencesService.WorkingHourStart = WorkingHourStart;
- PreferencesService.WorkingHourEnd = WorkingHourEnd;
- PreferencesService.HourHeight = CellHourHeight;
-
- Messenger.Send(new CalendarSettingsUpdatedMessage());
- }
-}
diff --git a/Wino.Calendar.ViewModels/CalendarSettingsSectionViewModelBase.cs b/Wino.Calendar.ViewModels/CalendarSettingsSectionViewModelBase.cs
new file mode 100644
index 00000000..3058479f
--- /dev/null
+++ b/Wino.Calendar.ViewModels/CalendarSettingsSectionViewModelBase.cs
@@ -0,0 +1,197 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.Linq;
+using Wino.Calendar.ViewModels.Data;
+using Wino.Core.Domain;
+using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Interfaces;
+using Wino.Core.Domain.Translations;
+using Wino.Core.ViewModels;
+
+namespace Wino.Calendar.ViewModels;
+
+public abstract class CalendarSettingsSectionViewModelBase : CalendarBaseViewModel
+{
+ protected CalendarSettingsSectionViewModelBase(
+ IPreferencesService preferencesService,
+ ICalendarService calendarService,
+ IAccountService accountService)
+ {
+ PreferencesService = preferencesService;
+ CalendarService = calendarService;
+ AccountService = accountService;
+
+ var languageCode = WinoTranslationDictionary.GetLanguageFileNameRelativePath(preferencesService.CurrentLanguage);
+ CalendarCulture = new CultureInfo(languageCode);
+
+ for (var index = 0; index < 7; index++)
+ {
+ DayNames.Add(CalendarCulture.DateTimeFormat.DayNames[index]);
+ }
+ }
+
+ protected IPreferencesService PreferencesService { get; }
+ protected ICalendarService CalendarService { get; }
+ protected IAccountService AccountService { get; }
+ protected CultureInfo CalendarCulture { get; }
+ protected bool IsLoaded { get; set; }
+
+ public ObservableCollection DayNames { get; } = [];
+ public ObservableCollection ReminderOptions { get; } = [];
+ public ObservableCollection SnoozeOptions { get; } = [];
+ public ObservableCollection NewEventBehaviorOptions { get; } = [];
+ public ObservableCollection AvailableNewEventCalendars { get; } = [];
+ public ObservableCollection TimedDayHeaderFormatPresets { get; } =
+ [
+ "ddd dd",
+ "dddd dd",
+ "ddd d MMM",
+ "dd MMM ddd",
+ "M/d ddd"
+ ];
+
+ protected void LoadReminderOptions()
+ {
+ ReminderOptions.Clear();
+
+ var predefinedMinutes = CalendarService.GetPredefinedReminderMinutes();
+ ReminderOptions.Add("None");
+
+ foreach (var minutes in predefinedMinutes)
+ {
+ var displayText = minutes switch
+ {
+ >= 60 => $"{minutes / 60} Hour{(minutes / 60 > 1 ? "s" : "")}",
+ _ => $"{minutes} Minute{(minutes > 1 ? "s" : "")}"
+ };
+
+ ReminderOptions.Add(displayText);
+ }
+ }
+
+ protected int GetSelectedReminderIndex()
+ {
+ if (PreferencesService.DefaultReminderDurationInSeconds == 0)
+ return 0;
+
+ var minutes = (int)(PreferencesService.DefaultReminderDurationInSeconds / 60);
+ var predefinedMinutes = CalendarService.GetPredefinedReminderMinutes();
+ var index = Array.IndexOf(predefinedMinutes, minutes);
+ return index >= 0 ? index + 1 : 0;
+ }
+
+ protected void SaveReminderIndex(int selectedDefaultReminderIndex)
+ {
+ if (selectedDefaultReminderIndex == 0)
+ {
+ PreferencesService.DefaultReminderDurationInSeconds = 0;
+ return;
+ }
+
+ var predefinedMinutes = CalendarService.GetPredefinedReminderMinutes();
+ var minutes = predefinedMinutes[selectedDefaultReminderIndex - 1];
+ PreferencesService.DefaultReminderDurationInSeconds = minutes * 60;
+ }
+
+ protected void LoadSnoozeOptions()
+ {
+ SnoozeOptions.Clear();
+
+ foreach (var snoozeMinutes in CalendarReminderSnoozeOptions.GetSupportedSnoozeMinutes())
+ {
+ SnoozeOptions.Add(string.Format(Translator.CalendarReminder_SnoozeMinutesOption, snoozeMinutes));
+ }
+ }
+
+ protected int GetSelectedSnoozeIndex()
+ {
+ var supportedSnoozeMinutes = CalendarReminderSnoozeOptions.GetSupportedSnoozeMinutes().ToArray();
+ var selectedIndex = Array.IndexOf(supportedSnoozeMinutes, PreferencesService.DefaultSnoozeDurationInMinutes);
+ return selectedIndex >= 0 ? selectedIndex : 0;
+ }
+
+ protected void SaveSnoozeIndex(int selectedDefaultSnoozeIndex)
+ {
+ var supportedSnoozeMinutes = CalendarReminderSnoozeOptions.GetSupportedSnoozeMinutes();
+ if (supportedSnoozeMinutes.Count == 0)
+ return;
+
+ var selectedIndex = Math.Clamp(selectedDefaultSnoozeIndex, 0, supportedSnoozeMinutes.Count - 1);
+ PreferencesService.DefaultSnoozeDurationInMinutes = supportedSnoozeMinutes[selectedIndex];
+ }
+
+ protected void LoadNewEventBehaviorOptions()
+ {
+ NewEventBehaviorOptions.Clear();
+ NewEventBehaviorOptions.Add(new CalendarNewEventBehaviorOption(NewEventButtonBehavior.AskEachTime, Translator.CalendarSettings_NewEventBehavior_AskEachTime));
+ NewEventBehaviorOptions.Add(new CalendarNewEventBehaviorOption(NewEventButtonBehavior.AlwaysUseSpecificCalendar, Translator.CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar));
+ }
+
+ protected CalendarNewEventBehaviorOption GetSelectedNewEventBehaviorOption()
+ => NewEventBehaviorOptions.FirstOrDefault(option => option.Behavior == PreferencesService.NewEventButtonBehavior)
+ ?? NewEventBehaviorOptions.First();
+
+ protected async void LoadCalendarsAsync(Action applySelection)
+ {
+ var accounts = await AccountService.GetAccountsAsync().ConfigureAwait(false);
+ var calendarsByAccount = new List();
+
+ foreach (var account in accounts)
+ {
+ var calendars = await CalendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false);
+ calendarsByAccount.AddRange(calendars.Select(calendar => new AccountCalendarViewModel(account, calendar)));
+ }
+
+ await ExecuteUIThread(() =>
+ {
+ AvailableNewEventCalendars.Clear();
+
+ foreach (var calendar in calendarsByAccount)
+ {
+ AvailableNewEventCalendars.Add(calendar);
+ }
+
+ applySelection();
+ });
+ }
+
+ protected AccountCalendarViewModel ResolveSelectedNewEventCalendar()
+ {
+ var configuredCalendarId = PreferencesService.DefaultNewEventCalendarId;
+ return configuredCalendarId.HasValue
+ ? AvailableNewEventCalendars.FirstOrDefault(calendar => calendar.Id == configuredCalendarId.Value)
+ : null;
+ }
+
+ protected AccountCalendarViewModel ResolveFallbackNewEventCalendar()
+ => AvailableNewEventCalendars.FirstOrDefault(calendar => calendar.IsPrimary)
+ ?? AvailableNewEventCalendars.FirstOrDefault();
+
+ protected void SaveNewEventBehavior(CalendarNewEventBehaviorOption selectedBehaviorOption, AccountCalendarViewModel selectedCalendar)
+ {
+ var newEventBehavior = selectedBehaviorOption?.Behavior ?? NewEventButtonBehavior.AskEachTime;
+ if (newEventBehavior == NewEventButtonBehavior.AlwaysUseSpecificCalendar && selectedCalendar != null)
+ {
+ PreferencesService.NewEventButtonBehavior = NewEventButtonBehavior.AlwaysUseSpecificCalendar;
+ PreferencesService.DefaultNewEventCalendarId = selectedCalendar.Id;
+ return;
+ }
+
+ PreferencesService.NewEventButtonBehavior = NewEventButtonBehavior.AskEachTime;
+ PreferencesService.DefaultNewEventCalendarId = null;
+ }
+}
+
+public sealed class CalendarNewEventBehaviorOption
+{
+ public CalendarNewEventBehaviorOption(NewEventButtonBehavior behavior, string displayText)
+ {
+ Behavior = behavior;
+ DisplayText = displayText;
+ }
+
+ public NewEventButtonBehavior Behavior { get; }
+ public string DisplayText { get; }
+}
diff --git a/Wino.Calendar.ViewModels/CalendarViewModelContainerSetup.cs b/Wino.Calendar.ViewModels/CalendarViewModelContainerSetup.cs
deleted file mode 100644
index 2f8b452d..00000000
--- a/Wino.Calendar.ViewModels/CalendarViewModelContainerSetup.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using Microsoft.Extensions.DependencyInjection;
-using Wino.Core;
-
-namespace Wino.Calendar.ViewModels;
-
-public static class CalendarViewModelContainerSetup
-{
- public static void RegisterCalendarViewModelServices(this IServiceCollection services)
- {
- services.RegisterCoreServices();
- }
-}
diff --git a/Wino.Calendar.ViewModels/Data/AccountCalendarViewModel.cs b/Wino.Calendar.ViewModels/Data/AccountCalendarViewModel.cs
index df55f72f..1f41f633 100644
--- a/Wino.Calendar.ViewModels/Data/AccountCalendarViewModel.cs
+++ b/Wino.Calendar.ViewModels/Data/AccountCalendarViewModel.cs
@@ -2,6 +2,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Entities.Shared;
+using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
namespace Wino.Calendar.ViewModels.Data;
@@ -54,6 +55,12 @@ public partial class AccountCalendarViewModel : ObservableObject, IAccountCalend
set => SetProperty(AccountCalendar.IsPrimary, value, AccountCalendar, (u, i) => u.IsPrimary = i);
}
+ public bool IsSynchronizationEnabled
+ {
+ get => AccountCalendar.IsSynchronizationEnabled;
+ set => SetProperty(AccountCalendar.IsSynchronizationEnabled, value, AccountCalendar, (u, i) => u.IsSynchronizationEnabled = i);
+ }
+
public Guid AccountId
{
get => AccountCalendar.AccountId;
@@ -65,5 +72,12 @@ public partial class AccountCalendarViewModel : ObservableObject, IAccountCalend
get => AccountCalendar.RemoteCalendarId;
set => SetProperty(AccountCalendar.RemoteCalendarId, value, AccountCalendar, (u, r) => u.RemoteCalendarId = r);
}
+
+ public CalendarItemShowAs DefaultShowAs
+ {
+ get => AccountCalendar.DefaultShowAs;
+ set => SetProperty(AccountCalendar.DefaultShowAs, value, AccountCalendar, (u, s) => u.DefaultShowAs = s);
+ }
public Guid Id { get => ((IAccountCalendar)AccountCalendar).Id; set => ((IAccountCalendar)AccountCalendar).Id = value; }
+ public MailAccount MailAccount { get => MailAccount; set => MailAccount = value; }
}
diff --git a/Wino.Calendar.ViewModels/Data/CalendarAttachmentViewModel.cs b/Wino.Calendar.ViewModels/Data/CalendarAttachmentViewModel.cs
new file mode 100644
index 00000000..3202909c
--- /dev/null
+++ b/Wino.Calendar.ViewModels/Data/CalendarAttachmentViewModel.cs
@@ -0,0 +1,71 @@
+using System;
+using System.IO;
+using CommunityToolkit.Mvvm.ComponentModel;
+using Wino.Core.Domain.Entities.Calendar;
+using Wino.Core.Domain.Enums;
+using Wino.Core.Extensions;
+
+namespace Wino.Calendar.ViewModels.Data;
+
+public partial class CalendarAttachmentViewModel : ObservableObject
+{
+ public CalendarAttachment Attachment { get; }
+
+ public Guid Id => Attachment.Id;
+ public string FileName => Attachment.FileName;
+ public string ReadableSize { get; }
+ public MailAttachmentType AttachmentType { get; }
+ public bool IsDownloaded => Attachment.IsDownloaded;
+
+ [ObservableProperty]
+ public partial bool IsBusy { get; set; }
+
+ public CalendarAttachmentViewModel(CalendarAttachment attachment)
+ {
+ Attachment = attachment;
+ ReadableSize = attachment.Size.GetBytesReadable();
+
+ var extension = Path.GetExtension(FileName);
+ AttachmentType = GetAttachmentType(extension);
+ }
+
+ private MailAttachmentType GetAttachmentType(string extension)
+ {
+ if (string.IsNullOrEmpty(extension))
+ return MailAttachmentType.None;
+
+ switch (extension.ToLower())
+ {
+ case ".exe":
+ return MailAttachmentType.Executable;
+ case ".rar":
+ return MailAttachmentType.RarArchive;
+ case ".zip":
+ return MailAttachmentType.Archive;
+ case ".ogg":
+ case ".mp3":
+ case ".wav":
+ case ".aac":
+ case ".alac":
+ return MailAttachmentType.Audio;
+ case ".mp4":
+ case ".wmv":
+ case ".avi":
+ case ".flv":
+ return MailAttachmentType.Video;
+ case ".pdf":
+ return MailAttachmentType.PDF;
+ case ".htm":
+ case ".html":
+ return MailAttachmentType.HTML;
+ case ".png":
+ case ".jpg":
+ case ".jpeg":
+ case ".gif":
+ case ".jiff":
+ return MailAttachmentType.Image;
+ default:
+ return MailAttachmentType.Other;
+ }
+ }
+}
diff --git a/Wino.Calendar.ViewModels/Data/CalendarComposeAttachmentViewModel.cs b/Wino.Calendar.ViewModels/Data/CalendarComposeAttachmentViewModel.cs
new file mode 100644
index 00000000..34aece53
--- /dev/null
+++ b/Wino.Calendar.ViewModels/Data/CalendarComposeAttachmentViewModel.cs
@@ -0,0 +1,57 @@
+using System;
+using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Models.Calendar;
+using Wino.Core.Extensions;
+
+namespace Wino.Calendar.ViewModels.Data;
+
+public class CalendarComposeAttachmentViewModel
+{
+ public Guid Id { get; } = Guid.NewGuid();
+ public string FileName { get; }
+ public string FilePath { get; }
+ public string FileExtension { get; }
+ public long Size { get; }
+ public string ReadableSize => Size.GetBytesReadable();
+ public MailAttachmentType AttachmentType { get; }
+
+ public CalendarComposeAttachmentViewModel(string fileName, string filePath, string fileExtension, long size)
+ {
+ FileName = fileName;
+ FilePath = filePath;
+ FileExtension = fileExtension;
+ Size = size;
+ AttachmentType = GetAttachmentType(fileExtension);
+ }
+
+ public CalendarEventComposeAttachmentDraft ToDraftModel()
+ {
+ return new CalendarEventComposeAttachmentDraft
+ {
+ Id = Id,
+ FileName = FileName,
+ FilePath = FilePath,
+ FileExtension = FileExtension,
+ Size = Size
+ };
+ }
+
+ private static MailAttachmentType GetAttachmentType(string extension)
+ {
+ if (string.IsNullOrWhiteSpace(extension))
+ return MailAttachmentType.None;
+
+ return extension.ToLowerInvariant() switch
+ {
+ ".exe" => MailAttachmentType.Executable,
+ ".rar" => MailAttachmentType.RarArchive,
+ ".zip" => MailAttachmentType.Archive,
+ ".ogg" or ".mp3" or ".wav" or ".aac" or ".alac" => MailAttachmentType.Audio,
+ ".mp4" or ".wmv" or ".avi" or ".flv" => MailAttachmentType.Video,
+ ".pdf" => MailAttachmentType.PDF,
+ ".htm" or ".html" => MailAttachmentType.HTML,
+ ".png" or ".jpg" or ".jpeg" or ".gif" or ".jiff" => MailAttachmentType.Image,
+ _ => MailAttachmentType.Other
+ };
+ }
+}
diff --git a/Wino.Calendar.ViewModels/Data/CalendarComposeAttendeeViewModel.cs b/Wino.Calendar.ViewModels/Data/CalendarComposeAttendeeViewModel.cs
new file mode 100644
index 00000000..f6231563
--- /dev/null
+++ b/Wino.Calendar.ViewModels/Data/CalendarComposeAttendeeViewModel.cs
@@ -0,0 +1,23 @@
+using Wino.Core.Domain.Entities.Shared;
+
+namespace Wino.Calendar.ViewModels.Data;
+
+public class CalendarComposeAttendeeViewModel : IContactDisplayItem
+{
+ public string DisplayName { get; }
+ public string Email { get; }
+ public AccountContact ResolvedContact { get; }
+ public string Address => Email;
+ public AccountContact PreviewContact => ResolvedContact;
+ public bool HasDistinctDisplayName => !string.IsNullOrWhiteSpace(DisplayName) && !DisplayName.Equals(Email, System.StringComparison.OrdinalIgnoreCase);
+
+ public CalendarComposeAttendeeViewModel(string displayName, string email, AccountContact resolvedContact = null)
+ {
+ DisplayName = string.IsNullOrWhiteSpace(displayName) ? email : displayName;
+ Email = email;
+ ResolvedContact = resolvedContact;
+ }
+
+ public static CalendarComposeAttendeeViewModel FromContact(AccountContact contact)
+ => new(contact.Name, contact.Address, contact);
+}
diff --git a/Wino.Calendar.ViewModels/Data/CalendarItemViewModel.cs b/Wino.Calendar.ViewModels/Data/CalendarItemViewModel.cs
index f402a562..de2416a6 100644
--- a/Wino.Calendar.ViewModels/Data/CalendarItemViewModel.cs
+++ b/Wino.Calendar.ViewModels/Data/CalendarItemViewModel.cs
@@ -2,8 +2,11 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using Itenso.TimePeriod;
+using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Calendar;
+using Wino.Core.Domain.Extensions;
using Wino.Core.Domain.Interfaces;
+using Wino.Core.Domain.Models.Calendar;
namespace Wino.Calendar.ViewModels.Data;
@@ -17,13 +20,52 @@ public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, IC
public IAccountCalendar AssignedCalendar => CalendarItem.AssignedCalendar;
- public DateTime StartDate { get => CalendarItem.StartDate; set => CalendarItem.StartDate = value; }
+ ///
+ /// Gets or sets the start date converted to user's local timezone for display.
+ /// The underlying CalendarItem stores dates according to their timezone.
+ ///
+ public DateTime StartDate
+ {
+ get
+ {
+ // Get start date in user's local timezone
+ return CalendarItem.LocalStartDate;
+ }
+ set
+ {
+ // All-day events use floating dates and should not shift across timezones.
+ CalendarItem.StartDate = CalendarItem.IsAllDayEvent
+ ? value.Date
+ : value.ToTimeZoneFromLocal(CalendarItem.StartTimeZone);
+ }
+ }
- public DateTime EndDate => CalendarItem.EndDate;
+ ///
+ /// Gets the end date converted to user's local timezone for display.
+ /// The underlying CalendarItem stores dates according to their timezone.
+ ///
+ public DateTime EndDate
+ {
+ get
+ {
+ // Get end date in user's local timezone
+ return CalendarItem.LocalEndDate;
+ }
+ }
public double DurationInSeconds { get => CalendarItem.DurationInSeconds; set => CalendarItem.DurationInSeconds = value; }
- public ITimePeriod Period => CalendarItem.Period;
+ ///
+ /// Gets the time period in local time.
+ ///
+ public ITimePeriod Period
+ {
+ get
+ {
+ // Return a period using local times for UI display
+ return new TimeRange(StartDate, EndDate);
+ }
+ }
public bool IsAllDayEvent => CalendarItem.IsAllDayEvent;
public bool IsMultiDayEvent => CalendarItem.IsMultiDayEvent;
@@ -32,7 +74,39 @@ public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, IC
public bool IsRecurringParent => CalendarItem.IsRecurringParent;
[ObservableProperty]
- private bool _isSelected;
+ public partial bool IsSelected { get; set; }
+
+ [ObservableProperty]
+ public partial bool IsBusy { get; set; }
+
+ ///
+ /// The period of the day where this item is currently being displayed.
+ /// Used for multi-day event title formatting.
+ ///
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(DisplayTitle))]
+ public partial ITimePeriod DisplayingPeriod { get; set; }
+
+ ///
+ /// Calendar settings for time formatting.
+ ///
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(DisplayTitle))]
+ public partial CalendarSettings CalendarSettings { get; set; }
+
+ ///
+ /// Gets the display title based on the current displaying period.
+ ///
+ public string DisplayTitle
+ {
+ get
+ {
+ if (DisplayingPeriod == null || CalendarSettings == null)
+ return Title;
+
+ return GetDisplayTitle(DisplayingPeriod, CalendarSettings);
+ }
+ }
public ObservableCollection Attendees { get; } = new ObservableCollection();
@@ -41,5 +115,82 @@ public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, IC
CalendarItem = calendarItem;
}
+ ///
+ /// Updates the underlying CalendarItem with new data and raises property change notifications.
+ ///
+ /// The updated calendar item data.
+ public void UpdateFrom(CalendarItem calendarItem)
+ {
+ if (calendarItem == null || calendarItem.Id != CalendarItem.Id)
+ return;
+
+ // Update all mutable properties
+ CalendarItem.Title = calendarItem.Title;
+ CalendarItem.Description = calendarItem.Description;
+ CalendarItem.Location = calendarItem.Location;
+ CalendarItem.StartDate = calendarItem.StartDate;
+ CalendarItem.StartTimeZone = calendarItem.StartTimeZone;
+ CalendarItem.EndTimeZone = calendarItem.EndTimeZone;
+ CalendarItem.DurationInSeconds = calendarItem.DurationInSeconds;
+ CalendarItem.Recurrence = calendarItem.Recurrence;
+ CalendarItem.RecurringCalendarItemId = calendarItem.RecurringCalendarItemId;
+ CalendarItem.OrganizerDisplayName = calendarItem.OrganizerDisplayName;
+ CalendarItem.OrganizerEmail = calendarItem.OrganizerEmail;
+ CalendarItem.IsLocked = calendarItem.IsLocked;
+ CalendarItem.IsHidden = calendarItem.IsHidden;
+ CalendarItem.CustomEventColorHex = calendarItem.CustomEventColorHex;
+ CalendarItem.HtmlLink = calendarItem.HtmlLink;
+ CalendarItem.Status = calendarItem.Status;
+ CalendarItem.Visibility = calendarItem.Visibility;
+ CalendarItem.ShowAs = calendarItem.ShowAs;
+ CalendarItem.UpdatedAt = calendarItem.UpdatedAt;
+ CalendarItem.AssignedCalendar = calendarItem.AssignedCalendar;
+
+ // Raise property changed for all bindable properties
+ OnPropertyChanged(nameof(Title));
+ OnPropertyChanged(nameof(StartDate));
+ OnPropertyChanged(nameof(EndDate));
+ OnPropertyChanged(nameof(DurationInSeconds));
+ OnPropertyChanged(nameof(Period));
+ OnPropertyChanged(nameof(IsAllDayEvent));
+ OnPropertyChanged(nameof(IsMultiDayEvent));
+ OnPropertyChanged(nameof(IsRecurringEvent));
+ OnPropertyChanged(nameof(IsRecurringChild));
+ OnPropertyChanged(nameof(IsRecurringParent));
+ OnPropertyChanged(nameof(AssignedCalendar));
+ OnPropertyChanged(nameof(DisplayTitle));
+ }
+
+ ///
+ /// Gets the display title for this calendar item when rendered in a specific day.
+ ///
+ public string GetDisplayTitle(ITimePeriod displayingPeriod, CalendarSettings calendarSettings)
+ {
+ if (!IsMultiDayEvent)
+ return Title;
+
+ var periodRelation = Period.GetRelation(displayingPeriod);
+
+ if (periodRelation == PeriodRelation.StartInside || periodRelation == PeriodRelation.EnclosingStartTouching)
+ {
+ // Event starts within this day: "HH:mm -> Title"
+ return $"{calendarSettings.GetTimeString(StartDate.TimeOfDay)} -> {Title}";
+ }
+ else if (periodRelation == PeriodRelation.EndInside || periodRelation == PeriodRelation.EnclosingEndTouching)
+ {
+ // Event ends within this day: "Title <- HH:mm"
+ return $"{Title} <- {calendarSettings.GetTimeString(EndDate.TimeOfDay)}";
+ }
+ else if (periodRelation == PeriodRelation.Enclosing)
+ {
+ // Event spans the entire day
+ return $"{Translator.CalendarItemAllDay} {Title}";
+ }
+ else
+ {
+ return Title;
+ }
+ }
+
public override string ToString() => CalendarItem.Title;
}
diff --git a/Wino.Calendar.ViewModels/Data/GroupedAccountCalendarViewModel.cs b/Wino.Calendar.ViewModels/Data/GroupedAccountCalendarViewModel.cs
index 92b388ba..0994b8c0 100644
--- a/Wino.Calendar.ViewModels/Data/GroupedAccountCalendarViewModel.cs
+++ b/Wino.Calendar.ViewModels/Data/GroupedAccountCalendarViewModel.cs
@@ -20,6 +20,7 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
{
Account = account;
AccountCalendars = new ObservableCollection(calendarViewModels);
+ AccountColorHex = account.AccountColorHex;
ManageIsCheckedState();
@@ -69,10 +70,22 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
}
[ObservableProperty]
- private bool _isExpanded = true;
+ public partial bool IsExpanded { get; set; } = true;
[ObservableProperty]
- private bool? isCheckedState = true;
+ public partial bool? IsCheckedState { get; set; } = true;
+
+ [ObservableProperty]
+ public partial string AccountColorHex { get; set; } = string.Empty;
+
+ [ObservableProperty]
+ public partial bool IsSynchronizationInProgress { get; set; }
+
+ [ObservableProperty]
+ public partial string SynchronizationStatus { get; set; } = string.Empty;
+
+ public bool CanSynchronize => !IsSynchronizationInProgress;
+ public bool IsSynchronizationProgressVisible => IsSynchronizationInProgress;
private bool _isExternalPropChangeBlocked = false;
@@ -98,7 +111,7 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
_isExternalPropChangeBlocked = false;
}
- partial void OnIsCheckedStateChanged(bool? newValue)
+ partial void OnIsCheckedStateChanged(bool? oldValue, bool? newValue)
{
if (_isExternalPropChangeBlocked) return;
@@ -142,4 +155,24 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
CalendarSelectionStateChanged?.Invoke(this, accountCalendarViewModel);
}
+
+ partial void OnIsSynchronizationInProgressChanged(bool value)
+ {
+ OnPropertyChanged(nameof(CanSynchronize));
+ OnPropertyChanged(nameof(IsSynchronizationProgressVisible));
+ }
+
+ public void UpdateAccount(MailAccount updatedAccount)
+ {
+ if (updatedAccount == null || updatedAccount.Id != Account.Id)
+ return;
+
+ Account.Name = updatedAccount.Name;
+ Account.Address = updatedAccount.Address;
+ Account.AccountColorHex = updatedAccount.AccountColorHex;
+ Account.AttentionReason = updatedAccount.AttentionReason;
+ Account.MergedInboxId = updatedAccount.MergedInboxId;
+ AccountColorHex = updatedAccount.AccountColorHex;
+ OnPropertyChanged(nameof(Account));
+ }
}
diff --git a/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs b/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs
index f57d797e..2cc5a0d2 100644
--- a/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs
+++ b/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs
@@ -1,14 +1,23 @@
using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Diagnostics;
+using System.IO;
+using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
+using Serilog;
using Wino.Calendar.ViewModels.Data;
+using Wino.Core.Domain;
+using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar;
+using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.Navigation;
+using Wino.Core.Services;
using Wino.Core.ViewModels;
using Wino.Messaging.Client.Calendar;
@@ -19,43 +28,210 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
private readonly ICalendarService _calendarService;
private readonly INativeAppService _nativeAppService;
private readonly IPreferencesService _preferencesService;
+ private readonly IMailDialogService _dialogService;
+ private readonly IWinoRequestDelegator _winoRequestDelegator;
+ private readonly INavigationService _navigationService;
+ private readonly IUnderlyingThemeService _underlyingThemeService;
+ private readonly INotificationBuilder _notificationBuilder;
+ private readonly IContactService _contactService;
public CalendarSettings CurrentSettings { get; }
+ public INativeAppService NativeAppService => _nativeAppService;
+
+ [ObservableProperty]
+ public partial bool IsDarkWebviewRenderer { get; set; }
+
+ public ObservableCollection Attachments { get; } = new ObservableCollection();
+
+ ///
+ /// Returns true if the current event has attachments.
+ ///
+ public bool HasAttachments => Attachments.Count > 0;
#region Details
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanViewSeries))]
- private CalendarItemViewModel _currentEvent;
+ [NotifyPropertyChangedFor(nameof(CanEditSeries))]
+ [NotifyPropertyChangedFor(nameof(IsCurrentUserOrganizer))]
+ [NotifyPropertyChangedFor(nameof(CurrentRsvpText))]
+ [NotifyPropertyChangedFor(nameof(CurrentRsvpStatus))]
+ public partial CalendarItemViewModel CurrentEvent { get; set; }
+
+ partial void OnCurrentEventChanged(CalendarItemViewModel value)
+ {
+ // Notify the view to re-render the description
+ Messenger.Send(new CalendarDescriptionRenderingRequested());
+ }
[ObservableProperty]
- private CalendarItemViewModel _seriesParent;
+ public partial CalendarItemViewModel SeriesParent { get; set; }
+ [ObservableProperty]
+ public partial List Reminders { get; set; }
+ public ObservableCollection ReminderOptions { get; } = new ObservableCollection();
+
+ ///
+ /// Returns true if the event is part of a recurring series (as a child occurrence).
+ /// Used to enable "View Series" functionality.
+ ///
public bool CanViewSeries => CurrentEvent?.IsRecurringChild ?? false;
+ ///
+ /// Returns true if the "Edit Series" button should be visible.
+ /// Only visible for child occurrences of recurring events, not for master events or single events.
+ ///
+ public bool CanEditSeries => CurrentEvent?.IsRecurringChild ?? false;
+
+ ///
+ /// Returns true if the current user is the organizer of the event.
+ /// Used to determine if the user can invite attendees or modify the event.
+ ///
+ public bool IsCurrentUserOrganizer => CurrentEvent?.Attendees?.Any(a => a.IsOrganizer) ?? true;
+
#endregion
- public EventDetailsPageViewModel(ICalendarService calendarService, INativeAppService nativeAppService, IPreferencesService preferencesService)
+ #region Show As Options
+
+ public ObservableCollection ShowAsOptions { get; } = new ObservableCollection();
+
+ [ObservableProperty]
+ public partial ShowAsOption SelectedShowAsOption { get; set; }
+
+ #endregion
+
+ #region RSVP Panel
+
+ [ObservableProperty]
+ public partial bool IsRsvpPanelVisible { get; set; }
+
+ public bool IncludeRsvpMessage => !string.IsNullOrEmpty(RsvpMessage);
+
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(IncludeRsvpMessage))]
+ public partial string RsvpMessage { get; set; } = string.Empty;
+
+ public ObservableCollection RsvpStatusOptions { get; } = new ObservableCollection();
+
+ public CalendarItemStatus CurrentRsvpStatus
+ {
+ get
+ {
+ return CurrentEvent?.CalendarItem?.Status ?? CalendarItemStatus.NotResponded;
+ }
+ }
+
+ public string CurrentRsvpText
+ {
+ get
+ {
+ if (CurrentEvent?.CalendarItem == null) return Translator.CalendarEventResponse_Accept;
+
+ return CurrentEvent.CalendarItem.Status switch
+ {
+ CalendarItemStatus.Accepted => Translator.CalendarEventResponse_AcceptedResponse,
+ CalendarItemStatus.Tentative => Translator.CalendarEventResponse_TentativeResponse,
+ CalendarItemStatus.Cancelled => Translator.CalendarEventResponse_DeclinedResponse,
+ CalendarItemStatus.NotResponded => Translator.CalendarEventResponse_NotResponded,
+ _ => Translator.CalendarEventResponse_NotResponded
+ };
+ }
+ }
+
+ #endregion
+
+ public EventDetailsPageViewModel(ICalendarService calendarService,
+ INativeAppService nativeAppService,
+ IPreferencesService preferencesService,
+ IMailDialogService dialogService,
+ IWinoRequestDelegator winoRequestDelegator,
+ INavigationService navigationService,
+ INotificationBuilder notificationBuilder,
+ IUnderlyingThemeService underlyingThemeService,
+ IContactService contactService)
{
_calendarService = calendarService;
_nativeAppService = nativeAppService;
_preferencesService = preferencesService;
+ _dialogService = dialogService;
+ _winoRequestDelegator = winoRequestDelegator;
+ _navigationService = navigationService;
+ _underlyingThemeService = underlyingThemeService;
+ _notificationBuilder = notificationBuilder;
+ _contactService = contactService;
CurrentSettings = _preferencesService.GetCurrentCalendarSettings();
+ IsDarkWebviewRenderer = _underlyingThemeService.IsUnderlyingThemeDark();
+
+ // Initialize Show As options
+ ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Free));
+ ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Tentative));
+ ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Busy));
+ ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.OutOfOffice));
+ ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.WorkingElsewhere));
+ SelectedShowAsOption = ShowAsOptions[2]; // Default to Busy
+
+ // Initialize RSVP status options
+ RsvpStatusOptions.Add(new RsvpStatusOption(CalendarItemStatus.Accepted));
+ RsvpStatusOptions.Add(new RsvpStatusOption(CalendarItemStatus.Tentative));
+ RsvpStatusOptions.Add(new RsvpStatusOption(CalendarItemStatus.Cancelled));
}
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
{
base.OnNavigatedTo(mode, parameters);
- Messenger.Send(new DetailsPageStateChangedMessage(true));
-
if (parameters == null || parameters is not CalendarItemTarget args)
return;
await LoadCalendarItemTargetAsync(args);
}
+ protected override async void OnCalendarItemUpdated(CalendarItem calendarItem, CalendarItemUpdateSource source)
+ {
+ base.OnCalendarItemUpdated(calendarItem, source);
+
+ // If the current event was updated, reload it
+ if (CurrentEvent?.CalendarItem?.Id == calendarItem.Id || CurrentEvent?.CalendarItem.RecurringCalendarItemId == calendarItem.Id)
+ {
+ // Reflect client-side optimistic changes immediately; fallback to DB for server updates.
+ if (source == CalendarItemUpdateSource.ClientUpdated || source == CalendarItemUpdateSource.ClientReverted)
+ {
+ var previousAttendees = CurrentEvent?.Attendees?.ToList() ?? [];
+ CurrentEvent = new CalendarItemViewModel(calendarItem)
+ {
+ IsBusy = source == CalendarItemUpdateSource.ClientUpdated
+ };
+
+ foreach (var attendee in previousAttendees)
+ {
+ CurrentEvent.Attendees.Add(attendee);
+ }
+
+ return;
+ }
+
+ // Refresh from DB when update comes from server sync.
+ var refreshedEvent = await _calendarService.GetCalendarItemAsync(calendarItem.Id);
+ if (refreshedEvent != null)
+ {
+ CurrentEvent = new CalendarItemViewModel(refreshedEvent);
+ await LoadAttendeesAsync(refreshedEvent.Id, refreshedEvent);
+ }
+ }
+ }
+
+ protected override void OnCalendarItemDeleted(CalendarItem calendarItem)
+ {
+ base.OnCalendarItemDeleted(calendarItem);
+
+ // If the current event was deleted, navigate back
+ if (CurrentEvent?.CalendarItem?.Id == calendarItem.Id || CurrentEvent?.CalendarItem.RecurringCalendarItemId == calendarItem.Id)
+ {
+ NavigateBackToCalendar(forceReload: true);
+ }
+ }
+
private async Task LoadCalendarItemTargetAsync(CalendarItemTarget target)
{
try
@@ -67,12 +243,17 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
CurrentEvent = new CalendarItemViewModel(currentEventItem);
- var attendees = await _calendarService.GetAttendeesAsync(currentEventItem.EventTrackingId);
+ await LoadAttendeesAsync(currentEventItem.Id, currentEventItem);
- foreach (var item in attendees)
- {
- CurrentEvent.Attendees.Add(item);
- }
+ // Initialize SelectedShowAsOption based on current event's ShowAs
+ SelectedShowAsOption = ShowAsOptions.FirstOrDefault(o => o.ShowAs == currentEventItem.ShowAs) ?? ShowAsOptions[2];
+
+ // Load reminders for this calendar item
+ Reminders = await _calendarService.GetRemindersAsync(currentEventItem.Id);
+ InitializeReminderOptions();
+
+ // Load attachments
+ await LoadAttachmentsAsync(currentEventItem.Id);
}
catch (Exception ex)
{
@@ -80,36 +261,572 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
}
}
- public override void OnNavigatedFrom(NavigationMode mode, object parameters)
+ private async Task LoadAttendeesAsync(Guid calendarItemId, CalendarItem calendarItem)
{
- base.OnNavigatedFrom(mode, parameters);
+ var attendees = await _calendarService.GetAttendeesAsync(calendarItemId);
- Messenger.Send(new DetailsPageStateChangedMessage(false));
+ // Resolve contacts for all attendees in a single batch DB query.
+ var emails = attendees
+ .Where(a => !string.IsNullOrEmpty(a.Email))
+ .Select(a => a.Email)
+ .ToList();
+
+ if (!string.IsNullOrEmpty(calendarItem.OrganizerEmail))
+ emails.Add(calendarItem.OrganizerEmail);
+
+ var contacts = await _contactService.GetContactsByAddressesAsync(emails).ConfigureAwait(false);
+ var contactLookup = contacts.ToDictionary(c => c.Address, StringComparer.OrdinalIgnoreCase);
+
+ foreach (var attendee in attendees)
+ {
+ if (!string.IsNullOrEmpty(attendee.Email) && contactLookup.TryGetValue(attendee.Email, out var contact))
+ attendee.ResolvedContact = contact;
+ }
+
+ // Separate organizer from other attendees to ensure organizer is always first
+ var organizer = attendees.FirstOrDefault(a => a.IsOrganizer);
+ var nonOrganizerAttendees = attendees.Where(a => !a.IsOrganizer).ToList();
+
+ var attendeesForUi = new List();
+
+ // If the organizer is in the list, add them first
+ if (organizer != null)
+ {
+ attendeesForUi.Add(organizer);
+ }
+ else if (!string.IsNullOrEmpty(calendarItem.OrganizerEmail))
+ {
+ // If the organizer is not in the attendees list, create and add them first
+ var organizerAttendee = new CalendarEventAttendee
+ {
+ Id = Guid.NewGuid(),
+ CalendarItemId = calendarItem.Id,
+ Name = calendarItem.OrganizerDisplayName ?? calendarItem.OrganizerEmail,
+ Email = calendarItem.OrganizerEmail,
+ IsOrganizer = true,
+ AttendenceStatus = AttendeeStatus.Accepted
+ };
+
+ if (contactLookup.TryGetValue(calendarItem.OrganizerEmail, out var organizerContact))
+ organizerAttendee.ResolvedContact = organizerContact;
+
+ attendeesForUi.Add(organizerAttendee);
+ }
+
+ // Add all other attendees after the organizer
+ foreach (var item in nonOrganizerAttendees)
+ {
+ attendeesForUi.Add(item);
+ }
+
+ await ExecuteUIThread(() =>
+ {
+ if (CurrentEvent == null)
+ return;
+
+ CurrentEvent.Attendees.Clear();
+
+ foreach (var attendee in attendeesForUi)
+ {
+ CurrentEvent.Attendees.Add(attendee);
+ }
+ });
}
+ private async Task LoadAttachmentsAsync(Guid calendarItemId)
+ {
+ Attachments.Clear();
+
+ try
+ {
+ var attachments = await _calendarService.GetAttachmentsAsync(calendarItemId);
+
+ foreach (var attachment in attachments)
+ {
+ Attachments.Add(new CalendarAttachmentViewModel(attachment));
+ }
+
+ OnPropertyChanged(nameof(HasAttachments));
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Error loading attachments: {ex.Message}");
+ }
+ }
+
+ private void InitializeReminderOptions()
+ {
+ ReminderOptions.Clear();
+
+ // Add predefined options from service
+ var predefinedMinutes = _calendarService.GetPredefinedReminderMinutes();
+ var predefinedOptions = predefinedMinutes.Select(m => new ReminderOption(m)).ToList();
+
+ // Add custom reminders from synced data
+ if (Reminders != null)
+ {
+ foreach (var reminder in Reminders)
+ {
+ // Convert seconds to minutes
+ var minutesDiff = (int)(reminder.DurationInSeconds / 60);
+
+ // Check if this is a custom value not in predefined list
+ if (!predefinedMinutes.Contains(minutesDiff))
+ {
+ predefinedOptions.Add(new ReminderOption(minutesDiff, isCustom: true));
+ }
+ }
+ }
+
+ // Sort by minutes descending and add to collection
+ foreach (var option in predefinedOptions.OrderByDescending(o => o.Minutes))
+ {
+ ReminderOptions.Add(option);
+ }
+
+ // Set selected state based on current reminders
+ if (Reminders != null)
+ {
+ foreach (var reminder in Reminders)
+ {
+ // Convert seconds to minutes
+ var minutesDiff = (int)(reminder.DurationInSeconds / 60);
+
+ var matchingOption = ReminderOptions.FirstOrDefault(o => o.Minutes == minutesDiff);
+ matchingOption?.IsSelected = true;
+ }
+ }
+ }
+
+
[RelayCommand]
private async Task SaveAsync()
{
+ if (CurrentEvent == null) return;
+ try
+ {
+ // Capture original state BEFORE making any changes for potential revert
+ var originalItem = await _calendarService.GetCalendarItemAsync(CurrentEvent.CalendarItem.Id);
+ var originalAttendees = await _calendarService.GetAttendeesAsync(CurrentEvent.CalendarItem.Id);
+
+ // Get selected reminder options
+ var selectedOptions = ReminderOptions.Where(o => o.IsSelected).ToList();
+
+ // Create separate Reminder entities for each selected option
+ var newReminders = new List();
+
+ foreach (var option in selectedOptions)
+ {
+ var durationInSeconds = option.Minutes * 60; // Convert minutes to seconds
+
+ newReminders.Add(new Reminder
+ {
+ Id = Guid.NewGuid(),
+ CalendarItemId = CurrentEvent.Id,
+ DurationInSeconds = durationInSeconds,
+ ReminderType = CalendarItemReminderType.Popup
+ });
+ }
+
+ // Save reminders to database
+ await _calendarService.SaveRemindersAsync(CurrentEvent.CalendarItem.Id, newReminders);
+ Reminders = newReminders;
+
+ // Update ShowAs if changed
+ if (SelectedShowAsOption != null)
+ {
+ CurrentEvent.CalendarItem.ShowAs = SelectedShowAsOption.ShowAs;
+ }
+
+ // Update the calendar item and attendees in database
+ await _calendarService.UpdateCalendarItemAsync(CurrentEvent.CalendarItem, CurrentEvent.Attendees.ToList());
+
+ // Queue the update request to synchronizer with original state for revert capability
+ var preparationRequest = new CalendarOperationPreparationRequest(
+ CalendarSynchronizerOperation.UpdateEvent,
+ CurrentEvent.CalendarItem,
+ CurrentEvent.Attendees.ToList(),
+ ResponseMessage: null,
+ OriginalItem: originalItem,
+ OriginalAttendees: originalAttendees);
+
+ await _winoRequestDelegator.ExecuteAsync(preparationRequest);
+
+ NavigateBackToCalendar(forceReload: true);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Error saving event: {ex.Message}");
+ _dialogService.InfoBarMessage(
+ Translator.Info_AttachmentSaveFailedTitle,
+ ex.Message,
+ InfoBarMessageType.Error);
+ }
}
[RelayCommand]
private async Task DeleteAsync()
{
+ if (CurrentEvent == null) return;
+ // If the event is a master recurring event, ask for confirmation
+ if (CurrentEvent.IsRecurringParent)
+ {
+ var confirmed = await _dialogService.ShowConfirmationDialogAsync(
+ Translator.DialogMessage_DeleteRecurringSeriesMessage,
+ Translator.DialogMessage_DeleteRecurringSeriesTitle,
+ Translator.Buttons_Delete);
+
+ if (!confirmed) return;
+ }
+
+ try
+ {
+ var preparationRequest = new CalendarOperationPreparationRequest(
+ CalendarSynchronizerOperation.DeleteEvent,
+ CurrentEvent.CalendarItem,
+ null);
+
+ await _winoRequestDelegator.ExecuteAsync(preparationRequest);
+
+ NavigateBackToCalendar(forceReload: true);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Error deleting calendar event: {ex.Message}");
+ }
+ }
+
+ private void NavigateBackToCalendar(bool forceReload)
+ {
+ var navigationDate = CurrentEvent?.CalendarItem.LocalStartDate ?? DateTime.Now;
+
+ _navigationService.Navigate(
+ WinoPage.CalendarPage,
+ new CalendarPageNavigationArgs
+ {
+ NavigationDate = navigationDate,
+ ForceReload = forceReload
+ });
+ }
+
+ public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args)
+ {
+ if (args.Handled || args.Mode != WinoApplicationMode.Calendar || args.Action != KeyboardShortcutAction.Delete)
+ return;
+
+ await DeleteAsync();
+ args.Handled = true;
}
[RelayCommand]
- private Task JoinOnline()
+ private Task JoinOnlineAsync()
{
- if (CurrentEvent == null || string.IsNullOrEmpty(CurrentEvent.CalendarItem.HtmlLink)) return Task.CompletedTask;
+ if (CurrentEvent == null || string.IsNullOrEmpty(CurrentEvent.CalendarItem.HtmlLink))
+ return Task.CompletedTask;
return _nativeAppService.LaunchUriAsync(new Uri(CurrentEvent.CalendarItem.HtmlLink));
}
[RelayCommand]
- private async Task Respond(CalendarItemStatus status)
+ private Task CreateTestNotificationAsync()
+ {
+ if (CurrentEvent?.CalendarItem == null)
+ return Task.CompletedTask;
+
+ var reminderDurationInSeconds = Reminders?
+ .Where(x => x.DurationInSeconds > 0)
+ .OrderByDescending(x => x.DurationInSeconds)
+ .Select(x => x.DurationInSeconds)
+ .FirstOrDefault() ?? 0;
+
+ if (reminderDurationInSeconds <= 0)
+ reminderDurationInSeconds = Math.Max(_preferencesService.DefaultReminderDurationInSeconds, 30 * 60);
+
+ return _notificationBuilder.CreateCalendarReminderNotificationAsync(CurrentEvent.CalendarItem, reminderDurationInSeconds);
+ }
+
+ [RelayCommand]
+ private void ToggleRsvpPanel()
+ {
+ IsRsvpPanelVisible = !IsRsvpPanelVisible;
+
+ if (IsRsvpPanelVisible && CurrentEvent?.CalendarItem != null)
+ {
+ // Initialize selection based on current status
+ foreach (var item in RsvpStatusOptions)
+ {
+ item.IsSelected = CurrentEvent?.CalendarItem?.Status == item.Status;
+ }
+ }
+ }
+
+ [RelayCommand]
+ private void CloseRsvpPanel()
+ {
+ IsRsvpPanelVisible = false;
+ RsvpMessage = string.Empty;
+ }
+
+ [RelayCommand]
+ private async Task SendRsvpResponse(AttendeeStatus status)
{
if (CurrentEvent == null) return;
+
+ try
+ {
+ // Get the optional response message if user wants to include it
+ var responseMessage = IncludeRsvpMessage ? RsvpMessage : null;
+
+ // Map status to operation
+ CalendarSynchronizerOperation operation = status switch
+ {
+ AttendeeStatus.Accepted => CalendarSynchronizerOperation.AcceptEvent,
+ AttendeeStatus.Tentative => CalendarSynchronizerOperation.TentativeEvent,
+ AttendeeStatus.Declined => CalendarSynchronizerOperation.DeclineEvent,
+ _ => throw new InvalidOperationException($"Invalid RSVP status: {status}")
+ };
+
+ // Create preparation request with the optional message
+ var preparationRequest = new CalendarOperationPreparationRequest(
+ operation,
+ CurrentEvent.CalendarItem,
+ null,
+ responseMessage);
+
+ await _winoRequestDelegator.ExecuteAsync(preparationRequest);
+
+ // Reload attendees to get the updated status from the server
+ await LoadAttendeesAsync(CurrentEvent.CalendarItem.Id, CurrentEvent.CalendarItem);
+
+ OnPropertyChanged(nameof(CurrentRsvpText));
+ OnPropertyChanged(nameof(CurrentRsvpStatus));
+
+ CloseRsvpPanel();
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Error sending RSVP response: {ex.Message}");
+ _dialogService.InfoBarMessage(
+ Translator.Info_AttachmentSaveFailedTitle,
+ ex.Message,
+ InfoBarMessageType.Error);
+ }
+ }
+
+ [RelayCommand]
+ private async Task ViewSeriesAsync()
+ {
+ if (CurrentEvent == null || !CurrentEvent.IsRecurringChild) return;
+
+ try
+ {
+ // Get the master event from the recurring series
+ var masterEventId = CurrentEvent.CalendarItem.RecurringCalendarItemId.Value;
+ var masterEvent = await _calendarService.GetCalendarItemAsync(masterEventId);
+
+ if (masterEvent == null) return;
+
+ // Load the master event without navigation
+ var target = new CalendarItemTarget(masterEvent, CalendarEventTargetType.Series);
+ await LoadCalendarItemTargetAsync(target);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Error loading series: {ex.Message}");
+ }
+ }
+
+ [RelayCommand]
+ private async Task OpenAttachmentAsync(CalendarAttachmentViewModel attachmentViewModel)
+ {
+ if (attachmentViewModel == null || CurrentEvent?.CalendarItem == null) return;
+
+ try
+ {
+ attachmentViewModel.IsBusy = true;
+
+ // If not downloaded, download it first
+ if (!attachmentViewModel.IsDownloaded)
+ {
+ await DownloadAttachmentAsync(attachmentViewModel);
+ }
+
+ // Launch the file
+ if (!string.IsNullOrEmpty(attachmentViewModel.Attachment.LocalFilePath) &&
+ File.Exists(attachmentViewModel.Attachment.LocalFilePath))
+ {
+ await _nativeAppService.LaunchFileAsync(attachmentViewModel.Attachment.LocalFilePath);
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Failed to open calendar attachment.");
+ _dialogService.InfoBarMessage(
+ Translator.Info_AttachmentOpenFailedTitle,
+ Translator.Info_AttachmentOpenFailedMessage,
+ InfoBarMessageType.Error);
+ }
+ finally
+ {
+ attachmentViewModel.IsBusy = false;
+ }
+ }
+
+ [RelayCommand]
+ private async Task SaveAttachmentAsync(CalendarAttachmentViewModel attachmentViewModel)
+ {
+ if (attachmentViewModel == null) return;
+
+ try
+ {
+ attachmentViewModel.IsBusy = true;
+
+ var pickedPath = await _dialogService.PickWindowsFolderAsync();
+ if (string.IsNullOrEmpty(pickedPath)) return;
+
+ // Download if not already downloaded
+ if (!attachmentViewModel.IsDownloaded)
+ {
+ await DownloadAttachmentAsync(attachmentViewModel);
+ }
+
+ // Copy to selected location
+ if (!string.IsNullOrEmpty(attachmentViewModel.Attachment.LocalFilePath) &&
+ File.Exists(attachmentViewModel.Attachment.LocalFilePath))
+ {
+ var destinationPath = Path.Combine(pickedPath, attachmentViewModel.FileName);
+ File.Copy(attachmentViewModel.Attachment.LocalFilePath, destinationPath, overwrite: true);
+
+ _dialogService.InfoBarMessage(
+ Translator.Info_AttachmentSaveSuccessTitle,
+ Translator.Info_AttachmentSaveSuccessMessage,
+ InfoBarMessageType.Success);
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Failed to save calendar attachment.");
+ _dialogService.InfoBarMessage(
+ Translator.Info_AttachmentSaveFailedTitle,
+ Translator.Info_AttachmentSaveFailedMessage,
+ InfoBarMessageType.Error);
+ }
+ finally
+ {
+ attachmentViewModel.IsBusy = false;
+ }
+ }
+
+ private async Task DownloadAttachmentAsync(CalendarAttachmentViewModel attachmentViewModel)
+ {
+ if (CurrentEvent?.CalendarItem == null) return;
+
+ // Create attachments folder for this calendar item
+ var attachmentsFolder = Path.Combine(
+ _nativeAppService.GetCalendarAttachmentsFolderPath(),
+ CurrentEvent.CalendarItem.Id.ToString());
+
+ Directory.CreateDirectory(attachmentsFolder);
+
+ var localFilePath = Path.Combine(attachmentsFolder, attachmentViewModel.FileName);
+
+ // Download attachment using synchronizer
+ await SynchronizationManager.Instance.DownloadCalendarAttachmentAsync(
+ CurrentEvent.CalendarItem,
+ attachmentViewModel.Attachment,
+ localFilePath);
+
+ // Mark as downloaded
+ await _calendarService.MarkAttachmentDownloadedAsync(
+ attachmentViewModel.Id,
+ localFilePath);
+
+ // Update view model
+ attachmentViewModel.Attachment.IsDownloaded = true;
+ attachmentViewModel.Attachment.LocalFilePath = localFilePath;
+ OnPropertyChanged(nameof(attachmentViewModel.IsDownloaded));
+ }
+}
+
+public partial class ReminderOption : ObservableObject
+{
+ public int Minutes { get; }
+ public bool IsCustom { get; }
+
+ [ObservableProperty]
+ public partial bool IsSelected { get; set; }
+
+ public string DisplayText
+ {
+ get
+ {
+ if (Minutes >= 60)
+ {
+ var hours = Minutes / 60;
+ return hours == 1 ? "1 Hour" : $"{hours} Hours";
+ }
+ return Minutes == 1 ? "1 Minute" : $"{Minutes} Minutes";
+ }
+ }
+
+ public ReminderOption(int minutes, bool isCustom = false)
+ {
+ Minutes = minutes;
+ IsCustom = isCustom;
+ }
+}
+
+public partial class ShowAsOption : ObservableObject
+{
+ public CalendarItemShowAs ShowAs { get; }
+
+ public string DisplayText
+ {
+ get
+ {
+ return ShowAs switch
+ {
+ CalendarItemShowAs.Free => Translator.CalendarShowAs_Free,
+ CalendarItemShowAs.Tentative => Translator.CalendarShowAs_Tentative,
+ CalendarItemShowAs.Busy => Translator.CalendarShowAs_Busy,
+ CalendarItemShowAs.OutOfOffice => Translator.CalendarShowAs_OutOfOffice,
+ CalendarItemShowAs.WorkingElsewhere => Translator.CalendarShowAs_WorkingElsewhere,
+ _ => Translator.CalendarShowAs_Busy
+ };
+ }
+ }
+
+ public ShowAsOption(CalendarItemShowAs showAs)
+ {
+ ShowAs = showAs;
+ }
+}
+
+public partial class RsvpStatusOption : ObservableObject
+{
+ public CalendarItemStatus Status { get; }
+
+ public string StatusText
+ {
+ get
+ {
+ return Status switch
+ {
+ CalendarItemStatus.Accepted => Translator.CalendarEventResponse_Accept,
+ CalendarItemStatus.Tentative => Translator.CalendarEventResponse_Tentative,
+ CalendarItemStatus.Cancelled => Translator.CalendarEventResponse_Decline,
+ _ => Translator.CalendarEventResponse_Accept
+ };
+ }
+ }
+
+ [ObservableProperty]
+ public partial bool IsSelected { get; set; }
+
+ public RsvpStatusOption(CalendarItemStatus status)
+ {
+ Status = status;
}
}
diff --git a/Wino.Calendar.ViewModels/Interfaces/IAccountCalendarStateService.cs b/Wino.Calendar.ViewModels/Interfaces/IAccountCalendarStateService.cs
index 9aaf17a4..d639040d 100644
--- a/Wino.Calendar.ViewModels/Interfaces/IAccountCalendarStateService.cs
+++ b/Wino.Calendar.ViewModels/Interfaces/IAccountCalendarStateService.cs
@@ -2,14 +2,16 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
-using System.Linq;
+using CommunityToolkit.Mvvm.Collections;
using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain.Entities.Shared;
+using Wino.Core.Domain.Interfaces;
namespace Wino.Calendar.ViewModels.Interfaces;
public interface IAccountCalendarStateService : INotifyPropertyChanged
{
+ IDispatcher Dispatcher { get; set; }
ReadOnlyObservableCollection GroupedAccountCalendars { get; }
event EventHandler CollectiveAccountGroupSelectionStateChanged;
@@ -17,7 +19,7 @@ public interface IAccountCalendarStateService : INotifyPropertyChanged
public void AddGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar);
public void RemoveGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar);
- public void ClearGroupedAccountCalendar();
+ public void ClearGroupedAccountCalendars();
public void AddAccountCalendar(AccountCalendarViewModel accountCalendar);
public void RemoveAccountCalendar(AccountCalendarViewModel accountCalendar);
@@ -26,5 +28,7 @@ public interface IAccountCalendarStateService : INotifyPropertyChanged
/// Enumeration of currently selected calendars.
///
IEnumerable ActiveCalendars { get; }
- IEnumerable> GroupedAccountCalendarsEnumerable { get; }
+ IEnumerable AllCalendars { get; }
+ bool IsAnySynchronizationInProgress { get; }
+ ReadOnlyObservableGroupedCollection GroupedCalendars { get; set; }
}
diff --git a/Wino.Calendar.ViewModels/Messages/CalendarItemTappedMessage.cs b/Wino.Calendar.ViewModels/Messages/CalendarItemTappedMessage.cs
index 05f2d99c..8419eaba 100644
--- a/Wino.Calendar.ViewModels/Messages/CalendarItemTappedMessage.cs
+++ b/Wino.Calendar.ViewModels/Messages/CalendarItemTappedMessage.cs
@@ -1,16 +1,12 @@
using Wino.Calendar.ViewModels.Data;
-using Wino.Core.Domain.Models.Calendar;
-
namespace Wino.Calendar.ViewModels.Messages;
public class CalendarItemTappedMessage
{
- public CalendarItemTappedMessage(CalendarItemViewModel calendarItemViewModel, CalendarDayModel clickedPeriod)
+ public CalendarItemTappedMessage(CalendarItemViewModel calendarItemViewModel)
{
CalendarItemViewModel = calendarItemViewModel;
- ClickedPeriod = clickedPeriod;
}
public CalendarItemViewModel CalendarItemViewModel { get; }
- public CalendarDayModel ClickedPeriod { get; }
}
diff --git a/Wino.Calendar.ViewModels/Wino.Calendar.ViewModels.csproj b/Wino.Calendar.ViewModels/Wino.Calendar.ViewModels.csproj
index e0e88340..b51d389c 100644
--- a/Wino.Calendar.ViewModels/Wino.Calendar.ViewModels.csproj
+++ b/Wino.Calendar.ViewModels/Wino.Calendar.ViewModels.csproj
@@ -1,13 +1,17 @@
- net9.0
+ net10.0
x86;x64;arm64
win-x86;win-x64;win-arm64
true
true
+ true
+ true
+ true
-
+
+
diff --git a/Wino.Calendar/Activation/DefaultActivationHandler.cs b/Wino.Calendar/Activation/DefaultActivationHandler.cs
deleted file mode 100644
index e2d1b8f7..00000000
--- a/Wino.Calendar/Activation/DefaultActivationHandler.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System.Threading.Tasks;
-using Windows.ApplicationModel.Activation;
-using Windows.UI.Xaml;
-using Windows.UI.Xaml.Controls;
-using Windows.UI.Xaml.Media.Animation;
-using Wino.Activation;
-using Wino.Calendar.Views;
-
-namespace Wino.Calendar.Activation;
-
-public class DefaultActivationHandler : ActivationHandler
-{
- protected override Task HandleInternalAsync(IActivatedEventArgs args)
- {
- (Window.Current.Content as Frame).Navigate(typeof(AppShell), null, new DrillInNavigationTransitionInfo());
-
- return Task.CompletedTask;
- }
-
- // Only navigate if Frame content doesn't exist.
- protected override bool CanHandleInternal(IActivatedEventArgs args)
- => (Window.Current?.Content as Frame)?.Content == null;
-}
diff --git a/Wino.Calendar/App.xaml b/Wino.Calendar/App.xaml
deleted file mode 100644
index 1b0db3ef..00000000
--- a/Wino.Calendar/App.xaml
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Wino.Calendar/App.xaml.cs b/Wino.Calendar/App.xaml.cs
deleted file mode 100644
index 0ae35172..00000000
--- a/Wino.Calendar/App.xaml.cs
+++ /dev/null
@@ -1,159 +0,0 @@
-using System;
-using System.Collections.Generic;
-using CommunityToolkit.Mvvm.Messaging;
-using Microsoft.Extensions.DependencyInjection;
-using Serilog;
-using Windows.ApplicationModel;
-using Windows.ApplicationModel.Activation;
-using Windows.ApplicationModel.AppService;
-using Windows.ApplicationModel.Background;
-using Windows.UI.Core.Preview;
-using Wino.Activation;
-using Wino.Calendar.Activation;
-using Wino.Calendar.Services;
-using Wino.Calendar.ViewModels;
-using Wino.Calendar.ViewModels.Interfaces;
-using Wino.Core.Domain;
-using Wino.Core.Domain.Enums;
-using Wino.Core.Domain.Exceptions;
-using Wino.Core.Domain.Interfaces;
-using Wino.Core.Domain.Models.Synchronization;
-using Wino.Core.UWP;
-using Wino.Core.ViewModels;
-using Wino.Messaging.Client.Connection;
-using Wino.Messaging.Server;
-using Wino.Services;
-
-namespace Wino.Calendar;
-
-public sealed partial class App : WinoApplication, IRecipient
-{
- private BackgroundTaskDeferral connectionBackgroundTaskDeferral;
-
- public App()
- {
- InitializeComponent();
- WeakReferenceMessenger.Default.Register(this);
- }
-
- public override IServiceProvider ConfigureServices()
- {
- var services = new ServiceCollection();
-
- services.RegisterSharedServices();
- services.RegisterCalendarViewModelServices();
- services.RegisterCoreUWPServices();
- services.RegisterCoreViewModels();
-
- RegisterUWPServices(services);
- RegisterViewModels(services);
- RegisterActivationHandlers(services);
-
- return services.BuildServiceProvider();
- }
-
- #region Dependency Injection
-
- private void RegisterActivationHandlers(IServiceCollection services)
- {
- //services.AddTransient();
- //services.AddTransient();
- //services.AddTransient();
- }
-
- private void RegisterUWPServices(IServiceCollection services)
- {
- services.AddSingleton();
- services.AddSingleton();
- services.AddTransient();
- services.AddTransient();
- services.AddSingleton();
- services.AddSingleton();
- }
-
- private void RegisterViewModels(IServiceCollection services)
- {
- services.AddSingleton(typeof(AppShellViewModel));
- services.AddSingleton(typeof(CalendarPageViewModel));
- services.AddTransient(typeof(CalendarSettingsPageViewModel));
- services.AddTransient(typeof(AccountManagementViewModel));
- services.AddTransient(typeof(PersonalizationPageViewModel));
- services.AddTransient(typeof(AccountDetailsPageViewModel));
- services.AddTransient(typeof(EventDetailsPageViewModel));
- }
-
- #endregion
-
- protected override void OnApplicationCloseRequested(object sender, SystemNavigationCloseRequestedPreviewEventArgs e)
- {
- // TODO: Check server running.
- }
-
- protected override async void OnLaunched(LaunchActivatedEventArgs args)
- {
- LogActivation($"OnLaunched -> {args.GetType().Name}, Kind -> {args.Kind}, PreviousExecutionState -> {args.PreviousExecutionState}, IsPrelaunch -> {args.PrelaunchActivated}");
-
- if (!args.PrelaunchActivated)
- {
- await ActivateWinoAsync(args);
- }
- }
-
- protected override IEnumerable GetActivationHandlers()
- {
- return null;
- }
-
- protected override ActivationHandler GetDefaultActivationHandler()
- => new DefaultActivationHandler();
-
- protected override void OnBackgroundActivated(BackgroundActivatedEventArgs args)
- {
- base.OnBackgroundActivated(args);
-
- if (args.TaskInstance.TriggerDetails is AppServiceTriggerDetails appServiceTriggerDetails)
- {
- LogActivation("OnBackgroundActivated -> AppServiceTriggerDetails received.");
-
- // Only accept connections from callers in the same package
- if (appServiceTriggerDetails.CallerPackageFamilyName == Package.Current.Id.FamilyName)
- {
- // Connection established from the fulltrust process
-
- connectionBackgroundTaskDeferral = args.TaskInstance.GetDeferral();
- args.TaskInstance.Canceled += OnConnectionBackgroundTaskCanceled;
-
- AppServiceConnectionManager.Connection = appServiceTriggerDetails.AppServiceConnection;
-
- WeakReferenceMessenger.Default.Send(new WinoServerConnectionEstablished());
- }
- }
- }
-
- public void OnConnectionBackgroundTaskCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
- {
- sender.Canceled -= OnConnectionBackgroundTaskCanceled;
-
- Log.Information($"Server connection background task was canceled. Reason: {reason}");
-
- connectionBackgroundTaskDeferral?.Complete();
- connectionBackgroundTaskDeferral = null;
-
- AppServiceConnectionManager.Connection = null;
- }
-
- public async void Receive(NewCalendarSynchronizationRequested message)
- {
- try
- {
- var synchronizationResultResponse = await AppServiceConnectionManager.GetResponseAsync(message);
- synchronizationResultResponse.ThrowIfFailed();
- }
- catch (WinoServerException serverException)
- {
- var dialogService = Services.GetService();
-
- dialogService.InfoBarMessage(Translator.Info_SyncFailedTitle, serverException.Message, InfoBarMessageType.Error);
- }
- }
-}
diff --git a/Wino.Calendar/Assets/LargeTile.scale-100.png b/Wino.Calendar/Assets/LargeTile.scale-100.png
deleted file mode 100644
index 42334872..00000000
Binary files a/Wino.Calendar/Assets/LargeTile.scale-100.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/LargeTile.scale-125.png b/Wino.Calendar/Assets/LargeTile.scale-125.png
deleted file mode 100644
index 9c03a75c..00000000
Binary files a/Wino.Calendar/Assets/LargeTile.scale-125.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/LargeTile.scale-150.png b/Wino.Calendar/Assets/LargeTile.scale-150.png
deleted file mode 100644
index e29aa6e0..00000000
Binary files a/Wino.Calendar/Assets/LargeTile.scale-150.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/LargeTile.scale-200.png b/Wino.Calendar/Assets/LargeTile.scale-200.png
deleted file mode 100644
index 5ad11086..00000000
Binary files a/Wino.Calendar/Assets/LargeTile.scale-200.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/LargeTile.scale-400.png b/Wino.Calendar/Assets/LargeTile.scale-400.png
deleted file mode 100644
index af446cf4..00000000
Binary files a/Wino.Calendar/Assets/LargeTile.scale-400.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/LockScreenLogo.scale-200.png b/Wino.Calendar/Assets/LockScreenLogo.scale-200.png
deleted file mode 100644
index 735f57ad..00000000
Binary files a/Wino.Calendar/Assets/LockScreenLogo.scale-200.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/SmallTile.scale-100.png b/Wino.Calendar/Assets/SmallTile.scale-100.png
deleted file mode 100644
index 2d1167f7..00000000
Binary files a/Wino.Calendar/Assets/SmallTile.scale-100.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/SmallTile.scale-125.png b/Wino.Calendar/Assets/SmallTile.scale-125.png
deleted file mode 100644
index d8bc11c5..00000000
Binary files a/Wino.Calendar/Assets/SmallTile.scale-125.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/SmallTile.scale-150.png b/Wino.Calendar/Assets/SmallTile.scale-150.png
deleted file mode 100644
index e124da41..00000000
Binary files a/Wino.Calendar/Assets/SmallTile.scale-150.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/SmallTile.scale-200.png b/Wino.Calendar/Assets/SmallTile.scale-200.png
deleted file mode 100644
index 3854c86c..00000000
Binary files a/Wino.Calendar/Assets/SmallTile.scale-200.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/SmallTile.scale-400.png b/Wino.Calendar/Assets/SmallTile.scale-400.png
deleted file mode 100644
index f242a580..00000000
Binary files a/Wino.Calendar/Assets/SmallTile.scale-400.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/SplashScreen.scale-100.png b/Wino.Calendar/Assets/SplashScreen.scale-100.png
deleted file mode 100644
index 86ade338..00000000
Binary files a/Wino.Calendar/Assets/SplashScreen.scale-100.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/SplashScreen.scale-125.png b/Wino.Calendar/Assets/SplashScreen.scale-125.png
deleted file mode 100644
index 55d0473d..00000000
Binary files a/Wino.Calendar/Assets/SplashScreen.scale-125.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/SplashScreen.scale-150.png b/Wino.Calendar/Assets/SplashScreen.scale-150.png
deleted file mode 100644
index 80b9ed0d..00000000
Binary files a/Wino.Calendar/Assets/SplashScreen.scale-150.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/SplashScreen.scale-200.png b/Wino.Calendar/Assets/SplashScreen.scale-200.png
deleted file mode 100644
index 8510c90c..00000000
Binary files a/Wino.Calendar/Assets/SplashScreen.scale-200.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/SplashScreen.scale-400.png b/Wino.Calendar/Assets/SplashScreen.scale-400.png
deleted file mode 100644
index 7efc715b..00000000
Binary files a/Wino.Calendar/Assets/SplashScreen.scale-400.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Square150x150Logo.scale-100.png b/Wino.Calendar/Assets/Square150x150Logo.scale-100.png
deleted file mode 100644
index 37501fe7..00000000
Binary files a/Wino.Calendar/Assets/Square150x150Logo.scale-100.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Square150x150Logo.scale-125.png b/Wino.Calendar/Assets/Square150x150Logo.scale-125.png
deleted file mode 100644
index 0622878d..00000000
Binary files a/Wino.Calendar/Assets/Square150x150Logo.scale-125.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Square150x150Logo.scale-150.png b/Wino.Calendar/Assets/Square150x150Logo.scale-150.png
deleted file mode 100644
index 15dc07b5..00000000
Binary files a/Wino.Calendar/Assets/Square150x150Logo.scale-150.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Square150x150Logo.scale-200.png b/Wino.Calendar/Assets/Square150x150Logo.scale-200.png
deleted file mode 100644
index 75c9945a..00000000
Binary files a/Wino.Calendar/Assets/Square150x150Logo.scale-200.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Square150x150Logo.scale-400.png b/Wino.Calendar/Assets/Square150x150Logo.scale-400.png
deleted file mode 100644
index cb218b68..00000000
Binary files a/Wino.Calendar/Assets/Square150x150Logo.scale-400.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png b/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png
deleted file mode 100644
index 2c72b37d..00000000
Binary files a/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png b/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png
deleted file mode 100644
index 895753c9..00000000
Binary files a/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png b/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png
deleted file mode 100644
index eeaaad33..00000000
Binary files a/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png b/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png
deleted file mode 100644
index e7c0cb66..00000000
Binary files a/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png b/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png
deleted file mode 100644
index 56792258..00000000
Binary files a/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Square44x44Logo.altform-unplated_targetsize-16.png b/Wino.Calendar/Assets/Square44x44Logo.altform-unplated_targetsize-16.png
deleted file mode 100644
index 2c72b37d..00000000
Binary files a/Wino.Calendar/Assets/Square44x44Logo.altform-unplated_targetsize-16.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Square44x44Logo.altform-unplated_targetsize-256.png b/Wino.Calendar/Assets/Square44x44Logo.altform-unplated_targetsize-256.png
deleted file mode 100644
index eeaaad33..00000000
Binary files a/Wino.Calendar/Assets/Square44x44Logo.altform-unplated_targetsize-256.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Square44x44Logo.altform-unplated_targetsize-32.png b/Wino.Calendar/Assets/Square44x44Logo.altform-unplated_targetsize-32.png
deleted file mode 100644
index e7c0cb66..00000000
Binary files a/Wino.Calendar/Assets/Square44x44Logo.altform-unplated_targetsize-32.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Square44x44Logo.altform-unplated_targetsize-48.png b/Wino.Calendar/Assets/Square44x44Logo.altform-unplated_targetsize-48.png
deleted file mode 100644
index 56792258..00000000
Binary files a/Wino.Calendar/Assets/Square44x44Logo.altform-unplated_targetsize-48.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Square44x44Logo.scale-100.png b/Wino.Calendar/Assets/Square44x44Logo.scale-100.png
deleted file mode 100644
index 43b04c94..00000000
Binary files a/Wino.Calendar/Assets/Square44x44Logo.scale-100.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Square44x44Logo.scale-125.png b/Wino.Calendar/Assets/Square44x44Logo.scale-125.png
deleted file mode 100644
index c6ca31f5..00000000
Binary files a/Wino.Calendar/Assets/Square44x44Logo.scale-125.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Square44x44Logo.scale-150.png b/Wino.Calendar/Assets/Square44x44Logo.scale-150.png
deleted file mode 100644
index 97bf5eb8..00000000
Binary files a/Wino.Calendar/Assets/Square44x44Logo.scale-150.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Square44x44Logo.scale-200.png b/Wino.Calendar/Assets/Square44x44Logo.scale-200.png
deleted file mode 100644
index f1a2ddef..00000000
Binary files a/Wino.Calendar/Assets/Square44x44Logo.scale-200.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Square44x44Logo.scale-400.png b/Wino.Calendar/Assets/Square44x44Logo.scale-400.png
deleted file mode 100644
index b25cb5bc..00000000
Binary files a/Wino.Calendar/Assets/Square44x44Logo.scale-400.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Square44x44Logo.targetsize-16.png b/Wino.Calendar/Assets/Square44x44Logo.targetsize-16.png
deleted file mode 100644
index 22a4ac74..00000000
Binary files a/Wino.Calendar/Assets/Square44x44Logo.targetsize-16.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Square44x44Logo.targetsize-24.png b/Wino.Calendar/Assets/Square44x44Logo.targetsize-24.png
deleted file mode 100644
index ac5a485d..00000000
Binary files a/Wino.Calendar/Assets/Square44x44Logo.targetsize-24.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/Wino.Calendar/Assets/Square44x44Logo.targetsize-24_altform-unplated.png
deleted file mode 100644
index 895753c9..00000000
Binary files a/Wino.Calendar/Assets/Square44x44Logo.targetsize-24_altform-unplated.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Square44x44Logo.targetsize-256.png b/Wino.Calendar/Assets/Square44x44Logo.targetsize-256.png
deleted file mode 100644
index 9e6a53b2..00000000
Binary files a/Wino.Calendar/Assets/Square44x44Logo.targetsize-256.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Square44x44Logo.targetsize-32.png b/Wino.Calendar/Assets/Square44x44Logo.targetsize-32.png
deleted file mode 100644
index 79388f0d..00000000
Binary files a/Wino.Calendar/Assets/Square44x44Logo.targetsize-32.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Square44x44Logo.targetsize-48.png b/Wino.Calendar/Assets/Square44x44Logo.targetsize-48.png
deleted file mode 100644
index 47aea727..00000000
Binary files a/Wino.Calendar/Assets/Square44x44Logo.targetsize-48.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/StoreLogo.backup.png b/Wino.Calendar/Assets/StoreLogo.backup.png
deleted file mode 100644
index 7385b56c..00000000
Binary files a/Wino.Calendar/Assets/StoreLogo.backup.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/StoreLogo.scale-100.png b/Wino.Calendar/Assets/StoreLogo.scale-100.png
deleted file mode 100644
index caafaee8..00000000
Binary files a/Wino.Calendar/Assets/StoreLogo.scale-100.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/StoreLogo.scale-125.png b/Wino.Calendar/Assets/StoreLogo.scale-125.png
deleted file mode 100644
index 59f77d4e..00000000
Binary files a/Wino.Calendar/Assets/StoreLogo.scale-125.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/StoreLogo.scale-150.png b/Wino.Calendar/Assets/StoreLogo.scale-150.png
deleted file mode 100644
index 6adf5536..00000000
Binary files a/Wino.Calendar/Assets/StoreLogo.scale-150.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/StoreLogo.scale-200.png b/Wino.Calendar/Assets/StoreLogo.scale-200.png
deleted file mode 100644
index b4b471f8..00000000
Binary files a/Wino.Calendar/Assets/StoreLogo.scale-200.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/StoreLogo.scale-400.png b/Wino.Calendar/Assets/StoreLogo.scale-400.png
deleted file mode 100644
index be94d352..00000000
Binary files a/Wino.Calendar/Assets/StoreLogo.scale-400.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Wide310x150Logo.scale-100.png b/Wino.Calendar/Assets/Wide310x150Logo.scale-100.png
deleted file mode 100644
index 0847e264..00000000
Binary files a/Wino.Calendar/Assets/Wide310x150Logo.scale-100.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Wide310x150Logo.scale-125.png b/Wino.Calendar/Assets/Wide310x150Logo.scale-125.png
deleted file mode 100644
index 0be7583f..00000000
Binary files a/Wino.Calendar/Assets/Wide310x150Logo.scale-125.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Wide310x150Logo.scale-150.png b/Wino.Calendar/Assets/Wide310x150Logo.scale-150.png
deleted file mode 100644
index f969318f..00000000
Binary files a/Wino.Calendar/Assets/Wide310x150Logo.scale-150.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Wide310x150Logo.scale-200.png b/Wino.Calendar/Assets/Wide310x150Logo.scale-200.png
deleted file mode 100644
index 86ade338..00000000
Binary files a/Wino.Calendar/Assets/Wide310x150Logo.scale-200.png and /dev/null differ
diff --git a/Wino.Calendar/Assets/Wide310x150Logo.scale-400.png b/Wino.Calendar/Assets/Wide310x150Logo.scale-400.png
deleted file mode 100644
index 8510c90c..00000000
Binary files a/Wino.Calendar/Assets/Wide310x150Logo.scale-400.png and /dev/null differ
diff --git a/Wino.Calendar/Controls/CalendarItemControl.xaml.cs b/Wino.Calendar/Controls/CalendarItemControl.xaml.cs
deleted file mode 100644
index dbd2333b..00000000
--- a/Wino.Calendar/Controls/CalendarItemControl.xaml.cs
+++ /dev/null
@@ -1,197 +0,0 @@
-using System.Threading.Tasks;
-using CommunityToolkit.Mvvm.Messaging;
-using Itenso.TimePeriod;
-using Windows.UI.Xaml;
-using Windows.UI.Xaml.Controls;
-using Windows.UI.Xaml.Input;
-using Wino.Calendar.ViewModels.Data;
-using Wino.Calendar.ViewModels.Messages;
-using Wino.Core.Domain;
-using Wino.Core.Domain.Models.Calendar;
-
-namespace Wino.Calendar.Controls;
-
-public sealed partial class CalendarItemControl : UserControl
-{
- // Single tap has a delay to report double taps properly.
- private bool isSingleTap = false;
-
- public static readonly DependencyProperty CalendarItemProperty = DependencyProperty.Register(nameof(CalendarItem), typeof(CalendarItemViewModel), typeof(CalendarItemControl), new PropertyMetadata(null, new PropertyChangedCallback(OnCalendarItemChanged)));
- public static readonly DependencyProperty IsDraggingProperty = DependencyProperty.Register(nameof(IsDragging), typeof(bool), typeof(CalendarItemControl), new PropertyMetadata(false));
- public static readonly DependencyProperty IsCustomEventAreaProperty = DependencyProperty.Register(nameof(IsCustomEventArea), typeof(bool), typeof(CalendarItemControl), new PropertyMetadata(false));
- public static readonly DependencyProperty CalendarItemTitleProperty = DependencyProperty.Register(nameof(CalendarItemTitle), typeof(string), typeof(CalendarItemControl), new PropertyMetadata(string.Empty));
- public static readonly DependencyProperty DisplayingDateProperty = DependencyProperty.Register(nameof(DisplayingDate), typeof(CalendarDayModel), typeof(CalendarItemControl), new PropertyMetadata(null, new PropertyChangedCallback(OnDisplayDateChanged)));
-
- ///
- /// Whether the control is displaying as regular event or all-multi day area in the day control.
- ///
- public bool IsCustomEventArea
- {
- get { return (bool)GetValue(IsCustomEventAreaProperty); }
- set { SetValue(IsCustomEventAreaProperty, value); }
- }
-
- ///
- /// Day that the calendar item is rendered at.
- /// It's needed for title manipulation and some other adjustments later on.
- ///
- public CalendarDayModel DisplayingDate
- {
- get { return (CalendarDayModel)GetValue(DisplayingDateProperty); }
- set { SetValue(DisplayingDateProperty, value); }
- }
-
- public string CalendarItemTitle
- {
- get { return (string)GetValue(CalendarItemTitleProperty); }
- set { SetValue(CalendarItemTitleProperty, value); }
- }
-
- public CalendarItemViewModel CalendarItem
- {
- get { return (CalendarItemViewModel)GetValue(CalendarItemProperty); }
- set { SetValue(CalendarItemProperty, value); }
- }
-
- public bool IsDragging
- {
- get { return (bool)GetValue(IsDraggingProperty); }
- set { SetValue(IsDraggingProperty, value); }
- }
-
- public CalendarItemControl()
- {
- InitializeComponent();
- }
-
- private static void OnDisplayDateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
- {
- if (d is CalendarItemControl control)
- {
- control.UpdateControlVisuals();
- }
- }
-
- private static void OnCalendarItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
- {
- if (d is CalendarItemControl control)
- {
- control.UpdateControlVisuals();
- }
- }
-
- private void UpdateControlVisuals()
- {
- // Depending on the calendar item's duration and attributes, we might need to change the display title.
- // 1. Multi-Day events should display the start date and end date.
- // 2. Multi-Day events that occupy the whole day just shows 'all day'.
- // 3. Other events should display the title.
-
- if (CalendarItem == null) return;
- if (DisplayingDate == null) return;
-
- if (CalendarItem.IsMultiDayEvent)
- {
- // Multi day events are divided into 3 categories:
- // 1. All day events
- // 2. Events that started after the period.
- // 3. Events that started before the period and finishes within the period.
-
- var periodRelation = CalendarItem.Period.GetRelation(DisplayingDate.Period);
-
- if (periodRelation == Itenso.TimePeriod.PeriodRelation.StartInside ||
- periodRelation == PeriodRelation.EnclosingStartTouching)
- {
- // hour -> title
- CalendarItemTitle = $"{DisplayingDate.CalendarRenderOptions.CalendarSettings.GetTimeString(CalendarItem.StartDate.TimeOfDay)} -> {CalendarItem.Title}";
- }
- else if (
- periodRelation == PeriodRelation.EndInside ||
- periodRelation == PeriodRelation.EnclosingEndTouching)
- {
- // title <- hour
- CalendarItemTitle = $"{CalendarItem.Title} <- {DisplayingDate.CalendarRenderOptions.CalendarSettings.GetTimeString(CalendarItem.EndDate.TimeOfDay)}";
- }
- else if (periodRelation == PeriodRelation.Enclosing)
- {
- // This event goes all day and it's multi-day.
- // Item must be hidden in the calendar but displayed on the custom area at the top.
-
- CalendarItemTitle = $"{Translator.CalendarItemAllDay} {CalendarItem.Title}";
- }
- else
- {
- // Not expected, but there it is.
- CalendarItemTitle = CalendarItem.Title;
- }
-
- // Debug.WriteLine($"{CalendarItem.Title} Period relation with {DisplayingDate.Period.ToString()}: {periodRelation}");
- }
- else
- {
- CalendarItemTitle = CalendarItem.Title;
- }
-
- UpdateVisualStates();
- }
-
- private void UpdateVisualStates()
- {
- if (CalendarItem == null) return;
-
- if (CalendarItem.IsAllDayEvent)
- {
- VisualStateManager.GoToState(this, "AllDayEvent", true);
- }
- else if (CalendarItem.IsMultiDayEvent)
- {
- if (IsCustomEventArea)
- {
- VisualStateManager.GoToState(this, "CustomAreaMultiDayEvent", true);
- }
- else
- {
- // Hide it.
- VisualStateManager.GoToState(this, "MultiDayEvent", true);
- }
- }
- else
- {
- VisualStateManager.GoToState(this, "RegularEvent", true);
- }
- }
-
- private void ControlDragStarting(UIElement sender, DragStartingEventArgs args) => IsDragging = true;
-
- private void ControlDropped(UIElement sender, DropCompletedEventArgs args) => IsDragging = false;
-
- private async void ControlTapped(object sender, TappedRoutedEventArgs e)
- {
- if (CalendarItem == null) return;
-
- isSingleTap = true;
-
- await Task.Delay(100);
-
- if (isSingleTap)
- {
- WeakReferenceMessenger.Default.Send(new CalendarItemTappedMessage(CalendarItem, DisplayingDate));
- }
- }
-
- private void ControlDoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
- {
- if (CalendarItem == null) return;
-
- isSingleTap = false;
-
- WeakReferenceMessenger.Default.Send(new CalendarItemDoubleTappedMessage(CalendarItem));
- }
-
- private void ControlRightTapped(object sender, RightTappedRoutedEventArgs e)
- {
- if (CalendarItem == null) return;
-
- WeakReferenceMessenger.Default.Send(new CalendarItemRightTappedMessage(CalendarItem));
- }
-}
diff --git a/Wino.Calendar/Controls/CustomCalendarFlipView.cs b/Wino.Calendar/Controls/CustomCalendarFlipView.cs
deleted file mode 100644
index 8d73220e..00000000
--- a/Wino.Calendar/Controls/CustomCalendarFlipView.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-using Windows.UI.Xaml.Automation.Peers;
-using Windows.UI.Xaml.Controls;
-
-namespace Wino.Calendar.Controls;
-
-///
-/// FlipView that hides the navigation buttons and exposes methods to navigate to the next and previous items with animations.
-///
-public partial class CustomCalendarFlipView : FlipView
-{
- private const string PART_PreviousButton = "PreviousButtonHorizontal";
- private const string PART_NextButton = "NextButtonHorizontal";
-
- private Button PreviousButton;
- private Button NextButton;
-
- protected override void OnApplyTemplate()
- {
- base.OnApplyTemplate();
-
- PreviousButton = GetTemplateChild(PART_PreviousButton) as Button;
- NextButton = GetTemplateChild(PART_NextButton) as Button;
-
- // Hide navigation buttons
- PreviousButton.Opacity = NextButton.Opacity = 0;
- PreviousButton.IsHitTestVisible = NextButton.IsHitTestVisible = false;
-
- var t = FindName("ScrollingHost");
- }
-
- public void GoPreviousFlip()
- {
- var backPeer = new ButtonAutomationPeer(PreviousButton);
- backPeer.Invoke();
- }
-
- public void GoNextFlip()
- {
- var nextPeer = new ButtonAutomationPeer(NextButton);
- nextPeer.Invoke();
- }
-}
diff --git a/Wino.Calendar/Controls/DayColumnControl.cs b/Wino.Calendar/Controls/DayColumnControl.cs
deleted file mode 100644
index a3f3cb78..00000000
--- a/Wino.Calendar/Controls/DayColumnControl.cs
+++ /dev/null
@@ -1,77 +0,0 @@
-using System;
-using Windows.UI.Xaml;
-using Windows.UI.Xaml.Controls;
-using Wino.Core.Domain.Models.Calendar;
-
-namespace Wino.Calendar.Controls;
-
-public partial class DayColumnControl : Control
-{
- private const string PART_HeaderDateDayText = nameof(PART_HeaderDateDayText);
- private const string PART_IsTodayBorder = nameof(PART_IsTodayBorder);
- private const string PART_ColumnHeaderText = nameof(PART_ColumnHeaderText);
-
- private const string PART_AllDayItemsControl = nameof(PART_AllDayItemsControl);
-
- private const string TodayState = nameof(TodayState);
- private const string NotTodayState = nameof(NotTodayState);
-
- private TextBlock HeaderDateDayText;
- private TextBlock ColumnHeaderText;
- private Border IsTodayBorder;
- private ItemsControl AllDayItemsControl;
-
- public CalendarDayModel DayModel
- {
- get { return (CalendarDayModel)GetValue(DayModelProperty); }
- set { SetValue(DayModelProperty, value); }
- }
-
- public static readonly DependencyProperty DayModelProperty = DependencyProperty.Register(nameof(DayModel), typeof(CalendarDayModel), typeof(DayColumnControl), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
-
- public DayColumnControl()
- {
- DefaultStyleKey = typeof(DayColumnControl);
- }
-
- protected override void OnApplyTemplate()
- {
- base.OnApplyTemplate();
-
- HeaderDateDayText = GetTemplateChild(PART_HeaderDateDayText) as TextBlock;
- ColumnHeaderText = GetTemplateChild(PART_ColumnHeaderText) as TextBlock;
- IsTodayBorder = GetTemplateChild(PART_IsTodayBorder) as Border;
- AllDayItemsControl = GetTemplateChild(PART_AllDayItemsControl) as ItemsControl;
-
- UpdateValues();
- }
-
- private static void OnRenderingPropertiesChanged(DependencyObject control, DependencyPropertyChangedEventArgs e)
- {
- if (control is DayColumnControl columnControl)
- {
- columnControl.UpdateValues();
- }
- }
-
- private void UpdateValues()
- {
- if (HeaderDateDayText == null || IsTodayBorder == null || DayModel == null) return;
-
- HeaderDateDayText.Text = DayModel.RepresentingDate.Day.ToString();
-
- // Monthly template does not use it.
- if (ColumnHeaderText != null)
- {
- ColumnHeaderText.Text = DayModel.RepresentingDate.ToString("dddd", DayModel.CalendarRenderOptions.CalendarSettings.CultureInfo);
- }
-
- AllDayItemsControl.ItemsSource = DayModel.EventsCollection.AllDayEvents;
-
- bool isToday = DayModel.RepresentingDate.Date == DateTime.Now.Date;
-
- VisualStateManager.GoToState(this, isToday ? TodayState : NotTodayState, false);
-
- UpdateLayout();
- }
-}
diff --git a/Wino.Calendar/Controls/DayHeaderControl.cs b/Wino.Calendar/Controls/DayHeaderControl.cs
deleted file mode 100644
index 567cdaef..00000000
--- a/Wino.Calendar/Controls/DayHeaderControl.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-using System;
-using Windows.UI.Xaml;
-using Windows.UI.Xaml.Controls;
-using Wino.Core.Domain.Enums;
-
-namespace Wino.Calendar.Controls;
-
-public partial class DayHeaderControl : Control
-{
- private const string PART_DayHeaderTextBlock = nameof(PART_DayHeaderTextBlock);
- private TextBlock HeaderTextblock;
-
- public DayHeaderDisplayType DisplayType
- {
- get { return (DayHeaderDisplayType)GetValue(DisplayTypeProperty); }
- set { SetValue(DisplayTypeProperty, value); }
- }
-
- public DateTime Date
- {
- get { return (DateTime)GetValue(DateProperty); }
- set { SetValue(DateProperty, value); }
- }
-
- public static readonly DependencyProperty DateProperty = DependencyProperty.Register(nameof(Date), typeof(DateTime), typeof(DayHeaderControl), new PropertyMetadata(default(DateTime), new PropertyChangedCallback(OnHeaderPropertyChanged)));
- public static readonly DependencyProperty DisplayTypeProperty = DependencyProperty.Register(nameof(DisplayType), typeof(DayHeaderDisplayType), typeof(DayHeaderControl), new PropertyMetadata(DayHeaderDisplayType.TwentyFourHour, new PropertyChangedCallback(OnHeaderPropertyChanged)));
-
- public DayHeaderControl()
- {
- DefaultStyleKey = typeof(DayHeaderControl);
- }
-
- protected override void OnApplyTemplate()
- {
- base.OnApplyTemplate();
-
- HeaderTextblock = GetTemplateChild(PART_DayHeaderTextBlock) as TextBlock;
- UpdateHeaderText();
- }
-
- private static void OnHeaderPropertyChanged(DependencyObject control, DependencyPropertyChangedEventArgs e)
- {
- if (control is DayHeaderControl headerControl)
- {
- headerControl.UpdateHeaderText();
- }
- }
-
- private void UpdateHeaderText()
- {
- if (HeaderTextblock != null)
- {
- HeaderTextblock.Text = DisplayType == DayHeaderDisplayType.TwelveHour ? Date.ToString("h tt") : Date.ToString("HH:mm");
- }
- }
-}
diff --git a/Wino.Calendar/Controls/WinoCalendarControl.cs b/Wino.Calendar/Controls/WinoCalendarControl.cs
deleted file mode 100644
index e2930e6c..00000000
--- a/Wino.Calendar/Controls/WinoCalendarControl.cs
+++ /dev/null
@@ -1,299 +0,0 @@
-using System;
-using System.Collections.ObjectModel;
-using System.Linq;
-using System.Threading.Tasks;
-using Windows.UI.Xaml;
-using Windows.UI.Xaml.Controls;
-using Wino.Calendar.Args;
-using Wino.Calendar.ViewModels.Data;
-using Wino.Core.Domain.Enums;
-using Wino.Core.Domain.Models.Calendar;
-using Wino.Helpers;
-
-namespace Wino.Calendar.Controls;
-
-public partial class WinoCalendarControl : Control
-{
- private const string PART_WinoFlipView = nameof(PART_WinoFlipView);
- private const string PART_IdleGrid = nameof(PART_IdleGrid);
-
- public event EventHandler TimelineCellSelected;
- public event EventHandler TimelineCellUnselected;
-
- public event EventHandler ScrollPositionChanging;
-
- #region Dependency Properties
-
- public static readonly DependencyProperty DayRangesProperty = DependencyProperty.Register(nameof(DayRanges), typeof(ObservableCollection), typeof(WinoCalendarControl), new PropertyMetadata(null));
- public static readonly DependencyProperty SelectedFlipViewIndexProperty = DependencyProperty.Register(nameof(SelectedFlipViewIndex), typeof(int), typeof(WinoCalendarControl), new PropertyMetadata(-1));
- public static readonly DependencyProperty SelectedFlipViewDayRangeProperty = DependencyProperty.Register(nameof(SelectedFlipViewDayRange), typeof(DayRangeRenderModel), typeof(WinoCalendarControl), new PropertyMetadata(null));
- public static readonly DependencyProperty ActiveCanvasProperty = DependencyProperty.Register(nameof(ActiveCanvas), typeof(WinoDayTimelineCanvas), typeof(WinoCalendarControl), new PropertyMetadata(null, new PropertyChangedCallback(OnActiveCanvasChanged)));
- public static readonly DependencyProperty IsFlipIdleProperty = DependencyProperty.Register(nameof(IsFlipIdle), typeof(bool), typeof(WinoCalendarControl), new PropertyMetadata(true, new PropertyChangedCallback(OnIdleStateChanged)));
- public static readonly DependencyProperty ActiveScrollViewerProperty = DependencyProperty.Register(nameof(ActiveScrollViewer), typeof(ScrollViewer), typeof(WinoCalendarControl), new PropertyMetadata(null, new PropertyChangedCallback(OnActiveVerticalScrollViewerChanged)));
-
- public static readonly DependencyProperty VerticalItemsPanelTemplateProperty = DependencyProperty.Register(nameof(VerticalItemsPanelTemplate), typeof(ItemsPanelTemplate), typeof(WinoCalendarControl), new PropertyMetadata(null, new PropertyChangedCallback(OnCalendarOrientationPropertiesUpdated)));
- public static readonly DependencyProperty HorizontalItemsPanelTemplateProperty = DependencyProperty.Register(nameof(HorizontalItemsPanelTemplate), typeof(ItemsPanelTemplate), typeof(WinoCalendarControl), new PropertyMetadata(null, new PropertyChangedCallback(OnCalendarOrientationPropertiesUpdated)));
- public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(nameof(Orientation), typeof(CalendarOrientation), typeof(WinoCalendarControl), new PropertyMetadata(CalendarOrientation.Horizontal, new PropertyChangedCallback(OnCalendarOrientationPropertiesUpdated)));
- public static readonly DependencyProperty DisplayTypeProperty = DependencyProperty.Register(nameof(DisplayType), typeof(CalendarDisplayType), typeof(WinoCalendarControl), new PropertyMetadata(CalendarDisplayType.Day));
-
- ///
- /// Gets or sets the day-week-month-year display type.
- /// Orientation is not determined by this property, but Orientation property.
- /// This property is used to determine the template to use for the calendar.
- ///
- public CalendarDisplayType DisplayType
- {
- get { return (CalendarDisplayType)GetValue(DisplayTypeProperty); }
- set { SetValue(DisplayTypeProperty, value); }
- }
-
- public CalendarOrientation Orientation
- {
- get { return (CalendarOrientation)GetValue(OrientationProperty); }
- set { SetValue(OrientationProperty, value); }
- }
-
- public ItemsPanelTemplate VerticalItemsPanelTemplate
- {
- get { return (ItemsPanelTemplate)GetValue(VerticalItemsPanelTemplateProperty); }
- set { SetValue(VerticalItemsPanelTemplateProperty, value); }
- }
-
- public ItemsPanelTemplate HorizontalItemsPanelTemplate
- {
- get { return (ItemsPanelTemplate)GetValue(HorizontalItemsPanelTemplateProperty); }
- set { SetValue(HorizontalItemsPanelTemplateProperty, value); }
- }
-
- public DayRangeRenderModel SelectedFlipViewDayRange
- {
- get { return (DayRangeRenderModel)GetValue(SelectedFlipViewDayRangeProperty); }
- set { SetValue(SelectedFlipViewDayRangeProperty, value); }
- }
-
- public ScrollViewer ActiveScrollViewer
- {
- get { return (ScrollViewer)GetValue(ActiveScrollViewerProperty); }
- set { SetValue(ActiveScrollViewerProperty, value); }
- }
-
- public WinoDayTimelineCanvas ActiveCanvas
- {
- get { return (WinoDayTimelineCanvas)GetValue(ActiveCanvasProperty); }
- set { SetValue(ActiveCanvasProperty, value); }
- }
-
- public bool IsFlipIdle
- {
- get { return (bool)GetValue(IsFlipIdleProperty); }
- set { SetValue(IsFlipIdleProperty, value); }
- }
-
- ///
- /// Gets or sets the collection of day ranges to render.
- /// Each day range usually represents a week, but it may support other ranges.
- ///
- public ObservableCollection DayRanges
- {
- get { return (ObservableCollection)GetValue(DayRangesProperty); }
- set { SetValue(DayRangesProperty, value); }
- }
-
- public int SelectedFlipViewIndex
- {
- get { return (int)GetValue(SelectedFlipViewIndexProperty); }
- set { SetValue(SelectedFlipViewIndexProperty, value); }
- }
-
- #endregion
-
- private WinoCalendarFlipView InternalFlipView;
- private Grid IdleGrid;
-
- public WinoCalendarControl()
- {
- DefaultStyleKey = typeof(WinoCalendarControl);
- SizeChanged += CalendarSizeChanged;
- }
-
- private static void OnCalendarOrientationPropertiesUpdated(DependencyObject calendar, DependencyPropertyChangedEventArgs e)
- {
- if (calendar is WinoCalendarControl control)
- {
- control.ManageCalendarOrientation();
- }
- }
-
- private static void OnIdleStateChanged(DependencyObject calendar, DependencyPropertyChangedEventArgs e)
- {
- if (calendar is WinoCalendarControl calendarControl)
- {
- calendarControl.UpdateIdleState();
- }
- }
-
-
- private static void OnActiveVerticalScrollViewerChanged(DependencyObject calendar, DependencyPropertyChangedEventArgs e)
- {
- if (calendar is WinoCalendarControl calendarControl)
- {
- if (e.OldValue is ScrollViewer oldScrollViewer)
- {
- calendarControl.DeregisterScrollChanges(oldScrollViewer);
- }
-
- if (e.NewValue is ScrollViewer newScrollViewer)
- {
- calendarControl.RegisterScrollChanges(newScrollViewer);
- }
-
- calendarControl.ManageHighlightedDateRange();
- }
- }
-
-
- private static void OnActiveCanvasChanged(DependencyObject calendar, DependencyPropertyChangedEventArgs e)
- {
- if (calendar is WinoCalendarControl calendarControl)
- {
- if (e.OldValue is WinoDayTimelineCanvas oldCanvas)
- {
- // Dismiss any selection on the old canvas.
- calendarControl.DeregisterCanvas(oldCanvas);
- }
-
- if (e.NewValue is WinoDayTimelineCanvas newCanvas)
- {
- calendarControl.RegisterCanvas(newCanvas);
- }
-
- calendarControl.ManageHighlightedDateRange();
- }
- }
-
- private void ManageCalendarOrientation()
- {
- if (InternalFlipView == null || HorizontalItemsPanelTemplate == null || VerticalItemsPanelTemplate == null) return;
-
- InternalFlipView.ItemsPanel = Orientation == CalendarOrientation.Horizontal ? HorizontalItemsPanelTemplate : VerticalItemsPanelTemplate;
- }
-
- private void ManageHighlightedDateRange()
- => SelectedFlipViewDayRange = InternalFlipView.SelectedItem as DayRangeRenderModel;
-
- private void DeregisterCanvas(WinoDayTimelineCanvas canvas)
- {
- if (canvas == null) return;
-
- canvas.SelectedDateTime = null;
- canvas.TimelineCellSelected -= ActiveTimelineCellSelected;
- canvas.TimelineCellUnselected -= ActiveTimelineCellUnselected;
- }
-
- private void RegisterCanvas(WinoDayTimelineCanvas canvas)
- {
- if (canvas == null) return;
-
- canvas.SelectedDateTime = null;
- canvas.TimelineCellSelected += ActiveTimelineCellSelected;
- canvas.TimelineCellUnselected += ActiveTimelineCellUnselected;
- }
-
- private void RegisterScrollChanges(ScrollViewer scrollViewer)
- {
- if (scrollViewer == null) return;
-
- scrollViewer.ViewChanging += ScrollViewChanging;
- }
-
- private void DeregisterScrollChanges(ScrollViewer scrollViewer)
- {
- if (scrollViewer == null) return;
-
- scrollViewer.ViewChanging -= ScrollViewChanging;
- }
-
- private void ScrollViewChanging(object sender, ScrollViewerViewChangingEventArgs e)
- => ScrollPositionChanging?.Invoke(this, EventArgs.Empty);
-
- private void CalendarSizeChanged(object sender, SizeChangedEventArgs e)
- {
- if (ActiveCanvas == null) return;
-
- ActiveCanvas.SelectedDateTime = null;
- }
-
- protected override void OnApplyTemplate()
- {
- base.OnApplyTemplate();
-
- InternalFlipView = GetTemplateChild(PART_WinoFlipView) as WinoCalendarFlipView;
- IdleGrid = GetTemplateChild(PART_IdleGrid) as Grid;
-
- UpdateIdleState();
- ManageCalendarOrientation();
- }
-
- private void UpdateIdleState()
- {
- InternalFlipView.Opacity = IsFlipIdle ? 0 : 1;
- IdleGrid.Visibility = IsFlipIdle ? Visibility.Visible : Visibility.Collapsed;
- }
-
- private void ActiveTimelineCellUnselected(object sender, TimelineCellUnselectedArgs e)
- => TimelineCellUnselected?.Invoke(this, e);
-
- private void ActiveTimelineCellSelected(object sender, TimelineCellSelectedArgs e)
- => TimelineCellSelected?.Invoke(this, e);
-
- public void NavigateToDay(DateTime dateTime) => InternalFlipView.NavigateToDay(dateTime);
-
- public async void NavigateToHour(TimeSpan timeSpan)
- {
- if (ActiveScrollViewer == null) return;
-
- // Total height of the FlipViewItem is the same as vertical ScrollViewer to position day headers.
-
- await Task.Yield();
- await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.High, () =>
- {
- double hourHeght = 60;
- double totalHeight = ActiveScrollViewer.ScrollableHeight;
- double scrollPosition = timeSpan.TotalHours * hourHeght;
-
- ActiveScrollViewer.ChangeView(null, scrollPosition, null, disableAnimation: false);
- });
- }
- public void ResetTimelineSelection()
- {
- if (ActiveCanvas == null) return;
-
- ActiveCanvas.SelectedDateTime = null;
- }
-
- public void GoNextRange()
- {
- if (InternalFlipView == null) return;
-
- InternalFlipView.GoNextFlip();
- }
-
- public void GoPreviousRange()
- {
- if (InternalFlipView == null) return;
-
- InternalFlipView.GoPreviousFlip();
- }
-
- public void UnselectActiveTimelineCell()
- {
- if (ActiveCanvas == null) return;
-
- ActiveCanvas.SelectedDateTime = null;
- }
-
- public CalendarItemControl GetCalendarItemControl(CalendarItemViewModel calendarItemViewModel)
- {
- return this.FindDescendants().FirstOrDefault(a => a.CalendarItem == calendarItemViewModel);
- }
-}
diff --git a/Wino.Calendar/Controls/WinoCalendarFlipView.cs b/Wino.Calendar/Controls/WinoCalendarFlipView.cs
deleted file mode 100644
index 679490d4..00000000
--- a/Wino.Calendar/Controls/WinoCalendarFlipView.cs
+++ /dev/null
@@ -1,185 +0,0 @@
-using System;
-using System.Collections.Specialized;
-using System.Linq;
-using System.Threading.Tasks;
-using CommunityToolkit.WinUI;
-using Windows.UI.Xaml;
-using Windows.UI.Xaml.Controls;
-using Wino.Core.Domain.Collections;
-using Wino.Core.Domain.Models.Calendar;
-
-namespace Wino.Calendar.Controls;
-
-public partial class WinoCalendarFlipView : CustomCalendarFlipView
-{
- public static readonly DependencyProperty IsIdleProperty = DependencyProperty.Register(nameof(IsIdle), typeof(bool), typeof(WinoCalendarFlipView), new PropertyMetadata(true));
- public static readonly DependencyProperty ActiveCanvasProperty = DependencyProperty.Register(nameof(ActiveCanvas), typeof(WinoDayTimelineCanvas), typeof(WinoCalendarFlipView), new PropertyMetadata(null));
- public static readonly DependencyProperty ActiveVerticalScrollViewerProperty = DependencyProperty.Register(nameof(ActiveVerticalScrollViewer), typeof(ScrollViewer), typeof(WinoCalendarFlipView), new PropertyMetadata(null));
-
- ///
- /// Gets or sets the active canvas that is currently displayed in the flip view.
- /// Each day-range of flip view item has a canvas that displays the day timeline.
- ///
- public WinoDayTimelineCanvas ActiveCanvas
- {
- get { return (WinoDayTimelineCanvas)GetValue(ActiveCanvasProperty); }
- set { SetValue(ActiveCanvasProperty, value); }
- }
-
- ///
- /// Gets or sets the scroll viewer that is currently active in the flip view.
- /// It's the vertical scroll that scrolls the timeline only, not the header part that belongs
- /// to parent FlipView control.
- ///
- public ScrollViewer ActiveVerticalScrollViewer
- {
- get { return (ScrollViewer)GetValue(ActiveVerticalScrollViewerProperty); }
- set { SetValue(ActiveVerticalScrollViewerProperty, value); }
- }
-
- public bool IsIdle
- {
- get { return (bool)GetValue(IsIdleProperty); }
- set { SetValue(IsIdleProperty, value); }
- }
-
- public WinoCalendarFlipView()
- {
- RegisterPropertyChangedCallback(SelectedIndexProperty, new DependencyPropertyChangedCallback(OnSelectedIndexUpdated));
- RegisterPropertyChangedCallback(ItemsSourceProperty, new DependencyPropertyChangedCallback(OnItemsSourceChanged));
- }
-
- private static void OnItemsSourceChanged(DependencyObject d, DependencyProperty e)
- {
- if (d is WinoCalendarFlipView flipView)
- {
- flipView.RegisterItemsSourceChange();
- }
- }
-
- private static void OnSelectedIndexUpdated(DependencyObject d, DependencyProperty e)
- {
- if (d is WinoCalendarFlipView flipView)
- {
- flipView.UpdateActiveCanvas();
- flipView.UpdateActiveScrollViewer();
- }
- }
-
- private void RegisterItemsSourceChange()
- {
- if (GetItemsSource() is INotifyCollectionChanged notifyCollectionChanged)
- {
- notifyCollectionChanged.CollectionChanged += ItemsSourceUpdated;
- }
- }
-
- private void ItemsSourceUpdated(object sender, NotifyCollectionChangedEventArgs e)
- {
- IsIdle = e.Action == NotifyCollectionChangedAction.Reset || e.Action == NotifyCollectionChangedAction.Replace;
- }
-
- private async Task GetCurrentFlipViewItem()
- {
- // TODO: Refactor this mechanism by listening to PrepareContainerForItemOverride and Loaded events together.
- while (ContainerFromIndex(SelectedIndex) == null)
- {
- await Task.Delay(100);
- }
-
- return ContainerFromIndex(SelectedIndex) as FlipViewItem;
-
-
- }
-
- private void UpdateActiveScrollViewer()
- {
- if (SelectedIndex < 0)
- ActiveVerticalScrollViewer = null;
- else
- {
- GetCurrentFlipViewItem().ContinueWith(task =>
- {
- if (task.IsCompletedSuccessfully)
- {
- var flipViewItem = task.Result;
-
- _ = Dispatcher.TryRunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
- {
- ActiveVerticalScrollViewer = flipViewItem.FindDescendant();
- });
- }
- });
- }
- }
-
- public void UpdateActiveCanvas()
- {
- if (SelectedIndex < 0)
- ActiveCanvas = null;
- else
- {
- GetCurrentFlipViewItem().ContinueWith(task =>
- {
- if (task.IsCompletedSuccessfully)
- {
- var flipViewItem = task.Result;
-
- _ = Dispatcher.TryRunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
- {
- ActiveCanvas = flipViewItem.FindDescendant();
- });
- }
- });
- }
- }
-
- ///
- /// Navigates to the specified date in the calendar.
- ///
- /// Date to navigate.
- public async void NavigateToDay(DateTime dateTime)
- {
- await Task.Yield();
-
- await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.High, () =>
- {
- // Find the day range that contains the date.
- var dayRange = GetItemsSource()?.FirstOrDefault(a => a.CalendarDays.Any(b => b.RepresentingDate.Date == dateTime.Date));
-
- if (dayRange != null)
- {
- var navigationItemIndex = GetItemsSource().IndexOf(dayRange);
-
- if (Math.Abs(navigationItemIndex - SelectedIndex) > 4)
- {
- // Difference between dates are high.
- // No need to animate this much, just go without animating.
-
- SelectedIndex = navigationItemIndex;
- }
- else
- {
- // Until we reach the day in the flip, simulate next-prev button clicks.
- // This will make sure the FlipView animations are triggered.
- // Setting SelectedIndex directly doesn't trigger the animations.
-
- while (SelectedIndex != navigationItemIndex)
- {
- if (SelectedIndex > navigationItemIndex)
- {
- GoPreviousFlip();
- }
- else
- {
- GoNextFlip();
- }
- }
- }
- }
- });
- }
-
- private ObservableRangeCollection GetItemsSource()
- => ItemsSource as ObservableRangeCollection;
-}
diff --git a/Wino.Calendar/Controls/WinoCalendarPanel.cs b/Wino.Calendar/Controls/WinoCalendarPanel.cs
deleted file mode 100644
index c76801ea..00000000
--- a/Wino.Calendar/Controls/WinoCalendarPanel.cs
+++ /dev/null
@@ -1,293 +0,0 @@
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using CommunityToolkit.WinUI;
-using Itenso.TimePeriod;
-using Windows.Foundation;
-using Windows.UI.Xaml;
-using Windows.UI.Xaml.Controls;
-using Wino.Calendar.Models;
-using Wino.Calendar.ViewModels.Data;
-using Wino.Core.Domain.Interfaces;
-
-namespace Wino.Calendar.Controls;
-
-public partial class WinoCalendarPanel : Panel
-{
- private const double LastItemRightExtraMargin = 12d;
-
- // Store each ICalendarItem measurements by their Id.
- private readonly Dictionary _measurements = new Dictionary();
-
- public static readonly DependencyProperty EventItemMarginProperty = DependencyProperty.Register(nameof(EventItemMargin), typeof(Thickness), typeof(WinoCalendarPanel), new PropertyMetadata(new Thickness(0, 0, 0, 0)));
- public static readonly DependencyProperty HourHeightProperty = DependencyProperty.Register(nameof(HourHeight), typeof(double), typeof(WinoCalendarPanel), new PropertyMetadata(0d));
- public static readonly DependencyProperty PeriodProperty = DependencyProperty.Register(nameof(Period), typeof(ITimePeriod), typeof(WinoCalendarPanel), new PropertyMetadata(null));
-
- public ITimePeriod Period
- {
- get { return (ITimePeriod)GetValue(PeriodProperty); }
- set { SetValue(PeriodProperty, value); }
- }
-
- public double HourHeight
- {
- get { return (double)GetValue(HourHeightProperty); }
- set { SetValue(HourHeightProperty, value); }
- }
-
- public Thickness EventItemMargin
- {
- get { return (Thickness)GetValue(EventItemMarginProperty); }
- set { SetValue(EventItemMarginProperty, value); }
- }
-
- private void ResetMeasurements() => _measurements.Clear();
-
- private double GetChildTopMargin(ICalendarItem calendarItemViewModel, double availableHeight)
- {
- var childStart = calendarItemViewModel.StartDate;
-
- if (childStart <= Period.Start)
- {
- // Event started before or exactly at the periods tart. This might be a multi-day event.
- // We can simply consider event must not have a top margin.
-
- return 0d;
- }
-
- double minutesFromStart = (childStart - Period.Start).TotalMinutes;
- return (minutesFromStart / 1440) * availableHeight;
- }
-
- private double GetChildWidth(CalendarItemMeasurement calendarItemMeasurement, double availableWidth)
- {
- return (calendarItemMeasurement.Right - calendarItemMeasurement.Left) * availableWidth;
- }
-
- private double GetChildLeftMargin(CalendarItemMeasurement calendarItemMeasurement, double availableWidth)
- => availableWidth * calendarItemMeasurement.Left;
-
- private double GetChildHeight(ICalendarItem child)
- {
- // All day events are not measured.
- if (child.IsAllDayEvent) return 0;
-
- double childDurationInMinutes = 0d;
- double availableHeight = HourHeight * 24;
-
- var periodRelation = child.Period.GetRelation(Period);
-
- // Debug.WriteLine($"Render relation of {child.Title} ({child.Period.Start} - {child.Period.End}) is {periodRelation} with {Period.Start.Day}");
-
- if (!child.IsMultiDayEvent)
- {
- childDurationInMinutes = child.Period.Duration.TotalMinutes;
- }
- else
- {
- // Multi-day event.
- // Check how many of the event falls into the current period.
- childDurationInMinutes = (child.Period.End - Period.Start).TotalMinutes;
- }
-
- return (childDurationInMinutes / 1440) * availableHeight;
- }
-
- protected override Size MeasureOverride(Size availableSize)
- {
- ResetMeasurements();
- return base.MeasureOverride(availableSize);
- }
-
- protected override Size ArrangeOverride(Size finalSize)
- {
- if (Period == null || HourHeight == 0d) return finalSize;
-
- // Measure/arrange each child height and width.
- // This is a vertical calendar. Therefore the height of each child is the duration of the event.
- // Children weights for left and right will be saved if they don't exist.
- // This is important because we don't want to measure the weights again.
- // They don't change until new event is added or removed.
- // Width of the each child may depend on the rectangle packing algorithm.
- // Children are first categorized into columns. Then each column is shifted to the left until
- // no overlap occurs. The width of each child is calculated based on the number of columns it spans.
-
- double availableHeight = finalSize.Height;
- double availableWidth = finalSize.Width;
-
- var calendarControls = Children.Cast();
-
- if (!calendarControls.Any()) return base.ArrangeOverride(finalSize);
-
- var events = calendarControls.Select(a => a.Content as CalendarItemViewModel);
-
- LayoutEvents(events);
-
- foreach (var control in calendarControls)
- {
- // We can't arrange this child.
- if (!(control.Content is ICalendarItem child)) continue;
-
- bool isHorizontallyLastItem = false;
-
- double childWidth = 0,
- childHeight = Math.Max(0, GetChildHeight(child)),
- childTop = Math.Max(0, GetChildTopMargin(child, availableHeight)),
- childLeft = 0;
-
- // No need to measure anything here.
- if (childHeight == 0) continue;
-
- if (!_measurements.ContainsKey(child))
- {
- // Multi-day event.
-
- childLeft = 0;
- childWidth = availableWidth;
- }
- else
- {
- var childMeasurement = _measurements[child];
-
- childWidth = Math.Max(0, GetChildWidth(childMeasurement, finalSize.Width));
- childLeft = Math.Max(0, GetChildLeftMargin(childMeasurement, availableWidth));
-
- isHorizontallyLastItem = childMeasurement.Right == 1;
- }
-
- // Add additional right margin to items that falls on the right edge of the panel.
- double extraRightMargin = 0;
-
- // Multi-day events don't have any margin and their hit test is disabled.
- if (!child.IsMultiDayEvent)
- {
- // Max of 5% of the width or 20px max.
- extraRightMargin = isHorizontallyLastItem ? Math.Max(LastItemRightExtraMargin, finalSize.Width * 5 / 100) : 0;
- }
-
- if (childWidth < 0) childWidth = 1;
-
- // Regular events must have 2px margin
- if (!child.IsMultiDayEvent && !child.IsAllDayEvent)
- {
- childLeft += 2;
- childTop += 2;
- childHeight -= 2;
- childWidth -= 2;
- }
-
- var arrangementRect = new Rect(childLeft + EventItemMargin.Left, childTop + EventItemMargin.Top, Math.Max(childWidth - extraRightMargin, 1), childHeight);
-
- // Make sure measured size will fit in the arranged box.
- var measureSize = arrangementRect.ToSize();
- control.Measure(measureSize);
- control.Arrange(arrangementRect);
-
- //Debug.WriteLine($"{child.Title}, Measured: {measureSize}, Arranged: {arrangementRect}");
- }
-
-
- return finalSize;
- }
-
- #region ColumSpanning and Packing Algorithm
-
- private void AddOrUpdateMeasurement(ICalendarItem calendarItem, CalendarItemMeasurement measurement)
- {
- if (_measurements.ContainsKey(calendarItem))
- {
- _measurements[calendarItem] = measurement;
- }
- else
- {
- _measurements.Add(calendarItem, measurement);
- }
- }
-
- // Pick the left and right positions of each event, such that there are no overlap.
- private void LayoutEvents(IEnumerable events)
- {
- var columns = new List>();
- DateTime? lastEventEnding = null;
-
- foreach (var ev in events.OrderBy(ev => ev.StartDate).ThenBy(ev => ev.EndDate))
- {
- // Multi-day events are not measured.
- if (ev.IsMultiDayEvent) continue;
-
- if (ev.Period.Start >= lastEventEnding)
- {
- PackEvents(columns);
- columns.Clear();
- lastEventEnding = null;
- }
-
- bool placed = false;
-
- foreach (var col in columns)
- {
- if (!col.Last().Period.OverlapsWith(ev.Period))
- {
- col.Add(ev);
- placed = true;
- break;
- }
- }
- if (!placed)
- {
- columns.Add(new List { ev });
- }
- if (lastEventEnding == null || ev.Period.End > lastEventEnding.Value)
- {
- lastEventEnding = ev.Period.End;
- }
- }
- if (columns.Count > 0)
- {
- PackEvents(columns);
- }
- }
-
- // Set the left and right positions for each event in the connected group.
- private void PackEvents(List> columns)
- {
- float numColumns = columns.Count;
- int iColumn = 0;
-
- foreach (var col in columns)
- {
- foreach (var ev in col)
- {
- int colSpan = ExpandEvent(ev, iColumn, columns);
-
- var leftWeight = iColumn / numColumns;
- var rightWeight = (iColumn + colSpan) / numColumns;
-
- AddOrUpdateMeasurement(ev, new CalendarItemMeasurement(leftWeight, rightWeight));
- }
-
- iColumn++;
- }
- }
-
- // Checks how many columns the event can expand into, without colliding with other events.
- private int ExpandEvent(ICalendarItem ev, int iColumn, List> columns)
- {
- int colSpan = 1;
-
- foreach (var col in columns.Skip(iColumn + 1))
- {
- foreach (var ev1 in col)
- {
- if (ev1.Period.OverlapsWith(ev.Period)) return colSpan;
- }
-
- colSpan++;
- }
-
- return colSpan;
- }
-
- #endregion
-}
diff --git a/Wino.Calendar/Controls/WinoCalendarTypeSelectorControl.cs b/Wino.Calendar/Controls/WinoCalendarTypeSelectorControl.cs
deleted file mode 100644
index 899e32da..00000000
--- a/Wino.Calendar/Controls/WinoCalendarTypeSelectorControl.cs
+++ /dev/null
@@ -1,91 +0,0 @@
-using System.Windows.Input;
-using CommunityToolkit.Diagnostics;
-using Windows.UI.Xaml;
-using Windows.UI.Xaml.Controls;
-using Wino.Core.Domain.Enums;
-
-namespace Wino.Calendar.Controls;
-
-public partial class WinoCalendarTypeSelectorControl : Control
-{
- private const string PART_TodayButton = nameof(PART_TodayButton);
- private const string PART_DayToggle = nameof(PART_DayToggle);
- private const string PART_WeekToggle = nameof(PART_WeekToggle);
- private const string PART_MonthToggle = nameof(PART_MonthToggle);
- private const string PART_YearToggle = nameof(PART_YearToggle);
-
- public static readonly DependencyProperty SelectedTypeProperty = DependencyProperty.Register(nameof(SelectedType), typeof(CalendarDisplayType), typeof(WinoCalendarTypeSelectorControl), new PropertyMetadata(CalendarDisplayType.Week));
- public static readonly DependencyProperty DisplayDayCountProperty = DependencyProperty.Register(nameof(DisplayDayCount), typeof(int), typeof(WinoCalendarTypeSelectorControl), new PropertyMetadata(0));
- public static readonly DependencyProperty TodayClickedCommandProperty = DependencyProperty.Register(nameof(TodayClickedCommand), typeof(ICommand), typeof(WinoCalendarTypeSelectorControl), new PropertyMetadata(null));
-
- public ICommand TodayClickedCommand
- {
- get { return (ICommand)GetValue(TodayClickedCommandProperty); }
- set { SetValue(TodayClickedCommandProperty, value); }
- }
-
- public CalendarDisplayType SelectedType
- {
- get { return (CalendarDisplayType)GetValue(SelectedTypeProperty); }
- set { SetValue(SelectedTypeProperty, value); }
- }
-
- public int DisplayDayCount
- {
- get { return (int)GetValue(DisplayDayCountProperty); }
- set { SetValue(DisplayDayCountProperty, value); }
- }
-
- private AppBarButton _todayButton;
- private AppBarToggleButton _dayToggle;
- private AppBarToggleButton _weekToggle;
- private AppBarToggleButton _monthToggle;
- private AppBarToggleButton _yearToggle;
-
- public WinoCalendarTypeSelectorControl()
- {
- DefaultStyleKey = typeof(WinoCalendarTypeSelectorControl);
- }
-
- protected override void OnApplyTemplate()
- {
- base.OnApplyTemplate();
-
- _todayButton = GetTemplateChild(PART_TodayButton) as AppBarButton;
- _dayToggle = GetTemplateChild(PART_DayToggle) as AppBarToggleButton;
- _weekToggle = GetTemplateChild(PART_WeekToggle) as AppBarToggleButton;
- _monthToggle = GetTemplateChild(PART_MonthToggle) as AppBarToggleButton;
- _yearToggle = GetTemplateChild(PART_YearToggle) as AppBarToggleButton;
-
- Guard.IsNotNull(_todayButton, nameof(_todayButton));
- Guard.IsNotNull(_dayToggle, nameof(_dayToggle));
- Guard.IsNotNull(_weekToggle, nameof(_weekToggle));
- Guard.IsNotNull(_monthToggle, nameof(_monthToggle));
- Guard.IsNotNull(_yearToggle, nameof(_yearToggle));
-
- _todayButton.Click += TodayClicked;
-
- _dayToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Day); };
- _weekToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Week); };
- _monthToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Month); };
- _yearToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Year); };
-
- UpdateToggleButtonStates();
- }
-
- private void TodayClicked(object sender, RoutedEventArgs e) => TodayClickedCommand?.Execute(null);
-
- private void SetSelectedType(CalendarDisplayType type)
- {
- SelectedType = type;
- UpdateToggleButtonStates();
- }
-
- private void UpdateToggleButtonStates()
- {
- _dayToggle.IsChecked = SelectedType == CalendarDisplayType.Day;
- _weekToggle.IsChecked = SelectedType == CalendarDisplayType.Week;
- _monthToggle.IsChecked = SelectedType == CalendarDisplayType.Month;
- _yearToggle.IsChecked = SelectedType == CalendarDisplayType.Year;
- }
-}
diff --git a/Wino.Calendar/Controls/WinoCalendarView.cs b/Wino.Calendar/Controls/WinoCalendarView.cs
deleted file mode 100644
index 6048d03c..00000000
--- a/Wino.Calendar/Controls/WinoCalendarView.cs
+++ /dev/null
@@ -1,147 +0,0 @@
-using System;
-using System.Windows.Input;
-using CommunityToolkit.Diagnostics;
-using Windows.UI;
-using Windows.UI.Xaml;
-using Windows.UI.Xaml.Controls;
-using Windows.UI.Xaml.Media;
-using Wino.Core.Domain.Models.Calendar;
-using Wino.Helpers;
-
-namespace Wino.Calendar.Controls;
-
-public partial class WinoCalendarView : Control
-{
- private const string PART_DayViewItemBorder = nameof(PART_DayViewItemBorder);
- private const string PART_CalendarView = nameof(PART_CalendarView);
-
- public static readonly DependencyProperty HighlightedDateRangeProperty = DependencyProperty.Register(nameof(HighlightedDateRange), typeof(DateRange), typeof(WinoCalendarView), new PropertyMetadata(null, new PropertyChangedCallback(OnHighlightedDateRangeChanged)));
- public static readonly DependencyProperty VisibleDateBackgroundProperty = DependencyProperty.Register(nameof(VisibleDateBackground), typeof(Brush), typeof(WinoCalendarView), new PropertyMetadata(null, new PropertyChangedCallback(OnPropertiesChanged)));
- public static readonly DependencyProperty DateClickedCommandProperty = DependencyProperty.Register(nameof(DateClickedCommand), typeof(ICommand), typeof(WinoCalendarView), new PropertyMetadata(null));
- public static readonly DependencyProperty TodayBackgroundColorProperty = DependencyProperty.Register(nameof(TodayBackgroundColor), typeof(Color), typeof(WinoCalendarView), new PropertyMetadata(null));
-
- public Color TodayBackgroundColor
- {
- get { return (Color)GetValue(TodayBackgroundColorProperty); }
- set { SetValue(TodayBackgroundColorProperty, value); }
- }
-
- ///
- /// Gets or sets the command to execute when a date is picked.
- /// Unused.
- ///
- public ICommand DateClickedCommand
- {
- get { return (ICommand)GetValue(DateClickedCommandProperty); }
- set { SetValue(DateClickedCommandProperty, value); }
- }
-
- ///
- /// Gets or sets the highlighted range of dates.
- ///
- public DateRange HighlightedDateRange
- {
- get { return (DateRange)GetValue(HighlightedDateRangeProperty); }
- set { SetValue(HighlightedDateRangeProperty, value); }
- }
-
- public Brush VisibleDateBackground
- {
- get { return (Brush)GetValue(VisibleDateBackgroundProperty); }
- set { SetValue(VisibleDateBackgroundProperty, value); }
- }
-
-
-
- private CalendarView CalendarView;
-
- public WinoCalendarView()
- {
- DefaultStyleKey = typeof(WinoCalendarView);
- }
-
- protected override void OnApplyTemplate()
- {
- base.OnApplyTemplate();
-
- CalendarView = GetTemplateChild(PART_CalendarView) as CalendarView;
-
- Guard.IsNotNull(CalendarView, nameof(CalendarView));
-
- CalendarView.SelectedDatesChanged -= InternalCalendarViewSelectionChanged;
- CalendarView.SelectedDatesChanged += InternalCalendarViewSelectionChanged;
-
- // TODO: Should come from settings.
- CalendarView.FirstDayOfWeek = Windows.Globalization.DayOfWeek.Monday;
-
- // Everytime display mode changes, update the visible date range backgrounds.
- // If users go back from year -> month -> day, we need to update the visible date range backgrounds.
-
- CalendarView.RegisterPropertyChangedCallback(CalendarView.DisplayModeProperty, (s, e) => UpdateVisibleDateRangeBackgrounds());
- }
-
- private void InternalCalendarViewSelectionChanged(CalendarView sender, CalendarViewSelectedDatesChangedEventArgs args)
- {
- if (args.AddedDates?.Count > 0)
- {
- var clickedDate = args.AddedDates[0].Date;
- SetInnerDisplayDate(clickedDate);
-
- var clickArgs = new CalendarViewDayClickedEventArgs(clickedDate);
- DateClickedCommand?.Execute(clickArgs);
- }
-
- // Reset selection, we don't show selected dates but react to them.
- CalendarView.SelectedDates.Clear();
- }
-
- private static void OnPropertiesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
- {
- if (d is WinoCalendarView control)
- {
- control.UpdateVisibleDateRangeBackgrounds();
- }
- }
-
- private void SetInnerDisplayDate(DateTime dateTime) => CalendarView?.SetDisplayDate(dateTime);
-
- // Changing selected dates will trigger the selection changed event.
- // It will behave like user clicked the date.
- public void GoToDay(DateTime dateTime) => CalendarView.SelectedDates.Add(dateTime);
-
- private static void OnHighlightedDateRangeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
- {
- if (d is WinoCalendarView control)
- {
- control.SetInnerDisplayDate(control.HighlightedDateRange.StartDate);
- control.UpdateVisibleDateRangeBackgrounds();
- }
- }
-
- public void UpdateVisibleDateRangeBackgrounds()
- {
- if (HighlightedDateRange == null || VisibleDateBackground == null || TodayBackgroundColor == null || CalendarView == null) return;
-
- var markDateCalendarDayItems = WinoVisualTreeHelper.FindDescendants(CalendarView);
-
- foreach (var calendarDayItem in markDateCalendarDayItems)
- {
- var border = WinoVisualTreeHelper.GetChildObject(calendarDayItem, PART_DayViewItemBorder);
-
- if (border == null) return;
-
- if (calendarDayItem.Date.Date == DateTime.Today.Date)
- {
- border.Background = new SolidColorBrush(TodayBackgroundColor);
- }
- else if (calendarDayItem.Date.Date >= HighlightedDateRange.StartDate.Date && calendarDayItem.Date.Date < HighlightedDateRange.EndDate.Date)
- {
- border.Background = VisibleDateBackground;
- }
- else
- {
- border.Background = null;
- }
- }
- }
-}
diff --git a/Wino.Calendar/Controls/WinoDayTimelineCanvas.cs b/Wino.Calendar/Controls/WinoDayTimelineCanvas.cs
deleted file mode 100644
index 66b3bdd8..00000000
--- a/Wino.Calendar/Controls/WinoDayTimelineCanvas.cs
+++ /dev/null
@@ -1,277 +0,0 @@
-using System;
-using System.Diagnostics;
-using Microsoft.Graphics.Canvas.Geometry;
-using Microsoft.Graphics.Canvas.UI.Xaml;
-using Windows.Foundation;
-using Windows.UI.Input;
-using Windows.UI.Xaml;
-using Windows.UI.Xaml.Controls;
-using Windows.UI.Xaml.Media;
-using Wino.Calendar.Args;
-using Wino.Core.Domain.Models.Calendar;
-
-namespace Wino.Calendar.Controls;
-
-public partial class WinoDayTimelineCanvas : Control, IDisposable
-{
- public event EventHandler TimelineCellSelected;
- public event EventHandler TimelineCellUnselected;
-
- private const string PART_InternalCanvas = nameof(PART_InternalCanvas);
- private CanvasControl Canvas;
-
- public static readonly DependencyProperty RenderOptionsProperty = DependencyProperty.Register(nameof(RenderOptions), typeof(CalendarRenderOptions), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
- public static readonly DependencyProperty SeperatorColorProperty = DependencyProperty.Register(nameof(SeperatorColor), typeof(SolidColorBrush), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
- public static readonly DependencyProperty HalfHourSeperatorColorProperty = DependencyProperty.Register(nameof(HalfHourSeperatorColor), typeof(SolidColorBrush), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
- public static readonly DependencyProperty SelectedCellBackgroundBrushProperty = DependencyProperty.Register(nameof(SelectedCellBackgroundBrush), typeof(SolidColorBrush), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
- public static readonly DependencyProperty WorkingHourCellBackgroundColorProperty = DependencyProperty.Register(nameof(WorkingHourCellBackgroundColor), typeof(SolidColorBrush), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
- public static readonly DependencyProperty SelectedDateTimeProperty = DependencyProperty.Register(nameof(SelectedDateTime), typeof(DateTime?), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnSelectedDateTimeChanged)));
- public static readonly DependencyProperty PositionerUIElementProperty = DependencyProperty.Register(nameof(PositionerUIElement), typeof(UIElement), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null));
-
- public UIElement PositionerUIElement
- {
- get { return (UIElement)GetValue(PositionerUIElementProperty); }
- set { SetValue(PositionerUIElementProperty, value); }
- }
-
- public CalendarRenderOptions RenderOptions
- {
- get { return (CalendarRenderOptions)GetValue(RenderOptionsProperty); }
- set { SetValue(RenderOptionsProperty, value); }
- }
-
- public SolidColorBrush HalfHourSeperatorColor
- {
- get { return (SolidColorBrush)GetValue(HalfHourSeperatorColorProperty); }
- set { SetValue(HalfHourSeperatorColorProperty, value); }
- }
-
- public SolidColorBrush SeperatorColor
- {
- get { return (SolidColorBrush)GetValue(SeperatorColorProperty); }
- set { SetValue(SeperatorColorProperty, value); }
- }
-
- public SolidColorBrush WorkingHourCellBackgroundColor
- {
- get { return (SolidColorBrush)GetValue(WorkingHourCellBackgroundColorProperty); }
- set { SetValue(WorkingHourCellBackgroundColorProperty, value); }
- }
-
- public SolidColorBrush SelectedCellBackgroundBrush
- {
- get { return (SolidColorBrush)GetValue(SelectedCellBackgroundBrushProperty); }
- set { SetValue(SelectedCellBackgroundBrushProperty, value); }
- }
-
- public DateTime? SelectedDateTime
- {
- get { return (DateTime?)GetValue(SelectedDateTimeProperty); }
- set { SetValue(SelectedDateTimeProperty, value); }
- }
-
- protected override void OnApplyTemplate()
- {
- base.OnApplyTemplate();
-
- Canvas = GetTemplateChild(PART_InternalCanvas) as CanvasControl;
-
- // TODO: These will leak. Dispose them properly when needed.
- Canvas.Draw += OnCanvasDraw;
- Canvas.PointerPressed += OnCanvasPointerPressed;
-
- ForceDraw();
- }
-
- private static void OnSelectedDateTimeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
- {
- if (d is WinoDayTimelineCanvas control)
- {
- if (e.OldValue != null && e.NewValue == null)
- {
- control.RaiseCellUnselected();
- }
-
- control.ForceDraw();
- }
- }
-
- private void RaiseCellUnselected()
- {
- TimelineCellUnselected?.Invoke(this, new TimelineCellUnselectedArgs());
- }
-
- private void OnCanvasPointerPressed(object sender, Windows.UI.Xaml.Input.PointerRoutedEventArgs e)
- {
- if (RenderOptions == null) return;
-
- var hourHeight = RenderOptions.CalendarSettings.HourHeight;
-
- // When users click to cell we need to find the day, hour and minutes (first 30 minutes or second 30 minutes) that it represents on the timeline.
-
- PointerPoint positionerRootPoint = e.GetCurrentPoint(PositionerUIElement);
- PointerPoint canvasPointerPoint = e.GetCurrentPoint(Canvas);
-
- Point touchPoint = canvasPointerPoint.Position;
-
- var singleDayWidth = (Canvas.ActualWidth / RenderOptions.TotalDayCount);
-
- int day = (int)(touchPoint.X / singleDayWidth);
- int hour = (int)(touchPoint.Y / hourHeight);
-
- bool isSecondHalf = touchPoint.Y % hourHeight > (hourHeight / 2);
-
- var diffX = positionerRootPoint.Position.X - touchPoint.X;
- var diffY = positionerRootPoint.Position.Y - touchPoint.Y;
-
- var cellStartRelativePositionX = diffX + (day * singleDayWidth);
- var cellEndRelativePositionX = cellStartRelativePositionX + singleDayWidth;
-
- var cellStartRelativePositionY = diffY + (hour * hourHeight) + (isSecondHalf ? hourHeight / 2 : 0);
- var cellEndRelativePositionY = cellStartRelativePositionY + (isSecondHalf ? (hourHeight / 2) : hourHeight);
-
- var cellSize = new Size(cellEndRelativePositionX - cellStartRelativePositionX, hourHeight / 2);
- var positionerPoint = new Point(cellStartRelativePositionX, cellStartRelativePositionY);
-
- var clickedDateTime = RenderOptions.DateRange.StartDate.AddDays(day).AddHours(hour).AddMinutes(isSecondHalf ? 30 : 0);
-
- // If there is already a selected date, in order to mimic the popup behavior, we need to dismiss the previous selection first.
- // Next click will be a new selection.
-
- // Raise the events directly here instead of DP to not lose pointer position.
- if (clickedDateTime == SelectedDateTime || SelectedDateTime != null)
- {
- SelectedDateTime = null;
- }
- else
- {
- SelectedDateTime = clickedDateTime;
- TimelineCellSelected?.Invoke(this, new TimelineCellSelectedArgs(clickedDateTime, touchPoint, positionerPoint, cellSize));
- }
-
- Debug.WriteLine($"Clicked: {clickedDateTime}");
- }
-
- public WinoDayTimelineCanvas()
- {
- DefaultStyleKey = typeof(WinoDayTimelineCanvas);
- }
-
- private static void OnRenderingPropertiesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
- {
- if (d is WinoDayTimelineCanvas control)
- {
- control.ForceDraw();
- }
- }
-
- private void ForceDraw() => Canvas?.Invalidate();
-
- private bool CanDrawTimeline()
- {
- return RenderOptions != null
- && Canvas != null
- && Canvas.ReadyToDraw
- && WorkingHourCellBackgroundColor != null
- && SeperatorColor != null
- && HalfHourSeperatorColor != null
- && SelectedCellBackgroundBrush != null;
- }
-
- private void OnCanvasDraw(CanvasControl sender, CanvasDrawEventArgs args)
- {
- if (!CanDrawTimeline()) return;
-
- int hours = 24;
-
- double canvasWidth = Canvas.ActualWidth;
- double canvasHeight = Canvas.ActualHeight;
-
- if (canvasWidth == 0 || canvasHeight == 0) return;
-
- // Calculate the width of each rectangle (1 day column)
- // Equal distribution of the whole width.
- double rectWidth = canvasWidth / RenderOptions.TotalDayCount;
-
- // Calculate the height of each rectangle (1 hour row)
- double rectHeight = RenderOptions.CalendarSettings.HourHeight;
-
- // Define stroke and fill colors
- var strokeColor = SeperatorColor.Color;
- float strokeThickness = 0.5f;
-
- for (int day = 0; day < RenderOptions.TotalDayCount; day++)
- {
- var currentDay = RenderOptions.DateRange.StartDate.AddDays(day);
-
- bool isWorkingDay = RenderOptions.CalendarSettings.WorkingDays.Contains(currentDay.DayOfWeek);
-
- // Loop through each hour (rows)
- for (int hour = 0; hour < hours; hour++)
- {
- var renderTime = TimeSpan.FromHours(hour);
-
- var representingDateTime = currentDay.AddHours(hour);
-
- // Calculate the position and size of the rectangle
- double x = day * rectWidth;
- double y = hour * rectHeight;
-
- var rectangle = new Rect(x, y, rectWidth, rectHeight);
-
- // Draw the rectangle border.
- // This is the main rectangle.
- args.DrawingSession.DrawRectangle(rectangle, strokeColor, strokeThickness);
-
- // Fill another rectangle with the working hour background color
- // This rectangle must be placed with -1 margin to prevent invisible borders of the main rectangle.
- if (isWorkingDay && renderTime >= RenderOptions.CalendarSettings.WorkingHourStart && renderTime <= RenderOptions.CalendarSettings.WorkingHourEnd)
- {
- var backgroundRectangle = new Rect(x + 1, y + 1, rectWidth - 1, rectHeight - 1);
-
- args.DrawingSession.DrawRectangle(backgroundRectangle, strokeColor, strokeThickness);
- args.DrawingSession.FillRectangle(backgroundRectangle, WorkingHourCellBackgroundColor.Color);
- }
-
- // Draw a line in the center of the rectangle for representing half hours.
- double lineY = y + rectHeight / 2;
-
- args.DrawingSession.DrawLine((float)x, (float)lineY, (float)(x + rectWidth), (float)lineY, HalfHourSeperatorColor.Color, strokeThickness, new CanvasStrokeStyle()
- {
- DashStyle = CanvasDashStyle.Dot
- });
- }
-
- // Draw selected item background color for the date if possible.
- if (SelectedDateTime != null)
- {
- var selectedDateTime = SelectedDateTime.Value;
- if (selectedDateTime.Date == currentDay.Date)
- {
- var selectionRectHeight = rectHeight / 2;
- var selectedY = selectedDateTime.Hour * rectHeight + (selectedDateTime.Minute / 60) * rectHeight;
-
- // Second half of the hour is selected.
- if (selectedDateTime.TimeOfDay.Minutes == 30)
- {
- selectedY += rectHeight / 2;
- }
-
- var selectedRectangle = new Rect(day * rectWidth, selectedY, rectWidth, selectionRectHeight);
- args.DrawingSession.FillRectangle(selectedRectangle, SelectedCellBackgroundBrush.Color);
- }
- }
- }
- }
-
- public void Dispose()
- {
- if (Canvas == null) return;
-
- Canvas.Draw -= OnCanvasDraw;
- Canvas.PointerPressed -= OnCanvasPointerPressed;
- Canvas.RemoveFromVisualTree();
-
- Canvas = null;
- }
-}
diff --git a/Wino.Calendar/MainPage.xaml.cs b/Wino.Calendar/MainPage.xaml.cs
deleted file mode 100644
index 7fb5bdf3..00000000
--- a/Wino.Calendar/MainPage.xaml.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Runtime.InteropServices.WindowsRuntime;
-using Windows.Foundation;
-using Windows.Foundation.Collections;
-using Windows.UI.Xaml;
-using Windows.UI.Xaml.Controls;
-using Windows.UI.Xaml.Controls.Primitives;
-using Windows.UI.Xaml.Data;
-using Windows.UI.Xaml.Input;
-using Windows.UI.Xaml.Media;
-using Windows.UI.Xaml.Navigation;
-
-// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409
-
-namespace Wino.Calendar;
-
-///
-/// An empty page that can be used on its own or navigated to within a Frame.
-///
-public sealed partial class MainPage : Page
-{
- public MainPage()
- {
- this.InitializeComponent();
- }
-}
diff --git a/Wino.Calendar/Package.appxmanifest b/Wino.Calendar/Package.appxmanifest
deleted file mode 100644
index 7d96dede..00000000
--- a/Wino.Calendar/Package.appxmanifest
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Wino Calendar
- Burak KÖSE
- Assets\StoreLogo.png
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Google Auth Protocol
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Wino.Calendar/Properties/launchSettings.json b/Wino.Calendar/Properties/launchSettings.json
deleted file mode 100644
index 0f9c975c..00000000
--- a/Wino.Calendar/Properties/launchSettings.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "profiles": {
- "Wino.Calendar": {
- "commandName": "MsixPackage"
- }
- }
-}
diff --git a/Wino.Calendar/Services/AccountCalendarStateService.cs b/Wino.Calendar/Services/AccountCalendarStateService.cs
deleted file mode 100644
index cf58b2f3..00000000
--- a/Wino.Calendar/Services/AccountCalendarStateService.cs
+++ /dev/null
@@ -1,113 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Linq;
-using CommunityToolkit.Mvvm.ComponentModel;
-using Wino.Calendar.ViewModels.Data;
-using Wino.Calendar.ViewModels.Interfaces;
-using Wino.Core.Domain.Entities.Shared;
-
-namespace Wino.Calendar.Services;
-
-///
-/// Encapsulated state manager for collectively managing the state of account calendars.
-/// Callers must react to the events to update their state only from this service.
-///
-public partial class AccountCalendarStateService : ObservableObject, IAccountCalendarStateService
-{
- public event EventHandler CollectiveAccountGroupSelectionStateChanged;
- public event EventHandler AccountCalendarSelectionStateChanged;
-
- [ObservableProperty]
- public partial ReadOnlyObservableCollection GroupedAccountCalendars { get; set; }
-
- private ObservableCollection _internalGroupedAccountCalendars = new ObservableCollection();
-
- public IEnumerable ActiveCalendars
- {
- get
- {
- return GroupedAccountCalendars
- .SelectMany(a => a.AccountCalendars)
- .Where(b => b.IsChecked);
- }
- }
-
- public IEnumerable> GroupedAccountCalendarsEnumerable
- {
- get
- {
- return GroupedAccountCalendars
- .Select(a => a.AccountCalendars)
- .SelectMany(b => b)
- .GroupBy(c => c.Account);
- }
- }
-
- public AccountCalendarStateService()
- {
- GroupedAccountCalendars = new ReadOnlyObservableCollection(_internalGroupedAccountCalendars);
- }
-
- private void SingleGroupCalendarCollectiveStateChanged(object sender, EventArgs e)
- => CollectiveAccountGroupSelectionStateChanged?.Invoke(this, sender as GroupedAccountCalendarViewModel);
-
- private void SingleCalendarSelectionStateChanged(object sender, AccountCalendarViewModel e)
- => AccountCalendarSelectionStateChanged?.Invoke(this, e);
-
- public void AddGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar)
- {
- groupedAccountCalendar.CalendarSelectionStateChanged += SingleCalendarSelectionStateChanged;
- groupedAccountCalendar.CollectiveSelectionStateChanged += SingleGroupCalendarCollectiveStateChanged;
-
- _internalGroupedAccountCalendars.Add(groupedAccountCalendar);
- }
-
- public void RemoveGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar)
- {
- groupedAccountCalendar.CalendarSelectionStateChanged -= SingleCalendarSelectionStateChanged;
- groupedAccountCalendar.CollectiveSelectionStateChanged -= SingleGroupCalendarCollectiveStateChanged;
-
- _internalGroupedAccountCalendars.Remove(groupedAccountCalendar);
- }
-
- public void ClearGroupedAccountCalendar()
- {
- foreach (var groupedAccountCalendar in _internalGroupedAccountCalendars)
- {
- RemoveGroupedAccountCalendar(groupedAccountCalendar);
- }
- }
-
- public void AddAccountCalendar(AccountCalendarViewModel accountCalendar)
- {
- // Find the group that this calendar belongs to.
- var group = _internalGroupedAccountCalendars.FirstOrDefault(g => g.Account.Id == accountCalendar.Account.Id);
-
- if (group == null)
- {
- // If the group doesn't exist, create it.
- group = new GroupedAccountCalendarViewModel(accountCalendar.Account, new[] { accountCalendar });
- AddGroupedAccountCalendar(group);
- }
- else
- {
- group.AccountCalendars.Add(accountCalendar);
- }
- }
-
- public void RemoveAccountCalendar(AccountCalendarViewModel accountCalendar)
- {
- var group = _internalGroupedAccountCalendars.FirstOrDefault(g => g.Account.Id == accountCalendar.Account.Id);
-
- // We don't expect but just in case.
- if (group == null) return;
-
- group.AccountCalendars.Remove(accountCalendar);
-
- if (group.AccountCalendars.Count == 0)
- {
- RemoveGroupedAccountCalendar(group);
- }
- }
-}
diff --git a/Wino.Calendar/Services/CalendarAuthenticatorConfig.cs b/Wino.Calendar/Services/CalendarAuthenticatorConfig.cs
deleted file mode 100644
index 391ff7f2..00000000
--- a/Wino.Calendar/Services/CalendarAuthenticatorConfig.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using Wino.Core.Domain.Interfaces;
-
-namespace Wino.Calendar.Services;
-
-public class CalendarAuthenticatorConfig : IAuthenticatorConfig
-{
- public string OutlookAuthenticatorClientId => "b19c2035-d740-49ff-b297-de6ec561b208";
-
- public string[] OutlookScope => new string[]
- {
- "Calendars.Read",
- "Calendars.Read.Shared",
- "offline_access",
- "Calendars.ReadBasic",
- "Calendars.ReadWrite",
- "Calendars.ReadWrite.Shared",
- "User.Read"
- };
-
- public string GmailAuthenticatorClientId => "973025879644-s7b4ur9p3rlgop6a22u7iuptdc0brnrn.apps.googleusercontent.com";
-
- public string[] GmailScope => new string[]
- {
- "https://www.googleapis.com/auth/calendar",
- "https://www.googleapis.com/auth/calendar.events",
- "https://www.googleapis.com/auth/calendar.settings.readonly",
- "https://www.googleapis.com/auth/userinfo.profile",
- "https://www.googleapis.com/auth/userinfo.email"
- };
-
- public string GmailTokenStoreIdentifier => "WinoCalendarGmailTokenStore";
-}
diff --git a/Wino.Calendar/Services/DialogService.cs b/Wino.Calendar/Services/DialogService.cs
deleted file mode 100644
index 5a8c8965..00000000
--- a/Wino.Calendar/Services/DialogService.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using Windows.UI.Xaml;
-using Wino.Core.Domain.Interfaces;
-using Wino.Core.UWP.Services;
-
-namespace Wino.Calendar.Services;
-
-public class DialogService : DialogServiceBase, ICalendarDialogService
-{
- public DialogService(IThemeService themeService,
- IConfigurationService configurationService,
- IApplicationResourceManager applicationResourceManager) : base(themeService, configurationService, applicationResourceManager)
- {
- }
-}
diff --git a/Wino.Calendar/Services/NavigationService.cs b/Wino.Calendar/Services/NavigationService.cs
deleted file mode 100644
index f409c4b3..00000000
--- a/Wino.Calendar/Services/NavigationService.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-using System;
-using Windows.UI.Xaml;
-using Windows.UI.Xaml.Controls;
-using Wino.Calendar.Views;
-using Wino.Calendar.Views.Account;
-using Wino.Calendar.Views.Settings;
-using Wino.Core.Domain.Enums;
-using Wino.Core.Domain.Interfaces;
-using Wino.Core.Domain.Models.Navigation;
-using Wino.Core.UWP.Services;
-using Wino.Views;
-
-namespace Wino.Calendar.Services;
-
-public class NavigationService : NavigationServiceBase, INavigationService
-{
- public Type GetPageType(WinoPage winoPage)
- {
- return winoPage switch
- {
- WinoPage.CalendarPage => typeof(CalendarPage),
- WinoPage.SettingsPage => typeof(SettingsPage),
- WinoPage.CalendarSettingsPage => typeof(CalendarSettingsPage),
- WinoPage.AccountManagementPage => typeof(AccountManagementPage),
- WinoPage.ManageAccountsPage => typeof(ManageAccountsPage),
- WinoPage.PersonalizationPage => typeof(PersonalizationPage),
- WinoPage.AccountDetailsPage => typeof(AccountDetailsPage),
- WinoPage.EventDetailsPage => typeof(EventDetailsPage),
- _ => throw new Exception("Page is not implemented yet."),
- };
- }
-
- public void GoBack()
- {
- if (Window.Current.Content is Frame appFrame && appFrame.Content is AppShell shellPage)
- {
- var shellFrame = shellPage.GetShellFrame();
-
- if (shellFrame.CanGoBack)
- {
- shellFrame.GoBack();
- }
- }
- }
-
- public bool Navigate(WinoPage page, object parameter = null, NavigationReferenceFrame frame = NavigationReferenceFrame.ShellFrame, NavigationTransitionType transition = NavigationTransitionType.None)
- {
- // All navigations are performed on shell frame for calendar.
-
- if (Window.Current.Content is Frame appFrame && appFrame.Content is AppShell shellPage)
- {
- var shellFrame = shellPage.GetShellFrame();
-
- var pageType = GetPageType(page);
-
- shellFrame.Navigate(pageType, parameter);
- return true;
- }
-
- return false;
- }
-}
diff --git a/Wino.Calendar/Services/ProviderService.cs b/Wino.Calendar/Services/ProviderService.cs
deleted file mode 100644
index e0cac8ad..00000000
--- a/Wino.Calendar/Services/ProviderService.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using Wino.Core.Domain.Enums;
-using Wino.Core.Domain.Interfaces;
-using Wino.Core.Domain.Models.Accounts;
-
-namespace Wino.Calendar.Services;
-
-public class ProviderService : IProviderService
-{
- public IProviderDetail GetProviderDetail(MailProviderType type)
- {
- var details = GetAvailableProviders();
-
- return details.FirstOrDefault(a => a.Type == type);
- }
-
- public List GetAvailableProviders()
- {
- var providerList = new List();
-
- var providers = new MailProviderType[]
- {
- MailProviderType.Outlook,
- MailProviderType.Gmail
- };
-
- foreach (var type in providers)
- {
- providerList.Add(new ProviderDetail(type, SpecialImapProvider.None));
- }
-
- return providerList;
- }
-}
diff --git a/Wino.Calendar/Services/SettingsBuilderService.cs b/Wino.Calendar/Services/SettingsBuilderService.cs
deleted file mode 100644
index 953a4031..00000000
--- a/Wino.Calendar/Services/SettingsBuilderService.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using System.Collections.Generic;
-using Wino.Core.Domain;
-using Wino.Core.Domain.Enums;
-using Wino.Core.Domain.Interfaces;
-using Wino.Core.Domain.Models.Settings;
-
-namespace Wino.Calendar.Services;
-
-public class SettingsBuilderService : ISettingsBuilderService
-{
- public List GetSettingItems()
- => new List()
- {
- new SettingOption(Translator.SettingsManageAccountSettings_Title, Translator.SettingsManageAccountSettings_Description, WinoPage.AccountManagementPage,"F1 M 3.75 5 L 3.75 4.902344 C 3.75 4.225262 3.885091 3.588867 4.155273 2.993164 C 4.425456 2.397461 4.790039 1.878256 5.249023 1.435547 C 5.708008 0.99284 6.238606 0.642904 6.84082 0.385742 C 7.443034 0.128582 8.079427 0 8.75 0 C 9.420572 0 10.056966 0.128582 10.65918 0.385742 C 11.261393 0.642904 11.791992 0.99284 12.250977 1.435547 C 12.709961 1.878256 13.074544 2.397461 13.344727 2.993164 C 13.614908 3.588867 13.75 4.225262 13.75 4.902344 C 13.75 5.397137 13.689778 5.87077 13.569336 6.323242 C 13.448893 6.775717 13.258463 7.213542 12.998047 7.636719 C 12.229817 7.799479 11.510416 8.079428 10.839844 8.476562 C 10.169271 8.873698 9.583333 9.378256 9.082031 9.990234 C 9.042969 9.996745 9.005533 10 8.969727 10 C 8.933919 10 8.899739 10 8.867188 10 L 8.652344 10 C 7.97526 10 7.338867 9.864909 6.743164 9.594727 C 6.147461 9.324545 5.628255 8.959961 5.185547 8.500977 C 4.742838 8.041993 4.392903 7.511395 4.135742 6.90918 C 3.878581 6.306967 3.75 5.670573 3.75 5 Z M 12.5 5 L 12.5 4.921875 C 12.5 4.414062 12.399088 3.937176 12.197266 3.491211 C 11.995442 3.045248 11.722005 2.65625 11.376953 2.324219 C 11.0319 1.992188 10.633138 1.730145 10.180664 1.538086 C 9.728189 1.346029 9.251302 1.25 8.75 1.25 C 8.229166 1.25 7.740885 1.347656 7.285156 1.542969 C 6.829427 1.738281 6.432292 2.005209 6.09375 2.34375 C 5.755208 2.682293 5.488281 3.079428 5.292969 3.535156 C 5.097656 3.990887 5 4.479167 5 5 C 5 5.520834 5.097656 6.009115 5.292969 6.464844 C 5.488281 6.920573 5.755208 7.317709 6.09375 7.65625 C 6.432292 7.994792 6.829427 8.261719 7.285156 8.457031 C 7.740885 8.652344 8.229166 8.75 8.75 8.75 C 9.270833 8.75 9.759114 8.652344 10.214844 8.457031 C 10.670572 8.261719 11.067708 7.994792 11.40625 7.65625 C 11.744791 7.317709 12.011719 6.920573 12.207031 6.464844 C 12.402344 6.009115 12.5 5.520834 12.5 5 Z M 12.148438 19.365234 C 11.777344 19.208984 11.41276 19.010416 11.054688 18.769531 C 11.113281 18.561197 11.170247 18.357748 11.225586 18.15918 C 11.280924 17.960611 11.308594 17.753906 11.308594 17.539062 C 11.308594 17.252604 11.259766 16.977539 11.162109 16.713867 C 11.064453 16.450195 10.930989 16.210938 10.761719 15.996094 C 10.592447 15.78125 10.390625 15.597331 10.15625 15.444336 C 9.921875 15.291342 9.664713 15.182292 9.384766 15.117188 L 8.779297 14.980469 C 8.759766 14.778646 8.75 14.576823 8.75 14.375 C 8.75 14.179688 8.759766 13.977865 8.779297 13.769531 L 9.384766 13.632812 C 9.664713 13.567709 9.921875 13.458659 10.15625 13.305664 C 10.390625 13.15267 10.592447 12.96875 10.761719 12.753906 C 10.930989 12.539062 11.064453 12.299805 11.162109 12.036133 C 11.259766 11.772461 11.308594 11.497396 11.308594 11.210938 C 11.308594 10.996094 11.280924 10.789389 11.225586 10.59082 C 11.170247 10.392253 11.113281 10.188803 11.054688 9.980469 C 11.41276 9.739584 11.777344 9.541016 12.148438 9.384766 C 12.324219 9.560547 12.488606 9.720053 12.641602 9.863281 C 12.794596 10.006511 12.957356 10.131836 13.129883 10.239258 C 13.302408 10.34668 13.487955 10.429688 13.686523 10.488281 C 13.885091 10.546875 14.114583 10.576172 14.375 10.576172 C 14.635416 10.576172 14.866535 10.546875 15.068359 10.488281 C 15.270182 10.429688 15.455729 10.34668 15.625 10.239258 C 15.79427 10.131836 15.955402 10.006511 16.108398 9.863281 C 16.261393 9.720053 16.425781 9.560547 16.601562 9.384766 C 16.972656 9.541016 17.337238 9.739584 17.695312 9.980469 C 17.636719 10.188803 17.579752 10.392253 17.524414 10.59082 C 17.469074 10.789389 17.441406 10.996094 17.441406 11.210938 C 17.441406 11.497396 17.488605 11.772461 17.583008 12.036133 C 17.677408 12.299805 17.810871 12.539062 17.983398 12.753906 C 18.155924 12.96875 18.359375 13.15267 18.59375 13.305664 C 18.828125 13.458659 19.085285 13.567709 19.365234 13.632812 L 19.970703 13.769531 C 19.990234 13.977865 20 14.179688 20 14.375 C 20 14.576823 19.990234 14.778646 19.970703 14.980469 L 19.365234 15.117188 C 19.085285 15.182292 18.828125 15.291342 18.59375 15.444336 C 18.359375 15.597331 18.155924 15.78125 17.983398 15.996094 C 17.810871 16.210938 17.677408 16.450195 17.583008 16.713867 C 17.488605 16.977539 17.441406 17.252604 17.441406 17.539062 C 17.441406 17.753906 17.469074 17.960611 17.524414 18.15918 C 17.579752 18.357748 17.636719 18.561197 17.695312 18.769531 C 17.337238 19.010416 16.972656 19.208984 16.601562 19.365234 C 16.425781 19.189453 16.261393 19.029947 16.108398 18.886719 C 15.955402 18.74349 15.79427 18.618164 15.625 18.510742 C 15.455729 18.40332 15.270182 18.320312 15.068359 18.261719 C 14.866535 18.203125 14.635416 18.173828 14.375 18.173828 C 14.108072 18.173828 13.875324 18.203125 13.676758 18.261719 C 13.478189 18.320312 13.294271 18.40332 13.125 18.510742 C 12.955729 18.618164 12.794596 18.74349 12.641602 18.886719 C 12.488606 19.029947 12.324219 19.189453 12.148438 19.365234 Z M 0 13.75 C 0 13.404948 0.065104 13.081055 0.195312 12.77832 C 0.325521 12.475586 0.504557 12.210287 0.732422 11.982422 C 0.960286 11.754558 1.225586 11.575521 1.52832 11.445312 C 1.831055 11.315104 2.154948 11.25 2.5 11.25 L 8.251953 11.25 C 8.043619 11.660156 7.880859 12.076823 7.763672 12.5 L 2.5 12.5 C 2.324219 12.5 2.159831 12.532553 2.006836 12.597656 C 1.853841 12.662761 1.722005 12.750651 1.611328 12.861328 C 1.500651 12.972006 1.41276 13.103842 1.347656 13.256836 C 1.282552 13.409831 1.25 13.574219 1.25 13.75 C 1.25 14.420573 1.360677 15.008139 1.582031 15.512695 C 1.803385 16.017252 2.102865 16.455078 2.480469 16.826172 C 2.858073 17.197266 3.297526 17.50651 3.798828 17.753906 C 4.30013 18.001303 4.827474 18.198242 5.380859 18.344727 C 5.934244 18.491211 6.50065 18.595377 7.080078 18.657227 C 7.659505 18.719076 8.216146 18.75 8.75 18.75 L 9.072266 18.75 C 9.449869 19.199219 9.866536 19.5931 10.322266 19.931641 C 10.061849 19.964193 9.801432 19.983725 9.541016 19.990234 C 9.280599 19.996744 9.016927 20 8.75 20 C 8.190104 20 7.618814 19.973959 7.036133 19.921875 C 6.45345 19.869791 5.878906 19.775391 5.3125 19.638672 C 4.746094 19.501953 4.197591 19.319662 3.666992 19.091797 C 3.136393 18.863932 2.646484 18.574219 2.197266 18.222656 C 1.474609 17.66276 0.927734 17.005209 0.556641 16.25 C 0.185547 15.494792 0 14.661458 0 13.75 Z M 15.625 14.375 C 15.625 14.205729 15.592447 14.044597 15.527344 13.891602 C 15.462239 13.738607 15.372721 13.605144 15.258789 13.491211 C 15.144855 13.377279 15.011393 13.287761 14.858398 13.222656 C 14.705403 13.157553 14.544271 13.125 14.375 13.125 C 14.199219 13.125 14.036458 13.157553 13.886719 13.222656 C 13.736979 13.287761 13.605143 13.377279 13.491211 13.491211 C 13.377278 13.605144 13.28776 13.736979 13.222656 13.886719 C 13.157551 14.036459 13.124999 14.199219 13.125 14.375 C 13.124999 14.550781 13.157551 14.71517 13.222656 14.868164 C 13.28776 15.021159 13.37565 15.152995 13.486328 15.263672 C 13.597004 15.37435 13.72884 15.46224 13.881836 15.527344 C 14.03483 15.592448 14.199219 15.625 14.375 15.625 C 14.550781 15.625 14.713541 15.592448 14.863281 15.527344 C 15.01302 15.46224 15.144855 15.372722 15.258789 15.258789 C 15.372721 15.144857 15.462239 15.013021 15.527344 14.863281 C 15.592447 14.713542 15.625 14.550781 15.625 14.375 Z "),
- new SettingOption(Translator.SettingsAppPreferences_Title, Translator.SettingsAppPreferences_Description, WinoPage.AppPreferencesPage,"F1 M 15.078125 1.25 C 15.566406 1.25 16.033527 1.349285 16.479492 1.547852 C 16.925455 1.74642 17.31608 2.013348 17.651367 2.348633 C 17.986652 2.68392 18.25358 3.074545 18.452148 3.520508 C 18.650715 3.966473 18.75 4.433594 18.75 4.921875 L 18.75 15.078125 C 18.75 15.566406 18.650715 16.033529 18.452148 16.479492 C 18.25358 16.925455 17.986652 17.31608 17.651367 17.651367 C 17.31608 17.986654 16.925455 18.25358 16.479492 18.452148 C 16.033527 18.650717 15.566406 18.75 15.078125 18.75 L 4.921875 18.75 C 4.433594 18.75 3.966471 18.650717 3.520508 18.452148 C 3.074544 18.25358 2.683919 17.986654 2.348633 17.651367 C 2.013346 17.31608 1.746419 16.925455 1.547852 16.479492 C 1.349284 16.033529 1.25 15.566406 1.25 15.078125 L 1.25 4.921875 C 1.25 4.433594 1.349284 3.966473 1.547852 3.520508 C 1.746419 3.074545 2.013346 2.68392 2.348633 2.348633 C 2.683919 2.013348 3.074544 1.74642 3.520508 1.547852 C 3.966471 1.349285 4.433594 1.25 4.921875 1.25 Z M 4.951172 2.5 C 4.625651 2.5 4.314778 2.566732 4.018555 2.700195 C 3.722331 2.83366 3.461914 3.012695 3.237305 3.237305 C 3.012695 3.461914 2.833659 3.722332 2.700195 4.018555 C 2.566732 4.314779 2.5 4.625651 2.5 4.951172 L 2.5 5 L 17.5 5 L 17.5 4.951172 C 17.5 4.625651 17.433268 4.314779 17.299805 4.018555 C 17.16634 3.722332 16.987305 3.461914 16.762695 3.237305 C 16.538086 3.012695 16.277668 2.83366 15.981445 2.700195 C 15.685221 2.566732 15.374349 2.5 15.048828 2.5 Z M 15.048828 17.5 C 15.374349 17.5 15.685221 17.433268 15.981445 17.299805 C 16.277668 17.166342 16.538086 16.987305 16.762695 16.762695 C 16.987305 16.538086 17.16634 16.27767 17.299805 15.981445 C 17.433268 15.685222 17.5 15.37435 17.5 15.048828 L 17.5 6.25 L 2.5 6.25 L 2.5 15.048828 C 2.5 15.37435 2.566732 15.685222 2.700195 15.981445 C 2.833659 16.27767 3.012695 16.538086 3.237305 16.762695 C 3.461914 16.987305 3.722331 17.166342 4.018555 17.299805 C 4.314778 17.433268 4.625651 17.5 4.951172 17.5 Z M 12.724609 8.935547 C 12.724609 9.195964 12.762044 9.446615 12.836914 9.6875 C 12.911783 9.928386 13.020832 10.151367 13.164062 10.356445 C 13.307291 10.561523 13.476562 10.742188 13.671875 10.898438 C 13.867188 11.054688 14.088541 11.178386 14.335938 11.269531 C 14.348957 11.367188 14.358723 11.466472 14.365234 11.567383 C 14.371744 11.668295 14.375 11.770834 14.375 11.875 C 14.375 11.979167 14.371744 12.081706 14.365234 12.182617 C 14.358723 12.283529 14.348957 12.382812 14.335938 12.480469 C 14.088541 12.571615 13.867188 12.695312 13.671875 12.851562 C 13.476562 13.007812 13.307291 13.188477 13.164062 13.393555 C 13.020832 13.598633 12.911783 13.821615 12.836914 14.0625 C 12.762044 14.303386 12.724609 14.554037 12.724609 14.814453 C 12.724609 14.886068 12.727864 14.960938 12.734375 15.039062 C 12.740885 15.117188 12.75065 15.192058 12.763672 15.263672 C 12.470703 15.511068 12.145182 15.712891 11.787109 15.869141 C 11.546224 15.621745 11.274414 15.43457 10.97168 15.307617 C 10.668945 15.180664 10.345052 15.117188 10 15.117188 C 9.654947 15.117188 9.331055 15.180664 9.02832 15.307617 C 8.725586 15.43457 8.453775 15.621745 8.212891 15.869141 C 7.854817 15.712891 7.529296 15.511068 7.236328 15.263672 C 7.249349 15.192058 7.259114 15.117188 7.265625 15.039062 C 7.272135 14.960938 7.275391 14.886068 7.275391 14.814453 C 7.275391 14.554037 7.237956 14.303386 7.163086 14.0625 C 7.088216 13.821615 6.979167 13.598633 6.835938 13.393555 C 6.692708 13.188477 6.52181 13.007812 6.323242 12.851562 C 6.124674 12.695312 5.904948 12.571615 5.664062 12.480469 C 5.651042 12.382812 5.641276 12.283529 5.634766 12.182617 C 5.628255 12.081706 5.625 11.979167 5.625 11.875 C 5.625 11.770834 5.628255 11.668295 5.634766 11.567383 C 5.641276 11.466472 5.651042 11.367188 5.664062 11.269531 C 5.904948 11.178386 6.124674 11.054688 6.323242 10.898438 C 6.52181 10.742188 6.692708 10.561523 6.835938 10.356445 C 6.979167 10.151367 7.088216 9.928386 7.163086 9.6875 C 7.237956 9.446615 7.275391 9.195964 7.275391 8.935547 C 7.275391 8.863933 7.272135 8.789062 7.265625 8.710938 C 7.259114 8.632812 7.249349 8.557943 7.236328 8.486328 C 7.529296 8.238933 7.854817 8.037109 8.212891 7.880859 C 8.440755 8.121745 8.712564 8.307292 9.02832 8.4375 C 9.344075 8.567709 9.667969 8.632812 10 8.632812 C 10.332031 8.632812 10.655924 8.567709 10.97168 8.4375 C 11.287435 8.307292 11.559244 8.121745 11.787109 7.880859 C 12.145182 8.037109 12.470703 8.238933 12.763672 8.486328 C 12.75065 8.557943 12.740885 8.632812 12.734375 8.710938 C 12.727864 8.789062 12.724609 8.863933 12.724609 8.935547 Z M 10.9375 11.875 C 10.9375 11.614584 10.846354 11.393229 10.664062 11.210938 C 10.481771 11.028646 10.260416 10.9375 10 10.9375 C 9.739583 10.9375 9.518229 11.028646 9.335938 11.210938 C 9.153646 11.393229 9.0625 11.614584 9.0625 11.875 C 9.0625 12.135417 9.153646 12.356771 9.335938 12.539062 C 9.518229 12.721354 9.739583 12.8125 10 12.8125 C 10.260416 12.8125 10.481771 12.721354 10.664062 12.539062 C 10.846354 12.356771 10.9375 12.135417 10.9375 11.875 Z "),
- new SettingOption(Translator.SettingsPersonalization_Title, Translator.SettingsPersonalization_Description, WinoPage.PersonalizationPage,"F1 M 10 17.5 L 10 18.75 L 12.5 18.75 L 12.5 20 L 6.25 20 L 6.25 18.75 L 8.75 18.75 L 8.75 17.5 L 0 17.5 L 0 6.25 L 10 6.25 L 8.740234 7.5 L 1.25 7.5 L 1.25 16.25 L 17.5 16.25 L 17.5 8.75 L 18.75 7.5 L 18.75 17.5 Z M 5 13.75 C 5.175781 13.75 5.338542 13.717448 5.488281 13.652344 C 5.638021 13.58724 5.769856 13.497722 5.883789 13.383789 C 5.997721 13.269857 6.087239 13.138021 6.152344 12.988281 C 6.217448 12.838542 6.25 12.675781 6.25 12.5 C 6.25 12.18099 6.306966 11.878256 6.420898 11.591797 C 6.53483 11.305339 6.69108 11.051433 6.889648 10.830078 C 7.088216 10.608725 7.322591 10.424805 7.592773 10.27832 C 7.862955 10.131836 8.157552 10.042318 8.476562 10.009766 L 15.419922 3.066406 C 15.602213 2.884115 15.813802 2.744141 16.054688 2.646484 C 16.295572 2.548828 16.542969 2.5 16.796875 2.5 C 17.063801 2.5 17.31608 2.550457 17.553711 2.651367 C 17.79134 2.752279 17.998047 2.890625 18.173828 3.066406 C 18.349609 3.242188 18.487955 3.448895 18.588867 3.686523 C 18.689777 3.924154 18.740234 4.179688 18.740234 4.453125 C 18.740234 4.707031 18.691406 4.954428 18.59375 5.195312 C 18.496094 5.436199 18.356119 5.647787 18.173828 5.830078 L 11.230469 12.773438 C 11.197916 13.085938 11.110025 13.378906 10.966797 13.652344 C 10.823567 13.925781 10.639648 14.161784 10.415039 14.360352 C 10.19043 14.55892 9.936523 14.71517 9.65332 14.829102 C 9.370117 14.943034 9.06901 15 8.75 15 L 2.5 15 L 2.5 13.75 Z M 16.796875 3.75 C 16.608072 3.75 16.445312 3.818359 16.308594 3.955078 L 11.962891 8.300781 C 12.333984 8.58724 12.66276 8.916016 12.949219 9.287109 L 17.294922 4.941406 C 17.431641 4.804688 17.5 4.641928 17.5 4.453125 C 17.5 4.257812 17.430012 4.091797 17.290039 3.955078 C 17.150064 3.818359 16.985676 3.75 16.796875 3.75 Z M 10.175781 10.087891 C 10.572916 10.348308 10.901692 10.677084 11.162109 11.074219 L 12.060547 10.185547 C 11.787109 9.794922 11.455078 9.462891 11.064453 9.189453 Z M 10 12.5 C 10 12.324219 9.967447 12.161459 9.902344 12.011719 C 9.837239 11.861979 9.747721 11.730144 9.633789 11.616211 C 9.519856 11.502279 9.388021 11.412761 9.238281 11.347656 C 9.088541 11.282553 8.925781 11.25 8.75 11.25 C 8.574219 11.25 8.411458 11.282553 8.261719 11.347656 C 8.111979 11.412761 7.980143 11.502279 7.866211 11.616211 C 7.752278 11.730144 7.66276 11.861979 7.597656 12.011719 C 7.532552 12.161459 7.5 12.324219 7.5 12.5 C 7.5 12.942709 7.389322 13.359375 7.167969 13.75 L 8.75 13.75 C 8.919271 13.75 9.080403 13.717448 9.233398 13.652344 C 9.386393 13.58724 9.519856 13.497722 9.633789 13.383789 C 9.747721 13.269857 9.837239 13.136394 9.902344 12.983398 C 9.967447 12.830404 10 12.669271 10 12.5 Z "),
- new SettingOption(Translator.SettingsCalendarSettings_Title, Translator.SettingsCalendarSettings_Description, WinoPage.CalendarSettingsPage,"F1 M 1.25 3.75 C 1.25 3.404949 1.315104 3.081055 1.445312 2.77832 C 1.575521 2.475586 1.754557 2.210287 1.982422 1.982422 C 2.210286 1.754559 2.475586 1.575521 2.77832 1.445312 C 3.081055 1.315105 3.404948 1.25 3.75 1.25 L 16.25 1.25 C 16.595051 1.25 16.918945 1.315105 17.22168 1.445312 C 17.524414 1.575521 17.789713 1.754559 18.017578 1.982422 C 18.245441 2.210287 18.424479 2.475586 18.554688 2.77832 C 18.684895 3.081055 18.75 3.404949 18.75 3.75 L 18.75 5 L 1.25 5 Z M 3.75 18.75 C 3.404948 18.75 3.081055 18.684896 2.77832 18.554688 C 2.475586 18.424479 2.210286 18.245443 1.982422 18.017578 C 1.754557 17.789713 1.575521 17.524414 1.445312 17.22168 C 1.315104 16.918945 1.25 16.595053 1.25 16.25 L 1.25 6.25 L 18.75 6.25 L 18.75 16.25 C 18.75 16.595053 18.684895 16.918945 18.554688 17.22168 C 18.424479 17.524414 18.245441 17.789713 18.017578 18.017578 C 17.789713 18.245443 17.524414 18.424479 17.22168 18.554688 C 16.918945 18.684896 16.595051 18.75 16.25 18.75 Z M 7.65625 9.990234 C 7.65625 9.794922 7.618815 9.612631 7.543945 9.443359 C 7.469075 9.274089 7.368164 9.125977 7.241211 8.999023 C 7.114258 8.87207 6.966146 8.772787 6.796875 8.701172 C 6.627604 8.629558 6.445312 8.59375 6.25 8.59375 C 6.054688 8.59375 5.870768 8.631186 5.698242 8.706055 C 5.525716 8.780925 5.375977 8.881836 5.249023 9.008789 C 5.12207 9.135742 5.022786 9.283854 4.951172 9.453125 C 4.879557 9.622396 4.84375 9.804688 4.84375 10 C 4.84375 10.195312 4.879557 10.377604 4.951172 10.546875 C 5.022786 10.716146 5.12207 10.864258 5.249023 10.991211 C 5.375977 11.118164 5.524088 11.219076 5.693359 11.293945 C 5.86263 11.368815 6.044922 11.40625 6.240234 11.40625 C 6.442057 11.40625 6.629231 11.370443 6.801758 11.298828 C 6.974283 11.227214 7.124023 11.12793 7.250977 11.000977 C 7.377929 10.874023 7.477213 10.724284 7.548828 10.551758 C 7.620442 10.379232 7.65625 10.192058 7.65625 9.990234 Z M 11.40625 9.990234 C 11.40625 9.794922 11.368814 9.612631 11.293945 9.443359 C 11.219075 9.274089 11.118164 9.125977 10.991211 8.999023 C 10.864258 8.87207 10.716146 8.772787 10.546875 8.701172 C 10.377604 8.629558 10.195312 8.59375 10 8.59375 C 9.804688 8.59375 9.620768 8.631186 9.448242 8.706055 C 9.275716 8.780925 9.125977 8.881836 8.999023 9.008789 C 8.87207 9.135742 8.772786 9.283854 8.701172 9.453125 C 8.629557 9.622396 8.59375 9.804688 8.59375 10 C 8.59375 10.195312 8.629557 10.377604 8.701172 10.546875 C 8.772786 10.716146 8.87207 10.864258 8.999023 10.991211 C 9.125977 11.118164 9.274088 11.219076 9.443359 11.293945 C 9.61263 11.368815 9.794922 11.40625 9.990234 11.40625 C 10.192057 11.40625 10.379231 11.370443 10.551758 11.298828 C 10.724283 11.227214 10.874023 11.12793 11.000977 11.000977 C 11.12793 10.874023 11.227213 10.724284 11.298828 10.551758 C 11.370442 10.379232 11.40625 10.192058 11.40625 9.990234 Z M 15.15625 9.990234 C 15.15625 9.794922 15.118814 9.612631 15.043945 9.443359 C 14.969075 9.274089 14.868164 9.125977 14.741211 8.999023 C 14.614258 8.87207 14.466146 8.772787 14.296875 8.701172 C 14.127604 8.629558 13.945312 8.59375 13.75 8.59375 C 13.554688 8.59375 13.370768 8.631186 13.198242 8.706055 C 13.025716 8.780925 12.875977 8.881836 12.749023 9.008789 C 12.62207 9.135742 12.522786 9.283854 12.451172 9.453125 C 12.379557 9.622396 12.34375 9.804688 12.34375 10 C 12.34375 10.195312 12.379557 10.377604 12.451172 10.546875 C 12.522786 10.716146 12.62207 10.864258 12.749023 10.991211 C 12.875977 11.118164 13.024088 11.219076 13.193359 11.293945 C 13.362629 11.368815 13.544921 11.40625 13.740234 11.40625 C 13.942057 11.40625 14.129231 11.370443 14.301758 11.298828 C 14.474283 11.227214 14.624022 11.12793 14.750977 11.000977 C 14.877929 10.874023 14.977213 10.724284 15.048828 10.551758 C 15.120442 10.379232 15.15625 10.192058 15.15625 9.990234 Z M 7.65625 13.740234 C 7.65625 13.544922 7.618815 13.362631 7.543945 13.193359 C 7.469075 13.024089 7.368164 12.875977 7.241211 12.749023 C 7.114258 12.62207 6.966146 12.522787 6.796875 12.451172 C 6.627604 12.379558 6.445312 12.34375 6.25 12.34375 C 6.054688 12.34375 5.870768 12.381186 5.698242 12.456055 C 5.525716 12.530925 5.375977 12.631836 5.249023 12.758789 C 5.12207 12.885742 5.022786 13.033854 4.951172 13.203125 C 4.879557 13.372396 4.84375 13.554688 4.84375 13.75 C 4.84375 13.945312 4.879557 14.127604 4.951172 14.296875 C 5.022786 14.466146 5.12207 14.614258 5.249023 14.741211 C 5.375977 14.868164 5.524088 14.969076 5.693359 15.043945 C 5.86263 15.118815 6.044922 15.15625 6.240234 15.15625 C 6.442057 15.15625 6.629231 15.120443 6.801758 15.048828 C 6.974283 14.977214 7.124023 14.87793 7.250977 14.750977 C 7.377929 14.624023 7.477213 14.474284 7.548828 14.301758 C 7.620442 14.129232 7.65625 13.942058 7.65625 13.740234 Z M 11.40625 13.740234 C 11.40625 13.544922 11.368814 13.362631 11.293945 13.193359 C 11.219075 13.024089 11.118164 12.875977 10.991211 12.749023 C 10.864258 12.62207 10.716146 12.522787 10.546875 12.451172 C 10.377604 12.379558 10.195312 12.34375 10 12.34375 C 9.804688 12.34375 9.620768 12.381186 9.448242 12.456055 C 9.275716 12.530925 9.125977 12.631836 8.999023 12.758789 C 8.87207 12.885742 8.772786 13.033854 8.701172 13.203125 C 8.629557 13.372396 8.59375 13.554688 8.59375 13.75 C 8.59375 13.945312 8.629557 14.127604 8.701172 14.296875 C 8.772786 14.466146 8.87207 14.614258 8.999023 14.741211 C 9.125977 14.868164 9.274088 14.969076 9.443359 15.043945 C 9.61263 15.118815 9.794922 15.15625 9.990234 15.15625 C 10.192057 15.15625 10.379231 15.120443 10.551758 15.048828 C 10.724283 14.977214 10.874023 14.87793 11.000977 14.750977 C 11.12793 14.624023 11.227213 14.474284 11.298828 14.301758 C 11.370442 14.129232 11.40625 13.942058 11.40625 13.740234 Z "),
- new SettingOption(Translator.SettingsAbout_Title, Translator.SettingsAbout_Description, WinoPage.AboutPage,"F1 M 9.375 18.75 C 8.509114 18.75 7.677409 18.639322 6.879883 18.417969 C 6.082356 18.196615 5.335286 17.882486 4.638672 17.475586 C 3.942057 17.068686 3.308919 16.580404 2.739258 16.010742 C 2.169596 15.441081 1.681315 14.807943 1.274414 14.111328 C 0.867513 13.414714 0.553385 12.667644 0.332031 11.870117 C 0.110677 11.072592 0 10.240886 0 9.375 C 0 8.509115 0.110677 7.677409 0.332031 6.879883 C 0.553385 6.082357 0.867513 5.335287 1.274414 4.638672 C 1.681315 3.942059 2.169596 3.30892 2.739258 2.739258 C 3.308919 2.169598 3.942057 1.681316 4.638672 1.274414 C 5.335286 0.867514 6.082356 0.553387 6.879883 0.332031 C 7.677409 0.110678 8.509114 0 9.375 0 C 10.240885 0 11.072591 0.110678 11.870117 0.332031 C 12.667643 0.553387 13.414713 0.867514 14.111328 1.274414 C 14.807942 1.681316 15.44108 2.169598 16.010742 2.739258 C 16.580402 3.30892 17.068684 3.942059 17.475586 4.638672 C 17.882486 5.335287 18.196613 6.082357 18.417969 6.879883 C 18.639322 7.677409 18.75 8.509115 18.75 9.375 C 18.75 10.240886 18.639322 11.072592 18.417969 11.870117 C 18.196613 12.667644 17.882486 13.414714 17.475586 14.111328 C 17.068684 14.807943 16.580402 15.441081 16.010742 16.010742 C 15.44108 16.580404 14.807942 17.068686 14.111328 17.475586 C 13.414713 17.882486 12.667643 18.196615 11.870117 18.417969 C 11.072591 18.639322 10.240885 18.75 9.375 18.75 Z M 9.375 1.25 C 8.626302 1.25 7.906901 1.347656 7.216797 1.542969 C 6.526692 1.738281 5.880533 2.011719 5.27832 2.363281 C 4.676106 2.714844 4.127604 3.138021 3.632812 3.632812 C 3.138021 4.127604 2.714844 4.676107 2.363281 5.27832 C 2.011719 5.880534 1.738281 6.52832 1.542969 7.22168 C 1.347656 7.915039 1.25 8.632812 1.25 9.375 C 1.25 10.117188 1.347656 10.834961 1.542969 11.52832 C 1.738281 12.22168 2.011719 12.869467 2.363281 13.47168 C 2.714844 14.073894 3.138021 14.622396 3.632812 15.117188 C 4.127604 15.611979 4.676106 16.035156 5.27832 16.386719 C 5.880533 16.738281 6.526692 17.011719 7.216797 17.207031 C 7.906901 17.402344 8.626302 17.5 9.375 17.5 C 10.117188 17.5 10.834961 17.402344 11.52832 17.207031 C 12.221679 17.011719 12.869465 16.738281 13.47168 16.386719 C 14.073893 16.035156 14.622396 15.611979 15.117188 15.117188 C 15.611979 14.622396 16.035156 14.073894 16.386719 13.47168 C 16.738281 12.869467 17.011719 12.223308 17.207031 11.533203 C 17.402344 10.8431 17.5 10.123698 17.5 9.375 C 17.5 8.632812 17.402344 7.915039 17.207031 7.22168 C 17.011719 6.52832 16.738281 5.880534 16.386719 5.27832 C 16.035156 4.676107 15.611979 4.127604 15.117188 3.632812 C 14.622396 3.138021 14.073893 2.714844 13.47168 2.363281 C 12.869465 2.011719 12.221679 1.738281 11.52832 1.542969 C 10.834961 1.347656 10.117188 1.25 9.375 1.25 Z M 8.75 7.5 L 10 7.5 L 10 13.75 L 8.75 13.75 Z M 8.75 5 L 10 5 L 10 6.25 L 8.75 6.25 Z "),
- };
-}
diff --git a/Wino.Calendar/Styles/DayHeaderControl.xaml b/Wino.Calendar/Styles/DayHeaderControl.xaml
deleted file mode 100644
index 396a0cf0..00000000
--- a/Wino.Calendar/Styles/DayHeaderControl.xaml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
-
diff --git a/Wino.Calendar/Styles/WinoCalendarResources.xaml b/Wino.Calendar/Styles/WinoCalendarResources.xaml
deleted file mode 100644
index c0469ff1..00000000
--- a/Wino.Calendar/Styles/WinoCalendarResources.xaml
+++ /dev/null
@@ -1,424 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Wino.Calendar/Styles/WinoCalendarResources.xaml.cs b/Wino.Calendar/Styles/WinoCalendarResources.xaml.cs
deleted file mode 100644
index fd055e7b..00000000
--- a/Wino.Calendar/Styles/WinoCalendarResources.xaml.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Windows.UI.Xaml;
-
-namespace Wino.Calendar.Styles;
-
-public sealed partial class WinoCalendarResources : ResourceDictionary
-{
- public WinoCalendarResources()
- {
- this.InitializeComponent();
- }
-}
diff --git a/Wino.Calendar/Styles/WinoCalendarView.xaml b/Wino.Calendar/Styles/WinoCalendarView.xaml
deleted file mode 100644
index 8259f570..00000000
--- a/Wino.Calendar/Styles/WinoCalendarView.xaml
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Wino.Calendar/Styles/WinoDayTimelineCanvas.xaml b/Wino.Calendar/Styles/WinoDayTimelineCanvas.xaml
deleted file mode 100644
index 96257f2f..00000000
--- a/Wino.Calendar/Styles/WinoDayTimelineCanvas.xaml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
diff --git a/Wino.Calendar/Views/Abstract/AccountDetailsPageAbstract.cs b/Wino.Calendar/Views/Abstract/AccountDetailsPageAbstract.cs
deleted file mode 100644
index de113056..00000000
--- a/Wino.Calendar/Views/Abstract/AccountDetailsPageAbstract.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-using Wino.Calendar.ViewModels;
-using Wino.Core.UWP;
-
-namespace Wino.Calendar.Views.Abstract;
-
-public abstract class AccountDetailsPageAbstract : BasePage { }
diff --git a/Wino.Calendar/Views/Abstract/AccountManagementPageAbstract.cs b/Wino.Calendar/Views/Abstract/AccountManagementPageAbstract.cs
deleted file mode 100644
index e7edd065..00000000
--- a/Wino.Calendar/Views/Abstract/AccountManagementPageAbstract.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-using Wino.Calendar.ViewModels;
-using Wino.Core.UWP;
-
-namespace Wino.Calendar.Views.Abstract;
-
-public partial class AccountManagementPageAbstract : BasePage { }
diff --git a/Wino.Calendar/Views/Abstract/AppShellAbstract.cs b/Wino.Calendar/Views/Abstract/AppShellAbstract.cs
deleted file mode 100644
index 8ab81b95..00000000
--- a/Wino.Calendar/Views/Abstract/AppShellAbstract.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-using Wino.Calendar.ViewModels;
-using Wino.Core.UWP;
-
-namespace Wino.Calendar.Views.Abstract;
-
-public abstract class AppShellAbstract : BasePage { }
diff --git a/Wino.Calendar/Views/Abstract/CalendarPageAbstract.cs b/Wino.Calendar/Views/Abstract/CalendarPageAbstract.cs
deleted file mode 100644
index abd1fb5f..00000000
--- a/Wino.Calendar/Views/Abstract/CalendarPageAbstract.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-using Wino.Calendar.ViewModels;
-using Wino.Core.UWP;
-
-namespace Wino.Calendar.Views.Abstract;
-
-public abstract class CalendarPageAbstract : BasePage { }
diff --git a/Wino.Calendar/Views/Abstract/CalendarSettingsPageAbstract.cs b/Wino.Calendar/Views/Abstract/CalendarSettingsPageAbstract.cs
deleted file mode 100644
index 47eef1d4..00000000
--- a/Wino.Calendar/Views/Abstract/CalendarSettingsPageAbstract.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-using Wino.Calendar.ViewModels;
-using Wino.Core.UWP;
-
-namespace Wino.Calendar.Views.Abstract;
-
-public abstract class CalendarSettingsPageAbstract : BasePage { }
diff --git a/Wino.Calendar/Views/Abstract/PersonalizationPageAbstract.cs b/Wino.Calendar/Views/Abstract/PersonalizationPageAbstract.cs
deleted file mode 100644
index b970f09a..00000000
--- a/Wino.Calendar/Views/Abstract/PersonalizationPageAbstract.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-using Wino.Core.UWP;
-using Wino.Core.ViewModels;
-
-namespace Wino.Calendar.Views.Abstract;
-
-public partial class PersonalizationPageAbstract : BasePage { }
diff --git a/Wino.Calendar/Views/Account/AccountManagementPage.xaml b/Wino.Calendar/Views/Account/AccountManagementPage.xaml
deleted file mode 100644
index 636a25df..00000000
--- a/Wino.Calendar/Views/Account/AccountManagementPage.xaml
+++ /dev/null
@@ -1,179 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Wino.Calendar/Views/Account/AccountManagementPage.xaml.cs b/Wino.Calendar/Views/Account/AccountManagementPage.xaml.cs
deleted file mode 100644
index 4e63cca5..00000000
--- a/Wino.Calendar/Views/Account/AccountManagementPage.xaml.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Wino.Calendar.Views.Abstract;
-
-namespace Wino.Calendar.Views.Account;
-
-public sealed partial class AccountManagementPage : AccountManagementPageAbstract
-{
- public AccountManagementPage()
- {
- InitializeComponent();
- }
-}
diff --git a/Wino.Calendar/Views/AppShell.xaml b/Wino.Calendar/Views/AppShell.xaml
deleted file mode 100644
index a8b31d8f..00000000
--- a/Wino.Calendar/Views/AppShell.xaml
+++ /dev/null
@@ -1,405 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Wino.Calendar/Views/AppShell.xaml.cs b/Wino.Calendar/Views/AppShell.xaml.cs
deleted file mode 100644
index b68a2ca9..00000000
--- a/Wino.Calendar/Views/AppShell.xaml.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using CommunityToolkit.Mvvm.Messaging;
-using Windows.UI.Xaml;
-using Windows.UI.Xaml.Controls;
-using Wino.Calendar.Views.Abstract;
-using Wino.Core.UWP;
-using Wino.Messaging.Client.Calendar;
-
-namespace Wino.Calendar.Views;
-
-public sealed partial class AppShell : AppShellAbstract,
- IRecipient
-{
- private const string STATE_HorizontalCalendar = "HorizontalCalendar";
- private const string STATE_VerticalCalendar = "VerticalCalendar";
-
- public Frame GetShellFrame() => ShellFrame;
-
- public AppShell()
- {
- InitializeComponent();
-
- Window.Current.SetTitleBar(DragArea);
- ManageCalendarDisplayType();
- }
-
- private void ManageCalendarDisplayType()
- {
- // Go to different states based on the display type.
- if (ViewModel.IsVerticalCalendar)
- {
- VisualStateManager.GoToState(this, STATE_VerticalCalendar, false);
- }
- else
- {
- VisualStateManager.GoToState(this, STATE_HorizontalCalendar, false);
- }
- }
-
- private void PreviousDateClicked(object sender, RoutedEventArgs e) => WeakReferenceMessenger.Default.Send(new GoPreviousDateRequestedMessage());
-
- private void NextDateClicked(object sender, RoutedEventArgs e) => WeakReferenceMessenger.Default.Send(new GoNextDateRequestedMessage());
-
- public void Receive(CalendarDisplayTypeChangedMessage message)
- {
- ManageCalendarDisplayType();
- }
-
- private void ShellFrameContentNavigated(object sender, Windows.UI.Xaml.Navigation.NavigationEventArgs e)
- => RealAppBar.ShellFrameContent = (e.Content as BasePage).ShellContent;
-
- private void AppBarBackButtonClicked(Core.UWP.Controls.WinoAppTitleBar sender, RoutedEventArgs args)
- => ViewModel.NavigationService.GoBack();
-}
diff --git a/Wino.Calendar/Views/CalendarPage.xaml b/Wino.Calendar/Views/CalendarPage.xaml
deleted file mode 100644
index 85d569cb..00000000
--- a/Wino.Calendar/Views/CalendarPage.xaml
+++ /dev/null
@@ -1,400 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Wino.Calendar/Views/CalendarPage.xaml.cs b/Wino.Calendar/Views/CalendarPage.xaml.cs
deleted file mode 100644
index d8ce61ee..00000000
--- a/Wino.Calendar/Views/CalendarPage.xaml.cs
+++ /dev/null
@@ -1,160 +0,0 @@
-using System;
-using CommunityToolkit.Mvvm.Messaging;
-using Windows.UI.Xaml.Controls;
-using Windows.UI.Xaml.Controls.Primitives;
-using Windows.UI.Xaml.Navigation;
-using Wino.Calendar.Args;
-using Wino.Calendar.Views.Abstract;
-using Wino.Core.Domain.Enums;
-using Wino.Core.Domain.Models.Calendar;
-using Wino.Messaging.Client.Calendar;
-
-namespace Wino.Calendar.Views;
-
-public sealed partial class CalendarPage : CalendarPageAbstract,
- IRecipient,
- IRecipient,
- IRecipient,
- IRecipient
-{
- private const int PopupDialogOffset = 12;
-
- public CalendarPage()
- {
- InitializeComponent();
- NavigationCacheMode = NavigationCacheMode.Enabled;
-
- ViewModel.DetailsShowCalendarItemChanged += CalendarItemDetailContextChanged;
- }
-
- private void CalendarItemDetailContextChanged(object sender, EventArgs e)
- {
- if (ViewModel.DisplayDetailsCalendarItemViewModel != null)
- {
- var control = CalendarControl.GetCalendarItemControl(ViewModel.DisplayDetailsCalendarItemViewModel);
-
- if (control != null)
- {
- EventDetailsPopup.PlacementTarget = control;
- }
- }
- }
-
- public void Receive(ScrollToHourMessage message) => CalendarControl.NavigateToHour(message.TimeSpan);
- public void Receive(ScrollToDateMessage message) => CalendarControl.NavigateToDay(message.Date);
- public void Receive(GoNextDateRequestedMessage message) => CalendarControl.GoNextRange();
- public void Receive(GoPreviousDateRequestedMessage message) => CalendarControl.GoPreviousRange();
-
- protected override void OnNavigatedTo(NavigationEventArgs e)
- {
- base.OnNavigatedTo(e);
-
- if (e.NavigationMode == NavigationMode.Back) return;
-
- if (e.Parameter is CalendarPageNavigationArgs args)
- {
- if (args.RequestDefaultNavigation)
- {
- // Go today.
- WeakReferenceMessenger.Default.Send(new LoadCalendarMessage(DateTime.Now.Date, CalendarInitInitiative.App));
- }
- else
- {
- // Go specified date.
- WeakReferenceMessenger.Default.Send(new LoadCalendarMessage(args.NavigationDate, CalendarInitInitiative.User));
- }
- }
- }
-
- private void CellSelected(object sender, TimelineCellSelectedArgs e)
- {
- // Dismiss event details if exists and cancel the selection.
- // This is to prevent the event details from being displayed when the user clicks somewhere else.
-
- if (EventDetailsPopup.IsOpen)
- {
- CalendarControl.UnselectActiveTimelineCell();
- ViewModel.DisplayDetailsCalendarItemViewModel = null;
-
- return;
- }
-
- ViewModel.SelectedQuickEventDate = e.ClickedDate;
-
- TeachingTipPositionerGrid.Width = e.CellSize.Width;
- TeachingTipPositionerGrid.Height = e.CellSize.Height;
-
- Canvas.SetLeft(TeachingTipPositionerGrid, e.PositionerPoint.X);
- Canvas.SetTop(TeachingTipPositionerGrid, e.PositionerPoint.Y);
-
- // Adjust the start and end time in the flyout.
- var startTime = ViewModel.SelectedQuickEventDate.Value.TimeOfDay;
- var endTime = startTime.Add(TimeSpan.FromMinutes(30));
-
- ViewModel.SelectQuickEventTimeRange(startTime, endTime);
-
- QuickEventPopupDialog.IsOpen = true;
- }
-
- private void CellUnselected(object sender, TimelineCellUnselectedArgs e)
- {
- QuickEventPopupDialog.IsOpen = false;
- }
-
- private void QuickEventAccountSelectorSelectionChanged(object sender, SelectionChangedEventArgs e)
- {
- QuickEventAccountSelectorFlyout.Hide();
- }
-
- private void QuickEventPopupClosed(object sender, object e)
- {
- // Reset the timeline selection when the tip is closed.
- CalendarControl.ResetTimelineSelection();
- }
-
- private void PopupPlacementChanged(object sender, object e)
- {
- if (sender is Popup senderPopup)
- {
- // When the quick event Popup is positioned for different calendar types,
- // we must adjust the offset to make sure the tip is not hidden and has nice
- // spacing from the cell.
-
- switch (senderPopup.ActualPlacement)
- {
- case PopupPlacementMode.Top:
- senderPopup.VerticalOffset = PopupDialogOffset * -1;
- break;
- case PopupPlacementMode.Bottom:
- senderPopup.VerticalOffset = PopupDialogOffset;
- break;
- case PopupPlacementMode.Left:
- senderPopup.HorizontalOffset = PopupDialogOffset * -1;
- break;
- case PopupPlacementMode.Right:
- senderPopup.HorizontalOffset = PopupDialogOffset;
- break;
- default:
- break;
- }
- }
-
- }
-
- private void StartTimeDurationSubmitted(ComboBox sender, ComboBoxTextSubmittedEventArgs args)
- => ViewModel.SelectedStartTimeString = args.Text;
-
- private void EndTimeDurationSubmitted(ComboBox sender, ComboBoxTextSubmittedEventArgs args)
- => ViewModel.SelectedEndTimeString = args.Text;
-
- private void EventDetailsPopupClosed(object sender, object e)
- {
- ViewModel.DisplayDetailsCalendarItemViewModel = null;
- }
-
- private void CalendarScrolling(object sender, EventArgs e)
- {
- // In case of scrolling, we must dismiss the event details dialog.
- ViewModel.DisplayDetailsCalendarItemViewModel = null;
- }
-}
diff --git a/Wino.Calendar/Views/EventDetailsPage.xaml b/Wino.Calendar/Views/EventDetailsPage.xaml
deleted file mode 100644
index 875b5689..00000000
--- a/Wino.Calendar/Views/EventDetailsPage.xaml
+++ /dev/null
@@ -1,297 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Wino.Calendar/Views/EventDetailsPage.xaml.cs b/Wino.Calendar/Views/EventDetailsPage.xaml.cs
deleted file mode 100644
index ac3cff0a..00000000
--- a/Wino.Calendar/Views/EventDetailsPage.xaml.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-
-using Wino.Calendar.Views.Abstract;
-
-namespace Wino.Calendar.Views;
-
-public sealed partial class EventDetailsPage : EventDetailsPageAbstract
-{
- public EventDetailsPage()
- {
- this.InitializeComponent();
- }
-}
diff --git a/Wino.Calendar/Views/Settings/AccountDetailsPage.xaml b/Wino.Calendar/Views/Settings/AccountDetailsPage.xaml
deleted file mode 100644
index afab9d7f..00000000
--- a/Wino.Calendar/Views/Settings/AccountDetailsPage.xaml
+++ /dev/null
@@ -1,125 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Wino.Calendar/Views/Settings/AccountDetailsPage.xaml.cs b/Wino.Calendar/Views/Settings/AccountDetailsPage.xaml.cs
deleted file mode 100644
index 0cb918b0..00000000
--- a/Wino.Calendar/Views/Settings/AccountDetailsPage.xaml.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Wino.Calendar.Views.Abstract;
-
-namespace Wino.Calendar.Views.Settings;
-
-public sealed partial class AccountDetailsPage : AccountDetailsPageAbstract
-{
- public AccountDetailsPage()
- {
- this.InitializeComponent();
- }
-}
diff --git a/Wino.Calendar/Views/Settings/CalendarSettingsPage.xaml b/Wino.Calendar/Views/Settings/CalendarSettingsPage.xaml
deleted file mode 100644
index 7254969f..00000000
--- a/Wino.Calendar/Views/Settings/CalendarSettingsPage.xaml
+++ /dev/null
@@ -1,200 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Wino.Calendar/Views/Settings/CalendarSettingsPage.xaml.cs b/Wino.Calendar/Views/Settings/CalendarSettingsPage.xaml.cs
deleted file mode 100644
index 1114ea5d..00000000
--- a/Wino.Calendar/Views/Settings/CalendarSettingsPage.xaml.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using Wino.Calendar.Views.Abstract;
-
-
-namespace Wino.Calendar.Views.Settings;
-
-public sealed partial class CalendarSettingsPage : CalendarSettingsPageAbstract
-{
- public CalendarSettingsPage()
- {
- InitializeComponent();
- }
-}
diff --git a/Wino.Calendar/Views/Settings/PersonalizationPage.xaml.cs b/Wino.Calendar/Views/Settings/PersonalizationPage.xaml.cs
deleted file mode 100644
index a9518467..00000000
--- a/Wino.Calendar/Views/Settings/PersonalizationPage.xaml.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Wino.Calendar.Views.Abstract;
-
-namespace Wino.Calendar.Views.Settings;
-
-public sealed partial class PersonalizationPage : PersonalizationPageAbstract
-{
- public PersonalizationPage()
- {
- this.InitializeComponent();
- }
-}
diff --git a/Wino.Calendar/Wino.Calendar.csproj b/Wino.Calendar/Wino.Calendar.csproj
deleted file mode 100644
index 97d6df07..00000000
--- a/Wino.Calendar/Wino.Calendar.csproj
+++ /dev/null
@@ -1,98 +0,0 @@
-
-
- WinExe
- net9.0-windows10.0.26100.0
- 10.0.18362.0
- true
- x86;x64;arm64
- win-x86;win-x64;win-arm64
- en-US
-
- win-$(Platform).pubxml
- true
- true
- True
- True
- SHA256
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Wino.Core.Domain/AppUrls.cs b/Wino.Core.Domain/AppUrls.cs
new file mode 100644
index 00000000..72740d3f
--- /dev/null
+++ b/Wino.Core.Domain/AppUrls.cs
@@ -0,0 +1,10 @@
+namespace Wino.Core.Domain;
+
+public static class AppUrls
+{
+ public const string Website = "https://www.winomail.app";
+ public const string Discord = "https://discord.gg/windows-apps-hub-714581497222398064";
+ public const string GitHub = "https://github.com/bkaankose/Wino-Mail/";
+ public const string PrivacyPolicy = "https://www.winomail.app/support/privacy";
+ public const string Paypal = "https://paypal.me/bkaankose?country.x=PL&locale.x=en_US";
+}
diff --git a/Wino.Core.Domain/BasicTypesJsonContext.cs b/Wino.Core.Domain/BasicTypesJsonContext.cs
index b9dba02d..f6085c1c 100644
--- a/Wino.Core.Domain/BasicTypesJsonContext.cs
+++ b/Wino.Core.Domain/BasicTypesJsonContext.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
+using Wino.Core.Domain.Models.Updates;
namespace Wino.Core.Domain;
@@ -8,4 +9,6 @@ namespace Wino.Core.Domain;
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(List))]
[JsonSerializable(typeof(bool))]
+[JsonSerializable(typeof(UpdateNotes))]
+[JsonSerializable(typeof(List))]
public partial class BasicTypesJsonContext : JsonSerializerContext;
diff --git a/Wino.Core.Domain/CalendarRecurrenceSummaryFormatter.cs b/Wino.Core.Domain/CalendarRecurrenceSummaryFormatter.cs
new file mode 100644
index 00000000..3092b0f8
--- /dev/null
+++ b/Wino.Core.Domain/CalendarRecurrenceSummaryFormatter.cs
@@ -0,0 +1,126 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Models.Calendar;
+
+namespace Wino.Core.Domain;
+
+public static class CalendarRecurrenceSummaryFormatter
+{
+ private static readonly DayOfWeek[] OrderedDays =
+ [
+ DayOfWeek.Monday,
+ DayOfWeek.Tuesday,
+ DayOfWeek.Wednesday,
+ DayOfWeek.Thursday,
+ DayOfWeek.Friday,
+ DayOfWeek.Saturday,
+ DayOfWeek.Sunday
+ ];
+
+ public static string BuildSummary(
+ bool isRecurring,
+ DateTimeOffset effectiveStart,
+ DateTimeOffset effectiveEnd,
+ bool isAllDay,
+ CalendarSettings settings,
+ int interval,
+ CalendarItemRecurrenceFrequency frequency,
+ IReadOnlyCollection daysOfWeek,
+ DateTimeOffset? recurrenceEndDate)
+ {
+ var culture = settings?.CultureInfo ?? CultureInfo.CurrentCulture;
+ var timeSummary = isAllDay
+ ? Translator.CalendarItemAllDay
+ : string.Format(
+ culture,
+ Translator.CalendarEventCompose_TimeRangeSummary,
+ effectiveStart.ToString(settings?.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour ? "HH:mm" : "h:mm tt", culture),
+ effectiveEnd.ToString(settings?.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour ? "HH:mm" : "h:mm tt", culture));
+
+ if (!isRecurring)
+ {
+ return string.Format(
+ culture,
+ Translator.CalendarEventCompose_SingleOccurrenceSummary,
+ effectiveStart.ToString("dddd yyyy-MM-dd", culture),
+ timeSummary);
+ }
+
+ var normalizedDays = NormalizeDays(daysOfWeek);
+ var isEveryDay = (frequency == CalendarItemRecurrenceFrequency.Daily && interval == 1) ||
+ (frequency == CalendarItemRecurrenceFrequency.Weekly && interval == 1 && normalizedDays.Count == 7);
+
+ var cadenceSummary = isEveryDay
+ ? $"{Translator.CalendarEventCompose_Every} {Translator.CalendarEventCompose_FrequencyDay}"
+ : interval == 1
+ ? $"{Translator.CalendarEventCompose_Every} {GetSingularFrequencyLabel(frequency)}"
+ : $"{Translator.CalendarEventCompose_Every} {interval.ToString(culture)} {GetPluralFrequencyLabel(frequency)}";
+
+ var weekdaySummary = string.Empty;
+ if (frequency == CalendarItemRecurrenceFrequency.Weekly && normalizedDays.Count > 0 && normalizedDays.Count < 7)
+ {
+ weekdaySummary = string.Format(
+ culture,
+ Translator.CalendarEventCompose_WeekdaySummary,
+ string.Join(", ", normalizedDays.Select(day => culture.DateTimeFormat.GetDayName(day))));
+ }
+
+ var untilSummary = recurrenceEndDate.HasValue
+ ? string.Format(
+ culture,
+ Translator.CalendarEventCompose_UntilSummary,
+ recurrenceEndDate.Value.ToString("ddd yyyy-MM-dd", culture))
+ : string.Empty;
+
+ return string.Format(
+ culture,
+ Translator.CalendarEventCompose_RecurringSummarySmart,
+ cadenceSummary,
+ weekdaySummary,
+ timeSummary,
+ effectiveStart.ToString("dddd yyyy-MM-dd", culture),
+ untilSummary).Trim();
+ }
+
+ private static IReadOnlyList NormalizeDays(IReadOnlyCollection daysOfWeek)
+ {
+ if (daysOfWeek == null || daysOfWeek.Count == 0)
+ {
+ return [];
+ }
+
+ return daysOfWeek
+ .Distinct()
+ .OrderBy(day => Array.IndexOf(OrderedDays, day))
+ .ToList();
+ }
+
+ private static string GetSingularFrequencyLabel(CalendarItemRecurrenceFrequency frequency)
+ {
+ return frequency switch
+ {
+ CalendarItemRecurrenceFrequency.Daily => Translator.CalendarEventCompose_FrequencyDay,
+ CalendarItemRecurrenceFrequency.Weekly => Translator.CalendarEventCompose_FrequencyWeek,
+ CalendarItemRecurrenceFrequency.Monthly => Translator.CalendarEventCompose_FrequencyMonth,
+ CalendarItemRecurrenceFrequency.Yearly => Translator.CalendarEventCompose_FrequencyYear,
+ _ => Translator.CalendarEventCompose_FrequencyWeek
+ };
+ }
+
+ private static string GetPluralFrequencyLabel(CalendarItemRecurrenceFrequency frequency)
+ {
+ return frequency switch
+ {
+ CalendarItemRecurrenceFrequency.Daily => Translator.CalendarEventCompose_FrequencyDayPlural,
+ CalendarItemRecurrenceFrequency.Weekly => Translator.CalendarEventCompose_FrequencyWeekPlural,
+ CalendarItemRecurrenceFrequency.Monthly => Translator.CalendarEventCompose_FrequencyMonthPlural,
+ CalendarItemRecurrenceFrequency.Yearly => Translator.CalendarEventCompose_FrequencyYearPlural,
+ _ => Translator.CalendarEventCompose_FrequencyWeekPlural
+ };
+ }
+}
+
+
diff --git a/Wino.Core.Domain/CalendarReminderSnoozeOptions.cs b/Wino.Core.Domain/CalendarReminderSnoozeOptions.cs
new file mode 100644
index 00000000..5786c42f
--- /dev/null
+++ b/Wino.Core.Domain/CalendarReminderSnoozeOptions.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Wino.Core.Domain;
+
+public static class CalendarReminderSnoozeOptions
+{
+ private static readonly int[] SupportedSnoozeMinutes = [5, 10, 15, 30];
+
+ public static IReadOnlyList GetSupportedSnoozeMinutes()
+ => SupportedSnoozeMinutes;
+
+ public static IReadOnlyList GetAllowedSnoozeMinutes(long reminderDurationInSeconds, long defaultReminderDurationInSeconds)
+ {
+ var reminderMinutes = (int)Math.Max(0, reminderDurationInSeconds / 60);
+
+ if (reminderMinutes <= 0)
+ return [];
+
+ var maxSnoozeMinutes = reminderMinutes;
+ var defaultReminderMinutes = (int)Math.Max(0, defaultReminderDurationInSeconds / 60);
+
+ if (defaultReminderMinutes > 0)
+ maxSnoozeMinutes = Math.Min(maxSnoozeMinutes, defaultReminderMinutes);
+
+ return SupportedSnoozeMinutes.Where(minutes => minutes <= maxSnoozeMinutes).ToArray();
+ }
+}
diff --git a/Wino.Core.Domain/Collections/CalendarEventCollection.cs b/Wino.Core.Domain/Collections/CalendarEventCollection.cs
index 94a1eef6..1b070837 100644
--- a/Wino.Core.Domain/Collections/CalendarEventCollection.cs
+++ b/Wino.Core.Domain/Collections/CalendarEventCollection.cs
@@ -13,6 +13,7 @@ public class CalendarEventCollection
{
public event EventHandler CalendarItemAdded;
public event EventHandler CalendarItemRemoved;
+ public event EventHandler CalendarItemUpdated;
public event EventHandler CalendarItemsCleared;
@@ -114,11 +115,27 @@ public class CalendarEventCollection
}
}
+ public void RemoveCalendarItems(Func predicate)
+ {
+ if (predicate == null) return;
+
+ var itemsToRemove = _allItems.Where(predicate).ToList();
+
+ foreach (var item in itemsToRemove)
+ {
+ RemoveCalendarItem(item);
+ }
+ }
+
private void AddCalendarItemInternal(ObservableRangeCollection collection, ICalendarItem calendarItem, bool create = true)
{
- if (calendarItem is not ICalendarItemViewModel)
+ if (calendarItem is not ICalendarItemViewModel viewModel)
throw new ArgumentException("CalendarItem must be of type ICalendarItemViewModel", nameof(calendarItem));
+ // Set the displaying context for proper title calculation
+ viewModel.DisplayingPeriod = Period;
+ viewModel.CalendarSettings = Settings;
+
collection.Add(calendarItem);
if (create)
@@ -144,6 +161,53 @@ public class CalendarEventCollection
CalendarItemRemoved?.Invoke(this, calendarItem);
}
+ ///
+ /// Updates an existing calendar item in-place. If the item's type changed (all-day vs regular),
+ /// it will be moved to the appropriate collection.
+ ///
+ /// The updated calendar item data.
+ /// True if the item was found and updated; false otherwise.
+ public bool UpdateCalendarItem(CalendarItem calendarItem)
+ {
+ var existingItem = _allItems.FirstOrDefault(x => x.Id == calendarItem.Id);
+ if (existingItem == null)
+ return false;
+
+ // Get the collections this item is currently in (before update)
+ var oldCollections = GetProperCollectionsForCalendarItem(existingItem).ToList();
+
+ // Update the underlying data
+ if (existingItem is ICalendarItemViewModel viewModel)
+ {
+ viewModel.UpdateFrom(calendarItem);
+ }
+
+ // Get the collections this item should be in (after update)
+ var newCollections = GetProperCollectionsForCalendarItem(existingItem).ToList();
+
+ // Check if the collections changed
+ var collectionsToRemoveFrom = oldCollections.Except(newCollections).ToList();
+ var collectionsToAddTo = newCollections.Except(oldCollections).ToList();
+
+ // Remove from old collections that are no longer applicable
+ foreach (var collection in collectionsToRemoveFrom)
+ {
+ collection.Remove(existingItem);
+ }
+
+ // Add to new collections that are now applicable
+ foreach (var collection in collectionsToAddTo)
+ {
+ if (!collection.Contains(existingItem))
+ {
+ collection.Add(existingItem);
+ }
+ }
+
+ CalendarItemUpdated?.Invoke(this, existingItem);
+ return true;
+ }
+
public void Clear()
{
_internalAllDayEvents.Clear();
diff --git a/Wino.Core.Domain/Collections/DayRangeCollection.cs b/Wino.Core.Domain/Collections/DayRangeCollection.cs
deleted file mode 100644
index 89afeb68..00000000
--- a/Wino.Core.Domain/Collections/DayRangeCollection.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using System.Linq;
-using Wino.Core.Domain.Interfaces;
-using Wino.Core.Domain.Models.Calendar;
-
-namespace Wino.Core.Domain.Collections;
-
-public class DayRangeCollection : ObservableRangeCollection
-{
- ///
- /// Gets the range of dates that are currently displayed in the collection.
- ///
- public DateRange DisplayRange
- {
- get
- {
- if (Count == 0) return null;
-
- var minimumLoadedDate = this[0].CalendarRenderOptions.DateRange.StartDate;
- var maximumLoadedDate = this[Count - 1].CalendarRenderOptions.DateRange.EndDate;
-
- return new DateRange(minimumLoadedDate, maximumLoadedDate);
- }
- }
-
- public void RemoveCalendarItem(ICalendarItem calendarItem)
- {
- foreach (var dayRange in this)
- {
-
- }
- }
-
- public void AddCalendarItem(ICalendarItem calendarItem)
- {
- foreach (var dayRange in this)
- {
- var calendarDayModel = dayRange.CalendarDays.FirstOrDefault(x => x.Period.HasInside(calendarItem.Period.Start));
- calendarDayModel?.EventsCollection.AddCalendarItem(calendarItem);
- }
- }
-}
diff --git a/Wino.Core.Domain/Constants.cs b/Wino.Core.Domain/Constants.cs
index 0406edcd..cded77eb 100644
--- a/Wino.Core.Domain/Constants.cs
+++ b/Wino.Core.Domain/Constants.cs
@@ -1,4 +1,4 @@
-namespace Wino.Core.Domain;
+namespace Wino.Core.Domain;
public static class Constants
{
@@ -12,7 +12,17 @@ public static class Constants
public const string ToastMailUniqueIdKey = nameof(ToastMailUniqueIdKey);
public const string ToastActionKey = nameof(ToastActionKey);
-
+ public const string ToastMailAccountIdKey = nameof(ToastMailAccountIdKey);
+ public const string ToastCalendarItemIdKey = nameof(ToastCalendarItemIdKey);
+ public const string ToastCalendarActionKey = nameof(ToastCalendarActionKey);
+ public const string ToastCalendarNavigateAction = nameof(ToastCalendarNavigateAction);
+ public const string ToastCalendarSnoozeAction = nameof(ToastCalendarSnoozeAction);
+ public const string ToastCalendarSnoozeDurationInputId = nameof(ToastCalendarSnoozeDurationInputId);
+ public const string ToastModeKey = nameof(ToastModeKey);
+ public const string ToastModeMail = nameof(ToastModeMail);
+ public const string ToastModeCalendar = nameof(ToastModeCalendar);
+ public const string ToastStoreUpdateActionKey = nameof(ToastStoreUpdateActionKey);
+ public const string ToastStoreUpdateActionInstall = nameof(ToastStoreUpdateActionInstall);
public const string ClientLogFile = "Client_.log";
public const string ServerLogFile = "Server_.log";
public const string LogArchiveFileName = "WinoLogs.zip";
@@ -20,3 +30,4 @@ public static class Constants
public const string WinoMailIdentiifer = nameof(WinoMailIdentiifer);
public const string WinoCalendarIdentifier = nameof(WinoCalendarIdentifier);
}
+
diff --git a/Wino.Core.Domain/Entities/Calendar/AccountCalendar.cs b/Wino.Core.Domain/Entities/Calendar/AccountCalendar.cs
index 29a200cf..e3fe9ba6 100644
--- a/Wino.Core.Domain/Entities/Calendar/AccountCalendar.cs
+++ b/Wino.Core.Domain/Entities/Calendar/AccountCalendar.cs
@@ -1,9 +1,12 @@
using System;
using SQLite;
+using Wino.Core.Domain.Entities.Shared;
+using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Domain.Entities.Calendar;
+[Preserve]
public class AccountCalendar : IAccountCalendar
{
[PrimaryKey]
@@ -13,7 +16,9 @@ public class AccountCalendar : IAccountCalendar
public string SynchronizationDeltaToken { get; set; }
public string Name { get; set; }
public bool IsPrimary { get; set; }
+ public bool IsSynchronizationEnabled { get; set; } = true;
public bool IsExtended { get; set; } = true;
+ public CalendarItemShowAs DefaultShowAs { get; set; } = CalendarItemShowAs.Busy;
///
/// Unused for now.
@@ -21,4 +26,7 @@ public class AccountCalendar : IAccountCalendar
public string TextColorHex { get; set; }
public string BackgroundColorHex { get; set; }
public string TimeZone { get; set; }
+
+ [Ignore]
+ public MailAccount MailAccount { get; set; }
}
diff --git a/Wino.Core.Domain/Entities/Calendar/CalendarAttachment.cs b/Wino.Core.Domain/Entities/Calendar/CalendarAttachment.cs
new file mode 100644
index 00000000..477acd38
--- /dev/null
+++ b/Wino.Core.Domain/Entities/Calendar/CalendarAttachment.cs
@@ -0,0 +1,55 @@
+using System;
+using SQLite;
+using Wino.Core.Domain.Enums;
+
+namespace Wino.Core.Domain.Entities.Calendar;
+
+///
+/// Represents metadata for calendar event attachments.
+/// Actual file content is downloaded on-demand.
+///
+public class CalendarAttachment
+{
+ [PrimaryKey]
+ public Guid Id { get; set; }
+
+ ///
+ /// The calendar item this attachment belongs to.
+ ///
+ public Guid CalendarItemId { get; set; }
+
+ ///
+ /// Remote identifier for the attachment from the provider (Outlook, Gmail, etc.).
+ ///
+ public string RemoteAttachmentId { get; set; }
+
+ ///
+ /// File name of the attachment.
+ ///
+ public string FileName { get; set; }
+
+ ///
+ /// Size of the attachment in bytes.
+ ///
+ public long Size { get; set; }
+
+ ///
+ /// MIME content type (e.g., "application/pdf", "image/png").
+ ///
+ public string ContentType { get; set; }
+
+ ///
+ /// Whether the attachment has been downloaded to local storage.
+ ///
+ public bool IsDownloaded { get; set; }
+
+ ///
+ /// Local file path where the attachment is stored (if downloaded).
+ ///
+ public string LocalFilePath { get; set; }
+
+ ///
+ /// When the attachment was last modified.
+ ///
+ public DateTimeOffset LastModified { get; set; }
+}
diff --git a/Wino.Core.Domain/Entities/Calendar/CalendarEventAttendee.cs b/Wino.Core.Domain/Entities/Calendar/CalendarEventAttendee.cs
index e9c47da3..8935589a 100644
--- a/Wino.Core.Domain/Entities/Calendar/CalendarEventAttendee.cs
+++ b/Wino.Core.Domain/Entities/Calendar/CalendarEventAttendee.cs
@@ -1,10 +1,10 @@
using System;
using SQLite;
+using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Entities.Calendar;
-// TODO: Connect to Contact store with Wino People.
public class CalendarEventAttendee
{
[PrimaryKey]
@@ -16,4 +16,11 @@ public class CalendarEventAttendee
public bool IsOrganizer { get; set; }
public bool IsOptionalAttendee { get; set; }
public string Comment { get; set; }
+
+ ///
+ /// Resolved contact from the contact store. Populated at runtime via IContactService;
+ /// not persisted to the database.
+ ///
+ [Ignore]
+ public AccountContact ResolvedContact { get; set; }
}
diff --git a/Wino.Core.Domain/Entities/Calendar/CalendarItem.cs b/Wino.Core.Domain/Entities/Calendar/CalendarItem.cs
index de03e74e..a04576f9 100644
--- a/Wino.Core.Domain/Entities/Calendar/CalendarItem.cs
+++ b/Wino.Core.Domain/Entities/Calendar/CalendarItem.cs
@@ -3,7 +3,9 @@ using System.Diagnostics;
using Itenso.TimePeriod;
using SQLite;
using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Extensions;
using Wino.Core.Domain.Interfaces;
+using Wino.Core.Domain.Models.Calendar;
namespace Wino.Core.Domain.Entities.Calendar;
@@ -17,6 +19,14 @@ public class CalendarItem : ICalendarItem
public string Description { get; set; }
public string Location { get; set; }
+ ///
+ /// Indicates whether this item is a local preview that hasn't been synced to the server yet.
+ /// When true, the item exists only in the local database without a RemoteEventId.
+ /// Used to prevent duplicates when the server returns the newly created event.
+ ///
+ [Ignore]
+ public bool IsLocalPreview => string.IsNullOrEmpty(RemoteEventId);
+
public DateTime StartDate { get; set; }
public DateTime EndDate
@@ -27,8 +37,17 @@ public class CalendarItem : ICalendarItem
}
}
- public TimeSpan StartDateOffset { get; set; }
- public TimeSpan EndDateOffset { get; set; }
+ ///
+ /// IANA timezone identifier for the start time (e.g., "America/New_York", "Europe/London").
+ /// If null or empty, UTC is assumed.
+ ///
+ public string StartTimeZone { get; set; }
+
+ ///
+ /// IANA timezone identifier for the end time (e.g., "America/New_York", "Europe/London").
+ /// If null or empty, UTC is assumed.
+ ///
+ public string EndTimeZone { get; set; }
private ITimePeriod _period;
public ITimePeriod Period
@@ -55,9 +74,7 @@ public class CalendarItem : ICalendarItem
}
///
- /// Events that are either an exceptional instance of a recurring event or occurrences.
- /// IsOccurrence is used to display occurrence instances of parent recurring events.
- /// IsOccurrence == false && IsRecurringChild == true => exceptional single instance.
+ /// Events that are child instances of a recurring event (occurrences or exceptions).
///
public bool IsRecurringChild
{
@@ -68,7 +85,7 @@ public class CalendarItem : ICalendarItem
}
///
- /// Events that are either an exceptional instance of a recurring event or occurrences.
+ /// Events that are part of a recurring series (either as parent or child).
///
public bool IsRecurringEvent => IsRecurringChild || IsRecurringParent;
@@ -121,8 +138,15 @@ public class CalendarItem : ICalendarItem
// TODO
public string CustomEventColorHex { get; set; }
public string HtmlLink { get; set; }
+ public DateTime? SnoozedUntil { get; set; }
public CalendarItemStatus Status { get; set; }
public CalendarItemVisibility Visibility { get; set; }
+
+ ///
+ /// Indicates how the event should be shown in the calendar (Free, Busy, Tentative, etc.).
+ ///
+ public CalendarItemShowAs ShowAs { get; set; } = CalendarItemShowAs.Busy;
+
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
public Guid CalendarId { get; set; }
@@ -131,49 +155,37 @@ public class CalendarItem : ICalendarItem
public IAccountCalendar AssignedCalendar { get; set; }
///
- /// Whether this item does not really exist in the database or not.
- /// These are used to display occurrence instances of parent recurring events.
+ /// Id to load information related to this event (attendees, reminders, etc.).
+ /// For child events, if they have their own data, use their own Id.
+ /// For events that share data with their parent, return parent's Id.
///
- [Ignore]
- public bool IsOccurrence { get; set; }
+ public Guid EventTrackingId => Id;
///
- /// Id to load information related to this event.
- /// Occurrences tracked by the parent recurring event if they are not exceptional instances.
- /// Recurring children here are exceptional instances. They have their own info in the database including Id.
+ /// Gets the start date converted to user's local timezone for display.
+ /// StartDate is stored according to StartTimeZone.
///
- public Guid EventTrackingId => IsOccurrence ? RecurringCalendarItemId.Value : Id;
-
- public CalendarItem CreateRecurrence(DateTime startDate, double durationInSeconds)
+ [Ignore]
+ public DateTime LocalStartDate
{
- // Create a copy with the new start date and duration
-
- return new CalendarItem
+ get
{
- Id = Guid.NewGuid(),
- Title = Title,
- Description = Description,
- Location = Location,
- StartDate = startDate,
- DurationInSeconds = durationInSeconds,
- Recurrence = Recurrence,
- OrganizerDisplayName = OrganizerDisplayName,
- OrganizerEmail = OrganizerEmail,
- RecurringCalendarItemId = Id,
- AssignedCalendar = AssignedCalendar,
- CalendarId = CalendarId,
- CreatedAt = CreatedAt,
- UpdatedAt = UpdatedAt,
- Visibility = Visibility,
- Status = Status,
- CustomEventColorHex = CustomEventColorHex,
- HtmlLink = HtmlLink,
- StartDateOffset = StartDateOffset,
- EndDateOffset = EndDateOffset,
- RemoteEventId = RemoteEventId,
- IsHidden = IsHidden,
- IsLocked = IsLocked,
- IsOccurrence = true
- };
+ return this.GetLocalStartDate();
+ }
}
+
+ ///
+ /// Gets the end date converted to user's local timezone for display.
+ /// EndDate is calculated from StartDate and is in StartTimeZone.
+ ///
+ [Ignore]
+ public DateTime LocalEndDate
+ {
+ get
+ {
+ return this.GetLocalEndDate();
+ }
+ }
+
+ public string GetDisplayTitle(ITimePeriod displayingPeriod, CalendarSettings calendarSettings) => Period.ToString();
}
diff --git a/Wino.Core.Domain/Entities/Calendar/Reminder.cs b/Wino.Core.Domain/Entities/Calendar/Reminder.cs
index 3dc64b81..389c4cef 100644
--- a/Wino.Core.Domain/Entities/Calendar/Reminder.cs
+++ b/Wino.Core.Domain/Entities/Calendar/Reminder.cs
@@ -10,6 +10,10 @@ public class Reminder
public Guid Id { get; set; }
public Guid CalendarItemId { get; set; }
- public DateTimeOffset ReminderTime { get; set; }
+ ///
+ /// Duration in seconds before the event start time when the reminder should trigger.
+ /// For example, 900 seconds = 15 minutes before event.
+ ///
+ public long DurationInSeconds { get; set; }
public CalendarItemReminderType ReminderType { get; set; }
}
diff --git a/Wino.Core.Domain/Entities/Mail/EmailTemplate.cs b/Wino.Core.Domain/Entities/Mail/EmailTemplate.cs
new file mode 100644
index 00000000..271e51b0
--- /dev/null
+++ b/Wino.Core.Domain/Entities/Mail/EmailTemplate.cs
@@ -0,0 +1,16 @@
+using System;
+using SQLite;
+
+namespace Wino.Core.Domain.Entities.Mail;
+
+public class EmailTemplate
+{
+ [PrimaryKey]
+ public Guid Id { get; set; }
+
+ public string Name { get; set; } = string.Empty;
+
+ public string Description { get; set; } = string.Empty;
+
+ public string HtmlContent { get; set; } = string.Empty;
+}
diff --git a/Wino.Core.Domain/Entities/Mail/MailAccountAlias.cs b/Wino.Core.Domain/Entities/Mail/MailAccountAlias.cs
index f13dd5ef..b23aaf4e 100644
--- a/Wino.Core.Domain/Entities/Mail/MailAccountAlias.cs
+++ b/Wino.Core.Domain/Entities/Mail/MailAccountAlias.cs
@@ -1,4 +1,6 @@
using System;
+using System.Collections.ObjectModel;
+using System.Security.Cryptography.X509Certificates;
using SQLite;
namespace Wino.Core.Domain.Entities.Mail;
@@ -59,4 +61,13 @@ public class MailAccountAlias : RemoteAccountAlias
/// Root aliases can't be deleted.
///
public bool CanDelete => !IsRootAlias;
+
+ public string SelectedSigningCertificateThumbprint { get; set; }
+ public bool IsSmimeEncryptionEnabled { get; set; }
+
+ [Ignore]
+ public X509Certificate2 SelectedSigningCertificate { get; set; }
+
+ [Ignore]
+ public ObservableCollection Certificates { get; set; } = [];
}
diff --git a/Wino.Core.Domain/Entities/Mail/MailCopy.cs b/Wino.Core.Domain/Entities/Mail/MailCopy.cs
index b3c1049d..ebd71c61 100644
--- a/Wino.Core.Domain/Entities/Mail/MailCopy.cs
+++ b/Wino.Core.Domain/Entities/Mail/MailCopy.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using SQLite;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
-using Wino.Core.Domain.Models.MailItem;
namespace Wino.Core.Domain.Entities.Mail;
@@ -11,7 +10,7 @@ namespace Wino.Core.Domain.Entities.Mail;
/// Summary of the parsed MIME messages.
/// Wino will do non-network operations on this table and others from the original MIME.
///
-public class MailCopy : IMailItem
+public class MailCopy
{
///
/// Unique Id of the mail.
@@ -104,6 +103,11 @@ public class MailCopy : IMailItem
///
public bool HasAttachments { get; set; }
+ ///
+ /// Type of mail item (regular mail, calendar invitation, calendar response, etc.).
+ ///
+ public MailItemType ItemType { get; set; } = MailItemType.Mail;
+
///
/// Assigned draft id.
///
diff --git a/Wino.Core.Domain/Entities/Mail/MailInvitationCalendarMapping.cs b/Wino.Core.Domain/Entities/Mail/MailInvitationCalendarMapping.cs
new file mode 100644
index 00000000..d1290062
--- /dev/null
+++ b/Wino.Core.Domain/Entities/Mail/MailInvitationCalendarMapping.cs
@@ -0,0 +1,31 @@
+using System;
+using SQLite;
+
+namespace Wino.Core.Domain.Entities.Mail;
+
+///
+/// Maps a calendar invitation mail item to a persisted calendar event.
+///
+public class MailInvitationCalendarMapping
+{
+ [PrimaryKey]
+ public Guid Id { get; set; }
+
+ public Guid AccountId { get; set; }
+
+ ///
+ /// MailCopy.Id value of the invitation mail.
+ ///
+ public string MailCopyId { get; set; }
+
+ ///
+ /// iCalendar UID extracted from invitation MIME/ICS content.
+ ///
+ public string InvitationUid { get; set; }
+
+ public Guid CalendarId { get; set; }
+ public Guid CalendarItemId { get; set; }
+ public string CalendarRemoteEventId { get; set; }
+
+ public DateTime UpdatedAtUtc { get; set; } = DateTime.UtcNow;
+}
diff --git a/Wino.Core.Domain/Entities/Mail/MailItemFolder.cs b/Wino.Core.Domain/Entities/Mail/MailItemFolder.cs
index bb1a2cae..4555799e 100644
--- a/Wino.Core.Domain/Entities/Mail/MailItemFolder.cs
+++ b/Wino.Core.Domain/Entities/Mail/MailItemFolder.cs
@@ -29,6 +29,8 @@ public class MailItemFolder : IMailItemFolder
// For IMAP
public uint UidValidity { get; set; }
public long HighestModeSeq { get; set; }
+ public uint HighestKnownUid { get; set; }
+ public DateTime? LastUidReconcileUtc { get; set; }
///
/// Outlook shares delta changes per-folder. Gmail is for per-account.
diff --git a/Wino.Core.Domain/Entities/Shared/AccountContact.cs b/Wino.Core.Domain/Entities/Shared/AccountContact.cs
index 49949665..8ca24de4 100644
--- a/Wino.Core.Domain/Entities/Shared/AccountContact.cs
+++ b/Wino.Core.Domain/Entities/Shared/AccountContact.cs
@@ -11,7 +11,7 @@ namespace Wino.Core.Domain.Entities.Shared;
// TODO: This can easily evolve to Contact store, just like People app in Windows 10/11.
// Do it.
-public class AccountContact : IEquatable
+public class AccountContact : IEquatable, IContactDisplayItem
{
///
/// E-mail address of the contact.
@@ -25,9 +25,10 @@ public class AccountContact : IEquatable
public string Name { get; set; }
///
- /// Base64 encoded profile image of the contact.
+ /// File ID for the contact picture stored on disk.
+ /// The actual file lives at {ApplicationDataFolderPath}/contacts/{ContactPictureFileId}.jpg.
///
- public string Base64ContactPicture { get; set; }
+ public Guid? ContactPictureFileId { get; set; }
///
/// All registered accounts have their contacts registered as root.
@@ -36,6 +37,15 @@ public class AccountContact : IEquatable
///
public bool IsRootContact { get; set; }
+ ///
+ /// When true, indicates that the contact has been manually modified by the user.
+ /// Contacts with this flag set to true should not be updated during synchronization.
+ ///
+ public bool IsOverridden { get; set; } = false;
+
+ public string DisplayName => string.IsNullOrWhiteSpace(Name) ? Address : Name;
+ AccountContact IContactDisplayItem.PreviewContact => this;
+
public override bool Equals(object obj)
{
return Equals(obj as AccountContact);
diff --git a/Wino.Core.Domain/Entities/Shared/ContactGroup.cs b/Wino.Core.Domain/Entities/Shared/ContactGroup.cs
new file mode 100644
index 00000000..6fd28dcd
--- /dev/null
+++ b/Wino.Core.Domain/Entities/Shared/ContactGroup.cs
@@ -0,0 +1,19 @@
+using System;
+using SQLite;
+
+namespace Wino.Core.Domain.Entities.Shared;
+
+///
+/// A named group of contacts that can be expanded to individual addresses during mail composition.
+///
+public class ContactGroup
+{
+ [PrimaryKey]
+ public Guid Id { get; set; }
+
+ /// Display name of the group (e.g., "Team Alpha", "Family").
+ public string Name { get; set; }
+
+ /// Optional description for the group.
+ public string Description { get; set; }
+}
diff --git a/Wino.Core.Domain/Entities/Shared/ContactGroupMember.cs b/Wino.Core.Domain/Entities/Shared/ContactGroupMember.cs
new file mode 100644
index 00000000..1209f5e3
--- /dev/null
+++ b/Wino.Core.Domain/Entities/Shared/ContactGroupMember.cs
@@ -0,0 +1,21 @@
+using System;
+using SQLite;
+
+namespace Wino.Core.Domain.Entities.Shared;
+
+///
+/// Associates an e-mail address with a .
+///
+public class ContactGroupMember
+{
+ [PrimaryKey, AutoIncrement]
+ public int Id { get; set; }
+
+ /// Group this member belongs to.
+ [Indexed]
+ public Guid GroupId { get; set; }
+
+ /// E-mail address of the member (FK to AccountContact.Address).
+ [Indexed]
+ public string MemberAddress { get; set; }
+}
diff --git a/Wino.Core.Domain/Entities/Shared/CustomServerInformation.cs b/Wino.Core.Domain/Entities/Shared/CustomServerInformation.cs
index 00aee83b..555fb64d 100644
--- a/Wino.Core.Domain/Entities/Shared/CustomServerInformation.cs
+++ b/Wino.Core.Domain/Entities/Shared/CustomServerInformation.cs
@@ -30,6 +30,11 @@ public class CustomServerInformation
public string OutgoingServerUsername { get; set; }
public string OutgoingServerPassword { get; set; }
+ public string CalDavServiceUrl { get; set; }
+ public string CalDavUsername { get; set; }
+ public string CalDavPassword { get; set; }
+ public ImapCalendarSupportMode CalendarSupportMode { get; set; }
+
///
/// useSSL True: SslOnConnect
/// useSSL False: StartTlsWhenAvailable
@@ -65,6 +70,8 @@ public class CustomServerInformation
{ "OutgoingServerPort", OutgoingServerPort },
{ "OutgoingServerSocketOption", OutgoingServerSocketOption.ToString() },
{ "OutgoingAuthenticationMethod", OutgoingAuthenticationMethod.ToString() },
+ { "CalendarSupportMode", CalendarSupportMode.ToString() },
+ { "CalDavServiceUrl", CalDavServiceUrl },
{ "ProxyServer", ProxyServer },
{ "ProxyServerPort", ProxyServerPort }
};
diff --git a/Wino.Core.Domain/Entities/Shared/IContactDisplayItem.cs b/Wino.Core.Domain/Entities/Shared/IContactDisplayItem.cs
new file mode 100644
index 00000000..4938f8d4
--- /dev/null
+++ b/Wino.Core.Domain/Entities/Shared/IContactDisplayItem.cs
@@ -0,0 +1,8 @@
+namespace Wino.Core.Domain.Entities.Shared;
+
+public interface IContactDisplayItem
+{
+ string DisplayName { get; }
+ string Address { get; }
+ AccountContact PreviewContact { get; }
+}
diff --git a/Wino.Core.Domain/Entities/Shared/KeyboardShortcut.cs b/Wino.Core.Domain/Entities/Shared/KeyboardShortcut.cs
new file mode 100644
index 00000000..8b066ad5
--- /dev/null
+++ b/Wino.Core.Domain/Entities/Shared/KeyboardShortcut.cs
@@ -0,0 +1,65 @@
+using System;
+using SQLite;
+using Wino.Core.Domain.Enums;
+
+namespace Wino.Core.Domain.Entities.Shared;
+
+///
+/// Represents a user-defined keyboard shortcut for mail operations.
+///
+public class KeyboardShortcut
+{
+ [PrimaryKey]
+ public Guid Id { get; set; }
+
+ ///
+ /// The application mode this shortcut applies to.
+ ///
+ public WinoApplicationMode Mode { get; set; } = WinoApplicationMode.Mail;
+
+ ///
+ /// The key combination string (e.g., "D", "Delete", "F1").
+ ///
+ public string Key { get; set; }
+
+ ///
+ /// The modifier keys for this shortcut.
+ ///
+ public ModifierKeys ModifierKeys { get; set; }
+
+ ///
+ /// The shortcut action this shortcut triggers.
+ ///
+ public KeyboardShortcutAction Action { get; set; }
+
+ ///
+ /// Whether this shortcut is enabled.
+ ///
+ public bool IsEnabled { get; set; } = true;
+
+ ///
+ /// When this shortcut was created.
+ ///
+ public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+
+ ///
+ /// User-friendly display name for the shortcut.
+ ///
+ public string DisplayName
+ {
+ get
+ {
+ var modifierText = string.Empty;
+ if (ModifierKeys.HasFlag(ModifierKeys.Control))
+ modifierText += "Ctrl+";
+ if (ModifierKeys.HasFlag(ModifierKeys.Alt))
+ modifierText += "Alt+";
+ if (ModifierKeys.HasFlag(ModifierKeys.Shift))
+ modifierText += "Shift+";
+ if (ModifierKeys.HasFlag(ModifierKeys.Windows))
+ modifierText += "Win+";
+
+ return modifierText + Key;
+ }
+ }
+}
diff --git a/Wino.Core.Domain/Entities/Shared/MailAccount.cs b/Wino.Core.Domain/Entities/Shared/MailAccount.cs
index 8f40f70d..c27008a4 100644
--- a/Wino.Core.Domain/Entities/Shared/MailAccount.cs
+++ b/Wino.Core.Domain/Entities/Shared/MailAccount.cs
@@ -78,6 +78,14 @@ public class MailAccount
///
public SpecialImapProvider SpecialImapProvider { get; set; }
+ ///
+ /// Gets or sets whether calendar access is granted for this account.
+ /// When false, synchronizers will not process EventMessages or calendar invitations.
+ /// Default is false for existing accounts to prevent scope issues.
+ /// New accounts created after this feature will have this set to true.
+ ///
+ public bool IsCalendarAccessGranted { get; set; }
+
///
/// Contains the merged inbox this account belongs to.
/// Ignored for all SQLite operations.
@@ -98,6 +106,12 @@ public class MailAccount
[Ignore]
public MailAccountPreferences Preferences { get; set; }
+ ///
+ /// Last time folder structure was synchronized.
+ /// Used for optimization - skip folder sync if synced recently.
+ ///
+ public DateTime? LastFolderStructureSyncDate { get; set; }
+
///
/// Gets whether the account can perform ProfileInformation sync type.
///
@@ -107,4 +121,6 @@ public class MailAccount
/// Gets whether the account can perform AliasInformation sync type.
///
public bool IsAliasSyncSupported => ProviderType == MailProviderType.Gmail;
+
+ public override string ToString() => Name;
}
diff --git a/Wino.Core.Domain/Entities/Shared/WinoAccount.cs b/Wino.Core.Domain/Entities/Shared/WinoAccount.cs
new file mode 100644
index 00000000..9b0a92e1
--- /dev/null
+++ b/Wino.Core.Domain/Entities/Shared/WinoAccount.cs
@@ -0,0 +1,30 @@
+using System;
+using SQLite;
+
+namespace Wino.Core.Domain.Entities.Shared;
+
+public class WinoAccount
+{
+ [PrimaryKey]
+ public Guid Id { get; set; }
+
+ public string Email { get; set; } = string.Empty;
+
+ public string AccountStatus { get; set; } = string.Empty;
+
+ public bool HasPassword { get; set; }
+
+ public bool HasGoogleLogin { get; set; }
+
+ public bool HasFacebookLogin { get; set; }
+
+ public string AccessToken { get; set; } = string.Empty;
+
+ public DateTime AccessTokenExpiresAtUtc { get; set; }
+
+ public string RefreshToken { get; set; } = string.Empty;
+
+ public DateTime RefreshTokenExpiresAtUtc { get; set; }
+
+ public DateTime LastAuthenticatedUtc { get; set; }
+}
diff --git a/Wino.Core.Domain/Enums/AccountCreationDialogState.cs b/Wino.Core.Domain/Enums/AccountCreationDialogState.cs
index 0cc10546..2e91d743 100644
--- a/Wino.Core.Domain/Enums/AccountCreationDialogState.cs
+++ b/Wino.Core.Domain/Enums/AccountCreationDialogState.cs
@@ -5,6 +5,7 @@ public enum AccountCreationDialogState
Idle,
SigningIn,
PreparingFolders,
+ CalendarMetadataFetch,
Completed,
ManuelSetupWaiting,
TestingConnection,
diff --git a/Wino.Core.Domain/Enums/AccountSetupStepStatus.cs b/Wino.Core.Domain/Enums/AccountSetupStepStatus.cs
new file mode 100644
index 00000000..a0bb99a3
--- /dev/null
+++ b/Wino.Core.Domain/Enums/AccountSetupStepStatus.cs
@@ -0,0 +1,9 @@
+namespace Wino.Core.Domain.Enums;
+
+public enum AccountSetupStepStatus
+{
+ Pending,
+ InProgress,
+ Succeeded,
+ Failed
+}
diff --git a/Wino.Core.Domain/Enums/AiActionType.cs b/Wino.Core.Domain/Enums/AiActionType.cs
new file mode 100644
index 00000000..26501bb0
--- /dev/null
+++ b/Wino.Core.Domain/Enums/AiActionType.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace Wino.Core.Domain.Enums;
+
+[Flags]
+public enum AiActionType
+{
+ None = 0,
+ Translate = 1,
+ Rewrite = 2,
+ Summarize = 4,
+}
diff --git a/Wino.Core.Domain/Enums/CalendarDisplayType.cs b/Wino.Core.Domain/Enums/CalendarDisplayType.cs
index 5582899b..afb838d2 100644
--- a/Wino.Core.Domain/Enums/CalendarDisplayType.cs
+++ b/Wino.Core.Domain/Enums/CalendarDisplayType.cs
@@ -5,6 +5,5 @@ public enum CalendarDisplayType
Day,
Week,
WorkWeek,
- Month,
- Year
+ Month
}
diff --git a/Wino.Core.Domain/Enums/CalendarInitInitiative.cs b/Wino.Core.Domain/Enums/CalendarInitInitiative.cs
deleted file mode 100644
index d5e08a76..00000000
--- a/Wino.Core.Domain/Enums/CalendarInitInitiative.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace Wino.Core.Domain.Enums;
-
-///
-/// Trigger to load more data.
-///
-public enum CalendarInitInitiative
-{
- User,
- App
-}
diff --git a/Wino.Core.Domain/Enums/CalendarItemShowAs.cs b/Wino.Core.Domain/Enums/CalendarItemShowAs.cs
new file mode 100644
index 00000000..57c55f00
--- /dev/null
+++ b/Wino.Core.Domain/Enums/CalendarItemShowAs.cs
@@ -0,0 +1,13 @@
+namespace Wino.Core.Domain.Enums;
+
+///
+/// Defines how a calendar item should be displayed in terms of availability.
+///
+public enum CalendarItemShowAs
+{
+ Free,
+ Tentative,
+ Busy,
+ OutOfOffice,
+ WorkingElsewhere
+}
diff --git a/Wino.Core.Domain/Enums/CalendarItemStatus.cs b/Wino.Core.Domain/Enums/CalendarItemStatus.cs
index e8605573..c6d08b52 100644
--- a/Wino.Core.Domain/Enums/CalendarItemStatus.cs
+++ b/Wino.Core.Domain/Enums/CalendarItemStatus.cs
@@ -3,7 +3,7 @@
public enum CalendarItemStatus
{
NotResponded,
- Confirmed,
+ Accepted,
Tentative,
Cancelled,
}
diff --git a/Wino.Core.Domain/Enums/CalendarItemUpdateSource.cs b/Wino.Core.Domain/Enums/CalendarItemUpdateSource.cs
new file mode 100644
index 00000000..f4e3828c
--- /dev/null
+++ b/Wino.Core.Domain/Enums/CalendarItemUpdateSource.cs
@@ -0,0 +1,22 @@
+namespace Wino.Core.Domain.Enums;
+
+///
+/// Indicates the source of a calendar item update.
+///
+public enum CalendarItemUpdateSource
+{
+ ///
+ /// Update originated from client-side UI changes (ApplyUIChanges).
+ ///
+ ClientUpdated,
+
+ ///
+ /// Update originated from client-side UI revert (RevertUIChanges).
+ ///
+ ClientReverted,
+
+ ///
+ /// Update originated from server synchronization or database operations.
+ ///
+ Server
+}
diff --git a/Wino.Core.Domain/Enums/CalendarSynchronizationType.cs b/Wino.Core.Domain/Enums/CalendarSynchronizationType.cs
index d9f01a19..96351243 100644
--- a/Wino.Core.Domain/Enums/CalendarSynchronizationType.cs
+++ b/Wino.Core.Domain/Enums/CalendarSynchronizationType.cs
@@ -5,6 +5,7 @@ public enum CalendarSynchronizationType
ExecuteRequests, // Execute all requests in the queue.
CalendarMetadata, // Sync calendar metadata.
CalendarEvents, // Sync all events for all calendars.
+ Strict, // Run metadata and event synchronization in sequence.
SingleCalendar, // Sync events for only specified calendars.
UpdateProfile // Update profile information only.
}
diff --git a/Wino.Core.Domain/Enums/EmailGroupingType.cs b/Wino.Core.Domain/Enums/EmailGroupingType.cs
new file mode 100644
index 00000000..703cc21a
--- /dev/null
+++ b/Wino.Core.Domain/Enums/EmailGroupingType.cs
@@ -0,0 +1,10 @@
+namespace Wino.Core.Domain.Enums;
+
+///
+/// Grouping options for emails
+///
+public enum EmailGroupingType
+{
+ ByFromName,
+ ByDate
+}
diff --git a/Wino.Core.Domain/Enums/ImapCalendarSupportMode.cs b/Wino.Core.Domain/Enums/ImapCalendarSupportMode.cs
new file mode 100644
index 00000000..4e082a87
--- /dev/null
+++ b/Wino.Core.Domain/Enums/ImapCalendarSupportMode.cs
@@ -0,0 +1,8 @@
+namespace Wino.Core.Domain.Enums;
+
+public enum ImapCalendarSupportMode
+{
+ Disabled = 0,
+ CalDav = 1,
+ LocalOnly = 2
+}
diff --git a/Wino.Core.Domain/Enums/KeyboardShortcutAction.cs b/Wino.Core.Domain/Enums/KeyboardShortcutAction.cs
new file mode 100644
index 00000000..f48f5cf4
--- /dev/null
+++ b/Wino.Core.Domain/Enums/KeyboardShortcutAction.cs
@@ -0,0 +1,16 @@
+namespace Wino.Core.Domain.Enums;
+
+public enum KeyboardShortcutAction
+{
+ None,
+ NewMail,
+ ToggleReadUnread,
+ ToggleFlag,
+ ToggleArchive,
+ Delete,
+ Move,
+ Reply,
+ ReplyAll,
+ Send,
+ NewEvent
+}
diff --git a/Wino.Core.Domain/Enums/MailCopyChangeFlags.cs b/Wino.Core.Domain/Enums/MailCopyChangeFlags.cs
new file mode 100644
index 00000000..f500aa5d
--- /dev/null
+++ b/Wino.Core.Domain/Enums/MailCopyChangeFlags.cs
@@ -0,0 +1,57 @@
+using System;
+
+namespace Wino.Core.Domain.Enums;
+
+[Flags]
+public enum MailCopyChangeFlags
+{
+ None = 0,
+ Id = 1 << 0,
+ FolderId = 1 << 1,
+ ThreadId = 1 << 2,
+ MessageId = 1 << 3,
+ References = 1 << 4,
+ InReplyTo = 1 << 5,
+ FromName = 1 << 6,
+ FromAddress = 1 << 7,
+ Subject = 1 << 8,
+ PreviewText = 1 << 9,
+ CreationDate = 1 << 10,
+ Importance = 1 << 11,
+ IsRead = 1 << 12,
+ IsFlagged = 1 << 13,
+ IsFocused = 1 << 14,
+ HasAttachments = 1 << 15,
+ ItemType = 1 << 16,
+ DraftId = 1 << 17,
+ IsDraft = 1 << 18,
+ FileId = 1 << 19,
+ AssignedFolder = 1 << 20,
+ AssignedAccount = 1 << 21,
+ SenderContact = 1 << 22,
+ UniqueId = 1 << 23,
+ All = Id |
+ FolderId |
+ ThreadId |
+ MessageId |
+ References |
+ InReplyTo |
+ FromName |
+ FromAddress |
+ Subject |
+ PreviewText |
+ CreationDate |
+ Importance |
+ IsRead |
+ IsFlagged |
+ IsFocused |
+ HasAttachments |
+ ItemType |
+ DraftId |
+ IsDraft |
+ FileId |
+ AssignedFolder |
+ AssignedAccount |
+ SenderContact |
+ UniqueId
+}
diff --git a/Wino.Core.Domain/Enums/MailItemType.cs b/Wino.Core.Domain/Enums/MailItemType.cs
new file mode 100644
index 00000000..63fcfa5a
--- /dev/null
+++ b/Wino.Core.Domain/Enums/MailItemType.cs
@@ -0,0 +1,27 @@
+namespace Wino.Core.Domain.Enums;
+
+///
+/// Represents the type of mail item.
+///
+public enum MailItemType
+{
+ ///
+ /// Regular mail message.
+ ///
+ Mail = 0,
+
+ ///
+ /// Calendar invitation (meeting request).
+ ///
+ CalendarInvitation = 1,
+
+ ///
+ /// Calendar response (meeting accepted, tentatively accepted, or declined).
+ ///
+ CalendarResponse = 2,
+
+ ///
+ /// Calendar cancellation (meeting cancelled).
+ ///
+ CalendarCancellation = 3
+}
diff --git a/Wino.Core.Domain/Enums/MailOperation.cs b/Wino.Core.Domain/Enums/MailOperation.cs
index 973ff62f..9d482958 100644
--- a/Wino.Core.Domain/Enums/MailOperation.cs
+++ b/Wino.Core.Domain/Enums/MailOperation.cs
@@ -19,6 +19,18 @@ public enum FolderSynchronizerOperation
RenameFolder,
EmptyFolder,
MarkFolderRead,
+ DeleteFolder,
+ CreateSubFolder,
+}
+
+public enum CalendarSynchronizerOperation
+{
+ CreateEvent,
+ UpdateEvent,
+ DeleteEvent,
+ AcceptEvent,
+ DeclineEvent,
+ TentativeEvent,
}
// UI requests
diff --git a/Wino.Core.Domain/Enums/MailUpdateSource.cs b/Wino.Core.Domain/Enums/MailUpdateSource.cs
new file mode 100644
index 00000000..e8a3b91f
--- /dev/null
+++ b/Wino.Core.Domain/Enums/MailUpdateSource.cs
@@ -0,0 +1,22 @@
+namespace Wino.Core.Domain.Enums;
+
+///
+/// Indicates the source of a mail update.
+///
+public enum MailUpdateSource
+{
+ ///
+ /// Update originated from client-side UI changes (ApplyUIChanges).
+ ///
+ ClientUpdated,
+
+ ///
+ /// Update originated from client-side UI revert (RevertUIChanges).
+ ///
+ ClientReverted,
+
+ ///
+ /// Update originated from server synchronization or database operations.
+ ///
+ Server
+}
diff --git a/Wino.Core.Domain/Enums/ModifierKeys.cs b/Wino.Core.Domain/Enums/ModifierKeys.cs
new file mode 100644
index 00000000..f5d7147d
--- /dev/null
+++ b/Wino.Core.Domain/Enums/ModifierKeys.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace Wino.Core.Domain.Enums;
+
+///
+/// Defines keyboard modifier keys that can be used in keyboard shortcuts.
+///
+[Flags]
+public enum ModifierKeys
+{
+ None = 0,
+ Control = 1,
+ Alt = 2,
+ Shift = 4,
+ Windows = 8
+}
\ No newline at end of file
diff --git a/Wino.Core.Domain/Enums/NavigationReferenceFrame.cs b/Wino.Core.Domain/Enums/NavigationReferenceFrame.cs
index 3495a98e..e7279206 100644
--- a/Wino.Core.Domain/Enums/NavigationReferenceFrame.cs
+++ b/Wino.Core.Domain/Enums/NavigationReferenceFrame.cs
@@ -3,5 +3,6 @@
public enum NavigationReferenceFrame
{
ShellFrame,
+ InnerShellFrame,
RenderingFrame
}
diff --git a/Wino.Core.Domain/Enums/NavigationTransitionEffect.cs b/Wino.Core.Domain/Enums/NavigationTransitionEffect.cs
new file mode 100644
index 00000000..73be75f6
--- /dev/null
+++ b/Wino.Core.Domain/Enums/NavigationTransitionEffect.cs
@@ -0,0 +1,27 @@
+namespace Wino.Core.Domain.Enums;
+
+///
+/// Specifies the animation effect to use during a slide navigation transition.
+///
+public enum NavigationTransitionEffect
+{
+ ///
+ /// The navigation transition effect starts from the left edge of the frame.
+ ///
+ FromLeft,
+
+ ///
+ /// The navigation transition effect starts from the right edge of the frame.
+ ///
+ FromRight,
+
+ ///
+ /// The navigation transition effect starts from the top edge of the frame.
+ ///
+ FromTop,
+
+ ///
+ /// The navigation transition effect starts from the bottom edge of the frame.
+ ///
+ FromBottom
+}
diff --git a/Wino.Core.Domain/Enums/NewEventButtonBehavior.cs b/Wino.Core.Domain/Enums/NewEventButtonBehavior.cs
new file mode 100644
index 00000000..9ef143e4
--- /dev/null
+++ b/Wino.Core.Domain/Enums/NewEventButtonBehavior.cs
@@ -0,0 +1,7 @@
+namespace Wino.Core.Domain.Enums;
+
+public enum NewEventButtonBehavior
+{
+ AskEachTime = 0,
+ AlwaysUseSpecificCalendar = 1
+}
diff --git a/Wino.Core.Domain/Enums/PrintCollation.cs b/Wino.Core.Domain/Enums/PrintCollation.cs
new file mode 100644
index 00000000..94b5bf93
--- /dev/null
+++ b/Wino.Core.Domain/Enums/PrintCollation.cs
@@ -0,0 +1,22 @@
+namespace Wino.Core.Domain.Enums;
+
+///
+/// Print collation options.
+///
+public enum PrintCollation
+{
+ ///
+ /// Default collation.
+ ///
+ Default = 0,
+
+ ///
+ /// Collated printing.
+ ///
+ Collated = 1,
+
+ ///
+ /// Uncollated printing.
+ ///
+ Uncollated = 2
+}
\ No newline at end of file
diff --git a/Wino.Core.Domain/Enums/PrintColorMode.cs b/Wino.Core.Domain/Enums/PrintColorMode.cs
new file mode 100644
index 00000000..869f9b15
--- /dev/null
+++ b/Wino.Core.Domain/Enums/PrintColorMode.cs
@@ -0,0 +1,22 @@
+namespace Wino.Core.Domain.Enums;
+
+///
+/// Print color mode options.
+///
+public enum PrintColorMode
+{
+ ///
+ /// Default color mode.
+ ///
+ Default = 0,
+
+ ///
+ /// Color printing.
+ ///
+ Color = 1,
+
+ ///
+ /// Grayscale printing.
+ ///
+ Grayscale = 2
+}
\ No newline at end of file
diff --git a/Wino.Core.Domain/Enums/PrintDuplex.cs b/Wino.Core.Domain/Enums/PrintDuplex.cs
new file mode 100644
index 00000000..51becc23
--- /dev/null
+++ b/Wino.Core.Domain/Enums/PrintDuplex.cs
@@ -0,0 +1,27 @@
+namespace Wino.Core.Domain.Enums;
+
+///
+/// Print duplex (double-sided) options.
+///
+public enum PrintDuplex
+{
+ ///
+ /// Default duplex mode.
+ ///
+ Default = 0,
+
+ ///
+ /// Single-sided printing.
+ ///
+ Simplex = 1,
+
+ ///
+ /// Double-sided printing with pages flipped horizontally.
+ ///
+ DuplexShortEdge = 2,
+
+ ///
+ /// Double-sided printing with pages flipped vertically.
+ ///
+ DuplexLongEdge = 3
+}
\ No newline at end of file
diff --git a/Wino.Core.Domain/Enums/PrintMediaSize.cs b/Wino.Core.Domain/Enums/PrintMediaSize.cs
new file mode 100644
index 00000000..5e454992
--- /dev/null
+++ b/Wino.Core.Domain/Enums/PrintMediaSize.cs
@@ -0,0 +1,57 @@
+namespace Wino.Core.Domain.Enums;
+
+///
+/// Print media size options.
+///
+public enum PrintMediaSize
+{
+ ///
+ /// Default media size.
+ ///
+ Default = 0,
+
+ ///
+ /// Letter size (8.5 x 11 inches).
+ ///
+ NorthAmericaLetter = 1,
+
+ ///
+ /// Legal size (8.5 x 14 inches).
+ ///
+ NorthAmericaLegal = 2,
+
+ ///
+ /// A4 size (210 x 297 mm).
+ ///
+ IsoA4 = 3,
+
+ ///
+ /// A3 size (297 x 420 mm).
+ ///
+ IsoA3 = 4,
+
+ ///
+ /// A5 size (148 x 210 mm).
+ ///
+ IsoA5 = 5,
+
+ ///
+ /// Tabloid size (11 x 17 inches).
+ ///
+ NorthAmericaTabloid = 6,
+
+ ///
+ /// Executive size (7.25 x 10.5 inches).
+ ///
+ NorthAmericaExecutive = 7,
+
+ ///
+ /// B4 size (250 x 353 mm).
+ ///
+ JisB4 = 8,
+
+ ///
+ /// B5 size (176 x 250 mm).
+ ///
+ JisB5 = 9
+}
\ No newline at end of file
diff --git a/Wino.Core.Domain/Enums/PrintOrientation.cs b/Wino.Core.Domain/Enums/PrintOrientation.cs
new file mode 100644
index 00000000..8b9b169c
--- /dev/null
+++ b/Wino.Core.Domain/Enums/PrintOrientation.cs
@@ -0,0 +1,17 @@
+namespace Wino.Core.Domain.Enums;
+
+///
+/// Print orientation options.
+///
+public enum PrintOrientation
+{
+ ///
+ /// Portrait orientation (default).
+ ///
+ Portrait = 0,
+
+ ///
+ /// Landscape orientation.
+ ///
+ Landscape = 1
+}
\ No newline at end of file
diff --git a/Wino.Core.Domain/Enums/ServerBackgroundMode.cs b/Wino.Core.Domain/Enums/ServerBackgroundMode.cs
deleted file mode 100644
index d7f9e5df..00000000
--- a/Wino.Core.Domain/Enums/ServerBackgroundMode.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-namespace Wino.Core.Domain.Enums;
-
-///
-/// What should happen to server app when the client is terminated.
-///
-public enum ServerBackgroundMode
-{
- MinimizedTray, // Still runs, tray icon is visible.
- Invisible, // Still runs, tray icon is invisible.
- Terminate // Server is terminated as Wino terminates.
-}
diff --git a/Wino.Core.Domain/Enums/SynchronizationCompletedState.cs b/Wino.Core.Domain/Enums/SynchronizationCompletedState.cs
index 6a2edb6c..fc794169 100644
--- a/Wino.Core.Domain/Enums/SynchronizationCompletedState.cs
+++ b/Wino.Core.Domain/Enums/SynchronizationCompletedState.cs
@@ -4,5 +4,6 @@ public enum SynchronizationCompletedState
{
Success, // All succeeded.
Canceled, // Canceled by user or HTTP call.
- Failed // Exception.
+ Failed, // Exception.
+ PartiallyCompleted // Some folders succeeded, some failed.
}
diff --git a/Wino.Core.Domain/Enums/SynchronizationSource.cs b/Wino.Core.Domain/Enums/SynchronizationSource.cs
deleted file mode 100644
index d7b3e2d3..00000000
--- a/Wino.Core.Domain/Enums/SynchronizationSource.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-namespace Wino.Core.Domain.Enums;
-
-///
-/// Enumeration for the source of synchronization.
-/// Right now it can either be from the client or the server.
-///
-public enum SynchronizationSource
-{
- Client,
- Server
-}
diff --git a/Wino.Core.Domain/Enums/SynchronizerErrorCategory.cs b/Wino.Core.Domain/Enums/SynchronizerErrorCategory.cs
new file mode 100644
index 00000000..4590273c
--- /dev/null
+++ b/Wino.Core.Domain/Enums/SynchronizerErrorCategory.cs
@@ -0,0 +1,47 @@
+namespace Wino.Core.Domain.Enums;
+
+///
+/// Categorizes synchronization errors by their root cause for targeted handling.
+///
+public enum SynchronizerErrorCategory
+{
+ ///
+ /// Network-related issues: connection timeouts, DNS failures, socket errors.
+ ///
+ Network,
+
+ ///
+ /// Authentication failures: invalid credentials, expired tokens, revoked access.
+ ///
+ Authentication,
+
+ ///
+ /// Rate limiting: too many requests (HTTP 429), quota exceeded.
+ ///
+ RateLimit,
+
+ ///
+ /// Resource not found: folder or message deleted externally (HTTP 404).
+ ///
+ ResourceNotFound,
+
+ ///
+ /// Server errors: internal server errors (HTTP 5xx), service unavailable.
+ ///
+ ServerError,
+
+ ///
+ /// Protocol errors: IMAP/SMTP command failures, malformed responses.
+ ///
+ ProtocolError,
+
+ ///
+ /// Validation errors: invalid data, constraint violations.
+ ///
+ Validation,
+
+ ///
+ /// Unknown or unclassified error.
+ ///
+ Unknown
+}
diff --git a/Wino.Core.Domain/Enums/SynchronizerErrorSeverity.cs b/Wino.Core.Domain/Enums/SynchronizerErrorSeverity.cs
new file mode 100644
index 00000000..49ce1d4c
--- /dev/null
+++ b/Wino.Core.Domain/Enums/SynchronizerErrorSeverity.cs
@@ -0,0 +1,31 @@
+namespace Wino.Core.Domain.Enums;
+
+///
+/// Classifies the severity of synchronization errors to determine retry behavior.
+///
+public enum SynchronizerErrorSeverity
+{
+ ///
+ /// Transient error that should be retried with exponential backoff.
+ /// Examples: network timeout, temporary server unavailability, rate limiting.
+ ///
+ Transient,
+
+ ///
+ /// Error that can be recovered from by skipping the affected item/folder and continuing sync.
+ /// Examples: folder deleted externally, message not found, permission denied on single item.
+ ///
+ Recoverable,
+
+ ///
+ /// Fatal error that requires stopping synchronization and user intervention.
+ /// Examples: account disabled, server permanently unavailable, critical configuration error.
+ ///
+ Fatal,
+
+ ///
+ /// Authentication error that requires the user to re-authenticate.
+ /// Examples: token expired, password changed, OAuth refresh failed.
+ ///
+ AuthRequired
+}
diff --git a/Wino.Core.Domain/Enums/WindowBackdropType.cs b/Wino.Core.Domain/Enums/WindowBackdropType.cs
new file mode 100644
index 00000000..62b74cf8
--- /dev/null
+++ b/Wino.Core.Domain/Enums/WindowBackdropType.cs
@@ -0,0 +1,11 @@
+namespace Wino.Core.Domain.Enums;
+
+public enum WindowBackdropType
+{
+ None,
+ Mica,
+ MicaAlt,
+ DesktopAcrylic,
+ AcrylicBase,
+ AcrylicThin
+}
\ No newline at end of file
diff --git a/Wino.Core.Domain/Enums/WinoAddOnProductType.cs b/Wino.Core.Domain/Enums/WinoAddOnProductType.cs
new file mode 100644
index 00000000..e4e08540
--- /dev/null
+++ b/Wino.Core.Domain/Enums/WinoAddOnProductType.cs
@@ -0,0 +1,7 @@
+namespace Wino.Core.Domain.Enums;
+
+public enum WinoAddOnProductType
+{
+ AI_PACK,
+ UNLIMITED_ACCOUNTS
+}
diff --git a/Wino.Core.Domain/Enums/WinoApplicationMode.cs b/Wino.Core.Domain/Enums/WinoApplicationMode.cs
new file mode 100644
index 00000000..a1b6114d
--- /dev/null
+++ b/Wino.Core.Domain/Enums/WinoApplicationMode.cs
@@ -0,0 +1,9 @@
+namespace Wino.Core.Domain.Enums;
+
+public enum WinoApplicationMode
+{
+ Mail,
+ Calendar,
+ Contacts,
+ Settings
+}
diff --git a/Wino.Core.Domain/Enums/WinoPage.cs b/Wino.Core.Domain/Enums/WinoPage.cs
index 9bf5347a..eb78105a 100644
--- a/Wino.Core.Domain/Enums/WinoPage.cs
+++ b/Wino.Core.Domain/Enums/WinoPage.cs
@@ -9,8 +9,8 @@ public enum WinoPage
IdlePage,
ComposePage,
SettingsPage,
+ ContactsPage,
MailRenderingPage,
- WelcomePage,
AccountDetailsPage,
MergedAccountDetailsPage,
ManageAccountsPage,
@@ -21,13 +21,27 @@ public enum WinoPage
MessageListPage,
MailListPage,
ReadComposePanePage,
- LanguageTimePage,
AppPreferencesPage,
SettingOptionsPage,
AliasManagementPage,
- EditAccountDetailsPage,
- // Calendar
+ ImapCalDavSettingsPage,
+ KeyboardShortcutsPage,
CalendarPage,
CalendarSettingsPage,
- EventDetailsPage
+ CalendarRenderingSettingsPage,
+ CalendarNotificationSettingsPage,
+ CalendarPreferenceSettingsPage,
+ CalendarAccountSettingsPage,
+ EventDetailsPage,
+ CalendarEventComposePage,
+ SignatureAndEncryptionPage,
+ EmailTemplatesPage,
+ CreateEmailTemplatePage,
+ StoragePage,
+ WinoAccountManagementPage,
+ WelcomePageV2,
+ WelcomeHostPage,
+ ProviderSelectionPage,
+ AccountSetupProgressPage,
+ SpecialImapCredentialsPage
}
diff --git a/Wino.Core.Domain/Enums/WinoServerConnectionStatus.cs b/Wino.Core.Domain/Enums/WinoServerConnectionStatus.cs
deleted file mode 100644
index 9897b5bd..00000000
--- a/Wino.Core.Domain/Enums/WinoServerConnectionStatus.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace Wino.Core.Domain.Enums;
-
-public enum WinoServerConnectionStatus
-{
- None,
- Connecting,
- Connected,
- Disconnected,
- Failed
-}
diff --git a/Wino.Core.Domain/Exceptions/BackgroundTaskRegistrationFailedException.cs b/Wino.Core.Domain/Exceptions/BackgroundTaskRegistrationFailedException.cs
deleted file mode 100644
index 1a8ac966..00000000
--- a/Wino.Core.Domain/Exceptions/BackgroundTaskRegistrationFailedException.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using System;
-
-namespace Wino.Core.Domain.Exceptions;
-
-///
-/// An exception thrown when the background task registration is failed.
-///
-public class BackgroundTaskRegistrationFailedException : Exception { }
diff --git a/Wino.Core.Domain/Exceptions/CalendarEventComposeValidationException.cs b/Wino.Core.Domain/Exceptions/CalendarEventComposeValidationException.cs
new file mode 100644
index 00000000..834606cc
--- /dev/null
+++ b/Wino.Core.Domain/Exceptions/CalendarEventComposeValidationException.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace Wino.Core.Domain.Exceptions;
+
+public sealed class CalendarEventComposeValidationException : Exception
+{
+ public CalendarEventComposeValidationException(string message) : base(message)
+ {
+ }
+}
diff --git a/Wino.Core.Domain/Exceptions/ImapClientPoolException.cs b/Wino.Core.Domain/Exceptions/ImapClientPoolException.cs
index f109d383..4ff337b5 100644
--- a/Wino.Core.Domain/Exceptions/ImapClientPoolException.cs
+++ b/Wino.Core.Domain/Exceptions/ImapClientPoolException.cs
@@ -9,22 +9,18 @@ public class ImapClientPoolException : Exception
{
}
- public ImapClientPoolException(string message, CustomServerInformation customServerInformation, string protocolLog) : base(message)
+ public ImapClientPoolException(string message, CustomServerInformation customServerInformation) : base(message)
{
CustomServerInformation = customServerInformation;
- ProtocolLog = protocolLog;
}
- public ImapClientPoolException(string message, string protocolLog) : base(message)
+ public ImapClientPoolException(string message) : base(message)
{
- ProtocolLog = protocolLog;
}
- public ImapClientPoolException(Exception innerException, string protocolLog) : base(innerException.Message, innerException)
+ public ImapClientPoolException(Exception innerException) : base(innerException.Message, innerException)
{
- ProtocolLog = protocolLog;
}
public CustomServerInformation CustomServerInformation { get; }
- public string ProtocolLog { get; }
}
diff --git a/Wino.Core.Domain/Exceptions/ImapConnectionFailedPackage.cs b/Wino.Core.Domain/Exceptions/ImapConnectionFailedPackage.cs
deleted file mode 100644
index e431d969..00000000
--- a/Wino.Core.Domain/Exceptions/ImapConnectionFailedPackage.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using Wino.Core.Domain.Models.AutoDiscovery;
-
-namespace Wino.Core.Domain.Exceptions;
-
-public class ImapConnectionFailedPackage
-{
- public ImapConnectionFailedPackage(string errorMessage, string protocolLog, AutoDiscoverySettings settings)
- {
- ErrorMessage = errorMessage;
- ProtocolLog = protocolLog;
- Settings = settings;
- }
-
- public AutoDiscoverySettings Settings { get; }
- public string ErrorMessage { get; set; }
- public string ProtocolLog { get; }
-}
diff --git a/Wino.Core.Domain/Exceptions/WinoServerException.cs b/Wino.Core.Domain/Exceptions/WinoServerException.cs
deleted file mode 100644
index afa7f132..00000000
--- a/Wino.Core.Domain/Exceptions/WinoServerException.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using System;
-
-namespace Wino.Core.Domain.Exceptions;
-
-///
-/// All server crash types. Wino Server ideally should not throw anything else than this Exception type.
-///
-public class WinoServerException : Exception
-{
- public WinoServerException(string message) : base(message) { }
-}
diff --git a/Wino.Core.Domain/Extensions/CalendarRemoteEventIdExtensions.cs b/Wino.Core.Domain/Extensions/CalendarRemoteEventIdExtensions.cs
new file mode 100644
index 00000000..a21f9ccc
--- /dev/null
+++ b/Wino.Core.Domain/Extensions/CalendarRemoteEventIdExtensions.cs
@@ -0,0 +1,67 @@
+using System;
+
+namespace Wino.Core.Domain.Extensions;
+
+public static class CalendarRemoteEventIdExtensions
+{
+ private const string ClientTrackingSeparator = "::";
+ private const string CalDavClientTrackingPrefix = "caldav-";
+ private const string LocalClientTrackingPrefix = "local-";
+
+ public static string GetProviderRemoteEventId(this string remoteEventId)
+ {
+ if (string.IsNullOrWhiteSpace(remoteEventId))
+ return string.Empty;
+
+ var separatorIndex = remoteEventId.IndexOf(ClientTrackingSeparator, StringComparison.Ordinal);
+ return separatorIndex >= 0 ? remoteEventId[..separatorIndex] : remoteEventId;
+ }
+
+ public static Guid? GetClientTrackingId(this string remoteEventId)
+ {
+ if (string.IsNullOrWhiteSpace(remoteEventId))
+ return null;
+
+ if (remoteEventId.Contains(ClientTrackingSeparator, StringComparison.Ordinal))
+ {
+ var trackedPart = remoteEventId[(remoteEventId.LastIndexOf(ClientTrackingSeparator, StringComparison.Ordinal) + ClientTrackingSeparator.Length)..];
+ if (TryParseGuid(trackedPart, out var trackedId))
+ return trackedId;
+ }
+
+ if (TryParseGuid(remoteEventId, out var directId))
+ return directId;
+
+ if (remoteEventId.StartsWith(CalDavClientTrackingPrefix, StringComparison.OrdinalIgnoreCase) &&
+ TryParseGuid(remoteEventId[CalDavClientTrackingPrefix.Length..], out var calDavId))
+ {
+ return calDavId;
+ }
+
+ if (remoteEventId.StartsWith(LocalClientTrackingPrefix, StringComparison.OrdinalIgnoreCase) &&
+ TryParseGuid(remoteEventId[LocalClientTrackingPrefix.Length..], out var localId))
+ {
+ return localId;
+ }
+
+ return null;
+ }
+
+ public static string WithClientTrackingId(this string providerRemoteEventId, Guid? clientTrackingId)
+ {
+ if (string.IsNullOrWhiteSpace(providerRemoteEventId) || !clientTrackingId.HasValue)
+ return providerRemoteEventId ?? string.Empty;
+
+ return $"{providerRemoteEventId}{ClientTrackingSeparator}{clientTrackingId.Value:N}";
+ }
+
+ private static bool TryParseGuid(string value, out Guid parsedGuid)
+ {
+ parsedGuid = Guid.Empty;
+
+ if (string.IsNullOrWhiteSpace(value))
+ return false;
+
+ return Guid.TryParseExact(value, "N", out parsedGuid) || Guid.TryParse(value, out parsedGuid);
+ }
+}
diff --git a/Wino.Core.Domain/Extensions/DateTimeExtensions.cs b/Wino.Core.Domain/Extensions/DateTimeExtensions.cs
index f026c80f..0268a93a 100644
--- a/Wino.Core.Domain/Extensions/DateTimeExtensions.cs
+++ b/Wino.Core.Domain/Extensions/DateTimeExtensions.cs
@@ -1,4 +1,5 @@
-using System;
+using System;
+using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Core.Domain.Extensions;
@@ -29,4 +30,60 @@ public static class DateTimeExtensions
// Start loading from this date instead of visible date.
return date.AddDays(-diff).Date;
}
+
+ ///
+ /// Converts a datetime from source timezone into local timezone.
+ /// If timezone lookup fails, returns original value.
+ ///
+ public static DateTime ToLocalTimeFromTimeZone(this DateTime dateTime, string sourceTimeZoneId)
+ {
+ if (string.IsNullOrWhiteSpace(sourceTimeZoneId))
+ return dateTime;
+
+ try
+ {
+ var sourceTimeZone = TimeZoneInfo.FindSystemTimeZoneById(sourceTimeZoneId);
+ var localTimeZone = TimeZoneInfo.Local;
+ var unspecifiedDateTime = DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified);
+
+ return TimeZoneInfo.ConvertTime(unspecifiedDateTime, sourceTimeZone, localTimeZone);
+ }
+ catch
+ {
+ return dateTime;
+ }
+ }
+
+ ///
+ /// Converts local datetime into target timezone.
+ /// If timezone lookup fails, returns original value.
+ ///
+ public static DateTime ToTimeZoneFromLocal(this DateTime localDateTime, string targetTimeZoneId)
+ {
+ if (string.IsNullOrWhiteSpace(targetTimeZoneId))
+ return localDateTime;
+
+ try
+ {
+ var sourceTimeZone = TimeZoneInfo.Local;
+ var targetTimeZone = TimeZoneInfo.FindSystemTimeZoneById(targetTimeZoneId);
+ var unspecifiedDateTime = DateTime.SpecifyKind(localDateTime, DateTimeKind.Unspecified);
+
+ return TimeZoneInfo.ConvertTime(unspecifiedDateTime, sourceTimeZone, targetTimeZone);
+ }
+ catch
+ {
+ return localDateTime;
+ }
+ }
+
+ public static DateTime GetLocalStartDate(this CalendarItem calendarItem)
+ => calendarItem.IsAllDayEvent
+ ? calendarItem.StartDate
+ : calendarItem.StartDate.ToLocalTimeFromTimeZone(calendarItem.StartTimeZone);
+
+ public static DateTime GetLocalEndDate(this CalendarItem calendarItem)
+ => calendarItem.IsAllDayEvent
+ ? calendarItem.EndDate
+ : calendarItem.EndDate.ToLocalTimeFromTimeZone(calendarItem.EndTimeZone);
}
diff --git a/Wino.Core.Domain/Extensions/MailHeaderExtensions.cs b/Wino.Core.Domain/Extensions/MailHeaderExtensions.cs
new file mode 100644
index 00000000..aa41729b
--- /dev/null
+++ b/Wino.Core.Domain/Extensions/MailHeaderExtensions.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Linq;
+
+namespace Wino.Core.Domain.Extensions;
+
+public static class MailHeaderExtensions
+{
+ ///
+ /// Strips angle brackets from a Message-ID or In-Reply-To value.
+ /// RFC 5322 Message-IDs are formatted as <id@domain>, but MimeKit
+ /// properties store them without brackets. This normalizes raw header
+ /// values to match MimeKit's convention.
+ ///
+ public static string StripAngleBrackets(string value)
+ {
+ if (string.IsNullOrEmpty(value)) return value;
+
+ value = value.Trim();
+
+ if (value.StartsWith("<") && value.EndsWith(">"))
+ return value.Substring(1, value.Length - 2);
+
+ return value;
+ }
+
+ ///
+ /// Normalizes a raw RFC References header value into semicolon-separated Message-IDs
+ /// without angle brackets. Raw References headers contain space-separated bracketed IDs
+ /// like "<id1@domain> <id2@domain>". This converts them to "id1@domain;id2@domain".
+ ///
+ public static string NormalizeReferences(string rawReferences)
+ {
+ if (string.IsNullOrEmpty(rawReferences)) return rawReferences;
+
+ var ids = rawReferences
+ .Split(new[] { ' ', '\t', '\r', '\n', ';', ',' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(StripAngleBrackets)
+ .Where(id => !string.IsNullOrEmpty(id));
+
+ return string.Join(";", ids);
+ }
+}
diff --git a/Wino.Core.Domain/Interfaces/IAccountCalendar.cs b/Wino.Core.Domain/Interfaces/IAccountCalendar.cs
index 85a340c3..7e070f40 100644
--- a/Wino.Core.Domain/Interfaces/IAccountCalendar.cs
+++ b/Wino.Core.Domain/Interfaces/IAccountCalendar.cs
@@ -1,4 +1,6 @@
using System;
+using Wino.Core.Domain.Entities.Shared;
+using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Interfaces;
@@ -8,8 +10,11 @@ public interface IAccountCalendar
string TextColorHex { get; set; }
string BackgroundColorHex { get; set; }
bool IsPrimary { get; set; }
+ bool IsSynchronizationEnabled { get; set; }
Guid AccountId { get; set; }
string RemoteCalendarId { get; set; }
bool IsExtended { get; set; }
+ CalendarItemShowAs DefaultShowAs { get; set; }
Guid Id { get; set; }
+ MailAccount MailAccount { get; set; }
}
diff --git a/Wino.Core.Domain/Interfaces/IAccountMenuItem.cs b/Wino.Core.Domain/Interfaces/IAccountMenuItem.cs
index b3a917bd..b978978f 100644
--- a/Wino.Core.Domain/Interfaces/IAccountMenuItem.cs
+++ b/Wino.Core.Domain/Interfaces/IAccountMenuItem.cs
@@ -7,7 +7,27 @@ namespace Wino.Core.Domain.Interfaces;
public interface IAccountMenuItem : IMenuItem
{
bool IsEnabled { get; set; }
- double SynchronizationProgress { get; set; }
+
+ ///
+ /// Calculated synchronization progress percentage (0-100). -1 for indeterminate.
+ ///
+ double SynchronizationProgress { get; }
+
+ ///
+ /// Total items to sync. 0 for indeterminate progress.
+ ///
+ int TotalItemsToSync { get; set; }
+
+ ///
+ /// Remaining items to sync.
+ ///
+ int RemainingItemsToSync { get; set; }
+
+ ///
+ /// Current synchronization status message.
+ ///
+ string SynchronizationStatus { get; set; }
+
int UnreadItemCount { get; set; }
IEnumerable HoldingAccounts { get; }
void UpdateAccount(MailAccount account);
diff --git a/Wino.Core.Domain/Interfaces/IAccountService.cs b/Wino.Core.Domain/Interfaces/IAccountService.cs
index 6c590df7..cc31f0d9 100644
--- a/Wino.Core.Domain/Interfaces/IAccountService.cs
+++ b/Wino.Core.Domain/Interfaces/IAccountService.cs
@@ -171,4 +171,19 @@ public interface IAccountService
/// Whether the notifications should be created after sync or not.
Task IsNotificationsEnabled(Guid accountId);
Task UpdateAccountCustomServerInformationAsync(CustomServerInformation customServerInformation);
+
+ ///
+ /// Updates the last folder structure sync date for the given account.
+ /// Used for optimization to skip folder sync if it was done recently.
+ ///
+ /// Account id.
+ Task UpdateLastFolderStructureSyncDateAsync(Guid accountId);
+
+ ///
+ /// Checks if folder structure should be synced based on the configured interval.
+ /// Returns true if LastFolderStructureSyncDate is null or older than the interval.
+ ///
+ /// Account id.
+ /// Minimum interval between folder syncs.
+ Task ShouldSyncFolderStructureAsync(Guid accountId, TimeSpan syncInterval);
}
diff --git a/Wino.Core.Domain/Interfaces/IAiActionOptionsService.cs b/Wino.Core.Domain/Interfaces/IAiActionOptionsService.cs
new file mode 100644
index 00000000..5efaf1e7
--- /dev/null
+++ b/Wino.Core.Domain/Interfaces/IAiActionOptionsService.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+using Wino.Core.Domain.Models.Ai;
+
+namespace Wino.Core.Domain.Interfaces;
+
+public interface IAiActionOptionsService
+{
+ IReadOnlyList GetTranslateLanguageOptions();
+ IReadOnlyList GetRewriteModeOptions();
+}
diff --git a/Wino.Core.Domain/Interfaces/IAutoDiscoveryService.cs b/Wino.Core.Domain/Interfaces/IAutoDiscoveryService.cs
index 0ba6642a..3f49c0bf 100644
--- a/Wino.Core.Domain/Interfaces/IAutoDiscoveryService.cs
+++ b/Wino.Core.Domain/Interfaces/IAutoDiscoveryService.cs
@@ -1,17 +1,22 @@
-using System.Threading.Tasks;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
using Wino.Core.Domain.Models.AutoDiscovery;
namespace Wino.Core.Domain.Interfaces;
///
-/// Searches for Auto Discovery settings for custom mail accounts.
+/// Searches for auto-discovery settings for custom mail accounts.
///
public interface IAutoDiscoveryService
{
///
/// Tries to return the best mail server settings using different techniques.
///
- /// Address to search settings for.
- /// CustomServerInformation with only settings applied.
Task GetAutoDiscoverySettings(AutoDiscoveryMinimalSettings autoDiscoveryMinimalSettings);
+
+ ///
+ /// Tries to resolve a CalDAV endpoint for the mailbox address.
+ ///
+ Task DiscoverCalDavServiceUriAsync(string mailAddress, CancellationToken cancellationToken = default);
}
diff --git a/Wino.Core.Domain/Interfaces/IBackgroundTaskService.cs b/Wino.Core.Domain/Interfaces/IBackgroundTaskService.cs
deleted file mode 100644
index 651ee659..00000000
--- a/Wino.Core.Domain/Interfaces/IBackgroundTaskService.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using System.Threading.Tasks;
-
-namespace Wino.Core.Domain.Interfaces;
-
-public interface IBackgroundTaskService
-{
- ///
- /// Unregisters all background tasks once.
- /// This is used to clean up the background tasks when the app is updated.
- ///
- void UnregisterAllBackgroundTask();
-
- ///
- /// Registers required background tasks.
- ///
- Task RegisterBackgroundTasksAsync();
-}
diff --git a/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs b/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs
index 00f2f406..f423eadf 100644
--- a/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs
+++ b/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs
@@ -1,4 +1,6 @@
-using System.Threading.Tasks;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Accounts;
@@ -23,6 +25,23 @@ public interface IBaseSynchronizer
/// Request to queue.
void QueueRequest(IRequestBase request);
+ ///
+ /// Returns whether there is an in-progress (queued or currently executing) operation for the given mail unique id.
+ ///
+ /// Mail unique id to check.
+ bool HasPendingOperation(Guid mailUniqueId);
+
+ ///
+ /// Returns mail unique ids that currently have queued or executing operations.
+ ///
+ IReadOnlyCollection GetPendingOperationUniqueIds();
+
+ ///
+ /// Returns whether there is an in-progress (queued or currently executing) operation for the given calendar item id.
+ ///
+ /// Calendar item id to check.
+ bool HasPendingCalendarOperation(Guid calendarItemId);
+
///
/// Synchronizes profile information with the server.
/// Sender name and Profile picture are updated.
diff --git a/Wino.Core.Domain/Interfaces/ICalDavClient.cs b/Wino.Core.Domain/Interfaces/ICalDavClient.cs
new file mode 100644
index 00000000..824ff22f
--- /dev/null
+++ b/Wino.Core.Domain/Interfaces/ICalDavClient.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Wino.Core.Domain.Models.Calendar;
+
+namespace Wino.Core.Domain.Interfaces;
+
+public interface ICalDavClient
+{
+ Task> DiscoverCalendarsAsync(
+ CalDavConnectionSettings connectionSettings,
+ CancellationToken cancellationToken = default);
+
+ Task> GetCalendarEventsAsync(
+ CalDavConnectionSettings connectionSettings,
+ CalDavCalendar calendar,
+ DateTimeOffset startUtc,
+ DateTimeOffset endUtc,
+ CancellationToken cancellationToken = default);
+
+ Task UpsertCalendarEventAsync(
+ CalDavConnectionSettings connectionSettings,
+ CalDavCalendar calendar,
+ string remoteEventId,
+ string icsContent,
+ CancellationToken cancellationToken = default);
+
+ Task DeleteCalendarEventAsync(
+ CalDavConnectionSettings connectionSettings,
+ CalDavCalendar calendar,
+ string remoteEventId,
+ CancellationToken cancellationToken = default);
+}
+
diff --git a/Wino.Core.Domain/Interfaces/ICalendarDialogService.cs b/Wino.Core.Domain/Interfaces/ICalendarDialogService.cs
deleted file mode 100644
index 7b48f868..00000000
--- a/Wino.Core.Domain/Interfaces/ICalendarDialogService.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-namespace Wino.Core.Domain.Interfaces;
-
-public interface ICalendarDialogService : IDialogServiceBase
-{
-}
diff --git a/Wino.Core.Domain/Interfaces/ICalendarIcsFileService.cs b/Wino.Core.Domain/Interfaces/ICalendarIcsFileService.cs
new file mode 100644
index 00000000..919ea737
--- /dev/null
+++ b/Wino.Core.Domain/Interfaces/ICalendarIcsFileService.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Wino.Core.Domain.Interfaces;
+
+///
+/// Persists CalDAV ICS payloads on disk for IMAP accounts.
+///
+public interface ICalendarIcsFileService
+{
+ Task SaveCalendarItemIcsAsync(Guid accountId, Guid calendarId, Guid calendarItemId, string remoteEventId, string remoteResourceHref, string eTag, string icsContent);
+ Task GetCalendarItemIcsETagAsync(Guid accountId, Guid calendarId, Guid calendarItemId);
+ Task DeleteCalendarItemIcsAsync(Guid accountId, Guid calendarItemId);
+ Task DeleteCalendarIcsForCalendarAsync(Guid accountId, Guid calendarId);
+}
diff --git a/Wino.Core.Domain/Interfaces/ICalendarItem.cs b/Wino.Core.Domain/Interfaces/ICalendarItem.cs
index e83827a6..cfa0cb31 100644
--- a/Wino.Core.Domain/Interfaces/ICalendarItem.cs
+++ b/Wino.Core.Domain/Interfaces/ICalendarItem.cs
@@ -1,5 +1,6 @@
using System;
using Itenso.TimePeriod;
+using Wino.Core.Domain.Models.Calendar;
namespace Wino.Core.Domain.Interfaces;
@@ -19,4 +20,13 @@ public interface ICalendarItem
bool IsRecurringChild { get; }
bool IsRecurringParent { get; }
bool IsRecurringEvent { get; }
+
+ ///
+ /// Gets the display title for this calendar item when rendered in a specific day.
+ /// For multi-day events, includes start/end time indicators.
+ ///
+ /// The period of the day where this item is being rendered.
+ /// Calendar settings for time formatting.
+ /// The formatted title string.
+ string GetDisplayTitle(ITimePeriod displayingPeriod, CalendarSettings calendarSettings);
}
diff --git a/Wino.Core.Domain/Interfaces/ICalendarItemViewModel.cs b/Wino.Core.Domain/Interfaces/ICalendarItemViewModel.cs
index 3eaf1ff7..55a91f68 100644
--- a/Wino.Core.Domain/Interfaces/ICalendarItemViewModel.cs
+++ b/Wino.Core.Domain/Interfaces/ICalendarItemViewModel.cs
@@ -1,4 +1,8 @@
-namespace Wino.Core.Domain.Interfaces;
+using Itenso.TimePeriod;
+using Wino.Core.Domain.Entities.Calendar;
+using Wino.Core.Domain.Models.Calendar;
+
+namespace Wino.Core.Domain.Interfaces;
///
/// Temporarily to enforce CalendarItemViewModel. Used in CalendarEventCollection.
@@ -6,4 +10,23 @@
public interface ICalendarItemViewModel
{
bool IsSelected { get; set; }
+
+ bool IsBusy { get; set; }
+
+ ///
+ /// The period of the day where this item is currently being displayed.
+ ///
+ ITimePeriod DisplayingPeriod { get; set; }
+
+ ///
+ /// Calendar settings for time formatting.
+ ///
+ CalendarSettings CalendarSettings { get; set; }
+
+ ///
+ /// Updates the view model's underlying CalendarItem from new data.
+ /// This allows in-place updates without removing and re-adding items.
+ ///
+ /// The updated calendar item data.
+ void UpdateFrom(CalendarItem calendarItem);
}
diff --git a/Wino.Core.Domain/Interfaces/ICalendarService.cs b/Wino.Core.Domain/Interfaces/ICalendarService.cs
index f0e18b4a..fae3d08f 100644
--- a/Wino.Core.Domain/Interfaces/ICalendarService.cs
+++ b/Wino.Core.Domain/Interfaces/ICalendarService.cs
@@ -1,6 +1,8 @@
-using System;
+using System;
using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
+using Itenso.TimePeriod;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Models.Calendar;
@@ -11,12 +13,22 @@ public interface ICalendarService
Task> GetAccountCalendarsAsync(Guid accountId);
Task GetAccountCalendarAsync(Guid accountCalendarId);
Task DeleteCalendarItemAsync(Guid calendarItemId);
+ Task DeleteCalendarItemAsync(string calendarRemoteEventId, Guid calendarId);
Task DeleteAccountCalendarAsync(AccountCalendar accountCalendar);
Task InsertAccountCalendarAsync(AccountCalendar accountCalendar);
Task UpdateAccountCalendarAsync(AccountCalendar accountCalendar);
+ Task SetPrimaryCalendarAsync(Guid accountId, Guid accountCalendarId);
Task CreateNewCalendarItemAsync(CalendarItem calendarItem, List attendees);
- Task> GetCalendarEventsAsync(IAccountCalendar calendar, DayRangeRenderModel dayRangeRenderModel);
+
+ ///
+ /// Retrieves calendar events for a given calendar within the specified time period.
+ ///
+ /// The calendar to retrieve events from.
+ /// The time period to query events for.
+ /// List of calendar items that fall within the requested period.
+ Task> GetCalendarEventsAsync(IAccountCalendar calendar, ITimePeriod period);
+
Task GetCalendarItemAsync(Guid accountCalendarId, string remoteEventId);
Task UpdateCalendarDeltaSynchronizationToken(Guid calendarId, string deltaToken);
@@ -28,4 +40,43 @@ public interface ICalendarService
Task GetCalendarItemAsync(Guid id);
Task> GetAttendeesAsync(Guid calendarEventTrackingId);
Task> ManageEventAttendeesAsync(Guid calendarItemId, List allAttendees);
+ Task UpdateCalendarItemAsync(CalendarItem calendarItem, List attendees);
+ Task> SearchCalendarItemsAsync(string searchQuery, int limit, CancellationToken cancellationToken = default);
+ Task> GetRemindersAsync(Guid calendarItemId);
+ Task SaveRemindersAsync(Guid calendarItemId, List reminders);
+ Task SnoozeCalendarItemAsync(Guid calendarItemId, DateTime snoozedUntilLocal);
+
+ ///
+ /// Checks due reminder windows and returns reminder notifications that should trigger now.
+ ///
+ Task> CheckAndNotifyAsync(DateTime lastCheckLocal, DateTime nowLocal, ISet sentReminderKeys, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets predefined reminder options in minutes (1 Hour, 30 Min, 15 Min, 5 Min, 1 Min).
+ ///
+ int[] GetPredefinedReminderMinutes();
+
+ #region Attachments
+
+ ///
+ /// Gets all attachments for a calendar event.
+ ///
+ Task> GetAttachmentsAsync(Guid calendarItemId);
+
+ ///
+ /// Inserts or updates calendar attachments.
+ ///
+ Task InsertOrReplaceAttachmentsAsync(List attachments);
+
+ ///
+ /// Marks an attachment as downloaded and updates its local file path.
+ ///
+ Task MarkAttachmentDownloadedAsync(Guid attachmentId, string localFilePath);
+
+ ///
+ /// Deletes all attachments for a calendar item.
+ ///
+ Task DeleteAttachmentsAsync(Guid calendarItemId);
+
+ #endregion
}
diff --git a/Wino.Core.Domain/Interfaces/IContactPictureFileService.cs b/Wino.Core.Domain/Interfaces/IContactPictureFileService.cs
new file mode 100644
index 00000000..ea654a17
--- /dev/null
+++ b/Wino.Core.Domain/Interfaces/IContactPictureFileService.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Wino.Core.Domain.Interfaces;
+
+///
+/// Manages contact picture files stored on disk instead of as base64 in SQLite,
+/// eliminating DB bloat and enabling native WIC hardware-accelerated image loading.
+///
+public interface IContactPictureFileService
+{
+ ///
+ /// Returns the full file path for the given file ID, or null if the file does not exist on disk.
+ ///
+ string GetContactPicturePath(Guid fileId);
+
+ ///
+ /// Saves raw image bytes to disk and returns the new file ID.
+ ///
+ Task SaveContactPictureAsync(byte[] imageData);
+
+ ///
+ /// Deletes the picture file for the given file ID if it exists.
+ ///
+ Task DeleteContactPictureAsync(Guid fileId);
+}
diff --git a/Wino.Core.Domain/Interfaces/IContactService.cs b/Wino.Core.Domain/Interfaces/IContactService.cs
index e721ec2a..6462026d 100644
--- a/Wino.Core.Domain/Interfaces/IContactService.cs
+++ b/Wino.Core.Domain/Interfaces/IContactService.cs
@@ -1,7 +1,10 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Threading.Tasks;
using MimeKit;
using Wino.Core.Domain.Entities.Shared;
+using Wino.Core.Domain.Models.Contacts;
namespace Wino.Core.Domain.Interfaces;
@@ -9,6 +12,30 @@ public interface IContactService
{
Task> GetAddressInformationAsync(string queryText);
Task GetAddressInformationByAddressAsync(string address);
+ Task> GetContactsByAddressesAsync(IEnumerable addresses);
Task SaveAddressInformationAsync(MimeMessage message);
+ Task SaveAddressInformationAsync(IEnumerable contacts);
Task CreateNewContactAsync(string address, string displayName);
+
+ // Paged contact queries for ContactsPage
+ Task> GetAllContactsAsync();
+ Task> SearchContactsAsync(string searchQuery);
+ Task GetContactsPageAsync(int offset, int pageSize, string searchQuery = null, bool excludeRootContacts = false);
+ Task UpdateContactAsync(AccountContact contact);
+ Task DeleteContactAsync(string address);
+ Task DeleteContactsAsync(IEnumerable addresses);
+
+ // Group / distribution list support
+ Task> GetGroupsAsync();
+ Task CreateGroupAsync(string name, string description = null);
+ Task DeleteGroupAsync(Guid groupId);
+ Task> GetGroupMembersAsync(Guid groupId);
+ Task AddGroupMemberAsync(Guid groupId, string memberAddress);
+ Task RemoveGroupMemberAsync(Guid groupId, string memberAddress);
+
+ ///
+ /// Expands a contact group to the individual entries of its members.
+ /// Returns an empty list if the group does not exist or has no members.
+ ///
+ Task> ExpandGroupAsync(Guid groupId);
}
diff --git a/Wino.Core.Domain/Interfaces/IContextMenuItemService.cs b/Wino.Core.Domain/Interfaces/IContextMenuItemService.cs
index c3b1c54d..eee75ad5 100644
--- a/Wino.Core.Domain/Interfaces/IContextMenuItemService.cs
+++ b/Wino.Core.Domain/Interfaces/IContextMenuItemService.cs
@@ -1,6 +1,6 @@
using System.Collections.Generic;
+using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Models.Folders;
-using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Menus;
namespace Wino.Core.Domain.Interfaces;
@@ -8,6 +8,6 @@ namespace Wino.Core.Domain.Interfaces;
public interface IContextMenuItemService
{
IEnumerable GetFolderContextMenuActions(IBaseFolderMenuItem folderInformation);
- IEnumerable GetMailItemContextMenuActions(IEnumerable selectedMailItems);
- IEnumerable GetMailItemRenderMenuActions(IMailItem mailItem, bool isDarkEditor);
+ IEnumerable GetMailItemContextMenuActions(IEnumerable selectedMailItems);
+ IEnumerable GetMailItemRenderMenuActions(MailCopy mailItem, bool isDarkEditor);
}
diff --git a/Wino.Core.Domain/Interfaces/IContextMenuProvider.cs b/Wino.Core.Domain/Interfaces/IContextMenuProvider.cs
index 7e7ef089..d5b676b4 100644
--- a/Wino.Core.Domain/Interfaces/IContextMenuProvider.cs
+++ b/Wino.Core.Domain/Interfaces/IContextMenuProvider.cs
@@ -1,6 +1,6 @@
using System.Collections.Generic;
+using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Models.Folders;
-using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Menus;
namespace Wino.Core.Domain.Interfaces;
@@ -18,12 +18,12 @@ public interface IContextMenuProvider
///
/// Current folder that asks for the menu items.
/// Selected menu items in the given folder.
- IEnumerable GetMailItemContextMenuActions(IMailItemFolder folderInformation, IEnumerable selectedMailItems);
+ IEnumerable GetMailItemContextMenuActions(IMailItemFolder folderInformation, IEnumerable selectedMailItems);
///
/// Calculates and returns available mail operations for mail rendering CommandBar.
///
/// Rendered mail item.
/// Folder that mail item belongs to.
- IEnumerable GetMailItemRenderMenuActions(IMailItem mailItem, IMailItemFolder activeFolder, bool isDarkEditor);
+ IEnumerable GetMailItemRenderMenuActions(MailCopy mailItem, IMailItemFolder activeFolder, bool isDarkEditor);
}
diff --git a/Wino.Core.Domain/Interfaces/IDialogServiceBase.cs b/Wino.Core.Domain/Interfaces/IDialogServiceBase.cs
index 8cc97e6d..c915d5b9 100644
--- a/Wino.Core.Domain/Interfaces/IDialogServiceBase.cs
+++ b/Wino.Core.Domain/Interfaces/IDialogServiceBase.cs
@@ -4,6 +4,7 @@ using System.Threading.Tasks;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.Common;
+using Wino.Core.Domain.Models.Printing;
namespace Wino.Core.Domain.Interfaces;
@@ -27,5 +28,7 @@ public interface IDialogServiceBase
Task ShowAccountProviderSelectionDialogAsync(List availableProviders);
IAccountCreationDialog GetAccountCreationDialog(AccountCreationDialogResult accountCreationDialogResult);
Task> PickFilesAsync(params object[] typeFilters);
+ Task> PickFilesMetadataAsync(params object[] typeFilters);
Task PickFilePathAsync(string saveFileName);
+ Task ShowPrintDialogAsync(WebView2PrintSettingsModel initialSettings = null);
}
diff --git a/Wino.Core.Domain/Interfaces/IEmailTemplateService.cs b/Wino.Core.Domain/Interfaces/IEmailTemplateService.cs
new file mode 100644
index 00000000..52afff79
--- /dev/null
+++ b/Wino.Core.Domain/Interfaces/IEmailTemplateService.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Wino.Core.Domain.Entities.Mail;
+
+namespace Wino.Core.Domain.Interfaces;
+
+public interface IEmailTemplateService
+{
+ Task> GetEmailTemplatesAsync();
+ Task GetEmailTemplateAsync(Guid templateId);
+ Task CreateEmailTemplateAsync(EmailTemplate template);
+ Task UpdateEmailTemplateAsync(EmailTemplate template);
+ Task DeleteEmailTemplateAsync(EmailTemplate template);
+}
diff --git a/Wino.Core.Domain/Interfaces/IFolderMenuItem.cs b/Wino.Core.Domain/Interfaces/IFolderMenuItem.cs
index 7deff38d..26413208 100644
--- a/Wino.Core.Domain/Interfaces/IFolderMenuItem.cs
+++ b/Wino.Core.Domain/Interfaces/IFolderMenuItem.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.Collections.ObjectModel;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Folders;
@@ -20,7 +21,7 @@ public interface IBaseFolderMenuItem : IMenuItem
int UnreadItemCount { get; set; }
SpecialFolderType SpecialFolderType { get; }
IEnumerable HandlingFolders { get; }
- IEnumerable SubMenuItems { get; }
+ ObservableCollection SubMenuItems { get; }
bool IsMoveTarget { get; }
bool IsSticky { get; }
bool IsSystemFolder { get; }
diff --git a/Wino.Core.Domain/Interfaces/IFolderService.cs b/Wino.Core.Domain/Interfaces/IFolderService.cs
index 33cf1f35..754a67e2 100644
--- a/Wino.Core.Domain/Interfaces/IFolderService.cs
+++ b/Wino.Core.Domain/Interfaces/IFolderService.cs
@@ -79,6 +79,13 @@ public interface IFolderService
/// Folder to update.
Task UpdateFolderAsync(MailItemFolder folder);
+ ///
+ /// Updates only IMAP HighestModeSeq for the given folder.
+ ///
+ /// Folder id to update.
+ /// Latest known mod-seq value.
+ Task UpdateFolderHighestModeSeqAsync(Guid folderId, long highestModeSeq);
+
///
/// Returns the active folder menu items for the given account for UI.
///
diff --git a/Wino.Core.Domain/Interfaces/IGmailThreadingStrategy.cs b/Wino.Core.Domain/Interfaces/IGmailThreadingStrategy.cs
deleted file mode 100644
index 17ec3b74..00000000
--- a/Wino.Core.Domain/Interfaces/IGmailThreadingStrategy.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace Wino.Core.Domain.Interfaces;
-
-public interface IGmailThreadingStrategy : IThreadingStrategy { }
diff --git a/Wino.Core.Domain/Interfaces/IImapAccountCreationDialog.cs b/Wino.Core.Domain/Interfaces/IImapAccountCreationDialog.cs
deleted file mode 100644
index 21d9fb47..00000000
--- a/Wino.Core.Domain/Interfaces/IImapAccountCreationDialog.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using System.Threading.Tasks;
-using Wino.Core.Domain.Entities.Shared;
-
-namespace Wino.Core.Domain.Interfaces;
-
-public interface IImapAccountCreationDialog : IAccountCreationDialog
-{
- ///
- /// Returns the custom server information from the dialog..
- ///
- /// Null if canceled.
- Task GetCustomServerInformationAsync();
-
- ///
- /// Displays preparing folders page.
- ///
- void ShowPreparingFolders();
-
- ///
- /// Updates account properties for the welcome imap setup dialog and starts the setup.
- ///
- /// Account properties.
- void StartImapConnectionSetup(MailAccount account);
-}
diff --git a/Wino.Core.Domain/Interfaces/IImapSynchronizationStrategyProvider.cs b/Wino.Core.Domain/Interfaces/IImapSynchronizationStrategyProvider.cs
deleted file mode 100644
index 3fafc48e..00000000
--- a/Wino.Core.Domain/Interfaces/IImapSynchronizationStrategyProvider.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using MailKit.Net.Imap;
-
-namespace Wino.Core.Domain.Interfaces;
-
-///
-/// Provides a synchronization strategy for synchronizing IMAP folders based on the server capabilities.
-///
-public interface IImapSynchronizationStrategyProvider
-{
- IImapSynchronizerStrategy GetSynchronizationStrategy(IImapClient client);
-}
diff --git a/Wino.Core.Domain/Interfaces/IImapSynchronizerStrategy.cs b/Wino.Core.Domain/Interfaces/IImapSynchronizerStrategy.cs
deleted file mode 100644
index 8b7769b6..00000000
--- a/Wino.Core.Domain/Interfaces/IImapSynchronizerStrategy.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-using MailKit;
-using MailKit.Net.Imap;
-using Wino.Core.Domain.Entities.Mail;
-
-namespace Wino.Core.Domain.Interfaces;
-
-public interface IImapSynchronizerStrategy
-{
- ///
- /// Synchronizes given folder with the ImapClient client from the client pool.
- ///
- /// Client to perform sync with. I love Mira and Jasminka
- /// Folder to synchronize.
- /// Imap synchronizer that downloads messages.
- /// Cancellation token.
- /// List of new downloaded message ids that don't exist locally.
- Task> HandleSynchronizationAsync(IImapClient client,
- MailItemFolder folder,
- IImapSynchronizer synchronizer,
- CancellationToken cancellationToken = default);
-
- ///
- /// Downloads given set of messages from the folder.
- /// Folder is expected to be opened and synchronizer is connected.
- ///
- /// Synchronizer that performs the action.
- /// Remote folder to download messages from.
- /// Local folder to assign mails to.
- /// Set of message uniqueids.
- /// Cancellation token.
- Task DownloadMessagesAsync(IImapSynchronizer synchronizer,
- IMailFolder remoteFolder,
- MailItemFolder localFolder,
- UniqueIdSet uniqueIdSet,
- CancellationToken cancellationToken = default);
-}
-
diff --git a/Wino.Core.Domain/Interfaces/IImapThreadingStrategy.cs b/Wino.Core.Domain/Interfaces/IImapThreadingStrategy.cs
deleted file mode 100644
index b613cd70..00000000
--- a/Wino.Core.Domain/Interfaces/IImapThreadingStrategy.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace Wino.Core.Domain.Interfaces;
-
-public interface IImapThreadingStrategy : IThreadingStrategy { }
diff --git a/Wino.Core.Domain/Interfaces/IKeyboardShortcutService.cs b/Wino.Core.Domain/Interfaces/IKeyboardShortcutService.cs
new file mode 100644
index 00000000..eb242d84
--- /dev/null
+++ b/Wino.Core.Domain/Interfaces/IKeyboardShortcutService.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Wino.Core.Domain.Entities.Shared;
+using Wino.Core.Domain.Enums;
+
+namespace Wino.Core.Domain.Interfaces;
+
+///
+/// Service for managing keyboard shortcuts for mail operations.
+///
+public interface IKeyboardShortcutService
+{
+ ///
+ /// Gets all available keyboard shortcuts.
+ ///
+ /// Collection of keyboard shortcuts.
+ Task> GetKeyboardShortcutsAsync();
+
+ ///
+ /// Gets enabled keyboard shortcuts only.
+ ///
+ /// Collection of enabled keyboard shortcuts.
+ Task> GetEnabledKeyboardShortcutsAsync();
+
+ ///
+ /// Creates or updates a keyboard shortcut.
+ ///
+ /// The keyboard shortcut to save.
+ /// The saved keyboard shortcut.
+ Task SaveKeyboardShortcutAsync(KeyboardShortcut shortcut);
+
+ ///
+ /// Deletes a keyboard shortcut.
+ ///
+ /// The ID of the shortcut to delete.
+ Task DeleteKeyboardShortcutAsync(Guid shortcutId);
+
+ ///
+ /// Gets the keyboard shortcut for the given key combination in a specific mode.
+ ///
+ /// The application mode to search within.
+ /// The pressed key.
+ /// The modifier keys pressed.
+ /// The matching shortcut if found, otherwise null.
+ Task GetShortcutForKeyAsync(WinoApplicationMode mode, string key, ModifierKeys modifierKeys);
+
+ ///
+ /// Checks if a key combination is already assigned to another shortcut.
+ ///
+ /// The application mode to check within.
+ /// The key to check.
+ /// The modifier keys to check.
+ /// Optional ID to exclude from the check (for updates).
+ /// True if the combination is already used, false otherwise.
+ Task IsKeyCombinationInUseAsync(WinoApplicationMode mode, string key, ModifierKeys modifierKeys, Guid? excludeShortcutId = null);
+
+ ///
+ /// Creates default keyboard shortcuts for common mail operations.
+ ///
+ Task CreateDefaultShortcutsAsync();
+
+ ///
+ /// Resets all shortcuts to defaults.
+ ///
+ Task ResetToDefaultShortcutsAsync();
+}
diff --git a/Wino.Core.Domain/Interfaces/ILaunchProtocolService.cs b/Wino.Core.Domain/Interfaces/ILaunchProtocolService.cs
index 3e11c83d..39a75615 100644
--- a/Wino.Core.Domain/Interfaces/ILaunchProtocolService.cs
+++ b/Wino.Core.Domain/Interfaces/ILaunchProtocolService.cs
@@ -1,4 +1,5 @@
-using Wino.Core.Domain.Models.Launch;
+using System;
+using Wino.Core.Domain.Models.Launch;
namespace Wino.Core.Domain.Interfaces;
@@ -13,4 +14,5 @@ public interface ILaunchProtocolService
/// Used to handle mailto links.
///
MailToUri MailToUri { get; set; }
+
}
diff --git a/Wino.Core.Domain/Interfaces/IMailDialogService.cs b/Wino.Core.Domain/Interfaces/IMailDialogService.cs
index 53050931..dc60c422 100644
--- a/Wino.Core.Domain/Interfaces/IMailDialogService.cs
+++ b/Wino.Core.Domain/Interfaces/IMailDialogService.cs
@@ -1,10 +1,15 @@
-using System;
+#nullable enable
+using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
+using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Models;
+using Wino.Core.Domain.Models.Accounts;
+using Wino.Core.Domain.Models.Calendar;
using Wino.Core.Domain.Models.Folders;
namespace Wino.Core.Domain.Interfaces;
@@ -17,6 +22,7 @@ public interface IMailDialogService : IDialogServiceBase
// Custom dialogs
Task ShowMoveMailFolderDialogAsync(List availableFolders);
Task ShowAccountPickerDialogAsync(List availableAccounts);
+ Task ShowSingleCalendarPickerDialogAsync(List availableCalendarGroups);
///
/// Displays a dialog to the user for reordering accounts.
@@ -37,7 +43,7 @@ public interface IMailDialogService : IDialogServiceBase
/// Presents a dialog to the user for signature creation/modification.
///
/// Signature information. Null if canceled.
- Task ShowSignatureEditorDialog(AccountSignature signatureModel = null);
+ Task ShowSignatureEditorDialog(AccountSignature? signatureModel = null);
///
/// Presents a dialog to the user for account alias creation/modification.
@@ -49,4 +55,26 @@ public interface IMailDialogService : IDialogServiceBase
/// Presents a dialog to the user to show email source.
///
Task ShowMessageSourceDialogAsync(string messageSource);
+
+ ///
+ /// Presents a dialog to the user for keyboard shortcut creation/modification.
+ ///
+ /// Existing shortcut to edit, or null for new shortcut.
+ /// Dialog result with shortcut information.
+#pragma warning disable CS8625
+ Task ShowKeyboardShortcutDialogAsync(KeyboardShortcut existingShortcut = null);
+#pragma warning restore CS8625
+
+ ///
+ /// Presents a dialog to the user for contact creation/modification.
+ ///
+ /// Existing contact to edit, or null for new contact.
+ /// Contact information. Null if canceled.
+ Task ShowEditContactDialogAsync(AccountContact? contact = null);
+
+ Task ShowWinoAccountRegistrationDialogAsync();
+
+ Task ShowWinoAccountLoginDialogAsync();
+
+ Task ShowWinoAccountExportDialogAsync();
}
diff --git a/Wino.Core.Domain/Interfaces/IMailHashContainer.cs b/Wino.Core.Domain/Interfaces/IMailHashContainer.cs
new file mode 100644
index 00000000..9b4a0952
--- /dev/null
+++ b/Wino.Core.Domain/Interfaces/IMailHashContainer.cs
@@ -0,0 +1,9 @@
+using System;
+using System.Collections.Generic;
+
+namespace Wino.Core.Domain.Interfaces;
+
+public interface IMailHashContainer
+{
+ IEnumerable GetContainingIds();
+}
diff --git a/Wino.Core.Domain/Interfaces/IMailItemDisplayInformation.cs b/Wino.Core.Domain/Interfaces/IMailItemDisplayInformation.cs
new file mode 100644
index 00000000..613f2be2
--- /dev/null
+++ b/Wino.Core.Domain/Interfaces/IMailItemDisplayInformation.cs
@@ -0,0 +1,27 @@
+using System;
+using System.ComponentModel;
+using Wino.Core.Domain.Entities.Shared;
+
+namespace Wino.Core.Domain.Interfaces;
+
+///
+/// Shared display contract for mail list item rendering.
+/// Implemented by both single mail and thread mail view models.
+///
+public interface IMailItemDisplayInformation : INotifyPropertyChanged
+{
+ string Subject { get; }
+ string FromName { get; }
+ string FromAddress { get; }
+ string PreviewText { get; }
+ bool IsRead { get; }
+ bool IsDraft { get; }
+ bool HasAttachments { get; }
+ bool IsCalendarEvent { get; }
+ bool IsFlagged { get; }
+ DateTime CreationDate { get; }
+ Guid? ContactPictureFileId { get; }
+ bool ThumbnailUpdatedEvent { get; }
+ bool IsThreadExpanded { get; }
+ AccountContact SenderContact { get; }
+}
diff --git a/Wino.Core.Domain/Interfaces/IMailListItemSorting.cs b/Wino.Core.Domain/Interfaces/IMailListItemSorting.cs
new file mode 100644
index 00000000..5d4c24f5
--- /dev/null
+++ b/Wino.Core.Domain/Interfaces/IMailListItemSorting.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace Wino.Core.Domain.Interfaces;
+
+public interface IMailListItemSorting
+{
+ DateTime SortingDate { get; }
+ string SortingName { get; }
+}
diff --git a/Wino.Core.Domain/Interfaces/IMailService.cs b/Wino.Core.Domain/Interfaces/IMailService.cs
index 544889ea..f5f4b30d 100644
--- a/Wino.Core.Domain/Interfaces/IMailService.cs
+++ b/Wino.Core.Domain/Interfaces/IMailService.cs
@@ -25,7 +25,7 @@ public interface IMailService
/// Caution: This method is not safe. Use other overrides.
///
Task> GetMailItemsAsync(IEnumerable mailCopyIds);
- Task> FetchMailsAsync(MailListInitializationOptions options, CancellationToken cancellationToken = default);
+ Task> FetchMailsAsync(MailListInitializationOptions options, CancellationToken cancellationToken = default);
///
/// Deletes all mail copies for all folders.
@@ -131,6 +131,12 @@ public interface IMailService
/// Draft MailCopy and Draft MimeMessage as base64.
Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(Guid accountId, DraftCreationOptions draftCreationOptions);
+ ///
+ /// Finds a mail copy in the given account by RFC Message-Id.
+ /// Returns null when no local match exists.
+ ///
+ Task GetMailCopyByMessageIdAsync(Guid accountId, string messageId);
+
///
/// Returns ids
///
@@ -162,4 +168,17 @@ public interface IMailService
/// Retrieved MailCopy ids from search result.
/// Result model that contains added and removed mail copy ids.
Task GetGmailArchiveComparisonResultAsync(Guid archiveFolderId, List onlineArchiveMailIds);
+
+ ///
+ /// Gets the most recent mail IDs for a folder.
+ /// Used for notification purposes after sync completes.
+ ///
+ /// Folder ID.
+ /// Number of recent mails to return.
+ Task> GetRecentMailIdsForFolderAsync(Guid folderId, int count);
+
+ ///
+ /// Returns all mail copies for the account created before the given UTC date.
+ ///
+ Task> GetMailCopiesBeforeDateAsync(Guid accountId, DateTime cutoffDateUtc);
}
diff --git a/Wino.Core.Domain/Interfaces/IMenuOperation.cs b/Wino.Core.Domain/Interfaces/IMenuOperation.cs
index 2d588edb..340260c3 100644
--- a/Wino.Core.Domain/Interfaces/IMenuOperation.cs
+++ b/Wino.Core.Domain/Interfaces/IMenuOperation.cs
@@ -4,4 +4,5 @@ public interface IMenuOperation
{
bool IsEnabled { get; }
string Identifier { get; }
+ bool IsSecondaryMenuPreferred { get; }
}
diff --git a/Wino.Core.Domain/Interfaces/IMimeFileService.cs b/Wino.Core.Domain/Interfaces/IMimeFileService.cs
index a7b5b8db..b27a9df8 100644
--- a/Wino.Core.Domain/Interfaces/IMimeFileService.cs
+++ b/Wino.Core.Domain/Interfaces/IMimeFileService.cs
@@ -59,6 +59,26 @@ public interface IMimeFileService
///
Task DeleteMimeMessageAsync(Guid accountId, Guid fileId);
+ ///
+ /// Returns cached translated html for the given mime resource if it exists.
+ ///
+ Task GetTranslatedHtmlAsync(Guid accountId, Guid fileId, string targetLanguage, CancellationToken cancellationToken = default);
+
+ ///
+ /// Saves translated html for the given mime resource.
+ ///
+ Task SaveTranslatedHtmlAsync(Guid accountId, Guid fileId, string targetLanguage, string html, CancellationToken cancellationToken = default);
+
+ ///
+ /// Returns cached summary text for the given mime resource if it exists.
+ ///
+ Task GetSummaryTextAsync(Guid accountId, Guid fileId, CancellationToken cancellationToken = default);
+
+ ///
+ /// Saves summary text for the given mime resource.
+ ///
+ Task SaveSummaryTextAsync(Guid accountId, Guid fileId, string summary, CancellationToken cancellationToken = default);
+
///
/// Prepares the final model containing rendering details.
///
diff --git a/Wino.Core.Domain/Interfaces/IMimeStorageService.cs b/Wino.Core.Domain/Interfaces/IMimeStorageService.cs
new file mode 100644
index 00000000..9c67ebea
--- /dev/null
+++ b/Wino.Core.Domain/Interfaces/IMimeStorageService.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Wino.Core.Domain.Interfaces;
+
+public interface IMimeStorageService
+{
+ Task GetMimeRootPathAsync();
+ Task> GetAccountsMimeStorageSizesAsync(IEnumerable accountIds);
+ Task DeleteAccountMimeStorageAsync(Guid accountId);
+ Task DeleteAccountMimeStorageOlderThanAsync(Guid accountId, DateTime cutoffDateUtc);
+}
diff --git a/Wino.Core.Domain/Interfaces/INativeAppService.cs b/Wino.Core.Domain/Interfaces/INativeAppService.cs
index cfbea72f..2c5c4662 100644
--- a/Wino.Core.Domain/Interfaces/INativeAppService.cs
+++ b/Wino.Core.Domain/Interfaces/INativeAppService.cs
@@ -22,4 +22,9 @@ public interface INativeAppService
/// This is used to display WAM broker dialog on running UWP app called by a windowless server code.
///
Func GetCoreWindowHwnd { get; set; }
+
+ ///
+ /// Gets the folder path where calendar attachments are stored.
+ ///
+ string GetCalendarAttachmentsFolderPath();
}
diff --git a/Wino.Core.Domain/Interfaces/INewThemeService.cs b/Wino.Core.Domain/Interfaces/INewThemeService.cs
new file mode 100644
index 00000000..037b4bee
--- /dev/null
+++ b/Wino.Core.Domain/Interfaces/INewThemeService.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Models.Personalization;
+
+namespace Wino.Core.Domain.Interfaces;
+
+public interface INewThemeService : IInitializeAsync
+{
+ event EventHandler ElementThemeChanged;
+ event EventHandler AccentColorChanged;
+ event EventHandler BackdropChanged;
+
+ Task> GetAvailableThemesAsync();
+ Task CreateNewCustomThemeAsync(string themeName, string accentColor, byte[] wallpaperData);
+ Task> GetCurrentCustomThemesAsync();
+ List GetAvailableAccountColors();
+ Task ApplyCustomThemeAsync(bool isInitializing);
+
+ // Window Backdrop Management
+ WindowBackdropType CurrentBackdropType { get; set; }
+ void ApplyBackdrop(WindowBackdropType backdropType);
+
+ // Settings
+ ApplicationElementTheme RootTheme { get; set; }
+ Guid? CurrentApplicationThemeId { get; set; }
+ string AccentColor { get; set; }
+ string GetSystemAccentColorHex();
+ bool IsCustomTheme { get; }
+
+ // Improved accent color management
+ Task SetAccentColorAsync(string hexColor, bool preserveTheme = true);
+
+ // Title bar color management
+ void UpdateSystemCaptionButtonColors();
+
+ // Backdrop management
+ List GetAvailableBackdropTypes();
+
+ ///
+ /// Re-applies the current theme (backdrop, root theme, accent, caption colors)
+ /// to the currently active window. Use after a window transition.
+ ///
+ Task ApplyThemeToActiveWindowAsync();
+}
diff --git a/Wino.Core.Domain/Interfaces/INotificationBuilder.cs b/Wino.Core.Domain/Interfaces/INotificationBuilder.cs
index 86961ad7..ed41e474 100644
--- a/Wino.Core.Domain/Interfaces/INotificationBuilder.cs
+++ b/Wino.Core.Domain/Interfaces/INotificationBuilder.cs
@@ -1,7 +1,9 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Threading.Tasks;
-using Wino.Core.Domain.Models.MailItem;
+using Wino.Core.Domain.Entities.Calendar;
+using Wino.Core.Domain.Entities.Mail;
+using Wino.Core.Domain.Entities.Shared;
namespace Wino.Core.Domain.Interfaces;
@@ -10,7 +12,7 @@ public interface INotificationBuilder
///
/// Creates toast notifications for new mails.
///
- Task CreateNotificationsAsync(Guid inboxFolderId, IEnumerable newMailItems);
+ Task CreateNotificationsAsync(IEnumerable newMailItems);
///
/// Gets the unread Inbox messages for each account and updates the taskbar icon.
@@ -18,13 +20,30 @@ public interface INotificationBuilder
///
Task UpdateTaskbarIconBadgeAsync();
- ///
- /// Creates test notification for test purposes.
- ///
- Task CreateTestNotificationAsync(string title, string message);
-
///
/// Removes the toast notification for a specific mail by unique id.
///
void RemoveNotification(Guid mailUniqueId);
+
+ ///
+ /// Shows a notification that the account requires attention.
+ ///
+ /// Account that needs attention.
+ void CreateAttentionRequiredNotification(MailAccount account);
+
+ ///
+ /// Shows a notification when WebView2 runtime is unavailable.
+ ///
+ void CreateWebView2RuntimeMissingNotification();
+
+ ///
+ /// Shows a notification when a Microsoft Store update is available.
+ ///
+ void CreateStoreUpdateNotification();
+
+ ///
+ /// Creates a calendar reminder toast for the specified calendar item.
+ ///
+ Task CreateCalendarReminderNotificationAsync(CalendarItem calendarItem, long reminderDurationInSeconds);
}
+
diff --git a/Wino.Core.Domain/Interfaces/IOutlookThreadingStrategy.cs b/Wino.Core.Domain/Interfaces/IOutlookThreadingStrategy.cs
deleted file mode 100644
index 03525f6f..00000000
--- a/Wino.Core.Domain/Interfaces/IOutlookThreadingStrategy.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace Wino.Core.Domain.Interfaces;
-
-public interface IOutlookThreadingStrategy : IThreadingStrategy { }
diff --git a/Wino.Core.Domain/Interfaces/IPreferencesService.cs b/Wino.Core.Domain/Interfaces/IPreferencesService.cs
index 95def0d5..b4f93412 100644
--- a/Wino.Core.Domain/Interfaces/IPreferencesService.cs
+++ b/Wino.Core.Domain/Interfaces/IPreferencesService.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.ComponentModel;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Calendar;
@@ -6,7 +6,7 @@ using Wino.Core.Domain.Models.Reader;
namespace Wino.Core.Domain.Interfaces;
-public interface IPreferencesService: INotifyPropertyChanged
+public interface IPreferencesService : INotifyPropertyChanged
{
///
/// When any of the preferences are changed.
@@ -30,11 +30,6 @@ public interface IPreferencesService: INotifyPropertyChanged
///
bool IsNavigationPaneOpened { get; set; }
- ///
- /// Setting: Gets or sets what should happen to server app when the client is terminated.
- ///
- ServerBackgroundMode ServerTerminationBehavior { get; set; }
-
///
/// Setting: Preferred time format for mail or calendar header display.
///
@@ -57,6 +52,47 @@ public interface IPreferencesService: INotifyPropertyChanged
///
int EmailSyncIntervalMinutes { get; set; }
+ ///
+ /// Setting: Default application mode to open when activation does not specify one.
+ ///
+ WinoApplicationMode DefaultApplicationMode { get; set; }
+
+ ///
+ /// Setting: Whether Microsoft Store update notifications should be shown.
+ ///
+ bool IsStoreUpdateNotificationsEnabled { get; set; }
+
+ ///
+ /// Setting: Whether the Wino account profile button in the shell title bar should be hidden.
+ ///
+ bool IsWinoAccountButtonHidden { get; set; }
+
+ ///
+ /// Setting: Default target language code used for AI translation actions.
+ ///
+ string AiDefaultTranslationLanguageCode { get; set; }
+
+ ///
+ /// Setting: Preferred target language code for AI summarize actions.
+ ///
+ string AiSummarizeLanguageCode { get; set; }
+
+ ///
+ /// Setting: Preferred folder path used when saving AI summaries.
+ ///
+ string AiSummarySavePath { get; set; }
+
+ ///
+ /// Serializes the current syncable preferences snapshot.
+ ///
+ string ExportPreferences();
+
+ ///
+ /// Deserializes and applies a preferences snapshot.
+ /// Returns the applied and failed property counts.
+ ///
+ (int appliedCount, int failedCount) ImportPreferences(string settingsJson);
+
#endregion
#region Mail
@@ -96,11 +132,6 @@ public interface IPreferencesService: INotifyPropertyChanged
///
bool IsShowPreviewEnabled { get; set; }
- ///
- /// Setting: Enable/disable semantic zoom on clicking date headers.
- ///
- bool IsSemanticZoomEnabled { get; set; }
-
///
/// Setting: Set whether 'img' tags in rendered HTMLs should be removed.
///
@@ -151,11 +182,6 @@ public interface IPreferencesService: INotifyPropertyChanged
///
MailOperation RightHoverAction { get; set; }
- ///
- /// Setting: Whether Mailkit Protocol Logger is enabled for ImapTestService or not.
- ///
- bool IsMailkitProtocolLoggerEnabled { get; set; }
-
///
/// Setting: Which entity id (merged account or folder) should be expanded automatically on startup.
///
@@ -215,12 +241,34 @@ public interface IPreferencesService: INotifyPropertyChanged
#region Calendar
DayOfWeek FirstDayOfWeek { get; set; }
+ bool IsWorkingHoursEnabled { get; set; }
TimeSpan WorkingHourStart { get; set; }
TimeSpan WorkingHourEnd { get; set; }
DayOfWeek WorkingDayStart { get; set; }
DayOfWeek WorkingDayEnd { get; set; }
double HourHeight { get; set; }
+ string CalendarTimedDayHeaderDateFormat { get; set; }
+ ///
+ /// Setting: Default reminder duration in seconds for new calendar events.
+ /// Set to 0 to disable default reminders.
+ ///
+ long DefaultReminderDurationInSeconds { get; set; }
+
+ ///
+ /// Setting: Default snooze duration in minutes for calendar reminder notifications.
+ ///
+ int DefaultSnoozeDurationInMinutes { get; set; }
+
+ ///
+ /// Setting: How the New Event button chooses a calendar.
+ ///
+ NewEventButtonBehavior NewEventButtonBehavior { get; set; }
+
+ ///
+ /// Setting: Default calendar used when New Event is configured to always use a specific calendar.
+ ///
+ Guid? DefaultNewEventCalendarId { get; set; }
CalendarSettings GetCurrentCalendarSettings();
diff --git a/Wino.Core.Domain/Interfaces/IRequestBundle.cs b/Wino.Core.Domain/Interfaces/IRequestBundle.cs
index f88a3dc2..955b89d8 100644
--- a/Wino.Core.Domain/Interfaces/IRequestBundle.cs
+++ b/Wino.Core.Domain/Interfaces/IRequestBundle.cs
@@ -1,4 +1,6 @@
-using Wino.Core.Domain.Entities.Mail;
+using System;
+using Wino.Core.Domain.Entities.Calendar;
+using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Interfaces;
@@ -63,3 +65,10 @@ public interface IFolderActionRequest : IRequestBase
FolderSynchronizerOperation Operation { get; }
}
+
+public interface ICalendarActionRequest : IRequestBase
+{
+ CalendarItem Item { get; }
+ Guid? LocalCalendarItemId { get; }
+ CalendarSynchronizerOperation Operation { get; }
+}
diff --git a/Wino.Core.Domain/Interfaces/IRetryExecutor.cs b/Wino.Core.Domain/Interfaces/IRetryExecutor.cs
new file mode 100644
index 00000000..e7d31c34
--- /dev/null
+++ b/Wino.Core.Domain/Interfaces/IRetryExecutor.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Wino.Core.Domain.Models.Retry;
+using Wino.Core.Domain.Models.Synchronization;
+
+namespace Wino.Core.Domain.Interfaces;
+
+///
+/// Executes operations with automatic retry and error handling support.
+///
+public interface IRetryExecutor
+{
+ ///
+ /// Executes an operation with automatic retry based on the specified policy.
+ ///
+ /// The return type of the operation.
+ /// The async operation to execute.
+ /// The retry policy to apply.
+ /// Factory to create error context from exceptions.
+ /// Optional error handler for custom error processing.
+ /// Cancellation token.
+ /// The result of the operation.
+ /// Thrown when all retries are exhausted or a fatal error occurs.
+ Task ExecuteWithRetryAsync(
+ Func> operation,
+ RetryPolicy policy,
+ Func errorContextFactory,
+ ISynchronizerErrorHandlerFactory errorHandler = null,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Executes an operation with automatic retry based on the specified policy (void return).
+ ///
+ /// The async operation to execute.
+ /// The retry policy to apply.
+ /// Factory to create error context from exceptions.
+ /// Optional error handler for custom error processing.
+ /// Cancellation token.
+ /// Thrown when all retries are exhausted or a fatal error occurs.
+ Task ExecuteWithRetryAsync(
+ Func operation,
+ RetryPolicy policy,
+ Func errorContextFactory,
+ ISynchronizerErrorHandlerFactory errorHandler = null,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Executes an operation with default retry policy.
+ ///
+ /// The return type of the operation.
+ /// The async operation to execute.
+ /// Factory to create error context from exceptions.
+ /// Cancellation token.
+ /// The result of the operation.
+ Task ExecuteWithRetryAsync(
+ Func> operation,
+ Func errorContextFactory,
+ CancellationToken cancellationToken = default);
+}
diff --git a/Wino.Core.Domain/Interfaces/ISettingsBuilderService.cs b/Wino.Core.Domain/Interfaces/ISettingsBuilderService.cs
deleted file mode 100644
index 7cca8604..00000000
--- a/Wino.Core.Domain/Interfaces/ISettingsBuilderService.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using System.Collections.Generic;
-using Wino.Core.Domain.Models.Settings;
-
-namespace Wino.Core.Domain.Interfaces;
-
-public interface ISettingsBuilderService
-{
- List GetSettingItems();
-}
diff --git a/Wino.Core.Domain/Interfaces/IShellClient.cs b/Wino.Core.Domain/Interfaces/IShellClient.cs
new file mode 100644
index 00000000..ab9ad0c9
--- /dev/null
+++ b/Wino.Core.Domain/Interfaces/IShellClient.cs
@@ -0,0 +1,79 @@
+#nullable enable
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Input;
+using Wino.Core.Domain.Entities.Mail;
+using Wino.Core.Domain.Entities.Shared;
+using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.MenuItems;
+using Wino.Core.Domain.Models;
+using Wino.Core.Domain.Models.Calendar;
+using Wino.Core.Domain.Models.Folders;
+using Wino.Core.Domain.Models.Navigation;
+
+namespace Wino.Core.Domain.Interfaces;
+
+public interface IShellClient : INotifyPropertyChanged
+{
+ WinoApplicationMode Mode { get; }
+ IDispatcher Dispatcher { get; set; }
+ MenuItemCollection? MenuItems { get; }
+ object? SelectedMenuItem { get; set; }
+ bool HandlesNavigationSelection { get; }
+
+ void Activate(ShellModeActivationContext activationContext);
+ void Deactivate();
+ Task HandleNavigationItemInvokedAsync(IMenuItem? menuItem);
+ Task HandleNavigationSelectionChangedAsync(IMenuItem? menuItem);
+ Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args);
+}
+
+public interface IMailShellClient : IShellClient
+{
+ IMenuItem CreatePrimaryMenuItem { get; }
+
+ IEnumerable GetFolderContextMenuActions(IBaseFolderMenuItem folder);
+ Task HandleAccountCreatedAsync(MailAccount createdAccount);
+ Task NavigateFolderAsync(IBaseFolderMenuItem baseFolderMenuItem, TaskCompletionSource? folderInitAwaitTask = null);
+ Task ChangeLoadedAccountAsync(IAccountMenuItem clickedBaseAccountMenuItem, bool navigateInbox = true);
+ Task PerformFolderOperationAsync(FolderOperation operation, IBaseFolderMenuItem folderMenuItem);
+ Task PerformMoveOperationAsync(IEnumerable items, IBaseFolderMenuItem targetFolderMenuItem);
+ Task CreateNewMailForAsync(MailAccount account);
+}
+
+public interface ICalendarShellClient : IShellClient
+{
+ IStatePersistanceService StatePersistenceService { get; }
+ IEnumerable DateNavigationHeaderItems { get; }
+ int SelectedDateNavigationHeaderIndex { get; }
+ VisibleDateRange? CurrentVisibleRange { get; }
+ string VisibleDateRangeText { get; }
+ bool CanSynchronizeCalendars { get; }
+ ICommand SyncCommand { get; }
+ ICommand TodayClickedCommand { get; }
+ ICommand DateClickedCommand { get; }
+ ICommand PreviousDateRangeCommand { get; }
+ ICommand NextDateRangeCommand { get; }
+ IEnumerable GroupedAccountCalendars { get; }
+}
+
+public interface IShellViewModel
+{
+ WinoApplicationMode CurrentMode { get; }
+ IShellClient CurrentClient { get; }
+ MenuItemCollection? CurrentMenuItems { get; }
+ object? SelectedMenuItem { get; set; }
+
+ void SetCurrentMode(WinoApplicationMode mode);
+ IShellClient GetClient(WinoApplicationMode mode);
+}
+
+public interface IShellHost
+{
+ bool HasShellContent { get; }
+
+ void ActivateMode(WinoApplicationMode mode, ShellModeActivationContext activationContext);
+}
diff --git a/Wino.Core.Domain/Interfaces/ISmimeCertificateService.cs b/Wino.Core.Domain/Interfaces/ISmimeCertificateService.cs
new file mode 100644
index 00000000..9b9fee6a
--- /dev/null
+++ b/Wino.Core.Domain/Interfaces/ISmimeCertificateService.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+using System.Security.Cryptography.X509Certificates;
+
+namespace Wino.Core.Domain.Interfaces;
+
+public interface ISmimeCertificateService
+{
+ public IEnumerable GetCertificates(StoreName storeName = StoreName.My, StoreLocation storeLocation = StoreLocation.CurrentUser, string emailAddress = null);
+ public void ImportCertificate(string fileExtension, byte[] rawData, string password = null, StoreName storeName = StoreName.My, StoreLocation storeLocation = StoreLocation.CurrentUser);
+ public void RemoveCertificate(string thumbprint, StoreName storeName = StoreName.My, StoreLocation storeLocation = StoreLocation.CurrentUser);
+}
diff --git a/Wino.Core.Domain/Interfaces/IStatePersistenceService.cs b/Wino.Core.Domain/Interfaces/IStatePersistenceService.cs
index 8ed05626..834415d0 100644
--- a/Wino.Core.Domain/Interfaces/IStatePersistenceService.cs
+++ b/Wino.Core.Domain/Interfaces/IStatePersistenceService.cs
@@ -18,16 +18,26 @@ public interface IStatePersistanceService : INotifyPropertyChanged
///
string CoreWindowTitle { get; set; }
+ ///
+ /// App mode title shown in the title bar.
+ ///
+ string AppModeTitle { get; set; }
+
///
/// When only reader page is visible in small sized window.
///
bool IsReaderNarrowed { get; set; }
///
- /// Should display back button on the shell title bar.
+ /// Current application mode (Mail or Calendar).
+ /// Not persisted to configuration, only kept in memory.
///
- bool IsBackButtonVisible { get; }
+ WinoApplicationMode ApplicationMode { get; set; }
+ ///
+ /// Whether event details page is visible in Calendar mode.
+ ///
+ bool IsEventDetailsVisible { get; set; }
///
/// Setting: Opened pane length for the navigation view.
@@ -54,4 +64,5 @@ public interface IStatePersistanceService : INotifyPropertyChanged
/// Setting: Calendar display count for the day view.
///
int DayDisplayCount { get; set; }
+
}
diff --git a/Wino.Core.Domain/Interfaces/IStoreManagementService.cs b/Wino.Core.Domain/Interfaces/IStoreManagementService.cs
index ea61f1b7..bd945540 100644
--- a/Wino.Core.Domain/Interfaces/IStoreManagementService.cs
+++ b/Wino.Core.Domain/Interfaces/IStoreManagementService.cs
@@ -1,6 +1,6 @@
-using System.Threading.Tasks;
+#nullable enable
+using System.Threading.Tasks;
using Wino.Core.Domain.Enums;
-using Wino.Core.Domain.Models.Store;
namespace Wino.Core.Domain.Interfaces;
@@ -9,10 +9,20 @@ public interface IStoreManagementService
///
/// Checks whether user has the type of an add-on purchased.
///
- Task HasProductAsync(StoreProductType productType);
+ Task HasProductAsync(WinoAddOnProductType productType);
///
/// Attempts to purchase the given add-on.
///
- Task PurchaseAsync(StoreProductType productType);
+ Task PurchaseAsync(WinoAddOnProductType productType);
+
+ ///
+ /// Requests a Microsoft Store collections ID key for the current customer.
+ ///
+ Task GetCustomerCollectionsIdAsync(string serviceTicket, string publisherUserId);
+
+ ///
+ /// Requests a Microsoft Store purchase ID key for the current customer.
+ ///
+ Task GetCustomerPurchaseIdAsync(string serviceTicket, string publisherUserId);
}
diff --git a/Wino.Core.Domain/Interfaces/IStoreUpdateService.cs b/Wino.Core.Domain/Interfaces/IStoreUpdateService.cs
new file mode 100644
index 00000000..1977b13e
--- /dev/null
+++ b/Wino.Core.Domain/Interfaces/IStoreUpdateService.cs
@@ -0,0 +1,12 @@
+using System.Threading.Tasks;
+
+namespace Wino.Core.Domain.Interfaces;
+
+public interface IStoreUpdateService
+{
+ bool HasAvailableUpdate { get; }
+
+ Task RefreshAvailabilityAsync(bool showNotification = false);
+
+ Task StartUpdateAsync();
+}
diff --git a/Wino.Core.Domain/Interfaces/ISynchronizationManager.cs b/Wino.Core.Domain/Interfaces/ISynchronizationManager.cs
new file mode 100644
index 00000000..bfacad73
--- /dev/null
+++ b/Wino.Core.Domain/Interfaces/ISynchronizationManager.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Wino.Core.Domain.Entities.Mail;
+using Wino.Core.Domain.Entities.Shared;
+using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Models.Authentication;
+using Wino.Core.Domain.Models.Connectivity;
+using Wino.Core.Domain.Models.Synchronization;
+
+namespace Wino.Core.Domain.Interfaces;
+
+///
+/// Interface for the singleton synchronization manager that handles synchronizer instances and operations.
+///
+public interface ISynchronizationManager
+{
+ ///
+ /// Initializes the SynchronizationManager with required dependencies.
+ ///
+ Task InitializeAsync(ISynchronizerFactory synchronizerFactory,
+ IImapTestService imapTestService,
+ IAccountService accountService,
+ INotificationBuilder notificationBuilder,
+ IAuthenticationProvider authenticationProvider);
+
+ ///
+ /// Tests IMAP server connectivity for the given server information.
+ ///
+ Task TestImapConnectivityAsync(CustomServerInformation serverInformation, bool allowSSLHandshake);
+
+ ///
+ /// Starts a new mail synchronization for the given account.
+ ///
+ Task SynchronizeMailAsync(MailSynchronizationOptions options,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Checks if there is an ongoing synchronization for the given account.
+ ///
+ bool IsAccountSynchronizing(Guid accountId);
+
+ ///
+ /// Queues a mail action request to the corresponding account's synchronizer with optional synchronization triggering.
+ ///
+ Task QueueRequestAsync(IRequestBase request, Guid accountId, bool triggerSynchronization);
+
+ ///
+ /// Handles folder synchronization for the given account.
+ ///
+ Task SynchronizeFoldersAsync(Guid accountId,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Handles alias synchronization for the given account.
+ ///
+ Task SynchronizeAliasesAsync(Guid accountId,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Handles profile synchronization for the given account.
+ ///
+ Task SynchronizeProfileAsync(Guid accountId,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Handles calendar synchronization for the given account.
+ ///
+ Task SynchronizeCalendarAsync(CalendarSynchronizationOptions options,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Downloads a MIME message for the given mail item.
+ ///
+ Task DownloadMimeMessageAsync(MailCopy mailItem, Guid accountId,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Creates a new synchronizer for a newly added account.
+ ///
+ IWinoSynchronizerBase CreateSynchronizerForAccount(MailAccount account);
+
+ ///
+ /// Cancels ongoing synchronizations for the given account.
+ ///
+ Task CancelSynchronizationsAsync(Guid accountId);
+
+ ///
+ /// Destroys the synchronizer for the given account.
+ ///
+ Task DestroySynchronizerAsync(Guid accountId);
+
+ ///
+ /// Gets all cached synchronizers.
+ ///
+ IEnumerable GetAllSynchronizers();
+
+ ///
+ /// Gets a synchronizer for the given account ID.
+ ///
+ Task GetSynchronizerAsync(Guid accountId);
+
+ ///
+ /// Handles OAuth authentication for the specified provider.
+ ///
+ Task HandleAuthorizationAsync(MailProviderType providerType,
+ MailAccount account = null,
+ bool proposeCopyAuthorizationURL = false);
+}
diff --git a/Wino.Core.Domain/Interfaces/ISynchronizerErrorHandlerFactory.cs b/Wino.Core.Domain/Interfaces/ISynchronizerErrorHandlerFactory.cs
new file mode 100644
index 00000000..79f32f84
--- /dev/null
+++ b/Wino.Core.Domain/Interfaces/ISynchronizerErrorHandlerFactory.cs
@@ -0,0 +1,9 @@
+using System.Threading.Tasks;
+using Wino.Core.Domain.Models.Synchronization;
+
+namespace Wino.Core.Domain.Interfaces;
+
+public interface ISynchronizerErrorHandlerFactory
+{
+ Task HandleErrorAsync(SynchronizerErrorContext error);
+}
diff --git a/Wino.Core.Domain/Interfaces/IThemeService.cs b/Wino.Core.Domain/Interfaces/IThemeService.cs
deleted file mode 100644
index 322643af..00000000
--- a/Wino.Core.Domain/Interfaces/IThemeService.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading.Tasks;
-using Wino.Core.Domain.Enums;
-using Wino.Core.Domain.Models.Personalization;
-
-namespace Wino.Core.Domain.Interfaces;
-
-public interface IThemeService : IInitializeAsync
-{
- event EventHandler ElementThemeChanged;
- event EventHandler AccentColorChanged;
-
- Task> GetAvailableThemesAsync();
- Task CreateNewCustomThemeAsync(string themeName, string accentColor, byte[] wallpaperData);
- Task> GetCurrentCustomThemesAsync();
- List GetAvailableAccountColors();
- Task ApplyCustomThemeAsync(bool isInitializing);
-
- // Settings
- ApplicationElementTheme RootTheme { get; set; }
- Guid CurrentApplicationThemeId { get; set; }
- string AccentColor { get; set; }
- string GetSystemAccentColorHex();
- bool IsCustomTheme { get; }
-}
diff --git a/Wino.Core.Domain/Interfaces/IThreadingStrategy.cs b/Wino.Core.Domain/Interfaces/IThreadingStrategy.cs
deleted file mode 100644
index c27b8e56..00000000
--- a/Wino.Core.Domain/Interfaces/IThreadingStrategy.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System.Collections.Generic;
-using System.Threading.Tasks;
-using Wino.Core.Domain.Entities.Mail;
-using Wino.Core.Domain.Models.Folders;
-using Wino.Core.Domain.Models.MailItem;
-
-namespace Wino.Core.Domain.Interfaces;
-
-public interface IThreadingStrategy
-{
- ///
- /// Attach thread mails to the list.
- ///
- /// Original mails.
- /// Original mails with thread mails.
- Task> ThreadItemsAsync(List items, IMailItemFolder threadingForFolder);
- bool ShouldThreadWithItem(IMailItem originalItem, IMailItem targetItem);
-}
diff --git a/Wino.Core.Domain/Interfaces/IThreadingStrategyProvider.cs b/Wino.Core.Domain/Interfaces/IThreadingStrategyProvider.cs
deleted file mode 100644
index cca544f5..00000000
--- a/Wino.Core.Domain/Interfaces/IThreadingStrategyProvider.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using Wino.Core.Domain.Enums;
-
-namespace Wino.Core.Domain.Interfaces;
-
-public interface IThreadingStrategyProvider
-{
- ///
- /// Returns corresponding threading strategy that applies to given provider type.
- ///
- /// Provider type.
- IThreadingStrategy GetStrategy(MailProviderType mailProviderType);
-}
diff --git a/Wino.Core.Domain/Interfaces/IUpdateManager.cs b/Wino.Core.Domain/Interfaces/IUpdateManager.cs
new file mode 100644
index 00000000..d0e4a900
--- /dev/null
+++ b/Wino.Core.Domain/Interfaces/IUpdateManager.cs
@@ -0,0 +1,21 @@
+using System.Threading.Tasks;
+using Wino.Core.Domain.Models.Updates;
+using System.Collections.Generic;
+
+namespace Wino.Core.Domain.Interfaces;
+
+public interface IUpdateManager
+{
+ /// Loads and parses the update notes for the current version from the bundled asset file.
+ Task GetLatestUpdateNotesAsync();
+
+ /// Loads and parses the app feature highlights from the bundled asset file.
+ Task> GetFeaturesAsync();
+
+ /// Returns true if the current version's update notes have not yet been shown to the user.
+ bool ShouldShowUpdateNotes();
+
+ /// Stores a flag in local settings indicating the update notes for the current version have been seen.
+ void MarkUpdateNotesAsSeen();
+
+}
diff --git a/Wino.Core.Domain/Interfaces/IWebView2RuntimeValidatorService.cs b/Wino.Core.Domain/Interfaces/IWebView2RuntimeValidatorService.cs
new file mode 100644
index 00000000..aafa0669
--- /dev/null
+++ b/Wino.Core.Domain/Interfaces/IWebView2RuntimeValidatorService.cs
@@ -0,0 +1,12 @@
+using System.Threading.Tasks;
+
+namespace Wino.Core.Domain.Interfaces;
+
+public interface IWebView2RuntimeValidatorService
+{
+ ///
+ /// Validates whether WebView2 runtime is installed and available for use.
+ ///
+ Task IsRuntimeAvailableAsync();
+}
+
diff --git a/Wino.Core.Domain/Interfaces/IWinoAccountApiClient.cs b/Wino.Core.Domain/Interfaces/IWinoAccountApiClient.cs
new file mode 100644
index 00000000..79d21e29
--- /dev/null
+++ b/Wino.Core.Domain/Interfaces/IWinoAccountApiClient.cs
@@ -0,0 +1,33 @@
+#nullable enable
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Wino.Core.Domain.Models.Accounts;
+using Wino.Mail.Api.Contracts.Ai;
+using Wino.Mail.Api.Contracts.Auth;
+using Wino.Mail.Api.Contracts.Common;
+using Wino.Mail.Api.Contracts.Users;
+
+namespace Wino.Core.Domain.Interfaces;
+
+public interface IWinoAccountApiClient
+{
+ Task> RegisterAsync(string email, string password, CancellationToken cancellationToken = default);
+ Task> LoginAsync(string email, string password, CancellationToken cancellationToken = default);
+ Task> RefreshAsync(string refreshToken, CancellationToken cancellationToken = default);
+ Task> ResendEmailConfirmationAsync(string endpoint, string ticket, CancellationToken cancellationToken = default);
+ Task> ForgotPasswordAsync(string email, CancellationToken cancellationToken = default);
+ Task> LogoutAsync(string refreshToken, CancellationToken cancellationToken = default);
+ Task> GetCurrentUserAsync(CancellationToken cancellationToken = default);
+ Task> GetAiStatusAsync(CancellationToken cancellationToken = default);
+ Task> SummarizeAsync(string html, string targetLanguage, CancellationToken cancellationToken = default);
+ Task> TranslateAsync(string html, string targetLanguage, CancellationToken cancellationToken = default);
+ Task> RewriteAsync(string html, string mode, CancellationToken cancellationToken = default);
+ Task