Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c6cd06c65f | |||
| c942066878 |
@@ -100,7 +100,6 @@ _dialogService.InfoBarMessage(Translator.Info_MissingFolderTitle, message);
|
|||||||
- **NEVER** create IValueConverter classes or add them to Converters.xaml
|
- **NEVER** create IValueConverter classes or add them to Converters.xaml
|
||||||
- **NEVER** use BoolToVisibilityConverter - WinUI 3 SDK automatically converts bool to Visibility
|
- **NEVER** use BoolToVisibilityConverter - WinUI 3 SDK automatically converts bool to Visibility
|
||||||
- Direct binding: `Visibility="{x:Bind IsVisible, Mode=OneWay}"`
|
- 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
|
### XamlHelpers for Complex Conversions
|
||||||
- **ALWAYS** use XamlHelpers static methods instead of converters
|
- **ALWAYS** use XamlHelpers static methods instead of converters
|
||||||
|
|||||||
@@ -1,187 +0,0 @@
|
|||||||
name: Manual Beta Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
release_title:
|
|
||||||
description: Optional GitHub release title override
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
packages: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release-beta:
|
|
||||||
name: Build and publish beta release
|
|
||||||
runs-on: windows-latest
|
|
||||||
env:
|
|
||||||
PROJECT_PATH: Wino.Mail.WinUI/Wino.Mail.WinUI.csproj
|
|
||||||
MANIFEST_PATH: Wino.Mail.WinUI/Package.appxmanifest
|
|
||||||
CHANGELOG_PATH: CHANGELOG.md
|
|
||||||
NUGET_CONFIG_PATH: ${{ github.workspace }}\nuget.config
|
|
||||||
PACKAGE_OUTPUT_DIR: ${{ github.workspace }}\artifacts\package
|
|
||||||
RELEASE_OUTPUT_DIR: ${{ github.workspace }}\artifacts\release
|
|
||||||
CERTIFICATE_PFX_PATH: ${{ github.workspace }}\artifacts\signing\beta-signing-cert.pfx
|
|
||||||
CERTIFICATE_CER_PATH: ${{ github.workspace }}\artifacts\release\Wino-Mail-Beta.cer
|
|
||||||
steps:
|
|
||||||
- name: Checkout selected branch
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ github.ref }}
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Fetch tags from origin
|
|
||||||
shell: pwsh
|
|
||||||
run: git fetch origin --force --tags
|
|
||||||
|
|
||||||
- name: Validate release secrets
|
|
||||||
shell: pwsh
|
|
||||||
env:
|
|
||||||
BETA_SIGNING_CERT_PFX_BASE64: ${{ secrets.BETA_SIGNING_CERT_PFX_BASE64 }}
|
|
||||||
run: |
|
|
||||||
if ([string]::IsNullOrWhiteSpace($env:BETA_SIGNING_CERT_PFX_BASE64)) {
|
|
||||||
throw "Missing required secret: BETA_SIGNING_CERT_PFX_BASE64"
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Setup .NET SDK
|
|
||||||
uses: actions/setup-dotnet@v4
|
|
||||||
with:
|
|
||||||
dotnet-version: 10.0.x
|
|
||||||
env:
|
|
||||||
NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Compute beta version and release metadata
|
|
||||||
id: metadata
|
|
||||||
shell: pwsh
|
|
||||||
env:
|
|
||||||
RELEASE_TITLE_INPUT: ${{ github.event.inputs.release_title }}
|
|
||||||
run: |
|
|
||||||
$manifestPath = Join-Path $env:GITHUB_WORKSPACE $env:MANIFEST_PATH
|
|
||||||
if (-not (Test-Path $manifestPath)) {
|
|
||||||
throw "Package manifest not found: $manifestPath"
|
|
||||||
}
|
|
||||||
|
|
||||||
$changelogPath = Join-Path $env:GITHUB_WORKSPACE $env:CHANGELOG_PATH
|
|
||||||
if (-not (Test-Path $changelogPath)) {
|
|
||||||
throw "Release notes file not found: $changelogPath"
|
|
||||||
}
|
|
||||||
|
|
||||||
[xml]$manifest = Get-Content -LiteralPath $manifestPath
|
|
||||||
$identityNode = $manifest.Package.Identity
|
|
||||||
if (-not $identityNode) {
|
|
||||||
throw "Could not locate the Package/Identity node in $manifestPath"
|
|
||||||
}
|
|
||||||
|
|
||||||
$currentVersionText = [string]$identityNode.Version
|
|
||||||
if ($currentVersionText -notmatch '^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)\.(?<revision>\d+)$') {
|
|
||||||
throw "Manifest version '$currentVersionText' is not a four-part numeric version."
|
|
||||||
}
|
|
||||||
|
|
||||||
$packageVersion = $currentVersionText
|
|
||||||
$releaseTag = "v$packageVersion"
|
|
||||||
$releaseTitleInput = $env:RELEASE_TITLE_INPUT
|
|
||||||
$releaseTitle = if ([string]::IsNullOrWhiteSpace($releaseTitleInput)) { $releaseTag } else { $releaseTitleInput.Trim() }
|
|
||||||
|
|
||||||
$headSha = (git rev-parse HEAD).Trim()
|
|
||||||
if ([string]::IsNullOrWhiteSpace($headSha)) {
|
|
||||||
throw "Failed to resolve the checked out commit SHA."
|
|
||||||
}
|
|
||||||
|
|
||||||
$notesInput = Get-Content -LiteralPath $changelogPath -Raw
|
|
||||||
if ([string]::IsNullOrWhiteSpace($notesInput)) {
|
|
||||||
throw "Release notes file is empty: $changelogPath"
|
|
||||||
}
|
|
||||||
|
|
||||||
$notesInput = $notesInput.Trim()
|
|
||||||
New-Item -ItemType Directory -Path $env:RELEASE_OUTPUT_DIR -Force | Out-Null
|
|
||||||
$releaseNotesPath = Join-Path $env:RELEASE_OUTPUT_DIR 'beta-release-notes.md'
|
|
||||||
$notesInput | Set-Content -LiteralPath $releaseNotesPath -Encoding utf8
|
|
||||||
|
|
||||||
"package_version=$packageVersion" >> $env:GITHUB_OUTPUT
|
|
||||||
"release_tag=$releaseTag" >> $env:GITHUB_OUTPUT
|
|
||||||
"release_title=$releaseTitle" >> $env:GITHUB_OUTPUT
|
|
||||||
"release_notes_path=$releaseNotesPath" >> $env:GITHUB_OUTPUT
|
|
||||||
"head_sha=$headSha" >> $env:GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Materialize signing certificate
|
|
||||||
shell: pwsh
|
|
||||||
env:
|
|
||||||
BETA_SIGNING_CERT_PFX_BASE64: ${{ secrets.BETA_SIGNING_CERT_PFX_BASE64 }}
|
|
||||||
run: |
|
|
||||||
$signingDir = Split-Path -Parent $env:CERTIFICATE_PFX_PATH
|
|
||||||
New-Item -ItemType Directory -Path $signingDir -Force | Out-Null
|
|
||||||
[IO.File]::WriteAllBytes($env:CERTIFICATE_PFX_PATH, [Convert]::FromBase64String($env:BETA_SIGNING_CERT_PFX_BASE64))
|
|
||||||
|
|
||||||
$certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($env:CERTIFICATE_PFX_PATH, $null, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable)
|
|
||||||
|
|
||||||
New-Item -ItemType Directory -Path (Split-Path -Parent $env:CERTIFICATE_CER_PATH) -Force | Out-Null
|
|
||||||
[IO.File]::WriteAllBytes($env:CERTIFICATE_CER_PATH, $certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert))
|
|
||||||
|
|
||||||
- name: Restore WinUI project dependencies
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
if (-not (Test-Path $env:NUGET_CONFIG_PATH)) {
|
|
||||||
throw "NuGet config file not found: $env:NUGET_CONFIG_PATH"
|
|
||||||
}
|
|
||||||
|
|
||||||
dotnet restore $env:PROJECT_PATH `
|
|
||||||
--configfile $env:NUGET_CONFIG_PATH `
|
|
||||||
-p:Platform=x64 `
|
|
||||||
/p:RestoreConfigFile="$env:NUGET_CONFIG_PATH"
|
|
||||||
|
|
||||||
- name: Build MSIX bundle
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
New-Item -ItemType Directory -Path $env:PACKAGE_OUTPUT_DIR -Force | Out-Null
|
|
||||||
|
|
||||||
dotnet build $env:PROJECT_PATH `
|
|
||||||
--configuration Release `
|
|
||||||
--no-restore `
|
|
||||||
--configfile $env:NUGET_CONFIG_PATH `
|
|
||||||
/p:Platform=x64 `
|
|
||||||
/p:RestoreConfigFile="$env:NUGET_CONFIG_PATH" `
|
|
||||||
/p:GenerateAppxPackageOnBuild=true `
|
|
||||||
/p:UapAppxPackageBuildMode=SideloadOnly `
|
|
||||||
/p:AppxBundle=Always `
|
|
||||||
/p:AppxBundlePlatforms="x86|x64|arm64" `
|
|
||||||
/p:AppxPackageDir="$env:PACKAGE_OUTPUT_DIR\\" `
|
|
||||||
/p:AppxPackageVersion=${{ steps.metadata.outputs.package_version }} `
|
|
||||||
/p:PackageCertificateKeyFile="$env:CERTIFICATE_PFX_PATH" `
|
|
||||||
/p:PackageCertificatePassword= `
|
|
||||||
/p:PackageCertificateThumbprint= `
|
|
||||||
/p:AppxPackageSigningEnabled=true
|
|
||||||
|
|
||||||
- name: Collect packaged artifacts
|
|
||||||
id: package
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
$bundle = Get-ChildItem -Path $env:PACKAGE_OUTPUT_DIR -Recurse -Filter *.msixbundle | Select-Object -First 1
|
|
||||||
if (-not $bundle) {
|
|
||||||
throw "No .msixbundle file was generated under $env:PACKAGE_OUTPUT_DIR"
|
|
||||||
}
|
|
||||||
|
|
||||||
$releaseAssetPath = Join-Path $env:RELEASE_OUTPUT_DIR "Wino_${{ steps.metadata.outputs.package_version }}.zip"
|
|
||||||
if (Test-Path $releaseAssetPath) {
|
|
||||||
Remove-Item -LiteralPath $releaseAssetPath -Force
|
|
||||||
}
|
|
||||||
|
|
||||||
Compress-Archive -LiteralPath @($bundle.FullName, $env:CERTIFICATE_CER_PATH) -DestinationPath $releaseAssetPath -Force
|
|
||||||
|
|
||||||
"bundle_path=$($bundle.FullName)" >> $env:GITHUB_OUTPUT
|
|
||||||
"bundle_name=$($bundle.Name)" >> $env:GITHUB_OUTPUT
|
|
||||||
"release_asset_path=$releaseAssetPath" >> $env:GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Create GitHub prerelease
|
|
||||||
shell: pwsh
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
gh release create "${{ steps.metadata.outputs.release_tag }}" `
|
|
||||||
"${{ steps.package.outputs.release_asset_path }}" `
|
|
||||||
--repo "${{ github.repository }}" `
|
|
||||||
--target "${{ steps.metadata.outputs.head_sha }}" `
|
|
||||||
--title "${{ steps.metadata.outputs.release_title }}" `
|
|
||||||
--notes-file "${{ steps.metadata.outputs.release_notes_path }}" `
|
|
||||||
--prerelease
|
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
name: PR WinUI Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- synchronize
|
||||||
|
- reopened
|
||||||
|
- ready_for_review
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- 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."
|
||||||
@@ -400,4 +400,3 @@ Wino/obj/x86/Debug/XamlSaveStateFile.xml
|
|||||||
*.cache
|
*.cache
|
||||||
.vs/Wino/v16/.suo
|
.vs/Wino/v16/.suo
|
||||||
/.claude/settings.local.json
|
/.claude/settings.local.json
|
||||||
scripts/translate_resources.local.bat
|
|
||||||
|
|||||||
@@ -1,163 +0,0 @@
|
|||||||
# 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
|
|
||||||
6. In XAML, translation bindings must use `Mode=OneTime` because `Wino.Core.Domain/Translator.cs` does not implement `INotifyPropertyChanged`
|
|
||||||
|
|
||||||
## 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)`).
|
|
||||||
- `ConfigureAwait(false)` continues execution on a background thread. Any UI-bound property change, `INotifyPropertyChanged` notification, collection mutation, or similar UI-facing state update after that point must be marshaled back with `ExecuteUIThread(...)` or the appropriate dispatcher call, otherwise the app can crash.
|
|
||||||
- Messenger messages are raised from a background thread by default, while UI control event handlers such as `Button.Click` start on the UI thread. Be deliberate when combining dispatcher usage with `ConfigureAwait(false)` so post-await UI updates always return to the UI thread.
|
|
||||||
- ViewModels should only handle UI interaction/state and delegate business logic to services; account-management work belongs in `WinoAccountProfileService`, and preferences import/export/apply logic belongs in `PreferencesService`.
|
|
||||||
- In `EventDetailsPageViewModel.LoadAttendeesAsync`, never mutate `CurrentEvent.Attendees` outside `ExecuteUIThread(...)`.
|
|
||||||
- Never create pure C# controls or controls that heavily manipulate UI structure from `.cs` files. Define controls in XAML and keep UI composition in XAML.
|
|
||||||
- Never add XAML-backed UI controls to `.xaml.cs`. If a view has XAML, all control declarations, flyouts, templates, and visual composition belong in the `.xaml` file; keep `.xaml.cs` limited to event handling and view glue.
|
|
||||||
- Never subscribe to framework events like `Loaded`, `Unloaded`, or input events from constructors in `.xaml.cs` for XAML-backed controls and pages; wire them directly in XAML instead.
|
|
||||||
- If you use `x:Load` in XAML, always give that `UIElement` an `x:Name`.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) 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 from command line
|
||||||
|
dotnet build WinoMail.slnx -c Debug
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
dotnet test Wino.Core.Tests/Wino.Core.Tests.csproj
|
||||||
|
|
||||||
|
# Build specific platform
|
||||||
|
dotnet build WinoMail.slnx -c Debug /p:Platform=x64
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prerequisites:** Visual Studio 2022+ with ".NET desktop development" workload, .NET SDK 10+
|
||||||
|
|
||||||
|
**Startup project:** Wino.Mail.WinUI
|
||||||
|
|
||||||
|
**Platforms:** x86, x64, ARM64
|
||||||
|
|
||||||
|
## 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)}`
|
||||||
|
|
||||||
|
## 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 other language files - Crowdin manages translations
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
|
||||||
|
## 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
|
||||||
|
- In ViewModels, update all UI-bound properties/collections via `ExecuteUIThread(...)` (especially after awaited calls and any use of `ConfigureAwait(false)`).
|
||||||
+34
-37
@@ -4,9 +4,9 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageVersion Include="ColorHashSharp" Version="1.1.0" />
|
<PackageVersion Include="ColorHashSharp" Version="1.1.0" />
|
||||||
<PackageVersion Include="CommunityToolkit.Common" Version="8.4.2" />
|
<PackageVersion Include="CommunityToolkit.Common" Version="8.4.0" />
|
||||||
<PackageVersion Include="CommunityToolkit.Diagnostics" Version="8.4.2" />
|
<PackageVersion Include="CommunityToolkit.Diagnostics" Version="8.4.0" />
|
||||||
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.1-build.4" />
|
||||||
<PackageVersion Include="CommunityToolkit.WinUI.Animations" Version="8.2.251219" />
|
<PackageVersion Include="CommunityToolkit.WinUI.Animations" Version="8.2.251219" />
|
||||||
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.251219" />
|
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.251219" />
|
||||||
<PackageVersion Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.251219" />
|
<PackageVersion Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.251219" />
|
||||||
@@ -15,66 +15,63 @@
|
|||||||
<PackageVersion Include="CommunityToolkit.WinUI.Controls.TokenizingTextBox" Version="8.2.251219" />
|
<PackageVersion Include="CommunityToolkit.WinUI.Controls.TokenizingTextBox" Version="8.2.251219" />
|
||||||
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.251219" />
|
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.251219" />
|
||||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.250926-build.2293" />
|
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.250926-build.2293" />
|
||||||
<PackageVersion Include="CommunityToolkit.WinUI.Lottie" Version="8.2.250604" />
|
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
|
||||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.DependencyPropertyGenerator" Version="0.1.250926-build.2293" />
|
<PackageVersion Include="CommunityToolkit.Labs.WinUI.DependencyPropertyGenerator" Version="0.1.250926-build.2293" />
|
||||||
<PackageVersion Include="EmailValidation" Version="1.3.0" />
|
<PackageVersion Include="EmailValidation" Version="1.3.0" />
|
||||||
<PackageVersion Include="gravatar-dotnet" Version="0.1.3" />
|
<PackageVersion Include="gravatar-dotnet" Version="0.1.3" />
|
||||||
<PackageVersion Include="HtmlAgilityPack" Version="1.12.4" />
|
<PackageVersion Include="HtmlAgilityPack" Version="1.12.4" />
|
||||||
<PackageVersion Include="Ical.Net" Version="5.2.1" />
|
<PackageVersion Include="Ical.Net" Version="4.3.1" />
|
||||||
<PackageVersion Include="IsExternalInit" Version="1.0.3" />
|
<PackageVersion Include="IsExternalInit" Version="1.0.3" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.6" />
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.6" />
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.1" />
|
||||||
<PackageVersion Include="Microsoft.Graph" Version="5.104.0" />
|
<PackageVersion Include="Microsoft.Graph" Version="5.99.0" />
|
||||||
<PackageVersion Include="Microsoft.Graphics.Win2D" Version="1.4.0" />
|
<PackageVersion Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
|
||||||
<PackageVersion Include="Microsoft.Identity.Client" Version="4.83.3" />
|
<PackageVersion Include="Microsoft.Identity.Client" Version="4.79.2" />
|
||||||
<PackageVersion Include="Microsoft.Identity.Client.Broker" Version="4.83.3" />
|
<PackageVersion Include="Microsoft.Identity.Client.Broker" Version="4.79.2" />
|
||||||
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.83.3" />
|
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.79.2" />
|
||||||
<PackageVersion Include="Microsoft.NETCore.UniversalWindowsPlatform" Version="6.2.14" />
|
<PackageVersion Include="Microsoft.NETCore.UniversalWindowsPlatform" Version="6.2.14" />
|
||||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools.MSIX" Version="1.7.260316102" />
|
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="3.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="3.0.1" />
|
<PackageVersion Include="MimeKit" Version="4.14.0" />
|
||||||
<PackageVersion Include="Wino.Mail.Contracts" Version="1.0.18" />
|
|
||||||
<PackageVersion Include="MimeKit" Version="4.16.0" />
|
|
||||||
<PackageVersion Include="morelinq" Version="4.4.0" />
|
<PackageVersion Include="morelinq" Version="4.4.0" />
|
||||||
<PackageVersion Include="Nito.AsyncEx" Version="5.1.2" />
|
<PackageVersion Include="Nito.AsyncEx" Version="5.1.2" />
|
||||||
<PackageVersion Include="Nito.AsyncEx.Tasks" Version="5.1.2" />
|
<PackageVersion Include="Nito.AsyncEx.Tasks" Version="5.1.2" />
|
||||||
<PackageVersion Include="NodaTime" Version="3.3.1" />
|
<PackageVersion Include="NodaTime" Version="3.2.3" />
|
||||||
<PackageVersion Include="Sentry.Serilog" Version="6.4.0" />
|
<PackageVersion Include="Sentry.Serilog" Version="6.0.0" />
|
||||||
<PackageVersion Include="Serilog" Version="4.3.1" />
|
<PackageVersion Include="Serilog" Version="4.3.0" />
|
||||||
<PackageVersion Include="Serilog.Exceptions" Version="8.4.0" />
|
<PackageVersion Include="Serilog.Exceptions" Version="8.4.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.Debug" Version="3.0.0" />
|
<PackageVersion Include="Serilog.Sinks.Debug" Version="3.0.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
|
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.ApplicationInsights" Version="4.0.0" />
|
<PackageVersion Include="Serilog.Sinks.ApplicationInsights" Version="4.0.0" />
|
||||||
<PackageVersion Include="SkiaSharp" Version="3.119.2" />
|
<PackageVersion Include="SkiaSharp" Version="3.119.1" />
|
||||||
<PackageVersion Include="SkiaSharp.Views.WinUI" Version="3.119.2" />
|
<PackageVersion Include="SkiaSharp.Views.WinUI" Version="3.119.1" />
|
||||||
<PackageVersion Include="sqlite-net-pcl" Version="1.10.196-beta" />
|
<PackageVersion Include="sqlite-net-pcl" Version="1.10.196-beta" />
|
||||||
<PackageVersion Include="System.Drawing.Common" Version="10.0.5" />
|
<PackageVersion Include="System.Drawing.Common" Version="10.0.1" />
|
||||||
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
|
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
|
||||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.10" />
|
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.10" />
|
||||||
<PackageVersion Include="System.Text.Json" Version="10.0.6" />
|
<PackageVersion Include="System.Text.Json" Version="10.0.1" />
|
||||||
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.3.0" />
|
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.3.0" />
|
||||||
<PackageVersion Include="H.NotifyIcon.WinUI" Version="2.4.1" />
|
<PackageVersion Include="H.NotifyIcon.WinUI" Version="2.4.1" />
|
||||||
<PackageVersion Include="CommunityToolkit.WinUI.Notifications" Version="7.1.2" />
|
<PackageVersion Include="CommunityToolkit.WinUI.Notifications" Version="7.1.2" />
|
||||||
<PackageVersion Include="Google.Apis.Auth" Version="1.73.0" />
|
<PackageVersion Include="Google.Apis.Auth" Version="1.73.0" />
|
||||||
<PackageVersion Include="Google.Apis.Calendar.v3" Version="1.73.0.4073" />
|
<PackageVersion Include="Google.Apis.Calendar.v3" Version="1.73.0.3993" />
|
||||||
<PackageVersion Include="Google.Apis.Drive.v3" Version="1.73.0.4112" />
|
<PackageVersion Include="Google.Apis.Gmail.v1" Version="1.73.0.3987" />
|
||||||
<PackageVersion Include="Google.Apis.Gmail.v1" Version="1.73.0.4029" />
|
|
||||||
<PackageVersion Include="Google.Apis.PeopleService.v1" Version="1.72.0.3973" />
|
<PackageVersion Include="Google.Apis.PeopleService.v1" Version="1.72.0.3973" />
|
||||||
<PackageVersion Include="HtmlKit" Version="1.2.0" />
|
<PackageVersion Include="HtmlKit" Version="1.2.0" />
|
||||||
<PackageVersion Include="MailKit" Version="4.16.0" />
|
<PackageVersion Include="MailKit" Version="4.14.1" />
|
||||||
<PackageVersion Include="TimePeriodLibrary.NET" Version="2.1.6" />
|
<PackageVersion Include="TimePeriodLibrary.NET" Version="2.1.6" />
|
||||||
<PackageVersion Include="System.Reactive" Version="6.1.0" />
|
<PackageVersion Include="System.Reactive" Version="6.1.0" />
|
||||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.6" />
|
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.1" />
|
||||||
<PackageVersion Include="System.Text.Encodings.Web" Version="10.0.6" />
|
<PackageVersion Include="System.Text.Encodings.Web" Version="10.0.1" />
|
||||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1721" />
|
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7175" />
|
||||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="2.0.250930001-experimental1" />
|
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="2.0.250930001-experimental1" />
|
||||||
<PackageVersion Include="WinUIEx" Version="2.9.0" />
|
<PackageVersion Include="WinUIEx" Version="2.9.0" />
|
||||||
<!-- Testing packages -->
|
<!-- Testing packages -->
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
|
||||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
<PackageVersion Include="xunit" Version="2.9.0" />
|
||||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
|
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
<PackageVersion Include="FluentAssertions" Version="8.9.0" />
|
<PackageVersion Include="FluentAssertions" Version="7.0.0" />
|
||||||
<PackageVersion Include="Moq" Version="4.20.72" />
|
<PackageVersion Include="Moq" Version="4.20.72" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,54 +1,45 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://apps.microsoft.com/detail/Wino%20Mail/9NCRCVJC50WL?launch=true&mode=full">
|
<a href="https://apps.microsoft.com/detail/Wino%20Mail/9NCRCVJC50WL?launch=true
|
||||||
<img src="https://www.winomail.app/images/v2/Logo.png" width="90" height="90" alt="Wino Mail logo">
|
&mode=full">
|
||||||
|
<img src="https://www.winomail.app/images/wino_logo.png" width=90 height=90>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<h3 align="center">Wino Mail</h3>
|
<h3 align="center">Wino Mail</h3>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
Native mail and calendar client for Windows.
|
Native mail client for Windows device families.
|
||||||
</p>
|
</p>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Motivation
|
## 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
|
## Features
|
||||||
|
|
||||||
- 📨 Outlook and Gmail API integration
|
- API integration for Outlook and Gmail
|
||||||
- 🌐 IMAP/SMTP support for custom mail servers
|
- IMAP/SMTP support for custom mail servers
|
||||||
- 📅 Calendar support with event creation/compose and reminders
|
- Send, receive, mark as (read,important,spam etc), move mails.
|
||||||
- 👥 Contact management and people-centric account experience
|
- Linked/Merged Accounts
|
||||||
- ✅ Core mail actions: send, receive, read/unread, move, spam, and more
|
- Toast notifications with background sync.
|
||||||
- 🔗 Linked/Merged accounts
|
- Instant startup performance
|
||||||
- 🔔 Toast notifications with background sync
|
- Offline use / search.
|
||||||
- ⚡ Instant startup-oriented architecture
|
- Modern and responsive UI
|
||||||
- 🔎 Offline-capable workflows and search improvements
|
- Lots of personalization options
|
||||||
- 🎛️ Modern responsive WinUI interface with personalization options
|
- Dark / Light mode for mail reader
|
||||||
- 🌗 Dark/Light mode for mail reader and app surfaces
|
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
||||||
Download latest version of Wino Mail from Microsoft Store for free.
|
Download latest version of Wino Mail from Microsoft Store for free.
|
||||||
|
|
||||||
<a href="https://apps.microsoft.com/detail/Wino%20Mail/9NCRCVJC50WL?launch=true&mode=full">
|
<a href="https://apps.microsoft.com/detail/Wino%20Mail/9NCRCVJC50WL?launch=true
|
||||||
<img src="https://get.microsoft.com/images/en-us%20dark.svg" width="200" alt="Get Wino Mail from Microsoft Store"/>
|
&mode=full">
|
||||||
|
<img src="https://get.microsoft.com/images/en-us%20dark.svg" width="200"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## Beta Releases
|
## Beta Releases
|
||||||
@@ -57,6 +48,7 @@ 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)
|
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
|
## 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.
|
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.
|
||||||
@@ -67,4 +59,3 @@ 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 [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.
|
- You can buy Unlimited Accounts add-on in the application. It's a one-time payment for lifetime, not a monthly recurring payment.
|
||||||
|
|
||||||
|
|||||||
@@ -1,158 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -45,6 +45,6 @@ public class GmailAuthenticator : BaseAuthenticator, IGmailAuthenticator
|
|||||||
return GoogleWebAuthorizationBroker.AuthorizeAsync(new ClientSecrets()
|
return GoogleWebAuthorizationBroker.AuthorizeAsync(new ClientSecrets()
|
||||||
{
|
{
|
||||||
ClientId = ClientId
|
ClientId = ClientId
|
||||||
}, AuthenticatorConfig.GetGmailScope(account?.IsMailAccessGranted != false, account?.IsCalendarAccessGranted == true), account.Id.ToString(), CancellationToken.None, new FileDataStore(AuthenticatorConfig.GmailTokenStoreIdentifier));
|
}, AuthenticatorConfig.GmailScope, account.Id.ToString(), CancellationToken.None, new FileDataStore(AuthenticatorConfig.GmailTokenStoreIdentifier));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,10 +65,7 @@ public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator
|
|||||||
_publicClientApplication = outlookAppBuilder.Build();
|
_publicClientApplication = outlookAppBuilder.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private string[] GetScope(MailAccount account)
|
public string[] Scope => AuthenticatorConfig.OutlookScope;
|
||||||
=> AuthenticatorConfig.GetOutlookScope(
|
|
||||||
account?.IsMailAccessGranted != false,
|
|
||||||
account?.IsCalendarAccessGranted == true);
|
|
||||||
|
|
||||||
private async Task EnsureTokenCacheAttachedAsync()
|
private async Task EnsureTokenCacheAttachedAsync()
|
||||||
{
|
{
|
||||||
@@ -94,7 +91,7 @@ public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var authResult = await _publicClientApplication.AcquireTokenSilent(GetScope(account), storedAccount).ExecuteAsync();
|
var authResult = await _publicClientApplication.AcquireTokenSilent(Scope, storedAccount).ExecuteAsync();
|
||||||
|
|
||||||
return new TokenInformationEx(authResult.AccessToken, authResult.Account.Username);
|
return new TokenInformationEx(authResult.AccessToken, authResult.Account.Username);
|
||||||
}
|
}
|
||||||
@@ -106,6 +103,10 @@ public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator
|
|||||||
|
|
||||||
return await GenerateTokenInformationAsync(account);
|
return await GenerateTokenInformationAsync(account);
|
||||||
}
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TokenInformationEx> GenerateTokenInformationAsync(MailAccount account)
|
public async Task<TokenInformationEx> GenerateTokenInformationAsync(MailAccount account)
|
||||||
@@ -121,11 +122,17 @@ public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator
|
|||||||
if (_nativeAppService.GetCoreWindowHwnd == null) throw new AuthenticationAttentionException(account);
|
if (_nativeAppService.GetCoreWindowHwnd == null) throw new AuthenticationAttentionException(account);
|
||||||
|
|
||||||
AuthenticationResult authResult = await _publicClientApplication
|
AuthenticationResult authResult = await _publicClientApplication
|
||||||
.AcquireTokenInteractive(GetScope(account))
|
.AcquireTokenInteractive(Scope)
|
||||||
.ExecuteAsync();
|
.ExecuteAsync();
|
||||||
|
|
||||||
// Microsoft 365 work/school tenants can use a sign-in UPN that differs from
|
// If the account is null, it means it's the initial creation of it.
|
||||||
// the mailbox primary SMTP address, so interactive reauth must not reject them.
|
// If not, make sure the authenticated user address matches the username.
|
||||||
|
// When people refresh their token, accounts must match.
|
||||||
|
|
||||||
|
if (account?.Address != null && !account.Address.Equals(authResult.Account.Username, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new AuthenticationException("Authenticated address does not match with your account address. If you are signing with a Office365, it is not officially supported yet.");
|
||||||
|
}
|
||||||
|
|
||||||
return new TokenInformationEx(authResult.AccessToken, authResult.Account.Username);
|
return new TokenInformationEx(authResult.AccessToken, authResult.Account.Username);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ public partial class CalendarAccountSettingsPageViewModel : CalendarBaseViewMode
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial bool IsSyncEnabled { get; set; }
|
public partial bool IsSyncEnabled { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial bool IsPrimaryCalendar { get; set; }
|
||||||
|
|
||||||
public ObservableCollection<ShowAsOption> ShowAsOptions { get; } = new ObservableCollection<ShowAsOption>();
|
public ObservableCollection<ShowAsOption> ShowAsOptions { get; } = new ObservableCollection<ShowAsOption>();
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
@@ -79,6 +82,7 @@ public partial class CalendarAccountSettingsPageViewModel : CalendarBaseViewMode
|
|||||||
// Initialize properties from AccountCalendar
|
// Initialize properties from AccountCalendar
|
||||||
AccountColorHex = AccountCalendar.BackgroundColorHex ?? "#0078D4";
|
AccountColorHex = AccountCalendar.BackgroundColorHex ?? "#0078D4";
|
||||||
IsSyncEnabled = AccountCalendar.IsSynchronizationEnabled;
|
IsSyncEnabled = AccountCalendar.IsSynchronizationEnabled;
|
||||||
|
IsPrimaryCalendar = AccountCalendar.IsPrimary;
|
||||||
SelectedDefaultShowAsOption = ShowAsOptions.FirstOrDefault(o => o.ShowAs == AccountCalendar.DefaultShowAs) ?? ShowAsOptions[2];
|
SelectedDefaultShowAsOption = ShowAsOptions.FirstOrDefault(o => o.ShowAs == AccountCalendar.DefaultShowAs) ?? ShowAsOptions[2];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +91,6 @@ public partial class CalendarAccountSettingsPageViewModel : CalendarBaseViewMode
|
|||||||
if (AccountCalendar != null && !string.IsNullOrEmpty(value))
|
if (AccountCalendar != null && !string.IsNullOrEmpty(value))
|
||||||
{
|
{
|
||||||
AccountCalendar.BackgroundColorHex = value;
|
AccountCalendar.BackgroundColorHex = value;
|
||||||
AccountCalendar.IsBackgroundColorUserOverridden = true;
|
|
||||||
SaveChangesAsync();
|
SaveChangesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,6 +104,15 @@ public partial class CalendarAccountSettingsPageViewModel : CalendarBaseViewMode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partial void OnIsPrimaryCalendarChanged(bool value)
|
||||||
|
{
|
||||||
|
if (AccountCalendar != null)
|
||||||
|
{
|
||||||
|
AccountCalendar.IsPrimary = value;
|
||||||
|
SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
partial void OnSelectedDefaultShowAsOptionChanged(ShowAsOption value)
|
partial void OnSelectedDefaultShowAsOptionChanged(ShowAsOption value)
|
||||||
{
|
{
|
||||||
if (AccountCalendar != null && value != null)
|
if (AccountCalendar != null && value != null)
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
@@ -11,25 +9,25 @@ using CommunityToolkit.Mvvm.Messaging;
|
|||||||
using Serilog;
|
using Serilog;
|
||||||
using Wino.Calendar.ViewModels.Data;
|
using Wino.Calendar.ViewModels.Data;
|
||||||
using Wino.Calendar.ViewModels.Interfaces;
|
using Wino.Calendar.ViewModels.Interfaces;
|
||||||
using Wino.Core.Domain;
|
|
||||||
using Wino.Core.Domain.Collections;
|
using Wino.Core.Domain.Collections;
|
||||||
using Wino.Core.Domain.Entities.Calendar;
|
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
|
using Wino.Core.Domain.Extensions;
|
||||||
using Wino.Core.Domain.Interfaces;
|
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.Calendar;
|
||||||
using Wino.Core.Domain.Models.Navigation;
|
using Wino.Core.Domain.Models.Navigation;
|
||||||
using Wino.Core.Domain.Models.Synchronization;
|
using Wino.Core.Domain.Models.Synchronization;
|
||||||
using Wino.Core.ViewModels;
|
using Wino.Core.ViewModels;
|
||||||
using Wino.Messaging.Client.Calendar;
|
using Wino.Messaging.Client.Calendar;
|
||||||
|
using Wino.Messaging.Client.Navigation;
|
||||||
using Wino.Messaging.Server;
|
using Wino.Messaging.Server;
|
||||||
using Wino.Messaging.UI;
|
using Wino.Messaging.UI;
|
||||||
|
|
||||||
namespace Wino.Calendar.ViewModels;
|
namespace Wino.Calendar.ViewModels;
|
||||||
|
|
||||||
public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||||
ICalendarShellClient,
|
IRecipient<VisibleDateRangeChangedMessage>,
|
||||||
|
IRecipient<CalendarEnableStatusChangedMessage>,
|
||||||
|
IRecipient<NavigateManageAccountsRequested>,
|
||||||
IRecipient<CalendarDisplayTypeChangedMessage>,
|
IRecipient<CalendarDisplayTypeChangedMessage>,
|
||||||
IRecipient<AccountRemovedMessage>
|
IRecipient<AccountRemovedMessage>
|
||||||
{
|
{
|
||||||
@@ -37,31 +35,27 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
public IStatePersistanceService StatePersistenceService { get; }
|
public IStatePersistanceService StatePersistenceService { get; }
|
||||||
public IAccountCalendarStateService AccountCalendarStateService { get; }
|
public IAccountCalendarStateService AccountCalendarStateService { get; }
|
||||||
public INavigationService NavigationService { 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]
|
[ObservableProperty]
|
||||||
private int _selectedMenuItemIndex = -1;
|
private int _selectedMenuItemIndex = -1;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool isCalendarEnabled;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the display date of the calendar.
|
||||||
|
/// </summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
private DateTimeOffset _displayDate;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the highlighted range in the CalendarView and displayed date range in FlipView.
|
||||||
|
/// </summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
private DateRange highlightedDateRange;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private ObservableRangeCollection<string> dateNavigationHeaderItems = [];
|
private ObservableRangeCollection<string> dateNavigationHeaderItems = [];
|
||||||
|
|
||||||
@@ -70,47 +64,28 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
|
|
||||||
public bool IsVerticalCalendar => StatePersistenceService.CalendarDisplayType == CalendarDisplayType.Month;
|
public bool IsVerticalCalendar => StatePersistenceService.CalendarDisplayType == CalendarDisplayType.Month;
|
||||||
|
|
||||||
[ObservableProperty]
|
// For updating account calendars asynchronously.
|
||||||
private bool isStoreUpdateItemVisible;
|
private SemaphoreSlim _accountCalendarUpdateSemaphoreSlim = new(1);
|
||||||
|
|
||||||
private readonly SettingsItem _settingsItem = new();
|
public CalendarAppShellViewModel(IPreferencesService preferencesService,
|
||||||
private readonly StoreUpdateMenuItem _storeUpdateMenuItem = new();
|
IStatePersistanceService statePersistanceService,
|
||||||
private readonly SemaphoreSlim _accountCalendarUpdateSemaphoreSlim = new(1);
|
IAccountService accountService,
|
||||||
private readonly CalendarPageViewModel _calendarPageViewModel;
|
ICalendarService calendarService,
|
||||||
private readonly IMailDialogService _dialogService;
|
IAccountCalendarStateService accountCalendarStateService,
|
||||||
private readonly IStoreUpdateService _storeUpdateService;
|
INavigationService navigationService)
|
||||||
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;
|
_accountService = accountService;
|
||||||
_calendarService = calendarService;
|
_calendarService = calendarService;
|
||||||
_calendarPageViewModel = calendarPageViewModel;
|
|
||||||
_dialogService = dialogService;
|
|
||||||
_storeUpdateService = storeUpdateService;
|
|
||||||
_dateContextProvider = dateContextProvider;
|
|
||||||
|
|
||||||
_calendarPageViewModel.PropertyChanged += CalendarPageViewModelPropertyChanged;
|
AccountCalendarStateService = accountCalendarStateService;
|
||||||
AccountCalendarStateService.PropertyChanged += AccountCalendarStateServicePropertyChanged;
|
AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested;
|
||||||
|
AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged;
|
||||||
|
|
||||||
|
NavigationService = navigationService;
|
||||||
|
PreferencesService = preferencesService;
|
||||||
|
|
||||||
|
StatePersistenceService = statePersistanceService;
|
||||||
|
StatePersistenceService.StatePropertyChanged += PrefefencesChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnDispatcherAssigned()
|
protected override void OnDispatcherAssigned()
|
||||||
@@ -118,157 +93,45 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
base.OnDispatcherAssigned();
|
base.OnDispatcherAssigned();
|
||||||
|
|
||||||
AccountCalendarStateService.Dispatcher = Dispatcher;
|
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)
|
private void PrefefencesChanged(object sender, string e)
|
||||||
{
|
{
|
||||||
if (e != nameof(StatePersistenceService.CalendarDisplayType))
|
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);
|
Messenger.Send(new CalendarDisplayTypeChangedMessage(StatePersistenceService.CalendarDisplayType));
|
||||||
|
|
||||||
|
UpdateDateNavigationHeaderItems();
|
||||||
|
|
||||||
|
// Change the calendar.
|
||||||
|
DateClicked(new CalendarViewDayClickedEventArgs(GetDisplayTypeSwitchDate()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||||
{
|
{
|
||||||
if (!_hasRegisteredPersistentRecipients)
|
base.OnNavigatedTo(mode, parameters);
|
||||||
|
|
||||||
|
// Account list may have changed while this shell was inactive.
|
||||||
|
if (mode == NavigationMode.Back)
|
||||||
{
|
{
|
||||||
RegisterRecipients();
|
await InitializeAccountCalendarsAsync();
|
||||||
_hasRegisteredPersistentRecipients = true;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
AttachRuntimeSubscriptions();
|
|
||||||
|
|
||||||
var activationContext = parameters as ShellModeActivationContext;
|
|
||||||
var shouldRunStartupFlows = activationContext?.IsInitialActivation ?? true;
|
|
||||||
var navigationArgs = activationContext?.Parameter as CalendarPageNavigationArgs;
|
|
||||||
|
|
||||||
PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
|
|
||||||
PreferencesService.PreferenceChanged += PreferencesServiceChanged;
|
|
||||||
|
|
||||||
await RefreshFooterItemsAsync(mode == NavigationMode.New);
|
|
||||||
UpdateDateNavigationHeaderItems();
|
UpdateDateNavigationHeaderItems();
|
||||||
|
|
||||||
await InitializeAccountCalendarsAsync();
|
await InitializeAccountCalendarsAsync();
|
||||||
ValidateConfiguredNewEventCalendar();
|
|
||||||
|
|
||||||
if (navigationArgs != null)
|
TodayClicked();
|
||||||
{
|
|
||||||
NavigationService.Navigate(WinoPage.CalendarPage, navigationArgs);
|
|
||||||
}
|
|
||||||
else if (shouldRunStartupFlows || _calendarPageViewModel.CurrentVisibleRange == null)
|
|
||||||
{
|
|
||||||
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)
|
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
|
try
|
||||||
{
|
{
|
||||||
await _accountCalendarUpdateSemaphoreSlim.WaitAsync();
|
await _accountCalendarUpdateSemaphoreSlim.WaitAsync();
|
||||||
@@ -299,11 +162,16 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
|
|
||||||
foreach (var account in accounts)
|
foreach (var account in accounts)
|
||||||
{
|
{
|
||||||
if (!GroupedAccountCalendarViewModel.SupportsCalendar(account))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var accountCalendars = await _calendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false);
|
var accountCalendars = await _calendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false);
|
||||||
var calendarViewModels = accountCalendars.Select(calendar => new AccountCalendarViewModel(account, calendar)).ToList();
|
var calendarViewModels = new List<AccountCalendarViewModel>();
|
||||||
|
|
||||||
|
foreach (var calendar in accountCalendars)
|
||||||
|
{
|
||||||
|
var calendarViewModel = new AccountCalendarViewModel(account, calendar);
|
||||||
|
|
||||||
|
calendarViewModels.Add(calendarViewModel);
|
||||||
|
}
|
||||||
|
|
||||||
var groupedAccountCalendarViewModel = new GroupedAccountCalendarViewModel(account, calendarViewModels);
|
var groupedAccountCalendarViewModel = new GroupedAccountCalendarViewModel(account, calendarViewModels);
|
||||||
|
|
||||||
await Dispatcher.ExecuteOnUIThread(() =>
|
await Dispatcher.ExecuteOnUIThread(() =>
|
||||||
@@ -313,154 +181,121 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void NavigateCalendarDate(DateTime date)
|
|
||||||
{
|
|
||||||
_navigationDate = date.Date;
|
|
||||||
ForceNavigateCalendarDate();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ForceNavigateCalendarDate()
|
private void ForceNavigateCalendarDate()
|
||||||
{
|
{
|
||||||
var args = new CalendarPageNavigationArgs
|
if (SelectedMenuItemIndex == -1)
|
||||||
{
|
{
|
||||||
NavigationDate = _navigationDate ?? DateTime.Now.Date
|
var args = new CalendarPageNavigationArgs()
|
||||||
};
|
|
||||||
|
|
||||||
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,
|
NavigationDate = _navigationDate ?? DateTime.Now.Date
|
||||||
Type = CalendarSynchronizationType.Strict
|
};
|
||||||
}));
|
|
||||||
|
// 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.CalendarEvents
|
||||||
|
});
|
||||||
|
|
||||||
|
Messenger.Send(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When calendar type switches, we need to navigate to the most ideal date.
|
||||||
|
/// This method returns that date.
|
||||||
|
/// </summary>
|
||||||
private DateTime GetDisplayTypeSwitchDate()
|
private DateTime GetDisplayTypeSwitchDate()
|
||||||
{
|
{
|
||||||
var today = _dateContextProvider.GetToday();
|
|
||||||
var settings = PreferencesService.GetCurrentCalendarSettings();
|
var settings = PreferencesService.GetCurrentCalendarSettings();
|
||||||
var referenceRange = CurrentVisibleRange
|
switch (StatePersistenceService.CalendarDisplayType)
|
||||||
?? CalendarRangeResolver.Resolve(new CalendarDisplayRequest(StatePersistenceService.CalendarDisplayType, today), settings, today);
|
{
|
||||||
var targetRange = CalendarRangeResolver.ChangeDisplayType(referenceRange, StatePersistenceService.CalendarDisplayType, settings, today);
|
case CalendarDisplayType.Day:
|
||||||
return targetRange.AnchorDate.ToDateTime(TimeOnly.MinValue);
|
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;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DateTime.Today.Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DateTime? _navigationDate;
|
||||||
|
private readonly IAccountService _accountService;
|
||||||
|
private readonly ICalendarService _calendarService;
|
||||||
|
|
||||||
|
#region Commands
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void TodayClicked()
|
private void TodayClicked()
|
||||||
{
|
{
|
||||||
NavigateCalendarDate(_dateContextProvider.GetToday().ToDateTime(TimeOnly.MinValue));
|
_navigationDate = DateTime.Now.Date;
|
||||||
|
|
||||||
|
ForceNavigateCalendarDate();
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void PreviousDateRange()
|
public void ManageAccounts() => NavigationService.Navigate(WinoPage.AccountManagementPage);
|
||||||
{
|
|
||||||
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]
|
[RelayCommand]
|
||||||
private void DateClicked(CalendarViewDayClickedEventArgs clickedDateArgs)
|
private void DateClicked(CalendarViewDayClickedEventArgs clickedDateArgs)
|
||||||
=> NavigateCalendarDate(clickedDateArgs.ClickedDate);
|
{
|
||||||
|
_navigationDate = clickedDateArgs.ClickedDate;
|
||||||
|
|
||||||
|
ForceNavigateCalendarDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
protected override void RegisterRecipients()
|
protected override void RegisterRecipients()
|
||||||
{
|
{
|
||||||
@@ -468,6 +303,9 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
|
|
||||||
UnregisterRecipients();
|
UnregisterRecipients();
|
||||||
|
|
||||||
|
Messenger.Register<VisibleDateRangeChangedMessage>(this);
|
||||||
|
Messenger.Register<CalendarEnableStatusChangedMessage>(this);
|
||||||
|
Messenger.Register<NavigateManageAccountsRequested>(this);
|
||||||
Messenger.Register<CalendarDisplayTypeChangedMessage>(this);
|
Messenger.Register<CalendarDisplayTypeChangedMessage>(this);
|
||||||
Messenger.Register<AccountRemovedMessage>(this);
|
Messenger.Register<AccountRemovedMessage>(this);
|
||||||
}
|
}
|
||||||
@@ -476,17 +314,99 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
{
|
{
|
||||||
base.UnregisterRecipients();
|
base.UnregisterRecipients();
|
||||||
|
|
||||||
|
Messenger.Unregister<VisibleDateRangeChangedMessage>(this);
|
||||||
|
Messenger.Unregister<CalendarEnableStatusChangedMessage>(this);
|
||||||
|
Messenger.Unregister<NavigateManageAccountsRequested>(this);
|
||||||
Messenger.Unregister<CalendarDisplayTypeChangedMessage>(this);
|
Messenger.Unregister<CalendarDisplayTypeChangedMessage>(this);
|
||||||
Messenger.Unregister<AccountRemovedMessage>(this);
|
Messenger.Unregister<AccountRemovedMessage>(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Receive(VisibleDateRangeChangedMessage message) => HighlightedDateRange = message.DateRange;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the header navigation items based on visible date range and calendar type.
|
||||||
|
/// </summary>
|
||||||
private void UpdateDateNavigationHeaderItems()
|
private void UpdateDateNavigationHeaderItems()
|
||||||
{
|
{
|
||||||
var headerText = VisibleDateRangeText;
|
var settings = PreferencesService.GetCurrentCalendarSettings();
|
||||||
DateNavigationHeaderItems.ReplaceRange(string.IsNullOrWhiteSpace(headerText) ? [] : [headerText]);
|
var cultureInfo = settings.CultureInfo ?? CultureInfo.CurrentUICulture;
|
||||||
|
|
||||||
|
var visibleRange = HighlightedDateRange ?? new DateRange(DateTime.Today, DateTime.Today.AddDays(1));
|
||||||
|
var headerText = GetHeaderText(visibleRange, cultureInfo);
|
||||||
|
|
||||||
|
DateNavigationHeaderItems.ReplaceRange([headerText]);
|
||||||
SelectedDateNavigationHeaderIndex = DateNavigationHeaderItems.Count > 0 ? 0 : -1;
|
SelectedDateNavigationHeaderIndex = DateNavigationHeaderItems.Count > 0 ? 0 : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GetHeaderText(DateRange visibleRange, CultureInfo cultureInfo)
|
||||||
|
{
|
||||||
|
var startDate = visibleRange.StartDate.Date;
|
||||||
|
var endDate = visibleRange.EndDate.Date > startDate ? visibleRange.EndDate.Date.AddDays(-1) : startDate;
|
||||||
|
|
||||||
|
switch (StatePersistenceService.CalendarDisplayType)
|
||||||
|
{
|
||||||
|
case CalendarDisplayType.Day:
|
||||||
|
return startDate.ToString("MMMM d, dddd", cultureInfo);
|
||||||
|
case CalendarDisplayType.Week:
|
||||||
|
case CalendarDisplayType.WorkWeek:
|
||||||
|
if (startDate.Month == endDate.Month && startDate.Year == endDate.Year)
|
||||||
|
{
|
||||||
|
return $"{startDate.ToString("MMMM d", cultureInfo)} - {endDate.ToString("%d", cultureInfo)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{startDate.ToString("MMMM d", cultureInfo)} - {endDate.ToString("MMMM d", cultureInfo)}";
|
||||||
|
case CalendarDisplayType.Month:
|
||||||
|
return GetDominantMonthHeaderText(startDate, endDate, cultureInfo);
|
||||||
|
default:
|
||||||
|
return startDate.ToString("d", cultureInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetDominantMonthHeaderText(DateTime startDate, DateTime endDate, CultureInfo cultureInfo)
|
||||||
|
{
|
||||||
|
if (endDate < startDate)
|
||||||
|
{
|
||||||
|
endDate = startDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
var monthDayCounts = new Dictionary<(int Year, int Month), int>();
|
||||||
|
|
||||||
|
for (var day = startDate; day <= endDate; day = day.AddDays(1))
|
||||||
|
{
|
||||||
|
var key = (day.Year, day.Month);
|
||||||
|
|
||||||
|
if (monthDayCounts.TryGetValue(key, out var count))
|
||||||
|
{
|
||||||
|
monthDayCounts[key] = count + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
monthDayCounts[key] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var dominantKey = (Year: startDate.Year, Month: startDate.Month);
|
||||||
|
var dominantCount = -1;
|
||||||
|
|
||||||
|
foreach (var pair in monthDayCounts)
|
||||||
|
{
|
||||||
|
if (pair.Value > dominantCount)
|
||||||
|
{
|
||||||
|
dominantCount = pair.Value;
|
||||||
|
dominantKey = pair.Key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DateTime(dominantKey.Year, dominantKey.Month, 1).ToString("Y", cultureInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnHighlightedDateRangeChanged(DateRange value) => UpdateDateNavigationHeaderItems();
|
||||||
|
|
||||||
|
public async void Receive(CalendarEnableStatusChangedMessage message)
|
||||||
|
=> await ExecuteUIThread(() => IsCalendarEnabled = message.IsEnabled);
|
||||||
|
|
||||||
|
public void Receive(NavigateManageAccountsRequested message) => SelectedMenuItemIndex = 1;
|
||||||
|
|
||||||
public void Receive(CalendarDisplayTypeChangedMessage message)
|
public void Receive(CalendarDisplayTypeChangedMessage message)
|
||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(IsVerticalCalendar));
|
OnPropertyChanged(nameof(IsVerticalCalendar));
|
||||||
@@ -494,69 +414,5 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async void Receive(AccountRemovedMessage message)
|
public async void Receive(AccountRemovedMessage message)
|
||||||
{
|
=> await InitializeAccountCalendarsAsync();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,763 +0,0 @@
|
|||||||
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<Task<string>> GetHtmlNotesAsync { get; set; }
|
|
||||||
|
|
||||||
public ObservableCollection<AccountCalendarViewModel> AvailableCalendars { get; } = [];
|
|
||||||
public ObservableCollection<GroupedAccountCalendarViewModel> AvailableCalendarGroups { get; } = [];
|
|
||||||
public ObservableCollection<CalendarComposeAttendeeViewModel> Attendees { get; } = [];
|
|
||||||
public ObservableCollection<CalendarComposeAttachmentViewModel> Attachments { get; } = [];
|
|
||||||
public ObservableCollection<ShowAsOption> ShowAsOptions { get; } = [];
|
|
||||||
public ObservableCollection<ReminderOption> ReminderOptions { get; } = [];
|
|
||||||
public ObservableCollection<int> RecurrenceIntervalOptions { get; } = [];
|
|
||||||
public ObservableCollection<CalendarComposeFrequencyOption> RecurrenceFrequencyOptions { get; } = [];
|
|
||||||
public ObservableCollection<CalendarComposeWeekdayOption> 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<List<AccountContact>> SearchContactsAsync(string queryText)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(queryText) || queryText.Length < 2)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
return await _contactService.GetAddressInformationAsync(queryText).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<CalendarComposeAttendeeViewModel> 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<AccountCalendarViewModel>();
|
|
||||||
var groupedCalendars = new List<GroupedAccountCalendarViewModel>();
|
|
||||||
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
foreach (var account in accounts)
|
|
||||||
{
|
|
||||||
if (!GroupedAccountCalendarViewModel.SupportsCalendar(account))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
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<CalendarEventComposeResult> BuildResultAsync(List<CalendarComposeAttendeeViewModel> 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<Reminder> BuildSelectedReminders()
|
|
||||||
{
|
|
||||||
if (SelectedReminderOption == null)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
return
|
|
||||||
[
|
|
||||||
new Reminder
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
CalendarItemId = Guid.Empty,
|
|
||||||
DurationInSeconds = SelectedReminderOption.Minutes * 60L,
|
|
||||||
ReminderType = CalendarItemReminderType.Popup
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<CalendarEventAttendee> BuildAttendees(IEnumerable<CalendarComposeAttendeeViewModel> 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<string>
|
|
||||||
{
|
|
||||||
$"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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,62 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
using Wino.Core.Domain;
|
||||||
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
|
using Wino.Core.Domain.Enums;
|
||||||
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Core.Domain.Translations;
|
||||||
|
using Wino.Core.ViewModels;
|
||||||
|
using Wino.Messaging.Client.Calendar;
|
||||||
|
using Wino.Messaging.Client.Navigation;
|
||||||
|
|
||||||
|
namespace Wino.Calendar.ViewModels;
|
||||||
|
|
||||||
|
public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
|
||||||
|
{
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial double CellHourHeight { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial int SelectedFirstDayOfWeekIndex { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial bool Is24HourHeaders { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial TimeSpan WorkingHourStart { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial TimeSpan WorkingHourEnd { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial List<string> DayNames { get; set; } = [];
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial int WorkingDayStartIndex { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial int WorkingDayEndIndex { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial List<string> ReminderOptions { get; set; } = [];
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial int SelectedDefaultReminderIndex { get; set; }
|
||||||
|
|
||||||
|
public IPreferencesService PreferencesService { get; }
|
||||||
|
private readonly ICalendarService _calendarService;
|
||||||
|
private readonly IAccountService _accountService;
|
||||||
|
|
||||||
|
public ObservableCollection<MailAccount> Accounts { get; } = new ObservableCollection<MailAccount>();
|
||||||
|
|
||||||
|
private readonly bool _isLoaded = false;
|
||||||
|
|
||||||
|
public CalendarSettingsPageViewModel(IPreferencesService preferencesService, ICalendarService calendarService, IAccountService accountService)
|
||||||
|
{
|
||||||
|
PreferencesService = preferencesService;
|
||||||
|
_calendarService = calendarService;
|
||||||
|
_accountService = accountService;
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
// Initialize reminder options
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set selected index based on current default reminder setting
|
||||||
|
if (preferencesService.DefaultReminderDurationInSeconds == 0)
|
||||||
|
{
|
||||||
|
SelectedDefaultReminderIndex = 0; // None
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var minutes = (int)(preferencesService.DefaultReminderDurationInSeconds / 60);
|
||||||
|
var index = Array.IndexOf(predefinedMinutes, minutes);
|
||||||
|
SelectedDefaultReminderIndex = index >= 0 ? index + 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isLoaded = true;
|
||||||
|
|
||||||
|
// Load accounts with calendar support
|
||||||
|
LoadAccountsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void LoadAccountsAsync()
|
||||||
|
{
|
||||||
|
var accounts = await _accountService.GetAccountsAsync();
|
||||||
|
|
||||||
|
await Dispatcher.ExecuteOnUIThread(() =>
|
||||||
|
{
|
||||||
|
Accounts.Clear();
|
||||||
|
foreach (var account in accounts)
|
||||||
|
{
|
||||||
|
Accounts.Add(account);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void NavigateToAccountSettings(MailAccount account)
|
||||||
|
{
|
||||||
|
if (account == null) return;
|
||||||
|
|
||||||
|
Messenger.Send(new BreadcrumbNavigationRequested(
|
||||||
|
string.Format(Translator.CalendarAccountSettings_Description, account.Name),
|
||||||
|
WinoPage.CalendarAccountSettingsPage,
|
||||||
|
account.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
partial void OnSelectedDefaultReminderIndexChanged(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;
|
||||||
|
|
||||||
|
// Save default reminder setting
|
||||||
|
if (SelectedDefaultReminderIndex == 0)
|
||||||
|
{
|
||||||
|
PreferencesService.DefaultReminderDurationInSeconds = 0; // None
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var predefinedMinutes = _calendarService.GetPredefinedReminderMinutes();
|
||||||
|
var minutes = predefinedMinutes[SelectedDefaultReminderIndex - 1];
|
||||||
|
PreferencesService.DefaultReminderDurationInSeconds = minutes * 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
Messenger.Send(new CalendarSettingsUpdatedMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
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<string> DayNames { get; } = [];
|
|
||||||
public ObservableCollection<string> ReminderOptions { get; } = [];
|
|
||||||
public ObservableCollection<string> SnoozeOptions { get; } = [];
|
|
||||||
public ObservableCollection<CalendarNewEventBehaviorOption> NewEventBehaviorOptions { get; } = [];
|
|
||||||
public ObservableCollection<AccountCalendarViewModel> AvailableNewEventCalendars { get; } = [];
|
|
||||||
public ObservableCollection<string> 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<AccountCalendarViewModel>();
|
|
||||||
|
|
||||||
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; }
|
|
||||||
}
|
|
||||||
@@ -55,12 +55,6 @@ public partial class AccountCalendarViewModel : ObservableObject, IAccountCalend
|
|||||||
set => SetProperty(AccountCalendar.IsPrimary, value, AccountCalendar, (u, i) => u.IsPrimary = i);
|
set => SetProperty(AccountCalendar.IsPrimary, value, AccountCalendar, (u, i) => u.IsPrimary = i);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsReadOnly
|
|
||||||
{
|
|
||||||
get => AccountCalendar.IsReadOnly;
|
|
||||||
set => SetProperty(AccountCalendar.IsReadOnly, value, AccountCalendar, (u, i) => u.IsReadOnly = i);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsSynchronizationEnabled
|
public bool IsSynchronizationEnabled
|
||||||
{
|
{
|
||||||
get => AccountCalendar.IsSynchronizationEnabled;
|
get => AccountCalendar.IsSynchronizationEnabled;
|
||||||
@@ -85,9 +79,5 @@ public partial class AccountCalendarViewModel : ObservableObject, IAccountCalend
|
|||||||
set => SetProperty(AccountCalendar.DefaultShowAs, value, AccountCalendar, (u, s) => u.DefaultShowAs = s);
|
set => SetProperty(AccountCalendar.DefaultShowAs, value, AccountCalendar, (u, s) => u.DefaultShowAs = s);
|
||||||
}
|
}
|
||||||
public Guid Id { get => ((IAccountCalendar)AccountCalendar).Id; set => ((IAccountCalendar)AccountCalendar).Id = value; }
|
public Guid Id { get => ((IAccountCalendar)AccountCalendar).Id; set => ((IAccountCalendar)AccountCalendar).Id = value; }
|
||||||
public MailAccount MailAccount
|
public MailAccount MailAccount { get => MailAccount; set => MailAccount = value; }
|
||||||
{
|
|
||||||
get => AccountCalendar.MailAccount ?? Account;
|
|
||||||
set => AccountCalendar.MailAccount = value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
@@ -33,10 +33,8 @@ public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, IC
|
|||||||
}
|
}
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
// All-day events use floating dates and should not shift across timezones.
|
// When setting from UI (in local time), convert to event's timezone for storage.
|
||||||
CalendarItem.StartDate = CalendarItem.IsAllDayEvent
|
CalendarItem.StartDate = value.ToTimeZoneFromLocal(CalendarItem.StartTimeZone);
|
||||||
? value.Date
|
|
||||||
: value.ToTimeZoneFromLocal(CalendarItem.StartTimeZone);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +70,6 @@ public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, IC
|
|||||||
public bool IsRecurringEvent => CalendarItem.IsRecurringEvent;
|
public bool IsRecurringEvent => CalendarItem.IsRecurringEvent;
|
||||||
public bool IsRecurringChild => CalendarItem.IsRecurringChild;
|
public bool IsRecurringChild => CalendarItem.IsRecurringChild;
|
||||||
public bool IsRecurringParent => CalendarItem.IsRecurringParent;
|
public bool IsRecurringParent => CalendarItem.IsRecurringParent;
|
||||||
public bool CanDragDrop => CalendarItem.CanChangeStartAndEndDate;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial bool IsSelected { get; set; }
|
public partial bool IsSelected { get; set; }
|
||||||
@@ -158,7 +155,6 @@ public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, IC
|
|||||||
OnPropertyChanged(nameof(IsRecurringEvent));
|
OnPropertyChanged(nameof(IsRecurringEvent));
|
||||||
OnPropertyChanged(nameof(IsRecurringChild));
|
OnPropertyChanged(nameof(IsRecurringChild));
|
||||||
OnPropertyChanged(nameof(IsRecurringParent));
|
OnPropertyChanged(nameof(IsRecurringParent));
|
||||||
OnPropertyChanged(nameof(CanDragDrop));
|
|
||||||
OnPropertyChanged(nameof(AssignedCalendar));
|
OnPropertyChanged(nameof(AssignedCalendar));
|
||||||
OnPropertyChanged(nameof(DisplayTitle));
|
OnPropertyChanged(nameof(DisplayTitle));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Models.Synchronization;
|
|
||||||
|
|
||||||
namespace Wino.Calendar.ViewModels.Data;
|
namespace Wino.Calendar.ViewModels.Data;
|
||||||
|
|
||||||
@@ -17,14 +16,10 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
|
|||||||
public MailAccount Account { get; }
|
public MailAccount Account { get; }
|
||||||
public ObservableCollection<AccountCalendarViewModel> AccountCalendars { get; }
|
public ObservableCollection<AccountCalendarViewModel> AccountCalendars { get; }
|
||||||
|
|
||||||
public static bool SupportsCalendar(MailAccount account)
|
|
||||||
=> account?.IsCalendarAccessGranted == true;
|
|
||||||
|
|
||||||
public GroupedAccountCalendarViewModel(MailAccount account, IEnumerable<AccountCalendarViewModel> calendarViewModels)
|
public GroupedAccountCalendarViewModel(MailAccount account, IEnumerable<AccountCalendarViewModel> calendarViewModels)
|
||||||
{
|
{
|
||||||
Account = account;
|
Account = account;
|
||||||
AccountCalendars = new ObservableCollection<AccountCalendarViewModel>(calendarViewModels);
|
AccountCalendars = new ObservableCollection<AccountCalendarViewModel>(calendarViewModels);
|
||||||
AccountColorHex = account.AccountColorHex;
|
|
||||||
|
|
||||||
ManageIsCheckedState();
|
ManageIsCheckedState();
|
||||||
|
|
||||||
@@ -36,7 +31,7 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
|
|||||||
AccountCalendars.CollectionChanged += CalendarListUpdated;
|
AccountCalendars.CollectionChanged += CalendarListUpdated;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CalendarListUpdated(object sender, NotifyCollectionChangedEventArgs e)
|
private void CalendarListUpdated(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Action == NotifyCollectionChangedAction.Add)
|
if (e.Action == NotifyCollectionChangedAction.Add)
|
||||||
{
|
{
|
||||||
@@ -63,11 +58,13 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
|
|||||||
|
|
||||||
private void CalendarPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
private void CalendarPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (sender is AccountCalendarViewModel viewModel &&
|
if (sender is AccountCalendarViewModel viewModel)
|
||||||
e.PropertyName == nameof(AccountCalendarViewModel.IsChecked))
|
|
||||||
{
|
{
|
||||||
ManageIsCheckedState();
|
if (e.PropertyName == nameof(AccountCalendarViewModel.IsChecked))
|
||||||
UpdateCalendarCheckedState(viewModel, viewModel.IsChecked, true);
|
{
|
||||||
|
ManageIsCheckedState();
|
||||||
|
UpdateCalendarCheckedState(viewModel, viewModel.IsChecked, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,59 +74,11 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial bool? IsCheckedState { get; set; } = true;
|
public partial bool? IsCheckedState { get; set; } = true;
|
||||||
|
|
||||||
[ObservableProperty]
|
private bool _isExternalPropChangeBlocked = false;
|
||||||
public partial string AccountColorHex { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
[NotifyPropertyChangedFor(nameof(CanSynchronize), nameof(IsSynchronizationProgressVisible), nameof(IsProgressIndeterminate))]
|
|
||||||
public partial bool IsSynchronizationInProgress { get; set; }
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
[NotifyPropertyChangedFor(nameof(SynchronizationProgress), nameof(SynchronizationProgressValue), nameof(IsProgressIndeterminate))]
|
|
||||||
public partial int TotalItemsToSync { get; set; }
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
[NotifyPropertyChangedFor(nameof(SynchronizationProgress), nameof(SynchronizationProgressValue), nameof(IsProgressIndeterminate))]
|
|
||||||
public partial int RemainingItemsToSync { get; set; }
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
public partial string SynchronizationStatus { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
public bool CanSynchronize => !IsSynchronizationInProgress;
|
|
||||||
public bool IsSynchronizationProgressVisible => IsSynchronizationInProgress;
|
|
||||||
public bool IsProgressIndeterminate => IsSynchronizationInProgress && TotalItemsToSync <= 0;
|
|
||||||
public string AccountAddressDisplay => string.IsNullOrWhiteSpace(Account?.Address) ? string.Empty : $" ({Account.Address})";
|
|
||||||
|
|
||||||
public double SynchronizationProgress
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (TotalItemsToSync <= 0)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
return ((double)(TotalItemsToSync - RemainingItemsToSync) / TotalItemsToSync) * 100;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public double SynchronizationProgressValue => SynchronizationProgress;
|
|
||||||
|
|
||||||
private bool _isExternalPropChangeBlocked;
|
|
||||||
|
|
||||||
public void ApplySynchronizationProgress(AccountSynchronizationProgress progress)
|
|
||||||
{
|
|
||||||
if (progress == null || progress.AccountId != Account.Id)
|
|
||||||
return;
|
|
||||||
|
|
||||||
IsSynchronizationInProgress = progress.IsInProgress;
|
|
||||||
TotalItemsToSync = progress.TotalUnits;
|
|
||||||
RemainingItemsToSync = progress.RemainingUnits;
|
|
||||||
SynchronizationStatus = progress.Status ?? string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ManageIsCheckedState()
|
private void ManageIsCheckedState()
|
||||||
{
|
{
|
||||||
if (_isExternalPropChangeBlocked)
|
if (_isExternalPropChangeBlocked) return;
|
||||||
return;
|
|
||||||
|
|
||||||
_isExternalPropChangeBlocked = true;
|
_isExternalPropChangeBlocked = true;
|
||||||
|
|
||||||
@@ -151,13 +100,17 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
|
|||||||
|
|
||||||
partial void OnIsCheckedStateChanged(bool? oldValue, bool? newValue)
|
partial void OnIsCheckedStateChanged(bool? oldValue, bool? newValue)
|
||||||
{
|
{
|
||||||
if (_isExternalPropChangeBlocked)
|
if (_isExternalPropChangeBlocked) return;
|
||||||
return;
|
|
||||||
|
// Update is triggered by user on the three-state checkbox.
|
||||||
|
// We should not report all changes one by one.
|
||||||
|
|
||||||
_isExternalPropChangeBlocked = true;
|
_isExternalPropChangeBlocked = true;
|
||||||
|
|
||||||
if (newValue == null)
|
if (newValue == null)
|
||||||
{
|
{
|
||||||
|
// Only primary calendars must be checked.
|
||||||
|
|
||||||
foreach (var calendar in AccountCalendars)
|
foreach (var calendar in AccountCalendars)
|
||||||
{
|
{
|
||||||
UpdateCalendarCheckedState(calendar, calendar.IsPrimary);
|
UpdateCalendarCheckedState(calendar, calendar.IsPrimary);
|
||||||
@@ -172,6 +125,7 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
|
|
||||||
_isExternalPropChangeBlocked = false;
|
_isExternalPropChangeBlocked = false;
|
||||||
|
|
||||||
CollectiveSelectionStateChanged?.Invoke(this, EventArgs.Empty);
|
CollectiveSelectionStateChanged?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,29 +133,13 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
var currentValue = accountCalendarViewModel.IsChecked;
|
var currentValue = accountCalendarViewModel.IsChecked;
|
||||||
|
|
||||||
if (currentValue == newValue && !ignoreValueCheck)
|
if (currentValue == newValue && !ignoreValueCheck) return;
|
||||||
return;
|
|
||||||
|
|
||||||
accountCalendarViewModel.IsChecked = newValue;
|
accountCalendarViewModel.IsChecked = newValue;
|
||||||
|
|
||||||
if (_isExternalPropChangeBlocked)
|
// No need to report.
|
||||||
return;
|
if (_isExternalPropChangeBlocked == true) return;
|
||||||
|
|
||||||
CalendarSelectionStateChanged?.Invoke(this, accountCalendarViewModel);
|
CalendarSelectionStateChanged?.Invoke(this, accountCalendarViewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
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));
|
|
||||||
OnPropertyChanged(nameof(AccountAddressDisplay));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,9 @@ using Serilog;
|
|||||||
using Wino.Calendar.ViewModels.Data;
|
using Wino.Calendar.ViewModels.Data;
|
||||||
using Wino.Core.Domain;
|
using Wino.Core.Domain;
|
||||||
using Wino.Core.Domain.Entities.Calendar;
|
using Wino.Core.Domain.Entities.Calendar;
|
||||||
using Wino.Core.Domain.Extensions;
|
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
using Wino.Core.Domain.Models.Calendar;
|
using Wino.Core.Domain.Models.Calendar;
|
||||||
using Wino.Core.Domain.Models;
|
|
||||||
using Wino.Core.Domain.Models.Navigation;
|
using Wino.Core.Domain.Models.Navigation;
|
||||||
using Wino.Core.Services;
|
using Wino.Core.Services;
|
||||||
using Wino.Core.ViewModels;
|
using Wino.Core.ViewModels;
|
||||||
@@ -33,8 +31,6 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
|||||||
private readonly IWinoRequestDelegator _winoRequestDelegator;
|
private readonly IWinoRequestDelegator _winoRequestDelegator;
|
||||||
private readonly INavigationService _navigationService;
|
private readonly INavigationService _navigationService;
|
||||||
private readonly IUnderlyingThemeService _underlyingThemeService;
|
private readonly IUnderlyingThemeService _underlyingThemeService;
|
||||||
private readonly INotificationBuilder _notificationBuilder;
|
|
||||||
private readonly IContactService _contactService;
|
|
||||||
|
|
||||||
public CalendarSettings CurrentSettings { get; }
|
public CalendarSettings CurrentSettings { get; }
|
||||||
public INativeAppService NativeAppService => _nativeAppService;
|
public INativeAppService NativeAppService => _nativeAppService;
|
||||||
@@ -147,9 +143,7 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
|||||||
IMailDialogService dialogService,
|
IMailDialogService dialogService,
|
||||||
IWinoRequestDelegator winoRequestDelegator,
|
IWinoRequestDelegator winoRequestDelegator,
|
||||||
INavigationService navigationService,
|
INavigationService navigationService,
|
||||||
INotificationBuilder notificationBuilder,
|
IUnderlyingThemeService underlyingThemeService)
|
||||||
IUnderlyingThemeService underlyingThemeService,
|
|
||||||
IContactService contactService)
|
|
||||||
{
|
{
|
||||||
_calendarService = calendarService;
|
_calendarService = calendarService;
|
||||||
_nativeAppService = nativeAppService;
|
_nativeAppService = nativeAppService;
|
||||||
@@ -158,23 +152,22 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
|||||||
_winoRequestDelegator = winoRequestDelegator;
|
_winoRequestDelegator = winoRequestDelegator;
|
||||||
_navigationService = navigationService;
|
_navigationService = navigationService;
|
||||||
_underlyingThemeService = underlyingThemeService;
|
_underlyingThemeService = underlyingThemeService;
|
||||||
_notificationBuilder = notificationBuilder;
|
|
||||||
_contactService = contactService;
|
|
||||||
|
|
||||||
CurrentSettings = _preferencesService.GetCurrentCalendarSettings();
|
CurrentSettings = _preferencesService.GetCurrentCalendarSettings();
|
||||||
IsDarkWebviewRenderer = _underlyingThemeService.IsUnderlyingThemeDark();
|
IsDarkWebviewRenderer = _underlyingThemeService.IsUnderlyingThemeDark();
|
||||||
|
|
||||||
foreach (var showAs in CalendarItemActionOptions.ShowAsOptions)
|
// Initialize Show As options
|
||||||
{
|
ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Free));
|
||||||
ShowAsOptions.Add(new ShowAsOption(showAs));
|
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
|
||||||
|
|
||||||
SelectedShowAsOption = ShowAsOptions.FirstOrDefault(option => option.ShowAs == CalendarItemShowAs.Busy) ?? ShowAsOptions.FirstOrDefault();
|
// Initialize RSVP status options
|
||||||
|
RsvpStatusOptions.Add(new RsvpStatusOption(CalendarItemStatus.Accepted));
|
||||||
foreach (var responseStatus in CalendarItemActionOptions.ResponseOptions)
|
RsvpStatusOptions.Add(new RsvpStatusOption(CalendarItemStatus.Tentative));
|
||||||
{
|
RsvpStatusOptions.Add(new RsvpStatusOption(CalendarItemStatus.Cancelled));
|
||||||
RsvpStatusOptions.Add(new RsvpStatusOption(responseStatus));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||||
@@ -187,20 +180,20 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
|||||||
await LoadCalendarItemTargetAsync(args);
|
await LoadCalendarItemTargetAsync(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async void OnCalendarItemUpdated(CalendarItem calendarItem, EntityUpdateSource source)
|
protected override async void OnCalendarItemUpdated(CalendarItem calendarItem, CalendarItemUpdateSource source)
|
||||||
{
|
{
|
||||||
base.OnCalendarItemUpdated(calendarItem, source);
|
base.OnCalendarItemUpdated(calendarItem, source);
|
||||||
|
|
||||||
// If the current event was updated, reload it
|
// If the current event was updated, reload it
|
||||||
if (IsCurrentEventMatch(calendarItem))
|
if (CurrentEvent?.CalendarItem?.Id == calendarItem.Id || CurrentEvent?.CalendarItem.RecurringCalendarItemId == calendarItem.Id)
|
||||||
{
|
{
|
||||||
// Reflect client-side optimistic changes immediately; fallback to DB for server updates.
|
// Reflect client-side optimistic changes immediately; fallback to DB for server updates.
|
||||||
if (source == EntityUpdateSource.ClientUpdated || source == EntityUpdateSource.ClientReverted)
|
if (source == CalendarItemUpdateSource.ClientUpdated || source == CalendarItemUpdateSource.ClientReverted)
|
||||||
{
|
{
|
||||||
var previousAttendees = CurrentEvent?.Attendees?.ToList() ?? [];
|
var previousAttendees = CurrentEvent?.Attendees?.ToList() ?? [];
|
||||||
CurrentEvent = new CalendarItemViewModel(calendarItem)
|
CurrentEvent = new CalendarItemViewModel(calendarItem)
|
||||||
{
|
{
|
||||||
IsBusy = source == EntityUpdateSource.ClientUpdated
|
IsBusy = source == CalendarItemUpdateSource.ClientUpdated
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var attendee in previousAttendees)
|
foreach (var attendee in previousAttendees)
|
||||||
@@ -221,54 +214,17 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async void OnCalendarItemAdded(CalendarItem calendarItem, EntityUpdateSource source)
|
protected override void OnCalendarItemDeleted(CalendarItem calendarItem)
|
||||||
{
|
{
|
||||||
base.OnCalendarItemAdded(calendarItem, source);
|
base.OnCalendarItemDeleted(calendarItem);
|
||||||
|
|
||||||
if (!IsCurrentEventMatch(calendarItem))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (source == EntityUpdateSource.ClientUpdated || source == EntityUpdateSource.ClientReverted)
|
|
||||||
{
|
|
||||||
CurrentEvent = new CalendarItemViewModel(calendarItem)
|
|
||||||
{
|
|
||||||
IsBusy = source == EntityUpdateSource.ClientUpdated
|
|
||||||
};
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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, EntityUpdateSource source)
|
|
||||||
{
|
|
||||||
base.OnCalendarItemDeleted(calendarItem, source);
|
|
||||||
|
|
||||||
// If the current event was deleted, navigate back
|
// If the current event was deleted, navigate back
|
||||||
if (IsCurrentEventMatch(calendarItem))
|
if (CurrentEvent?.CalendarItem?.Id == calendarItem.Id || CurrentEvent?.CalendarItem.RecurringCalendarItemId == calendarItem.Id)
|
||||||
{
|
{
|
||||||
NavigateBackToCalendar(forceReload: true);
|
_navigationService.GoBack();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsCurrentEventMatch(CalendarItem calendarItem)
|
|
||||||
{
|
|
||||||
if (CurrentEvent?.CalendarItem == null || calendarItem == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var trackedLocalItemId = calendarItem.RemoteEventId.GetClientTrackingId();
|
|
||||||
|
|
||||||
return CurrentEvent.CalendarItem.Id == calendarItem.Id ||
|
|
||||||
(trackedLocalItemId.HasValue && CurrentEvent.CalendarItem.Id == trackedLocalItemId.Value) ||
|
|
||||||
CurrentEvent.CalendarItem.RecurringCalendarItemId == calendarItem.Id;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadCalendarItemTargetAsync(CalendarItemTarget target)
|
private async Task LoadCalendarItemTargetAsync(CalendarItemTarget target)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -300,36 +256,18 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
|||||||
|
|
||||||
private async Task LoadAttendeesAsync(Guid calendarItemId, CalendarItem calendarItem)
|
private async Task LoadAttendeesAsync(Guid calendarItemId, CalendarItem calendarItem)
|
||||||
{
|
{
|
||||||
|
CurrentEvent.Attendees.Clear();
|
||||||
|
|
||||||
var attendees = await _calendarService.GetAttendeesAsync(calendarItemId);
|
var attendees = await _calendarService.GetAttendeesAsync(calendarItemId);
|
||||||
|
|
||||||
// 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
|
// Separate organizer from other attendees to ensure organizer is always first
|
||||||
var organizer = attendees.FirstOrDefault(a => a.IsOrganizer);
|
var organizer = attendees.FirstOrDefault(a => a.IsOrganizer);
|
||||||
var nonOrganizerAttendees = attendees.Where(a => !a.IsOrganizer).ToList();
|
var nonOrganizerAttendees = attendees.Where(a => !a.IsOrganizer).ToList();
|
||||||
|
|
||||||
var attendeesForUi = new List<CalendarEventAttendee>();
|
|
||||||
|
|
||||||
// If the organizer is in the list, add them first
|
// If the organizer is in the list, add them first
|
||||||
if (organizer != null)
|
if (organizer != null)
|
||||||
{
|
{
|
||||||
attendeesForUi.Add(organizer);
|
CurrentEvent.Attendees.Add(organizer);
|
||||||
}
|
}
|
||||||
else if (!string.IsNullOrEmpty(calendarItem.OrganizerEmail))
|
else if (!string.IsNullOrEmpty(calendarItem.OrganizerEmail))
|
||||||
{
|
{
|
||||||
@@ -343,31 +281,14 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
|||||||
IsOrganizer = true,
|
IsOrganizer = true,
|
||||||
AttendenceStatus = AttendeeStatus.Accepted
|
AttendenceStatus = AttendeeStatus.Accepted
|
||||||
};
|
};
|
||||||
|
CurrentEvent.Attendees.Add(organizerAttendee);
|
||||||
if (contactLookup.TryGetValue(calendarItem.OrganizerEmail, out var organizerContact))
|
|
||||||
organizerAttendee.ResolvedContact = organizerContact;
|
|
||||||
|
|
||||||
attendeesForUi.Add(organizerAttendee);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add all other attendees after the organizer
|
// Add all other attendees after the organizer
|
||||||
foreach (var item in nonOrganizerAttendees)
|
foreach (var item in nonOrganizerAttendees)
|
||||||
{
|
{
|
||||||
attendeesForUi.Add(item);
|
CurrentEvent.Attendees.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)
|
private async Task LoadAttachmentsAsync(Guid calendarItemId)
|
||||||
@@ -440,11 +361,6 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
|||||||
private async Task SaveAsync()
|
private async Task SaveAsync()
|
||||||
{
|
{
|
||||||
if (CurrentEvent == null) return;
|
if (CurrentEvent == null) return;
|
||||||
if (CurrentEvent.AssignedCalendar?.IsReadOnly == true)
|
|
||||||
{
|
|
||||||
_dialogService.ShowReadOnlyCalendarMessage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -495,7 +411,7 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
|||||||
|
|
||||||
await _winoRequestDelegator.ExecuteAsync(preparationRequest);
|
await _winoRequestDelegator.ExecuteAsync(preparationRequest);
|
||||||
|
|
||||||
NavigateBackToCalendar(forceReload: true);
|
_navigationService.GoBack();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -511,11 +427,6 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
|||||||
private async Task DeleteAsync()
|
private async Task DeleteAsync()
|
||||||
{
|
{
|
||||||
if (CurrentEvent == null) return;
|
if (CurrentEvent == null) return;
|
||||||
if (CurrentEvent.AssignedCalendar?.IsReadOnly == true)
|
|
||||||
{
|
|
||||||
_dialogService.ShowReadOnlyCalendarMessage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the event is a master recurring event, ask for confirmation
|
// If the event is a master recurring event, ask for confirmation
|
||||||
if (CurrentEvent.IsRecurringParent)
|
if (CurrentEvent.IsRecurringParent)
|
||||||
@@ -537,7 +448,8 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
|||||||
|
|
||||||
await _winoRequestDelegator.ExecuteAsync(preparationRequest);
|
await _winoRequestDelegator.ExecuteAsync(preparationRequest);
|
||||||
|
|
||||||
NavigateBackToCalendar(forceReload: true);
|
// Navigate back after successful deletion
|
||||||
|
_navigationService.GoBack();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -545,28 +457,6 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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]
|
[RelayCommand]
|
||||||
private Task JoinOnlineAsync()
|
private Task JoinOnlineAsync()
|
||||||
{
|
{
|
||||||
@@ -576,24 +466,6 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
|||||||
return _nativeAppService.LaunchUriAsync(new Uri(CurrentEvent.CalendarItem.HtmlLink));
|
return _nativeAppService.LaunchUriAsync(new Uri(CurrentEvent.CalendarItem.HtmlLink));
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
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]
|
[RelayCommand]
|
||||||
private void ToggleRsvpPanel()
|
private void ToggleRsvpPanel()
|
||||||
{
|
{
|
||||||
@@ -620,11 +492,6 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
|||||||
private async Task SendRsvpResponse(AttendeeStatus status)
|
private async Task SendRsvpResponse(AttendeeStatus status)
|
||||||
{
|
{
|
||||||
if (CurrentEvent == null) return;
|
if (CurrentEvent == null) return;
|
||||||
if (CurrentEvent.AssignedCalendar?.IsReadOnly == true)
|
|
||||||
{
|
|
||||||
_dialogService.ShowReadOnlyCalendarMessage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,6 +29,5 @@ public interface IAccountCalendarStateService : INotifyPropertyChanged
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
IEnumerable<AccountCalendarViewModel> ActiveCalendars { get; }
|
IEnumerable<AccountCalendarViewModel> ActiveCalendars { get; }
|
||||||
IEnumerable<AccountCalendarViewModel> AllCalendars { get; }
|
IEnumerable<AccountCalendarViewModel> AllCalendars { get; }
|
||||||
bool IsAnySynchronizationInProgress { get; }
|
|
||||||
ReadOnlyObservableGroupedCollection<MailAccount, AccountCalendarViewModel> GroupedCalendars { get; set; }
|
ReadOnlyObservableGroupedCollection<MailAccount, AccountCalendarViewModel> GroupedCalendars { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
using Wino.Calendar.ViewModels.Data;
|
|
||||||
using Wino.Core.Domain.Models.Calendar;
|
|
||||||
|
|
||||||
namespace Wino.Calendar.ViewModels.Messages;
|
|
||||||
|
|
||||||
public sealed record CalendarItemContextActionRequestedMessage(
|
|
||||||
CalendarItemViewModel CalendarItemViewModel,
|
|
||||||
CalendarContextMenuAction Action);
|
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
using Wino.Calendar.ViewModels.Data;
|
using Wino.Calendar.ViewModels.Data;
|
||||||
|
using Wino.Core.Domain.Models.Calendar;
|
||||||
|
|
||||||
namespace Wino.Calendar.ViewModels.Messages;
|
namespace Wino.Calendar.ViewModels.Messages;
|
||||||
|
|
||||||
public class CalendarItemTappedMessage
|
public class CalendarItemTappedMessage
|
||||||
{
|
{
|
||||||
public CalendarItemTappedMessage(CalendarItemViewModel calendarItemViewModel)
|
public CalendarItemTappedMessage(CalendarItemViewModel calendarItemViewModel, CalendarDayModel clickedPeriod)
|
||||||
{
|
{
|
||||||
CalendarItemViewModel = calendarItemViewModel;
|
CalendarItemViewModel = calendarItemViewModel;
|
||||||
|
ClickedPeriod = clickedPeriod;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CalendarItemViewModel CalendarItemViewModel { get; }
|
public CalendarItemViewModel CalendarItemViewModel { get; }
|
||||||
|
public CalendarDayModel ClickedPeriod { get; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,7 @@
|
|||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="TimePeriodLibrary.NET" />
|
<PackageReference Include="TimePeriodLibrary.NET" />
|
||||||
<PackageReference Include="EmailValidation" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
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";
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Wino.Core.Domain.Models.Updates;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain;
|
namespace Wino.Core.Domain;
|
||||||
|
|
||||||
@@ -9,6 +8,4 @@ namespace Wino.Core.Domain;
|
|||||||
[JsonSerializable(typeof(int))]
|
[JsonSerializable(typeof(int))]
|
||||||
[JsonSerializable(typeof(List<string>))]
|
[JsonSerializable(typeof(List<string>))]
|
||||||
[JsonSerializable(typeof(bool))]
|
[JsonSerializable(typeof(bool))]
|
||||||
[JsonSerializable(typeof(UpdateNotes))]
|
|
||||||
[JsonSerializable(typeof(List<UpdateNoteSection>))]
|
|
||||||
public partial class BasicTypesJsonContext : JsonSerializerContext;
|
public partial class BasicTypesJsonContext : JsonSerializerContext;
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using Wino.Core.Domain.Enums;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain;
|
|
||||||
|
|
||||||
public static class CalendarItemActionOptions
|
|
||||||
{
|
|
||||||
public static IReadOnlyList<CalendarItemShowAs> ShowAsOptions { get; } =
|
|
||||||
[
|
|
||||||
CalendarItemShowAs.Free,
|
|
||||||
CalendarItemShowAs.Tentative,
|
|
||||||
CalendarItemShowAs.Busy,
|
|
||||||
CalendarItemShowAs.OutOfOffice,
|
|
||||||
CalendarItemShowAs.WorkingElsewhere
|
|
||||||
];
|
|
||||||
|
|
||||||
public static IReadOnlyList<CalendarItemStatus> ResponseOptions { get; } =
|
|
||||||
[
|
|
||||||
CalendarItemStatus.Accepted,
|
|
||||||
CalendarItemStatus.Tentative,
|
|
||||||
CalendarItemStatus.Cancelled
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
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<DayOfWeek> 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<DayOfWeek> NormalizeDays(IReadOnlyCollection<DayOfWeek> 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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -8,9 +8,6 @@ public static class CalendarReminderSnoozeOptions
|
|||||||
{
|
{
|
||||||
private static readonly int[] SupportedSnoozeMinutes = [5, 10, 15, 30];
|
private static readonly int[] SupportedSnoozeMinutes = [5, 10, 15, 30];
|
||||||
|
|
||||||
public static IReadOnlyList<int> GetSupportedSnoozeMinutes()
|
|
||||||
=> SupportedSnoozeMinutes;
|
|
||||||
|
|
||||||
public static IReadOnlyList<int> GetAllowedSnoozeMinutes(long reminderDurationInSeconds, long defaultReminderDurationInSeconds)
|
public static IReadOnlyList<int> GetAllowedSnoozeMinutes(long reminderDurationInSeconds, long defaultReminderDurationInSeconds)
|
||||||
{
|
{
|
||||||
var reminderMinutes = (int)Math.Max(0, reminderDurationInSeconds / 60);
|
var reminderMinutes = (int)Math.Max(0, reminderDurationInSeconds / 60);
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Core.Domain.Models.Calendar;
|
||||||
|
|
||||||
|
namespace Wino.Core.Domain.Collections;
|
||||||
|
|
||||||
|
public class DayRangeCollection : ObservableRangeCollection<DayRangeRenderModel>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the range of dates that are currently displayed in the collection.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Wino.Core.Domain;
|
namespace Wino.Core.Domain;
|
||||||
|
|
||||||
public static class Constants
|
public static class Constants
|
||||||
{
|
{
|
||||||
@@ -6,8 +6,6 @@ public static class Constants
|
|||||||
/// MIME header that exists in all the drafts created from Wino.
|
/// MIME header that exists in all the drafts created from Wino.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string WinoLocalDraftHeader = "X-Wino-Draft-Id";
|
public const string WinoLocalDraftHeader = "X-Wino-Draft-Id";
|
||||||
public const string DispositionNotificationToHeader = "Disposition-Notification-To";
|
|
||||||
public const string OriginalMessageIdHeader = "Original-Message-ID";
|
|
||||||
public const string LocalDraftStartPrefix = "localDraft_";
|
public const string LocalDraftStartPrefix = "localDraft_";
|
||||||
|
|
||||||
public const string CalendarEventRecurrenceRuleSeperator = "___";
|
public const string CalendarEventRecurrenceRuleSeperator = "___";
|
||||||
@@ -18,15 +16,12 @@ public static class Constants
|
|||||||
public const string ToastCalendarItemIdKey = nameof(ToastCalendarItemIdKey);
|
public const string ToastCalendarItemIdKey = nameof(ToastCalendarItemIdKey);
|
||||||
public const string ToastCalendarActionKey = nameof(ToastCalendarActionKey);
|
public const string ToastCalendarActionKey = nameof(ToastCalendarActionKey);
|
||||||
public const string ToastCalendarNavigateAction = nameof(ToastCalendarNavigateAction);
|
public const string ToastCalendarNavigateAction = nameof(ToastCalendarNavigateAction);
|
||||||
public const string ToastCalendarJoinOnlineAction = nameof(ToastCalendarJoinOnlineAction);
|
|
||||||
public const string ToastCalendarSnoozeAction = nameof(ToastCalendarSnoozeAction);
|
public const string ToastCalendarSnoozeAction = nameof(ToastCalendarSnoozeAction);
|
||||||
public const string ToastCalendarSnoozeDurationInputId = nameof(ToastCalendarSnoozeDurationInputId);
|
public const string ToastCalendarSnoozeDurationInputId = nameof(ToastCalendarSnoozeDurationInputId);
|
||||||
public const string ToastModeKey = nameof(ToastModeKey);
|
public const string ToastModeKey = nameof(ToastModeKey);
|
||||||
public const string ToastModeMail = nameof(ToastModeMail);
|
public const string ToastModeMail = nameof(ToastModeMail);
|
||||||
public const string ToastModeCalendar = nameof(ToastModeCalendar);
|
public const string ToastModeCalendar = nameof(ToastModeCalendar);
|
||||||
public const string ToastDismissActionKey = nameof(ToastDismissActionKey);
|
|
||||||
public const string ToastStoreUpdateActionKey = nameof(ToastStoreUpdateActionKey);
|
|
||||||
public const string ToastStoreUpdateActionInstall = nameof(ToastStoreUpdateActionInstall);
|
|
||||||
public const string ClientLogFile = "Client_.log";
|
public const string ClientLogFile = "Client_.log";
|
||||||
public const string ServerLogFile = "Server_.log";
|
public const string ServerLogFile = "Server_.log";
|
||||||
public const string LogArchiveFileName = "WinoLogs.zip";
|
public const string LogArchiveFileName = "WinoLogs.zip";
|
||||||
@@ -34,4 +29,3 @@ public static class Constants
|
|||||||
public const string WinoMailIdentiifer = nameof(WinoMailIdentiifer);
|
public const string WinoMailIdentiifer = nameof(WinoMailIdentiifer);
|
||||||
public const string WinoCalendarIdentifier = nameof(WinoCalendarIdentifier);
|
public const string WinoCalendarIdentifier = nameof(WinoCalendarIdentifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ public class AccountCalendar : IAccountCalendar
|
|||||||
public string SynchronizationDeltaToken { get; set; }
|
public string SynchronizationDeltaToken { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public bool IsPrimary { get; set; }
|
public bool IsPrimary { get; set; }
|
||||||
public bool IsReadOnly { get; set; }
|
|
||||||
public bool IsSynchronizationEnabled { get; set; } = true;
|
public bool IsSynchronizationEnabled { get; set; } = true;
|
||||||
public bool IsExtended { get; set; } = true;
|
public bool IsExtended { get; set; } = true;
|
||||||
public CalendarItemShowAs DefaultShowAs { get; set; } = CalendarItemShowAs.Busy;
|
public CalendarItemShowAs DefaultShowAs { get; set; } = CalendarItemShowAs.Busy;
|
||||||
@@ -26,7 +25,6 @@ public class AccountCalendar : IAccountCalendar
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string TextColorHex { get; set; }
|
public string TextColorHex { get; set; }
|
||||||
public string BackgroundColorHex { get; set; }
|
public string BackgroundColorHex { get; set; }
|
||||||
public bool IsBackgroundColorUserOverridden { get; set; }
|
|
||||||
public string TimeZone { get; set; }
|
public string TimeZone { get; set; }
|
||||||
|
|
||||||
[Ignore]
|
[Ignore]
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using SQLite;
|
using SQLite;
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Entities.Calendar;
|
namespace Wino.Core.Domain.Entities.Calendar;
|
||||||
|
|
||||||
|
// TODO: Connect to Contact store with Wino People.
|
||||||
public class CalendarEventAttendee
|
public class CalendarEventAttendee
|
||||||
{
|
{
|
||||||
[PrimaryKey]
|
[PrimaryKey]
|
||||||
@@ -16,11 +16,4 @@ public class CalendarEventAttendee
|
|||||||
public bool IsOrganizer { get; set; }
|
public bool IsOrganizer { get; set; }
|
||||||
public bool IsOptionalAttendee { get; set; }
|
public bool IsOptionalAttendee { get; set; }
|
||||||
public string Comment { get; set; }
|
public string Comment { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Resolved contact from the contact store. Populated at runtime via IContactService;
|
|
||||||
/// not persisted to the database.
|
|
||||||
/// </summary>
|
|
||||||
[Ignore]
|
|
||||||
public AccountContact ResolvedContact { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,24 +154,6 @@ public class CalendarItem : ICalendarItem
|
|||||||
[Ignore]
|
[Ignore]
|
||||||
public IAccountCalendar AssignedCalendar { get; set; }
|
public IAccountCalendar AssignedCalendar { get; set; }
|
||||||
|
|
||||||
[Ignore]
|
|
||||||
public bool CanChangeStartAndEndDate
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (IsLocked)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var accountAddress = AssignedCalendar?.MailAccount?.Address;
|
|
||||||
|
|
||||||
return string.IsNullOrWhiteSpace(OrganizerEmail) ||
|
|
||||||
string.IsNullOrWhiteSpace(accountAddress) ||
|
|
||||||
string.Equals(OrganizerEmail, accountAddress, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Id to load information related to this event (attendees, reminders, etc.).
|
/// 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 child events, if they have their own data, use their own Id.
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -2,8 +2,6 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
using SQLite;
|
using SQLite;
|
||||||
using Wino.Core.Domain;
|
|
||||||
using Wino.Core.Domain.Enums;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Entities.Mail;
|
namespace Wino.Core.Domain.Entities.Mail;
|
||||||
|
|
||||||
@@ -44,16 +42,6 @@ public class RemoteAccountAlias
|
|||||||
/// Used for Gmail only.
|
/// Used for Gmail only.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string AliasSenderName { get; set; }
|
public string AliasSenderName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether the alias was entered by the user or discovered from the provider.
|
|
||||||
/// </summary>
|
|
||||||
public AliasSource Source { get; set; } = AliasSource.Manual;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents Wino's confidence that the alias can be used for sending.
|
|
||||||
/// </summary>
|
|
||||||
public AliasSendCapability SendCapability { get; set; } = AliasSendCapability.Unknown;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MailAccountAlias : RemoteAccountAlias
|
public class MailAccountAlias : RemoteAccountAlias
|
||||||
@@ -82,28 +70,4 @@ public class MailAccountAlias : RemoteAccountAlias
|
|||||||
|
|
||||||
[Ignore]
|
[Ignore]
|
||||||
public ObservableCollection<X509Certificate2> Certificates { get; set; } = [];
|
public ObservableCollection<X509Certificate2> Certificates { get; set; } = [];
|
||||||
|
|
||||||
[Ignore]
|
|
||||||
public bool IsCapabilityConfirmed => SendCapability == AliasSendCapability.Confirmed;
|
|
||||||
|
|
||||||
[Ignore]
|
|
||||||
public bool IsCapabilityUnknown => SendCapability == AliasSendCapability.Unknown;
|
|
||||||
|
|
||||||
[Ignore]
|
|
||||||
public bool IsCapabilityDenied => SendCapability == AliasSendCapability.Denied;
|
|
||||||
|
|
||||||
[Ignore]
|
|
||||||
public string CapabilityDisplayName => SendCapability switch
|
|
||||||
{
|
|
||||||
AliasSendCapability.Confirmed => Translator.AccountAlias_Status_Confirmed,
|
|
||||||
AliasSendCapability.Denied => Translator.AccountAlias_Status_Denied,
|
|
||||||
_ => Translator.AccountAlias_Status_Unknown
|
|
||||||
};
|
|
||||||
|
|
||||||
[Ignore]
|
|
||||||
public string SourceDisplayName => Source switch
|
|
||||||
{
|
|
||||||
AliasSource.ProviderDiscovered => Translator.AccountAlias_Source_ProviderDiscovered,
|
|
||||||
_ => Translator.AccountAlias_Source_Manual
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
using System;
|
|
||||||
using SQLite;
|
|
||||||
using Wino.Core.Domain.Enums;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Entities.Mail;
|
|
||||||
|
|
||||||
public class MailCategory
|
|
||||||
{
|
|
||||||
[PrimaryKey]
|
|
||||||
public Guid Id { get; set; }
|
|
||||||
|
|
||||||
public Guid MailAccountId { get; set; }
|
|
||||||
|
|
||||||
public string RemoteId { get; set; }
|
|
||||||
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
public bool IsFavorite { get; set; }
|
|
||||||
|
|
||||||
public string BackgroundColorHex { get; set; }
|
|
||||||
|
|
||||||
public string TextColorHex { get; set; }
|
|
||||||
|
|
||||||
public MailCategorySource Source { get; set; } = MailCategorySource.Local;
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using System;
|
|
||||||
using SQLite;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Entities.Mail;
|
|
||||||
|
|
||||||
public class MailCategoryAssignment
|
|
||||||
{
|
|
||||||
[PrimaryKey]
|
|
||||||
public Guid Id { get; set; }
|
|
||||||
|
|
||||||
public Guid MailCategoryId { get; set; }
|
|
||||||
|
|
||||||
public Guid MailCopyUniqueId { get; set; }
|
|
||||||
}
|
|
||||||
@@ -92,11 +92,6 @@ public class MailCopy
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsFlagged { get; set; }
|
public bool IsFlagged { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether this mail should stay pinned to the top locally.
|
|
||||||
/// </summary>
|
|
||||||
public bool IsPinned { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// To support Outlook.
|
/// To support Outlook.
|
||||||
/// Gmail doesn't use it.
|
/// Gmail doesn't use it.
|
||||||
@@ -160,21 +155,6 @@ public class MailCopy
|
|||||||
[Ignore]
|
[Ignore]
|
||||||
public AccountContact SenderContact { get; set; }
|
public AccountContact SenderContact { get; set; }
|
||||||
|
|
||||||
[Ignore]
|
|
||||||
public bool IsReadReceiptRequested { get; set; }
|
|
||||||
|
|
||||||
[Ignore]
|
|
||||||
public SentMailReceiptStatus ReadReceiptStatus { get; set; }
|
|
||||||
|
|
||||||
[Ignore]
|
|
||||||
public DateTime? ReadReceiptAcknowledgedAtUtc { get; set; }
|
|
||||||
|
|
||||||
[Ignore]
|
|
||||||
public Guid? ReadReceiptMessageUniqueId { get; set; }
|
|
||||||
|
|
||||||
[Ignore]
|
|
||||||
public List<MailCategory> Categories { get; set; } = [];
|
|
||||||
|
|
||||||
public IEnumerable<Guid> GetContainingIds() => [UniqueId];
|
public IEnumerable<Guid> GetContainingIds() => [UniqueId];
|
||||||
public override string ToString() => $"{Subject} <-> {Id}";
|
public override string ToString() => $"{Subject} <-> {Id}";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,11 +24,6 @@ public class MailItemFolder : IMailItemFolder
|
|||||||
public bool IsSynchronizationEnabled { get; set; }
|
public bool IsSynchronizationEnabled { get; set; }
|
||||||
public bool IsHidden { get; set; }
|
public bool IsHidden { get; set; }
|
||||||
public bool ShowUnreadCount { get; set; }
|
public bool ShowUnreadCount { get; set; }
|
||||||
|
|
||||||
// User-defined ordering within its navigation section (Pinned / Categories / More).
|
|
||||||
// 0 means "no custom order set" — the folder falls back to the default sort
|
|
||||||
// (alphabetic for More, canonical SpecialFolderType order as a tiebreak for Pinned).
|
|
||||||
public int Order { get; set; }
|
|
||||||
public DateTime? LastSynchronizedDate { get; set; }
|
public DateTime? LastSynchronizedDate { get; set; }
|
||||||
|
|
||||||
// For IMAP
|
// For IMAP
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
using System;
|
|
||||||
using SQLite;
|
|
||||||
using Wino.Core.Domain.Enums;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Entities.Mail;
|
|
||||||
|
|
||||||
public class SentMailReceiptState
|
|
||||||
{
|
|
||||||
[PrimaryKey]
|
|
||||||
public Guid MailUniqueId { get; set; }
|
|
||||||
|
|
||||||
public Guid AccountId { get; set; }
|
|
||||||
|
|
||||||
public string MessageId { get; set; }
|
|
||||||
|
|
||||||
public bool IsReceiptRequested { get; set; }
|
|
||||||
|
|
||||||
public DateTime RequestedAtUtc { get; set; }
|
|
||||||
|
|
||||||
public SentMailReceiptStatus Status { get; set; }
|
|
||||||
|
|
||||||
public DateTime? AcknowledgedAtUtc { get; set; }
|
|
||||||
|
|
||||||
public Guid? ReceiptMessageUniqueId { get; set; }
|
|
||||||
}
|
|
||||||
@@ -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.
|
// TODO: This can easily evolve to Contact store, just like People app in Windows 10/11.
|
||||||
// Do it.
|
// Do it.
|
||||||
public class AccountContact : IEquatable<AccountContact>, IContactDisplayItem
|
public class AccountContact : IEquatable<AccountContact>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// E-mail address of the contact.
|
/// E-mail address of the contact.
|
||||||
@@ -25,10 +25,9 @@ public class AccountContact : IEquatable<AccountContact>, IContactDisplayItem
|
|||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// File ID for the contact picture stored on disk.
|
/// Base64 encoded profile image of the contact.
|
||||||
/// The actual file lives at {ApplicationDataFolderPath}/contacts/{ContactPictureFileId}.jpg.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid? ContactPictureFileId { get; set; }
|
public string Base64ContactPicture { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All registered accounts have their contacts registered as root.
|
/// All registered accounts have their contacts registered as root.
|
||||||
@@ -43,9 +42,6 @@ public class AccountContact : IEquatable<AccountContact>, IContactDisplayItem
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsOverridden { get; set; } = false;
|
public bool IsOverridden { get; set; } = false;
|
||||||
|
|
||||||
public string DisplayName => string.IsNullOrWhiteSpace(Name) ? Address : Name;
|
|
||||||
AccountContact IContactDisplayItem.PreviewContact => this;
|
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
public override bool Equals(object obj)
|
||||||
{
|
{
|
||||||
return Equals(obj as AccountContact);
|
return Equals(obj as AccountContact);
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
using System;
|
|
||||||
using SQLite;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Entities.Shared;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A named group of contacts that can be expanded to individual addresses during mail composition.
|
|
||||||
/// </summary>
|
|
||||||
public class ContactGroup
|
|
||||||
{
|
|
||||||
[PrimaryKey]
|
|
||||||
public Guid Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Display name of the group (e.g., "Team Alpha", "Family").</summary>
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Optional description for the group.</summary>
|
|
||||||
public string Description { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
using System;
|
|
||||||
using SQLite;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Entities.Shared;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Associates an e-mail address with a <see cref="ContactGroup"/>.
|
|
||||||
/// </summary>
|
|
||||||
public class ContactGroupMember
|
|
||||||
{
|
|
||||||
[PrimaryKey, AutoIncrement]
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Group this member belongs to.</summary>
|
|
||||||
[Indexed]
|
|
||||||
public Guid GroupId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>E-mail address of the member (FK to AccountContact.Address).</summary>
|
|
||||||
[Indexed]
|
|
||||||
public string MemberAddress { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace Wino.Core.Domain.Entities.Shared;
|
|
||||||
|
|
||||||
public interface IContactDisplayItem
|
|
||||||
{
|
|
||||||
string DisplayName { get; }
|
|
||||||
string Address { get; }
|
|
||||||
AccountContact PreviewContact { get; }
|
|
||||||
}
|
|
||||||
@@ -12,11 +12,6 @@ public class KeyboardShortcut
|
|||||||
[PrimaryKey]
|
[PrimaryKey]
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The application mode this shortcut applies to.
|
|
||||||
/// </summary>
|
|
||||||
public WinoApplicationMode Mode { get; set; } = WinoApplicationMode.Mail;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The key combination string (e.g., "D", "Delete", "F1").
|
/// The key combination string (e.g., "D", "Delete", "F1").
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -28,9 +23,9 @@ public class KeyboardShortcut
|
|||||||
public ModifierKeys ModifierKeys { get; set; }
|
public ModifierKeys ModifierKeys { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The shortcut action this shortcut triggers.
|
/// The mail operation this shortcut triggers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public KeyboardShortcutAction Action { get; set; }
|
public MailOperation MailOperation { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this shortcut is enabled.
|
/// Whether this shortcut is enabled.
|
||||||
@@ -60,6 +55,6 @@ public class KeyboardShortcut
|
|||||||
modifierText += "Win+";
|
modifierText += "Win+";
|
||||||
|
|
||||||
return modifierText + Key;
|
return modifierText + Key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,13 +78,6 @@ public class MailAccount
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public SpecialImapProvider SpecialImapProvider { get; set; }
|
public SpecialImapProvider SpecialImapProvider { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets whether mail access is granted for this account.
|
|
||||||
/// When false, mail folders, aliases, compose flows, and mail synchronization are unavailable.
|
|
||||||
/// Default is true for legacy accounts to preserve existing behavior.
|
|
||||||
/// </summary>
|
|
||||||
public bool IsMailAccessGranted { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets whether calendar access is granted for this account.
|
/// Gets or sets whether calendar access is granted for this account.
|
||||||
/// When false, synchronizers will not process EventMessages or calendar invitations.
|
/// When false, synchronizers will not process EventMessages or calendar invitations.
|
||||||
@@ -119,16 +112,6 @@ public class MailAccount
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? LastFolderStructureSyncDate { get; set; }
|
public DateTime? LastFolderStructureSyncDate { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets when the account was created in Wino.
|
|
||||||
/// </summary>
|
|
||||||
public DateTime? CreatedAt { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the timespan used for the account's initial mail synchronization.
|
|
||||||
/// </summary>
|
|
||||||
public InitialSynchronizationRange InitialSynchronizationRange { get; set; } = InitialSynchronizationRange.SixMonths;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets whether the account can perform ProfileInformation sync type.
|
/// Gets whether the account can perform ProfileInformation sync type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -137,12 +120,7 @@ public class MailAccount
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets whether the account can perform AliasInformation sync type.
|
/// Gets whether the account can perform AliasInformation sync type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsAliasSyncSupported => ProviderType == MailProviderType.Gmail || ProviderType == MailProviderType.Outlook;
|
public bool IsAliasSyncSupported => ProviderType == MailProviderType.Gmail;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets whether the account can perform category definition sync type.
|
|
||||||
/// </summary>
|
|
||||||
public bool IsCategorySyncSupported => ProviderType == MailProviderType.Outlook;
|
|
||||||
|
|
||||||
public override string ToString() => Name;
|
public override string ToString() => Name;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace Wino.Core.Domain.Enums;
|
|
||||||
|
|
||||||
public enum AccountSetupStepStatus
|
|
||||||
{
|
|
||||||
Pending,
|
|
||||||
InProgress,
|
|
||||||
Succeeded,
|
|
||||||
Failed
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Enums;
|
|
||||||
|
|
||||||
[Flags]
|
|
||||||
public enum AiActionType
|
|
||||||
{
|
|
||||||
None = 0,
|
|
||||||
Translate = 1,
|
|
||||||
Rewrite = 2,
|
|
||||||
Summarize = 4,
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace Wino.Core.Domain.Enums;
|
|
||||||
|
|
||||||
public enum AliasSendCapability
|
|
||||||
{
|
|
||||||
Unknown = 0,
|
|
||||||
Confirmed = 1,
|
|
||||||
Denied = 2
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Wino.Core.Domain.Enums;
|
|
||||||
|
|
||||||
public enum AliasSource
|
|
||||||
{
|
|
||||||
Manual = 0,
|
|
||||||
ProviderDiscovered = 1
|
|
||||||
}
|
|
||||||
@@ -16,6 +16,5 @@ public enum AppLanguage
|
|||||||
Greek,
|
Greek,
|
||||||
PortugeseBrazil,
|
PortugeseBrazil,
|
||||||
Italian,
|
Italian,
|
||||||
Romanian,
|
Romanian
|
||||||
Korean
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
namespace Wino.Core.Domain.Enums;
|
|
||||||
|
|
||||||
public enum CalendarContextMenuActionType
|
|
||||||
{
|
|
||||||
Open,
|
|
||||||
JoinOnline,
|
|
||||||
Delete,
|
|
||||||
ShowAs,
|
|
||||||
Respond
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Wino.Core.Domain.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger to load more data.
|
||||||
|
/// </summary>
|
||||||
|
public enum CalendarInitInitiative
|
||||||
|
{
|
||||||
|
User,
|
||||||
|
App
|
||||||
|
}
|
||||||
+4
-4
@@ -1,17 +1,17 @@
|
|||||||
namespace Wino.Core.Domain.Enums;
|
namespace Wino.Core.Domain.Enums;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates the source of an entity update.
|
/// Indicates the source of a calendar item update.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum EntityUpdateSource
|
public enum CalendarItemUpdateSource
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update originated from client-side optimistic UI changes (ApplyUIChanges).
|
/// Update originated from client-side UI changes (ApplyUIChanges).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
ClientUpdated,
|
ClientUpdated,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update originated from reverting client-side optimistic UI changes (RevertUIChanges).
|
/// Update originated from client-side UI revert (RevertUIChanges).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
ClientReverted,
|
ClientReverted,
|
||||||
|
|
||||||
@@ -5,7 +5,6 @@ public enum CalendarSynchronizationType
|
|||||||
ExecuteRequests, // Execute all requests in the queue.
|
ExecuteRequests, // Execute all requests in the queue.
|
||||||
CalendarMetadata, // Sync calendar metadata.
|
CalendarMetadata, // Sync calendar metadata.
|
||||||
CalendarEvents, // Sync all events for all calendars.
|
CalendarEvents, // Sync all events for all calendars.
|
||||||
Strict, // Run metadata and event synchronization in sequence.
|
|
||||||
SingleCalendar, // Sync events for only specified calendars.
|
SingleCalendar, // Sync events for only specified calendars.
|
||||||
UpdateProfile // Update profile information only.
|
UpdateProfile // Update profile information only.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
namespace Wino.Core.Domain.Enums;
|
|
||||||
|
|
||||||
public enum InitialSynchronizationRange
|
|
||||||
{
|
|
||||||
SixMonths = 0,
|
|
||||||
ThreeMonths = 1,
|
|
||||||
NineMonths = 2,
|
|
||||||
OneYear = 3,
|
|
||||||
Everything = 4
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
namespace Wino.Core.Domain.Enums;
|
|
||||||
|
|
||||||
public enum KeyboardShortcutAction
|
|
||||||
{
|
|
||||||
None,
|
|
||||||
NewMail,
|
|
||||||
ToggleReadUnread,
|
|
||||||
ToggleFlag,
|
|
||||||
ToggleArchive,
|
|
||||||
Delete,
|
|
||||||
Move,
|
|
||||||
Reply,
|
|
||||||
ReplyAll,
|
|
||||||
Send,
|
|
||||||
NewEvent
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Wino.Core.Domain.Enums;
|
|
||||||
|
|
||||||
public enum MailCategorySource
|
|
||||||
{
|
|
||||||
Local,
|
|
||||||
Outlook
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
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,
|
|
||||||
IsPinned = 1 << 14,
|
|
||||||
IsFocused = 1 << 15,
|
|
||||||
HasAttachments = 1 << 16,
|
|
||||||
ItemType = 1 << 17,
|
|
||||||
DraftId = 1 << 18,
|
|
||||||
IsDraft = 1 << 19,
|
|
||||||
FileId = 1 << 20,
|
|
||||||
AssignedFolder = 1 << 21,
|
|
||||||
AssignedAccount = 1 << 22,
|
|
||||||
SenderContact = 1 << 23,
|
|
||||||
UniqueId = 1 << 24,
|
|
||||||
ReadReceiptState = 1 << 25,
|
|
||||||
Categories = 1 << 26,
|
|
||||||
All = Id |
|
|
||||||
FolderId |
|
|
||||||
ThreadId |
|
|
||||||
MessageId |
|
|
||||||
References |
|
|
||||||
InReplyTo |
|
|
||||||
FromName |
|
|
||||||
FromAddress |
|
|
||||||
Subject |
|
|
||||||
PreviewText |
|
|
||||||
CreationDate |
|
|
||||||
Importance |
|
|
||||||
IsRead |
|
|
||||||
IsFlagged |
|
|
||||||
IsPinned |
|
|
||||||
IsFocused |
|
|
||||||
HasAttachments |
|
|
||||||
ItemType |
|
|
||||||
DraftId |
|
|
||||||
IsDraft |
|
|
||||||
FileId |
|
|
||||||
AssignedFolder |
|
|
||||||
AssignedAccount |
|
|
||||||
SenderContact |
|
|
||||||
UniqueId |
|
|
||||||
ReadReceiptState |
|
|
||||||
Categories
|
|
||||||
}
|
|
||||||
@@ -9,11 +9,9 @@ public enum MailSynchronizerOperation
|
|||||||
CreateDraft,
|
CreateDraft,
|
||||||
Send,
|
Send,
|
||||||
ChangeFlag,
|
ChangeFlag,
|
||||||
ChangeJunkState,
|
|
||||||
AlwaysMoveTo,
|
AlwaysMoveTo,
|
||||||
MoveToFocused,
|
MoveToFocused,
|
||||||
Archive,
|
Archive,
|
||||||
UpdateCategories,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum FolderSynchronizerOperation
|
public enum FolderSynchronizerOperation
|
||||||
@@ -23,27 +21,18 @@ public enum FolderSynchronizerOperation
|
|||||||
MarkFolderRead,
|
MarkFolderRead,
|
||||||
DeleteFolder,
|
DeleteFolder,
|
||||||
CreateSubFolder,
|
CreateSubFolder,
|
||||||
CreateRootFolder,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum CalendarSynchronizerOperation
|
public enum CalendarSynchronizerOperation
|
||||||
{
|
{
|
||||||
CreateEvent,
|
CreateEvent,
|
||||||
UpdateEvent,
|
UpdateEvent,
|
||||||
ChangeStartAndEndDate,
|
|
||||||
DeleteEvent,
|
DeleteEvent,
|
||||||
AcceptEvent,
|
AcceptEvent,
|
||||||
DeclineEvent,
|
DeclineEvent,
|
||||||
TentativeEvent,
|
TentativeEvent,
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum CategorySynchronizerOperation
|
|
||||||
{
|
|
||||||
CreateCategory,
|
|
||||||
UpdateCategory,
|
|
||||||
DeleteCategory,
|
|
||||||
}
|
|
||||||
|
|
||||||
// UI requests
|
// UI requests
|
||||||
public enum MailOperation
|
public enum MailOperation
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
public enum MailSynchronizationType
|
public enum MailSynchronizationType
|
||||||
{
|
{
|
||||||
UpdateProfile, // Only update profile information
|
UpdateProfile, // Only update profile information
|
||||||
Categories, // Only update mail categories
|
|
||||||
ExecuteRequests, // Run the queued requests, and then synchronize if needed.
|
ExecuteRequests, // Run the queued requests, and then synchronize if needed.
|
||||||
FoldersOnly, // Only synchronize folder metadata.
|
FoldersOnly, // Only synchronize folder metadata.
|
||||||
InboxOnly, // Only Inbox, Sent, Draft and Deleted folders.
|
InboxOnly, // Only Inbox, Sent, Draft and Deleted folders.
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace Wino.Core.Domain.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates the source of a mail update.
|
||||||
|
/// </summary>
|
||||||
|
public enum MailUpdateSource
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Update originated from client-side UI changes (ApplyUIChanges).
|
||||||
|
/// </summary>
|
||||||
|
ClientUpdated,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update originated from client-side UI revert (RevertUIChanges).
|
||||||
|
/// </summary>
|
||||||
|
ClientReverted,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update originated from server synchronization or database operations.
|
||||||
|
/// </summary>
|
||||||
|
Server
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Wino.Core.Domain.Enums;
|
|
||||||
|
|
||||||
public enum NewEventButtonBehavior
|
|
||||||
{
|
|
||||||
AskEachTime = 0,
|
|
||||||
AlwaysUseSpecificCalendar = 1
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace Wino.Core.Domain.Enums;
|
|
||||||
|
|
||||||
public enum SentMailReceiptStatus
|
|
||||||
{
|
|
||||||
None = 0,
|
|
||||||
Requested = 1,
|
|
||||||
Acknowledged = 2,
|
|
||||||
FailedToCorrelate = 3
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Wino.Core.Domain.Enums;
|
|
||||||
|
|
||||||
public enum SynchronizationProgressCategory
|
|
||||||
{
|
|
||||||
Mail,
|
|
||||||
Calendar
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace Wino.Core.Domain.Enums;
|
|
||||||
|
|
||||||
public enum ThreeButtonDialogResult
|
|
||||||
{
|
|
||||||
Primary,
|
|
||||||
Secondary,
|
|
||||||
Cancel
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Wino.Core.Domain.Enums;
|
|
||||||
|
|
||||||
public enum WinoAddOnProductType
|
|
||||||
{
|
|
||||||
AI_PACK,
|
|
||||||
UNLIMITED_ACCOUNTS
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,5 @@
|
|||||||
public enum WinoApplicationMode
|
public enum WinoApplicationMode
|
||||||
{
|
{
|
||||||
Mail,
|
Mail,
|
||||||
Calendar,
|
Calendar
|
||||||
Contacts,
|
|
||||||
Settings
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ public enum WinoPage
|
|||||||
SettingsPage,
|
SettingsPage,
|
||||||
ContactsPage,
|
ContactsPage,
|
||||||
MailRenderingPage,
|
MailRenderingPage,
|
||||||
|
WelcomePage,
|
||||||
AccountDetailsPage,
|
AccountDetailsPage,
|
||||||
FolderCustomizationPage,
|
|
||||||
MergedAccountDetailsPage,
|
MergedAccountDetailsPage,
|
||||||
ManageAccountsPage,
|
ManageAccountsPage,
|
||||||
AccountManagementPage,
|
AccountManagementPage,
|
||||||
@@ -20,31 +20,19 @@ public enum WinoPage
|
|||||||
AboutPage,
|
AboutPage,
|
||||||
PersonalizationPage,
|
PersonalizationPage,
|
||||||
MessageListPage,
|
MessageListPage,
|
||||||
MailNotificationSettingsPage,
|
|
||||||
MailListPage,
|
MailListPage,
|
||||||
ReadComposePanePage,
|
ReadComposePanePage,
|
||||||
|
LanguageTimePage,
|
||||||
AppPreferencesPage,
|
AppPreferencesPage,
|
||||||
SettingOptionsPage,
|
SettingOptionsPage,
|
||||||
AliasManagementPage,
|
AliasManagementPage,
|
||||||
MailCategoryManagementPage,
|
EditAccountDetailsPage,
|
||||||
ImapCalDavSettingsPage,
|
ImapCalDavSettingsPage,
|
||||||
KeyboardShortcutsPage,
|
KeyboardShortcutsPage,
|
||||||
CalendarPage,
|
CalendarPage,
|
||||||
CalendarSettingsPage,
|
CalendarSettingsPage,
|
||||||
CalendarRenderingSettingsPage,
|
|
||||||
CalendarNotificationSettingsPage,
|
|
||||||
CalendarPreferenceSettingsPage,
|
|
||||||
CalendarAccountSettingsPage,
|
CalendarAccountSettingsPage,
|
||||||
EventDetailsPage,
|
EventDetailsPage,
|
||||||
CalendarEventComposePage,
|
|
||||||
SignatureAndEncryptionPage,
|
SignatureAndEncryptionPage,
|
||||||
EmailTemplatesPage,
|
StoragePage
|
||||||
CreateEmailTemplatePage,
|
|
||||||
StoragePage,
|
|
||||||
WinoAccountManagementPage,
|
|
||||||
WelcomePageV2,
|
|
||||||
WelcomeHostPage,
|
|
||||||
ProviderSelectionPage,
|
|
||||||
AccountSetupProgressPage,
|
|
||||||
SpecialImapCredentialsPage
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Exceptions;
|
|
||||||
|
|
||||||
public sealed class CalendarEventComposeValidationException : Exception
|
|
||||||
{
|
|
||||||
public CalendarEventComposeValidationException(string message) : base(message)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,18 +9,22 @@ public class ImapClientPoolException : Exception
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public ImapClientPoolException(string message, CustomServerInformation customServerInformation) : base(message)
|
public ImapClientPoolException(string message, CustomServerInformation customServerInformation, string protocolLog) : base(message)
|
||||||
{
|
{
|
||||||
CustomServerInformation = customServerInformation;
|
CustomServerInformation = customServerInformation;
|
||||||
|
ProtocolLog = protocolLog;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ImapClientPoolException(string message) : base(message)
|
public ImapClientPoolException(string message, string protocolLog) : base(message)
|
||||||
{
|
{
|
||||||
|
ProtocolLog = protocolLog;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ImapClientPoolException(Exception innerException) : base(innerException.Message, innerException)
|
public ImapClientPoolException(Exception innerException, string protocolLog) : base(innerException.Message, innerException)
|
||||||
{
|
{
|
||||||
|
ProtocolLog = protocolLog;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CustomServerInformation CustomServerInformation { get; }
|
public CustomServerInformation CustomServerInformation { get; }
|
||||||
|
public string ProtocolLog { get; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -78,12 +78,8 @@ public static class DateTimeExtensions
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static DateTime GetLocalStartDate(this CalendarItem calendarItem)
|
public static DateTime GetLocalStartDate(this CalendarItem calendarItem)
|
||||||
=> calendarItem.IsAllDayEvent
|
=> calendarItem.StartDate.ToLocalTimeFromTimeZone(calendarItem.StartTimeZone);
|
||||||
? calendarItem.StartDate
|
|
||||||
: calendarItem.StartDate.ToLocalTimeFromTimeZone(calendarItem.StartTimeZone);
|
|
||||||
|
|
||||||
public static DateTime GetLocalEndDate(this CalendarItem calendarItem)
|
public static DateTime GetLocalEndDate(this CalendarItem calendarItem)
|
||||||
=> calendarItem.IsAllDayEvent
|
=> calendarItem.EndDate.ToLocalTimeFromTimeZone(calendarItem.EndTimeZone);
|
||||||
? calendarItem.EndDate
|
|
||||||
: calendarItem.EndDate.ToLocalTimeFromTimeZone(calendarItem.EndTimeZone);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Wino.Core.Domain.Enums;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Extensions;
|
|
||||||
|
|
||||||
public static class InitialSynchronizationRangeExtensions
|
|
||||||
{
|
|
||||||
public static DateTime? ToCutoffDateUtc(this InitialSynchronizationRange range, DateTime utcNow)
|
|
||||||
{
|
|
||||||
var normalizedUtcNow = utcNow.Kind == DateTimeKind.Utc
|
|
||||||
? utcNow
|
|
||||||
: utcNow.ToUniversalTime();
|
|
||||||
|
|
||||||
return range switch
|
|
||||||
{
|
|
||||||
InitialSynchronizationRange.ThreeMonths => normalizedUtcNow.AddMonths(-3),
|
|
||||||
InitialSynchronizationRange.SixMonths => normalizedUtcNow.AddMonths(-6),
|
|
||||||
InitialSynchronizationRange.NineMonths => normalizedUtcNow.AddMonths(-9),
|
|
||||||
InitialSynchronizationRange.OneYear => normalizedUtcNow.AddYears(-1),
|
|
||||||
_ => null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Extensions;
|
namespace Wino.Core.Domain.Extensions;
|
||||||
|
|
||||||
public static class MailHeaderExtensions
|
public static class MailHeaderExtensions
|
||||||
{
|
{
|
||||||
public static string NormalizeMessageId(string value)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var normalized = StripAngleBrackets(value)?.Trim();
|
|
||||||
return string.IsNullOrWhiteSpace(normalized) ? string.Empty : normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string ToHeaderMessageId(string value)
|
|
||||||
{
|
|
||||||
var normalized = NormalizeMessageId(value);
|
|
||||||
return string.IsNullOrEmpty(normalized) ? string.Empty : $"<{normalized}>";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Strips angle brackets from a Message-ID or In-Reply-To value.
|
/// Strips angle brackets from a Message-ID or In-Reply-To value.
|
||||||
/// RFC 5322 Message-IDs are formatted as <id@domain>, but MimeKit
|
/// RFC 5322 Message-IDs are formatted as <id@domain>, but MimeKit
|
||||||
@@ -45,53 +29,14 @@ public static class MailHeaderExtensions
|
|||||||
/// like "<id1@domain> <id2@domain>". This converts them to "id1@domain;id2@domain".
|
/// like "<id1@domain> <id2@domain>". This converts them to "id1@domain;id2@domain".
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string NormalizeReferences(string rawReferences)
|
public static string NormalizeReferences(string rawReferences)
|
||||||
=> JoinStoredReferences(SplitMessageIds(rawReferences));
|
|
||||||
|
|
||||||
public static IEnumerable<string> SplitMessageIds(string values)
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(values))
|
if (string.IsNullOrEmpty(rawReferences)) return rawReferences;
|
||||||
return [];
|
|
||||||
|
|
||||||
return values
|
var ids = rawReferences
|
||||||
.Split(new[] { ' ', '\t', '\r', '\n', ';', ',' }, StringSplitOptions.RemoveEmptyEntries)
|
.Split(new[] { ' ', '\t', '\r', '\n', ';', ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||||
.Select(NormalizeMessageId)
|
.Select(StripAngleBrackets)
|
||||||
.Where(id => !string.IsNullOrEmpty(id));
|
.Where(id => !string.IsNullOrEmpty(id));
|
||||||
}
|
|
||||||
|
|
||||||
public static string JoinStoredReferences(IEnumerable<string> values)
|
return string.Join(";", ids);
|
||||||
=> string.Join(";", NormalizeDistinctMessageIds(values));
|
|
||||||
|
|
||||||
public static string BuildReferencesHeaderValue(IEnumerable<string> values)
|
|
||||||
=> string.Join(" ", NormalizeDistinctMessageIds(values).Select(ToHeaderMessageId));
|
|
||||||
|
|
||||||
public static List<string> BuildReferencesChain(IEnumerable<string> existingReferences, string parentMessageId)
|
|
||||||
{
|
|
||||||
var results = NormalizeDistinctMessageIds(existingReferences).ToList();
|
|
||||||
var normalizedParentMessageId = NormalizeMessageId(parentMessageId);
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(normalizedParentMessageId) &&
|
|
||||||
!results.Contains(normalizedParentMessageId, StringComparer.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
results.Add(normalizedParentMessageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<string> NormalizeDistinctMessageIds(IEnumerable<string> values)
|
|
||||||
{
|
|
||||||
if (values == null)
|
|
||||||
yield break;
|
|
||||||
|
|
||||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
foreach (var value in values)
|
|
||||||
{
|
|
||||||
var normalized = NormalizeMessageId(value);
|
|
||||||
if (string.IsNullOrEmpty(normalized) || !seen.Add(normalized))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
yield return normalized;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using MimeKit;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Extensions;
|
|
||||||
|
|
||||||
public static class ReadReceiptExtensions
|
|
||||||
{
|
|
||||||
public static bool HasReadReceiptRequest(this MimeMessage mimeMessage)
|
|
||||||
=> mimeMessage?.Headers?.Contains(Constants.DispositionNotificationToHeader) == true
|
|
||||||
&& !string.IsNullOrWhiteSpace(mimeMessage.Headers[Constants.DispositionNotificationToHeader]);
|
|
||||||
|
|
||||||
public static void SetReadReceiptRequest(this MimeMessage mimeMessage, string address, bool isRequested)
|
|
||||||
{
|
|
||||||
if (mimeMessage == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
mimeMessage.Headers.Remove(Constants.DispositionNotificationToHeader);
|
|
||||||
|
|
||||||
if (isRequested && !string.IsNullOrWhiteSpace(address))
|
|
||||||
{
|
|
||||||
mimeMessage.Headers.Add(Constants.DispositionNotificationToHeader, address.Trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool LooksLikeReadReceipt(this MimeMessage mimeMessage)
|
|
||||||
{
|
|
||||||
if (mimeMessage?.Body == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return mimeMessage.BodyParts.Any(IsReadReceiptEntity) || IsReadReceiptEntity(mimeMessage.Body);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ReadReceiptParseResult ParseReadReceipt(this MimeMessage mimeMessage)
|
|
||||||
{
|
|
||||||
if (!mimeMessage.LooksLikeReadReceipt())
|
|
||||||
return ReadReceiptParseResult.Empty;
|
|
||||||
|
|
||||||
var entity = mimeMessage.BodyParts.FirstOrDefault(IsReadReceiptEntity) ?? mimeMessage.Body;
|
|
||||||
var lines = ReadEntityLines(entity);
|
|
||||||
|
|
||||||
string originalMessageId = null;
|
|
||||||
|
|
||||||
foreach (var line in lines)
|
|
||||||
{
|
|
||||||
if (line.StartsWith(Constants.OriginalMessageIdHeader + ":", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
originalMessageId = line.Substring(line.IndexOf(':') + 1).Trim();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var acknowledgedAtUtc = mimeMessage.Date != DateTimeOffset.MinValue
|
|
||||||
? mimeMessage.Date.UtcDateTime
|
|
||||||
: (DateTime?)null;
|
|
||||||
|
|
||||||
return new ReadReceiptParseResult(
|
|
||||||
true,
|
|
||||||
MailHeaderExtensions.NormalizeMessageId(originalMessageId),
|
|
||||||
acknowledgedAtUtc);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsReadReceiptEntity(MimeEntity entity)
|
|
||||||
{
|
|
||||||
if (entity?.ContentType == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (entity.ContentType.MimeType.Equals("message/disposition-notification", StringComparison.OrdinalIgnoreCase))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
var reportType = entity.ContentType.Parameters["report-type"];
|
|
||||||
return entity.ContentType.MimeType.Equals("multipart/report", StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& !string.IsNullOrWhiteSpace(reportType)
|
|
||||||
&& reportType.Equals("disposition-notification", StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<string> ReadEntityLines(MimeEntity entity)
|
|
||||||
{
|
|
||||||
if (entity is TextPart textPart)
|
|
||||||
{
|
|
||||||
return SplitLines(textPart.Text);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entity is MimePart mimePart)
|
|
||||||
{
|
|
||||||
using var memoryStream = new MemoryStream();
|
|
||||||
mimePart.Content?.DecodeTo(memoryStream);
|
|
||||||
memoryStream.Position = 0;
|
|
||||||
using var reader = new StreamReader(memoryStream);
|
|
||||||
return SplitLines(reader.ReadToEnd());
|
|
||||||
}
|
|
||||||
|
|
||||||
using var serializedStream = new MemoryStream();
|
|
||||||
entity.WriteTo(serializedStream);
|
|
||||||
serializedStream.Position = 0;
|
|
||||||
using var serializedReader = new StreamReader(serializedStream);
|
|
||||||
return SplitLines(serializedReader.ReadToEnd());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<string> SplitLines(string content)
|
|
||||||
=> string.IsNullOrWhiteSpace(content)
|
|
||||||
? []
|
|
||||||
: content.Split(["\r\n", "\n"], StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed record ReadReceiptParseResult(bool IsReadReceipt, string OriginalMessageId, DateTime? AcknowledgedAtUtc)
|
|
||||||
{
|
|
||||||
public static ReadReceiptParseResult Empty { get; } = new(false, string.Empty, null);
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,6 @@ public interface IAccountCalendar
|
|||||||
string TextColorHex { get; set; }
|
string TextColorHex { get; set; }
|
||||||
string BackgroundColorHex { get; set; }
|
string BackgroundColorHex { get; set; }
|
||||||
bool IsPrimary { get; set; }
|
bool IsPrimary { get; set; }
|
||||||
bool IsReadOnly { get; set; }
|
|
||||||
bool IsSynchronizationEnabled { get; set; }
|
bool IsSynchronizationEnabled { get; set; }
|
||||||
Guid AccountId { get; set; }
|
Guid AccountId { get; set; }
|
||||||
string RemoteCalendarId { get; set; }
|
string RemoteCalendarId { get; set; }
|
||||||
|
|||||||
@@ -1,43 +1,35 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Wino.Core.Domain.Entities.Mail;
|
using Wino.Core.Domain.Entities.Mail;
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Models.Synchronization;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Interfaces;
|
namespace Wino.Core.Domain.Interfaces;
|
||||||
|
|
||||||
public interface IAccountMenuItem : IMenuItem
|
public interface IAccountMenuItem : IMenuItem
|
||||||
{
|
{
|
||||||
bool IsEnabled { get; set; }
|
bool IsEnabled { get; set; }
|
||||||
bool IsSynchronizationInProgress { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculated synchronization progress percentage (0-100).
|
/// Calculated synchronization progress percentage (0-100). -1 for indeterminate.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
double SynchronizationProgress { get; }
|
double SynchronizationProgress { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Progress value clamped for XAML progress controls.
|
|
||||||
/// </summary>
|
|
||||||
double SynchronizationProgressValue { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Total items to sync. 0 for indeterminate progress.
|
/// Total items to sync. 0 for indeterminate progress.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
int TotalItemsToSync { get; set; }
|
int TotalItemsToSync { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Remaining items to sync.
|
/// Remaining items to sync.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
int RemainingItemsToSync { get; set; }
|
int RemainingItemsToSync { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Current synchronization status message.
|
/// Current synchronization status message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
string SynchronizationStatus { get; set; }
|
string SynchronizationStatus { get; set; }
|
||||||
|
|
||||||
int UnreadItemCount { get; set; }
|
int UnreadItemCount { get; set; }
|
||||||
IEnumerable<MailAccount> HoldingAccounts { get; }
|
IEnumerable<MailAccount> HoldingAccounts { get; }
|
||||||
void ApplySynchronizationProgress(AccountSynchronizationProgress progress);
|
|
||||||
void UpdateAccount(MailAccount account);
|
void UpdateAccount(MailAccount account);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,20 +23,6 @@ public interface IAccountService
|
|||||||
/// <returns>All local accounts</returns>
|
/// <returns>All local accounts</returns>
|
||||||
Task<List<MailAccount>> GetAccountsAsync();
|
Task<List<MailAccount>> GetAccountsAsync();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks whether an account with the same display name already exists.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="name">Account display name.</param>
|
|
||||||
/// <param name="excludedAccountId">Optional account id to exclude from the check.</param>
|
|
||||||
Task<bool> AccountNameExistsAsync(string name, Guid? excludedAccountId = null);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks whether an account with the same primary address already exists.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="address">Primary e-mail address.</param>
|
|
||||||
/// <param name="excludedAccountId">Optional account id to exclude from the check.</param>
|
|
||||||
Task<bool> AccountAddressExistsAsync(string address, Guid? excludedAccountId = null);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns single MailAccount
|
/// Returns single MailAccount
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -161,7 +147,6 @@ public interface IAccountService
|
|||||||
/// <param name="accountId">Account id.</param>
|
/// <param name="accountId">Account id.</param>
|
||||||
/// <returns>Primary alias for the account.</returns>
|
/// <returns>Primary alias for the account.</returns>
|
||||||
Task<MailAccountAlias> GetPrimaryAccountAliasAsync(Guid accountId);
|
Task<MailAccountAlias> GetPrimaryAccountAliasAsync(Guid accountId);
|
||||||
Task UpdateAliasSendCapabilityAsync(Guid accountId, string aliasAddress, AliasSendCapability capability);
|
|
||||||
Task<bool> IsAccountFocusedEnabledAsync(Guid accountId);
|
Task<bool> IsAccountFocusedEnabledAsync(Guid accountId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using Wino.Core.Domain.Models.Ai;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Interfaces;
|
|
||||||
|
|
||||||
public interface IAiActionOptionsService
|
|
||||||
{
|
|
||||||
IReadOnlyList<AiTranslateLanguageOption> GetTranslateLanguageOptions();
|
|
||||||
IReadOnlyList<AiRewriteModeOption> GetRewriteModeOptions();
|
|
||||||
}
|
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
public interface IAuthenticatorConfig
|
public interface IAuthenticatorConfig
|
||||||
{
|
{
|
||||||
string OutlookAuthenticatorClientId { get; }
|
string OutlookAuthenticatorClientId { get; }
|
||||||
string[] GetOutlookScope(bool isMailAccessGranted, bool isCalendarAccessGranted);
|
string[] OutlookScope { get; }
|
||||||
string GmailAuthenticatorClientId { get; }
|
string GmailAuthenticatorClientId { get; }
|
||||||
string[] GetGmailScope(bool isMailAccessGranted, bool isCalendarAccessGranted);
|
string[] GmailScope { get; }
|
||||||
string GmailTokenStoreIdentifier { get; }
|
string GmailTokenStoreIdentifier { get; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
@@ -31,22 +30,12 @@ public interface IBaseSynchronizer
|
|||||||
/// <param name="mailUniqueId">Mail unique id to check.</param>
|
/// <param name="mailUniqueId">Mail unique id to check.</param>
|
||||||
bool HasPendingOperation(Guid mailUniqueId);
|
bool HasPendingOperation(Guid mailUniqueId);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns mail unique ids that currently have queued or executing operations.
|
|
||||||
/// </summary>
|
|
||||||
IReadOnlyCollection<Guid> GetPendingOperationUniqueIds();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns whether there is an in-progress (queued or currently executing) operation for the given calendar item id.
|
/// Returns whether there is an in-progress (queued or currently executing) operation for the given calendar item id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="calendarItemId">Calendar item id to check.</param>
|
/// <param name="calendarItemId">Calendar item id to check.</param>
|
||||||
bool HasPendingCalendarOperation(Guid calendarItemId);
|
bool HasPendingCalendarOperation(Guid calendarItemId);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns calendar item ids that currently have queued or executing operations.
|
|
||||||
/// </summary>
|
|
||||||
IReadOnlyCollection<Guid> GetPendingCalendarOperationIds();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Synchronizes profile information with the server.
|
/// Synchronizes profile information with the server.
|
||||||
/// Sender name and Profile picture are updated.
|
/// Sender name and Profile picture are updated.
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using Wino.Core.Domain.Entities.Calendar;
|
|
||||||
using Wino.Core.Domain.Models.Calendar;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Interfaces;
|
|
||||||
|
|
||||||
public interface ICalendarContextMenuItemService
|
|
||||||
{
|
|
||||||
IReadOnlyList<CalendarContextMenuItem> GetContextMenuItems(CalendarItem calendarItem);
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,6 @@ public interface ICalendarService
|
|||||||
Task DeleteAccountCalendarAsync(AccountCalendar accountCalendar);
|
Task DeleteAccountCalendarAsync(AccountCalendar accountCalendar);
|
||||||
Task InsertAccountCalendarAsync(AccountCalendar accountCalendar);
|
Task InsertAccountCalendarAsync(AccountCalendar accountCalendar);
|
||||||
Task UpdateAccountCalendarAsync(AccountCalendar accountCalendar);
|
Task UpdateAccountCalendarAsync(AccountCalendar accountCalendar);
|
||||||
Task SetPrimaryCalendarAsync(Guid accountId, Guid accountCalendarId);
|
|
||||||
Task CreateNewCalendarItemAsync(CalendarItem calendarItem, List<CalendarEventAttendee> attendees);
|
Task CreateNewCalendarItemAsync(CalendarItem calendarItem, List<CalendarEventAttendee> attendees);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -41,7 +40,6 @@ public interface ICalendarService
|
|||||||
Task<List<CalendarEventAttendee>> GetAttendeesAsync(Guid calendarEventTrackingId);
|
Task<List<CalendarEventAttendee>> GetAttendeesAsync(Guid calendarEventTrackingId);
|
||||||
Task<List<CalendarEventAttendee>> ManageEventAttendeesAsync(Guid calendarItemId, List<CalendarEventAttendee> allAttendees);
|
Task<List<CalendarEventAttendee>> ManageEventAttendeesAsync(Guid calendarItemId, List<CalendarEventAttendee> allAttendees);
|
||||||
Task UpdateCalendarItemAsync(CalendarItem calendarItem, List<CalendarEventAttendee> attendees);
|
Task UpdateCalendarItemAsync(CalendarItem calendarItem, List<CalendarEventAttendee> attendees);
|
||||||
Task<List<CalendarItem>> SearchCalendarItemsAsync(string searchQuery, int limit, CancellationToken cancellationToken = default);
|
|
||||||
Task<List<Reminder>> GetRemindersAsync(Guid calendarItemId);
|
Task<List<Reminder>> GetRemindersAsync(Guid calendarItemId);
|
||||||
Task SaveRemindersAsync(Guid calendarItemId, List<Reminder> reminders);
|
Task SaveRemindersAsync(Guid calendarItemId, List<Reminder> reminders);
|
||||||
Task SnoozeCalendarItemAsync(Guid calendarItemId, DateTime snoozedUntilLocal);
|
Task SnoozeCalendarItemAsync(Guid calendarItemId, DateTime snoozedUntilLocal);
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
public interface IConfigurationService
|
public interface IConfigurationService
|
||||||
{
|
{
|
||||||
bool Contains(string key);
|
|
||||||
|
|
||||||
void Set(string key, object value);
|
void Set(string key, object value);
|
||||||
T Get<T>(string key, T defaultValue = default);
|
T Get<T>(string key, T defaultValue = default);
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Interfaces;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Manages contact picture files stored on disk instead of as base64 in SQLite,
|
|
||||||
/// eliminating DB bloat and enabling native WIC hardware-accelerated image loading.
|
|
||||||
/// </summary>
|
|
||||||
public interface IContactPictureFileService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the full file path for the given file ID, or null if the file does not exist on disk.
|
|
||||||
/// </summary>
|
|
||||||
string GetContactPicturePath(Guid fileId);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Saves raw image bytes to disk and returns the new file ID.
|
|
||||||
/// </summary>
|
|
||||||
Task<Guid> SaveContactPictureAsync(byte[] imageData);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deletes the picture file for the given file ID if it exists.
|
|
||||||
/// </summary>
|
|
||||||
Task DeleteContactPictureAsync(Guid fileId);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MimeKit;
|
using MimeKit;
|
||||||
@@ -12,30 +11,15 @@ public interface IContactService
|
|||||||
{
|
{
|
||||||
Task<List<AccountContact>> GetAddressInformationAsync(string queryText);
|
Task<List<AccountContact>> GetAddressInformationAsync(string queryText);
|
||||||
Task<AccountContact> GetAddressInformationByAddressAsync(string address);
|
Task<AccountContact> GetAddressInformationByAddressAsync(string address);
|
||||||
Task<List<AccountContact>> GetContactsByAddressesAsync(IEnumerable<string> addresses);
|
|
||||||
Task SaveAddressInformationAsync(MimeMessage message);
|
Task SaveAddressInformationAsync(MimeMessage message);
|
||||||
Task SaveAddressInformationAsync(IEnumerable<AccountContact> contacts);
|
Task SaveAddressInformationAsync(IEnumerable<AccountContact> contacts);
|
||||||
Task<AccountContact> CreateNewContactAsync(string address, string displayName);
|
Task<AccountContact> CreateNewContactAsync(string address, string displayName);
|
||||||
|
|
||||||
// Paged contact queries for ContactsPage
|
// New methods for ContactsPage
|
||||||
Task<List<AccountContact>> GetAllContactsAsync();
|
Task<List<AccountContact>> GetAllContactsAsync();
|
||||||
Task<List<AccountContact>> SearchContactsAsync(string searchQuery);
|
Task<List<AccountContact>> SearchContactsAsync(string searchQuery);
|
||||||
Task<PagedContactsResult> GetContactsPageAsync(int offset, int pageSize, string searchQuery = null, bool excludeRootContacts = false);
|
Task<PagedContactsResult> GetContactsPageAsync(int offset, int pageSize, string searchQuery = null, bool excludeRootContacts = false);
|
||||||
Task<AccountContact> UpdateContactAsync(AccountContact contact);
|
Task<AccountContact> UpdateContactAsync(AccountContact contact);
|
||||||
Task DeleteContactAsync(string address);
|
Task DeleteContactAsync(string address);
|
||||||
Task DeleteContactsAsync(IEnumerable<string> addresses);
|
Task DeleteContactsAsync(IEnumerable<string> addresses);
|
||||||
|
|
||||||
// Group / distribution list support
|
|
||||||
Task<List<ContactGroup>> GetGroupsAsync();
|
|
||||||
Task<ContactGroup> CreateGroupAsync(string name, string description = null);
|
|
||||||
Task DeleteGroupAsync(Guid groupId);
|
|
||||||
Task<List<AccountContact>> GetGroupMembersAsync(Guid groupId);
|
|
||||||
Task AddGroupMemberAsync(Guid groupId, string memberAddress);
|
|
||||||
Task RemoveGroupMemberAsync(Guid groupId, string memberAddress);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Expands a contact group to the individual <see cref="AccountContact"/> entries of its members.
|
|
||||||
/// Returns an empty list if the group does not exist or has no members.
|
|
||||||
/// </summary>
|
|
||||||
Task<List<AccountContact>> ExpandGroupAsync(Guid groupId);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Models.Accounts;
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
using Wino.Core.Domain.Models.Common;
|
using Wino.Core.Domain.Models.Common;
|
||||||
|
using Wino.Core.Domain.Models.Printing;
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Interfaces;
|
namespace Wino.Core.Domain.Interfaces;
|
||||||
|
|
||||||
@@ -27,6 +28,6 @@ public interface IDialogServiceBase
|
|||||||
Task<AccountCreationDialogResult> ShowAccountProviderSelectionDialogAsync(List<IProviderDetail> availableProviders);
|
Task<AccountCreationDialogResult> ShowAccountProviderSelectionDialogAsync(List<IProviderDetail> availableProviders);
|
||||||
IAccountCreationDialog GetAccountCreationDialog(AccountCreationDialogResult accountCreationDialogResult);
|
IAccountCreationDialog GetAccountCreationDialog(AccountCreationDialogResult accountCreationDialogResult);
|
||||||
Task<List<SharedFile>> PickFilesAsync(params object[] typeFilters);
|
Task<List<SharedFile>> PickFilesAsync(params object[] typeFilters);
|
||||||
Task<List<PickedFileMetadata>> PickFilesMetadataAsync(params object[] typeFilters);
|
|
||||||
Task<string> PickFilePathAsync(string saveFileName);
|
Task<string> PickFilePathAsync(string saveFileName);
|
||||||
|
Task<WebView2PrintSettingsModel> ShowPrintDialogAsync(WebView2PrintSettingsModel initialSettings = null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
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<List<EmailTemplate>> GetEmailTemplatesAsync();
|
|
||||||
Task<EmailTemplate> GetEmailTemplateAsync(Guid templateId);
|
|
||||||
Task<EmailTemplate> CreateEmailTemplateAsync(EmailTemplate template);
|
|
||||||
Task<EmailTemplate> UpdateEmailTemplateAsync(EmailTemplate template);
|
|
||||||
Task<EmailTemplate> DeleteEmailTemplateAsync(EmailTemplate template);
|
|
||||||
}
|
|
||||||
@@ -14,22 +14,6 @@ public interface IFolderMenuItem : IBaseFolderMenuItem
|
|||||||
|
|
||||||
public interface IMergedAccountFolderMenuItem : IBaseFolderMenuItem { }
|
public interface IMergedAccountFolderMenuItem : IBaseFolderMenuItem { }
|
||||||
|
|
||||||
public interface IMailCategoryMenuItem : IBaseFolderMenuItem
|
|
||||||
{
|
|
||||||
Entities.Mail.MailCategory MailCategory { get; }
|
|
||||||
string TextColorHex { get; }
|
|
||||||
string BackgroundColorHex { get; }
|
|
||||||
bool HasTextColor { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IMergedMailCategoryMenuItem : IBaseFolderMenuItem
|
|
||||||
{
|
|
||||||
IReadOnlyList<Entities.Mail.MailCategory> Categories { get; }
|
|
||||||
string TextColorHex { get; }
|
|
||||||
string BackgroundColorHex { get; }
|
|
||||||
bool HasTextColor { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IBaseFolderMenuItem : IMenuItem
|
public interface IBaseFolderMenuItem : IMenuItem
|
||||||
{
|
{
|
||||||
string FolderName { get; }
|
string FolderName { get; }
|
||||||
|
|||||||
@@ -22,25 +22,6 @@ public interface IFolderService
|
|||||||
Task<int> GetFolderNotificationBadgeAsync(Guid folderId);
|
Task<int> GetFolderNotificationBadgeAsync(Guid folderId);
|
||||||
Task ChangeStickyStatusAsync(Guid folderId, bool isSticky);
|
Task ChangeStickyStatusAsync(Guid folderId, bool isSticky);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Toggles a folder's visibility in the navigation menu.
|
|
||||||
/// Hidden folders are still synchronized if sync is enabled.
|
|
||||||
/// </summary>
|
|
||||||
Task ChangeFolderHiddenStatusAsync(Guid folderId, bool isHidden);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Persists a new custom ordering for the given folders.
|
|
||||||
/// The first id becomes Order=1, second Order=2, etc.
|
|
||||||
/// Caller is responsible for notifying the shell to refresh.
|
|
||||||
/// </summary>
|
|
||||||
Task UpdateFolderOrdersAsync(Guid accountId, IReadOnlyList<Guid> orderedFolderIds);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Wipes every user folder customization for the account: clears custom Order,
|
|
||||||
/// un-hides folders, and restores IsSticky on system folders.
|
|
||||||
/// </summary>
|
|
||||||
Task ResetFolderCustomizationAsync(Guid accountId);
|
|
||||||
|
|
||||||
Task<MailAccount> UpdateSystemFolderConfigurationAsync(Guid accountId, SystemFolderConfiguration configuration);
|
Task<MailAccount> UpdateSystemFolderConfigurationAsync(Guid accountId, SystemFolderConfiguration configuration);
|
||||||
Task ChangeFolderSynchronizationStateAsync(Guid folderId, bool isSynchronizationEnabled);
|
Task ChangeFolderSynchronizationStateAsync(Guid folderId, bool isSynchronizationEnabled);
|
||||||
Task ChangeFolderShowUnreadCountStateAsync(Guid folderId, bool showUnreadCount);
|
Task ChangeFolderShowUnreadCountStateAsync(Guid folderId, bool showUnreadCount);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user