diff --git a/.editorconfig b/.editorconfig index b06e6570..ba6b725a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,6 +8,9 @@ dotnet_style_operator_placement_when_wrapping = beginning_of_line tab_width = 4 indent_size = 4 end_of_line = crlf +[XamlTypeInfo.g.cs] +dotnet_diagnostic.CS0612.severity = none +dotnet_diagnostic.CS0618.severity = none dotnet_style_coalesce_expression = true:suggestion dotnet_style_null_propagation = true:suggestion dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion @@ -289,4 +292,4 @@ csharp_style_prefer_readonly_struct = true:suggestion csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent csharp_style_prefer_primary_constructors = true:silent -csharp_prefer_system_threading_lock = true:suggestion \ No newline at end of file +csharp_prefer_system_threading_lock = true:suggestion diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..91271c54 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,191 @@ +# Copilot Instructions for Wino-Mail Project + +## Project Overview + +Wino Mail is a native Windows mail client targeting Windows 10 1809+ and Windows 11. The project is **transitioning from UWP to WinUI 3** - always work with WinUI projects (Wino.Mail.WinUI, Wino.Core.WinUI), never edit the old Wino.Mail UWP project. + +### Key Technologies +- **WinUI 3** for UI (previously UWP/WinUI 2) +- **MVVM Toolkit** (CommunityToolkit.Mvvm) for ViewModels with source generators +- **Messenger** pattern (WeakReferenceMessenger.Default) for event pub-sub throughout the codebase +- **SQLite** database stored in publisher cache folder (not local storage) +- **WebView2** for mail rendering/composition with custom HTML/JavaScript editors +- **MimeKit/MailKit** for IMAP/SMTP operations +- **Microsoft Graph SDK** for Outlook synchronization +- **Gmail API** for Gmail synchronization + +### Solution Structure +``` +Wino.Core.Domain → Entities, interfaces, translations, enums (shared contracts) +Wino.Core → Synchronization engine, authenticators, request processing +Wino.Services → Database, mail, folder, account services +Wino.Mail.ViewModels → Mail-specific ViewModels +Wino.Core.ViewModels → Shared ViewModels (settings, personalization) +Wino.Mail.WinUI → **Active WinUI 3 UI project** (use this) +Wino.Mail → **Deprecated UWP project** (DO NOT EDIT) +``` + +## Architecture Patterns + +### Mail Synchronization Flow +1. **WinoRequestDelegator** → Validates and delegates user actions (mark read, delete, move) +2. **WinoRequestProcessor** → Batches requests using RequestComparer, queues to synchronizers +3. **Synchronizers** (OutlookSynchronizer, GmailSynchronizer, ImapSynchronizer) → Execute batched operations +4. **ChangeProcessors** (OutlookChangeProcessor, etc.) → Apply changes to local database +5. Database updates trigger **Messenger** events (MailAddedMessage, MailUpdatedMessage, etc.) + +### Queue-Based Sync (New Pattern - See QUEUE_SYNC_IMPLEMENTATION.md) +- Initial sync now queues mail IDs first (MailItemQueue table), downloads metadata only (no MIME) +- MIME content downloaded on-demand when user opens mail +- Synchronizers override `QueueMailIdsForInitialSyncAsync()`, `DownloadMailsFromQueueAsync()`, `CreateMinimalMailCopyAsync()` +- Check `MailItemFolder.IsInitialSyncCompleted` to determine sync state + +### Dependency Injection Setup +Services registered in extension methods across projects: +- `RegisterCoreServices()` in Wino.Core/CoreContainerSetup.cs +- `RegisterSharedServices()` in Wino.Services/ServicesContainerSetup.cs +- `RegisterCoreUWPServices()` in CoreUWPContainerSetup.cs +- ViewModels registered in App.xaml.cs with AddTransient/AddSingleton + +### Messenger Pattern (Event Pub-Sub) +- All ViewModels inherit from CoreBaseViewModel or MailBaseViewModel which implement IRecipient +- Register/unregister message handlers in `RegisterRecipients()` / `UnregisterRecipients()` +- Send messages via `WeakReferenceMessenger.Default.Send(new MessageType(...))` +- Common messages: MailAddedMessage, MailUpdatedMessage, NavigationRequested, ThemeChanged + +## ViewModels Development Guidelines + +### Observable Properties - Critical Pattern +- **ALWAYS** use `public partial` observable properties with MVVM Toolkit source generators +- **NEVER** use private fields with `[ObservableProperty]` attribute +- **Correct:** + ```csharp + [ObservableProperty] + public partial string SearchQuery { get; set; } = string.Empty; + ``` +- **Incorrect:** + ```csharp + [ObservableProperty] + private string searchQuery = string.Empty; // WRONG - will not work + ``` + +### ViewModels Structure +- Inherit from MailBaseViewModel (for mail features) or CoreBaseViewModel (for shared features) +- Use `[RelayCommand]` for command methods - source generator creates Command properties +- Implement IRecipient for message handlers +- Use `IMailDialogService` for Mail-related dialogs, `IDialogServiceBase` for core dialogs +- Call `RegisterRecipients()` in constructor/OnNavigatedTo, `UnregisterRecipients()` in OnNavigatedFrom + +## Localization System + +### Translation Workflow (Custom T4-based System) +1. Add English strings ONLY to `Wino.Core.Domain/Translations/en_US/resources.json` +2. Build the project - source generators automatically create Translator properties +3. Use `Translator.{PropertyName}` in ViewModels, XAML (with x:Bind, OneTime mode) +4. **NEVER** edit other language files - Crowdin manages translations automatically +5. **NEVER** hardcode user-facing strings + +### Usage Examples +```csharp +// ViewModel +_dialogService.InfoBarMessage(Translator.Info_MissingFolderTitle, message); + +// XAML + +``` + +## UI Data Binding and Converters + +### WinUI 3 Automatic Conversions +- **NEVER** create IValueConverter classes or add them to Converters.xaml +- **NEVER** use BoolToVisibilityConverter - WinUI 3 SDK automatically converts bool to Visibility +- Direct binding: `Visibility="{x:Bind IsVisible, Mode=OneWay}"` +- Register control events (for example `Loaded`, `Unloaded`, `SizeChanged`, `PointerEntered`) in XAML markup, not with `+=` in `.xaml.cs`. + +### XamlHelpers for Complex Conversions +- **ALWAYS** use XamlHelpers static methods instead of converters +- Add xmlns: `xmlns:helpers="using:Wino.Helpers"` +- Usage: `{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(PropertyName), Mode=OneWay}` +- Available methods: ReverseBoolToVisibilityConverter, CountToBooleanConverter, BoolToSelectionMode, Base64ToBitmapImage +- Add new methods to XamlHelpers.cs when needed, don't create converters + +## WebView2 Mail Rendering + +### Architecture +- **reader.html** (Wino.Mail.WinUI/JS/) for reading mails +- **editor.html** for composing mails (uses Jodit editor, not Quill as originally planned) +- WebView2 uses virtual host mapping: `https://wino.mail/reader.html` +- JavaScript interop via `ExecuteScriptFunctionAsync()` to call functions like `RenderHTML()` +- MIME content downloaded on-demand, not during sync + +### Key Patterns +- Set environment variables for WebView2 before initialization (overlay scrollbars, cache) +- Wait for DOMContentLoaded event before script execution +- Handle theme changes by updating editor CSS dynamically +- Cancel external navigation, open in browser via Launcher.LaunchUriAsync() + +## File Structure and Project Organization + +### Critical Rules +- **NEVER** edit files in Wino.Mail (UWP) project - it's deprecated +- **ALWAYS** work with Wino.Mail.WinUI for UI components +- Place ViewModels in Wino.Mail.ViewModels (mail-specific) or Wino.Core.ViewModels (shared) +- Create abstract base classes in Views/Abstract folders +- Mail-specific dialog services go in Wino.Mail.WinUI/Services + +### Database and Storage +- SQLite database in publisher cache folder (not app local storage) +- EML files stored in app local storage, referenced by MailCopy.FileId +- Paths resolved via MimeFileService.GetMimeMessagePath() +- Database entities in Wino.Core.Domain/Entities + +## Error Handling and User Feedback + +### Exception Handling Patterns +```csharp +try { + await operation(); +} catch (UnavailableSpecialFolderException ex) { + _dialogService.InfoBarMessage(title, message, InfoBarMessageType.Warning, buttonText, action); +} catch (NotImplementedException) { + _dialogService.ShowNotSupportedMessage(); +} +``` + +### Dialog Service Methods +- `InfoBarMessage()` - simple notifications with optional action button +- `ShowConfirmationDialogAsync()` - yes/no dialogs +- `PickFilesAsync()` - file selection +- Always check for null/empty results from dialog operations + +## Code Style and Best Practices + +- Use `var` where type is obvious from right side +- String interpolation over string.Format for simple cases +- Keep methods focused and single-responsibility +- Add XML documentation for public APIs +- Avoid introducing new NuGet packages - maximize use of existing libraries +- Wrap async operations in try-catch blocks +- Log errors via IWinoLogger but don't expose technical details to users + +## Development Workflow + +### Building and Running +- Open WinoMail.slnx in Visual Studio 2022+ +- Target platforms: x86, x64, ARM64 (ARM32 being phased out) +- Minimum: Windows 10 1809, Target: Windows 11 22H2 +- Set Wino.Mail.WinUI as startup project + +### Testing +- Test suite in Wino.Core.Tests +- Manual testing required for UI/WebView2 interactions +- Test synchronization with real accounts when modifying synchronizers + +### Common Pitfalls +- Forgetting to register ViewModels in App.xaml.cs RegisterViewModels() +- Not calling RegisterRecipients() for message handlers +- Using private fields with [ObservableProperty] (won't work - must be public partial) +- Creating IValueConverter classes instead of using XamlHelpers +- Editing UWP project files instead of WinUI equivalents +- Hardcoding strings instead of using Translator +- Forgetting to unregister Messenger recipients (memory leaks) diff --git a/.github/workflows/pr-winui-build.yml b/.github/workflows/pr-winui-build.yml new file mode 100644 index 00000000..93be3bc4 --- /dev/null +++ b/.github/workflows/pr-winui-build.yml @@ -0,0 +1,125 @@ +name: PR WinUI Build + +on: + pull_request: + types: + - opened + - synchronize + - reopened + - ready_for_review + +permissions: + contents: read + packages: read + +jobs: + build-winui: + name: Build project (${{ matrix.platform }}) + if: github.event.pull_request.draft == false + runs-on: windows-latest + continue-on-error: ${{ contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association) }} + strategy: + fail-fast: false + matrix: + include: + - platform: x86 + rid: win-x86 + - platform: x64 + rid: win-x64 + - platform: ARM64 + rid: win-arm64 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + source-url: https://nuget.pkg.github.com/bkaankose/index.json + env: + NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Restore WinUI project dependencies + run: dotnet restore Wino.Mail.WinUI/Wino.Mail.WinUI.csproj --configfile nuget.config -p:Platform=${{ matrix.platform }} -p:RuntimeIdentifier=${{ matrix.rid }} + + - name: Build WinUI project + run: dotnet build Wino.Mail.WinUI/Wino.Mail.WinUI.csproj --configuration Release --no-restore -p:Platform=${{ matrix.platform }} -p:RuntimeIdentifier=${{ matrix.rid }} -p:GenerateAppxPackageOnBuild=false -p:AppxPackageSigningEnabled=false + + core-tests: + name: Run Core tests + if: github.event.pull_request.draft == false + runs-on: windows-latest + continue-on-error: ${{ contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association) }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + source-url: https://nuget.pkg.github.com/bkaankose/index.json + env: + NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Restore Core test projects + shell: pwsh + run: | + $coreTests = Get-ChildItem -Path . -Recurse -Filter "*Core*.Tests.csproj" | ForEach-Object { $_.FullName } + if (-not $coreTests) { + throw "No Core test projects were found." + } + + foreach ($project in $coreTests) { + dotnet restore $project --configfile nuget.config + } + + - name: Run Core test projects + shell: pwsh + run: | + New-Item -ItemType Directory -Path TestResults -Force | Out-Null + $coreTests = Get-ChildItem -Path . -Recurse -Filter "*Core*.Tests.csproj" + if (-not $coreTests) { + throw "No Core test projects were found." + } + + foreach ($project in $coreTests) { + $name = $project.BaseName + dotnet test $project.FullName --configuration Release --no-restore --verbosity normal --logger "trx;LogFileName=$name.trx" --results-directory TestResults + } + + - name: Upload Core test result artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: core-test-results + path: TestResults/*.trx + if-no-files-found: warn + + - name: Publish Core test report + if: always() + uses: EnricoMi/publish-unit-test-result-action/windows@v2 + with: + trx_files: TestResults/*.trx + check_name: Core test results + + enforce-for-non-maintainers: + name: Enforce required checks (non-maintainers) + if: github.event.pull_request.draft == false && !contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association) + runs-on: ubuntu-latest + needs: + - build-winui + - core-tests + + steps: + - name: Fail when build or tests fail for non-maintainers + if: needs.build-winui.result != 'success' || needs.core-tests.result != 'success' + run: | + echo "WinUI build and Core tests must pass for non-maintainer pull requests." + exit 1 + + - name: Confirm build and test success for non-maintainers + run: echo "WinUI build and Core tests passed." diff --git a/.gitignore b/.gitignore index cfa63f40..b19b3eb0 100644 --- a/.gitignore +++ b/.gitignore @@ -399,3 +399,4 @@ Wino/obj/x86/Debug/XamlSaveStateFile.xml Wino/obj/x86/Debug/XamlSaveStateFile.xml *.cache .vs/Wino/v16/.suo +/.claude/settings.local.json diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..08771976 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,160 @@ +# AGENTS.md + +This file provides guidance to AI agent when working with code in this repository. + +## Project Overview + +Wino Mail is a native Windows mail client (Windows 10 1809+ / Windows 11) replacing the deprecated Windows Mail & Calendar. It's **transitioning from UWP to WinUI 3** - always work with WinUI projects (Wino.Mail.WinUI), never edit the deprecated Wino.Mail UWP project. + +## Build and Development Commands + +```bash +# Open solution +# WinoMail.slnx is the main solution file (VS 2022+) + +# Build WinUI project (Debug x64) +dotnet restore Wino.Mail.WinUI/Wino.Mail.WinUI.csproj --configfile nuget.config -p:Platform=x64 -p:RuntimeIdentifier=win-x64 && dotnet build Wino.Mail.WinUI/Wino.Mail.WinUI.csproj -c Debug --no-restore /p:Platform=x64 /p:RuntimeIdentifier=win-x64 /p:GenerateAppxPackageOnBuild=false /p:AppxPackageSigningEnabled=false + +# Build WinUI project with diagnostic XAML/compiler logging (use when plain build only shows "XamlCompiler.exe exited with code 1") +dotnet build Wino.Mail.WinUI/Wino.Mail.WinUI.csproj -c Debug --no-restore /p:Platform=x64 /p:RuntimeIdentifier=win-x64 /p:GenerateAppxPackageOnBuild=false /p:AppxPackageSigningEnabled=false "/flp:logfile=winui-build.log;verbosity=diagnostic" /bl:winui-build.binlog + +# Run tests (Debug x64) +dotnet test Wino.Core.Tests/Wino.Core.Tests.csproj -c Debug /p:Platform=x64 + +# Copilot CLI build command (Debug x64) +dotnet restore Wino.Mail.WinUI/Wino.Mail.WinUI.csproj --configfile nuget.config -p:Platform=x64 -p:RuntimeIdentifier=win-x64 && dotnet build Wino.Mail.WinUI/Wino.Mail.WinUI.csproj -c Debug --no-restore /p:Platform=x64 /p:RuntimeIdentifier=win-x64 /p:GenerateAppxPackageOnBuild=false /p:AppxPackageSigningEnabled=false +``` + +**Prerequisites:** Visual Studio 2022+ with ".NET desktop development" workload, .NET SDK 10+ + +**Startup project:** Wino.Mail.WinUI + +**Platforms:** x86, x64, ARM64 + +## Efficient Workflow + +- Start with targeted symbol or file search before reading full files +- Prefer one focused task per thread; use a new thread for unrelated follow-up work +- Keep verification narrow: build only the affected project, not the full solution, unless cross-project changes require it +- After the first restore, prefer `--no-restore` builds unless package or project references changed +- Summarize long build logs and inspect only the files named in diagnostics instead of loading large logs into context +- When the prompt already names likely files, types, or symbols, start there instead of re-mapping the repository +- If a WinUI build only reports `XamlCompiler.exe exited with code 1`, rerun with the diagnostic logging command above and inspect the terminal output plus `winui-build.log` for real `WMC`/`WMC1121`/binding diagnostics before guessing + +## Architecture + +### Solution Structure +``` +Wino.Core.Domain → Entities, interfaces, translations, enums (shared contracts) +Wino.Core → Synchronization engine, authenticators, request processing +Wino.Services → Database, mail, folder, account services +Wino.Authentication → OAuth2 authenticators (Outlook, Gmail) +Wino.Mail.ViewModels → Mail-specific ViewModels +Wino.Core.ViewModels → Shared ViewModels (settings, personalization) +Wino.Messaging → Pub-sub message definitions +Wino.Mail.WinUI → **Active WinUI 3 UI project** (use this) +Wino.Mail → **Deprecated UWP project** (DO NOT EDIT) +``` + +### Mail Synchronization Flow +1. **WinoRequestDelegator** → Validates and delegates user actions (mark read, delete, move) +2. **WinoRequestProcessor** → Batches requests using RequestComparer, queues to synchronizers +3. **Synchronizers** (OutlookSynchronizer, GmailSynchronizer, ImapSynchronizer) → Execute batched operations +4. **ChangeProcessors** → Apply changes to local SQLite database +5. Database updates trigger **Messenger** events (MailAddedMessage, MailUpdatedMessage, etc.) + +### Synchronizer Types +- **OutlookSynchronizer** - Microsoft Graph SDK for Office 365 +- **GmailSynchronizer** - Gmail API +- **ImapSynchronizer** - MimeKit/MailKit for IMAP/SMTP + +### Queue-Based Sync Pattern +- Initial sync queues mail IDs first (MailItemQueue table), downloads metadata only +- MIME content downloaded on-demand when user opens mail +- Check `MailItemFolder.IsInitialSyncCompleted` for sync state +- See QUEUE_SYNC_IMPLEMENTATION.md for details + +### Dependency Injection +- `RegisterCoreServices()` in Wino.Core/CoreContainerSetup.cs +- `RegisterSharedServices()` in Wino.Services/ServicesContainerSetup.cs +- ViewModels registered in App.xaml.cs + +## Key Patterns + +### MVVM with Source Generators +**CORRECT - use public partial properties:** +```csharp +[ObservableProperty] +public partial string SearchQuery { get; set; } = string.Empty; +``` + +**WRONG - will not work:** +```csharp +[ObservableProperty] +private string searchQuery = string.Empty; +``` + +### Messenger Pattern +- ViewModels inherit from CoreBaseViewModel or MailBaseViewModel +- Register handlers in `RegisterRecipients()`, unregister in `UnregisterRecipients()` +- Send via `WeakReferenceMessenger.Default.Send(new MessageType(...))` + +### Data Binding - No Converters +- **NEVER** create IValueConverter classes +- WinUI 3 auto-converts bool to Visibility: `Visibility="{x:Bind IsVisible, Mode=OneWay}"` +- Use XamlHelpers for complex conversions: `{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(Prop)}` +- `x:Bind` does not implicitly convert `double` to `GridLength`; when binding `RowDefinition.Height` or `ColumnDefinition.Width`, use a `XamlHelpers` method such as `DoubleToGridLength(...)` +- For `ComboBox` controls in XAML, never use `DisplayMemberPath` or `SelectedValuePath`; use a typed `ItemTemplate` and bind `SelectedItem` explicitly, preferably with `x:Bind` + +## Localization + +1. Add English strings ONLY to Wino.Core.Domain/Translations/en_US/resources.json +2. Build project - source generators create Translator properties +3. Use Translator.{PropertyName} in code/XAML +4. NEVER edit any resources.json file outside Wino.Core.Domain/Translations/en_US/resources.json +5. Treat all non-en_US translation files as managed externally and leave them untouched, even when adding new localization keys + +## Storage + +- **SQLite database** in publisher cache folder (shared with future Wino Calendar) +- **EML files** in app local storage, referenced by `MailCopy.FileId` +- Paths resolved via `MimeFileService.GetMimeMessagePath()` + +## WebView2 Mail Rendering + +- `reader.html` for reading mails, `editor.html` for composing (Jodit editor) +- Virtual host mapping: `https://wino.mail/reader.html` +- JavaScript interop via `ExecuteScriptFunctionAsync()` +- MIME content downloaded on-demand, not during sync + +## Common Pitfalls + +- Forgetting to register ViewModels in App.xaml.cs `RegisterViewModels()` +- Not calling `RegisterRecipients()` for message handlers +- Using private fields with `[ObservableProperty]` instead of public partial +- Creating IValueConverter classes instead of using XamlHelpers +- Editing UWP project files instead of WinUI equivalents +- Hardcoding strings instead of using Translator +- Forgetting to unregister Messenger recipients (memory leaks) +- Putting authentication validation, token refresh, account API calls, settings serialization/deserialization, or preference-application logic into ViewModels instead of the corresponding service + +## Code Style + +- Avoid introducing new NuGet packages when possible +- Use existing libraries (MimeKit, MailKit, Microsoft Graph, Gmail API) +- Use `var` where type is obvious +- String interpolation over string.Format +- Wrap async operations in try-catch +- Log errors via IWinoLogger +- For dependency properties in WinUI code, always prefer `[GeneratedDependencyProperty]` from CommunityToolkit over manual `DependencyProperty.Register(...)` declarations. +- When a `[RelayCommand]` needs enable/disable logic, prefer the command's `CanExecute` over binding `Button.IsEnabled` in XAML; use `[NotifyCanExecuteChangedFor]` on dependent properties and call `NotifyCanExecuteChanged()` explicitly when non-generated state affects the command. +- In ViewModels, update all UI-bound properties/collections via `ExecuteUIThread(...)` (especially after awaited calls and any use of `ConfigureAwait(false)`). +- ViewModels should only handle UI interaction/state and delegate business logic to services; account-management work belongs in `WinoAccountProfileService`, and preferences import/export/apply logic belongs in `PreferencesService`. +- In `EventDetailsPageViewModel.LoadAttendeesAsync`, never mutate `CurrentEvent.Attendees` outside `ExecuteUIThread(...)`. +- Never create pure C# controls or controls that heavily manipulate UI structure from `.cs` files. Define controls in XAML and keep UI composition in XAML. +- Never add XAML-backed UI controls to `.xaml.cs`. If a view has XAML, all control declarations, flyouts, templates, and visual composition belong in the `.xaml` file; keep `.xaml.cs` limited to event handling and view glue. +- Never subscribe to framework events like `Loaded`, `Unloaded`, or input events from constructors in `.xaml.cs` for XAML-backed controls and pages; wire them directly in XAML instead. +- If you use `x:Load` in XAML, always give that `UIElement` an `x:Name`. + + + diff --git a/Directory.Packages.props b/Directory.Packages.props index c6262929..e0dee889 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,66 +3,78 @@ true - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - + - - - - - - - + + + + + + + + - - - + + + - - - + + + - + - - - + + + + - - - + + + - - - - + + + + + - + - - - + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 93f88c5c..a4dd1da0 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,54 @@ -

- - + + Wino Mail logo

Wino Mail

- Native mail client for Windows device families. + Native mail and calendar client for Windows.


-![pdark](https://user-images.githubusercontent.com/12009960/232114528-2d2c8e3c-dbe7-429a-94e0-6aecc73bdf70.png) +![Wino Mail screenshot](https://user-images.githubusercontent.com/12009960/232114528-2d2c8e3c-dbe7-429a-94e0-6aecc73bdf70.png) ## Motivation -I'm a big fan of Windows Mail & Calendars due to its simplicity. Personally, I find it more intuitive for daily use cases compared to Outlook desktop and the new WebView2 powered Outlook version. Seeing [Microsoft deprecating it](https://support.microsoft.com/en-us/office/outlook-for-windows-the-future-of-mail-calendar-and-people-on-windows-11-715fc27c-e0f4-4652-9174-47faa751b199#:~:text=The%20Mail%20and%20Calendar%20applications,will%20no%20longer%20be%20supported.) dragged me into starting to work on Wino a couple of years ago. Wino's main motivation is to bring all the existing functionality from Mail & Calendars over time without changing the user experience that millions have loved since the Windows 8 days in Mail & Calendars +I'm a big fan of Windows Mail & Calendars due to its simplicity. Personally, I find it more intuitive for daily use cases compared to Outlook desktop and the new WebView2 powered Outlook version. Seeing [Microsoft deprecating it](https://support.microsoft.com/en-us/office/outlook-for-windows-the-future-of-mail-calendar-and-people-on-windows-11-715fc27c-e0f4-4652-9174-47faa751b199#:~:text=The%20Mail%20and%20Calendar%20applications,will%20no%20longer%20be%20supported.) dragged me into starting to work on Wino a couple of years ago. Wino's main motivation is to bring all the existing functionality from Mail & Calendars over time without changing the user experience that millions have loved since the Windows 8 days in Mail & Calendars. + +## vNext Release Highlights + +Wino vNext focuses on making Mail, Calendar, and Contacts feel like one cohesive native Windows experience while improving sync reliability and startup responsiveness. + +- 📅 **Calendar management:** Event compose/create flow, calendar-mail mapping, reminder snooze support, occurrence and detail-page improvements, and CalDAV correctness fixes. +- 👥 **Contact management:** Improved contact workflows, account/settings integration, and contact data-model cleanup. +- 🔄 **Synchronization reliability:** Refactored synchronizers, better state handling, 404 + 429 error handling, and duplicate-operation prevention. +- ✉️ **Compose and drafts:** Refined editor/toolbar architecture, better rendering pipeline, Gmail draft support, and large Outlook attachment upload sessions. +- ⚡ **Performance and quality:** Faster mail fetching with batched DB queries and caching, SQLite indexing/foreign key enforcement, and broader test + CI coverage. +- 🎨 **WinUI polish:** Improved onboarding/startup, settings and dialogs refresh, notification routing fixes, and keyboard/navigation quality-of-life improvements. ## Features -- API integration for Outlook and Gmail -- IMAP/SMTP support for custom mail servers -- Send, receive, mark as (read,important,spam etc), move mails. -- Linked/Merged Accounts -- Toast notifications with background sync. -- Instant startup performance -- Offline use / search. -- Modern and responsive UI -- Lots of personalization options -- Dark / Light mode for mail reader +- 📨 Outlook and Gmail API integration +- 🌐 IMAP/SMTP support for custom mail servers +- 📅 Calendar support with event creation/compose and reminders +- 👥 Contact management and people-centric account experience +- ✅ Core mail actions: send, receive, read/unread, move, spam, and more +- 🔗 Linked/Merged accounts +- 🔔 Toast notifications with background sync +- ⚡ Instant startup-oriented architecture +- 🔎 Offline-capable workflows and search improvements +- 🎛️ Modern responsive WinUI interface with personalization options +- 🌗 Dark/Light mode for mail reader and app surfaces ## Download Download latest version of Wino Mail from Microsoft Store for free. - - + + Get Wino Mail from Microsoft Store ## Beta Releases @@ -48,7 +57,6 @@ Stable releases will always be distributed on Microsoft Store. However, beta rel These releases are distributed as side-loaded packages. To install them, download the **.msixbundle** file in GitHub releases and [follow the steps explained here.](https://learn.microsoft.com/en-us/windows/application-management/sideload-apps-in-windows) - ## Contributing Check out the [contribution guidelines](/CONTRIBUTING.md) before diving into the source code or opening an issue. There are multiple ways to contribute and all of them are explained in detail there. @@ -59,3 +67,4 @@ Your donations will motivate me more to work on Wino in my spare time and cover - You can [donate via Paypal by clicking here](https://www.paypal.com/donate/?hosted_button_id=LGPERGGXFMQ7U) - You can buy Unlimited Accounts add-on in the application. It's a one-time payment for lifetime, not a monthly recurring payment. + diff --git a/RELEASE_NOTES_vNext.md b/RELEASE_NOTES_vNext.md new file mode 100644 index 00000000..3625fe02 --- /dev/null +++ b/RELEASE_NOTES_vNext.md @@ -0,0 +1,158 @@ +# Wino Mail vNext Improvements + +This document summarizes the major improvements on `feature/vNext` compared to `main`, based on the commit history between the current branch and the merge-base with `main`. + +## Wino Calendar + +Calendar has grown from an early implementation into a much more complete product area on this branch. + +### A full Wino Calendar experience + +- Added a dedicated Wino Calendar app entry, making calendar a first-class experience instead of a secondary add-on. +- Built out the calendar rendering experience with multiple rounds of rendering improvements, updated calendar view styling, calendar buttons, and better event visuals. +- Added event creation and full event compose flows, including follow-up improvements for attachments, attendees, recurrence summaries, RSVP actions, reminders, and event details. +- Improved support for all-day events, better display dates, occurrence handling, and mail-to-calendar mapping so calendar actions connect more naturally with messages and invitations. + +### Local calendar support + +- Added local calendar operation coverage and supporting behavior for IMAP-backed/local calendar scenarios. +- Prevented duplicate operations by ignoring local calendar apply-changes in the wrong paths. +- Added busy-state support and metadata fetch flows so newly created accounts can initialize calendar data more reliably. + +### CalDAV sync + +- Introduced a dedicated CalDAV synchronizer and supporting service/client work. +- Fixed CalDAV delta sync issues. +- Fixed CalDAV timezone issues. +- Added manual live CalDAV workflow tests to validate real-world sync behavior. + +This means local and self-hosted calendar scenarios are much better represented on this branch than on `main`. + +### API calendar sync for Outlook and Gmail + +- Expanded Outlook calendar sync behavior, including broader sync windows and fixes around date/time handling. +- Improved Gmail drafting and mail/calendar integration so event-related actions work better across providers. +- Added mail and calendar synchronizer state tracking to make sync progress and error handling more reliable. +- Added auto calendar sync on account creation and broader auto-sync trigger and cancellation support. + +### Calendar polish and reliability + +- Fixed calendar crashes and null-handling issues in calendar view date range updates. +- Fixed double initialization in calendar day views. +- Improved reaction to calendar changes and calendar item update-source handling. +- Added reminder snooze support across toast UI, services, and database storage. + +Overall, Wino Calendar is one of the biggest themes of this branch: richer UI, more complete event workflows, and real sync support across local, CalDAV, Outlook, and Gmail-backed scenarios. + +## Wino Accounts + +Wino Accounts was significantly expanded and polished on this branch. + +### Account flows and identity + +- Added sign in, sign out, and registration flows. +- Redesigned login and registration dialogs. +- Added privacy policy presentation during registration. +- Added forgot password and email confirmation flows. +- Pointed the app to the real API and improved profile caching. + +### Account management and settings + +- Added Wino account settings and a dedicated management page. +- Added a special navigation item for Wino Accounts. +- Added import functionality for Wino Accounts. +- Added a preference to hide the title bar Wino account button. +- Improved the top-shell account icon and signed-out identity visuals. + +### Purchases and add-ons + +- Added handling for Paddle purchases and add-ons. +- Added purchase-success deep linking. +- Added support for AI pack handling through the Microsoft Store. + +### User-facing polish + +- Redesigned the Wino Account flyout and menu with a more polished Fluent-style presentation. +- Improved account cleanup behavior when an account is deleted. +- Added account attention handling and better account details/settings behavior. + +Compared to `main`, this branch turns Wino Accounts into a much more complete platform feature rather than a minimal sign-in surface. + +## Improved Stability and Reliability + +A large part of this branch is about making the app more dependable in everyday use. + +### Synchronization stability + +- Refactored synchronizers to address long-standing reliability issues. +- Improved thread mapping across synchronizers. +- Added generic 404 handling for synchronizers. +- Added specific Outlook 429 handling for rate-limit scenarios. +- Improved Outlook authentication and Outlook sync reliability. +- Improved Gmail synchronizer behavior. +- Added explicit mail and calendar synchronizer state support. + +### Mail and data reliability + +- Optimized mail fetching with batched database queries and in-memory caching. +- Added SQLite indexes and enabled foreign key enforcement. +- Switched away from the old mail item queue approach and returned to a simpler initial sync strategy. +- Improved local draft resend behavior and added grace-period handling for local drafts. +- Added better handling for large Outlook attachments via upload sessions. +- Fixed issues with sent/draft placement, loading mails with infinite scroll, selection cleanup, and deleted-object scenarios. + +### UI and lifecycle stability + +- Fixed mail rendering page disposal issues. +- Fixed WebView2 runtime toast dispatching on the UI thread. +- Fixed startup mode issues, single-instancing problems, and shell/navigation regressions. +- Fixed multiple thread selection, container, flicker, and context-menu issues. +- Fixed crashes and null-reference style issues in several calendar and shell flows. + +### Engineering quality + +- Added more tests across calendar, CalDAV, IMAP, view-model, sanitization, and account sync scenarios. +- Added a GitHub Actions workflow to build WinUI and run Core tests on pull requests. +- Resolved warnings and moved the WinUI project toward warnings-as-errors discipline. +- Added AOT compatibility work and related cleanup across the app. + +The branch is not just adding features; it is also clearly reducing failure points throughout sync, rendering, navigation, and storage. + +## Contacts, Settings, and General UX + +This branch also improves the everyday product experience outside mail and calendar core flows. + +### Contacts + +- Added contacts management. +- Improved contacts UI and related thread/image preview behavior. +- Removed legacy SQLite base64 contact storage from `AccountContact`. +- Added contact picture handling support and supporting contact service improvements. + +### Settings + +- Added a dedicated settings shell and refactored settings home/navigation. +- Expanded settings UI and introduced new setting options. +- Added calendar settings into the settings experience. +- Improved account details/settings pages and storage settings navigation. +- Refined settings visuals, shell integration, and menu behavior. + +### Onboarding and app experience + +- Added a new startup window and a more guided onboarding flow with wizard-like steps. +- Added a "What's New" implementation for feature communication. +- Improved dialogs, title bar behavior, shell content, navigation, and shell polish across multiple iterations. +- Added live store update notifications. +- Improved keyboard shortcuts and related dialogs. +- Added tray icon support and better toast routing between mail and calendar app entries. + +## Summary + +Compared to `main`, `feature/vNext` delivers four major leaps: + +1. Wino Calendar becomes a substantially more complete feature set, including local calendar support, CalDAV sync, and stronger Outlook and Gmail calendar integration. +2. Wino Accounts becomes a real product surface with better authentication flows, management, imports, purchases, and polish. +3. The app is more stable thanks to synchronization refactors, storage improvements, test expansion, and many crash and lifecycle fixes. +4. Contacts, settings, onboarding, and shell/navigation experience all feel more mature and more consistent. + +In short, this branch is a broad product maturation release rather than a narrow feature drop. diff --git a/Wino.Authentication/OutlookAuthenticator.cs b/Wino.Authentication/OutlookAuthenticator.cs index 10bc5f26..156cc321 100644 --- a/Wino.Authentication/OutlookAuthenticator.cs +++ b/Wino.Authentication/OutlookAuthenticator.cs @@ -24,12 +24,14 @@ public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator public override MailProviderType ProviderType => MailProviderType.Outlook; private readonly IPublicClientApplication _publicClientApplication; + private readonly INativeAppService _nativeAppService; private readonly IApplicationConfiguration _applicationConfiguration; public OutlookAuthenticator(INativeAppService nativeAppService, IApplicationConfiguration applicationConfiguration, IAuthenticatorConfig authenticatorConfig) : base(authenticatorConfig) { + _nativeAppService = nativeAppService; _applicationConfiguration = applicationConfiguration; var authenticationRedirectUri = nativeAppService.GetWebAuthenticationBrokerUri(); @@ -40,11 +42,25 @@ public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator ListOperatingSystemAccounts = true, }; - var outlookAppBuilder = PublicClientApplicationBuilder.Create(AuthenticatorConfig.OutlookAuthenticatorClientId) - .WithParentActivityOrWindow(nativeAppService.GetCoreWindowHwnd) - .WithBroker(options) - .WithDefaultRedirectUri() - .WithAuthority(Authority); + PublicClientApplicationBuilder outlookAppBuilder = null; + + // Being created from an app notification. + // This is where we avoid all interactive shit for authentication. + if (nativeAppService.GetCoreWindowHwnd == null) + { + outlookAppBuilder = PublicClientApplicationBuilder.Create(AuthenticatorConfig.OutlookAuthenticatorClientId) + .WithDefaultRedirectUri() + .WithBroker(options) + .WithAuthority(Authority); + } + else + { + outlookAppBuilder = PublicClientApplicationBuilder.Create(AuthenticatorConfig.OutlookAuthenticatorClientId) + .WithBroker(options) + .WithParentActivityOrWindow(_nativeAppService.GetCoreWindowHwnd) + .WithDefaultRedirectUri() + .WithAuthority(Authority); + } _publicClientApplication = outlookAppBuilder.Build(); } @@ -82,7 +98,8 @@ public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator catch (MsalUiRequiredException) { // Somehow MSAL is not able to refresh the token silently. - // Force interactive login. + // Force interactive login which will include calendar scopes. + // The calling code should update account.IsCalendarAccessGranted = true after successful authentication. return await GenerateTokenInformationAsync(account); } @@ -98,7 +115,13 @@ public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator { await EnsureTokenCacheAttachedAsync(); - var authResult = await _publicClientApplication + // Interactive authentication required but window doesn't exist. + // This can happen when being called from a notification background task and the token is expired. + // Force account attention; + + if (_nativeAppService.GetCoreWindowHwnd == null) throw new AuthenticationAttentionException(account); + + AuthenticationResult authResult = await _publicClientApplication .AcquireTokenInteractive(Scope) .ExecuteAsync(); diff --git a/Wino.Authentication/Wino.Authentication.csproj b/Wino.Authentication/Wino.Authentication.csproj index 3c7c77ce..86d3b5e3 100644 --- a/Wino.Authentication/Wino.Authentication.csproj +++ b/Wino.Authentication/Wino.Authentication.csproj @@ -1,11 +1,14 @@  - net9.0 + net10.0 win-x86;win-x64;win-arm64 Wino.Authentication x86;x64;arm64 true true + true + true + true diff --git a/Wino.BackgroundTasks/AppUpdatedTask.cs b/Wino.BackgroundTasks/AppUpdatedTask.cs deleted file mode 100644 index 1a70899f..00000000 --- a/Wino.BackgroundTasks/AppUpdatedTask.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Microsoft.Toolkit.Uwp.Notifications; -using Windows.ApplicationModel; -using Windows.ApplicationModel.Background; - -namespace Wino.BackgroundTasks -{ - /// - /// Creates a toast notification to notify user when the Store update happens. - /// - public sealed class AppUpdatedTask : IBackgroundTask - { - public void Run(IBackgroundTaskInstance taskInstance) - { - var def = taskInstance.GetDeferral(); - - var builder = new ToastContentBuilder(); - builder.SetToastScenario(ToastScenario.Default); - - Package package = Package.Current; - PackageId packageId = package.Id; - PackageVersion version = packageId.Version; - - var versionText = string.Format("{0}.{1}.{2}.{3}", version.Major, version.Minor, version.Build, version.Revision); - - // TODO: Handle with Translator, but it's not initialized here yet. - builder.AddText("Wino Mail is updated!"); - builder.AddText(string.Format("New version {0} is ready.", versionText)); - - builder.Show(); - - def.Complete(); - } - } -} diff --git a/Wino.BackgroundTasks/Properties/AssemblyInfo.cs b/Wino.BackgroundTasks/Properties/AssemblyInfo.cs deleted file mode 100644 index 9887885d..00000000 --- a/Wino.BackgroundTasks/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Wino.BackgroundTasks")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Wino.BackgroundTasks")] -[assembly: AssemblyCopyright("Copyright © 2023")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] -[assembly: ComVisible(false)] \ No newline at end of file diff --git a/Wino.BackgroundTasks/Wino.BackgroundTasks.csproj b/Wino.BackgroundTasks/Wino.BackgroundTasks.csproj deleted file mode 100644 index 67f9296c..00000000 --- a/Wino.BackgroundTasks/Wino.BackgroundTasks.csproj +++ /dev/null @@ -1,129 +0,0 @@ - - - - - Debug - AnyCPU - {D9EF0F59-F5F2-4D6C-A5BA-84043D8F3E08} - winmdobj - Properties - Wino.BackgroundTasks - Wino.BackgroundTasks - en-US - UAP - 10.0.22621.0 - 10.0.17763.0 - 14 - 512 - {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - false - - - x86 - true - bin\x86\Debug\ - DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP - ;2008 - full - false - prompt - - - x86 - bin\x86\Release\ - TRACE;NETFX_CORE;WINDOWS_UWP - true - ;2008 - pdbonly - false - prompt - - - ARM64 - true - bin\ARM64\Debug\ - DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP - ;2008 - full - false - prompt - - - ARM64 - bin\ARM64\Release\ - TRACE;NETFX_CORE;WINDOWS_UWP - true - ;2008 - pdbonly - false - prompt - - - x64 - true - bin\x64\Debug\ - DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP - ;2008 - full - false - prompt - - - x64 - bin\x64\Release\ - TRACE;NETFX_CORE;WINDOWS_UWP - true - ;2008 - pdbonly - false - prompt - - - PackageReference - - - - - - - - 4.66.2 - - - 6.2.14 - - - 7.1.3 - - - - - {CF3312E5-5DA0-4867-9945-49EA7598AF1F} - Wino.Core.Domain - - - {395f19ba-1e42-495c-9db5-1a6f537fccb8} - Wino.Core.UWP - - - {e6b1632a-8901-41e8-9ddf-6793c7698b0b} - Wino.Core - - - - - Windows Desktop Extensions for the UWP - - - - 14.0 - - - - \ No newline at end of file diff --git a/Wino.Calendar.ViewModels/AccountDetailsPageViewModel.cs b/Wino.Calendar.ViewModels/AccountDetailsPageViewModel.cs deleted file mode 100644 index a110d386..00000000 --- a/Wino.Calendar.ViewModels/AccountDetailsPageViewModel.cs +++ /dev/null @@ -1,37 +0,0 @@ -using CommunityToolkit.Mvvm.Input; -using CommunityToolkit.Mvvm.Messaging; -using Wino.Calendar.ViewModels.Interfaces; -using Wino.Core.Domain; -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Navigation; -using Wino.Core.ViewModels; -using Wino.Mail.ViewModels.Data; -using Wino.Messaging.Client.Navigation; - -namespace Wino.Calendar.ViewModels; - -public partial class AccountDetailsPageViewModel : CalendarBaseViewModel -{ - private readonly IAccountService _accountService; - - public AccountProviderDetailViewModel Account { get; private set; } - public ICalendarDialogService CalendarDialogService { get; } - public IAccountCalendarStateService AccountCalendarStateService { get; } - - public AccountDetailsPageViewModel(ICalendarDialogService calendarDialogService, IAccountService accountService, IAccountCalendarStateService accountCalendarStateService) - { - CalendarDialogService = calendarDialogService; - _accountService = accountService; - AccountCalendarStateService = accountCalendarStateService; - } - - [RelayCommand] - private void EditAccountDetails() - => Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsEditAccountDetails_Title, WinoPage.EditAccountDetailsPage, Account)); - - public override void OnNavigatedTo(NavigationMode mode, object parameters) - { - base.OnNavigatedTo(mode, parameters); - } -} diff --git a/Wino.Calendar.ViewModels/AccountManagementViewModel.cs b/Wino.Calendar.ViewModels/AccountManagementViewModel.cs deleted file mode 100644 index f7eb9563..00000000 --- a/Wino.Calendar.ViewModels/AccountManagementViewModel.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.Input; -using Wino.Core.Domain; -using Wino.Core.Domain.Entities.Shared; -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Exceptions; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Authentication; -using Wino.Core.Domain.Models.Navigation; -using Wino.Core.Domain.Models.Synchronization; -using Wino.Core.ViewModels; -using Wino.Messaging.Server; - -namespace Wino.Calendar.ViewModels; - -public partial class AccountManagementViewModel : AccountManagementPageViewModelBase -{ - private readonly IProviderService _providerService; - - public AccountManagementViewModel(ICalendarDialogService dialogService, - IWinoServerConnectionManager winoServerConnectionManager, - INavigationService navigationService, - IAccountService accountService, - IProviderService providerService, - IStoreManagementService storeManagementService, - IAuthenticationProvider authenticationProvider, - IPreferencesService preferencesService) : base(dialogService, winoServerConnectionManager, navigationService, accountService, providerService, storeManagementService, authenticationProvider, preferencesService) - { - CalendarDialogService = dialogService; - _providerService = providerService; - } - - public ICalendarDialogService CalendarDialogService { get; } - - public override async void OnNavigatedTo(NavigationMode mode, object parameters) - { - base.OnNavigatedTo(mode, parameters); - - await InitializeAccountsAsync(); - } - - public override async Task InitializeAccountsAsync() - { - Accounts.Clear(); - - var accounts = await AccountService.GetAccountsAsync().ConfigureAwait(false); - - await ExecuteUIThread(() => - { - foreach (var account in accounts) - { - var accountDetails = GetAccountProviderDetails(account); - - Accounts.Add(accountDetails); - } - }); - - await ManageStorePurchasesAsync().ConfigureAwait(false); - } - - [RelayCommand] - private async Task AddNewAccountAsync() - { - if (IsAccountCreationBlocked) - { - var isPurchaseClicked = await DialogService.ShowConfirmationDialogAsync(Translator.DialogMessage_AccountLimitMessage, Translator.DialogMessage_AccountLimitTitle, Translator.Buttons_Purchase); - - if (!isPurchaseClicked) return; - - await PurchaseUnlimitedAccountAsync(); - - return; - } - - var availableProviders = _providerService.GetAvailableProviders(); - - var accountCreationDialogResult = await DialogService.ShowAccountProviderSelectionDialogAsync(availableProviders); - - if (accountCreationDialogResult == null) return; - - var accountCreationCancellationTokenSource = new CancellationTokenSource(); - var accountCreationDialog = CalendarDialogService.GetAccountCreationDialog(accountCreationDialogResult); - - await accountCreationDialog.ShowDialogAsync(accountCreationCancellationTokenSource); - await Task.Delay(500); - - // For OAuth authentications, we just generate token and assign it to the MailAccount. - - var createdAccount = new MailAccount() - { - ProviderType = accountCreationDialogResult.ProviderType, - Name = accountCreationDialogResult.AccountName, - Id = Guid.NewGuid() - }; - - var tokenInformationResponse = await WinoServerConnectionManager - .GetResponseAsync(new AuthorizationRequested(accountCreationDialogResult.ProviderType, - createdAccount, - createdAccount.ProviderType == MailProviderType.Gmail), accountCreationCancellationTokenSource.Token); - - if (accountCreationDialog.State == AccountCreationDialogState.Canceled) - throw new AccountSetupCanceledException(); - - tokenInformationResponse.ThrowIfFailed(); - - await AccountService.CreateAccountAsync(createdAccount, null); - - // Sync profile information if supported. - if (createdAccount.IsProfileInfoSyncSupported) - { - // Start profile information synchronization. - // It's only available for Outlook and Gmail synchronizers. - - var profileSyncOptions = new MailSynchronizationOptions() - { - AccountId = createdAccount.Id, - Type = MailSynchronizationType.UpdateProfile - }; - - var profileSynchronizationResponse = await WinoServerConnectionManager.GetResponseAsync(new NewMailSynchronizationRequested(profileSyncOptions, SynchronizationSource.Client)); - - var profileSynchronizationResult = profileSynchronizationResponse.Data; - - if (profileSynchronizationResult.CompletedState != SynchronizationCompletedState.Success) - throw new Exception(Translator.Exception_FailedToSynchronizeProfileInformation); - - createdAccount.SenderName = profileSynchronizationResult.ProfileInformation.SenderName; - createdAccount.Base64ProfilePictureData = profileSynchronizationResult.ProfileInformation.Base64ProfilePictureData; - - await AccountService.UpdateProfileInformationAsync(createdAccount.Id, profileSynchronizationResult.ProfileInformation); - } - - accountCreationDialog.State = AccountCreationDialogState.FetchingEvents; - - // Start synchronizing events. - var synchronizationOptions = new CalendarSynchronizationOptions() - { - AccountId = createdAccount.Id, - Type = CalendarSynchronizationType.CalendarMetadata - }; - - var synchronizationResponse = await WinoServerConnectionManager.GetResponseAsync(new NewCalendarSynchronizationRequested(synchronizationOptions, SynchronizationSource.Client)); - } -} diff --git a/Wino.Calendar.ViewModels/AppShellViewModel.cs b/Wino.Calendar.ViewModels/AppShellViewModel.cs deleted file mode 100644 index a7454eb5..00000000 --- a/Wino.Calendar.ViewModels/AppShellViewModel.cs +++ /dev/null @@ -1,366 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using CommunityToolkit.Mvvm.Messaging; -using Serilog; -using Wino.Calendar.ViewModels.Data; -using Wino.Calendar.ViewModels.Interfaces; -using Wino.Core.Domain.Collections; -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Extensions; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Calendar; -using Wino.Core.Domain.Models.Navigation; -using Wino.Core.Domain.Models.Synchronization; -using Wino.Core.ViewModels; -using Wino.Messaging.Client.Calendar; -using Wino.Messaging.Client.Navigation; -using Wino.Messaging.Server; - -namespace Wino.Calendar.ViewModels; - -public partial class AppShellViewModel : CalendarBaseViewModel, - IRecipient, - IRecipient, - IRecipient, - IRecipient, - IRecipient -{ - public IPreferencesService PreferencesService { get; } - public IStatePersistanceService StatePersistenceService { get; } - public IAccountCalendarStateService AccountCalendarStateService { get; } - public INavigationService NavigationService { get; } - public IWinoServerConnectionManager ServerConnectionManager { get; } - - [ObservableProperty] - private bool _isEventDetailsPageActive; - - [ObservableProperty] - private int _selectedMenuItemIndex = -1; - - [ObservableProperty] - private bool isCalendarEnabled; - - /// - /// Gets or sets the active connection status of the Wino server. - /// - [ObservableProperty] - private WinoServerConnectionStatus activeConnectionStatus; - - /// - /// Gets or sets the display date of the calendar. - /// - [ObservableProperty] - private DateTimeOffset _displayDate; - - /// - /// Gets or sets the highlighted range in the CalendarView and displayed date range in FlipView. - /// - [ObservableProperty] - private DateRange highlightedDateRange; - - [ObservableProperty] - private ObservableRangeCollection dateNavigationHeaderItems = []; - - [ObservableProperty] - private int _selectedDateNavigationHeaderIndex; - - public bool IsVerticalCalendar => StatePersistenceService.CalendarDisplayType == CalendarDisplayType.Month; - - // For updating account calendars asynchronously. - private SemaphoreSlim _accountCalendarUpdateSemaphoreSlim = new(1); - - public AppShellViewModel(IPreferencesService preferencesService, - IStatePersistanceService statePersistanceService, - IAccountService accountService, - ICalendarService calendarService, - IAccountCalendarStateService accountCalendarStateService, - INavigationService navigationService, - IWinoServerConnectionManager serverConnectionManager) - { - _accountService = accountService; - _calendarService = calendarService; - - AccountCalendarStateService = accountCalendarStateService; - AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested; - AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged; - - NavigationService = navigationService; - ServerConnectionManager = serverConnectionManager; - PreferencesService = preferencesService; - - StatePersistenceService = statePersistanceService; - StatePersistenceService.StatePropertyChanged += PrefefencesChanged; - } - - private void SelectedCalendarItemsChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) - { - throw new NotImplementedException(); - } - - private void PrefefencesChanged(object sender, string e) - { - if (e == nameof(StatePersistenceService.CalendarDisplayType)) - { - Messenger.Send(new CalendarDisplayTypeChangedMessage(StatePersistenceService.CalendarDisplayType)); - - - // Change the calendar. - DateClicked(new CalendarViewDayClickedEventArgs(GetDisplayTypeSwitchDate())); - } - } - - public override async void OnNavigatedTo(NavigationMode mode, object parameters) - { - base.OnNavigatedTo(mode, parameters); - - UpdateDateNavigationHeaderItems(); - - await InitializeAccountCalendarsAsync(); - - TodayClicked(); - } - - private async void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e) - { - // When using three-state checkbox, multiple accounts will be selected/unselected at the same time. - // Reporting all these changes one by one to the UI is not efficient and may cause problems in the future. - - // Update all calendar states at once. - try - { - await _accountCalendarUpdateSemaphoreSlim.WaitAsync(); - - foreach (var calendar in e.AccountCalendars) - { - await _calendarService.UpdateAccountCalendarAsync(calendar.AccountCalendar).ConfigureAwait(false); - } - } - catch (Exception ex) - { - Log.Error(ex, "Error while waiting for account calendar update semaphore."); - } - finally - { - _accountCalendarUpdateSemaphoreSlim.Release(); - } - } - - private async void UpdateAccountCalendarRequested(object sender, AccountCalendarViewModel e) - => await _calendarService.UpdateAccountCalendarAsync(e.AccountCalendar).ConfigureAwait(false); - - private async Task InitializeAccountCalendarsAsync() - { - await Dispatcher.ExecuteOnUIThread(() => AccountCalendarStateService.ClearGroupedAccountCalendar()); - - var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false); - - foreach (var account in accounts) - { - var accountCalendars = await _calendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false); - var calendarViewModels = new List(); - - foreach (var calendar in accountCalendars) - { - var calendarViewModel = new AccountCalendarViewModel(account, calendar); - - calendarViewModels.Add(calendarViewModel); - } - - var groupedAccountCalendarViewModel = new GroupedAccountCalendarViewModel(account, calendarViewModels); - - await Dispatcher.ExecuteOnUIThread(() => - { - AccountCalendarStateService.AddGroupedAccountCalendar(groupedAccountCalendarViewModel); - }); - } - } - - private void ForceNavigateCalendarDate() - { - if (SelectedMenuItemIndex == -1) - { - var args = new CalendarPageNavigationArgs() - { - NavigationDate = _navigationDate ?? DateTime.Now.Date - }; - - // Already on calendar. Just navigate. - NavigationService.Navigate(WinoPage.CalendarPage, args); - - _navigationDate = null; - } - else - { - SelectedMenuItemIndex = -1; - } - } - - partial void OnSelectedMenuItemIndexChanged(int oldValue, int newValue) - { - switch (newValue) - { - case -1: - ForceNavigateCalendarDate(); - break; - case 0: - NavigationService.Navigate(WinoPage.ManageAccountsPage); - break; - case 1: - NavigationService.Navigate(WinoPage.SettingsPage); - break; - default: - break; - } - } - - [RelayCommand] - private async Task Sync() - { - // Sync all calendars. - var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false); - - foreach (var account in accounts) - { - var t = new NewCalendarSynchronizationRequested(new CalendarSynchronizationOptions() - { - AccountId = account.Id, - Type = CalendarSynchronizationType.CalendarMetadata - }, SynchronizationSource.Client); - - Messenger.Send(t); - } - } - - /// - /// When calendar type switches, we need to navigate to the most ideal date. - /// This method returns that date. - /// - private DateTime GetDisplayTypeSwitchDate() - { - var settings = PreferencesService.GetCurrentCalendarSettings(); - switch (StatePersistenceService.CalendarDisplayType) - { - case CalendarDisplayType.Day: - if (HighlightedDateRange.IsInRange(DateTime.Now)) return DateTime.Now.Date; - - return HighlightedDateRange.StartDate; - case CalendarDisplayType.Week: - if (HighlightedDateRange == null || HighlightedDateRange.IsInRange(DateTime.Now)) - { - return DateTime.Now.Date.GetWeekStartDateForDate(settings.FirstDayOfWeek); - } - - return HighlightedDateRange.StartDate.GetWeekStartDateForDate(settings.FirstDayOfWeek); - case CalendarDisplayType.WorkWeek: - break; - case CalendarDisplayType.Month: - break; - case CalendarDisplayType.Year: - break; - default: - break; - } - - return DateTime.Today.Date; - } - - private DateTime? _navigationDate; - private readonly IAccountService _accountService; - private readonly ICalendarService _calendarService; - - #region Commands - - [RelayCommand] - private void TodayClicked() - { - _navigationDate = DateTime.Now.Date; - - ForceNavigateCalendarDate(); - } - - [RelayCommand] - public void ManageAccounts() => NavigationService.Navigate(WinoPage.AccountManagementPage); - - [RelayCommand] - private Task ReconnectServerAsync() => ServerConnectionManager.ConnectAsync(); - - [RelayCommand] - private void DateClicked(CalendarViewDayClickedEventArgs clickedDateArgs) - { - _navigationDate = clickedDateArgs.ClickedDate; - - ForceNavigateCalendarDate(); - } - - #endregion - - public void Receive(VisibleDateRangeChangedMessage message) => HighlightedDateRange = message.DateRange; - - /// - /// Sets the header navigation items based on visible date range and calendar type. - /// - private void UpdateDateNavigationHeaderItems() - { - DateNavigationHeaderItems.Clear(); - - // TODO: From settings - var testInfo = new CultureInfo("en-US"); - - switch (StatePersistenceService.CalendarDisplayType) - { - case CalendarDisplayType.Day: - case CalendarDisplayType.Week: - case CalendarDisplayType.WorkWeek: - case CalendarDisplayType.Month: - DateNavigationHeaderItems.ReplaceRange(testInfo.DateTimeFormat.MonthNames); - break; - case CalendarDisplayType.Year: - break; - default: - break; - } - - SetDateNavigationHeaderItems(); - } - - partial void OnHighlightedDateRangeChanged(DateRange value) => SetDateNavigationHeaderItems(); - - private void SetDateNavigationHeaderItems() - { - if (HighlightedDateRange == null) return; - - if (DateNavigationHeaderItems.Count == 0) - { - UpdateDateNavigationHeaderItems(); - } - - // TODO: Year view - var monthIndex = HighlightedDateRange.GetMostVisibleMonthIndex(); - - SelectedDateNavigationHeaderIndex = Math.Max(monthIndex - 1, -1); - } - - public async void Receive(CalendarEnableStatusChangedMessage message) - => await ExecuteUIThread(() => IsCalendarEnabled = message.IsEnabled); - - public void Receive(NavigateManageAccountsRequested message) => SelectedMenuItemIndex = 1; - - public void Receive(CalendarDisplayTypeChangedMessage message) => OnPropertyChanged(nameof(IsVerticalCalendar)); - - public async void Receive(DetailsPageStateChangedMessage message) - { - await ExecuteUIThread(() => - { - IsEventDetailsPageActive = message.IsActivated; - - // TODO: This is for Wino Mail. Generalize this later on. - StatePersistenceService.IsReaderNarrowed = message.IsActivated; - StatePersistenceService.IsReadingMail = message.IsActivated; - }); - } -} diff --git a/Wino.Calendar.ViewModels/CalendarAccountSettingsPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarAccountSettingsPageViewModel.cs new file mode 100644 index 00000000..79400c7d --- /dev/null +++ b/Wino.Calendar.ViewModels/CalendarAccountSettingsPageViewModel.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Messaging; +using Wino.Core.Domain; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Navigation; +using Wino.Core.ViewModels; +using Wino.Messaging.Client.Calendar; + +namespace Wino.Calendar.ViewModels; + +/// +/// ViewModel for managing calendar account settings. +/// +public partial class CalendarAccountSettingsPageViewModel : CalendarBaseViewModel +{ + private readonly ICalendarService _calendarService; + private readonly IAccountService _accountService; + + [ObservableProperty] + public partial MailAccount Account { get; set; } + + [ObservableProperty] + public partial AccountCalendar AccountCalendar { get; set; } + + [ObservableProperty] + public partial string AccountColorHex { get; set; } = "#0078D4"; + + [ObservableProperty] + public partial bool IsSyncEnabled { get; set; } + + public ObservableCollection ShowAsOptions { get; } = new ObservableCollection(); + + [ObservableProperty] + public partial ShowAsOption SelectedDefaultShowAsOption { get; set; } + + public CalendarAccountSettingsPageViewModel(ICalendarService calendarService, IAccountService accountService) + { + _calendarService = calendarService; + _accountService = accountService; + + // Initialize ShowAs options + ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Free)); + ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Tentative)); + ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Busy)); + ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.OutOfOffice)); + ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.WorkingElsewhere)); + } + + public override async void OnNavigatedTo(NavigationMode mode, object parameters) + { + base.OnNavigatedTo(mode, parameters); + + if (parameters is AccountCalendar selectedCalendar) + { + Account = await _accountService.GetAccountAsync(selectedCalendar.AccountId); + AccountCalendar = await _calendarService.GetAccountCalendarAsync(selectedCalendar.Id) ?? selectedCalendar; + } + else if (parameters is Guid accountId) + { + Account = await _accountService.GetAccountAsync(accountId); + var calendars = await _calendarService.GetAccountCalendarsAsync(accountId); + AccountCalendar = calendars.FirstOrDefault(c => c.IsPrimary) ?? calendars.FirstOrDefault(); + } + else + { + return; + } + + if (Account == null || AccountCalendar == null) + return; + + // Initialize properties from AccountCalendar + AccountColorHex = AccountCalendar.BackgroundColorHex ?? "#0078D4"; + IsSyncEnabled = AccountCalendar.IsSynchronizationEnabled; + SelectedDefaultShowAsOption = ShowAsOptions.FirstOrDefault(o => o.ShowAs == AccountCalendar.DefaultShowAs) ?? ShowAsOptions[2]; + } + + partial void OnAccountColorHexChanged(string value) + { + if (AccountCalendar != null && !string.IsNullOrEmpty(value)) + { + AccountCalendar.BackgroundColorHex = value; + SaveChangesAsync(); + } + } + + partial void OnIsSyncEnabledChanged(bool value) + { + if (AccountCalendar != null) + { + AccountCalendar.IsSynchronizationEnabled = value; + SaveChangesAsync(); + } + } + + partial void OnSelectedDefaultShowAsOptionChanged(ShowAsOption value) + { + if (AccountCalendar != null && value != null) + { + AccountCalendar.DefaultShowAs = value.ShowAs; + SaveChangesAsync(); + } + } + + private async void SaveChangesAsync() + { + if (AccountCalendar == null) + return; + + await _calendarService.UpdateAccountCalendarAsync(AccountCalendar); + + // Send message to update UI + Messenger.Send(new CalendarListUpdated(AccountCalendar)); + } +} diff --git a/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs b/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs new file mode 100644 index 00000000..43cf07b7 --- /dev/null +++ b/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs @@ -0,0 +1,551 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using Serilog; +using Wino.Calendar.ViewModels.Data; +using Wino.Calendar.ViewModels.Interfaces; +using Wino.Core.Domain; +using Wino.Core.Domain.Collections; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.MenuItems; +using Wino.Core.Domain.Models; +using Wino.Core.Domain.Models.Calendar; +using Wino.Core.Domain.Models.Navigation; +using Wino.Core.Domain.Models.Synchronization; +using Wino.Core.ViewModels; +using Wino.Messaging.Client.Calendar; +using Wino.Messaging.Server; +using Wino.Messaging.UI; + +namespace Wino.Calendar.ViewModels; + +public partial class CalendarAppShellViewModel : CalendarBaseViewModel, + ICalendarShellClient, + IRecipient, + IRecipient +{ + public IPreferencesService PreferencesService { get; } + public IStatePersistanceService StatePersistenceService { get; } + public IAccountCalendarStateService AccountCalendarStateService { get; } + public INavigationService NavigationService { get; } + public WinoApplicationMode Mode => WinoApplicationMode.Calendar; + public bool HandlesNavigationSelection => false; + public VisibleDateRange CurrentVisibleRange => _calendarPageViewModel.CurrentVisibleRange; + public string VisibleDateRangeText => _calendarPageViewModel.VisibleDateRangeText; + System.Collections.IEnumerable ICalendarShellClient.GroupedAccountCalendars => AccountCalendarStateService.GroupedAccountCalendars; + System.Collections.IEnumerable ICalendarShellClient.DateNavigationHeaderItems => DateNavigationHeaderItems; + object IShellClient.SelectedMenuItem + { + get => null; + set { } + } + System.Windows.Input.ICommand ICalendarShellClient.TodayClickedCommand => TodayClickedCommand; + System.Windows.Input.ICommand ICalendarShellClient.DateClickedCommand => DateClickedCommand; + System.Windows.Input.ICommand ICalendarShellClient.PreviousDateRangeCommand => PreviousDateRangeCommand; + System.Windows.Input.ICommand ICalendarShellClient.NextDateRangeCommand => NextDateRangeCommand; + System.Windows.Input.ICommand ICalendarShellClient.SyncCommand => SyncCommand; + + public bool CanSynchronizeCalendars => !AccountCalendarStateService.IsAnySynchronizationInProgress; + + public MenuItemCollection MenuItems { get; private set; } + public MenuItemCollection FooterItems { get; private set; } + + [ObservableProperty] + private int _selectedMenuItemIndex = -1; + + [ObservableProperty] + private ObservableRangeCollection dateNavigationHeaderItems = []; + + [ObservableProperty] + private int _selectedDateNavigationHeaderIndex; + + public bool IsVerticalCalendar => StatePersistenceService.CalendarDisplayType == CalendarDisplayType.Month; + + [ObservableProperty] + private bool isStoreUpdateItemVisible; + + private readonly SettingsItem _settingsItem = new(); + private readonly StoreUpdateMenuItem _storeUpdateMenuItem = new(); + private readonly SemaphoreSlim _accountCalendarUpdateSemaphoreSlim = new(1); + private readonly CalendarPageViewModel _calendarPageViewModel; + private readonly IMailDialogService _dialogService; + private readonly IStoreUpdateService _storeUpdateService; + private readonly IAccountService _accountService; + private readonly ICalendarService _calendarService; + private readonly IDateContextProvider _dateContextProvider; + private bool _runtimeSubscriptionsAttached; + private bool _hasRegisteredPersistentRecipients; + private DateTime? _navigationDate; + + public CalendarAppShellViewModel( + IPreferencesService preferencesService, + IStatePersistanceService statePersistanceService, + IAccountService accountService, + ICalendarService calendarService, + IAccountCalendarStateService accountCalendarStateService, + INavigationService navigationService, + CalendarPageViewModel calendarPageViewModel, + IMailDialogService dialogService, + IStoreUpdateService storeUpdateService, + IDateContextProvider dateContextProvider) + { + PreferencesService = preferencesService; + StatePersistenceService = statePersistanceService; + AccountCalendarStateService = accountCalendarStateService; + NavigationService = navigationService; + _accountService = accountService; + _calendarService = calendarService; + _calendarPageViewModel = calendarPageViewModel; + _dialogService = dialogService; + _storeUpdateService = storeUpdateService; + _dateContextProvider = dateContextProvider; + + _calendarPageViewModel.PropertyChanged += CalendarPageViewModelPropertyChanged; + AccountCalendarStateService.PropertyChanged += AccountCalendarStateServicePropertyChanged; + } + + protected override void OnDispatcherAssigned() + { + base.OnDispatcherAssigned(); + + AccountCalendarStateService.Dispatcher = Dispatcher; + MenuItems = new MenuItemCollection(Dispatcher); + FooterItems = new MenuItemCollection(Dispatcher); + _ = RefreshFooterItemsAsync(false); + } + + private void CalendarPageViewModelPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(CalendarPageViewModel.CurrentVisibleRange)) + { + OnPropertyChanged(nameof(CurrentVisibleRange)); + } + + if (e.PropertyName == nameof(CalendarPageViewModel.CurrentVisibleRange) || + e.PropertyName == nameof(CalendarPageViewModel.VisibleDateRangeText)) + { + OnPropertyChanged(nameof(VisibleDateRangeText)); + UpdateDateNavigationHeaderItems(); + } + } + + private void PrefefencesChanged(object sender, string e) + { + if (e != nameof(StatePersistenceService.CalendarDisplayType)) + return; + + Messenger.Send(new CalendarDisplayTypeChangedMessage(StatePersistenceService.CalendarDisplayType)); + OnPropertyChanged(nameof(IsVerticalCalendar)); + UpdateDateNavigationHeaderItems(); + NavigateCalendarDate(GetDisplayTypeSwitchDate()); + } + + private async void PreferencesServiceChanged(object sender, string e) + { + if (e == nameof(IPreferencesService.IsStoreUpdateNotificationsEnabled)) + { + await RefreshFooterItemsAsync(false); + } + } + + public override async void OnNavigatedTo(NavigationMode mode, object parameters) + { + if (!_hasRegisteredPersistentRecipients) + { + RegisterRecipients(); + _hasRegisteredPersistentRecipients = true; + } + + AttachRuntimeSubscriptions(); + + var activationContext = parameters as ShellModeActivationContext; + var shouldRunStartupFlows = activationContext?.IsInitialActivation ?? true; + + PreferencesService.PreferenceChanged -= PreferencesServiceChanged; + PreferencesService.PreferenceChanged += PreferencesServiceChanged; + + await RefreshFooterItemsAsync(mode == NavigationMode.New); + UpdateDateNavigationHeaderItems(); + await InitializeAccountCalendarsAsync(); + ValidateConfiguredNewEventCalendar(); + + TodayClicked(); + } + + public override void OnNavigatedFrom(NavigationMode mode, object parameters) + { + DetachRuntimeSubscriptions(); + PreferencesService.PreferenceChanged -= PreferencesServiceChanged; + _ = ExecuteUIThread(() => + { + DateNavigationHeaderItems.Clear(); + AccountCalendarStateService.ClearGroupedAccountCalendars(); + SelectedDateNavigationHeaderIndex = -1; + }); + _calendarPageViewModel.CleanupForShellDeactivation(); + } + + public void PrepareForShellShutdown() + { + DetachRuntimeSubscriptions(); + PreferencesService.PreferenceChanged -= PreferencesServiceChanged; + + if (_hasRegisteredPersistentRecipients) + { + UnregisterRecipients(); + _hasRegisteredPersistentRecipients = false; + } + + DateNavigationHeaderItems.Clear(); + SelectedDateNavigationHeaderIndex = -1; + SelectedMenuItemIndex = -1; + MenuItems?.Clear(); + FooterItems?.Clear(); + AccountCalendarStateService.ClearGroupedAccountCalendars(); + _calendarPageViewModel.CleanupForShellDeactivation(); + } + + private void AccountCalendarStateServicePropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName != nameof(IAccountCalendarStateService.IsAnySynchronizationInProgress)) + return; + + OnPropertyChanged(nameof(CanSynchronizeCalendars)); + SyncCommand.NotifyCanExecuteChanged(); + } + + private void AttachRuntimeSubscriptions() + { + if (_runtimeSubscriptionsAttached) + return; + + AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested; + AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged; + StatePersistenceService.StatePropertyChanged += PrefefencesChanged; + _runtimeSubscriptionsAttached = true; + } + + private void DetachRuntimeSubscriptions() + { + if (!_runtimeSubscriptionsAttached) + return; + + AccountCalendarStateService.AccountCalendarSelectionStateChanged -= UpdateAccountCalendarRequested; + AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged -= AccountCalendarStateCollectivelyChanged; + StatePersistenceService.StatePropertyChanged -= PrefefencesChanged; + _runtimeSubscriptionsAttached = false; + } + + private async Task RefreshFooterItemsAsync(bool showNotification) + { + await ExecuteUIThread(() => + { + FooterItems.Clear(); + }); + } + + private async Task StartStoreUpdateAsync() + { + await _storeUpdateService.StartUpdateAsync().ConfigureAwait(false); + await RefreshFooterItemsAsync(false).ConfigureAwait(false); + } + + private async void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e) + { + try + { + await _accountCalendarUpdateSemaphoreSlim.WaitAsync(); + + foreach (var calendar in e.AccountCalendars) + { + await _calendarService.UpdateAccountCalendarAsync(calendar.AccountCalendar).ConfigureAwait(false); + } + } + catch (Exception ex) + { + Log.Error(ex, "Error while waiting for account calendar update semaphore."); + } + finally + { + _accountCalendarUpdateSemaphoreSlim.Release(); + } + } + + private async void UpdateAccountCalendarRequested(object sender, AccountCalendarViewModel e) + => await _calendarService.UpdateAccountCalendarAsync(e.AccountCalendar).ConfigureAwait(false); + + private async Task InitializeAccountCalendarsAsync() + { + await Dispatcher.ExecuteOnUIThread(() => AccountCalendarStateService.ClearGroupedAccountCalendars()); + + var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false); + + foreach (var account in accounts) + { + var accountCalendars = await _calendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false); + var calendarViewModels = accountCalendars.Select(calendar => new AccountCalendarViewModel(account, calendar)).ToList(); + var groupedAccountCalendarViewModel = new GroupedAccountCalendarViewModel(account, calendarViewModels); + + await Dispatcher.ExecuteOnUIThread(() => + { + AccountCalendarStateService.AddGroupedAccountCalendar(groupedAccountCalendarViewModel); + }); + } + } + + private void NavigateCalendarDate(DateTime date) + { + _navigationDate = date.Date; + ForceNavigateCalendarDate(); + } + + private void ForceNavigateCalendarDate() + { + var args = new CalendarPageNavigationArgs + { + NavigationDate = _navigationDate ?? DateTime.Now.Date + }; + + NavigationService.Navigate(WinoPage.CalendarPage, args); + _navigationDate = null; + } + + [RelayCommand(CanExecute = nameof(CanSynchronizeCalendars))] + private async Task Sync() + { + var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false); + foreach (var account in accounts) + { + Messenger.Send(new NewCalendarSynchronizationRequested(new CalendarSynchronizationOptions + { + AccountId = account.Id, + Type = CalendarSynchronizationType.Strict + })); + } + } + + private DateTime GetDisplayTypeSwitchDate() + { + var today = _dateContextProvider.GetToday(); + var settings = PreferencesService.GetCurrentCalendarSettings(); + var referenceRange = CurrentVisibleRange + ?? CalendarRangeResolver.Resolve(new CalendarDisplayRequest(StatePersistenceService.CalendarDisplayType, today), settings, today); + var targetRange = CalendarRangeResolver.ChangeDisplayType(referenceRange, StatePersistenceService.CalendarDisplayType, settings, today); + return targetRange.AnchorDate.ToDateTime(TimeOnly.MinValue); + } + + [RelayCommand] + private void TodayClicked() + { + NavigateCalendarDate(_dateContextProvider.GetToday().ToDateTime(TimeOnly.MinValue)); + } + + [RelayCommand] + private void PreviousDateRange() + { + NavigateRelativePeriod(-1); + } + + [RelayCommand] + private void NextDateRange() + { + NavigateRelativePeriod(1); + } + + private void NavigateRelativePeriod(int direction) + { + var today = _dateContextProvider.GetToday(); + var settings = PreferencesService.GetCurrentCalendarSettings(); + var referenceRange = CurrentVisibleRange + ?? CalendarRangeResolver.Resolve(new CalendarDisplayRequest(StatePersistenceService.CalendarDisplayType, today), settings, today); + var targetRange = CalendarRangeResolver.Navigate(referenceRange, direction, settings, today); + NavigateCalendarDate(targetRange.AnchorDate.ToDateTime(TimeOnly.MinValue)); + } + + public async Task HandleNavigationItemInvokedAsync(IMenuItem menuItem) + { + switch (menuItem) + { + case NewMailMenuItem: + await NewEventAsync().ConfigureAwait(false); + break; + case SettingsItem: + NavigationService.Navigate(WinoPage.SettingsPage); + break; + case StoreUpdateMenuItem: + await StartStoreUpdateAsync().ConfigureAwait(false); + break; + } + } + + [RelayCommand] + private async Task NewEventAsync() + { + var pickedCalendar = TryResolveConfiguredNewEventCalendar(); + + if (pickedCalendar == null) + { + var availableGroups = AccountCalendarStateService.GroupedAccountCalendars + .Where(group => group.AccountCalendars.Count > 0) + .Select(group => new CalendarPickerAccountGroup + { + Account = group.Account, + Calendars = group.AccountCalendars.Select(calendar => calendar.AccountCalendar).ToList() + }) + .ToList(); + + if (availableGroups.Count == 0) + { + _dialogService.InfoBarMessage( + Translator.CalendarEventCompose_NoCalendarsTitle, + Translator.CalendarEventCompose_NoCalendarsMessage, + InfoBarMessageType.Warning); + return; + } + + var pickingResult = await _dialogService.ShowSingleCalendarPickerDialogAsync(availableGroups); + if (pickingResult.ShouldNavigateToCalendarSettings) + { + NavigationService.Navigate(WinoPage.CalendarPreferenceSettingsPage); + return; + } + + pickedCalendar = pickingResult.PickedCalendar; + } + + if (pickedCalendar == null) + return; + + var (startDate, endDate) = GetDefaultComposeDateRange(); + + NavigationService.Navigate(WinoPage.CalendarEventComposePage, new CalendarEventComposeNavigationArgs + { + SelectedCalendarId = pickedCalendar.Id, + StartDate = startDate, + EndDate = endDate + }); + } + + public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args) + { + if (args.Handled || args.Mode != WinoApplicationMode.Calendar) + return; + + if (args.Action == KeyboardShortcutAction.NewEvent) + { + await NewEventAsync(); + args.Handled = true; + } + } + + [RelayCommand] + private void DateClicked(CalendarViewDayClickedEventArgs clickedDateArgs) + => NavigateCalendarDate(clickedDateArgs.ClickedDate); + + protected override void RegisterRecipients() + { + base.RegisterRecipients(); + + UnregisterRecipients(); + + Messenger.Register(this); + Messenger.Register(this); + } + + protected override void UnregisterRecipients() + { + base.UnregisterRecipients(); + + Messenger.Unregister(this); + Messenger.Unregister(this); + } + + private void UpdateDateNavigationHeaderItems() + { + var headerText = VisibleDateRangeText; + DateNavigationHeaderItems.ReplaceRange(string.IsNullOrWhiteSpace(headerText) ? [] : [headerText]); + SelectedDateNavigationHeaderIndex = DateNavigationHeaderItems.Count > 0 ? 0 : -1; + } + + public void Receive(CalendarDisplayTypeChangedMessage message) + { + OnPropertyChanged(nameof(IsVerticalCalendar)); + UpdateDateNavigationHeaderItems(); + } + + public async void Receive(AccountRemovedMessage message) + { + await InitializeAccountCalendarsAsync(); + ValidateConfiguredNewEventCalendar(); + } + + private AccountCalendar TryResolveConfiguredNewEventCalendar() + { + ValidateConfiguredNewEventCalendar(); + + if (PreferencesService.NewEventButtonBehavior != NewEventButtonBehavior.AlwaysUseSpecificCalendar + || !PreferencesService.DefaultNewEventCalendarId.HasValue) + { + return null; + } + + return AccountCalendarStateService.AllCalendars + .FirstOrDefault(calendar => calendar.Id == PreferencesService.DefaultNewEventCalendarId.Value)? + .AccountCalendar; + } + + private void ValidateConfiguredNewEventCalendar() + { + if (PreferencesService.NewEventButtonBehavior != NewEventButtonBehavior.AlwaysUseSpecificCalendar + || !PreferencesService.DefaultNewEventCalendarId.HasValue) + { + return; + } + + var exists = AccountCalendarStateService.AllCalendars + .Any(calendar => calendar.Id == PreferencesService.DefaultNewEventCalendarId.Value); + + if (!exists) + { + PreferencesService.NewEventButtonBehavior = NewEventButtonBehavior.AskEachTime; + PreferencesService.DefaultNewEventCalendarId = null; + } + } + + private static (DateTime StartDate, DateTime EndDate) GetDefaultComposeDateRange() + { + var localNow = DateTime.Now; + var roundedMinutes = localNow.Minute switch + { + < 30 => 30, + 30 when localNow.Second == 0 && localNow.Millisecond == 0 => 30, + _ => 60 + }; + + var startDate = new DateTime(localNow.Year, localNow.Month, localNow.Day, localNow.Hour, 0, 0); + startDate = roundedMinutes == 60 ? startDate.AddHours(1) : startDate.AddMinutes(roundedMinutes); + + return (startDate, startDate.AddMinutes(30)); + } + + void IShellClient.Activate(ShellModeActivationContext activationContext) + => OnNavigatedTo(NavigationMode.New, activationContext); + + void IShellClient.Deactivate() + => OnNavigatedFrom(NavigationMode.New, null!); + + Task IShellClient.HandleNavigationItemInvokedAsync(IMenuItem menuItem) + => menuItem == null ? Task.CompletedTask : HandleNavigationItemInvokedAsync(menuItem); + + Task IShellClient.HandleNavigationSelectionChangedAsync(IMenuItem menuItem) + => Task.CompletedTask; +} diff --git a/Wino.Calendar.ViewModels/CalendarEventComposePageViewModel.cs b/Wino.Calendar.ViewModels/CalendarEventComposePageViewModel.cs new file mode 100644 index 00000000..951493be --- /dev/null +++ b/Wino.Calendar.ViewModels/CalendarEventComposePageViewModel.cs @@ -0,0 +1,760 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using EmailValidation; +using Wino.Calendar.ViewModels.Data; +using Wino.Core.Domain; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Exceptions; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Calendar; +using Wino.Core.Domain.Models.Navigation; +using Wino.Core.Domain.Validation; +using Wino.Core.ViewModels; + +namespace Wino.Calendar.ViewModels; + +public partial class CalendarEventComposePageViewModel : CalendarBaseViewModel +{ + private readonly IAccountService _accountService; + private readonly ICalendarService _calendarService; + private readonly INavigationService _navigationService; + private readonly IMailDialogService _dialogService; + private readonly IContactService _contactService; + private readonly IPreferencesService _preferencesService; + private readonly IUnderlyingThemeService _underlyingThemeService; + private readonly IWinoRequestDelegator _winoRequestDelegator; + private readonly CalendarEventComposeResultValidator _composeResultValidator = new(); + + public Func> GetHtmlNotesAsync { get; set; } + + public ObservableCollection AvailableCalendars { get; } = []; + public ObservableCollection AvailableCalendarGroups { get; } = []; + public ObservableCollection Attendees { get; } = []; + public ObservableCollection Attachments { get; } = []; + public ObservableCollection ShowAsOptions { get; } = []; + public ObservableCollection ReminderOptions { get; } = []; + public ObservableCollection RecurrenceIntervalOptions { get; } = []; + public ObservableCollection RecurrenceFrequencyOptions { get; } = []; + public ObservableCollection WeekdayOptions { get; } = []; + + [ObservableProperty] + public partial AccountCalendarViewModel SelectedCalendar { get; set; } + + [ObservableProperty] + public partial string Title { get; set; } = string.Empty; + + [ObservableProperty] + public partial string Location { get; set; } = string.Empty; + + [ObservableProperty] + public partial bool IsAllDay { get; set; } + + [ObservableProperty] + public partial DateTimeOffset StartDate { get; set; } + + [ObservableProperty] + public partial TimeSpan StartTime { get; set; } + + [ObservableProperty] + public partial TimeSpan EndTime { get; set; } + + [ObservableProperty] + public partial DateTimeOffset AllDayEndDate { get; set; } + + [ObservableProperty] + public partial bool IsRecurring { get; set; } + + [ObservableProperty] + public partial int SelectedRecurrenceInterval { get; set; } = 1; + + [ObservableProperty] + public partial CalendarComposeFrequencyOption SelectedRecurrenceFrequencyOption { get; set; } + + [ObservableProperty] + public partial DateTimeOffset? RecurrenceEndDate { get; set; } + + [ObservableProperty] + public partial string RecurrenceSummary { get; set; } = string.Empty; + + [ObservableProperty] + public partial ReminderOption SelectedReminderOption { get; set; } + + [ObservableProperty] + public partial ShowAsOption SelectedShowAsOption { get; set; } + + [ObservableProperty] + public partial bool IsDarkWebviewRenderer { get; set; } + + [ObservableProperty] + public partial CalendarEventComposeResult LastCreatedResult { get; set; } + + public CalendarSettings CurrentSettings { get; } + public string TimePickerClockIdentifier => CurrentSettings.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour ? "24HourClock" : "12HourClock"; + public bool HasAttachments => Attachments.Count > 0; + public bool IsSelectedCalendarCalDav => SelectedCalendar?.Account?.ProviderType == MailProviderType.IMAP4 && + SelectedCalendar.Account.ServerInformation?.CalendarSupportMode == ImapCalendarSupportMode.CalDav; + public bool CanAddAttachments => !IsSelectedCalendarCalDav; + public string AttachmentsDisabledTooltipText => IsSelectedCalendarCalDav + ? Translator.CalendarEventCompose_AttachmentsNotSupportedForCalDav + : string.Empty; + public string SelectedCalendarDisplayText => SelectedCalendar?.Name ?? Translator.CalendarEventCompose_SelectCalendar; + public string SelectedCalendarAccountText => SelectedCalendar?.Account?.Address ?? string.Empty; + public bool IsDailyRecurrenceSelected => SelectedRecurrenceFrequencyOption?.Frequency == CalendarItemRecurrenceFrequency.Daily; + + public CalendarEventComposePageViewModel(IAccountService accountService, + ICalendarService calendarService, + INavigationService navigationService, + IMailDialogService dialogService, + IContactService contactService, + IPreferencesService preferencesService, + IUnderlyingThemeService underlyingThemeService, + IWinoRequestDelegator winoRequestDelegator) + { + _accountService = accountService; + _calendarService = calendarService; + _navigationService = navigationService; + _dialogService = dialogService; + _contactService = contactService; + _preferencesService = preferencesService; + _underlyingThemeService = underlyingThemeService; + _winoRequestDelegator = winoRequestDelegator; + + CurrentSettings = _preferencesService.GetCurrentCalendarSettings(); + IsDarkWebviewRenderer = _underlyingThemeService.IsUnderlyingThemeDark(); + + Attachments.CollectionChanged += AttachmentsCollectionChanged; + + ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Free)); + ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Tentative)); + ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Busy)); + ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.OutOfOffice)); + ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.WorkingElsewhere)); + + foreach (var reminderMinutes in _calendarService.GetPredefinedReminderMinutes().OrderByDescending(x => x)) + { + ReminderOptions.Add(new ReminderOption(reminderMinutes)); + } + + foreach (var interval in Enumerable.Range(1, 99)) + { + RecurrenceIntervalOptions.Add(interval); + } + + RecurrenceFrequencyOptions.Add(new CalendarComposeFrequencyOption(CalendarItemRecurrenceFrequency.Daily, Translator.CalendarEventCompose_FrequencyDay)); + RecurrenceFrequencyOptions.Add(new CalendarComposeFrequencyOption(CalendarItemRecurrenceFrequency.Weekly, Translator.CalendarEventCompose_FrequencyWeek)); + RecurrenceFrequencyOptions.Add(new CalendarComposeFrequencyOption(CalendarItemRecurrenceFrequency.Monthly, Translator.CalendarEventCompose_FrequencyMonth)); + RecurrenceFrequencyOptions.Add(new CalendarComposeFrequencyOption(CalendarItemRecurrenceFrequency.Yearly, Translator.CalendarEventCompose_FrequencyYear)); + SelectedRecurrenceFrequencyOption = RecurrenceFrequencyOptions.FirstOrDefault(); + + WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Monday, "MO", Translator.CalendarEventCompose_Weekday_Monday)); + WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Tuesday, "TU", Translator.CalendarEventCompose_Weekday_Tuesday)); + WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Wednesday, "WE", Translator.CalendarEventCompose_Weekday_Wednesday)); + WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Thursday, "TH", Translator.CalendarEventCompose_Weekday_Thursday)); + WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Friday, "FR", Translator.CalendarEventCompose_Weekday_Friday)); + WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Saturday, "SA", Translator.CalendarEventCompose_Weekday_Saturday)); + WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Sunday, "SU", Translator.CalendarEventCompose_Weekday_Sunday)); + + SelectedReminderOption = GetDefaultReminderOption(); + SelectedShowAsOption = ShowAsOptions.FirstOrDefault(option => option.ShowAs == CalendarItemShowAs.Busy); + + var (defaultStart, defaultEnd) = GetDefaultComposeDateRange(); + ApplyDateRange(defaultStart, defaultEnd, false); + } + + public override async void OnNavigatedTo(NavigationMode mode, object parameters) + { + base.OnNavigatedTo(mode, parameters); + + await LoadAvailableCalendarsAsync(); + + var args = parameters as CalendarEventComposeNavigationArgs; + ApplyNavigationArgs(args); + UpdateRecurrenceSummary(); + } + + partial void OnSelectedCalendarChanged(AccountCalendarViewModel value) + { + if (value == null) + return; + + SelectedShowAsOption = ShowAsOptions.FirstOrDefault(option => option.ShowAs == value.DefaultShowAs) + ?? ShowAsOptions.FirstOrDefault(); + + if (IsSelectedCalendarCalDav && Attachments.Count > 0) + { + Attachments.Clear(); + } + + OnPropertyChanged(nameof(IsSelectedCalendarCalDav)); + OnPropertyChanged(nameof(CanAddAttachments)); + OnPropertyChanged(nameof(AttachmentsDisabledTooltipText)); + OnPropertyChanged(nameof(SelectedCalendarDisplayText)); + OnPropertyChanged(nameof(SelectedCalendarAccountText)); + } + + partial void OnIsAllDayChanged(bool value) + { + if (value) + { + if (AllDayEndDate.Date <= StartDate.Date) + { + AllDayEndDate = StartDate.AddDays(1); + } + } + + UpdateRecurrenceSummary(); + } + + partial void OnStartDateChanged(DateTimeOffset value) + { + if (IsAllDay && AllDayEndDate.Date <= value.Date) + { + AllDayEndDate = value.AddDays(1); + } + + if (IsRecurring && WeekdayOptions.All(option => !option.IsSelected)) + { + SelectSingleWeekday(value.DayOfWeek); + } + + UpdateRecurrenceSummary(); + } + + partial void OnStartTimeChanged(TimeSpan value) => UpdateRecurrenceSummary(); + partial void OnEndTimeChanged(TimeSpan value) => UpdateRecurrenceSummary(); + partial void OnAllDayEndDateChanged(DateTimeOffset value) => UpdateRecurrenceSummary(); + partial void OnIsRecurringChanged(bool value) + { + if (value && WeekdayOptions.All(option => !option.IsSelected)) + { + SelectSingleWeekday(StartDate.DayOfWeek); + } + + UpdateRecurrenceSummary(); + } + partial void OnSelectedRecurrenceIntervalChanged(int value) => UpdateRecurrenceSummary(); + partial void OnSelectedRecurrenceFrequencyOptionChanged(CalendarComposeFrequencyOption value) + { + OnPropertyChanged(nameof(IsDailyRecurrenceSelected)); + UpdateRecurrenceSummary(); + } + partial void OnRecurrenceEndDateChanged(DateTimeOffset? value) => UpdateRecurrenceSummary(); + + [RelayCommand] + private async Task AddAttachmentsAsync() + { + if (!CanAddAttachments) + return; + + var pickedFiles = await _dialogService.PickFilesMetadataAsync("*"); + if (pickedFiles.Count == 0) + return; + + await ExecuteUIThread(() => + { + foreach (var file in pickedFiles) + { + TryAddAttachment(file.FileName, file.FullFilePath, file.FileExtension, file.Size); + } + }); + } + + public bool TryAddAttachment(string filePath, long size) + { + if (string.IsNullOrWhiteSpace(filePath)) + return false; + + var fileName = Path.GetFileName(filePath); + var fileExtension = Path.GetExtension(filePath); + return TryAddAttachment(fileName, filePath, fileExtension, size); + } + + [RelayCommand] + private void RemoveAttachment(CalendarComposeAttachmentViewModel attachment) + { + if (attachment == null) + return; + + Attachments.Remove(attachment); + } + + [RelayCommand] + private void ClearRecurrenceEndDate() + { + RecurrenceEndDate = null; + } + + [RelayCommand] + private void Cancel() + { + _navigationService.GoBack(); + } + + [RelayCommand] + private async Task CreateAsync() + { + var uniqueAttendees = Attendees + .GroupBy(attendee => attendee.Email, StringComparer.OrdinalIgnoreCase) + .Select(group => group.First()) + .ToList(); + + var createdResult = await BuildResultAsync(uniqueAttendees); + + try + { + _composeResultValidator.Validate(createdResult); + } + catch (CalendarEventComposeValidationException ex) + { + ShowValidationMessage(ex.Message); + return; + } + + LastCreatedResult = createdResult; + + await _winoRequestDelegator.ExecuteAsync(new CalendarOperationPreparationRequest( + CalendarSynchronizerOperation.CreateEvent, + ComposeResult: createdResult)); + + NavigateBackToCalendar(createdResult.StartDate); + } + + private void NavigateBackToCalendar(DateTime targetDate) + { + _navigationService.Navigate( + WinoPage.CalendarPage, + new CalendarPageNavigationArgs + { + NavigationDate = targetDate, + ForceReload = true + }); + } + + public async Task> SearchContactsAsync(string queryText) + { + if (string.IsNullOrWhiteSpace(queryText) || queryText.Length < 2) + return []; + + return await _contactService.GetAddressInformationAsync(queryText).ConfigureAwait(false); + } + + public async Task GetAttendeeAsync(string tokenText) + { + if (!EmailValidator.Validate(tokenText)) + return null; + + var existing = Attendees.Any(attendee => attendee.Email.Equals(tokenText, StringComparison.OrdinalIgnoreCase)); + if (existing) + return null; + + var info = await _contactService.GetAddressInformationByAddressAsync(tokenText).ConfigureAwait(false); + if (info != null) + { + return CalendarComposeAttendeeViewModel.FromContact(info); + } + + return new CalendarComposeAttendeeViewModel(string.Empty, tokenText); + } + + public void AddAttendee(CalendarComposeAttendeeViewModel attendee) + { + if (Attendees.Any(existing => existing.Email.Equals(attendee.Email, StringComparison.OrdinalIgnoreCase))) + return; + + Attendees.Add(attendee); + } + + [RelayCommand] + private void RemoveAttendee(CalendarComposeAttendeeViewModel attendee) + { + if (attendee == null) + return; + + Attendees.Remove(attendee); + } + + public void NotifyAddressExists() + { + _dialogService.InfoBarMessage( + Translator.Info_ContactExistsTitle, + Translator.Info_ContactExistsMessage, + InfoBarMessageType.Warning); + } + + public void NotifyInvalidEmail(string address) + { + _dialogService.InfoBarMessage( + Translator.Info_InvalidAddressTitle, + string.Format(Translator.Info_InvalidAddressMessage, address), + InfoBarMessageType.Warning); + } + + private async Task LoadAvailableCalendarsAsync() + { + var accountCalendars = new List(); + var groupedCalendars = new List(); + var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false); + + foreach (var account in accounts) + { + var calendars = await _calendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false); + var viewModels = calendars + .Select(calendar => new AccountCalendarViewModel(account, calendar)) + .ToList(); + + accountCalendars.AddRange(viewModels); + + if (viewModels.Count > 0) + { + groupedCalendars.Add(new GroupedAccountCalendarViewModel(account, viewModels)); + } + } + + await ExecuteUIThread(() => + { + AvailableCalendars.Clear(); + AvailableCalendarGroups.Clear(); + + foreach (var calendar in accountCalendars.OrderBy(calendar => calendar.Account.Name).ThenBy(calendar => calendar.Name)) + { + AvailableCalendars.Add(calendar); + } + + foreach (var group in groupedCalendars.OrderBy(group => group.Account.Name)) + { + AvailableCalendarGroups.Add(group); + } + }); + } + + private void ApplyNavigationArgs(CalendarEventComposeNavigationArgs args) + { + var (defaultStart, defaultEnd) = GetDefaultComposeDateRange(); + var startDate = args?.StartDate != default ? args!.StartDate : defaultStart; + var endDate = args?.EndDate != default ? args!.EndDate : defaultEnd; + var isAllDay = args?.IsAllDay ?? false; + + Title = args?.Title ?? string.Empty; + Location = args?.Location ?? string.Empty; + + ApplyDateRange(startDate, endDate, isAllDay); + + SelectedCalendar = ResolveSelectedCalendar(args?.SelectedCalendarId); + if (SelectedCalendar != null) + { + SelectedShowAsOption = ShowAsOptions.FirstOrDefault(option => option.ShowAs == SelectedCalendar.DefaultShowAs) + ?? SelectedShowAsOption + ?? ShowAsOptions.FirstOrDefault(); + } + } + + private AccountCalendarViewModel ResolveSelectedCalendar(Guid? selectedCalendarId) + { + if (selectedCalendarId.HasValue) + { + var selectedCalendar = AvailableCalendars.FirstOrDefault(calendar => calendar.Id == selectedCalendarId.Value); + if (selectedCalendar != null) + return selectedCalendar; + } + + return AvailableCalendars.FirstOrDefault(calendar => calendar.IsPrimary) ?? AvailableCalendars.FirstOrDefault(); + } + + private void ApplyDateRange(DateTime startDate, DateTime endDate, bool isAllDay) + { + IsAllDay = isAllDay; + StartDate = new DateTimeOffset(startDate.Date); + StartTime = startDate.TimeOfDay; + EndTime = endDate.TimeOfDay; + AllDayEndDate = new DateTimeOffset((isAllDay ? endDate.Date : startDate.Date.AddDays(1))); + } + + private async Task BuildResultAsync(List uniqueAttendees) + { + if (RecurrenceEndDate.HasValue && RecurrenceEndDate.Value.Date < StartDate.Date) + { + throw new CalendarEventComposeValidationException(Translator.CalendarEventCompose_ValidationInvalidRecurrenceEnd); + } + + var htmlNotes = GetHtmlNotesAsync == null ? string.Empty : await GetHtmlNotesAsync(); + var effectiveStart = GetEffectiveStartDateTime(); + var effectiveEnd = GetEffectiveEndDateTime(); + + return new CalendarEventComposeResult + { + CalendarId = SelectedCalendar?.Id ?? Guid.Empty, + AccountId = SelectedCalendar?.Account.Id ?? Guid.Empty, + Title = Title.Trim(), + Location = Location?.Trim() ?? string.Empty, + HtmlNotes = htmlNotes, + StartDate = effectiveStart, + EndDate = effectiveEnd, + IsAllDay = IsAllDay, + TimeZoneId = TimeZoneInfo.Local.Id, + ShowAs = SelectedShowAsOption?.ShowAs ?? SelectedCalendar?.DefaultShowAs ?? CalendarItemShowAs.Busy, + SelectedReminders = BuildSelectedReminders(), + Attendees = BuildAttendees(uniqueAttendees), + Attachments = CanAddAttachments + ? Attachments.Select(attachment => attachment.ToDraftModel()).ToList() + : [], + Recurrence = BuildRecurrenceRule(), + RecurrenceSummary = RecurrenceSummary + }; + } + + private List BuildSelectedReminders() + { + if (SelectedReminderOption == null) + return []; + + return + [ + new Reminder + { + Id = Guid.NewGuid(), + CalendarItemId = Guid.Empty, + DurationInSeconds = SelectedReminderOption.Minutes * 60L, + ReminderType = CalendarItemReminderType.Popup + } + ]; + } + + private static List BuildAttendees(IEnumerable attendees) + { + return attendees + .Select(attendee => new CalendarEventAttendee + { + Id = Guid.NewGuid(), + CalendarItemId = Guid.Empty, + Name = attendee.HasDistinctDisplayName ? attendee.DisplayName : string.Empty, + Email = attendee.Email, + AttendenceStatus = AttendeeStatus.NeedsAction, + IsOrganizer = false, + ResolvedContact = attendee.ResolvedContact + }) + .ToList(); + } + + private ReminderOption GetDefaultReminderOption() + { + var reminderMinutes = Math.Max(1, _preferencesService.DefaultReminderDurationInSeconds / 60); + return ReminderOptions.FirstOrDefault(option => option.Minutes == reminderMinutes) + ?? ReminderOptions.FirstOrDefault(); + } + + private void UpdateRecurrenceSummary() + { + if (!HasInitializedComposeDateRange()) + { + RecurrenceSummary = string.Empty; + return; + } + + var effectiveStart = GetEffectiveStartDateTime(); + var effectiveEnd = GetEffectiveEndDateTime(); + var selectedDays = IsDailyRecurrenceSelected + ? WeekdayOptions + .Where(option => option.IsSelected) + .Select(option => option.DayOfWeek) + .ToList() + : []; + + RecurrenceSummary = CalendarRecurrenceSummaryFormatter.BuildSummary( + IsRecurring, + effectiveStart, + effectiveEnd, + IsAllDay, + CurrentSettings, + SelectedRecurrenceInterval, + SelectedRecurrenceFrequencyOption?.Frequency ?? CalendarItemRecurrenceFrequency.Weekly, + selectedDays, + RecurrenceEndDate); + } + + private bool HasInitializedComposeDateRange() + { + if (StartDate == default) + { + return false; + } + + return !IsAllDay || AllDayEndDate != default; + } + + private string BuildRecurrenceRule() + { + if (!IsRecurring || SelectedRecurrenceFrequencyOption == null) + return string.Empty; + + var parts = new List + { + $"FREQ={SelectedRecurrenceFrequencyOption.Frequency.ToString().ToUpperInvariant()}", + $"INTERVAL={SelectedRecurrenceInterval}" + }; + + var selectedDays = IsDailyRecurrenceSelected + ? WeekdayOptions + .Where(option => option.IsSelected) + .Select(option => option.RuleValue) + .ToList() + : []; + + if (selectedDays.Count > 0) + { + parts.Add($"BYDAY={string.Join(",", selectedDays)}"); + } + + if (RecurrenceEndDate.HasValue) + { + var untilValue = IsAllDay + ? RecurrenceEndDate.Value.ToString("yyyyMMdd", CultureInfo.InvariantCulture) + : RecurrenceEndDate.Value.Date.AddDays(1).AddSeconds(-1).ToString("yyyyMMdd'T'HHmmss", CultureInfo.InvariantCulture); + + parts.Add($"UNTIL={untilValue}"); + } + + return $"RRULE:{string.Join(";", parts)}"; + } + + private DateTime GetEffectiveStartDateTime() + => StartDate.Date.Add(IsAllDay ? TimeSpan.Zero : StartTime); + + private DateTime GetEffectiveEndDateTime() + => IsAllDay + ? AllDayEndDate.Date + : StartDate.Date.Add(EndTime); + + private static (DateTime StartDate, DateTime EndDate) GetDefaultComposeDateRange() + { + var localNow = DateTime.Now; + var roundedMinutes = localNow.Minute switch + { + < 30 => 30, + 30 when localNow.Second == 0 && localNow.Millisecond == 0 => 30, + _ => 60 + }; + + var startDate = new DateTime(localNow.Year, localNow.Month, localNow.Day, localNow.Hour, 0, 0); + startDate = roundedMinutes == 60 ? startDate.AddHours(1) : startDate.AddMinutes(roundedMinutes); + + return (startDate, startDate.AddMinutes(30)); + } + + private CalendarComposeWeekdayOption CreateWeekdayOption(DayOfWeek dayOfWeek, string ruleValue, string label) + { + var option = new CalendarComposeWeekdayOption(dayOfWeek, ruleValue, label); + option.PropertyChanged += WeekdayOptionPropertyChanged; + return option; + } + + private void WeekdayOptionPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(CalendarComposeWeekdayOption.IsSelected)) + { + UpdateRecurrenceSummary(); + } + } + + private void SelectSingleWeekday(DayOfWeek dayOfWeek) + { + foreach (var option in WeekdayOptions) + { + option.IsSelected = option.DayOfWeek == dayOfWeek; + } + } + + private void ShowValidationMessage(string message) + { + _dialogService.InfoBarMessage( + Translator.CalendarEventCompose_ValidationTitle, + message, + InfoBarMessageType.Warning); + } + + private void AttachmentsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + OnPropertyChanged(nameof(HasAttachments)); + } + + private bool TryAddAttachment(string fileName, string filePath, string fileExtension, long size) + { + if (!CanAddAttachments || + string.IsNullOrWhiteSpace(filePath) || + Attachments.Any(existing => existing.FilePath.Equals(filePath, StringComparison.OrdinalIgnoreCase))) + { + return false; + } + + Attachments.Add(new CalendarComposeAttachmentViewModel(fileName, filePath, fileExtension, size)); + return true; + } + +} + +public partial class CalendarComposeFrequencyOption : ObservableObject +{ + public CalendarItemRecurrenceFrequency Frequency { get; } + public string DisplayText { get; } + + public CalendarComposeFrequencyOption(CalendarItemRecurrenceFrequency frequency, string displayText) + { + Frequency = frequency; + DisplayText = displayText; + } + + public string PluralLabel(int interval) + { + if (interval == 1) + return DisplayText; + + return Frequency switch + { + CalendarItemRecurrenceFrequency.Daily => Translator.CalendarEventCompose_FrequencyDayPlural, + CalendarItemRecurrenceFrequency.Weekly => Translator.CalendarEventCompose_FrequencyWeekPlural, + CalendarItemRecurrenceFrequency.Monthly => Translator.CalendarEventCompose_FrequencyMonthPlural, + CalendarItemRecurrenceFrequency.Yearly => Translator.CalendarEventCompose_FrequencyYearPlural, + _ => DisplayText + }; + } +} + +public partial class CalendarComposeWeekdayOption : ObservableObject +{ + public DayOfWeek DayOfWeek { get; } + public string RuleValue { get; } + public string Label { get; } + public string FullDayName => DayOfWeek switch + { + DayOfWeek.Monday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[1], + DayOfWeek.Tuesday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[2], + DayOfWeek.Wednesday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[3], + DayOfWeek.Thursday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[4], + DayOfWeek.Friday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[5], + DayOfWeek.Saturday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[6], + DayOfWeek.Sunday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[0], + _ => string.Empty + }; + + [ObservableProperty] + public partial bool IsSelected { get; set; } + + public CalendarComposeWeekdayOption(DayOfWeek dayOfWeek, string ruleValue, string label) + { + DayOfWeek = dayOfWeek; + RuleValue = ruleValue; + Label = label; + } +} + + diff --git a/Wino.Calendar.ViewModels/CalendarNotificationSettingsPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarNotificationSettingsPageViewModel.cs new file mode 100644 index 00000000..df4af29f --- /dev/null +++ b/Wino.Calendar.ViewModels/CalendarNotificationSettingsPageViewModel.cs @@ -0,0 +1,44 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using Wino.Core.Domain.Interfaces; + +namespace Wino.Calendar.ViewModels; + +public partial class CalendarNotificationSettingsPageViewModel : CalendarSettingsSectionViewModelBase +{ + [ObservableProperty] + public partial int SelectedDefaultReminderIndex { get; set; } + + [ObservableProperty] + public partial int SelectedDefaultSnoozeIndex { get; set; } + + public CalendarNotificationSettingsPageViewModel( + IPreferencesService preferencesService, + ICalendarService calendarService, + IAccountService accountService) + : base(preferencesService, calendarService, accountService) + { + LoadReminderOptions(); + LoadSnoozeOptions(); + + SelectedDefaultReminderIndex = GetSelectedReminderIndex(); + SelectedDefaultSnoozeIndex = GetSelectedSnoozeIndex(); + + IsLoaded = true; + } + + partial void OnSelectedDefaultReminderIndexChanged(int value) + { + if (!IsLoaded) + return; + + SaveReminderIndex(value); + } + + partial void OnSelectedDefaultSnoozeIndexChanged(int value) + { + if (!IsLoaded) + return; + + SaveSnoozeIndex(value); + } +} diff --git a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs index 99cd7a94..fedfce72 100644 --- a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs +++ b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs @@ -1,27 +1,27 @@ -using System; +using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; -using CommunityToolkit.Diagnostics; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; -using MoreLinq; +using Itenso.TimePeriod; using Serilog; using Wino.Calendar.ViewModels.Data; using Wino.Calendar.ViewModels.Interfaces; using Wino.Calendar.ViewModels.Messages; -using Wino.Core.Domain.Collections; +using Wino.Core.Domain; using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models; using Wino.Core.Domain.Models.Calendar; -using Wino.Core.Domain.Models.Calendar.CalendarTypeStrategies; using Wino.Core.Domain.Models.Navigation; using Wino.Core.ViewModels; using Wino.Messaging.Client.Calendar; +using Wino.Messaging.UI; namespace Wino.Calendar.ViewModels; @@ -31,81 +31,95 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, IRecipient, IRecipient, IRecipient, - IRecipient + IRecipient, + IRecipient, + IDisposable { #region Quick Event Creation - [ObservableProperty] - private bool _isQuickEventDialogOpen; - [ObservableProperty] [NotifyPropertyChangedFor(nameof(SelectedQuickEventAccountCalendarName))] [NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))] - private AccountCalendarViewModel _selectedQuickEventAccountCalendar; + public partial AccountCalendarViewModel SelectedQuickEventAccountCalendar { get; set; } public string SelectedQuickEventAccountCalendarName - { - get - { - return SelectedQuickEventAccountCalendar == null ? "Pick a calendar" : SelectedQuickEventAccountCalendar.Name; - } - } + => SelectedQuickEventAccountCalendar == null ? "Pick a calendar" : SelectedQuickEventAccountCalendar.Name; [ObservableProperty] - private List _hourSelectionStrings; + public partial List HourSelectionStrings { get; set; } = []; - // To be able to revert the values when the user enters an invalid time. - private string _previousSelectedStartTimeString; - private string _previousSelectedEndTimeString; - - [ObservableProperty] - private DateTime? _selectedQuickEventDate; - - [ObservableProperty] - private bool _isAllDay; + private string _previousSelectedStartTimeString = string.Empty; + private string _previousSelectedEndTimeString = string.Empty; [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))] - private string _selectedStartTimeString; + public partial DateTime? SelectedQuickEventDate { get; set; } [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))] - private string _selectedEndTimeString; - - [ObservableProperty] - private string _location; + public partial bool IsAllDay { get; set; } [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))] - private string _eventName; + public partial string SelectedStartTimeString { get; set; } = string.Empty; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))] + public partial string SelectedEndTimeString { get; set; } = string.Empty; + + [ObservableProperty] + public partial string Location { get; set; } = string.Empty; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))] + public partial string EventName { get; set; } = string.Empty; public DateTime QuickEventStartTime => SelectedQuickEventDate.Value.Date.Add(CurrentSettings.GetTimeSpan(SelectedStartTimeString).Value); public DateTime QuickEventEndTime => SelectedQuickEventDate.Value.Date.Add(CurrentSettings.GetTimeSpan(SelectedEndTimeString).Value); - public bool CanSaveQuickEvent => SelectedQuickEventAccountCalendar != null && - !string.IsNullOrWhiteSpace(EventName) && - !string.IsNullOrWhiteSpace(SelectedStartTimeString) && - !string.IsNullOrWhiteSpace(SelectedEndTimeString) && - QuickEventEndTime > QuickEventStartTime; + public bool CanSaveQuickEvent + { + get + { + if (SelectedQuickEventAccountCalendar == null || + SelectedQuickEventDate == null || + string.IsNullOrWhiteSpace(EventName) || + string.IsNullOrWhiteSpace(SelectedStartTimeString) || + string.IsNullOrWhiteSpace(SelectedEndTimeString)) + { + return false; + } + + var startTime = CurrentSettings.GetTimeSpan(SelectedStartTimeString); + var endTime = CurrentSettings.GetTimeSpan(SelectedEndTimeString); + + if (!startTime.HasValue || !endTime.HasValue) + { + return false; + } + + return IsAllDay || endTime > startTime; + } + } #endregion - #region Data Initialization + #region Visible Range [ObservableProperty] - private CalendarOrientation _calendarOrientation = CalendarOrientation.Horizontal; + public partial VisibleDateRange CurrentVisibleRange { get; set; } [ObservableProperty] - private DayRangeCollection _dayRanges = []; + public partial string VisibleDateRangeText { get; set; } = string.Empty; [ObservableProperty] - private int _selectedDateRangeIndex; + public partial DateRange LoadedDateWindow { get; set; } [ObservableProperty] - private DayRangeRenderModel _selectedDayRange; + public partial bool IsCalendarEnabled { get; set; } = true; [ObservableProperty] - private bool _isCalendarEnabled = true; + public partial IReadOnlyList CalendarItems { get; set; } = []; #endregion @@ -113,107 +127,305 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, public event EventHandler DetailsShowCalendarItemChanged; + public bool CanJoinOnline => DisplayDetailsCalendarItemViewModel != null && + !string.IsNullOrEmpty(DisplayDetailsCalendarItemViewModel.CalendarItem.HtmlLink); + [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsEventDetailsVisible))] - private CalendarItemViewModel _displayDetailsCalendarItemViewModel; + [NotifyCanExecuteChangedFor(nameof(JoinOnlineCommand))] + [NotifyPropertyChangedFor(nameof(CanJoinOnline))] + public partial CalendarItemViewModel DisplayDetailsCalendarItemViewModel { get; set; } public bool IsEventDetailsVisible => DisplayDetailsCalendarItemViewModel != null; #endregion - // TODO: Get rid of some of the items if we have too many. - private const int maxDayRangeSize = 10; - private readonly ICalendarService _calendarService; private readonly INavigationService _navigationService; - private readonly IKeyPressService _keyPressService; + private readonly INativeAppService _nativeAppService; private readonly IPreferencesService _preferencesService; + private readonly IWinoRequestDelegator _winoRequestDelegator; + private readonly IMailDialogService _dialogService; + private readonly IDateContextProvider _dateContextProvider; + private readonly ICalendarRangeTextFormatter _calendarRangeTextFormatter; - // Store latest rendered options. - private CalendarDisplayType _currentDisplayType; - private int _displayDayCount; - - private SemaphoreSlim _calendarLoadingSemaphore = new(1); - private bool isLoadMoreBlocked = false; + private readonly SemaphoreSlim _calendarLoadingSemaphore = new(1); + private bool _subscriptionsAttached; + private CancellationTokenSource _pageLifetimeCts = new(); + private long _pageLifetimeVersion; + private List _loadedCalendarItems = []; [ObservableProperty] - private CalendarSettings _currentSettings; + public partial CalendarSettings CurrentSettings { get; set; } public IStatePersistanceService StatePersistanceService { get; } public IAccountCalendarStateService AccountCalendarStateService { get; } - public CalendarPageViewModel(IStatePersistanceService statePersistanceService, - ICalendarService calendarService, - INavigationService navigationService, - IKeyPressService keyPressService, - IAccountCalendarStateService accountCalendarStateService, - IPreferencesService preferencesService) + public CalendarPageViewModel( + IStatePersistanceService statePersistanceService, + ICalendarService calendarService, + INavigationService navigationService, + IKeyPressService keyPressService, + INativeAppService nativeAppService, + IAccountCalendarStateService accountCalendarStateService, + IPreferencesService preferencesService, + IWinoRequestDelegator winoRequestDelegator, + IMailDialogService dialogService, + IDateContextProvider dateContextProvider, + ICalendarRangeTextFormatter calendarRangeTextFormatter) { StatePersistanceService = statePersistanceService; AccountCalendarStateService = accountCalendarStateService; - _calendarService = calendarService; _navigationService = navigationService; - _keyPressService = keyPressService; + _nativeAppService = nativeAppService; _preferencesService = preferencesService; + _winoRequestDelegator = winoRequestDelegator; + _dialogService = dialogService; + _dateContextProvider = dateContextProvider; + _calendarRangeTextFormatter = calendarRangeTextFormatter; - AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested; - AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged; + RefreshSettings(); + } + + public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args) + { + if (args.Handled || args.Mode != WinoApplicationMode.Calendar || args.Action != KeyboardShortcutAction.Delete) + return; + + if (DisplayDetailsCalendarItemViewModel?.CalendarItem == null) + return; + + if (DisplayDetailsCalendarItemViewModel.CalendarItem.IsRecurringParent) + { + var confirmed = await _dialogService.ShowConfirmationDialogAsync( + Translator.DialogMessage_DeleteRecurringSeriesMessage, + Translator.DialogMessage_DeleteRecurringSeriesTitle, + Translator.Buttons_Delete); + + if (!confirmed) + return; + } + + var preparationRequest = new CalendarOperationPreparationRequest( + CalendarSynchronizerOperation.DeleteEvent, + DisplayDetailsCalendarItemViewModel.CalendarItem, + null); + + await _winoRequestDelegator.ExecuteAsync(preparationRequest); + DisplayDetailsCalendarItemViewModel = null; + args.Handled = true; + } + + protected override void RegisterRecipients() + { + base.RegisterRecipients(); + + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + } + + protected override void UnregisterRecipients() + { + base.UnregisterRecipients(); + + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); } private void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e) - => FilterActiveCalendars(DayRanges); + { + EnsureSelectedQuickEventAccountCalendar(); + _ = ReloadCurrentVisibleRangeAsync(); + } private void UpdateAccountCalendarRequested(object sender, AccountCalendarViewModel e) - => FilterActiveCalendars(DayRanges); - - private async void FilterActiveCalendars(IEnumerable dayRangeRenderModels) { - await ExecuteUIThread(() => - { - var days = dayRangeRenderModels.SelectMany(a => a.CalendarDays); - - days.ForEach(a => a.EventsCollection.FilterByCalendars(AccountCalendarStateService.ActiveCalendars.Select(a => a.Id))); - - DisplayDetailsCalendarItemViewModel = null; - }); + EnsureSelectedQuickEventAccountCalendar(); + _ = ReloadCurrentVisibleRangeAsync(); } - // TODO: Replace when calendar settings are updated. - // Should be a field ideally. - private BaseCalendarTypeDrawingStrategy GetDrawingStrategy(CalendarDisplayType displayType) + [RelayCommand(CanExecute = nameof(CanJoinOnline))] + private async Task JoinOnlineAsync() { - return displayType switch - { - CalendarDisplayType.Day => new DayCalendarDrawingStrategy(CurrentSettings), - CalendarDisplayType.Week => new WeekCalendarDrawingStrategy(CurrentSettings), - CalendarDisplayType.Month => new MonthCalendarDrawingStrategy(CurrentSettings), - _ => throw new NotImplementedException(), - }; - } + if (DisplayDetailsCalendarItemViewModel == null || string.IsNullOrEmpty(DisplayDetailsCalendarItemViewModel.CalendarItem.HtmlLink)) + return; - public override void OnNavigatedFrom(NavigationMode mode, object parameters) - { - // Do not call base method because that will unregister messenger recipient. - // This is a singleton view model and should not be unregistered. + await _nativeAppService.LaunchUriAsync(new Uri(DisplayDetailsCalendarItemViewModel.CalendarItem.HtmlLink)); } public override void OnNavigatedTo(NavigationMode mode, object parameters) { + ResetPageLifetime(); base.OnNavigatedTo(mode, parameters); - - if (mode == NavigationMode.Back) return; - + AttachSubscriptions(); RefreshSettings(); + IsCalendarEnabled = true; + EnsureSelectedQuickEventAccountCalendar(); + } - // Automatically select the first primary calendar for quick event dialog. - SelectedQuickEventAccountCalendar = AccountCalendarStateService.ActiveCalendars.FirstOrDefault(a => a.IsPrimary); + public override void OnNavigatedFrom(NavigationMode mode, object parameters) + { + base.OnNavigatedFrom(mode, parameters); + + if (StatePersistanceService.ApplicationMode == WinoApplicationMode.Calendar) + { + CancelPendingOperations(); + DetachSubscriptions(); + return; + } + + CleanupForShellDeactivation(); + } + + private void AttachSubscriptions() + { + if (_subscriptionsAttached) + return; + + AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested; + AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged; + _subscriptionsAttached = true; + } + + private void DetachSubscriptions() + { + if (!_subscriptionsAttached) + return; + + AccountCalendarStateService.AccountCalendarSelectionStateChanged -= UpdateAccountCalendarRequested; + AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged -= AccountCalendarStateCollectivelyChanged; + _subscriptionsAttached = false; + } + + private void ReleasePageState() + { + DetachSubscriptions(); + DisplayDetailsCalendarItemViewModel = null; + SelectedQuickEventAccountCalendar = null; + SelectedQuickEventDate = null; + HourSelectionStrings = []; + CurrentVisibleRange = null; + VisibleDateRangeText = string.Empty; + LoadedDateWindow = null; + _loadedCalendarItems = []; + CalendarItems = []; + } + + public void Dispose() + { + CleanupForShellDeactivation(); + } + + public void CleanupForShellDeactivation() + { + CancelPendingOperations(); + ReleasePageState(); + GC.SuppressFinalize(this); + } + + public bool RestoreVisibleState() => CurrentVisibleRange != null; + + public DateTime GetRestoreDate() + => CurrentVisibleRange?.AnchorDate.ToDateTime(TimeOnly.MinValue) ?? DateTime.Now.Date; + + private long CurrentPageLifetimeVersion => Interlocked.Read(ref _pageLifetimeVersion); + + private bool IsPageActive(long lifetimeVersion) + => lifetimeVersion == CurrentPageLifetimeVersion && !_pageLifetimeCts.IsCancellationRequested; + + private void ResetPageLifetime() + { + CancelPendingOperations(); + _pageLifetimeCts = new CancellationTokenSource(); + Interlocked.Increment(ref _pageLifetimeVersion); + } + + private void CancelPendingOperations() + { + if (!_pageLifetimeCts.IsCancellationRequested) + { + _pageLifetimeCts.Cancel(); + } + } + + private async Task WaitForCalendarLoadingLockAsync(long lifetimeVersion) + { + if (!IsPageActive(lifetimeVersion)) + return false; + + try + { + await _calendarLoadingSemaphore.WaitAsync(_pageLifetimeCts.Token).ConfigureAwait(false); + return IsPageActive(lifetimeVersion); + } + catch (OperationCanceledException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + + private void ReleaseCalendarLoadingLock() + { + try + { + _calendarLoadingSemaphore.Release(); + } + catch (ObjectDisposedException) + { + } + catch (SemaphoreFullException) + { + } + } + + private async Task ExecuteUIThreadIfActiveAsync(long lifetimeVersion, Action action) + { + if (action == null || !IsPageActive(lifetimeVersion)) + return; + + try + { + await ExecuteUIThread(() => + { + if (IsPageActive(lifetimeVersion)) + { + action(); + } + }).ConfigureAwait(false); + } + catch (COMException) when (!IsPageActive(lifetimeVersion)) + { + } + catch (ObjectDisposedException) when (!IsPageActive(lifetimeVersion)) + { + } } [RelayCommand] private void NavigateSeries() { - if (DisplayDetailsCalendarItemViewModel == null) return; + if (DisplayDetailsCalendarItemViewModel == null) + return; NavigateEvent(DisplayDetailsCalendarItemViewModel, CalendarEventTargetType.Series); } @@ -221,7 +433,8 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, [RelayCommand] private void NavigateEventDetails() { - if (DisplayDetailsCalendarItemViewModel == null) return; + if (DisplayDetailsCalendarItemViewModel == null) + return; NavigateEvent(DisplayDetailsCalendarItemViewModel, CalendarEventTargetType.Single); } @@ -235,57 +448,81 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, [RelayCommand(AllowConcurrentExecutions = false, CanExecute = nameof(CanSaveQuickEvent))] private async Task SaveQuickEventAsync() { - var durationSeconds = (QuickEventEndTime - QuickEventStartTime).TotalSeconds; - - var testCalendarItem = new CalendarItem + var startDate = IsAllDay ? SelectedQuickEventDate.Value.Date : QuickEventStartTime; + var endDate = IsAllDay ? SelectedQuickEventDate.Value.Date.AddDays(1) : QuickEventEndTime; + var composeResult = new CalendarEventComposeResult { CalendarId = SelectedQuickEventAccountCalendar.Id, - StartDate = QuickEventStartTime, - DurationInSeconds = durationSeconds, - CreatedAt = DateTime.UtcNow, - Description = string.Empty, - Location = Location, + AccountId = SelectedQuickEventAccountCalendar.Account.Id, Title = EventName, - Id = Guid.NewGuid() + Location = Location ?? string.Empty, + HtmlNotes = string.Empty, + StartDate = startDate, + EndDate = endDate, + IsAllDay = IsAllDay, + TimeZoneId = TimeZoneInfo.Local.Id, + ShowAs = SelectedQuickEventAccountCalendar.DefaultShowAs, + SelectedReminders = [], + Attendees = [], + Attachments = [], + Recurrence = string.Empty, + RecurrenceSummary = string.Empty }; - IsQuickEventDialogOpen = false; - await _calendarService.CreateNewCalendarItemAsync(testCalendarItem, null); - - // TODO: Create the request with the synchronizer. + var preparationRequest = new CalendarOperationPreparationRequest( + CalendarSynchronizerOperation.CreateEvent, + ComposeResult: composeResult); + await _winoRequestDelegator.ExecuteAsync(preparationRequest); } [RelayCommand] - private void MoreDetails() + private void GoToEventComposePage() { - // TODO: Navigate to advanced event creation page with existing parameters. + if (SelectedQuickEventDate == null) + return; + + var startDate = SelectedQuickEventDate.Value; + var endDate = SelectedQuickEventDate.Value.AddMinutes(30); + + if (!IsAllDay) + { + var selectedStartTime = CurrentSettings.GetTimeSpan(SelectedStartTimeString); + var selectedEndTime = CurrentSettings.GetTimeSpan(SelectedEndTimeString); + + if (selectedStartTime.HasValue) + { + startDate = SelectedQuickEventDate.Value.Date.Add(selectedStartTime.Value); + } + + if (selectedEndTime.HasValue) + { + endDate = SelectedQuickEventDate.Value.Date.Add(selectedEndTime.Value); + } + } + else + { + startDate = SelectedQuickEventDate.Value.Date; + endDate = SelectedQuickEventDate.Value.Date.AddDays(1); + } + + _navigationService.Navigate(WinoPage.CalendarEventComposePage, new CalendarEventComposeNavigationArgs + { + SelectedCalendarId = SelectedQuickEventAccountCalendar?.Id, + Title = EventName ?? string.Empty, + Location = Location ?? string.Empty, + IsAllDay = IsAllDay, + StartDate = startDate, + EndDate = endDate + }); } public void SelectQuickEventTimeRange(TimeSpan startTime, TimeSpan endTime) { IsAllDay = false; - SelectedStartTimeString = CurrentSettings.GetTimeString(startTime); SelectedEndTimeString = CurrentSettings.GetTimeString(endTime); } - // Manage event detail popup context and select-unselect the proper items. - // Item selection rules are defined in the selection method. - partial void OnDisplayDetailsCalendarItemViewModelChanging(CalendarItemViewModel oldValue, CalendarItemViewModel newValue) - { - if (oldValue != null) - { - UnselectCalendarItem(oldValue); - } - - if (newValue != null) - { - SelectCalendarItem(newValue); - } - } - - // Notify view that the detail context changed. - // This will align the event detail popup to the selected event. partial void OnDisplayDetailsCalendarItemViewModelChanged(CalendarItemViewModel value) => DetailsShowCalendarItemChanged?.Invoke(this, EventArgs.Empty); @@ -293,414 +530,293 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, { CurrentSettings = _preferencesService.GetCurrentCalendarSettings(); - // Populate the hour selection strings. var timeStrings = new List(); - for (int hour = 0; hour < 24; hour++) { for (int minute = 0; minute < 60; minute += 30) { var time = new DateTime(1, 1, 1, hour, minute, 0); - - if (CurrentSettings.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour) - { - timeStrings.Add(time.ToString("HH:mm")); - } - else - { - timeStrings.Add(time.ToString("h:mm tt")); - } + timeStrings.Add(CurrentSettings.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour + ? time.ToString("HH:mm") + : time.ToString("h:mm tt")); } } HourSelectionStrings = timeStrings; - } - partial void OnIsCalendarEnabledChanging(bool oldValue, bool newValue) => Messenger.Send(new CalendarEnableStatusChangedMessage(newValue)); - - private bool ShouldResetDayRanges(LoadCalendarMessage message) - { - if (message.ForceRedraw) return true; - - // Never reset if the initiative is from the app. - if (message.CalendarInitInitiative == CalendarInitInitiative.App) return false; - - // 1. Display type is different. - // 2. Day display count is different. - // 3. Display date is not in the visible range. - - if (DayRanges.DisplayRange == null) return false; - - return - (_currentDisplayType != StatePersistanceService.CalendarDisplayType || - _displayDayCount != StatePersistanceService.DayDisplayCount || - !(message.DisplayDate >= DayRanges.DisplayRange.StartDate && message.DisplayDate <= DayRanges.DisplayRange.EndDate)); - } - - private void AdjustCalendarOrientation() - { - // Orientation only changes when we should reset. - // Handle the FlipView orientation here. - // We don't want to change the orientation while the item manipulation is going on. - // That causes a glitch in the UI. - - bool isRequestedVerticalCalendar = StatePersistanceService.CalendarDisplayType == CalendarDisplayType.Month; - bool isLastRenderedVerticalCalendar = _currentDisplayType == CalendarDisplayType.Month; - - if (isRequestedVerticalCalendar && !isLastRenderedVerticalCalendar) + if (CurrentVisibleRange != null) { - CalendarOrientation = CalendarOrientation.Vertical; - } - else - { - CalendarOrientation = CalendarOrientation.Horizontal; + VisibleDateRangeText = _calendarRangeTextFormatter.Format(CurrentVisibleRange, _dateContextProvider); } } - public async void Receive(LoadCalendarMessage message) + public async Task ApplyDisplayRequestAsync(CalendarDisplayRequest request, bool forceReload = false) { - await _calendarLoadingSemaphore.WaitAsync(); + var lifetimeVersion = CurrentPageLifetimeVersion; + var hasLoadingLock = await WaitForCalendarLoadingLockAsync(lifetimeVersion).ConfigureAwait(false); + + if (!hasLoadingLock) + return; try { - await ExecuteUIThread(() => IsCalendarEnabled = false); + await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () => IsCalendarEnabled = false).ConfigureAwait(false); - if (ShouldResetDayRanges(message)) - { - Debug.WriteLine("Will reset day ranges."); - await ClearDayRangeModelsAsync(); - } - else if (ShouldScrollToItem(message)) - { - // Scroll to the selected date. - Messenger.Send(new ScrollToDateMessage(message.DisplayDate)); - Debug.WriteLine("Scrolling to selected date."); + if (!IsPageActive(lifetimeVersion)) return; + + var currentSettings = CurrentSettings; + if (currentSettings == null) + { + RefreshSettings(); + currentSettings = CurrentSettings; } - AdjustCalendarOrientation(); + var today = _dateContextProvider.GetToday(); + var visibleRange = CalendarRangeResolver.Resolve(request, currentSettings, today); + var previousRange = CalendarRangeResolver.Navigate(visibleRange, -1, currentSettings, today); + var nextRange = CalendarRangeResolver.Navigate(visibleRange, 1, currentSettings, today); + var loadedDateWindow = new DateRange( + previousRange.StartDate.ToDateTime(TimeOnly.MinValue), + nextRange.EndDate.AddDays(1).ToDateTime(TimeOnly.MinValue)); - // This will replace the whole collection because the user initiated a new render. - await RenderDatesAsync(message.CalendarInitInitiative, - message.DisplayDate, - CalendarLoadDirection.Replace); + var shouldReload = forceReload || !IsSameVisibleRange(CurrentVisibleRange, visibleRange) || !IsSameDateRange(LoadedDateWindow, loadedDateWindow); + List loadedItems = null; - // Scroll to the current hour. - Messenger.Send(new ScrollToHourMessage(TimeSpan.FromHours(DateTime.Now.Hour))); + if (shouldReload) + { + loadedItems = await LoadCalendarItemsAsync(loadedDateWindow, lifetimeVersion).ConfigureAwait(false); + if (!IsPageActive(lifetimeVersion)) + return; + } + + await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () => + { + if (loadedItems != null) + { + _loadedCalendarItems = loadedItems; + CalendarItems = loadedItems; + } + + EnsureSelectedQuickEventAccountCalendar(); + CurrentVisibleRange = visibleRange; + LoadedDateWindow = loadedDateWindow; + VisibleDateRangeText = _calendarRangeTextFormatter.Format(visibleRange, _dateContextProvider); + if (DisplayDetailsCalendarItemViewModel != null && !IsCalendarActive(DisplayDetailsCalendarItemViewModel.AssignedCalendar?.Id)) + { + DisplayDetailsCalendarItemViewModel = null; + } + }).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + } + catch (COMException) when (!IsPageActive(lifetimeVersion)) + { + } + catch (ObjectDisposedException) when (!IsPageActive(lifetimeVersion)) + { } catch (Exception ex) { - Log.Error(ex, "Error while loading calendar."); - Debugger.Break(); + Log.Error(ex, "Error while loading visible calendar range."); } finally { - _calendarLoadingSemaphore.Release(); - - await ExecuteUIThread(() => IsCalendarEnabled = true); + ReleaseCalendarLoadingLock(); + await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () => IsCalendarEnabled = true).ConfigureAwait(false); } } - - private async Task AddDayRangeModelAsync(DayRangeRenderModel dayRangeRenderModel) + public Task ReloadCurrentVisibleRangeAsync() { - if (dayRangeRenderModel == null) return; + if (CurrentVisibleRange == null) + return Task.CompletedTask; - await ExecuteUIThread(() => - { - DayRanges.Add(dayRangeRenderModel); - }); + return ApplyDisplayRequestAsync(new CalendarDisplayRequest(CurrentVisibleRange.DisplayType, CurrentVisibleRange.AnchorDate), forceReload: true); } - private async Task InsertDayRangeModelAsync(DayRangeRenderModel dayRangeRenderModel, int index) + public async Task> SearchCalendarItemsAsync(string queryText, int limit, CancellationToken cancellationToken) { - if (dayRangeRenderModel == null) return; + var results = await _calendarService.SearchCalendarItemsAsync(queryText, limit, cancellationToken).ConfigureAwait(false); + var activeCalendarIds = AccountCalendarStateService.ActiveCalendars.Select(calendar => calendar.Id).ToHashSet(); - await ExecuteUIThread(() => - { - DayRanges.Insert(index, dayRangeRenderModel); - }); + return results + .Where(result => activeCalendarIds.Contains(result.CalendarId)) + .ToList(); } - private async Task RemoveDayRangeModelAsync(DayRangeRenderModel dayRangeRenderModel) + public void OpenCalendarSearchResult(CalendarItem calendarItem) { - if (dayRangeRenderModel == null) return; - - await ExecuteUIThread(() => - { - DayRanges.Remove(dayRangeRenderModel); - }); + ArgumentNullException.ThrowIfNull(calendarItem); + NavigateEvent(new CalendarItemViewModel(calendarItem), CalendarEventTargetType.Single); } - private async Task ClearDayRangeModelsAsync() + private async Task> LoadCalendarItemsAsync(DateRange loadedDateWindow, long lifetimeVersion) { - await ExecuteUIThread(() => + var loadedItems = new Dictionary(); + var loadPeriod = new TimeRange(loadedDateWindow.StartDate, loadedDateWindow.EndDate); + + foreach (var calendarViewModel in AccountCalendarStateService.ActiveCalendars) { - DayRanges.Clear(); - }); - } + if (!IsPageActive(lifetimeVersion)) + return []; - private async Task RenderDatesAsync(CalendarInitInitiative calendarInitInitiative, - DateTime? loadingDisplayDate = null, - CalendarLoadDirection calendarLoadDirection = CalendarLoadDirection.Replace) - { - isLoadMoreBlocked = calendarLoadDirection == CalendarLoadDirection.Replace; - - // This is the part we arrange the flip view calendar logic. - - /* Loading for a month of the selected date is fine. - * If the selected date is in the loaded range, we'll just change the selected flip index to scroll. - * If the selected date is not in the loaded range: - * 1. Detect the direction of the scroll. - * 2. Load the next month. - * 3. Replace existing month with the new month. - */ - - // 2 things are important: How many items should 1 flip have, and, where we should start loading? - - // User initiated renders must always have a date to start with. - if (calendarInitInitiative == CalendarInitInitiative.User) Guard.IsNotNull(loadingDisplayDate, nameof(loadingDisplayDate)); - - var strategy = GetDrawingStrategy(StatePersistanceService.CalendarDisplayType); - var displayDate = loadingDisplayDate.GetValueOrDefault(); - - // How many days should be placed in 1 flip view item? - int eachFlipItemCount = strategy.GetRenderDayCount(displayDate, StatePersistanceService.DayDisplayCount); - - DateRange flipLoadRange = null; - - - if (calendarInitInitiative == CalendarInitInitiative.User || DayRanges.DisplayRange == null) - { - flipLoadRange = strategy.GetRenderDateRange(displayDate, StatePersistanceService.DayDisplayCount); - } - else - { - // App is trying to load. - // This should be based on direction. We'll load the next or previous range. - // DisplayDate is either the start or end date of the current visible range. - - if (calendarLoadDirection == CalendarLoadDirection.Previous) + var events = await _calendarService.GetCalendarEventsAsync(calendarViewModel, loadPeriod).ConfigureAwait(false); + foreach (var calendarItem in events) { - flipLoadRange = strategy.GetPreviousDateRange(DayRanges.DisplayRange, StatePersistanceService.DayDisplayCount); - } - else - { - flipLoadRange = strategy.GetNextDateRange(DayRanges.DisplayRange, StatePersistanceService.DayDisplayCount); - } - } + if (calendarItem.IsRecurringParent || calendarItem.IsHidden) + continue; - // Create day ranges for each flip item until we reach the total days to load. - int totalFlipItemCount = (int)Math.Ceiling((double)flipLoadRange.TotalDays / eachFlipItemCount); + calendarItem.AssignedCalendar ??= calendarViewModel; - List renderModels = new(); - - for (int i = 0; i < totalFlipItemCount; i++) - { - var startDate = flipLoadRange.StartDate.AddDays(i * eachFlipItemCount); - var endDate = startDate.AddDays(eachFlipItemCount); - - var range = new DateRange(startDate, endDate); - var renderOptions = new CalendarRenderOptions(range, CurrentSettings); - - var dayRangeHeaderModel = new DayRangeRenderModel(renderOptions); - renderModels.Add(dayRangeHeaderModel); - } - - // Dates are loaded. Now load the events for them. - foreach (var renderModel in renderModels) - { - await InitializeCalendarEventsForDayRangeAsync(renderModel).ConfigureAwait(false); - } - - // Filter by active calendars. This is a quick operation, and things are not on the UI yet. - FilterActiveCalendars(renderModels); - - CalendarLoadDirection animationDirection = calendarLoadDirection; - - //bool removeCurrent = calendarLoadDirection == CalendarLoadDirection.Replace; - - if (calendarLoadDirection == CalendarLoadDirection.Replace) - { - // New date ranges are being replaced. - // We must preserve existing selection if any, add the items before/after the current one, remove the current one. - // This will make sure the new dates are animated in the correct direction. - - isLoadMoreBlocked = true; - - // Remove all other dates except this one. - var rangesToRemove = DayRanges.Where(a => a != SelectedDayRange).ToList(); - - foreach (var range in rangesToRemove) - { - await RemoveDayRangeModelAsync(range); - } - - animationDirection = displayDate <= SelectedDayRange?.CalendarRenderOptions.DateRange.StartDate ? - CalendarLoadDirection.Previous : CalendarLoadDirection.Next; - } - - if (animationDirection == CalendarLoadDirection.Next) - { - foreach (var item in renderModels) - { - await AddDayRangeModelAsync(item); - } - } - else if (animationDirection == CalendarLoadDirection.Previous) - { - // Wait for the animation to finish. - // Otherwise it somehow shutters a little, which is annoying. - - // if (!removeCurrent) await Task.Delay(350); - - // Insert each render model in reverse order. - for (int i = renderModels.Count - 1; i >= 0; i--) - { - await InsertDayRangeModelAsync(renderModels[i], 0); - } - } - - Debug.WriteLine($"Flip count: ({DayRanges.Count})"); - - foreach (var item in DayRanges) - { - Debug.WriteLine($"- {item.CalendarRenderOptions.DateRange.ToString()}"); - } - - //if (removeCurrent) - //{ - // await RemoveDayRangeModelAsync(SelectedDayRange); - //} - - // TODO... - // await TryConsolidateItemsAsync(); - - isLoadMoreBlocked = false; - - // Only scroll if the render is initiated by user. - // Otherwise we'll scroll to the app rendered invisible date range. - if (calendarInitInitiative == CalendarInitInitiative.User) - { - // Save the current settings for the page for later comparison. - _currentDisplayType = StatePersistanceService.CalendarDisplayType; - _displayDayCount = StatePersistanceService.DayDisplayCount; - - Messenger.Send(new ScrollToDateMessage(displayDate)); - } - } - - protected override async void OnCalendarItemAdded(CalendarItem calendarItem) - { - base.OnCalendarItemAdded(calendarItem); - - // Check if event falls into the current date range. - - - if (DayRanges.DisplayRange == null) return; - - // Check whether this event falls into any of the loaded date ranges. - var allDaysForEvent = DayRanges.SelectMany(a => a.CalendarDays).Where(a => a.Period.OverlapsWith(calendarItem.Period)); - - foreach (var calendarDay in allDaysForEvent) - { - var calendarItemViewModel = new CalendarItemViewModel(calendarItem); - - await ExecuteUIThread(() => - { - calendarDay.EventsCollection.AddCalendarItem(calendarItemViewModel); - }); - } - - FilterActiveCalendars(DayRanges); - } - - private async Task InitializeCalendarEventsForDayRangeAsync(DayRangeRenderModel dayRangeRenderModel) - { - // Clear all events first for all days. - foreach (var day in dayRangeRenderModel.CalendarDays) - { - await ExecuteUIThread(() => - { - day.EventsCollection.Clear(); - }); - } - - // Initialization is done for all calendars, regardless whether they are actively selected or not. - // This is because the filtering is cached internally of the calendar items in CalendarEventCollection. - var allCalendars = AccountCalendarStateService.GroupedAccountCalendars.SelectMany(a => a.AccountCalendars); - - foreach (var calendarViewModel in allCalendars) - { - // Check all the events for the given date range and calendar. - // Then find the day representation for all the events returned, and add to the collection. - - var events = await _calendarService.GetCalendarEventsAsync(calendarViewModel, dayRangeRenderModel).ConfigureAwait(false); - - foreach (var @event in events) - { - // Find the days that the event falls into. - var allDaysForEvent = dayRangeRenderModel.CalendarDays.Where(a => a.Period.OverlapsWith(@event.Period)); - - foreach (var calendarDay in allDaysForEvent) + if (!loadedItems.ContainsKey(calendarItem.Id)) { - var calendarItemViewModel = new CalendarItemViewModel(@event); - await ExecuteUIThread(() => - { - calendarDay.EventsCollection.AddCalendarItem(calendarItemViewModel); - }); + loadedItems.Add(calendarItem.Id, new CalendarItemViewModel(calendarItem)); } } } + + return loadedItems.Values.ToList(); } - private async Task TryConsolidateItemsAsync() + private static bool IsSameVisibleRange(VisibleDateRange current, VisibleDateRange next) { - // Check if trimming is necessary - if (DayRanges.Count > maxDayRangeSize) + if (current == null && next == null) + return true; + + if (current == null || next == null) + return false; + + return current.DisplayType == next.DisplayType && + current.AnchorDate == next.AnchorDate && + current.StartDate == next.StartDate && + current.EndDate == next.EndDate; + } + + private static bool IsSameDateRange(DateRange current, DateRange next) + { + if (current == null && next == null) + return true; + + if (current == null || next == null) + return false; + + return current.StartDate == next.StartDate && current.EndDate == next.EndDate; + } + + private bool IsCalendarActive(Guid? calendarId) + => calendarId.HasValue && AccountCalendarStateService.ActiveCalendars.Any(calendar => calendar.Id == calendarId.Value); + + private void EnsureSelectedQuickEventAccountCalendar() + { + if (SelectedQuickEventAccountCalendar != null && IsCalendarActive(SelectedQuickEventAccountCalendar.Id)) { - Debug.WriteLine("Trimming items."); + return; + } - isLoadMoreBlocked = true; + SelectedQuickEventAccountCalendar = AccountCalendarStateService.ActiveCalendars.FirstOrDefault(a => a.IsPrimary) + ?? AccountCalendarStateService.ActiveCalendars.FirstOrDefault(); + } - var removeCount = DayRanges.Count - maxDayRangeSize; + public async void Receive(LoadCalendarMessage message) + => await ApplyDisplayRequestAsync(message.DisplayRequest, message.ForceReload); - await Task.Delay(500); + public void Receive(CalendarSettingsUpdatedMessage message) + { + RefreshSettings(); + _ = ReloadCurrentVisibleRangeAsync(); + } - // Right shifted, remove from the start. - if (SelectedDateRangeIndex > DayRanges.Count / 2) - { - DayRanges.RemoveRange(DayRanges.Take(removeCount).ToList()); - } - else - { - // Left shifted, remove from the end. - DayRanges.RemoveRange(DayRanges.Skip(DayRanges.Count - removeCount).Take(removeCount)); - } + public void Receive(CalendarItemTappedMessage message) + { + if (message.CalendarItemViewModel == null) + return; - SelectedDateRangeIndex = DayRanges.IndexOf(SelectedDayRange); + DisplayDetailsCalendarItemViewModel = message.CalendarItemViewModel; + } + + public void Receive(CalendarItemDoubleTappedMessage message) + => NavigateEvent(message.CalendarItemViewModel, CalendarEventTargetType.Single); + + public void Receive(CalendarItemRightTappedMessage message) + { + } + + public async void Receive(AccountRemovedMessage message) + { + if (DisplayDetailsCalendarItemViewModel?.AssignedCalendar?.AccountId == message.Account.Id) + { + DisplayDetailsCalendarItemViewModel = null; + } + + EnsureSelectedQuickEventAccountCalendar(); + await ReloadCurrentVisibleRangeAsync().ConfigureAwait(false); + } + + protected override void OnCalendarItemDeleted(CalendarItem calendarItem) + { + base.OnCalendarItemDeleted(calendarItem); + + if (DisplayDetailsCalendarItemViewModel?.Id == calendarItem.Id || + DisplayDetailsCalendarItemViewModel?.CalendarItem?.RecurringCalendarItemId == calendarItem.Id) + { + DisplayDetailsCalendarItemViewModel = null; + } + + if (ShouldReloadFor(calendarItem)) + { + _ = ReloadCurrentVisibleRangeAsync(); } } - private bool ShouldScrollToItem(LoadCalendarMessage message) + protected override void OnCalendarItemUpdated(CalendarItem calendarItem, CalendarItemUpdateSource source) { - // Never scroll if the initiative is from the app. - if (message.CalendarInitInitiative == CalendarInitInitiative.App) return false; + base.OnCalendarItemUpdated(calendarItem, source); - // Nothing to scroll. - if (DayRanges.Count == 0) return false; + if (DisplayDetailsCalendarItemViewModel?.Id == calendarItem.Id) + { + calendarItem.AssignedCalendar ??= DisplayDetailsCalendarItemViewModel.AssignedCalendar; + DisplayDetailsCalendarItemViewModel = new CalendarItemViewModel(calendarItem); + } - if (DayRanges.DisplayRange == null) return false; + if (ShouldReloadFor(calendarItem)) + { + _ = ReloadCurrentVisibleRangeAsync(); + } + } - var selectedDate = message.DisplayDate; + protected override void OnCalendarItemAdded(CalendarItem calendarItem) + { + base.OnCalendarItemAdded(calendarItem); - return selectedDate >= DayRanges.DisplayRange.StartDate && selectedDate <= DayRanges.DisplayRange.EndDate; + if (calendarItem.IsRecurringParent) + { + _ = ReloadCurrentVisibleRangeAsync(); + return; + } + + if (ShouldReloadFor(calendarItem)) + { + _ = ReloadCurrentVisibleRangeAsync(); + } + } + + private bool ShouldReloadFor(CalendarItem calendarItem) + { + if (calendarItem == null || LoadedDateWindow == null) + return false; + + var loadedWindow = new TimeRange(LoadedDateWindow.StartDate, LoadedDateWindow.EndDate); + return loadedWindow.OverlapsWith(calendarItem.Period); } partial void OnIsAllDayChanged(bool value) { if (value) { + _previousSelectedStartTimeString = SelectedStartTimeString; + _previousSelectedEndTimeString = SelectedEndTimeString; SelectedStartTimeString = HourSelectionStrings.FirstOrDefault(); SelectedEndTimeString = HourSelectionStrings.FirstOrDefault(); } @@ -711,7 +827,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, } } - partial void OnSelectedStartTimeStringChanged(string newValue) + partial void OnSelectedStartTimeStringChanged(string oldValue, string newValue) { var parsedTime = CurrentSettings.GetTimeSpan(newValue); @@ -719,152 +835,23 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, { SelectedStartTimeString = _previousSelectedStartTimeString; } - else if (IsAllDay) + else if (!IsAllDay) { _previousSelectedStartTimeString = newValue; } } - partial void OnSelectedEndTimeStringChanged(string newValue) + partial void OnSelectedEndTimeStringChanged(string oldValue, string newValue) { var parsedTime = CurrentSettings.GetTimeSpan(newValue); if (parsedTime == null) { - SelectedEndTimeString = _previousSelectedStartTimeString; + SelectedEndTimeString = _previousSelectedEndTimeString; } - else if (IsAllDay) + else if (!IsAllDay) { _previousSelectedEndTimeString = newValue; } } - - partial void OnSelectedDayRangeChanged(DayRangeRenderModel value) - { - DisplayDetailsCalendarItemViewModel = null; - - if (DayRanges.Count == 0 || SelectedDateRangeIndex < 0) return; - - var selectedRange = DayRanges[SelectedDateRangeIndex]; - - Messenger.Send(new VisibleDateRangeChangedMessage(new DateRange(selectedRange.Period.Start, selectedRange.Period.End))); - - if (isLoadMoreBlocked) return; - - _ = LoadMoreAsync(); - } - - private async Task LoadMoreAsync() - { - try - { - await _calendarLoadingSemaphore.WaitAsync(); - - // Depending on the selected index, we'll load more dates. - // Day ranges may change while the async update is in progress. - // Therefore we wait for semaphore to be released before we continue. - // There is no need to load more if the current index is not in ideal position. - - if (SelectedDateRangeIndex == 0) - { - await RenderDatesAsync(CalendarInitInitiative.App, calendarLoadDirection: CalendarLoadDirection.Previous); - } - else if (SelectedDateRangeIndex == DayRanges.Count - 1) - { - await RenderDatesAsync(CalendarInitInitiative.App, calendarLoadDirection: CalendarLoadDirection.Next); - } - } - catch (Exception) - { - Debugger.Break(); - } - finally - { - _calendarLoadingSemaphore.Release(); - } - } - - public void Receive(CalendarSettingsUpdatedMessage message) - { - RefreshSettings(); - - // TODO: This might need throttling due to slider in the settings page for hour height. - // or make sure the slider does not update on each tick but on focus lost. - - // Messenger.Send(new LoadCalendarMessage(DateTime.UtcNow.Date, CalendarInitInitiative.App, true)); - } - - private IEnumerable GetCalendarItems(CalendarItemViewModel calendarItemViewModel, CalendarDayModel selectedDay) - { - // All-day and multi-day events are selected collectively. - // Recurring events must be selected as a single instance. - // We need to find the day that the event is in, and then select the event. - - if (!calendarItemViewModel.IsRecurringEvent) - { - return [calendarItemViewModel]; - } - else - { - return DayRanges - .SelectMany(a => a.CalendarDays) - .Select(b => b.EventsCollection.GetCalendarItem(calendarItemViewModel.Id)) - .Where(c => c != null) - .Cast() - .Distinct(); - } - } - - private void UnselectCalendarItem(CalendarItemViewModel calendarItemViewModel, CalendarDayModel calendarDay = null) - { - if (calendarItemViewModel == null) return; - - var itemsToUnselect = GetCalendarItems(calendarItemViewModel, calendarDay); - - foreach (var item in itemsToUnselect) - { - item.IsSelected = false; - } - } - - private void SelectCalendarItem(CalendarItemViewModel calendarItemViewModel, CalendarDayModel calendarDay = null) - { - if (calendarItemViewModel == null) return; - - var itemsToSelect = GetCalendarItems(calendarItemViewModel, calendarDay); - - foreach (var item in itemsToSelect) - { - item.IsSelected = true; - } - } - - public void Receive(CalendarItemTappedMessage message) - { - if (message.CalendarItemViewModel == null) return; - - DisplayDetailsCalendarItemViewModel = message.CalendarItemViewModel; - } - - public void Receive(CalendarItemDoubleTappedMessage message) => NavigateEvent(message.CalendarItemViewModel, CalendarEventTargetType.Single); - - public void Receive(CalendarItemRightTappedMessage message) - { - - } - - public async void Receive(CalendarItemDeleted message) - { - // Each deleted recurrence will report for it's own. - - await ExecuteUIThread(() => - { - var deletedItem = message.CalendarItem; - - // Event might be spreaded into multiple days. - // Remove from all. - - // var calendarItems = GetCalendarItems(deletedItem.Id); - }); - } } diff --git a/Wino.Calendar.ViewModels/CalendarPreferenceSettingsPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarPreferenceSettingsPageViewModel.cs new file mode 100644 index 00000000..c66b1d1d --- /dev/null +++ b/Wino.Calendar.ViewModels/CalendarPreferenceSettingsPageViewModel.cs @@ -0,0 +1,62 @@ +using System.Linq; +using CommunityToolkit.Mvvm.ComponentModel; +using Wino.Calendar.ViewModels.Data; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; + +namespace Wino.Calendar.ViewModels; + +public partial class CalendarPreferenceSettingsPageViewModel : CalendarSettingsSectionViewModelBase +{ + [ObservableProperty] + public partial CalendarNewEventBehaviorOption SelectedNewEventBehaviorOption { get; set; } + + [ObservableProperty] + public partial AccountCalendarViewModel SelectedNewEventCalendar { get; set; } + + public bool ShouldShowSpecificNewEventCalendar + => SelectedNewEventBehaviorOption?.Behavior == NewEventButtonBehavior.AlwaysUseSpecificCalendar; + + public CalendarPreferenceSettingsPageViewModel( + IPreferencesService preferencesService, + ICalendarService calendarService, + IAccountService accountService) + : base(preferencesService, calendarService, accountService) + { + LoadNewEventBehaviorOptions(); + SelectedNewEventBehaviorOption = GetSelectedNewEventBehaviorOption(); + + IsLoaded = true; + LoadCalendarsAsync(ApplyStoredNewEventCalendarPreference); + } + + partial void OnSelectedNewEventBehaviorOptionChanged(CalendarNewEventBehaviorOption value) + { + if (!IsLoaded) + return; + + OnPropertyChanged(nameof(ShouldShowSpecificNewEventCalendar)); + SaveNewEventBehavior(SelectedNewEventBehaviorOption, SelectedNewEventCalendar); + } + + partial void OnSelectedNewEventCalendarChanged(AccountCalendarViewModel value) + { + if (!IsLoaded) + return; + + SaveNewEventBehavior(SelectedNewEventBehaviorOption, value); + } + + private void ApplyStoredNewEventCalendarPreference() + { + var configuredCalendar = ResolveSelectedNewEventCalendar(); + if (PreferencesService.NewEventButtonBehavior == NewEventButtonBehavior.AlwaysUseSpecificCalendar && configuredCalendar == null) + { + SelectedNewEventBehaviorOption = NewEventBehaviorOptions.First(option => option.Behavior == NewEventButtonBehavior.AskEachTime); + SelectedNewEventCalendar = null; + return; + } + + SelectedNewEventCalendar = configuredCalendar ?? ResolveFallbackNewEventCalendar(); + } +} diff --git a/Wino.Calendar.ViewModels/CalendarRenderingSettingsPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarRenderingSettingsPageViewModel.cs new file mode 100644 index 00000000..5a7fe685 --- /dev/null +++ b/Wino.Calendar.ViewModels/CalendarRenderingSettingsPageViewModel.cs @@ -0,0 +1,191 @@ +using System; +using System.Linq; +using CommunityToolkit.Mvvm.ComponentModel; +using Wino.Core.Domain.Interfaces; + +namespace Wino.Calendar.ViewModels; + +public partial class CalendarRenderingSettingsPageViewModel : CalendarSettingsSectionViewModelBase +{ + [ObservableProperty] + public partial double CellHourHeight { get; set; } + + [ObservableProperty] + public partial int SelectedFirstDayOfWeekIndex { get; set; } + + [ObservableProperty] + public partial bool Is24HourHeaders { get; set; } + + [ObservableProperty] + public partial bool IsWorkingHoursEnabled { get; set; } + + [ObservableProperty] + public partial TimeSpan WorkingHourStart { get; set; } + + [ObservableProperty] + public partial TimeSpan WorkingHourEnd { get; set; } + + [ObservableProperty] + public partial int WorkingDayStartIndex { get; set; } + + [ObservableProperty] + public partial int WorkingDayEndIndex { get; set; } + + [ObservableProperty] + public partial string TimedDayHeaderDateFormat { get; set; } = "ddd dd"; + + [ObservableProperty] + public partial int SelectedTimedDayHeaderFormatPresetIndex { get; set; } = -1; + + public CalendarRenderingSettingsPageViewModel( + IPreferencesService preferencesService, + ICalendarService calendarService, + IAccountService accountService) + : base(preferencesService, calendarService, accountService) + { + SelectedFirstDayOfWeekIndex = DayNames.IndexOf(CalendarCulture.DateTimeFormat.GetDayName(preferencesService.FirstDayOfWeek)); + Is24HourHeaders = preferencesService.Prefer24HourTimeFormat; + IsWorkingHoursEnabled = preferencesService.IsWorkingHoursEnabled; + WorkingHourStart = preferencesService.WorkingHourStart; + WorkingHourEnd = preferencesService.WorkingHourEnd; + CellHourHeight = preferencesService.HourHeight; + WorkingDayStartIndex = DayNames.IndexOf(CalendarCulture.DateTimeFormat.GetDayName(preferencesService.WorkingDayStart)); + WorkingDayEndIndex = DayNames.IndexOf(CalendarCulture.DateTimeFormat.GetDayName(preferencesService.WorkingDayEnd)); + TimedDayHeaderDateFormat = preferencesService.CalendarTimedDayHeaderDateFormat; + SelectedTimedDayHeaderFormatPresetIndex = TimedDayHeaderFormatPresets.IndexOf(TimedDayHeaderDateFormat); + + IsLoaded = true; + } + + partial void OnCellHourHeightChanged(double oldValue, double newValue) => SaveSettings(); + + partial void OnIs24HourHeadersChanged(bool value) + { + OnPropertyChanged(nameof(TimedHourLabelPreview)); + SaveSettings(); + } + + partial void OnSelectedFirstDayOfWeekIndexChanged(int value) => SaveSettings(); + partial void OnIsWorkingHoursEnabledChanged(bool value) => SaveSettings(); + partial void OnWorkingHourStartChanged(TimeSpan value) => SaveSettings(); + partial void OnWorkingHourEndChanged(TimeSpan value) => SaveSettings(); + partial void OnWorkingDayStartIndexChanged(int value) => SaveSettings(); + partial void OnWorkingDayEndIndexChanged(int value) => SaveSettings(); + + partial void OnTimedDayHeaderDateFormatChanged(string value) + { + OnPropertyChanged(nameof(TimedDayHeaderFormatPreview)); + OnPropertyChanged(nameof(TimedHourLabelPreview)); + + var normalizedFormat = string.IsNullOrWhiteSpace(value) ? "ddd dd" : value.Trim(); + var matchingPresetIndex = TimedDayHeaderFormatPresets + .Select((format, index) => new { format, index }) + .Where(item => string.Equals(item.format, normalizedFormat, StringComparison.Ordinal)) + .Select(item => item.index) + .DefaultIfEmpty(-1) + .First(); + + if (SelectedTimedDayHeaderFormatPresetIndex != matchingPresetIndex) + { + SelectedTimedDayHeaderFormatPresetIndex = matchingPresetIndex; + } + + SaveSettings(); + } + + partial void OnSelectedTimedDayHeaderFormatPresetIndexChanged(int value) + { + if (value < 0 || value >= TimedDayHeaderFormatPresets.Count) + return; + + var selectedPreset = TimedDayHeaderFormatPresets[value]; + if (string.Equals(TimedDayHeaderDateFormat, selectedPreset, StringComparison.Ordinal)) + return; + + TimedDayHeaderDateFormat = selectedPreset; + } + + public string TimedDayHeaderFormatPreview + { + get + { + var format = string.IsNullOrWhiteSpace(TimedDayHeaderDateFormat) ? "ddd dd" : TimedDayHeaderDateFormat.Trim(); + var previewDates = new[] + { + new DateTime(2026, 3, 23), + new DateTime(2026, 3, 24), + new DateTime(2026, 3, 25) + }; + + try + { + return string.Join(" · ", previewDates.Select(date => date.ToString(format, CalendarCulture))); + } + catch (FormatException) + { + return string.Join(" · ", previewDates.Select(date => date.ToString("ddd dd", CalendarCulture))); + } + } + } + + public string TimedHourLabelPreview + => string.Join(" · ", new[] { 0, 9, 14, 24 }.Select(CurrentSettingsPreviewLabel)); + + private string CurrentSettingsPreviewLabel(int hour) + { + if (Is24HourHeaders) + return hour.ToString(CalendarCulture); + + var displayHour = hour % 24; + return DateTime.Today.AddHours(displayHour).ToString("h tt", CalendarCulture); + } + + private void SaveSettings() + { + if (!IsLoaded) + return; + + PreferencesService.FirstDayOfWeek = SelectedFirstDayOfWeekIndex switch + { + 0 => DayOfWeek.Sunday, + 1 => DayOfWeek.Monday, + 2 => DayOfWeek.Tuesday, + 3 => DayOfWeek.Wednesday, + 4 => DayOfWeek.Thursday, + 5 => DayOfWeek.Friday, + 6 => DayOfWeek.Saturday, + _ => throw new ArgumentOutOfRangeException() + }; + + PreferencesService.WorkingDayStart = WorkingDayStartIndex switch + { + 0 => DayOfWeek.Sunday, + 1 => DayOfWeek.Monday, + 2 => DayOfWeek.Tuesday, + 3 => DayOfWeek.Wednesday, + 4 => DayOfWeek.Thursday, + 5 => DayOfWeek.Friday, + 6 => DayOfWeek.Saturday, + _ => throw new ArgumentOutOfRangeException() + }; + + PreferencesService.WorkingDayEnd = WorkingDayEndIndex switch + { + 0 => DayOfWeek.Sunday, + 1 => DayOfWeek.Monday, + 2 => DayOfWeek.Tuesday, + 3 => DayOfWeek.Wednesday, + 4 => DayOfWeek.Thursday, + 5 => DayOfWeek.Friday, + 6 => DayOfWeek.Saturday, + _ => throw new ArgumentOutOfRangeException() + }; + + PreferencesService.Prefer24HourTimeFormat = Is24HourHeaders; + PreferencesService.IsWorkingHoursEnabled = IsWorkingHoursEnabled; + PreferencesService.WorkingHourStart = WorkingHourStart; + PreferencesService.WorkingHourEnd = WorkingHourEnd; + PreferencesService.HourHeight = CellHourHeight; + PreferencesService.CalendarTimedDayHeaderDateFormat = TimedDayHeaderDateFormat; + } +} diff --git a/Wino.Calendar.ViewModels/CalendarSettingsPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarSettingsPageViewModel.cs deleted file mode 100644 index 4d0075c7..00000000 --- a/Wino.Calendar.ViewModels/CalendarSettingsPageViewModel.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Messaging; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Translations; -using Wino.Core.ViewModels; -using Wino.Messaging.Client.Calendar; - -namespace Wino.Calendar.ViewModels; - -public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel -{ - [ObservableProperty] - private double _cellHourHeight; - - [ObservableProperty] - private int _selectedFirstDayOfWeekIndex; - - [ObservableProperty] - private bool _is24HourHeaders; - - [ObservableProperty] - private TimeSpan _workingHourStart; - - [ObservableProperty] - private TimeSpan _workingHourEnd; - - [ObservableProperty] - private List _dayNames = []; - - [ObservableProperty] - private int _workingDayStartIndex; - - [ObservableProperty] - private int _workingDayEndIndex; - - public IPreferencesService PreferencesService { get; } - - private readonly bool _isLoaded = false; - - public CalendarSettingsPageViewModel(IPreferencesService preferencesService) - { - PreferencesService = preferencesService; - - var currentLanguageLanguageCode = WinoTranslationDictionary.GetLanguageFileNameRelativePath(preferencesService.CurrentLanguage); - - var cultureInfo = new CultureInfo(currentLanguageLanguageCode); - - // Populate the day names list - for (var i = 0; i < 7; i++) - { - _dayNames.Add(cultureInfo.DateTimeFormat.DayNames[i]); - } - - var cultureFirstDayName = cultureInfo.DateTimeFormat.GetDayName(preferencesService.FirstDayOfWeek); - - _selectedFirstDayOfWeekIndex = _dayNames.IndexOf(cultureFirstDayName); - _is24HourHeaders = preferencesService.Prefer24HourTimeFormat; - _workingHourStart = preferencesService.WorkingHourStart; - _workingHourEnd = preferencesService.WorkingHourEnd; - _cellHourHeight = preferencesService.HourHeight; - - _workingDayStartIndex = _dayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayStart)); - _workingDayEndIndex = _dayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayEnd)); - - _isLoaded = true; - } - - partial void OnCellHourHeightChanged(double oldValue, double newValue) => SaveSettings(); - partial void OnIs24HourHeadersChanged(bool value) => SaveSettings(); - partial void OnSelectedFirstDayOfWeekIndexChanged(int value) => SaveSettings(); - partial void OnWorkingHourStartChanged(TimeSpan value) => SaveSettings(); - partial void OnWorkingHourEndChanged(TimeSpan value) => SaveSettings(); - partial void OnWorkingDayStartIndexChanged(int value) => SaveSettings(); - partial void OnWorkingDayEndIndexChanged(int value) => SaveSettings(); - - public void SaveSettings() - { - if (!_isLoaded) return; - - PreferencesService.FirstDayOfWeek = SelectedFirstDayOfWeekIndex switch - { - 0 => DayOfWeek.Sunday, - 1 => DayOfWeek.Monday, - 2 => DayOfWeek.Tuesday, - 3 => DayOfWeek.Wednesday, - 4 => DayOfWeek.Thursday, - 5 => DayOfWeek.Friday, - 6 => DayOfWeek.Saturday, - _ => throw new ArgumentOutOfRangeException() - }; - - PreferencesService.WorkingDayStart = WorkingDayStartIndex switch - { - 0 => DayOfWeek.Sunday, - 1 => DayOfWeek.Monday, - 2 => DayOfWeek.Tuesday, - 3 => DayOfWeek.Wednesday, - 4 => DayOfWeek.Thursday, - 5 => DayOfWeek.Friday, - 6 => DayOfWeek.Saturday, - _ => throw new ArgumentOutOfRangeException() - }; - - PreferencesService.WorkingDayEnd = WorkingDayEndIndex switch - { - 0 => DayOfWeek.Sunday, - 1 => DayOfWeek.Monday, - 2 => DayOfWeek.Tuesday, - 3 => DayOfWeek.Wednesday, - 4 => DayOfWeek.Thursday, - 5 => DayOfWeek.Friday, - 6 => DayOfWeek.Saturday, - _ => throw new ArgumentOutOfRangeException() - }; - - PreferencesService.Prefer24HourTimeFormat = Is24HourHeaders; - PreferencesService.WorkingHourStart = WorkingHourStart; - PreferencesService.WorkingHourEnd = WorkingHourEnd; - PreferencesService.HourHeight = CellHourHeight; - - Messenger.Send(new CalendarSettingsUpdatedMessage()); - } -} diff --git a/Wino.Calendar.ViewModels/CalendarSettingsSectionViewModelBase.cs b/Wino.Calendar.ViewModels/CalendarSettingsSectionViewModelBase.cs new file mode 100644 index 00000000..3058479f --- /dev/null +++ b/Wino.Calendar.ViewModels/CalendarSettingsSectionViewModelBase.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Linq; +using Wino.Calendar.ViewModels.Data; +using Wino.Core.Domain; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Translations; +using Wino.Core.ViewModels; + +namespace Wino.Calendar.ViewModels; + +public abstract class CalendarSettingsSectionViewModelBase : CalendarBaseViewModel +{ + protected CalendarSettingsSectionViewModelBase( + IPreferencesService preferencesService, + ICalendarService calendarService, + IAccountService accountService) + { + PreferencesService = preferencesService; + CalendarService = calendarService; + AccountService = accountService; + + var languageCode = WinoTranslationDictionary.GetLanguageFileNameRelativePath(preferencesService.CurrentLanguage); + CalendarCulture = new CultureInfo(languageCode); + + for (var index = 0; index < 7; index++) + { + DayNames.Add(CalendarCulture.DateTimeFormat.DayNames[index]); + } + } + + protected IPreferencesService PreferencesService { get; } + protected ICalendarService CalendarService { get; } + protected IAccountService AccountService { get; } + protected CultureInfo CalendarCulture { get; } + protected bool IsLoaded { get; set; } + + public ObservableCollection DayNames { get; } = []; + public ObservableCollection ReminderOptions { get; } = []; + public ObservableCollection SnoozeOptions { get; } = []; + public ObservableCollection NewEventBehaviorOptions { get; } = []; + public ObservableCollection AvailableNewEventCalendars { get; } = []; + public ObservableCollection TimedDayHeaderFormatPresets { get; } = + [ + "ddd dd", + "dddd dd", + "ddd d MMM", + "dd MMM ddd", + "M/d ddd" + ]; + + protected void LoadReminderOptions() + { + ReminderOptions.Clear(); + + var predefinedMinutes = CalendarService.GetPredefinedReminderMinutes(); + ReminderOptions.Add("None"); + + foreach (var minutes in predefinedMinutes) + { + var displayText = minutes switch + { + >= 60 => $"{minutes / 60} Hour{(minutes / 60 > 1 ? "s" : "")}", + _ => $"{minutes} Minute{(minutes > 1 ? "s" : "")}" + }; + + ReminderOptions.Add(displayText); + } + } + + protected int GetSelectedReminderIndex() + { + if (PreferencesService.DefaultReminderDurationInSeconds == 0) + return 0; + + var minutes = (int)(PreferencesService.DefaultReminderDurationInSeconds / 60); + var predefinedMinutes = CalendarService.GetPredefinedReminderMinutes(); + var index = Array.IndexOf(predefinedMinutes, minutes); + return index >= 0 ? index + 1 : 0; + } + + protected void SaveReminderIndex(int selectedDefaultReminderIndex) + { + if (selectedDefaultReminderIndex == 0) + { + PreferencesService.DefaultReminderDurationInSeconds = 0; + return; + } + + var predefinedMinutes = CalendarService.GetPredefinedReminderMinutes(); + var minutes = predefinedMinutes[selectedDefaultReminderIndex - 1]; + PreferencesService.DefaultReminderDurationInSeconds = minutes * 60; + } + + protected void LoadSnoozeOptions() + { + SnoozeOptions.Clear(); + + foreach (var snoozeMinutes in CalendarReminderSnoozeOptions.GetSupportedSnoozeMinutes()) + { + SnoozeOptions.Add(string.Format(Translator.CalendarReminder_SnoozeMinutesOption, snoozeMinutes)); + } + } + + protected int GetSelectedSnoozeIndex() + { + var supportedSnoozeMinutes = CalendarReminderSnoozeOptions.GetSupportedSnoozeMinutes().ToArray(); + var selectedIndex = Array.IndexOf(supportedSnoozeMinutes, PreferencesService.DefaultSnoozeDurationInMinutes); + return selectedIndex >= 0 ? selectedIndex : 0; + } + + protected void SaveSnoozeIndex(int selectedDefaultSnoozeIndex) + { + var supportedSnoozeMinutes = CalendarReminderSnoozeOptions.GetSupportedSnoozeMinutes(); + if (supportedSnoozeMinutes.Count == 0) + return; + + var selectedIndex = Math.Clamp(selectedDefaultSnoozeIndex, 0, supportedSnoozeMinutes.Count - 1); + PreferencesService.DefaultSnoozeDurationInMinutes = supportedSnoozeMinutes[selectedIndex]; + } + + protected void LoadNewEventBehaviorOptions() + { + NewEventBehaviorOptions.Clear(); + NewEventBehaviorOptions.Add(new CalendarNewEventBehaviorOption(NewEventButtonBehavior.AskEachTime, Translator.CalendarSettings_NewEventBehavior_AskEachTime)); + NewEventBehaviorOptions.Add(new CalendarNewEventBehaviorOption(NewEventButtonBehavior.AlwaysUseSpecificCalendar, Translator.CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar)); + } + + protected CalendarNewEventBehaviorOption GetSelectedNewEventBehaviorOption() + => NewEventBehaviorOptions.FirstOrDefault(option => option.Behavior == PreferencesService.NewEventButtonBehavior) + ?? NewEventBehaviorOptions.First(); + + protected async void LoadCalendarsAsync(Action applySelection) + { + var accounts = await AccountService.GetAccountsAsync().ConfigureAwait(false); + var calendarsByAccount = new List(); + + foreach (var account in accounts) + { + var calendars = await CalendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false); + calendarsByAccount.AddRange(calendars.Select(calendar => new AccountCalendarViewModel(account, calendar))); + } + + await ExecuteUIThread(() => + { + AvailableNewEventCalendars.Clear(); + + foreach (var calendar in calendarsByAccount) + { + AvailableNewEventCalendars.Add(calendar); + } + + applySelection(); + }); + } + + protected AccountCalendarViewModel ResolveSelectedNewEventCalendar() + { + var configuredCalendarId = PreferencesService.DefaultNewEventCalendarId; + return configuredCalendarId.HasValue + ? AvailableNewEventCalendars.FirstOrDefault(calendar => calendar.Id == configuredCalendarId.Value) + : null; + } + + protected AccountCalendarViewModel ResolveFallbackNewEventCalendar() + => AvailableNewEventCalendars.FirstOrDefault(calendar => calendar.IsPrimary) + ?? AvailableNewEventCalendars.FirstOrDefault(); + + protected void SaveNewEventBehavior(CalendarNewEventBehaviorOption selectedBehaviorOption, AccountCalendarViewModel selectedCalendar) + { + var newEventBehavior = selectedBehaviorOption?.Behavior ?? NewEventButtonBehavior.AskEachTime; + if (newEventBehavior == NewEventButtonBehavior.AlwaysUseSpecificCalendar && selectedCalendar != null) + { + PreferencesService.NewEventButtonBehavior = NewEventButtonBehavior.AlwaysUseSpecificCalendar; + PreferencesService.DefaultNewEventCalendarId = selectedCalendar.Id; + return; + } + + PreferencesService.NewEventButtonBehavior = NewEventButtonBehavior.AskEachTime; + PreferencesService.DefaultNewEventCalendarId = null; + } +} + +public sealed class CalendarNewEventBehaviorOption +{ + public CalendarNewEventBehaviorOption(NewEventButtonBehavior behavior, string displayText) + { + Behavior = behavior; + DisplayText = displayText; + } + + public NewEventButtonBehavior Behavior { get; } + public string DisplayText { get; } +} diff --git a/Wino.Calendar.ViewModels/CalendarViewModelContainerSetup.cs b/Wino.Calendar.ViewModels/CalendarViewModelContainerSetup.cs deleted file mode 100644 index 2f8b452d..00000000 --- a/Wino.Calendar.ViewModels/CalendarViewModelContainerSetup.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Wino.Core; - -namespace Wino.Calendar.ViewModels; - -public static class CalendarViewModelContainerSetup -{ - public static void RegisterCalendarViewModelServices(this IServiceCollection services) - { - services.RegisterCoreServices(); - } -} diff --git a/Wino.Calendar.ViewModels/Data/AccountCalendarViewModel.cs b/Wino.Calendar.ViewModels/Data/AccountCalendarViewModel.cs index df55f72f..1f41f633 100644 --- a/Wino.Calendar.ViewModels/Data/AccountCalendarViewModel.cs +++ b/Wino.Calendar.ViewModels/Data/AccountCalendarViewModel.cs @@ -2,6 +2,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; namespace Wino.Calendar.ViewModels.Data; @@ -54,6 +55,12 @@ public partial class AccountCalendarViewModel : ObservableObject, IAccountCalend set => SetProperty(AccountCalendar.IsPrimary, value, AccountCalendar, (u, i) => u.IsPrimary = i); } + public bool IsSynchronizationEnabled + { + get => AccountCalendar.IsSynchronizationEnabled; + set => SetProperty(AccountCalendar.IsSynchronizationEnabled, value, AccountCalendar, (u, i) => u.IsSynchronizationEnabled = i); + } + public Guid AccountId { get => AccountCalendar.AccountId; @@ -65,5 +72,12 @@ public partial class AccountCalendarViewModel : ObservableObject, IAccountCalend get => AccountCalendar.RemoteCalendarId; set => SetProperty(AccountCalendar.RemoteCalendarId, value, AccountCalendar, (u, r) => u.RemoteCalendarId = r); } + + public CalendarItemShowAs DefaultShowAs + { + get => AccountCalendar.DefaultShowAs; + set => SetProperty(AccountCalendar.DefaultShowAs, value, AccountCalendar, (u, s) => u.DefaultShowAs = s); + } public Guid Id { get => ((IAccountCalendar)AccountCalendar).Id; set => ((IAccountCalendar)AccountCalendar).Id = value; } + public MailAccount MailAccount { get => MailAccount; set => MailAccount = value; } } diff --git a/Wino.Calendar.ViewModels/Data/CalendarAttachmentViewModel.cs b/Wino.Calendar.ViewModels/Data/CalendarAttachmentViewModel.cs new file mode 100644 index 00000000..3202909c --- /dev/null +++ b/Wino.Calendar.ViewModels/Data/CalendarAttachmentViewModel.cs @@ -0,0 +1,71 @@ +using System; +using System.IO; +using CommunityToolkit.Mvvm.ComponentModel; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Enums; +using Wino.Core.Extensions; + +namespace Wino.Calendar.ViewModels.Data; + +public partial class CalendarAttachmentViewModel : ObservableObject +{ + public CalendarAttachment Attachment { get; } + + public Guid Id => Attachment.Id; + public string FileName => Attachment.FileName; + public string ReadableSize { get; } + public MailAttachmentType AttachmentType { get; } + public bool IsDownloaded => Attachment.IsDownloaded; + + [ObservableProperty] + public partial bool IsBusy { get; set; } + + public CalendarAttachmentViewModel(CalendarAttachment attachment) + { + Attachment = attachment; + ReadableSize = attachment.Size.GetBytesReadable(); + + var extension = Path.GetExtension(FileName); + AttachmentType = GetAttachmentType(extension); + } + + private MailAttachmentType GetAttachmentType(string extension) + { + if (string.IsNullOrEmpty(extension)) + return MailAttachmentType.None; + + switch (extension.ToLower()) + { + case ".exe": + return MailAttachmentType.Executable; + case ".rar": + return MailAttachmentType.RarArchive; + case ".zip": + return MailAttachmentType.Archive; + case ".ogg": + case ".mp3": + case ".wav": + case ".aac": + case ".alac": + return MailAttachmentType.Audio; + case ".mp4": + case ".wmv": + case ".avi": + case ".flv": + return MailAttachmentType.Video; + case ".pdf": + return MailAttachmentType.PDF; + case ".htm": + case ".html": + return MailAttachmentType.HTML; + case ".png": + case ".jpg": + case ".jpeg": + case ".gif": + case ".jiff": + return MailAttachmentType.Image; + default: + return MailAttachmentType.Other; + } + } +} diff --git a/Wino.Calendar.ViewModels/Data/CalendarComposeAttachmentViewModel.cs b/Wino.Calendar.ViewModels/Data/CalendarComposeAttachmentViewModel.cs new file mode 100644 index 00000000..34aece53 --- /dev/null +++ b/Wino.Calendar.ViewModels/Data/CalendarComposeAttachmentViewModel.cs @@ -0,0 +1,57 @@ +using System; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models.Calendar; +using Wino.Core.Extensions; + +namespace Wino.Calendar.ViewModels.Data; + +public class CalendarComposeAttachmentViewModel +{ + public Guid Id { get; } = Guid.NewGuid(); + public string FileName { get; } + public string FilePath { get; } + public string FileExtension { get; } + public long Size { get; } + public string ReadableSize => Size.GetBytesReadable(); + public MailAttachmentType AttachmentType { get; } + + public CalendarComposeAttachmentViewModel(string fileName, string filePath, string fileExtension, long size) + { + FileName = fileName; + FilePath = filePath; + FileExtension = fileExtension; + Size = size; + AttachmentType = GetAttachmentType(fileExtension); + } + + public CalendarEventComposeAttachmentDraft ToDraftModel() + { + return new CalendarEventComposeAttachmentDraft + { + Id = Id, + FileName = FileName, + FilePath = FilePath, + FileExtension = FileExtension, + Size = Size + }; + } + + private static MailAttachmentType GetAttachmentType(string extension) + { + if (string.IsNullOrWhiteSpace(extension)) + return MailAttachmentType.None; + + return extension.ToLowerInvariant() switch + { + ".exe" => MailAttachmentType.Executable, + ".rar" => MailAttachmentType.RarArchive, + ".zip" => MailAttachmentType.Archive, + ".ogg" or ".mp3" or ".wav" or ".aac" or ".alac" => MailAttachmentType.Audio, + ".mp4" or ".wmv" or ".avi" or ".flv" => MailAttachmentType.Video, + ".pdf" => MailAttachmentType.PDF, + ".htm" or ".html" => MailAttachmentType.HTML, + ".png" or ".jpg" or ".jpeg" or ".gif" or ".jiff" => MailAttachmentType.Image, + _ => MailAttachmentType.Other + }; + } +} diff --git a/Wino.Calendar.ViewModels/Data/CalendarComposeAttendeeViewModel.cs b/Wino.Calendar.ViewModels/Data/CalendarComposeAttendeeViewModel.cs new file mode 100644 index 00000000..f6231563 --- /dev/null +++ b/Wino.Calendar.ViewModels/Data/CalendarComposeAttendeeViewModel.cs @@ -0,0 +1,23 @@ +using Wino.Core.Domain.Entities.Shared; + +namespace Wino.Calendar.ViewModels.Data; + +public class CalendarComposeAttendeeViewModel : IContactDisplayItem +{ + public string DisplayName { get; } + public string Email { get; } + public AccountContact ResolvedContact { get; } + public string Address => Email; + public AccountContact PreviewContact => ResolvedContact; + public bool HasDistinctDisplayName => !string.IsNullOrWhiteSpace(DisplayName) && !DisplayName.Equals(Email, System.StringComparison.OrdinalIgnoreCase); + + public CalendarComposeAttendeeViewModel(string displayName, string email, AccountContact resolvedContact = null) + { + DisplayName = string.IsNullOrWhiteSpace(displayName) ? email : displayName; + Email = email; + ResolvedContact = resolvedContact; + } + + public static CalendarComposeAttendeeViewModel FromContact(AccountContact contact) + => new(contact.Name, contact.Address, contact); +} diff --git a/Wino.Calendar.ViewModels/Data/CalendarItemViewModel.cs b/Wino.Calendar.ViewModels/Data/CalendarItemViewModel.cs index f402a562..de2416a6 100644 --- a/Wino.Calendar.ViewModels/Data/CalendarItemViewModel.cs +++ b/Wino.Calendar.ViewModels/Data/CalendarItemViewModel.cs @@ -2,8 +2,11 @@ using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using Itenso.TimePeriod; +using Wino.Core.Domain; using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Extensions; using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Calendar; namespace Wino.Calendar.ViewModels.Data; @@ -17,13 +20,52 @@ public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, IC public IAccountCalendar AssignedCalendar => CalendarItem.AssignedCalendar; - public DateTime StartDate { get => CalendarItem.StartDate; set => CalendarItem.StartDate = value; } + /// + /// Gets or sets the start date converted to user's local timezone for display. + /// The underlying CalendarItem stores dates according to their timezone. + /// + public DateTime StartDate + { + get + { + // Get start date in user's local timezone + return CalendarItem.LocalStartDate; + } + set + { + // All-day events use floating dates and should not shift across timezones. + CalendarItem.StartDate = CalendarItem.IsAllDayEvent + ? value.Date + : value.ToTimeZoneFromLocal(CalendarItem.StartTimeZone); + } + } - public DateTime EndDate => CalendarItem.EndDate; + /// + /// Gets the end date converted to user's local timezone for display. + /// The underlying CalendarItem stores dates according to their timezone. + /// + public DateTime EndDate + { + get + { + // Get end date in user's local timezone + return CalendarItem.LocalEndDate; + } + } public double DurationInSeconds { get => CalendarItem.DurationInSeconds; set => CalendarItem.DurationInSeconds = value; } - public ITimePeriod Period => CalendarItem.Period; + /// + /// Gets the time period in local time. + /// + public ITimePeriod Period + { + get + { + // Return a period using local times for UI display + return new TimeRange(StartDate, EndDate); + } + } public bool IsAllDayEvent => CalendarItem.IsAllDayEvent; public bool IsMultiDayEvent => CalendarItem.IsMultiDayEvent; @@ -32,7 +74,39 @@ public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, IC public bool IsRecurringParent => CalendarItem.IsRecurringParent; [ObservableProperty] - private bool _isSelected; + public partial bool IsSelected { get; set; } + + [ObservableProperty] + public partial bool IsBusy { get; set; } + + /// + /// The period of the day where this item is currently being displayed. + /// Used for multi-day event title formatting. + /// + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(DisplayTitle))] + public partial ITimePeriod DisplayingPeriod { get; set; } + + /// + /// Calendar settings for time formatting. + /// + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(DisplayTitle))] + public partial CalendarSettings CalendarSettings { get; set; } + + /// + /// Gets the display title based on the current displaying period. + /// + public string DisplayTitle + { + get + { + if (DisplayingPeriod == null || CalendarSettings == null) + return Title; + + return GetDisplayTitle(DisplayingPeriod, CalendarSettings); + } + } public ObservableCollection Attendees { get; } = new ObservableCollection(); @@ -41,5 +115,82 @@ public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, IC CalendarItem = calendarItem; } + /// + /// Updates the underlying CalendarItem with new data and raises property change notifications. + /// + /// The updated calendar item data. + public void UpdateFrom(CalendarItem calendarItem) + { + if (calendarItem == null || calendarItem.Id != CalendarItem.Id) + return; + + // Update all mutable properties + CalendarItem.Title = calendarItem.Title; + CalendarItem.Description = calendarItem.Description; + CalendarItem.Location = calendarItem.Location; + CalendarItem.StartDate = calendarItem.StartDate; + CalendarItem.StartTimeZone = calendarItem.StartTimeZone; + CalendarItem.EndTimeZone = calendarItem.EndTimeZone; + CalendarItem.DurationInSeconds = calendarItem.DurationInSeconds; + CalendarItem.Recurrence = calendarItem.Recurrence; + CalendarItem.RecurringCalendarItemId = calendarItem.RecurringCalendarItemId; + CalendarItem.OrganizerDisplayName = calendarItem.OrganizerDisplayName; + CalendarItem.OrganizerEmail = calendarItem.OrganizerEmail; + CalendarItem.IsLocked = calendarItem.IsLocked; + CalendarItem.IsHidden = calendarItem.IsHidden; + CalendarItem.CustomEventColorHex = calendarItem.CustomEventColorHex; + CalendarItem.HtmlLink = calendarItem.HtmlLink; + CalendarItem.Status = calendarItem.Status; + CalendarItem.Visibility = calendarItem.Visibility; + CalendarItem.ShowAs = calendarItem.ShowAs; + CalendarItem.UpdatedAt = calendarItem.UpdatedAt; + CalendarItem.AssignedCalendar = calendarItem.AssignedCalendar; + + // Raise property changed for all bindable properties + OnPropertyChanged(nameof(Title)); + OnPropertyChanged(nameof(StartDate)); + OnPropertyChanged(nameof(EndDate)); + OnPropertyChanged(nameof(DurationInSeconds)); + OnPropertyChanged(nameof(Period)); + OnPropertyChanged(nameof(IsAllDayEvent)); + OnPropertyChanged(nameof(IsMultiDayEvent)); + OnPropertyChanged(nameof(IsRecurringEvent)); + OnPropertyChanged(nameof(IsRecurringChild)); + OnPropertyChanged(nameof(IsRecurringParent)); + OnPropertyChanged(nameof(AssignedCalendar)); + OnPropertyChanged(nameof(DisplayTitle)); + } + + /// + /// Gets the display title for this calendar item when rendered in a specific day. + /// + public string GetDisplayTitle(ITimePeriod displayingPeriod, CalendarSettings calendarSettings) + { + if (!IsMultiDayEvent) + return Title; + + var periodRelation = Period.GetRelation(displayingPeriod); + + if (periodRelation == PeriodRelation.StartInside || periodRelation == PeriodRelation.EnclosingStartTouching) + { + // Event starts within this day: "HH:mm -> Title" + return $"{calendarSettings.GetTimeString(StartDate.TimeOfDay)} -> {Title}"; + } + else if (periodRelation == PeriodRelation.EndInside || periodRelation == PeriodRelation.EnclosingEndTouching) + { + // Event ends within this day: "Title <- HH:mm" + return $"{Title} <- {calendarSettings.GetTimeString(EndDate.TimeOfDay)}"; + } + else if (periodRelation == PeriodRelation.Enclosing) + { + // Event spans the entire day + return $"{Translator.CalendarItemAllDay} {Title}"; + } + else + { + return Title; + } + } + public override string ToString() => CalendarItem.Title; } diff --git a/Wino.Calendar.ViewModels/Data/GroupedAccountCalendarViewModel.cs b/Wino.Calendar.ViewModels/Data/GroupedAccountCalendarViewModel.cs index 92b388ba..0994b8c0 100644 --- a/Wino.Calendar.ViewModels/Data/GroupedAccountCalendarViewModel.cs +++ b/Wino.Calendar.ViewModels/Data/GroupedAccountCalendarViewModel.cs @@ -20,6 +20,7 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject { Account = account; AccountCalendars = new ObservableCollection(calendarViewModels); + AccountColorHex = account.AccountColorHex; ManageIsCheckedState(); @@ -69,10 +70,22 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject } [ObservableProperty] - private bool _isExpanded = true; + public partial bool IsExpanded { get; set; } = true; [ObservableProperty] - private bool? isCheckedState = true; + public partial bool? IsCheckedState { get; set; } = true; + + [ObservableProperty] + public partial string AccountColorHex { get; set; } = string.Empty; + + [ObservableProperty] + public partial bool IsSynchronizationInProgress { get; set; } + + [ObservableProperty] + public partial string SynchronizationStatus { get; set; } = string.Empty; + + public bool CanSynchronize => !IsSynchronizationInProgress; + public bool IsSynchronizationProgressVisible => IsSynchronizationInProgress; private bool _isExternalPropChangeBlocked = false; @@ -98,7 +111,7 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject _isExternalPropChangeBlocked = false; } - partial void OnIsCheckedStateChanged(bool? newValue) + partial void OnIsCheckedStateChanged(bool? oldValue, bool? newValue) { if (_isExternalPropChangeBlocked) return; @@ -142,4 +155,24 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject CalendarSelectionStateChanged?.Invoke(this, accountCalendarViewModel); } + + partial void OnIsSynchronizationInProgressChanged(bool value) + { + OnPropertyChanged(nameof(CanSynchronize)); + OnPropertyChanged(nameof(IsSynchronizationProgressVisible)); + } + + public void UpdateAccount(MailAccount updatedAccount) + { + if (updatedAccount == null || updatedAccount.Id != Account.Id) + return; + + Account.Name = updatedAccount.Name; + Account.Address = updatedAccount.Address; + Account.AccountColorHex = updatedAccount.AccountColorHex; + Account.AttentionReason = updatedAccount.AttentionReason; + Account.MergedInboxId = updatedAccount.MergedInboxId; + AccountColorHex = updatedAccount.AccountColorHex; + OnPropertyChanged(nameof(Account)); + } } diff --git a/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs b/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs index f57d797e..2cc5a0d2 100644 --- a/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs +++ b/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs @@ -1,14 +1,23 @@ using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics; +using System.IO; +using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; +using Serilog; using Wino.Calendar.ViewModels.Data; +using Wino.Core.Domain; +using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Calendar; +using Wino.Core.Domain.Models; using Wino.Core.Domain.Models.Navigation; +using Wino.Core.Services; using Wino.Core.ViewModels; using Wino.Messaging.Client.Calendar; @@ -19,43 +28,210 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel private readonly ICalendarService _calendarService; private readonly INativeAppService _nativeAppService; private readonly IPreferencesService _preferencesService; + private readonly IMailDialogService _dialogService; + private readonly IWinoRequestDelegator _winoRequestDelegator; + private readonly INavigationService _navigationService; + private readonly IUnderlyingThemeService _underlyingThemeService; + private readonly INotificationBuilder _notificationBuilder; + private readonly IContactService _contactService; public CalendarSettings CurrentSettings { get; } + public INativeAppService NativeAppService => _nativeAppService; + + [ObservableProperty] + public partial bool IsDarkWebviewRenderer { get; set; } + + public ObservableCollection Attachments { get; } = new ObservableCollection(); + + /// + /// Returns true if the current event has attachments. + /// + public bool HasAttachments => Attachments.Count > 0; #region Details [ObservableProperty] [NotifyPropertyChangedFor(nameof(CanViewSeries))] - private CalendarItemViewModel _currentEvent; + [NotifyPropertyChangedFor(nameof(CanEditSeries))] + [NotifyPropertyChangedFor(nameof(IsCurrentUserOrganizer))] + [NotifyPropertyChangedFor(nameof(CurrentRsvpText))] + [NotifyPropertyChangedFor(nameof(CurrentRsvpStatus))] + public partial CalendarItemViewModel CurrentEvent { get; set; } + + partial void OnCurrentEventChanged(CalendarItemViewModel value) + { + // Notify the view to re-render the description + Messenger.Send(new CalendarDescriptionRenderingRequested()); + } [ObservableProperty] - private CalendarItemViewModel _seriesParent; + public partial CalendarItemViewModel SeriesParent { get; set; } + [ObservableProperty] + public partial List Reminders { get; set; } + public ObservableCollection ReminderOptions { get; } = new ObservableCollection(); + + /// + /// Returns true if the event is part of a recurring series (as a child occurrence). + /// Used to enable "View Series" functionality. + /// public bool CanViewSeries => CurrentEvent?.IsRecurringChild ?? false; + /// + /// Returns true if the "Edit Series" button should be visible. + /// Only visible for child occurrences of recurring events, not for master events or single events. + /// + public bool CanEditSeries => CurrentEvent?.IsRecurringChild ?? false; + + /// + /// Returns true if the current user is the organizer of the event. + /// Used to determine if the user can invite attendees or modify the event. + /// + public bool IsCurrentUserOrganizer => CurrentEvent?.Attendees?.Any(a => a.IsOrganizer) ?? true; + #endregion - public EventDetailsPageViewModel(ICalendarService calendarService, INativeAppService nativeAppService, IPreferencesService preferencesService) + #region Show As Options + + public ObservableCollection ShowAsOptions { get; } = new ObservableCollection(); + + [ObservableProperty] + public partial ShowAsOption SelectedShowAsOption { get; set; } + + #endregion + + #region RSVP Panel + + [ObservableProperty] + public partial bool IsRsvpPanelVisible { get; set; } + + public bool IncludeRsvpMessage => !string.IsNullOrEmpty(RsvpMessage); + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IncludeRsvpMessage))] + public partial string RsvpMessage { get; set; } = string.Empty; + + public ObservableCollection RsvpStatusOptions { get; } = new ObservableCollection(); + + public CalendarItemStatus CurrentRsvpStatus + { + get + { + return CurrentEvent?.CalendarItem?.Status ?? CalendarItemStatus.NotResponded; + } + } + + public string CurrentRsvpText + { + get + { + if (CurrentEvent?.CalendarItem == null) return Translator.CalendarEventResponse_Accept; + + return CurrentEvent.CalendarItem.Status switch + { + CalendarItemStatus.Accepted => Translator.CalendarEventResponse_AcceptedResponse, + CalendarItemStatus.Tentative => Translator.CalendarEventResponse_TentativeResponse, + CalendarItemStatus.Cancelled => Translator.CalendarEventResponse_DeclinedResponse, + CalendarItemStatus.NotResponded => Translator.CalendarEventResponse_NotResponded, + _ => Translator.CalendarEventResponse_NotResponded + }; + } + } + + #endregion + + public EventDetailsPageViewModel(ICalendarService calendarService, + INativeAppService nativeAppService, + IPreferencesService preferencesService, + IMailDialogService dialogService, + IWinoRequestDelegator winoRequestDelegator, + INavigationService navigationService, + INotificationBuilder notificationBuilder, + IUnderlyingThemeService underlyingThemeService, + IContactService contactService) { _calendarService = calendarService; _nativeAppService = nativeAppService; _preferencesService = preferencesService; + _dialogService = dialogService; + _winoRequestDelegator = winoRequestDelegator; + _navigationService = navigationService; + _underlyingThemeService = underlyingThemeService; + _notificationBuilder = notificationBuilder; + _contactService = contactService; CurrentSettings = _preferencesService.GetCurrentCalendarSettings(); + IsDarkWebviewRenderer = _underlyingThemeService.IsUnderlyingThemeDark(); + + // Initialize Show As options + ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Free)); + ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Tentative)); + ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Busy)); + ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.OutOfOffice)); + ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.WorkingElsewhere)); + SelectedShowAsOption = ShowAsOptions[2]; // Default to Busy + + // Initialize RSVP status options + RsvpStatusOptions.Add(new RsvpStatusOption(CalendarItemStatus.Accepted)); + RsvpStatusOptions.Add(new RsvpStatusOption(CalendarItemStatus.Tentative)); + RsvpStatusOptions.Add(new RsvpStatusOption(CalendarItemStatus.Cancelled)); } public override async void OnNavigatedTo(NavigationMode mode, object parameters) { base.OnNavigatedTo(mode, parameters); - Messenger.Send(new DetailsPageStateChangedMessage(true)); - if (parameters == null || parameters is not CalendarItemTarget args) return; await LoadCalendarItemTargetAsync(args); } + protected override async void OnCalendarItemUpdated(CalendarItem calendarItem, CalendarItemUpdateSource source) + { + base.OnCalendarItemUpdated(calendarItem, source); + + // If the current event was updated, reload it + if (CurrentEvent?.CalendarItem?.Id == calendarItem.Id || CurrentEvent?.CalendarItem.RecurringCalendarItemId == calendarItem.Id) + { + // Reflect client-side optimistic changes immediately; fallback to DB for server updates. + if (source == CalendarItemUpdateSource.ClientUpdated || source == CalendarItemUpdateSource.ClientReverted) + { + var previousAttendees = CurrentEvent?.Attendees?.ToList() ?? []; + CurrentEvent = new CalendarItemViewModel(calendarItem) + { + IsBusy = source == CalendarItemUpdateSource.ClientUpdated + }; + + foreach (var attendee in previousAttendees) + { + CurrentEvent.Attendees.Add(attendee); + } + + return; + } + + // Refresh from DB when update comes from server sync. + var refreshedEvent = await _calendarService.GetCalendarItemAsync(calendarItem.Id); + if (refreshedEvent != null) + { + CurrentEvent = new CalendarItemViewModel(refreshedEvent); + await LoadAttendeesAsync(refreshedEvent.Id, refreshedEvent); + } + } + } + + protected override void OnCalendarItemDeleted(CalendarItem calendarItem) + { + base.OnCalendarItemDeleted(calendarItem); + + // If the current event was deleted, navigate back + if (CurrentEvent?.CalendarItem?.Id == calendarItem.Id || CurrentEvent?.CalendarItem.RecurringCalendarItemId == calendarItem.Id) + { + NavigateBackToCalendar(forceReload: true); + } + } + private async Task LoadCalendarItemTargetAsync(CalendarItemTarget target) { try @@ -67,12 +243,17 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel CurrentEvent = new CalendarItemViewModel(currentEventItem); - var attendees = await _calendarService.GetAttendeesAsync(currentEventItem.EventTrackingId); + await LoadAttendeesAsync(currentEventItem.Id, currentEventItem); - foreach (var item in attendees) - { - CurrentEvent.Attendees.Add(item); - } + // Initialize SelectedShowAsOption based on current event's ShowAs + SelectedShowAsOption = ShowAsOptions.FirstOrDefault(o => o.ShowAs == currentEventItem.ShowAs) ?? ShowAsOptions[2]; + + // Load reminders for this calendar item + Reminders = await _calendarService.GetRemindersAsync(currentEventItem.Id); + InitializeReminderOptions(); + + // Load attachments + await LoadAttachmentsAsync(currentEventItem.Id); } catch (Exception ex) { @@ -80,36 +261,572 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel } } - public override void OnNavigatedFrom(NavigationMode mode, object parameters) + private async Task LoadAttendeesAsync(Guid calendarItemId, CalendarItem calendarItem) { - base.OnNavigatedFrom(mode, parameters); + var attendees = await _calendarService.GetAttendeesAsync(calendarItemId); - Messenger.Send(new DetailsPageStateChangedMessage(false)); + // Resolve contacts for all attendees in a single batch DB query. + var emails = attendees + .Where(a => !string.IsNullOrEmpty(a.Email)) + .Select(a => a.Email) + .ToList(); + + if (!string.IsNullOrEmpty(calendarItem.OrganizerEmail)) + emails.Add(calendarItem.OrganizerEmail); + + var contacts = await _contactService.GetContactsByAddressesAsync(emails).ConfigureAwait(false); + var contactLookup = contacts.ToDictionary(c => c.Address, StringComparer.OrdinalIgnoreCase); + + foreach (var attendee in attendees) + { + if (!string.IsNullOrEmpty(attendee.Email) && contactLookup.TryGetValue(attendee.Email, out var contact)) + attendee.ResolvedContact = contact; + } + + // Separate organizer from other attendees to ensure organizer is always first + var organizer = attendees.FirstOrDefault(a => a.IsOrganizer); + var nonOrganizerAttendees = attendees.Where(a => !a.IsOrganizer).ToList(); + + var attendeesForUi = new List(); + + // If the organizer is in the list, add them first + if (organizer != null) + { + attendeesForUi.Add(organizer); + } + else if (!string.IsNullOrEmpty(calendarItem.OrganizerEmail)) + { + // If the organizer is not in the attendees list, create and add them first + var organizerAttendee = new CalendarEventAttendee + { + Id = Guid.NewGuid(), + CalendarItemId = calendarItem.Id, + Name = calendarItem.OrganizerDisplayName ?? calendarItem.OrganizerEmail, + Email = calendarItem.OrganizerEmail, + IsOrganizer = true, + AttendenceStatus = AttendeeStatus.Accepted + }; + + if (contactLookup.TryGetValue(calendarItem.OrganizerEmail, out var organizerContact)) + organizerAttendee.ResolvedContact = organizerContact; + + attendeesForUi.Add(organizerAttendee); + } + + // Add all other attendees after the organizer + foreach (var item in nonOrganizerAttendees) + { + attendeesForUi.Add(item); + } + + await ExecuteUIThread(() => + { + if (CurrentEvent == null) + return; + + CurrentEvent.Attendees.Clear(); + + foreach (var attendee in attendeesForUi) + { + CurrentEvent.Attendees.Add(attendee); + } + }); } + private async Task LoadAttachmentsAsync(Guid calendarItemId) + { + Attachments.Clear(); + + try + { + var attachments = await _calendarService.GetAttachmentsAsync(calendarItemId); + + foreach (var attachment in attachments) + { + Attachments.Add(new CalendarAttachmentViewModel(attachment)); + } + + OnPropertyChanged(nameof(HasAttachments)); + } + catch (Exception ex) + { + Debug.WriteLine($"Error loading attachments: {ex.Message}"); + } + } + + private void InitializeReminderOptions() + { + ReminderOptions.Clear(); + + // Add predefined options from service + var predefinedMinutes = _calendarService.GetPredefinedReminderMinutes(); + var predefinedOptions = predefinedMinutes.Select(m => new ReminderOption(m)).ToList(); + + // Add custom reminders from synced data + if (Reminders != null) + { + foreach (var reminder in Reminders) + { + // Convert seconds to minutes + var minutesDiff = (int)(reminder.DurationInSeconds / 60); + + // Check if this is a custom value not in predefined list + if (!predefinedMinutes.Contains(minutesDiff)) + { + predefinedOptions.Add(new ReminderOption(minutesDiff, isCustom: true)); + } + } + } + + // Sort by minutes descending and add to collection + foreach (var option in predefinedOptions.OrderByDescending(o => o.Minutes)) + { + ReminderOptions.Add(option); + } + + // Set selected state based on current reminders + if (Reminders != null) + { + foreach (var reminder in Reminders) + { + // Convert seconds to minutes + var minutesDiff = (int)(reminder.DurationInSeconds / 60); + + var matchingOption = ReminderOptions.FirstOrDefault(o => o.Minutes == minutesDiff); + matchingOption?.IsSelected = true; + } + } + } + + [RelayCommand] private async Task SaveAsync() { + if (CurrentEvent == null) return; + try + { + // Capture original state BEFORE making any changes for potential revert + var originalItem = await _calendarService.GetCalendarItemAsync(CurrentEvent.CalendarItem.Id); + var originalAttendees = await _calendarService.GetAttendeesAsync(CurrentEvent.CalendarItem.Id); + + // Get selected reminder options + var selectedOptions = ReminderOptions.Where(o => o.IsSelected).ToList(); + + // Create separate Reminder entities for each selected option + var newReminders = new List(); + + foreach (var option in selectedOptions) + { + var durationInSeconds = option.Minutes * 60; // Convert minutes to seconds + + newReminders.Add(new Reminder + { + Id = Guid.NewGuid(), + CalendarItemId = CurrentEvent.Id, + DurationInSeconds = durationInSeconds, + ReminderType = CalendarItemReminderType.Popup + }); + } + + // Save reminders to database + await _calendarService.SaveRemindersAsync(CurrentEvent.CalendarItem.Id, newReminders); + Reminders = newReminders; + + // Update ShowAs if changed + if (SelectedShowAsOption != null) + { + CurrentEvent.CalendarItem.ShowAs = SelectedShowAsOption.ShowAs; + } + + // Update the calendar item and attendees in database + await _calendarService.UpdateCalendarItemAsync(CurrentEvent.CalendarItem, CurrentEvent.Attendees.ToList()); + + // Queue the update request to synchronizer with original state for revert capability + var preparationRequest = new CalendarOperationPreparationRequest( + CalendarSynchronizerOperation.UpdateEvent, + CurrentEvent.CalendarItem, + CurrentEvent.Attendees.ToList(), + ResponseMessage: null, + OriginalItem: originalItem, + OriginalAttendees: originalAttendees); + + await _winoRequestDelegator.ExecuteAsync(preparationRequest); + + NavigateBackToCalendar(forceReload: true); + } + catch (Exception ex) + { + Debug.WriteLine($"Error saving event: {ex.Message}"); + _dialogService.InfoBarMessage( + Translator.Info_AttachmentSaveFailedTitle, + ex.Message, + InfoBarMessageType.Error); + } } [RelayCommand] private async Task DeleteAsync() { + if (CurrentEvent == null) return; + // If the event is a master recurring event, ask for confirmation + if (CurrentEvent.IsRecurringParent) + { + var confirmed = await _dialogService.ShowConfirmationDialogAsync( + Translator.DialogMessage_DeleteRecurringSeriesMessage, + Translator.DialogMessage_DeleteRecurringSeriesTitle, + Translator.Buttons_Delete); + + if (!confirmed) return; + } + + try + { + var preparationRequest = new CalendarOperationPreparationRequest( + CalendarSynchronizerOperation.DeleteEvent, + CurrentEvent.CalendarItem, + null); + + await _winoRequestDelegator.ExecuteAsync(preparationRequest); + + NavigateBackToCalendar(forceReload: true); + } + catch (Exception ex) + { + Debug.WriteLine($"Error deleting calendar event: {ex.Message}"); + } + } + + private void NavigateBackToCalendar(bool forceReload) + { + var navigationDate = CurrentEvent?.CalendarItem.LocalStartDate ?? DateTime.Now; + + _navigationService.Navigate( + WinoPage.CalendarPage, + new CalendarPageNavigationArgs + { + NavigationDate = navigationDate, + ForceReload = forceReload + }); + } + + public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args) + { + if (args.Handled || args.Mode != WinoApplicationMode.Calendar || args.Action != KeyboardShortcutAction.Delete) + return; + + await DeleteAsync(); + args.Handled = true; } [RelayCommand] - private Task JoinOnline() + private Task JoinOnlineAsync() { - if (CurrentEvent == null || string.IsNullOrEmpty(CurrentEvent.CalendarItem.HtmlLink)) return Task.CompletedTask; + if (CurrentEvent == null || string.IsNullOrEmpty(CurrentEvent.CalendarItem.HtmlLink)) + return Task.CompletedTask; return _nativeAppService.LaunchUriAsync(new Uri(CurrentEvent.CalendarItem.HtmlLink)); } [RelayCommand] - private async Task Respond(CalendarItemStatus status) + private Task CreateTestNotificationAsync() + { + if (CurrentEvent?.CalendarItem == null) + return Task.CompletedTask; + + var reminderDurationInSeconds = Reminders? + .Where(x => x.DurationInSeconds > 0) + .OrderByDescending(x => x.DurationInSeconds) + .Select(x => x.DurationInSeconds) + .FirstOrDefault() ?? 0; + + if (reminderDurationInSeconds <= 0) + reminderDurationInSeconds = Math.Max(_preferencesService.DefaultReminderDurationInSeconds, 30 * 60); + + return _notificationBuilder.CreateCalendarReminderNotificationAsync(CurrentEvent.CalendarItem, reminderDurationInSeconds); + } + + [RelayCommand] + private void ToggleRsvpPanel() + { + IsRsvpPanelVisible = !IsRsvpPanelVisible; + + if (IsRsvpPanelVisible && CurrentEvent?.CalendarItem != null) + { + // Initialize selection based on current status + foreach (var item in RsvpStatusOptions) + { + item.IsSelected = CurrentEvent?.CalendarItem?.Status == item.Status; + } + } + } + + [RelayCommand] + private void CloseRsvpPanel() + { + IsRsvpPanelVisible = false; + RsvpMessage = string.Empty; + } + + [RelayCommand] + private async Task SendRsvpResponse(AttendeeStatus status) { if (CurrentEvent == null) return; + + try + { + // Get the optional response message if user wants to include it + var responseMessage = IncludeRsvpMessage ? RsvpMessage : null; + + // Map status to operation + CalendarSynchronizerOperation operation = status switch + { + AttendeeStatus.Accepted => CalendarSynchronizerOperation.AcceptEvent, + AttendeeStatus.Tentative => CalendarSynchronizerOperation.TentativeEvent, + AttendeeStatus.Declined => CalendarSynchronizerOperation.DeclineEvent, + _ => throw new InvalidOperationException($"Invalid RSVP status: {status}") + }; + + // Create preparation request with the optional message + var preparationRequest = new CalendarOperationPreparationRequest( + operation, + CurrentEvent.CalendarItem, + null, + responseMessage); + + await _winoRequestDelegator.ExecuteAsync(preparationRequest); + + // Reload attendees to get the updated status from the server + await LoadAttendeesAsync(CurrentEvent.CalendarItem.Id, CurrentEvent.CalendarItem); + + OnPropertyChanged(nameof(CurrentRsvpText)); + OnPropertyChanged(nameof(CurrentRsvpStatus)); + + CloseRsvpPanel(); + } + catch (Exception ex) + { + Debug.WriteLine($"Error sending RSVP response: {ex.Message}"); + _dialogService.InfoBarMessage( + Translator.Info_AttachmentSaveFailedTitle, + ex.Message, + InfoBarMessageType.Error); + } + } + + [RelayCommand] + private async Task ViewSeriesAsync() + { + if (CurrentEvent == null || !CurrentEvent.IsRecurringChild) return; + + try + { + // Get the master event from the recurring series + var masterEventId = CurrentEvent.CalendarItem.RecurringCalendarItemId.Value; + var masterEvent = await _calendarService.GetCalendarItemAsync(masterEventId); + + if (masterEvent == null) return; + + // Load the master event without navigation + var target = new CalendarItemTarget(masterEvent, CalendarEventTargetType.Series); + await LoadCalendarItemTargetAsync(target); + } + catch (Exception ex) + { + Debug.WriteLine($"Error loading series: {ex.Message}"); + } + } + + [RelayCommand] + private async Task OpenAttachmentAsync(CalendarAttachmentViewModel attachmentViewModel) + { + if (attachmentViewModel == null || CurrentEvent?.CalendarItem == null) return; + + try + { + attachmentViewModel.IsBusy = true; + + // If not downloaded, download it first + if (!attachmentViewModel.IsDownloaded) + { + await DownloadAttachmentAsync(attachmentViewModel); + } + + // Launch the file + if (!string.IsNullOrEmpty(attachmentViewModel.Attachment.LocalFilePath) && + File.Exists(attachmentViewModel.Attachment.LocalFilePath)) + { + await _nativeAppService.LaunchFileAsync(attachmentViewModel.Attachment.LocalFilePath); + } + } + catch (Exception ex) + { + Log.Error(ex, "Failed to open calendar attachment."); + _dialogService.InfoBarMessage( + Translator.Info_AttachmentOpenFailedTitle, + Translator.Info_AttachmentOpenFailedMessage, + InfoBarMessageType.Error); + } + finally + { + attachmentViewModel.IsBusy = false; + } + } + + [RelayCommand] + private async Task SaveAttachmentAsync(CalendarAttachmentViewModel attachmentViewModel) + { + if (attachmentViewModel == null) return; + + try + { + attachmentViewModel.IsBusy = true; + + var pickedPath = await _dialogService.PickWindowsFolderAsync(); + if (string.IsNullOrEmpty(pickedPath)) return; + + // Download if not already downloaded + if (!attachmentViewModel.IsDownloaded) + { + await DownloadAttachmentAsync(attachmentViewModel); + } + + // Copy to selected location + if (!string.IsNullOrEmpty(attachmentViewModel.Attachment.LocalFilePath) && + File.Exists(attachmentViewModel.Attachment.LocalFilePath)) + { + var destinationPath = Path.Combine(pickedPath, attachmentViewModel.FileName); + File.Copy(attachmentViewModel.Attachment.LocalFilePath, destinationPath, overwrite: true); + + _dialogService.InfoBarMessage( + Translator.Info_AttachmentSaveSuccessTitle, + Translator.Info_AttachmentSaveSuccessMessage, + InfoBarMessageType.Success); + } + } + catch (Exception ex) + { + Log.Error(ex, "Failed to save calendar attachment."); + _dialogService.InfoBarMessage( + Translator.Info_AttachmentSaveFailedTitle, + Translator.Info_AttachmentSaveFailedMessage, + InfoBarMessageType.Error); + } + finally + { + attachmentViewModel.IsBusy = false; + } + } + + private async Task DownloadAttachmentAsync(CalendarAttachmentViewModel attachmentViewModel) + { + if (CurrentEvent?.CalendarItem == null) return; + + // Create attachments folder for this calendar item + var attachmentsFolder = Path.Combine( + _nativeAppService.GetCalendarAttachmentsFolderPath(), + CurrentEvent.CalendarItem.Id.ToString()); + + Directory.CreateDirectory(attachmentsFolder); + + var localFilePath = Path.Combine(attachmentsFolder, attachmentViewModel.FileName); + + // Download attachment using synchronizer + await SynchronizationManager.Instance.DownloadCalendarAttachmentAsync( + CurrentEvent.CalendarItem, + attachmentViewModel.Attachment, + localFilePath); + + // Mark as downloaded + await _calendarService.MarkAttachmentDownloadedAsync( + attachmentViewModel.Id, + localFilePath); + + // Update view model + attachmentViewModel.Attachment.IsDownloaded = true; + attachmentViewModel.Attachment.LocalFilePath = localFilePath; + OnPropertyChanged(nameof(attachmentViewModel.IsDownloaded)); + } +} + +public partial class ReminderOption : ObservableObject +{ + public int Minutes { get; } + public bool IsCustom { get; } + + [ObservableProperty] + public partial bool IsSelected { get; set; } + + public string DisplayText + { + get + { + if (Minutes >= 60) + { + var hours = Minutes / 60; + return hours == 1 ? "1 Hour" : $"{hours} Hours"; + } + return Minutes == 1 ? "1 Minute" : $"{Minutes} Minutes"; + } + } + + public ReminderOption(int minutes, bool isCustom = false) + { + Minutes = minutes; + IsCustom = isCustom; + } +} + +public partial class ShowAsOption : ObservableObject +{ + public CalendarItemShowAs ShowAs { get; } + + public string DisplayText + { + get + { + return ShowAs switch + { + CalendarItemShowAs.Free => Translator.CalendarShowAs_Free, + CalendarItemShowAs.Tentative => Translator.CalendarShowAs_Tentative, + CalendarItemShowAs.Busy => Translator.CalendarShowAs_Busy, + CalendarItemShowAs.OutOfOffice => Translator.CalendarShowAs_OutOfOffice, + CalendarItemShowAs.WorkingElsewhere => Translator.CalendarShowAs_WorkingElsewhere, + _ => Translator.CalendarShowAs_Busy + }; + } + } + + public ShowAsOption(CalendarItemShowAs showAs) + { + ShowAs = showAs; + } +} + +public partial class RsvpStatusOption : ObservableObject +{ + public CalendarItemStatus Status { get; } + + public string StatusText + { + get + { + return Status switch + { + CalendarItemStatus.Accepted => Translator.CalendarEventResponse_Accept, + CalendarItemStatus.Tentative => Translator.CalendarEventResponse_Tentative, + CalendarItemStatus.Cancelled => Translator.CalendarEventResponse_Decline, + _ => Translator.CalendarEventResponse_Accept + }; + } + } + + [ObservableProperty] + public partial bool IsSelected { get; set; } + + public RsvpStatusOption(CalendarItemStatus status) + { + Status = status; } } diff --git a/Wino.Calendar.ViewModels/Interfaces/IAccountCalendarStateService.cs b/Wino.Calendar.ViewModels/Interfaces/IAccountCalendarStateService.cs index 9aaf17a4..d639040d 100644 --- a/Wino.Calendar.ViewModels/Interfaces/IAccountCalendarStateService.cs +++ b/Wino.Calendar.ViewModels/Interfaces/IAccountCalendarStateService.cs @@ -2,14 +2,16 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; -using System.Linq; +using CommunityToolkit.Mvvm.Collections; using Wino.Calendar.ViewModels.Data; using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Interfaces; namespace Wino.Calendar.ViewModels.Interfaces; public interface IAccountCalendarStateService : INotifyPropertyChanged { + IDispatcher Dispatcher { get; set; } ReadOnlyObservableCollection GroupedAccountCalendars { get; } event EventHandler CollectiveAccountGroupSelectionStateChanged; @@ -17,7 +19,7 @@ public interface IAccountCalendarStateService : INotifyPropertyChanged public void AddGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar); public void RemoveGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar); - public void ClearGroupedAccountCalendar(); + public void ClearGroupedAccountCalendars(); public void AddAccountCalendar(AccountCalendarViewModel accountCalendar); public void RemoveAccountCalendar(AccountCalendarViewModel accountCalendar); @@ -26,5 +28,7 @@ public interface IAccountCalendarStateService : INotifyPropertyChanged /// Enumeration of currently selected calendars. /// IEnumerable ActiveCalendars { get; } - IEnumerable> GroupedAccountCalendarsEnumerable { get; } + IEnumerable AllCalendars { get; } + bool IsAnySynchronizationInProgress { get; } + ReadOnlyObservableGroupedCollection GroupedCalendars { get; set; } } diff --git a/Wino.Calendar.ViewModels/Messages/CalendarItemTappedMessage.cs b/Wino.Calendar.ViewModels/Messages/CalendarItemTappedMessage.cs index 05f2d99c..8419eaba 100644 --- a/Wino.Calendar.ViewModels/Messages/CalendarItemTappedMessage.cs +++ b/Wino.Calendar.ViewModels/Messages/CalendarItemTappedMessage.cs @@ -1,16 +1,12 @@ using Wino.Calendar.ViewModels.Data; -using Wino.Core.Domain.Models.Calendar; - namespace Wino.Calendar.ViewModels.Messages; public class CalendarItemTappedMessage { - public CalendarItemTappedMessage(CalendarItemViewModel calendarItemViewModel, CalendarDayModel clickedPeriod) + public CalendarItemTappedMessage(CalendarItemViewModel calendarItemViewModel) { CalendarItemViewModel = calendarItemViewModel; - ClickedPeriod = clickedPeriod; } public CalendarItemViewModel CalendarItemViewModel { get; } - public CalendarDayModel ClickedPeriod { get; } } diff --git a/Wino.Calendar.ViewModels/Wino.Calendar.ViewModels.csproj b/Wino.Calendar.ViewModels/Wino.Calendar.ViewModels.csproj index e0e88340..b51d389c 100644 --- a/Wino.Calendar.ViewModels/Wino.Calendar.ViewModels.csproj +++ b/Wino.Calendar.ViewModels/Wino.Calendar.ViewModels.csproj @@ -1,13 +1,17 @@  - net9.0 + net10.0 x86;x64;arm64 win-x86;win-x64;win-arm64 true true + true + true + true - + + diff --git a/Wino.Calendar/Activation/DefaultActivationHandler.cs b/Wino.Calendar/Activation/DefaultActivationHandler.cs deleted file mode 100644 index e2d1b8f7..00000000 --- a/Wino.Calendar/Activation/DefaultActivationHandler.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Threading.Tasks; -using Windows.ApplicationModel.Activation; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; -using Windows.UI.Xaml.Media.Animation; -using Wino.Activation; -using Wino.Calendar.Views; - -namespace Wino.Calendar.Activation; - -public class DefaultActivationHandler : ActivationHandler -{ - protected override Task HandleInternalAsync(IActivatedEventArgs args) - { - (Window.Current.Content as Frame).Navigate(typeof(AppShell), null, new DrillInNavigationTransitionInfo()); - - return Task.CompletedTask; - } - - // Only navigate if Frame content doesn't exist. - protected override bool CanHandleInternal(IActivatedEventArgs args) - => (Window.Current?.Content as Frame)?.Content == null; -} diff --git a/Wino.Calendar/App.xaml b/Wino.Calendar/App.xaml deleted file mode 100644 index 1b0db3ef..00000000 --- a/Wino.Calendar/App.xaml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Wino.Calendar/App.xaml.cs b/Wino.Calendar/App.xaml.cs deleted file mode 100644 index 0ae35172..00000000 --- a/Wino.Calendar/App.xaml.cs +++ /dev/null @@ -1,159 +0,0 @@ -using System; -using System.Collections.Generic; -using CommunityToolkit.Mvvm.Messaging; -using Microsoft.Extensions.DependencyInjection; -using Serilog; -using Windows.ApplicationModel; -using Windows.ApplicationModel.Activation; -using Windows.ApplicationModel.AppService; -using Windows.ApplicationModel.Background; -using Windows.UI.Core.Preview; -using Wino.Activation; -using Wino.Calendar.Activation; -using Wino.Calendar.Services; -using Wino.Calendar.ViewModels; -using Wino.Calendar.ViewModels.Interfaces; -using Wino.Core.Domain; -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Exceptions; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Synchronization; -using Wino.Core.UWP; -using Wino.Core.ViewModels; -using Wino.Messaging.Client.Connection; -using Wino.Messaging.Server; -using Wino.Services; - -namespace Wino.Calendar; - -public sealed partial class App : WinoApplication, IRecipient -{ - private BackgroundTaskDeferral connectionBackgroundTaskDeferral; - - public App() - { - InitializeComponent(); - WeakReferenceMessenger.Default.Register(this); - } - - public override IServiceProvider ConfigureServices() - { - var services = new ServiceCollection(); - - services.RegisterSharedServices(); - services.RegisterCalendarViewModelServices(); - services.RegisterCoreUWPServices(); - services.RegisterCoreViewModels(); - - RegisterUWPServices(services); - RegisterViewModels(services); - RegisterActivationHandlers(services); - - return services.BuildServiceProvider(); - } - - #region Dependency Injection - - private void RegisterActivationHandlers(IServiceCollection services) - { - //services.AddTransient(); - //services.AddTransient(); - //services.AddTransient(); - } - - private void RegisterUWPServices(IServiceCollection services) - { - services.AddSingleton(); - services.AddSingleton(); - services.AddTransient(); - services.AddTransient(); - services.AddSingleton(); - services.AddSingleton(); - } - - private void RegisterViewModels(IServiceCollection services) - { - services.AddSingleton(typeof(AppShellViewModel)); - services.AddSingleton(typeof(CalendarPageViewModel)); - services.AddTransient(typeof(CalendarSettingsPageViewModel)); - services.AddTransient(typeof(AccountManagementViewModel)); - services.AddTransient(typeof(PersonalizationPageViewModel)); - services.AddTransient(typeof(AccountDetailsPageViewModel)); - services.AddTransient(typeof(EventDetailsPageViewModel)); - } - - #endregion - - protected override void OnApplicationCloseRequested(object sender, SystemNavigationCloseRequestedPreviewEventArgs e) - { - // TODO: Check server running. - } - - protected override async void OnLaunched(LaunchActivatedEventArgs args) - { - LogActivation($"OnLaunched -> {args.GetType().Name}, Kind -> {args.Kind}, PreviousExecutionState -> {args.PreviousExecutionState}, IsPrelaunch -> {args.PrelaunchActivated}"); - - if (!args.PrelaunchActivated) - { - await ActivateWinoAsync(args); - } - } - - protected override IEnumerable GetActivationHandlers() - { - return null; - } - - protected override ActivationHandler GetDefaultActivationHandler() - => new DefaultActivationHandler(); - - protected override void OnBackgroundActivated(BackgroundActivatedEventArgs args) - { - base.OnBackgroundActivated(args); - - if (args.TaskInstance.TriggerDetails is AppServiceTriggerDetails appServiceTriggerDetails) - { - LogActivation("OnBackgroundActivated -> AppServiceTriggerDetails received."); - - // Only accept connections from callers in the same package - if (appServiceTriggerDetails.CallerPackageFamilyName == Package.Current.Id.FamilyName) - { - // Connection established from the fulltrust process - - connectionBackgroundTaskDeferral = args.TaskInstance.GetDeferral(); - args.TaskInstance.Canceled += OnConnectionBackgroundTaskCanceled; - - AppServiceConnectionManager.Connection = appServiceTriggerDetails.AppServiceConnection; - - WeakReferenceMessenger.Default.Send(new WinoServerConnectionEstablished()); - } - } - } - - public void OnConnectionBackgroundTaskCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason) - { - sender.Canceled -= OnConnectionBackgroundTaskCanceled; - - Log.Information($"Server connection background task was canceled. Reason: {reason}"); - - connectionBackgroundTaskDeferral?.Complete(); - connectionBackgroundTaskDeferral = null; - - AppServiceConnectionManager.Connection = null; - } - - public async void Receive(NewCalendarSynchronizationRequested message) - { - try - { - var synchronizationResultResponse = await AppServiceConnectionManager.GetResponseAsync(message); - synchronizationResultResponse.ThrowIfFailed(); - } - catch (WinoServerException serverException) - { - var dialogService = Services.GetService(); - - dialogService.InfoBarMessage(Translator.Info_SyncFailedTitle, serverException.Message, InfoBarMessageType.Error); - } - } -} diff --git a/Wino.Calendar/Assets/LargeTile.scale-100.png b/Wino.Calendar/Assets/LargeTile.scale-100.png deleted file mode 100644 index 42334872..00000000 Binary files a/Wino.Calendar/Assets/LargeTile.scale-100.png and /dev/null differ diff --git a/Wino.Calendar/Assets/LargeTile.scale-125.png b/Wino.Calendar/Assets/LargeTile.scale-125.png deleted file mode 100644 index 9c03a75c..00000000 Binary files a/Wino.Calendar/Assets/LargeTile.scale-125.png and /dev/null differ diff --git a/Wino.Calendar/Assets/LargeTile.scale-150.png b/Wino.Calendar/Assets/LargeTile.scale-150.png deleted file mode 100644 index e29aa6e0..00000000 Binary files a/Wino.Calendar/Assets/LargeTile.scale-150.png and /dev/null differ diff --git a/Wino.Calendar/Assets/LargeTile.scale-200.png b/Wino.Calendar/Assets/LargeTile.scale-200.png deleted file mode 100644 index 5ad11086..00000000 Binary files a/Wino.Calendar/Assets/LargeTile.scale-200.png and /dev/null differ diff --git a/Wino.Calendar/Assets/LargeTile.scale-400.png b/Wino.Calendar/Assets/LargeTile.scale-400.png deleted file mode 100644 index af446cf4..00000000 Binary files a/Wino.Calendar/Assets/LargeTile.scale-400.png and /dev/null differ diff --git a/Wino.Calendar/Assets/LockScreenLogo.scale-200.png b/Wino.Calendar/Assets/LockScreenLogo.scale-200.png deleted file mode 100644 index 735f57ad..00000000 Binary files a/Wino.Calendar/Assets/LockScreenLogo.scale-200.png and /dev/null differ diff --git a/Wino.Calendar/Assets/SmallTile.scale-100.png b/Wino.Calendar/Assets/SmallTile.scale-100.png deleted file mode 100644 index 2d1167f7..00000000 Binary files a/Wino.Calendar/Assets/SmallTile.scale-100.png and /dev/null differ diff --git a/Wino.Calendar/Assets/SmallTile.scale-125.png b/Wino.Calendar/Assets/SmallTile.scale-125.png deleted file mode 100644 index d8bc11c5..00000000 Binary files a/Wino.Calendar/Assets/SmallTile.scale-125.png and /dev/null differ diff --git a/Wino.Calendar/Assets/SmallTile.scale-150.png b/Wino.Calendar/Assets/SmallTile.scale-150.png deleted file mode 100644 index e124da41..00000000 Binary files a/Wino.Calendar/Assets/SmallTile.scale-150.png and /dev/null differ diff --git a/Wino.Calendar/Assets/SmallTile.scale-200.png b/Wino.Calendar/Assets/SmallTile.scale-200.png deleted file mode 100644 index 3854c86c..00000000 Binary files a/Wino.Calendar/Assets/SmallTile.scale-200.png and /dev/null differ diff --git a/Wino.Calendar/Assets/SmallTile.scale-400.png b/Wino.Calendar/Assets/SmallTile.scale-400.png deleted file mode 100644 index f242a580..00000000 Binary files a/Wino.Calendar/Assets/SmallTile.scale-400.png and /dev/null differ diff --git a/Wino.Calendar/Assets/SplashScreen.scale-100.png b/Wino.Calendar/Assets/SplashScreen.scale-100.png deleted file mode 100644 index 86ade338..00000000 Binary files a/Wino.Calendar/Assets/SplashScreen.scale-100.png and /dev/null differ diff --git a/Wino.Calendar/Assets/SplashScreen.scale-125.png b/Wino.Calendar/Assets/SplashScreen.scale-125.png deleted file mode 100644 index 55d0473d..00000000 Binary files a/Wino.Calendar/Assets/SplashScreen.scale-125.png and /dev/null differ diff --git a/Wino.Calendar/Assets/SplashScreen.scale-150.png b/Wino.Calendar/Assets/SplashScreen.scale-150.png deleted file mode 100644 index 80b9ed0d..00000000 Binary files a/Wino.Calendar/Assets/SplashScreen.scale-150.png and /dev/null differ diff --git a/Wino.Calendar/Assets/SplashScreen.scale-200.png b/Wino.Calendar/Assets/SplashScreen.scale-200.png deleted file mode 100644 index 8510c90c..00000000 Binary files a/Wino.Calendar/Assets/SplashScreen.scale-200.png and /dev/null differ diff --git a/Wino.Calendar/Assets/SplashScreen.scale-400.png b/Wino.Calendar/Assets/SplashScreen.scale-400.png deleted file mode 100644 index 7efc715b..00000000 Binary files a/Wino.Calendar/Assets/SplashScreen.scale-400.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Square150x150Logo.scale-100.png b/Wino.Calendar/Assets/Square150x150Logo.scale-100.png deleted file mode 100644 index 37501fe7..00000000 Binary files a/Wino.Calendar/Assets/Square150x150Logo.scale-100.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Square150x150Logo.scale-125.png b/Wino.Calendar/Assets/Square150x150Logo.scale-125.png deleted file mode 100644 index 0622878d..00000000 Binary files a/Wino.Calendar/Assets/Square150x150Logo.scale-125.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Square150x150Logo.scale-150.png b/Wino.Calendar/Assets/Square150x150Logo.scale-150.png deleted file mode 100644 index 15dc07b5..00000000 Binary files a/Wino.Calendar/Assets/Square150x150Logo.scale-150.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Square150x150Logo.scale-200.png b/Wino.Calendar/Assets/Square150x150Logo.scale-200.png deleted file mode 100644 index 75c9945a..00000000 Binary files a/Wino.Calendar/Assets/Square150x150Logo.scale-200.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Square150x150Logo.scale-400.png b/Wino.Calendar/Assets/Square150x150Logo.scale-400.png deleted file mode 100644 index cb218b68..00000000 Binary files a/Wino.Calendar/Assets/Square150x150Logo.scale-400.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png b/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png deleted file mode 100644 index 2c72b37d..00000000 Binary files a/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png b/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png deleted file mode 100644 index 895753c9..00000000 Binary files a/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png b/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png deleted file mode 100644 index eeaaad33..00000000 Binary files a/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png b/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png deleted file mode 100644 index e7c0cb66..00000000 Binary files a/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png b/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png deleted file mode 100644 index 56792258..00000000 Binary files a/Wino.Calendar/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Square44x44Logo.altform-unplated_targetsize-16.png b/Wino.Calendar/Assets/Square44x44Logo.altform-unplated_targetsize-16.png deleted file mode 100644 index 2c72b37d..00000000 Binary files a/Wino.Calendar/Assets/Square44x44Logo.altform-unplated_targetsize-16.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Square44x44Logo.altform-unplated_targetsize-256.png b/Wino.Calendar/Assets/Square44x44Logo.altform-unplated_targetsize-256.png deleted file mode 100644 index eeaaad33..00000000 Binary files a/Wino.Calendar/Assets/Square44x44Logo.altform-unplated_targetsize-256.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Square44x44Logo.altform-unplated_targetsize-32.png b/Wino.Calendar/Assets/Square44x44Logo.altform-unplated_targetsize-32.png deleted file mode 100644 index e7c0cb66..00000000 Binary files a/Wino.Calendar/Assets/Square44x44Logo.altform-unplated_targetsize-32.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Square44x44Logo.altform-unplated_targetsize-48.png b/Wino.Calendar/Assets/Square44x44Logo.altform-unplated_targetsize-48.png deleted file mode 100644 index 56792258..00000000 Binary files a/Wino.Calendar/Assets/Square44x44Logo.altform-unplated_targetsize-48.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Square44x44Logo.scale-100.png b/Wino.Calendar/Assets/Square44x44Logo.scale-100.png deleted file mode 100644 index 43b04c94..00000000 Binary files a/Wino.Calendar/Assets/Square44x44Logo.scale-100.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Square44x44Logo.scale-125.png b/Wino.Calendar/Assets/Square44x44Logo.scale-125.png deleted file mode 100644 index c6ca31f5..00000000 Binary files a/Wino.Calendar/Assets/Square44x44Logo.scale-125.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Square44x44Logo.scale-150.png b/Wino.Calendar/Assets/Square44x44Logo.scale-150.png deleted file mode 100644 index 97bf5eb8..00000000 Binary files a/Wino.Calendar/Assets/Square44x44Logo.scale-150.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Square44x44Logo.scale-200.png b/Wino.Calendar/Assets/Square44x44Logo.scale-200.png deleted file mode 100644 index f1a2ddef..00000000 Binary files a/Wino.Calendar/Assets/Square44x44Logo.scale-200.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Square44x44Logo.scale-400.png b/Wino.Calendar/Assets/Square44x44Logo.scale-400.png deleted file mode 100644 index b25cb5bc..00000000 Binary files a/Wino.Calendar/Assets/Square44x44Logo.scale-400.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Square44x44Logo.targetsize-16.png b/Wino.Calendar/Assets/Square44x44Logo.targetsize-16.png deleted file mode 100644 index 22a4ac74..00000000 Binary files a/Wino.Calendar/Assets/Square44x44Logo.targetsize-16.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Square44x44Logo.targetsize-24.png b/Wino.Calendar/Assets/Square44x44Logo.targetsize-24.png deleted file mode 100644 index ac5a485d..00000000 Binary files a/Wino.Calendar/Assets/Square44x44Logo.targetsize-24.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/Wino.Calendar/Assets/Square44x44Logo.targetsize-24_altform-unplated.png deleted file mode 100644 index 895753c9..00000000 Binary files a/Wino.Calendar/Assets/Square44x44Logo.targetsize-24_altform-unplated.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Square44x44Logo.targetsize-256.png b/Wino.Calendar/Assets/Square44x44Logo.targetsize-256.png deleted file mode 100644 index 9e6a53b2..00000000 Binary files a/Wino.Calendar/Assets/Square44x44Logo.targetsize-256.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Square44x44Logo.targetsize-32.png b/Wino.Calendar/Assets/Square44x44Logo.targetsize-32.png deleted file mode 100644 index 79388f0d..00000000 Binary files a/Wino.Calendar/Assets/Square44x44Logo.targetsize-32.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Square44x44Logo.targetsize-48.png b/Wino.Calendar/Assets/Square44x44Logo.targetsize-48.png deleted file mode 100644 index 47aea727..00000000 Binary files a/Wino.Calendar/Assets/Square44x44Logo.targetsize-48.png and /dev/null differ diff --git a/Wino.Calendar/Assets/StoreLogo.backup.png b/Wino.Calendar/Assets/StoreLogo.backup.png deleted file mode 100644 index 7385b56c..00000000 Binary files a/Wino.Calendar/Assets/StoreLogo.backup.png and /dev/null differ diff --git a/Wino.Calendar/Assets/StoreLogo.scale-100.png b/Wino.Calendar/Assets/StoreLogo.scale-100.png deleted file mode 100644 index caafaee8..00000000 Binary files a/Wino.Calendar/Assets/StoreLogo.scale-100.png and /dev/null differ diff --git a/Wino.Calendar/Assets/StoreLogo.scale-125.png b/Wino.Calendar/Assets/StoreLogo.scale-125.png deleted file mode 100644 index 59f77d4e..00000000 Binary files a/Wino.Calendar/Assets/StoreLogo.scale-125.png and /dev/null differ diff --git a/Wino.Calendar/Assets/StoreLogo.scale-150.png b/Wino.Calendar/Assets/StoreLogo.scale-150.png deleted file mode 100644 index 6adf5536..00000000 Binary files a/Wino.Calendar/Assets/StoreLogo.scale-150.png and /dev/null differ diff --git a/Wino.Calendar/Assets/StoreLogo.scale-200.png b/Wino.Calendar/Assets/StoreLogo.scale-200.png deleted file mode 100644 index b4b471f8..00000000 Binary files a/Wino.Calendar/Assets/StoreLogo.scale-200.png and /dev/null differ diff --git a/Wino.Calendar/Assets/StoreLogo.scale-400.png b/Wino.Calendar/Assets/StoreLogo.scale-400.png deleted file mode 100644 index be94d352..00000000 Binary files a/Wino.Calendar/Assets/StoreLogo.scale-400.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Wide310x150Logo.scale-100.png b/Wino.Calendar/Assets/Wide310x150Logo.scale-100.png deleted file mode 100644 index 0847e264..00000000 Binary files a/Wino.Calendar/Assets/Wide310x150Logo.scale-100.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Wide310x150Logo.scale-125.png b/Wino.Calendar/Assets/Wide310x150Logo.scale-125.png deleted file mode 100644 index 0be7583f..00000000 Binary files a/Wino.Calendar/Assets/Wide310x150Logo.scale-125.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Wide310x150Logo.scale-150.png b/Wino.Calendar/Assets/Wide310x150Logo.scale-150.png deleted file mode 100644 index f969318f..00000000 Binary files a/Wino.Calendar/Assets/Wide310x150Logo.scale-150.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Wide310x150Logo.scale-200.png b/Wino.Calendar/Assets/Wide310x150Logo.scale-200.png deleted file mode 100644 index 86ade338..00000000 Binary files a/Wino.Calendar/Assets/Wide310x150Logo.scale-200.png and /dev/null differ diff --git a/Wino.Calendar/Assets/Wide310x150Logo.scale-400.png b/Wino.Calendar/Assets/Wide310x150Logo.scale-400.png deleted file mode 100644 index 8510c90c..00000000 Binary files a/Wino.Calendar/Assets/Wide310x150Logo.scale-400.png and /dev/null differ diff --git a/Wino.Calendar/Controls/CalendarItemControl.xaml.cs b/Wino.Calendar/Controls/CalendarItemControl.xaml.cs deleted file mode 100644 index dbd2333b..00000000 --- a/Wino.Calendar/Controls/CalendarItemControl.xaml.cs +++ /dev/null @@ -1,197 +0,0 @@ -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.Messaging; -using Itenso.TimePeriod; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; -using Windows.UI.Xaml.Input; -using Wino.Calendar.ViewModels.Data; -using Wino.Calendar.ViewModels.Messages; -using Wino.Core.Domain; -using Wino.Core.Domain.Models.Calendar; - -namespace Wino.Calendar.Controls; - -public sealed partial class CalendarItemControl : UserControl -{ - // Single tap has a delay to report double taps properly. - private bool isSingleTap = false; - - public static readonly DependencyProperty CalendarItemProperty = DependencyProperty.Register(nameof(CalendarItem), typeof(CalendarItemViewModel), typeof(CalendarItemControl), new PropertyMetadata(null, new PropertyChangedCallback(OnCalendarItemChanged))); - public static readonly DependencyProperty IsDraggingProperty = DependencyProperty.Register(nameof(IsDragging), typeof(bool), typeof(CalendarItemControl), new PropertyMetadata(false)); - public static readonly DependencyProperty IsCustomEventAreaProperty = DependencyProperty.Register(nameof(IsCustomEventArea), typeof(bool), typeof(CalendarItemControl), new PropertyMetadata(false)); - public static readonly DependencyProperty CalendarItemTitleProperty = DependencyProperty.Register(nameof(CalendarItemTitle), typeof(string), typeof(CalendarItemControl), new PropertyMetadata(string.Empty)); - public static readonly DependencyProperty DisplayingDateProperty = DependencyProperty.Register(nameof(DisplayingDate), typeof(CalendarDayModel), typeof(CalendarItemControl), new PropertyMetadata(null, new PropertyChangedCallback(OnDisplayDateChanged))); - - /// - /// Whether the control is displaying as regular event or all-multi day area in the day control. - /// - public bool IsCustomEventArea - { - get { return (bool)GetValue(IsCustomEventAreaProperty); } - set { SetValue(IsCustomEventAreaProperty, value); } - } - - /// - /// Day that the calendar item is rendered at. - /// It's needed for title manipulation and some other adjustments later on. - /// - public CalendarDayModel DisplayingDate - { - get { return (CalendarDayModel)GetValue(DisplayingDateProperty); } - set { SetValue(DisplayingDateProperty, value); } - } - - public string CalendarItemTitle - { - get { return (string)GetValue(CalendarItemTitleProperty); } - set { SetValue(CalendarItemTitleProperty, value); } - } - - public CalendarItemViewModel CalendarItem - { - get { return (CalendarItemViewModel)GetValue(CalendarItemProperty); } - set { SetValue(CalendarItemProperty, value); } - } - - public bool IsDragging - { - get { return (bool)GetValue(IsDraggingProperty); } - set { SetValue(IsDraggingProperty, value); } - } - - public CalendarItemControl() - { - InitializeComponent(); - } - - private static void OnDisplayDateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is CalendarItemControl control) - { - control.UpdateControlVisuals(); - } - } - - private static void OnCalendarItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is CalendarItemControl control) - { - control.UpdateControlVisuals(); - } - } - - private void UpdateControlVisuals() - { - // Depending on the calendar item's duration and attributes, we might need to change the display title. - // 1. Multi-Day events should display the start date and end date. - // 2. Multi-Day events that occupy the whole day just shows 'all day'. - // 3. Other events should display the title. - - if (CalendarItem == null) return; - if (DisplayingDate == null) return; - - if (CalendarItem.IsMultiDayEvent) - { - // Multi day events are divided into 3 categories: - // 1. All day events - // 2. Events that started after the period. - // 3. Events that started before the period and finishes within the period. - - var periodRelation = CalendarItem.Period.GetRelation(DisplayingDate.Period); - - if (periodRelation == Itenso.TimePeriod.PeriodRelation.StartInside || - periodRelation == PeriodRelation.EnclosingStartTouching) - { - // hour -> title - CalendarItemTitle = $"{DisplayingDate.CalendarRenderOptions.CalendarSettings.GetTimeString(CalendarItem.StartDate.TimeOfDay)} -> {CalendarItem.Title}"; - } - else if ( - periodRelation == PeriodRelation.EndInside || - periodRelation == PeriodRelation.EnclosingEndTouching) - { - // title <- hour - CalendarItemTitle = $"{CalendarItem.Title} <- {DisplayingDate.CalendarRenderOptions.CalendarSettings.GetTimeString(CalendarItem.EndDate.TimeOfDay)}"; - } - else if (periodRelation == PeriodRelation.Enclosing) - { - // This event goes all day and it's multi-day. - // Item must be hidden in the calendar but displayed on the custom area at the top. - - CalendarItemTitle = $"{Translator.CalendarItemAllDay} {CalendarItem.Title}"; - } - else - { - // Not expected, but there it is. - CalendarItemTitle = CalendarItem.Title; - } - - // Debug.WriteLine($"{CalendarItem.Title} Period relation with {DisplayingDate.Period.ToString()}: {periodRelation}"); - } - else - { - CalendarItemTitle = CalendarItem.Title; - } - - UpdateVisualStates(); - } - - private void UpdateVisualStates() - { - if (CalendarItem == null) return; - - if (CalendarItem.IsAllDayEvent) - { - VisualStateManager.GoToState(this, "AllDayEvent", true); - } - else if (CalendarItem.IsMultiDayEvent) - { - if (IsCustomEventArea) - { - VisualStateManager.GoToState(this, "CustomAreaMultiDayEvent", true); - } - else - { - // Hide it. - VisualStateManager.GoToState(this, "MultiDayEvent", true); - } - } - else - { - VisualStateManager.GoToState(this, "RegularEvent", true); - } - } - - private void ControlDragStarting(UIElement sender, DragStartingEventArgs args) => IsDragging = true; - - private void ControlDropped(UIElement sender, DropCompletedEventArgs args) => IsDragging = false; - - private async void ControlTapped(object sender, TappedRoutedEventArgs e) - { - if (CalendarItem == null) return; - - isSingleTap = true; - - await Task.Delay(100); - - if (isSingleTap) - { - WeakReferenceMessenger.Default.Send(new CalendarItemTappedMessage(CalendarItem, DisplayingDate)); - } - } - - private void ControlDoubleTapped(object sender, DoubleTappedRoutedEventArgs e) - { - if (CalendarItem == null) return; - - isSingleTap = false; - - WeakReferenceMessenger.Default.Send(new CalendarItemDoubleTappedMessage(CalendarItem)); - } - - private void ControlRightTapped(object sender, RightTappedRoutedEventArgs e) - { - if (CalendarItem == null) return; - - WeakReferenceMessenger.Default.Send(new CalendarItemRightTappedMessage(CalendarItem)); - } -} diff --git a/Wino.Calendar/Controls/CustomCalendarFlipView.cs b/Wino.Calendar/Controls/CustomCalendarFlipView.cs deleted file mode 100644 index 8d73220e..00000000 --- a/Wino.Calendar/Controls/CustomCalendarFlipView.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Windows.UI.Xaml.Automation.Peers; -using Windows.UI.Xaml.Controls; - -namespace Wino.Calendar.Controls; - -/// -/// FlipView that hides the navigation buttons and exposes methods to navigate to the next and previous items with animations. -/// -public partial class CustomCalendarFlipView : FlipView -{ - private const string PART_PreviousButton = "PreviousButtonHorizontal"; - private const string PART_NextButton = "NextButtonHorizontal"; - - private Button PreviousButton; - private Button NextButton; - - protected override void OnApplyTemplate() - { - base.OnApplyTemplate(); - - PreviousButton = GetTemplateChild(PART_PreviousButton) as Button; - NextButton = GetTemplateChild(PART_NextButton) as Button; - - // Hide navigation buttons - PreviousButton.Opacity = NextButton.Opacity = 0; - PreviousButton.IsHitTestVisible = NextButton.IsHitTestVisible = false; - - var t = FindName("ScrollingHost"); - } - - public void GoPreviousFlip() - { - var backPeer = new ButtonAutomationPeer(PreviousButton); - backPeer.Invoke(); - } - - public void GoNextFlip() - { - var nextPeer = new ButtonAutomationPeer(NextButton); - nextPeer.Invoke(); - } -} diff --git a/Wino.Calendar/Controls/DayColumnControl.cs b/Wino.Calendar/Controls/DayColumnControl.cs deleted file mode 100644 index a3f3cb78..00000000 --- a/Wino.Calendar/Controls/DayColumnControl.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; -using Wino.Core.Domain.Models.Calendar; - -namespace Wino.Calendar.Controls; - -public partial class DayColumnControl : Control -{ - private const string PART_HeaderDateDayText = nameof(PART_HeaderDateDayText); - private const string PART_IsTodayBorder = nameof(PART_IsTodayBorder); - private const string PART_ColumnHeaderText = nameof(PART_ColumnHeaderText); - - private const string PART_AllDayItemsControl = nameof(PART_AllDayItemsControl); - - private const string TodayState = nameof(TodayState); - private const string NotTodayState = nameof(NotTodayState); - - private TextBlock HeaderDateDayText; - private TextBlock ColumnHeaderText; - private Border IsTodayBorder; - private ItemsControl AllDayItemsControl; - - public CalendarDayModel DayModel - { - get { return (CalendarDayModel)GetValue(DayModelProperty); } - set { SetValue(DayModelProperty, value); } - } - - public static readonly DependencyProperty DayModelProperty = DependencyProperty.Register(nameof(DayModel), typeof(CalendarDayModel), typeof(DayColumnControl), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged))); - - public DayColumnControl() - { - DefaultStyleKey = typeof(DayColumnControl); - } - - protected override void OnApplyTemplate() - { - base.OnApplyTemplate(); - - HeaderDateDayText = GetTemplateChild(PART_HeaderDateDayText) as TextBlock; - ColumnHeaderText = GetTemplateChild(PART_ColumnHeaderText) as TextBlock; - IsTodayBorder = GetTemplateChild(PART_IsTodayBorder) as Border; - AllDayItemsControl = GetTemplateChild(PART_AllDayItemsControl) as ItemsControl; - - UpdateValues(); - } - - private static void OnRenderingPropertiesChanged(DependencyObject control, DependencyPropertyChangedEventArgs e) - { - if (control is DayColumnControl columnControl) - { - columnControl.UpdateValues(); - } - } - - private void UpdateValues() - { - if (HeaderDateDayText == null || IsTodayBorder == null || DayModel == null) return; - - HeaderDateDayText.Text = DayModel.RepresentingDate.Day.ToString(); - - // Monthly template does not use it. - if (ColumnHeaderText != null) - { - ColumnHeaderText.Text = DayModel.RepresentingDate.ToString("dddd", DayModel.CalendarRenderOptions.CalendarSettings.CultureInfo); - } - - AllDayItemsControl.ItemsSource = DayModel.EventsCollection.AllDayEvents; - - bool isToday = DayModel.RepresentingDate.Date == DateTime.Now.Date; - - VisualStateManager.GoToState(this, isToday ? TodayState : NotTodayState, false); - - UpdateLayout(); - } -} diff --git a/Wino.Calendar/Controls/DayHeaderControl.cs b/Wino.Calendar/Controls/DayHeaderControl.cs deleted file mode 100644 index 567cdaef..00000000 --- a/Wino.Calendar/Controls/DayHeaderControl.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; -using Wino.Core.Domain.Enums; - -namespace Wino.Calendar.Controls; - -public partial class DayHeaderControl : Control -{ - private const string PART_DayHeaderTextBlock = nameof(PART_DayHeaderTextBlock); - private TextBlock HeaderTextblock; - - public DayHeaderDisplayType DisplayType - { - get { return (DayHeaderDisplayType)GetValue(DisplayTypeProperty); } - set { SetValue(DisplayTypeProperty, value); } - } - - public DateTime Date - { - get { return (DateTime)GetValue(DateProperty); } - set { SetValue(DateProperty, value); } - } - - public static readonly DependencyProperty DateProperty = DependencyProperty.Register(nameof(Date), typeof(DateTime), typeof(DayHeaderControl), new PropertyMetadata(default(DateTime), new PropertyChangedCallback(OnHeaderPropertyChanged))); - public static readonly DependencyProperty DisplayTypeProperty = DependencyProperty.Register(nameof(DisplayType), typeof(DayHeaderDisplayType), typeof(DayHeaderControl), new PropertyMetadata(DayHeaderDisplayType.TwentyFourHour, new PropertyChangedCallback(OnHeaderPropertyChanged))); - - public DayHeaderControl() - { - DefaultStyleKey = typeof(DayHeaderControl); - } - - protected override void OnApplyTemplate() - { - base.OnApplyTemplate(); - - HeaderTextblock = GetTemplateChild(PART_DayHeaderTextBlock) as TextBlock; - UpdateHeaderText(); - } - - private static void OnHeaderPropertyChanged(DependencyObject control, DependencyPropertyChangedEventArgs e) - { - if (control is DayHeaderControl headerControl) - { - headerControl.UpdateHeaderText(); - } - } - - private void UpdateHeaderText() - { - if (HeaderTextblock != null) - { - HeaderTextblock.Text = DisplayType == DayHeaderDisplayType.TwelveHour ? Date.ToString("h tt") : Date.ToString("HH:mm"); - } - } -} diff --git a/Wino.Calendar/Controls/WinoCalendarControl.cs b/Wino.Calendar/Controls/WinoCalendarControl.cs deleted file mode 100644 index e2930e6c..00000000 --- a/Wino.Calendar/Controls/WinoCalendarControl.cs +++ /dev/null @@ -1,299 +0,0 @@ -using System; -using System.Collections.ObjectModel; -using System.Linq; -using System.Threading.Tasks; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; -using Wino.Calendar.Args; -using Wino.Calendar.ViewModels.Data; -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Models.Calendar; -using Wino.Helpers; - -namespace Wino.Calendar.Controls; - -public partial class WinoCalendarControl : Control -{ - private const string PART_WinoFlipView = nameof(PART_WinoFlipView); - private const string PART_IdleGrid = nameof(PART_IdleGrid); - - public event EventHandler TimelineCellSelected; - public event EventHandler TimelineCellUnselected; - - public event EventHandler ScrollPositionChanging; - - #region Dependency Properties - - public static readonly DependencyProperty DayRangesProperty = DependencyProperty.Register(nameof(DayRanges), typeof(ObservableCollection), typeof(WinoCalendarControl), new PropertyMetadata(null)); - public static readonly DependencyProperty SelectedFlipViewIndexProperty = DependencyProperty.Register(nameof(SelectedFlipViewIndex), typeof(int), typeof(WinoCalendarControl), new PropertyMetadata(-1)); - public static readonly DependencyProperty SelectedFlipViewDayRangeProperty = DependencyProperty.Register(nameof(SelectedFlipViewDayRange), typeof(DayRangeRenderModel), typeof(WinoCalendarControl), new PropertyMetadata(null)); - public static readonly DependencyProperty ActiveCanvasProperty = DependencyProperty.Register(nameof(ActiveCanvas), typeof(WinoDayTimelineCanvas), typeof(WinoCalendarControl), new PropertyMetadata(null, new PropertyChangedCallback(OnActiveCanvasChanged))); - public static readonly DependencyProperty IsFlipIdleProperty = DependencyProperty.Register(nameof(IsFlipIdle), typeof(bool), typeof(WinoCalendarControl), new PropertyMetadata(true, new PropertyChangedCallback(OnIdleStateChanged))); - public static readonly DependencyProperty ActiveScrollViewerProperty = DependencyProperty.Register(nameof(ActiveScrollViewer), typeof(ScrollViewer), typeof(WinoCalendarControl), new PropertyMetadata(null, new PropertyChangedCallback(OnActiveVerticalScrollViewerChanged))); - - public static readonly DependencyProperty VerticalItemsPanelTemplateProperty = DependencyProperty.Register(nameof(VerticalItemsPanelTemplate), typeof(ItemsPanelTemplate), typeof(WinoCalendarControl), new PropertyMetadata(null, new PropertyChangedCallback(OnCalendarOrientationPropertiesUpdated))); - public static readonly DependencyProperty HorizontalItemsPanelTemplateProperty = DependencyProperty.Register(nameof(HorizontalItemsPanelTemplate), typeof(ItemsPanelTemplate), typeof(WinoCalendarControl), new PropertyMetadata(null, new PropertyChangedCallback(OnCalendarOrientationPropertiesUpdated))); - public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(nameof(Orientation), typeof(CalendarOrientation), typeof(WinoCalendarControl), new PropertyMetadata(CalendarOrientation.Horizontal, new PropertyChangedCallback(OnCalendarOrientationPropertiesUpdated))); - public static readonly DependencyProperty DisplayTypeProperty = DependencyProperty.Register(nameof(DisplayType), typeof(CalendarDisplayType), typeof(WinoCalendarControl), new PropertyMetadata(CalendarDisplayType.Day)); - - /// - /// Gets or sets the day-week-month-year display type. - /// Orientation is not determined by this property, but Orientation property. - /// This property is used to determine the template to use for the calendar. - /// - public CalendarDisplayType DisplayType - { - get { return (CalendarDisplayType)GetValue(DisplayTypeProperty); } - set { SetValue(DisplayTypeProperty, value); } - } - - public CalendarOrientation Orientation - { - get { return (CalendarOrientation)GetValue(OrientationProperty); } - set { SetValue(OrientationProperty, value); } - } - - public ItemsPanelTemplate VerticalItemsPanelTemplate - { - get { return (ItemsPanelTemplate)GetValue(VerticalItemsPanelTemplateProperty); } - set { SetValue(VerticalItemsPanelTemplateProperty, value); } - } - - public ItemsPanelTemplate HorizontalItemsPanelTemplate - { - get { return (ItemsPanelTemplate)GetValue(HorizontalItemsPanelTemplateProperty); } - set { SetValue(HorizontalItemsPanelTemplateProperty, value); } - } - - public DayRangeRenderModel SelectedFlipViewDayRange - { - get { return (DayRangeRenderModel)GetValue(SelectedFlipViewDayRangeProperty); } - set { SetValue(SelectedFlipViewDayRangeProperty, value); } - } - - public ScrollViewer ActiveScrollViewer - { - get { return (ScrollViewer)GetValue(ActiveScrollViewerProperty); } - set { SetValue(ActiveScrollViewerProperty, value); } - } - - public WinoDayTimelineCanvas ActiveCanvas - { - get { return (WinoDayTimelineCanvas)GetValue(ActiveCanvasProperty); } - set { SetValue(ActiveCanvasProperty, value); } - } - - public bool IsFlipIdle - { - get { return (bool)GetValue(IsFlipIdleProperty); } - set { SetValue(IsFlipIdleProperty, value); } - } - - /// - /// Gets or sets the collection of day ranges to render. - /// Each day range usually represents a week, but it may support other ranges. - /// - public ObservableCollection DayRanges - { - get { return (ObservableCollection)GetValue(DayRangesProperty); } - set { SetValue(DayRangesProperty, value); } - } - - public int SelectedFlipViewIndex - { - get { return (int)GetValue(SelectedFlipViewIndexProperty); } - set { SetValue(SelectedFlipViewIndexProperty, value); } - } - - #endregion - - private WinoCalendarFlipView InternalFlipView; - private Grid IdleGrid; - - public WinoCalendarControl() - { - DefaultStyleKey = typeof(WinoCalendarControl); - SizeChanged += CalendarSizeChanged; - } - - private static void OnCalendarOrientationPropertiesUpdated(DependencyObject calendar, DependencyPropertyChangedEventArgs e) - { - if (calendar is WinoCalendarControl control) - { - control.ManageCalendarOrientation(); - } - } - - private static void OnIdleStateChanged(DependencyObject calendar, DependencyPropertyChangedEventArgs e) - { - if (calendar is WinoCalendarControl calendarControl) - { - calendarControl.UpdateIdleState(); - } - } - - - private static void OnActiveVerticalScrollViewerChanged(DependencyObject calendar, DependencyPropertyChangedEventArgs e) - { - if (calendar is WinoCalendarControl calendarControl) - { - if (e.OldValue is ScrollViewer oldScrollViewer) - { - calendarControl.DeregisterScrollChanges(oldScrollViewer); - } - - if (e.NewValue is ScrollViewer newScrollViewer) - { - calendarControl.RegisterScrollChanges(newScrollViewer); - } - - calendarControl.ManageHighlightedDateRange(); - } - } - - - private static void OnActiveCanvasChanged(DependencyObject calendar, DependencyPropertyChangedEventArgs e) - { - if (calendar is WinoCalendarControl calendarControl) - { - if (e.OldValue is WinoDayTimelineCanvas oldCanvas) - { - // Dismiss any selection on the old canvas. - calendarControl.DeregisterCanvas(oldCanvas); - } - - if (e.NewValue is WinoDayTimelineCanvas newCanvas) - { - calendarControl.RegisterCanvas(newCanvas); - } - - calendarControl.ManageHighlightedDateRange(); - } - } - - private void ManageCalendarOrientation() - { - if (InternalFlipView == null || HorizontalItemsPanelTemplate == null || VerticalItemsPanelTemplate == null) return; - - InternalFlipView.ItemsPanel = Orientation == CalendarOrientation.Horizontal ? HorizontalItemsPanelTemplate : VerticalItemsPanelTemplate; - } - - private void ManageHighlightedDateRange() - => SelectedFlipViewDayRange = InternalFlipView.SelectedItem as DayRangeRenderModel; - - private void DeregisterCanvas(WinoDayTimelineCanvas canvas) - { - if (canvas == null) return; - - canvas.SelectedDateTime = null; - canvas.TimelineCellSelected -= ActiveTimelineCellSelected; - canvas.TimelineCellUnselected -= ActiveTimelineCellUnselected; - } - - private void RegisterCanvas(WinoDayTimelineCanvas canvas) - { - if (canvas == null) return; - - canvas.SelectedDateTime = null; - canvas.TimelineCellSelected += ActiveTimelineCellSelected; - canvas.TimelineCellUnselected += ActiveTimelineCellUnselected; - } - - private void RegisterScrollChanges(ScrollViewer scrollViewer) - { - if (scrollViewer == null) return; - - scrollViewer.ViewChanging += ScrollViewChanging; - } - - private void DeregisterScrollChanges(ScrollViewer scrollViewer) - { - if (scrollViewer == null) return; - - scrollViewer.ViewChanging -= ScrollViewChanging; - } - - private void ScrollViewChanging(object sender, ScrollViewerViewChangingEventArgs e) - => ScrollPositionChanging?.Invoke(this, EventArgs.Empty); - - private void CalendarSizeChanged(object sender, SizeChangedEventArgs e) - { - if (ActiveCanvas == null) return; - - ActiveCanvas.SelectedDateTime = null; - } - - protected override void OnApplyTemplate() - { - base.OnApplyTemplate(); - - InternalFlipView = GetTemplateChild(PART_WinoFlipView) as WinoCalendarFlipView; - IdleGrid = GetTemplateChild(PART_IdleGrid) as Grid; - - UpdateIdleState(); - ManageCalendarOrientation(); - } - - private void UpdateIdleState() - { - InternalFlipView.Opacity = IsFlipIdle ? 0 : 1; - IdleGrid.Visibility = IsFlipIdle ? Visibility.Visible : Visibility.Collapsed; - } - - private void ActiveTimelineCellUnselected(object sender, TimelineCellUnselectedArgs e) - => TimelineCellUnselected?.Invoke(this, e); - - private void ActiveTimelineCellSelected(object sender, TimelineCellSelectedArgs e) - => TimelineCellSelected?.Invoke(this, e); - - public void NavigateToDay(DateTime dateTime) => InternalFlipView.NavigateToDay(dateTime); - - public async void NavigateToHour(TimeSpan timeSpan) - { - if (ActiveScrollViewer == null) return; - - // Total height of the FlipViewItem is the same as vertical ScrollViewer to position day headers. - - await Task.Yield(); - await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.High, () => - { - double hourHeght = 60; - double totalHeight = ActiveScrollViewer.ScrollableHeight; - double scrollPosition = timeSpan.TotalHours * hourHeght; - - ActiveScrollViewer.ChangeView(null, scrollPosition, null, disableAnimation: false); - }); - } - public void ResetTimelineSelection() - { - if (ActiveCanvas == null) return; - - ActiveCanvas.SelectedDateTime = null; - } - - public void GoNextRange() - { - if (InternalFlipView == null) return; - - InternalFlipView.GoNextFlip(); - } - - public void GoPreviousRange() - { - if (InternalFlipView == null) return; - - InternalFlipView.GoPreviousFlip(); - } - - public void UnselectActiveTimelineCell() - { - if (ActiveCanvas == null) return; - - ActiveCanvas.SelectedDateTime = null; - } - - public CalendarItemControl GetCalendarItemControl(CalendarItemViewModel calendarItemViewModel) - { - return this.FindDescendants().FirstOrDefault(a => a.CalendarItem == calendarItemViewModel); - } -} diff --git a/Wino.Calendar/Controls/WinoCalendarFlipView.cs b/Wino.Calendar/Controls/WinoCalendarFlipView.cs deleted file mode 100644 index 679490d4..00000000 --- a/Wino.Calendar/Controls/WinoCalendarFlipView.cs +++ /dev/null @@ -1,185 +0,0 @@ -using System; -using System.Collections.Specialized; -using System.Linq; -using System.Threading.Tasks; -using CommunityToolkit.WinUI; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; -using Wino.Core.Domain.Collections; -using Wino.Core.Domain.Models.Calendar; - -namespace Wino.Calendar.Controls; - -public partial class WinoCalendarFlipView : CustomCalendarFlipView -{ - public static readonly DependencyProperty IsIdleProperty = DependencyProperty.Register(nameof(IsIdle), typeof(bool), typeof(WinoCalendarFlipView), new PropertyMetadata(true)); - public static readonly DependencyProperty ActiveCanvasProperty = DependencyProperty.Register(nameof(ActiveCanvas), typeof(WinoDayTimelineCanvas), typeof(WinoCalendarFlipView), new PropertyMetadata(null)); - public static readonly DependencyProperty ActiveVerticalScrollViewerProperty = DependencyProperty.Register(nameof(ActiveVerticalScrollViewer), typeof(ScrollViewer), typeof(WinoCalendarFlipView), new PropertyMetadata(null)); - - /// - /// Gets or sets the active canvas that is currently displayed in the flip view. - /// Each day-range of flip view item has a canvas that displays the day timeline. - /// - public WinoDayTimelineCanvas ActiveCanvas - { - get { return (WinoDayTimelineCanvas)GetValue(ActiveCanvasProperty); } - set { SetValue(ActiveCanvasProperty, value); } - } - - /// - /// Gets or sets the scroll viewer that is currently active in the flip view. - /// It's the vertical scroll that scrolls the timeline only, not the header part that belongs - /// to parent FlipView control. - /// - public ScrollViewer ActiveVerticalScrollViewer - { - get { return (ScrollViewer)GetValue(ActiveVerticalScrollViewerProperty); } - set { SetValue(ActiveVerticalScrollViewerProperty, value); } - } - - public bool IsIdle - { - get { return (bool)GetValue(IsIdleProperty); } - set { SetValue(IsIdleProperty, value); } - } - - public WinoCalendarFlipView() - { - RegisterPropertyChangedCallback(SelectedIndexProperty, new DependencyPropertyChangedCallback(OnSelectedIndexUpdated)); - RegisterPropertyChangedCallback(ItemsSourceProperty, new DependencyPropertyChangedCallback(OnItemsSourceChanged)); - } - - private static void OnItemsSourceChanged(DependencyObject d, DependencyProperty e) - { - if (d is WinoCalendarFlipView flipView) - { - flipView.RegisterItemsSourceChange(); - } - } - - private static void OnSelectedIndexUpdated(DependencyObject d, DependencyProperty e) - { - if (d is WinoCalendarFlipView flipView) - { - flipView.UpdateActiveCanvas(); - flipView.UpdateActiveScrollViewer(); - } - } - - private void RegisterItemsSourceChange() - { - if (GetItemsSource() is INotifyCollectionChanged notifyCollectionChanged) - { - notifyCollectionChanged.CollectionChanged += ItemsSourceUpdated; - } - } - - private void ItemsSourceUpdated(object sender, NotifyCollectionChangedEventArgs e) - { - IsIdle = e.Action == NotifyCollectionChangedAction.Reset || e.Action == NotifyCollectionChangedAction.Replace; - } - - private async Task GetCurrentFlipViewItem() - { - // TODO: Refactor this mechanism by listening to PrepareContainerForItemOverride and Loaded events together. - while (ContainerFromIndex(SelectedIndex) == null) - { - await Task.Delay(100); - } - - return ContainerFromIndex(SelectedIndex) as FlipViewItem; - - - } - - private void UpdateActiveScrollViewer() - { - if (SelectedIndex < 0) - ActiveVerticalScrollViewer = null; - else - { - GetCurrentFlipViewItem().ContinueWith(task => - { - if (task.IsCompletedSuccessfully) - { - var flipViewItem = task.Result; - - _ = Dispatcher.TryRunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => - { - ActiveVerticalScrollViewer = flipViewItem.FindDescendant(); - }); - } - }); - } - } - - public void UpdateActiveCanvas() - { - if (SelectedIndex < 0) - ActiveCanvas = null; - else - { - GetCurrentFlipViewItem().ContinueWith(task => - { - if (task.IsCompletedSuccessfully) - { - var flipViewItem = task.Result; - - _ = Dispatcher.TryRunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => - { - ActiveCanvas = flipViewItem.FindDescendant(); - }); - } - }); - } - } - - /// - /// Navigates to the specified date in the calendar. - /// - /// Date to navigate. - public async void NavigateToDay(DateTime dateTime) - { - await Task.Yield(); - - await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.High, () => - { - // Find the day range that contains the date. - var dayRange = GetItemsSource()?.FirstOrDefault(a => a.CalendarDays.Any(b => b.RepresentingDate.Date == dateTime.Date)); - - if (dayRange != null) - { - var navigationItemIndex = GetItemsSource().IndexOf(dayRange); - - if (Math.Abs(navigationItemIndex - SelectedIndex) > 4) - { - // Difference between dates are high. - // No need to animate this much, just go without animating. - - SelectedIndex = navigationItemIndex; - } - else - { - // Until we reach the day in the flip, simulate next-prev button clicks. - // This will make sure the FlipView animations are triggered. - // Setting SelectedIndex directly doesn't trigger the animations. - - while (SelectedIndex != navigationItemIndex) - { - if (SelectedIndex > navigationItemIndex) - { - GoPreviousFlip(); - } - else - { - GoNextFlip(); - } - } - } - } - }); - } - - private ObservableRangeCollection GetItemsSource() - => ItemsSource as ObservableRangeCollection; -} diff --git a/Wino.Calendar/Controls/WinoCalendarPanel.cs b/Wino.Calendar/Controls/WinoCalendarPanel.cs deleted file mode 100644 index c76801ea..00000000 --- a/Wino.Calendar/Controls/WinoCalendarPanel.cs +++ /dev/null @@ -1,293 +0,0 @@ - -using System; -using System.Collections.Generic; -using System.Linq; -using CommunityToolkit.WinUI; -using Itenso.TimePeriod; -using Windows.Foundation; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; -using Wino.Calendar.Models; -using Wino.Calendar.ViewModels.Data; -using Wino.Core.Domain.Interfaces; - -namespace Wino.Calendar.Controls; - -public partial class WinoCalendarPanel : Panel -{ - private const double LastItemRightExtraMargin = 12d; - - // Store each ICalendarItem measurements by their Id. - private readonly Dictionary _measurements = new Dictionary(); - - public static readonly DependencyProperty EventItemMarginProperty = DependencyProperty.Register(nameof(EventItemMargin), typeof(Thickness), typeof(WinoCalendarPanel), new PropertyMetadata(new Thickness(0, 0, 0, 0))); - public static readonly DependencyProperty HourHeightProperty = DependencyProperty.Register(nameof(HourHeight), typeof(double), typeof(WinoCalendarPanel), new PropertyMetadata(0d)); - public static readonly DependencyProperty PeriodProperty = DependencyProperty.Register(nameof(Period), typeof(ITimePeriod), typeof(WinoCalendarPanel), new PropertyMetadata(null)); - - public ITimePeriod Period - { - get { return (ITimePeriod)GetValue(PeriodProperty); } - set { SetValue(PeriodProperty, value); } - } - - public double HourHeight - { - get { return (double)GetValue(HourHeightProperty); } - set { SetValue(HourHeightProperty, value); } - } - - public Thickness EventItemMargin - { - get { return (Thickness)GetValue(EventItemMarginProperty); } - set { SetValue(EventItemMarginProperty, value); } - } - - private void ResetMeasurements() => _measurements.Clear(); - - private double GetChildTopMargin(ICalendarItem calendarItemViewModel, double availableHeight) - { - var childStart = calendarItemViewModel.StartDate; - - if (childStart <= Period.Start) - { - // Event started before or exactly at the periods tart. This might be a multi-day event. - // We can simply consider event must not have a top margin. - - return 0d; - } - - double minutesFromStart = (childStart - Period.Start).TotalMinutes; - return (minutesFromStart / 1440) * availableHeight; - } - - private double GetChildWidth(CalendarItemMeasurement calendarItemMeasurement, double availableWidth) - { - return (calendarItemMeasurement.Right - calendarItemMeasurement.Left) * availableWidth; - } - - private double GetChildLeftMargin(CalendarItemMeasurement calendarItemMeasurement, double availableWidth) - => availableWidth * calendarItemMeasurement.Left; - - private double GetChildHeight(ICalendarItem child) - { - // All day events are not measured. - if (child.IsAllDayEvent) return 0; - - double childDurationInMinutes = 0d; - double availableHeight = HourHeight * 24; - - var periodRelation = child.Period.GetRelation(Period); - - // Debug.WriteLine($"Render relation of {child.Title} ({child.Period.Start} - {child.Period.End}) is {periodRelation} with {Period.Start.Day}"); - - if (!child.IsMultiDayEvent) - { - childDurationInMinutes = child.Period.Duration.TotalMinutes; - } - else - { - // Multi-day event. - // Check how many of the event falls into the current period. - childDurationInMinutes = (child.Period.End - Period.Start).TotalMinutes; - } - - return (childDurationInMinutes / 1440) * availableHeight; - } - - protected override Size MeasureOverride(Size availableSize) - { - ResetMeasurements(); - return base.MeasureOverride(availableSize); - } - - protected override Size ArrangeOverride(Size finalSize) - { - if (Period == null || HourHeight == 0d) return finalSize; - - // Measure/arrange each child height and width. - // This is a vertical calendar. Therefore the height of each child is the duration of the event. - // Children weights for left and right will be saved if they don't exist. - // This is important because we don't want to measure the weights again. - // They don't change until new event is added or removed. - // Width of the each child may depend on the rectangle packing algorithm. - // Children are first categorized into columns. Then each column is shifted to the left until - // no overlap occurs. The width of each child is calculated based on the number of columns it spans. - - double availableHeight = finalSize.Height; - double availableWidth = finalSize.Width; - - var calendarControls = Children.Cast(); - - if (!calendarControls.Any()) return base.ArrangeOverride(finalSize); - - var events = calendarControls.Select(a => a.Content as CalendarItemViewModel); - - LayoutEvents(events); - - foreach (var control in calendarControls) - { - // We can't arrange this child. - if (!(control.Content is ICalendarItem child)) continue; - - bool isHorizontallyLastItem = false; - - double childWidth = 0, - childHeight = Math.Max(0, GetChildHeight(child)), - childTop = Math.Max(0, GetChildTopMargin(child, availableHeight)), - childLeft = 0; - - // No need to measure anything here. - if (childHeight == 0) continue; - - if (!_measurements.ContainsKey(child)) - { - // Multi-day event. - - childLeft = 0; - childWidth = availableWidth; - } - else - { - var childMeasurement = _measurements[child]; - - childWidth = Math.Max(0, GetChildWidth(childMeasurement, finalSize.Width)); - childLeft = Math.Max(0, GetChildLeftMargin(childMeasurement, availableWidth)); - - isHorizontallyLastItem = childMeasurement.Right == 1; - } - - // Add additional right margin to items that falls on the right edge of the panel. - double extraRightMargin = 0; - - // Multi-day events don't have any margin and their hit test is disabled. - if (!child.IsMultiDayEvent) - { - // Max of 5% of the width or 20px max. - extraRightMargin = isHorizontallyLastItem ? Math.Max(LastItemRightExtraMargin, finalSize.Width * 5 / 100) : 0; - } - - if (childWidth < 0) childWidth = 1; - - // Regular events must have 2px margin - if (!child.IsMultiDayEvent && !child.IsAllDayEvent) - { - childLeft += 2; - childTop += 2; - childHeight -= 2; - childWidth -= 2; - } - - var arrangementRect = new Rect(childLeft + EventItemMargin.Left, childTop + EventItemMargin.Top, Math.Max(childWidth - extraRightMargin, 1), childHeight); - - // Make sure measured size will fit in the arranged box. - var measureSize = arrangementRect.ToSize(); - control.Measure(measureSize); - control.Arrange(arrangementRect); - - //Debug.WriteLine($"{child.Title}, Measured: {measureSize}, Arranged: {arrangementRect}"); - } - - - return finalSize; - } - - #region ColumSpanning and Packing Algorithm - - private void AddOrUpdateMeasurement(ICalendarItem calendarItem, CalendarItemMeasurement measurement) - { - if (_measurements.ContainsKey(calendarItem)) - { - _measurements[calendarItem] = measurement; - } - else - { - _measurements.Add(calendarItem, measurement); - } - } - - // Pick the left and right positions of each event, such that there are no overlap. - private void LayoutEvents(IEnumerable events) - { - var columns = new List>(); - DateTime? lastEventEnding = null; - - foreach (var ev in events.OrderBy(ev => ev.StartDate).ThenBy(ev => ev.EndDate)) - { - // Multi-day events are not measured. - if (ev.IsMultiDayEvent) continue; - - if (ev.Period.Start >= lastEventEnding) - { - PackEvents(columns); - columns.Clear(); - lastEventEnding = null; - } - - bool placed = false; - - foreach (var col in columns) - { - if (!col.Last().Period.OverlapsWith(ev.Period)) - { - col.Add(ev); - placed = true; - break; - } - } - if (!placed) - { - columns.Add(new List { ev }); - } - if (lastEventEnding == null || ev.Period.End > lastEventEnding.Value) - { - lastEventEnding = ev.Period.End; - } - } - if (columns.Count > 0) - { - PackEvents(columns); - } - } - - // Set the left and right positions for each event in the connected group. - private void PackEvents(List> columns) - { - float numColumns = columns.Count; - int iColumn = 0; - - foreach (var col in columns) - { - foreach (var ev in col) - { - int colSpan = ExpandEvent(ev, iColumn, columns); - - var leftWeight = iColumn / numColumns; - var rightWeight = (iColumn + colSpan) / numColumns; - - AddOrUpdateMeasurement(ev, new CalendarItemMeasurement(leftWeight, rightWeight)); - } - - iColumn++; - } - } - - // Checks how many columns the event can expand into, without colliding with other events. - private int ExpandEvent(ICalendarItem ev, int iColumn, List> columns) - { - int colSpan = 1; - - foreach (var col in columns.Skip(iColumn + 1)) - { - foreach (var ev1 in col) - { - if (ev1.Period.OverlapsWith(ev.Period)) return colSpan; - } - - colSpan++; - } - - return colSpan; - } - - #endregion -} diff --git a/Wino.Calendar/Controls/WinoCalendarTypeSelectorControl.cs b/Wino.Calendar/Controls/WinoCalendarTypeSelectorControl.cs deleted file mode 100644 index 899e32da..00000000 --- a/Wino.Calendar/Controls/WinoCalendarTypeSelectorControl.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.Windows.Input; -using CommunityToolkit.Diagnostics; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; -using Wino.Core.Domain.Enums; - -namespace Wino.Calendar.Controls; - -public partial class WinoCalendarTypeSelectorControl : Control -{ - private const string PART_TodayButton = nameof(PART_TodayButton); - private const string PART_DayToggle = nameof(PART_DayToggle); - private const string PART_WeekToggle = nameof(PART_WeekToggle); - private const string PART_MonthToggle = nameof(PART_MonthToggle); - private const string PART_YearToggle = nameof(PART_YearToggle); - - public static readonly DependencyProperty SelectedTypeProperty = DependencyProperty.Register(nameof(SelectedType), typeof(CalendarDisplayType), typeof(WinoCalendarTypeSelectorControl), new PropertyMetadata(CalendarDisplayType.Week)); - public static readonly DependencyProperty DisplayDayCountProperty = DependencyProperty.Register(nameof(DisplayDayCount), typeof(int), typeof(WinoCalendarTypeSelectorControl), new PropertyMetadata(0)); - public static readonly DependencyProperty TodayClickedCommandProperty = DependencyProperty.Register(nameof(TodayClickedCommand), typeof(ICommand), typeof(WinoCalendarTypeSelectorControl), new PropertyMetadata(null)); - - public ICommand TodayClickedCommand - { - get { return (ICommand)GetValue(TodayClickedCommandProperty); } - set { SetValue(TodayClickedCommandProperty, value); } - } - - public CalendarDisplayType SelectedType - { - get { return (CalendarDisplayType)GetValue(SelectedTypeProperty); } - set { SetValue(SelectedTypeProperty, value); } - } - - public int DisplayDayCount - { - get { return (int)GetValue(DisplayDayCountProperty); } - set { SetValue(DisplayDayCountProperty, value); } - } - - private AppBarButton _todayButton; - private AppBarToggleButton _dayToggle; - private AppBarToggleButton _weekToggle; - private AppBarToggleButton _monthToggle; - private AppBarToggleButton _yearToggle; - - public WinoCalendarTypeSelectorControl() - { - DefaultStyleKey = typeof(WinoCalendarTypeSelectorControl); - } - - protected override void OnApplyTemplate() - { - base.OnApplyTemplate(); - - _todayButton = GetTemplateChild(PART_TodayButton) as AppBarButton; - _dayToggle = GetTemplateChild(PART_DayToggle) as AppBarToggleButton; - _weekToggle = GetTemplateChild(PART_WeekToggle) as AppBarToggleButton; - _monthToggle = GetTemplateChild(PART_MonthToggle) as AppBarToggleButton; - _yearToggle = GetTemplateChild(PART_YearToggle) as AppBarToggleButton; - - Guard.IsNotNull(_todayButton, nameof(_todayButton)); - Guard.IsNotNull(_dayToggle, nameof(_dayToggle)); - Guard.IsNotNull(_weekToggle, nameof(_weekToggle)); - Guard.IsNotNull(_monthToggle, nameof(_monthToggle)); - Guard.IsNotNull(_yearToggle, nameof(_yearToggle)); - - _todayButton.Click += TodayClicked; - - _dayToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Day); }; - _weekToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Week); }; - _monthToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Month); }; - _yearToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Year); }; - - UpdateToggleButtonStates(); - } - - private void TodayClicked(object sender, RoutedEventArgs e) => TodayClickedCommand?.Execute(null); - - private void SetSelectedType(CalendarDisplayType type) - { - SelectedType = type; - UpdateToggleButtonStates(); - } - - private void UpdateToggleButtonStates() - { - _dayToggle.IsChecked = SelectedType == CalendarDisplayType.Day; - _weekToggle.IsChecked = SelectedType == CalendarDisplayType.Week; - _monthToggle.IsChecked = SelectedType == CalendarDisplayType.Month; - _yearToggle.IsChecked = SelectedType == CalendarDisplayType.Year; - } -} diff --git a/Wino.Calendar/Controls/WinoCalendarView.cs b/Wino.Calendar/Controls/WinoCalendarView.cs deleted file mode 100644 index 6048d03c..00000000 --- a/Wino.Calendar/Controls/WinoCalendarView.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System; -using System.Windows.Input; -using CommunityToolkit.Diagnostics; -using Windows.UI; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; -using Windows.UI.Xaml.Media; -using Wino.Core.Domain.Models.Calendar; -using Wino.Helpers; - -namespace Wino.Calendar.Controls; - -public partial class WinoCalendarView : Control -{ - private const string PART_DayViewItemBorder = nameof(PART_DayViewItemBorder); - private const string PART_CalendarView = nameof(PART_CalendarView); - - public static readonly DependencyProperty HighlightedDateRangeProperty = DependencyProperty.Register(nameof(HighlightedDateRange), typeof(DateRange), typeof(WinoCalendarView), new PropertyMetadata(null, new PropertyChangedCallback(OnHighlightedDateRangeChanged))); - public static readonly DependencyProperty VisibleDateBackgroundProperty = DependencyProperty.Register(nameof(VisibleDateBackground), typeof(Brush), typeof(WinoCalendarView), new PropertyMetadata(null, new PropertyChangedCallback(OnPropertiesChanged))); - public static readonly DependencyProperty DateClickedCommandProperty = DependencyProperty.Register(nameof(DateClickedCommand), typeof(ICommand), typeof(WinoCalendarView), new PropertyMetadata(null)); - public static readonly DependencyProperty TodayBackgroundColorProperty = DependencyProperty.Register(nameof(TodayBackgroundColor), typeof(Color), typeof(WinoCalendarView), new PropertyMetadata(null)); - - public Color TodayBackgroundColor - { - get { return (Color)GetValue(TodayBackgroundColorProperty); } - set { SetValue(TodayBackgroundColorProperty, value); } - } - - /// - /// Gets or sets the command to execute when a date is picked. - /// Unused. - /// - public ICommand DateClickedCommand - { - get { return (ICommand)GetValue(DateClickedCommandProperty); } - set { SetValue(DateClickedCommandProperty, value); } - } - - /// - /// Gets or sets the highlighted range of dates. - /// - public DateRange HighlightedDateRange - { - get { return (DateRange)GetValue(HighlightedDateRangeProperty); } - set { SetValue(HighlightedDateRangeProperty, value); } - } - - public Brush VisibleDateBackground - { - get { return (Brush)GetValue(VisibleDateBackgroundProperty); } - set { SetValue(VisibleDateBackgroundProperty, value); } - } - - - - private CalendarView CalendarView; - - public WinoCalendarView() - { - DefaultStyleKey = typeof(WinoCalendarView); - } - - protected override void OnApplyTemplate() - { - base.OnApplyTemplate(); - - CalendarView = GetTemplateChild(PART_CalendarView) as CalendarView; - - Guard.IsNotNull(CalendarView, nameof(CalendarView)); - - CalendarView.SelectedDatesChanged -= InternalCalendarViewSelectionChanged; - CalendarView.SelectedDatesChanged += InternalCalendarViewSelectionChanged; - - // TODO: Should come from settings. - CalendarView.FirstDayOfWeek = Windows.Globalization.DayOfWeek.Monday; - - // Everytime display mode changes, update the visible date range backgrounds. - // If users go back from year -> month -> day, we need to update the visible date range backgrounds. - - CalendarView.RegisterPropertyChangedCallback(CalendarView.DisplayModeProperty, (s, e) => UpdateVisibleDateRangeBackgrounds()); - } - - private void InternalCalendarViewSelectionChanged(CalendarView sender, CalendarViewSelectedDatesChangedEventArgs args) - { - if (args.AddedDates?.Count > 0) - { - var clickedDate = args.AddedDates[0].Date; - SetInnerDisplayDate(clickedDate); - - var clickArgs = new CalendarViewDayClickedEventArgs(clickedDate); - DateClickedCommand?.Execute(clickArgs); - } - - // Reset selection, we don't show selected dates but react to them. - CalendarView.SelectedDates.Clear(); - } - - private static void OnPropertiesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is WinoCalendarView control) - { - control.UpdateVisibleDateRangeBackgrounds(); - } - } - - private void SetInnerDisplayDate(DateTime dateTime) => CalendarView?.SetDisplayDate(dateTime); - - // Changing selected dates will trigger the selection changed event. - // It will behave like user clicked the date. - public void GoToDay(DateTime dateTime) => CalendarView.SelectedDates.Add(dateTime); - - private static void OnHighlightedDateRangeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is WinoCalendarView control) - { - control.SetInnerDisplayDate(control.HighlightedDateRange.StartDate); - control.UpdateVisibleDateRangeBackgrounds(); - } - } - - public void UpdateVisibleDateRangeBackgrounds() - { - if (HighlightedDateRange == null || VisibleDateBackground == null || TodayBackgroundColor == null || CalendarView == null) return; - - var markDateCalendarDayItems = WinoVisualTreeHelper.FindDescendants(CalendarView); - - foreach (var calendarDayItem in markDateCalendarDayItems) - { - var border = WinoVisualTreeHelper.GetChildObject(calendarDayItem, PART_DayViewItemBorder); - - if (border == null) return; - - if (calendarDayItem.Date.Date == DateTime.Today.Date) - { - border.Background = new SolidColorBrush(TodayBackgroundColor); - } - else if (calendarDayItem.Date.Date >= HighlightedDateRange.StartDate.Date && calendarDayItem.Date.Date < HighlightedDateRange.EndDate.Date) - { - border.Background = VisibleDateBackground; - } - else - { - border.Background = null; - } - } - } -} diff --git a/Wino.Calendar/Controls/WinoDayTimelineCanvas.cs b/Wino.Calendar/Controls/WinoDayTimelineCanvas.cs deleted file mode 100644 index 66b3bdd8..00000000 --- a/Wino.Calendar/Controls/WinoDayTimelineCanvas.cs +++ /dev/null @@ -1,277 +0,0 @@ -using System; -using System.Diagnostics; -using Microsoft.Graphics.Canvas.Geometry; -using Microsoft.Graphics.Canvas.UI.Xaml; -using Windows.Foundation; -using Windows.UI.Input; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; -using Windows.UI.Xaml.Media; -using Wino.Calendar.Args; -using Wino.Core.Domain.Models.Calendar; - -namespace Wino.Calendar.Controls; - -public partial class WinoDayTimelineCanvas : Control, IDisposable -{ - public event EventHandler TimelineCellSelected; - public event EventHandler TimelineCellUnselected; - - private const string PART_InternalCanvas = nameof(PART_InternalCanvas); - private CanvasControl Canvas; - - public static readonly DependencyProperty RenderOptionsProperty = DependencyProperty.Register(nameof(RenderOptions), typeof(CalendarRenderOptions), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged))); - public static readonly DependencyProperty SeperatorColorProperty = DependencyProperty.Register(nameof(SeperatorColor), typeof(SolidColorBrush), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged))); - public static readonly DependencyProperty HalfHourSeperatorColorProperty = DependencyProperty.Register(nameof(HalfHourSeperatorColor), typeof(SolidColorBrush), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged))); - public static readonly DependencyProperty SelectedCellBackgroundBrushProperty = DependencyProperty.Register(nameof(SelectedCellBackgroundBrush), typeof(SolidColorBrush), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged))); - public static readonly DependencyProperty WorkingHourCellBackgroundColorProperty = DependencyProperty.Register(nameof(WorkingHourCellBackgroundColor), typeof(SolidColorBrush), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged))); - public static readonly DependencyProperty SelectedDateTimeProperty = DependencyProperty.Register(nameof(SelectedDateTime), typeof(DateTime?), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnSelectedDateTimeChanged))); - public static readonly DependencyProperty PositionerUIElementProperty = DependencyProperty.Register(nameof(PositionerUIElement), typeof(UIElement), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null)); - - public UIElement PositionerUIElement - { - get { return (UIElement)GetValue(PositionerUIElementProperty); } - set { SetValue(PositionerUIElementProperty, value); } - } - - public CalendarRenderOptions RenderOptions - { - get { return (CalendarRenderOptions)GetValue(RenderOptionsProperty); } - set { SetValue(RenderOptionsProperty, value); } - } - - public SolidColorBrush HalfHourSeperatorColor - { - get { return (SolidColorBrush)GetValue(HalfHourSeperatorColorProperty); } - set { SetValue(HalfHourSeperatorColorProperty, value); } - } - - public SolidColorBrush SeperatorColor - { - get { return (SolidColorBrush)GetValue(SeperatorColorProperty); } - set { SetValue(SeperatorColorProperty, value); } - } - - public SolidColorBrush WorkingHourCellBackgroundColor - { - get { return (SolidColorBrush)GetValue(WorkingHourCellBackgroundColorProperty); } - set { SetValue(WorkingHourCellBackgroundColorProperty, value); } - } - - public SolidColorBrush SelectedCellBackgroundBrush - { - get { return (SolidColorBrush)GetValue(SelectedCellBackgroundBrushProperty); } - set { SetValue(SelectedCellBackgroundBrushProperty, value); } - } - - public DateTime? SelectedDateTime - { - get { return (DateTime?)GetValue(SelectedDateTimeProperty); } - set { SetValue(SelectedDateTimeProperty, value); } - } - - protected override void OnApplyTemplate() - { - base.OnApplyTemplate(); - - Canvas = GetTemplateChild(PART_InternalCanvas) as CanvasControl; - - // TODO: These will leak. Dispose them properly when needed. - Canvas.Draw += OnCanvasDraw; - Canvas.PointerPressed += OnCanvasPointerPressed; - - ForceDraw(); - } - - private static void OnSelectedDateTimeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is WinoDayTimelineCanvas control) - { - if (e.OldValue != null && e.NewValue == null) - { - control.RaiseCellUnselected(); - } - - control.ForceDraw(); - } - } - - private void RaiseCellUnselected() - { - TimelineCellUnselected?.Invoke(this, new TimelineCellUnselectedArgs()); - } - - private void OnCanvasPointerPressed(object sender, Windows.UI.Xaml.Input.PointerRoutedEventArgs e) - { - if (RenderOptions == null) return; - - var hourHeight = RenderOptions.CalendarSettings.HourHeight; - - // When users click to cell we need to find the day, hour and minutes (first 30 minutes or second 30 minutes) that it represents on the timeline. - - PointerPoint positionerRootPoint = e.GetCurrentPoint(PositionerUIElement); - PointerPoint canvasPointerPoint = e.GetCurrentPoint(Canvas); - - Point touchPoint = canvasPointerPoint.Position; - - var singleDayWidth = (Canvas.ActualWidth / RenderOptions.TotalDayCount); - - int day = (int)(touchPoint.X / singleDayWidth); - int hour = (int)(touchPoint.Y / hourHeight); - - bool isSecondHalf = touchPoint.Y % hourHeight > (hourHeight / 2); - - var diffX = positionerRootPoint.Position.X - touchPoint.X; - var diffY = positionerRootPoint.Position.Y - touchPoint.Y; - - var cellStartRelativePositionX = diffX + (day * singleDayWidth); - var cellEndRelativePositionX = cellStartRelativePositionX + singleDayWidth; - - var cellStartRelativePositionY = diffY + (hour * hourHeight) + (isSecondHalf ? hourHeight / 2 : 0); - var cellEndRelativePositionY = cellStartRelativePositionY + (isSecondHalf ? (hourHeight / 2) : hourHeight); - - var cellSize = new Size(cellEndRelativePositionX - cellStartRelativePositionX, hourHeight / 2); - var positionerPoint = new Point(cellStartRelativePositionX, cellStartRelativePositionY); - - var clickedDateTime = RenderOptions.DateRange.StartDate.AddDays(day).AddHours(hour).AddMinutes(isSecondHalf ? 30 : 0); - - // If there is already a selected date, in order to mimic the popup behavior, we need to dismiss the previous selection first. - // Next click will be a new selection. - - // Raise the events directly here instead of DP to not lose pointer position. - if (clickedDateTime == SelectedDateTime || SelectedDateTime != null) - { - SelectedDateTime = null; - } - else - { - SelectedDateTime = clickedDateTime; - TimelineCellSelected?.Invoke(this, new TimelineCellSelectedArgs(clickedDateTime, touchPoint, positionerPoint, cellSize)); - } - - Debug.WriteLine($"Clicked: {clickedDateTime}"); - } - - public WinoDayTimelineCanvas() - { - DefaultStyleKey = typeof(WinoDayTimelineCanvas); - } - - private static void OnRenderingPropertiesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is WinoDayTimelineCanvas control) - { - control.ForceDraw(); - } - } - - private void ForceDraw() => Canvas?.Invalidate(); - - private bool CanDrawTimeline() - { - return RenderOptions != null - && Canvas != null - && Canvas.ReadyToDraw - && WorkingHourCellBackgroundColor != null - && SeperatorColor != null - && HalfHourSeperatorColor != null - && SelectedCellBackgroundBrush != null; - } - - private void OnCanvasDraw(CanvasControl sender, CanvasDrawEventArgs args) - { - if (!CanDrawTimeline()) return; - - int hours = 24; - - double canvasWidth = Canvas.ActualWidth; - double canvasHeight = Canvas.ActualHeight; - - if (canvasWidth == 0 || canvasHeight == 0) return; - - // Calculate the width of each rectangle (1 day column) - // Equal distribution of the whole width. - double rectWidth = canvasWidth / RenderOptions.TotalDayCount; - - // Calculate the height of each rectangle (1 hour row) - double rectHeight = RenderOptions.CalendarSettings.HourHeight; - - // Define stroke and fill colors - var strokeColor = SeperatorColor.Color; - float strokeThickness = 0.5f; - - for (int day = 0; day < RenderOptions.TotalDayCount; day++) - { - var currentDay = RenderOptions.DateRange.StartDate.AddDays(day); - - bool isWorkingDay = RenderOptions.CalendarSettings.WorkingDays.Contains(currentDay.DayOfWeek); - - // Loop through each hour (rows) - for (int hour = 0; hour < hours; hour++) - { - var renderTime = TimeSpan.FromHours(hour); - - var representingDateTime = currentDay.AddHours(hour); - - // Calculate the position and size of the rectangle - double x = day * rectWidth; - double y = hour * rectHeight; - - var rectangle = new Rect(x, y, rectWidth, rectHeight); - - // Draw the rectangle border. - // This is the main rectangle. - args.DrawingSession.DrawRectangle(rectangle, strokeColor, strokeThickness); - - // Fill another rectangle with the working hour background color - // This rectangle must be placed with -1 margin to prevent invisible borders of the main rectangle. - if (isWorkingDay && renderTime >= RenderOptions.CalendarSettings.WorkingHourStart && renderTime <= RenderOptions.CalendarSettings.WorkingHourEnd) - { - var backgroundRectangle = new Rect(x + 1, y + 1, rectWidth - 1, rectHeight - 1); - - args.DrawingSession.DrawRectangle(backgroundRectangle, strokeColor, strokeThickness); - args.DrawingSession.FillRectangle(backgroundRectangle, WorkingHourCellBackgroundColor.Color); - } - - // Draw a line in the center of the rectangle for representing half hours. - double lineY = y + rectHeight / 2; - - args.DrawingSession.DrawLine((float)x, (float)lineY, (float)(x + rectWidth), (float)lineY, HalfHourSeperatorColor.Color, strokeThickness, new CanvasStrokeStyle() - { - DashStyle = CanvasDashStyle.Dot - }); - } - - // Draw selected item background color for the date if possible. - if (SelectedDateTime != null) - { - var selectedDateTime = SelectedDateTime.Value; - if (selectedDateTime.Date == currentDay.Date) - { - var selectionRectHeight = rectHeight / 2; - var selectedY = selectedDateTime.Hour * rectHeight + (selectedDateTime.Minute / 60) * rectHeight; - - // Second half of the hour is selected. - if (selectedDateTime.TimeOfDay.Minutes == 30) - { - selectedY += rectHeight / 2; - } - - var selectedRectangle = new Rect(day * rectWidth, selectedY, rectWidth, selectionRectHeight); - args.DrawingSession.FillRectangle(selectedRectangle, SelectedCellBackgroundBrush.Color); - } - } - } - } - - public void Dispose() - { - if (Canvas == null) return; - - Canvas.Draw -= OnCanvasDraw; - Canvas.PointerPressed -= OnCanvasPointerPressed; - Canvas.RemoveFromVisualTree(); - - Canvas = null; - } -} diff --git a/Wino.Calendar/MainPage.xaml.cs b/Wino.Calendar/MainPage.xaml.cs deleted file mode 100644 index 7fb5bdf3..00000000 --- a/Wino.Calendar/MainPage.xaml.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices.WindowsRuntime; -using Windows.Foundation; -using Windows.Foundation.Collections; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; -using Windows.UI.Xaml.Controls.Primitives; -using Windows.UI.Xaml.Data; -using Windows.UI.Xaml.Input; -using Windows.UI.Xaml.Media; -using Windows.UI.Xaml.Navigation; - -// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409 - -namespace Wino.Calendar; - -/// -/// An empty page that can be used on its own or navigated to within a Frame. -/// -public sealed partial class MainPage : Page -{ - public MainPage() - { - this.InitializeComponent(); - } -} diff --git a/Wino.Calendar/Package.appxmanifest b/Wino.Calendar/Package.appxmanifest deleted file mode 100644 index 7d96dede..00000000 --- a/Wino.Calendar/Package.appxmanifest +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - Wino Calendar - Burak KÖSE - Assets\StoreLogo.png - - - - - - - - - - - - - - - - - - - - - - Google Auth Protocol - - - - - - - - - - - - - - - - diff --git a/Wino.Calendar/Properties/launchSettings.json b/Wino.Calendar/Properties/launchSettings.json deleted file mode 100644 index 0f9c975c..00000000 --- a/Wino.Calendar/Properties/launchSettings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "profiles": { - "Wino.Calendar": { - "commandName": "MsixPackage" - } - } -} diff --git a/Wino.Calendar/Services/AccountCalendarStateService.cs b/Wino.Calendar/Services/AccountCalendarStateService.cs deleted file mode 100644 index cf58b2f3..00000000 --- a/Wino.Calendar/Services/AccountCalendarStateService.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using CommunityToolkit.Mvvm.ComponentModel; -using Wino.Calendar.ViewModels.Data; -using Wino.Calendar.ViewModels.Interfaces; -using Wino.Core.Domain.Entities.Shared; - -namespace Wino.Calendar.Services; - -/// -/// Encapsulated state manager for collectively managing the state of account calendars. -/// Callers must react to the events to update their state only from this service. -/// -public partial class AccountCalendarStateService : ObservableObject, IAccountCalendarStateService -{ - public event EventHandler CollectiveAccountGroupSelectionStateChanged; - public event EventHandler AccountCalendarSelectionStateChanged; - - [ObservableProperty] - public partial ReadOnlyObservableCollection GroupedAccountCalendars { get; set; } - - private ObservableCollection _internalGroupedAccountCalendars = new ObservableCollection(); - - public IEnumerable ActiveCalendars - { - get - { - return GroupedAccountCalendars - .SelectMany(a => a.AccountCalendars) - .Where(b => b.IsChecked); - } - } - - public IEnumerable> GroupedAccountCalendarsEnumerable - { - get - { - return GroupedAccountCalendars - .Select(a => a.AccountCalendars) - .SelectMany(b => b) - .GroupBy(c => c.Account); - } - } - - public AccountCalendarStateService() - { - GroupedAccountCalendars = new ReadOnlyObservableCollection(_internalGroupedAccountCalendars); - } - - private void SingleGroupCalendarCollectiveStateChanged(object sender, EventArgs e) - => CollectiveAccountGroupSelectionStateChanged?.Invoke(this, sender as GroupedAccountCalendarViewModel); - - private void SingleCalendarSelectionStateChanged(object sender, AccountCalendarViewModel e) - => AccountCalendarSelectionStateChanged?.Invoke(this, e); - - public void AddGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar) - { - groupedAccountCalendar.CalendarSelectionStateChanged += SingleCalendarSelectionStateChanged; - groupedAccountCalendar.CollectiveSelectionStateChanged += SingleGroupCalendarCollectiveStateChanged; - - _internalGroupedAccountCalendars.Add(groupedAccountCalendar); - } - - public void RemoveGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar) - { - groupedAccountCalendar.CalendarSelectionStateChanged -= SingleCalendarSelectionStateChanged; - groupedAccountCalendar.CollectiveSelectionStateChanged -= SingleGroupCalendarCollectiveStateChanged; - - _internalGroupedAccountCalendars.Remove(groupedAccountCalendar); - } - - public void ClearGroupedAccountCalendar() - { - foreach (var groupedAccountCalendar in _internalGroupedAccountCalendars) - { - RemoveGroupedAccountCalendar(groupedAccountCalendar); - } - } - - public void AddAccountCalendar(AccountCalendarViewModel accountCalendar) - { - // Find the group that this calendar belongs to. - var group = _internalGroupedAccountCalendars.FirstOrDefault(g => g.Account.Id == accountCalendar.Account.Id); - - if (group == null) - { - // If the group doesn't exist, create it. - group = new GroupedAccountCalendarViewModel(accountCalendar.Account, new[] { accountCalendar }); - AddGroupedAccountCalendar(group); - } - else - { - group.AccountCalendars.Add(accountCalendar); - } - } - - public void RemoveAccountCalendar(AccountCalendarViewModel accountCalendar) - { - var group = _internalGroupedAccountCalendars.FirstOrDefault(g => g.Account.Id == accountCalendar.Account.Id); - - // We don't expect but just in case. - if (group == null) return; - - group.AccountCalendars.Remove(accountCalendar); - - if (group.AccountCalendars.Count == 0) - { - RemoveGroupedAccountCalendar(group); - } - } -} diff --git a/Wino.Calendar/Services/CalendarAuthenticatorConfig.cs b/Wino.Calendar/Services/CalendarAuthenticatorConfig.cs deleted file mode 100644 index 391ff7f2..00000000 --- a/Wino.Calendar/Services/CalendarAuthenticatorConfig.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Wino.Core.Domain.Interfaces; - -namespace Wino.Calendar.Services; - -public class CalendarAuthenticatorConfig : IAuthenticatorConfig -{ - public string OutlookAuthenticatorClientId => "b19c2035-d740-49ff-b297-de6ec561b208"; - - public string[] OutlookScope => new string[] - { - "Calendars.Read", - "Calendars.Read.Shared", - "offline_access", - "Calendars.ReadBasic", - "Calendars.ReadWrite", - "Calendars.ReadWrite.Shared", - "User.Read" - }; - - public string GmailAuthenticatorClientId => "973025879644-s7b4ur9p3rlgop6a22u7iuptdc0brnrn.apps.googleusercontent.com"; - - public string[] GmailScope => new string[] - { - "https://www.googleapis.com/auth/calendar", - "https://www.googleapis.com/auth/calendar.events", - "https://www.googleapis.com/auth/calendar.settings.readonly", - "https://www.googleapis.com/auth/userinfo.profile", - "https://www.googleapis.com/auth/userinfo.email" - }; - - public string GmailTokenStoreIdentifier => "WinoCalendarGmailTokenStore"; -} diff --git a/Wino.Calendar/Services/DialogService.cs b/Wino.Calendar/Services/DialogService.cs deleted file mode 100644 index 5a8c8965..00000000 --- a/Wino.Calendar/Services/DialogService.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Windows.UI.Xaml; -using Wino.Core.Domain.Interfaces; -using Wino.Core.UWP.Services; - -namespace Wino.Calendar.Services; - -public class DialogService : DialogServiceBase, ICalendarDialogService -{ - public DialogService(IThemeService themeService, - IConfigurationService configurationService, - IApplicationResourceManager applicationResourceManager) : base(themeService, configurationService, applicationResourceManager) - { - } -} diff --git a/Wino.Calendar/Services/NavigationService.cs b/Wino.Calendar/Services/NavigationService.cs deleted file mode 100644 index f409c4b3..00000000 --- a/Wino.Calendar/Services/NavigationService.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; -using Wino.Calendar.Views; -using Wino.Calendar.Views.Account; -using Wino.Calendar.Views.Settings; -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Navigation; -using Wino.Core.UWP.Services; -using Wino.Views; - -namespace Wino.Calendar.Services; - -public class NavigationService : NavigationServiceBase, INavigationService -{ - public Type GetPageType(WinoPage winoPage) - { - return winoPage switch - { - WinoPage.CalendarPage => typeof(CalendarPage), - WinoPage.SettingsPage => typeof(SettingsPage), - WinoPage.CalendarSettingsPage => typeof(CalendarSettingsPage), - WinoPage.AccountManagementPage => typeof(AccountManagementPage), - WinoPage.ManageAccountsPage => typeof(ManageAccountsPage), - WinoPage.PersonalizationPage => typeof(PersonalizationPage), - WinoPage.AccountDetailsPage => typeof(AccountDetailsPage), - WinoPage.EventDetailsPage => typeof(EventDetailsPage), - _ => throw new Exception("Page is not implemented yet."), - }; - } - - public void GoBack() - { - if (Window.Current.Content is Frame appFrame && appFrame.Content is AppShell shellPage) - { - var shellFrame = shellPage.GetShellFrame(); - - if (shellFrame.CanGoBack) - { - shellFrame.GoBack(); - } - } - } - - public bool Navigate(WinoPage page, object parameter = null, NavigationReferenceFrame frame = NavigationReferenceFrame.ShellFrame, NavigationTransitionType transition = NavigationTransitionType.None) - { - // All navigations are performed on shell frame for calendar. - - if (Window.Current.Content is Frame appFrame && appFrame.Content is AppShell shellPage) - { - var shellFrame = shellPage.GetShellFrame(); - - var pageType = GetPageType(page); - - shellFrame.Navigate(pageType, parameter); - return true; - } - - return false; - } -} diff --git a/Wino.Calendar/Services/ProviderService.cs b/Wino.Calendar/Services/ProviderService.cs deleted file mode 100644 index e0cac8ad..00000000 --- a/Wino.Calendar/Services/ProviderService.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Accounts; - -namespace Wino.Calendar.Services; - -public class ProviderService : IProviderService -{ - public IProviderDetail GetProviderDetail(MailProviderType type) - { - var details = GetAvailableProviders(); - - return details.FirstOrDefault(a => a.Type == type); - } - - public List GetAvailableProviders() - { - var providerList = new List(); - - var providers = new MailProviderType[] - { - MailProviderType.Outlook, - MailProviderType.Gmail - }; - - foreach (var type in providers) - { - providerList.Add(new ProviderDetail(type, SpecialImapProvider.None)); - } - - return providerList; - } -} diff --git a/Wino.Calendar/Services/SettingsBuilderService.cs b/Wino.Calendar/Services/SettingsBuilderService.cs deleted file mode 100644 index 953a4031..00000000 --- a/Wino.Calendar/Services/SettingsBuilderService.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; -using Wino.Core.Domain; -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Settings; - -namespace Wino.Calendar.Services; - -public class SettingsBuilderService : ISettingsBuilderService -{ - public List GetSettingItems() - => new List() - { - new SettingOption(Translator.SettingsManageAccountSettings_Title, Translator.SettingsManageAccountSettings_Description, WinoPage.AccountManagementPage,"F1 M 3.75 5 L 3.75 4.902344 C 3.75 4.225262 3.885091 3.588867 4.155273 2.993164 C 4.425456 2.397461 4.790039 1.878256 5.249023 1.435547 C 5.708008 0.99284 6.238606 0.642904 6.84082 0.385742 C 7.443034 0.128582 8.079427 0 8.75 0 C 9.420572 0 10.056966 0.128582 10.65918 0.385742 C 11.261393 0.642904 11.791992 0.99284 12.250977 1.435547 C 12.709961 1.878256 13.074544 2.397461 13.344727 2.993164 C 13.614908 3.588867 13.75 4.225262 13.75 4.902344 C 13.75 5.397137 13.689778 5.87077 13.569336 6.323242 C 13.448893 6.775717 13.258463 7.213542 12.998047 7.636719 C 12.229817 7.799479 11.510416 8.079428 10.839844 8.476562 C 10.169271 8.873698 9.583333 9.378256 9.082031 9.990234 C 9.042969 9.996745 9.005533 10 8.969727 10 C 8.933919 10 8.899739 10 8.867188 10 L 8.652344 10 C 7.97526 10 7.338867 9.864909 6.743164 9.594727 C 6.147461 9.324545 5.628255 8.959961 5.185547 8.500977 C 4.742838 8.041993 4.392903 7.511395 4.135742 6.90918 C 3.878581 6.306967 3.75 5.670573 3.75 5 Z M 12.5 5 L 12.5 4.921875 C 12.5 4.414062 12.399088 3.937176 12.197266 3.491211 C 11.995442 3.045248 11.722005 2.65625 11.376953 2.324219 C 11.0319 1.992188 10.633138 1.730145 10.180664 1.538086 C 9.728189 1.346029 9.251302 1.25 8.75 1.25 C 8.229166 1.25 7.740885 1.347656 7.285156 1.542969 C 6.829427 1.738281 6.432292 2.005209 6.09375 2.34375 C 5.755208 2.682293 5.488281 3.079428 5.292969 3.535156 C 5.097656 3.990887 5 4.479167 5 5 C 5 5.520834 5.097656 6.009115 5.292969 6.464844 C 5.488281 6.920573 5.755208 7.317709 6.09375 7.65625 C 6.432292 7.994792 6.829427 8.261719 7.285156 8.457031 C 7.740885 8.652344 8.229166 8.75 8.75 8.75 C 9.270833 8.75 9.759114 8.652344 10.214844 8.457031 C 10.670572 8.261719 11.067708 7.994792 11.40625 7.65625 C 11.744791 7.317709 12.011719 6.920573 12.207031 6.464844 C 12.402344 6.009115 12.5 5.520834 12.5 5 Z M 12.148438 19.365234 C 11.777344 19.208984 11.41276 19.010416 11.054688 18.769531 C 11.113281 18.561197 11.170247 18.357748 11.225586 18.15918 C 11.280924 17.960611 11.308594 17.753906 11.308594 17.539062 C 11.308594 17.252604 11.259766 16.977539 11.162109 16.713867 C 11.064453 16.450195 10.930989 16.210938 10.761719 15.996094 C 10.592447 15.78125 10.390625 15.597331 10.15625 15.444336 C 9.921875 15.291342 9.664713 15.182292 9.384766 15.117188 L 8.779297 14.980469 C 8.759766 14.778646 8.75 14.576823 8.75 14.375 C 8.75 14.179688 8.759766 13.977865 8.779297 13.769531 L 9.384766 13.632812 C 9.664713 13.567709 9.921875 13.458659 10.15625 13.305664 C 10.390625 13.15267 10.592447 12.96875 10.761719 12.753906 C 10.930989 12.539062 11.064453 12.299805 11.162109 12.036133 C 11.259766 11.772461 11.308594 11.497396 11.308594 11.210938 C 11.308594 10.996094 11.280924 10.789389 11.225586 10.59082 C 11.170247 10.392253 11.113281 10.188803 11.054688 9.980469 C 11.41276 9.739584 11.777344 9.541016 12.148438 9.384766 C 12.324219 9.560547 12.488606 9.720053 12.641602 9.863281 C 12.794596 10.006511 12.957356 10.131836 13.129883 10.239258 C 13.302408 10.34668 13.487955 10.429688 13.686523 10.488281 C 13.885091 10.546875 14.114583 10.576172 14.375 10.576172 C 14.635416 10.576172 14.866535 10.546875 15.068359 10.488281 C 15.270182 10.429688 15.455729 10.34668 15.625 10.239258 C 15.79427 10.131836 15.955402 10.006511 16.108398 9.863281 C 16.261393 9.720053 16.425781 9.560547 16.601562 9.384766 C 16.972656 9.541016 17.337238 9.739584 17.695312 9.980469 C 17.636719 10.188803 17.579752 10.392253 17.524414 10.59082 C 17.469074 10.789389 17.441406 10.996094 17.441406 11.210938 C 17.441406 11.497396 17.488605 11.772461 17.583008 12.036133 C 17.677408 12.299805 17.810871 12.539062 17.983398 12.753906 C 18.155924 12.96875 18.359375 13.15267 18.59375 13.305664 C 18.828125 13.458659 19.085285 13.567709 19.365234 13.632812 L 19.970703 13.769531 C 19.990234 13.977865 20 14.179688 20 14.375 C 20 14.576823 19.990234 14.778646 19.970703 14.980469 L 19.365234 15.117188 C 19.085285 15.182292 18.828125 15.291342 18.59375 15.444336 C 18.359375 15.597331 18.155924 15.78125 17.983398 15.996094 C 17.810871 16.210938 17.677408 16.450195 17.583008 16.713867 C 17.488605 16.977539 17.441406 17.252604 17.441406 17.539062 C 17.441406 17.753906 17.469074 17.960611 17.524414 18.15918 C 17.579752 18.357748 17.636719 18.561197 17.695312 18.769531 C 17.337238 19.010416 16.972656 19.208984 16.601562 19.365234 C 16.425781 19.189453 16.261393 19.029947 16.108398 18.886719 C 15.955402 18.74349 15.79427 18.618164 15.625 18.510742 C 15.455729 18.40332 15.270182 18.320312 15.068359 18.261719 C 14.866535 18.203125 14.635416 18.173828 14.375 18.173828 C 14.108072 18.173828 13.875324 18.203125 13.676758 18.261719 C 13.478189 18.320312 13.294271 18.40332 13.125 18.510742 C 12.955729 18.618164 12.794596 18.74349 12.641602 18.886719 C 12.488606 19.029947 12.324219 19.189453 12.148438 19.365234 Z M 0 13.75 C 0 13.404948 0.065104 13.081055 0.195312 12.77832 C 0.325521 12.475586 0.504557 12.210287 0.732422 11.982422 C 0.960286 11.754558 1.225586 11.575521 1.52832 11.445312 C 1.831055 11.315104 2.154948 11.25 2.5 11.25 L 8.251953 11.25 C 8.043619 11.660156 7.880859 12.076823 7.763672 12.5 L 2.5 12.5 C 2.324219 12.5 2.159831 12.532553 2.006836 12.597656 C 1.853841 12.662761 1.722005 12.750651 1.611328 12.861328 C 1.500651 12.972006 1.41276 13.103842 1.347656 13.256836 C 1.282552 13.409831 1.25 13.574219 1.25 13.75 C 1.25 14.420573 1.360677 15.008139 1.582031 15.512695 C 1.803385 16.017252 2.102865 16.455078 2.480469 16.826172 C 2.858073 17.197266 3.297526 17.50651 3.798828 17.753906 C 4.30013 18.001303 4.827474 18.198242 5.380859 18.344727 C 5.934244 18.491211 6.50065 18.595377 7.080078 18.657227 C 7.659505 18.719076 8.216146 18.75 8.75 18.75 L 9.072266 18.75 C 9.449869 19.199219 9.866536 19.5931 10.322266 19.931641 C 10.061849 19.964193 9.801432 19.983725 9.541016 19.990234 C 9.280599 19.996744 9.016927 20 8.75 20 C 8.190104 20 7.618814 19.973959 7.036133 19.921875 C 6.45345 19.869791 5.878906 19.775391 5.3125 19.638672 C 4.746094 19.501953 4.197591 19.319662 3.666992 19.091797 C 3.136393 18.863932 2.646484 18.574219 2.197266 18.222656 C 1.474609 17.66276 0.927734 17.005209 0.556641 16.25 C 0.185547 15.494792 0 14.661458 0 13.75 Z M 15.625 14.375 C 15.625 14.205729 15.592447 14.044597 15.527344 13.891602 C 15.462239 13.738607 15.372721 13.605144 15.258789 13.491211 C 15.144855 13.377279 15.011393 13.287761 14.858398 13.222656 C 14.705403 13.157553 14.544271 13.125 14.375 13.125 C 14.199219 13.125 14.036458 13.157553 13.886719 13.222656 C 13.736979 13.287761 13.605143 13.377279 13.491211 13.491211 C 13.377278 13.605144 13.28776 13.736979 13.222656 13.886719 C 13.157551 14.036459 13.124999 14.199219 13.125 14.375 C 13.124999 14.550781 13.157551 14.71517 13.222656 14.868164 C 13.28776 15.021159 13.37565 15.152995 13.486328 15.263672 C 13.597004 15.37435 13.72884 15.46224 13.881836 15.527344 C 14.03483 15.592448 14.199219 15.625 14.375 15.625 C 14.550781 15.625 14.713541 15.592448 14.863281 15.527344 C 15.01302 15.46224 15.144855 15.372722 15.258789 15.258789 C 15.372721 15.144857 15.462239 15.013021 15.527344 14.863281 C 15.592447 14.713542 15.625 14.550781 15.625 14.375 Z "), - new SettingOption(Translator.SettingsAppPreferences_Title, Translator.SettingsAppPreferences_Description, WinoPage.AppPreferencesPage,"F1 M 15.078125 1.25 C 15.566406 1.25 16.033527 1.349285 16.479492 1.547852 C 16.925455 1.74642 17.31608 2.013348 17.651367 2.348633 C 17.986652 2.68392 18.25358 3.074545 18.452148 3.520508 C 18.650715 3.966473 18.75 4.433594 18.75 4.921875 L 18.75 15.078125 C 18.75 15.566406 18.650715 16.033529 18.452148 16.479492 C 18.25358 16.925455 17.986652 17.31608 17.651367 17.651367 C 17.31608 17.986654 16.925455 18.25358 16.479492 18.452148 C 16.033527 18.650717 15.566406 18.75 15.078125 18.75 L 4.921875 18.75 C 4.433594 18.75 3.966471 18.650717 3.520508 18.452148 C 3.074544 18.25358 2.683919 17.986654 2.348633 17.651367 C 2.013346 17.31608 1.746419 16.925455 1.547852 16.479492 C 1.349284 16.033529 1.25 15.566406 1.25 15.078125 L 1.25 4.921875 C 1.25 4.433594 1.349284 3.966473 1.547852 3.520508 C 1.746419 3.074545 2.013346 2.68392 2.348633 2.348633 C 2.683919 2.013348 3.074544 1.74642 3.520508 1.547852 C 3.966471 1.349285 4.433594 1.25 4.921875 1.25 Z M 4.951172 2.5 C 4.625651 2.5 4.314778 2.566732 4.018555 2.700195 C 3.722331 2.83366 3.461914 3.012695 3.237305 3.237305 C 3.012695 3.461914 2.833659 3.722332 2.700195 4.018555 C 2.566732 4.314779 2.5 4.625651 2.5 4.951172 L 2.5 5 L 17.5 5 L 17.5 4.951172 C 17.5 4.625651 17.433268 4.314779 17.299805 4.018555 C 17.16634 3.722332 16.987305 3.461914 16.762695 3.237305 C 16.538086 3.012695 16.277668 2.83366 15.981445 2.700195 C 15.685221 2.566732 15.374349 2.5 15.048828 2.5 Z M 15.048828 17.5 C 15.374349 17.5 15.685221 17.433268 15.981445 17.299805 C 16.277668 17.166342 16.538086 16.987305 16.762695 16.762695 C 16.987305 16.538086 17.16634 16.27767 17.299805 15.981445 C 17.433268 15.685222 17.5 15.37435 17.5 15.048828 L 17.5 6.25 L 2.5 6.25 L 2.5 15.048828 C 2.5 15.37435 2.566732 15.685222 2.700195 15.981445 C 2.833659 16.27767 3.012695 16.538086 3.237305 16.762695 C 3.461914 16.987305 3.722331 17.166342 4.018555 17.299805 C 4.314778 17.433268 4.625651 17.5 4.951172 17.5 Z M 12.724609 8.935547 C 12.724609 9.195964 12.762044 9.446615 12.836914 9.6875 C 12.911783 9.928386 13.020832 10.151367 13.164062 10.356445 C 13.307291 10.561523 13.476562 10.742188 13.671875 10.898438 C 13.867188 11.054688 14.088541 11.178386 14.335938 11.269531 C 14.348957 11.367188 14.358723 11.466472 14.365234 11.567383 C 14.371744 11.668295 14.375 11.770834 14.375 11.875 C 14.375 11.979167 14.371744 12.081706 14.365234 12.182617 C 14.358723 12.283529 14.348957 12.382812 14.335938 12.480469 C 14.088541 12.571615 13.867188 12.695312 13.671875 12.851562 C 13.476562 13.007812 13.307291 13.188477 13.164062 13.393555 C 13.020832 13.598633 12.911783 13.821615 12.836914 14.0625 C 12.762044 14.303386 12.724609 14.554037 12.724609 14.814453 C 12.724609 14.886068 12.727864 14.960938 12.734375 15.039062 C 12.740885 15.117188 12.75065 15.192058 12.763672 15.263672 C 12.470703 15.511068 12.145182 15.712891 11.787109 15.869141 C 11.546224 15.621745 11.274414 15.43457 10.97168 15.307617 C 10.668945 15.180664 10.345052 15.117188 10 15.117188 C 9.654947 15.117188 9.331055 15.180664 9.02832 15.307617 C 8.725586 15.43457 8.453775 15.621745 8.212891 15.869141 C 7.854817 15.712891 7.529296 15.511068 7.236328 15.263672 C 7.249349 15.192058 7.259114 15.117188 7.265625 15.039062 C 7.272135 14.960938 7.275391 14.886068 7.275391 14.814453 C 7.275391 14.554037 7.237956 14.303386 7.163086 14.0625 C 7.088216 13.821615 6.979167 13.598633 6.835938 13.393555 C 6.692708 13.188477 6.52181 13.007812 6.323242 12.851562 C 6.124674 12.695312 5.904948 12.571615 5.664062 12.480469 C 5.651042 12.382812 5.641276 12.283529 5.634766 12.182617 C 5.628255 12.081706 5.625 11.979167 5.625 11.875 C 5.625 11.770834 5.628255 11.668295 5.634766 11.567383 C 5.641276 11.466472 5.651042 11.367188 5.664062 11.269531 C 5.904948 11.178386 6.124674 11.054688 6.323242 10.898438 C 6.52181 10.742188 6.692708 10.561523 6.835938 10.356445 C 6.979167 10.151367 7.088216 9.928386 7.163086 9.6875 C 7.237956 9.446615 7.275391 9.195964 7.275391 8.935547 C 7.275391 8.863933 7.272135 8.789062 7.265625 8.710938 C 7.259114 8.632812 7.249349 8.557943 7.236328 8.486328 C 7.529296 8.238933 7.854817 8.037109 8.212891 7.880859 C 8.440755 8.121745 8.712564 8.307292 9.02832 8.4375 C 9.344075 8.567709 9.667969 8.632812 10 8.632812 C 10.332031 8.632812 10.655924 8.567709 10.97168 8.4375 C 11.287435 8.307292 11.559244 8.121745 11.787109 7.880859 C 12.145182 8.037109 12.470703 8.238933 12.763672 8.486328 C 12.75065 8.557943 12.740885 8.632812 12.734375 8.710938 C 12.727864 8.789062 12.724609 8.863933 12.724609 8.935547 Z M 10.9375 11.875 C 10.9375 11.614584 10.846354 11.393229 10.664062 11.210938 C 10.481771 11.028646 10.260416 10.9375 10 10.9375 C 9.739583 10.9375 9.518229 11.028646 9.335938 11.210938 C 9.153646 11.393229 9.0625 11.614584 9.0625 11.875 C 9.0625 12.135417 9.153646 12.356771 9.335938 12.539062 C 9.518229 12.721354 9.739583 12.8125 10 12.8125 C 10.260416 12.8125 10.481771 12.721354 10.664062 12.539062 C 10.846354 12.356771 10.9375 12.135417 10.9375 11.875 Z "), - new SettingOption(Translator.SettingsPersonalization_Title, Translator.SettingsPersonalization_Description, WinoPage.PersonalizationPage,"F1 M 10 17.5 L 10 18.75 L 12.5 18.75 L 12.5 20 L 6.25 20 L 6.25 18.75 L 8.75 18.75 L 8.75 17.5 L 0 17.5 L 0 6.25 L 10 6.25 L 8.740234 7.5 L 1.25 7.5 L 1.25 16.25 L 17.5 16.25 L 17.5 8.75 L 18.75 7.5 L 18.75 17.5 Z M 5 13.75 C 5.175781 13.75 5.338542 13.717448 5.488281 13.652344 C 5.638021 13.58724 5.769856 13.497722 5.883789 13.383789 C 5.997721 13.269857 6.087239 13.138021 6.152344 12.988281 C 6.217448 12.838542 6.25 12.675781 6.25 12.5 C 6.25 12.18099 6.306966 11.878256 6.420898 11.591797 C 6.53483 11.305339 6.69108 11.051433 6.889648 10.830078 C 7.088216 10.608725 7.322591 10.424805 7.592773 10.27832 C 7.862955 10.131836 8.157552 10.042318 8.476562 10.009766 L 15.419922 3.066406 C 15.602213 2.884115 15.813802 2.744141 16.054688 2.646484 C 16.295572 2.548828 16.542969 2.5 16.796875 2.5 C 17.063801 2.5 17.31608 2.550457 17.553711 2.651367 C 17.79134 2.752279 17.998047 2.890625 18.173828 3.066406 C 18.349609 3.242188 18.487955 3.448895 18.588867 3.686523 C 18.689777 3.924154 18.740234 4.179688 18.740234 4.453125 C 18.740234 4.707031 18.691406 4.954428 18.59375 5.195312 C 18.496094 5.436199 18.356119 5.647787 18.173828 5.830078 L 11.230469 12.773438 C 11.197916 13.085938 11.110025 13.378906 10.966797 13.652344 C 10.823567 13.925781 10.639648 14.161784 10.415039 14.360352 C 10.19043 14.55892 9.936523 14.71517 9.65332 14.829102 C 9.370117 14.943034 9.06901 15 8.75 15 L 2.5 15 L 2.5 13.75 Z M 16.796875 3.75 C 16.608072 3.75 16.445312 3.818359 16.308594 3.955078 L 11.962891 8.300781 C 12.333984 8.58724 12.66276 8.916016 12.949219 9.287109 L 17.294922 4.941406 C 17.431641 4.804688 17.5 4.641928 17.5 4.453125 C 17.5 4.257812 17.430012 4.091797 17.290039 3.955078 C 17.150064 3.818359 16.985676 3.75 16.796875 3.75 Z M 10.175781 10.087891 C 10.572916 10.348308 10.901692 10.677084 11.162109 11.074219 L 12.060547 10.185547 C 11.787109 9.794922 11.455078 9.462891 11.064453 9.189453 Z M 10 12.5 C 10 12.324219 9.967447 12.161459 9.902344 12.011719 C 9.837239 11.861979 9.747721 11.730144 9.633789 11.616211 C 9.519856 11.502279 9.388021 11.412761 9.238281 11.347656 C 9.088541 11.282553 8.925781 11.25 8.75 11.25 C 8.574219 11.25 8.411458 11.282553 8.261719 11.347656 C 8.111979 11.412761 7.980143 11.502279 7.866211 11.616211 C 7.752278 11.730144 7.66276 11.861979 7.597656 12.011719 C 7.532552 12.161459 7.5 12.324219 7.5 12.5 C 7.5 12.942709 7.389322 13.359375 7.167969 13.75 L 8.75 13.75 C 8.919271 13.75 9.080403 13.717448 9.233398 13.652344 C 9.386393 13.58724 9.519856 13.497722 9.633789 13.383789 C 9.747721 13.269857 9.837239 13.136394 9.902344 12.983398 C 9.967447 12.830404 10 12.669271 10 12.5 Z "), - new SettingOption(Translator.SettingsCalendarSettings_Title, Translator.SettingsCalendarSettings_Description, WinoPage.CalendarSettingsPage,"F1 M 1.25 3.75 C 1.25 3.404949 1.315104 3.081055 1.445312 2.77832 C 1.575521 2.475586 1.754557 2.210287 1.982422 1.982422 C 2.210286 1.754559 2.475586 1.575521 2.77832 1.445312 C 3.081055 1.315105 3.404948 1.25 3.75 1.25 L 16.25 1.25 C 16.595051 1.25 16.918945 1.315105 17.22168 1.445312 C 17.524414 1.575521 17.789713 1.754559 18.017578 1.982422 C 18.245441 2.210287 18.424479 2.475586 18.554688 2.77832 C 18.684895 3.081055 18.75 3.404949 18.75 3.75 L 18.75 5 L 1.25 5 Z M 3.75 18.75 C 3.404948 18.75 3.081055 18.684896 2.77832 18.554688 C 2.475586 18.424479 2.210286 18.245443 1.982422 18.017578 C 1.754557 17.789713 1.575521 17.524414 1.445312 17.22168 C 1.315104 16.918945 1.25 16.595053 1.25 16.25 L 1.25 6.25 L 18.75 6.25 L 18.75 16.25 C 18.75 16.595053 18.684895 16.918945 18.554688 17.22168 C 18.424479 17.524414 18.245441 17.789713 18.017578 18.017578 C 17.789713 18.245443 17.524414 18.424479 17.22168 18.554688 C 16.918945 18.684896 16.595051 18.75 16.25 18.75 Z M 7.65625 9.990234 C 7.65625 9.794922 7.618815 9.612631 7.543945 9.443359 C 7.469075 9.274089 7.368164 9.125977 7.241211 8.999023 C 7.114258 8.87207 6.966146 8.772787 6.796875 8.701172 C 6.627604 8.629558 6.445312 8.59375 6.25 8.59375 C 6.054688 8.59375 5.870768 8.631186 5.698242 8.706055 C 5.525716 8.780925 5.375977 8.881836 5.249023 9.008789 C 5.12207 9.135742 5.022786 9.283854 4.951172 9.453125 C 4.879557 9.622396 4.84375 9.804688 4.84375 10 C 4.84375 10.195312 4.879557 10.377604 4.951172 10.546875 C 5.022786 10.716146 5.12207 10.864258 5.249023 10.991211 C 5.375977 11.118164 5.524088 11.219076 5.693359 11.293945 C 5.86263 11.368815 6.044922 11.40625 6.240234 11.40625 C 6.442057 11.40625 6.629231 11.370443 6.801758 11.298828 C 6.974283 11.227214 7.124023 11.12793 7.250977 11.000977 C 7.377929 10.874023 7.477213 10.724284 7.548828 10.551758 C 7.620442 10.379232 7.65625 10.192058 7.65625 9.990234 Z M 11.40625 9.990234 C 11.40625 9.794922 11.368814 9.612631 11.293945 9.443359 C 11.219075 9.274089 11.118164 9.125977 10.991211 8.999023 C 10.864258 8.87207 10.716146 8.772787 10.546875 8.701172 C 10.377604 8.629558 10.195312 8.59375 10 8.59375 C 9.804688 8.59375 9.620768 8.631186 9.448242 8.706055 C 9.275716 8.780925 9.125977 8.881836 8.999023 9.008789 C 8.87207 9.135742 8.772786 9.283854 8.701172 9.453125 C 8.629557 9.622396 8.59375 9.804688 8.59375 10 C 8.59375 10.195312 8.629557 10.377604 8.701172 10.546875 C 8.772786 10.716146 8.87207 10.864258 8.999023 10.991211 C 9.125977 11.118164 9.274088 11.219076 9.443359 11.293945 C 9.61263 11.368815 9.794922 11.40625 9.990234 11.40625 C 10.192057 11.40625 10.379231 11.370443 10.551758 11.298828 C 10.724283 11.227214 10.874023 11.12793 11.000977 11.000977 C 11.12793 10.874023 11.227213 10.724284 11.298828 10.551758 C 11.370442 10.379232 11.40625 10.192058 11.40625 9.990234 Z M 15.15625 9.990234 C 15.15625 9.794922 15.118814 9.612631 15.043945 9.443359 C 14.969075 9.274089 14.868164 9.125977 14.741211 8.999023 C 14.614258 8.87207 14.466146 8.772787 14.296875 8.701172 C 14.127604 8.629558 13.945312 8.59375 13.75 8.59375 C 13.554688 8.59375 13.370768 8.631186 13.198242 8.706055 C 13.025716 8.780925 12.875977 8.881836 12.749023 9.008789 C 12.62207 9.135742 12.522786 9.283854 12.451172 9.453125 C 12.379557 9.622396 12.34375 9.804688 12.34375 10 C 12.34375 10.195312 12.379557 10.377604 12.451172 10.546875 C 12.522786 10.716146 12.62207 10.864258 12.749023 10.991211 C 12.875977 11.118164 13.024088 11.219076 13.193359 11.293945 C 13.362629 11.368815 13.544921 11.40625 13.740234 11.40625 C 13.942057 11.40625 14.129231 11.370443 14.301758 11.298828 C 14.474283 11.227214 14.624022 11.12793 14.750977 11.000977 C 14.877929 10.874023 14.977213 10.724284 15.048828 10.551758 C 15.120442 10.379232 15.15625 10.192058 15.15625 9.990234 Z M 7.65625 13.740234 C 7.65625 13.544922 7.618815 13.362631 7.543945 13.193359 C 7.469075 13.024089 7.368164 12.875977 7.241211 12.749023 C 7.114258 12.62207 6.966146 12.522787 6.796875 12.451172 C 6.627604 12.379558 6.445312 12.34375 6.25 12.34375 C 6.054688 12.34375 5.870768 12.381186 5.698242 12.456055 C 5.525716 12.530925 5.375977 12.631836 5.249023 12.758789 C 5.12207 12.885742 5.022786 13.033854 4.951172 13.203125 C 4.879557 13.372396 4.84375 13.554688 4.84375 13.75 C 4.84375 13.945312 4.879557 14.127604 4.951172 14.296875 C 5.022786 14.466146 5.12207 14.614258 5.249023 14.741211 C 5.375977 14.868164 5.524088 14.969076 5.693359 15.043945 C 5.86263 15.118815 6.044922 15.15625 6.240234 15.15625 C 6.442057 15.15625 6.629231 15.120443 6.801758 15.048828 C 6.974283 14.977214 7.124023 14.87793 7.250977 14.750977 C 7.377929 14.624023 7.477213 14.474284 7.548828 14.301758 C 7.620442 14.129232 7.65625 13.942058 7.65625 13.740234 Z M 11.40625 13.740234 C 11.40625 13.544922 11.368814 13.362631 11.293945 13.193359 C 11.219075 13.024089 11.118164 12.875977 10.991211 12.749023 C 10.864258 12.62207 10.716146 12.522787 10.546875 12.451172 C 10.377604 12.379558 10.195312 12.34375 10 12.34375 C 9.804688 12.34375 9.620768 12.381186 9.448242 12.456055 C 9.275716 12.530925 9.125977 12.631836 8.999023 12.758789 C 8.87207 12.885742 8.772786 13.033854 8.701172 13.203125 C 8.629557 13.372396 8.59375 13.554688 8.59375 13.75 C 8.59375 13.945312 8.629557 14.127604 8.701172 14.296875 C 8.772786 14.466146 8.87207 14.614258 8.999023 14.741211 C 9.125977 14.868164 9.274088 14.969076 9.443359 15.043945 C 9.61263 15.118815 9.794922 15.15625 9.990234 15.15625 C 10.192057 15.15625 10.379231 15.120443 10.551758 15.048828 C 10.724283 14.977214 10.874023 14.87793 11.000977 14.750977 C 11.12793 14.624023 11.227213 14.474284 11.298828 14.301758 C 11.370442 14.129232 11.40625 13.942058 11.40625 13.740234 Z "), - new SettingOption(Translator.SettingsAbout_Title, Translator.SettingsAbout_Description, WinoPage.AboutPage,"F1 M 9.375 18.75 C 8.509114 18.75 7.677409 18.639322 6.879883 18.417969 C 6.082356 18.196615 5.335286 17.882486 4.638672 17.475586 C 3.942057 17.068686 3.308919 16.580404 2.739258 16.010742 C 2.169596 15.441081 1.681315 14.807943 1.274414 14.111328 C 0.867513 13.414714 0.553385 12.667644 0.332031 11.870117 C 0.110677 11.072592 0 10.240886 0 9.375 C 0 8.509115 0.110677 7.677409 0.332031 6.879883 C 0.553385 6.082357 0.867513 5.335287 1.274414 4.638672 C 1.681315 3.942059 2.169596 3.30892 2.739258 2.739258 C 3.308919 2.169598 3.942057 1.681316 4.638672 1.274414 C 5.335286 0.867514 6.082356 0.553387 6.879883 0.332031 C 7.677409 0.110678 8.509114 0 9.375 0 C 10.240885 0 11.072591 0.110678 11.870117 0.332031 C 12.667643 0.553387 13.414713 0.867514 14.111328 1.274414 C 14.807942 1.681316 15.44108 2.169598 16.010742 2.739258 C 16.580402 3.30892 17.068684 3.942059 17.475586 4.638672 C 17.882486 5.335287 18.196613 6.082357 18.417969 6.879883 C 18.639322 7.677409 18.75 8.509115 18.75 9.375 C 18.75 10.240886 18.639322 11.072592 18.417969 11.870117 C 18.196613 12.667644 17.882486 13.414714 17.475586 14.111328 C 17.068684 14.807943 16.580402 15.441081 16.010742 16.010742 C 15.44108 16.580404 14.807942 17.068686 14.111328 17.475586 C 13.414713 17.882486 12.667643 18.196615 11.870117 18.417969 C 11.072591 18.639322 10.240885 18.75 9.375 18.75 Z M 9.375 1.25 C 8.626302 1.25 7.906901 1.347656 7.216797 1.542969 C 6.526692 1.738281 5.880533 2.011719 5.27832 2.363281 C 4.676106 2.714844 4.127604 3.138021 3.632812 3.632812 C 3.138021 4.127604 2.714844 4.676107 2.363281 5.27832 C 2.011719 5.880534 1.738281 6.52832 1.542969 7.22168 C 1.347656 7.915039 1.25 8.632812 1.25 9.375 C 1.25 10.117188 1.347656 10.834961 1.542969 11.52832 C 1.738281 12.22168 2.011719 12.869467 2.363281 13.47168 C 2.714844 14.073894 3.138021 14.622396 3.632812 15.117188 C 4.127604 15.611979 4.676106 16.035156 5.27832 16.386719 C 5.880533 16.738281 6.526692 17.011719 7.216797 17.207031 C 7.906901 17.402344 8.626302 17.5 9.375 17.5 C 10.117188 17.5 10.834961 17.402344 11.52832 17.207031 C 12.221679 17.011719 12.869465 16.738281 13.47168 16.386719 C 14.073893 16.035156 14.622396 15.611979 15.117188 15.117188 C 15.611979 14.622396 16.035156 14.073894 16.386719 13.47168 C 16.738281 12.869467 17.011719 12.223308 17.207031 11.533203 C 17.402344 10.8431 17.5 10.123698 17.5 9.375 C 17.5 8.632812 17.402344 7.915039 17.207031 7.22168 C 17.011719 6.52832 16.738281 5.880534 16.386719 5.27832 C 16.035156 4.676107 15.611979 4.127604 15.117188 3.632812 C 14.622396 3.138021 14.073893 2.714844 13.47168 2.363281 C 12.869465 2.011719 12.221679 1.738281 11.52832 1.542969 C 10.834961 1.347656 10.117188 1.25 9.375 1.25 Z M 8.75 7.5 L 10 7.5 L 10 13.75 L 8.75 13.75 Z M 8.75 5 L 10 5 L 10 6.25 L 8.75 6.25 Z "), - }; -} diff --git a/Wino.Calendar/Styles/DayHeaderControl.xaml b/Wino.Calendar/Styles/DayHeaderControl.xaml deleted file mode 100644 index 396a0cf0..00000000 --- a/Wino.Calendar/Styles/DayHeaderControl.xaml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - diff --git a/Wino.Calendar/Styles/WinoCalendarResources.xaml b/Wino.Calendar/Styles/WinoCalendarResources.xaml deleted file mode 100644 index c0469ff1..00000000 --- a/Wino.Calendar/Styles/WinoCalendarResources.xaml +++ /dev/null @@ -1,424 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Wino.Calendar/Styles/WinoCalendarResources.xaml.cs b/Wino.Calendar/Styles/WinoCalendarResources.xaml.cs deleted file mode 100644 index fd055e7b..00000000 --- a/Wino.Calendar/Styles/WinoCalendarResources.xaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Windows.UI.Xaml; - -namespace Wino.Calendar.Styles; - -public sealed partial class WinoCalendarResources : ResourceDictionary -{ - public WinoCalendarResources() - { - this.InitializeComponent(); - } -} diff --git a/Wino.Calendar/Styles/WinoCalendarView.xaml b/Wino.Calendar/Styles/WinoCalendarView.xaml deleted file mode 100644 index 8259f570..00000000 --- a/Wino.Calendar/Styles/WinoCalendarView.xaml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/Wino.Calendar/Styles/WinoDayTimelineCanvas.xaml b/Wino.Calendar/Styles/WinoDayTimelineCanvas.xaml deleted file mode 100644 index 96257f2f..00000000 --- a/Wino.Calendar/Styles/WinoDayTimelineCanvas.xaml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - diff --git a/Wino.Calendar/Views/Abstract/AccountDetailsPageAbstract.cs b/Wino.Calendar/Views/Abstract/AccountDetailsPageAbstract.cs deleted file mode 100644 index de113056..00000000 --- a/Wino.Calendar/Views/Abstract/AccountDetailsPageAbstract.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Wino.Calendar.ViewModels; -using Wino.Core.UWP; - -namespace Wino.Calendar.Views.Abstract; - -public abstract class AccountDetailsPageAbstract : BasePage { } diff --git a/Wino.Calendar/Views/Abstract/AccountManagementPageAbstract.cs b/Wino.Calendar/Views/Abstract/AccountManagementPageAbstract.cs deleted file mode 100644 index e7edd065..00000000 --- a/Wino.Calendar/Views/Abstract/AccountManagementPageAbstract.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Wino.Calendar.ViewModels; -using Wino.Core.UWP; - -namespace Wino.Calendar.Views.Abstract; - -public partial class AccountManagementPageAbstract : BasePage { } diff --git a/Wino.Calendar/Views/Abstract/AppShellAbstract.cs b/Wino.Calendar/Views/Abstract/AppShellAbstract.cs deleted file mode 100644 index 8ab81b95..00000000 --- a/Wino.Calendar/Views/Abstract/AppShellAbstract.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Wino.Calendar.ViewModels; -using Wino.Core.UWP; - -namespace Wino.Calendar.Views.Abstract; - -public abstract class AppShellAbstract : BasePage { } diff --git a/Wino.Calendar/Views/Abstract/CalendarPageAbstract.cs b/Wino.Calendar/Views/Abstract/CalendarPageAbstract.cs deleted file mode 100644 index abd1fb5f..00000000 --- a/Wino.Calendar/Views/Abstract/CalendarPageAbstract.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Wino.Calendar.ViewModels; -using Wino.Core.UWP; - -namespace Wino.Calendar.Views.Abstract; - -public abstract class CalendarPageAbstract : BasePage { } diff --git a/Wino.Calendar/Views/Abstract/CalendarSettingsPageAbstract.cs b/Wino.Calendar/Views/Abstract/CalendarSettingsPageAbstract.cs deleted file mode 100644 index 47eef1d4..00000000 --- a/Wino.Calendar/Views/Abstract/CalendarSettingsPageAbstract.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Wino.Calendar.ViewModels; -using Wino.Core.UWP; - -namespace Wino.Calendar.Views.Abstract; - -public abstract class CalendarSettingsPageAbstract : BasePage { } diff --git a/Wino.Calendar/Views/Abstract/PersonalizationPageAbstract.cs b/Wino.Calendar/Views/Abstract/PersonalizationPageAbstract.cs deleted file mode 100644 index b970f09a..00000000 --- a/Wino.Calendar/Views/Abstract/PersonalizationPageAbstract.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Wino.Core.UWP; -using Wino.Core.ViewModels; - -namespace Wino.Calendar.Views.Abstract; - -public partial class PersonalizationPageAbstract : BasePage { } diff --git a/Wino.Calendar/Views/Account/AccountManagementPage.xaml b/Wino.Calendar/Views/Account/AccountManagementPage.xaml deleted file mode 100644 index 636a25df..00000000 --- a/Wino.Calendar/Views/Account/AccountManagementPage.xaml +++ /dev/null @@ -1,179 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Wino.Calendar/Views/Account/AccountManagementPage.xaml.cs b/Wino.Calendar/Views/Account/AccountManagementPage.xaml.cs deleted file mode 100644 index 4e63cca5..00000000 --- a/Wino.Calendar/Views/Account/AccountManagementPage.xaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Wino.Calendar.Views.Abstract; - -namespace Wino.Calendar.Views.Account; - -public sealed partial class AccountManagementPage : AccountManagementPageAbstract -{ - public AccountManagementPage() - { - InitializeComponent(); - } -} diff --git a/Wino.Calendar/Views/AppShell.xaml b/Wino.Calendar/Views/AppShell.xaml deleted file mode 100644 index a8b31d8f..00000000 --- a/Wino.Calendar/Views/AppShell.xaml +++ /dev/null @@ -1,405 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Wino.Calendar/Views/Settings/AccountDetailsPage.xaml.cs b/Wino.Calendar/Views/Settings/AccountDetailsPage.xaml.cs deleted file mode 100644 index 0cb918b0..00000000 --- a/Wino.Calendar/Views/Settings/AccountDetailsPage.xaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Wino.Calendar.Views.Abstract; - -namespace Wino.Calendar.Views.Settings; - -public sealed partial class AccountDetailsPage : AccountDetailsPageAbstract -{ - public AccountDetailsPage() - { - this.InitializeComponent(); - } -} diff --git a/Wino.Calendar/Views/Settings/CalendarSettingsPage.xaml b/Wino.Calendar/Views/Settings/CalendarSettingsPage.xaml deleted file mode 100644 index 7254969f..00000000 --- a/Wino.Calendar/Views/Settings/CalendarSettingsPage.xaml +++ /dev/null @@ -1,200 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Wino.Calendar/Views/Settings/CalendarSettingsPage.xaml.cs b/Wino.Calendar/Views/Settings/CalendarSettingsPage.xaml.cs deleted file mode 100644 index 1114ea5d..00000000 --- a/Wino.Calendar/Views/Settings/CalendarSettingsPage.xaml.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Wino.Calendar.Views.Abstract; - - -namespace Wino.Calendar.Views.Settings; - -public sealed partial class CalendarSettingsPage : CalendarSettingsPageAbstract -{ - public CalendarSettingsPage() - { - InitializeComponent(); - } -} diff --git a/Wino.Calendar/Views/Settings/PersonalizationPage.xaml.cs b/Wino.Calendar/Views/Settings/PersonalizationPage.xaml.cs deleted file mode 100644 index a9518467..00000000 --- a/Wino.Calendar/Views/Settings/PersonalizationPage.xaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Wino.Calendar.Views.Abstract; - -namespace Wino.Calendar.Views.Settings; - -public sealed partial class PersonalizationPage : PersonalizationPageAbstract -{ - public PersonalizationPage() - { - this.InitializeComponent(); - } -} diff --git a/Wino.Calendar/Wino.Calendar.csproj b/Wino.Calendar/Wino.Calendar.csproj deleted file mode 100644 index 97d6df07..00000000 --- a/Wino.Calendar/Wino.Calendar.csproj +++ /dev/null @@ -1,98 +0,0 @@ - - - WinExe - net9.0-windows10.0.26100.0 - 10.0.18362.0 - true - x86;x64;arm64 - win-x86;win-x64;win-arm64 - en-US - - win-$(Platform).pubxml - true - true - True - True - SHA256 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Wino.Core.Domain/AppUrls.cs b/Wino.Core.Domain/AppUrls.cs new file mode 100644 index 00000000..72740d3f --- /dev/null +++ b/Wino.Core.Domain/AppUrls.cs @@ -0,0 +1,10 @@ +namespace Wino.Core.Domain; + +public static class AppUrls +{ + public const string Website = "https://www.winomail.app"; + public const string Discord = "https://discord.gg/windows-apps-hub-714581497222398064"; + public const string GitHub = "https://github.com/bkaankose/Wino-Mail/"; + public const string PrivacyPolicy = "https://www.winomail.app/support/privacy"; + public const string Paypal = "https://paypal.me/bkaankose?country.x=PL&locale.x=en_US"; +} diff --git a/Wino.Core.Domain/BasicTypesJsonContext.cs b/Wino.Core.Domain/BasicTypesJsonContext.cs index b9dba02d..f6085c1c 100644 --- a/Wino.Core.Domain/BasicTypesJsonContext.cs +++ b/Wino.Core.Domain/BasicTypesJsonContext.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Text.Json.Serialization; +using Wino.Core.Domain.Models.Updates; namespace Wino.Core.Domain; @@ -8,4 +9,6 @@ namespace Wino.Core.Domain; [JsonSerializable(typeof(int))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(bool))] +[JsonSerializable(typeof(UpdateNotes))] +[JsonSerializable(typeof(List))] public partial class BasicTypesJsonContext : JsonSerializerContext; diff --git a/Wino.Core.Domain/CalendarRecurrenceSummaryFormatter.cs b/Wino.Core.Domain/CalendarRecurrenceSummaryFormatter.cs new file mode 100644 index 00000000..3092b0f8 --- /dev/null +++ b/Wino.Core.Domain/CalendarRecurrenceSummaryFormatter.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models.Calendar; + +namespace Wino.Core.Domain; + +public static class CalendarRecurrenceSummaryFormatter +{ + private static readonly DayOfWeek[] OrderedDays = + [ + DayOfWeek.Monday, + DayOfWeek.Tuesday, + DayOfWeek.Wednesday, + DayOfWeek.Thursday, + DayOfWeek.Friday, + DayOfWeek.Saturday, + DayOfWeek.Sunday + ]; + + public static string BuildSummary( + bool isRecurring, + DateTimeOffset effectiveStart, + DateTimeOffset effectiveEnd, + bool isAllDay, + CalendarSettings settings, + int interval, + CalendarItemRecurrenceFrequency frequency, + IReadOnlyCollection daysOfWeek, + DateTimeOffset? recurrenceEndDate) + { + var culture = settings?.CultureInfo ?? CultureInfo.CurrentCulture; + var timeSummary = isAllDay + ? Translator.CalendarItemAllDay + : string.Format( + culture, + Translator.CalendarEventCompose_TimeRangeSummary, + effectiveStart.ToString(settings?.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour ? "HH:mm" : "h:mm tt", culture), + effectiveEnd.ToString(settings?.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour ? "HH:mm" : "h:mm tt", culture)); + + if (!isRecurring) + { + return string.Format( + culture, + Translator.CalendarEventCompose_SingleOccurrenceSummary, + effectiveStart.ToString("dddd yyyy-MM-dd", culture), + timeSummary); + } + + var normalizedDays = NormalizeDays(daysOfWeek); + var isEveryDay = (frequency == CalendarItemRecurrenceFrequency.Daily && interval == 1) || + (frequency == CalendarItemRecurrenceFrequency.Weekly && interval == 1 && normalizedDays.Count == 7); + + var cadenceSummary = isEveryDay + ? $"{Translator.CalendarEventCompose_Every} {Translator.CalendarEventCompose_FrequencyDay}" + : interval == 1 + ? $"{Translator.CalendarEventCompose_Every} {GetSingularFrequencyLabel(frequency)}" + : $"{Translator.CalendarEventCompose_Every} {interval.ToString(culture)} {GetPluralFrequencyLabel(frequency)}"; + + var weekdaySummary = string.Empty; + if (frequency == CalendarItemRecurrenceFrequency.Weekly && normalizedDays.Count > 0 && normalizedDays.Count < 7) + { + weekdaySummary = string.Format( + culture, + Translator.CalendarEventCompose_WeekdaySummary, + string.Join(", ", normalizedDays.Select(day => culture.DateTimeFormat.GetDayName(day)))); + } + + var untilSummary = recurrenceEndDate.HasValue + ? string.Format( + culture, + Translator.CalendarEventCompose_UntilSummary, + recurrenceEndDate.Value.ToString("ddd yyyy-MM-dd", culture)) + : string.Empty; + + return string.Format( + culture, + Translator.CalendarEventCompose_RecurringSummarySmart, + cadenceSummary, + weekdaySummary, + timeSummary, + effectiveStart.ToString("dddd yyyy-MM-dd", culture), + untilSummary).Trim(); + } + + private static IReadOnlyList NormalizeDays(IReadOnlyCollection daysOfWeek) + { + if (daysOfWeek == null || daysOfWeek.Count == 0) + { + return []; + } + + return daysOfWeek + .Distinct() + .OrderBy(day => Array.IndexOf(OrderedDays, day)) + .ToList(); + } + + private static string GetSingularFrequencyLabel(CalendarItemRecurrenceFrequency frequency) + { + return frequency switch + { + CalendarItemRecurrenceFrequency.Daily => Translator.CalendarEventCompose_FrequencyDay, + CalendarItemRecurrenceFrequency.Weekly => Translator.CalendarEventCompose_FrequencyWeek, + CalendarItemRecurrenceFrequency.Monthly => Translator.CalendarEventCompose_FrequencyMonth, + CalendarItemRecurrenceFrequency.Yearly => Translator.CalendarEventCompose_FrequencyYear, + _ => Translator.CalendarEventCompose_FrequencyWeek + }; + } + + private static string GetPluralFrequencyLabel(CalendarItemRecurrenceFrequency frequency) + { + return frequency switch + { + CalendarItemRecurrenceFrequency.Daily => Translator.CalendarEventCompose_FrequencyDayPlural, + CalendarItemRecurrenceFrequency.Weekly => Translator.CalendarEventCompose_FrequencyWeekPlural, + CalendarItemRecurrenceFrequency.Monthly => Translator.CalendarEventCompose_FrequencyMonthPlural, + CalendarItemRecurrenceFrequency.Yearly => Translator.CalendarEventCompose_FrequencyYearPlural, + _ => Translator.CalendarEventCompose_FrequencyWeekPlural + }; + } +} + + diff --git a/Wino.Core.Domain/CalendarReminderSnoozeOptions.cs b/Wino.Core.Domain/CalendarReminderSnoozeOptions.cs new file mode 100644 index 00000000..5786c42f --- /dev/null +++ b/Wino.Core.Domain/CalendarReminderSnoozeOptions.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Wino.Core.Domain; + +public static class CalendarReminderSnoozeOptions +{ + private static readonly int[] SupportedSnoozeMinutes = [5, 10, 15, 30]; + + public static IReadOnlyList GetSupportedSnoozeMinutes() + => SupportedSnoozeMinutes; + + public static IReadOnlyList GetAllowedSnoozeMinutes(long reminderDurationInSeconds, long defaultReminderDurationInSeconds) + { + var reminderMinutes = (int)Math.Max(0, reminderDurationInSeconds / 60); + + if (reminderMinutes <= 0) + return []; + + var maxSnoozeMinutes = reminderMinutes; + var defaultReminderMinutes = (int)Math.Max(0, defaultReminderDurationInSeconds / 60); + + if (defaultReminderMinutes > 0) + maxSnoozeMinutes = Math.Min(maxSnoozeMinutes, defaultReminderMinutes); + + return SupportedSnoozeMinutes.Where(minutes => minutes <= maxSnoozeMinutes).ToArray(); + } +} diff --git a/Wino.Core.Domain/Collections/CalendarEventCollection.cs b/Wino.Core.Domain/Collections/CalendarEventCollection.cs index 94a1eef6..1b070837 100644 --- a/Wino.Core.Domain/Collections/CalendarEventCollection.cs +++ b/Wino.Core.Domain/Collections/CalendarEventCollection.cs @@ -13,6 +13,7 @@ public class CalendarEventCollection { public event EventHandler CalendarItemAdded; public event EventHandler CalendarItemRemoved; + public event EventHandler CalendarItemUpdated; public event EventHandler CalendarItemsCleared; @@ -114,11 +115,27 @@ public class CalendarEventCollection } } + public void RemoveCalendarItems(Func predicate) + { + if (predicate == null) return; + + var itemsToRemove = _allItems.Where(predicate).ToList(); + + foreach (var item in itemsToRemove) + { + RemoveCalendarItem(item); + } + } + private void AddCalendarItemInternal(ObservableRangeCollection collection, ICalendarItem calendarItem, bool create = true) { - if (calendarItem is not ICalendarItemViewModel) + if (calendarItem is not ICalendarItemViewModel viewModel) throw new ArgumentException("CalendarItem must be of type ICalendarItemViewModel", nameof(calendarItem)); + // Set the displaying context for proper title calculation + viewModel.DisplayingPeriod = Period; + viewModel.CalendarSettings = Settings; + collection.Add(calendarItem); if (create) @@ -144,6 +161,53 @@ public class CalendarEventCollection CalendarItemRemoved?.Invoke(this, calendarItem); } + /// + /// Updates an existing calendar item in-place. If the item's type changed (all-day vs regular), + /// it will be moved to the appropriate collection. + /// + /// The updated calendar item data. + /// True if the item was found and updated; false otherwise. + public bool UpdateCalendarItem(CalendarItem calendarItem) + { + var existingItem = _allItems.FirstOrDefault(x => x.Id == calendarItem.Id); + if (existingItem == null) + return false; + + // Get the collections this item is currently in (before update) + var oldCollections = GetProperCollectionsForCalendarItem(existingItem).ToList(); + + // Update the underlying data + if (existingItem is ICalendarItemViewModel viewModel) + { + viewModel.UpdateFrom(calendarItem); + } + + // Get the collections this item should be in (after update) + var newCollections = GetProperCollectionsForCalendarItem(existingItem).ToList(); + + // Check if the collections changed + var collectionsToRemoveFrom = oldCollections.Except(newCollections).ToList(); + var collectionsToAddTo = newCollections.Except(oldCollections).ToList(); + + // Remove from old collections that are no longer applicable + foreach (var collection in collectionsToRemoveFrom) + { + collection.Remove(existingItem); + } + + // Add to new collections that are now applicable + foreach (var collection in collectionsToAddTo) + { + if (!collection.Contains(existingItem)) + { + collection.Add(existingItem); + } + } + + CalendarItemUpdated?.Invoke(this, existingItem); + return true; + } + public void Clear() { _internalAllDayEvents.Clear(); diff --git a/Wino.Core.Domain/Collections/DayRangeCollection.cs b/Wino.Core.Domain/Collections/DayRangeCollection.cs deleted file mode 100644 index 89afeb68..00000000 --- a/Wino.Core.Domain/Collections/DayRangeCollection.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Linq; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Calendar; - -namespace Wino.Core.Domain.Collections; - -public class DayRangeCollection : ObservableRangeCollection -{ - /// - /// Gets the range of dates that are currently displayed in the collection. - /// - public DateRange DisplayRange - { - get - { - if (Count == 0) return null; - - var minimumLoadedDate = this[0].CalendarRenderOptions.DateRange.StartDate; - var maximumLoadedDate = this[Count - 1].CalendarRenderOptions.DateRange.EndDate; - - return new DateRange(minimumLoadedDate, maximumLoadedDate); - } - } - - public void RemoveCalendarItem(ICalendarItem calendarItem) - { - foreach (var dayRange in this) - { - - } - } - - public void AddCalendarItem(ICalendarItem calendarItem) - { - foreach (var dayRange in this) - { - var calendarDayModel = dayRange.CalendarDays.FirstOrDefault(x => x.Period.HasInside(calendarItem.Period.Start)); - calendarDayModel?.EventsCollection.AddCalendarItem(calendarItem); - } - } -} diff --git a/Wino.Core.Domain/Constants.cs b/Wino.Core.Domain/Constants.cs index 0406edcd..cded77eb 100644 --- a/Wino.Core.Domain/Constants.cs +++ b/Wino.Core.Domain/Constants.cs @@ -1,4 +1,4 @@ -namespace Wino.Core.Domain; +namespace Wino.Core.Domain; public static class Constants { @@ -12,7 +12,17 @@ public static class Constants public const string ToastMailUniqueIdKey = nameof(ToastMailUniqueIdKey); public const string ToastActionKey = nameof(ToastActionKey); - + public const string ToastMailAccountIdKey = nameof(ToastMailAccountIdKey); + public const string ToastCalendarItemIdKey = nameof(ToastCalendarItemIdKey); + public const string ToastCalendarActionKey = nameof(ToastCalendarActionKey); + public const string ToastCalendarNavigateAction = nameof(ToastCalendarNavigateAction); + public const string ToastCalendarSnoozeAction = nameof(ToastCalendarSnoozeAction); + public const string ToastCalendarSnoozeDurationInputId = nameof(ToastCalendarSnoozeDurationInputId); + public const string ToastModeKey = nameof(ToastModeKey); + public const string ToastModeMail = nameof(ToastModeMail); + public const string ToastModeCalendar = nameof(ToastModeCalendar); + public const string ToastStoreUpdateActionKey = nameof(ToastStoreUpdateActionKey); + public const string ToastStoreUpdateActionInstall = nameof(ToastStoreUpdateActionInstall); public const string ClientLogFile = "Client_.log"; public const string ServerLogFile = "Server_.log"; public const string LogArchiveFileName = "WinoLogs.zip"; @@ -20,3 +30,4 @@ public static class Constants public const string WinoMailIdentiifer = nameof(WinoMailIdentiifer); public const string WinoCalendarIdentifier = nameof(WinoCalendarIdentifier); } + diff --git a/Wino.Core.Domain/Entities/Calendar/AccountCalendar.cs b/Wino.Core.Domain/Entities/Calendar/AccountCalendar.cs index 29a200cf..e3fe9ba6 100644 --- a/Wino.Core.Domain/Entities/Calendar/AccountCalendar.cs +++ b/Wino.Core.Domain/Entities/Calendar/AccountCalendar.cs @@ -1,9 +1,12 @@ using System; using SQLite; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; namespace Wino.Core.Domain.Entities.Calendar; +[Preserve] public class AccountCalendar : IAccountCalendar { [PrimaryKey] @@ -13,7 +16,9 @@ public class AccountCalendar : IAccountCalendar public string SynchronizationDeltaToken { get; set; } public string Name { get; set; } public bool IsPrimary { get; set; } + public bool IsSynchronizationEnabled { get; set; } = true; public bool IsExtended { get; set; } = true; + public CalendarItemShowAs DefaultShowAs { get; set; } = CalendarItemShowAs.Busy; /// /// Unused for now. @@ -21,4 +26,7 @@ public class AccountCalendar : IAccountCalendar public string TextColorHex { get; set; } public string BackgroundColorHex { get; set; } public string TimeZone { get; set; } + + [Ignore] + public MailAccount MailAccount { get; set; } } diff --git a/Wino.Core.Domain/Entities/Calendar/CalendarAttachment.cs b/Wino.Core.Domain/Entities/Calendar/CalendarAttachment.cs new file mode 100644 index 00000000..477acd38 --- /dev/null +++ b/Wino.Core.Domain/Entities/Calendar/CalendarAttachment.cs @@ -0,0 +1,55 @@ +using System; +using SQLite; +using Wino.Core.Domain.Enums; + +namespace Wino.Core.Domain.Entities.Calendar; + +/// +/// Represents metadata for calendar event attachments. +/// Actual file content is downloaded on-demand. +/// +public class CalendarAttachment +{ + [PrimaryKey] + public Guid Id { get; set; } + + /// + /// The calendar item this attachment belongs to. + /// + public Guid CalendarItemId { get; set; } + + /// + /// Remote identifier for the attachment from the provider (Outlook, Gmail, etc.). + /// + public string RemoteAttachmentId { get; set; } + + /// + /// File name of the attachment. + /// + public string FileName { get; set; } + + /// + /// Size of the attachment in bytes. + /// + public long Size { get; set; } + + /// + /// MIME content type (e.g., "application/pdf", "image/png"). + /// + public string ContentType { get; set; } + + /// + /// Whether the attachment has been downloaded to local storage. + /// + public bool IsDownloaded { get; set; } + + /// + /// Local file path where the attachment is stored (if downloaded). + /// + public string LocalFilePath { get; set; } + + /// + /// When the attachment was last modified. + /// + public DateTimeOffset LastModified { get; set; } +} diff --git a/Wino.Core.Domain/Entities/Calendar/CalendarEventAttendee.cs b/Wino.Core.Domain/Entities/Calendar/CalendarEventAttendee.cs index e9c47da3..8935589a 100644 --- a/Wino.Core.Domain/Entities/Calendar/CalendarEventAttendee.cs +++ b/Wino.Core.Domain/Entities/Calendar/CalendarEventAttendee.cs @@ -1,10 +1,10 @@ using System; using SQLite; +using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; namespace Wino.Core.Domain.Entities.Calendar; -// TODO: Connect to Contact store with Wino People. public class CalendarEventAttendee { [PrimaryKey] @@ -16,4 +16,11 @@ public class CalendarEventAttendee public bool IsOrganizer { get; set; } public bool IsOptionalAttendee { get; set; } public string Comment { get; set; } + + /// + /// Resolved contact from the contact store. Populated at runtime via IContactService; + /// not persisted to the database. + /// + [Ignore] + public AccountContact ResolvedContact { get; set; } } diff --git a/Wino.Core.Domain/Entities/Calendar/CalendarItem.cs b/Wino.Core.Domain/Entities/Calendar/CalendarItem.cs index de03e74e..a04576f9 100644 --- a/Wino.Core.Domain/Entities/Calendar/CalendarItem.cs +++ b/Wino.Core.Domain/Entities/Calendar/CalendarItem.cs @@ -3,7 +3,9 @@ using System.Diagnostics; using Itenso.TimePeriod; using SQLite; using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Extensions; using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Calendar; namespace Wino.Core.Domain.Entities.Calendar; @@ -17,6 +19,14 @@ public class CalendarItem : ICalendarItem public string Description { get; set; } public string Location { get; set; } + /// + /// Indicates whether this item is a local preview that hasn't been synced to the server yet. + /// When true, the item exists only in the local database without a RemoteEventId. + /// Used to prevent duplicates when the server returns the newly created event. + /// + [Ignore] + public bool IsLocalPreview => string.IsNullOrEmpty(RemoteEventId); + public DateTime StartDate { get; set; } public DateTime EndDate @@ -27,8 +37,17 @@ public class CalendarItem : ICalendarItem } } - public TimeSpan StartDateOffset { get; set; } - public TimeSpan EndDateOffset { get; set; } + /// + /// IANA timezone identifier for the start time (e.g., "America/New_York", "Europe/London"). + /// If null or empty, UTC is assumed. + /// + public string StartTimeZone { get; set; } + + /// + /// IANA timezone identifier for the end time (e.g., "America/New_York", "Europe/London"). + /// If null or empty, UTC is assumed. + /// + public string EndTimeZone { get; set; } private ITimePeriod _period; public ITimePeriod Period @@ -55,9 +74,7 @@ public class CalendarItem : ICalendarItem } /// - /// Events that are either an exceptional instance of a recurring event or occurrences. - /// IsOccurrence is used to display occurrence instances of parent recurring events. - /// IsOccurrence == false && IsRecurringChild == true => exceptional single instance. + /// Events that are child instances of a recurring event (occurrences or exceptions). /// public bool IsRecurringChild { @@ -68,7 +85,7 @@ public class CalendarItem : ICalendarItem } /// - /// Events that are either an exceptional instance of a recurring event or occurrences. + /// Events that are part of a recurring series (either as parent or child). /// public bool IsRecurringEvent => IsRecurringChild || IsRecurringParent; @@ -121,8 +138,15 @@ public class CalendarItem : ICalendarItem // TODO public string CustomEventColorHex { get; set; } public string HtmlLink { get; set; } + public DateTime? SnoozedUntil { get; set; } public CalendarItemStatus Status { get; set; } public CalendarItemVisibility Visibility { get; set; } + + /// + /// Indicates how the event should be shown in the calendar (Free, Busy, Tentative, etc.). + /// + public CalendarItemShowAs ShowAs { get; set; } = CalendarItemShowAs.Busy; + public DateTimeOffset CreatedAt { get; set; } public DateTimeOffset UpdatedAt { get; set; } public Guid CalendarId { get; set; } @@ -131,49 +155,37 @@ public class CalendarItem : ICalendarItem public IAccountCalendar AssignedCalendar { get; set; } /// - /// Whether this item does not really exist in the database or not. - /// These are used to display occurrence instances of parent recurring events. + /// Id to load information related to this event (attendees, reminders, etc.). + /// For child events, if they have their own data, use their own Id. + /// For events that share data with their parent, return parent's Id. /// - [Ignore] - public bool IsOccurrence { get; set; } + public Guid EventTrackingId => Id; /// - /// Id to load information related to this event. - /// Occurrences tracked by the parent recurring event if they are not exceptional instances. - /// Recurring children here are exceptional instances. They have their own info in the database including Id. + /// Gets the start date converted to user's local timezone for display. + /// StartDate is stored according to StartTimeZone. /// - public Guid EventTrackingId => IsOccurrence ? RecurringCalendarItemId.Value : Id; - - public CalendarItem CreateRecurrence(DateTime startDate, double durationInSeconds) + [Ignore] + public DateTime LocalStartDate { - // Create a copy with the new start date and duration - - return new CalendarItem + get { - Id = Guid.NewGuid(), - Title = Title, - Description = Description, - Location = Location, - StartDate = startDate, - DurationInSeconds = durationInSeconds, - Recurrence = Recurrence, - OrganizerDisplayName = OrganizerDisplayName, - OrganizerEmail = OrganizerEmail, - RecurringCalendarItemId = Id, - AssignedCalendar = AssignedCalendar, - CalendarId = CalendarId, - CreatedAt = CreatedAt, - UpdatedAt = UpdatedAt, - Visibility = Visibility, - Status = Status, - CustomEventColorHex = CustomEventColorHex, - HtmlLink = HtmlLink, - StartDateOffset = StartDateOffset, - EndDateOffset = EndDateOffset, - RemoteEventId = RemoteEventId, - IsHidden = IsHidden, - IsLocked = IsLocked, - IsOccurrence = true - }; + return this.GetLocalStartDate(); + } } + + /// + /// Gets the end date converted to user's local timezone for display. + /// EndDate is calculated from StartDate and is in StartTimeZone. + /// + [Ignore] + public DateTime LocalEndDate + { + get + { + return this.GetLocalEndDate(); + } + } + + public string GetDisplayTitle(ITimePeriod displayingPeriod, CalendarSettings calendarSettings) => Period.ToString(); } diff --git a/Wino.Core.Domain/Entities/Calendar/Reminder.cs b/Wino.Core.Domain/Entities/Calendar/Reminder.cs index 3dc64b81..389c4cef 100644 --- a/Wino.Core.Domain/Entities/Calendar/Reminder.cs +++ b/Wino.Core.Domain/Entities/Calendar/Reminder.cs @@ -10,6 +10,10 @@ public class Reminder public Guid Id { get; set; } public Guid CalendarItemId { get; set; } - public DateTimeOffset ReminderTime { get; set; } + /// + /// Duration in seconds before the event start time when the reminder should trigger. + /// For example, 900 seconds = 15 minutes before event. + /// + public long DurationInSeconds { get; set; } public CalendarItemReminderType ReminderType { get; set; } } diff --git a/Wino.Core.Domain/Entities/Mail/EmailTemplate.cs b/Wino.Core.Domain/Entities/Mail/EmailTemplate.cs new file mode 100644 index 00000000..271e51b0 --- /dev/null +++ b/Wino.Core.Domain/Entities/Mail/EmailTemplate.cs @@ -0,0 +1,16 @@ +using System; +using SQLite; + +namespace Wino.Core.Domain.Entities.Mail; + +public class EmailTemplate +{ + [PrimaryKey] + public Guid Id { get; set; } + + public string Name { get; set; } = string.Empty; + + public string Description { get; set; } = string.Empty; + + public string HtmlContent { get; set; } = string.Empty; +} diff --git a/Wino.Core.Domain/Entities/Mail/MailAccountAlias.cs b/Wino.Core.Domain/Entities/Mail/MailAccountAlias.cs index f13dd5ef..b23aaf4e 100644 --- a/Wino.Core.Domain/Entities/Mail/MailAccountAlias.cs +++ b/Wino.Core.Domain/Entities/Mail/MailAccountAlias.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.ObjectModel; +using System.Security.Cryptography.X509Certificates; using SQLite; namespace Wino.Core.Domain.Entities.Mail; @@ -59,4 +61,13 @@ public class MailAccountAlias : RemoteAccountAlias /// Root aliases can't be deleted. /// public bool CanDelete => !IsRootAlias; + + public string SelectedSigningCertificateThumbprint { get; set; } + public bool IsSmimeEncryptionEnabled { get; set; } + + [Ignore] + public X509Certificate2 SelectedSigningCertificate { get; set; } + + [Ignore] + public ObservableCollection Certificates { get; set; } = []; } diff --git a/Wino.Core.Domain/Entities/Mail/MailCopy.cs b/Wino.Core.Domain/Entities/Mail/MailCopy.cs index b3c1049d..ebd71c61 100644 --- a/Wino.Core.Domain/Entities/Mail/MailCopy.cs +++ b/Wino.Core.Domain/Entities/Mail/MailCopy.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using SQLite; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Models.MailItem; namespace Wino.Core.Domain.Entities.Mail; @@ -11,7 +10,7 @@ namespace Wino.Core.Domain.Entities.Mail; /// Summary of the parsed MIME messages. /// Wino will do non-network operations on this table and others from the original MIME. /// -public class MailCopy : IMailItem +public class MailCopy { /// /// Unique Id of the mail. @@ -104,6 +103,11 @@ public class MailCopy : IMailItem /// public bool HasAttachments { get; set; } + /// + /// Type of mail item (regular mail, calendar invitation, calendar response, etc.). + /// + public MailItemType ItemType { get; set; } = MailItemType.Mail; + /// /// Assigned draft id. /// diff --git a/Wino.Core.Domain/Entities/Mail/MailInvitationCalendarMapping.cs b/Wino.Core.Domain/Entities/Mail/MailInvitationCalendarMapping.cs new file mode 100644 index 00000000..d1290062 --- /dev/null +++ b/Wino.Core.Domain/Entities/Mail/MailInvitationCalendarMapping.cs @@ -0,0 +1,31 @@ +using System; +using SQLite; + +namespace Wino.Core.Domain.Entities.Mail; + +/// +/// Maps a calendar invitation mail item to a persisted calendar event. +/// +public class MailInvitationCalendarMapping +{ + [PrimaryKey] + public Guid Id { get; set; } + + public Guid AccountId { get; set; } + + /// + /// MailCopy.Id value of the invitation mail. + /// + public string MailCopyId { get; set; } + + /// + /// iCalendar UID extracted from invitation MIME/ICS content. + /// + public string InvitationUid { get; set; } + + public Guid CalendarId { get; set; } + public Guid CalendarItemId { get; set; } + public string CalendarRemoteEventId { get; set; } + + public DateTime UpdatedAtUtc { get; set; } = DateTime.UtcNow; +} diff --git a/Wino.Core.Domain/Entities/Mail/MailItemFolder.cs b/Wino.Core.Domain/Entities/Mail/MailItemFolder.cs index bb1a2cae..4555799e 100644 --- a/Wino.Core.Domain/Entities/Mail/MailItemFolder.cs +++ b/Wino.Core.Domain/Entities/Mail/MailItemFolder.cs @@ -29,6 +29,8 @@ public class MailItemFolder : IMailItemFolder // For IMAP public uint UidValidity { get; set; } public long HighestModeSeq { get; set; } + public uint HighestKnownUid { get; set; } + public DateTime? LastUidReconcileUtc { get; set; } /// /// Outlook shares delta changes per-folder. Gmail is for per-account. diff --git a/Wino.Core.Domain/Entities/Shared/AccountContact.cs b/Wino.Core.Domain/Entities/Shared/AccountContact.cs index 49949665..8ca24de4 100644 --- a/Wino.Core.Domain/Entities/Shared/AccountContact.cs +++ b/Wino.Core.Domain/Entities/Shared/AccountContact.cs @@ -11,7 +11,7 @@ namespace Wino.Core.Domain.Entities.Shared; // TODO: This can easily evolve to Contact store, just like People app in Windows 10/11. // Do it. -public class AccountContact : IEquatable +public class AccountContact : IEquatable, IContactDisplayItem { /// /// E-mail address of the contact. @@ -25,9 +25,10 @@ public class AccountContact : IEquatable public string Name { get; set; } /// - /// Base64 encoded profile image of the contact. + /// File ID for the contact picture stored on disk. + /// The actual file lives at {ApplicationDataFolderPath}/contacts/{ContactPictureFileId}.jpg. /// - public string Base64ContactPicture { get; set; } + public Guid? ContactPictureFileId { get; set; } /// /// All registered accounts have their contacts registered as root. @@ -36,6 +37,15 @@ public class AccountContact : IEquatable /// public bool IsRootContact { get; set; } + /// + /// When true, indicates that the contact has been manually modified by the user. + /// Contacts with this flag set to true should not be updated during synchronization. + /// + public bool IsOverridden { get; set; } = false; + + public string DisplayName => string.IsNullOrWhiteSpace(Name) ? Address : Name; + AccountContact IContactDisplayItem.PreviewContact => this; + public override bool Equals(object obj) { return Equals(obj as AccountContact); diff --git a/Wino.Core.Domain/Entities/Shared/ContactGroup.cs b/Wino.Core.Domain/Entities/Shared/ContactGroup.cs new file mode 100644 index 00000000..6fd28dcd --- /dev/null +++ b/Wino.Core.Domain/Entities/Shared/ContactGroup.cs @@ -0,0 +1,19 @@ +using System; +using SQLite; + +namespace Wino.Core.Domain.Entities.Shared; + +/// +/// A named group of contacts that can be expanded to individual addresses during mail composition. +/// +public class ContactGroup +{ + [PrimaryKey] + public Guid Id { get; set; } + + /// Display name of the group (e.g., "Team Alpha", "Family"). + public string Name { get; set; } + + /// Optional description for the group. + public string Description { get; set; } +} diff --git a/Wino.Core.Domain/Entities/Shared/ContactGroupMember.cs b/Wino.Core.Domain/Entities/Shared/ContactGroupMember.cs new file mode 100644 index 00000000..1209f5e3 --- /dev/null +++ b/Wino.Core.Domain/Entities/Shared/ContactGroupMember.cs @@ -0,0 +1,21 @@ +using System; +using SQLite; + +namespace Wino.Core.Domain.Entities.Shared; + +/// +/// Associates an e-mail address with a . +/// +public class ContactGroupMember +{ + [PrimaryKey, AutoIncrement] + public int Id { get; set; } + + /// Group this member belongs to. + [Indexed] + public Guid GroupId { get; set; } + + /// E-mail address of the member (FK to AccountContact.Address). + [Indexed] + public string MemberAddress { get; set; } +} diff --git a/Wino.Core.Domain/Entities/Shared/CustomServerInformation.cs b/Wino.Core.Domain/Entities/Shared/CustomServerInformation.cs index 00aee83b..555fb64d 100644 --- a/Wino.Core.Domain/Entities/Shared/CustomServerInformation.cs +++ b/Wino.Core.Domain/Entities/Shared/CustomServerInformation.cs @@ -30,6 +30,11 @@ public class CustomServerInformation public string OutgoingServerUsername { get; set; } public string OutgoingServerPassword { get; set; } + public string CalDavServiceUrl { get; set; } + public string CalDavUsername { get; set; } + public string CalDavPassword { get; set; } + public ImapCalendarSupportMode CalendarSupportMode { get; set; } + /// /// useSSL True: SslOnConnect /// useSSL False: StartTlsWhenAvailable @@ -65,6 +70,8 @@ public class CustomServerInformation { "OutgoingServerPort", OutgoingServerPort }, { "OutgoingServerSocketOption", OutgoingServerSocketOption.ToString() }, { "OutgoingAuthenticationMethod", OutgoingAuthenticationMethod.ToString() }, + { "CalendarSupportMode", CalendarSupportMode.ToString() }, + { "CalDavServiceUrl", CalDavServiceUrl }, { "ProxyServer", ProxyServer }, { "ProxyServerPort", ProxyServerPort } }; diff --git a/Wino.Core.Domain/Entities/Shared/IContactDisplayItem.cs b/Wino.Core.Domain/Entities/Shared/IContactDisplayItem.cs new file mode 100644 index 00000000..4938f8d4 --- /dev/null +++ b/Wino.Core.Domain/Entities/Shared/IContactDisplayItem.cs @@ -0,0 +1,8 @@ +namespace Wino.Core.Domain.Entities.Shared; + +public interface IContactDisplayItem +{ + string DisplayName { get; } + string Address { get; } + AccountContact PreviewContact { get; } +} diff --git a/Wino.Core.Domain/Entities/Shared/KeyboardShortcut.cs b/Wino.Core.Domain/Entities/Shared/KeyboardShortcut.cs new file mode 100644 index 00000000..8b066ad5 --- /dev/null +++ b/Wino.Core.Domain/Entities/Shared/KeyboardShortcut.cs @@ -0,0 +1,65 @@ +using System; +using SQLite; +using Wino.Core.Domain.Enums; + +namespace Wino.Core.Domain.Entities.Shared; + +/// +/// Represents a user-defined keyboard shortcut for mail operations. +/// +public class KeyboardShortcut +{ + [PrimaryKey] + public Guid Id { get; set; } + + /// + /// The application mode this shortcut applies to. + /// + public WinoApplicationMode Mode { get; set; } = WinoApplicationMode.Mail; + + /// + /// The key combination string (e.g., "D", "Delete", "F1"). + /// + public string Key { get; set; } + + /// + /// The modifier keys for this shortcut. + /// + public ModifierKeys ModifierKeys { get; set; } + + /// + /// The shortcut action this shortcut triggers. + /// + public KeyboardShortcutAction Action { get; set; } + + /// + /// Whether this shortcut is enabled. + /// + public bool IsEnabled { get; set; } = true; + + /// + /// When this shortcut was created. + /// + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + /// + /// User-friendly display name for the shortcut. + /// + public string DisplayName + { + get + { + var modifierText = string.Empty; + if (ModifierKeys.HasFlag(ModifierKeys.Control)) + modifierText += "Ctrl+"; + if (ModifierKeys.HasFlag(ModifierKeys.Alt)) + modifierText += "Alt+"; + if (ModifierKeys.HasFlag(ModifierKeys.Shift)) + modifierText += "Shift+"; + if (ModifierKeys.HasFlag(ModifierKeys.Windows)) + modifierText += "Win+"; + + return modifierText + Key; + } + } +} diff --git a/Wino.Core.Domain/Entities/Shared/MailAccount.cs b/Wino.Core.Domain/Entities/Shared/MailAccount.cs index 8f40f70d..c27008a4 100644 --- a/Wino.Core.Domain/Entities/Shared/MailAccount.cs +++ b/Wino.Core.Domain/Entities/Shared/MailAccount.cs @@ -78,6 +78,14 @@ public class MailAccount /// public SpecialImapProvider SpecialImapProvider { get; set; } + /// + /// Gets or sets whether calendar access is granted for this account. + /// When false, synchronizers will not process EventMessages or calendar invitations. + /// Default is false for existing accounts to prevent scope issues. + /// New accounts created after this feature will have this set to true. + /// + public bool IsCalendarAccessGranted { get; set; } + /// /// Contains the merged inbox this account belongs to. /// Ignored for all SQLite operations. @@ -98,6 +106,12 @@ public class MailAccount [Ignore] public MailAccountPreferences Preferences { get; set; } + /// + /// Last time folder structure was synchronized. + /// Used for optimization - skip folder sync if synced recently. + /// + public DateTime? LastFolderStructureSyncDate { get; set; } + /// /// Gets whether the account can perform ProfileInformation sync type. /// @@ -107,4 +121,6 @@ public class MailAccount /// Gets whether the account can perform AliasInformation sync type. /// public bool IsAliasSyncSupported => ProviderType == MailProviderType.Gmail; + + public override string ToString() => Name; } diff --git a/Wino.Core.Domain/Entities/Shared/WinoAccount.cs b/Wino.Core.Domain/Entities/Shared/WinoAccount.cs new file mode 100644 index 00000000..9b0a92e1 --- /dev/null +++ b/Wino.Core.Domain/Entities/Shared/WinoAccount.cs @@ -0,0 +1,30 @@ +using System; +using SQLite; + +namespace Wino.Core.Domain.Entities.Shared; + +public class WinoAccount +{ + [PrimaryKey] + public Guid Id { get; set; } + + public string Email { get; set; } = string.Empty; + + public string AccountStatus { get; set; } = string.Empty; + + public bool HasPassword { get; set; } + + public bool HasGoogleLogin { get; set; } + + public bool HasFacebookLogin { get; set; } + + public string AccessToken { get; set; } = string.Empty; + + public DateTime AccessTokenExpiresAtUtc { get; set; } + + public string RefreshToken { get; set; } = string.Empty; + + public DateTime RefreshTokenExpiresAtUtc { get; set; } + + public DateTime LastAuthenticatedUtc { get; set; } +} diff --git a/Wino.Core.Domain/Enums/AccountCreationDialogState.cs b/Wino.Core.Domain/Enums/AccountCreationDialogState.cs index 0cc10546..2e91d743 100644 --- a/Wino.Core.Domain/Enums/AccountCreationDialogState.cs +++ b/Wino.Core.Domain/Enums/AccountCreationDialogState.cs @@ -5,6 +5,7 @@ public enum AccountCreationDialogState Idle, SigningIn, PreparingFolders, + CalendarMetadataFetch, Completed, ManuelSetupWaiting, TestingConnection, diff --git a/Wino.Core.Domain/Enums/AccountSetupStepStatus.cs b/Wino.Core.Domain/Enums/AccountSetupStepStatus.cs new file mode 100644 index 00000000..a0bb99a3 --- /dev/null +++ b/Wino.Core.Domain/Enums/AccountSetupStepStatus.cs @@ -0,0 +1,9 @@ +namespace Wino.Core.Domain.Enums; + +public enum AccountSetupStepStatus +{ + Pending, + InProgress, + Succeeded, + Failed +} diff --git a/Wino.Core.Domain/Enums/AiActionType.cs b/Wino.Core.Domain/Enums/AiActionType.cs new file mode 100644 index 00000000..26501bb0 --- /dev/null +++ b/Wino.Core.Domain/Enums/AiActionType.cs @@ -0,0 +1,12 @@ +using System; + +namespace Wino.Core.Domain.Enums; + +[Flags] +public enum AiActionType +{ + None = 0, + Translate = 1, + Rewrite = 2, + Summarize = 4, +} diff --git a/Wino.Core.Domain/Enums/CalendarDisplayType.cs b/Wino.Core.Domain/Enums/CalendarDisplayType.cs index 5582899b..afb838d2 100644 --- a/Wino.Core.Domain/Enums/CalendarDisplayType.cs +++ b/Wino.Core.Domain/Enums/CalendarDisplayType.cs @@ -5,6 +5,5 @@ public enum CalendarDisplayType Day, Week, WorkWeek, - Month, - Year + Month } diff --git a/Wino.Core.Domain/Enums/CalendarInitInitiative.cs b/Wino.Core.Domain/Enums/CalendarInitInitiative.cs deleted file mode 100644 index d5e08a76..00000000 --- a/Wino.Core.Domain/Enums/CalendarInitInitiative.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Wino.Core.Domain.Enums; - -/// -/// Trigger to load more data. -/// -public enum CalendarInitInitiative -{ - User, - App -} diff --git a/Wino.Core.Domain/Enums/CalendarItemShowAs.cs b/Wino.Core.Domain/Enums/CalendarItemShowAs.cs new file mode 100644 index 00000000..57c55f00 --- /dev/null +++ b/Wino.Core.Domain/Enums/CalendarItemShowAs.cs @@ -0,0 +1,13 @@ +namespace Wino.Core.Domain.Enums; + +/// +/// Defines how a calendar item should be displayed in terms of availability. +/// +public enum CalendarItemShowAs +{ + Free, + Tentative, + Busy, + OutOfOffice, + WorkingElsewhere +} diff --git a/Wino.Core.Domain/Enums/CalendarItemStatus.cs b/Wino.Core.Domain/Enums/CalendarItemStatus.cs index e8605573..c6d08b52 100644 --- a/Wino.Core.Domain/Enums/CalendarItemStatus.cs +++ b/Wino.Core.Domain/Enums/CalendarItemStatus.cs @@ -3,7 +3,7 @@ public enum CalendarItemStatus { NotResponded, - Confirmed, + Accepted, Tentative, Cancelled, } diff --git a/Wino.Core.Domain/Enums/CalendarItemUpdateSource.cs b/Wino.Core.Domain/Enums/CalendarItemUpdateSource.cs new file mode 100644 index 00000000..f4e3828c --- /dev/null +++ b/Wino.Core.Domain/Enums/CalendarItemUpdateSource.cs @@ -0,0 +1,22 @@ +namespace Wino.Core.Domain.Enums; + +/// +/// Indicates the source of a calendar item update. +/// +public enum CalendarItemUpdateSource +{ + /// + /// Update originated from client-side UI changes (ApplyUIChanges). + /// + ClientUpdated, + + /// + /// Update originated from client-side UI revert (RevertUIChanges). + /// + ClientReverted, + + /// + /// Update originated from server synchronization or database operations. + /// + Server +} diff --git a/Wino.Core.Domain/Enums/CalendarSynchronizationType.cs b/Wino.Core.Domain/Enums/CalendarSynchronizationType.cs index d9f01a19..96351243 100644 --- a/Wino.Core.Domain/Enums/CalendarSynchronizationType.cs +++ b/Wino.Core.Domain/Enums/CalendarSynchronizationType.cs @@ -5,6 +5,7 @@ public enum CalendarSynchronizationType ExecuteRequests, // Execute all requests in the queue. CalendarMetadata, // Sync calendar metadata. CalendarEvents, // Sync all events for all calendars. + Strict, // Run metadata and event synchronization in sequence. SingleCalendar, // Sync events for only specified calendars. UpdateProfile // Update profile information only. } diff --git a/Wino.Core.Domain/Enums/EmailGroupingType.cs b/Wino.Core.Domain/Enums/EmailGroupingType.cs new file mode 100644 index 00000000..703cc21a --- /dev/null +++ b/Wino.Core.Domain/Enums/EmailGroupingType.cs @@ -0,0 +1,10 @@ +namespace Wino.Core.Domain.Enums; + +/// +/// Grouping options for emails +/// +public enum EmailGroupingType +{ + ByFromName, + ByDate +} diff --git a/Wino.Core.Domain/Enums/ImapCalendarSupportMode.cs b/Wino.Core.Domain/Enums/ImapCalendarSupportMode.cs new file mode 100644 index 00000000..4e082a87 --- /dev/null +++ b/Wino.Core.Domain/Enums/ImapCalendarSupportMode.cs @@ -0,0 +1,8 @@ +namespace Wino.Core.Domain.Enums; + +public enum ImapCalendarSupportMode +{ + Disabled = 0, + CalDav = 1, + LocalOnly = 2 +} diff --git a/Wino.Core.Domain/Enums/KeyboardShortcutAction.cs b/Wino.Core.Domain/Enums/KeyboardShortcutAction.cs new file mode 100644 index 00000000..f48f5cf4 --- /dev/null +++ b/Wino.Core.Domain/Enums/KeyboardShortcutAction.cs @@ -0,0 +1,16 @@ +namespace Wino.Core.Domain.Enums; + +public enum KeyboardShortcutAction +{ + None, + NewMail, + ToggleReadUnread, + ToggleFlag, + ToggleArchive, + Delete, + Move, + Reply, + ReplyAll, + Send, + NewEvent +} diff --git a/Wino.Core.Domain/Enums/MailCopyChangeFlags.cs b/Wino.Core.Domain/Enums/MailCopyChangeFlags.cs new file mode 100644 index 00000000..f500aa5d --- /dev/null +++ b/Wino.Core.Domain/Enums/MailCopyChangeFlags.cs @@ -0,0 +1,57 @@ +using System; + +namespace Wino.Core.Domain.Enums; + +[Flags] +public enum MailCopyChangeFlags +{ + None = 0, + Id = 1 << 0, + FolderId = 1 << 1, + ThreadId = 1 << 2, + MessageId = 1 << 3, + References = 1 << 4, + InReplyTo = 1 << 5, + FromName = 1 << 6, + FromAddress = 1 << 7, + Subject = 1 << 8, + PreviewText = 1 << 9, + CreationDate = 1 << 10, + Importance = 1 << 11, + IsRead = 1 << 12, + IsFlagged = 1 << 13, + IsFocused = 1 << 14, + HasAttachments = 1 << 15, + ItemType = 1 << 16, + DraftId = 1 << 17, + IsDraft = 1 << 18, + FileId = 1 << 19, + AssignedFolder = 1 << 20, + AssignedAccount = 1 << 21, + SenderContact = 1 << 22, + UniqueId = 1 << 23, + All = Id | + FolderId | + ThreadId | + MessageId | + References | + InReplyTo | + FromName | + FromAddress | + Subject | + PreviewText | + CreationDate | + Importance | + IsRead | + IsFlagged | + IsFocused | + HasAttachments | + ItemType | + DraftId | + IsDraft | + FileId | + AssignedFolder | + AssignedAccount | + SenderContact | + UniqueId +} diff --git a/Wino.Core.Domain/Enums/MailItemType.cs b/Wino.Core.Domain/Enums/MailItemType.cs new file mode 100644 index 00000000..63fcfa5a --- /dev/null +++ b/Wino.Core.Domain/Enums/MailItemType.cs @@ -0,0 +1,27 @@ +namespace Wino.Core.Domain.Enums; + +/// +/// Represents the type of mail item. +/// +public enum MailItemType +{ + /// + /// Regular mail message. + /// + Mail = 0, + + /// + /// Calendar invitation (meeting request). + /// + CalendarInvitation = 1, + + /// + /// Calendar response (meeting accepted, tentatively accepted, or declined). + /// + CalendarResponse = 2, + + /// + /// Calendar cancellation (meeting cancelled). + /// + CalendarCancellation = 3 +} diff --git a/Wino.Core.Domain/Enums/MailOperation.cs b/Wino.Core.Domain/Enums/MailOperation.cs index 973ff62f..9d482958 100644 --- a/Wino.Core.Domain/Enums/MailOperation.cs +++ b/Wino.Core.Domain/Enums/MailOperation.cs @@ -19,6 +19,18 @@ public enum FolderSynchronizerOperation RenameFolder, EmptyFolder, MarkFolderRead, + DeleteFolder, + CreateSubFolder, +} + +public enum CalendarSynchronizerOperation +{ + CreateEvent, + UpdateEvent, + DeleteEvent, + AcceptEvent, + DeclineEvent, + TentativeEvent, } // UI requests diff --git a/Wino.Core.Domain/Enums/MailUpdateSource.cs b/Wino.Core.Domain/Enums/MailUpdateSource.cs new file mode 100644 index 00000000..e8a3b91f --- /dev/null +++ b/Wino.Core.Domain/Enums/MailUpdateSource.cs @@ -0,0 +1,22 @@ +namespace Wino.Core.Domain.Enums; + +/// +/// Indicates the source of a mail update. +/// +public enum MailUpdateSource +{ + /// + /// Update originated from client-side UI changes (ApplyUIChanges). + /// + ClientUpdated, + + /// + /// Update originated from client-side UI revert (RevertUIChanges). + /// + ClientReverted, + + /// + /// Update originated from server synchronization or database operations. + /// + Server +} diff --git a/Wino.Core.Domain/Enums/ModifierKeys.cs b/Wino.Core.Domain/Enums/ModifierKeys.cs new file mode 100644 index 00000000..f5d7147d --- /dev/null +++ b/Wino.Core.Domain/Enums/ModifierKeys.cs @@ -0,0 +1,16 @@ +using System; + +namespace Wino.Core.Domain.Enums; + +/// +/// Defines keyboard modifier keys that can be used in keyboard shortcuts. +/// +[Flags] +public enum ModifierKeys +{ + None = 0, + Control = 1, + Alt = 2, + Shift = 4, + Windows = 8 +} \ No newline at end of file diff --git a/Wino.Core.Domain/Enums/NavigationReferenceFrame.cs b/Wino.Core.Domain/Enums/NavigationReferenceFrame.cs index 3495a98e..e7279206 100644 --- a/Wino.Core.Domain/Enums/NavigationReferenceFrame.cs +++ b/Wino.Core.Domain/Enums/NavigationReferenceFrame.cs @@ -3,5 +3,6 @@ public enum NavigationReferenceFrame { ShellFrame, + InnerShellFrame, RenderingFrame } diff --git a/Wino.Core.Domain/Enums/NavigationTransitionEffect.cs b/Wino.Core.Domain/Enums/NavigationTransitionEffect.cs new file mode 100644 index 00000000..73be75f6 --- /dev/null +++ b/Wino.Core.Domain/Enums/NavigationTransitionEffect.cs @@ -0,0 +1,27 @@ +namespace Wino.Core.Domain.Enums; + +/// +/// Specifies the animation effect to use during a slide navigation transition. +/// +public enum NavigationTransitionEffect +{ + /// + /// The navigation transition effect starts from the left edge of the frame. + /// + FromLeft, + + /// + /// The navigation transition effect starts from the right edge of the frame. + /// + FromRight, + + /// + /// The navigation transition effect starts from the top edge of the frame. + /// + FromTop, + + /// + /// The navigation transition effect starts from the bottom edge of the frame. + /// + FromBottom +} diff --git a/Wino.Core.Domain/Enums/NewEventButtonBehavior.cs b/Wino.Core.Domain/Enums/NewEventButtonBehavior.cs new file mode 100644 index 00000000..9ef143e4 --- /dev/null +++ b/Wino.Core.Domain/Enums/NewEventButtonBehavior.cs @@ -0,0 +1,7 @@ +namespace Wino.Core.Domain.Enums; + +public enum NewEventButtonBehavior +{ + AskEachTime = 0, + AlwaysUseSpecificCalendar = 1 +} diff --git a/Wino.Core.Domain/Enums/PrintCollation.cs b/Wino.Core.Domain/Enums/PrintCollation.cs new file mode 100644 index 00000000..94b5bf93 --- /dev/null +++ b/Wino.Core.Domain/Enums/PrintCollation.cs @@ -0,0 +1,22 @@ +namespace Wino.Core.Domain.Enums; + +/// +/// Print collation options. +/// +public enum PrintCollation +{ + /// + /// Default collation. + /// + Default = 0, + + /// + /// Collated printing. + /// + Collated = 1, + + /// + /// Uncollated printing. + /// + Uncollated = 2 +} \ No newline at end of file diff --git a/Wino.Core.Domain/Enums/PrintColorMode.cs b/Wino.Core.Domain/Enums/PrintColorMode.cs new file mode 100644 index 00000000..869f9b15 --- /dev/null +++ b/Wino.Core.Domain/Enums/PrintColorMode.cs @@ -0,0 +1,22 @@ +namespace Wino.Core.Domain.Enums; + +/// +/// Print color mode options. +/// +public enum PrintColorMode +{ + /// + /// Default color mode. + /// + Default = 0, + + /// + /// Color printing. + /// + Color = 1, + + /// + /// Grayscale printing. + /// + Grayscale = 2 +} \ No newline at end of file diff --git a/Wino.Core.Domain/Enums/PrintDuplex.cs b/Wino.Core.Domain/Enums/PrintDuplex.cs new file mode 100644 index 00000000..51becc23 --- /dev/null +++ b/Wino.Core.Domain/Enums/PrintDuplex.cs @@ -0,0 +1,27 @@ +namespace Wino.Core.Domain.Enums; + +/// +/// Print duplex (double-sided) options. +/// +public enum PrintDuplex +{ + /// + /// Default duplex mode. + /// + Default = 0, + + /// + /// Single-sided printing. + /// + Simplex = 1, + + /// + /// Double-sided printing with pages flipped horizontally. + /// + DuplexShortEdge = 2, + + /// + /// Double-sided printing with pages flipped vertically. + /// + DuplexLongEdge = 3 +} \ No newline at end of file diff --git a/Wino.Core.Domain/Enums/PrintMediaSize.cs b/Wino.Core.Domain/Enums/PrintMediaSize.cs new file mode 100644 index 00000000..5e454992 --- /dev/null +++ b/Wino.Core.Domain/Enums/PrintMediaSize.cs @@ -0,0 +1,57 @@ +namespace Wino.Core.Domain.Enums; + +/// +/// Print media size options. +/// +public enum PrintMediaSize +{ + /// + /// Default media size. + /// + Default = 0, + + /// + /// Letter size (8.5 x 11 inches). + /// + NorthAmericaLetter = 1, + + /// + /// Legal size (8.5 x 14 inches). + /// + NorthAmericaLegal = 2, + + /// + /// A4 size (210 x 297 mm). + /// + IsoA4 = 3, + + /// + /// A3 size (297 x 420 mm). + /// + IsoA3 = 4, + + /// + /// A5 size (148 x 210 mm). + /// + IsoA5 = 5, + + /// + /// Tabloid size (11 x 17 inches). + /// + NorthAmericaTabloid = 6, + + /// + /// Executive size (7.25 x 10.5 inches). + /// + NorthAmericaExecutive = 7, + + /// + /// B4 size (250 x 353 mm). + /// + JisB4 = 8, + + /// + /// B5 size (176 x 250 mm). + /// + JisB5 = 9 +} \ No newline at end of file diff --git a/Wino.Core.Domain/Enums/PrintOrientation.cs b/Wino.Core.Domain/Enums/PrintOrientation.cs new file mode 100644 index 00000000..8b9b169c --- /dev/null +++ b/Wino.Core.Domain/Enums/PrintOrientation.cs @@ -0,0 +1,17 @@ +namespace Wino.Core.Domain.Enums; + +/// +/// Print orientation options. +/// +public enum PrintOrientation +{ + /// + /// Portrait orientation (default). + /// + Portrait = 0, + + /// + /// Landscape orientation. + /// + Landscape = 1 +} \ No newline at end of file diff --git a/Wino.Core.Domain/Enums/ServerBackgroundMode.cs b/Wino.Core.Domain/Enums/ServerBackgroundMode.cs deleted file mode 100644 index d7f9e5df..00000000 --- a/Wino.Core.Domain/Enums/ServerBackgroundMode.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Wino.Core.Domain.Enums; - -/// -/// What should happen to server app when the client is terminated. -/// -public enum ServerBackgroundMode -{ - MinimizedTray, // Still runs, tray icon is visible. - Invisible, // Still runs, tray icon is invisible. - Terminate // Server is terminated as Wino terminates. -} diff --git a/Wino.Core.Domain/Enums/SynchronizationCompletedState.cs b/Wino.Core.Domain/Enums/SynchronizationCompletedState.cs index 6a2edb6c..fc794169 100644 --- a/Wino.Core.Domain/Enums/SynchronizationCompletedState.cs +++ b/Wino.Core.Domain/Enums/SynchronizationCompletedState.cs @@ -4,5 +4,6 @@ public enum SynchronizationCompletedState { Success, // All succeeded. Canceled, // Canceled by user or HTTP call. - Failed // Exception. + Failed, // Exception. + PartiallyCompleted // Some folders succeeded, some failed. } diff --git a/Wino.Core.Domain/Enums/SynchronizationSource.cs b/Wino.Core.Domain/Enums/SynchronizationSource.cs deleted file mode 100644 index d7b3e2d3..00000000 --- a/Wino.Core.Domain/Enums/SynchronizationSource.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Wino.Core.Domain.Enums; - -/// -/// Enumeration for the source of synchronization. -/// Right now it can either be from the client or the server. -/// -public enum SynchronizationSource -{ - Client, - Server -} diff --git a/Wino.Core.Domain/Enums/SynchronizerErrorCategory.cs b/Wino.Core.Domain/Enums/SynchronizerErrorCategory.cs new file mode 100644 index 00000000..4590273c --- /dev/null +++ b/Wino.Core.Domain/Enums/SynchronizerErrorCategory.cs @@ -0,0 +1,47 @@ +namespace Wino.Core.Domain.Enums; + +/// +/// Categorizes synchronization errors by their root cause for targeted handling. +/// +public enum SynchronizerErrorCategory +{ + /// + /// Network-related issues: connection timeouts, DNS failures, socket errors. + /// + Network, + + /// + /// Authentication failures: invalid credentials, expired tokens, revoked access. + /// + Authentication, + + /// + /// Rate limiting: too many requests (HTTP 429), quota exceeded. + /// + RateLimit, + + /// + /// Resource not found: folder or message deleted externally (HTTP 404). + /// + ResourceNotFound, + + /// + /// Server errors: internal server errors (HTTP 5xx), service unavailable. + /// + ServerError, + + /// + /// Protocol errors: IMAP/SMTP command failures, malformed responses. + /// + ProtocolError, + + /// + /// Validation errors: invalid data, constraint violations. + /// + Validation, + + /// + /// Unknown or unclassified error. + /// + Unknown +} diff --git a/Wino.Core.Domain/Enums/SynchronizerErrorSeverity.cs b/Wino.Core.Domain/Enums/SynchronizerErrorSeverity.cs new file mode 100644 index 00000000..49ce1d4c --- /dev/null +++ b/Wino.Core.Domain/Enums/SynchronizerErrorSeverity.cs @@ -0,0 +1,31 @@ +namespace Wino.Core.Domain.Enums; + +/// +/// Classifies the severity of synchronization errors to determine retry behavior. +/// +public enum SynchronizerErrorSeverity +{ + /// + /// Transient error that should be retried with exponential backoff. + /// Examples: network timeout, temporary server unavailability, rate limiting. + /// + Transient, + + /// + /// Error that can be recovered from by skipping the affected item/folder and continuing sync. + /// Examples: folder deleted externally, message not found, permission denied on single item. + /// + Recoverable, + + /// + /// Fatal error that requires stopping synchronization and user intervention. + /// Examples: account disabled, server permanently unavailable, critical configuration error. + /// + Fatal, + + /// + /// Authentication error that requires the user to re-authenticate. + /// Examples: token expired, password changed, OAuth refresh failed. + /// + AuthRequired +} diff --git a/Wino.Core.Domain/Enums/WindowBackdropType.cs b/Wino.Core.Domain/Enums/WindowBackdropType.cs new file mode 100644 index 00000000..62b74cf8 --- /dev/null +++ b/Wino.Core.Domain/Enums/WindowBackdropType.cs @@ -0,0 +1,11 @@ +namespace Wino.Core.Domain.Enums; + +public enum WindowBackdropType +{ + None, + Mica, + MicaAlt, + DesktopAcrylic, + AcrylicBase, + AcrylicThin +} \ No newline at end of file diff --git a/Wino.Core.Domain/Enums/WinoAddOnProductType.cs b/Wino.Core.Domain/Enums/WinoAddOnProductType.cs new file mode 100644 index 00000000..e4e08540 --- /dev/null +++ b/Wino.Core.Domain/Enums/WinoAddOnProductType.cs @@ -0,0 +1,7 @@ +namespace Wino.Core.Domain.Enums; + +public enum WinoAddOnProductType +{ + AI_PACK, + UNLIMITED_ACCOUNTS +} diff --git a/Wino.Core.Domain/Enums/WinoApplicationMode.cs b/Wino.Core.Domain/Enums/WinoApplicationMode.cs new file mode 100644 index 00000000..a1b6114d --- /dev/null +++ b/Wino.Core.Domain/Enums/WinoApplicationMode.cs @@ -0,0 +1,9 @@ +namespace Wino.Core.Domain.Enums; + +public enum WinoApplicationMode +{ + Mail, + Calendar, + Contacts, + Settings +} diff --git a/Wino.Core.Domain/Enums/WinoPage.cs b/Wino.Core.Domain/Enums/WinoPage.cs index 9bf5347a..eb78105a 100644 --- a/Wino.Core.Domain/Enums/WinoPage.cs +++ b/Wino.Core.Domain/Enums/WinoPage.cs @@ -9,8 +9,8 @@ public enum WinoPage IdlePage, ComposePage, SettingsPage, + ContactsPage, MailRenderingPage, - WelcomePage, AccountDetailsPage, MergedAccountDetailsPage, ManageAccountsPage, @@ -21,13 +21,27 @@ public enum WinoPage MessageListPage, MailListPage, ReadComposePanePage, - LanguageTimePage, AppPreferencesPage, SettingOptionsPage, AliasManagementPage, - EditAccountDetailsPage, - // Calendar + ImapCalDavSettingsPage, + KeyboardShortcutsPage, CalendarPage, CalendarSettingsPage, - EventDetailsPage + CalendarRenderingSettingsPage, + CalendarNotificationSettingsPage, + CalendarPreferenceSettingsPage, + CalendarAccountSettingsPage, + EventDetailsPage, + CalendarEventComposePage, + SignatureAndEncryptionPage, + EmailTemplatesPage, + CreateEmailTemplatePage, + StoragePage, + WinoAccountManagementPage, + WelcomePageV2, + WelcomeHostPage, + ProviderSelectionPage, + AccountSetupProgressPage, + SpecialImapCredentialsPage } diff --git a/Wino.Core.Domain/Enums/WinoServerConnectionStatus.cs b/Wino.Core.Domain/Enums/WinoServerConnectionStatus.cs deleted file mode 100644 index 9897b5bd..00000000 --- a/Wino.Core.Domain/Enums/WinoServerConnectionStatus.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Wino.Core.Domain.Enums; - -public enum WinoServerConnectionStatus -{ - None, - Connecting, - Connected, - Disconnected, - Failed -} diff --git a/Wino.Core.Domain/Exceptions/BackgroundTaskRegistrationFailedException.cs b/Wino.Core.Domain/Exceptions/BackgroundTaskRegistrationFailedException.cs deleted file mode 100644 index 1a8ac966..00000000 --- a/Wino.Core.Domain/Exceptions/BackgroundTaskRegistrationFailedException.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace Wino.Core.Domain.Exceptions; - -/// -/// An exception thrown when the background task registration is failed. -/// -public class BackgroundTaskRegistrationFailedException : Exception { } diff --git a/Wino.Core.Domain/Exceptions/CalendarEventComposeValidationException.cs b/Wino.Core.Domain/Exceptions/CalendarEventComposeValidationException.cs new file mode 100644 index 00000000..834606cc --- /dev/null +++ b/Wino.Core.Domain/Exceptions/CalendarEventComposeValidationException.cs @@ -0,0 +1,10 @@ +using System; + +namespace Wino.Core.Domain.Exceptions; + +public sealed class CalendarEventComposeValidationException : Exception +{ + public CalendarEventComposeValidationException(string message) : base(message) + { + } +} diff --git a/Wino.Core.Domain/Exceptions/ImapClientPoolException.cs b/Wino.Core.Domain/Exceptions/ImapClientPoolException.cs index f109d383..4ff337b5 100644 --- a/Wino.Core.Domain/Exceptions/ImapClientPoolException.cs +++ b/Wino.Core.Domain/Exceptions/ImapClientPoolException.cs @@ -9,22 +9,18 @@ public class ImapClientPoolException : Exception { } - public ImapClientPoolException(string message, CustomServerInformation customServerInformation, string protocolLog) : base(message) + public ImapClientPoolException(string message, CustomServerInformation customServerInformation) : base(message) { CustomServerInformation = customServerInformation; - ProtocolLog = protocolLog; } - public ImapClientPoolException(string message, string protocolLog) : base(message) + public ImapClientPoolException(string message) : base(message) { - ProtocolLog = protocolLog; } - public ImapClientPoolException(Exception innerException, string protocolLog) : base(innerException.Message, innerException) + public ImapClientPoolException(Exception innerException) : base(innerException.Message, innerException) { - ProtocolLog = protocolLog; } public CustomServerInformation CustomServerInformation { get; } - public string ProtocolLog { get; } } diff --git a/Wino.Core.Domain/Exceptions/ImapConnectionFailedPackage.cs b/Wino.Core.Domain/Exceptions/ImapConnectionFailedPackage.cs deleted file mode 100644 index e431d969..00000000 --- a/Wino.Core.Domain/Exceptions/ImapConnectionFailedPackage.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Wino.Core.Domain.Models.AutoDiscovery; - -namespace Wino.Core.Domain.Exceptions; - -public class ImapConnectionFailedPackage -{ - public ImapConnectionFailedPackage(string errorMessage, string protocolLog, AutoDiscoverySettings settings) - { - ErrorMessage = errorMessage; - ProtocolLog = protocolLog; - Settings = settings; - } - - public AutoDiscoverySettings Settings { get; } - public string ErrorMessage { get; set; } - public string ProtocolLog { get; } -} diff --git a/Wino.Core.Domain/Exceptions/WinoServerException.cs b/Wino.Core.Domain/Exceptions/WinoServerException.cs deleted file mode 100644 index afa7f132..00000000 --- a/Wino.Core.Domain/Exceptions/WinoServerException.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Wino.Core.Domain.Exceptions; - -/// -/// All server crash types. Wino Server ideally should not throw anything else than this Exception type. -/// -public class WinoServerException : Exception -{ - public WinoServerException(string message) : base(message) { } -} diff --git a/Wino.Core.Domain/Extensions/CalendarRemoteEventIdExtensions.cs b/Wino.Core.Domain/Extensions/CalendarRemoteEventIdExtensions.cs new file mode 100644 index 00000000..a21f9ccc --- /dev/null +++ b/Wino.Core.Domain/Extensions/CalendarRemoteEventIdExtensions.cs @@ -0,0 +1,67 @@ +using System; + +namespace Wino.Core.Domain.Extensions; + +public static class CalendarRemoteEventIdExtensions +{ + private const string ClientTrackingSeparator = "::"; + private const string CalDavClientTrackingPrefix = "caldav-"; + private const string LocalClientTrackingPrefix = "local-"; + + public static string GetProviderRemoteEventId(this string remoteEventId) + { + if (string.IsNullOrWhiteSpace(remoteEventId)) + return string.Empty; + + var separatorIndex = remoteEventId.IndexOf(ClientTrackingSeparator, StringComparison.Ordinal); + return separatorIndex >= 0 ? remoteEventId[..separatorIndex] : remoteEventId; + } + + public static Guid? GetClientTrackingId(this string remoteEventId) + { + if (string.IsNullOrWhiteSpace(remoteEventId)) + return null; + + if (remoteEventId.Contains(ClientTrackingSeparator, StringComparison.Ordinal)) + { + var trackedPart = remoteEventId[(remoteEventId.LastIndexOf(ClientTrackingSeparator, StringComparison.Ordinal) + ClientTrackingSeparator.Length)..]; + if (TryParseGuid(trackedPart, out var trackedId)) + return trackedId; + } + + if (TryParseGuid(remoteEventId, out var directId)) + return directId; + + if (remoteEventId.StartsWith(CalDavClientTrackingPrefix, StringComparison.OrdinalIgnoreCase) && + TryParseGuid(remoteEventId[CalDavClientTrackingPrefix.Length..], out var calDavId)) + { + return calDavId; + } + + if (remoteEventId.StartsWith(LocalClientTrackingPrefix, StringComparison.OrdinalIgnoreCase) && + TryParseGuid(remoteEventId[LocalClientTrackingPrefix.Length..], out var localId)) + { + return localId; + } + + return null; + } + + public static string WithClientTrackingId(this string providerRemoteEventId, Guid? clientTrackingId) + { + if (string.IsNullOrWhiteSpace(providerRemoteEventId) || !clientTrackingId.HasValue) + return providerRemoteEventId ?? string.Empty; + + return $"{providerRemoteEventId}{ClientTrackingSeparator}{clientTrackingId.Value:N}"; + } + + private static bool TryParseGuid(string value, out Guid parsedGuid) + { + parsedGuid = Guid.Empty; + + if (string.IsNullOrWhiteSpace(value)) + return false; + + return Guid.TryParseExact(value, "N", out parsedGuid) || Guid.TryParse(value, out parsedGuid); + } +} diff --git a/Wino.Core.Domain/Extensions/DateTimeExtensions.cs b/Wino.Core.Domain/Extensions/DateTimeExtensions.cs index f026c80f..0268a93a 100644 --- a/Wino.Core.Domain/Extensions/DateTimeExtensions.cs +++ b/Wino.Core.Domain/Extensions/DateTimeExtensions.cs @@ -1,4 +1,5 @@ -using System; +using System; +using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Models.Calendar; namespace Wino.Core.Domain.Extensions; @@ -29,4 +30,60 @@ public static class DateTimeExtensions // Start loading from this date instead of visible date. return date.AddDays(-diff).Date; } + + /// + /// Converts a datetime from source timezone into local timezone. + /// If timezone lookup fails, returns original value. + /// + public static DateTime ToLocalTimeFromTimeZone(this DateTime dateTime, string sourceTimeZoneId) + { + if (string.IsNullOrWhiteSpace(sourceTimeZoneId)) + return dateTime; + + try + { + var sourceTimeZone = TimeZoneInfo.FindSystemTimeZoneById(sourceTimeZoneId); + var localTimeZone = TimeZoneInfo.Local; + var unspecifiedDateTime = DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified); + + return TimeZoneInfo.ConvertTime(unspecifiedDateTime, sourceTimeZone, localTimeZone); + } + catch + { + return dateTime; + } + } + + /// + /// Converts local datetime into target timezone. + /// If timezone lookup fails, returns original value. + /// + public static DateTime ToTimeZoneFromLocal(this DateTime localDateTime, string targetTimeZoneId) + { + if (string.IsNullOrWhiteSpace(targetTimeZoneId)) + return localDateTime; + + try + { + var sourceTimeZone = TimeZoneInfo.Local; + var targetTimeZone = TimeZoneInfo.FindSystemTimeZoneById(targetTimeZoneId); + var unspecifiedDateTime = DateTime.SpecifyKind(localDateTime, DateTimeKind.Unspecified); + + return TimeZoneInfo.ConvertTime(unspecifiedDateTime, sourceTimeZone, targetTimeZone); + } + catch + { + return localDateTime; + } + } + + public static DateTime GetLocalStartDate(this CalendarItem calendarItem) + => calendarItem.IsAllDayEvent + ? calendarItem.StartDate + : calendarItem.StartDate.ToLocalTimeFromTimeZone(calendarItem.StartTimeZone); + + public static DateTime GetLocalEndDate(this CalendarItem calendarItem) + => calendarItem.IsAllDayEvent + ? calendarItem.EndDate + : calendarItem.EndDate.ToLocalTimeFromTimeZone(calendarItem.EndTimeZone); } diff --git a/Wino.Core.Domain/Extensions/MailHeaderExtensions.cs b/Wino.Core.Domain/Extensions/MailHeaderExtensions.cs new file mode 100644 index 00000000..aa41729b --- /dev/null +++ b/Wino.Core.Domain/Extensions/MailHeaderExtensions.cs @@ -0,0 +1,42 @@ +using System; +using System.Linq; + +namespace Wino.Core.Domain.Extensions; + +public static class MailHeaderExtensions +{ + /// + /// Strips angle brackets from a Message-ID or In-Reply-To value. + /// RFC 5322 Message-IDs are formatted as <id@domain>, but MimeKit + /// properties store them without brackets. This normalizes raw header + /// values to match MimeKit's convention. + /// + public static string StripAngleBrackets(string value) + { + if (string.IsNullOrEmpty(value)) return value; + + value = value.Trim(); + + if (value.StartsWith("<") && value.EndsWith(">")) + return value.Substring(1, value.Length - 2); + + return value; + } + + /// + /// Normalizes a raw RFC References header value into semicolon-separated Message-IDs + /// without angle brackets. Raw References headers contain space-separated bracketed IDs + /// like "<id1@domain> <id2@domain>". This converts them to "id1@domain;id2@domain". + /// + public static string NormalizeReferences(string rawReferences) + { + if (string.IsNullOrEmpty(rawReferences)) return rawReferences; + + var ids = rawReferences + .Split(new[] { ' ', '\t', '\r', '\n', ';', ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(StripAngleBrackets) + .Where(id => !string.IsNullOrEmpty(id)); + + return string.Join(";", ids); + } +} diff --git a/Wino.Core.Domain/Interfaces/IAccountCalendar.cs b/Wino.Core.Domain/Interfaces/IAccountCalendar.cs index 85a340c3..7e070f40 100644 --- a/Wino.Core.Domain/Interfaces/IAccountCalendar.cs +++ b/Wino.Core.Domain/Interfaces/IAccountCalendar.cs @@ -1,4 +1,6 @@ using System; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; namespace Wino.Core.Domain.Interfaces; @@ -8,8 +10,11 @@ public interface IAccountCalendar string TextColorHex { get; set; } string BackgroundColorHex { get; set; } bool IsPrimary { get; set; } + bool IsSynchronizationEnabled { get; set; } Guid AccountId { get; set; } string RemoteCalendarId { get; set; } bool IsExtended { get; set; } + CalendarItemShowAs DefaultShowAs { get; set; } Guid Id { get; set; } + MailAccount MailAccount { get; set; } } diff --git a/Wino.Core.Domain/Interfaces/IAccountMenuItem.cs b/Wino.Core.Domain/Interfaces/IAccountMenuItem.cs index b3a917bd..b978978f 100644 --- a/Wino.Core.Domain/Interfaces/IAccountMenuItem.cs +++ b/Wino.Core.Domain/Interfaces/IAccountMenuItem.cs @@ -7,7 +7,27 @@ namespace Wino.Core.Domain.Interfaces; public interface IAccountMenuItem : IMenuItem { bool IsEnabled { get; set; } - double SynchronizationProgress { get; set; } + + /// + /// Calculated synchronization progress percentage (0-100). -1 for indeterminate. + /// + double SynchronizationProgress { get; } + + /// + /// Total items to sync. 0 for indeterminate progress. + /// + int TotalItemsToSync { get; set; } + + /// + /// Remaining items to sync. + /// + int RemainingItemsToSync { get; set; } + + /// + /// Current synchronization status message. + /// + string SynchronizationStatus { get; set; } + int UnreadItemCount { get; set; } IEnumerable HoldingAccounts { get; } void UpdateAccount(MailAccount account); diff --git a/Wino.Core.Domain/Interfaces/IAccountService.cs b/Wino.Core.Domain/Interfaces/IAccountService.cs index 6c590df7..cc31f0d9 100644 --- a/Wino.Core.Domain/Interfaces/IAccountService.cs +++ b/Wino.Core.Domain/Interfaces/IAccountService.cs @@ -171,4 +171,19 @@ public interface IAccountService /// Whether the notifications should be created after sync or not. Task IsNotificationsEnabled(Guid accountId); Task UpdateAccountCustomServerInformationAsync(CustomServerInformation customServerInformation); + + /// + /// Updates the last folder structure sync date for the given account. + /// Used for optimization to skip folder sync if it was done recently. + /// + /// Account id. + Task UpdateLastFolderStructureSyncDateAsync(Guid accountId); + + /// + /// Checks if folder structure should be synced based on the configured interval. + /// Returns true if LastFolderStructureSyncDate is null or older than the interval. + /// + /// Account id. + /// Minimum interval between folder syncs. + Task ShouldSyncFolderStructureAsync(Guid accountId, TimeSpan syncInterval); } diff --git a/Wino.Core.Domain/Interfaces/IAiActionOptionsService.cs b/Wino.Core.Domain/Interfaces/IAiActionOptionsService.cs new file mode 100644 index 00000000..5efaf1e7 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IAiActionOptionsService.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Wino.Core.Domain.Models.Ai; + +namespace Wino.Core.Domain.Interfaces; + +public interface IAiActionOptionsService +{ + IReadOnlyList GetTranslateLanguageOptions(); + IReadOnlyList GetRewriteModeOptions(); +} diff --git a/Wino.Core.Domain/Interfaces/IAutoDiscoveryService.cs b/Wino.Core.Domain/Interfaces/IAutoDiscoveryService.cs index 0ba6642a..3f49c0bf 100644 --- a/Wino.Core.Domain/Interfaces/IAutoDiscoveryService.cs +++ b/Wino.Core.Domain/Interfaces/IAutoDiscoveryService.cs @@ -1,17 +1,22 @@ -using System.Threading.Tasks; +using System; +using System.Threading; +using System.Threading.Tasks; using Wino.Core.Domain.Models.AutoDiscovery; namespace Wino.Core.Domain.Interfaces; /// -/// Searches for Auto Discovery settings for custom mail accounts. +/// Searches for auto-discovery settings for custom mail accounts. /// public interface IAutoDiscoveryService { /// /// Tries to return the best mail server settings using different techniques. /// - /// Address to search settings for. - /// CustomServerInformation with only settings applied. Task GetAutoDiscoverySettings(AutoDiscoveryMinimalSettings autoDiscoveryMinimalSettings); + + /// + /// Tries to resolve a CalDAV endpoint for the mailbox address. + /// + Task DiscoverCalDavServiceUriAsync(string mailAddress, CancellationToken cancellationToken = default); } diff --git a/Wino.Core.Domain/Interfaces/IBackgroundTaskService.cs b/Wino.Core.Domain/Interfaces/IBackgroundTaskService.cs deleted file mode 100644 index 651ee659..00000000 --- a/Wino.Core.Domain/Interfaces/IBackgroundTaskService.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Threading.Tasks; - -namespace Wino.Core.Domain.Interfaces; - -public interface IBackgroundTaskService -{ - /// - /// Unregisters all background tasks once. - /// This is used to clean up the background tasks when the app is updated. - /// - void UnregisterAllBackgroundTask(); - - /// - /// Registers required background tasks. - /// - Task RegisterBackgroundTasksAsync(); -} diff --git a/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs b/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs index 00f2f406..f423eadf 100644 --- a/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs +++ b/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs @@ -1,4 +1,6 @@ -using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Models.Accounts; @@ -23,6 +25,23 @@ public interface IBaseSynchronizer /// Request to queue. void QueueRequest(IRequestBase request); + /// + /// Returns whether there is an in-progress (queued or currently executing) operation for the given mail unique id. + /// + /// Mail unique id to check. + bool HasPendingOperation(Guid mailUniqueId); + + /// + /// Returns mail unique ids that currently have queued or executing operations. + /// + IReadOnlyCollection GetPendingOperationUniqueIds(); + + /// + /// Returns whether there is an in-progress (queued or currently executing) operation for the given calendar item id. + /// + /// Calendar item id to check. + bool HasPendingCalendarOperation(Guid calendarItemId); + /// /// Synchronizes profile information with the server. /// Sender name and Profile picture are updated. diff --git a/Wino.Core.Domain/Interfaces/ICalDavClient.cs b/Wino.Core.Domain/Interfaces/ICalDavClient.cs new file mode 100644 index 00000000..824ff22f --- /dev/null +++ b/Wino.Core.Domain/Interfaces/ICalDavClient.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Wino.Core.Domain.Models.Calendar; + +namespace Wino.Core.Domain.Interfaces; + +public interface ICalDavClient +{ + Task> DiscoverCalendarsAsync( + CalDavConnectionSettings connectionSettings, + CancellationToken cancellationToken = default); + + Task> GetCalendarEventsAsync( + CalDavConnectionSettings connectionSettings, + CalDavCalendar calendar, + DateTimeOffset startUtc, + DateTimeOffset endUtc, + CancellationToken cancellationToken = default); + + Task UpsertCalendarEventAsync( + CalDavConnectionSettings connectionSettings, + CalDavCalendar calendar, + string remoteEventId, + string icsContent, + CancellationToken cancellationToken = default); + + Task DeleteCalendarEventAsync( + CalDavConnectionSettings connectionSettings, + CalDavCalendar calendar, + string remoteEventId, + CancellationToken cancellationToken = default); +} + diff --git a/Wino.Core.Domain/Interfaces/ICalendarDialogService.cs b/Wino.Core.Domain/Interfaces/ICalendarDialogService.cs deleted file mode 100644 index 7b48f868..00000000 --- a/Wino.Core.Domain/Interfaces/ICalendarDialogService.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Wino.Core.Domain.Interfaces; - -public interface ICalendarDialogService : IDialogServiceBase -{ -} diff --git a/Wino.Core.Domain/Interfaces/ICalendarIcsFileService.cs b/Wino.Core.Domain/Interfaces/ICalendarIcsFileService.cs new file mode 100644 index 00000000..919ea737 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/ICalendarIcsFileService.cs @@ -0,0 +1,15 @@ +using System; +using System.Threading.Tasks; + +namespace Wino.Core.Domain.Interfaces; + +/// +/// Persists CalDAV ICS payloads on disk for IMAP accounts. +/// +public interface ICalendarIcsFileService +{ + Task SaveCalendarItemIcsAsync(Guid accountId, Guid calendarId, Guid calendarItemId, string remoteEventId, string remoteResourceHref, string eTag, string icsContent); + Task GetCalendarItemIcsETagAsync(Guid accountId, Guid calendarId, Guid calendarItemId); + Task DeleteCalendarItemIcsAsync(Guid accountId, Guid calendarItemId); + Task DeleteCalendarIcsForCalendarAsync(Guid accountId, Guid calendarId); +} diff --git a/Wino.Core.Domain/Interfaces/ICalendarItem.cs b/Wino.Core.Domain/Interfaces/ICalendarItem.cs index e83827a6..cfa0cb31 100644 --- a/Wino.Core.Domain/Interfaces/ICalendarItem.cs +++ b/Wino.Core.Domain/Interfaces/ICalendarItem.cs @@ -1,5 +1,6 @@ using System; using Itenso.TimePeriod; +using Wino.Core.Domain.Models.Calendar; namespace Wino.Core.Domain.Interfaces; @@ -19,4 +20,13 @@ public interface ICalendarItem bool IsRecurringChild { get; } bool IsRecurringParent { get; } bool IsRecurringEvent { get; } + + /// + /// Gets the display title for this calendar item when rendered in a specific day. + /// For multi-day events, includes start/end time indicators. + /// + /// The period of the day where this item is being rendered. + /// Calendar settings for time formatting. + /// The formatted title string. + string GetDisplayTitle(ITimePeriod displayingPeriod, CalendarSettings calendarSettings); } diff --git a/Wino.Core.Domain/Interfaces/ICalendarItemViewModel.cs b/Wino.Core.Domain/Interfaces/ICalendarItemViewModel.cs index 3eaf1ff7..55a91f68 100644 --- a/Wino.Core.Domain/Interfaces/ICalendarItemViewModel.cs +++ b/Wino.Core.Domain/Interfaces/ICalendarItemViewModel.cs @@ -1,4 +1,8 @@ -namespace Wino.Core.Domain.Interfaces; +using Itenso.TimePeriod; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Models.Calendar; + +namespace Wino.Core.Domain.Interfaces; /// /// Temporarily to enforce CalendarItemViewModel. Used in CalendarEventCollection. @@ -6,4 +10,23 @@ public interface ICalendarItemViewModel { bool IsSelected { get; set; } + + bool IsBusy { get; set; } + + /// + /// The period of the day where this item is currently being displayed. + /// + ITimePeriod DisplayingPeriod { get; set; } + + /// + /// Calendar settings for time formatting. + /// + CalendarSettings CalendarSettings { get; set; } + + /// + /// Updates the view model's underlying CalendarItem from new data. + /// This allows in-place updates without removing and re-adding items. + /// + /// The updated calendar item data. + void UpdateFrom(CalendarItem calendarItem); } diff --git a/Wino.Core.Domain/Interfaces/ICalendarService.cs b/Wino.Core.Domain/Interfaces/ICalendarService.cs index f0e18b4a..fae3d08f 100644 --- a/Wino.Core.Domain/Interfaces/ICalendarService.cs +++ b/Wino.Core.Domain/Interfaces/ICalendarService.cs @@ -1,6 +1,8 @@ -using System; +using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; +using Itenso.TimePeriod; using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Models.Calendar; @@ -11,12 +13,22 @@ public interface ICalendarService Task> GetAccountCalendarsAsync(Guid accountId); Task GetAccountCalendarAsync(Guid accountCalendarId); Task DeleteCalendarItemAsync(Guid calendarItemId); + Task DeleteCalendarItemAsync(string calendarRemoteEventId, Guid calendarId); Task DeleteAccountCalendarAsync(AccountCalendar accountCalendar); Task InsertAccountCalendarAsync(AccountCalendar accountCalendar); Task UpdateAccountCalendarAsync(AccountCalendar accountCalendar); + Task SetPrimaryCalendarAsync(Guid accountId, Guid accountCalendarId); Task CreateNewCalendarItemAsync(CalendarItem calendarItem, List attendees); - Task> GetCalendarEventsAsync(IAccountCalendar calendar, DayRangeRenderModel dayRangeRenderModel); + + /// + /// Retrieves calendar events for a given calendar within the specified time period. + /// + /// The calendar to retrieve events from. + /// The time period to query events for. + /// List of calendar items that fall within the requested period. + Task> GetCalendarEventsAsync(IAccountCalendar calendar, ITimePeriod period); + Task GetCalendarItemAsync(Guid accountCalendarId, string remoteEventId); Task UpdateCalendarDeltaSynchronizationToken(Guid calendarId, string deltaToken); @@ -28,4 +40,43 @@ public interface ICalendarService Task GetCalendarItemAsync(Guid id); Task> GetAttendeesAsync(Guid calendarEventTrackingId); Task> ManageEventAttendeesAsync(Guid calendarItemId, List allAttendees); + Task UpdateCalendarItemAsync(CalendarItem calendarItem, List attendees); + Task> SearchCalendarItemsAsync(string searchQuery, int limit, CancellationToken cancellationToken = default); + Task> GetRemindersAsync(Guid calendarItemId); + Task SaveRemindersAsync(Guid calendarItemId, List reminders); + Task SnoozeCalendarItemAsync(Guid calendarItemId, DateTime snoozedUntilLocal); + + /// + /// Checks due reminder windows and returns reminder notifications that should trigger now. + /// + Task> CheckAndNotifyAsync(DateTime lastCheckLocal, DateTime nowLocal, ISet sentReminderKeys, CancellationToken cancellationToken = default); + + /// + /// Gets predefined reminder options in minutes (1 Hour, 30 Min, 15 Min, 5 Min, 1 Min). + /// + int[] GetPredefinedReminderMinutes(); + + #region Attachments + + /// + /// Gets all attachments for a calendar event. + /// + Task> GetAttachmentsAsync(Guid calendarItemId); + + /// + /// Inserts or updates calendar attachments. + /// + Task InsertOrReplaceAttachmentsAsync(List attachments); + + /// + /// Marks an attachment as downloaded and updates its local file path. + /// + Task MarkAttachmentDownloadedAsync(Guid attachmentId, string localFilePath); + + /// + /// Deletes all attachments for a calendar item. + /// + Task DeleteAttachmentsAsync(Guid calendarItemId); + + #endregion } diff --git a/Wino.Core.Domain/Interfaces/IContactPictureFileService.cs b/Wino.Core.Domain/Interfaces/IContactPictureFileService.cs new file mode 100644 index 00000000..ea654a17 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IContactPictureFileService.cs @@ -0,0 +1,26 @@ +using System; +using System.Threading.Tasks; + +namespace Wino.Core.Domain.Interfaces; + +/// +/// Manages contact picture files stored on disk instead of as base64 in SQLite, +/// eliminating DB bloat and enabling native WIC hardware-accelerated image loading. +/// +public interface IContactPictureFileService +{ + /// + /// Returns the full file path for the given file ID, or null if the file does not exist on disk. + /// + string GetContactPicturePath(Guid fileId); + + /// + /// Saves raw image bytes to disk and returns the new file ID. + /// + Task SaveContactPictureAsync(byte[] imageData); + + /// + /// Deletes the picture file for the given file ID if it exists. + /// + Task DeleteContactPictureAsync(Guid fileId); +} diff --git a/Wino.Core.Domain/Interfaces/IContactService.cs b/Wino.Core.Domain/Interfaces/IContactService.cs index e721ec2a..6462026d 100644 --- a/Wino.Core.Domain/Interfaces/IContactService.cs +++ b/Wino.Core.Domain/Interfaces/IContactService.cs @@ -1,7 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using MimeKit; using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Models.Contacts; namespace Wino.Core.Domain.Interfaces; @@ -9,6 +12,30 @@ public interface IContactService { Task> GetAddressInformationAsync(string queryText); Task GetAddressInformationByAddressAsync(string address); + Task> GetContactsByAddressesAsync(IEnumerable addresses); Task SaveAddressInformationAsync(MimeMessage message); + Task SaveAddressInformationAsync(IEnumerable contacts); Task CreateNewContactAsync(string address, string displayName); + + // Paged contact queries for ContactsPage + Task> GetAllContactsAsync(); + Task> SearchContactsAsync(string searchQuery); + Task GetContactsPageAsync(int offset, int pageSize, string searchQuery = null, bool excludeRootContacts = false); + Task UpdateContactAsync(AccountContact contact); + Task DeleteContactAsync(string address); + Task DeleteContactsAsync(IEnumerable addresses); + + // Group / distribution list support + Task> GetGroupsAsync(); + Task CreateGroupAsync(string name, string description = null); + Task DeleteGroupAsync(Guid groupId); + Task> GetGroupMembersAsync(Guid groupId); + Task AddGroupMemberAsync(Guid groupId, string memberAddress); + Task RemoveGroupMemberAsync(Guid groupId, string memberAddress); + + /// + /// Expands a contact group to the individual entries of its members. + /// Returns an empty list if the group does not exist or has no members. + /// + Task> ExpandGroupAsync(Guid groupId); } diff --git a/Wino.Core.Domain/Interfaces/IContextMenuItemService.cs b/Wino.Core.Domain/Interfaces/IContextMenuItemService.cs index c3b1c54d..eee75ad5 100644 --- a/Wino.Core.Domain/Interfaces/IContextMenuItemService.cs +++ b/Wino.Core.Domain/Interfaces/IContextMenuItemService.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; +using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Models.Folders; -using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Menus; namespace Wino.Core.Domain.Interfaces; @@ -8,6 +8,6 @@ namespace Wino.Core.Domain.Interfaces; public interface IContextMenuItemService { IEnumerable GetFolderContextMenuActions(IBaseFolderMenuItem folderInformation); - IEnumerable GetMailItemContextMenuActions(IEnumerable selectedMailItems); - IEnumerable GetMailItemRenderMenuActions(IMailItem mailItem, bool isDarkEditor); + IEnumerable GetMailItemContextMenuActions(IEnumerable selectedMailItems); + IEnumerable GetMailItemRenderMenuActions(MailCopy mailItem, bool isDarkEditor); } diff --git a/Wino.Core.Domain/Interfaces/IContextMenuProvider.cs b/Wino.Core.Domain/Interfaces/IContextMenuProvider.cs index 7e7ef089..d5b676b4 100644 --- a/Wino.Core.Domain/Interfaces/IContextMenuProvider.cs +++ b/Wino.Core.Domain/Interfaces/IContextMenuProvider.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; +using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Models.Folders; -using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Menus; namespace Wino.Core.Domain.Interfaces; @@ -18,12 +18,12 @@ public interface IContextMenuProvider /// /// Current folder that asks for the menu items. /// Selected menu items in the given folder. - IEnumerable GetMailItemContextMenuActions(IMailItemFolder folderInformation, IEnumerable selectedMailItems); + IEnumerable GetMailItemContextMenuActions(IMailItemFolder folderInformation, IEnumerable selectedMailItems); /// /// Calculates and returns available mail operations for mail rendering CommandBar. /// /// Rendered mail item. /// Folder that mail item belongs to. - IEnumerable GetMailItemRenderMenuActions(IMailItem mailItem, IMailItemFolder activeFolder, bool isDarkEditor); + IEnumerable GetMailItemRenderMenuActions(MailCopy mailItem, IMailItemFolder activeFolder, bool isDarkEditor); } diff --git a/Wino.Core.Domain/Interfaces/IDialogServiceBase.cs b/Wino.Core.Domain/Interfaces/IDialogServiceBase.cs index 8cc97e6d..c915d5b9 100644 --- a/Wino.Core.Domain/Interfaces/IDialogServiceBase.cs +++ b/Wino.Core.Domain/Interfaces/IDialogServiceBase.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Models.Accounts; using Wino.Core.Domain.Models.Common; +using Wino.Core.Domain.Models.Printing; namespace Wino.Core.Domain.Interfaces; @@ -27,5 +28,7 @@ public interface IDialogServiceBase Task ShowAccountProviderSelectionDialogAsync(List availableProviders); IAccountCreationDialog GetAccountCreationDialog(AccountCreationDialogResult accountCreationDialogResult); Task> PickFilesAsync(params object[] typeFilters); + Task> PickFilesMetadataAsync(params object[] typeFilters); Task PickFilePathAsync(string saveFileName); + Task ShowPrintDialogAsync(WebView2PrintSettingsModel initialSettings = null); } diff --git a/Wino.Core.Domain/Interfaces/IEmailTemplateService.cs b/Wino.Core.Domain/Interfaces/IEmailTemplateService.cs new file mode 100644 index 00000000..52afff79 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IEmailTemplateService.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Wino.Core.Domain.Entities.Mail; + +namespace Wino.Core.Domain.Interfaces; + +public interface IEmailTemplateService +{ + Task> GetEmailTemplatesAsync(); + Task GetEmailTemplateAsync(Guid templateId); + Task CreateEmailTemplateAsync(EmailTemplate template); + Task UpdateEmailTemplateAsync(EmailTemplate template); + Task DeleteEmailTemplateAsync(EmailTemplate template); +} diff --git a/Wino.Core.Domain/Interfaces/IFolderMenuItem.cs b/Wino.Core.Domain/Interfaces/IFolderMenuItem.cs index 7deff38d..26413208 100644 --- a/Wino.Core.Domain/Interfaces/IFolderMenuItem.cs +++ b/Wino.Core.Domain/Interfaces/IFolderMenuItem.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Collections.ObjectModel; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Models.Folders; @@ -20,7 +21,7 @@ public interface IBaseFolderMenuItem : IMenuItem int UnreadItemCount { get; set; } SpecialFolderType SpecialFolderType { get; } IEnumerable HandlingFolders { get; } - IEnumerable SubMenuItems { get; } + ObservableCollection SubMenuItems { get; } bool IsMoveTarget { get; } bool IsSticky { get; } bool IsSystemFolder { get; } diff --git a/Wino.Core.Domain/Interfaces/IFolderService.cs b/Wino.Core.Domain/Interfaces/IFolderService.cs index 33cf1f35..754a67e2 100644 --- a/Wino.Core.Domain/Interfaces/IFolderService.cs +++ b/Wino.Core.Domain/Interfaces/IFolderService.cs @@ -79,6 +79,13 @@ public interface IFolderService /// Folder to update. Task UpdateFolderAsync(MailItemFolder folder); + /// + /// Updates only IMAP HighestModeSeq for the given folder. + /// + /// Folder id to update. + /// Latest known mod-seq value. + Task UpdateFolderHighestModeSeqAsync(Guid folderId, long highestModeSeq); + /// /// Returns the active folder menu items for the given account for UI. /// diff --git a/Wino.Core.Domain/Interfaces/IGmailThreadingStrategy.cs b/Wino.Core.Domain/Interfaces/IGmailThreadingStrategy.cs deleted file mode 100644 index 17ec3b74..00000000 --- a/Wino.Core.Domain/Interfaces/IGmailThreadingStrategy.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Wino.Core.Domain.Interfaces; - -public interface IGmailThreadingStrategy : IThreadingStrategy { } diff --git a/Wino.Core.Domain/Interfaces/IImapAccountCreationDialog.cs b/Wino.Core.Domain/Interfaces/IImapAccountCreationDialog.cs deleted file mode 100644 index 21d9fb47..00000000 --- a/Wino.Core.Domain/Interfaces/IImapAccountCreationDialog.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Threading.Tasks; -using Wino.Core.Domain.Entities.Shared; - -namespace Wino.Core.Domain.Interfaces; - -public interface IImapAccountCreationDialog : IAccountCreationDialog -{ - /// - /// Returns the custom server information from the dialog.. - /// - /// Null if canceled. - Task GetCustomServerInformationAsync(); - - /// - /// Displays preparing folders page. - /// - void ShowPreparingFolders(); - - /// - /// Updates account properties for the welcome imap setup dialog and starts the setup. - /// - /// Account properties. - void StartImapConnectionSetup(MailAccount account); -} diff --git a/Wino.Core.Domain/Interfaces/IImapSynchronizationStrategyProvider.cs b/Wino.Core.Domain/Interfaces/IImapSynchronizationStrategyProvider.cs deleted file mode 100644 index 3fafc48e..00000000 --- a/Wino.Core.Domain/Interfaces/IImapSynchronizationStrategyProvider.cs +++ /dev/null @@ -1,11 +0,0 @@ -using MailKit.Net.Imap; - -namespace Wino.Core.Domain.Interfaces; - -/// -/// Provides a synchronization strategy for synchronizing IMAP folders based on the server capabilities. -/// -public interface IImapSynchronizationStrategyProvider -{ - IImapSynchronizerStrategy GetSynchronizationStrategy(IImapClient client); -} diff --git a/Wino.Core.Domain/Interfaces/IImapSynchronizerStrategy.cs b/Wino.Core.Domain/Interfaces/IImapSynchronizerStrategy.cs deleted file mode 100644 index 8b7769b6..00000000 --- a/Wino.Core.Domain/Interfaces/IImapSynchronizerStrategy.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using MailKit; -using MailKit.Net.Imap; -using Wino.Core.Domain.Entities.Mail; - -namespace Wino.Core.Domain.Interfaces; - -public interface IImapSynchronizerStrategy -{ - /// - /// Synchronizes given folder with the ImapClient client from the client pool. - /// - /// Client to perform sync with. I love Mira and Jasminka - /// Folder to synchronize. - /// Imap synchronizer that downloads messages. - /// Cancellation token. - /// List of new downloaded message ids that don't exist locally. - Task> HandleSynchronizationAsync(IImapClient client, - MailItemFolder folder, - IImapSynchronizer synchronizer, - CancellationToken cancellationToken = default); - - /// - /// Downloads given set of messages from the folder. - /// Folder is expected to be opened and synchronizer is connected. - /// - /// Synchronizer that performs the action. - /// Remote folder to download messages from. - /// Local folder to assign mails to. - /// Set of message uniqueids. - /// Cancellation token. - Task DownloadMessagesAsync(IImapSynchronizer synchronizer, - IMailFolder remoteFolder, - MailItemFolder localFolder, - UniqueIdSet uniqueIdSet, - CancellationToken cancellationToken = default); -} - diff --git a/Wino.Core.Domain/Interfaces/IImapThreadingStrategy.cs b/Wino.Core.Domain/Interfaces/IImapThreadingStrategy.cs deleted file mode 100644 index b613cd70..00000000 --- a/Wino.Core.Domain/Interfaces/IImapThreadingStrategy.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Wino.Core.Domain.Interfaces; - -public interface IImapThreadingStrategy : IThreadingStrategy { } diff --git a/Wino.Core.Domain/Interfaces/IKeyboardShortcutService.cs b/Wino.Core.Domain/Interfaces/IKeyboardShortcutService.cs new file mode 100644 index 00000000..eb242d84 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IKeyboardShortcutService.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; + +namespace Wino.Core.Domain.Interfaces; + +/// +/// Service for managing keyboard shortcuts for mail operations. +/// +public interface IKeyboardShortcutService +{ + /// + /// Gets all available keyboard shortcuts. + /// + /// Collection of keyboard shortcuts. + Task> GetKeyboardShortcutsAsync(); + + /// + /// Gets enabled keyboard shortcuts only. + /// + /// Collection of enabled keyboard shortcuts. + Task> GetEnabledKeyboardShortcutsAsync(); + + /// + /// Creates or updates a keyboard shortcut. + /// + /// The keyboard shortcut to save. + /// The saved keyboard shortcut. + Task SaveKeyboardShortcutAsync(KeyboardShortcut shortcut); + + /// + /// Deletes a keyboard shortcut. + /// + /// The ID of the shortcut to delete. + Task DeleteKeyboardShortcutAsync(Guid shortcutId); + + /// + /// Gets the keyboard shortcut for the given key combination in a specific mode. + /// + /// The application mode to search within. + /// The pressed key. + /// The modifier keys pressed. + /// The matching shortcut if found, otherwise null. + Task GetShortcutForKeyAsync(WinoApplicationMode mode, string key, ModifierKeys modifierKeys); + + /// + /// Checks if a key combination is already assigned to another shortcut. + /// + /// The application mode to check within. + /// The key to check. + /// The modifier keys to check. + /// Optional ID to exclude from the check (for updates). + /// True if the combination is already used, false otherwise. + Task IsKeyCombinationInUseAsync(WinoApplicationMode mode, string key, ModifierKeys modifierKeys, Guid? excludeShortcutId = null); + + /// + /// Creates default keyboard shortcuts for common mail operations. + /// + Task CreateDefaultShortcutsAsync(); + + /// + /// Resets all shortcuts to defaults. + /// + Task ResetToDefaultShortcutsAsync(); +} diff --git a/Wino.Core.Domain/Interfaces/ILaunchProtocolService.cs b/Wino.Core.Domain/Interfaces/ILaunchProtocolService.cs index 3e11c83d..39a75615 100644 --- a/Wino.Core.Domain/Interfaces/ILaunchProtocolService.cs +++ b/Wino.Core.Domain/Interfaces/ILaunchProtocolService.cs @@ -1,4 +1,5 @@ -using Wino.Core.Domain.Models.Launch; +using System; +using Wino.Core.Domain.Models.Launch; namespace Wino.Core.Domain.Interfaces; @@ -13,4 +14,5 @@ public interface ILaunchProtocolService /// Used to handle mailto links. /// MailToUri MailToUri { get; set; } + } diff --git a/Wino.Core.Domain/Interfaces/IMailDialogService.cs b/Wino.Core.Domain/Interfaces/IMailDialogService.cs index 53050931..dc60c422 100644 --- a/Wino.Core.Domain/Interfaces/IMailDialogService.cs +++ b/Wino.Core.Domain/Interfaces/IMailDialogService.cs @@ -1,10 +1,15 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Threading.Tasks; +using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models; +using Wino.Core.Domain.Models.Accounts; +using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Folders; namespace Wino.Core.Domain.Interfaces; @@ -17,6 +22,7 @@ public interface IMailDialogService : IDialogServiceBase // Custom dialogs Task ShowMoveMailFolderDialogAsync(List availableFolders); Task ShowAccountPickerDialogAsync(List availableAccounts); + Task ShowSingleCalendarPickerDialogAsync(List availableCalendarGroups); /// /// Displays a dialog to the user for reordering accounts. @@ -37,7 +43,7 @@ public interface IMailDialogService : IDialogServiceBase /// Presents a dialog to the user for signature creation/modification. /// /// Signature information. Null if canceled. - Task ShowSignatureEditorDialog(AccountSignature signatureModel = null); + Task ShowSignatureEditorDialog(AccountSignature? signatureModel = null); /// /// Presents a dialog to the user for account alias creation/modification. @@ -49,4 +55,26 @@ public interface IMailDialogService : IDialogServiceBase /// Presents a dialog to the user to show email source. /// Task ShowMessageSourceDialogAsync(string messageSource); + + /// + /// Presents a dialog to the user for keyboard shortcut creation/modification. + /// + /// Existing shortcut to edit, or null for new shortcut. + /// Dialog result with shortcut information. +#pragma warning disable CS8625 + Task ShowKeyboardShortcutDialogAsync(KeyboardShortcut existingShortcut = null); +#pragma warning restore CS8625 + + /// + /// Presents a dialog to the user for contact creation/modification. + /// + /// Existing contact to edit, or null for new contact. + /// Contact information. Null if canceled. + Task ShowEditContactDialogAsync(AccountContact? contact = null); + + Task ShowWinoAccountRegistrationDialogAsync(); + + Task ShowWinoAccountLoginDialogAsync(); + + Task ShowWinoAccountExportDialogAsync(); } diff --git a/Wino.Core.Domain/Interfaces/IMailHashContainer.cs b/Wino.Core.Domain/Interfaces/IMailHashContainer.cs new file mode 100644 index 00000000..9b4a0952 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IMailHashContainer.cs @@ -0,0 +1,9 @@ +using System; +using System.Collections.Generic; + +namespace Wino.Core.Domain.Interfaces; + +public interface IMailHashContainer +{ + IEnumerable GetContainingIds(); +} diff --git a/Wino.Core.Domain/Interfaces/IMailItemDisplayInformation.cs b/Wino.Core.Domain/Interfaces/IMailItemDisplayInformation.cs new file mode 100644 index 00000000..613f2be2 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IMailItemDisplayInformation.cs @@ -0,0 +1,27 @@ +using System; +using System.ComponentModel; +using Wino.Core.Domain.Entities.Shared; + +namespace Wino.Core.Domain.Interfaces; + +/// +/// Shared display contract for mail list item rendering. +/// Implemented by both single mail and thread mail view models. +/// +public interface IMailItemDisplayInformation : INotifyPropertyChanged +{ + string Subject { get; } + string FromName { get; } + string FromAddress { get; } + string PreviewText { get; } + bool IsRead { get; } + bool IsDraft { get; } + bool HasAttachments { get; } + bool IsCalendarEvent { get; } + bool IsFlagged { get; } + DateTime CreationDate { get; } + Guid? ContactPictureFileId { get; } + bool ThumbnailUpdatedEvent { get; } + bool IsThreadExpanded { get; } + AccountContact SenderContact { get; } +} diff --git a/Wino.Core.Domain/Interfaces/IMailListItemSorting.cs b/Wino.Core.Domain/Interfaces/IMailListItemSorting.cs new file mode 100644 index 00000000..5d4c24f5 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IMailListItemSorting.cs @@ -0,0 +1,9 @@ +using System; + +namespace Wino.Core.Domain.Interfaces; + +public interface IMailListItemSorting +{ + DateTime SortingDate { get; } + string SortingName { get; } +} diff --git a/Wino.Core.Domain/Interfaces/IMailService.cs b/Wino.Core.Domain/Interfaces/IMailService.cs index 544889ea..f5f4b30d 100644 --- a/Wino.Core.Domain/Interfaces/IMailService.cs +++ b/Wino.Core.Domain/Interfaces/IMailService.cs @@ -25,7 +25,7 @@ public interface IMailService /// Caution: This method is not safe. Use other overrides. /// Task> GetMailItemsAsync(IEnumerable mailCopyIds); - Task> FetchMailsAsync(MailListInitializationOptions options, CancellationToken cancellationToken = default); + Task> FetchMailsAsync(MailListInitializationOptions options, CancellationToken cancellationToken = default); /// /// Deletes all mail copies for all folders. @@ -131,6 +131,12 @@ public interface IMailService /// Draft MailCopy and Draft MimeMessage as base64. Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(Guid accountId, DraftCreationOptions draftCreationOptions); + /// + /// Finds a mail copy in the given account by RFC Message-Id. + /// Returns null when no local match exists. + /// + Task GetMailCopyByMessageIdAsync(Guid accountId, string messageId); + /// /// Returns ids /// @@ -162,4 +168,17 @@ public interface IMailService /// Retrieved MailCopy ids from search result. /// Result model that contains added and removed mail copy ids. Task GetGmailArchiveComparisonResultAsync(Guid archiveFolderId, List onlineArchiveMailIds); + + /// + /// Gets the most recent mail IDs for a folder. + /// Used for notification purposes after sync completes. + /// + /// Folder ID. + /// Number of recent mails to return. + Task> GetRecentMailIdsForFolderAsync(Guid folderId, int count); + + /// + /// Returns all mail copies for the account created before the given UTC date. + /// + Task> GetMailCopiesBeforeDateAsync(Guid accountId, DateTime cutoffDateUtc); } diff --git a/Wino.Core.Domain/Interfaces/IMenuOperation.cs b/Wino.Core.Domain/Interfaces/IMenuOperation.cs index 2d588edb..340260c3 100644 --- a/Wino.Core.Domain/Interfaces/IMenuOperation.cs +++ b/Wino.Core.Domain/Interfaces/IMenuOperation.cs @@ -4,4 +4,5 @@ public interface IMenuOperation { bool IsEnabled { get; } string Identifier { get; } + bool IsSecondaryMenuPreferred { get; } } diff --git a/Wino.Core.Domain/Interfaces/IMimeFileService.cs b/Wino.Core.Domain/Interfaces/IMimeFileService.cs index a7b5b8db..b27a9df8 100644 --- a/Wino.Core.Domain/Interfaces/IMimeFileService.cs +++ b/Wino.Core.Domain/Interfaces/IMimeFileService.cs @@ -59,6 +59,26 @@ public interface IMimeFileService /// Task DeleteMimeMessageAsync(Guid accountId, Guid fileId); + /// + /// Returns cached translated html for the given mime resource if it exists. + /// + Task GetTranslatedHtmlAsync(Guid accountId, Guid fileId, string targetLanguage, CancellationToken cancellationToken = default); + + /// + /// Saves translated html for the given mime resource. + /// + Task SaveTranslatedHtmlAsync(Guid accountId, Guid fileId, string targetLanguage, string html, CancellationToken cancellationToken = default); + + /// + /// Returns cached summary text for the given mime resource if it exists. + /// + Task GetSummaryTextAsync(Guid accountId, Guid fileId, CancellationToken cancellationToken = default); + + /// + /// Saves summary text for the given mime resource. + /// + Task SaveSummaryTextAsync(Guid accountId, Guid fileId, string summary, CancellationToken cancellationToken = default); + /// /// Prepares the final model containing rendering details. /// diff --git a/Wino.Core.Domain/Interfaces/IMimeStorageService.cs b/Wino.Core.Domain/Interfaces/IMimeStorageService.cs new file mode 100644 index 00000000..9c67ebea --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IMimeStorageService.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Wino.Core.Domain.Interfaces; + +public interface IMimeStorageService +{ + Task GetMimeRootPathAsync(); + Task> GetAccountsMimeStorageSizesAsync(IEnumerable accountIds); + Task DeleteAccountMimeStorageAsync(Guid accountId); + Task DeleteAccountMimeStorageOlderThanAsync(Guid accountId, DateTime cutoffDateUtc); +} diff --git a/Wino.Core.Domain/Interfaces/INativeAppService.cs b/Wino.Core.Domain/Interfaces/INativeAppService.cs index cfbea72f..2c5c4662 100644 --- a/Wino.Core.Domain/Interfaces/INativeAppService.cs +++ b/Wino.Core.Domain/Interfaces/INativeAppService.cs @@ -22,4 +22,9 @@ public interface INativeAppService /// This is used to display WAM broker dialog on running UWP app called by a windowless server code. /// Func GetCoreWindowHwnd { get; set; } + + /// + /// Gets the folder path where calendar attachments are stored. + /// + string GetCalendarAttachmentsFolderPath(); } diff --git a/Wino.Core.Domain/Interfaces/INewThemeService.cs b/Wino.Core.Domain/Interfaces/INewThemeService.cs new file mode 100644 index 00000000..037b4bee --- /dev/null +++ b/Wino.Core.Domain/Interfaces/INewThemeService.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models.Personalization; + +namespace Wino.Core.Domain.Interfaces; + +public interface INewThemeService : IInitializeAsync +{ + event EventHandler ElementThemeChanged; + event EventHandler AccentColorChanged; + event EventHandler BackdropChanged; + + Task> GetAvailableThemesAsync(); + Task CreateNewCustomThemeAsync(string themeName, string accentColor, byte[] wallpaperData); + Task> GetCurrentCustomThemesAsync(); + List GetAvailableAccountColors(); + Task ApplyCustomThemeAsync(bool isInitializing); + + // Window Backdrop Management + WindowBackdropType CurrentBackdropType { get; set; } + void ApplyBackdrop(WindowBackdropType backdropType); + + // Settings + ApplicationElementTheme RootTheme { get; set; } + Guid? CurrentApplicationThemeId { get; set; } + string AccentColor { get; set; } + string GetSystemAccentColorHex(); + bool IsCustomTheme { get; } + + // Improved accent color management + Task SetAccentColorAsync(string hexColor, bool preserveTheme = true); + + // Title bar color management + void UpdateSystemCaptionButtonColors(); + + // Backdrop management + List GetAvailableBackdropTypes(); + + /// + /// Re-applies the current theme (backdrop, root theme, accent, caption colors) + /// to the currently active window. Use after a window transition. + /// + Task ApplyThemeToActiveWindowAsync(); +} diff --git a/Wino.Core.Domain/Interfaces/INotificationBuilder.cs b/Wino.Core.Domain/Interfaces/INotificationBuilder.cs index 86961ad7..ed41e474 100644 --- a/Wino.Core.Domain/Interfaces/INotificationBuilder.cs +++ b/Wino.Core.Domain/Interfaces/INotificationBuilder.cs @@ -1,7 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; -using Wino.Core.Domain.Models.MailItem; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Entities.Shared; namespace Wino.Core.Domain.Interfaces; @@ -10,7 +12,7 @@ public interface INotificationBuilder /// /// Creates toast notifications for new mails. /// - Task CreateNotificationsAsync(Guid inboxFolderId, IEnumerable newMailItems); + Task CreateNotificationsAsync(IEnumerable newMailItems); /// /// Gets the unread Inbox messages for each account and updates the taskbar icon. @@ -18,13 +20,30 @@ public interface INotificationBuilder /// Task UpdateTaskbarIconBadgeAsync(); - /// - /// Creates test notification for test purposes. - /// - Task CreateTestNotificationAsync(string title, string message); - /// /// Removes the toast notification for a specific mail by unique id. /// void RemoveNotification(Guid mailUniqueId); + + /// + /// Shows a notification that the account requires attention. + /// + /// Account that needs attention. + void CreateAttentionRequiredNotification(MailAccount account); + + /// + /// Shows a notification when WebView2 runtime is unavailable. + /// + void CreateWebView2RuntimeMissingNotification(); + + /// + /// Shows a notification when a Microsoft Store update is available. + /// + void CreateStoreUpdateNotification(); + + /// + /// Creates a calendar reminder toast for the specified calendar item. + /// + Task CreateCalendarReminderNotificationAsync(CalendarItem calendarItem, long reminderDurationInSeconds); } + diff --git a/Wino.Core.Domain/Interfaces/IOutlookThreadingStrategy.cs b/Wino.Core.Domain/Interfaces/IOutlookThreadingStrategy.cs deleted file mode 100644 index 03525f6f..00000000 --- a/Wino.Core.Domain/Interfaces/IOutlookThreadingStrategy.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Wino.Core.Domain.Interfaces; - -public interface IOutlookThreadingStrategy : IThreadingStrategy { } diff --git a/Wino.Core.Domain/Interfaces/IPreferencesService.cs b/Wino.Core.Domain/Interfaces/IPreferencesService.cs index 95def0d5..b4f93412 100644 --- a/Wino.Core.Domain/Interfaces/IPreferencesService.cs +++ b/Wino.Core.Domain/Interfaces/IPreferencesService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Models.Calendar; @@ -6,7 +6,7 @@ using Wino.Core.Domain.Models.Reader; namespace Wino.Core.Domain.Interfaces; -public interface IPreferencesService: INotifyPropertyChanged +public interface IPreferencesService : INotifyPropertyChanged { /// /// When any of the preferences are changed. @@ -30,11 +30,6 @@ public interface IPreferencesService: INotifyPropertyChanged /// bool IsNavigationPaneOpened { get; set; } - /// - /// Setting: Gets or sets what should happen to server app when the client is terminated. - /// - ServerBackgroundMode ServerTerminationBehavior { get; set; } - /// /// Setting: Preferred time format for mail or calendar header display. /// @@ -57,6 +52,47 @@ public interface IPreferencesService: INotifyPropertyChanged /// int EmailSyncIntervalMinutes { get; set; } + /// + /// Setting: Default application mode to open when activation does not specify one. + /// + WinoApplicationMode DefaultApplicationMode { get; set; } + + /// + /// Setting: Whether Microsoft Store update notifications should be shown. + /// + bool IsStoreUpdateNotificationsEnabled { get; set; } + + /// + /// Setting: Whether the Wino account profile button in the shell title bar should be hidden. + /// + bool IsWinoAccountButtonHidden { get; set; } + + /// + /// Setting: Default target language code used for AI translation actions. + /// + string AiDefaultTranslationLanguageCode { get; set; } + + /// + /// Setting: Preferred target language code for AI summarize actions. + /// + string AiSummarizeLanguageCode { get; set; } + + /// + /// Setting: Preferred folder path used when saving AI summaries. + /// + string AiSummarySavePath { get; set; } + + /// + /// Serializes the current syncable preferences snapshot. + /// + string ExportPreferences(); + + /// + /// Deserializes and applies a preferences snapshot. + /// Returns the applied and failed property counts. + /// + (int appliedCount, int failedCount) ImportPreferences(string settingsJson); + #endregion #region Mail @@ -96,11 +132,6 @@ public interface IPreferencesService: INotifyPropertyChanged /// bool IsShowPreviewEnabled { get; set; } - /// - /// Setting: Enable/disable semantic zoom on clicking date headers. - /// - bool IsSemanticZoomEnabled { get; set; } - /// /// Setting: Set whether 'img' tags in rendered HTMLs should be removed. /// @@ -151,11 +182,6 @@ public interface IPreferencesService: INotifyPropertyChanged /// MailOperation RightHoverAction { get; set; } - /// - /// Setting: Whether Mailkit Protocol Logger is enabled for ImapTestService or not. - /// - bool IsMailkitProtocolLoggerEnabled { get; set; } - /// /// Setting: Which entity id (merged account or folder) should be expanded automatically on startup. /// @@ -215,12 +241,34 @@ public interface IPreferencesService: INotifyPropertyChanged #region Calendar DayOfWeek FirstDayOfWeek { get; set; } + bool IsWorkingHoursEnabled { get; set; } TimeSpan WorkingHourStart { get; set; } TimeSpan WorkingHourEnd { get; set; } DayOfWeek WorkingDayStart { get; set; } DayOfWeek WorkingDayEnd { get; set; } double HourHeight { get; set; } + string CalendarTimedDayHeaderDateFormat { get; set; } + /// + /// Setting: Default reminder duration in seconds for new calendar events. + /// Set to 0 to disable default reminders. + /// + long DefaultReminderDurationInSeconds { get; set; } + + /// + /// Setting: Default snooze duration in minutes for calendar reminder notifications. + /// + int DefaultSnoozeDurationInMinutes { get; set; } + + /// + /// Setting: How the New Event button chooses a calendar. + /// + NewEventButtonBehavior NewEventButtonBehavior { get; set; } + + /// + /// Setting: Default calendar used when New Event is configured to always use a specific calendar. + /// + Guid? DefaultNewEventCalendarId { get; set; } CalendarSettings GetCurrentCalendarSettings(); diff --git a/Wino.Core.Domain/Interfaces/IRequestBundle.cs b/Wino.Core.Domain/Interfaces/IRequestBundle.cs index f88a3dc2..955b89d8 100644 --- a/Wino.Core.Domain/Interfaces/IRequestBundle.cs +++ b/Wino.Core.Domain/Interfaces/IRequestBundle.cs @@ -1,4 +1,6 @@ -using Wino.Core.Domain.Entities.Mail; +using System; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Enums; namespace Wino.Core.Domain.Interfaces; @@ -63,3 +65,10 @@ public interface IFolderActionRequest : IRequestBase FolderSynchronizerOperation Operation { get; } } + +public interface ICalendarActionRequest : IRequestBase +{ + CalendarItem Item { get; } + Guid? LocalCalendarItemId { get; } + CalendarSynchronizerOperation Operation { get; } +} diff --git a/Wino.Core.Domain/Interfaces/IRetryExecutor.cs b/Wino.Core.Domain/Interfaces/IRetryExecutor.cs new file mode 100644 index 00000000..e7d31c34 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IRetryExecutor.cs @@ -0,0 +1,60 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Wino.Core.Domain.Models.Retry; +using Wino.Core.Domain.Models.Synchronization; + +namespace Wino.Core.Domain.Interfaces; + +/// +/// Executes operations with automatic retry and error handling support. +/// +public interface IRetryExecutor +{ + /// + /// Executes an operation with automatic retry based on the specified policy. + /// + /// The return type of the operation. + /// The async operation to execute. + /// The retry policy to apply. + /// Factory to create error context from exceptions. + /// Optional error handler for custom error processing. + /// Cancellation token. + /// The result of the operation. + /// Thrown when all retries are exhausted or a fatal error occurs. + Task ExecuteWithRetryAsync( + Func> operation, + RetryPolicy policy, + Func errorContextFactory, + ISynchronizerErrorHandlerFactory errorHandler = null, + CancellationToken cancellationToken = default); + + /// + /// Executes an operation with automatic retry based on the specified policy (void return). + /// + /// The async operation to execute. + /// The retry policy to apply. + /// Factory to create error context from exceptions. + /// Optional error handler for custom error processing. + /// Cancellation token. + /// Thrown when all retries are exhausted or a fatal error occurs. + Task ExecuteWithRetryAsync( + Func operation, + RetryPolicy policy, + Func errorContextFactory, + ISynchronizerErrorHandlerFactory errorHandler = null, + CancellationToken cancellationToken = default); + + /// + /// Executes an operation with default retry policy. + /// + /// The return type of the operation. + /// The async operation to execute. + /// Factory to create error context from exceptions. + /// Cancellation token. + /// The result of the operation. + Task ExecuteWithRetryAsync( + Func> operation, + Func errorContextFactory, + CancellationToken cancellationToken = default); +} diff --git a/Wino.Core.Domain/Interfaces/ISettingsBuilderService.cs b/Wino.Core.Domain/Interfaces/ISettingsBuilderService.cs deleted file mode 100644 index 7cca8604..00000000 --- a/Wino.Core.Domain/Interfaces/ISettingsBuilderService.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; -using Wino.Core.Domain.Models.Settings; - -namespace Wino.Core.Domain.Interfaces; - -public interface ISettingsBuilderService -{ - List GetSettingItems(); -} diff --git a/Wino.Core.Domain/Interfaces/IShellClient.cs b/Wino.Core.Domain/Interfaces/IShellClient.cs new file mode 100644 index 00000000..ab9ad0c9 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IShellClient.cs @@ -0,0 +1,79 @@ +#nullable enable +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Input; +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.MenuItems; +using Wino.Core.Domain.Models; +using Wino.Core.Domain.Models.Calendar; +using Wino.Core.Domain.Models.Folders; +using Wino.Core.Domain.Models.Navigation; + +namespace Wino.Core.Domain.Interfaces; + +public interface IShellClient : INotifyPropertyChanged +{ + WinoApplicationMode Mode { get; } + IDispatcher Dispatcher { get; set; } + MenuItemCollection? MenuItems { get; } + object? SelectedMenuItem { get; set; } + bool HandlesNavigationSelection { get; } + + void Activate(ShellModeActivationContext activationContext); + void Deactivate(); + Task HandleNavigationItemInvokedAsync(IMenuItem? menuItem); + Task HandleNavigationSelectionChangedAsync(IMenuItem? menuItem); + Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args); +} + +public interface IMailShellClient : IShellClient +{ + IMenuItem CreatePrimaryMenuItem { get; } + + IEnumerable GetFolderContextMenuActions(IBaseFolderMenuItem folder); + Task HandleAccountCreatedAsync(MailAccount createdAccount); + Task NavigateFolderAsync(IBaseFolderMenuItem baseFolderMenuItem, TaskCompletionSource? folderInitAwaitTask = null); + Task ChangeLoadedAccountAsync(IAccountMenuItem clickedBaseAccountMenuItem, bool navigateInbox = true); + Task PerformFolderOperationAsync(FolderOperation operation, IBaseFolderMenuItem folderMenuItem); + Task PerformMoveOperationAsync(IEnumerable items, IBaseFolderMenuItem targetFolderMenuItem); + Task CreateNewMailForAsync(MailAccount account); +} + +public interface ICalendarShellClient : IShellClient +{ + IStatePersistanceService StatePersistenceService { get; } + IEnumerable DateNavigationHeaderItems { get; } + int SelectedDateNavigationHeaderIndex { get; } + VisibleDateRange? CurrentVisibleRange { get; } + string VisibleDateRangeText { get; } + bool CanSynchronizeCalendars { get; } + ICommand SyncCommand { get; } + ICommand TodayClickedCommand { get; } + ICommand DateClickedCommand { get; } + ICommand PreviousDateRangeCommand { get; } + ICommand NextDateRangeCommand { get; } + IEnumerable GroupedAccountCalendars { get; } +} + +public interface IShellViewModel +{ + WinoApplicationMode CurrentMode { get; } + IShellClient CurrentClient { get; } + MenuItemCollection? CurrentMenuItems { get; } + object? SelectedMenuItem { get; set; } + + void SetCurrentMode(WinoApplicationMode mode); + IShellClient GetClient(WinoApplicationMode mode); +} + +public interface IShellHost +{ + bool HasShellContent { get; } + + void ActivateMode(WinoApplicationMode mode, ShellModeActivationContext activationContext); +} diff --git a/Wino.Core.Domain/Interfaces/ISmimeCertificateService.cs b/Wino.Core.Domain/Interfaces/ISmimeCertificateService.cs new file mode 100644 index 00000000..9b9fee6a --- /dev/null +++ b/Wino.Core.Domain/Interfaces/ISmimeCertificateService.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; + +namespace Wino.Core.Domain.Interfaces; + +public interface ISmimeCertificateService +{ + public IEnumerable GetCertificates(StoreName storeName = StoreName.My, StoreLocation storeLocation = StoreLocation.CurrentUser, string emailAddress = null); + public void ImportCertificate(string fileExtension, byte[] rawData, string password = null, StoreName storeName = StoreName.My, StoreLocation storeLocation = StoreLocation.CurrentUser); + public void RemoveCertificate(string thumbprint, StoreName storeName = StoreName.My, StoreLocation storeLocation = StoreLocation.CurrentUser); +} diff --git a/Wino.Core.Domain/Interfaces/IStatePersistenceService.cs b/Wino.Core.Domain/Interfaces/IStatePersistenceService.cs index 8ed05626..834415d0 100644 --- a/Wino.Core.Domain/Interfaces/IStatePersistenceService.cs +++ b/Wino.Core.Domain/Interfaces/IStatePersistenceService.cs @@ -18,16 +18,26 @@ public interface IStatePersistanceService : INotifyPropertyChanged /// string CoreWindowTitle { get; set; } + /// + /// App mode title shown in the title bar. + /// + string AppModeTitle { get; set; } + /// /// When only reader page is visible in small sized window. /// bool IsReaderNarrowed { get; set; } /// - /// Should display back button on the shell title bar. + /// Current application mode (Mail or Calendar). + /// Not persisted to configuration, only kept in memory. /// - bool IsBackButtonVisible { get; } + WinoApplicationMode ApplicationMode { get; set; } + /// + /// Whether event details page is visible in Calendar mode. + /// + bool IsEventDetailsVisible { get; set; } /// /// Setting: Opened pane length for the navigation view. @@ -54,4 +64,5 @@ public interface IStatePersistanceService : INotifyPropertyChanged /// Setting: Calendar display count for the day view. /// int DayDisplayCount { get; set; } + } diff --git a/Wino.Core.Domain/Interfaces/IStoreManagementService.cs b/Wino.Core.Domain/Interfaces/IStoreManagementService.cs index ea61f1b7..bd945540 100644 --- a/Wino.Core.Domain/Interfaces/IStoreManagementService.cs +++ b/Wino.Core.Domain/Interfaces/IStoreManagementService.cs @@ -1,6 +1,6 @@ -using System.Threading.Tasks; +#nullable enable +using System.Threading.Tasks; using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Models.Store; namespace Wino.Core.Domain.Interfaces; @@ -9,10 +9,20 @@ public interface IStoreManagementService /// /// Checks whether user has the type of an add-on purchased. /// - Task HasProductAsync(StoreProductType productType); + Task HasProductAsync(WinoAddOnProductType productType); /// /// Attempts to purchase the given add-on. /// - Task PurchaseAsync(StoreProductType productType); + Task PurchaseAsync(WinoAddOnProductType productType); + + /// + /// Requests a Microsoft Store collections ID key for the current customer. + /// + Task GetCustomerCollectionsIdAsync(string serviceTicket, string publisherUserId); + + /// + /// Requests a Microsoft Store purchase ID key for the current customer. + /// + Task GetCustomerPurchaseIdAsync(string serviceTicket, string publisherUserId); } diff --git a/Wino.Core.Domain/Interfaces/IStoreUpdateService.cs b/Wino.Core.Domain/Interfaces/IStoreUpdateService.cs new file mode 100644 index 00000000..1977b13e --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IStoreUpdateService.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + +namespace Wino.Core.Domain.Interfaces; + +public interface IStoreUpdateService +{ + bool HasAvailableUpdate { get; } + + Task RefreshAvailabilityAsync(bool showNotification = false); + + Task StartUpdateAsync(); +} diff --git a/Wino.Core.Domain/Interfaces/ISynchronizationManager.cs b/Wino.Core.Domain/Interfaces/ISynchronizationManager.cs new file mode 100644 index 00000000..bfacad73 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/ISynchronizationManager.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models.Authentication; +using Wino.Core.Domain.Models.Connectivity; +using Wino.Core.Domain.Models.Synchronization; + +namespace Wino.Core.Domain.Interfaces; + +/// +/// Interface for the singleton synchronization manager that handles synchronizer instances and operations. +/// +public interface ISynchronizationManager +{ + /// + /// Initializes the SynchronizationManager with required dependencies. + /// + Task InitializeAsync(ISynchronizerFactory synchronizerFactory, + IImapTestService imapTestService, + IAccountService accountService, + INotificationBuilder notificationBuilder, + IAuthenticationProvider authenticationProvider); + + /// + /// Tests IMAP server connectivity for the given server information. + /// + Task TestImapConnectivityAsync(CustomServerInformation serverInformation, bool allowSSLHandshake); + + /// + /// Starts a new mail synchronization for the given account. + /// + Task SynchronizeMailAsync(MailSynchronizationOptions options, + CancellationToken cancellationToken = default); + + /// + /// Checks if there is an ongoing synchronization for the given account. + /// + bool IsAccountSynchronizing(Guid accountId); + + /// + /// Queues a mail action request to the corresponding account's synchronizer with optional synchronization triggering. + /// + Task QueueRequestAsync(IRequestBase request, Guid accountId, bool triggerSynchronization); + + /// + /// Handles folder synchronization for the given account. + /// + Task SynchronizeFoldersAsync(Guid accountId, + CancellationToken cancellationToken = default); + + /// + /// Handles alias synchronization for the given account. + /// + Task SynchronizeAliasesAsync(Guid accountId, + CancellationToken cancellationToken = default); + + /// + /// Handles profile synchronization for the given account. + /// + Task SynchronizeProfileAsync(Guid accountId, + CancellationToken cancellationToken = default); + + /// + /// Handles calendar synchronization for the given account. + /// + Task SynchronizeCalendarAsync(CalendarSynchronizationOptions options, + CancellationToken cancellationToken = default); + + /// + /// Downloads a MIME message for the given mail item. + /// + Task DownloadMimeMessageAsync(MailCopy mailItem, Guid accountId, + CancellationToken cancellationToken = default); + + /// + /// Creates a new synchronizer for a newly added account. + /// + IWinoSynchronizerBase CreateSynchronizerForAccount(MailAccount account); + + /// + /// Cancels ongoing synchronizations for the given account. + /// + Task CancelSynchronizationsAsync(Guid accountId); + + /// + /// Destroys the synchronizer for the given account. + /// + Task DestroySynchronizerAsync(Guid accountId); + + /// + /// Gets all cached synchronizers. + /// + IEnumerable GetAllSynchronizers(); + + /// + /// Gets a synchronizer for the given account ID. + /// + Task GetSynchronizerAsync(Guid accountId); + + /// + /// Handles OAuth authentication for the specified provider. + /// + Task HandleAuthorizationAsync(MailProviderType providerType, + MailAccount account = null, + bool proposeCopyAuthorizationURL = false); +} diff --git a/Wino.Core.Domain/Interfaces/ISynchronizerErrorHandlerFactory.cs b/Wino.Core.Domain/Interfaces/ISynchronizerErrorHandlerFactory.cs new file mode 100644 index 00000000..79f32f84 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/ISynchronizerErrorHandlerFactory.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; +using Wino.Core.Domain.Models.Synchronization; + +namespace Wino.Core.Domain.Interfaces; + +public interface ISynchronizerErrorHandlerFactory +{ + Task HandleErrorAsync(SynchronizerErrorContext error); +} diff --git a/Wino.Core.Domain/Interfaces/IThemeService.cs b/Wino.Core.Domain/Interfaces/IThemeService.cs deleted file mode 100644 index 322643af..00000000 --- a/Wino.Core.Domain/Interfaces/IThemeService.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Models.Personalization; - -namespace Wino.Core.Domain.Interfaces; - -public interface IThemeService : IInitializeAsync -{ - event EventHandler ElementThemeChanged; - event EventHandler AccentColorChanged; - - Task> GetAvailableThemesAsync(); - Task CreateNewCustomThemeAsync(string themeName, string accentColor, byte[] wallpaperData); - Task> GetCurrentCustomThemesAsync(); - List GetAvailableAccountColors(); - Task ApplyCustomThemeAsync(bool isInitializing); - - // Settings - ApplicationElementTheme RootTheme { get; set; } - Guid CurrentApplicationThemeId { get; set; } - string AccentColor { get; set; } - string GetSystemAccentColorHex(); - bool IsCustomTheme { get; } -} diff --git a/Wino.Core.Domain/Interfaces/IThreadingStrategy.cs b/Wino.Core.Domain/Interfaces/IThreadingStrategy.cs deleted file mode 100644 index c27b8e56..00000000 --- a/Wino.Core.Domain/Interfaces/IThreadingStrategy.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Wino.Core.Domain.Entities.Mail; -using Wino.Core.Domain.Models.Folders; -using Wino.Core.Domain.Models.MailItem; - -namespace Wino.Core.Domain.Interfaces; - -public interface IThreadingStrategy -{ - /// - /// Attach thread mails to the list. - /// - /// Original mails. - /// Original mails with thread mails. - Task> ThreadItemsAsync(List items, IMailItemFolder threadingForFolder); - bool ShouldThreadWithItem(IMailItem originalItem, IMailItem targetItem); -} diff --git a/Wino.Core.Domain/Interfaces/IThreadingStrategyProvider.cs b/Wino.Core.Domain/Interfaces/IThreadingStrategyProvider.cs deleted file mode 100644 index cca544f5..00000000 --- a/Wino.Core.Domain/Interfaces/IThreadingStrategyProvider.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Wino.Core.Domain.Enums; - -namespace Wino.Core.Domain.Interfaces; - -public interface IThreadingStrategyProvider -{ - /// - /// Returns corresponding threading strategy that applies to given provider type. - /// - /// Provider type. - IThreadingStrategy GetStrategy(MailProviderType mailProviderType); -} diff --git a/Wino.Core.Domain/Interfaces/IUpdateManager.cs b/Wino.Core.Domain/Interfaces/IUpdateManager.cs new file mode 100644 index 00000000..d0e4a900 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IUpdateManager.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; +using Wino.Core.Domain.Models.Updates; +using System.Collections.Generic; + +namespace Wino.Core.Domain.Interfaces; + +public interface IUpdateManager +{ + /// Loads and parses the update notes for the current version from the bundled asset file. + Task GetLatestUpdateNotesAsync(); + + /// Loads and parses the app feature highlights from the bundled asset file. + Task> GetFeaturesAsync(); + + /// Returns true if the current version's update notes have not yet been shown to the user. + bool ShouldShowUpdateNotes(); + + /// Stores a flag in local settings indicating the update notes for the current version have been seen. + void MarkUpdateNotesAsSeen(); + +} diff --git a/Wino.Core.Domain/Interfaces/IWebView2RuntimeValidatorService.cs b/Wino.Core.Domain/Interfaces/IWebView2RuntimeValidatorService.cs new file mode 100644 index 00000000..aafa0669 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IWebView2RuntimeValidatorService.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + +namespace Wino.Core.Domain.Interfaces; + +public interface IWebView2RuntimeValidatorService +{ + /// + /// Validates whether WebView2 runtime is installed and available for use. + /// + Task IsRuntimeAvailableAsync(); +} + diff --git a/Wino.Core.Domain/Interfaces/IWinoAccountApiClient.cs b/Wino.Core.Domain/Interfaces/IWinoAccountApiClient.cs new file mode 100644 index 00000000..79d21e29 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IWinoAccountApiClient.cs @@ -0,0 +1,33 @@ +#nullable enable +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Wino.Core.Domain.Models.Accounts; +using Wino.Mail.Api.Contracts.Ai; +using Wino.Mail.Api.Contracts.Auth; +using Wino.Mail.Api.Contracts.Common; +using Wino.Mail.Api.Contracts.Users; + +namespace Wino.Core.Domain.Interfaces; + +public interface IWinoAccountApiClient +{ + Task> RegisterAsync(string email, string password, CancellationToken cancellationToken = default); + Task> LoginAsync(string email, string password, CancellationToken cancellationToken = default); + Task> RefreshAsync(string refreshToken, CancellationToken cancellationToken = default); + Task> ResendEmailConfirmationAsync(string endpoint, string ticket, CancellationToken cancellationToken = default); + Task> ForgotPasswordAsync(string email, CancellationToken cancellationToken = default); + Task> LogoutAsync(string refreshToken, CancellationToken cancellationToken = default); + Task> GetCurrentUserAsync(CancellationToken cancellationToken = default); + Task> GetAiStatusAsync(CancellationToken cancellationToken = default); + Task> SummarizeAsync(string html, string targetLanguage, CancellationToken cancellationToken = default); + Task> TranslateAsync(string html, string targetLanguage, CancellationToken cancellationToken = default); + Task> RewriteAsync(string html, string mode, CancellationToken cancellationToken = default); + Task> CreateCollectionsIdTicketAsync(CancellationToken cancellationToken = default); + Task> CreatePurchaseIdTicketAsync(CancellationToken cancellationToken = default); + Task> SyncStoreEntitlementsAsync(string? storeIdKey, string? purchaseIdKey, CancellationToken cancellationToken = default); + Task GetSettingsAsync(CancellationToken cancellationToken = default); + Task SaveSettingsAsync(string settingsJson, CancellationToken cancellationToken = default); + Task GetMailboxesAsync(CancellationToken cancellationToken = default); + Task ReplaceMailboxesAsync(ReplaceUserMailboxesRequestDto request, CancellationToken cancellationToken = default); +} diff --git a/Wino.Core.Domain/Interfaces/IWinoAccountDataSyncService.cs b/Wino.Core.Domain/Interfaces/IWinoAccountDataSyncService.cs new file mode 100644 index 00000000..da08a933 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IWinoAccountDataSyncService.cs @@ -0,0 +1,11 @@ +using System.Threading; +using System.Threading.Tasks; +using Wino.Core.Domain.Models.Accounts; + +namespace Wino.Core.Domain.Interfaces; + +public interface IWinoAccountDataSyncService +{ + Task ExportAsync(WinoAccountSyncSelection selection, CancellationToken cancellationToken = default); + Task ImportAsync(WinoAccountSyncSelection selection, CancellationToken cancellationToken = default); +} diff --git a/Wino.Core.Domain/Interfaces/IWinoAccountProfileService.cs b/Wino.Core.Domain/Interfaces/IWinoAccountProfileService.cs new file mode 100644 index 00000000..d91ffa0e --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IWinoAccountProfileService.cs @@ -0,0 +1,38 @@ +#nullable enable +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Models.Accounts; +using Wino.Mail.Api.Contracts.Ai; +using Wino.Mail.Api.Contracts.Auth; +using Wino.Mail.Api.Contracts.Common; +using Wino.Mail.Api.Contracts.Users; + +namespace Wino.Core.Domain.Interfaces; + +public interface IWinoAccountProfileService +{ + Task RegisterAsync(string email, string password, CancellationToken cancellationToken = default); + Task LoginAsync(string email, string password, CancellationToken cancellationToken = default); + Task RefreshAsync(CancellationToken cancellationToken = default); + Task RefreshProfileAsync(CancellationToken cancellationToken = default); + Task> ResendEmailConfirmationAsync(string endpoint, string ticket, CancellationToken cancellationToken = default); + Task> ForgotPasswordAsync(string email, CancellationToken cancellationToken = default); + Task GetActiveAccountAsync(); + Task GetAuthenticatedAccountAsync(CancellationToken cancellationToken = default); + Task HasActiveAccountAsync(); + Task> GetCurrentUserAsync(CancellationToken cancellationToken = default); + Task> GetAiStatusAsync(CancellationToken cancellationToken = default); + Task> SummarizeAsync(string html, string targetLanguage, CancellationToken cancellationToken = default); + Task> TranslateAsync(string html, string targetLanguage, CancellationToken cancellationToken = default); + Task> RewriteAsync(string html, string mode, CancellationToken cancellationToken = default); + Task> SyncStoreEntitlementsAsync(CancellationToken cancellationToken = default); + Task GetSettingsAsync(CancellationToken cancellationToken = default); + Task SaveSettingsAsync(string settingsJson, CancellationToken cancellationToken = default); + Task GetMailboxesAsync(CancellationToken cancellationToken = default); + Task ReplaceMailboxesAsync(ReplaceUserMailboxesRequestDto request, CancellationToken cancellationToken = default); + Task ProcessBillingCallbackAsync(Uri callbackUri, CancellationToken cancellationToken = default); + Task SignOutAsync(CancellationToken cancellationToken = default); +} diff --git a/Wino.Core.Domain/Interfaces/IWinoNavigationService.cs b/Wino.Core.Domain/Interfaces/IWinoNavigationService.cs index 28b015c7..458f820c 100644 --- a/Wino.Core.Domain/Interfaces/IWinoNavigationService.cs +++ b/Wino.Core.Domain/Interfaces/IWinoNavigationService.cs @@ -8,9 +8,12 @@ public interface INavigationService { bool Navigate(WinoPage page, object parameter = null, - NavigationReferenceFrame frame = NavigationReferenceFrame.ShellFrame, + NavigationReferenceFrame frame = NavigationReferenceFrame.InnerShellFrame, NavigationTransitionType transition = NavigationTransitionType.None); Type GetPageType(WinoPage winoPage); - void GoBack(); + bool ChangeApplicationMode(WinoApplicationMode mode); + bool ChangeApplicationMode(WinoApplicationMode mode, ShellModeActivationContext activationContext); + bool CanGoBack(); + void GoBack(NavigationTransitionEffect slideEffect = NavigationTransitionEffect.FromRight); } diff --git a/Wino.Core.Domain/Interfaces/IWinoRequestDelegator.cs b/Wino.Core.Domain/Interfaces/IWinoRequestDelegator.cs index de50c517..0cf1cabf 100644 --- a/Wino.Core.Domain/Interfaces/IWinoRequestDelegator.cs +++ b/Wino.Core.Domain/Interfaces/IWinoRequestDelegator.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.MailItem; @@ -29,4 +30,10 @@ public interface IWinoRequestDelegator /// /// Folder prep request. Task ExecuteAsync(FolderOperationPreperationRequest folderOperationPreperationRequest); + + /// + /// Prepares and queues calendar action requests for proper synchronizers. + /// + /// Calendar preparation request. + Task ExecuteAsync(CalendarOperationPreparationRequest calendarOperationPreparationRequest); } diff --git a/Wino.Core.Domain/Interfaces/IWinoServerConnectionManager.cs b/Wino.Core.Domain/Interfaces/IWinoServerConnectionManager.cs deleted file mode 100644 index 69949a0a..00000000 --- a/Wino.Core.Domain/Interfaces/IWinoServerConnectionManager.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Models.Server; - -namespace Wino.Core.Domain.Interfaces; - -public interface IWinoServerConnectionManager -{ - /// - /// When the connection status changes, this event will be triggered. - /// - event EventHandler StatusChanged; - - /// - /// Gets the connection status. - /// - WinoServerConnectionStatus Status { get; } - - /// - /// Launches Full Trust process (Wino Server) and awaits connection completion. - /// If connection is not established in 10 seconds, it will return false. - /// If the server process is already running, it'll connect to existing one. - /// If the server process is not running, it'll be launched and connection establishment is awaited. - /// - /// Whether connection is established or not. - Task ConnectAsync(); - - /// - /// Queues a new user request to be processed by Wino Server. - /// Healthy connection must present before calling this method. - /// - /// Request to queue for synchronizer in the server. - /// Account id to queueu request for. - Task QueueRequestAsync(IRequestBase request, Guid accountId); - - /// - /// Returns response from server for the given request. - /// - /// Response type. - /// Request type. - /// Request type. - /// Response received from the server for the given TResponse type. - Task> GetResponseAsync(TRequestType clientMessage, CancellationToken cancellationToken = default) where TRequestType : IClientMessage; - - /// - /// Handle for connecting to the server. - /// If the server is already running, it'll connect to existing one. - /// Callers can await this handle to wait for connection establishment. - /// - TaskCompletionSource ConnectingHandle { get; } -} - -public interface IWinoServerConnectionManager : IWinoServerConnectionManager, IInitializeAsync -{ - /// - /// Existing connection handle to the server of TAppServiceConnection type. - /// - TAppServiceConnection Connection { get; set; } -} diff --git a/Wino.Core.Domain/Interfaces/IWinoSynchronizerBase.cs b/Wino.Core.Domain/Interfaces/IWinoSynchronizerBase.cs index ba2384b5..f19c1e5e 100644 --- a/Wino.Core.Domain/Interfaces/IWinoSynchronizerBase.cs +++ b/Wino.Core.Domain/Interfaces/IWinoSynchronizerBase.cs @@ -2,9 +2,9 @@ using System.Threading; using System.Threading.Tasks; using MailKit; +using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Models.Folders; -using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Synchronization; namespace Wino.Core.Domain.Interfaces; @@ -30,7 +30,7 @@ public interface IWinoSynchronizerBase : IBaseSynchronizer /// Mail item to download from server. /// Optional progress reporting for download operation. /// Cancellation token. - Task DownloadMissingMimeMessageAsync(IMailItem mailItem, ITransferProgress transferProgress, CancellationToken cancellationToken = default); + Task DownloadMissingMimeMessageAsync(MailCopy mailItem, ITransferProgress transferProgress, CancellationToken cancellationToken = default); /// /// 1. Cancel active synchronization. @@ -47,4 +47,5 @@ public interface IWinoSynchronizerBase : IBaseSynchronizer /// Cancellation token. /// Search results after downloading missing mail copies from server. Task> OnlineSearchAsync(string queryText, List folders, CancellationToken cancellationToken = default); + Task DownloadCalendarAttachmentAsync(CalendarItem calendarItem, CalendarAttachment attachment, string localFilePath, CancellationToken cancellationToken); } diff --git a/Wino.Core.Domain/MenuItems/AccountMenuItem.cs b/Wino.Core.Domain/MenuItems/AccountMenuItem.cs index 7b1e881e..fc836dbe 100644 --- a/Wino.Core.Domain/MenuItems/AccountMenuItem.cs +++ b/Wino.Core.Domain/MenuItems/AccountMenuItem.cs @@ -14,18 +14,57 @@ public partial class AccountMenuItem : MenuItemBase + /// Total items to sync. 0 means indeterminate progress. + /// [ObservableProperty] - [NotifyPropertyChangedFor(nameof(IsSynchronizationProgressVisible))] - private double synchronizationProgress; + [NotifyPropertyChangedFor(nameof(IsSynchronizationProgressVisible), nameof(SynchronizationProgress), nameof(IsProgressIndeterminate))] + public partial int TotalItemsToSync { get; set; } + + /// + /// Remaining items to sync. + /// + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(SynchronizationProgress))] + public partial int RemainingItemsToSync { get; set; } + + /// + /// Current synchronization status message. + /// + [ObservableProperty] + public partial string SynchronizationStatus { get; set; } = string.Empty; [ObservableProperty] private bool _isEnabled = true; public bool IsAttentionRequired => AttentionReason != AccountAttentionReason.None; - public bool IsSynchronizationProgressVisible => SynchronizationProgress > 0 && SynchronizationProgress < 100; - // We can't determine the progress for gmail synchronization since it is based on history changes. - public bool IsProgressIndeterminate => Parameter?.ProviderType == MailProviderType.Gmail; + /// + /// Calculates synchronization progress percentage (0-100). + /// Returns -1 for indeterminate progress when TotalItemsToSync is 0. + /// + public double SynchronizationProgress + { + get + { + if (TotalItemsToSync == 0 || RemainingItemsToSync == 0) + return -1; // Indeterminate + + return ((double)(TotalItemsToSync - RemainingItemsToSync) / TotalItemsToSync) * 100; + } + } + + /// + /// Whether synchronization progress should be visible. + /// Visible when there's active synchronization (TotalItemsToSync > 0 or RemainingItemsToSync > 0). + /// + public bool IsSynchronizationProgressVisible => TotalItemsToSync > 0 || RemainingItemsToSync > 0; + + /// + /// Whether progress should be indeterminate (when total is 0 but there's still synchronization happening). + /// + public bool IsProgressIndeterminate => TotalItemsToSync == 0 && RemainingItemsToSync == 0 && IsSynchronizationProgressVisible; + public Guid AccountId => Parameter.Id; private AccountAttentionReason attentionReason; @@ -72,6 +111,7 @@ public partial class AccountMenuItem : MenuItemBase Parameter.IsSticky; public bool IsSystemFolder => Parameter.IsSystemFolder; + + /// /// Display name of the folder. More and Category folders have localized display names. /// @@ -53,7 +56,7 @@ public partial class FolderMenuItem : MenuItemBase Parameter.ShowUnreadCount; - IEnumerable IBaseFolderMenuItem.SubMenuItems => SubMenuItems; + public new ObservableCollection SubMenuItems { get; set; } = new ObservableCollection(); public FolderMenuItem(IMailItemFolder folderStructure, MailAccount parentAccount, IMenuItem parentMenuItem) : base(folderStructure, folderStructure.Id, parentMenuItem) { diff --git a/Wino.Core.Domain/MenuItems/ManageAccountsMenuItem.cs b/Wino.Core.Domain/MenuItems/ManageAccountsMenuItem.cs deleted file mode 100644 index 147501b2..00000000 --- a/Wino.Core.Domain/MenuItems/ManageAccountsMenuItem.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Wino.Core.Domain.MenuItems; - -public class ManageAccountsMenuItem : MenuItemBase { } diff --git a/Wino.Core.Domain/MenuItems/MenuItemBase.cs b/Wino.Core.Domain/MenuItems/MenuItemBase.cs index eaf53f82..db4958de 100644 --- a/Wino.Core.Domain/MenuItems/MenuItemBase.cs +++ b/Wino.Core.Domain/MenuItems/MenuItemBase.cs @@ -8,10 +8,10 @@ namespace Wino.Core.Domain.MenuItems; public partial class MenuItemBase : ObservableObject, IMenuItem { [ObservableProperty] - private bool _isExpanded; + public partial bool IsExpanded { get; set; } [ObservableProperty] - private bool _isSelected; + public partial bool IsSelected { get; set; } public IMenuItem ParentMenuItem { get; } @@ -46,7 +46,7 @@ public partial class MenuItemBase : ObservableObject, IMenuItem public partial class MenuItemBase : MenuItemBase { [ObservableProperty] - private T _parameter; + public partial T Parameter { get; set; } public MenuItemBase(T parameter, Guid? entityId, IMenuItem parentMenuItem = null) : base(entityId, parentMenuItem) => Parameter = parameter; } @@ -54,7 +54,7 @@ public partial class MenuItemBase : MenuItemBase public partial class MenuItemBase : MenuItemBase { [ObservableProperty] - private bool _isChildSelected; + public partial bool IsChildSelected { get; set; } protected MenuItemBase(TValue parameter, Guid? entityId, IMenuItem parentMenuItem = null) : base(parameter, entityId, parentMenuItem) { } diff --git a/Wino.Core.Domain/MenuItems/MenuItemCollection.cs b/Wino.Core.Domain/MenuItems/MenuItemCollection.cs index bd0ac6b2..7504c0a7 100644 --- a/Wino.Core.Domain/MenuItems/MenuItemCollection.cs +++ b/Wino.Core.Domain/MenuItems/MenuItemCollection.cs @@ -188,6 +188,35 @@ public class MenuItemCollection : ObservableRangeCollection Insert(insertIndex, accountMenuItem); } + public bool RemoveFolderMenuItem(Guid folderId) + { + // Check root-level items. + var rootItem = this.OfType() + .FirstOrDefault(a => a.HandlingFolders.Any(b => b.Id == folderId)); + + if (rootItem != null) + { + Remove(rootItem); + return true; + } + + // Check sub-items of root folders. + foreach (var rootFolder in this.OfType()) + { + var subItem = rootFolder.SubMenuItems + .OfType() + .FirstOrDefault(a => a.HandlingFolders.Any(b => b.Id == folderId)); + + if (subItem != null) + { + rootFolder.SubMenuItems.Remove(subItem); + return true; + } + } + + return false; + } + private void ClearFolderAreaMenuItems() { var itemsToRemove = this.Where(a => !_preservingTypesForFolderArea.Contains(a.GetType())).ToList(); diff --git a/Wino.Core.Domain/MenuItems/MergedAccountMenuItem.cs b/Wino.Core.Domain/MenuItems/MergedAccountMenuItem.cs index 5cbf7c97..3e970d65 100644 --- a/Wino.Core.Domain/MenuItems/MergedAccountMenuItem.cs +++ b/Wino.Core.Domain/MenuItems/MergedAccountMenuItem.cs @@ -16,8 +16,50 @@ public partial class MergedAccountMenuItem : MenuItemBase + /// Total items to sync across all merged accounts. + /// [ObservableProperty] - private double synchronizationProgress; + [NotifyPropertyChangedFor(nameof(SynchronizationProgress), nameof(IsSynchronizationProgressVisible), nameof(IsProgressIndeterminate))] + public partial int TotalItemsToSync { get; set; } + + /// + /// Remaining items to sync across all merged accounts. + /// + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(SynchronizationProgress), nameof(IsSynchronizationProgressVisible), nameof(IsProgressIndeterminate))] + public partial int RemainingItemsToSync { get; set; } + + /// + /// Current synchronization status message. + /// + [ObservableProperty] + public partial string SynchronizationStatus { get; set; } = string.Empty; + + /// + /// Calculated synchronization progress for merged accounts. + /// + public double SynchronizationProgress + { + get + { + if (TotalItemsToSync == 0 || RemainingItemsToSync == 0) + return -1; // Indeterminate + + return ((double)(TotalItemsToSync - RemainingItemsToSync) / TotalItemsToSync) * 100; + } + } + + /// + /// Whether synchronization progress should be visible. + /// Visible when there's active synchronization (TotalItemsToSync > 0 or RemainingItemsToSync > 0). + /// + public bool IsSynchronizationProgressVisible => TotalItemsToSync > 0 || RemainingItemsToSync > 0; + + /// + /// Whether progress should be indeterminate. + /// + public bool IsProgressIndeterminate => TotalItemsToSync == 0 && IsSynchronizationProgressVisible; [ObservableProperty] private string mergedAccountName; @@ -35,6 +77,20 @@ public partial class MergedAccountMenuItem : MenuItemBase().Sum(a => a.UnreadItemCount); } + + /// + /// Aggregates synchronization progress from all child account menu items. + /// + public void RefreshSynchronizationProgress() + { + var accountMenuItems = SubMenuItems.OfType().ToList(); + + TotalItemsToSync = accountMenuItems.Sum(a => a.TotalItemsToSync); + RemainingItemsToSync = accountMenuItems.Sum(a => a.RemainingItemsToSync); + + // Use first non-empty status message + SynchronizationStatus = accountMenuItems.FirstOrDefault(a => !string.IsNullOrEmpty(a.SynchronizationStatus))?.SynchronizationStatus ?? string.Empty; + } public void UpdateAccount(MailAccount account) { diff --git a/Wino.Core.Domain/MenuItems/NewCalendarEventMenuItem.cs b/Wino.Core.Domain/MenuItems/NewCalendarEventMenuItem.cs new file mode 100644 index 00000000..8fe87fec --- /dev/null +++ b/Wino.Core.Domain/MenuItems/NewCalendarEventMenuItem.cs @@ -0,0 +1,3 @@ +namespace Wino.Core.Domain.MenuItems; + +public sealed class NewCalendarEventMenuItem : NewMailMenuItem { } diff --git a/Wino.Core.Domain/MenuItems/NewContactMenuItem.cs b/Wino.Core.Domain/MenuItems/NewContactMenuItem.cs new file mode 100644 index 00000000..24d09bc0 --- /dev/null +++ b/Wino.Core.Domain/MenuItems/NewContactMenuItem.cs @@ -0,0 +1,3 @@ +namespace Wino.Core.Domain.MenuItems; + +public sealed class NewContactMenuItem : MenuItemBase { } diff --git a/Wino.Core.Domain/MenuItems/SettingsShellPageMenuItem.cs b/Wino.Core.Domain/MenuItems/SettingsShellPageMenuItem.cs new file mode 100644 index 00000000..f478ab72 --- /dev/null +++ b/Wino.Core.Domain/MenuItems/SettingsShellPageMenuItem.cs @@ -0,0 +1,22 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using Wino.Core.Domain.Enums; + +namespace Wino.Core.Domain.MenuItems; + +public partial class SettingsShellPageMenuItem( + WinoPage pageType, + string title, + string description, + string glyph) : MenuItemBase +{ + public WinoPage PageType { get; } = pageType; + + [ObservableProperty] + public partial string Title { get; set; } = title; + + [ObservableProperty] + public partial string Description { get; set; } = description; + + [ObservableProperty] + public partial string Glyph { get; set; } = glyph; +} diff --git a/Wino.Core.Domain/MenuItems/SettingsShellSectionMenuItem.cs b/Wino.Core.Domain/MenuItems/SettingsShellSectionMenuItem.cs new file mode 100644 index 00000000..223d76c2 --- /dev/null +++ b/Wino.Core.Domain/MenuItems/SettingsShellSectionMenuItem.cs @@ -0,0 +1,12 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Wino.Core.Domain.MenuItems; + +public partial class SettingsShellSectionMenuItem(string title, string glyph) : MenuItemBase +{ + [ObservableProperty] + public partial string Title { get; set; } = title; + + [ObservableProperty] + public partial string Glyph { get; set; } = glyph; +} diff --git a/Wino.Core.Domain/MenuItems/StoreUpdateMenuItem.cs b/Wino.Core.Domain/MenuItems/StoreUpdateMenuItem.cs new file mode 100644 index 00000000..6afa77c4 --- /dev/null +++ b/Wino.Core.Domain/MenuItems/StoreUpdateMenuItem.cs @@ -0,0 +1,3 @@ +namespace Wino.Core.Domain.MenuItems; + +public class StoreUpdateMenuItem : MenuItemBase { } diff --git a/Wino.Core.Domain/Misc/CalendarColorPalette.cs b/Wino.Core.Domain/Misc/CalendarColorPalette.cs new file mode 100644 index 00000000..6adc8620 --- /dev/null +++ b/Wino.Core.Domain/Misc/CalendarColorPalette.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace Wino.Core.Domain.Misc; + +public static class CalendarColorPalette +{ + private static readonly string[] FlatUiColorPalette = + [ + "#E53935", "#D81B60", "#C2185B", "#AD1457", "#8E24AA", "#7B1FA2", "#6A1B9A", "#5E35B1", "#512DA8", "#4527A0", + "#3949AB", "#303F9F", "#283593", "#1E88E5", "#1976D2", "#1565C0", "#039BE5", "#0288D1", "#0277BD", "#00ACC1", + "#0097A7", "#00838F", "#00897B", "#00796B", "#00695C", "#43A047", "#388E3C", "#2E7D32", "#7CB342", "#689F38", + "#558B2F", "#9CCC65", "#8BC34A", "#AED581", "#C0CA33", "#AFB42B", "#9E9D24", "#D4E157", "#CDDC39", "#FDD835", + "#FBC02D", "#F9A825", "#FFB300", "#FFA000", "#FF8F00", "#FB8C00", "#F57C00", "#EF6C00", "#F4511E", "#E64A19", + "#D84315", "#FF7043", "#FF8A65", "#FFAB91", "#6D4C41", "#5D4037", "#4E342E", "#8D6E63", "#795548", "#A1887F", + "#546E7A", "#455A64", "#37474F", "#607D8B", "#78909C", "#90A4AE", "#757575", "#616161", "#424242", "#9E9E9E", + "#BDBDBD", "#EC407A", "#F06292", "#F48FB1", "#BA68C8", "#CE93D8", "#9575CD", "#B39DDB", "#7986CB", "#9FA8DA", + "#64B5F6", "#90CAF9", "#4FC3F7", "#81D4FA", "#4DD0E1", "#80DEEA", "#4DB6AC", "#80CBC4", "#81C784", "#A5D6A7", + "#C5E1A5", "#E6EE9C", "#FFF176", "#FFD54F", "#FFCC80", "#FFB74D", "#FFAB40", "#FF9E80", "#BCAAA4", "#A1887F" + ]; + + public static IReadOnlyList GetColors() => FlatUiColorPalette; + + public static string GetDistinctColor(IEnumerable usedColors) + { + var used = new HashSet(StringComparer.OrdinalIgnoreCase); + + if (usedColors != null) + { + foreach (var color in usedColors) + { + if (TryNormalizeHexColor(color, out var normalized)) + { + used.Add(normalized); + } + } + } + + foreach (var color in FlatUiColorPalette) + { + if (!used.Contains(color)) + { + return color; + } + } + + return FlatUiColorPalette[0]; + } + + private static bool TryNormalizeHexColor(string value, out string normalized) + { + normalized = null; + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + var color = value.Trim(); + if (color.StartsWith('#')) + { + color = color[1..]; + } + + if (color.Length != 6) + { + return false; + } + + if (!int.TryParse(color, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _)) + { + return false; + } + + normalized = $"#{color.ToUpperInvariant()}"; + return true; + } +} diff --git a/Wino.Core.Domain/Models/Accounts/AccountSetupStepModel.cs b/Wino.Core.Domain/Models/Accounts/AccountSetupStepModel.cs new file mode 100644 index 00000000..65befde6 --- /dev/null +++ b/Wino.Core.Domain/Models/Accounts/AccountSetupStepModel.cs @@ -0,0 +1,24 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using Wino.Core.Domain.Enums; + +namespace Wino.Core.Domain.Models.Accounts; + +public partial class AccountSetupStepModel : ObservableObject +{ + public string Title { get; init; } + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsPending))] + [NotifyPropertyChangedFor(nameof(IsInProgress))] + [NotifyPropertyChangedFor(nameof(IsSucceeded))] + [NotifyPropertyChangedFor(nameof(IsFailed))] + public partial AccountSetupStepStatus Status { get; set; } = AccountSetupStepStatus.Pending; + + [ObservableProperty] + public partial string ErrorMessage { get; set; } + + public bool IsPending => Status == AccountSetupStepStatus.Pending; + public bool IsInProgress => Status == AccountSetupStepStatus.InProgress; + public bool IsSucceeded => Status == AccountSetupStepStatus.Succeeded; + public bool IsFailed => Status == AccountSetupStepStatus.Failed; +} diff --git a/Wino.Core.Domain/Models/Accounts/ProviderDetail.cs b/Wino.Core.Domain/Models/Accounts/ProviderDetail.cs index c5449892..b495db70 100644 --- a/Wino.Core.Domain/Models/Accounts/ProviderDetail.cs +++ b/Wino.Core.Domain/Models/Accounts/ProviderDetail.cs @@ -17,11 +17,11 @@ public class ProviderDetail : IProviderDetail { if (SpecialImapProvider == SpecialImapProvider.None) { - return $"/Wino.Core.UWP/Assets/Providers/{Type}.png"; + return $"/Assets/Providers/{Type}.png"; } else { - return $"/Wino.Core.UWP/Assets/Providers/{SpecialImapProvider}.png"; + return $"/Assets/Providers/{SpecialImapProvider}.png"; } } } diff --git a/Wino.Core.Domain/Models/Accounts/SpecialImapProviderDetails.cs b/Wino.Core.Domain/Models/Accounts/SpecialImapProviderDetails.cs index 1286d431..a91e9f35 100644 --- a/Wino.Core.Domain/Models/Accounts/SpecialImapProviderDetails.cs +++ b/Wino.Core.Domain/Models/Accounts/SpecialImapProviderDetails.cs @@ -2,4 +2,9 @@ namespace Wino.Core.Domain.Models.Accounts; -public record SpecialImapProviderDetails(string Address, string Password, string SenderName, SpecialImapProvider SpecialImapProvider); +public record SpecialImapProviderDetails( + string Address, + string Password, + string SenderName, + SpecialImapProvider SpecialImapProvider, + ImapCalendarSupportMode CalendarSupportMode = ImapCalendarSupportMode.CalDav); diff --git a/Wino.Core.Domain/Models/Accounts/WinoAccountApiResult.cs b/Wino.Core.Domain/Models/Accounts/WinoAccountApiResult.cs new file mode 100644 index 00000000..ee4f2c3d --- /dev/null +++ b/Wino.Core.Domain/Models/Accounts/WinoAccountApiResult.cs @@ -0,0 +1,29 @@ +#nullable enable +using System.Text.Json; + +namespace Wino.Core.Domain.Models.Accounts; + +public sealed class WinoAccountApiResult +{ + public bool IsSuccess { get; init; } + public string? ErrorCode { get; init; } + public string? ErrorMessage { get; init; } + public JsonElement? ErrorDetails { get; init; } + public T? Result { get; init; } + + public static WinoAccountApiResult Success(T result) + => new() + { + IsSuccess = true, + Result = result + }; + + public static WinoAccountApiResult Failure(string? errorCode, string? errorMessage = null, JsonElement? errorDetails = null) + => new() + { + IsSuccess = false, + ErrorCode = errorCode, + ErrorMessage = errorMessage, + ErrorDetails = errorDetails + }; +} diff --git a/Wino.Core.Domain/Models/Accounts/WinoAccountOperationResult.cs b/Wino.Core.Domain/Models/Accounts/WinoAccountOperationResult.cs new file mode 100644 index 00000000..8b14fdc7 --- /dev/null +++ b/Wino.Core.Domain/Models/Accounts/WinoAccountOperationResult.cs @@ -0,0 +1,30 @@ +#nullable enable +using System.Text.Json; +using Wino.Core.Domain.Entities.Shared; + +namespace Wino.Core.Domain.Models.Accounts; + +public sealed class WinoAccountOperationResult +{ + public bool IsSuccess { get; init; } + public string? ErrorCode { get; init; } + public string? ErrorMessage { get; init; } + public JsonElement? ErrorDetails { get; init; } + public WinoAccount? Account { get; init; } + + public static WinoAccountOperationResult Success(WinoAccount account) + => new() + { + IsSuccess = true, + Account = account + }; + + public static WinoAccountOperationResult Failure(string? errorCode, string? errorMessage = null, JsonElement? errorDetails = null) + => new() + { + IsSuccess = false, + ErrorCode = errorCode, + ErrorMessage = errorMessage, + ErrorDetails = errorDetails + }; +} diff --git a/Wino.Core.Domain/Models/Accounts/WinoAccountSyncExportResult.cs b/Wino.Core.Domain/Models/Accounts/WinoAccountSyncExportResult.cs new file mode 100644 index 00000000..998bfcb1 --- /dev/null +++ b/Wino.Core.Domain/Models/Accounts/WinoAccountSyncExportResult.cs @@ -0,0 +1,8 @@ +namespace Wino.Core.Domain.Models.Accounts; + +public sealed class WinoAccountSyncExportResult +{ + public bool IncludedPreferences { get; init; } + public bool IncludedAccounts { get; init; } + public int ExportedMailboxCount { get; init; } +} diff --git a/Wino.Core.Domain/Models/Accounts/WinoAccountSyncImportResult.cs b/Wino.Core.Domain/Models/Accounts/WinoAccountSyncImportResult.cs new file mode 100644 index 00000000..df2ebcf5 --- /dev/null +++ b/Wino.Core.Domain/Models/Accounts/WinoAccountSyncImportResult.cs @@ -0,0 +1,15 @@ +namespace Wino.Core.Domain.Models.Accounts; + +public sealed class WinoAccountSyncImportResult +{ + public bool IncludedPreferences { get; init; } + public bool IncludedAccounts { get; init; } + public bool HadRemotePreferences { get; init; } + public int AppliedPreferenceCount { get; init; } + public int FailedPreferenceCount { get; init; } + public int ImportedMailboxCount { get; init; } + public int SkippedDuplicateMailboxCount { get; init; } + public int RemoteMailboxCount { get; init; } + + public bool HasAnyRemoteData => HadRemotePreferences || RemoteMailboxCount > 0; +} diff --git a/Wino.Core.Domain/Models/Accounts/WinoAccountSyncSelection.cs b/Wino.Core.Domain/Models/Accounts/WinoAccountSyncSelection.cs new file mode 100644 index 00000000..9116c197 --- /dev/null +++ b/Wino.Core.Domain/Models/Accounts/WinoAccountSyncSelection.cs @@ -0,0 +1,5 @@ +namespace Wino.Core.Domain.Models.Accounts; + +public sealed record WinoAccountSyncSelection( + bool IncludePreferences = true, + bool IncludeAccounts = true); diff --git a/Wino.Core.Domain/Models/Accounts/WinoStoreCollectionsIdTicketInfo.cs b/Wino.Core.Domain/Models/Accounts/WinoStoreCollectionsIdTicketInfo.cs new file mode 100644 index 00000000..19278214 --- /dev/null +++ b/Wino.Core.Domain/Models/Accounts/WinoStoreCollectionsIdTicketInfo.cs @@ -0,0 +1,9 @@ +#nullable enable +using System; + +namespace Wino.Core.Domain.Models.Accounts; + +public sealed record WinoStoreCollectionsIdTicketInfo( + string ServiceTicket, + string PublisherUserId, + DateTimeOffset ExpiresAtUtc); diff --git a/Wino.Core.Domain/Models/Ai/AiRewriteModeOption.cs b/Wino.Core.Domain/Models/Ai/AiRewriteModeOption.cs new file mode 100644 index 00000000..3b3013bb --- /dev/null +++ b/Wino.Core.Domain/Models/Ai/AiRewriteModeOption.cs @@ -0,0 +1,3 @@ +namespace Wino.Core.Domain.Models.Ai; + +public sealed record AiRewriteModeOption(string Mode, string Label, string Description, bool IsCustom = false); diff --git a/Wino.Core.Domain/Models/Ai/AiTranslateLanguageOption.cs b/Wino.Core.Domain/Models/Ai/AiTranslateLanguageOption.cs new file mode 100644 index 00000000..ed0b97be --- /dev/null +++ b/Wino.Core.Domain/Models/Ai/AiTranslateLanguageOption.cs @@ -0,0 +1,3 @@ +namespace Wino.Core.Domain.Models.Ai; + +public sealed record AiTranslateLanguageOption(string Code, string Label); diff --git a/Wino.Core.Domain/Models/AutoDiscovery/AutoDiscoverySettings.cs b/Wino.Core.Domain/Models/AutoDiscovery/AutoDiscoverySettings.cs index 87494dcc..793edad9 100644 --- a/Wino.Core.Domain/Models/AutoDiscovery/AutoDiscoverySettings.cs +++ b/Wino.Core.Domain/Models/AutoDiscovery/AutoDiscoverySettings.cs @@ -64,8 +64,8 @@ public class AutoDiscoverySettings } public AutoDiscoveryProviderSetting GetImapSettings() - => Settings?.Find(a => a.Protocol == "IMAP"); + => Settings?.Find(a => string.Equals(a.Protocol, "IMAP", StringComparison.OrdinalIgnoreCase)); public AutoDiscoveryProviderSetting GetSmptpSettings() - => Settings?.Find(a => a.Protocol == "SMTP"); + => Settings?.Find(a => string.Equals(a.Protocol, "SMTP", StringComparison.OrdinalIgnoreCase)); } diff --git a/Wino.Core.Domain/Models/Calendar/AccountCalendarPickingResult.cs b/Wino.Core.Domain/Models/Calendar/AccountCalendarPickingResult.cs new file mode 100644 index 00000000..8cbe511a --- /dev/null +++ b/Wino.Core.Domain/Models/Calendar/AccountCalendarPickingResult.cs @@ -0,0 +1,7 @@ +#nullable enable + +using Wino.Core.Domain.Entities.Calendar; + +namespace Wino.Core.Domain.Models.Calendar; + +public sealed record AccountCalendarPickingResult(AccountCalendar? PickedCalendar, bool ShouldNavigateToCalendarSettings); diff --git a/Wino.Core.Domain/Models/Calendar/CalDavCalendar.cs b/Wino.Core.Domain/Models/Calendar/CalDavCalendar.cs new file mode 100644 index 00000000..edf1890e --- /dev/null +++ b/Wino.Core.Domain/Models/Calendar/CalDavCalendar.cs @@ -0,0 +1,10 @@ +namespace Wino.Core.Domain.Models.Calendar; + +public sealed class CalDavCalendar +{ + public string RemoteCalendarId { get; init; } = string.Empty; + public string Name { get; init; } = string.Empty; + public string CTag { get; init; } = string.Empty; + public string SyncToken { get; init; } = string.Empty; +} + diff --git a/Wino.Core.Domain/Models/Calendar/CalDavCalendarEvent.cs b/Wino.Core.Domain/Models/Calendar/CalDavCalendarEvent.cs new file mode 100644 index 00000000..d77164ef --- /dev/null +++ b/Wino.Core.Domain/Models/Calendar/CalDavCalendarEvent.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using Wino.Core.Domain.Enums; + +namespace Wino.Core.Domain.Models.Calendar; + +public sealed class CalDavCalendarEvent +{ + public string RemoteEventId { get; init; } = string.Empty; + public string RemoteResourceHref { get; init; } = string.Empty; + public string ETag { get; init; } = string.Empty; + public string IcsContent { get; init; } = string.Empty; + + public string Uid { get; init; } = string.Empty; + public string SeriesMasterRemoteEventId { get; init; } = string.Empty; + public bool IsSeriesMaster { get; init; } + public bool IsRecurringInstance { get; init; } + + public string Title { get; init; } = string.Empty; + public string Description { get; init; } = string.Empty; + public string Location { get; init; } = string.Empty; + + public DateTimeOffset Start { get; init; } + public DateTimeOffset End { get; init; } + public string StartTimeZone { get; init; } = string.Empty; + public string EndTimeZone { get; init; } = string.Empty; + public string Recurrence { get; init; } = string.Empty; + + public string OrganizerDisplayName { get; init; } = string.Empty; + public string OrganizerEmail { get; init; } = string.Empty; + + public CalendarItemStatus Status { get; init; } = CalendarItemStatus.Accepted; + public CalendarItemVisibility Visibility { get; init; } = CalendarItemVisibility.Default; + public CalendarItemShowAs ShowAs { get; init; } = CalendarItemShowAs.Busy; + public bool IsHidden { get; init; } + + public IReadOnlyList Attendees { get; init; } = []; + public IReadOnlyList Reminders { get; init; } = []; +} + +public sealed class CalDavEventAttendee +{ + public string Name { get; init; } = string.Empty; + public string Email { get; init; } = string.Empty; + public AttendeeStatus AttendenceStatus { get; init; } = AttendeeStatus.NeedsAction; + public bool IsOrganizer { get; init; } + public bool IsOptionalAttendee { get; init; } +} + +public sealed class CalDavEventReminder +{ + public int DurationInSeconds { get; init; } + public CalendarItemReminderType ReminderType { get; init; } = CalendarItemReminderType.Popup; +} + diff --git a/Wino.Core.Domain/Models/Calendar/CalDavConnectionSettings.cs b/Wino.Core.Domain/Models/Calendar/CalDavConnectionSettings.cs new file mode 100644 index 00000000..3b9c874f --- /dev/null +++ b/Wino.Core.Domain/Models/Calendar/CalDavConnectionSettings.cs @@ -0,0 +1,11 @@ +using System; + +namespace Wino.Core.Domain.Models.Calendar; + +public sealed class CalDavConnectionSettings +{ + public Uri ServiceUri { get; init; } + public string Username { get; init; } = string.Empty; + public string Password { get; init; } = string.Empty; +} + diff --git a/Wino.Core.Domain/Models/Calendar/CalendarDayModel.cs b/Wino.Core.Domain/Models/Calendar/CalendarDayModel.cs deleted file mode 100644 index 4f3f06a8..00000000 --- a/Wino.Core.Domain/Models/Calendar/CalendarDayModel.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using Itenso.TimePeriod; -using Wino.Core.Domain.Collections; - -namespace Wino.Core.Domain.Models.Calendar; - -/// -/// Represents a day in the calendar. -/// Can hold events, appointments, wheather status etc. -/// -public class CalendarDayModel -{ - public ITimePeriod Period { get; } - public CalendarEventCollection EventsCollection { get; } - - public CalendarDayModel(DateTime representingDate, CalendarRenderOptions calendarRenderOptions) - { - RepresentingDate = representingDate; - Period = new TimeRange(representingDate, representingDate.AddDays(1)); - CalendarRenderOptions = calendarRenderOptions; - EventsCollection = new CalendarEventCollection(Period, calendarRenderOptions.CalendarSettings); - } - - public DateTime RepresentingDate { get; } - public CalendarRenderOptions CalendarRenderOptions { get; } -} diff --git a/Wino.Core.Domain/Models/Calendar/CalendarDisplayRequest.cs b/Wino.Core.Domain/Models/Calendar/CalendarDisplayRequest.cs new file mode 100644 index 00000000..bc172fae --- /dev/null +++ b/Wino.Core.Domain/Models/Calendar/CalendarDisplayRequest.cs @@ -0,0 +1,6 @@ +using System; +using Wino.Core.Domain.Enums; + +namespace Wino.Core.Domain.Models.Calendar; + +public readonly record struct CalendarDisplayRequest(CalendarDisplayType DisplayType, DateOnly AnchorDate); diff --git a/Wino.Core.Domain/Models/Calendar/CalendarEventComposeAttachmentDraft.cs b/Wino.Core.Domain/Models/Calendar/CalendarEventComposeAttachmentDraft.cs new file mode 100644 index 00000000..44b17035 --- /dev/null +++ b/Wino.Core.Domain/Models/Calendar/CalendarEventComposeAttachmentDraft.cs @@ -0,0 +1,12 @@ +using System; + +namespace Wino.Core.Domain.Models.Calendar; + +public class CalendarEventComposeAttachmentDraft +{ + public Guid Id { get; set; } + public string FileName { get; set; } = string.Empty; + public string FilePath { get; set; } = string.Empty; + public string FileExtension { get; set; } = string.Empty; + public long Size { get; set; } +} diff --git a/Wino.Core.Domain/Models/Calendar/CalendarEventComposeNavigationArgs.cs b/Wino.Core.Domain/Models/Calendar/CalendarEventComposeNavigationArgs.cs new file mode 100644 index 00000000..a2522ce6 --- /dev/null +++ b/Wino.Core.Domain/Models/Calendar/CalendarEventComposeNavigationArgs.cs @@ -0,0 +1,13 @@ +using System; + +namespace Wino.Core.Domain.Models.Calendar; + +public class CalendarEventComposeNavigationArgs +{ + public Guid? SelectedCalendarId { get; set; } + public string Title { get; set; } = string.Empty; + public string Location { get; set; } = string.Empty; + public bool IsAllDay { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } +} diff --git a/Wino.Core.Domain/Models/Calendar/CalendarEventComposeResult.cs b/Wino.Core.Domain/Models/Calendar/CalendarEventComposeResult.cs new file mode 100644 index 00000000..2be440ac --- /dev/null +++ b/Wino.Core.Domain/Models/Calendar/CalendarEventComposeResult.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Enums; + +namespace Wino.Core.Domain.Models.Calendar; + +public class CalendarEventComposeResult +{ + public Guid CalendarId { get; set; } + public Guid AccountId { get; set; } + public string Title { get; set; } = string.Empty; + public string Location { get; set; } = string.Empty; + public string HtmlNotes { get; set; } = string.Empty; + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public bool IsAllDay { get; set; } + public string TimeZoneId { get; set; } = string.Empty; + public CalendarItemShowAs ShowAs { get; set; } + public List SelectedReminders { get; set; } = []; + public List Attendees { get; set; } = []; + public List Attachments { get; set; } = []; + public string Recurrence { get; set; } = string.Empty; + public string RecurrenceSummary { get; set; } = string.Empty; +} diff --git a/Wino.Core.Domain/Models/Calendar/CalendarOperationPreparationRequest.cs b/Wino.Core.Domain/Models/Calendar/CalendarOperationPreparationRequest.cs new file mode 100644 index 00000000..a43c6e10 --- /dev/null +++ b/Wino.Core.Domain/Models/Calendar/CalendarOperationPreparationRequest.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Enums; + +namespace Wino.Core.Domain.Models.Calendar; + +/// +/// Encapsulates the options for preparing calendar operation requests. +/// +/// Calendar operation to execute (Create, Update, Delete, Accept, Decline, Tentative). +/// Calendar item to operate on. +/// List of attendees for the calendar event. +/// Optional message to include with event responses (Accept, Decline, Tentative). +/// Original calendar item state before update (for revert capability). +/// Original attendees list before update (for revert capability). +public record CalendarOperationPreparationRequest( + CalendarSynchronizerOperation Operation, + CalendarItem CalendarItem = null, + List Attendees = null, + string ResponseMessage = null, + CalendarItem OriginalItem = null, + List OriginalAttendees = null, + CalendarEventComposeResult ComposeResult = null); diff --git a/Wino.Core.Domain/Models/Calendar/CalendarPageNavigationArgs.cs b/Wino.Core.Domain/Models/Calendar/CalendarPageNavigationArgs.cs index a39d536b..6e1c0ba7 100644 --- a/Wino.Core.Domain/Models/Calendar/CalendarPageNavigationArgs.cs +++ b/Wino.Core.Domain/Models/Calendar/CalendarPageNavigationArgs.cs @@ -13,4 +13,9 @@ public class CalendarPageNavigationArgs /// Display the calendar view for the specified date. /// public DateTime NavigationDate { get; set; } + + /// + /// Force reloading the calendar data even when the target range does not change. + /// + public bool ForceReload { get; set; } } diff --git a/Wino.Core.Domain/Models/Calendar/CalendarPickerAccountGroup.cs b/Wino.Core.Domain/Models/Calendar/CalendarPickerAccountGroup.cs new file mode 100644 index 00000000..62c2295c --- /dev/null +++ b/Wino.Core.Domain/Models/Calendar/CalendarPickerAccountGroup.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Entities.Shared; + +namespace Wino.Core.Domain.Models.Calendar; + +public class CalendarPickerAccountGroup +{ + public MailAccount Account { get; set; } = null!; + public List Calendars { get; set; } = []; +} diff --git a/Wino.Core.Domain/Models/Calendar/CalendarRangeResolver.cs b/Wino.Core.Domain/Models/Calendar/CalendarRangeResolver.cs new file mode 100644 index 00000000..1c20bd79 --- /dev/null +++ b/Wino.Core.Domain/Models/Calendar/CalendarRangeResolver.cs @@ -0,0 +1,103 @@ +using System; +using System.Linq; +using Wino.Core.Domain.Enums; + +namespace Wino.Core.Domain.Models.Calendar; + +public static class CalendarRangeResolver +{ + public static VisibleDateRange Resolve(CalendarDisplayRequest request, CalendarSettings settings, DateOnly today) + { + var startDate = GetStartDate(request.DisplayType, request.AnchorDate, settings); + var endDate = GetEndDate(request.DisplayType, request.AnchorDate, startDate, settings); + var dayCount = endDate.DayNumber - startDate.DayNumber + 1; + var dates = Enumerable.Range(0, dayCount) + .Select(offset => startDate.AddDays(offset)) + .ToArray(); + + return new VisibleDateRange( + request.DisplayType, + request.AnchorDate, + startDate, + endDate, + request.AnchorDate, + dayCount, + today >= startDate && today <= endDate, + startDate.Year == endDate.Year && startDate.Month == endDate.Month, + dates); + } + + public static VisibleDateRange ChangeDisplayType(VisibleDateRange currentRange, CalendarDisplayType targetDisplayType, CalendarSettings settings, DateOnly today) + { + if (currentRange.DisplayType == targetDisplayType) + { + return currentRange; + } + + var anchorDate = currentRange.AnchorDate; + + if (currentRange.DisplayType == CalendarDisplayType.Month) + { + anchorDate = currentRange.Contains(today) ? today : currentRange.StartDate; + } + + return Resolve(new CalendarDisplayRequest(targetDisplayType, anchorDate), settings, today); + } + + public static VisibleDateRange Navigate(VisibleDateRange currentRange, int direction, CalendarSettings settings, DateOnly today) + { + if (direction == 0) + { + return currentRange; + } + + var normalizedDirection = Math.Sign(direction); + var anchorDate = currentRange.DisplayType switch + { + CalendarDisplayType.Day => currentRange.AnchorDate.AddDays(normalizedDirection), + CalendarDisplayType.Week => currentRange.AnchorDate.AddDays(7 * normalizedDirection), + CalendarDisplayType.WorkWeek => currentRange.AnchorDate.AddDays(7 * normalizedDirection), + CalendarDisplayType.Month => currentRange.AnchorDate.AddMonths(normalizedDirection), + _ => currentRange.AnchorDate + }; + + return Resolve(new CalendarDisplayRequest(currentRange.DisplayType, anchorDate), settings, today); + } + + private static DateOnly GetStartDate(CalendarDisplayType displayType, DateOnly anchorDate, CalendarSettings settings) + { + return displayType switch + { + CalendarDisplayType.Day => anchorDate, + CalendarDisplayType.Week => GetStartOfWeek(anchorDate, settings.FirstDayOfWeek), + CalendarDisplayType.WorkWeek => GetStartOfWorkWeek(anchorDate, settings), + CalendarDisplayType.Month => new DateOnly(anchorDate.Year, anchorDate.Month, 1), + _ => anchorDate + }; + } + + private static DateOnly GetEndDate(CalendarDisplayType displayType, DateOnly anchorDate, DateOnly startDate, CalendarSettings settings) + { + return displayType switch + { + CalendarDisplayType.Day => anchorDate, + CalendarDisplayType.Week => startDate.AddDays(6), + CalendarDisplayType.WorkWeek => startDate.AddDays(settings.WorkWeekDayCount - 1), + CalendarDisplayType.Month => new DateOnly(anchorDate.Year, anchorDate.Month, DateTime.DaysInMonth(anchorDate.Year, anchorDate.Month)), + _ => anchorDate + }; + } + + private static DateOnly GetStartOfWeek(DateOnly date, DayOfWeek firstDayOfWeek) + { + var offset = ((int)date.DayOfWeek - (int)firstDayOfWeek + 7) % 7; + return date.AddDays(-offset); + } + + private static DateOnly GetStartOfWorkWeek(DateOnly anchorDate, CalendarSettings settings) + { + var startOfWeek = GetStartOfWeek(anchorDate, settings.FirstDayOfWeek); + var offsetToWorkWeekStart = settings.GetWeekOffset(settings.WorkWeekStart); + return startOfWeek.AddDays(offsetToWorkWeekStart); + } +} diff --git a/Wino.Core.Domain/Models/Calendar/CalendarRangeTextFormatter.cs b/Wino.Core.Domain/Models/Calendar/CalendarRangeTextFormatter.cs new file mode 100644 index 00000000..598ab3e5 --- /dev/null +++ b/Wino.Core.Domain/Models/Calendar/CalendarRangeTextFormatter.cs @@ -0,0 +1,39 @@ +using System; +using System.Globalization; +using Wino.Core.Domain.Enums; + +namespace Wino.Core.Domain.Models.Calendar; + +public sealed class CalendarRangeTextFormatter : ICalendarRangeTextFormatter +{ + public string Format(VisibleDateRange range, IDateContextProvider dateContextProvider) + { + var culture = dateContextProvider.Culture; + + if (range.DayCount >= 28) + { + return FormatMonth(range.PrimaryDate, culture); + } + + if (range.DayCount == 1 || range.DisplayType == CalendarDisplayType.Day) + { + return FormatDate(range.StartDate, culture); + } + + if (range.SpansSingleMonth) + { + return $"{FormatDate(range.StartDate, culture)} - {FormatDay(range.EndDate, culture)}"; + } + + return $"{FormatDate(range.StartDate, culture)} - {FormatDate(range.EndDate, culture)}"; + } + + private static string FormatDate(DateOnly date, CultureInfo culture) + => date.ToString(culture.DateTimeFormat.MonthDayPattern, culture); + + private static string FormatDay(DateOnly date, CultureInfo culture) + => date.Day.ToString(culture); + + private static string FormatMonth(DateOnly date, CultureInfo culture) + => date.ToString(culture.DateTimeFormat.YearMonthPattern, culture); +} diff --git a/Wino.Core.Domain/Models/Calendar/CalendarReminderNotificationRequest.cs b/Wino.Core.Domain/Models/Calendar/CalendarReminderNotificationRequest.cs new file mode 100644 index 00000000..4a0c634e --- /dev/null +++ b/Wino.Core.Domain/Models/Calendar/CalendarReminderNotificationRequest.cs @@ -0,0 +1,10 @@ +using Wino.Core.Domain.Entities.Calendar; + +namespace Wino.Core.Domain.Models.Calendar; + +public sealed class CalendarReminderNotificationRequest +{ + public CalendarItem CalendarItem { get; init; } = null!; + public long ReminderDurationInSeconds { get; init; } + public string ReminderKey { get; init; } = string.Empty; +} diff --git a/Wino.Core.Domain/Models/Calendar/CalendarRenderOptions.cs b/Wino.Core.Domain/Models/Calendar/CalendarRenderOptions.cs deleted file mode 100644 index 2f31db08..00000000 --- a/Wino.Core.Domain/Models/Calendar/CalendarRenderOptions.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Wino.Core.Domain.Models.Calendar; - -public class CalendarRenderOptions -{ - public CalendarRenderOptions(DateRange dateRange, CalendarSettings calendarSettings) - { - DateRange = dateRange; - CalendarSettings = calendarSettings; - } - public int TotalDayCount => DateRange.TotalDays; - public DateRange DateRange { get; } - public CalendarSettings CalendarSettings { get; } -} diff --git a/Wino.Core.Domain/Models/Calendar/CalendarSettings.cs b/Wino.Core.Domain/Models/Calendar/CalendarSettings.cs index 573654c1..bb1359d8 100644 --- a/Wino.Core.Domain/Models/Calendar/CalendarSettings.cs +++ b/Wino.Core.Domain/Models/Calendar/CalendarSettings.cs @@ -7,12 +7,35 @@ namespace Wino.Core.Domain.Models.Calendar; public record CalendarSettings(DayOfWeek FirstDayOfWeek, List WorkingDays, + bool IsWorkingHoursEnabled, + DayOfWeek WorkWeekStart, + DayOfWeek WorkWeekEnd, TimeSpan WorkingHourStart, TimeSpan WorkingHourEnd, double HourHeight, DayHeaderDisplayType DayHeaderDisplayType, - CultureInfo CultureInfo) + CultureInfo CultureInfo, + string TimedDayHeaderDateFormat = "ddd dd") { + public int WorkWeekDayCount + { + get + { + var startOffset = GetWeekOffset(WorkWeekStart); + var endOffset = GetWeekOffset(WorkWeekEnd); + + if (endOffset < startOffset) + { + endOffset += 7; + } + + return (endOffset - startOffset) + 1; + } + } + + public int GetWeekOffset(DayOfWeek dayOfWeek) + => ((int)dayOfWeek - (int)FirstDayOfWeek + 7) % 7; + public TimeSpan? GetTimeSpan(string selectedTime) { // Regardless of the format, we need to parse the time to a TimeSpan. @@ -44,4 +67,35 @@ public record CalendarSettings(DayOfWeek FirstDayOfWeek, var dateTime = DateTime.Today.Add(timeSpan); return dateTime.ToString(format, CultureInfo.InvariantCulture); } + + public string GetTimedDayHeaderText(DateOnly date) + { + var format = string.IsNullOrWhiteSpace(TimedDayHeaderDateFormat) ? "ddd dd" : TimedDayHeaderDateFormat; + + try + { + return date.ToDateTime(TimeOnly.MinValue).ToString(format, CultureInfo); + } + catch (FormatException) + { + return date.ToDateTime(TimeOnly.MinValue).ToString("ddd dd", CultureInfo); + } + } + + public string GetTimedHourLabelText(int hour) + { + if (hour < 0 || hour > 24) + { + throw new ArgumentOutOfRangeException(nameof(hour)); + } + + if (DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour) + { + return hour.ToString(CultureInfo); + } + + var displayHour = hour % 24; + var dateTime = DateTime.Today.AddHours(displayHour); + return dateTime.ToString("h tt", CultureInfo); + } } diff --git a/Wino.Core.Domain/Models/Calendar/DayHeaderRenderModel.cs b/Wino.Core.Domain/Models/Calendar/DayHeaderRenderModel.cs deleted file mode 100644 index 2df4b2aa..00000000 --- a/Wino.Core.Domain/Models/Calendar/DayHeaderRenderModel.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Wino.Core.Domain.Models.Calendar; - -public class DayHeaderRenderModel -{ - public DayHeaderRenderModel(string dayHeader, double hourHeight) - { - DayHeader = dayHeader; - HourHeight = hourHeight; - } - - public string DayHeader { get; } - public double HourHeight { get; } -} diff --git a/Wino.Core.Domain/Models/Calendar/DayRangeRenderModel.cs b/Wino.Core.Domain/Models/Calendar/DayRangeRenderModel.cs deleted file mode 100644 index 17245c32..00000000 --- a/Wino.Core.Domain/Models/Calendar/DayRangeRenderModel.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Itenso.TimePeriod; -using Wino.Core.Domain.Enums; - -namespace Wino.Core.Domain.Models.Calendar; - -/// -/// Represents a range of days in the calendar. -/// Corresponds to 1 view of the FlipView in CalendarPage. -/// -public class DayRangeRenderModel -{ - public ITimePeriod Period { get; } - public List CalendarDays { get; } = []; - - // TODO: Get rid of this at some point. - public List DayHeaders { get; } = []; - public CalendarRenderOptions CalendarRenderOptions { get; } - - public DayRangeRenderModel(CalendarRenderOptions calendarRenderOptions) - { - CalendarRenderOptions = calendarRenderOptions; - - for (var i = 0; i < CalendarRenderOptions.TotalDayCount; i++) - { - var representingDate = calendarRenderOptions.DateRange.StartDate.AddDays(i); - var calendarDayModel = new CalendarDayModel(representingDate, calendarRenderOptions); - - CalendarDays.Add(calendarDayModel); - } - - Period = new TimeRange(CalendarDays.First().RepresentingDate, CalendarDays.Last().RepresentingDate.AddDays(1)); - - // Create day headers based on culture info. - - for (var i = 0; i < 24; i++) - { - var representingDate = calendarRenderOptions.DateRange.StartDate.Date.AddHours(i); - - string dayHeader = calendarRenderOptions.CalendarSettings.DayHeaderDisplayType switch - { - DayHeaderDisplayType.TwelveHour => representingDate.ToString("h tt", calendarRenderOptions.CalendarSettings.CultureInfo), - DayHeaderDisplayType.TwentyFourHour => representingDate.ToString("HH", calendarRenderOptions.CalendarSettings.CultureInfo), - _ => "N/A" - }; - - DayHeaders.Add(new DayHeaderRenderModel(dayHeader, calendarRenderOptions.CalendarSettings.HourHeight)); - } - } -} diff --git a/Wino.Core.Domain/Models/Calendar/ICalendarRangeTextFormatter.cs b/Wino.Core.Domain/Models/Calendar/ICalendarRangeTextFormatter.cs new file mode 100644 index 00000000..78433f04 --- /dev/null +++ b/Wino.Core.Domain/Models/Calendar/ICalendarRangeTextFormatter.cs @@ -0,0 +1,6 @@ +namespace Wino.Core.Domain.Models.Calendar; + +public interface ICalendarRangeTextFormatter +{ + string Format(VisibleDateRange range, IDateContextProvider dateContextProvider); +} diff --git a/Wino.Core.Domain/Models/Calendar/IDateContextProvider.cs b/Wino.Core.Domain/Models/Calendar/IDateContextProvider.cs new file mode 100644 index 00000000..049d622d --- /dev/null +++ b/Wino.Core.Domain/Models/Calendar/IDateContextProvider.cs @@ -0,0 +1,11 @@ +using System; +using System.Globalization; + +namespace Wino.Core.Domain.Models.Calendar; + +public interface IDateContextProvider +{ + CultureInfo Culture { get; } + TimeZoneInfo TimeZone { get; } + DateOnly GetToday(); +} diff --git a/Wino.Core.Domain/Models/Calendar/SystemDateContextProvider.cs b/Wino.Core.Domain/Models/Calendar/SystemDateContextProvider.cs new file mode 100644 index 00000000..97bdc77d --- /dev/null +++ b/Wino.Core.Domain/Models/Calendar/SystemDateContextProvider.cs @@ -0,0 +1,17 @@ +using System; +using System.Globalization; + +namespace Wino.Core.Domain.Models.Calendar; + +public sealed class SystemDateContextProvider : IDateContextProvider +{ + public CultureInfo Culture => CultureInfo.CurrentCulture; + + public TimeZoneInfo TimeZone => TimeZoneInfo.Local; + + public DateOnly GetToday() + { + var localNow = TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, TimeZone); + return DateOnly.FromDateTime(localNow.DateTime); + } +} diff --git a/Wino.Core.Domain/Models/Calendar/VisibleDateRange.cs b/Wino.Core.Domain/Models/Calendar/VisibleDateRange.cs new file mode 100644 index 00000000..d218f260 --- /dev/null +++ b/Wino.Core.Domain/Models/Calendar/VisibleDateRange.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Itenso.TimePeriod; +using Wino.Core.Domain.Enums; + +namespace Wino.Core.Domain.Models.Calendar; + +public sealed record VisibleDateRange( + CalendarDisplayType DisplayType, + DateOnly AnchorDate, + DateOnly StartDate, + DateOnly EndDate, + DateOnly PrimaryDate, + int DayCount, + bool ContainsToday, + bool SpansSingleMonth, + IReadOnlyList Dates) +{ + public DateRange ToDateRangeExclusive() + => new(StartDate.ToDateTime(TimeOnly.MinValue), EndDate.AddDays(1).ToDateTime(TimeOnly.MinValue)); + + public ITimePeriod ToTimePeriod() + => new TimeRange(StartDate.ToDateTime(TimeOnly.MinValue), EndDate.AddDays(1).ToDateTime(TimeOnly.MinValue)); + + public bool Contains(DateOnly date) + => date >= StartDate && date <= EndDate; + + public bool Contains(DateTime date) + => Contains(DateOnly.FromDateTime(date)); + + public static VisibleDateRange FromDateRange(CalendarDisplayType displayType, DateRange dateRange, DateOnly anchorDate, DateOnly today) + { + var startDate = DateOnly.FromDateTime(dateRange.StartDate); + var endDate = DateOnly.FromDateTime(dateRange.EndDate.AddDays(-1)); + var dayCount = endDate.DayNumber - startDate.DayNumber + 1; + var dates = Enumerable.Range(0, dayCount) + .Select(offset => startDate.AddDays(offset)) + .ToArray(); + + return new VisibleDateRange( + displayType, + anchorDate, + startDate, + endDate, + anchorDate, + dayCount, + today >= startDate && today <= endDate, + startDate.Year == endDate.Year && startDate.Month == endDate.Month, + dates); + } +} diff --git a/Wino.Core.Domain/Models/Common/PickedFileMetadata.cs b/Wino.Core.Domain/Models/Common/PickedFileMetadata.cs new file mode 100644 index 00000000..57c0722c --- /dev/null +++ b/Wino.Core.Domain/Models/Common/PickedFileMetadata.cs @@ -0,0 +1,9 @@ +using System.IO; + +namespace Wino.Core.Domain.Models.Common; + +public record PickedFileMetadata(string FullFilePath, long Size) +{ + public string FileName => Path.GetFileName(FullFilePath); + public string FileExtension => Path.GetExtension(FullFilePath)?.ToLowerInvariant() ?? string.Empty; +} diff --git a/Wino.Core.Domain/Models/Common/SharedFile.cs b/Wino.Core.Domain/Models/Common/SharedFile.cs index c5b28f6a..fe1c3640 100644 --- a/Wino.Core.Domain/Models/Common/SharedFile.cs +++ b/Wino.Core.Domain/Models/Common/SharedFile.cs @@ -10,4 +10,5 @@ namespace Wino.Core.Domain.Models.Common; public record SharedFile(string FullFilePath, byte[] Data) { public string FileName => Path.GetFileName(FullFilePath); + public string FileExtension => Path.GetExtension(FullFilePath)?.ToLowerInvariant(); } diff --git a/Wino.Core.Domain/Models/Comparers/DateComparer.cs b/Wino.Core.Domain/Models/Comparers/DateComparer.cs deleted file mode 100644 index 7729831c..00000000 --- a/Wino.Core.Domain/Models/Comparers/DateComparer.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using Wino.Core.Domain.Models.MailItem; - -namespace Wino.Core.Domain.Models.Comparers; - -public class DateComparer : IComparer, IEqualityComparer -{ - public int Compare(IMailItem x, IMailItem y) - { - return DateTime.Compare(y.CreationDate, x.CreationDate); - } - - public new bool Equals(object x, object y) - { - if (x is IMailItem firstItem && y is IMailItem secondItem) - { - return firstItem.Equals(secondItem); - } - - return false; - } - - public int GetHashCode(object obj) => (obj as IMailItem).GetHashCode(); - - public DateComparer() - { - - } -} diff --git a/Wino.Core.Domain/Models/Comparers/DateTimeComparer.cs b/Wino.Core.Domain/Models/Comparers/DateTimeComparer.cs deleted file mode 100644 index c5ae37a0..00000000 --- a/Wino.Core.Domain/Models/Comparers/DateTimeComparer.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Wino.Core.Domain.Models.Comparers; - -/// -/// Used to insert date grouping into proper place in Reader page. -/// -public class DateTimeComparer : IComparer -{ - public int Compare(DateTime x, DateTime y) - { - return DateTime.Compare(y, x); - } -} diff --git a/Wino.Core.Domain/Models/Comparers/FolderNameComparer.cs b/Wino.Core.Domain/Models/Comparers/FolderNameComparer.cs deleted file mode 100644 index 440391f2..00000000 --- a/Wino.Core.Domain/Models/Comparers/FolderNameComparer.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; -using Wino.Core.Domain.Entities.Mail; - -namespace Wino.Core.Domain.Models.Comparers; - -public class FolderNameComparer : IComparer -{ - public int Compare(MailItemFolder x, MailItemFolder y) - { - return x.FolderName.CompareTo(y.FolderName); - } -} diff --git a/Wino.Core.Domain/Models/Comparers/ListItemComparer.cs b/Wino.Core.Domain/Models/Comparers/ListItemComparer.cs index 393b9f6b..50aecc8a 100644 --- a/Wino.Core.Domain/Models/Comparers/ListItemComparer.cs +++ b/Wino.Core.Domain/Models/Comparers/ListItemComparer.cs @@ -1,37 +1,22 @@ using System; using System.Collections.Generic; -using Wino.Core.Domain.Models.MailItem; - -namespace Wino.Core.Domain.Models.Comparers; +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Interfaces; public class ListItemComparer : IComparer { public bool SortByName { get; set; } - public DateComparer DateComparer = new DateComparer(); - public readonly NameComparer NameComparer = new NameComparer(); - public int Compare(object x, object y) { - if (x is IMailItem xMail && y is IMailItem yMail) - { - var itemComparer = GetItemComparer(); - - return itemComparer.Compare(xMail, yMail); - } + if (x is IMailListItemSorting xSorting && y is IMailListItemSorting ySorting) + return SortByName ? string.Compare(xSorting.SortingName, ySorting.SortingName, StringComparison.OrdinalIgnoreCase) : DateTime.Compare(ySorting.SortingDate, xSorting.SortingDate); + else if (x is MailCopy xMail && y is MailCopy yMail) + return SortByName ? string.Compare(xMail.FromName, yMail.FromName, StringComparison.OrdinalIgnoreCase) : DateTime.Compare(yMail.CreationDate, xMail.CreationDate); else if (x is DateTime dateX && y is DateTime dateY) return DateTime.Compare(dateY, dateX); else if (x is string stringX && y is string stringY) return stringY.CompareTo(stringX); - return 0; } - - public IComparer GetItemComparer() - { - if (SortByName) - return NameComparer; - else - return DateComparer; - } } diff --git a/Wino.Core.Domain/Models/Comparers/NameComparer.cs b/Wino.Core.Domain/Models/Comparers/NameComparer.cs deleted file mode 100644 index 9dd03636..00000000 --- a/Wino.Core.Domain/Models/Comparers/NameComparer.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; -using Wino.Core.Domain.Models.MailItem; - -namespace Wino.Core.Domain.Models.Comparers; - -public class NameComparer : IComparer -{ - public int Compare(IMailItem x, IMailItem y) - { - return string.Compare(x.FromName, y.FromName); - } -} diff --git a/Wino.Core.Domain/Models/Connectivity/ConnectionPoolHealth.cs b/Wino.Core.Domain/Models/Connectivity/ConnectionPoolHealth.cs new file mode 100644 index 00000000..a80424a8 --- /dev/null +++ b/Wino.Core.Domain/Models/Connectivity/ConnectionPoolHealth.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; + +namespace Wino.Core.Domain.Models.Connectivity; + +/// +/// Represents the health status of an IMAP connection pool. +/// +public class ConnectionPoolHealth +{ + /// + /// Gets or sets the total number of connections in the pool (including IDLE). + /// + public int TotalConnections { get; set; } + + /// + /// Gets or sets the number of connections available for use. + /// + public int AvailableConnections { get; set; } + + /// + /// Gets or sets the number of connections currently in use. + /// + public int InUseConnections { get; set; } + + /// + /// Gets or sets the number of connections that have failed and need reconnection. + /// + public int FailedConnections { get; set; } + + /// + /// Gets or sets the number of connections currently reconnecting. + /// + public int ReconnectingConnections { get; set; } + + /// + /// Gets or sets whether the dedicated IDLE connection is active and listening. + /// + public bool IdleConnectionActive { get; set; } + + /// + /// Gets or sets the timestamp of the last health check. + /// + public DateTime LastHealthCheck { get; set; } + + /// + /// Gets or sets recent issues encountered by the pool. + /// + public List RecentIssues { get; set; } = []; + + /// + /// Gets whether the pool is healthy (has minimum required connections). + /// + public bool IsHealthy => AvailableConnections >= 1 && FailedConnections == 0; + + /// + /// Gets a summary of the pool health. + /// + public string Summary => $"Total: {TotalConnections}, Available: {AvailableConnections}, InUse: {InUseConnections}, Failed: {FailedConnections}, IDLE: {(IdleConnectionActive ? "Active" : "Inactive")}"; +} diff --git a/Wino.Core.Domain/Models/Connectivity/ImapClientPoolOptions.cs b/Wino.Core.Domain/Models/Connectivity/ImapClientPoolOptions.cs index 9735e4f8..a9192d24 100644 --- a/Wino.Core.Domain/Models/Connectivity/ImapClientPoolOptions.cs +++ b/Wino.Core.Domain/Models/Connectivity/ImapClientPoolOptions.cs @@ -1,24 +1,21 @@ -using System.IO; using Wino.Core.Domain.Entities.Shared; namespace Wino.Core.Domain.Models.Connectivity; public class ImapClientPoolOptions { - public Stream ProtocolLog { get; } public CustomServerInformation ServerInformation { get; } public bool IsTestPool { get; } - protected ImapClientPoolOptions(CustomServerInformation serverInformation, Stream protocolLog, bool isTestPool) + protected ImapClientPoolOptions(CustomServerInformation serverInformation, bool isTestPool) { ServerInformation = serverInformation; - ProtocolLog = protocolLog; IsTestPool = isTestPool; } - public static ImapClientPoolOptions CreateDefault(CustomServerInformation serverInformation, Stream protocolLog) - => new(serverInformation, protocolLog, false); + public static ImapClientPoolOptions CreateDefault(CustomServerInformation serverInformation) + => new(serverInformation, false); - public static ImapClientPoolOptions CreateTestPool(CustomServerInformation serverInformation, Stream protocolLog) - => new(serverInformation, protocolLog, true); + public static ImapClientPoolOptions CreateTestPool(CustomServerInformation serverInformation) + => new(serverInformation, true); } diff --git a/Wino.Core.Domain/Models/Connectivity/ImapConnectivityTestResults.cs b/Wino.Core.Domain/Models/Connectivity/ImapConnectivityTestResults.cs index 4b50af52..48ed4b3c 100644 --- a/Wino.Core.Domain/Models/Connectivity/ImapConnectivityTestResults.cs +++ b/Wino.Core.Domain/Models/Connectivity/ImapConnectivityTestResults.cs @@ -18,13 +18,10 @@ public class ImapConnectivityTestResults public bool IsCertificateUIRequired { get; set; } public string FailedReason { get; set; } - public string FailureProtocolLog { get; set; } - public static ImapConnectivityTestResults Success() => new ImapConnectivityTestResults() { IsSuccess = true }; - public static ImapConnectivityTestResults Failure(Exception ex, string failureProtocolLog) => new ImapConnectivityTestResults() + public static ImapConnectivityTestResults Failure(Exception ex) => new ImapConnectivityTestResults() { - FailedReason = string.Join(Environment.NewLine, ex.GetInnerExceptions().Select(e => e.Message)), - FailureProtocolLog = failureProtocolLog + FailedReason = string.Join(Environment.NewLine, ex.GetInnerExceptions().Select(e => e.Message)) }; public static ImapConnectivityTestResults CertificateUIRequired(string issuer, diff --git a/Wino.Core.Domain/Models/Contacts/PagedContactsResult.cs b/Wino.Core.Domain/Models/Contacts/PagedContactsResult.cs new file mode 100644 index 00000000..20f6530a --- /dev/null +++ b/Wino.Core.Domain/Models/Contacts/PagedContactsResult.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Wino.Core.Domain.Entities.Shared; + +namespace Wino.Core.Domain.Models.Contacts; + +public record PagedContactsResult( + IReadOnlyList Contacts, + int TotalCount, + bool HasMore, + int Offset, + int PageSize); diff --git a/Wino.Core.Domain/Models/Folders/FolderOperationMenuItem.cs b/Wino.Core.Domain/Models/Folders/FolderOperationMenuItem.cs index ac81ad54..f0b2e5bc 100644 --- a/Wino.Core.Domain/Models/Folders/FolderOperationMenuItem.cs +++ b/Wino.Core.Domain/Models/Folders/FolderOperationMenuItem.cs @@ -1,13 +1,15 @@ -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Enums; using Wino.Core.Domain.Models.Menus; namespace Wino.Core.Domain.Models.Folders; -public class FolderOperationMenuItem : MenuOperationItemBase, IMenuOperation +public class FolderOperationMenuItem : MenuOperationItemBase { - protected FolderOperationMenuItem(FolderOperation operation, bool isEnabled) : base(operation, isEnabled) { } + protected FolderOperationMenuItem(FolderOperation operation, bool isEnabled, bool isSecondaryMenuItem = false) : base(operation, isEnabled) + { + IsSecondaryMenuPreferred = isSecondaryMenuItem; + } - public static FolderOperationMenuItem Create(FolderOperation operation, bool isEnabled = true) - => new FolderOperationMenuItem(operation, isEnabled); + public static FolderOperationMenuItem Create(FolderOperation operation, bool isEnabled = true, bool isSecondaryMenuItem = false) + => new FolderOperationMenuItem(operation, isEnabled, isSecondaryMenuItem); } diff --git a/Wino.Core.Domain/Models/Folders/IMailItemFolder.cs b/Wino.Core.Domain/Models/Folders/IMailItemFolder.cs index 866213f9..cd3c0a10 100644 --- a/Wino.Core.Domain/Models/Folders/IMailItemFolder.cs +++ b/Wino.Core.Domain/Models/Folders/IMailItemFolder.cs @@ -10,12 +10,14 @@ public interface IMailItemFolder string DeltaToken { get; set; } string FolderName { get; set; } long HighestModeSeq { get; set; } + uint HighestKnownUid { get; set; } Guid Id { get; set; } bool IsHidden { get; set; } bool IsSticky { get; set; } bool IsSynchronizationEnabled { get; set; } bool IsSystemFolder { get; set; } DateTime? LastSynchronizedDate { get; set; } + DateTime? LastUidReconcileUtc { get; set; } Guid MailAccountId { get; set; } string ParentRemoteFolderId { get; set; } string RemoteFolderId { get; set; } diff --git a/Wino.Core.Domain/Models/KeyboardShortcutDialogResult.cs b/Wino.Core.Domain/Models/KeyboardShortcutDialogResult.cs new file mode 100644 index 00000000..d8079ce5 --- /dev/null +++ b/Wino.Core.Domain/Models/KeyboardShortcutDialogResult.cs @@ -0,0 +1,60 @@ +using Wino.Core.Domain.Enums; + +namespace Wino.Core.Domain.Models; + +/// +/// Result returned from keyboard shortcut dialog. +/// +public class KeyboardShortcutDialogResult +{ + /// + /// Whether the dialog was completed successfully. + /// + public bool IsSuccess { get; set; } + + /// + /// The application mode selected by the user. + /// + public WinoApplicationMode Mode { get; set; } = WinoApplicationMode.Mail; + + /// + /// The key combination entered by the user. + /// + public string Key { get; set; } = string.Empty; + + /// + /// The modifier keys selected by the user. + /// + public ModifierKeys ModifierKeys { get; set; } + + /// + /// The shortcut action selected by the user. + /// + public KeyboardShortcutAction Action { get; set; } + + /// + /// Creates a successful result. + /// + public static KeyboardShortcutDialogResult Success(WinoApplicationMode mode, string key, ModifierKeys modifierKeys, KeyboardShortcutAction action) + { + return new KeyboardShortcutDialogResult + { + IsSuccess = true, + Mode = mode, + Key = key, + ModifierKeys = modifierKeys, + Action = action + }; + } + + /// + /// Creates a canceled result. + /// + public static KeyboardShortcutDialogResult Canceled() + { + return new KeyboardShortcutDialogResult + { + IsSuccess = false + }; + } +} diff --git a/Wino.Core.Domain/Models/KeyboardShortcutTriggerDetails.cs b/Wino.Core.Domain/Models/KeyboardShortcutTriggerDetails.cs new file mode 100644 index 00000000..40ac92b2 --- /dev/null +++ b/Wino.Core.Domain/Models/KeyboardShortcutTriggerDetails.cs @@ -0,0 +1,16 @@ +using System; +using Wino.Core.Domain.Enums; + +namespace Wino.Core.Domain.Models; + +public class KeyboardShortcutTriggerDetails +{ + public Guid ShortcutId { get; init; } + public WinoApplicationMode Mode { get; init; } + public KeyboardShortcutAction Action { get; init; } + public string Key { get; init; } = string.Empty; + public ModifierKeys ModifierKeys { get; init; } + public bool Handled { get; set; } + public object Sender { get; init; } + public object Origin { get; init; } +} diff --git a/Wino.Core.Domain/Models/MailItem/HtmlPreviewVisitor.cs b/Wino.Core.Domain/Models/MailItem/HtmlPreviewVisitor.cs index deb35a54..f12cab52 100644 --- a/Wino.Core.Domain/Models/MailItem/HtmlPreviewVisitor.cs +++ b/Wino.Core.Domain/Models/MailItem/HtmlPreviewVisitor.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using MimeKit; +using MimeKit.Cryptography; using MimeKit.Text; using MimeKit.Tnef; @@ -12,12 +13,23 @@ namespace Wino.Core.Domain.Models.MailItem; /// public class HtmlPreviewVisitor : MimeVisitor { - List stack = new List(); - List attachments = new List(); + private static readonly HashSet BlockedTags = new(StringComparer.OrdinalIgnoreCase) + { + "script", "iframe", "frame", "frameset", "object", "embed", "applet", "base", "meta", "form" + }; + + private static readonly HashSet AllowedDataImageMimeTypes = new(StringComparer.OrdinalIgnoreCase) + { + "image/png", "image/jpeg", "image/jpg", "image/gif", "image/webp", "image/bmp", "image/x-icon", "image/avif", "image/svg+xml" + }; + + private readonly List stack = []; + private readonly List attachments = []; readonly string tempDir; public string Body { get; set; } + public Dictionary Signatures = []; /// /// Creates a new HtmlPreviewVisitor. @@ -46,9 +58,22 @@ public class HtmlPreviewVisitor : MimeVisitor protected override void VisitMultipartAlternative(MultipartAlternative alternative) { - // walk the multipart/alternative children backwards from greatest level of faithfulness to the least faithful + // Prefer rich body alternatives first, and only fall back to calendar text if nothing else exists. for (int i = alternative.Count - 1; i >= 0 && Body == null; i--) + { + if (IsCalendarText(alternative[i])) + continue; + alternative[i].Accept(this); + } + + for (int i = alternative.Count - 1; i >= 0 && Body == null; i--) + { + if (!IsCalendarText(alternative[i])) + continue; + + alternative[i].Accept(this); + } } protected override void VisitMultipartRelated(MultipartRelated related) @@ -65,12 +90,23 @@ public class HtmlPreviewVisitor : MimeVisitor stack.RemoveAt(stack.Count - 1); } + protected override void VisitMultipartSigned(MultipartSigned signed) + { + VerifySignatures(signed.Verify()); + VisitMultipart(signed); + } + // look up the image based on the img src url within our multipart/related stack bool TryGetImage(string url, out MimePart image) { + image = null; + + if (string.IsNullOrWhiteSpace(url)) + return false; + UriKind kind; int index; - Uri uri; + Uri uri = null; if (Uri.IsWellFormedUriString(url, UriKind.Absolute)) kind = UriKind.Absolute; @@ -85,24 +121,50 @@ public class HtmlPreviewVisitor : MimeVisitor } catch { - image = null; - return false; + // noop: we still attempt CID/content-id lookup below. } for (int i = stack.Count - 1; i >= 0; i--) { - if ((index = stack[i].IndexOf(uri)) == -1) + if (uri != null && (index = stack[i].IndexOf(uri)) != -1) + { + image = stack[i][index] as MimePart; + + if (image != null) + return true; + } + + var normalizedContentId = NormalizeContentId(url); + + if (string.IsNullOrEmpty(normalizedContentId)) continue; - image = stack[i][index] as MimePart; - return image != null; + foreach (var relatedPart in stack[i]) + { + if (relatedPart is not MimePart candidate || string.IsNullOrEmpty(candidate.ContentId)) + continue; + + if (string.Equals(candidate.ContentId.Trim('<', '>'), normalizedContentId, StringComparison.OrdinalIgnoreCase)) + { + image = candidate; + return true; + } + } } - image = null; - return false; } + private static string NormalizeContentId(string url) + { + var trimmed = url.Trim().Trim('\'', '"', '<', '>'); + + if (trimmed.StartsWith("cid:", StringComparison.OrdinalIgnoreCase)) + trimmed = trimmed[4..]; + + return trimmed.Trim('<', '>'); + } + // Save the image to our temp directory and return a "file://" url suitable for // the browser control to load. // Note: if you'd rather embed the image data into the HTML, you can construct a @@ -118,83 +180,173 @@ public class HtmlPreviewVisitor : MimeVisitor return string.Format("data:{0};base64,{1}", image.ContentType.MimeType, base64); } - - //string fileName = url - // .Replace(':', '_') - // .Replace('\\', '_') - // .Replace('/', '_'); - - //string path = Path.Combine(tempDir, fileName); - - //if (!File.Exists(path)) - //{ - // using (var output = File.Create(path)) - // image.Content.DecodeTo(output); - //} - - //return "file://" + path.Replace('\\', '/'); } - // Replaces urls that refer to images embedded within the message with - // "file://" urls that the browser control will actually be able to load. + // Replaces image references that refer to images embedded within the message with + // "data:" urls the browser control can load. Also sanitizes dangerous tags/attributes. void HtmlTagCallback(HtmlTagContext ctx, HtmlWriter htmlWriter) { - if (ctx.TagId == HtmlTagId.Image && !ctx.IsEndTag && stack.Count > 0) + var tagName = ctx.TagName; + + if (BlockedTags.Contains(tagName)) { - ctx.WriteTag(htmlWriter, false); - - // replace the src attribute with a file:// URL - foreach (var attribute in ctx.Attributes) - { - if (attribute.Id == HtmlAttributeId.Src) - { - MimePart image; - string url; - - if (!TryGetImage(attribute.Value, out image)) - { - htmlWriter.WriteAttribute(attribute); - continue; - } - - url = SaveImage(image); - - htmlWriter.WriteAttributeName(attribute.Name); - htmlWriter.WriteAttributeValue(url); - } - else - { - htmlWriter.WriteAttribute(attribute); - } - } + ctx.DeleteTag = true; + ctx.DeleteEndTag = true; + return; } - else if (ctx.TagId == HtmlTagId.Body && !ctx.IsEndTag) - { - ctx.WriteTag(htmlWriter, false); - // add and/or replace oncontextmenu="return false;" - foreach (var attribute in ctx.Attributes) + if (ctx.IsEndTag) + { + ctx.WriteTag(htmlWriter, true); + return; + } + + ctx.WriteTag(htmlWriter, false); + + foreach (var attribute in ctx.Attributes) + { + var attributeName = attribute.Name; + + if (ShouldDropAttribute(tagName, attributeName)) + continue; + + if (TryResolveImageAttribute(tagName, attributeName, attribute.Value, out var resolvedValue)) { - if (attribute.Name.ToLowerInvariant() == "oncontextmenu") + htmlWriter.WriteAttributeName(attributeName); + htmlWriter.WriteAttributeValue(resolvedValue); + continue; + } + + if (IsUrlAttribute(attributeName)) + { + if (!TrySanitizeUrlValue(attribute.Value, out var sanitizedUrl)) continue; - htmlWriter.WriteAttribute(attribute); + htmlWriter.WriteAttributeName(attributeName); + htmlWriter.WriteAttributeValue(sanitizedUrl); + continue; } + htmlWriter.WriteAttribute(attribute); + } + + if (ctx.TagId == HtmlTagId.Body) htmlWriter.WriteAttribute("oncontextmenu", "return false;"); - } - else + } + + private bool TryResolveImageAttribute(string tagName, string attributeName, string value, out string resolvedValue) + { + resolvedValue = null; + + if (string.IsNullOrWhiteSpace(value)) + return false; + + var lowerAttributeName = attributeName.ToLowerInvariant(); + var isImageTag = string.Equals(tagName, "img", StringComparison.OrdinalIgnoreCase); + + if (isImageTag && lowerAttributeName == "srcset") { - if (ctx.TagId == HtmlTagId.Unknown) - { - ctx.DeleteTag = true; - ctx.DeleteEndTag = true; - } - else - { - ctx.WriteTag(htmlWriter, true); - } + resolvedValue = ResolveSrcSet(value); + return resolvedValue != value; } + + if (lowerAttributeName != "src" && lowerAttributeName != "background" && lowerAttributeName != "poster") + return false; + + if (TryGetImage(value, out var image)) + { + resolvedValue = SaveImage(image); + return true; + } + + return false; + } + + private string ResolveSrcSet(string srcSetValue) + { + var candidates = srcSetValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var updatedCandidates = new List(candidates.Length); + + foreach (var candidate in candidates) + { + var parts = candidate.Split(' ', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length == 0) + continue; + + var imageSource = parts[0]; + + if (TryGetImage(imageSource, out var image)) + imageSource = SaveImage(image); + + updatedCandidates.Add(parts.Length == 2 ? $"{imageSource} {parts[1]}" : imageSource); + } + + return string.Join(", ", updatedCandidates); + } + + private static bool ShouldDropAttribute(string tagName, string attributeName) + { + if (attributeName.StartsWith("on", StringComparison.OrdinalIgnoreCase)) + return true; + + if (string.Equals(tagName, "body", StringComparison.OrdinalIgnoreCase) + && string.Equals(attributeName, "oncontextmenu", StringComparison.OrdinalIgnoreCase)) + return true; + + if (string.Equals(attributeName, "srcdoc", StringComparison.OrdinalIgnoreCase)) + return true; + + return false; + } + + private static bool IsUrlAttribute(string attributeName) + => string.Equals(attributeName, "href", StringComparison.OrdinalIgnoreCase) + || string.Equals(attributeName, "src", StringComparison.OrdinalIgnoreCase) + || string.Equals(attributeName, "action", StringComparison.OrdinalIgnoreCase) + || string.Equals(attributeName, "xlink:href", StringComparison.OrdinalIgnoreCase) + || string.Equals(attributeName, "background", StringComparison.OrdinalIgnoreCase) + || string.Equals(attributeName, "poster", StringComparison.OrdinalIgnoreCase); + + private static bool TrySanitizeUrlValue(string rawValue, out string sanitizedValue) + { + sanitizedValue = null; + + if (string.IsNullOrWhiteSpace(rawValue)) + return false; + + var value = rawValue.Trim().Trim('"', '\''); + + if (value.StartsWith("javascript:", StringComparison.OrdinalIgnoreCase) + || value.StartsWith("vbscript:", StringComparison.OrdinalIgnoreCase)) + return false; + + if (value.StartsWith("data:", StringComparison.OrdinalIgnoreCase) && !IsAllowedImageDataUrl(value)) + return false; + + sanitizedValue = value; + return true; + } + + private static bool IsAllowedImageDataUrl(string value) + { + const string dataPrefix = "data:"; + + if (!value.StartsWith(dataPrefix, StringComparison.OrdinalIgnoreCase)) + return false; + + var payloadStart = value.IndexOf(',', StringComparison.Ordinal); + + if (payloadStart <= dataPrefix.Length) + return false; + + var metadata = value[dataPrefix.Length..payloadStart]; + var metadataParts = metadata.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + if (metadataParts.Length == 0) + return false; + + return AllowedDataImageMimeTypes.Contains(metadataParts[0]); } protected override void VisitTextPart(TextPart entity) @@ -233,6 +385,10 @@ public class HtmlPreviewVisitor : MimeVisitor Body = converter.Convert(entity.Text); } + private static bool IsCalendarText(MimeEntity entity) + => entity is TextPart textPart && + textPart.ContentType?.MimeType?.Equals("text/calendar", StringComparison.OrdinalIgnoreCase) == true; + protected override void VisitTnefPart(TnefPart entity) { // extract any attachments in the MS-TNEF part @@ -247,8 +403,40 @@ public class HtmlPreviewVisitor : MimeVisitor protected override void VisitMimePart(MimePart entity) { - // realistically, if we've gotten this far, then we can treat this as an attachment - // even if the IsAttachment property is false. - attachments.Add(entity); + if (entity is ApplicationPkcs7Mime { SecureMimeType: SecureMimeType.EnvelopedData } encrypted) + { + encrypted.Decrypt().Accept(this); + } + else if (entity is ApplicationPkcs7Mime { SecureMimeType: SecureMimeType.SignedData } signed) + { + MimeEntity extracted; + + VerifySignatures(signed.Verify(out extracted)); + + extracted.Accept(this); + } + else + { + // realistically, if we've gotten this far, then we can treat this as an attachment + // even if the IsAttachment property is false. + attachments.Add(entity); + } + } + + private void VerifySignatures(DigitalSignatureCollection signatures) + { + foreach (var signature in signatures) + { + try + { + bool valid = signature.Verify(); + Signatures.Add(signature, valid); + } + catch (DigitalSignatureVerifyException) + { + // There was an error verifying the signature. + Signatures.Add(signature, false); + } + } } } diff --git a/Wino.Core.Domain/Models/MailItem/IMailHashContainer.cs b/Wino.Core.Domain/Models/MailItem/IMailHashContainer.cs deleted file mode 100644 index 52cd64ad..00000000 --- a/Wino.Core.Domain/Models/MailItem/IMailHashContainer.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Wino.Core.Domain.Models.MailItem; - -/// -/// An interface that returns the UniqueId store for IMailItem. -/// For threads, it may be multiple items. -/// For single mails, it'll always be one item. -/// -public interface IMailHashContainer -{ - IEnumerable GetContainingIds(); -} diff --git a/Wino.Core.Domain/Models/MailItem/IMailItem.cs b/Wino.Core.Domain/Models/MailItem/IMailItem.cs deleted file mode 100644 index b5cd99b2..00000000 --- a/Wino.Core.Domain/Models/MailItem/IMailItem.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using Wino.Core.Domain.Entities.Mail; -using Wino.Core.Domain.Entities.Shared; - -namespace Wino.Core.Domain.Models.MailItem; - -/// -/// Interface of simplest representation of a MailCopy. -/// -public interface IMailItem : IMailHashContainer -{ - Guid UniqueId { get; } - string Id { get; } - string Subject { get; } - string ThreadId { get; } - string MessageId { get; } - string References { get; } - string InReplyTo { get; } - string PreviewText { get; } - string FromName { get; } - DateTime CreationDate { get; } - string FromAddress { get; } - bool HasAttachments { get; } - bool IsFlagged { get; } - bool IsFocused { get; } - bool IsRead { get; } - string DraftId { get; } - bool IsDraft { get; } - Guid FileId { get; } - - MailItemFolder AssignedFolder { get; } - MailAccount AssignedAccount { get; } - AccountContact SenderContact { get; } -} diff --git a/Wino.Core.Domain/Models/MailItem/IMailItemThread.cs b/Wino.Core.Domain/Models/MailItem/IMailItemThread.cs deleted file mode 100644 index 5c3cd554..00000000 --- a/Wino.Core.Domain/Models/MailItem/IMailItemThread.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.ObjectModel; - -namespace Wino.Core.Domain.Models.MailItem; - -/// -/// Interface that represents conversation threads. -/// Even though this type has 1 single UI representation most of the time, -/// it can contain multiple IMailItem. -/// -public interface IMailItemThread : IMailItem -{ - ObservableCollection ThreadItems { get; } - IMailItem LatestMailItem { get; } - IMailItem FirstMailItem { get; } -} diff --git a/Wino.Core.Domain/Models/MailItem/MailDragPackage.cs b/Wino.Core.Domain/Models/MailItem/MailDragPackage.cs index e2fb2949..871aded8 100644 --- a/Wino.Core.Domain/Models/MailItem/MailDragPackage.cs +++ b/Wino.Core.Domain/Models/MailItem/MailDragPackage.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Wino.Core.Domain.Entities.Mail; namespace Wino.Core.Domain.Models.MailItem; @@ -7,12 +8,12 @@ namespace Wino.Core.Domain.Models.MailItem; /// public class MailDragPackage { - public MailDragPackage(IEnumerable draggingMails) + public MailDragPackage(IEnumerable draggingMails) { DraggingMails = draggingMails; } - public MailDragPackage(IMailItem draggingMail) + public MailDragPackage(MailCopy draggingMail) { DraggingMails = [ @@ -20,5 +21,5 @@ public class MailDragPackage ]; } - public IEnumerable DraggingMails { get; set; } + public IEnumerable DraggingMails { get; set; } } diff --git a/Wino.Core.Domain/Models/MailItem/MailInsertPackage.cs b/Wino.Core.Domain/Models/MailItem/MailInsertPackage.cs index f1eec88c..c57b1589 100644 --- a/Wino.Core.Domain/Models/MailItem/MailInsertPackage.cs +++ b/Wino.Core.Domain/Models/MailItem/MailInsertPackage.cs @@ -1,6 +1,12 @@ -using MimeKit; +using System.Collections.Generic; +using MimeKit; using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Entities.Shared; namespace Wino.Core.Domain.Models.MailItem; -public record NewMailItemPackage(MailCopy Copy, MimeMessage Mime, string AssignedRemoteFolderId); +public record NewMailItemPackage( + MailCopy Copy, + MimeMessage Mime, + string AssignedRemoteFolderId, + IReadOnlyList ExtractedContacts = null); diff --git a/Wino.Core.Domain/Models/MailItem/MailListInitializationOptions.cs b/Wino.Core.Domain/Models/MailItem/MailListInitializationOptions.cs index 033ec012..7d97e19d 100644 --- a/Wino.Core.Domain/Models/MailItem/MailListInitializationOptions.cs +++ b/Wino.Core.Domain/Models/MailItem/MailListInitializationOptions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Enums; @@ -12,5 +13,7 @@ public record MailListInitializationOptions(IEnumerable Folders bool CreateThreads, bool? IsFocusedOnly, string SearchQuery, - IEnumerable ExistingUniqueIds, - List PreFetchMailCopies = null); + ConcurrentDictionary ExistingUniqueIds = null, + List PreFetchMailCopies = null, + int Skip = 0, + int Take = 0); diff --git a/Wino.Core.Domain/Models/MailItem/ThreadMailItem.cs b/Wino.Core.Domain/Models/MailItem/ThreadMailItem.cs deleted file mode 100644 index 1d13f909..00000000 --- a/Wino.Core.Domain/Models/MailItem/ThreadMailItem.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using Wino.Core.Domain.Entities.Mail; -using Wino.Core.Domain.Entities.Shared; - -namespace Wino.Core.Domain.Models.MailItem; - -public class ThreadMailItem : IMailItemThread -{ - // TODO: Ideally this should be SortedList. - public ObservableCollection ThreadItems { get; } = new ObservableCollection(); - - public IMailItem LatestMailItem => ThreadItems.LastOrDefault(); - public IMailItem FirstMailItem => ThreadItems.FirstOrDefault(); - - public bool AddThreadItem(IMailItem item) - { - if (item == null) return false; - - if (ThreadItems.Any(a => a.Id == item.Id)) - { - return false; - } - - if (item != null && item.IsDraft) - { - ThreadItems.Insert(0, item); - return true; - } - - var insertItem = ThreadItems.FirstOrDefault(a => !a.IsDraft && a.CreationDate < item.CreationDate); - - if (insertItem == null) - ThreadItems.Insert(ThreadItems.Count, item); - else - { - var index = ThreadItems.IndexOf(insertItem); - - ThreadItems.Insert(index, item); - } - - return true; - } - - public IEnumerable GetContainingIds() => ThreadItems?.Select(a => a.UniqueId) ?? default; - - #region IMailItem - - public Guid UniqueId => LatestMailItem?.UniqueId ?? Guid.Empty; - public string Id => LatestMailItem?.Id ?? string.Empty; - - // Show subject from last item. - public string Subject => LatestMailItem?.Subject ?? string.Empty; - - public string ThreadId => LatestMailItem?.ThreadId ?? string.Empty; - - public string PreviewText => FirstMailItem?.PreviewText ?? string.Empty; - - public string FromName => LatestMailItem?.FromName ?? string.Empty; - - public string FromAddress => LatestMailItem?.FromAddress ?? string.Empty; - - public bool HasAttachments => ThreadItems.Any(a => a.HasAttachments); - - public bool IsFlagged => ThreadItems.Any(a => a.IsFlagged); - - public bool IsFocused => LatestMailItem?.IsFocused ?? false; - - public bool IsRead => ThreadItems.All(a => a.IsRead); - - public DateTime CreationDate => FirstMailItem?.CreationDate ?? DateTime.MinValue; - - public bool IsDraft => ThreadItems.Any(a => a.IsDraft); - - public string DraftId => string.Empty; - - public string MessageId => LatestMailItem?.MessageId; - - public string References => LatestMailItem?.References ?? string.Empty; - - public string InReplyTo => LatestMailItem?.InReplyTo ?? string.Empty; - - public MailItemFolder AssignedFolder => LatestMailItem?.AssignedFolder; - - public MailAccount AssignedAccount => LatestMailItem?.AssignedAccount; - - public Guid FileId => LatestMailItem?.FileId ?? Guid.Empty; - - public AccountContact SenderContact => LatestMailItem?.SenderContact; - - #endregion -} diff --git a/Wino.Core.Domain/Models/MailItem/ToggleRequestRule.cs b/Wino.Core.Domain/Models/MailItem/ToggleRequestRule.cs index 70045ead..e3ebb453 100644 --- a/Wino.Core.Domain/Models/MailItem/ToggleRequestRule.cs +++ b/Wino.Core.Domain/Models/MailItem/ToggleRequestRule.cs @@ -1,4 +1,5 @@ using System; +using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Enums; namespace Wino.Core.Domain.Models.MailItem; @@ -10,4 +11,4 @@ namespace Wino.Core.Domain.Models.MailItem; /// /// /// -public record ToggleRequestRule(MailOperation SourceAction, MailOperation TargetAction, Func Condition); +public record ToggleRequestRule(MailOperation SourceAction, MailOperation TargetAction, Func Condition); diff --git a/Wino.Core.Domain/Models/Menus/MailOperationMenuItem.cs b/Wino.Core.Domain/Models/Menus/MailOperationMenuItem.cs index 7c243f55..a3e228b7 100644 --- a/Wino.Core.Domain/Models/Menus/MailOperationMenuItem.cs +++ b/Wino.Core.Domain/Models/Menus/MailOperationMenuItem.cs @@ -1,15 +1,9 @@ -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Enums; namespace Wino.Core.Domain.Models.Menus; -public class MailOperationMenuItem : MenuOperationItemBase, IMenuOperation +public class MailOperationMenuItem : MenuOperationItemBase { - /// - /// Gets or sets whether this menu item should be placed in SecondaryCommands if used in CommandBar. - /// - public bool IsSecondaryMenuPreferred { get; set; } - protected MailOperationMenuItem(MailOperation operation, bool isEnabled, bool isSecondaryMenuItem = false) : base(operation, isEnabled) { IsSecondaryMenuPreferred = isSecondaryMenuItem; diff --git a/Wino.Core.Domain/Models/Menus/MenuOperationItemBase.cs b/Wino.Core.Domain/Models/Menus/MenuOperationItemBase.cs index 30f4a1a0..cc2bcdcb 100644 --- a/Wino.Core.Domain/Models/Menus/MenuOperationItemBase.cs +++ b/Wino.Core.Domain/Models/Menus/MenuOperationItemBase.cs @@ -1,9 +1,16 @@ -using System; +using System; +using CommunityToolkit.Mvvm.ComponentModel; +using Wino.Core.Domain.Interfaces; namespace Wino.Core.Domain.Models.Menus; -public class MenuOperationItemBase where TOperation : Enum +public class MenuOperationItemBase : ObservableObject, IMenuOperation where TOperation : Enum { + private TOperation _operation; + private string _identifier = string.Empty; + private bool _isEnabled; + private bool _isSecondaryMenuPreferred; + public MenuOperationItemBase(TOperation operation, bool isEnabled) { Operation = operation; @@ -11,7 +18,33 @@ public class MenuOperationItemBase where TOperation : Enum Identifier = operation.ToString(); } - public TOperation Operation { get; set; } - public string Identifier { get; set; } - public bool IsEnabled { get; set; } + public TOperation Operation + { + get => _operation; + set + { + if (SetProperty(ref _operation, value)) + { + Identifier = value.ToString(); + } + } + } + + public string Identifier + { + get => _identifier; + protected set => SetProperty(ref _identifier, value); + } + + public bool IsEnabled + { + get => _isEnabled; + set => SetProperty(ref _isEnabled, value); + } + + public bool IsSecondaryMenuPreferred + { + get => _isSecondaryMenuPreferred; + set => SetProperty(ref _isSecondaryMenuPreferred, value); + } } diff --git a/Wino.Core.Domain/Models/Navigation/ShellModeActivationContext.cs b/Wino.Core.Domain/Models/Navigation/ShellModeActivationContext.cs new file mode 100644 index 00000000..eb3ea9ca --- /dev/null +++ b/Wino.Core.Domain/Models/Navigation/ShellModeActivationContext.cs @@ -0,0 +1,10 @@ +#nullable enable + +namespace Wino.Core.Domain.Models.Navigation; + +public sealed class ShellModeActivationContext +{ + public bool IsInitialActivation { get; init; } + public bool SuppressStartupFlows { get; init; } + public object? Parameter { get; init; } +} diff --git a/Wino.Core.Domain/Models/Personalization/BackdropTypeWrapper.cs b/Wino.Core.Domain/Models/Personalization/BackdropTypeWrapper.cs new file mode 100644 index 00000000..1b6e5911 --- /dev/null +++ b/Wino.Core.Domain/Models/Personalization/BackdropTypeWrapper.cs @@ -0,0 +1,20 @@ +using Wino.Core.Domain.Enums; + +namespace Wino.Core.Domain.Models.Personalization; + +public class BackdropTypeWrapper +{ + public WindowBackdropType BackdropType { get; set; } + public string DisplayName { get; set; } + + public BackdropTypeWrapper(WindowBackdropType backdropType, string displayName) + { + BackdropType = backdropType; + DisplayName = displayName; + } + + public override string ToString() + { + return DisplayName; + } +} \ No newline at end of file diff --git a/Wino.Core.Domain/Models/Printing/WebView2PrintSettingsModel.cs b/Wino.Core.Domain/Models/Printing/WebView2PrintSettingsModel.cs new file mode 100644 index 00000000..48455878 --- /dev/null +++ b/Wino.Core.Domain/Models/Printing/WebView2PrintSettingsModel.cs @@ -0,0 +1,137 @@ +using System; +using CommunityToolkit.Mvvm.ComponentModel; +using Wino.Core.Domain.Enums; + +namespace Wino.Core.Domain.Models.Printing; + +/// +/// Wrapper model for CoreWebView2PrintSettings that provides bindable properties for UI controls. +/// +public partial class WebView2PrintSettingsModel : ObservableObject +{ + [ObservableProperty] + public partial string PrinterName { get; set; } = string.Empty; + + [ObservableProperty] + public partial PrintOrientation Orientation { get; set; } = PrintOrientation.Portrait; + + [ObservableProperty] + public partial PrintColorMode ColorMode { get; set; } = PrintColorMode.Color; + + [ObservableProperty] + public partial PrintCollation Collation { get; set; } = PrintCollation.Default; + + [ObservableProperty] + public partial PrintDuplex Duplex { get; set; } = PrintDuplex.Default; + + [ObservableProperty] + public partial PrintMediaSize MediaSize { get; set; } = PrintMediaSize.Default; + + [ObservableProperty] + public partial int Copies { get; set; } = 1; + + [ObservableProperty] + public partial double MarginTop { get; set; } = 1.0; + + [ObservableProperty] + public partial double MarginBottom { get; set; } = 1.0; + + [ObservableProperty] + public partial double MarginLeft { get; set; } = 1.0; + + [ObservableProperty] + public partial double MarginRight { get; set; } = 1.0; + + [ObservableProperty] + public partial bool ShouldPrintBackgrounds { get; set; } = false; + + [ObservableProperty] + public partial bool ShouldPrintSelectionOnly { get; set; } = false; + + [ObservableProperty] + public partial bool ShouldPrintHeaderAndFooter { get; set; } = true; + + [ObservableProperty] + public partial string HeaderTitle { get; set; } = string.Empty; + + [ObservableProperty] + public partial string FooterUri { get; set; } = string.Empty; + + [ObservableProperty] + public partial double ScaleFactor { get; set; } = 1.0; + + [ObservableProperty] + public partial int PagesPerSide { get; set; } = 1; + + [ObservableProperty] + public partial string PageRanges { get; set; } = string.Empty; + + /// + /// Partial method for validation when Copies property changes. + /// + partial void OnCopiesChanged(int value) + { + if (value <= 0) + { + Copies = 1; // Reset to minimum valid value + } + } + + /// + /// Partial method for validation when ScaleFactor property changes. + /// + partial void OnScaleFactorChanged(double value) + { + if (value < 0.1 || value > 2.0) + { + ScaleFactor = Math.Clamp(value, 0.1, 2.0); + } + } + + /// + /// Partial method for validation when PagesPerSide property changes. + /// + partial void OnPagesPerSideChanged(int value) + { + var validValues = new[] { 1, 2, 4, 6, 9, 16 }; + if (System.Array.IndexOf(validValues, value) < 0) + { + PagesPerSide = 1; // Reset to default valid value + } + } + + /// + /// Partial method for validation when margin properties change. + /// + partial void OnMarginTopChanged(double value) + { + if (value < 0) + { + MarginTop = 0; + } + } + + partial void OnMarginBottomChanged(double value) + { + if (value < 0) + { + MarginBottom = 0; + } + } + + partial void OnMarginLeftChanged(double value) + { + if (value < 0) + { + MarginLeft = 0; + } + } + + partial void OnMarginRightChanged(double value) + { + if (value < 0) + { + MarginRight = 0; + } + } +} diff --git a/Wino.Core.Domain/Models/Reader/MailRenderModel.cs b/Wino.Core.Domain/Models/Reader/MailRenderModel.cs index 5ca16b78..1bcae631 100644 --- a/Wino.Core.Domain/Models/Reader/MailRenderModel.cs +++ b/Wino.Core.Domain/Models/Reader/MailRenderModel.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using MimeKit; +using MimeKit.Cryptography; namespace Wino.Core.Domain.Models.Reader; @@ -15,6 +16,11 @@ public class MailRenderModel public UnsubscribeInfo UnsubscribeInfo { get; set; } + public Dictionary Signatures = []; + + // Indicates if the mail is S/MIME encrypted + public bool IsSmimeEncrypted { get; set; } + public MailRenderModel(string renderHtml, MailRenderingOptions mailRenderingOptions = null) { RenderHtml = renderHtml; diff --git a/Wino.Core.Domain/Models/Reader/SortingOption.cs b/Wino.Core.Domain/Models/Reader/SortingOption.cs index 09deec80..bb887d1e 100644 --- a/Wino.Core.Domain/Models/Reader/SortingOption.cs +++ b/Wino.Core.Domain/Models/Reader/SortingOption.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Models.Comparers; -using Wino.Core.Domain.Models.MailItem; +using Wino.Core.Domain.Enums; namespace Wino.Core.Domain.Models.Reader; @@ -9,16 +6,6 @@ public class SortingOption { public SortingOptionType Type { get; set; } public string Title { get; set; } - public IComparer Comparer - { - get - { - if (Type == SortingOptionType.ReceiveDate) - return new DateComparer(); - else - return new NameComparer(); - } - } public SortingOption(string title, SortingOptionType type) { diff --git a/Wino.Core.Domain/Models/Requests/RequestBase.cs b/Wino.Core.Domain/Models/Requests/RequestBase.cs index af7b6039..9ea7876c 100644 --- a/Wino.Core.Domain/Models/Requests/RequestBase.cs +++ b/Wino.Core.Domain/Models/Requests/RequestBase.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; @@ -29,6 +30,11 @@ public abstract record FolderRequestBase(MailItemFolder Folder, FolderSynchroniz public virtual object GroupingKey() { return Operation; } } +public abstract record CalendarRequestBase(CalendarItem Item) : RequestBase, ICalendarActionRequest +{ + public virtual Guid? LocalCalendarItemId => Item?.Id; +} + public class BatchCollection : List, IUIChangeRequest where TRequestType : IUIChangeRequest { public BatchCollection(IEnumerable collection) : base(collection) diff --git a/Wino.Core.Domain/Models/Retry/RetryPolicy.cs b/Wino.Core.Domain/Models/Retry/RetryPolicy.cs new file mode 100644 index 00000000..e756a825 --- /dev/null +++ b/Wino.Core.Domain/Models/Retry/RetryPolicy.cs @@ -0,0 +1,105 @@ +using System; + +namespace Wino.Core.Domain.Models.Retry; + +/// +/// Defines retry behavior for synchronization operations with exponential backoff. +/// +public class RetryPolicy +{ + private static readonly Random _jitterRandom = new(); + + /// + /// Gets or sets the maximum number of retry attempts. Default is 3. + /// + public int MaxRetries { get; set; } = 3; + + /// + /// Gets or sets the initial delay before the first retry. Default is 1 second. + /// + public TimeSpan InitialDelay { get; set; } = TimeSpan.FromSeconds(1); + + /// + /// Gets or sets the multiplier for exponential backoff. Default is 2.0. + /// Each retry delay = previous delay * multiplier. + /// + public double BackoffMultiplier { get; set; } = 2.0; + + /// + /// Gets or sets the maximum delay between retries. Default is 2 minutes. + /// + public TimeSpan MaxDelay { get; set; } = TimeSpan.FromMinutes(2); + + /// + /// Gets or sets whether to add random jitter to delays to prevent thundering herd. + /// Default is true. + /// + public bool UseJitter { get; set; } = true; + + /// + /// Gets or sets the maximum jitter as a percentage of the delay (0.0 to 1.0). + /// Default is 0.25 (25%). + /// + public double JitterFactor { get; set; } = 0.25; + + /// + /// Calculates the delay for the given retry attempt using exponential backoff. + /// + /// The retry attempt number (1-based). + /// The delay to wait before the retry. + public TimeSpan GetDelay(int retryAttempt) + { + if (retryAttempt <= 0) + return TimeSpan.Zero; + + // Calculate base delay with exponential backoff + var baseDelayMs = InitialDelay.TotalMilliseconds * Math.Pow(BackoffMultiplier, retryAttempt - 1); + + // Apply max delay cap + baseDelayMs = Math.Min(baseDelayMs, MaxDelay.TotalMilliseconds); + + // Apply jitter if enabled + if (UseJitter) + { + var jitterRange = baseDelayMs * JitterFactor; + var jitter = (_jitterRandom.NextDouble() * 2 - 1) * jitterRange; // +/- jitter range + baseDelayMs = Math.Max(0, baseDelayMs + jitter); + } + + return TimeSpan.FromMilliseconds(baseDelayMs); + } + + /// + /// Creates a default retry policy suitable for most synchronization operations. + /// + public static RetryPolicy Default => new(); + + /// + /// Creates an aggressive retry policy with more attempts and shorter delays. + /// Suitable for transient network issues. + /// + public static RetryPolicy Aggressive => new() + { + MaxRetries = 5, + InitialDelay = TimeSpan.FromMilliseconds(500), + BackoffMultiplier = 1.5, + MaxDelay = TimeSpan.FromSeconds(30) + }; + + /// + /// Creates a conservative retry policy with longer delays. + /// Suitable for rate limiting scenarios. + /// + public static RetryPolicy RateLimited => new() + { + MaxRetries = 3, + InitialDelay = TimeSpan.FromSeconds(10), + BackoffMultiplier = 2.0, + MaxDelay = TimeSpan.FromMinutes(5) + }; + + /// + /// Creates a no-retry policy that doesn't retry on failure. + /// + public static RetryPolicy NoRetry => new() { MaxRetries = 0 }; +} diff --git a/Wino.Core.Domain/Models/Server/WinoServerResponse.cs b/Wino.Core.Domain/Models/Server/WinoServerResponse.cs deleted file mode 100644 index d334c89a..00000000 --- a/Wino.Core.Domain/Models/Server/WinoServerResponse.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Wino.Core.Domain.Exceptions; - -namespace Wino.Core.Domain.Models.Server; - -/// -/// Encapsulates responses from the Wino server. -/// Exceptions are stored separately in the Message and StackTrace properties due to serialization issues. -/// -/// Type of the expected response. -public class WinoServerResponse -{ - public bool IsSuccess { get; set; } - public string Message { get; set; } - public T Data { get; set; } - - public static WinoServerResponse CreateSuccessResponse(T data) - { - return new WinoServerResponse - { - IsSuccess = true, - Data = data - }; - } - - public static WinoServerResponse CreateErrorResponse(string message) - { - return new WinoServerResponse - { - IsSuccess = false, - Message = message - }; - } - - public void ThrowIfFailed() - { - if (!IsSuccess) - throw new WinoServerException(Message); - } -} diff --git a/Wino.Core.Domain/Models/Settings/SettingOption.cs b/Wino.Core.Domain/Models/Settings/SettingOption.cs deleted file mode 100644 index 68871d99..00000000 --- a/Wino.Core.Domain/Models/Settings/SettingOption.cs +++ /dev/null @@ -1,5 +0,0 @@ -using Wino.Core.Domain.Enums; - -namespace Wino.Core.Domain.Models.Settings; - -public record SettingOption(string Title, string Description, WinoPage NavigationPage, string PathIcon); diff --git a/Wino.Core.Domain/Models/Settings/SettingsNavigationItemInfo.cs b/Wino.Core.Domain/Models/Settings/SettingsNavigationItemInfo.cs new file mode 100644 index 00000000..af8ea957 --- /dev/null +++ b/Wino.Core.Domain/Models/Settings/SettingsNavigationItemInfo.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Wino.Core.Domain.Enums; + +namespace Wino.Core.Domain.Models.Settings; + +public sealed class SettingsNavigationItemInfo( + WinoPage? pageType, + string title, + string description, + string glyph = "", + bool isSeparator = false, + string searchKeywords = "") +{ + public WinoPage? PageType { get; } = pageType; + public string Title { get; } = title; + public string Description { get; } = description; + public string Glyph { get; } = glyph; + public bool IsSeparator { get; } = isSeparator; + public string SearchKeywords { get; } = searchKeywords; +} + +public static class SettingsNavigationInfoProvider +{ + public static IReadOnlyList GetNavigationItems(string manageAccountsDescription = "") + { + return + [ + new(WinoPage.SettingOptionsPage, + Translator.SettingsHome_Title, + Translator.SettingsOptions_HeroDescription, + "\uE80F"), + new(WinoPage.ManageAccountsPage, + Translator.SettingsManageAccountSettings_Title, + manageAccountsDescription, + "\uE77B", + searchKeywords: Translator.SettingsSearch_ManageAccounts_Keywords), + new(WinoPage.WinoAccountManagementPage, + Translator.WinoAccount_SettingsSection_Title, + Translator.WinoAccount_SettingsSection_Description, + "\uE77B", + searchKeywords: string.Empty), + new(null, Translator.SettingsOptions_GeneralSection, string.Empty, "\uE713", isSeparator: true), + new(WinoPage.AppPreferencesPage, + Translator.SettingsAppPreferences_Title, + Translator.SettingsAppPreferences_Description, + "\uE770", + searchKeywords: Translator.SettingsSearch_AppPreferences_Keywords), + new(WinoPage.KeyboardShortcutsPage, + Translator.Settings_KeyboardShortcuts_Title, + Translator.Settings_KeyboardShortcuts_Description, + "\uE765", + searchKeywords: Translator.SettingsSearch_KeyboardShortcuts_Keywords), + new(WinoPage.PersonalizationPage, + Translator.SettingsPersonalization_Title, + Translator.SettingsPersonalization_Description, + "\uE771", + searchKeywords: Translator.SettingsSearch_Personalization_Keywords), + new(WinoPage.AboutPage, + Translator.SettingsAbout_Title, + Translator.SettingsAbout_Description, + "\uE946", + searchKeywords: Translator.SettingsSearch_About_Keywords), + new(null, Translator.SettingsOptions_MailSection, string.Empty, "\uE715", isSeparator: true), + new(WinoPage.MessageListPage, + Translator.SettingsMessageList_Title, + Translator.SettingsMessageList_Description, + "\uE8C4", + searchKeywords: Translator.SettingsSearch_MessageList_Keywords), + new(WinoPage.ReadComposePanePage, + Translator.SettingsReadComposePane_Title, + Translator.SettingsReadComposePane_Description, + "\uE8BD", + searchKeywords: Translator.SettingsSearch_ReadComposePane_Keywords), + new(WinoPage.SignatureAndEncryptionPage, + Translator.SettingsSignatureAndEncryption_Title, + Translator.SettingsSignatureAndEncryption_Description, + "\uE8D7", + searchKeywords: Translator.SettingsSearch_SignatureAndEncryption_Keywords), + new(WinoPage.StoragePage, + Translator.SettingsStorage_Title, + Translator.SettingsStorage_Description, + "\uE81C", + searchKeywords: Translator.SettingsSearch_Storage_Keywords), + new(null, Translator.SettingsOptions_CalendarSection, string.Empty, "\uE787", isSeparator: true), + new(WinoPage.CalendarRenderingSettingsPage, + Translator.CalendarSettings_Rendering_Title, + Translator.CalendarSettings_Rendering_Description, + "\uE787", + searchKeywords: Translator.SettingsSearch_CalendarSettings_Keywords) + , + new(WinoPage.CalendarNotificationSettingsPage, + Translator.CalendarSettings_Notifications_Title, + Translator.CalendarSettings_Notifications_Description, + "\uE7F4", + searchKeywords: Translator.SettingsSearch_CalendarSettings_Keywords), + new(WinoPage.CalendarPreferenceSettingsPage, + Translator.CalendarSettings_Preferences_Title, + Translator.CalendarSettings_Preferences_Description, + "\uE713", + searchKeywords: Translator.SettingsSearch_CalendarSettings_Keywords) + ]; + } + + public static IReadOnlyList Search(string query, string manageAccountsDescription = "") + { + if (string.IsNullOrWhiteSpace(query)) + return []; + + var normalizedQuery = NormalizeSearchText(query); + + if (string.IsNullOrWhiteSpace(normalizedQuery)) + return []; + + var queryTerms = normalizedQuery.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + return GetNavigationItems(manageAccountsDescription) + .Where(item => item.PageType.HasValue && !item.IsSeparator && item.PageType.Value != WinoPage.SettingOptionsPage) + .Select(item => new + { + Item = item, + Score = CalculateSearchScore(item, normalizedQuery, queryTerms) + }) + .Where(x => x.Score > 0) + .OrderByDescending(x => x.Score) + .ThenBy(x => x.Item.Title) + .Select(x => x.Item) + .ToList(); + } + + public static SettingsNavigationItemInfo GetInfo(WinoPage pageType, string manageAccountsDescription = "") + { + var rootPage = GetRootPage(pageType); + return GetNavigationItems(manageAccountsDescription).First(item => item.PageType == rootPage); + } + + public static string GetPageTitle(WinoPage pageType) + => pageType switch + { + WinoPage.SettingOptionsPage => Translator.MenuSettings, + WinoPage.ManageAccountsPage => Translator.SettingsManageAccountSettings_Title, + WinoPage.AccountManagementPage => Translator.SettingsManageAccountSettings_Title, + WinoPage.WinoAccountManagementPage => Translator.WinoAccount_SettingsSection_Title, + WinoPage.PersonalizationPage => Translator.SettingsPersonalization_Title, + WinoPage.AboutPage => Translator.SettingsAbout_Title, + WinoPage.MessageListPage => Translator.SettingsMessageList_Title, + WinoPage.ReadComposePanePage => Translator.SettingsReadComposePane_Title, + WinoPage.AppPreferencesPage => Translator.SettingsAppPreferences_Title, + WinoPage.CalendarSettingsPage => Translator.CalendarSettings_Preferences_Title, + WinoPage.CalendarRenderingSettingsPage => Translator.CalendarSettings_Rendering_Title, + WinoPage.CalendarNotificationSettingsPage => Translator.CalendarSettings_Notifications_Title, + WinoPage.CalendarPreferenceSettingsPage => Translator.CalendarSettings_Preferences_Title, + WinoPage.SignatureAndEncryptionPage => Translator.SettingsSignatureAndEncryption_Title, + WinoPage.KeyboardShortcutsPage => Translator.Settings_KeyboardShortcuts_Title, + WinoPage.StoragePage => Translator.SettingsStorage_Title, + WinoPage.EmailTemplatesPage => Translator.SettingsEmailTemplates_Title, + WinoPage.CreateEmailTemplatePage => Translator.SettingsEmailTemplates_Title, + _ => GetInfo(pageType).Title + }; + + public static WinoPage GetRootPage(WinoPage pageType) + => pageType switch + { + WinoPage.AccountManagementPage => WinoPage.ManageAccountsPage, + WinoPage.AccountDetailsPage => WinoPage.ManageAccountsPage, + WinoPage.MergedAccountDetailsPage => WinoPage.ManageAccountsPage, + WinoPage.AliasManagementPage => WinoPage.ManageAccountsPage, + WinoPage.SignatureManagementPage => WinoPage.ManageAccountsPage, + WinoPage.ImapCalDavSettingsPage => WinoPage.ManageAccountsPage, + WinoPage.EmailTemplatesPage => WinoPage.ManageAccountsPage, + WinoPage.CreateEmailTemplatePage => WinoPage.ManageAccountsPage, + WinoPage.CalendarSettingsPage => WinoPage.CalendarPreferenceSettingsPage, + WinoPage.CalendarAccountSettingsPage => WinoPage.CalendarPreferenceSettingsPage, + _ => pageType + }; + + private static int CalculateSearchScore(SettingsNavigationItemInfo item, string normalizedQuery, IReadOnlyList queryTerms) + { + var title = NormalizeSearchText(item.Title); + var description = NormalizeSearchText(item.Description); + var keywords = NormalizeSearchText(item.SearchKeywords); + var combinedText = string.Join(' ', new[] { title, description, keywords }.Where(text => !string.IsNullOrWhiteSpace(text))); + + if (!combinedText.Contains(normalizedQuery, StringComparison.Ordinal) && + !queryTerms.All(term => combinedText.Contains(term, StringComparison.Ordinal))) + { + return 0; + } + + var score = 0; + + if (title.StartsWith(normalizedQuery, StringComparison.Ordinal)) + score += 500; + else if (title.Contains(normalizedQuery, StringComparison.Ordinal)) + score += 360; + + if (keywords.Contains(normalizedQuery, StringComparison.Ordinal)) + score += 280; + + if (description.Contains(normalizedQuery, StringComparison.Ordinal)) + score += 180; + + foreach (var term in queryTerms) + { + if (title.Contains(term, StringComparison.Ordinal)) + score += 70; + + if (keywords.Contains(term, StringComparison.Ordinal)) + score += 50; + + if (description.Contains(term, StringComparison.Ordinal)) + score += 30; + } + + return score; + } + + private static string NormalizeSearchText(string value) + { + if (string.IsNullOrWhiteSpace(value)) + return string.Empty; + + var sanitized = value + .ToLowerInvariant() + .Select(character => char.IsLetterOrDigit(character) ? character : ' ') + .ToArray(); + + return string.Join(' ', new string(sanitized).Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)); + } +} diff --git a/Wino.Core.Domain/Models/Store/StoreProductType.cs b/Wino.Core.Domain/Models/Store/StoreProductType.cs deleted file mode 100644 index f8c620ed..00000000 --- a/Wino.Core.Domain/Models/Store/StoreProductType.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Wino.Core.Domain.Models.Store; - -public enum StoreProductType -{ - UnlimitedAccounts -} diff --git a/Wino.Core.Domain/Models/Synchronization/FolderSyncResult.cs b/Wino.Core.Domain/Models/Synchronization/FolderSyncResult.cs new file mode 100644 index 00000000..2814fc2a --- /dev/null +++ b/Wino.Core.Domain/Models/Synchronization/FolderSyncResult.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using Wino.Core.Domain.Enums; + +namespace Wino.Core.Domain.Models.Synchronization; + +/// +/// Result of synchronizing a single folder. +/// Used for partial failure tracking when one folder fails but others succeed. +/// +public class FolderSyncResult +{ + /// + /// Gets or sets the folder ID. + /// + public Guid FolderId { get; set; } + + /// + /// Gets or sets the folder name for display purposes. + /// + public string FolderName { get; set; } + + /// + /// Gets or sets whether the folder sync was successful. + /// + public bool Success { get; set; } + + /// + /// Gets or sets the number of messages downloaded/synchronized. + /// + public int DownloadedCount { get; set; } + + /// + /// Gets or sets the number of messages deleted locally (removed from server). + /// + public int DeletedCount { get; set; } + + /// + /// Gets or sets the number of messages whose flags were updated. + /// + public int UpdatedCount { get; set; } + + /// + /// Gets or sets the error message if sync failed. + /// + public string ErrorMessage { get; set; } + + /// + /// Gets or sets the error severity if sync failed. + /// + public SynchronizerErrorSeverity? ErrorSeverity { get; set; } + + /// + /// Gets or sets the error category if sync failed. + /// + public SynchronizerErrorCategory? ErrorCategory { get; set; } + + /// + /// Gets or sets whether this folder was skipped (e.g., due to configuration). + /// + public bool WasSkipped { get; set; } + + /// + /// Gets or sets the reason the folder was skipped. + /// + public string SkipReason { get; set; } + + /// + /// Creates a successful folder sync result. + /// + public static FolderSyncResult Successful(Guid folderId, string folderName, int downloaded = 0, int deleted = 0, int updated = 0) + => new() + { + FolderId = folderId, + FolderName = folderName, + Success = true, + DownloadedCount = downloaded, + DeletedCount = deleted, + UpdatedCount = updated + }; + + /// + /// Creates a failed folder sync result. + /// + public static FolderSyncResult Failed(Guid folderId, string folderName, string errorMessage, + SynchronizerErrorSeverity severity = SynchronizerErrorSeverity.Fatal, + SynchronizerErrorCategory category = SynchronizerErrorCategory.Unknown) + => new() + { + FolderId = folderId, + FolderName = folderName, + Success = false, + ErrorMessage = errorMessage, + ErrorSeverity = severity, + ErrorCategory = category + }; + + /// + /// Creates a failed folder sync result from an error context. + /// + public static FolderSyncResult Failed(Guid folderId, string folderName, SynchronizerErrorContext errorContext) + => new() + { + FolderId = folderId, + FolderName = folderName, + Success = false, + ErrorMessage = errorContext?.ErrorMessage ?? "Unknown error", + ErrorSeverity = errorContext?.Severity ?? SynchronizerErrorSeverity.Fatal, + ErrorCategory = errorContext?.Category ?? SynchronizerErrorCategory.Unknown + }; + + /// + /// Creates a skipped folder sync result. + /// + public static FolderSyncResult Skipped(Guid folderId, string folderName, string reason) + => new() + { + FolderId = folderId, + FolderName = folderName, + Success = true, // Skipping is not a failure + WasSkipped = true, + SkipReason = reason + }; +} diff --git a/Wino.Core.Domain/Models/Synchronization/MailSynchronizationResult.cs b/Wino.Core.Domain/Models/Synchronization/MailSynchronizationResult.cs index f7280889..c38fa6e3 100644 --- a/Wino.Core.Domain/Models/Synchronization/MailSynchronizationResult.cs +++ b/Wino.Core.Domain/Models/Synchronization/MailSynchronizationResult.cs @@ -1,8 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; using System.Text.Json.Serialization; +using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Models.Accounts; -using Wino.Core.Domain.Models.MailItem; namespace Wino.Core.Domain.Models.Synchronization; @@ -16,16 +18,67 @@ public class MailSynchronizationResult /// It's ignored in serialization. Client should not react to this. /// [JsonIgnore] - public IEnumerable DownloadedMessages { get; set; } = []; + public IEnumerable DownloadedMessages { get; set; } = []; public ProfileInformation ProfileInformation { get; set; } public SynchronizationCompletedState CompletedState { get; set; } + public Exception Exception { get; set; } + + /// + /// Gets or sets the results for each folder that was synchronized. + /// Enables partial failure tracking - some folders may succeed while others fail. + /// + public List FolderResults { get; set; } = []; + + /// + /// Gets whether the synchronization had any partial failures. + /// True if at least one folder failed but others succeeded. + /// + [JsonIgnore] + public bool HasPartialFailures => FolderResults.Any(f => !f.Success) && FolderResults.Any(f => f.Success); + + /// + /// Gets the number of folders that were successfully synchronized. + /// + [JsonIgnore] + public int SuccessfulFolderCount => FolderResults.Count(f => f.Success); + + /// + /// Gets the number of folders that failed to synchronize. + /// + [JsonIgnore] + public int FailedFolderCount => FolderResults.Count(f => !f.Success); + + /// + /// Gets the total number of messages downloaded across all folders. + /// + [JsonIgnore] + public int TotalDownloadedCount => FolderResults.Sum(f => f.DownloadedCount); + + /// + /// Gets the total number of messages deleted across all folders. + /// + [JsonIgnore] + public int TotalDeletedCount => FolderResults.Sum(f => f.DeletedCount); + + /// + /// Gets the total number of messages updated across all folders. + /// + [JsonIgnore] + public int TotalUpdatedCount => FolderResults.Sum(f => f.UpdatedCount); + + /// + /// Gets the folders that failed to sync for error reporting. + /// + [JsonIgnore] + public IEnumerable FailedFolders => FolderResults.Where(f => !f.Success); + public static MailSynchronizationResult Empty => new() { CompletedState = SynchronizationCompletedState.Success }; // Mail synchronization - public static MailSynchronizationResult Completed(IEnumerable downloadedMessages) + public static MailSynchronizationResult Completed(IEnumerable downloadedMessages) => new() { DownloadedMessages = downloadedMessages, @@ -40,6 +93,32 @@ public class MailSynchronizationResult CompletedState = SynchronizationCompletedState.Success }; + /// + /// Creates a completed result with folder-level results. + /// + public static MailSynchronizationResult CompletedWithFolderResults( + IEnumerable downloadedMessages, + List folderResults) + { + var hasAnyFailure = folderResults.Any(f => !f.Success); + var hasAnySuccess = folderResults.Any(f => f.Success); + + return new() + { + DownloadedMessages = downloadedMessages, + FolderResults = folderResults, + CompletedState = hasAnyFailure && !hasAnySuccess + ? SynchronizationCompletedState.Failed + : hasAnyFailure + ? SynchronizationCompletedState.PartiallyCompleted + : SynchronizationCompletedState.Success + }; + } + public static MailSynchronizationResult Canceled => new() { CompletedState = SynchronizationCompletedState.Canceled }; - public static MailSynchronizationResult Failed => new() { CompletedState = SynchronizationCompletedState.Failed }; + public static MailSynchronizationResult Failed(Exception exception) => new() + { + CompletedState = SynchronizationCompletedState.Failed, + Exception = exception + }; } diff --git a/Wino.Core.Domain/Models/Synchronization/SynchronizationActionItem.cs b/Wino.Core.Domain/Models/Synchronization/SynchronizationActionItem.cs new file mode 100644 index 00000000..655e8918 --- /dev/null +++ b/Wino.Core.Domain/Models/Synchronization/SynchronizationActionItem.cs @@ -0,0 +1,15 @@ +using System; + +namespace Wino.Core.Domain.Models.Synchronization; + +/// +/// Represents a single grouped synchronization action displayed in the sync status flyout. +/// For example: "Deleting 3 mail(s)" or "Marking folder as read". +/// +public class SynchronizationActionItem +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public Guid AccountId { get; set; } + public string AccountName { get; set; } + public string Description { get; set; } +} diff --git a/Wino.Core.Domain/Models/Synchronization/SynchronizerErrorContext.cs b/Wino.Core.Domain/Models/Synchronization/SynchronizerErrorContext.cs new file mode 100644 index 00000000..9ba4c84c --- /dev/null +++ b/Wino.Core.Domain/Models/Synchronization/SynchronizerErrorContext.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; + +namespace Wino.Core.Domain.Models.Synchronization; + +/// +/// Contains context information about a synchronizer error +/// +public class SynchronizerErrorContext +{ + /// + /// Account associated with the error + /// + public MailAccount Account { get; set; } + + /// + /// Gets or sets the error code + /// + public int? ErrorCode { get; set; } + + /// + /// Gets or sets the error message + /// + public string ErrorMessage { get; set; } + + /// + /// Gets or sets the request bundle associated with the error + /// + public IRequestBundle RequestBundle { get; set; } + + /// + /// Gets or sets additional data associated with the error + /// + public Dictionary AdditionalData { get; set; } = new Dictionary(); + + /// + /// Gets or sets the exception associated with the error + /// + public Exception Exception { get; set; } + + /// + /// Gets or sets the severity of the error for retry decision making. + /// + public SynchronizerErrorSeverity Severity { get; set; } = SynchronizerErrorSeverity.Fatal; + + /// + /// Gets or sets the category of the error for targeted handling. + /// + public SynchronizerErrorCategory Category { get; set; } = SynchronizerErrorCategory.Unknown; + + /// + /// Gets or sets the current retry attempt count. + /// + public int RetryCount { get; set; } + + /// + /// Gets or sets the maximum number of retries allowed. + /// + public int MaxRetries { get; set; } = 3; + + /// + /// Gets or sets the suggested delay before retrying. + /// + public TimeSpan? RetryDelay { get; set; } + + /// + /// Gets or sets the folder ID associated with the error for partial failure tracking. + /// + public Guid? FolderId { get; set; } + + /// + /// Gets or sets the folder name for display purposes. + /// + public string FolderName { get; set; } + + /// + /// Gets or sets the type of operation that failed. + /// Examples: "FolderSync", "MailSync", "RequestExecution", "Idle" + /// + public string OperationType { get; set; } + + /// + /// Gets whether this error should be retried based on severity and retry count. + /// + public bool ShouldRetry => Severity == SynchronizerErrorSeverity.Transient && RetryCount < MaxRetries; + + /// + /// Gets whether synchronization can continue despite this error. + /// + public bool CanContinueSync => Severity == SynchronizerErrorSeverity.Recoverable || + (Severity == SynchronizerErrorSeverity.Transient && RetryCount >= MaxRetries); +} diff --git a/Wino.Core.Domain/Models/Updates/UpdateNoteSection.cs b/Wino.Core.Domain/Models/Updates/UpdateNoteSection.cs new file mode 100644 index 00000000..0bb8f7c8 --- /dev/null +++ b/Wino.Core.Domain/Models/Updates/UpdateNoteSection.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace Wino.Core.Domain.Models.Updates; + +public class UpdateNoteSection +{ + [JsonPropertyName("title")] + public string Title { get; set; } = string.Empty; + + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + [JsonPropertyName("imageUrl")] + public string ImageUrl { get; set; } = string.Empty; + + [JsonPropertyName("imageWidth")] + public double? ImageWidth { get; set; } + + [JsonPropertyName("imageHeight")] + public double? ImageHeight { get; set; } + + /// Gets the image width for binding, returning NaN for auto-sizing when not specified. + public double ActualImageWidth => ImageWidth ?? double.NaN; + + /// Gets the image height for binding, returning NaN for auto-sizing when not specified. + public double ActualImageHeight => ImageHeight ?? double.NaN; +} diff --git a/Wino.Core.Domain/Models/Updates/UpdateNotes.cs b/Wino.Core.Domain/Models/Updates/UpdateNotes.cs new file mode 100644 index 00000000..fad7d80f --- /dev/null +++ b/Wino.Core.Domain/Models/Updates/UpdateNotes.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +namespace Wino.Core.Domain.Models.Updates; + +public class UpdateNotes +{ + [JsonPropertyName("sections")] + public List Sections { get; set; } = []; +} diff --git a/Wino.Core.Domain/Translations/bg_BG/resources.json b/Wino.Core.Domain/Translations/bg_BG/resources.json index 4ef469a9..e00be13b 100644 --- a/Wino.Core.Domain/Translations/bg_BG/resources.json +++ b/Wino.Core.Domain/Translations/bg_BG/resources.json @@ -8,6 +8,7 @@ "AccountCacheReset_Message": "Този акаунт изисква пълна ресинхронизация, за да продължи да работи. Моля, изчакайте, докато Wino ресинхронизира съобщенията ви...", "AccountContactNameYou": "Вие", "AccountCreationDialog_Completed": "всичко е готово", + "AccountCreationDialog_FetchingCalendarMetadata": "Зареждане на календарни данни.", "AccountCreationDialog_FetchingEvents": "Извличане на събитията от календара.", "AccountCreationDialog_FetchingProfileInformation": "Извличане на данните за профила.", "AccountCreationDialog_GoogleAuthHelpClipboardText_Row0": "Ако браузърът ви не се е стартирал автоматично, за да завърши удостоверяването:", @@ -17,6 +18,7 @@ "AccountCreationDialog_Initializing": "инициализиране", "AccountCreationDialog_PreparingFolders": "В момента получаваме информация за папките.", "AccountCreationDialog_SigninIn": "Информацията за акаунта се запазва.", + "Purchased": "Покупено", "AccountEditDialog_Message": "Име на акаунта", "AccountEditDialog_Title": "Редактиране на акаунта", "AccountPickerDialog_Title": "Изберете акаунт", @@ -26,6 +28,10 @@ "AccountDetailsPage_Description": "Променете името на акаунта в Wino и задайте желаното име на изпращача.", "AccountDetailsPage_ColorPicker_Title": "Цвят на акаунта", "AccountDetailsPage_ColorPicker_Description": "Задайте цвят на акаунта, за да оцветите символа му в списъка.", + "AccountDetailsPage_TabGeneral": "Общи", + "AccountDetailsPage_TabMail": "Поща", + "AccountDetailsPage_TabCalendar": "Календар", + "AccountDetailsPage_CalendarListDescription": "Изберете календар, за да конфигурирате неговите настройки.", "AddHyperlink": "Добавяне", "AppCloseBackgroundSynchronizationWarningTitle": "Синхронизация на заден план", "AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Приложението не е настроено да се стартира при стартиране на Windows.", @@ -47,8 +53,10 @@ "BasicIMAPSetupDialog_Title": "IMAP акаунт", "Busy": "Зает", "Buttons_AddAccount": "Добавяне на акаунт", + "Buttons_FixAccount": "Поправи акаунта", "Buttons_AddNewAlias": "Добавяне на нов псевдоним", "Buttons_Allow": "Позволяване", + "Buttons_Apply": "Приложи", "Buttons_ApplyTheme": "Прилагане на темата", "Buttons_Browse": "Преглед", "Buttons_Cancel": "Отказ", @@ -62,6 +70,7 @@ "Buttons_Edit": "Редактиране", "Buttons_EnableImageRendering": "Активиране", "Buttons_Multiselect": "Избор на няколко", + "Buttons_Manage": "Управлявай", "Buttons_No": "Не", "Buttons_Open": "Отваряне", "Buttons_Purchase": "Купете", @@ -70,15 +79,134 @@ "Buttons_Save": "Запазване", "Buttons_SaveConfiguration": "Запазване на конфигурацията", "Buttons_Send": "Изпращане", + "Buttons_SendToServer": "Изпрати към сървър", "Buttons_Share": "Споделяне", "Buttons_SignIn": "Вход", "Buttons_Sync": "Синхронизиране", "Buttons_SyncAliases": "Синхронизиране на псевдонимите", "Buttons_TryAgain": "Нов опит", "Buttons_Yes": "Да", + "Sync_SynchronizingFolder": "Синхронизиране на {0} {1}", + "Sync_DownloadedMessages": "Изтеглени {0} съобщения от {1}", + "SyncAction_Archiving": "Архивиране на {0} имейл(а)", + "SyncAction_ClearingFlag": "Премахване на флага от {0} имейл(а)", + "SyncAction_CreatingDraft": "Създаване на чернова", + "SyncAction_CreatingEvent": "Създаване на събитие", + "SyncAction_Deleting": "Изтриване на {0} имейл(а)", + "SyncAction_EmptyingFolder": "Изпразване на папка", + "SyncAction_MarkingAsRead": "Маркиране на {0} имейл(а) като прочетени", + "SyncAction_MarkingAsUnread": "Маркиране на {0} имейл(а) като непрочетени", + "SyncAction_MarkingFolderAsRead": "Маркиране на папка като прочетена", + "SyncAction_Moving": "Преместване на {0} имейл(а)", + "SyncAction_MovingToFocused": "Преместване на {0} имейл(а) към Focused", + "SyncAction_RenamingFolder": "Преименуване на папка", + "SyncAction_SendingMail": "Изпращане на имейл", + "SyncAction_SettingFlag": "Поставяне на флаг на {0} имейл(а)", + "SyncAction_SynchronizingAccount": "Синхронизирам {0}", + "SyncAction_SynchronizingAccounts": "Синхронизирам {0} акаунт(и)", + "SyncAction_SynchronizingCalendarData": "Синхронизиране на календарни данни", + "SyncAction_SynchronizingCalendarEvents": "Синхронизиране на календарни събития", + "SyncAction_SynchronizingCalendarMetadata": "Синхронизиране на календарни метаданни", + "SyncAction_Unarchiving": "Разархивиране на {0} имейл(а)", "CalendarAllDayEventSummary": "целодневни събития", "CalendarDisplayOptions_Color": "Цвят", "CalendarDisplayOptions_Expand": "Разширяване", + "CalendarEventResponse_Accept": "Приеми", + "CalendarEventResponse_AcceptedResponse": "Прието", + "CalendarEventResponse_Decline": "Откажи", + "CalendarEventResponse_DeclinedResponse": "Отказано", + "CalendarEventResponse_NotResponded": "Без отговор", + "CalendarEventResponse_Tentative": "Възможно", + "CalendarEventResponse_TentativeResponse": "Възможно", + "CalendarEventRsvpPanel_Accept": "Приеми", + "CalendarEventRsvpPanel_AddMessage": "Добавете съобщение към отговора... (по избор)", + "CalendarEventRsvpPanel_Decline": "Откажи", + "CalendarEventRsvpPanel_Message": "Съобщение", + "CalendarEventRsvpPanel_SendReplyMessage": "Изпрати съобщение с отговор", + "CalendarEventRsvpPanel_Tentative": "Възможно", + "CalendarEventRsvpPanel_Title": "Опции за отговор", + "CalendarAttendeeStatus_Accepted": "Прието", + "CalendarAttendeeStatus_Declined": "Отхвърлено", + "CalendarAttendeeStatus_NeedsAction": "Изисква действие", + "CalendarAttendeeStatus_Tentative": "Възможно", + "CalendarEventDetails_Attachments": "Прикачени файлове", + "CalendarEventCompose_AddAttachment": "Добави прикачен файл", + "CalendarEventCompose_AllDay": "Цял ден", + "CalendarEventCompose_AttachmentsNotSupportedForCalDav": "Прикачените файлове не се поддържат за календарите CalDAV.", + "CalendarEventCompose_EndDate": "Крайна дата", + "CalendarEventCompose_EndTime": "Краен час", + "CalendarEventCompose_Every": "на всеки", + "CalendarEventCompose_ForWeekdays": "за", + "CalendarEventCompose_FrequencyDay": "ден", + "CalendarEventCompose_FrequencyDayPlural": "дни", + "CalendarEventCompose_FrequencyMonth": "месец", + "CalendarEventCompose_FrequencyMonthPlural": "месеци", + "CalendarEventCompose_FrequencyWeek": "седмица", + "CalendarEventCompose_FrequencyWeekPlural": "седмици", + "CalendarEventCompose_FrequencyYear": "година", + "CalendarEventCompose_FrequencyYearPlural": "години", + "CalendarEventCompose_Location": "Местоположение", + "CalendarEventCompose_LocationPlaceholder": "Добавете местоположение", + "CalendarEventCompose_NewEventButton": "Ново събитие", + "CalendarEventCompose_DefaultCalendarHint": "Можете да изберете календар по подразбиране за нови събития в настройките на календара.", + "CalendarEventCompose_DefaultCalendarSettingsLink": "Отворете настройките на календара", + "CalendarEventCompose_NoCalendarsMessage": "Всe още няма налични календари за създаване на събитиe", + "CalendarEventCompose_NoCalendarsTitle": "Няма налични календари", + "CalendarEventCompose_NoEndDate": "Няма крайна дата", + "CalendarEventCompose_Notes": "Бележки", + "CalendarEventCompose_PickCalendarTitle": "Изберете календар", + "CalendarEventCompose_Recurring": "Повтарящо се", + "CalendarEventCompose_RecurringSummary": "Се повтаря на всеки {0} {1}{2} {3} валидно {4}{5}", + "CalendarEventCompose_RecurringSummarySmart": "Се повтаря {0}{1} {2} валидно {3}{4}", + "CalendarEventCompose_RepeatEvery": "Повтаряйте на всеки", + "CalendarEventCompose_SelectCalendar": "Изберете календар", + "CalendarEventCompose_SingleOccurrenceSummary": "Се случва на {0} {1}", + "CalendarEventCompose_StartDate": "Начална дата", + "CalendarEventCompose_StartTime": "Начален час", + "CalendarEventCompose_TimeRangeSummary": "от {0} до {1}", + "CalendarEventCompose_Title": "Заглавие на събитието", + "CalendarEventCompose_TitlePlaceholder": "Добавете заглавие", + "CalendarEventCompose_Until": "до", + "CalendarEventCompose_UntilSummary": " до {0}", + "CalendarEventCompose_ValidationInvalidAllDayRange": "Датата на край за цял ден трябва да е след началната дата.", + "CalendarEventCompose_ValidationInvalidAttendee": "Един или повече участници имат невалиден имейл адрес.", + "CalendarEventCompose_ValidationInvalidRecurrenceEnd": "Датата на край на повторението трябва да е на или след датата на началото на събитието.", + "CalendarEventCompose_ValidationInvalidTimeRange": "Краят трябва да е по-късен от началото.", + "CalendarEventCompose_ValidationMissingAttachment": "Един или повече прикачени файлове вече не са налични: {0}", + "CalendarEventCompose_ValidationMissingCalendar": "Изберете календар преди да създадете събитието.", + "CalendarEventCompose_ValidationMissingTitle": "Въведете заглавие на събитието преди да го създадете.", + "CalendarEventCompose_ValidationTitle": "Проверката на събитието не успя", + "CalendarEventCompose_WeekdaySummary": " на {0}", + "CalendarEventCompose_Weekday_Friday": "F", + "CalendarEventCompose_Weekday_Monday": "M", + "CalendarEventCompose_Weekday_Saturday": "Сб", + "CalendarEventCompose_Weekday_Sunday": "Нд", + "CalendarEventCompose_Weekday_Thursday": "Чет", + "CalendarEventCompose_Weekday_Tuesday": "Вт", + "CalendarEventCompose_Weekday_Wednesday": "Ср", + "CalendarEventDetails_Details": "Детайли", + "CalendarEventDetails_EditSeries": "Редактиране на серия", + "CalendarEventDetails_Editing": "Редактиране", + "CalendarEventDetails_InviteSomeone": "Поканете някого", + "CalendarEventDetails_JoinOnline": "Присъединете се онлайн", + "CalendarEventDetails_Organizer": "Организатор", + "CalendarEventDetails_People": "Хора", + "CalendarEventDetails_ReadOnlyEvent": "Събитие само за четене", + "CalendarEventDetails_Reminder": "Напомняне", + "CalendarReminder_StartedHoursAgo": "Започна преди {0} часа", + "CalendarReminder_StartedMinutesAgo": "Започна преди {0} минути", + "CalendarReminder_StartedNow": "Започна точно сега", + "CalendarReminder_StartingNow": "Започва сега", + "CalendarReminder_StartsInHours": "Ще започне след {0} часа", + "CalendarReminder_StartsInMinutes": "Ще започне след {0} минути", + "CalendarReminder_SnoozeAction": "Отложи", + "CalendarReminder_SnoozeMinutesOption": "{0} минути", + "CalendarEventDetails_ShowAs": "Покажи като", + "CalendarShowAs_Free": "Свободен", + "CalendarShowAs_Tentative": "Временно", + "CalendarShowAs_Busy": "Зает", + "CalendarShowAs_OutOfOffice": "Извън офиса", + "CalendarShowAs_WorkingElsewhere": "Работя другаде", "CalendarItem_DetailsPopup_JoinOnline": "Присъединяване онлайн", "CalendarItem_DetailsPopup_ViewEventButton": "Преглед на събитието", "CalendarItem_DetailsPopup_ViewSeriesButton": "Преглед на сериите", @@ -88,6 +216,9 @@ "ClipboardTextCopied_Message": "{0} е копирано в клипборда.", "ClipboardTextCopied_Title": "Копирано", "ClipboardTextCopyFailed_Message": "Неуспешно копиране на {0} в клипборда.", + "ContactInfoBar_ErrorTitle": "Грешка при зареждане на информация за контакти", + "ContactInfoBar_SuccessTitle": "Информация за контактите заредена", + "ContactInfoBar_WarningTitle": "Възможна е непълна информация за контакт", "ComingSoon": "Очаквайте скоро...", "ComposerAttachmentsDragDropAttach_Message": "Прикачване", "ComposerAttachmentsDropZone_Message": "Пуснете файловете си тук", @@ -129,6 +260,10 @@ "DialogMessage_CreateLinkedAccountTitle": "Име на връзката с акаунта", "DialogMessage_DeleteAccountConfirmationMessage": "Изтриване на {0}?", "DialogMessage_DeleteAccountConfirmationTitle": "Всички данни, свързани с този акаунт, ще бъдат изтрити от диска за постоянно.", + "DialogMessage_DeleteEmailTemplateConfirmationMessage": "Изтриване на шаблона \\\"{0}\\\"?", + "DialogMessage_DeleteEmailTemplateConfirmationTitle": "Изтриване на шаблон за имейл", + "DialogMessage_DeleteRecurringSeriesMessage": "Това ще изтрие всички събития от серията. Искате ли да продължите?", + "DialogMessage_DeleteRecurringSeriesTitle": "Изтриване на повтаряща се серия", "DialogMessage_DiscardDraftConfirmationMessage": "Тази чернова ще бъде отхвърлена. Искате ли да продължите?", "DialogMessage_DiscardDraftConfirmationTitle": "Отхвърляне на черновата", "DialogMessage_EmptySubjectConfirmation": "Липсваща тема", @@ -172,11 +307,18 @@ "ElementTheme_Light": "Светъл режим", "Emoji": "Емотикони", "Error_FailedToSetupSystemFolders_Title": "Неуспешна настройка на системните папки", + "Exception_AccountNeedsAttention_Title": "Акаунтът се нуждае от внимание", + "Exception_AccountNeedsAttention_Message": "'{0}' изисква вашето внимание, за да продължите.", + "Exception_WebView2RuntimeMissing_Message": "Wino Mail не може да намери Microsoft Edge WebView2 Runtime. Моля, инсталирайте или поправете runtime-а, за да се показва правилно съдържанието на съобщението.", + "Exception_WebView2RuntimeMissing_Title": "Изисква се WebView2 Runtime", "Exception_AuthenticationCanceled": "Удостоверяването е отменено", "Exception_CustomThemeExists": "Тази тема вече съществува.", "Exception_CustomThemeMissingName": "Трябва да посочите име.", "Exception_CustomThemeMissingWallpaper": "Трябва да предоставите персонализирано фоново изображение.", "Exception_FailedToSynchronizeAliases": "Неуспешно синхронизиране на псевдонимите", + "Exception_FailedToSynchronizeCalendarData": "Неуспешна синхронизация на календарните данни", + "Exception_FailedToSynchronizeCalendarEvents": "Неуспешна синхронизация на календарните събития", + "Exception_FailedToSynchronizeCalendarMetadata": "Неуспешна синхронизация на календарните детайли", "Exception_FailedToSynchronizeFolders": "Неуспешно синхронизиране на папките", "Exception_FailedToSynchronizeProfileInformation": "Неуспешно синхронизиране на информацията за профила", "Exception_GoogleAuthCallbackNull": "Callback uri е празенl при активиране.", @@ -229,6 +371,32 @@ "HoverActionOption_MoveJunk": "Преместване в Нежелани", "HoverActionOption_ToggleFlag": "Отбелязване / Без отбелязване", "HoverActionOption_ToggleRead": "Прочетено / Непрочетено", + "KeyboardShortcuts_FailedToReset": "Неуспешно нулиране на клавишните комбинации.", + "KeyboardShortcuts_FailedToUpdate": "Неуспешно актуализиране на клавишните комбинации", + "KeyboardShortcuts_MailoperationAction": "Действие", + "KeyboardShortcuts_Action": "Действие", + "KeyboardShortcuts_FailedToLoad": "Неуспешно зареждане на клавишните комбинации.", + "KeyboardShortcuts_EnterKeyForShortcut": "Моля, въведете клавиш за клавишната комбинация.", + "KeyboardShortcuts_SelectOperationForShortcut": "Моля, изберете действие за клавишната комбинация.", + "KeyboardShortcuts_EnterKey": "Моля, въведете клавиш за клавишната комбинация.", + "KeyboardShortcuts_SelectOperation": "Моля, изберете действие за клавишната комбинация.", + "KeyboardShortcuts_ShortcutInUse": "Тази клавишна комбинация вече се използва от друга клавишна комбинация.", + "KeyboardShortcuts_FailedToSave": "Неуспешно запазване на клавишната комбинация.", + "KeyboardShortcuts_FailedToDelete": "Неуспешно изтриване на клавишната комбинация.", + "KeyboardShortcuts_PageDescription": "Настройте клавишни комбинации за бързи операции с поща. Натиснете клавиши, докато полето за въвеждане на клавиша е фокусирано, за да заснемете клавишни комбинации.", + "KeyboardShortcuts_Add": "Добави клавишна комбинация", + "KeyboardShortcuts_EditTitle": "Редактиране на клавишна комбинация", + "KeyboardShortcuts_ResetToDefaults": "Възстанови по подразбиране", + "KeyboardShortcuts_PressKeysHere": "Натиснете тук клавишите...", + "KeyboardShortcuts_KeyCombination": "Клавишна комбинация", + "KeyboardShortcuts_FocusArea": "Фокусирайте полето по-горе и натиснете желаната клавишна комбинация", + "KeyboardShortcuts_Modifiers": "Клавиши за модификатори", + "KeyboardShortcuts_Mode": "Режим на приложението", + "KeyboardShortcuts_ModeMail": "Поща", + "KeyboardShortcuts_ModeCalendar": "Календар", + "KeyboardShortcuts_ActionToggleReadUnread": "Превключване на прочетено/непрочетено", + "KeyboardShortcuts_ActionToggleFlag": "Превключване на флаг", + "KeyboardShortcuts_ActionToggleArchive": "Превключване на архивиране/отархивиране", "ImageRenderingDisabled": "Показването на изображения е деактивирано за това съобщение.", "ImapAdvancedSetupDialog_AuthenticationMethod": "Метод за удостоверяване", "ImapAdvancedSetupDialog_ConnectionSecurity": "Сигурност на връзката", @@ -295,12 +463,58 @@ "IMAPSetupDialog_Username": "Потребител", "IMAPSetupDialog_UsernamePlaceholder": "ivanivanov, ivanivanov@primer.com, domain/ivanivanov", "IMAPSetupDialog_UseSameConfig": "Използване на същото потребителско име и парола за изпращане на имейли", + "ImapCalDavSettingsPage_TitleCreate": "Настройка IMAP и календар", + "ImapCalDavSettingsPage_TitleEdit": "Редактиране на настройките за IMAP и календар", + "ImapCalDavSettingsPage_Subtitle": "Конфигурирайте IMAP/SMTP и опционална синхронизация на календара за този акаунт.", + "ImapCalDavSettingsPage_BasicSectionTitle": "Основна настройка", + "ImapCalDavSettingsPage_BasicSectionDescription": "Въведете вашата идентичност и данни за вход. Wino може да се опита да открие настройките на сървъра автоматично.", + "ImapCalDavSettingsPage_BasicTab": "Основни", + "ImapCalDavSettingsPage_EnableCalendarSupport": "Разреши поддръжка на календар", + "ImapCalDavSettingsPage_AutoDiscoverButton": "Автоматично откриване на настройки за поща", + "ImapCalDavSettingsPage_AutoDiscoverySuccessMessage": "Настройки за пощата открити и приложени.", + "ImapCalDavSettingsPage_AdvancedSectionTitle": "Разширени настройки", + "ImapCalDavSettingsPage_AdvancedSectionDescription": "Въведете настройки на сървъра ръчно, ако автоматичното откриване е недостъпно или неправилно.", + "ImapCalDavSettingsPage_AdvancedTab": "Разширени", + "ImapCalDavSettingsPage_CalendarSectionTitle": "Настройки на календара", + "ImapCalDavSettingsPage_CalendarSectionDescription": "Изберете как календарните данни да работят за този IMAP акаунт.", + "ImapCalDavSettingsPage_CalendarModeHeader": "Режим на календара", + "ImapCalDavSettingsPage_ConnectionSecurityHeader": "Сигурност на връзката", + "ImapCalDavSettingsPage_AuthenticationMethodHeader": "Метод на удостоверяване", + "ImapCalDavSettingsPage_CalendarModeDisabled": "Изключен", + "ImapCalDavSettingsPage_CalendarModeCalDav": "CalDAV синхронизация", + "ImapCalDavSettingsPage_CalendarModeLocalOnly": "Само локален календар", + "ImapCalDavSettingsPage_CalendarModeDisabledDescription": "Календарът е деактивиран за този акаунт.", + "ImapCalDavSettingsPage_CalendarModeCalDavDescription": "Елементите от календара се синхронизират с вашия CalDAV сървър.", + "ImapCalDavSettingsPage_CalendarModeLocalOnlyDescription": "Елементите от календара се съхраняват само на този компютър и не се синхронират в мрежата.", + "ImapCalDavSettingsPage_LocalCalendarLearnMore": "Как работи локалният календар", + "ImapCalDavSettingsPage_LocalCalendarDialogTitle": "Само локален календар", + "ImapCalDavSettingsPage_LocalCalendarDialogMessage": "Локалният календар запазва всички събития само на вашия компютър. Нищо не се синхронизира с iCloud, Yahoo или други доставчици.", + "ImapCalDavSettingsPage_CalDavServiceUrl": "CalDAV URL на услугата", + "ImapCalDavSettingsPage_CalDavUsername": "Потребителско име за CalDAV", + "ImapCalDavSettingsPage_CalDavPassword": "Парола за CalDAV", + "ImapCalDavSettingsPage_CalDavNotRequiredMessage": "Проверка за CalDAV е необходима само когато режимът на календар е CalDAV синхронизация.", + "ImapCalDavSettingsPage_CalDavUrlRequired": "Изисква се CalDAV URL на услугата.", + "ImapCalDavSettingsPage_CalDavUrlInvalid": "CalDAV URL на услугата трябва да бъде абсолютен URL.", + "ImapCalDavSettingsPage_CalDavUsernameRequired": "CalDAV потребителското име е задължително.", + "ImapCalDavSettingsPage_CalDavPasswordRequired": "CalDAV паролата е задължителна.", + "ImapCalDavSettingsPage_TestImapButton": "Тест IMAP връзка", + "ImapCalDavSettingsPage_TestCalDavButton": "Тест CalDAV връзка", + "ImapCalDavSettingsPage_ImapTestSuccessMessage": "IMAP връзката е успешно тествана.", + "ImapCalDavSettingsPage_CalDavTestSuccessMessage": "CalDAV връзката е успешно тествана.", + "ImapCalDavSettingsPage_SaveSuccessMessage": "Настройките на акаунта са валидирани и записани.", + "ImapCalDavSettingsPage_ICloudHint": "Използвайте парола за приложение, генерирана от настройките на вашия Apple акаунт.", + "ImapCalDavSettingsPage_YahooHint": "Използвайте парола за приложение от настройките за сигурност на вашия Yahoo акаунт.", "Info_AccountCreatedMessage": "{0} е създаден", "Info_AccountCreatedTitle": "Създаване на акаунт", "Info_AccountCreationFailedTitle": "Неуспешно създаване на акаунт", "Info_AccountDeletedMessage": "{0} е изтрит успешно.", "Info_AccountDeletedTitle": "Акаунтът е изтрит", "Info_AccountIssueFixFailedTitle": "Неуспешно", + "Info_AccountIssueFixImapMessage": "Отворете страницата за IMAP и календарни настройки, за да въведете отново данните за сървъра.", + "Info_AccountAttentionRequiredMessage": "Този акаунт се нуждае от внимание.", + "Info_AccountAttentionRequiredClickableMessage": "Кликнете, за да поправите този акаунт и да го ресинхронизирайте.", + "Info_AccountAttentionRequiredAction": "Поправи", + "Info_AccountAttentionRequiredActionHint": "Кликнете върху Поправи, за да разрешите проблема с този акаунт.", "Info_AccountIssueFixSuccessMessage": "Отстранени са всички проблеми с акаунтите.", "Info_AccountIssueFixSuccessTitle": "Успешно", "Info_AttachmentOpenFailedMessage": "Не може да се отвори този прикачен файл.", @@ -370,6 +584,7 @@ "InfoBarMessage_SynchronizationDisabledFolder": "Синхронизирането на тази папка е деактивирано.", "InfoBarTitle_SynchronizationDisabledFolder": "Изключена папка", "Justify": "Двустранно", + "MenuUpdateAvailable": "Налична актуализация", "Left": "Ляво", "Link": "Връзка", "LinkedAccountsCreatePolicyMessage": "Трябва да имате поне 2 акаунта, за да създадете връзка.\nВръзката ще бъде премахната при запазване.", @@ -403,6 +618,7 @@ "MailOperation_Unarchive": "Разархивиране", "MailOperation_ViewMessageSource": "Преглед на изходния код", "MailOperation_Zoom": "Мащаб", + "MailsDragging": "Преместване на {0} елемент(а)", "MailsSelected": "{0} избран(и) елемент(и)", "MarkFlagUnflag": "Отбелязано/Без отбелязване", "MarkReadUnread": "Маркиране като прочетено/непрочетено", @@ -434,6 +650,8 @@ "Notifications_MultipleNotificationsTitle": "Нов имейл", "Notifications_WinoUpdatedMessage": "Вижте новата версия {0}", "Notifications_WinoUpdatedTitle": "Wino Mail е актуализиран.", + "Notifications_StoreUpdateAvailableTitle": "Налична актуализация", + "Notifications_StoreUpdateAvailableMessage": "По-нова версия на Wino Mail е готова за инсталиране от Microsoft Store.", "OnlineSearchFailed_Message": "Неуспешно търсене\n{0}\n\nПоказване на офлайн имейли.", "OnlineSearchTry_Line1": "Не можете да намерите това, което търсите?", "OnlineSearchTry_Line2": "Опитайте онлайн търсене.", @@ -446,7 +664,6 @@ "PaneLengthOption_Small": "Малък", "Photos": "Снимки", "PreparingFoldersMessage": "Подготовка на папките", - "ProtocolLogAvailable_Message": "Протоколните дневници са достъпни за диагностика.", "ProviderDetail_Gmail_Description": "Акаунт в Google", "ProviderDetail_iCloud_Description": "Акаунт в Apple iCloud", "ProviderDetail_iCloud_Title": "iCloud", @@ -465,9 +682,14 @@ "SearchBarPlaceholder": "Търсене", "SearchingIn": "Търсене в", "SearchPivotName": "Резултати", + "Settings_KeyboardShortcuts_Title": "Клавишни комбинации", + "Settings_KeyboardShortcuts_Description": "Управлявайте клавишните комбинации за бързи действия в пощата.", "SettingConfigureSpecialFolders_Button": "Конфигуриране", "SettingsEditAccountDetails_IMAPConfiguration_Title": "Конфигурация на IMAP/SMTP", "SettingsEditAccountDetails_IMAPConfiguration_Description": "Променете настройките на входящия/изходящия сървър.", + "SettingsEditAccountDetails_ImapCalDavSettings_Title": "IMAP и календарни настройки", + "SettingsEditAccountDetails_ImapCalDavSettings_Description": "Отворете специализираната страница с настройки за IMAP, SMTP и CalDAV за този акаунт.", + "SettingsEditAccountDetails_ImapCalDavSettings_Action": "Отворете настройки", "SettingsAbout_Description": "Научете повече за Wino.", "SettingsAbout_Title": "За приложението", "SettingsAboutGithub_Description": "Към хранилището на GitHub за проследяване на проблеми.", @@ -490,6 +712,10 @@ "SettingsAppPreferences_SearchMode_Local": "Локално", "SettingsAppPreferences_SearchMode_Online": "Онлайн", "SettingsAppPreferences_SearchMode_Title": "Режим на търсене по подразбиране", + "SettingsAppPreferences_ApplicationMode_Title": "Режим по подразбиране на приложението.", + "SettingsAppPreferences_ApplicationMode_Description": "Изберете режим по подразбиране за стартиране на Wino, когато не е явно зададен активиращ тип.", + "SettingsAppPreferences_ApplicationMode_Mail": "Поща", + "SettingsAppPreferences_ApplicationMode_Calendar": "Календар", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description": "Wino Mail ще продължи да работи на заден план. Ще бъдете уведомявани за пристигането на нови имейли.", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title": "Работа на заден план", "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description": "Wino Mail ще продължи да работи в системната област. Можете да го стартирате, като щракнете върху иконата. Ще бъдете уведомявани при пристигането на нови имейли.", @@ -506,12 +732,30 @@ "SettingsAppPreferences_StartupBehavior_FatalError": "Възникна фатална грешка при промяна на режима на стартиране на Wino Mail.", "SettingsAppPreferences_StartupBehavior_Title": "Стартиране в минимизиран вид при стартиране на Windows", "SettingsAppPreferences_Title": "Предпочитания за приложението", + "SettingsAppPreferences_HideWinoAccountButton_Title": "Скрийте бутона за акаунта Wino в заглавната лента", + "SettingsAppPreferences_HideWinoAccountButton_Description": "Скрийте бутона за профила в заглавната лента, който отваря изскачащото меню за акаунта Wino.", + "SettingsAppPreferences_StoreUpdateNotifications_Title": "Уведомления за обновления в магазина", + "SettingsAppPreferences_StoreUpdateNotifications_Description": "Показвайте уведомления и действия в долния колонтитър, когато има налично обновление на Microsoft Store.", + "SettingsAppPreferences_AiActions_Title": "AI действия", + "SettingsAppPreferences_AiActions_Description": "Изберете езиците за AI по подразбиране и къде трябва да се запазват резюметата.", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Title": "Език по подразбиране за превод", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Description": "Изберете език по подразбиране за превод от AI.", + "SettingsAppPreferences_AiSummarizeLanguage_Title": "Език за обобщение", + "SettingsAppPreferences_AiSummarizeLanguage_Description": "Изберете език за обобщение за бъдещите AI резюмета.", + "SettingsAppPreferences_AiSummarySavePath_Title": "Път за запис на резюме по подразбиране", + "SettingsAppPreferences_AiSummarySavePath_Description": "Изберете папка, която Wino да използва по подразбиране при запазване на AI резюмета.", + "SettingsAppPreferences_AiSummarySavePath_Placeholder": "Използвайте системното местоположение за запис по подразбиране", + "SettingsAppPreferences_AiSummarySavePath_InvalidHint": "Тази папка не съществува. За резюметата ще се използва местоположението за запис по подразбиране.", "SettingsAutoSelectNextItem_Description": "Избиране на следващия елемент, след като изтриете или преместите имейл.", "SettingsAutoSelectNextItem_Title": "Автоматично избиране на следващия елемент", "SettingsAvailableThemes_Description": "Изберете тема от собствената колекция на Wino по ваш вкус или приложете свои собствени теми.", "SettingsAvailableThemes_Title": "Налични теми", "SettingsCalendarSettings_Description": "Променете първия ден от седмицата, височината на часовата клетка и други...", "SettingsCalendarSettings_Title": "Настройки на календара", + "CalendarSettings_DefaultSnoozeDuration_Header": "Продължителност на отлагане по подразбиране", + "CalendarSettings_DefaultSnoozeDuration_Description": "Задайте по подразбиране време за отлагане на календарните напомняния.", + "CalendarSettings_TimedDayHeaderFormat_Header": "Формат на заглавието на деня във времевия изглед", + "CalendarSettings_TimedDayHeaderFormat_Description": "Изберете как да се извеждат горните етикети на деня в изгледите за ден, седмица и работна седмица. Използвайте токени за форматиране на дати като ddd, dd, MMM или dddd.", "SettingsComposer_Title": "Съставяне", "SettingsComposerFont_Title": "Шрифт по подразбиране за съставяне", "SettingsComposerFontFamily_Description": "Промяна на шрифтовото семейство и размера на шрифта по подразбиране за съставяне на имейли.", @@ -531,6 +775,9 @@ "SettingsDiscord_Title": "Канал в Discord", "SettingsEditLinkedInbox_Description": "Добавяне/премахване на акаунти, преименуване или прекъсване на връзката между акаунтите.", "SettingsEditLinkedInbox_Title": "Редактиране на свързаната входяща поща", + "SettingsWindowBackdrop_Title": "Фон на прозореца", + "SettingsWindowBackdrop_Description": "Изберете ефект на фона за прозорците на Wino.", + "SettingsWindowBackdrop_Disabled": "Изборът на фона на прозореца е деактивиран, когато темата на приложението е различна от По подразбиране.", "SettingsElementTheme_Description": "Избор на тема на Windows за Wino", "SettingsElementTheme_Title": "Цветови режим", "SettingsElementThemeSelectionDisabled": "Изборът на цветови режим е забранен, когато е избрана тема на приложението, различна от тази по подразбиране.", @@ -581,6 +828,8 @@ "SettingsManageAliases_Title": "Псевдоними", "SettingsEditAccountDetails_Title": "Редактиране на данните на акаунта", "SettingsEditAccountDetails_Description": "Променете името на акаунта, името на подателя и задайте нов цвят, ако желаете.", + "EditAccountDetailsPage_SaveSuccess_Title": "Промените са запазени.", + "EditAccountDetailsPage_SaveSuccess_Message": "Данните за акаунта ви са успешно актуализирани.", "SettingsManageLink_Description": "Преместете елементите, за да добавите нова връзка или да премахнете съществуваща връзка.", "SettingsManageLink_Title": "Управление на връзката", "SettingsMarkAsRead_Description": "Променете какво трябва да се случи с избрания елемент.", @@ -596,7 +845,41 @@ "SettingsNotifications_Title": "Известия", "SettingsNotificationsAndTaskbar_Description": "Променете дали да се показват известия и значка в лентата на задачите за този акаунт.", "SettingsNotificationsAndTaskbar_Title": "Известия и лента на задачите", + "SettingsHome_Title": "Начало", + "SettingsHome_SearchTitle": "Намери настройка", + "SettingsHome_SearchDescription": "Търсете по функция, тема или ключова дума, за да стигнете директно до правилната страница за настройки.", + "SettingsHome_SearchPlaceholder": "Търсене в настройките", + "SettingsHome_SearchExamples": "Пример: тема, съхранение, език, подпис", + "SettingsHome_QuickLinks_Title": "Бързи връзки", + "SettingsHome_QuickLinks_Description": "Бърз достъп до настройките, които хората използват най-често.", + "SettingsHome_StorageCard_Description": "Проверете колко локално MIME съдържание съхранява Wino на това устройство и го почистете при нужда.", + "SettingsHome_StorageEmptySummary": "Все още не е открито кеширано MIME съдържание.", + "SettingsHome_StorageLoading": "Проверка на използването на локално MIME съдържание...", + "SettingsHome_Tips_Title": "Съвети и трикове", + "SettingsHome_Tips_Description": "Няколко малки промени могат да направят Wino по-личен.", + "SettingsHome_Tip_Theme": "Искате тъмна тема или промени в акцента? Отворете Персонализация.", + "SettingsHome_Tip_Background": "Използвайте настройките на приложението, за да контролирате стартиране и фоновата синхронизация.", + "SettingsHome_Tip_Shortcuts": "Клавишните комбинации ви помагат да преглеждате пощата по-бързо.", + "SettingsHome_Resources_Title": "Полезни връзки", + "SettingsHome_Resources_Description": "Отворете ресурсите на проекта, информация за поддръжка и канали за издания.", "SettingsOptions_Title": "Настройки", + "SettingsOptions_GeneralSection": "Общи", + "SettingsOptions_MailSection": "Поща", + "SettingsOptions_CalendarSection": "Календар", + "SettingsOptions_MoreComingSoon": "Още опции скоро.", + "SettingsOptions_HeroDescription": "Персонализирайте опита си с Wino Mail.", + "SettingsOptions_AccountsSummary": "{0} акаунт(а) конфигуриран(и)", + "SettingsSearch_ManageAccounts_Keywords": "акаунт;акаунти;пощенска кутия;пощенски кутии;псевдоним;псевдоними;профил;адрес;адреси", + "SettingsSearch_AppPreferences_Keywords": "стартиране;фонов режим;стартиране;синхронизация;известие;известия;търсене;системна лента;по подразбиране", + "SettingsSearch_LanguageTime_Keywords": "език;време;часовник;локал;регион;формат;24-часов;24ч", + "SettingsSearch_Personalization_Keywords": "тема;тъмна;светла;външен вид;акцент;цвят;цвят;режим;оформление;плътност", + "SettingsSearch_About_Keywords": "за;версия;уебсайт;поверителност;GitHub;дарение;магазин;поддръжка", + "SettingsSearch_KeyboardShortcuts_Keywords": "клавишна комбинация;клавишни комбинации;горещ клавиш;горещи клавиши;клавиатура;клавиши", + "SettingsSearch_MessageList_Keywords": "съобщение;съобщения;списък;нишки;нишки;аватар;преглед;изпращач", + "SettingsSearch_ReadComposePane_Keywords": "четец;съставяне;съставител;шрифт;шрифтове;външно съдържание;дисплей;четене", + "SettingsSearch_SignatureAndEncryption_Keywords": "подпис;подписи;криптиране;сертификат;сертификати;S/MIME;S/MIME;сигурност", + "SettingsSearch_Storage_Keywords": "съхранение;кеш; кеширане; MIME; диск; пространство; почистване; почистване; локални данни", + "SettingsSearch_CalendarSettings_Keywords": "календар;седмица;часове;график;събитие;събития", "SettingsPaneLengthReset_Description": "Възстановете първоначалния размер на списъка с имейли, ако имате проблеми с него.", "SettingsPaneLengthReset_Title": "Възстановяване размера на списъка с имейли", "SettingsPaypal_Description": "Покажете много повече любов ❤️ Всички дарения са добре дошли.", @@ -610,6 +893,8 @@ "SettingsPrefer24HourClock_Title": "Показване на часовете в 24-часов формат", "SettingsPrivacyPolicy_Description": "Прегледайте политиката за поверителност.", "SettingsPrivacyPolicy_Title": "Политика за поверителност", + "SettingsWebsite_Description": "Отворете уебсайта на Wino Mail.", + "SettingsWebsite_Title": "Уебсайт", "SettingsReadComposePane_Description": "Шрифтове, външно съдържание.", "SettingsReadComposePane_Title": "Четене и съставяне", "SettingsReader_Title": "Четене", @@ -625,6 +910,19 @@ "SettingsShowPreviewText_Title": "Показване визуализация на текста", "SettingsShowSenderPictures_Description": "Скриване/показване на миниатюрите на снимките на изпращача.", "SettingsShowSenderPictures_Title": "Показване на аватарите на подателите", + "SettingsEmailTemplates_Title": "Шаблони за имейли", + "SettingsEmailTemplates_Description": "Управление на шаблоните за електронна поща.", + "SettingsEmailTemplates_CreatePageTitle": "Нов шаблон", + "SettingsEmailTemplates_EditPageTitle": "Редактиране на шаблон", + "SettingsEmailTemplates_NewTemplateTitle": "Нов шаблон", + "SettingsEmailTemplates_NewTemplateDescription": "Създайте нов имейл шаблон.", + "SettingsEmailTemplates_NameTitle": "Име", + "SettingsEmailTemplates_NamePlaceholder": "Име на шаблон", + "SettingsEmailTemplates_DescriptionTitle": "Описание", + "SettingsEmailTemplates_DescriptionPlaceholder": "Опционално описание", + "SettingsEmailTemplates_ContentTitle": "Съдържание на шаблона", + "SettingsEmailTemplates_ContentDescription": "Редактирайте HTML съдържанието за този шаблон.", + "SettingsEmailTemplates_NameRequired": "Името на шаблона е задължително.", "SettingsEnableGravatarAvatars_Title": "Gravatar", "SettingsEnableGravatarAvatars_Description": "Use gravatar (if available) as sender picture", "SettingsEnableFavicons_Title": "Domain icons (Favicons)", @@ -645,6 +943,33 @@ "SettingsStartupItem_Title": "Елемент за стартиране", "SettingsStore_Description": "Покажете малко любов ❤️", "SettingsStore_Title": "Оценете ни в магазина", + "SettingsStorage_Title": "Съхранение", + "SettingsStorage_Description": "Сканиране и управление на кеша MIME, съхраняван във вашата локална папка с данни.", + "SettingsStorage_ScanFolder": "Сканирайте локалната папка за данни", + "SettingsStorage_NoLocalMimeDataFound": "Не са намерени локални MIME данни.", + "SettingsStorage_NoAccountsFound": "Не са намерени акаунти.", + "SettingsStorage_TotalUsage": "Общо използване на локалния MIME кеш: {0}", + "SettingsStorage_AccountUsageDescription": "{0} използвано в локалния MIME кеш", + "SettingsStorage_DeleteAll_Title": "Изтриване на всички MIME данни.", + "SettingsStorage_DeleteAll_Description": "Изтрийте цялата MIME кеш папка за този акаунт.", + "SettingsStorage_DeleteAll_Button": "Изтрийте всичко", + "SettingsStorage_DeleteAll_Confirm_Title": "Изтриване на всички MIME данни.", + "SettingsStorage_DeleteAll_Confirm_Message": "Изтриване на всички локални MIME данни за {0}?", + "SettingsStorage_DeleteAll_Success": "Цялото MIME съдържание беше изтрито.", + "SettingsStorage_DeleteOld_Title": "Изтриване на старо MIME съдържание", + "SettingsStorage_DeleteOld_Description": "Изтриване на MIME файлове според датата на създаване на писмото в локалната база данни.", + "SettingsStorage_DeleteOld_1Month": "> 1 месец", + "SettingsStorage_DeleteOld_3Months": "> 3 месеца", + "SettingsStorage_DeleteOld_6Months": "> 6 месеца", + "SettingsStorage_DeleteOld_1Year": "> 1 година", + "SettingsStorage_DeleteOld_Confirm_Title": "Изтриване на старо MIME съдържание", + "SettingsStorage_DeleteOld_Confirm_Message": "Да изтриете локалните MIME данни, по-стари от {0}, за {1}?", + "SettingsStorage_DeleteOld_Success": "Изтрити са {0} MIME папки, по-стари от {1}.", + "SettingsStorage_1Month": "1 месец", + "SettingsStorage_3Months": "3 месеца", + "SettingsStorage_6Months": "6 месеца", + "SettingsStorage_1Year": "1 година", + "SettingsStorage_Months": "{0} месеца", "SettingsTaskbarBadge_Description": "Показване броя на непрочетените писма в иконата в лентата на задачите.", "SettingsTaskbarBadge_Title": "Значка на лентата на задачите", "SettingsThreads_Description": "Организиране на съобщенията в разговори.", @@ -683,6 +1008,9 @@ "SystemFolderConfigDialogValidation_InboxSelected": "Не можете да присвоите папка Входящи към друга системна папка.", "SystemFolderConfigSetupSuccess_Message": "Системните папки са успешно конфигурирани.", "SystemFolderConfigSetupSuccess_Title": "Настройка на системните папки", + "SystemTrayMenu_ShowWino": "Отвори Wino Mail", + "SystemTrayMenu_ShowWinoCalendar": "Отвори Wino календар", + "SystemTrayMenu_ExitWino": "Изход", "TestingImapConnectionMessage": "Тестване на връзката със сървъра...", "TitleBarServerDisconnectedButton_Description": "Wino е изключен от мрежата. Щракнете върху Повторно свързване, за да възстановите връзката.", "TitleBarServerDisconnectedButton_Title": "няма връзка", @@ -699,8 +1027,422 @@ "WinoUpgradeMessage": "Надграждане до неограничен брой акаунти", "WinoUpgradeRemainingAccountsMessage": "Използвани са {0} от {1} безплатни акаунта.", "Yesterday": "Вчера", + "Smime_ImportCertificates_Success": "Сертификатите бяха успешно импортирани.", + "Smime_ImportCertificates_Error": "Грешка при импортирането на сертификатите: {0}", + "Smime_RemoveCertificates_Confirm": "Наистина ли искате да премахнете сертификатите {0}?", + "Smime_RemoveCertificates_Success": "Сертификатите са премахнати.", + "Smime_ExportCertificates_Success": "Сертификатите са експортирани.", + "Smime_ExportCertificates_Error": "Грешка при експортирането на сертификатите.", + "Smime_CertificateDetails": "Предмет: {0}\nИздател: {1}\nВалидно от: {2}\nВалидно до: {3}\nОтпечатък: {4}", + "Smime_CertificatePassword_Title": "Изисква се парола за сертификат", + "Smime_CertificatePassword_Placeholder": "Парола за сертификат за {0} (по избор)", + "Smime_Confirm_Title": "Потвърди", + "Buttons_OK": "ОК", + "Buttons_Refresh": "Обнови", + "SettingsSignatureAndEncryption_Title": "Подписване и криптиране", + "SettingsSignatureAndEncryption_Description": "Управление на S/MIME сертификати за подписване и криптиране на имейли.", + "SettingsSignatureAndEncryption_MyCertificatesHeader": "Моите сертификати", + "SettingsSignatureAndEncryption_MyCertificatesDescription": "Лични сертификати за подписване и криптиране", + "SettingsSignatureAndEncryption_RecipientCertificatesHeader": "Сертификати на получателя", + "SettingsSignatureAndEncryption_RecipientCertificatesDescription": "Сертификати на получателя за дешифриране", + "SettingsSignatureAndEncryption_NameColumn": "Име", + "SettingsSignatureAndEncryption_ExpiresColumn": "Изтича на", + "SettingsSignatureAndEncryption_ThumbprintColumn": "Отпечатък", + "Buttons_Remove": "Премахни", + "Buttons_Export": "Експортирай", + "Buttons_Import": "Импортирай", + "SettingsSignatureAndEncryption_SigningCertificate": "S/MIME Подписващ сертификат", + "SettingsSignatureAndEncryption_EncryptionCertificate": "S/MIME Сертификат за криптиране", + "SettingsSignatureAndEncryption_SigningCertificatePlaceholder": "Няма", + "SmimeSignaturesInMessage": "Подписи в това съобщение:", + "SmimeSignatureEntry": "• {0} {1} ({2}, валиден до {3} - {4})", + "SmimeSigningCertificateInfoTitle": "Информация за подписващ S/MIME сертификат", + "SmimeCertificateInfoTitle": "Информация за S/MIME сертификат", + "SmimeNoCertificateFileFound": "Не е намерен файл с сертификат", + "SmimeSaveCertificate": "Запазване на сертификат...", + "SmimeCertificate": "S/MIME сертификат", + "SmimeCertificateSavedTo": "Сертификатът е запазен в {0}", + "SmimeSignedTooltip": "Това съобщение е подписано с S/MIME сертификат. Щракнете за повече подробности", + "SmimeEncryptedTooltip": "Това съобщение е криптирано с S/MIME сертификат.", + "SmimeCertificateFileInfo": "Файл: {0}", + "Composer_LightTheme": "Светла тема", + "Composer_DarkTheme": "Тъмна тема", + "Composer_Outdent": "Намали отстъп", + "Composer_Indent": "Увеличи отстъп", + "Composer_BulletList": "Списък с водещи точки", + "Composer_OrderedList": "Номериран списък", + "Composer_Stroke": "Щрих", + "Composer_Bold": "Удебелен", + "Composer_Italic": "Курсив", + "Composer_Underline": "Подчертано", + "Composer_CcBcc": "Cc и Bcc", + "Composer_EnableSmimeSignature": "Включване/изключване на S/MIME подпис", + "Composer_EnableSmimeEncryption": "Включване/изключване на S/MIME криптиране", + "Composer_LocalDraftSyncInfo": "Тази чернова е локална. Wino не успя да я изпрати до вашия имейл сървър. Щракнете, за да опитате отново изпращането към сървъра.", + "Composer_CertificateExpires": "Изтича на: ", + "Composer_SmimeSignature": "S/MIME Подпис", + "Composer_SmimeEncryption": "S/MIME Криптиране", + "Composer_EmailTemplatesPlaceholder": "Имейл шаблони", + "Composer_AiSummarize": "Обобщи с AI", + "Composer_AiSummarizeDescription": "Извлича ключови точки, действия и решения от този имейл.", + "Composer_AiTranslate": "Преведи с AI", + "Composer_AiActions": "AI действия", + "Composer_AiRewrite": "Препиши с AI", + "AiActions_CheckingStatus": "Проверка на достъпа до AI...", + "AiActions_SignedOutTitle": "Разблокирайте Wino AI Pack", + "AiActions_SignedOutDescription": "Превеждайте, преписвайте и обобщавайте имейли с AI след влизане във вашия Wino акаунт и активиране на добавката AI Pack.", + "AiActions_NoPackTitle": "AI пакет", + "AiActions_NoPackDescription": "Вие сте вписани, но AI Pack не е активен. Купете го, за да използвате инструментите за AI превод, преписване и обобщаване на Wino.", + "AiActions_UsageSummary": "{0} от {1} кредити използвани този месец.", + "Composer_AiRewritePolite": "Направи го учтиво", + "Composer_AiRewritePoliteDescription": "Улеснява формулировката, запазвайки същия смисъл.", + "Composer_AiRewriteAngry": "Направи го ядосано", + "Composer_AiRewriteAngryDescription": "Използва по-остър и по-конфронтиращ тон.", + "Composer_AiRewriteHappy": "Направи го щастлив", + "Composer_AiRewriteHappyDescription": "Добавя по-оптимистичен и по-ентусиазиран тон.", + "Composer_AiRewriteFormal": "Направи го формално", + "Composer_AiRewriteFormalDescription": "Прави съобщението по-професионално и структурирано.", + "Composer_AiRewriteFriendly": "Направи го по-приятелски", + "Composer_AiRewriteFriendlyDescription": "Увеличава приветливия тон на съобщението.", + "Composer_AiRewriteShorter": "Направи го по-кратко", + "Composer_AiRewriteShorterDescription": "Уплътнява текста и премахва излишни детайли.", + "Composer_AiRewriteClearer": "Направи го по-ясен", + "Composer_AiRewriteClearerDescription": "Подобрява четимостта и прави съобщението по-лесно за следване.", + "Composer_AiRewriteCustom": "Персонализирано", + "Composer_AiRewriteCustomDescription": "Опишете вашето собствено намерение за пренаписване.", + "Composer_AiRewriteCustomPlaceholder": "Опишете как искате да бъде пренаписано съобщението", + "Composer_AiRewriteMode": "Тон на пренаписване", + "Composer_AiRewriteApply": "Приложи пренапис", + "Composer_AiTranslateDialogTitle": "Преведи с ИИ", + "Composer_AiTranslateDialogDescription": "Въведете целевия език или код за култура, например en-US, tr-TR, de-DE или fr-FR.", + "Composer_AiTranslateApply": "Преведи", + "Composer_AiTranslateLanguage": "Целеви език", + "Composer_AiTranslateCustomPlaceholder": "Въведете код за култура", + "Composer_AiTranslateLanguageEnglish": "Английски (en-US)", + "Composer_AiTranslateLanguageTurkish": "Турски (tr-TR)", + "Composer_AiTranslateLanguageGerman": "Немски (de-DE)", + "Composer_AiTranslateLanguageFrench": "Френски (fr-FR)", + "Composer_AiTranslateLanguageSpanish": "Испански (es-ES)", + "Composer_AiTranslateLanguageItalian": "Италиански (it-IT)", + "Composer_AiTranslateLanguagePortugueseBrazil": "Португалски (Бразилия) (pt-BR)", + "Composer_AiTranslateLanguageDutch": "Холандски (nl-NL)", + "Composer_AiTranslateLanguagePolish": "Полски (pl-PL)", + "Composer_AiTranslateLanguageRussian": "Руски (ru-RU)", + "Composer_AiTranslateLanguageJapanese": "Японски (ja-JP)", + "Composer_AiTranslateLanguageKorean": "Корейски (ko-KR)", + "Composer_AiTranslateLanguageChineseSimplified": "Китайски, опростен (zh-CN)", + "Composer_AiTranslateLanguageArabic": "Арабски (ar-SA)", + "Composer_AiTranslateLanguageHindi": "Хинди (hi-IN)", + "Composer_AiTranslateLanguageOther": "Друго...", + "Composer_AiBusyTitle": "ИИ вече работи", + "Composer_AiBusyMessage": "Моля, изчакайте завършването на текущото действие на ИИ.", + "Composer_AiSignInRequired": "Влезте в акаунта си в Wino, за да използвате AI функциите.", + "Composer_AiMissingHtml": "Все още няма съдържание на съобщение за изпращане към Wino AI.", + "Composer_AiQuotaUnavailable": "Резултатът на ИИ беше приложен.", + "Composer_AiAppliedMessage": "Резултатът на ИИ беше приложен към редактора за писане. Използвайте Отмяна, ако искате да върнете промените.", + "Composer_AiSummarizeSuccessTitle": "AI резюме приложено", + "Composer_AiTranslateSuccessTitle": "Преводът на ИИ е приложен", + "Composer_AiRewriteSuccessTitle": "Пренаписът на ИИ е приложен", + "Composer_AiErrorTitle": "Действието на ИИ не успя", + "Reader_AiAppliedMessage": "Резултатът от ИИ вече е показан за това съобщение. Отворете отново съобщението, за да видите оригиналното съдържание.", "SettingsAppPreferences_EmailSyncInterval_Title": "Email sync interval", - "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail." + "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail.", + "ContactsPage_Title": "Контакти", + "ContactsPage_AddContact": "Добави контакт", + "ContactsPage_EditContact": "Редактирай контакт", + "ContactsPage_DeleteContact": "Изтрий контакт", + "ContactsPage_SearchPlaceholder": "Търсене в контактите...", + "ContactsPage_NoContacts": "Няма контакти за показване", + "ContactsPage_ContactsCount": "{0} контакти", + "ContactsPage_SelectedContactsCount": "{0} избрани", + "ContactsPage_DeleteSelectedContacts": "Изтрий избраните", + "ContactEditDialog_Title": "Редактиране на контакт", + "ContactEditDialog_PhotoSection": "Снимка", + "ContactEditDialog_ChoosePhoto": "Изберете снимка", + "ContactEditDialog_RemovePhoto": "Премахни снимка", + "ContactEditDialog_NameHeader": "Име", + "ContactEditDialog_NamePlaceholder": "Име на контакт", + "ContactEditDialog_EmailHeader": "Имейл адрес", + "ContactEditDialog_EmailPlaceholder": "contact@example.com", + "ContactEditDialog_InfoSection": "Информация за контакт", + "ContactEditDialog_RootContactInfo": "Този контакт е основен и е свързан с вашите акаунти и не може да бъде изтрит.", + "ContactEditDialog_OverriddenContactInfo": "Този контакт е ръчно променен и няма да се актуализира по време на синхронизация.", + "ContactsPage_Subtitle": "Управлявайте контактите си за имейл и свързаната с тях информация.", + "ContactStatus_Account": "Акаунт", + "ContactStatus_Modified": "Променен", + "ContactAction_Edit": "Редактиране на контакт", + "ContactAction_ChangePhoto": "Смени снимка", + "ContactAction_Delete": "Изтрий контакт", + "ContactAction_Add": "Добави контакт", + "ContactSelection_Selected": "избрани", + "ContactSelection_SelectAll": "Избери всички", + "ContactSelection_Clear": "Изчисти избора", + "ContactsPage_EmptyState": "Няма контакти за показване", + "ContactsPage_AddFirstContact": "Добавете първия си контакт", + "ContactsPage_ContactsCountSuffix": "контакти", + "ContactsPane_NewContact": "Нов контакт", + "ContactsPane_DescriptionTitle": "Управлявайте контактите си", + "ContactsPane_DescriptionBody": "Създавайте контакти, преименувайте ги, актуализирайте профилните снимки и поддържайте запазената информация организирана на едно място.", + "ContactEditDialog_AddTitle": "Добави контакт", + "ContactInfoBar_ContactAdded": "Контактът е успешно добавен.", + "ContactInfoBar_ContactUpdated": "Контактът е успешно актуализиран.", + "ContactInfoBar_ContactsDeleted": "Контактите са успешно изтрити.", + "ContactInfoBar_ContactPhotoUpdated": "Снимката на контакта е успешно актуализирана.", + "ContactInfoBar_FailedToLoadContacts": "Неуспях да заредя контактите: {0}", + "ContactInfoBar_FailedToAddContact": "Неуспях да добавя контакт: {0}", + "ContactInfoBar_FailedToUpdateContact": "Неуспях да актуализирам контакта: {0}", + "ContactInfoBar_FailedToDeleteContacts": "Неуспях да изтрия контактите: {0}", + "ContactInfoBar_FailedToUpdatePhoto": "Неуспях да актуализирам снимката: {0}", + "ContactInfoBar_CannotDeleteRoot": "Коренните контакти не могат да бъдат изтрити.", + "ContactConfirmDialog_DeleteTitle": "Изтрий контакт", + "ContactConfirmDialog_DeleteMessage": "Сигурни ли сте, че искате да изтриете контакта '{0}'?", + "ContactConfirmDialog_DeleteMultipleMessage": "Сигурни ли сте, че искате да изтриете {0} контакт(а)?", + "ContactConfirmDialog_DeleteButton": "Изтрий", + "CalendarAccountSettings_Title": "Настройки на календарния акаунт", + "CalendarAccountSettings_Description": "Управлявайте настройките за календара за {0}", + "CalendarAccountSettings_AccountColor": "Цвят на акаунта", + "CalendarAccountSettings_AccountColorDescription": "Променете цвета на показване за този календарен акаунт.", + "CalendarAccountSettings_SyncEnabled": "Включване на синхронизацията", + "CalendarAccountSettings_SyncEnabledDescription": "Включване или деактивиране на синхронизацията на календара за този акаунт.", + "CalendarAccountSettings_DefaultShowAs": "Статус за наличност по подразбиране", + "CalendarAccountSettings_DefaultShowAsDescription": "Статусът за наличност по подразбиране за нови събития, създадени с този акаунт.", + "CalendarAccountSettings_PrimaryCalendar": "Основен календар", + "CalendarAccountSettings_PrimaryCalendarDescription": "Маркирайте този календар като основен календар за акаунта.", + "CalendarSettings_NewEventBehavior_Header": "Поведение на бутона Ново събитие", + "CalendarSettings_NewEventBehavior_Description": "Изберете дали бутонът за Ново събитие да пита за календар всеки път или да отваря винаги конкретен календар.", + "CalendarSettings_NewEventBehavior_AskEachTime": "Попитайте всеки път.", + "CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar": "Винаги използвайте конкретен календар.", + "CalendarSettings_Rendering_Title": "Изобразяване", + "CalendarSettings_Rendering_Description": "Конфигурирайте подредбата на календара и поведението на показване.", + "CalendarSettings_Notifications_Title": "Известия", + "CalendarSettings_Notifications_Description": "Изберете поведението по подразбиране за напомняне и отлагане.", + "CalendarSettings_Preferences_Title": "Настройки", + "CalendarSettings_Preferences_Description": "Задайте как да се държи бутонът за Ново събитие.", + "WhatIsNew_GetStartedButton": "Започнете", + "WhatIsNew_ContinueAnywayButton": "Продължете въпреки това", + "WhatIsNew_PreparingForNewVersionButton": "Подготвяме за нова версия...", + "WhatIsNew_MigrationPreparing_Title": "Подготовка на данните ви", + "WhatIsNew_MigrationPreparing_Description": "Wino прилага миграции на актуализацията. Моля, изчакайте докато подготвим данните за акаунта ви за тази версия.", + "WhatIsNew_MigrationFailedMessage": "Прилагането на миграциите завърши с грешка {0}. Можете да продължите да използвате приложението. Ако обаче срещнете сериозни проблеми, моля преинсталирайте приложението.", + "WhatIsNew_MigrationNotification_Title": "Wino Mail обновен", + "WhatIsNew_MigrationNotification_Message": "Отворете приложението, за да завършите актуализацията и да видите какво е новото.", + "WelcomeWindow_Title": "Добре дошли в Wino Mail", + "WelcomeWindow_Subtitle": "Нативно изживяване за Mail и Calendar в Windows.", + "WelcomeWindow_WhatsNewTitle": "Последни промени", + "WelcomeWindow_FeaturesTitle": "Функции", + "WelcomeWindow_WhatsNewTab": "Какво е ново", + "WelcomeWindow_FeaturesTab": "Функции", + "WelcomeWindow_GetStartedButton": "Добави акаунт", + "WelcomeWindow_GetStartedDescription": "Добавете своя акаунт Outlook, Gmail или IMAP, за да започнете с Wino Mail.", + "WelcomeWindow_ImportFromWinoAccount": "Импортирайте от вашия Wino акаунт.", + "WelcomeWindow_ImportInProgress": "Импортиране на синхронизирани настройки и акаунти...", + "WelcomeWindow_ImportNoAccountsFound": "Не са намерени синхронизирани акаунти във вашия Wino акаунт. Ако имаше налични настройки, те са възстановени. Използвайте Започнете, за да добавите акаунт ръчно.", + "WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} синхронизирани акаунти вече са налични на това устройство. Ако е необходимо, използвайте Започнете, за да добавите още акаунт ръчно.", + "WelcomeWindow_SetupTitle": "Настройване на акаунт", + "WelcomeWindow_SetupSubtitle": "Изберете вашия имейл доставчик, за да започнете.", + "WelcomeWindow_AddAccountButton": "Добави акаунт", + "WelcomeWindow_SkipForNow": "Прескочете за сега — ще го настроя по-късно", + "WelcomeWindow_AppDescription": "Бърза, фокусирана входяща кутия — преработена за Windows 11", + "WelcomeWizard_Step1Title": "Добре дошли", + "SystemTrayMenu_Open": "Отвори", + "WinoAccount_Titlebar_SyncBenefitTitle": "Синхронизиране на настройки", + "WinoAccount_Titlebar_SyncBenefitDescription": "Поддържайте вашите настройки на Wino синхронизирани между устройствата.", + "WinoAccount_Titlebar_AddonsBenefitTitle": "Разблокирайте добавки", + "WinoAccount_Titlebar_AddonsBenefitDescription": "Достъп до премиум функции като Wino AI Pack.", + "WinoAccount_Management_Description": "Управлявайте своя Wino акаунт, достъп до AI Pack и синхронизираните предпочитания и данни за акаунта.", + "WinoAccount_Management_SignedOutTitle": "Влезте в Wino Mail", + "WinoAccount_Management_SignedOutDescription": "Влезте или създайте акаунт, за да синхронирате имейла си, да получите достъп до AI функции и да управлявате настройките си между устройства.", + "WinoAccount_Management_ProfileSectionHeader": "Профил", + "WinoAccount_Management_AddOnsSectionHeader": "Wino добавки", + "WinoAccount_Management_DataSectionHeader": "Данни", + "WinoAccount_Management_AccountActionsSectionHeader": "Действия на акаунта", + "WinoAccount_Management_AccountCardTitle": "Акаунт", + "WinoAccount_Management_AccountCardDescription": "Имейл адресът на вашия Wino акаунт и текущото състояние на акаунта.", + "WinoAccount_Management_AiPackCardTitle": "AI пакет", + "WinoAccount_Management_AiPackCardDescription": "Проверете дали Wino AI Pack е активен и колко използване остава.", + "WinoAccount_Management_AiPackActive": "AI Pack е активен", + "WinoAccount_Management_AiPackInactive": "AI Pack не е активен", + "WinoAccount_Management_AiPackUsage": "{0} от {1} използвания са използвани. Останали са {2}.", + "WinoAccount_Management_AiPackBillingPeriod": "Период на фактуриране: {0:d} - {1:d}", + "WinoAccount_Management_AiPackUnknownUsage": "Детайлите за използване все още не са налични.", + "WinoAccount_Management_AiPackBuyDescription": "Купете Wino AI Pack, за да превеждате, преписвате или резюмирате имейли с изкуствен интелект.", + "WinoAccount_Management_AiPackPromoTitle": "Разблокирайте AI Pack", + "WinoAccount_Management_AiPackPromoDescription": "Ускорете работния поток на имейлите си с инструменти, захранени от AI. Превеждайте съобщения на над 50 езика, преработете за яснота и тон и получавайте мигновени резюми на дълги нишки.", + "WinoAccount_Management_AiPackPromoPrice": "$4.99 / месец", + "WinoAccount_Management_AiPackPromoRequests": "1 000 кредита", + "WinoAccount_Management_AiPackGetButton": "Вземи AI Pack", + "WinoAddOn_AI_PACK_Name": "Wino AI Pack", + "WinoAddOn_AI_PACK_Description": "AI-инструменти за превод, преработка и резюмиране на имейли в Wino Mail.", + "WinoAddOn_AI_PACK_Keywords": "AI, превод, преработка, резюмиране, производителност", + "WinoAddOn_UNLIMITED_ACCOUNTS_Name": "Неограничени акаунти", + "WinoAddOn_UNLIMITED_ACCOUNTS_Description": "Премахнете ограниченията за акаунти и добавяйте толкова имейл акаунти, колкото ви трябват.", + "WinoAddOn_UNLIMITED_ACCOUNTS_Keywords": "акаунти, неограничени, премиум, добавка", + "WinoAccount_Management_PurchaseRequiresSignIn": "Влезте с вашия Wino акаунт, за да завършите тази покупка.", + "WinoAccount_Management_PurchaseStartFailed": "Wino не успя да завърши тази покупка в Microsoft Store.", + "WinoAccount_Management_StoreSyncFailed": "Вашата покупка завърши, но Wino не успя да обнови ползите за акаунта ви. Моля, опитайте отново след малко.", + "WinoAccount_Management_AiPackSubscriptionActive": "Вашият абонамент е активен", + "WinoAccount_Management_AiPackRenews": "Подновява се {0:d}", + "WinoAccount_Management_AiPackRequestsUsed": "Използвани кредити този месец", + "WinoAccount_Management_AiPackResets": "Нулиране {0:d}", + "WinoAccount_Management_AiPackUsageLoadFailed": "Имаше проблеми при зареждането на баланса на използване на AI.", + "WinoAccount_Management_AiPackFeatureTranslate": "Превод", + "WinoAccount_Management_AiPackFeatureRewrite": "Преработване", + "WinoAccount_Management_AiPackFeatureSummarize": "Резюмиране", + "WinoAccount_Management_AddOnLoadFailed": "Имаше проблеми с зареждането на тази добавка.", + "WinoAccount_Management_SyncPreferencesTitle": "Синхронизиране на настройки и акаунти", + "WinoAccount_Management_SyncPreferencesDescription": "Импортирайте или експортирайте вашите настройки на Wino и детайлите за пощенската кутия между устройства. Паролите, токени и друга чувствителна информация никога не се синхронират.", + "WinoAccount_Management_SignOutTitle": "Излез", + "WinoAccount_Management_SignOutDescription": "Излезте от акаунта си на това устройство", + "WinoAccount_Management_StatusLabel": "Статус: {0}", + "WinoAccount_Management_NoRemoteSettings": "Все още няма синхронизирани данни за този акаунт.", + "WinoAccount_Management_ExportSucceeded": "Избраните данни на Wino са експортирани успешно.", + "WinoAccount_Management_ExportPreferencesSucceeded": "Вашите предпочитания бяха експортирани във вашия Wino акаунт.", + "WinoAccount_Management_ExportAccountsSucceeded": "Експортирани са подробности за {0} акаунт във вашия Wino акаунт.", + "WinoAccount_Management_ImportSucceeded": "Импортирани са синхронизирани данни от вашия Wino акаунт.", + "WinoAccount_Management_ImportPreferencesSucceeded": "Приложени {0} синхронизирани предпочитания.", + "WinoAccount_Management_ImportAccountsSucceeded": "Импортирани {0} акаунти.", + "WinoAccount_Management_ImportDuplicateAccountsSkipped": "Пропуснати са {0} акаунти, които вече съществуват на това устройство.", + "WinoAccount_Management_ImportPartial": "Приложени са {0} синхронизирани настройки. {1} настройки не успяха да бъдат възстановени.", + "WinoAccount_Management_ImportReloginReminder": "Паролите, токените и друга чувствителна информация не са импортирани. Влезте отново за всеки акаунт на това устройство преди да ги използвате.", + "WinoAccount_Management_SerializeFailed": "Wino не успя да сериализира текущите ви настройки.", + "WinoAccount_Management_EmptyExport": "Няма стойности за експортиране на настройки.", + "WinoAccount_Management_ImportEmpty": "Пакетът с данни за синхронизация не съдържа нови данни за възстановяване.", + "WinoAccount_Management_ExportDialog_Title": "Експортиране към вашия Wino акаунт", + "WinoAccount_Management_ExportDialog_Description": "Изберете какво искате да синхронирате във вашия Wino акаунт.", + "WinoAccount_Management_ExportDialog_IncludePreferences": "Настройки", + "WinoAccount_Management_ExportDialog_IncludeAccounts": "Акаунти", + "WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Паролите, токените и друга чувствителна информация не се синхронират.", + "WinoAccount_Management_ExportDialog_AccountsRelogin": "Импортираните акаунти на друг компютър ще трябва да влезете отново, преди да могат да се използват.", + "WinoAccount_Management_ExportDialog_InProgress": "Експортът на избраните от вас Wino данни...", + "WinoAccount_Management_LoadFailed": "Wino не успя да зареди най-новата информация за Wino акаунта.", + "WinoAccount_Management_ActionFailed": "Заявката за Wino акаунт не може да бъде завършена.", + "WinoAccount_SettingsSection_Title": "Wino акаунт", + "WinoAccount_SettingsSection_Description": "Създайте или се впишете в Wino акаунт, използвайки локалната услуга за удостоверяване.", + "WinoAccount_RegisterButton_Title": "Регистрирайте акаунт", + "WinoAccount_RegisterButton_Description": "Създайте Wino акаунт с имейл и парола.", + "WinoAccount_RegisterButton_Action": "Отвори регистрацията", + "WinoAccount_LoginButton_Title": "Влезте", + "WinoAccount_LoginButton_Description": "Влезте в съществуващ Wino акаунт с имейл и парола.", + "WinoAccount_LoginButton_Action": "Отвори вход", + "WinoAccount_SignOutButton_Title": "Излезте", + "WinoAccount_SignOutButton_Description": "Премахнете локално запазената сесия на Wino акаунта.", + "WinoAccount_SignOutButton_Action": "Излезте", + "WinoAccount_RegisterDialog_Title": "Създайте Wino акаунт", + "WinoAccount_RegisterDialog_Description": "Създайте Wino акаунт, за да синхронизирате вашето изживяване с Wino и да отключите добавки, базирани на акаунти.", + "WinoAccount_RegisterDialog_HeroTitle": "Създайте вашия Wino акаунт", + "WinoAccount_RegisterDialog_BenefitsTitle": "Защо да създадете такъв акаунт?", + "WinoAccount_RegisterDialog_BenefitSyncTitle": "Импортиране и експортиране на настройки между устройства", + "WinoAccount_RegisterDialog_BenefitSyncDescription": "Прехвърляйте настройките на Wino между устройства без да започвате отначало.", + "WinoAccount_RegisterDialog_BenefitAiTitle": "Достъп до ексклузивни добавки като Wino AI Pack (платен)", + "WinoAccount_RegisterDialog_BenefitAiDescription": "Използвайте един акаунт, за да отключвате премиум функции на Wino, когато станат достъпни.", + "WinoAccount_RegisterDialog_DifferenceTitle": "Wino акаунтът е отделен от вашите имейл акаунти.", + "WinoAccount_RegisterDialog_DifferenceDescription": "Вашите Outlook, Gmail, IMAP или други имейл акаунти остават точно както са. Wino акаунтът управлява само Wino-специфичните функции и добавки, базирани на акаунти.", + "WinoAccount_RegisterDialog_PrimaryButton": "Регистрирайте", + "WinoAccount_RegisterDialog_PrivacyTitle": "Поверителност и обработка на API", + "WinoAccount_RegisterDialog_PrivacyDescription": "По избор добавки, като Wino AI Pack, могат да изпращат избраното HTML съдържание на имейла към Wino API услугата само когато използвате тези функции.", + "WinoAccount_RegisterDialog_PrivacyLinkText": "Прочетете политиката за поверителност.", + "WinoAccount_RegisterDialog_PrivacyCheckbox": "Съгласен съм с политиката за поверителност.", + "WinoAccount_LoginDialog_Title": "Влезте в Wino акаунт", + "WinoAccount_LoginDialog_Description": "Влезте във вашия Wino акаунт, за да синхронизирате настройките си и да получите достъп до функции, базирани на акаунти.", + "WinoAccount_LoginDialog_HeroTitle": "Добре дошли обратно", + "WinoAccount_LoginDialog_BenefitsTitle": "Какво ви дава входът", + "WinoAccount_LoginDialog_BenefitsDescription": "Използвайте Wino акаунта си, за да продължите да синхронизирате настройките между устройства и да получите достъп до платени добавки като Wino AI Pack.", + "WinoAccount_LoginDialog_DifferenceTitle": "Това не е вход за вашия имейл акаунт", + "WinoAccount_LoginDialog_DifferenceDescription": "Входът тук не добавя или замества вашите Outlook, Gmail или IMAP акаунти във Wino. Входът тук е само за услуги, специфични за Wino.", + "WinoAccount_LoginDialog_ForgotPasswordLink": "Забравена парола?", + "WinoAccount_EmailLabel": "Имейл", + "WinoAccount_EmailPlaceholder": "name@example.com", + "WinoAccount_PasswordLabel": "Парола", + "WinoAccount_ConfirmPasswordLabel": "Потвърдете паролата", + "WinoAccount_ForgotPasswordDialog_Title": "Възстановете паролата си", + "WinoAccount_ForgotPasswordDialog_PrimaryButton": "Изпрати имейл за нулиране на паролата", + "WinoAccount_ForgotPasswordDialog_BackToSignIn": "Върнете се към вход", + "WinoAccount_ForgotPasswordDialog_Description": "Въведете имейл адреса на вашия Wino акаунт и ние ще изпратим линк за нулиране на паролата, ако адресът е регистриран.", + "WinoAccount_Validation_EmailRequired": "Имейлът е задължителен.", + "WinoAccount_Validation_PasswordRequired": "Паролата е задължителна.", + "WinoAccount_Validation_PasswordMismatch": "Паролите не съвпадат.", + "WinoAccount_Validation_PrivacyConsentRequired": "Трябва да приемете политиката за поверителност, преди да създадете Wino акаунт.", + "WinoAccount_Error_InvalidCredentials": "Имейл адресът или паролата са неправилни.", + "WinoAccount_Error_AccountLocked": "Този акаунт е временно заключен.", + "WinoAccount_Error_AccountBanned": "Този акаунт е забранен.", + "WinoAccount_Error_AccountSuspended": "Този акаунт е спрян.", + "WinoAccount_Error_EmailNotConfirmed": "Моля, потвърдете имейл адреса си преди да влезете.", + "WinoAccount_Error_EmailConfirmationRequired": "Моля, потвърдете имейл адреса си преди да влезете.", + "WinoAccount_Error_EmailConfirmationResendNotAvailable": "Нов имейл за потвърждение все още не е наличен.", + "WinoAccount_Error_EmailConfirmationResendInvalid": "Тази заявка за потвърждение вече не е валидна. Моля, опитайте отново да влезете.", + "WinoAccount_Error_EmailNotRegistered": "Този имейл адрес не е регистриран.", + "WinoAccount_Error_RefreshTokenInvalid": "Вашата сесия вече не е валидна. Моля, влезте отново.", + "WinoAccount_Error_EmailAlreadyRegistered": "Този имейл адрес вече е регистриран.", + "WinoAccount_Error_ExternalLoginEmailRequired": "Имейл адрес е необходим за завършване на външно вписване.", + "WinoAccount_Error_ExternalLoginInvalid": "Заявката за външно вписване е невалидна.", + "WinoAccount_Error_ExternalAuthStateInvalid": "Състоянието на външното вписване е невалидно или изтекло.", + "WinoAccount_Error_ExternalAuthCodeInvalid": "Кодът за външно вписване е невалиден или изтекъл.", + "WinoAccount_Error_AiPackRequired": "За това действие е необходим активен абонамент за Wino AI Pack.", + "WinoAccount_Error_AiQuotaExceeded": "Достигнат е лимитът за използване на Wino AI Pack за текущия период на фактуриране.", + "WinoAccount_Error_AiHtmlEmpty": "Няма съдържание на имейла за обработка.", + "WinoAccount_Error_AiHtmlTooLarge": "Този имейл е твърде голям за обработка с Wino AI.", + "WinoAccount_Error_AiUnsupportedLanguage": "Този език не се поддържа. Опитайте валиден културен код като en-US или tr-TR.", + "WinoAccount_Error_Forbidden": "Нямате разрешение да изпълните това действие.", + "WinoAccount_Error_ValidationFailed": "Заявката е невалидна. Моля, прегледайте въведените стойности.", + "WinoAccount_RegisterSuccessMessage": "Регистрацията на Wino акаунт завърши за {0}.", + "WinoAccount_LoginSuccessMessage": "Влезнахте в Wino акаунт като {0}.", + "WinoAccount_EmailConfirmationSentDialog_Title": "Потвърдете имейл адреса си", + "WinoAccount_EmailConfirmationSentDialog_Message": "Изпратихме имейл за потвърждение до {0}. Моля, потвърдете го и опитайте отново да влезете.", + "WinoAccount_EmailConfirmationPendingDialog_Title": "Изисква се потвърждение на имейла", + "WinoAccount_EmailConfirmationPendingDialog_Message": "Все още очакваме потвърждението на {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ResendButton": "Изпрати отново имейла за потвърждение", + "WinoAccount_EmailConfirmationPendingDialog_Countdown": "Можете да изпратите повторно имейла за потвърждение след {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ReadyToResend": "Можете да изпратите отново имейла за потвърждение сега.", + "WinoAccount_EmailConfirmationResentDialog_Title": "Имейлът за потвърждение е изпратен повторно.", + "WinoAccount_EmailConfirmationResentDialog_Message": "Изпратихме още един имейл за потвърждение до {0}. Моля, потвърдете го и опитайте отново да се впишете.", + "WinoAccount_ForgotPasswordDialog_SuccessTitle": "Изпратен е имейл за нулиране на паролата.", + "WinoAccount_ForgotPasswordDialog_SuccessMessage": "Изпратихме имейл за нулиране на паролата до {0}. Отворете това съобщение, за да изберете нова парола.", + "WinoAccount_ChangePassword_Title": "Сменете паролата", + "WinoAccount_ChangePassword_Description": "Изпратете имейл за нулиране на паролата до този Wino акаунт.", + "WinoAccount_ChangePassword_Action": "Изпрати имейл за нулиране.", + "WinoAccount_ChangePassword_ConfirmationMessage": "Искате ли Wino да изпрати имейл за нулиране на парола до {0}?", + "WinoAccount_SignOut_SuccessMessage": "Излязохте от Wino акаунт {0}.", + "WinoAccount_SignOut_NoAccountMessage": "Нямате активен Wino акаунт за излизане.", + "WinoAccount_Titlebar_SignedOutTitle": "Wino акаунт", + "WinoAccount_Titlebar_SignedOutDescription": "Влезете в акаунт Wino или създайте Wino акаунт, за да управлявате сесията си в Wino.", + "WinoAccount_Titlebar_SignedInStatus": "Статус: {0}", + "WelcomeWizard_Step2Title": "Добави акаунт", + "WelcomeWizard_Step3Title": "Завършване на настройката", + "ProviderSelection_Title": "Изберете вашия имейл доставчик", + "ProviderSelection_Subtitle": "Изберете доставчик по-долу, за да добавите вашия имейл акаунт към Wino Mail.", + "ProviderSelection_AccountNameHeader": "Име на акаунт", + "ProviderSelection_AccountNamePlaceholder": "напр. Personal, Work", + "ProviderSelection_DisplayNameHeader": "Име за показване", + "ProviderSelection_DisplayNamePlaceholder": "напр. John Doe", + "ProviderSelection_EmailHeader": "Имейл адрес", + "ProviderSelection_EmailPlaceholder": "напр. johndoe@example.com", + "ProviderSelection_AppPasswordHeader": "Парола за приложение", + "ProviderSelection_AppPasswordHelp": "Как да получа парола за приложение?", + "ProviderSelection_CalendarModeHeader": "Интеграция на календара", + "ProviderSelection_CalendarMode_DisabledTitle": "Изключен", + "ProviderSelection_CalendarMode_DisabledDescription": "Няма календарна интеграция", + "ProviderSelection_CalendarMode_CalDavTitle": "CalDAV синхронизация", + "ProviderSelection_CalendarMode_CalDavDescription_Apple": "Вашите календарни събития се синхронизират между вашите устройства чрез сървърите на Apple.", + "ProviderSelection_CalendarMode_CalDavDescription_Yahoo": "Вашите календарни събития се синхронизират между вашите устройства чрез сървърите на Yahoo.", + "ProviderSelection_CalendarMode_LocalTitle": "Локален календар", + "ProviderSelection_CalendarMode_LocalDescription": "Вашите събития се съхраняват само на компютъра ви. Няма свързаност със сървър.", + "ProviderSelection_ClearColor": "Изчисти цвета", + "ProviderSelection_ContinueButton": "Продължи", + "ProviderSelection_SpecialImap_Subtitle": "Въведете данните за акаунта си, за да се свържете.", + "AccountSetup_Title": "Настройване на вашия акаунт", + "AccountSetup_Step_Authenticating": "Аутентикация с {0}", + "AccountSetup_Step_TestingMailAuth": "Тестване на имейл аутентификацията", + "AccountSetup_Step_SyncingFolders": "Синхронизиране на метаданни за папки", + "AccountSetup_Step_FetchingProfile": "Извличане на информация за профила", + "AccountSetup_Step_DiscoveringCalDav": "Откриване на настройки CalDAV", + "AccountSetup_Step_TestingCalendarAuth": "Тестване на календарната автентикация", + "AccountSetup_Step_SavingAccount": "Запазване на информация за акаунта", + "AccountSetup_Step_FetchingCalendarMetadata": "Извличане на календарни метаданни", + "AccountSetup_Step_SyncingAliases": "Синхронизиране на псевдоними", + "AccountSetup_Step_Finalizing": "Завършване на настройката", + "AccountSetup_FailureMessage": "Настройката се провали. Върнете се назад, за да коригирате настройките си, или опитайте отново по-късно.", + "AccountSetup_SuccessMessage": "Вашият акаунт е успешно настроен!", + "AccountSetup_GoBackButton": "Назад", + "AccountSetup_TryAgainButton": "Опитайте отново", + "ImapCalDavSettings_AutoDiscoveryFailed": "Автоматичното откриване се провали. Моля въведете настройките ръчно в раздела Разширени настройки." } - - diff --git a/Wino.Core.Domain/Translations/ca_ES/resources.json b/Wino.Core.Domain/Translations/ca_ES/resources.json index 8eccf7fa..0010c1eb 100644 --- a/Wino.Core.Domain/Translations/ca_ES/resources.json +++ b/Wino.Core.Domain/Translations/ca_ES/resources.json @@ -8,6 +8,7 @@ "AccountCacheReset_Message": "This account requires full re-sychronization to continue working. Please wait while Wino re-synchronizes your messages...", "AccountContactNameYou": "You", "AccountCreationDialog_Completed": "all done", + "AccountCreationDialog_FetchingCalendarMetadata": "Obtint detalls del calendari.", "AccountCreationDialog_FetchingEvents": "Fetching calendar events.", "AccountCreationDialog_FetchingProfileInformation": "Fetching profile details.", "AccountCreationDialog_GoogleAuthHelpClipboardText_Row0": "If your browser did not launch automatically to complete authentication:", @@ -17,6 +18,7 @@ "AccountCreationDialog_Initializing": "initializing", "AccountCreationDialog_PreparingFolders": "We are getting folder information at the moment.", "AccountCreationDialog_SigninIn": "Account information is being saved.", + "Purchased": "Comprat", "AccountEditDialog_Message": "Account Name", "AccountEditDialog_Title": "Edit Account", "AccountPickerDialog_Title": "Pick an account", @@ -26,6 +28,10 @@ "AccountDetailsPage_Description": "Change the name of the account in Wino and set desired sender name.", "AccountDetailsPage_ColorPicker_Title": "Account color", "AccountDetailsPage_ColorPicker_Description": "Assign a new account color to colorize its symbol in the list.", + "AccountDetailsPage_TabGeneral": "General", + "AccountDetailsPage_TabMail": "Correu", + "AccountDetailsPage_TabCalendar": "Calendari", + "AccountDetailsPage_CalendarListDescription": "Selecciona un calendari per configurar la seva configuració.", "AddHyperlink": "Add", "AppCloseBackgroundSynchronizationWarningTitle": "Background Synchronization", "AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Application has not been set to launch on Windows startup.", @@ -47,8 +53,10 @@ "BasicIMAPSetupDialog_Title": "IMAP Account", "Busy": "Busy", "Buttons_AddAccount": "Add Account", + "Buttons_FixAccount": "Reparar el compte", "Buttons_AddNewAlias": "Add New Alias", "Buttons_Allow": "Allow", + "Buttons_Apply": "Aplicar", "Buttons_ApplyTheme": "Apply Theme", "Buttons_Browse": "Browse", "Buttons_Cancel": "Cancel", @@ -62,6 +70,7 @@ "Buttons_Edit": "Edit", "Buttons_EnableImageRendering": "Enable", "Buttons_Multiselect": "Select Multiple", + "Buttons_Manage": "Gestiona", "Buttons_No": "No", "Buttons_Open": "Open", "Buttons_Purchase": "Purchase", @@ -70,15 +79,134 @@ "Buttons_Save": "Save", "Buttons_SaveConfiguration": "Save Configuration", "Buttons_Send": "Send", + "Buttons_SendToServer": "Envia al servidor", "Buttons_Share": "Share", "Buttons_SignIn": "Sign In", "Buttons_Sync": "Synchronize", "Buttons_SyncAliases": "Synchronize Aliases", "Buttons_TryAgain": "Try Again", "Buttons_Yes": "Yes", + "Sync_SynchronizingFolder": "Sincronitzant {0} {1}%", + "Sync_DownloadedMessages": "S'han descarregat {0} missatges de {1}", + "SyncAction_Archiving": "Arxivant {0} correu(s)", + "SyncAction_ClearingFlag": "Desmarcant {0} correu(s)", + "SyncAction_CreatingDraft": "Creant un esborrany", + "SyncAction_CreatingEvent": "Creant un esdeveniment", + "SyncAction_Deleting": "Esborrant {0} correu(s)", + "SyncAction_EmptyingFolder": "Buidant la carpeta", + "SyncAction_MarkingAsRead": "Marcant {0} correu(s) com a llegits", + "SyncAction_MarkingAsUnread": "Marcant {0} correu(s) com a no llegits", + "SyncAction_MarkingFolderAsRead": "Marcant la carpeta com a llegida", + "SyncAction_Moving": "Movent {0} correu(s)", + "SyncAction_MovingToFocused": "Movent {0} correu(s) a Focused", + "SyncAction_RenamingFolder": "Canviant el nom de la carpeta", + "SyncAction_SendingMail": "Enviant correu", + "SyncAction_SettingFlag": "Marcant {0} correu(s) amb bandera", + "SyncAction_SynchronizingAccount": "Sincronitzant {0}", + "SyncAction_SynchronizingAccounts": "Sincronitzant {0} compte(s)", + "SyncAction_SynchronizingCalendarData": "Sincronitzant dades del calendari", + "SyncAction_SynchronizingCalendarEvents": "Sincronitzant esdeveniments del calendari", + "SyncAction_SynchronizingCalendarMetadata": "Sincronitzant metadades del calendari", + "SyncAction_Unarchiving": "Desarxivant {0} correu(s)", "CalendarAllDayEventSummary": "all-day events", "CalendarDisplayOptions_Color": "Color", "CalendarDisplayOptions_Expand": "Expand", + "CalendarEventResponse_Accept": "Acceptar", + "CalendarEventResponse_AcceptedResponse": "Acceptat", + "CalendarEventResponse_Decline": "Declinar", + "CalendarEventResponse_DeclinedResponse": "Declinat", + "CalendarEventResponse_NotResponded": "No contestat", + "CalendarEventResponse_Tentative": "Tentatiu", + "CalendarEventResponse_TentativeResponse": "Tentatiu", + "CalendarEventRsvpPanel_Accept": "Accepta", + "CalendarEventRsvpPanel_AddMessage": "Afegeix un missatge a la teva resposta... (opcional)", + "CalendarEventRsvpPanel_Decline": "Declina", + "CalendarEventRsvpPanel_Message": "Missatge", + "CalendarEventRsvpPanel_SendReplyMessage": "Envia un missatge de resposta", + "CalendarEventRsvpPanel_Tentative": "Tentatiu", + "CalendarEventRsvpPanel_Title": "Opcions de resposta", + "CalendarAttendeeStatus_Accepted": "Acceptat", + "CalendarAttendeeStatus_Declined": "Declinat", + "CalendarAttendeeStatus_NeedsAction": "Requereix acció", + "CalendarAttendeeStatus_Tentative": "Tentatiu", + "CalendarEventDetails_Attachments": "Adjunts", + "CalendarEventCompose_AddAttachment": "Afegeix un adjunt", + "CalendarEventCompose_AllDay": "Tot el dia", + "CalendarEventCompose_AttachmentsNotSupportedForCalDav": "Els adjunts no són compatibles amb calendaris CalDAV.", + "CalendarEventCompose_EndDate": "Data de finalització", + "CalendarEventCompose_EndTime": "Hora de finalització", + "CalendarEventCompose_Every": "cada", + "CalendarEventCompose_ForWeekdays": "per", + "CalendarEventCompose_FrequencyDay": "dia", + "CalendarEventCompose_FrequencyDayPlural": "dies", + "CalendarEventCompose_FrequencyMonth": "mes", + "CalendarEventCompose_FrequencyMonthPlural": "mesos", + "CalendarEventCompose_FrequencyWeek": "setmana", + "CalendarEventCompose_FrequencyWeekPlural": "setmanes", + "CalendarEventCompose_FrequencyYear": "any", + "CalendarEventCompose_FrequencyYearPlural": "anys", + "CalendarEventCompose_Location": "Ubicació", + "CalendarEventCompose_LocationPlaceholder": "Afegeix una ubicació", + "CalendarEventCompose_NewEventButton": "Nou esdeveniment", + "CalendarEventCompose_DefaultCalendarHint": "Podeu triar un calendari per defecte per als nous esdeveniments a la configuració del Calendari.", + "CalendarEventCompose_DefaultCalendarSettingsLink": "Obre la configuració del Calendari", + "CalendarEventCompose_NoCalendarsMessage": "Encara no hi ha calendaris disponibles per a la creació d'esdeveniments.", + "CalendarEventCompose_NoCalendarsTitle": "No hi ha calendaris disponibles", + "CalendarEventCompose_NoEndDate": "Sense data de finalització", + "CalendarEventCompose_Notes": "Notes", + "CalendarEventCompose_PickCalendarTitle": "Tria un calendari", + "CalendarEventCompose_Recurring": "Recurrent", + "CalendarEventCompose_RecurringSummary": "Ocorre cada {0} {1}{2} {3} efectiu{4}{5}", + "CalendarEventCompose_RecurringSummarySmart": "Ocorre {0}{1} {2} efectiu{3}{4}", + "CalendarEventCompose_RepeatEvery": "Repeteix cada", + "CalendarEventCompose_SelectCalendar": "Selecciona el calendari", + "CalendarEventCompose_SingleOccurrenceSummary": "Ocorre el {0} {1}", + "CalendarEventCompose_StartDate": "Data d'inici", + "CalendarEventCompose_StartTime": "Hora d'inici", + "CalendarEventCompose_TimeRangeSummary": "dels {0} al {1}", + "CalendarEventCompose_Title": "Títol de l'esdeveniment", + "CalendarEventCompose_TitlePlaceholder": "Afegeix un títol", + "CalendarEventCompose_Until": "fins a", + "CalendarEventCompose_UntilSummary": " fins a {0}", + "CalendarEventCompose_ValidationInvalidAllDayRange": "La data de finalització de tot el dia ha de ser posterior a la data d'inici.", + "CalendarEventCompose_ValidationInvalidAttendee": "Un o més assistents tenen una adreça de correu electrònic invàlida.", + "CalendarEventCompose_ValidationInvalidRecurrenceEnd": "La data de finalització de la recurrència ha de ser igual o posterior a la data d'inici de l'esdeveniment.", + "CalendarEventCompose_ValidationInvalidTimeRange": "La hora de finalització ha de ser posterior a la hora d'inici.", + "CalendarEventCompose_ValidationMissingAttachment": "Un o més adjunts ja no estan disponibles: {0}", + "CalendarEventCompose_ValidationMissingCalendar": "Selecciona un calendari abans de crear l'esdeveniment.", + "CalendarEventCompose_ValidationMissingTitle": "Introdueix un títol de l'esdeveniment abans de crear l'esdeveniment.", + "CalendarEventCompose_ValidationTitle": "La validació de l'esdeveniment ha fallat.", + "CalendarEventCompose_WeekdaySummary": " el {0}", + "CalendarEventCompose_Weekday_Friday": "Dv", + "CalendarEventCompose_Weekday_Monday": "Dl", + "CalendarEventCompose_Weekday_Saturday": "Dissabte", + "CalendarEventCompose_Weekday_Sunday": "Diumenge", + "CalendarEventCompose_Weekday_Thursday": "Dijous", + "CalendarEventCompose_Weekday_Tuesday": "Dimarts", + "CalendarEventCompose_Weekday_Wednesday": "Dimecres", + "CalendarEventDetails_Details": "Detalls", + "CalendarEventDetails_EditSeries": "Editar la sèrie", + "CalendarEventDetails_Editing": "Editant", + "CalendarEventDetails_InviteSomeone": "Convida algú", + "CalendarEventDetails_JoinOnline": "Uneix-te en línia", + "CalendarEventDetails_Organizer": "Organitzador", + "CalendarEventDetails_People": "Persones", + "CalendarEventDetails_ReadOnlyEvent": "Esdeveniment de només lectura", + "CalendarEventDetails_Reminder": "Recordatori", + "CalendarReminder_StartedHoursAgo": "Va començar fa {0} hores", + "CalendarReminder_StartedMinutesAgo": "Va començar fa {0} minuts", + "CalendarReminder_StartedNow": "Va començar ara mateix", + "CalendarReminder_StartingNow": "Començant ara", + "CalendarReminder_StartsInHours": "Comença en {0} hores", + "CalendarReminder_StartsInMinutes": "Comença en {0} minuts", + "CalendarReminder_SnoozeAction": "Aplazar", + "CalendarReminder_SnoozeMinutesOption": "{0} minuts", + "CalendarEventDetails_ShowAs": "Mostra com", + "CalendarShowAs_Free": "Lliure", + "CalendarShowAs_Tentative": "Tentatiu", + "CalendarShowAs_Busy": "Ocupat", + "CalendarShowAs_OutOfOffice": "Fora de l'oficina", + "CalendarShowAs_WorkingElsewhere": "Treballant en un altre lloc", "CalendarItem_DetailsPopup_JoinOnline": "Join online", "CalendarItem_DetailsPopup_ViewEventButton": "View event", "CalendarItem_DetailsPopup_ViewSeriesButton": "View series", @@ -88,6 +216,9 @@ "ClipboardTextCopied_Message": "{0} copied to clipboard.", "ClipboardTextCopied_Title": "Copied", "ClipboardTextCopyFailed_Message": "Failed to copy {0} to clipboard.", + "ContactInfoBar_ErrorTitle": "Error al carregar la informació de contactes", + "ContactInfoBar_SuccessTitle": "Informació de contacte carregada", + "ContactInfoBar_WarningTitle": "La informació de contacte podria estar incompleta", "ComingSoon": "Coming soon...", "ComposerAttachmentsDragDropAttach_Message": "Attach", "ComposerAttachmentsDropZone_Message": "Drop your files here", @@ -129,6 +260,10 @@ "DialogMessage_CreateLinkedAccountTitle": "Account Link Name", "DialogMessage_DeleteAccountConfirmationMessage": "Delete {0}?", "DialogMessage_DeleteAccountConfirmationTitle": "All data associated with this account will be deleted from disk permanently.", + "DialogMessage_DeleteEmailTemplateConfirmationMessage": "Eliminar la plantilla \"{0}\"?", + "DialogMessage_DeleteEmailTemplateConfirmationTitle": "Eliminar plantilla de correu", + "DialogMessage_DeleteRecurringSeriesMessage": "Aquesta acció eliminarà tots els esdeveniments de la sèrie. Voleu continuar?", + "DialogMessage_DeleteRecurringSeriesTitle": "Eliminar sèrie recurrent", "DialogMessage_DiscardDraftConfirmationMessage": "This draft will be discarded. Do you want to continue?", "DialogMessage_DiscardDraftConfirmationTitle": "Discard Draft", "DialogMessage_EmptySubjectConfirmation": "Missing Subject", @@ -172,11 +307,18 @@ "ElementTheme_Light": "Light mode", "Emoji": "Emoji", "Error_FailedToSetupSystemFolders_Title": "Failed to setup system folders", + "Exception_AccountNeedsAttention_Title": "El compte necessita atenció", + "Exception_AccountNeedsAttention_Message": "'{0}' requereix la teva atenció per continuar treballant.", + "Exception_WebView2RuntimeMissing_Message": "Wino Mail no va poder trobar el runtime de Microsoft Edge WebView2. Si us plau, instal·leu-lo o reparau el runtime per renderitzar correctament el contingut dels missatges.", + "Exception_WebView2RuntimeMissing_Title": "Es necessita el runtime WebView2", "Exception_AuthenticationCanceled": "Authentication canceled", "Exception_CustomThemeExists": "This theme already exists.", "Exception_CustomThemeMissingName": "You must provide a name.", "Exception_CustomThemeMissingWallpaper": "You must provide a custom background image.", "Exception_FailedToSynchronizeAliases": "Failed to synchronize aliases", + "Exception_FailedToSynchronizeCalendarData": "Error al sincronitzar les dades del calendari", + "Exception_FailedToSynchronizeCalendarEvents": "Error al sincronitzar els esdeveniments del calendari", + "Exception_FailedToSynchronizeCalendarMetadata": "Error al sincronitzar les metadades del calendari", "Exception_FailedToSynchronizeFolders": "Failed to synchronize folders", "Exception_FailedToSynchronizeProfileInformation": "Failed to synchronize profile information", "Exception_GoogleAuthCallbackNull": "Callback uri is null on activation.", @@ -229,6 +371,32 @@ "HoverActionOption_MoveJunk": "Move to Junk", "HoverActionOption_ToggleFlag": "Flag / Unflag", "HoverActionOption_ToggleRead": "Read / Unread", + "KeyboardShortcuts_FailedToReset": "Error al restablir les dreceres de teclat.", + "KeyboardShortcuts_FailedToUpdate": "Error en actualitzar les dreceres de teclat.", + "KeyboardShortcuts_MailoperationAction": "Acció", + "KeyboardShortcuts_Action": "Acció", + "KeyboardShortcuts_FailedToLoad": "Error al carregar les dreceres de teclat.", + "KeyboardShortcuts_EnterKeyForShortcut": "Si us plau, introdueix una tecla per a la drecera.", + "KeyboardShortcuts_SelectOperationForShortcut": "Si us plau, seleccioneu una acció per a la drecera.", + "KeyboardShortcuts_EnterKey": "Si us plau, introdueix una tecla per a la drecera.", + "KeyboardShortcuts_SelectOperation": "Si us plau, seleccioneu una acció per a la drecera.", + "KeyboardShortcuts_ShortcutInUse": "Aquest accés ràpid ja està en ús per un altre drecera.", + "KeyboardShortcuts_FailedToSave": "No s'ha pogut guardar la drecera.", + "KeyboardShortcuts_FailedToDelete": "No s'ha pogut eliminar la drecera.", + "KeyboardShortcuts_PageDescription": "Configura les dreceres de teclat per a les operacions ràpides de correu. Premeu les tecles mentre el camp d'entrada de tecles estigui enfocat per capturar les dreceres.", + "KeyboardShortcuts_Add": "Afegeix drecera", + "KeyboardShortcuts_EditTitle": "Edita la drecera de teclat", + "KeyboardShortcuts_ResetToDefaults": "Restableix als valors per defecte", + "KeyboardShortcuts_PressKeysHere": "Premieu aquí les tecles...", + "KeyboardShortcuts_KeyCombination": "Combinació de tecles", + "KeyboardShortcuts_FocusArea": "Focalitza el camp anterior i prem la combinació de tecles desitjada", + "KeyboardShortcuts_Modifiers": "Tecles modificadores", + "KeyboardShortcuts_Mode": "Modalitat", + "KeyboardShortcuts_ModeMail": "Correu", + "KeyboardShortcuts_ModeCalendar": "Calendari", + "KeyboardShortcuts_ActionToggleReadUnread": "Alternar llegit/no llegit", + "KeyboardShortcuts_ActionToggleFlag": "Alternar etiqueta", + "KeyboardShortcuts_ActionToggleArchive": "Alternar arxivar/no arxivar", "ImageRenderingDisabled": "Image rendering is disabled for this message.", "ImapAdvancedSetupDialog_AuthenticationMethod": "Authentication method", "ImapAdvancedSetupDialog_ConnectionSecurity": "Connection security", @@ -295,12 +463,58 @@ "IMAPSetupDialog_Username": "Username", "IMAPSetupDialog_UsernamePlaceholder": "johndoe, johndoe@fabrikam.com, domain/johndoe", "IMAPSetupDialog_UseSameConfig": "Use the same username and password for sending email", + "ImapCalDavSettingsPage_TitleCreate": "Configuració d'IMAP i Calendari", + "ImapCalDavSettingsPage_TitleEdit": "Editar la configuració d'IMAP i Calendari", + "ImapCalDavSettingsPage_Subtitle": "Configura IMAP/SMTP i la sincronització opcional del calendari per a aquest compte.", + "ImapCalDavSettingsPage_BasicSectionTitle": "Configuració bàsica", + "ImapCalDavSettingsPage_BasicSectionDescription": "Introdueix la teva identitat i credencials. Wino pot intentar detectar la configuració del servidor automàticament.", + "ImapCalDavSettingsPage_BasicTab": "Bàsic", + "ImapCalDavSettingsPage_EnableCalendarSupport": "Activa el suport de calendaris", + "ImapCalDavSettingsPage_AutoDiscoverButton": "Autodescobriment de la configuració de correu", + "ImapCalDavSettingsPage_AutoDiscoverySuccessMessage": "Configuracions de correu descobertes i aplicades.", + "ImapCalDavSettingsPage_AdvancedSectionTitle": "Configuració avançada", + "ImapCalDavSettingsPage_AdvancedSectionDescription": "Introduïu la configuració del servidor manualment si la descoberta automàtica no està disponible o és incorrecta.", + "ImapCalDavSettingsPage_AdvancedTab": "Avançat", + "ImapCalDavSettingsPage_CalendarSectionTitle": "Configuració del calendari", + "ImapCalDavSettingsPage_CalendarSectionDescription": "Trieu com han de funcionar les dades del calendari per a aquest compte IMAP.", + "ImapCalDavSettingsPage_CalendarModeHeader": "Mode de calendari", + "ImapCalDavSettingsPage_ConnectionSecurityHeader": "Seguretat de la connexió", + "ImapCalDavSettingsPage_AuthenticationMethodHeader": "Mètode d'autenticació", + "ImapCalDavSettingsPage_CalendarModeDisabled": "Desactivat", + "ImapCalDavSettingsPage_CalendarModeCalDav": "Sincronització CalDAV", + "ImapCalDavSettingsPage_CalendarModeLocalOnly": "Calendari local només", + "ImapCalDavSettingsPage_CalendarModeDisabledDescription": "El calendari està desactivat per a aquest compte.", + "ImapCalDavSettingsPage_CalendarModeCalDavDescription": "Els elements del calendari es sincronitzen amb el vostre servidor CalDAV.", + "ImapCalDavSettingsPage_CalendarModeLocalOnlyDescription": "Els elements del calendari s'emmagatzemen només en aquest ordinador i no es sincronitzen a la xarxa.", + "ImapCalDavSettingsPage_LocalCalendarLearnMore": "Com funciona el calendari local", + "ImapCalDavSettingsPage_LocalCalendarDialogTitle": "Calendari local només", + "ImapCalDavSettingsPage_LocalCalendarDialogMessage": "El calendari local guarda tots els esdeveniments només al vostre ordinador. Res es sincronitza amb iCloud, Yahoo o cap altre proveïdor.", + "ImapCalDavSettingsPage_CalDavServiceUrl": "URL del servei CalDAV", + "ImapCalDavSettingsPage_CalDavUsername": "Nom d'usuari CalDAV", + "ImapCalDavSettingsPage_CalDavPassword": "Contrasenya CalDAV", + "ImapCalDavSettingsPage_CalDavNotRequiredMessage": "La prova de CalDAV només és requerida quan el mode de calendari està configurat per a la sincronització CalDAV.", + "ImapCalDavSettingsPage_CalDavUrlRequired": "L'URL del servei CalDAV és obligatori.", + "ImapCalDavSettingsPage_CalDavUrlInvalid": "L'URL del servei CalDAV ha de ser una URL absoluta.", + "ImapCalDavSettingsPage_CalDavUsernameRequired": "CalDAV: s'ha d'introduir un nom d'usuari.", + "ImapCalDavSettingsPage_CalDavPasswordRequired": "CalDAV: s'ha d'introduir una contrasenya.", + "ImapCalDavSettingsPage_TestImapButton": "Prova la connexió IMAP", + "ImapCalDavSettingsPage_TestCalDavButton": "Prova la connexió CalDAV", + "ImapCalDavSettingsPage_ImapTestSuccessMessage": "La prova de connexió IMAP s'ha completat amb èxit.", + "ImapCalDavSettingsPage_CalDavTestSuccessMessage": "La prova de connexió CalDAV s'ha completat amb èxit.", + "ImapCalDavSettingsPage_SaveSuccessMessage": "La configuració del compte s'ha guardat.", + "ImapCalDavSettingsPage_ICloudHint": "Utilitza una contrasenya específica de l'aplicació generada des de la configuració del teu compte Apple.", + "ImapCalDavSettingsPage_YahooHint": "Utilitza una contrasenya d'aplicació de la configuració de seguretat del teu compte Yahoo.", "Info_AccountCreatedMessage": "{0} is created", "Info_AccountCreatedTitle": "Account Creation", "Info_AccountCreationFailedTitle": "Account Creation Failed", "Info_AccountDeletedMessage": "{0} is successfuly deleted.", "Info_AccountDeletedTitle": "Account Deleted", "Info_AccountIssueFixFailedTitle": "Failed", + "Info_AccountIssueFixImapMessage": "Obre la pàgina de configuració IMAP i Calendari per tornar a introduir les credencials del servidor.", + "Info_AccountAttentionRequiredMessage": "Aquest compte necessita la teva atenció.", + "Info_AccountAttentionRequiredClickableMessage": "Fes clic per solucionar aquest compte i tornar a sincronitzar-lo.", + "Info_AccountAttentionRequiredAction": "Solucionar", + "Info_AccountAttentionRequiredActionHint": "Feu clic a Solucionar per resoldre aquest problema del compte.", "Info_AccountIssueFixSuccessMessage": "Fixed all account issues.", "Info_AccountIssueFixSuccessTitle": "Success", "Info_AttachmentOpenFailedMessage": "Can't open this attachment.", @@ -370,6 +584,7 @@ "InfoBarMessage_SynchronizationDisabledFolder": "This folder is disabled for synchronization.", "InfoBarTitle_SynchronizationDisabledFolder": "Disabled Folder", "Justify": "Justify", + "MenuUpdateAvailable": "Actualització disponible", "Left": "Left", "Link": "Link", "LinkedAccountsCreatePolicyMessage": "you must have at least 2 accounts to create link\nlink will be removed on save", @@ -403,6 +618,7 @@ "MailOperation_Unarchive": "Unarchive", "MailOperation_ViewMessageSource": "View message source", "MailOperation_Zoom": "Zoom", + "MailsDragging": "Arrossegant {0} element(s)", "MailsSelected": "{0} item(s) selected", "MarkFlagUnflag": "Mark as flagged/unflagged", "MarkReadUnread": "Mark as read/unread", @@ -434,6 +650,8 @@ "Notifications_MultipleNotificationsTitle": "New Mail", "Notifications_WinoUpdatedMessage": "Checkout new version {0}", "Notifications_WinoUpdatedTitle": "Wino Mail has been updated.", + "Notifications_StoreUpdateAvailableTitle": "Actualització disponible", + "Notifications_StoreUpdateAvailableMessage": "Una nova versió de Wino Mail està preparada per a la instal·lació des de Microsoft Store.", "OnlineSearchFailed_Message": "Failed to perform search\n{0}\n\nListing offline mails.", "OnlineSearchTry_Line1": "Can't find what you are looking for?", "OnlineSearchTry_Line2": "Try online search.", @@ -446,7 +664,6 @@ "PaneLengthOption_Small": "Small", "Photos": "Photos", "PreparingFoldersMessage": "Preparing folders", - "ProtocolLogAvailable_Message": "Protocol logs are available for diagnostics.", "ProviderDetail_Gmail_Description": "Google Account", "ProviderDetail_iCloud_Description": "Apple iCloud Account", "ProviderDetail_iCloud_Title": "iCloud", @@ -465,9 +682,14 @@ "SearchBarPlaceholder": "Search", "SearchingIn": "Searching in", "SearchPivotName": "Results", + "Settings_KeyboardShortcuts_Title": "Atajos de teclat", + "Settings_KeyboardShortcuts_Description": "Gestiona els atajos de teclat per a accions ràpides als missatges.", "SettingConfigureSpecialFolders_Button": "Configure", "SettingsEditAccountDetails_IMAPConfiguration_Title": "IMAP/SMTP Configuration", "SettingsEditAccountDetails_IMAPConfiguration_Description": "Change your incoming/outgoing server settings.", + "SettingsEditAccountDetails_ImapCalDavSettings_Title": "Configuració IMAP i Calendari", + "SettingsEditAccountDetails_ImapCalDavSettings_Description": "Obre la pàgina de configuració dedicada d'IMAP, SMTP i CalDAV per a aquest compte.", + "SettingsEditAccountDetails_ImapCalDavSettings_Action": "Obre la configuració", "SettingsAbout_Description": "Learn more about Wino.", "SettingsAbout_Title": "About", "SettingsAboutGithub_Description": "Go to issue tracker GitHub repository.", @@ -490,6 +712,10 @@ "SettingsAppPreferences_SearchMode_Local": "Local", "SettingsAppPreferences_SearchMode_Online": "Online", "SettingsAppPreferences_SearchMode_Title": "Default search mode", + "SettingsAppPreferences_ApplicationMode_Title": "Mode de l'aplicació per defecte", + "SettingsAppPreferences_ApplicationMode_Description": "Trieu el mode per defecte amb què s'obrirà Wino quan no s'especifiqui cap tipus d'activació.", + "SettingsAppPreferences_ApplicationMode_Mail": "Correu", + "SettingsAppPreferences_ApplicationMode_Calendar": "Calendari", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description": "Wino Mail will keep running in the background. You will be notified as new mails arrive.", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title": "Run in the background", "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description": "Wino Mail will keep running on the system tray. Available to launch by clicking on an icon. You will be notified as new mails arrive.", @@ -506,12 +732,30 @@ "SettingsAppPreferences_StartupBehavior_FatalError": "Fatal error occurred while changing the startup mode for Wino Mail.", "SettingsAppPreferences_StartupBehavior_Title": "Start minimized on Windows startup", "SettingsAppPreferences_Title": "App Preferences", + "SettingsAppPreferences_HideWinoAccountButton_Title": "Amaga el botó de compte de Wino a la barra de títol.", + "SettingsAppPreferences_HideWinoAccountButton_Description": "Amaga el botó de perfil de la barra de títol que obre el desplegable del compte de Wino.", + "SettingsAppPreferences_StoreUpdateNotifications_Title": "Notificacions d'actualitzacions de la Botiga", + "SettingsAppPreferences_StoreUpdateNotifications_Description": "Mostra notificacions i accions al peu de pàgina quan hi hagi una actualització disponible a Microsoft Store.", + "SettingsAppPreferences_AiActions_Title": "Accions d'IA", + "SettingsAppPreferences_AiActions_Description": "Trieu les llengües d'IA predeterminades i on s'han d'emmagatzemar els resums.", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Title": "Idioma de traducció predeterminant", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Description": "Trieu l'idioma de destinació predeterminat utilitzat per les accions de traducció amb IA.", + "SettingsAppPreferences_AiSummarizeLanguage_Title": "Idioma de resum", + "SettingsAppPreferences_AiSummarizeLanguage_Description": "Seleccioneu l'idioma de resum preferit per a la sortida futura de resums d'IA.", + "SettingsAppPreferences_AiSummarySavePath_Title": "Ruta de desament predeterminada per als resums.", + "SettingsAppPreferences_AiSummarySavePath_Description": "Seleccioneu la carpeta que Wino hauria d'utilitzar com a predeterminada per desar resums d'IA.", + "SettingsAppPreferences_AiSummarySavePath_Placeholder": "Utilitza la ubicació de desament predeterminada del sistema.", + "SettingsAppPreferences_AiSummarySavePath_InvalidHint": "Aquesta carpeta no existeix. S'utilitzarà la ubicació de desament predeterminada per als resums.", "SettingsAutoSelectNextItem_Description": "Select the next item after you delete or move a mail.", "SettingsAutoSelectNextItem_Title": "Auto select next item", "SettingsAvailableThemes_Description": "Select a theme from Wino's own collection for your taste or apply your own themes.", "SettingsAvailableThemes_Title": "Available Themes", "SettingsCalendarSettings_Description": "Change first day of week, hour cell height and more...", "SettingsCalendarSettings_Title": "Calendar Settings", + "CalendarSettings_DefaultSnoozeDuration_Header": "Durada de postergació per defecte", + "CalendarSettings_DefaultSnoozeDuration_Description": "Estableix una durada de postergació per defecte per a les notificacions de recordatoris del calendari.", + "CalendarSettings_TimedDayHeaderFormat_Header": "Format d'encapçalament de dia de la vista amb hora", + "CalendarSettings_TimedDayHeaderFormat_Description": "Trieu com es representen les etiquetes de dia en les vistes de dia, setmana i setmana laboral. Utilitza tokens de format de data com ddd, dd, MMM o dddd.", "SettingsComposer_Title": "Composer", "SettingsComposerFont_Title": "Default Composer Font", "SettingsComposerFontFamily_Description": "Change the default font family and font size for composing mails.", @@ -531,6 +775,9 @@ "SettingsDiscord_Title": "Discord Channel", "SettingsEditLinkedInbox_Description": "Add / remove accounts, rename or break the link between accounts.", "SettingsEditLinkedInbox_Title": "Edit Linked Inbox", + "SettingsWindowBackdrop_Title": "Fons de la finestra", + "SettingsWindowBackdrop_Description": "Seleccioneu un efecte de fons per a les finestres de Wino.", + "SettingsWindowBackdrop_Disabled": "La selecció de fons de finestra està deshabilitada quan el tema de l'aplicació és diferent de Predeterminat.", "SettingsElementTheme_Description": "Select a Windows theme for Wino", "SettingsElementTheme_Title": "Element Theme", "SettingsElementThemeSelectionDisabled": "Element theme selection is disabled when application theme is selected other than Default.", @@ -581,6 +828,8 @@ "SettingsManageAliases_Title": "Aliases", "SettingsEditAccountDetails_Title": "Edit Account Details", "SettingsEditAccountDetails_Description": "Change account name, sender name and assign a new color if you like.", + "EditAccountDetailsPage_SaveSuccess_Title": "Canvis desats", + "EditAccountDetailsPage_SaveSuccess_Message": "Les dades del compte s'han actualitzat correctament.", "SettingsManageLink_Description": "Move items to add new link or remove existing link.", "SettingsManageLink_Title": "Manage Link", "SettingsMarkAsRead_Description": "Change what should happen to the selected item.", @@ -596,7 +845,41 @@ "SettingsNotifications_Title": "Notifications", "SettingsNotificationsAndTaskbar_Description": "Change whether notifications should be displayed and taskbar badge for this account.", "SettingsNotificationsAndTaskbar_Title": "Notifications & Taskbar", + "SettingsHome_Title": "Inici", + "SettingsHome_SearchTitle": "Cercar una configuració", + "SettingsHome_SearchDescription": "Cerca per característica, tema o paraula-clau per anar directament a la pàgina de configuració correcta.", + "SettingsHome_SearchPlaceholder": "Cerca configuracions", + "SettingsHome_SearchExamples": "Prova: tema, emmagatzematge, idioma, signatura", + "SettingsHome_QuickLinks_Title": "Enllaços ràpids", + "SettingsHome_QuickLinks_Description": "Accediu ràpidament a les configuracions que es consulten més sovint.", + "SettingsHome_StorageCard_Description": "Comproveu quina quantitat de contingut MIME local guarda Wino en aquest dispositiu i netegeu-lo quan sigui necessari.", + "SettingsHome_StorageEmptySummary": "Encara no s'ha detectat contingut MIME en memòria cau.", + "SettingsHome_StorageLoading": "Comprovant l'ús de MIME local...", + "SettingsHome_Tips_Title": "Consells i trucs", + "SettingsHome_Tips_Description": "Uns pocs canvis poden fer que Wino sigui molt més personal.", + "SettingsHome_Tip_Theme": "Vols mode fosc o canvis d'accent? Obre Personalització.", + "SettingsHome_Tip_Background": "Utilitza Preferències de l'Aplicació per controlar el comportament d'inici i la sincronització en segon pla.", + "SettingsHome_Tip_Shortcuts": "Els atajos de teclat t'ajuden a moure't pels correus més ràpidament.", + "SettingsHome_Resources_Title": "Enllaços útils", + "SettingsHome_Resources_Description": "Obre recursos del projecte, informació de suport i canals de llançament.", "SettingsOptions_Title": "Settings", + "SettingsOptions_GeneralSection": "General", + "SettingsOptions_MailSection": "Correu", + "SettingsOptions_CalendarSection": "Calendari", + "SettingsOptions_MoreComingSoon": "Més opcions aviat", + "SettingsOptions_HeroDescription": "Personalitza l'experiència de Wino Mail.", + "SettingsOptions_AccountsSummary": "{0} compte(s) configurat(s)", + "SettingsSearch_ManageAccounts_Keywords": "Compte;Comptes;Bústia;Bústies;Àlies;Àlies;Perfil;Adreça;Adreces", + "SettingsSearch_AppPreferences_Keywords": "Inici;Segon pla;Llançament;Sincronització;Notificació;Notificacions;Cerca;Safata;Valors predeterminats", + "SettingsSearch_LanguageTime_Keywords": "Idioma;Hora;Rellotge;Localització;Regió;Format;hores de 24;24h", + "SettingsSearch_Personalization_Keywords": "Tema;Fosc;Clar;Aparença;Accent;Color;Color;Mode;Disposició;Densitat", + "SettingsSearch_About_Keywords": "Sobre;Versió;Lloc web;Privacitat;GitHub;Donacions;Botiga;Suport", + "SettingsSearch_KeyboardShortcuts_Keywords": "Drecera;Drecers;Tecla ràpida;Tecles ràpides;Teclat;Tecles", + "SettingsSearch_MessageList_Keywords": "Missatge;Missatges;Llista;Conversacions;Conversacions;Avatar;Previsualització;Remitent", + "SettingsSearch_ReadComposePane_Keywords": "Lector;Redactar;Redactor;Tipus de lletra;Tipus de lletra;Contingut extern;Visualització;Lectura", + "SettingsSearch_SignatureAndEncryption_Keywords": "Signatura;Signatures;Xifratge;Certificat;Certificats;S/MIME;S/MIME;Seguretat", + "SettingsSearch_Storage_Keywords": "Emmagatzematge;Còpia en memòria cau;Emmagatzematge en memòria cau;MIME;Disc;Espai;Neteja;Netejar;Dades locals", + "SettingsSearch_CalendarSettings_Keywords": "Calendari;Setmana;Hores;Horari;Esdeveniment;Esdeveniments", "SettingsPaneLengthReset_Description": "Reset the size of the mail list to original if you have issues with it.", "SettingsPaneLengthReset_Title": "Reset Mail List Size", "SettingsPaypal_Description": "Show much more love ❤️ All donations are appreciated.", @@ -610,6 +893,8 @@ "SettingsPrefer24HourClock_Title": "Display Clock Format in 24 Hours", "SettingsPrivacyPolicy_Description": "Review privacy policy.", "SettingsPrivacyPolicy_Title": "Privacy Policy", + "SettingsWebsite_Description": "Obre el lloc web de Wino Mail.", + "SettingsWebsite_Title": "Lloc web", "SettingsReadComposePane_Description": "Fonts, external content.", "SettingsReadComposePane_Title": "Reader & Composer", "SettingsReader_Title": "Reader", @@ -625,6 +910,19 @@ "SettingsShowPreviewText_Title": "Show Preview Text", "SettingsShowSenderPictures_Description": "Hide/show the thumbnail sender pictures.", "SettingsShowSenderPictures_Title": "Show Sender Avatars", + "SettingsEmailTemplates_Title": "Plantilles de correu", + "SettingsEmailTemplates_Description": "Gestiona plantilles de correu electrònic.", + "SettingsEmailTemplates_CreatePageTitle": "Nova plantilla", + "SettingsEmailTemplates_EditPageTitle": "Edita plantilla", + "SettingsEmailTemplates_NewTemplateTitle": "Nova plantilla", + "SettingsEmailTemplates_NewTemplateDescription": "Crea una nova plantilla de correu electrònic", + "SettingsEmailTemplates_NameTitle": "Nom", + "SettingsEmailTemplates_NamePlaceholder": "Nom de la plantilla", + "SettingsEmailTemplates_DescriptionTitle": "Descripció", + "SettingsEmailTemplates_DescriptionPlaceholder": "Descripció opcional", + "SettingsEmailTemplates_ContentTitle": "Contingut de la plantilla", + "SettingsEmailTemplates_ContentDescription": "Edita el contingut HTML d'aquesta plantilla.", + "SettingsEmailTemplates_NameRequired": "Cal introduir un nom per a la plantilla.", "SettingsEnableGravatarAvatars_Title": "Gravatar", "SettingsEnableGravatarAvatars_Description": "Use gravatar (if available) as sender picture", "SettingsEnableFavicons_Title": "Domain icons (Favicons)", @@ -645,6 +943,33 @@ "SettingsStartupItem_Title": "Startup Item", "SettingsStore_Description": "Show some love ❤️", "SettingsStore_Title": "Rate in Store", + "SettingsStorage_Title": "Emmagatzematge", + "SettingsStorage_Description": "Escaneja i gestiona la memòria cau MIME emmagatzemada a la teva carpeta de dades locals.", + "SettingsStorage_ScanFolder": "Escaneja la carpeta de dades locals.", + "SettingsStorage_NoLocalMimeDataFound": "No s'han trobat dades MIME locals.", + "SettingsStorage_NoAccountsFound": "No s'han trobat comptes.", + "SettingsStorage_TotalUsage": "Ús local total de MIME: {0}", + "SettingsStorage_AccountUsageDescription": "{0} utilitzat en la memòria cau MIME local.", + "SettingsStorage_DeleteAll_Title": "Elimina tot el contingut MIME", + "SettingsStorage_DeleteAll_Description": "Esborrar tota la carpeta de la memòria cau MIME d'aquest compte.", + "SettingsStorage_DeleteAll_Button": "Elimina tot", + "SettingsStorage_DeleteAll_Confirm_Title": "Elimina tot el contingut MIME", + "SettingsStorage_DeleteAll_Confirm_Message": "Esborrar dades MIME locals per a {0}?", + "SettingsStorage_DeleteAll_Success": "Tot el contingut MIME s'ha eliminat.", + "SettingsStorage_DeleteOld_Title": "Esborrar contingut MIME vell", + "SettingsStorage_DeleteOld_Description": "Esborrar fitxers MIME segons la data de creació del correu electrònic a la base de dades local.", + "SettingsStorage_DeleteOld_1Month": "> 1 mes", + "SettingsStorage_DeleteOld_3Months": "> 3 mesos", + "SettingsStorage_DeleteOld_6Months": "> 6 mesos", + "SettingsStorage_DeleteOld_1Year": "> 1 any", + "SettingsStorage_DeleteOld_Confirm_Title": "Esborrar contingut MIME vell", + "SettingsStorage_DeleteOld_Confirm_Message": "Esborrar les dades MIME locals més antigues que {0} per a {1}?", + "SettingsStorage_DeleteOld_Success": "S'han eliminat {0} carpeta(s) MIME més antigues que {1}.", + "SettingsStorage_1Month": "1 mes", + "SettingsStorage_3Months": "3 mesos", + "SettingsStorage_6Months": "6 mesos", + "SettingsStorage_1Year": "1 any", + "SettingsStorage_Months": "{0} mesos", "SettingsTaskbarBadge_Description": "Include unread mail count in taskbar icon.", "SettingsTaskbarBadge_Title": "Taskbar Badge", "SettingsThreads_Description": "Organize messages into conversation threads.", @@ -683,6 +1008,9 @@ "SystemFolderConfigDialogValidation_InboxSelected": "You can't assign Inbox folder to any other system folder.", "SystemFolderConfigSetupSuccess_Message": "System folders are successfully configured.", "SystemFolderConfigSetupSuccess_Title": "System Folders Setup", + "SystemTrayMenu_ShowWino": "Obre Wino Mail", + "SystemTrayMenu_ShowWinoCalendar": "Obre Wino Calendar", + "SystemTrayMenu_ExitWino": "Surt", "TestingImapConnectionMessage": "Testing server connection...", "TitleBarServerDisconnectedButton_Description": "Wino is disconnected from the network. Click reconnect to restore connection.", "TitleBarServerDisconnectedButton_Title": "no connection", @@ -699,8 +1027,422 @@ "WinoUpgradeMessage": "Upgrade to Unlimited Accounts", "WinoUpgradeRemainingAccountsMessage": "{0} out of {1} free accounts used.", "Yesterday": "Yesterday", + "Smime_ImportCertificates_Success": "Certificats importats amb èxit.", + "Smime_ImportCertificates_Error": "Error en importar certificats: {0}", + "Smime_RemoveCertificates_Confirm": "Voleu realment eliminar els certificats {0}?", + "Smime_RemoveCertificates_Success": "Certificats eliminats.", + "Smime_ExportCertificates_Success": "Certificats exportats.", + "Smime_ExportCertificates_Error": "Error en exportar certificats.", + "Smime_CertificateDetails": "Subjecte: {0}\nEmissor: {1}\nVàlid des de: {2}\nVàlid fins a: {3}\nEmpremta: {4}", + "Smime_CertificatePassword_Title": "Contrasenya del certificat obligatòria", + "Smime_CertificatePassword_Placeholder": "Contrasenya del certificat per a {0} (opcional)", + "Smime_Confirm_Title": "Confirma", + "Buttons_OK": "D'acord", + "Buttons_Refresh": "Actualitza", + "SettingsSignatureAndEncryption_Title": "Signatura i xifrat", + "SettingsSignatureAndEncryption_Description": "Gestioneu els certificats S/MIME per signar i xifrar correus electrònics.", + "SettingsSignatureAndEncryption_MyCertificatesHeader": "Els meus certificats", + "SettingsSignatureAndEncryption_MyCertificatesDescription": "Certificats personals per signar i xifrar", + "SettingsSignatureAndEncryption_RecipientCertificatesHeader": "Certificats dels destinataris", + "SettingsSignatureAndEncryption_RecipientCertificatesDescription": "Certificats dels destinataris per desxifrar", + "SettingsSignatureAndEncryption_NameColumn": "Nom", + "SettingsSignatureAndEncryption_ExpiresColumn": "Data d'expiració", + "SettingsSignatureAndEncryption_ThumbprintColumn": "Empremta", + "Buttons_Remove": "Elimina", + "Buttons_Export": "Exporta", + "Buttons_Import": "Importa", + "SettingsSignatureAndEncryption_SigningCertificate": "Certificat de signatura S/MIME", + "SettingsSignatureAndEncryption_EncryptionCertificate": "Xifrat S/MIME", + "SettingsSignatureAndEncryption_SigningCertificatePlaceholder": "Cap", + "SmimeSignaturesInMessage": "Signatures en aquest missatge:", + "SmimeSignatureEntry": "• {0} {1} ({2}, vàlid fins a {3} - {4})", + "SmimeSigningCertificateInfoTitle": "Informació del certificat de signatura S/MIME", + "SmimeCertificateInfoTitle": "Informació del certificat S/MIME", + "SmimeNoCertificateFileFound": "No s'ha trobat cap fitxer de certificat", + "SmimeSaveCertificate": "Desa el certificat...", + "SmimeCertificate": "Certificat S/MIME", + "SmimeCertificateSavedTo": "Certificat desat a {0}", + "SmimeSignedTooltip": "Aquest missatge està signat amb un certificat S/MIME. Fes clic per a més detalls", + "SmimeEncryptedTooltip": "Aquest missatge està xifrat amb un certificat S/MIME.", + "SmimeCertificateFileInfo": "Fitxer: {0}\nTipus: {1}\nMida: {2:N0} bytes", + "Composer_LightTheme": "Tema clar", + "Composer_DarkTheme": "Tema fosc", + "Composer_Outdent": "Disminueix la sangria", + "Composer_Indent": "Sangria", + "Composer_BulletList": "Llista amb punts", + "Composer_OrderedList": "Llista numerada", + "Composer_Stroke": "Traç", + "Composer_Bold": "Negreta", + "Composer_Italic": "Cursiva", + "Composer_Underline": "Subratllat", + "Composer_CcBcc": "Cc i Còpia oculta", + "Composer_EnableSmimeSignature": "Activa/desactiva la signatura S/MIME", + "Composer_EnableSmimeEncryption": "Activa/desactiva l'encriptació S/MIME", + "Composer_LocalDraftSyncInfo": "Aquest esborrany és només local. Wino no va poder enviar-lo al vostre servidor de correu. Feu clic per tornar a intentar enviar-lo al servidor.", + "Composer_CertificateExpires": "Expira el: ", + "Composer_SmimeSignature": "Signatura S/MIME", + "Composer_SmimeEncryption": "Xifrat S/MIME", + "Composer_EmailTemplatesPlaceholder": "Plantilles d'e-mails", + "Composer_AiSummarize": "Resumir amb IA", + "Composer_AiSummarizeDescription": "Extreu punts clau, accions i decisions d'aquest correu electrònic.", + "Composer_AiTranslate": "Tradueix amb IA", + "Composer_AiActions": "Accions d'IA", + "Composer_AiRewrite": "Reescriu amb IA", + "AiActions_CheckingStatus": "Comprovant l'accés a IA...", + "AiActions_SignedOutTitle": "Desbloqueja el paquet IA de Wino", + "AiActions_SignedOutDescription": "Traduïu, reescriviu i resumi correus electrònics amb IA després d'iniciar sessió al teu compte Wino i activar l'extensió AI Pack.", + "AiActions_NoPackTitle": "Cal l'AI Pack", + "AiActions_NoPackDescription": "Estàs connectat, però l'AI Pack encara no està actiu. Adquireix-lo per utilitzar les eines de traducció, reescriptura i resum amb IA de Wino.", + "AiActions_UsageSummary": "{0} de {1} crèdits utilitzats aquest mes.", + "Composer_AiRewritePolite": "Fes-ho més educat", + "Composer_AiRewritePoliteDescription": "Suavitza la redacció mantenint la mateixa intenció.", + "Composer_AiRewriteAngry": "Fes-ho enfadat", + "Composer_AiRewriteAngryDescription": "Utilitza un to més contundent i confrontador.", + "Composer_AiRewriteHappy": "Fes-ho feliç", + "Composer_AiRewriteHappyDescription": "Afegeix un to més optimista i entusiasta.", + "Composer_AiRewriteFormal": "Fes-ho formal", + "Composer_AiRewriteFormalDescription": "Fa que el missatge soni més professional i estructurat.", + "Composer_AiRewriteFriendly": "Fes-ho amigable", + "Composer_AiRewriteFriendlyDescription": "Escalfa el missatge amb un to més proper.", + "Composer_AiRewriteShorter": "Fes-ho més breu", + "Composer_AiRewriteShorterDescription": "Redueix el text i elimina detalls innecessaris.", + "Composer_AiRewriteClearer": "Fes-ho més clar", + "Composer_AiRewriteClearerDescription": "Millora la llegibilitat i facilita la comprensió del missatge.", + "Composer_AiRewriteCustom": "Personalitzat", + "Composer_AiRewriteCustomDescription": "Descriu la teva intenció de reescriptura.", + "Composer_AiRewriteCustomPlaceholder": "Descriu com vols que el missatge sigui reescrit", + "Composer_AiRewriteMode": "Tonalitat de reescriptura", + "Composer_AiRewriteApply": "Aplica la reescriptura", + "Composer_AiTranslateDialogTitle": "Tradueix amb IA", + "Composer_AiTranslateDialogDescription": "Introdueix la llengua o el codi de cultura de destinació, com ara en-US, tr-TR, de-DE o fr-FR.", + "Composer_AiTranslateApply": "Tradueix", + "Composer_AiTranslateLanguage": "Llengua de destinació", + "Composer_AiTranslateCustomPlaceholder": "Introdueix el codi de cultura", + "Composer_AiTranslateLanguageEnglish": "Anglès (en-US)", + "Composer_AiTranslateLanguageTurkish": "Turc (tr-TR)", + "Composer_AiTranslateLanguageGerman": "Alemany (de-DE)", + "Composer_AiTranslateLanguageFrench": "Francès (fr-FR)", + "Composer_AiTranslateLanguageSpanish": "Espanyol (es-ES)", + "Composer_AiTranslateLanguageItalian": "Italià (it-IT)", + "Composer_AiTranslateLanguagePortugueseBrazil": "Portuguès (Brasil) (pt-BR)", + "Composer_AiTranslateLanguageDutch": "neerlandès (nl-NL)", + "Composer_AiTranslateLanguagePolish": "Polonès (pl-PL)", + "Composer_AiTranslateLanguageRussian": "Rus (ru-RU)", + "Composer_AiTranslateLanguageJapanese": "Japonès (ja-JP)", + "Composer_AiTranslateLanguageKorean": "coreà (ko-KR)", + "Composer_AiTranslateLanguageChineseSimplified": "Xinès, Simplificat (zh-CN)", + "Composer_AiTranslateLanguageArabic": "Àrab (ar-SA)", + "Composer_AiTranslateLanguageHindi": "Hindi (hi-IN)", + "Composer_AiTranslateLanguageOther": "Altres...", + "Composer_AiBusyTitle": "L'IA ja està treballant.", + "Composer_AiBusyMessage": "Si us plau, espereu que s'acabi l'acció actual de IA.", + "Composer_AiSignInRequired": "Inicia sessió al teu compte Wino per utilitzar les funcions de IA.", + "Composer_AiMissingHtml": "Encara no hi ha contingut de missatge per enviar a Wino IA.", + "Composer_AiQuotaUnavailable": "El resultat de IA s'ha aplicat.", + "Composer_AiAppliedMessage": "El resultat de IA s'ha aplicat al compositor. Utilitza Desfer si vols revertir-ho.", + "Composer_AiSummarizeSuccessTitle": "Resum de IA aplicat", + "Composer_AiTranslateSuccessTitle": "Traducció de IA aplicada", + "Composer_AiRewriteSuccessTitle": "Reescriptura amb IA aplicada", + "Composer_AiErrorTitle": "L'acció de IA ha fallat.", + "Reader_AiAppliedMessage": "El resultat de la IA ara s'ha mostrat per a aquest missatge. Reobre el missatge per veure de nou el contingut original.", "SettingsAppPreferences_EmailSyncInterval_Title": "Email sync interval", - "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail." + "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail.", + "ContactsPage_Title": "Contactes", + "ContactsPage_AddContact": "Afegeix contacte", + "ContactsPage_EditContact": "Edita contacte", + "ContactsPage_DeleteContact": "Elimina contacte", + "ContactsPage_SearchPlaceholder": "Cerca contactes...", + "ContactsPage_NoContacts": "No s'han trobat contactes.", + "ContactsPage_ContactsCount": "{0} contactes", + "ContactsPage_SelectedContactsCount": "{0} seleccionats", + "ContactsPage_DeleteSelectedContacts": "Elimina els seleccionats", + "ContactEditDialog_Title": "Edita contacte", + "ContactEditDialog_PhotoSection": "Foto", + "ContactEditDialog_ChoosePhoto": "Selecciona foto", + "ContactEditDialog_RemovePhoto": "Elimina foto", + "ContactEditDialog_NameHeader": "Nom", + "ContactEditDialog_NamePlaceholder": "Nom del contacte", + "ContactEditDialog_EmailHeader": "Adreça de correu electrònic", + "ContactEditDialog_EmailPlaceholder": "contact@example.com", + "ContactEditDialog_InfoSection": "Informació de contacte", + "ContactEditDialog_RootContactInfo": "Aquest és un contacte arrel associat amb els teus comptes i no es pot eliminar.", + "ContactEditDialog_OverriddenContactInfo": "Aquest contacte s'ha modificat manualment i no s'actualitzarà durant la sincronització.", + "ContactsPage_Subtitle": "Gestioneu els vostres contactes de correu electrònic i la seva informació", + "ContactStatus_Account": "Compte", + "ContactStatus_Modified": "Modificat", + "ContactAction_Edit": "Edita contacte", + "ContactAction_ChangePhoto": "Canvia la foto", + "ContactAction_Delete": "Elimina contacte", + "ContactAction_Add": "Afegeix contacte", + "ContactSelection_Selected": "seleccionat", + "ContactSelection_SelectAll": "Selecciona tot", + "ContactSelection_Clear": "Netejar", + "ContactsPage_EmptyState": "No hi ha contactes per mostrar", + "ContactsPage_AddFirstContact": "Afegeix el teu primer contacte", + "ContactsPage_ContactsCountSuffix": "contactes", + "ContactsPane_NewContact": "Nou contacte", + "ContactsPane_DescriptionTitle": "Gestioneu els vostres contactes", + "ContactsPane_DescriptionBody": "Crea contactes, renombra'ls, actualitza les fotos de perfil i mantingueu les dades guardades organitzades en un lloc.", + "ContactEditDialog_AddTitle": "Afegeix contacte", + "ContactInfoBar_ContactAdded": "Contacte afegit amb èxit.", + "ContactInfoBar_ContactUpdated": "Contacte actualitzat amb èxit.", + "ContactInfoBar_ContactsDeleted": "Contactes eliminats amb èxit.", + "ContactInfoBar_ContactPhotoUpdated": "La foto del contacte s'ha actualitzat amb èxit.", + "ContactInfoBar_FailedToLoadContacts": "Error en carregar contactes: {0}", + "ContactInfoBar_FailedToAddContact": "Error en afegir contacte: {0}", + "ContactInfoBar_FailedToUpdateContact": "Error en actualitzar contacte: {0}", + "ContactInfoBar_FailedToDeleteContacts": "Error en eliminar contactes: {0}", + "ContactInfoBar_FailedToUpdatePhoto": "Error en actualitzar la foto: {0}", + "ContactInfoBar_CannotDeleteRoot": "No es poden eliminar contactes arrel.", + "ContactConfirmDialog_DeleteTitle": "Elimina contacte", + "ContactConfirmDialog_DeleteMessage": "Estàs segur d'eliminar el contacte '{0}'?", + "ContactConfirmDialog_DeleteMultipleMessage": "Estàs segur d'eliminar {0} contacte(s)?", + "ContactConfirmDialog_DeleteButton": "Elimina", + "CalendarAccountSettings_Title": "Configuració del compte de calendari", + "CalendarAccountSettings_Description": "Gestioneu la configuració del calendari per a {0}", + "CalendarAccountSettings_AccountColor": "Color del compte", + "CalendarAccountSettings_AccountColorDescription": "Canvia el color de visualització per a aquest compte de calendari.", + "CalendarAccountSettings_SyncEnabled": "Activa la sincronització", + "CalendarAccountSettings_SyncEnabledDescription": "Activa o desactiva la sincronització del calendari per a aquest compte.", + "CalendarAccountSettings_DefaultShowAs": "Estat de disponibilitat per defecte", + "CalendarAccountSettings_DefaultShowAsDescription": "Estat de disponibilitat per defecte per als nous esdeveniments creats amb aquest compte.", + "CalendarAccountSettings_PrimaryCalendar": "Calendari principal", + "CalendarAccountSettings_PrimaryCalendarDescription": "Marca aquest calendari com a calendari principal del compte.", + "CalendarSettings_NewEventBehavior_Header": "Comportament del botó Nou Esdeveniment", + "CalendarSettings_NewEventBehavior_Description": "Tria si el botó Nou Esdeveniment ha de demanar un calendari cada vegada o obrir sempre un calendari específic.", + "CalendarSettings_NewEventBehavior_AskEachTime": "Demana-ho a cada vegada.", + "CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar": "Sempre utilitza un calendari específic.", + "CalendarSettings_Rendering_Title": "Representació", + "CalendarSettings_Rendering_Description": "Configura la disposició del calendari i el comportament de visualització.", + "CalendarSettings_Notifications_Title": "Notificacions", + "CalendarSettings_Notifications_Description": "Tria el recordatori per defecte i el comportament de posposar.", + "CalendarSettings_Preferences_Title": "Preferències", + "CalendarSettings_Preferences_Description": "Defineix com s'ha de comportar el botó Nou Esdeveniment.", + "WhatIsNew_GetStartedButton": "Comença", + "WhatIsNew_ContinueAnywayButton": "Continuar de totes maneres", + "WhatIsNew_PreparingForNewVersionButton": "Preparant la nova versió...", + "WhatIsNew_MigrationPreparing_Title": "Preparant les teves dades", + "WhatIsNew_MigrationPreparing_Description": "Wino està aplicant migracions de l'actualització. Si us plau espera mentre preparem les dades del teu compte per a aquesta versió.", + "WhatIsNew_MigrationFailedMessage": "La migració ha fallat amb el codi d'error {0}. Podeu continuar fent servir l'aplicació. No obstant això, si us trobeu amb problemes greus, torneu a instal·lar l'aplicació.", + "WhatIsNew_MigrationNotification_Title": "Wino Mail Actualitzat", + "WhatIsNew_MigrationNotification_Message": "Obriu l'aplicació per completar la actualització i veure les novetats.", + "WelcomeWindow_Title": "Benvingut a Wino Mail", + "WelcomeWindow_Subtitle": "Una experiència nativa de Windows per a Correu i Calendari.", + "WelcomeWindow_WhatsNewTitle": "Últims canvis", + "WelcomeWindow_FeaturesTitle": "Característiques", + "WelcomeWindow_WhatsNewTab": "Què hi ha de nou", + "WelcomeWindow_FeaturesTab": "Característiques", + "WelcomeWindow_GetStartedButton": "Comença afegint un compte", + "WelcomeWindow_GetStartedDescription": "Afegeix el teu compte de Outlook, Gmail o IMAP per començar a utilitzar Wino Mail.", + "WelcomeWindow_ImportFromWinoAccount": "Importa des del teu compte Wino.", + "WelcomeWindow_ImportInProgress": "S'importen les teves preferències i comptes sincronitzats.", + "WelcomeWindow_ImportNoAccountsFound": "No s'han trobat comptes sincronitzats al teu compte Wino. Si hi havia preferències disponibles, s'han restaurat. Utilitza Comença per afegir un compte manualment.", + "WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} comptes sincronitzats ja estan disponibles en aquest dispositiu. Utilitza Comença per afegir un altre compte manualment si és necessari.", + "WelcomeWindow_SetupTitle": "Configura el teu compte", + "WelcomeWindow_SetupSubtitle": "Tria el teu proveïdor de correu per començar", + "WelcomeWindow_AddAccountButton": "Afegir compte", + "WelcomeWindow_SkipForNow": "Ignora per ara — el configuraré més tard", + "WelcomeWindow_AppDescription": "Una bústia d'entrada ràpida i enfocada, redissenyada per a Windows 11", + "WelcomeWizard_Step1Title": "Benvingut", + "SystemTrayMenu_Open": "Obrir", + "WinoAccount_Titlebar_SyncBenefitTitle": "Configuració de sincronització", + "WinoAccount_Titlebar_SyncBenefitDescription": "Mantingues les teves preferències de Wino sincronitzades entre dispositius.", + "WinoAccount_Titlebar_AddonsBenefitTitle": "Desbloqueja complements", + "WinoAccount_Titlebar_AddonsBenefitDescription": "Accedeix a funcions premium com Wino AI Pack.", + "WinoAccount_Management_Description": "Gestiona el teu compte Wino, l'accés a Wino AI Pack i les preferències i detalls del compte sincronitzats.", + "WinoAccount_Management_SignedOutTitle": "Inicia sessió a Wino Mail", + "WinoAccount_Management_SignedOutDescription": "Inicia sessió o crea un compte per sincronitzar el teu correu, accedir a les funcions d'IA i gestionar la teva configuració entre dispositius.", + "WinoAccount_Management_ProfileSectionHeader": "Perfil", + "WinoAccount_Management_AddOnsSectionHeader": "Extencions Wino", + "WinoAccount_Management_DataSectionHeader": "Dades", + "WinoAccount_Management_AccountActionsSectionHeader": "Accions de compte", + "WinoAccount_Management_AccountCardTitle": "Compte", + "WinoAccount_Management_AccountCardDescription": "La direcció de correu del teu compte Wino i l'estat actual del compte.", + "WinoAccount_Management_AiPackCardTitle": "AI Pack", + "WinoAccount_Management_AiPackCardDescription": "Ves si Wino AI Pack està actiu i quanta utilització en queda.", + "WinoAccount_Management_AiPackActive": "AI Pack està actiu", + "WinoAccount_Management_AiPackInactive": "AI Pack no està actiu", + "WinoAccount_Management_AiPackUsage": "{0} de {1} usos consumits. {2} restants.", + "WinoAccount_Management_AiPackBillingPeriod": "Període de facturació: {0:d} - {1:d}", + "WinoAccount_Management_AiPackUnknownUsage": "Detalls d'ús encara no disponibles.", + "WinoAccount_Management_AiPackBuyDescription": "Compra Wino AI Pack per traduir, reescriure o resumir correus amb IA.", + "WinoAccount_Management_AiPackPromoTitle": "Desbloqueja Wino AI Pack", + "WinoAccount_Management_AiPackPromoDescription": "Dinamitza el flux de correu amb eines impulsades per IA. Traduïu missatges a més de 50 idiomes, reescriu per a claredat i to, i obtén resums immediats de fils llargs.", + "WinoAccount_Management_AiPackPromoPrice": "$4.99 / mes", + "WinoAccount_Management_AiPackPromoRequests": "1.000 crèdits", + "WinoAccount_Management_AiPackGetButton": "Obtenir AI Pack", + "WinoAddOn_AI_PACK_Name": "Wino AI Pack", + "WinoAddOn_AI_PACK_Description": "Eines alimentades per IA per traduir, reformular i resumir accions a Wino Mail.", + "WinoAddOn_AI_PACK_Keywords": "IA, traducció, reescriptura, resum, productivitat", + "WinoAddOn_UNLIMITED_ACCOUNTS_Name": "Comptes il·limitats", + "WinoAddOn_UNLIMITED_ACCOUNTS_Description": "Elimina la limitació de comptes i afegeix tants comptes de correu com necessitis.", + "WinoAddOn_UNLIMITED_ACCOUNTS_Keywords": "comptes, il·limitats, premium, complement", + "WinoAccount_Management_PurchaseRequiresSignIn": "Inicia sessió amb el teu compte Wino per completar aquesta compra.", + "WinoAccount_Management_PurchaseStartFailed": "No s'ha pogut completar aquesta compra a Microsoft Store.", + "WinoAccount_Management_StoreSyncFailed": "La compra s'ha completat, però Wino no ha pogut actualitzar encara els avantatges del teu compte. Prova-ho de nou en un moment.", + "WinoAccount_Management_AiPackSubscriptionActive": "La teva subscripció està activa.", + "WinoAccount_Management_AiPackRenews": "Renova {0:d}", + "WinoAccount_Management_AiPackRequestsUsed": "Crèdits utilitzats aquest mes", + "WinoAccount_Management_AiPackResets": "Restabliments {0:d}", + "WinoAccount_Management_AiPackUsageLoadFailed": "Hem tingut problemes en carregar l'equilibri d'ús d'IA.", + "WinoAccount_Management_AiPackFeatureTranslate": "Tradueix", + "WinoAccount_Management_AiPackFeatureRewrite": "Reescriu", + "WinoAccount_Management_AiPackFeatureSummarize": "Resum", + "WinoAccount_Management_AddOnLoadFailed": "Hem tingut problemes en carregar aquest complement.", + "WinoAccount_Management_SyncPreferencesTitle": "Sincronitza Preferències i Comptes", + "WinoAccount_Management_SyncPreferencesDescription": "Importa o exporta les teves preferències de Wino i els detalls de la bústia entre dispositius. Contrasenyes, tokens i altra informació sensible mai no s'intercanvien.", + "WinoAccount_Management_SignOutTitle": "Tanca la sessió", + "WinoAccount_Management_SignOutDescription": "Tanca la sessió del teu compte en aquest dispositiu.", + "WinoAccount_Management_StatusLabel": "Estat: {0}", + "WinoAccount_Management_NoRemoteSettings": "Encara no hi ha dades sincronitzades emmagatzemades per a aquest compte.", + "WinoAccount_Management_ExportSucceeded": "Les dades de Wino que has seleccionat s'han exportat correctament.", + "WinoAccount_Management_ExportPreferencesSucceeded": "Les teves preferències s'han exportat al teu compte Wino.", + "WinoAccount_Management_ExportAccountsSucceeded": "Exportats {0} detalls de compte al teu compte Wino.", + "WinoAccount_Management_ImportSucceeded": "Dades sincronitzades importades del teu compte Wino.", + "WinoAccount_Management_ImportPreferencesSucceeded": "Aplicades {0} preferències sincronitzades.", + "WinoAccount_Management_ImportAccountsSucceeded": "S'han importat {0} comptes.", + "WinoAccount_Management_ImportDuplicateAccountsSkipped": "S'han omès {0} comptes que ja existeixen en aquest dispositiu.", + "WinoAccount_Management_ImportPartial": "S'han aplicat {0} preferències sincronitzades. {1} preferències no s'han pogut restaurar.", + "WinoAccount_Management_ImportReloginReminder": "Les contrasenyes, tokens i altra informació sensible no s'han importat. Inicia sessió de nou per a cada compte en aquest dispositiu abans d'utilitzar-lo.", + "WinoAccount_Management_SerializeFailed": "No s'han pogut serialitzar les teves preferències actuals.", + "WinoAccount_Management_EmptyExport": "No hi ha valors de preferències per exportar.", + "WinoAccount_Management_ImportEmpty": "La càrrega de dades sincronitzades no conté res de nou per restaurar.", + "WinoAccount_Management_ExportDialog_Title": "Exporta al teu compte Wino.", + "WinoAccount_Management_ExportDialog_Description": "Tria què vols sincronitzar al teu compte Wino.", + "WinoAccount_Management_ExportDialog_IncludePreferences": "Preferències", + "WinoAccount_Management_ExportDialog_IncludeAccounts": "Comptes", + "WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Contrasenyes, tokens i altra informació sensible no es sincronitzen.", + "WinoAccount_Management_ExportDialog_AccountsRelogin": "Els comptes importats en un altre PC encara necessitaran que inicis sessió de nou abans de poder utilitzar-los.", + "WinoAccount_Management_ExportDialog_InProgress": "S'està exportant les teves dades de Wino seleccionades.", + "WinoAccount_Management_LoadFailed": "No s'ha pogut carregar la darrera informació del compte Wino.", + "WinoAccount_Management_ActionFailed": "La sol·licitud del compte Wino no s'ha pogut completar.", + "WinoAccount_SettingsSection_Title": "Compte Wino", + "WinoAccount_SettingsSection_Description": "Crea o inicia sessió en un compte Wino utilitzant el teu servei d'autenticació local.", + "WinoAccount_RegisterButton_Title": "Registra un compte", + "WinoAccount_RegisterButton_Description": "Crea un compte Wino amb correu electrònic i contrasenya.", + "WinoAccount_RegisterButton_Action": "Obre el registre", + "WinoAccount_LoginButton_Title": "Inicia sessió", + "WinoAccount_LoginButton_Description": "Inicia sessió en un compte Wino existent amb correu electrònic i contrasenya.", + "WinoAccount_LoginButton_Action": "Obre l'inici de sessió", + "WinoAccount_SignOutButton_Title": "Tanca sessió", + "WinoAccount_SignOutButton_Description": "Elimina la sessió del compte Wino emmagatzemada localment.", + "WinoAccount_SignOutButton_Action": "Tanca sessió", + "WinoAccount_RegisterDialog_Title": "Crea un compte Wino", + "WinoAccount_RegisterDialog_Description": "Crea un compte Wino per mantenir la teva experiència Wino sincronitzada i desbloquejar complements basats en el compte.", + "WinoAccount_RegisterDialog_HeroTitle": "Crea el teu compte Wino.", + "WinoAccount_RegisterDialog_BenefitsTitle": "Per què crear-ne un?", + "WinoAccount_RegisterDialog_BenefitSyncTitle": "Importa i exporta la configuració entre dispositius", + "WinoAccount_RegisterDialog_BenefitSyncDescription": "Mou les teves preferències de Wino entre dispositius sense haver de tornar a configurar-ho tot des de zero.", + "WinoAccount_RegisterDialog_BenefitAiTitle": "Accedeix a complements exclusius com Wino AI Pack (de pagament)", + "WinoAccount_RegisterDialog_BenefitAiDescription": "Utilitza un compte per desbloquejar les funcions premium de Wino a mesura que estiguin disponibles.", + "WinoAccount_RegisterDialog_DifferenceTitle": "El compte Wino és separat dels teus comptes de correu.", + "WinoAccount_RegisterDialog_DifferenceDescription": "Els teus comptes de Outlook, Gmail, IMAP o altres comptes de correu romanen tal com són. Un compte Wino només gestiona característiques i complements específics de Wino.", + "WinoAccount_RegisterDialog_PrimaryButton": "Registra", + "WinoAccount_RegisterDialog_PrivacyTitle": "Privacitat i processament de l'API", + "WinoAccount_RegisterDialog_PrivacyDescription": "Els complements opcionals com Wino AI Pack poden enviar contingut HTML de correu electrònic seleccionat al servei API de Wino només quan utilitzis aquestes funcions.", + "WinoAccount_RegisterDialog_PrivacyLinkText": "Llegeix la política de privacitat.", + "WinoAccount_RegisterDialog_PrivacyCheckbox": "Estic d'acord amb la política de privacitat.", + "WinoAccount_LoginDialog_Title": "Inicia sessió al compte Wino.", + "WinoAccount_LoginDialog_Description": "Inicia sessió en el teu compte Wino per sincronitzar la configuració de Wino i accedir a funcions basades en el compte.", + "WinoAccount_LoginDialog_HeroTitle": "Ben tornat", + "WinoAccount_LoginDialog_BenefitsTitle": "Quines avantatges té iniciar sessió.", + "WinoAccount_LoginDialog_BenefitsDescription": "Utilitza el teu compte Wino per continuar sincronitzant la configuració entre dispositius i accedir a complements de pagament com Wino AI Pack.", + "WinoAccount_LoginDialog_DifferenceTitle": "Això no és l'inici de sessió de la teva bústia de correu.", + "WinoAccount_LoginDialog_DifferenceDescription": "Iniciar sessió aquí no afegeix ni substitueix els teus comptes de Outlook, Gmail o IMAP en Wino. Només inicia sessió en serveis específics de Wino.", + "WinoAccount_LoginDialog_ForgotPasswordLink": "Has oblidat la contrasenya?", + "WinoAccount_EmailLabel": "Correu electrònic", + "WinoAccount_EmailPlaceholder": "name@example.com", + "WinoAccount_PasswordLabel": "Contrasenya", + "WinoAccount_ConfirmPasswordLabel": "Confirma la contrasenya.", + "WinoAccount_ForgotPasswordDialog_Title": "Restableix la teva contrasenya.", + "WinoAccount_ForgotPasswordDialog_PrimaryButton": "Envia un correu electrònic de restabliment.", + "WinoAccount_ForgotPasswordDialog_BackToSignIn": "Torna a iniciar sessió", + "WinoAccount_ForgotPasswordDialog_Description": "Introdueix l'adreça de correu electrònic del teu compte Wino i t'enviem un enllaç de restabliment de contrasenya si l'adreça està registrada.", + "WinoAccount_Validation_EmailRequired": "L'adreça de correu electrònic és obligatòria.", + "WinoAccount_Validation_PasswordRequired": "La contrasenya és obligatòria.", + "WinoAccount_Validation_PasswordMismatch": "Les contrasenyes no coincideixen.", + "WinoAccount_Validation_PrivacyConsentRequired": "Has d'acceptar la política de privacitat abans de crear un compte Wino.", + "WinoAccount_Error_InvalidCredentials": "L'adreça de correu electrònic o la contrasenya són incorrectes.", + "WinoAccount_Error_AccountLocked": "Aquest compte està temporalment bloquejat.", + "WinoAccount_Error_AccountBanned": "Aquest compte ha estat bloquejat.", + "WinoAccount_Error_AccountSuspended": "Aquest compte ha estat suspès.", + "WinoAccount_Error_EmailNotConfirmed": "Si us plau, confirma la teva adreça de correu electrònic abans d'iniciar sessió.", + "WinoAccount_Error_EmailConfirmationRequired": "Si us plau, confirma la teva adreça de correu electrònic abans d'iniciar sessió.", + "WinoAccount_Error_EmailConfirmationResendNotAvailable": "Encara no està disponible un nou correu de confirmació.", + "WinoAccount_Error_EmailConfirmationResendInvalid": "Aquesta sol·licitud de confirmació ja no és vàlida. Prova d'iniciar sessió de nou.", + "WinoAccount_Error_EmailNotRegistered": "Aquesta adreça de correu electrònic no està registrada.", + "WinoAccount_Error_RefreshTokenInvalid": "La teva sessió ja no és vàlida. Si us plau, inicia sessió de nou.", + "WinoAccount_Error_EmailAlreadyRegistered": "Aquesta adreça de correu electrònic ja està registrada.", + "WinoAccount_Error_ExternalLoginEmailRequired": "Cal una adreça de correu electrònic per completar l'inici de sessió extern.", + "WinoAccount_Error_ExternalLoginInvalid": "La sol·licitud d'inici de sessió externa és invàlida.", + "WinoAccount_Error_ExternalAuthStateInvalid": "L'estat d'inici de sessió externa és invàlid o caducat.", + "WinoAccount_Error_ExternalAuthCodeInvalid": "El codi d'inici de sessió externa és invàlid o caducat.", + "WinoAccount_Error_AiPackRequired": "Cal una subscripció activa de Wino AI Pack per a aquesta acció.", + "WinoAccount_Error_AiQuotaExceeded": "El límit d'ús de Wino AI Pack s'ha assolit per al període de facturació actual.", + "WinoAccount_Error_AiHtmlEmpty": "No hi ha contingut de correu electrònic per processar.", + "WinoAccount_Error_AiHtmlTooLarge": "Aquest correu és massa gran per processar amb Wino AI.", + "WinoAccount_Error_AiUnsupportedLanguage": "Aquest idioma no és compatible. Prova un codi de cultura vàlid com en-US o tr-TR.", + "WinoAccount_Error_Forbidden": "No tens permisos per realitzar aquesta acció.", + "WinoAccount_Error_ValidationFailed": "La sol·licitud és invàlida. Si us plau, revisa els valors introduïts.", + "WinoAccount_RegisterSuccessMessage": "El registre del compte Wino s'ha completat per a {0}.", + "WinoAccount_LoginSuccessMessage": "Inici de sessió al compte Wino com {0}.", + "WinoAccount_EmailConfirmationSentDialog_Title": "Confirma la teva adreça de correu electrònic.", + "WinoAccount_EmailConfirmationSentDialog_Message": "Hem enviat una confirmació per correu a {0}. Confirma-la i torna a iniciar sessió.", + "WinoAccount_EmailConfirmationPendingDialog_Title": "Cal confirmar l'adreça de correu electrònic.", + "WinoAccount_EmailConfirmationPendingDialog_Message": "Encara esperem que confirmis {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ResendButton": "Reenvia el correu de confirmació.", + "WinoAccount_EmailConfirmationPendingDialog_Countdown": "Pots tornar a enviar el correu de confirmació en {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ReadyToResend": "Ara pots tornar a enviar el correu de confirmació.", + "WinoAccount_EmailConfirmationResentDialog_Title": "Correu de confirmació reenviat.", + "WinoAccount_EmailConfirmationResentDialog_Message": "Hem enviat un altre correu de confirmació a {0}. Si us plau, confirma'l i torna a iniciar sessió.", + "WinoAccount_ForgotPasswordDialog_SuccessTitle": "S'ha enviat un correu de restabliment de contrasenya.", + "WinoAccount_ForgotPasswordDialog_SuccessMessage": "Hem enviat un correu de restabliment de contrasenya a {0}. Obre aquest missatge per triar una nova contrasenya.", + "WinoAccount_ChangePassword_Title": "Canvia la contrasenya.", + "WinoAccount_ChangePassword_Description": "Envia un correu de restabliment de contrasenya per a aquest compte Wino.", + "WinoAccount_ChangePassword_Action": "Envia el correu de restabliment", + "WinoAccount_ChangePassword_ConfirmationMessage": "Voleu que Wino enviï un correu electrònic de restabliment de contrasenya a {0}?", + "WinoAccount_SignOut_SuccessMessage": "S'ha tancat la sessió de Wino Account {0}.", + "WinoAccount_SignOut_NoAccountMessage": "No hi ha cap compte Wino actiu per desconnectar.", + "WinoAccount_Titlebar_SignedOutTitle": "Compte Wino", + "WinoAccount_Titlebar_SignedOutDescription": "Inicia sessió o crea un compte Wino per gestionar la teva sessió de Wino.", + "WinoAccount_Titlebar_SignedInStatus": "Estat: {0}", + "WelcomeWizard_Step2Title": "Afegeix un compte", + "WelcomeWizard_Step3Title": "Finalitza la configuració", + "ProviderSelection_Title": "Trieu el vostre proveïdor de correu electrònic", + "ProviderSelection_Subtitle": "Seleccioneu un proveïdor a continuació per afegir el vostre compte de correu a Wino Mail.", + "ProviderSelection_AccountNameHeader": "Nom del compte", + "ProviderSelection_AccountNamePlaceholder": "p. ex. Personal, Treball", + "ProviderSelection_DisplayNameHeader": "Nom a mostrar", + "ProviderSelection_DisplayNamePlaceholder": "p. ex. John Doe", + "ProviderSelection_EmailHeader": "Adreça de correu electrònic", + "ProviderSelection_EmailPlaceholder": "p. ex. johndoe@example.com", + "ProviderSelection_AppPasswordHeader": "Contrasenya específica de l'aplicació", + "ProviderSelection_AppPasswordHelp": "Com obtinc una contrasenya específica de l'aplicació?", + "ProviderSelection_CalendarModeHeader": "Integració del calendari", + "ProviderSelection_CalendarMode_DisabledTitle": "Desactivat", + "ProviderSelection_CalendarMode_DisabledDescription": "Sense integració de calendari", + "ProviderSelection_CalendarMode_CalDavTitle": "Sincronització CalDAV", + "ProviderSelection_CalendarMode_CalDavDescription_Apple": "Els esdeveniments del vostre calendari es sincronitzen entre els vostres dispositius amb els servidors d'Apple.", + "ProviderSelection_CalendarMode_CalDavDescription_Yahoo": "Els esdeveniments del vostre calendari es sincronitzen entre els vostres dispositius amb els servidors de Yahoo.", + "ProviderSelection_CalendarMode_LocalTitle": "Calendari local", + "ProviderSelection_CalendarMode_LocalDescription": "Els vostres esdeveniments s'emmagatzemen només al vostre ordinador. No hi ha connexió amb cap servidor.", + "ProviderSelection_ClearColor": "Netejar color", + "ProviderSelection_ContinueButton": "Continuar", + "ProviderSelection_SpecialImap_Subtitle": "Introdueu les credencials del vostre compte per connectar.", + "AccountSetup_Title": "Configuració del vostre compte", + "AccountSetup_Step_Authenticating": "Autenticant amb {0}", + "AccountSetup_Step_TestingMailAuth": "Provant l'autenticació del correu", + "AccountSetup_Step_SyncingFolders": "Sincronitzant les metadades de les carpetes", + "AccountSetup_Step_FetchingProfile": "Obtenint informació de perfil", + "AccountSetup_Step_DiscoveringCalDav": "Descobrint la configuració CalDAV", + "AccountSetup_Step_TestingCalendarAuth": "Provant l'autenticació del calendari", + "AccountSetup_Step_SavingAccount": "Guardant la informació del compte", + "AccountSetup_Step_FetchingCalendarMetadata": "Recuperant metadades del calendari", + "AccountSetup_Step_SyncingAliases": "Sincronitzant àlies", + "AccountSetup_Step_Finalizing": "Finalitzant la configuració", + "AccountSetup_FailureMessage": "La configuració ha fallat. Torna enrere per corregir la configuració o torna a provar-ho més tard.", + "AccountSetup_SuccessMessage": "El teu compte s'ha configurat amb èxit!", + "AccountSetup_GoBackButton": "Torna enrere", + "AccountSetup_TryAgainButton": "Prova-ho de nou", + "ImapCalDavSettings_AutoDiscoveryFailed": "La detecció automàtica ha fallat. Si us plau, introdueix la configuració manualment a la pestanya Avançat." } - - diff --git a/Wino.Core.Domain/Translations/cs_CZ/resources.json b/Wino.Core.Domain/Translations/cs_CZ/resources.json index b4193f0f..cb5fff64 100644 --- a/Wino.Core.Domain/Translations/cs_CZ/resources.json +++ b/Wino.Core.Domain/Translations/cs_CZ/resources.json @@ -8,6 +8,7 @@ "AccountCacheReset_Message": "This account requires full re-sychronization to continue working. Please wait while Wino re-synchronizes your messages...", "AccountContactNameYou": "Vy", "AccountCreationDialog_Completed": "hotovo", + "AccountCreationDialog_FetchingCalendarMetadata": "Načítám detaily kalendáře.", "AccountCreationDialog_FetchingEvents": "Fetching calendar events.", "AccountCreationDialog_FetchingProfileInformation": "Fetching profile details.", "AccountCreationDialog_GoogleAuthHelpClipboardText_Row0": "If your browser did not launch automatically to complete authentication:", @@ -17,6 +18,7 @@ "AccountCreationDialog_Initializing": "inicializace", "AccountCreationDialog_PreparingFolders": "Stahování informací o složkách.", "AccountCreationDialog_SigninIn": "Probíhá ukládání informací o účtu.", + "Purchased": "Zakoupeno", "AccountEditDialog_Message": "Název účtu", "AccountEditDialog_Title": "Upravit účet", "AccountPickerDialog_Title": "Vybrat účet", @@ -26,6 +28,10 @@ "AccountDetailsPage_Description": "Change the name of the account in Wino and set desired sender name.", "AccountDetailsPage_ColorPicker_Title": "Account color", "AccountDetailsPage_ColorPicker_Description": "Assign a new account color to colorize its symbol in the list.", + "AccountDetailsPage_TabGeneral": "Obecné", + "AccountDetailsPage_TabMail": "E-mail", + "AccountDetailsPage_TabCalendar": "Kalendář", + "AccountDetailsPage_CalendarListDescription": "Vyberte kalendář pro konfiguraci jeho nastavení.", "AddHyperlink": "Přidat", "AppCloseBackgroundSynchronizationWarningTitle": "Synchronizace na pozadí", "AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Application has not been set to launch on Windows startup.", @@ -47,8 +53,10 @@ "BasicIMAPSetupDialog_Title": "IMAP účet", "Busy": "Busy", "Buttons_AddAccount": "Přidat účet", + "Buttons_FixAccount": "Opravit účet", "Buttons_AddNewAlias": "Add New Alias", "Buttons_Allow": "Povolit", + "Buttons_Apply": "Použít", "Buttons_ApplyTheme": "Použít motiv", "Buttons_Browse": "Procházet", "Buttons_Cancel": "Zrušit", @@ -62,6 +70,7 @@ "Buttons_Edit": "Upravit", "Buttons_EnableImageRendering": "Povolit", "Buttons_Multiselect": "Select Multiple", + "Buttons_Manage": "Spravovat", "Buttons_No": "Ne", "Buttons_Open": "Otevřít", "Buttons_Purchase": "Koupit", @@ -70,15 +79,134 @@ "Buttons_Save": "Uložit", "Buttons_SaveConfiguration": "Uložit nastavení", "Buttons_Send": "Send", + "Buttons_SendToServer": "Odeslat na server", "Buttons_Share": "Sdílet", "Buttons_SignIn": "Přihlásit se", "Buttons_Sync": "Synchronizovat", "Buttons_SyncAliases": "Synchronize Aliases", "Buttons_TryAgain": "Zkusit znovu", "Buttons_Yes": "Ano", + "Sync_SynchronizingFolder": "Synchronizuji {0} {1}%", + "Sync_DownloadedMessages": "Staženo {0} zpráv ze {1}", + "SyncAction_Archiving": "Archivace {0} e-mailů", + "SyncAction_ClearingFlag": "Odebrání vlajky {0} e-mailů", + "SyncAction_CreatingDraft": "Vytváření konceptu zprávy", + "SyncAction_CreatingEvent": "Vytváření události", + "SyncAction_Deleting": "Mazání {0} e-mailů", + "SyncAction_EmptyingFolder": "Prázdnění složky", + "SyncAction_MarkingAsRead": "Označuji {0} e-mailů jako přečtené", + "SyncAction_MarkingAsUnread": "Označuji {0} e-mailů jako nepřečtené", + "SyncAction_MarkingFolderAsRead": "Označuji složku jako přečtenou", + "SyncAction_Moving": "Přesouvání {0} e-mailů", + "SyncAction_MovingToFocused": "Přesouvání {0} e-mailů do složky Focused", + "SyncAction_RenamingFolder": "Přejmenovávání složky", + "SyncAction_SendingMail": "Odesílání e-mailu", + "SyncAction_SettingFlag": "Označování vlajkou {0} e-mailů", + "SyncAction_SynchronizingAccount": "Synchronizuji {0}", + "SyncAction_SynchronizingAccounts": "Synchronizuji {0} účetů", + "SyncAction_SynchronizingCalendarData": "Synchronizuji data kalendáře", + "SyncAction_SynchronizingCalendarEvents": "Synchronizuji události kalendáře", + "SyncAction_SynchronizingCalendarMetadata": "Synchronizuji metadata kalendáře", + "SyncAction_Unarchiving": "Obnovování {0} e-mailů ze složky archivu", "CalendarAllDayEventSummary": "all-day events", "CalendarDisplayOptions_Color": "Color", "CalendarDisplayOptions_Expand": "Expand", + "CalendarEventResponse_Accept": "Přijmout", + "CalendarEventResponse_AcceptedResponse": "Přijato", + "CalendarEventResponse_Decline": "Odmítnout", + "CalendarEventResponse_DeclinedResponse": "Odmítnuto", + "CalendarEventResponse_NotResponded": "Dosud neodepsáno", + "CalendarEventResponse_Tentative": "Předběžný", + "CalendarEventResponse_TentativeResponse": "Předběžná odpověď", + "CalendarEventRsvpPanel_Accept": "Přijmout", + "CalendarEventRsvpPanel_AddMessage": "Přidat zprávu k odpovědi... (volitelné)", + "CalendarEventRsvpPanel_Decline": "Odmítnout", + "CalendarEventRsvpPanel_Message": "Zpráva", + "CalendarEventRsvpPanel_SendReplyMessage": "Odeslat odpověď", + "CalendarEventRsvpPanel_Tentative": "Předběžně", + "CalendarEventRsvpPanel_Title": "Možnosti odpovědi", + "CalendarAttendeeStatus_Accepted": "Přijato", + "CalendarAttendeeStatus_Declined": "Odmítnuto", + "CalendarAttendeeStatus_NeedsAction": "Vyžaduje akci", + "CalendarAttendeeStatus_Tentative": "Předběžně", + "CalendarEventDetails_Attachments": "Přílohy", + "CalendarEventCompose_AddAttachment": "Přidat přílohu", + "CalendarEventCompose_AllDay": "Celý den", + "CalendarEventCompose_AttachmentsNotSupportedForCalDav": "Přílohy nejsou podporovány pro kalendáře CalDAV.", + "CalendarEventCompose_EndDate": "Datum konce", + "CalendarEventCompose_EndTime": "Čas konce", + "CalendarEventCompose_Every": "každý", + "CalendarEventCompose_ForWeekdays": "pro", + "CalendarEventCompose_FrequencyDay": "den", + "CalendarEventCompose_FrequencyDayPlural": "dny", + "CalendarEventCompose_FrequencyMonth": "měsíc", + "CalendarEventCompose_FrequencyMonthPlural": "měsíců", + "CalendarEventCompose_FrequencyWeek": "týden", + "CalendarEventCompose_FrequencyWeekPlural": "týdny", + "CalendarEventCompose_FrequencyYear": "rok", + "CalendarEventCompose_FrequencyYearPlural": "roků", + "CalendarEventCompose_Location": "Místo", + "CalendarEventCompose_LocationPlaceholder": "Přidat místo", + "CalendarEventCompose_NewEventButton": "Nová událost", + "CalendarEventCompose_DefaultCalendarHint": "V nastavení Kalendáře si můžete vybrat výchozí kalendář pro nové události.", + "CalendarEventCompose_DefaultCalendarSettingsLink": "Otevřít nastavení Kalendáře", + "CalendarEventCompose_NoCalendarsMessage": "Zatím nejsou k dispozici žádné kalendáře pro vytvoření události.", + "CalendarEventCompose_NoCalendarsTitle": "Žádné kalendáře nejsou k dispozici", + "CalendarEventCompose_NoEndDate": "Žádné datum konce", + "CalendarEventCompose_Notes": "Poznámky", + "CalendarEventCompose_PickCalendarTitle": "Vyberte kalendář", + "CalendarEventCompose_Recurring": "Opakující se", + "CalendarEventCompose_RecurringSummary": "Probíhá každých {0} {1}{2} {3} platných {4}{5}", + "CalendarEventCompose_RecurringSummarySmart": "Probíhá {0}{1} {2} platných {3}{4}", + "CalendarEventCompose_RepeatEvery": "Opakovat každých", + "CalendarEventCompose_SelectCalendar": "Vybrat kalendář", + "CalendarEventCompose_SingleOccurrenceSummary": "Probíhá v {0} {1}", + "CalendarEventCompose_StartDate": "Datum začátku", + "CalendarEventCompose_StartTime": "Čas začátku", + "CalendarEventCompose_TimeRangeSummary": "od {0} do {1}", + "CalendarEventCompose_Title": "Název události", + "CalendarEventCompose_TitlePlaceholder": "Přidat název", + "CalendarEventCompose_Until": "až do", + "CalendarEventCompose_UntilSummary": " až {0}", + "CalendarEventCompose_ValidationInvalidAllDayRange": "Konec události trvající celý den musí být po začátku.", + "CalendarEventCompose_ValidationInvalidAttendee": "Jeden nebo více účastníků má neplatnou e-mailovou adresu.", + "CalendarEventCompose_ValidationInvalidRecurrenceEnd": "Datum ukončení opakování musí být stejné nebo pozdější než datum začátku události.", + "CalendarEventCompose_ValidationInvalidTimeRange": "Konec času musí být později než začátek.", + "CalendarEventCompose_ValidationMissingAttachment": "Jedna nebo více příloh již není k dispozici: {0}", + "CalendarEventCompose_ValidationMissingCalendar": "Vyberte kalendář před vytvořením události.", + "CalendarEventCompose_ValidationMissingTitle": "Zadejte název události před její tvorbou.", + "CalendarEventCompose_ValidationTitle": "Ověření události selhalo", + "CalendarEventCompose_WeekdaySummary": " na {0}", + "CalendarEventCompose_Weekday_Friday": "Pá", + "CalendarEventCompose_Weekday_Monday": "Po", + "CalendarEventCompose_Weekday_Saturday": "So", + "CalendarEventCompose_Weekday_Sunday": "Ne", + "CalendarEventCompose_Weekday_Thursday": "Čt", + "CalendarEventCompose_Weekday_Tuesday": "Út", + "CalendarEventCompose_Weekday_Wednesday": "St", + "CalendarEventDetails_Details": "Podrobnosti", + "CalendarEventDetails_EditSeries": "Upravit sérii", + "CalendarEventDetails_Editing": "Úpravy", + "CalendarEventDetails_InviteSomeone": "Pozvat někoho", + "CalendarEventDetails_JoinOnline": "Připojit se online", + "CalendarEventDetails_Organizer": "Organizátor", + "CalendarEventDetails_People": "Lidé", + "CalendarEventDetails_ReadOnlyEvent": "Událost pouze pro čtení", + "CalendarEventDetails_Reminder": "Upozornění", + "CalendarReminder_StartedHoursAgo": "Zahájeno před {0} hodinami", + "CalendarReminder_StartedMinutesAgo": "Zahájeno před {0} minutami", + "CalendarReminder_StartedNow": "Právě zahájeno", + "CalendarReminder_StartingNow": "Startuje se právě teď", + "CalendarReminder_StartsInHours": "Bude začínat za {0} hodin", + "CalendarReminder_StartsInMinutes": "Bude začínat za {0} minut", + "CalendarReminder_SnoozeAction": "Odložit", + "CalendarReminder_SnoozeMinutesOption": "{0} minut", + "CalendarEventDetails_ShowAs": "Zobrazit jako", + "CalendarShowAs_Free": "Volné", + "CalendarShowAs_Tentative": "Předběžné", + "CalendarShowAs_Busy": "Zaneprázdněno", + "CalendarShowAs_OutOfOffice": "Mimo kancelář", + "CalendarShowAs_WorkingElsewhere": "Pracuje jinde", "CalendarItem_DetailsPopup_JoinOnline": "Join online", "CalendarItem_DetailsPopup_ViewEventButton": "View event", "CalendarItem_DetailsPopup_ViewSeriesButton": "View series", @@ -88,6 +216,9 @@ "ClipboardTextCopied_Message": "\"{0}\" zkopírováno do schránky.", "ClipboardTextCopied_Title": "Zkopírováno", "ClipboardTextCopyFailed_Message": "Nepodařilo se zkopírovat \"{0}\" do schránky.", + "ContactInfoBar_ErrorTitle": "Nepodařilo se načíst kontaktní údaje", + "ContactInfoBar_SuccessTitle": "Kontaktní údaje načteny", + "ContactInfoBar_WarningTitle": "Kontaktní údaje mohou být neúplné", "ComingSoon": "Již brzy...", "ComposerAttachmentsDragDropAttach_Message": "Přiložit", "ComposerAttachmentsDropZone_Message": "Sem přetáhněte soubory", @@ -129,6 +260,10 @@ "DialogMessage_CreateLinkedAccountTitle": "Název propojeného účtu", "DialogMessage_DeleteAccountConfirmationMessage": "Odstranit {0}?", "DialogMessage_DeleteAccountConfirmationTitle": "Všechna data spojená s tímto účtem budou trvale smazána z disku.", + "DialogMessage_DeleteEmailTemplateConfirmationMessage": "Smazat šablonu \"{0}\"?", + "DialogMessage_DeleteEmailTemplateConfirmationTitle": "Smazat šablonu e-mailu", + "DialogMessage_DeleteRecurringSeriesMessage": "Tímto se smažou všechny události v sérii. Chcete pokračovat?", + "DialogMessage_DeleteRecurringSeriesTitle": "Smazat opakující se sérii", "DialogMessage_DiscardDraftConfirmationMessage": "Tento koncept bude zahozen. Chcete pokračovat?", "DialogMessage_DiscardDraftConfirmationTitle": "Zahodit koncept", "DialogMessage_EmptySubjectConfirmation": "Chybějící Předmět", @@ -172,11 +307,18 @@ "ElementTheme_Light": "Světlý režim", "Emoji": "Emoji", "Error_FailedToSetupSystemFolders_Title": "Nastavení systémových složek se nezdařilo", + "Exception_AccountNeedsAttention_Title": "Účet vyžaduje pozornost", + "Exception_AccountNeedsAttention_Message": "'{0}' vyžaduje vaši pozornost, abyste mohli pokračovat.", + "Exception_WebView2RuntimeMissing_Message": "Wino Mail nemohl najít Microsoft Edge WebView2 Runtime. Nainstalujte nebo opravte runtime, aby bylo možné správně zobrazit obsah zprávy.", + "Exception_WebView2RuntimeMissing_Title": "Vyžadován runtime WebView2", "Exception_AuthenticationCanceled": "Ověřování bylo zrušeno", "Exception_CustomThemeExists": "Tento motiv už existuje.", "Exception_CustomThemeMissingName": "Musíte zadat název.", "Exception_CustomThemeMissingWallpaper": "Musíte zadat vlastní obrázek pozadí.", "Exception_FailedToSynchronizeAliases": "Failed to synchronize aliases", + "Exception_FailedToSynchronizeCalendarData": "Nepodařilo se synchronizovat data kalendáře", + "Exception_FailedToSynchronizeCalendarEvents": "Nepodařilo se synchronizovat události kalendáře", + "Exception_FailedToSynchronizeCalendarMetadata": "Nepodařilo se synchronizovat podrobnosti kalendáře", "Exception_FailedToSynchronizeFolders": "Synchronizace složek se nezdařila", "Exception_FailedToSynchronizeProfileInformation": "Failed to synchronize profile information", "Exception_GoogleAuthCallbackNull": "Callback uri je při aktivaci null.", @@ -229,6 +371,32 @@ "HoverActionOption_MoveJunk": "Přesunout do Koše", "HoverActionOption_ToggleFlag": "Označit / Zrušit označení", "HoverActionOption_ToggleRead": "Přečtené / Nepřečtené", + "KeyboardShortcuts_FailedToReset": "Nepodařilo se resetovat klávesové zkratky.", + "KeyboardShortcuts_FailedToUpdate": "Nepodařilo se aktualizovat klávesové zkratky", + "KeyboardShortcuts_MailoperationAction": "Akce", + "KeyboardShortcuts_Action": "Akce", + "KeyboardShortcuts_FailedToLoad": "Nepodařilo se načíst klávesové zkratky.", + "KeyboardShortcuts_EnterKeyForShortcut": "Zadejte klávesu pro zkratku.", + "KeyboardShortcuts_SelectOperationForShortcut": "Prosím zvolte akci pro zkratku.", + "KeyboardShortcuts_EnterKey": "Zadejte klávesu pro zkratku.", + "KeyboardShortcuts_SelectOperation": "Prosím vyberte akci pro zkratku.", + "KeyboardShortcuts_ShortcutInUse": "Tato zkratka je již používána jinou zkratkou.", + "KeyboardShortcuts_FailedToSave": "Nepodařilo se uložit zkratku.", + "KeyboardShortcuts_FailedToDelete": "Nepodařilo se smazat zkratku.", + "KeyboardShortcuts_PageDescription": "Nastavte klávesové zkratky pro rychlé operace s poštou. Stiskněte klávesy v poli pro zadání klávesy, abyste zachytili zkratky.", + "KeyboardShortcuts_Add": "Přidat zkratku", + "KeyboardShortcuts_EditTitle": "Upravit klávesovou zkratku", + "KeyboardShortcuts_ResetToDefaults": "Obnovit výchozí nastavení", + "KeyboardShortcuts_PressKeysHere": "Stiskněte sem klávesy...", + "KeyboardShortcuts_KeyCombination": "Klávesová kombinace", + "KeyboardShortcuts_FocusArea": "Zaměřte se na pole výše a stiskněte požadovanou klávesovou kombinaci", + "KeyboardShortcuts_Modifiers": "Modifikátory", + "KeyboardShortcuts_Mode": "Režim aplikace", + "KeyboardShortcuts_ModeMail": "Pošta", + "KeyboardShortcuts_ModeCalendar": "Kalendář", + "KeyboardShortcuts_ActionToggleReadUnread": "Přepnout přečtené/nepřečtené", + "KeyboardShortcuts_ActionToggleFlag": "Přepnout vlajku", + "KeyboardShortcuts_ActionToggleArchive": "Přepnout archivaci/odarchivaci", "ImageRenderingDisabled": "Vykreslování obrázků je pro tuto zprávu zakázáno.", "ImapAdvancedSetupDialog_AuthenticationMethod": "Způsob ověření", "ImapAdvancedSetupDialog_ConnectionSecurity": "Zabezpečení připojení", @@ -295,12 +463,58 @@ "IMAPSetupDialog_Username": "Uživatelské jméno", "IMAPSetupDialog_UsernamePlaceholder": "jan.novak, jan.novak@seznam.cz", "IMAPSetupDialog_UseSameConfig": "Použít stejné uživatelské jméno a heslo pro odesílání e-mailu", + "ImapCalDavSettingsPage_TitleCreate": "Nastavení IMAP a kalendáře", + "ImapCalDavSettingsPage_TitleEdit": "Upravit nastavení IMAP a kalendáře", + "ImapCalDavSettingsPage_Subtitle": "Nastavte IMAP/SMTP a volitelnou synchronizaci kalendáře pro tento účet.", + "ImapCalDavSettingsPage_BasicSectionTitle": "Základní nastavení", + "ImapCalDavSettingsPage_BasicSectionDescription": "Zadejte svou identitu a údaje pro přihlášení. Wino se může pokusit automaticky detekovat nastavení serveru.", + "ImapCalDavSettingsPage_BasicTab": "Základní", + "ImapCalDavSettingsPage_EnableCalendarSupport": "Povolit podporu kalendáře", + "ImapCalDavSettingsPage_AutoDiscoverButton": "Automatická detekce nastavení pošty", + "ImapCalDavSettingsPage_AutoDiscoverySuccessMessage": "Nastavení pošty bylo zjištěno a použito.", + "ImapCalDavSettingsPage_AdvancedSectionTitle": "Pokročilé nastavení", + "ImapCalDavSettingsPage_AdvancedSectionDescription": "Zadejte nastavení serveru ručně, pokud autodetekce není k dispozici nebo je nesprávná.", + "ImapCalDavSettingsPage_AdvancedTab": "Pokročilé", + "ImapCalDavSettingsPage_CalendarSectionTitle": "Nastavení kalendáře", + "ImapCalDavSettingsPage_CalendarSectionDescription": "Zvolte, jak by měla kalendářová data fungovat pro tento účet IMAP.", + "ImapCalDavSettingsPage_CalendarModeHeader": "Kalendářový režim", + "ImapCalDavSettingsPage_ConnectionSecurityHeader": "Zabezpečení spojení", + "ImapCalDavSettingsPage_AuthenticationMethodHeader": "Způsob ověřování", + "ImapCalDavSettingsPage_CalendarModeDisabled": "Zakázáno", + "ImapCalDavSettingsPage_CalendarModeCalDav": "CalDAV synchronizace", + "ImapCalDavSettingsPage_CalendarModeLocalOnly": "Pouze místní kalendář", + "ImapCalDavSettingsPage_CalendarModeDisabledDescription": "Kalendář je pro tento účet vypnutý.", + "ImapCalDavSettingsPage_CalendarModeCalDavDescription": "Položky kalendáře jsou synchronizovány se vaším serverem CalDAV.", + "ImapCalDavSettingsPage_CalendarModeLocalOnlyDescription": "Položky kalendáře jsou uloženy pouze na tomto počítači a nejsou synchronizovány se sítí.", + "ImapCalDavSettingsPage_LocalCalendarLearnMore": "Jak místní kalendář funguje", + "ImapCalDavSettingsPage_LocalCalendarDialogTitle": "Pouze místní kalendář", + "ImapCalDavSettingsPage_LocalCalendarDialogMessage": "Místní kalendář uchovává všechny události pouze na vašem počítači. Žádná synchronizace s iCloud, Yahoo ani s žádným poskytovatelem.", + "ImapCalDavSettingsPage_CalDavServiceUrl": "URL služby CalDAV", + "ImapCalDavSettingsPage_CalDavUsername": "Uživatelské jméno CalDAV", + "ImapCalDavSettingsPage_CalDavPassword": "Heslo CalDAV", + "ImapCalDavSettingsPage_CalDavNotRequiredMessage": "Test CalDAV je vyžadován pouze tehdy, když je režim kalendáře nastaven na CalDAV synchronizaci.", + "ImapCalDavSettingsPage_CalDavUrlRequired": "URL služby CalDAV je vyžadována.", + "ImapCalDavSettingsPage_CalDavUrlInvalid": "URL služby CalDAV musí být absolutní URL.", + "ImapCalDavSettingsPage_CalDavUsernameRequired": "Uživatelské jméno CalDAV je vyžadováno.", + "ImapCalDavSettingsPage_CalDavPasswordRequired": "CalDAV heslo je vyžadováno.", + "ImapCalDavSettingsPage_TestImapButton": "Otestovat připojení k IMAP.", + "ImapCalDavSettingsPage_TestCalDavButton": "Otestovat připojení CalDAV.", + "ImapCalDavSettingsPage_ImapTestSuccessMessage": "Test připojení k IMAP byl úspěšný.", + "ImapCalDavSettingsPage_CalDavTestSuccessMessage": "Test připojení CalDAV byl úspěšný.", + "ImapCalDavSettingsPage_SaveSuccessMessage": "Nastavení účtu bylo ověřeno a uloženo.", + "ImapCalDavSettingsPage_ICloudHint": "Použijte heslo pro aplikaci, které vygenerujete v nastavení vašeho Apple ID.", + "ImapCalDavSettingsPage_YahooHint": "Použijte heslo pro aplikaci z nastavení zabezpečení účtu Yahoo.", "Info_AccountCreatedMessage": "{0} je vytvořen", "Info_AccountCreatedTitle": "Vytvoření účtu", "Info_AccountCreationFailedTitle": "Vytvoření účtu selhalo", "Info_AccountDeletedMessage": "{0} byl úspěšně smazán.", "Info_AccountDeletedTitle": "Účet byl smazán", "Info_AccountIssueFixFailedTitle": "Oprava účtu selhala", + "Info_AccountIssueFixImapMessage": "Otevřete stránku nastavení IMAP a kalendáře a zadejte znovu údaje o serveru.", + "Info_AccountAttentionRequiredMessage": "Tento účet vyžaduje vaši pozornost.", + "Info_AccountAttentionRequiredClickableMessage": "Klikněte pro opravu tohoto účtu a znovu jej synchronizujte.", + "Info_AccountAttentionRequiredAction": "Opravit", + "Info_AccountAttentionRequiredActionHint": "Klikněte na Opravit, aby se vyřešil problém s tímto účtem.", "Info_AccountIssueFixSuccessMessage": "Opraveny všechny problémy s účtem.", "Info_AccountIssueFixSuccessTitle": "Úspěšně dokončeno", "Info_AttachmentOpenFailedMessage": "Tuto přílohu nelze otevřít.", @@ -370,6 +584,7 @@ "InfoBarMessage_SynchronizationDisabledFolder": "Synchronizace této složky je vypnuta.", "InfoBarTitle_SynchronizationDisabledFolder": "Synchronizace složky vypnuta", "Justify": "Do bloku", + "MenuUpdateAvailable": "Dostupná aktualizace", "Left": "Zleva", "Link": "Odkaz", "LinkedAccountsCreatePolicyMessage": "musíte mít alespoň 2 účty, abyste vytvořili propojení\npropojení bude odstraněno po uložení", @@ -403,6 +618,7 @@ "MailOperation_Unarchive": "Odarchivovat", "MailOperation_ViewMessageSource": "View message source", "MailOperation_Zoom": "Přiblížit", + "MailsDragging": "Přetahuji {0} položku/položek", "MailsSelected": "Vybráno {0} položek", "MarkFlagUnflag": "Označit / Zrušit označení vlajkou", "MarkReadUnread": "Označit jako přečtené/nepřečtené", @@ -434,6 +650,8 @@ "Notifications_MultipleNotificationsTitle": "New Mail", "Notifications_WinoUpdatedMessage": "Vyzkoušejte novou verzi {0}", "Notifications_WinoUpdatedTitle": "Wino Mail byl aktualizován.", + "Notifications_StoreUpdateAvailableTitle": "Dostupná aktualizace", + "Notifications_StoreUpdateAvailableMessage": "Novější verze Wino Mail je připravena k instalaci z Microsoft Store.", "OnlineSearchFailed_Message": "Failed to perform search\n{0}\n\nListing offline mails.", "OnlineSearchTry_Line1": "Can't find what you are looking for?", "OnlineSearchTry_Line2": "Try online search.", @@ -446,7 +664,6 @@ "PaneLengthOption_Small": "Malé", "Photos": "Fotky", "PreparingFoldersMessage": "Připravování složek", - "ProtocolLogAvailable_Message": "Protokoly jsou k dispozici pro diagnostiku.", "ProviderDetail_Gmail_Description": "Google účet", "ProviderDetail_iCloud_Description": "Apple iCloud Account", "ProviderDetail_iCloud_Title": "iCloud", @@ -465,9 +682,14 @@ "SearchBarPlaceholder": "Vyhledávaný výraz", "SearchingIn": "Vyhledávání v", "SearchPivotName": "Výsledky", + "Settings_KeyboardShortcuts_Title": "Klávesové zkratky", + "Settings_KeyboardShortcuts_Description": "Spravujte klávesové zkratky pro rychlé akce u pošty.", "SettingConfigureSpecialFolders_Button": "Nastavit", "SettingsEditAccountDetails_IMAPConfiguration_Title": "IMAP/SMTP Configuration", "SettingsEditAccountDetails_IMAPConfiguration_Description": "Change your incoming/outgoing server settings.", + "SettingsEditAccountDetails_ImapCalDavSettings_Title": "Nastavení IMAP a kalendáře", + "SettingsEditAccountDetails_ImapCalDavSettings_Description": "Otevřete vyhrazenou stránku nastavení IMAP, SMTP a CalDAV pro tento účet.", + "SettingsEditAccountDetails_ImapCalDavSettings_Action": "Otevřít nastavení", "SettingsAbout_Description": "Zjistěte více o Wino.", "SettingsAbout_Title": "O aplikaci", "SettingsAboutGithub_Description": "Přejít na seznam chyb na GitHub.", @@ -490,6 +712,10 @@ "SettingsAppPreferences_SearchMode_Local": "Local", "SettingsAppPreferences_SearchMode_Online": "Online", "SettingsAppPreferences_SearchMode_Title": "Default search mode", + "SettingsAppPreferences_ApplicationMode_Title": "Výchozí režim aplikace", + "SettingsAppPreferences_ApplicationMode_Description": "Vyberte režim, ve kterém se Wino spustí, pokud typ aktivace není výslovně nastaven.", + "SettingsAppPreferences_ApplicationMode_Mail": "Pošta", + "SettingsAppPreferences_ApplicationMode_Calendar": "Kalendář", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description": "Wino Mail will keep running in the background. You will be notified as new mails arrive.", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title": "Run in the background", "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description": "Wino Mail will keep running on the system tray. Available to launch by clicking on an icon. You will be notified as new mails arrive.", @@ -506,12 +732,30 @@ "SettingsAppPreferences_StartupBehavior_FatalError": "Fatal error occurred while changing the startup mode for Wino Mail.", "SettingsAppPreferences_StartupBehavior_Title": "Start minimized on Windows startup", "SettingsAppPreferences_Title": "App Preferences", + "SettingsAppPreferences_HideWinoAccountButton_Title": "Skrýt tlačítko účtu Wino v liště titulku", + "SettingsAppPreferences_HideWinoAccountButton_Description": "Skrýt tlačítko profilu v liště titulku, které otevírá nabídku účtu Wino.", + "SettingsAppPreferences_StoreUpdateNotifications_Title": "Oznámení o aktualizacích z Microsoft Store.", + "SettingsAppPreferences_StoreUpdateNotifications_Description": "Zobrazovat oznámení a akce v patičce, když je k dispozici aktualizace z Microsoft Store.", + "SettingsAppPreferences_AiActions_Title": "AI akce", + "SettingsAppPreferences_AiActions_Description": "Vyberte výchozí jazyky pro AI a kde mají být souhrny ukládány.", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Title": "Výchozí jazyk překladu", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Description": "Vyberte výchozí cílový jazyk použitý při překladech AI.", + "SettingsAppPreferences_AiSummarizeLanguage_Title": "Jazyk shrnutí", + "SettingsAppPreferences_AiSummarizeLanguage_Description": "Zvolte preferovaný jazyk pro shrnutí výsledků AI.", + "SettingsAppPreferences_AiSummarySavePath_Title": "Výchozí cesta ukládání shrnutí", + "SettingsAppPreferences_AiSummarySavePath_Description": "Zvolte složku, kterou by Wino měl používat jako výchozí při ukládání AI shrnutí.", + "SettingsAppPreferences_AiSummarySavePath_Placeholder": "Použít výchozí umístění pro ukládání", + "SettingsAppPreferences_AiSummarySavePath_InvalidHint": "Tady adresář neexistuje. Pro shrnutí bude použito výchozí umístění pro ukládání.", "SettingsAutoSelectNextItem_Description": "Vyberat další položku poté, co se odstraní nebo přesune vybraný e-mail.", "SettingsAutoSelectNextItem_Title": "Automaticky vybrat další položku", "SettingsAvailableThemes_Description": "Vyberte si šablonu ze sbírky Wino dle vaší libosti nebo použijte vlastní motiv.", "SettingsAvailableThemes_Title": "Dostupné motivy", "SettingsCalendarSettings_Description": "Change first day of week, hour cell height and more...", "SettingsCalendarSettings_Title": "Calendar Settings", + "CalendarSettings_DefaultSnoozeDuration_Header": "Výchozí doba odložení", + "CalendarSettings_DefaultSnoozeDuration_Description": "Nastavte výchozí dobu odložení pro upozornění na připomenutí v kalendáři.", + "CalendarSettings_TimedDayHeaderFormat_Header": "Formát záhlaví dne v časovaném zobrazení", + "CalendarSettings_TimedDayHeaderFormat_Description": "Zvolte, jak se nahoře zobrazují názvy dnů ve zobrazeních dne, týdne a pracovního týdne. Používejte tokeny formátu data jako ddd, dd, MMM, nebo dddd.", "SettingsComposer_Title": "Composer", "SettingsComposerFont_Title": "Výchozí písmo editoru", "SettingsComposerFontFamily_Description": "Změna výchozího fontu a jeho velikosti pro editor e-mailu", @@ -531,6 +775,9 @@ "SettingsDiscord_Title": "Discord kanál", "SettingsEditLinkedInbox_Description": "Přidat / odebrat účty, přejmenovat nebo zrušit propojení mezi účty.", "SettingsEditLinkedInbox_Title": "Upravit propojený účet", + "SettingsWindowBackdrop_Title": "Pozadí okna", + "SettingsWindowBackdrop_Description": "Vyberte efekt pozadí pro okna Wino.", + "SettingsWindowBackdrop_Disabled": "Výběr pozadí okna je zakázán, pokud je vybrána aplikace motiv jiný než Výchozí.", "SettingsElementTheme_Description": "Vyberte motiv Windows pro Wino", "SettingsElementTheme_Title": "Motiv prvku", "SettingsElementThemeSelectionDisabled": "Výběr motivu prvku je zakázán, pokud je vybrán jiný motiv aplikace než výchozí.", @@ -581,6 +828,8 @@ "SettingsManageAliases_Title": "Aliases", "SettingsEditAccountDetails_Title": "Edit Account Details", "SettingsEditAccountDetails_Description": "Change account name, sender name and assign a new color if you like.", + "EditAccountDetailsPage_SaveSuccess_Title": "Změny uloženy", + "EditAccountDetailsPage_SaveSuccess_Message": "Detaily vašeho účtu byly úspěšně aktualizovány.", "SettingsManageLink_Description": "Přesunout položky pro přidání nového propojení účtů nebo odstranění již existujícího.", "SettingsManageLink_Title": "Spravovat propojení", "SettingsMarkAsRead_Description": "Změnit, co by se mělo stát s vybranou položkou.", @@ -596,7 +845,41 @@ "SettingsNotifications_Title": "Oznámení", "SettingsNotificationsAndTaskbar_Description": "Change whether notifications should be displayed and taskbar badge for this account.", "SettingsNotificationsAndTaskbar_Title": "Notifications & Taskbar", + "SettingsHome_Title": "Domov", + "SettingsHome_SearchTitle": "Najděte nastavení", + "SettingsHome_SearchDescription": "Vyhledejte podle funkce, tématu nebo klíčového slova a rychle se dostanete na správnou stránku nastavení.", + "SettingsHome_SearchPlaceholder": "Vyhledat nastavení", + "SettingsHome_SearchExamples": "Zkuste: motiv, úložiště, jazyk, podpis", + "SettingsHome_QuickLinks_Title": "Rychlé odkazy", + "SettingsHome_QuickLinks_Description": "Vstupte do nastavení, které lidé nejčastěji hledají.", + "SettingsHome_StorageCard_Description": "Zobrazte, kolik místního MIME obsahu má Wino na tomto zařízení a v případě potřeby ho vyčistěte.", + "SettingsHome_StorageEmptySummary": "Žádný uložený obsah MIME dosud nebyl zjištěn.", + "SettingsHome_StorageLoading": "Kontroluji použití místního MIME...", + "SettingsHome_Tips_Title": "Tipy a triky", + "SettingsHome_Tips_Description": "Několik drobných změn může učinit Wino mnohem osobnějším.", + "SettingsHome_Tip_Theme": "Chcete tmavý režim nebo změny hlavních barev? Otevřete Personalizaci.", + "SettingsHome_Tip_Background": "Použijte Předvolby aplikace k řízení spouštění a synchronizace na pozadí.", + "SettingsHome_Tip_Shortcuts": "Klávesové zkratky vám pomohou procházet poštu rychleji.", + "SettingsHome_Resources_Title": "Užitečné odkazy", + "SettingsHome_Resources_Description": "Otevřete zdroje projektu, informace o podpoře a kanálech vydání.", "SettingsOptions_Title": "Nastavení", + "SettingsOptions_GeneralSection": "Obecné", + "SettingsOptions_MailSection": "Pošta", + "SettingsOptions_CalendarSection": "Kalendář", + "SettingsOptions_MoreComingSoon": "Více možností bude brzy k dispozici", + "SettingsOptions_HeroDescription": "Přizpůsobte si zkušenost s Wino Mail", + "SettingsOptions_AccountsSummary": "{0} účet/ů nakonfigurováno", + "SettingsSearch_ManageAccounts_Keywords": "účet;účty;poštovní schránka;poštovní schránky;alias;aliasy;profil;adresa;adresy", + "SettingsSearch_AppPreferences_Keywords": "spouštění;pozadí;spuštění; synchronizace;notifikace;notifikace;vyhledávání;systémový panel;výchozí", + "SettingsSearch_LanguageTime_Keywords": "jazyk;čas;hodiny;lokalizace;region;formát;24 hodin;24h", + "SettingsSearch_Personalization_Keywords": "téma;tmavý;světlý;vzhled;akcent;barva;barva;mód;rozvržení;hustota", + "SettingsSearch_About_Keywords": "o;verze;webová stránka;ochrana soukromí;GitHub;darovat;obchod;podpora", + "SettingsSearch_KeyboardShortcuts_Keywords": "zkratka;zkratky;klávesová zkratka;klávesové zkratky;klávesnice;klávesy", + "SettingsSearch_MessageList_Keywords": "zpráva;zprávy;seznam;vlákna;vlákna;avatar;náhled;odesílatel", + "SettingsSearch_ReadComposePane_Keywords": "čtečka;psaní;tvůrce;písmo;písma;externí obsah;zobrazení;čtení", + "SettingsSearch_SignatureAndEncryption_Keywords": "podpis;podpisy;šifrování;certifikát;certifikáty;S/MIME;S/MIME;zabezpečení", + "SettingsSearch_Storage_Keywords": "úložiště;mezipaměť;kešování;MIME;disk;volné místo;odstranění;vyčistit;místní data", + "SettingsSearch_CalendarSettings_Keywords": "kalendář;týden;hodiny;rozvrh;událost;události", "SettingsPaneLengthReset_Description": "Reset the size of the mail list to original if you have issues with it.", "SettingsPaneLengthReset_Title": "Reset Mail List Size", "SettingsPaypal_Description": "Ukažte mnohem více lásky ❤️ Všechny dary jsou vítany.", @@ -610,6 +893,8 @@ "SettingsPrefer24HourClock_Title": "Zobrazit 24-hodinový formát času", "SettingsPrivacyPolicy_Description": "Zkontrolujte zásady ochrany osobních údajů.", "SettingsPrivacyPolicy_Title": "Zásady ochrany osobních údajů", + "SettingsWebsite_Description": "Otevřete web Wino Mail.", + "SettingsWebsite_Title": "Webová stránka", "SettingsReadComposePane_Description": "Fonts, external content.", "SettingsReadComposePane_Title": "Reader & Composer", "SettingsReader_Title": "Reader", @@ -625,6 +910,19 @@ "SettingsShowPreviewText_Title": "Zobrazit náhled textu", "SettingsShowSenderPictures_Description": "Skrýt/zobrazit náhled obrázku odesílatele.", "SettingsShowSenderPictures_Title": "Zobrazit avatary odesílatele", + "SettingsEmailTemplates_Title": "Šablony e-mailů", + "SettingsEmailTemplates_Description": "Spravovat šablony e-mailů", + "SettingsEmailTemplates_CreatePageTitle": "Nová šablona", + "SettingsEmailTemplates_EditPageTitle": "Upravit šablonu", + "SettingsEmailTemplates_NewTemplateTitle": "Nová šablona", + "SettingsEmailTemplates_NewTemplateDescription": "Vytvořte novou šablonu e-mailu", + "SettingsEmailTemplates_NameTitle": "Název", + "SettingsEmailTemplates_NamePlaceholder": "Název šablony", + "SettingsEmailTemplates_DescriptionTitle": "Popis", + "SettingsEmailTemplates_DescriptionPlaceholder": "Nepovinný popis", + "SettingsEmailTemplates_ContentTitle": "Obsah šablony", + "SettingsEmailTemplates_ContentDescription": "Upravte HTML obsah této šablony.", + "SettingsEmailTemplates_NameRequired": "Název šablony je vyžadován.", "SettingsEnableGravatarAvatars_Title": "Gravatar", "SettingsEnableGravatarAvatars_Description": "Use gravatar (if available) as sender picture", "SettingsEnableFavicons_Title": "Domain icons (Favicons)", @@ -645,6 +943,33 @@ "SettingsStartupItem_Title": "Primární účet", "SettingsStore_Description": "Ukaž trochu lásky ❤️", "SettingsStore_Title": "Ohodnotit v obchodě", + "SettingsStorage_Title": "Úložiště", + "SettingsStorage_Description": "Skenovat a spravovat MIME mezipaměť uloženou v lokální datové složce.", + "SettingsStorage_ScanFolder": "Skenovat lokální datovou složku", + "SettingsStorage_NoLocalMimeDataFound": "Nebyla nalezena žádná lokální MIME data.", + "SettingsStorage_NoAccountsFound": "Nebyly nalezeny žádné účty.", + "SettingsStorage_TotalUsage": "Celkové využití lokální MIME mezipaměti: {0}", + "SettingsStorage_AccountUsageDescription": "{0} použitých v lokální MIME mezipaměti.", + "SettingsStorage_DeleteAll_Title": "Smazat veškerý MIME obsah.", + "SettingsStorage_DeleteAll_Description": "Odstranit celou složku mezipaměti MIME tohoto účtu.", + "SettingsStorage_DeleteAll_Button": "Smazat vše", + "SettingsStorage_DeleteAll_Confirm_Title": "Smazat veškerý MIME obsah", + "SettingsStorage_DeleteAll_Confirm_Message": "Smazat všechna lokální MIME data pro {0}?", + "SettingsStorage_DeleteAll_Success": "Veškerý obsah MIME byl smazán.", + "SettingsStorage_DeleteOld_Title": "Odstranit starší MIME obsah", + "SettingsStorage_DeleteOld_Description": "Smazat MIME soubory na základě data vytvoření e-mailu v lokální databázi.", + "SettingsStorage_DeleteOld_1Month": "> 1 měsíc", + "SettingsStorage_DeleteOld_3Months": "> 3 měsíců", + "SettingsStorage_DeleteOld_6Months": "> 6 měsíců", + "SettingsStorage_DeleteOld_1Year": "> 1 rok", + "SettingsStorage_DeleteOld_Confirm_Title": "Odstranit starší MIME obsah", + "SettingsStorage_DeleteOld_Confirm_Message": "Smazat lokální MIME data starší než {0} pro {1}?", + "SettingsStorage_DeleteOld_Success": "Smazáno {0} MIME složek starších než {1}.", + "SettingsStorage_1Month": "1 měsíc", + "SettingsStorage_3Months": "3 měsíce", + "SettingsStorage_6Months": "6 měsíců", + "SettingsStorage_1Year": "1 rok", + "SettingsStorage_Months": "{0} měsíců", "SettingsTaskbarBadge_Description": "Include unread mail count in taskbar icon.", "SettingsTaskbarBadge_Title": "Taskbar Badge", "SettingsThreads_Description": "Uspořádat zprávy do konverzačních vláken.", @@ -683,6 +1008,9 @@ "SystemFolderConfigDialogValidation_InboxSelected": "Složku Doručená pošta nelze přiřadit k žádné jiné systémové složce.", "SystemFolderConfigSetupSuccess_Message": "Systémové složky jsou úspěšně nakonfigurovány.", "SystemFolderConfigSetupSuccess_Title": "Nastavení systémových složek", + "SystemTrayMenu_ShowWino": "Otevřít Wino Mail", + "SystemTrayMenu_ShowWinoCalendar": "Otevřít Wino Calendar", + "SystemTrayMenu_ExitWino": "Ukončit", "TestingImapConnectionMessage": "Testuji připojení k serveru...", "TitleBarServerDisconnectedButton_Description": "Wino is disconnected from the network. Click reconnect to restore connection.", "TitleBarServerDisconnectedButton_Title": "no connection", @@ -699,8 +1027,422 @@ "WinoUpgradeMessage": "Přejít na neomezený počet účtů", "WinoUpgradeRemainingAccountsMessage": "{0} z {1} použitých bezplatných účtů.", "Yesterday": "Včera", + "Smime_ImportCertificates_Success": "Certifikáty byly úspěšně importovány.", + "Smime_ImportCertificates_Error": "Chyba při importu certifikátů: {0}", + "Smime_RemoveCertificates_Confirm": "Opravdu chcete odstranit certifikáty {0}?", + "Smime_RemoveCertificates_Success": "Certifikáty byly odstraněny.", + "Smime_ExportCertificates_Success": "Certifikáty byly exportovány.", + "Smime_ExportCertificates_Error": "Chyba při exportu certifikátů.", + "Smime_CertificateDetails": "Předmět: {0}\\nVydavatel: {1}\\nPlatné od: {2}\\nPlatné do: {3}\\nOtisk: {4}", + "Smime_CertificatePassword_Title": "Vyžaduje se heslo certifikátu", + "Smime_CertificatePassword_Placeholder": "Heslo certifikátu pro {0} (nepovinné)", + "Smime_Confirm_Title": "Potvrdit", + "Buttons_OK": "OK", + "Buttons_Refresh": "Obnovit", + "SettingsSignatureAndEncryption_Title": "Podpis a šifrování", + "SettingsSignatureAndEncryption_Description": "Spravovat S/MIME certifikáty pro podepisování a šifrování e-mailů.", + "SettingsSignatureAndEncryption_MyCertificatesHeader": "Mé certifikáty", + "SettingsSignatureAndEncryption_MyCertificatesDescription": "Osobní certifikáty pro podepisování a šifrování", + "SettingsSignatureAndEncryption_RecipientCertificatesHeader": "Certifikáty příjemců", + "SettingsSignatureAndEncryption_RecipientCertificatesDescription": "Certifikáty příjemců pro dešifrování", + "SettingsSignatureAndEncryption_NameColumn": "Název", + "SettingsSignatureAndEncryption_ExpiresColumn": "Vyprší dne", + "SettingsSignatureAndEncryption_ThumbprintColumn": "Otisk", + "Buttons_Remove": "Odebrat", + "Buttons_Export": "Exportovat", + "Buttons_Import": "Importovat", + "SettingsSignatureAndEncryption_SigningCertificate": "S/MIME podpisový certifikát", + "SettingsSignatureAndEncryption_EncryptionCertificate": "S/MIME šifrovací certifikát", + "SettingsSignatureAndEncryption_SigningCertificatePlaceholder": "Žádný", + "SmimeSignaturesInMessage": "Podpisy v této zprávě:", + "SmimeSignatureEntry": "• {0} {1} ({2}, platné do {3} - {4})", + "SmimeSigningCertificateInfoTitle": "Informace o S/MIME podpisovém certifikátu", + "SmimeCertificateInfoTitle": "Informace o S/MIME certifikátu", + "SmimeNoCertificateFileFound": "Nebyl nalezen žádný soubor certifikátu.", + "SmimeSaveCertificate": "Uložit certifikát...", + "SmimeCertificate": "S/MIME certifikát", + "SmimeCertificateSavedTo": "Certifikát uložen do {0}", + "SmimeSignedTooltip": "Tato zpráva je podepsána certifikátem S/MIME. Kliknutím zobrazíte podrobnosti", + "SmimeEncryptedTooltip": "Tato zpráva je šifrována certifikátem S/MIME.", + "SmimeCertificateFileInfo": "Soubor: {0}", + "Composer_LightTheme": "Světlé téma", + "Composer_DarkTheme": "Tmavé téma", + "Composer_Outdent": "Zmenšit odsazení", + "Composer_Indent": "Zvětšit odsazení", + "Composer_BulletList": "Odrážkový seznam", + "Composer_OrderedList": "Číslovaný seznam", + "Composer_Stroke": "Čára", + "Composer_Bold": "Tučné", + "Composer_Italic": "Kurzíva", + "Composer_Underline": "Podtržené", + "Composer_CcBcc": "Kopie (Cc) a Skrytá kopie (Bcc)", + "Composer_EnableSmimeSignature": "Povolit/zakázat S/MIME podpis", + "Composer_EnableSmimeEncryption": "Povolit/zakázat šifrování S/MIME", + "Composer_LocalDraftSyncInfo": "Tento koncept je pouze lokální. Wino se nepodařilo odeslat na váš poštovní server. Klikněte pro opětovné odeslání na server.", + "Composer_CertificateExpires": "Vyprší dne: ", + "Composer_SmimeSignature": "S/MIME podpis", + "Composer_SmimeEncryption": "S/MIME šifrování", + "Composer_EmailTemplatesPlaceholder": "Šablony e-mailů", + "Composer_AiSummarize": "Shrnout pomocí AI", + "Composer_AiSummarizeDescription": "Z tohoto e-mailu vyextrahujte klíčové body, akční kroky a rozhodnutí.", + "Composer_AiTranslate": "Přeložit pomocí AI", + "Composer_AiActions": "AI akce", + "Composer_AiRewrite": "Přepsat pomocí AI", + "AiActions_CheckingStatus": "Ověřování přístupu k AI...", + "AiActions_SignedOutTitle": "Odemknout Wino AI balíček", + "AiActions_SignedOutDescription": "Překládejte, přepište a shrnujte e-maily pomocí AI po přihlášení k vašemu účtu Wino a aktivaci doplňku AI Pack.", + "AiActions_NoPackTitle": "AI Pack vyžadován", + "AiActions_NoPackDescription": "Jste přihlášeni, ale AI Pack ještě není aktivní. Zakupte si ho, abyste mohli používat Wino AI překlad, přepis a shrnutí.", + "AiActions_UsageSummary": "Použito {0} z {1} kreditů tento měsíc.", + "Composer_AiRewritePolite": "Udělej to zdvořile", + "Composer_AiRewritePoliteDescription": "Zjemní formulaci a zachová stejný záměr.", + "Composer_AiRewriteAngry": "Udělej to agresivně", + "Composer_AiRewriteAngryDescription": "Používá ostřejší a konfrontačnější tón.", + "Composer_AiRewriteHappy": "Nastavit na radostný tón", + "Composer_AiRewriteHappyDescription": "Přidá více optimistický a nadšený tón.", + "Composer_AiRewriteFormal": "Nastavit na formální tón", + "Composer_AiRewriteFormalDescription": "Zní to profesionálněji a lépe strukturovaná zpráva.", + "Composer_AiRewriteFriendly": "Nastavit přátelský tón", + "Composer_AiRewriteFriendlyDescription": "Zjemní zprávu a dodá jí přívětilejší tón.", + "Composer_AiRewriteShorter": "Udělej to stručnější", + "Composer_AiRewriteShorterDescription": "Zkracuje text a odstraňuje zbytečné detaily.", + "Composer_AiRewriteClearer": "Udělej to jasnější", + "Composer_AiRewriteClearerDescription": "Zlepšuje čitelnost a usnadňuje porozumění sdělení.", + "Composer_AiRewriteCustom": "Vlastní", + "Composer_AiRewriteCustomDescription": "Popište svůj vlastní záměr přeformulování.", + "Composer_AiRewriteCustomPlaceholder": "Popište, jak má být zpráva přeformulována", + "Composer_AiRewriteMode": "Přeformulovat tón", + "Composer_AiRewriteApply": "Použít přeformulování", + "Composer_AiTranslateDialogTitle": "Přeložit pomocí AI", + "Composer_AiTranslateDialogDescription": "Zadejte cílový jazyk nebo kód kultury, například en-US, tr-TR, de-DE nebo fr-FR.", + "Composer_AiTranslateApply": "Přeložit", + "Composer_AiTranslateLanguage": "Cílový jazyk", + "Composer_AiTranslateCustomPlaceholder": "Zadejte kód kultury", + "Composer_AiTranslateLanguageEnglish": "Angličtina (en-US)", + "Composer_AiTranslateLanguageTurkish": "Turečtina (tr-TR)", + "Composer_AiTranslateLanguageGerman": "Němčina (de-DE)", + "Composer_AiTranslateLanguageFrench": "Francouzština (fr-FR)", + "Composer_AiTranslateLanguageSpanish": "Španělština (es-ES)", + "Composer_AiTranslateLanguageItalian": "Italština (it-IT)", + "Composer_AiTranslateLanguagePortugueseBrazil": "Portugalština (Brazílie) (pt-BR)", + "Composer_AiTranslateLanguageDutch": "Nizozemština (nl-NL)", + "Composer_AiTranslateLanguagePolish": "Polština (pl-PL)", + "Composer_AiTranslateLanguageRussian": "Ruština (ru-RU)", + "Composer_AiTranslateLanguageJapanese": "Japonština (ja-JP)", + "Composer_AiTranslateLanguageKorean": "Korejština (ko-KR)", + "Composer_AiTranslateLanguageChineseSimplified": "Čínština, zjednodušená (zh-CN)", + "Composer_AiTranslateLanguageArabic": "Arabština (ar-SA)", + "Composer_AiTranslateLanguageHindi": "Hindština (hi-IN)", + "Composer_AiTranslateLanguageOther": "Jiné...", + "Composer_AiBusyTitle": "AI už pracuje", + "Composer_AiBusyMessage": "Prosím počkejte, než se dokončí probíhající akce AI.", + "Composer_AiSignInRequired": "Přihlaste se ke svému účtu Wino, abyste mohli používat funkce AI.", + "Composer_AiMissingHtml": "Zatím není žádný obsah zprávy k odeslání do Wino AI.", + "Composer_AiQuotaUnavailable": "Výsledek AI byl použit.", + "Composer_AiAppliedMessage": "Výsledek AI byl použit do editoru. Pokud chcete změny vrátit, použijte Zpět.", + "Composer_AiSummarizeSuccessTitle": "Shrnutí AI bylo aplikováno.", + "Composer_AiTranslateSuccessTitle": "Překlad AI byl aplikován.", + "Composer_AiRewriteSuccessTitle": "Přeformulování AI bylo aplikováno.", + "Composer_AiErrorTitle": "Akce AI selhala.", + "Reader_AiAppliedMessage": "Výsledek AI je nyní zobrazen pro tuto zprávu. Znovu otevřete zprávu, abyste viděli původní obsah.", "SettingsAppPreferences_EmailSyncInterval_Title": "Email sync interval", - "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail." + "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail.", + "ContactsPage_Title": "Kontakty", + "ContactsPage_AddContact": "Přidat kontakt", + "ContactsPage_EditContact": "Upravit kontakt", + "ContactsPage_DeleteContact": "Smazat kontakt", + "ContactsPage_SearchPlaceholder": "Hledat kontakty...", + "ContactsPage_NoContacts": "Žádné kontakty k zobrazení.", + "ContactsPage_ContactsCount": "{0} kontaktů", + "ContactsPage_SelectedContactsCount": "{0} vybraných", + "ContactsPage_DeleteSelectedContacts": "Smazat vybrané", + "ContactEditDialog_Title": "Upravit kontakt", + "ContactEditDialog_PhotoSection": "Fotografie", + "ContactEditDialog_ChoosePhoto": "Vybrat fotku", + "ContactEditDialog_RemovePhoto": "Odebrat fotku", + "ContactEditDialog_NameHeader": "Jméno", + "ContactEditDialog_NamePlaceholder": "Jméno kontaktu", + "ContactEditDialog_EmailHeader": "Emailová adresa", + "ContactEditDialog_EmailPlaceholder": "contact@example.com", + "ContactEditDialog_InfoSection": "Kontaktní informace", + "ContactEditDialog_RootContactInfo": "Toto je kořenový kontakt spojený s vašimi účty a není možné jej smazat.", + "ContactEditDialog_OverriddenContactInfo": "Tento kontakt byl ručně upraven a nebude během synchronizace aktualizován.", + "ContactsPage_Subtitle": "Spravujte své e-mailové kontakty a jejich informace", + "ContactStatus_Account": "Účet", + "ContactStatus_Modified": "Upraveno", + "ContactAction_Edit": "Upravit kontakt", + "ContactAction_ChangePhoto": "Změnit fotku", + "ContactAction_Delete": "Smazat kontakt", + "ContactAction_Add": "Přidat kontakt", + "ContactSelection_Selected": "vybráno", + "ContactSelection_SelectAll": "Vybrat vše", + "ContactSelection_Clear": "Vymazat výběr", + "ContactsPage_EmptyState": "Žádné kontakty k zobrazení", + "ContactsPage_AddFirstContact": "Přidejte svůj první kontakt", + "ContactsPage_ContactsCountSuffix": "kontakty", + "ContactsPane_NewContact": "Nový kontakt", + "ContactsPane_DescriptionTitle": "Spravujte své kontakty", + "ContactsPane_DescriptionBody": "Vytvářejte kontakty, přejmenovávejte je, aktualizujte jejich profilové fotky a mějte uložené údaje na jednom místě.", + "ContactEditDialog_AddTitle": "Přidat kontakt", + "ContactInfoBar_ContactAdded": "Kontakt byl úspěšně přidán.", + "ContactInfoBar_ContactUpdated": "Kontakt byl úspěšně aktualizován.", + "ContactInfoBar_ContactsDeleted": "Kontakty byly úspěšně smazány.", + "ContactInfoBar_ContactPhotoUpdated": "Fotografie kontaktu byla úspěšně aktualizována.", + "ContactInfoBar_FailedToLoadContacts": "Nepodařilo se načíst kontakty: {0}", + "ContactInfoBar_FailedToAddContact": "Nepodařilo se přidat kontakt: {0}", + "ContactInfoBar_FailedToUpdateContact": "Nepodařilo se aktualizovat kontakt: {0}", + "ContactInfoBar_FailedToDeleteContacts": "Nepodařilo se smazat kontakty: {0}", + "ContactInfoBar_FailedToUpdatePhoto": "Nepodařilo se aktualizovat fotografii: {0}", + "ContactInfoBar_CannotDeleteRoot": "Kořenové kontakty nelze smazat.", + "ContactConfirmDialog_DeleteTitle": "Smazat kontakt", + "ContactConfirmDialog_DeleteMessage": "Opravdu chcete smazat kontakt '{0}'?", + "ContactConfirmDialog_DeleteMultipleMessage": "Opravdu chcete smazat {0} kontakt(ů)?", + "ContactConfirmDialog_DeleteButton": "Smazat", + "CalendarAccountSettings_Title": "Nastavení účtu kalendáře", + "CalendarAccountSettings_Description": "Spravujte nastavení kalendáře pro {0}", + "CalendarAccountSettings_AccountColor": "Barva účtu", + "CalendarAccountSettings_AccountColorDescription": "Změňte barvu zobrazení tohoto kalendářního účtu.", + "CalendarAccountSettings_SyncEnabled": "Povolit synchronizaci", + "CalendarAccountSettings_SyncEnabledDescription": "Povolit nebo zakázat synchronizaci kalendáře pro tento účet.", + "CalendarAccountSettings_DefaultShowAs": "Výchozí stav dostupnosti", + "CalendarAccountSettings_DefaultShowAsDescription": "Výchozí stav dostupnosti pro nové události vytvořené tímto účtem.", + "CalendarAccountSettings_PrimaryCalendar": "Hlavní kalendář", + "CalendarAccountSettings_PrimaryCalendarDescription": "Označte tento kalendář jako hlavní kalendář pro účet.", + "CalendarSettings_NewEventBehavior_Header": "Chování tlačítka Nová událost", + "CalendarSettings_NewEventBehavior_Description": "Vyberte, zda má tlačítko Nová událost pokaždé vyžadovat výběr kalendáře, nebo má vždy otevírat konkrétní kalendář.", + "CalendarSettings_NewEventBehavior_AskEachTime": "Ptát se pokaždé.", + "CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar": "Vždy používat konkrétní kalendář.", + "CalendarSettings_Rendering_Title": "Zobrazení", + "CalendarSettings_Rendering_Description": "Nastavte rozložení kalendáře a způsob zobrazení.", + "CalendarSettings_Notifications_Title": "Upozornění", + "CalendarSettings_Notifications_Description": "Zvolte výchozí připomenutí a chování odkládání.", + "CalendarSettings_Preferences_Title": "Předvolby", + "CalendarSettings_Preferences_Description": "Nastavte, jak má tlačítko Nová událost fungovat.", + "WhatIsNew_GetStartedButton": "Začít", + "WhatIsNew_ContinueAnywayButton": "Pokračovat bez ohledu na to", + "WhatIsNew_PreparingForNewVersionButton": "Příprava nové verze...", + "WhatIsNew_MigrationPreparing_Title": "Příprava vašich dat", + "WhatIsNew_MigrationPreparing_Description": "Wino provádí migrační aktualizace. Počkejte, dokud nepřipravíme data vašeho účtu pro tuto verzi.", + "WhatIsNew_MigrationFailedMessage": "Při migracích došlo k chybě s kódem {0}. Můžete nadále používat aplikaci. Pokud však narazíte na vážné problémy, prosím přeinstalujte aplikaci.", + "WhatIsNew_MigrationNotification_Title": "Wino Mail byl aktualizován", + "WhatIsNew_MigrationNotification_Message": "Otevřete aplikaci, abyste dokončili aktualizaci a zjistili novinky.", + "WelcomeWindow_Title": "Vítejte v Wino Mail", + "WelcomeWindow_Subtitle": "Nativní uživatelská zkušenost pro Mail a Kalendář na Windows.", + "WelcomeWindow_WhatsNewTitle": "Nejnovější změny", + "WelcomeWindow_FeaturesTitle": "Funkce", + "WelcomeWindow_WhatsNewTab": "Co je nového", + "WelcomeWindow_FeaturesTab": "Funkce", + "WelcomeWindow_GetStartedButton": "Začněte přidáním účtu", + "WelcomeWindow_GetStartedDescription": "Přidejte svůj účet Outlook, Gmail nebo IMAP a začněte s Wino Mail.", + "WelcomeWindow_ImportFromWinoAccount": "Importovat z vašeho Wino účtu", + "WelcomeWindow_ImportInProgress": "Importuji synchronizované preference a účty...", + "WelcomeWindow_ImportNoAccountsFound": "Ve vašem Wino účtu nebyly nalezeny žádné synchronizované účty. Pokud byly k dispozici preference, byly obnoveny. Pro ruční přidání účtu použijte možnost Začít.", + "WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} synchronizovaných účtů je již k dispozici na tomto zařízení. Pokud je potřeba, použijte možnost Začít pro ruční přidání dalšího účtu.", + "WelcomeWindow_SetupTitle": "Nastavte si svůj účet", + "WelcomeWindow_SetupSubtitle": "Vyberte poskytovatele e-mailu a začněte.", + "WelcomeWindow_AddAccountButton": "Přidat účet", + "WelcomeWindow_SkipForNow": "Prozatím přeskočit — nastavím to později.", + "WelcomeWindow_AppDescription": "Rychlá, soustředěná doručená pošta — přepracována pro Windows 11.", + "WelcomeWizard_Step1Title": "Vítejte", + "SystemTrayMenu_Open": "Otevřít", + "WinoAccount_Titlebar_SyncBenefitTitle": "Nastavení synchronizace", + "WinoAccount_Titlebar_SyncBenefitDescription": "Udržujte své preference Wino synchronizované napříč zařízeními.", + "WinoAccount_Titlebar_AddonsBenefitTitle": "Odemknout doplňky", + "WinoAccount_Titlebar_AddonsBenefitDescription": "Získejte přístup k prémiovým funkcím, jako je Wino AI Pack.", + "WinoAccount_Management_Description": "Spravujte svůj účet Wino, přístup k AI Pack a synchronizované preference a údaje o účtu.", + "WinoAccount_Management_SignedOutTitle": "Přihlaste se do Wino Mail", + "WinoAccount_Management_SignedOutDescription": "Přihlaste se nebo si vytvořte účet pro synchronizaci e-mailu, přístup k AI funkcím a správu nastavení napříč zařízeními.", + "WinoAccount_Management_ProfileSectionHeader": "Profil", + "WinoAccount_Management_AddOnsSectionHeader": "Doplňky Wino", + "WinoAccount_Management_DataSectionHeader": "Data", + "WinoAccount_Management_AccountActionsSectionHeader": "Operace s účtem", + "WinoAccount_Management_AccountCardTitle": "Účet", + "WinoAccount_Management_AccountCardDescription": "E-mailová adresa vašeho účtu Wino a aktuální stav účtu.", + "WinoAccount_Management_AiPackCardTitle": "AI Pack", + "WinoAccount_Management_AiPackCardDescription": "Zjistěte, zda je Wino AI Pack aktivní a kolik používání zbývá.", + "WinoAccount_Management_AiPackActive": "AI Pack je aktivní", + "WinoAccount_Management_AiPackInactive": "AI Pack není aktivní", + "WinoAccount_Management_AiPackUsage": "{0} z {1} použití bylo spotřebováno. Zbývá {2}.", + "WinoAccount_Management_AiPackBillingPeriod": "Období fakturace: {0:d} - {1:d}", + "WinoAccount_Management_AiPackUnknownUsage": "Podrobnosti o využití AI zatím nejsou k dispozici.", + "WinoAccount_Management_AiPackBuyDescription": "Koupit Wino AI Pack pro překlad, přeformulování nebo shrnutí e-mailů pomocí AI.", + "WinoAccount_Management_AiPackPromoTitle": "Odemknout AI Pack", + "WinoAccount_Management_AiPackPromoDescription": "Vylepšete svůj e-mailový pracovní postup nástroji poháněnými AI. Překládejte zprávy do více než 50 jazyků, přeformulujte pro jasnost a tón a získejte okamžité shrnutí dlouhých vláken.", + "WinoAccount_Management_AiPackPromoPrice": "$4,99 / měs.", + "WinoAccount_Management_AiPackPromoRequests": "1 000 kreditů", + "WinoAccount_Management_AiPackGetButton": "Získat AI Pack", + "WinoAddOn_AI_PACK_Name": "Wino AI Pack", + "WinoAddOn_AI_PACK_Description": "Nástroje poháněné AI pro překlad, přeformulování a shrnutí e-mailů ve Wino Mail.", + "WinoAddOn_AI_PACK_Keywords": "AI, překlad, přeformulování, shrnutí, produktivita", + "WinoAddOn_UNLIMITED_ACCOUNTS_Name": "Neomezené účty", + "WinoAddOn_UNLIMITED_ACCOUNTS_Description": "Odeberte omezení počtu účtů a přidejte tolik e-mailových účtů, kolik potřebujete.", + "WinoAddOn_UNLIMITED_ACCOUNTS_Keywords": "účty, neomezené, prémiový, doplněk", + "WinoAccount_Management_PurchaseRequiresSignIn": "Přihlaste se ke svému účtu Wino, abyste dokončili nákup.", + "WinoAccount_Management_PurchaseStartFailed": "Nákup ve Microsoft Store nebyl dokončen.", + "WinoAccount_Management_StoreSyncFailed": "Nákup byl dokončen, ale Wino nemůže dosud obnovit výhody vašeho účtu. Zkuste to prosím za okamžik.", + "WinoAccount_Management_AiPackSubscriptionActive": "Váš předplatné je aktivní.", + "WinoAccount_Management_AiPackRenews": "Obnovuje {0:d}", + "WinoAccount_Management_AiPackRequestsUsed": "Použité kredity tento měsíc.", + "WinoAccount_Management_AiPackResets": "Obnovení {0:d}", + "WinoAccount_Management_AiPackUsageLoadFailed": "Nepodařilo se načíst zůstatek využití AI.", + "WinoAccount_Management_AiPackFeatureTranslate": "Překládat", + "WinoAccount_Management_AiPackFeatureRewrite": "Přeformulovat", + "WinoAccount_Management_AiPackFeatureSummarize": "Shrnout", + "WinoAccount_Management_AddOnLoadFailed": "Při načítání tohoto doplňku došlo k potížím.", + "WinoAccount_Management_SyncPreferencesTitle": "Synchronizace nastavení a účtů", + "WinoAccount_Management_SyncPreferencesDescription": "Importujte nebo exportujte nastavení a podrobnosti vaší poštovní schránky napříč zařízeními. Hesla, tokeny a další citlivé údaje se nikdy nesynchronizují.", + "WinoAccount_Management_SignOutTitle": "Odhlásit se", + "WinoAccount_Management_SignOutDescription": "Odhlašte se ze svého účtu na tomto zařízení.", + "WinoAccount_Management_StatusLabel": "Stav: {0}", + "WinoAccount_Management_NoRemoteSettings": "Pro tento účet dosud nejsou uložena žádná synchronizovaná data.", + "WinoAccount_Management_ExportSucceeded": "Vybraná data Wino byla úspěšně exportována.", + "WinoAccount_Management_ExportPreferencesSucceeded": "Vaše preference byly exportovány do vašeho Wino účtu.", + "WinoAccount_Management_ExportAccountsSucceeded": "Exportováno {0} podrobností o účtech do vašeho Wino účtu.", + "WinoAccount_Management_ImportSucceeded": "Importována synchronizovaná data z vašeho účtu Wino.", + "WinoAccount_Management_ImportPreferencesSucceeded": "Aplikováno {0} synchronizovaných preferencí.", + "WinoAccount_Management_ImportAccountsSucceeded": "Importováno {0} účtů.", + "WinoAccount_Management_ImportDuplicateAccountsSkipped": "Přeskočeno {0} duplicitních účtů, které na tomto zařízení již existují.", + "WinoAccount_Management_ImportPartial": "Použito {0} synchronizovaných nastavení. {1} nastavení nebylo obnoveno.", + "WinoAccount_Management_ImportReloginReminder": "Hesla, tokeny a další citlivé údaje nebyly importovány. Přihlaste se znovu pro každý účet na tomto zařízení před použitím.", + "WinoAccount_Management_SerializeFailed": "Wino nemohlo serializovat vaše aktuální nastavení.", + "WinoAccount_Management_EmptyExport": "Nejsou žádné hodnoty nastavení k exportu.", + "WinoAccount_Management_ImportEmpty": "Synchronizovaná sada dat neobsahuje nic nového k obnovení.", + "WinoAccount_Management_ExportDialog_Title": "Export do vašeho účtu Wino.", + "WinoAccount_Management_ExportDialog_Description": "Vyberte, co chcete synchronizovat do vašeho účtu Wino.", + "WinoAccount_Management_ExportDialog_IncludePreferences": "Předvolby", + "WinoAccount_Management_ExportDialog_IncludeAccounts": "Účty", + "WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Hesla, tokeny a další citlivé údaje se nesynchronizují.", + "WinoAccount_Management_ExportDialog_AccountsRelogin": "Importované účty na jiném počítači budou i nadále vyžadovat opětovné přihlášení před jejich použitím.", + "WinoAccount_Management_ExportDialog_InProgress": "Probíhá export vybraných dat Wino.", + "WinoAccount_Management_LoadFailed": "Nepodařilo se načíst nejnovější informace o účtu Wino.", + "WinoAccount_Management_ActionFailed": "Požadavek na účet Wino nebyl dokončen.", + "WinoAccount_SettingsSection_Title": "Wino účet", + "WinoAccount_SettingsSection_Description": "Vytvořte si účet Wino nebo se do něj přihlaste prostřednictvím vaší lokální autentizační služby.", + "WinoAccount_RegisterButton_Title": "Zaregistrovat účet", + "WinoAccount_RegisterButton_Description": "Vytvořte účet Wino pomocí e-mailu a hesla.", + "WinoAccount_RegisterButton_Action": "Otevřít registraci", + "WinoAccount_LoginButton_Title": "Přihlásit se", + "WinoAccount_LoginButton_Description": "Přihlaste se k již existujícímu účtu Wino pomocí e-mailu a hesla.", + "WinoAccount_LoginButton_Action": "Otevřít přihlášení", + "WinoAccount_SignOutButton_Title": "Odhlásit se", + "WinoAccount_SignOutButton_Description": "Odstranit místně uloženou relaci účtu Wino.", + "WinoAccount_SignOutButton_Action": "Odhlásit se", + "WinoAccount_RegisterDialog_Title": "Vytvořit účet Wino", + "WinoAccount_RegisterDialog_Description": "Vytvořte si účet Wino, aby se váš zážitek ze Wino synchronizoval a bylo možné odemknout doplňky založené na účtech.", + "WinoAccount_RegisterDialog_HeroTitle": "Vytvořte svůj účet Wino", + "WinoAccount_RegisterDialog_BenefitsTitle": "Proč si ho vytvořit?", + "WinoAccount_RegisterDialog_BenefitSyncTitle": "Import a export nastavení napříč zařízeními.", + "WinoAccount_RegisterDialog_BenefitSyncDescription": "Přesuňte své nastavení Wino mezi zařízeními, aniž byste museli znovu nastavovat celý systém.", + "WinoAccount_RegisterDialog_BenefitAiTitle": "Přístup k exkluzivním doplňkům, jako je Wino AI Pack (placené)", + "WinoAccount_RegisterDialog_BenefitAiDescription": "Použijte jeden účet k odemknutí prémiových funkcí Wino, jak budou dostupné.", + "WinoAccount_RegisterDialog_DifferenceTitle": "Wino Account je oddělen od vašich e‑mailových účtů", + "WinoAccount_RegisterDialog_DifferenceDescription": "Vaše účty Outlook, Gmail, IMAP nebo jiné e‑mailové účty zůstávají přesně takové, jaké jsou. Účet Wino spravuje pouze funkce související s Wino a doplňky založené na účtech.", + "WinoAccount_RegisterDialog_PrimaryButton": "Registrovat", + "WinoAccount_RegisterDialog_PrivacyTitle": "Ochrana soukromí a zpracování API", + "WinoAccount_RegisterDialog_PrivacyDescription": "Volitelné doplňky, jako například Wino AI Pack, mohou odesílat vybraný HTML obsah e-mailů do služby Wino API jen tehdy, když tyto funkce používáte.", + "WinoAccount_RegisterDialog_PrivacyLinkText": "Přečtěte si zásady ochrany soukromí.", + "WinoAccount_RegisterDialog_PrivacyCheckbox": "Souhlasím se zásadami ochrany soukromí.", + "WinoAccount_LoginDialog_Title": "Přihlásit se k účtu Wino", + "WinoAccount_LoginDialog_Description": "Přihlaste se k svému účtu Wino, abyste mohli synchronizovat nastavení Wino a získat přístup k funkcím založeným na účtu.", + "WinoAccount_LoginDialog_HeroTitle": "Vítejte zpět", + "WinoAccount_LoginDialog_BenefitsTitle": "Co vám přihlášení přináší", + "WinoAccount_LoginDialog_BenefitsDescription": "Použijte svůj účet Wino k pokračování synchronizace nastavení napříč zařízeními a k přístupu k placeným doplňkům, jako je Wino AI Pack.", + "WinoAccount_LoginDialog_DifferenceTitle": "Toto není vaše přihlášení k e-mailovému účtu", + "WinoAccount_LoginDialog_DifferenceDescription": "Přihlášení zde nepřidává ani nenahradí vaše účty Outlook, Gmail, IMAP ve Wino. Přihlašujete se pouze k službám určeným pro Wino.", + "WinoAccount_LoginDialog_ForgotPasswordLink": "Zapomenuté heslo?", + "WinoAccount_EmailLabel": "E-mail", + "WinoAccount_EmailPlaceholder": "name@example.com", + "WinoAccount_PasswordLabel": "Heslo", + "WinoAccount_ConfirmPasswordLabel": "Potvrdit heslo", + "WinoAccount_ForgotPasswordDialog_Title": "Obnovit heslo", + "WinoAccount_ForgotPasswordDialog_PrimaryButton": "Odeslat e‑mail pro obnovení hesla.", + "WinoAccount_ForgotPasswordDialog_BackToSignIn": "Zpět na přihlášení", + "WinoAccount_ForgotPasswordDialog_Description": "Zadejte e-mailovou adresu účtu Wino a my vám pošleme odkaz pro obnovení hesla, pokud je adresa registrována.", + "WinoAccount_Validation_EmailRequired": "E-mail je vyžadován.", + "WinoAccount_Validation_PasswordRequired": "Heslo je vyžadováno.", + "WinoAccount_Validation_PasswordMismatch": "Hesla se neshodují.", + "WinoAccount_Validation_PrivacyConsentRequired": "Před vytvořením účtu Wino musíte souhlasit se zásadami ochrany soukromí.", + "WinoAccount_Error_InvalidCredentials": "E‑mailová adresa nebo heslo jsou nesprávné.", + "WinoAccount_Error_AccountLocked": "Tento účet je dočasně uzamčen.", + "WinoAccount_Error_AccountBanned": "Tento účet byl zablokován.", + "WinoAccount_Error_AccountSuspended": "Tento účet byl pozastaven.", + "WinoAccount_Error_EmailNotConfirmed": "Před přihlášením potvrďte svou e‑mailovou adresu.", + "WinoAccount_Error_EmailConfirmationRequired": "Před přihlášením potvrďte svou e‑mailovou adresu.", + "WinoAccount_Error_EmailConfirmationResendNotAvailable": "Nový e‑mail s potvrzením zatím není k dispozici.", + "WinoAccount_Error_EmailConfirmationResendInvalid": "Tato žádost o potvrzení již není platná. Zkuste se prosím znovu přihlásit.", + "WinoAccount_Error_EmailNotRegistered": "Tato e‑mailová adresa není registrována.", + "WinoAccount_Error_RefreshTokenInvalid": "Vaše relace již není platná. Přihlaste se prosím znovu.", + "WinoAccount_Error_EmailAlreadyRegistered": "Tato e‑mailová adresa je již registrována.", + "WinoAccount_Error_ExternalLoginEmailRequired": "K dokončení externího přihlášení je vyžadována e-mailová adresa.", + "WinoAccount_Error_ExternalLoginInvalid": "Žádost o externí přihlášení je neplatná.", + "WinoAccount_Error_ExternalAuthStateInvalid": "Stav externího přihlášení je neplatný nebo vypršel.", + "WinoAccount_Error_ExternalAuthCodeInvalid": "Kód externího přihlášení je neplatný nebo vypršel.", + "WinoAccount_Error_AiPackRequired": "Pro tuto akci je vyžadováno aktivní předplatné Wino AI Pack.", + "WinoAccount_Error_AiQuotaExceeded": "V aktuálním fakturačním období byl dosažen limit využití AI Pack.", + "WinoAccount_Error_AiHtmlEmpty": "Není žádný obsah e-mailu k zpracování.", + "WinoAccount_Error_AiHtmlTooLarge": "Tento e-mail je příliš velký pro zpracování pomocí Wino AI.", + "WinoAccount_Error_AiUnsupportedLanguage": "Tento jazyk není podporován. Zkuste platný kód kultury, např. en-US nebo tr-TR.", + "WinoAccount_Error_Forbidden": "Nemáte oprávnění k provedení této akce.", + "WinoAccount_Error_ValidationFailed": "Požadavek je neplatný. Zkontrolujte prosím zadané hodnoty.", + "WinoAccount_RegisterSuccessMessage": "Registrace účtu Wino byla dokončena pro {0}.", + "WinoAccount_LoginSuccessMessage": "Přihlášení k účtu Wino jako {0}.", + "WinoAccount_EmailConfirmationSentDialog_Title": "Potvrďte svou e-mailovou adresu.", + "WinoAccount_EmailConfirmationSentDialog_Message": "Odeslali jsme potvrzení e-mailem na {0}. Prosím potvrďte jej a zkuste se znovu přihlásit.", + "WinoAccount_EmailConfirmationPendingDialog_Title": "Potvrzení e-mailu vyžadováno.", + "WinoAccount_EmailConfirmationPendingDialog_Message": "Stále čekáme na potvrzení {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ResendButton": "Znovu odeslat potvrzovací e-mail.", + "WinoAccount_EmailConfirmationPendingDialog_Countdown": "Potvrzovací e-mail můžete znovu odeslat za {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ReadyToResend": "Nyní můžete potvrzovací e-mail odeslat znovu.", + "WinoAccount_EmailConfirmationResentDialog_Title": "Potvrzovací e‑mail byl odeslán znovu.", + "WinoAccount_EmailConfirmationResentDialog_Message": "Na {0} jsme odeslali další potvrzovací e‑mail. Prosím potvrďte jej a zkuste se znovu přihlásit.", + "WinoAccount_ForgotPasswordDialog_SuccessTitle": "Odeslán e‑mail pro obnovení hesla.", + "WinoAccount_ForgotPasswordDialog_SuccessMessage": "Odeslali jsme e‑mail pro obnovení hesla na {0}. Otevřete tuto zprávu a zvolte nové heslo.", + "WinoAccount_ChangePassword_Title": "Změnit heslo.", + "WinoAccount_ChangePassword_Description": "Pošlete e‑mail pro obnovení hesla na tento účet Wino.", + "WinoAccount_ChangePassword_Action": "Odeslat e‑mail pro obnovení hesla.", + "WinoAccount_ChangePassword_ConfirmationMessage": "Chcete, aby Wino odeslalo e-mail pro reset hesla na {0}?", + "WinoAccount_SignOut_SuccessMessage": "Odhlášeno z účtu Wino {0}.", + "WinoAccount_SignOut_NoAccountMessage": "Neexistuje žádný aktivní účet Wino pro odhlášení.", + "WinoAccount_Titlebar_SignedOutTitle": "Účet Wino", + "WinoAccount_Titlebar_SignedOutDescription": "Přihlaste se nebo vytvořte účet Wino a spravujte svou relaci Wino.", + "WinoAccount_Titlebar_SignedInStatus": "Stav: {0}", + "WelcomeWizard_Step2Title": "Přidat účet", + "WelcomeWizard_Step3Title": "Dokončit nastavení", + "ProviderSelection_Title": "Zvolte poskytovatele e-mailu", + "ProviderSelection_Subtitle": "Vyberte níže uvedeného poskytovatele a přidejte si svůj e-mailový účet do Wino Mail.", + "ProviderSelection_AccountNameHeader": "Název účtu", + "ProviderSelection_AccountNamePlaceholder": "např. Osobní, Pracovní", + "ProviderSelection_DisplayNameHeader": "Zobrazené jméno", + "ProviderSelection_DisplayNamePlaceholder": "např. Jan Novák", + "ProviderSelection_EmailHeader": "E-mailová adresa", + "ProviderSelection_EmailPlaceholder": "např. johndoe@example.com", + "ProviderSelection_AppPasswordHeader": "Heslo pro aplikaci", + "ProviderSelection_AppPasswordHelp": "Jak získám heslo pro použití v aplikaci?", + "ProviderSelection_CalendarModeHeader": "Integrace kalendáře", + "ProviderSelection_CalendarMode_DisabledTitle": "Vypnuto", + "ProviderSelection_CalendarMode_DisabledDescription": "Žádná integrace kalendáře", + "ProviderSelection_CalendarMode_CalDavTitle": "CalDAV synchronizace", + "ProviderSelection_CalendarMode_CalDavDescription_Apple": "Vaše kalendářní události jsou mezi vašimi zařízeními synchronizovány se servery Apple.", + "ProviderSelection_CalendarMode_CalDavDescription_Yahoo": "Vaše kalendářní události jsou mezi vašimi zařízeními synchronizovány se servery Yahoo.", + "ProviderSelection_CalendarMode_LocalTitle": "Lokální kalendář", + "ProviderSelection_CalendarMode_LocalDescription": "Vaše události jsou uloženy pouze v počítači. Žádné připojení k serverům.", + "ProviderSelection_ClearColor": "Vymazat barvu", + "ProviderSelection_ContinueButton": "Pokračovat", + "ProviderSelection_SpecialImap_Subtitle": "Zadejte údaje o účtu pro připojení.", + "AccountSetup_Title": "Nastavuji váš účet.", + "AccountSetup_Step_Authenticating": "Ověřování s {0}", + "AccountSetup_Step_TestingMailAuth": "Testuji autentizaci e-mailu.", + "AccountSetup_Step_SyncingFolders": "Synchronizuji metadata složek", + "AccountSetup_Step_FetchingProfile": "Načítám informace o profilu", + "AccountSetup_Step_DiscoveringCalDav": "Objevování nastavení CalDAV", + "AccountSetup_Step_TestingCalendarAuth": "Testuji autentizaci kalendáře", + "AccountSetup_Step_SavingAccount": "Ukládám informace o účtu", + "AccountSetup_Step_FetchingCalendarMetadata": "Načítám metadata kalendáře", + "AccountSetup_Step_SyncingAliases": "Synchronizuji aliasy", + "AccountSetup_Step_Finalizing": "Dokončuji nastavení", + "AccountSetup_FailureMessage": "Nastavení selhalo. Vraťte se zpět a opravte nastavení, nebo to zkuste později.", + "AccountSetup_SuccessMessage": "Váš účet byl úspěšně nastaven!", + "AccountSetup_GoBackButton": "Zpět", + "AccountSetup_TryAgainButton": "Zkusit znovu", + "ImapCalDavSettings_AutoDiscoveryFailed": "Automatické zjištění selhalo. Zadejte nastavení ručně na kartě Pokročilé." } - - diff --git a/Wino.Core.Domain/Translations/da_DK/resources.json b/Wino.Core.Domain/Translations/da_DK/resources.json index 663580b2..4e841d23 100644 --- a/Wino.Core.Domain/Translations/da_DK/resources.json +++ b/Wino.Core.Domain/Translations/da_DK/resources.json @@ -1,182 +1,324 @@ { "AccountAlias_Column_Alias": "Alias", - "AccountAlias_Column_IsPrimaryAlias": "Primær", - "AccountAlias_Column_Verified": "Bekræftetag", - "AccountAlias_Disclaimer_FirstLine": "Wino kan kun importere aliaser fra dine Gmail kontoer.", - "AccountAlias_Disclaimer_SecondLine": "Hvis du gerne vil bruge aliaser på din Outlook eller IMAP konto, skal du tilføje dem selv.", - "AccountCacheReset_Title": "Nulstil Kontocache", - "AccountCacheReset_Message": "Denne konto kræver fuld gensynkronisering for at fortsætte. Vent venligst, mens Wino gensynkroniserer dine beskeder...", - "AccountContactNameYou": "Dig", - "AccountCreationDialog_Completed": "Alt er udført", - "AccountCreationDialog_FetchingEvents": "Henter kalenderbegivenheder.", - "AccountCreationDialog_FetchingProfileInformation": "Henter profiloplysninger.", - "AccountCreationDialog_GoogleAuthHelpClipboardText_Row0": "Hvis din browser ikke åbnede automatisk, for at fuldføre autentificeringen:", - "AccountCreationDialog_GoogleAuthHelpClipboardText_Row1": "1) Klik på knappen nedenfor for at kopiere godkendelsesadressen", - "AccountCreationDialog_GoogleAuthHelpClipboardText_Row2": "2) Åbn din webbrowser (Edge, Chrome, Firefox, osv...)", - "AccountCreationDialog_GoogleAuthHelpClipboardText_Row3": "3) Indsæt den kopierede adresse og gå til hjemmesiden for at fuldføre godkendelse manuelt.", - "AccountCreationDialog_Initializing": "initialisere", - "AccountCreationDialog_PreparingFolders": "Vi henter mappeinformation i øjeblikket.", - "AccountCreationDialog_SigninIn": "Kontooplysninger gemmes.", - "AccountEditDialog_Message": "Kontonavn", - "AccountEditDialog_Title": "Rediger Konto", - "AccountPickerDialog_Title": "Vælg en konto", - "AccountSettingsDialog_AccountName": "Afsenders Visningsnavn", - "AccountSettingsDialog_AccountNamePlaceholder": "f.eks. Jens Jensen", - "AccountDetailsPage_Title": "Kontooplysninger", - "AccountDetailsPage_Description": "Ændr navnet på kontoen i Wino og angiv ønsket afsendernavn.", - "AccountDetailsPage_ColorPicker_Title": "Kontofarve", - "AccountDetailsPage_ColorPicker_Description": "Tilknyt en ny kontofarve til at farve symbolet i listen.", - "AddHyperlink": "Tilføj", - "AppCloseBackgroundSynchronizationWarningTitle": "Baggrundssynkronisering", - "AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Applikationen er ikke indstillet til at starte ved Windows start.", - "AppCloseStartupLaunchDisabledWarningMessageSecondLine": "Dette vil få dig til at gå glip af notifikationer, når du genstarter din computer.", - "AppCloseStartupLaunchDisabledWarningMessageThirdLine": "Ønsker du at gå til siden Præferencer for at aktivere det?", - "AppCloseTerminateBehaviorWarningMessageFirstLine": "Du er ved at lukke Wino Mail, og din app-lukningsindstilling er sat til ‘Afslut’.", - "AppCloseTerminateBehaviorWarningMessageSecondLine": "Dette vil stoppe alle baggrundssynkroniseringer og notifikationer.", - "AppCloseTerminateBehaviorWarningMessageThirdLine": "Vil du gå til App-præferencer for at indstille Wino Mail til at køre minimeret eller i baggrunden?", - "AutoDiscoveryProgressMessage": "Søger efter indstillinger for mail...", - "BasicIMAPSetupDialog_AdvancedConfiguration": "Avanceret opsætning", - "BasicIMAPSetupDialog_CredentialLocalMessage": "Dine legitimationsoplysninger gemmes kun lokalt på din computer.", - "BasicIMAPSetupDialog_Description": "Nogle konti kræver yderligere trin for at logge på", - "BasicIMAPSetupDialog_DisplayName": "Visningsnavn", - "BasicIMAPSetupDialog_DisplayNamePlaceholder": "f.eks. Jens Jensen", - "BasicIMAPSetupDialog_LearnMore": "Læs mere", - "BasicIMAPSetupDialog_MailAddress": "E-mail-adresse", - "BasicIMAPSetupDialog_MailAddressPlaceholder": "jensjensen@eksempel.dk", - "BasicIMAPSetupDialog_Password": "Adgangskode", - "BasicIMAPSetupDialog_Title": "IMAP-konto", - "Busy": "Optaget", - "Buttons_AddAccount": "Tilføj Konto", - "Buttons_AddNewAlias": "Tilføj et nyt alias", - "Buttons_Allow": "Tillad", - "Buttons_ApplyTheme": "", - "Buttons_Browse": "Gennemse", - "Buttons_Cancel": "Annuller", - "Buttons_Close": "Luk", - "Buttons_Copy": "Kopier", - "Buttons_Create": "Opret", - "Buttons_CreateAccount": "Opret konto", - "Buttons_Delete": "Slet", - "Buttons_Deny": "Afvis", - "Buttons_Discard": "Kassér", - "Buttons_Edit": "Rediger", - "Buttons_EnableImageRendering": "Aktivér", - "Buttons_Multiselect": "Vælg Flere", - "Buttons_No": "Nej", - "Buttons_Open": "Åbn", - "Buttons_Purchase": "Køb", - "Buttons_RateWino": "Bedøm Wino", - "Buttons_Reset": "Nulstil", - "Buttons_Save": "Gem", - "Buttons_SaveConfiguration": "Gem konfiguration", + "AccountAlias_Column_IsPrimaryAlias": "Primary", + "AccountAlias_Column_Verified": "Verified", + "AccountAlias_Disclaimer_FirstLine": "Wino can only import aliases for your Gmail accounts.", + "AccountAlias_Disclaimer_SecondLine": "If you want to use aliases for your Outlook or IMAP account, please add them yourself.", + "AccountCacheReset_Title": "Account Cache Reset", + "AccountCacheReset_Message": "This account requires full re-sychronization to continue working. Please wait while Wino re-synchronizes your messages...", + "AccountContactNameYou": "You", + "AccountCreationDialog_Completed": "all done", + "AccountCreationDialog_FetchingCalendarMetadata": "Henter kalenderoplysninger.", + "AccountCreationDialog_FetchingEvents": "Fetching calendar events.", + "AccountCreationDialog_FetchingProfileInformation": "Fetching profile details.", + "AccountCreationDialog_GoogleAuthHelpClipboardText_Row0": "If your browser did not launch automatically to complete authentication:", + "AccountCreationDialog_GoogleAuthHelpClipboardText_Row1": "1) Click the button below to copy the authentication address", + "AccountCreationDialog_GoogleAuthHelpClipboardText_Row2": "2) Launch your web browser (Edge, Chrome, Firefox etc...)", + "AccountCreationDialog_GoogleAuthHelpClipboardText_Row3": "3) Paste the copied address and go to the website to complete authentication manually.", + "AccountCreationDialog_Initializing": "initializing", + "AccountCreationDialog_PreparingFolders": "We are getting folder information at the moment.", + "AccountCreationDialog_SigninIn": "Account information is being saved.", + "Purchased": "Købt", + "AccountEditDialog_Message": "Account Name", + "AccountEditDialog_Title": "Edit Account", + "AccountPickerDialog_Title": "Pick an account", + "AccountSettingsDialog_AccountName": "Sender Display Name", + "AccountSettingsDialog_AccountNamePlaceholder": "eg. John Doe", + "AccountDetailsPage_Title": "Account info", + "AccountDetailsPage_Description": "Change the name of the account in Wino and set desired sender name.", + "AccountDetailsPage_ColorPicker_Title": "Account color", + "AccountDetailsPage_ColorPicker_Description": "Assign a new account color to colorize its symbol in the list.", + "AccountDetailsPage_TabGeneral": "Generelt", + "AccountDetailsPage_TabMail": "Mail", + "AccountDetailsPage_TabCalendar": "Kalender", + "AccountDetailsPage_CalendarListDescription": "Vælg en kalender for at konfigurere dens indstillinger.", + "AddHyperlink": "Add", + "AppCloseBackgroundSynchronizationWarningTitle": "Background Synchronization", + "AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Application has not been set to launch on Windows startup.", + "AppCloseStartupLaunchDisabledWarningMessageSecondLine": "This will cause you to miss notifications when you restart your computer.", + "AppCloseStartupLaunchDisabledWarningMessageThirdLine": "Do you want to go to App Preferences page to enable it?", + "AppCloseTerminateBehaviorWarningMessageFirstLine": "You are terminating Wino Mail and your app close behavior is set to 'Terminate'.", + "AppCloseTerminateBehaviorWarningMessageSecondLine": "This will stop all background synchronizations and notifications.", + "AppCloseTerminateBehaviorWarningMessageThirdLine": "Do you want to go to App Preferences to set Wino Mail to run minimized or in the background?", + "AutoDiscoveryProgressMessage": "Searching for mail settings...", + "BasicIMAPSetupDialog_AdvancedConfiguration": "Advanced Configuration", + "BasicIMAPSetupDialog_CredentialLocalMessage": "Your credentials will only be stored locally on your computer.", + "BasicIMAPSetupDialog_Description": "Some accounts require additional steps to sign in", + "BasicIMAPSetupDialog_DisplayName": "Display Name", + "BasicIMAPSetupDialog_DisplayNamePlaceholder": "eg. John Doe", + "BasicIMAPSetupDialog_LearnMore": "Learn more", + "BasicIMAPSetupDialog_MailAddress": "E-Mail Address", + "BasicIMAPSetupDialog_MailAddressPlaceholder": "johndoe@fabrikam.com", + "BasicIMAPSetupDialog_Password": "Password", + "BasicIMAPSetupDialog_Title": "IMAP Account", + "Busy": "Busy", + "Buttons_AddAccount": "Add Account", + "Buttons_FixAccount": "Ret kontoen", + "Buttons_AddNewAlias": "Add New Alias", + "Buttons_Allow": "Allow", + "Buttons_Apply": "Anvend", + "Buttons_ApplyTheme": "Apply Theme", + "Buttons_Browse": "Browse", + "Buttons_Cancel": "Cancel", + "Buttons_Close": "Close", + "Buttons_Copy": "Copy", + "Buttons_Create": "Create", + "Buttons_CreateAccount": "Create Account", + "Buttons_Delete": "Delete", + "Buttons_Deny": "Deny", + "Buttons_Discard": "Discard", + "Buttons_Edit": "Edit", + "Buttons_EnableImageRendering": "Enable", + "Buttons_Multiselect": "Select Multiple", + "Buttons_Manage": "Administrer", + "Buttons_No": "No", + "Buttons_Open": "Open", + "Buttons_Purchase": "Purchase", + "Buttons_RateWino": "Rate Wino", + "Buttons_Reset": "Reset", + "Buttons_Save": "Save", + "Buttons_SaveConfiguration": "Save Configuration", "Buttons_Send": "Send", - "Buttons_Share": "Del", - "Buttons_SignIn": "Log ind", - "Buttons_Sync": "Synkronisér", - "Buttons_SyncAliases": "Synkronisér Aliaser", - "Buttons_TryAgain": "Prøv igen", - "Buttons_Yes": "Ja", - "CalendarAllDayEventSummary": "heldagsbegivenheder", - "CalendarDisplayOptions_Color": "Farve", - "CalendarDisplayOptions_Expand": "Udvid", - "CalendarItem_DetailsPopup_JoinOnline": "Tilmeld dig online", - "CalendarItem_DetailsPopup_ViewEventButton": "Vis begivenhed", - "CalendarItem_DetailsPopup_ViewSeriesButton": "Vis serie", - "CalendarItemAllDay": "hele dagen", - "CategoriesFolderNameOverride": "Kategorier", - "Center": "Centrer", - "ClipboardTextCopied_Message": "{0} kopierede til udklipsholderen.", - "ClipboardTextCopied_Title": "Kopieret", - "ClipboardTextCopyFailed_Message": "Kunne ikke kopiere {0} til udklipsholderen.", - "ComingSoon": "Kommer snart...", - "ComposerAttachmentsDragDropAttach_Message": "Vedhæft", - "ComposerAttachmentsDropZone_Message": "Slip dine filer her", - "ComposerFrom": "Fra:", - "ComposerImagesDropZone_Message": "Slip dine billeder her", - "ComposerSubject": "Emne: ", - "ComposerTo": "Til: ", - "ComposerToPlaceholder": "klik enter for at indtaste adresser", - "CreateAccountAliasDialog_AliasAddress": "Adresse", - "CreateAccountAliasDialog_AliasAddressPlaceholder": "f.eks. kontakt@mitdomæne.dk", - "CreateAccountAliasDialog_Description": "Sørg for at din udgående server tillader at sende mails fra dette alias.", - "CreateAccountAliasDialog_ReplyToAddress": "Svar-til-adresse", - "CreateAccountAliasDialog_ReplyToAddressPlaceholder": "admin@mitdomæne.dk", - "CreateAccountAliasDialog_Title": "Opret Kontoalias", - "CustomThemeBuilder_AccentColorDescription": "Indstil brugerdefineret accentfarve, hvis du ønsker det. Hvis du ikke vælger en farve, bruges din Windows-accentfarve.", - "CustomThemeBuilder_AccentColorTitle": "Accentfarve", - "CustomThemeBuilder_PickColor": "Vælg", - "CustomThemeBuilder_ThemeNameDescription": "Unikt navn til dit brugerdefinerede tema.", - "CustomThemeBuilder_ThemeNameTitle": "Temanavn", - "CustomThemeBuilder_Title": "Tilpasset Temabygger", - "CustomThemeBuilder_WallpaperDescription": "Angiv et brugerdefineret baggrundsbillede til Wino", - "CustomThemeBuilder_WallpaperTitle": "Indstil brugerdefineret baggrund", - "Dialog_DontAskAgain": "Spørg ikke igen", - "DialogMessage_AccountLimitMessage": "Du har nået grænsen for kontooprettelse.\nVil du købe tilføjelsen 'Ubegrænset konto' for at fortsætte?", - "DialogMessage_AccountLimitTitle": "Kontogrænse Nået", - "DialogMessage_AliasCreatedMessage": "Dit nye alias er oprettet.", - "DialogMessage_AliasCreatedTitle": "Oprettede Nyt Alias", - "DialogMessage_AliasExistsMessage": "Dette alias er allerede i brug.", - "DialogMessage_AliasExistsTitle": "Eksisterende Alias", - "DialogMessage_AliasNotSelectedMessage": "Du skal vælge et alias, før du sender en besked.", - "DialogMessage_AliasNotSelectedTitle": "Mangler Alias", - "DialogMessage_CantDeleteRootAliasMessage": "Grundalias kan ikke slettes. Dette er din primære identitet tilknyttet din konto.", - "DialogMessage_CantDeleteRootAliasTitle": "Kan ikke slette alias", - "DialogMessage_CleanupFolderMessage": "Vil du permanent slette alle mails i denne mappe?", - "DialogMessage_CleanupFolderTitle": "Oprydningsmappe", - "DialogMessage_ComposerMissingRecipientMessage": "Beskeden har ingen modtager.", - "DialogMessage_ComposerValidationFailedTitle": "Godkendelse mislykkedes", - "DialogMessage_CreateLinkedAccountMessage": "Giv dette nye link et navn. Konti vil blive flettet under dette navn.", - "DialogMessage_CreateLinkedAccountTitle": "Konto Linknavn", - "DialogMessage_DeleteAccountConfirmationMessage": "Slet {0}?", - "DialogMessage_DeleteAccountConfirmationTitle": "Alle data, der er forbundet med denne konto, vil blive slettet fra disken permanent.", - "DialogMessage_DiscardDraftConfirmationMessage": "Dette udkast vil blive kasseret. Vil du fortsætte?", - "DialogMessage_DiscardDraftConfirmationTitle": "Kassér udkast", - "DialogMessage_EmptySubjectConfirmation": "Mangler Emne", - "DialogMessage_EmptySubjectConfirmationMessage": "Beskeden har intet emne. Vil du fortsætte?", - "DialogMessage_EnableStartupLaunchDeniedMessage": "Du kan aktivere opstart fra Indstillinger -> App-præferencer.", - "DialogMessage_EnableStartupLaunchMessage": "Lad Wino Mail automatisk starte minimeret ved Windows-opstart, så du ikke går glip af nogen notifikationer.\n\nVil du aktivere automatisk opstart?", - "DialogMessage_EnableStartupLaunchTitle": "Aktiver automatisk opstart", - "DialogMessage_HardDeleteConfirmationMessage": "Slet Permanent", - "DialogMessage_HardDeleteConfirmationTitle": "Besked(er) vil blive slettet permanent. Vil du fortsætte?", - "DialogMessage_InvalidAliasMessage": "Dette alias er ikke gyldigt. Sørg for, at alle adresser på aliasset er gyldige e-mailadresser.", - "DialogMessage_InvalidAliasTitle": "Ugyldigt Alias", - "DialogMessage_NoAccountsForCreateMailMessage": "Du har ikke nogen konti at oprette en besked fra.", - "DialogMessage_NoAccountsForCreateMailTitle": "Konto mangler", - "DialogMessage_PrintingFailedMessage": "Kunne ikke udskrive denne mail. Resultat: {0}", - "DialogMessage_PrintingFailedTitle": "Mislykkedes", - "DialogMessage_PrintingSuccessMessage": "Mail er sendt til printer.", - "DialogMessage_PrintingSuccessTitle": "Succes", - "DialogMessage_RenameFolderMessage": "Indtast nyt navn for denne mappe", - "DialogMessage_RenameFolderTitle": "Omdøb mappe", - "DialogMessage_RenameLinkedAccountsMessage": "Indtast nyt navn for den tilknyttede konto", - "DialogMessage_RenameLinkedAccountsTitle": "Omdøb den tilknyttede konto", - "DialogMessage_UnlinkAccountsConfirmationMessage": "Denne handling sletter ikke dine konti, men fjerner kun forbindelsen for delte mapper. Vil du fortsætte?", - "DialogMessage_UnlinkAccountsConfirmationTitle": "Fjern kontotilknytning", - "DialogMessage_UnsubscribeConfirmationGoToWebsiteConfirmButton": "Gå til hjemmesiden", - "DialogMessage_UnsubscribeConfirmationGoToWebsiteMessage": "For at stoppe med at modtage beskeder fra {0}, gå til deres hjemmeside for at afmelde.", - "DialogMessage_UnsubscribeConfirmationMailtoMessage": "Vil du stoppe med at modtage beskeder fra {0}? Wino vil afmelde dig ved at sende en e-mail fra din e-mail-konto til {1}.", - "DialogMessage_UnsubscribeConfirmationOneClickMessage": "Vil du stoppe med at modtage beskeder fra {0}?", - "DialogMessage_UnsubscribeConfirmationTitle": "Afmeld", - "DiscordChannelDisclaimerMessage": "Wino har ikke sin egen Discord-server, men en specialkanal ‘wino-mail’ findes på ‘Developer Sanctuary’-serveren.\nFor at få opdateringer om Wino, join venligst Developer Sanctuary-serveren og følg ‘wino-mail’-kanalen under ‘Community Projects’.\n\nDu vil blive ført til serverens URL, da Discord ikke understøtter kanalinvitationer.", - "DiscordChannelDisclaimerTitle": "Vigtig Discord Information", - "Draft": "Udkast", - "DragMoveToFolderCaption": "Flyt til {0}", - "EditorToolbarOption_Draw": "Tegn", - "EditorToolbarOption_Format": "Formatér", - "EditorToolbarOption_Insert": "Indsæt", - "EditorToolbarOption_None": "Ingen", - "EditorToolbarOption_Options": "Valgmuligheder", - "EditorTooltip_WebViewEditor": "Brug webvisningseditor", - "ElementTheme_Dark": "Mørk tilstand", - "ElementTheme_Default": "Brug systemindstilling", - "ElementTheme_Light": "Lys tilstand", + "Buttons_SendToServer": "Send til server", + "Buttons_Share": "Share", + "Buttons_SignIn": "Sign In", + "Buttons_Sync": "Synchronize", + "Buttons_SyncAliases": "Synchronize Aliases", + "Buttons_TryAgain": "Try Again", + "Buttons_Yes": "Yes", + "Sync_SynchronizingFolder": "Synkroniserer {0} {1}%", + "Sync_DownloadedMessages": "Hentede {0} beskeder fra {1}", + "SyncAction_Archiving": "Arkiverer {0} mail", + "SyncAction_ClearingFlag": "Fjerner flag for {0} mail", + "SyncAction_CreatingDraft": "Opretter kladde", + "SyncAction_CreatingEvent": "Opretter begivenhed", + "SyncAction_Deleting": "Sletter {0} mail", + "SyncAction_EmptyingFolder": "Tømmer mappe", + "SyncAction_MarkingAsRead": "Markerer {0} mail som læst", + "SyncAction_MarkingAsUnread": "Markerer {0} mail som ulæst", + "SyncAction_MarkingFolderAsRead": "Markerer mappe som læst", + "SyncAction_Moving": "Flytter {0} mail", + "SyncAction_MovingToFocused": "Flytter {0} mail til Focused", + "SyncAction_RenamingFolder": "Omdøbning af mappe", + "SyncAction_SendingMail": "Sender mail", + "SyncAction_SettingFlag": "Markerer {0} mail", + "SyncAction_SynchronizingAccount": "Synkroniserer {0}", + "SyncAction_SynchronizingAccounts": "Synkroniserer {0} konti", + "SyncAction_SynchronizingCalendarData": "Synkroniserer kalenderdata", + "SyncAction_SynchronizingCalendarEvents": "Synkroniserer kalenderbegivenheder", + "SyncAction_SynchronizingCalendarMetadata": "Synkroniserer kalendermetadata", + "SyncAction_Unarchiving": "Fjerner arkivering af {0} mail", + "CalendarAllDayEventSummary": "all-day events", + "CalendarDisplayOptions_Color": "Color", + "CalendarDisplayOptions_Expand": "Expand", + "CalendarEventResponse_Accept": "Accepter", + "CalendarEventResponse_AcceptedResponse": "Accepteret", + "CalendarEventResponse_Decline": "Afvis", + "CalendarEventResponse_DeclinedResponse": "Afvist", + "CalendarEventResponse_NotResponded": "Ikke besvaret", + "CalendarEventResponse_Tentative": "Foreløbig", + "CalendarEventResponse_TentativeResponse": "Foreløbig", + "CalendarEventRsvpPanel_Accept": "Accepter", + "CalendarEventRsvpPanel_AddMessage": "Tilføj en besked til dit svar... (valgfrit)", + "CalendarEventRsvpPanel_Decline": "Afvist", + "CalendarEventRsvpPanel_Message": "Besked", + "CalendarEventRsvpPanel_SendReplyMessage": "Send svarbesked", + "CalendarEventRsvpPanel_Tentative": "Foreløbig", + "CalendarEventRsvpPanel_Title": "Svarmuligheder", + "CalendarAttendeeStatus_Accepted": "Accepteret", + "CalendarAttendeeStatus_Declined": "Afvist", + "CalendarAttendeeStatus_NeedsAction": "Krever handling", + "CalendarAttendeeStatus_Tentative": "Foreløbig", + "CalendarEventDetails_Attachments": "Vedhæftede filer", + "CalendarEventCompose_AddAttachment": "Tilføj vedhæftet fil", + "CalendarEventCompose_AllDay": "Hele dagen", + "CalendarEventCompose_AttachmentsNotSupportedForCalDav": "Vedhæftede filer understøttes ikke for CalDAV-kalendere.", + "CalendarEventCompose_EndDate": "Slutdato", + "CalendarEventCompose_EndTime": "Sluttid", + "CalendarEventCompose_Every": "hver", + "CalendarEventCompose_ForWeekdays": "for", + "CalendarEventCompose_FrequencyDay": "dag", + "CalendarEventCompose_FrequencyDayPlural": "dage", + "CalendarEventCompose_FrequencyMonth": "måned", + "CalendarEventCompose_FrequencyMonthPlural": "måneder", + "CalendarEventCompose_FrequencyWeek": "uge", + "CalendarEventCompose_FrequencyWeekPlural": "uger", + "CalendarEventCompose_FrequencyYear": "år", + "CalendarEventCompose_FrequencyYearPlural": "år", + "CalendarEventCompose_Location": "Placering", + "CalendarEventCompose_LocationPlaceholder": "Tilføj en placering", + "CalendarEventCompose_NewEventButton": "Ny begivenhed", + "CalendarEventCompose_DefaultCalendarHint": "Du kan vælge en standardkalender for nye begivenheder i Kalenderindstillinger.", + "CalendarEventCompose_DefaultCalendarSettingsLink": "Åbn Kalenderindstillinger", + "CalendarEventCompose_NoCalendarsMessage": "Der er endnu ingen kalendere til rådighed til oprettelse af begivenheder.", + "CalendarEventCompose_NoCalendarsTitle": "Ingen kalendere tilgængelige", + "CalendarEventCompose_NoEndDate": "Ingen slutdato", + "CalendarEventCompose_Notes": "Noter", + "CalendarEventCompose_PickCalendarTitle": "Vælg en kalender", + "CalendarEventCompose_Recurring": "Gentagende", + "CalendarEventCompose_RecurringSummary": "Forekommer hver {0} {1}{2} {3} gyldig {4}{5}", + "CalendarEventCompose_RecurringSummarySmart": "Forekommer {0}{1} {2} gyldig {3}{4}", + "CalendarEventCompose_RepeatEvery": "Gentag hver", + "CalendarEventCompose_SelectCalendar": "Vælg kalender", + "CalendarEventCompose_SingleOccurrenceSummary": "Forekommer den {0} {1}", + "CalendarEventCompose_StartDate": "Startdato", + "CalendarEventCompose_StartTime": "Starttid", + "CalendarEventCompose_TimeRangeSummary": "fra {0} til {1}", + "CalendarEventCompose_Title": "Begivenhedstitel", + "CalendarEventCompose_TitlePlaceholder": "Tilføj en titel", + "CalendarEventCompose_Until": "indtil", + "CalendarEventCompose_UntilSummary": " indtil {0}", + "CalendarEventCompose_ValidationInvalidAllDayRange": "Slutdatoen for hele dagen skal være efter startdatoen.", + "CalendarEventCompose_ValidationInvalidAttendee": "Én eller flere deltagere har en ugyldig e-mailadresse.", + "CalendarEventCompose_ValidationInvalidRecurrenceEnd": "Gentagelsens slutdato skal være på eller efter begivenhedens startdato.", + "CalendarEventCompose_ValidationInvalidTimeRange": "Sluttiden skal være senere end starttiden.", + "CalendarEventCompose_ValidationMissingAttachment": "Én eller flere vedhæftede filer er ikke længere tilgængelige: {0}", + "CalendarEventCompose_ValidationMissingCalendar": "Vælg en kalender før oprettelse af begivenheden.", + "CalendarEventCompose_ValidationMissingTitle": "Indtast en begivenhedstitel før oprettelsen af begivenheden.", + "CalendarEventCompose_ValidationTitle": "Begivenhedsvalidering mislykkedes", + "CalendarEventCompose_WeekdaySummary": " på {0}", + "CalendarEventCompose_Weekday_Friday": "F", + "CalendarEventCompose_Weekday_Monday": "M", + "CalendarEventCompose_Weekday_Saturday": "Lør", + "CalendarEventCompose_Weekday_Sunday": "Søn", + "CalendarEventCompose_Weekday_Thursday": "Tor", + "CalendarEventCompose_Weekday_Tuesday": "Tir", + "CalendarEventCompose_Weekday_Wednesday": "Ons", + "CalendarEventDetails_Details": "Detaljer", + "CalendarEventDetails_EditSeries": "Rediger serie", + "CalendarEventDetails_Editing": "Redigerer", + "CalendarEventDetails_InviteSomeone": "Inviter nogen", + "CalendarEventDetails_JoinOnline": "Deltag online", + "CalendarEventDetails_Organizer": "Arrangør", + "CalendarEventDetails_People": "Deltagere", + "CalendarEventDetails_ReadOnlyEvent": "Skrivebeskyttet begivenhed", + "CalendarEventDetails_Reminder": "Påmindelse", + "CalendarReminder_StartedHoursAgo": "Startede {0} timer siden", + "CalendarReminder_StartedMinutesAgo": "Startede {0} minutter siden", + "CalendarReminder_StartedNow": "Startede netop nu", + "CalendarReminder_StartingNow": "Starter nu", + "CalendarReminder_StartsInHours": "Starter om {0} timer", + "CalendarReminder_StartsInMinutes": "Starter om {0} minutter", + "CalendarReminder_SnoozeAction": "Udsæt", + "CalendarReminder_SnoozeMinutesOption": "{0} minutter", + "CalendarEventDetails_ShowAs": "Vis som", + "CalendarShowAs_Free": "Ledig", + "CalendarShowAs_Tentative": "Foreløbig", + "CalendarShowAs_Busy": "Optaget", + "CalendarShowAs_OutOfOffice": "Ude af kontoret", + "CalendarShowAs_WorkingElsewhere": "Arbejder et andet sted", + "CalendarItem_DetailsPopup_JoinOnline": "Join online", + "CalendarItem_DetailsPopup_ViewEventButton": "View event", + "CalendarItem_DetailsPopup_ViewSeriesButton": "View series", + "CalendarItemAllDay": "all day", + "CategoriesFolderNameOverride": "Categories", + "Center": "Center", + "ClipboardTextCopied_Message": "{0} copied to clipboard.", + "ClipboardTextCopied_Title": "Copied", + "ClipboardTextCopyFailed_Message": "Failed to copy {0} to clipboard.", + "ContactInfoBar_ErrorTitle": "Kunne ikke indlæse kontaktoplysninger", + "ContactInfoBar_SuccessTitle": "Kontaktoplysninger indlæst", + "ContactInfoBar_WarningTitle": "Kontaktoplysningerne kan være ufuldstændige", + "ComingSoon": "Coming soon...", + "ComposerAttachmentsDragDropAttach_Message": "Attach", + "ComposerAttachmentsDropZone_Message": "Drop your files here", + "ComposerFrom": "From: ", + "ComposerImagesDropZone_Message": "Drop your images here", + "ComposerSubject": "Subject: ", + "ComposerTo": "To: ", + "ComposerToPlaceholder": "click enter to input addresses", + "CreateAccountAliasDialog_AliasAddress": "Address", + "CreateAccountAliasDialog_AliasAddressPlaceholder": "eg. support@mydomain.com", + "CreateAccountAliasDialog_Description": "Make sure your outgoing server allows sending mails from this alias.", + "CreateAccountAliasDialog_ReplyToAddress": "Reply-To Address", + "CreateAccountAliasDialog_ReplyToAddressPlaceholder": "admin@mydomain.com", + "CreateAccountAliasDialog_Title": "Create Account Alias", + "CustomThemeBuilder_AccentColorDescription": "Set custom accent color if you wish. Not selecting a color will use your Windows accent color.", + "CustomThemeBuilder_AccentColorTitle": "Accent color", + "CustomThemeBuilder_PickColor": "Pick", + "CustomThemeBuilder_ThemeNameDescription": "Unique name for your custom theme.", + "CustomThemeBuilder_ThemeNameTitle": "Theme name", + "CustomThemeBuilder_Title": "Custom Theme Builder", + "CustomThemeBuilder_WallpaperDescription": "Set a custom wallpaper for Wino", + "CustomThemeBuilder_WallpaperTitle": "Set custom wallpaper", + "Dialog_DontAskAgain": "Don't ask again", + "DialogMessage_AccountLimitMessage": "You have reached the account creation limit.\nWould you like to purchase 'Unlimited Account' add-on to continue?", + "DialogMessage_AccountLimitTitle": "Account Limit Reached", + "DialogMessage_AliasCreatedMessage": "New alias is succesfully created.", + "DialogMessage_AliasCreatedTitle": "Created New Alias", + "DialogMessage_AliasExistsMessage": "This alias is already in use.", + "DialogMessage_AliasExistsTitle": "Existing Alias", + "DialogMessage_AliasNotSelectedMessage": "You must select an alias before sending a message.", + "DialogMessage_AliasNotSelectedTitle": "Missing Alias", + "DialogMessage_CantDeleteRootAliasMessage": "Root alias can't be deleted. This is your main identity associated with your account setup.", + "DialogMessage_CantDeleteRootAliasTitle": "Can't Delete Alias", + "DialogMessage_CleanupFolderMessage": "Do you want to permanently delete all the mails in this folder?", + "DialogMessage_CleanupFolderTitle": "Cleanup Folder", + "DialogMessage_ComposerMissingRecipientMessage": "Message has no recipient.", + "DialogMessage_ComposerValidationFailedTitle": "Validation Failed", + "DialogMessage_CreateLinkedAccountMessage": "Give this new link a name. Accounts will be merged under this name.", + "DialogMessage_CreateLinkedAccountTitle": "Account Link Name", + "DialogMessage_DeleteAccountConfirmationMessage": "Delete {0}?", + "DialogMessage_DeleteAccountConfirmationTitle": "All data associated with this account will be deleted from disk permanently.", + "DialogMessage_DeleteEmailTemplateConfirmationMessage": "Slet skabelon \\\"{0}\\\"?", + "DialogMessage_DeleteEmailTemplateConfirmationTitle": "Slet e-mail-skabelon", + "DialogMessage_DeleteRecurringSeriesMessage": "Dette vil slette alle begivenheder i serien. Vil du fortsætte?", + "DialogMessage_DeleteRecurringSeriesTitle": "Slet tilbagevendende serie", + "DialogMessage_DiscardDraftConfirmationMessage": "This draft will be discarded. Do you want to continue?", + "DialogMessage_DiscardDraftConfirmationTitle": "Discard Draft", + "DialogMessage_EmptySubjectConfirmation": "Missing Subject", + "DialogMessage_EmptySubjectConfirmationMessage": "Message has no subject. Do you want to continue?", + "DialogMessage_EnableStartupLaunchDeniedMessage": "You can enable startup launch from Settings -> App Preferences.", + "DialogMessage_EnableStartupLaunchMessage": "Let Wino Mail automatically launch minimized on Windows startup to not miss any notifications.\n\nDo you want to enable startup launch?", + "DialogMessage_EnableStartupLaunchTitle": "Enable Startup Launch", + "DialogMessage_HardDeleteConfirmationMessage": "Permanent Delete", + "DialogMessage_HardDeleteConfirmationTitle": "Message(s) will be permanently deleted. Do you want to continue?", + "DialogMessage_InvalidAliasMessage": "This alias is not valid. Make sure all addresses of the alias are valid e-mail addresses.", + "DialogMessage_InvalidAliasTitle": "Invalid Alias", + "DialogMessage_NoAccountsForCreateMailMessage": "You don't have any accounts to create message from.", + "DialogMessage_NoAccountsForCreateMailTitle": "Account Missing", + "DialogMessage_PrintingFailedMessage": "Failed to print this mail. Result: {0}", + "DialogMessage_PrintingFailedTitle": "Failed", + "DialogMessage_PrintingSuccessMessage": "Mail is sent to printer.", + "DialogMessage_PrintingSuccessTitle": "Success", + "DialogMessage_RenameFolderMessage": "Enter new name for this folder", + "DialogMessage_RenameFolderTitle": "Rename Folder", + "DialogMessage_RenameLinkedAccountsMessage": "Enter new name for linked account", + "DialogMessage_RenameLinkedAccountsTitle": "Rename Linked Account", + "DialogMessage_UnlinkAccountsConfirmationMessage": "This operation will not delete your accounts but only break the link for shared folder connections. Do you want to continue?", + "DialogMessage_UnlinkAccountsConfirmationTitle": "Unlink Accounts", + "DialogMessage_UnsubscribeConfirmationGoToWebsiteConfirmButton": "Go to website", + "DialogMessage_UnsubscribeConfirmationGoToWebsiteMessage": "To stop getting messages from {0}, go to their website to unsubscribe.", + "DialogMessage_UnsubscribeConfirmationMailtoMessage": "Do you want to stop getting messages from {0}? Wino will unsubscribe for you by sending an email from your email account to {1}.", + "DialogMessage_UnsubscribeConfirmationOneClickMessage": "Do you want to stop getting messages from {0}?", + "DialogMessage_UnsubscribeConfirmationTitle": "Unsubscribe", + "DiscordChannelDisclaimerMessage": "Wino doesn't have it's own Discord server, but special 'wino-mail' channel is hosted at 'Developer Sanctuary' server.\nTo get the updates about Wino please join Developer Sanctuary server and follow 'wino-mail' channel under 'Community Projects'\n\nYou will be directed to server URL since Discord doesn't support channel invites.", + "DiscordChannelDisclaimerTitle": "Important Discord Information", + "Draft": "Draft", + "DragMoveToFolderCaption": "Move to {0}", + "EditorToolbarOption_Draw": "Draw", + "EditorToolbarOption_Format": "Format", + "EditorToolbarOption_Insert": "Insert", + "EditorToolbarOption_None": "None", + "EditorToolbarOption_Options": "Options", + "EditorTooltip_WebViewEditor": "Use web view editor", + "ElementTheme_Dark": "Dark mode", + "ElementTheme_Default": "Use system setting", + "ElementTheme_Light": "Light mode", "Emoji": "Emoji", - "Error_FailedToSetupSystemFolders_Title": "Kunne ikke opsætte systemmapper", - "Exception_AuthenticationCanceled": "Godkendelsen blev annulleret", - "Exception_CustomThemeExists": "Temaet eksisterer allerede.", - "Exception_CustomThemeMissingName": "Du skal angive et navn.", - "Exception_CustomThemeMissingWallpaper": "Du skal angive et brugerdefineret baggrundsbillede.", + "Error_FailedToSetupSystemFolders_Title": "Failed to setup system folders", + "Exception_AccountNeedsAttention_Title": "Konto kræver opmærksomhed", + "Exception_AccountNeedsAttention_Message": "'{0}' kræver din opmærksomhed for at fortsætte med at arbejde.", + "Exception_WebView2RuntimeMissing_Message": "Wino Mail kunne ikke finde Microsoft Edge WebView2 Runtime. Installer eller reparer runtime'en for at gengive beskedindholdet korrekt.", + "Exception_WebView2RuntimeMissing_Title": "WebView2-runtime kræves", + "Exception_AuthenticationCanceled": "Authentication canceled", + "Exception_CustomThemeExists": "This theme already exists.", + "Exception_CustomThemeMissingName": "You must provide a name.", + "Exception_CustomThemeMissingWallpaper": "You must provide a custom background image.", "Exception_FailedToSynchronizeAliases": "Failed to synchronize aliases", + "Exception_FailedToSynchronizeCalendarData": "Kunne ikke synkronisere kalenderdata", + "Exception_FailedToSynchronizeCalendarEvents": "Kunne ikke synkronisere kalenderbegivenheder", + "Exception_FailedToSynchronizeCalendarMetadata": "Kunne ikke synkronisere kalenderdetaljer", "Exception_FailedToSynchronizeFolders": "Failed to synchronize folders", "Exception_FailedToSynchronizeProfileInformation": "Failed to synchronize profile information", "Exception_GoogleAuthCallbackNull": "Callback uri is null on activation.", @@ -186,11 +328,11 @@ "Exception_GoogleAuthorizationCodeExchangeFailed": "Authorization code exchange failed.", "Exception_ImapAutoDiscoveryFailed": "Couldn't find mailbox settings.", "Exception_ImapClientPoolFailed": "IMAP Client Pool failed.", - "Exception_InboxNotAvailable": "Kunne ikke konfigurere kontomapper.", - "Exception_InvalidSystemFolderConfiguration": "Systemmappekonfiguration er ikke gyldig. Tjek konfigurationen og prøv igen.", - "Exception_InvalidMultiAccountMoveTarget": "Du kan ikke flytte flere elementer, der tilhører forskellige konti i en tilknyttet konto.", - "Exception_MailProcessing": "Denne mail bliver stadig behandlet. Prøv igen om lidt.", - "Exception_MissingAlias": "Primært alias findes ikke for denne konto. Oprettelse af udkast mislykkedes.", + "Exception_InboxNotAvailable": "Couldn't setup account folders.", + "Exception_InvalidSystemFolderConfiguration": "System folder configuration is not valid. Check configuration and try again.", + "Exception_InvalidMultiAccountMoveTarget": "You can't move multiple items that belong to different accounts in linked account.", + "Exception_MailProcessing": "This mail is still being processed. Please try again after few seconds.", + "Exception_MissingAlias": "Primary alias does not exist for this account. Creating draft failed.", "Exception_NullAssignedAccount": "Assigned account is null", "Exception_NullAssignedFolder": "Assigned folder is null", "Exception_SynchronizerFailureHTTP": "Response handling failed with error HTTP code {0}", @@ -198,509 +340,1109 @@ "Exception_TokenInfoRetrivalFailed": "Failed to get token information.", "Exception_UnknowErrorDuringAuthentication": "Unknown error occurred during authentication", "Exception_UnsupportedAction": "Action {0} is not implemented in request processor", - "Exception_UnsupportedProvider": "Denne udbyder er ikke understøttet.", + "Exception_UnsupportedProvider": "This provider is not supported.", "Exception_UnsupportedSynchronizerOperation": "This operation is not supported for {0}", "Exception_UserCancelSystemFolderSetupDialog": "User canceled system folder config dialog.", - "Exception_WinoServerException": "Wino server fejlede.", - "Files": "Filer", - "FilteringOption_All": "Alle", - "FilteringOption_Files": "Har filer", - "FilteringOption_Flagged": "Markerede", - "FilteringOption_Unread": "Ulæste", - "Focused": "Fokuserede", - "FolderOperation_CreateSubFolder": "Opret undermappe", - "FolderOperation_Delete": "Slet", - "FolderOperation_DontSync": "Synkroniser ikke denne mappe", - "FolderOperation_Empty": "Tøm denne mappe", - "FolderOperation_MarkAllAsRead": "Markér alle som læst", - "FolderOperation_Move": "Flyt", - "FolderOperation_None": "Ingen", - "FolderOperation_Pin": "Fastgør", - "FolderOperation_Rename": "Omdøb", - "FolderOperation_Unpin": "Frigør", - "GeneralTitle_Error": "Fejl", + "Exception_WinoServerException": "Wino server failed.", + "Files": "Files", + "FilteringOption_All": "All", + "FilteringOption_Files": "Has files", + "FilteringOption_Flagged": "Flagged", + "FilteringOption_Unread": "Unread", + "Focused": "Focused", + "FolderOperation_CreateSubFolder": "Create sub folder", + "FolderOperation_Delete": "Delete", + "FolderOperation_DontSync": "Don't sync this folder", + "FolderOperation_Empty": "Empty this folder", + "FolderOperation_MarkAllAsRead": "Mark all as read", + "FolderOperation_Move": "Move", + "FolderOperation_None": "None", + "FolderOperation_Pin": "Pin", + "FolderOperation_Rename": "Rename", + "FolderOperation_Unpin": "Unpin", + "GeneralTitle_Error": "Error", "GeneralTitle_Info": "Information", - "GeneralTitle_Warning": "Advarsel", - "GmailServiceDisabled_Title": "Gmail Fejl", - "GmailServiceDisabled_Message": "Din Google Workspace konto ser ud til at være deaktiveret for Gmail service. Kontakt venligst din administrator for at aktivere Gmail service for din konto.", - "GmailArchiveFolderNameOverride": "Arkiv", - "HoverActionOption_Archive": "Arkivér", - "HoverActionOption_Delete": "Slet", - "HoverActionOption_MoveJunk": "Flyt til Spam", - "HoverActionOption_ToggleFlag": "Marker/Fjern markering", - "HoverActionOption_ToggleRead": "Læs/Ulæs", - "ImageRenderingDisabled": "Billedindlæsning er deaktiveret for denne besked.", - "ImapAdvancedSetupDialog_AuthenticationMethod": "Godkendelsesmetode", - "ImapAdvancedSetupDialog_ConnectionSecurity": "Forbindelsessikkerhed", - "IMAPAdvancedSetupDialog_ValidationAuthMethodRequired": "Godkendelsesmetode er påkrævet", - "IMAPAdvancedSetupDialog_ValidationConnectionSecurityRequired": "Sikkerhedstype for forbindelse er påkrævet", - "IMAPAdvancedSetupDialog_ValidationDisplayNameRequired": "Visningsnavn er påkrævet", - "IMAPAdvancedSetupDialog_ValidationEmailInvalid": "Angiv venligst en gyldig e-mailadresse", - "IMAPAdvancedSetupDialog_ValidationEmailRequired": "E-mailadresse er påkrævet", - "IMAPAdvancedSetupDialog_ValidationErrorTitle": "Tjek venligst følgende:", - "IMAPAdvancedSetupDialog_ValidationIncomingPortInvalid": "Indgående port skal være mellem 1-65535", - "IMAPAdvancedSetupDialog_ValidationIncomingPortRequired": "Indgående serverport er påkrævet", - "IMAPAdvancedSetupDialog_ValidationIncomingServerRequired": "Indgående serveradresse er påkrævet", - "IMAPAdvancedSetupDialog_ValidationOutgoingPasswordRequired": "Udgående serveradgangskode er påkrævet", - "IMAPAdvancedSetupDialog_ValidationOutgoingPortInvalid": "Udgående port skal være mellem 1-65535", - "IMAPAdvancedSetupDialog_ValidationOutgoingPortRequired": "Udgående serverport er påkrævet", - "IMAPAdvancedSetupDialog_ValidationOutgoingServerRequired": "Udgående serveradresse er påkrævet", - "IMAPAdvancedSetupDialog_ValidationOutgoingUsernameRequired": "Udgående serverbrugernavn er påkrævet", - "IMAPAdvancedSetupDialog_ValidationPasswordRequired": "Adgangskode er påkrævet", - "IMAPAdvancedSetupDialog_ValidationUsernameRequired": "Brugernavn er påkrævet", - "ImapAuthenticationMethod_Auto": "Automatisk", + "GeneralTitle_Warning": "Warning", + "GmailServiceDisabled_Title": "Gmail Error", + "GmailServiceDisabled_Message": "Your Google Workspace account seems to be disabled for Gmail service. Please contact your administrator to enable Gmail service for your account.", + "GmailArchiveFolderNameOverride": "Archive", + "HoverActionOption_Archive": "Archive", + "HoverActionOption_Delete": "Delete", + "HoverActionOption_MoveJunk": "Move to Junk", + "HoverActionOption_ToggleFlag": "Flag / Unflag", + "HoverActionOption_ToggleRead": "Read / Unread", + "KeyboardShortcuts_FailedToReset": "Nulstilling af tastaturgenveje mislykkedes.", + "KeyboardShortcuts_FailedToUpdate": "Kunne ikke opdatere tastaturgenveje.", + "KeyboardShortcuts_MailoperationAction": "Handling", + "KeyboardShortcuts_Action": "Handling", + "KeyboardShortcuts_FailedToLoad": "Kunne ikke indlæse tastaturgenveje.", + "KeyboardShortcuts_EnterKeyForShortcut": "Indtast venligst en tast til genvejen.", + "KeyboardShortcuts_SelectOperationForShortcut": "Vælg en handling at udføre for genvejen.", + "KeyboardShortcuts_EnterKey": "Indtast venligst en tast til genvejen.", + "KeyboardShortcuts_SelectOperation": "Vælg en handling til genvejen.", + "KeyboardShortcuts_ShortcutInUse": "Denne genvej er allerede i brug af en anden genvej.", + "KeyboardShortcuts_FailedToSave": "Kunne ikke gemme genvejen.", + "KeyboardShortcuts_FailedToDelete": "Kunne ikke slette genvejen.", + "KeyboardShortcuts_PageDescription": "Opret tastaturgenveje til hurtige mail-operationer. Tryk på tasterne, mens fokus er i tastinputfeltet for at fange genvejene.", + "KeyboardShortcuts_Add": "Tilføj genvej", + "KeyboardShortcuts_EditTitle": "Rediger tastaturgenvej", + "KeyboardShortcuts_ResetToDefaults": "Nulstil til standardindstillinger", + "KeyboardShortcuts_PressKeysHere": "Tryk på taster her...", + "KeyboardShortcuts_KeyCombination": "Tastkombination", + "KeyboardShortcuts_FocusArea": "Fokuser feltet ovenfor og tryk på den ønskede tastkombination.", + "KeyboardShortcuts_Modifiers": "Modifikator-taster", + "KeyboardShortcuts_Mode": "App-tilstand", + "KeyboardShortcuts_ModeMail": "Mail", + "KeyboardShortcuts_ModeCalendar": "Kalender", + "KeyboardShortcuts_ActionToggleReadUnread": "Skift læst/ulæst", + "KeyboardShortcuts_ActionToggleFlag": "Skift flag", + "KeyboardShortcuts_ActionToggleArchive": "Arkivér/afarkiver", + "ImageRenderingDisabled": "Image rendering is disabled for this message.", + "ImapAdvancedSetupDialog_AuthenticationMethod": "Authentication method", + "ImapAdvancedSetupDialog_ConnectionSecurity": "Connection security", + "IMAPAdvancedSetupDialog_ValidationAuthMethodRequired": "Authentication method is required", + "IMAPAdvancedSetupDialog_ValidationConnectionSecurityRequired": "Connection security type is required", + "IMAPAdvancedSetupDialog_ValidationDisplayNameRequired": "Display name is required", + "IMAPAdvancedSetupDialog_ValidationEmailInvalid": "Please enter a valid email address", + "IMAPAdvancedSetupDialog_ValidationEmailRequired": "Email address is required", + "IMAPAdvancedSetupDialog_ValidationErrorTitle": "Please check the following:", + "IMAPAdvancedSetupDialog_ValidationIncomingPortInvalid": "Incoming port must be between 1-65535", + "IMAPAdvancedSetupDialog_ValidationIncomingPortRequired": "Incoming server port is required", + "IMAPAdvancedSetupDialog_ValidationIncomingServerRequired": "Incoming server address is required", + "IMAPAdvancedSetupDialog_ValidationOutgoingPasswordRequired": "Outgoing server password is required", + "IMAPAdvancedSetupDialog_ValidationOutgoingPortInvalid": "Outgoing port must be between 1-65535", + "IMAPAdvancedSetupDialog_ValidationOutgoingPortRequired": "Outgoing server port is required", + "IMAPAdvancedSetupDialog_ValidationOutgoingServerRequired": "Outgoing server address is required", + "IMAPAdvancedSetupDialog_ValidationOutgoingUsernameRequired": "Outgoing server username is required", + "IMAPAdvancedSetupDialog_ValidationPasswordRequired": "Password is required", + "IMAPAdvancedSetupDialog_ValidationUsernameRequired": "Username is required", + "ImapAuthenticationMethod_Auto": "Auto", "ImapAuthenticationMethod_CramMD5": "CRAM-MD5", - "ImapAuthenticationMethod_DigestMD5": "DIGEST- MD5", - "ImapAuthenticationMethod_EncryptedPassword": "Krypteret adgangskode", - "ImapAuthenticationMethod_None": "Ingen godkendelse", + "ImapAuthenticationMethod_DigestMD5": "DIGEST-MD5", + "ImapAuthenticationMethod_EncryptedPassword": "Encrypted password", + "ImapAuthenticationMethod_None": "No authentication", "ImapAuthenticationMethod_Ntlm": "NTLM", - "ImapAuthenticationMethod_Plain": "Normal adgangskode", - "ImapConnectionSecurity_Auto": "Automatisk", - "ImapConnectionSecurity_None": "Ingen", + "ImapAuthenticationMethod_Plain": "Normal password", + "ImapConnectionSecurity_Auto": "Auto", + "ImapConnectionSecurity_None": "None", "ImapConnectionSecurity_SslTls": "SSL/TLS", "ImapConnectionSecurity_StartTls": "STARTTLS", - "IMAPSetupDialog_AccountType": "Kontotype", - "IMAPSetupDialog_ValidationSuccess_Title": "Succes", - "IMAPSetupDialog_ValidationSuccess_Message": "Validering succesfuld", - "IMAPSetupDialog_SaveImapSuccess_Title": "Succes", - "IMAPSetupDialog_SaveImapSuccess_Message": "IMAP-indstillinger blev gemt.", - "IMAPSetupDialog_ValidationFailed_Title": "Validering af IMAP-server mislykkedes.", - "IMAPSetupDialog_CertificateAllowanceRequired_Row0": "Denne server anmoder om et SSL-håndtryk for at fortsætte. Bekræft venligst certifikatdetaljerne nedenfor.", - "IMAPSetupDialog_CertificateAllowanceRequired_Row1": "Tillad håndtrykket at fortsætte opsætningen af din konto.", - "IMAPSetupDialog_CertificateDenied": "Brugeren godkendte ikke håndtrykket med certifikatet.", - "IMAPSetupDialog_CertificateIssuer": "Udsteder", - "IMAPSetupDialog_CertificateSubject": "Emne", - "IMAPSetupDialog_CertificateValidFrom": "Gyldig fra", - "IMAPSetupDialog_CertificateValidTo": "Gyldig til", - "IMAPSetupDialog_CertificateView": "Vis certifikat", - "IMAPSetupDialog_ConnectionFailedMessage": "IMAP-forbindelse mislykkedes.", - "IMAPSetupDialog_ConnectionFailedTitle": "Forbindelsen mislykkedes", - "IMAPSetupDialog_DisplayName": "Visningsnavn", - "IMAPSetupDialog_DisplayNamePlaceholder": "f.eks. Jens Jensen", - "IMAPSetupDialog_IncomingMailServer": "Indgående mail-server", + "IMAPSetupDialog_AccountType": "Account type", + "IMAPSetupDialog_ValidationSuccess_Title": "Success", + "IMAPSetupDialog_ValidationSuccess_Message": "Validation successful", + "IMAPSetupDialog_SaveImapSuccess_Title": "Success", + "IMAPSetupDialog_SaveImapSuccess_Message": "IMAP settings saved successfuly.", + "IMAPSetupDialog_ValidationFailed_Title": "IMAP Server validation failed.", + "IMAPSetupDialog_CertificateAllowanceRequired_Row0": "This server is requesting a SSL handshake to continue. Please confirm the certificate details below.", + "IMAPSetupDialog_CertificateAllowanceRequired_Row1": "Allow the handshake to continue setting up your account.", + "IMAPSetupDialog_CertificateDenied": "User didn't authorize the handshake with the certificate.", + "IMAPSetupDialog_CertificateIssuer": "Issuer", + "IMAPSetupDialog_CertificateSubject": "Subject", + "IMAPSetupDialog_CertificateValidFrom": "Valid from", + "IMAPSetupDialog_CertificateValidTo": "Valid to", + "IMAPSetupDialog_CertificateView": "View Certificate", + "IMAPSetupDialog_ConnectionFailedMessage": "IMAP connection failed.", + "IMAPSetupDialog_ConnectionFailedTitle": "Connection Failed", + "IMAPSetupDialog_DisplayName": "Display Name", + "IMAPSetupDialog_DisplayNamePlaceholder": "eg. John Doe", + "IMAPSetupDialog_IncomingMailServer": "Incoming mail server", "IMAPSetupDialog_IncomingMailServerPort": "Port", - "IMAPSetupDialog_IMAPSettings": "IMAP Serverindstillinger", - "IMAPSetupDialog_SMTPSettings": "SMTP Serverindstillinger", - "IMAPSetupDialog_MailAddress": "E-mailadresse", - "IMAPSetupDialog_MailAddressPlaceholder": "bruger@eksempel.dk", - "IMAPSetupDialog_OutgoingMailServer": "Udgående (SMTP) mailserver", - "IMAPSetupDialog_OutgoingMailServerPassword": "Udgående serveradgangskode", + "IMAPSetupDialog_IMAPSettings": "IMAP Server Settings", + "IMAPSetupDialog_SMTPSettings": "SMTP Server Settings", + "IMAPSetupDialog_MailAddress": "Email address", + "IMAPSetupDialog_MailAddressPlaceholder": "someone@example.com", + "IMAPSetupDialog_OutgoingMailServer": "Outgoing (SMTP) mail server", + "IMAPSetupDialog_OutgoingMailServerPassword": "Outgoing server password", "IMAPSetupDialog_OutgoingMailServerPort": "Port", - "IMAPSetupDialog_OutgoingMailServerRequireAuthentication": "Udgående server kræver godkendelse", - "IMAPSetupDialog_OutgoingMailServerUsername": "Udgående serverbrugernavn", - "IMAPSetupDialog_Password": "Adgangskode", - "IMAPSetupDialog_RequireSSLForIncomingMail": "Kræv SSL for indgående e-mail", - "IMAPSetupDialog_RequireSSLForOutgoingMail": "Kræv SSL for udgående e-mail", - "IMAPSetupDialog_Title": "Avanceret IMAP-konfiguration", - "IMAPSetupDialog_Username": "Brugernavn", - "IMAPSetupDialog_UsernamePlaceholder": "jensjensen, jensjensen@email.dk, domæne/jensjensen", - "IMAPSetupDialog_UseSameConfig": "Brug det samme brugernavn og adgangskode til at sende e-mail", - "Info_AccountCreatedMessage": "{0} er oprettet", - "Info_AccountCreatedTitle": "Kontooprettelse", - "Info_AccountCreationFailedTitle": "Oprettelse af konto mislykkedes", - "Info_AccountDeletedMessage": "{0} blev slettet.", - "Info_AccountDeletedTitle": "Konto slettet", - "Info_AccountIssueFixFailedTitle": "Mislykkedes", - "Info_AccountIssueFixSuccessMessage": "Løste alle kontoproblemer.", - "Info_AccountIssueFixSuccessTitle": "Succes", - "Info_AttachmentOpenFailedMessage": "Kan ikke åbne denne vedhæftede fil.", - "Info_AttachmentOpenFailedTitle": "Mislykkedes", - "Info_AttachmentSaveFailedMessage": "Kan ikke gemme denne vedhæftede fil.", - "Info_AttachmentSaveFailedTitle": "Mislykkedes", - "Info_AttachmentSaveSuccessMessage": "Vedhæftning er gemt.", - "Info_AttachmentSaveSuccessTitle": "Vedhæftning gemt", - "Info_BackgroundExecutionDeniedMessage": "Baggrundskørsel for appen er afvist. Dette kan påvirke baggrundssynkronisering og live-notifikationer.", - "Info_BackgroundExecutionDeniedTitle": "Afviste Baggrundskørsel", - "Info_BackgroundExecutionUnknownErrorMessage": "Ukendt undtagelse opstod ved registrering af baggrundssynkronisering.", - "Info_BackgroundExecutionUnknownErrorTitle": "Baggrunds kørselsfejl", - "Info_CantDeletePrimaryAliasMessage": "Primært alias kan ikke slettes. Skift venligst dit alias, før du sletter dette.", - "Info_ComposerMissingMIMEMessage": "Kunne ikke finde MIME-filen. Synkronisering kan muligvis hjælpe.", - "Info_ComposerMissingMIMETitle": "Mislykkedes", - "Info_ContactExistsMessage": "Denne kontakt er allerede på modtagerlisten.", - "Info_ContactExistsTitle": "Kontakt Eksisterer", - "Info_DraftFolderMissingMessage": "Udkastmappe mangler for denne konto. Tjek dine kontoindstillinger.", - "Info_DraftFolderMissingTitle": "Mangler Udkastmappe", - "Info_FailedToOpenFileMessage": "Filen kunne være fjernet fra disken.", - "Info_FailedToOpenFileTitle": "Kunne ikke åbne fil.", - "Info_FileLaunchFailedTitle": "Kunne ikke starte fil.", - "Info_InvalidAddressMessage": "'{0}' er ikke en gyldig e-mailadresse.", - "Info_InvalidAddressTitle": "Ugyldig adresse", - "Info_InvalidMoveTargetMessage": "Du kan ikke flytte valgte mails til denne mappe.", - "Info_InvalidMoveTargetTitle": "Ugyldigt flyttemål", - "Info_LogsNotFoundMessage": "Der er ingen logs at dele.", - "Info_LogsNotFoundTitle": "Logs Ikke Fundet", - "Info_LogsSavedMessage": "{0} er gemt i den valgte mappe.", - "Info_LogsSavedTitle": "Gemt", - "Info_MailListSizeResetSuccessMessage": "Mail Listestørrelsen er blevet nulstillet.", - "Info_MailRenderingFailedMessage": "Denne mail er beskadiget eller kan ikke åbnes.\n{0}", - "Info_MailRenderingFailedTitle": "Indlæsning Mislykkedes", - "Info_MessageCorruptedMessage": "Denne besked er beskadiget.", - "Info_MessageCorruptedTitle": "Fejl", - "Info_MissingFolderMessage": "{0} eksisterer ikke for denne konto.", - "Info_MissingFolderTitle": "Manglende Mappe", - "Info_PDFSaveFailedTitle": "Kunne ikke gemme PDF-fil", - "Info_PDFSaveSuccessMessage": "PDF-filen er gemt i {0}", - "Info_PDFSaveSuccessTitle": "Succes", - "Info_PurchaseExistsMessage": "Ser ud til, at dette produkt allerede er købt før.", - "Info_PurchaseExistsTitle": "Eksisterende Produkt", - "Info_PurchaseThankYouMessage": "Mange tak!", - "Info_PurchaseThankYouTitle": "Købet er gennemført", - "Info_RequestCreationFailedTitle": "Kunne ikke oprette anmodninger", - "Info_ReviewNetworkErrorMessage": "Der opstod et netværksproblem med din anmeldelse.", - "Info_ReviewNetworkErrorTitle": "Netværksproblem", - "Info_ReviewNewMessage": "Alt feedback er værdsat. Tak for anmeldelsen!", - "Info_ReviewSuccessTitle": "Mange tak!", - "Info_ReviewUnknownErrorMessage": "Der opstod en ukendt fejl med din anmeldelse. ({0})", - "Info_ReviewUnknownErrorTitle": "Ukendt fejl", - "Info_ReviewUpdatedMessage": "Tak for den opdaterede anmeldelse.", - "Info_SignatureDisabledMessage": "Deaktiverede signatur for denne konto", - "Info_SignatureDisabledTitle": "Succes", - "Info_SignatureSavedMessage": "Ny signatur er gemt", - "Info_SignatureSavedTitle": "Succes", - "Info_SyncCanceledMessage": "Annulleret", - "Info_SyncCanceledTitle": "Synkronisering", - "Info_SyncFailedTitle": "Synkronisering mislykkedes", - "Info_UnsubscribeErrorMessage": "Afmelding mislykkedes", - "Info_UnsubscribeLinkInvalidMessage": "Dette afmeldingslink er ugyldigt. Kunne ikke afmelde fra listen.", - "Info_UnsubscribeLinkInvalidTitle": "Ugyldigt Afmeldings-URI", - "Info_UnsubscribeSuccessMessage": "Abonnement afmeldt fra {0}.", - "Info_UnsupportedFunctionalityDescription": "Denne funktion er ikke implementeret endnu.", - "Info_UnsupportedFunctionalityTitle": "Ikke understøttet", - "InfoBarAction_Enable": "Aktiver", - "InfoBarMessage_SynchronizationDisabledFolder": "Denne mappe er deaktiveret for synkronisering.", - "InfoBarTitle_SynchronizationDisabledFolder": "Deaktiveret Mappe", - "Justify": "Udfyld", - "Left": "Venstre", + "IMAPSetupDialog_OutgoingMailServerRequireAuthentication": "Outgoing server requires authentication", + "IMAPSetupDialog_OutgoingMailServerUsername": "Outgoing server user name", + "IMAPSetupDialog_Password": "Password", + "IMAPSetupDialog_RequireSSLForIncomingMail": "Require SSL for incoming email", + "IMAPSetupDialog_RequireSSLForOutgoingMail": "Require SSL for outgoing email", + "IMAPSetupDialog_Title": "Advanced IMAP Configuration", + "IMAPSetupDialog_Username": "Username", + "IMAPSetupDialog_UsernamePlaceholder": "johndoe, johndoe@fabrikam.com, domain/johndoe", + "IMAPSetupDialog_UseSameConfig": "Use the same username and password for sending email", + "ImapCalDavSettingsPage_TitleCreate": "IMAP og kalenderopsætning", + "ImapCalDavSettingsPage_TitleEdit": "Rediger IMAP- og kalenderindstillinger", + "ImapCalDavSettingsPage_Subtitle": "Konfigurer IMAP/SMTP og valgfri kalendersynkronisering for denne konto.", + "ImapCalDavSettingsPage_BasicSectionTitle": "Grundlæggende opsætning", + "ImapCalDavSettingsPage_BasicSectionDescription": "Indtast din identitet og legitimationsoplysninger. Wino kan forsøge at registrere serverindstillingerne automatisk.", + "ImapCalDavSettingsPage_BasicTab": "Grundlæggende", + "ImapCalDavSettingsPage_EnableCalendarSupport": "Aktivér kalenderunderstøttelse", + "ImapCalDavSettingsPage_AutoDiscoverButton": "Automatisk opdagelse af mailindstillinger", + "ImapCalDavSettingsPage_AutoDiscoverySuccessMessage": "Mailindstillinger fundet og anvendt.", + "ImapCalDavSettingsPage_AdvancedSectionTitle": "Avanceret konfiguration", + "ImapCalDavSettingsPage_AdvancedSectionDescription": "Indtast serverindstillinger manuelt, hvis automatisk opdagelse ikke er tilgængelig eller er ukorrekt.", + "ImapCalDavSettingsPage_AdvancedTab": "Avanceret", + "ImapCalDavSettingsPage_CalendarSectionTitle": "Kalenderopsætning", + "ImapCalDavSettingsPage_CalendarSectionDescription": "Vælg hvordan kalenderdata skal fungere for denne IMAP-konto.", + "ImapCalDavSettingsPage_CalendarModeHeader": "Kalendertilstand", + "ImapCalDavSettingsPage_ConnectionSecurityHeader": "Forbindelsessikkerhed", + "ImapCalDavSettingsPage_AuthenticationMethodHeader": "Autentificeringsmetode", + "ImapCalDavSettingsPage_CalendarModeDisabled": "Deaktiveret", + "ImapCalDavSettingsPage_CalendarModeCalDav": "CalDAV-synkronisering", + "ImapCalDavSettingsPage_CalendarModeLocalOnly": "Kun lokal kalender", + "ImapCalDavSettingsPage_CalendarModeDisabledDescription": "Kalenderen er deaktiveret for denne konto.", + "ImapCalDavSettingsPage_CalendarModeCalDavDescription": "Kalenderposter synkroniseres med din CalDAV-server.", + "ImapCalDavSettingsPage_CalendarModeLocalOnlyDescription": "Kalenderposter gemmes kun på denne computer og synkroniseres ikke til netværket.", + "ImapCalDavSettingsPage_LocalCalendarLearnMore": "Sådan fungerer lokal kalender", + "ImapCalDavSettingsPage_LocalCalendarDialogTitle": "Kun lokal kalender", + "ImapCalDavSettingsPage_LocalCalendarDialogMessage": "Lokal kalender gemmer alle begivenheder kun på din computer. Intet synkroniseres til iCloud, Yahoo eller nogen anden udbyder.", + "ImapCalDavSettingsPage_CalDavServiceUrl": "CalDAV-tjeneste-URL", + "ImapCalDavSettingsPage_CalDavUsername": "CalDAV-brugernavn", + "ImapCalDavSettingsPage_CalDavPassword": "CalDAV-adgangskode", + "ImapCalDavSettingsPage_CalDavNotRequiredMessage": "CalDAV-test er kun påkrævet, når kalendertilstand er indstillet til CalDAV-synkronisering.", + "ImapCalDavSettingsPage_CalDavUrlRequired": "CalDAV-tjeneste-URL er påkrævet.", + "ImapCalDavSettingsPage_CalDavUrlInvalid": "CalDAV-tjeneste-URL skal være en absolut URL.", + "ImapCalDavSettingsPage_CalDavUsernameRequired": "CalDAV-brugernavn er påkrævet.", + "ImapCalDavSettingsPage_CalDavPasswordRequired": "CalDAV-adgangskode er påkrævet.", + "ImapCalDavSettingsPage_TestImapButton": "Test IMAP-forbindelse", + "ImapCalDavSettingsPage_TestCalDavButton": "Test CalDAV-forbindelse", + "ImapCalDavSettingsPage_ImapTestSuccessMessage": "IMAP-forbindelsestest lykkedes.", + "ImapCalDavSettingsPage_CalDavTestSuccessMessage": "CalDAV-forbindelsestest lykkedes.", + "ImapCalDavSettingsPage_SaveSuccessMessage": "Kontoindstillinger valideret og gemt.", + "ImapCalDavSettingsPage_ICloudHint": "Brug en app-specifik adgangskode, der er genereret ud fra dine Apple-kontoindstillinger.", + "ImapCalDavSettingsPage_YahooHint": "Brug en app-adgangskode fra Yahoo-kontoens sikkerhedsindstillinger.", + "Info_AccountCreatedMessage": "{0} is created", + "Info_AccountCreatedTitle": "Account Creation", + "Info_AccountCreationFailedTitle": "Account Creation Failed", + "Info_AccountDeletedMessage": "{0} is successfuly deleted.", + "Info_AccountDeletedTitle": "Account Deleted", + "Info_AccountIssueFixFailedTitle": "Failed", + "Info_AccountIssueFixImapMessage": "Åbn IMAP- og kalenderindstillingssiden for at indtaste dine serveroplysninger igen.", + "Info_AccountAttentionRequiredMessage": "Denne konto kræver din opmærksomhed.", + "Info_AccountAttentionRequiredClickableMessage": "Klik for at rette denne konto og synkronisere den igen.", + "Info_AccountAttentionRequiredAction": "Fiks", + "Info_AccountAttentionRequiredActionHint": "Klik på Fiks for at løse dette kontoproblem.", + "Info_AccountIssueFixSuccessMessage": "Fixed all account issues.", + "Info_AccountIssueFixSuccessTitle": "Success", + "Info_AttachmentOpenFailedMessage": "Can't open this attachment.", + "Info_AttachmentOpenFailedTitle": "Failed", + "Info_AttachmentSaveFailedMessage": "Can't save this attachment.", + "Info_AttachmentSaveFailedTitle": "Failed", + "Info_AttachmentSaveSuccessMessage": "Attachment is saved.", + "Info_AttachmentSaveSuccessTitle": "Attachment Saved", + "Info_BackgroundExecutionDeniedMessage": "Background execution for the app is denied. This may affect background synchronization and live notifications.", + "Info_BackgroundExecutionDeniedTitle": "Denied Background Execution", + "Info_BackgroundExecutionUnknownErrorMessage": "Unknown exception occurred when registering background synchronizer.", + "Info_BackgroundExecutionUnknownErrorTitle": "Background Execution Failure", + "Info_CantDeletePrimaryAliasMessage": "Primary alias can't be deleted. Please change your alias before deleting this one", + "Info_ComposerMissingMIMEMessage": "Couldn't locate the MIME file. Synchronizing may help.", + "Info_ComposerMissingMIMETitle": "Failed", + "Info_ContactExistsMessage": "This contact is already in the recipient list.", + "Info_ContactExistsTitle": "Contact Exists", + "Info_DraftFolderMissingMessage": "Draft folder is missing for this account. Please check your account settings.", + "Info_DraftFolderMissingTitle": "Missing Draft Folder", + "Info_FailedToOpenFileMessage": "File might be removed from the disk.", + "Info_FailedToOpenFileTitle": "Failed to launch file.", + "Info_FileLaunchFailedTitle": "Failed to launch file", + "Info_InvalidAddressMessage": "'{0}' is not a valid e-mail address.", + "Info_InvalidAddressTitle": "Invalid Address", + "Info_InvalidMoveTargetMessage": "You can't move selected mails to this folder.", + "Info_InvalidMoveTargetTitle": "Invalid Move Target", + "Info_LogsNotFoundMessage": "There are no logs to share.", + "Info_LogsNotFoundTitle": "Logs Not Found", + "Info_LogsSavedMessage": "{0} is saved to selected folder.", + "Info_LogsSavedTitle": "Saved", + "Info_MailListSizeResetSuccessMessage": "The Mail List size has been reset.", + "Info_MailRenderingFailedMessage": "This mail is corrupted or can't be opened.\n{0}", + "Info_MailRenderingFailedTitle": "Render Failed", + "Info_MessageCorruptedMessage": "This message is corrupted.", + "Info_MessageCorruptedTitle": "Error", + "Info_MissingFolderMessage": "{0} doesn't exist for this account.", + "Info_MissingFolderTitle": "Missing Folder", + "Info_PDFSaveFailedTitle": "Failed to save PDF file", + "Info_PDFSaveSuccessMessage": "PDF file is saved to {0}", + "Info_PDFSaveSuccessTitle": "Success", + "Info_PurchaseExistsMessage": "Looks like this product has already been purchased before.", + "Info_PurchaseExistsTitle": "Existing Product", + "Info_PurchaseThankYouMessage": "Thank You", + "Info_PurchaseThankYouTitle": "Purchase successful", + "Info_RequestCreationFailedTitle": "Failed to Create Requests", + "Info_ReviewNetworkErrorMessage": "There was a network issue with your review.", + "Info_ReviewNetworkErrorTitle": "Network Issue", + "Info_ReviewNewMessage": "All feedbacks are appreciated. Thank you for the review!", + "Info_ReviewSuccessTitle": "Thank you", + "Info_ReviewUnknownErrorMessage": "There was an unknown issue with your review. ({0})", + "Info_ReviewUnknownErrorTitle": "Unknown Error", + "Info_ReviewUpdatedMessage": "Thank you for the updated review.", + "Info_SignatureDisabledMessage": "Disabled signature for this account", + "Info_SignatureDisabledTitle": "Success", + "Info_SignatureSavedMessage": "New signature is saved", + "Info_SignatureSavedTitle": "Success", + "Info_SyncCanceledMessage": "Canceled", + "Info_SyncCanceledTitle": "Synchronization", + "Info_SyncFailedTitle": "Synchronization Failed", + "Info_UnsubscribeErrorMessage": "Failed to unsubscribe", + "Info_UnsubscribeLinkInvalidMessage": "This unsubscribe link is invalid. Failed to unsubscribe from the list.", + "Info_UnsubscribeLinkInvalidTitle": "Invalid Unsubscribe Uri", + "Info_UnsubscribeSuccessMessage": "Successfully unsubscribed from {0}.", + "Info_UnsupportedFunctionalityDescription": "This functionality is not implemented yet.", + "Info_UnsupportedFunctionalityTitle": "Unsupported", + "InfoBarAction_Enable": "Enable", + "InfoBarMessage_SynchronizationDisabledFolder": "This folder is disabled for synchronization.", + "InfoBarTitle_SynchronizationDisabledFolder": "Disabled Folder", + "Justify": "Justify", + "MenuUpdateAvailable": "Opdatering tilgængelig", + "Left": "Left", "Link": "Link", - "LinkedAccountsCreatePolicyMessage": "Du skal have mindst 2 konti for at oprette et link.\nLinket vil blive fjernet ved gem.", - "LinkedAccountsTitle": "Tilknyttede Konti", - "MailItemNoSubject": "Intet emne", - "MailOperation_AlwaysMoveFocused": "Flyt altid til fokuseret", - "MailOperation_AlwaysMoveOther": "Flyt altid til andet", - "MailOperation_Archive": "Arkiver", - "MailOperation_ClearFlag": "Fjern markering", - "MailOperation_DarkEditor": "Mørk", - "MailOperation_Delete": "Slet", - "MailOperation_ExportPDF": "Eksporter til PDF", + "LinkedAccountsCreatePolicyMessage": "you must have at least 2 accounts to create link\nlink will be removed on save", + "LinkedAccountsTitle": "Linked Accounts", + "MailItemNoSubject": "No subject", + "MailOperation_AlwaysMoveFocused": "Always Move to Focused", + "MailOperation_AlwaysMoveOther": "Always Move to Other", + "MailOperation_Archive": "Archive", + "MailOperation_ClearFlag": "Clear flag", + "MailOperation_DarkEditor": "Dark", + "MailOperation_Delete": "Delete", + "MailOperation_ExportPDF": "Export to PDF", "MailOperation_Find": "Find", - "MailOperation_Forward": "Videresend", - "MailOperation_Ignore": "Ignorer", - "MailOperation_LightEditor": "Lys", - "MailOperation_MarkAsJunk": "Marker som spam", - "MailOperation_MarkAsRead": "Marker som læst", - "MailOperation_MarkAsUnread": "Marker som ulæst", - "MailOperation_MarkNotJunk": "Marker som ikke spam", - "MailOperation_Move": "Flyt", - "MailOperation_MoveFocused": "Flyt til Fokuseret", - "MailOperation_MoveJunk": "Flyt til spam", - "MailOperation_MoveOther": "Flyt til andet", - "MailOperation_Navigate": "Naviger", - "MailOperation_Print": "Udskriv", - "MailOperation_Reply": "Svar", - "MailOperation_ReplyAll": "Svar alle", - "MailOperation_SaveAs": "Gem som", - "MailOperation_SetFlag": "Sæt markering", - "MailOperation_Unarchive": "Fjern fra arkiv", - "MailOperation_ViewMessageSource": "Vis meddelelseskilde", - "MailOperation_Zoom": "Forstør", - "MailsSelected": "{0} element(er) valgt", - "MarkFlagUnflag": "Marker / Fjern markering", - "MarkReadUnread": "Marker som læst/ulæst", - "MenuManageAccounts": "Administrer Konti", - "MenuMergedAccountItemAccountsSuffix": " konti", - "MenuNewMail": "Ny Mail", - "MenuRate": "Bedøm Wino", - "MenuSettings": "Indstillinger", - "MergedAccountCommonFolderArchive": "Arkiv", - "MergedAccountCommonFolderDraft": "Udkast", - "MergedAccountCommonFolderInbox": "Indbakke", - "MergedAccountCommonFolderJunk": "Spam", - "MergedAccountCommonFolderSent": "Sendt", - "MergedAccountCommonFolderTrash": "Slettet", - "MergedAccountsAvailableAccountsTitle": "Tilgængelige konti", - "MessageSourceDialog_Title": "Beskedkilde", - "More": "Flere", - "MoreFolderNameOverride": "Flere", - "MoveMailDialog_InvalidFolderMessage": "{0} er ikke en gyldig mappe til denne mail.", - "MoveMailDialog_Title": "Vælg en mappe", - "NewAccountDialog_AccountName": "Kontonavn", - "NewAccountDialog_AccountNameDefaultValue": "Personligt", - "NewAccountDialog_AccountNamePlaceholder": "f.eks. Personlig Mail", - "NewAccountDialog_Title": "Tilføj Ny Konto", - "NoMailSelected": "Ingen besked valgt", - "NoMessageCrieteria": "Ingen beskeder matcher dine søgekriterier", - "NoMessageEmptyFolder": "Denne mappe er tom", - "Notifications_MultipleNotificationsMessage": "Du har {0} nye beskeder.", - "Notifications_MultipleNotificationsTitle": "Ny e-mail", - "Notifications_WinoUpdatedMessage": "Tjek ny version {0} ud", - "Notifications_WinoUpdatedTitle": "Wino Mail er blevet opdateret.", - "OnlineSearchFailed_Message": "Søgning mislykkedes\n{0}\n\nViser offline-mails.", - "OnlineSearchTry_Line1": "Kan du ikke finde det, du leder efter?", - "OnlineSearchTry_Line2": "Prøv online søgning.", - "Other": "Andet", - "PaneLengthOption_Default": "Standard", - "PaneLengthOption_ExtraLarge": "Ekstra stor", - "PaneLengthOption_Large": "Stor", + "MailOperation_Forward": "Forward", + "MailOperation_Ignore": "Ignore", + "MailOperation_LightEditor": "Light", + "MailOperation_MarkAsJunk": "Mark as junk", + "MailOperation_MarkAsRead": "Mark as read", + "MailOperation_MarkAsUnread": "Mark as unread", + "MailOperation_MarkNotJunk": "Mark as Not Junk", + "MailOperation_Move": "Move", + "MailOperation_MoveFocused": "Move to Focused", + "MailOperation_MoveJunk": "Move to Junk", + "MailOperation_MoveOther": "Move to Other", + "MailOperation_Navigate": "Navigate", + "MailOperation_Print": "Print", + "MailOperation_Reply": "Reply", + "MailOperation_ReplyAll": "Reply all", + "MailOperation_SaveAs": "Save As", + "MailOperation_SetFlag": "Set flag", + "MailOperation_Unarchive": "Unarchive", + "MailOperation_ViewMessageSource": "View message source", + "MailOperation_Zoom": "Zoom", + "MailsDragging": "Trækker {0} element(er)", + "MailsSelected": "{0} item(s) selected", + "MarkFlagUnflag": "Mark as flagged/unflagged", + "MarkReadUnread": "Mark as read/unread", + "MenuManageAccounts": "Manage Accounts", + "MenuMergedAccountItemAccountsSuffix": " accounts", + "MenuNewMail": "New Mail", + "MenuRate": "Rate Wino", + "MenuSettings": "Settings", + "MergedAccountCommonFolderArchive": "Archive", + "MergedAccountCommonFolderDraft": "Draft", + "MergedAccountCommonFolderInbox": "Inbox", + "MergedAccountCommonFolderJunk": "Junk", + "MergedAccountCommonFolderSent": "Sent", + "MergedAccountCommonFolderTrash": "Deleted", + "MergedAccountsAvailableAccountsTitle": "Available Accounts", + "MessageSourceDialog_Title": "Message source", + "More": "More", + "MoreFolderNameOverride": "More", + "MoveMailDialog_InvalidFolderMessage": "{0} is not a valid folder for this mail.", + "MoveMailDialog_Title": "Pick a folder", + "NewAccountDialog_AccountName": "Account Name", + "NewAccountDialog_AccountNameDefaultValue": "Personal", + "NewAccountDialog_AccountNamePlaceholder": "eg. Personal Mail", + "NewAccountDialog_Title": "Add New Account", + "NoMailSelected": "No message selected", + "NoMessageCrieteria": "No messages match your search criteria", + "NoMessageEmptyFolder": "This folder is empty", + "Notifications_MultipleNotificationsMessage": "You have {0} new messages.", + "Notifications_MultipleNotificationsTitle": "New Mail", + "Notifications_WinoUpdatedMessage": "Checkout new version {0}", + "Notifications_WinoUpdatedTitle": "Wino Mail has been updated.", + "Notifications_StoreUpdateAvailableTitle": "Opdatering tilgængelig", + "Notifications_StoreUpdateAvailableMessage": "En nyere version af Wino Mail er klar til at blive installeret fra Microsoft Store.", + "OnlineSearchFailed_Message": "Failed to perform search\n{0}\n\nListing offline mails.", + "OnlineSearchTry_Line1": "Can't find what you are looking for?", + "OnlineSearchTry_Line2": "Try online search.", + "Other": "Other", + "PaneLengthOption_Default": "Default", + "PaneLengthOption_ExtraLarge": "Extra Large", + "PaneLengthOption_Large": "Large", "PaneLengthOption_Medium": "Medium", - "PaneLengthOption_Micro": "Ekstra Lille", - "PaneLengthOption_Small": "Lille", - "Photos": "Billeder", - "PreparingFoldersMessage": "Forbereder mapper", - "ProtocolLogAvailable_Message": "Protokollogs er tilgængelige til diagnosticering.", - "ProviderDetail_Gmail_Description": "Google-konto", - "ProviderDetail_iCloud_Description": "Apple iCloud-konto", + "PaneLengthOption_Micro": "Micro", + "PaneLengthOption_Small": "Small", + "Photos": "Photos", + "PreparingFoldersMessage": "Preparing folders", + "ProviderDetail_Gmail_Description": "Google Account", + "ProviderDetail_iCloud_Description": "Apple iCloud Account", "ProviderDetail_iCloud_Title": "iCloud", - "ProviderDetail_IMAP_Description": "Brugerdefineret IMAP/SMTP server", - "ProviderDetail_IMAP_Title": "IMAP server", - "ProviderDetail_Yahoo_Description": "Yahoo-konto", + "ProviderDetail_IMAP_Description": "Custom IMAP/SMTP server", + "ProviderDetail_IMAP_Title": "IMAP Server", + "ProviderDetail_Yahoo_Description": "Yahoo Account", "ProviderDetail_Yahoo_Title": "Yahoo Mail", - "QuickEventDialog_EventName": "Begivenhedens navn", - "QuickEventDialog_IsAllDay": "Hele dagen", - "QuickEventDialog_Location": "Lokation", - "QuickEventDialog_RemindMe": "Påmind mig", - "QuickEventDialogMoreDetailsButtonText": "Flere detaljer", - "Reader_SaveAllAttachmentButtonText": "Gem alle vedhæftede filer", - "Results": "Resultater", - "Right": "Højre", - "SearchBarPlaceholder": "Søg", - "SearchingIn": "Søger i", - "SearchPivotName": "Resultater", - "SettingConfigureSpecialFolders_Button": "Konfigurer", - "SettingsEditAccountDetails_IMAPConfiguration_Title": "IMAP/SMTP Konfiguration", - "SettingsEditAccountDetails_IMAPConfiguration_Description": "Skift dine indstillinger for indgående og udgående servere.", - "SettingsAbout_Description": "Læs mere om Wino.", - "SettingsAbout_Title": "Om", - "SettingsAboutGithub_Description": "Gå til issue tracker GitHub repository.", + "QuickEventDialog_EventName": "Event name", + "QuickEventDialog_IsAllDay": "All day", + "QuickEventDialog_Location": "Location", + "QuickEventDialog_RemindMe": "Remind me", + "QuickEventDialogMoreDetailsButtonText": "More details", + "Reader_SaveAllAttachmentButtonText": "Save all attachments", + "Results": "Results", + "Right": "Right", + "SearchBarPlaceholder": "Search", + "SearchingIn": "Searching in", + "SearchPivotName": "Results", + "Settings_KeyboardShortcuts_Title": "Tastaturgenveje", + "Settings_KeyboardShortcuts_Description": "Administrer tastaturgenveje til hurtige handlinger på e-mails.", + "SettingConfigureSpecialFolders_Button": "Configure", + "SettingsEditAccountDetails_IMAPConfiguration_Title": "IMAP/SMTP Configuration", + "SettingsEditAccountDetails_IMAPConfiguration_Description": "Change your incoming/outgoing server settings.", + "SettingsEditAccountDetails_ImapCalDavSettings_Title": "IMAP- og kalenderindstillinger", + "SettingsEditAccountDetails_ImapCalDavSettings_Description": "Åbn den dedikerede IMAP-, SMTP- og CalDAV-indstillingssiden for denne konto.", + "SettingsEditAccountDetails_ImapCalDavSettings_Action": "Åbn indstillinger", + "SettingsAbout_Description": "Learn more about Wino.", + "SettingsAbout_Title": "About", + "SettingsAboutGithub_Description": "Go to issue tracker GitHub repository.", "SettingsAboutGithub_Title": "GitHub", "SettingsAboutVersion": "Version ", - "SettingsAboutWinoDescription": "Letvægts mailklient til Windows-enheder.", - "SettingsAccentColor_Description": "Skift applikationens accentfarve", - "SettingsAccentColor_Title": "Accentfarve", - "SettingsAccentColor_UseWindowsAccentColor": "Brug min Windows accentfarve", - "SettingsAccountManagementAppendMessage_Description": "Opret en kopi af beskeden i mappen Sendt efter udkastet er sendt. Aktiver dette hvis du ikke ser dine mails efter du har sendt dem i Sendt mappe.", - "SettingsAccountManagementAppendMessage_Title": "Tilføj beskeder til mappen Sendt", - "SettingsAccountName_Description": "Ændr navnet på kontoen.", - "SettingsAccountName_Title": "Kontonavn", - "SettingsApplicationTheme_Description": "Tilpas Wino med forskellige brugerdefinerede applikationtemaer til dit behag.", - "SettingsApplicationTheme_Title": "Applikationstema", - "SettingsAppPreferences_CloseBehavior_Description": "Hvad skal der ske, når du lukker appen?", - "SettingsAppPreferences_CloseBehavior_Title": "Applikationens lukkeopførsel", - "SettingsAppPreferences_Description": "Generelle indstillinger for Wino Mail.", - "SettingsAppPreferences_SearchMode_Description": "Indstil, om Wino først skal tjekke hentede mails, når du søger, eller spørge din mailserver online. Lokal søgning er altid hurtigere, og du kan altid søge online, hvis din mail ikke findes i resultaterne.", - "SettingsAppPreferences_SearchMode_Local": "Lokalt", + "SettingsAboutWinoDescription": "Lightweight mail client for Windows device families.", + "SettingsAccentColor_Description": "Change application's accent color", + "SettingsAccentColor_Title": "Accent Color", + "SettingsAccentColor_UseWindowsAccentColor": "Use my Windows accent color", + "SettingsAccountManagementAppendMessage_Description": "Create a copy of the message in Sent folder after the draft is sent. Enable this if you don't see your mails after you sent them in Sent folder.", + "SettingsAccountManagementAppendMessage_Title": "Append messages to Sent folder", + "SettingsAccountName_Description": "Change the name of the account.", + "SettingsAccountName_Title": "Account Name", + "SettingsApplicationTheme_Description": "Personalize Wino with different custom application themes for your like.", + "SettingsApplicationTheme_Title": "Application Theme", + "SettingsAppPreferences_CloseBehavior_Description": "What should happen when you close the app?", + "SettingsAppPreferences_CloseBehavior_Title": "Application close behavior", + "SettingsAppPreferences_Description": "General settings / preferences for Wino Mail.", + "SettingsAppPreferences_SearchMode_Description": "Set whether Wino should check fetched mails first while doing a search or ask your mail server online. Local search is always faster and you can always do an online search if your mail is not in the results.", + "SettingsAppPreferences_SearchMode_Local": "Local", "SettingsAppPreferences_SearchMode_Online": "Online", - "SettingsAppPreferences_SearchMode_Title": "Standard søgetilstand", - "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description": "Wino Mail kører fortsat i baggrunden. Du får besked, når nye mails ankommer.", - "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title": "Kør i baggrunden", - "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description": "Wino Mail vil blive ved med at køre på systembakken. Tilgængelig til start ved at klikke på et ikon. Du vil blive underrettet om nye mails ankommer.", - "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Title": "Minimer til systembakke", - "SettingsAppPreferences_ServerBackgroundingMode_Terminate_Description": "Wino Mail vil ikke fortsætte med at køre. Du vil ikke få besked, når nye mails ankommer. Start Wino Mail igen for at fortsætte mailsynkronisering.", - "SettingsAppPreferences_ServerBackgroundingMode_Terminate_Title": "Afslut", - "SettingsAppPreferences_StartupBehavior_Description": "Tillad Wino Mail at starte minimeret, når Windows starter. Tillad altid at modtage notifikationer.", - "SettingsAppPreferences_StartupBehavior_Disable": "Deaktiver", - "SettingsAppPreferences_StartupBehavior_Disabled": "Wino Mail vil ikke blive startet automatisk ved Windows-opstart. Du vil derfor gå glip af notifikationer, når du genstarter computeren.", - "SettingsAppPreferences_StartupBehavior_DisabledByPolicy": "Din administrator eller gruppepolitikker har deaktiveret, at programmer kan starte automatisk. Derfor kan Wino Mail ikke sættes til automatisk opstart.", - "SettingsAppPreferences_StartupBehavior_DisabledByUser": "Gå til Jobliste -> fanen Opstart for at tillade, at Wino Mail starter ved Windows-opstart.", - "SettingsAppPreferences_StartupBehavior_Enable": "Aktiver", - "SettingsAppPreferences_StartupBehavior_Enabled": "Wino Mail blev indstillet til at starte i baggrunden ved opstart af Windows.", - "SettingsAppPreferences_StartupBehavior_FatalError": "Kritisk fejl opstod under ændring af opstartstilstand for Wino Mail.", - "SettingsAppPreferences_StartupBehavior_Title": "Start minimeret ved opstart af Windows", - "SettingsAppPreferences_Title": "Indstillinger For Appen", - "SettingsAutoSelectNextItem_Description": "Vælg det næste element, når du sletter eller flytter en mail.", - "SettingsAutoSelectNextItem_Title": "Vælg automatisk næste element", - "SettingsAvailableThemes_Description": "Vælg et tema fra Wino egen samling til din smag eller anvende dine egne temaer.", - "SettingsAvailableThemes_Title": "Tilgængelige temaer", - "SettingsCalendarSettings_Description": "Ændr første dag i ugen, time celle højde og mere...", - "SettingsCalendarSettings_Title": "Kalenderindstillinger", - "SettingsComposer_Title": "Skriv mail", - "SettingsComposerFont_Title": "Standard skrifttype til beskededitor", - "SettingsComposerFontFamily_Description": "Skift standard skrifttype og skriftstørrelse for at skrive mails.", - "SettingsConfigureSpecialFolders_Description": "Indstil mapper med særlige funktioner. Mapper som Arkiv, Indbakke og Udkast er essentielle for, at Wino fungerer korrekt.", - "SettingsConfigureSpecialFolders_Title": "Konfigurer Systemmapper", - "SettingsCustomTheme_Description": "Opret dit eget brugerdefinerede tema med brugerdefineret baggrund og accent farve.", - "SettingsCustomTheme_Title": "Brugerdefineret tema", - "SettingsDeleteAccount_Description": "Slet alle e-mails og legitimationsoplysninger tilknyttet denne konto.", - "SettingsDeleteAccount_Title": "Slet denne konto", - "SettingsDeleteProtection_Description": "Skal Wino bede dig om bekræftelse, hver gang du forsøger at permanent slette en mail ved hjælp af Shift + Del-taster?", - "SettingsDeleteProtection_Title": "Permanent Slet Beskyttelse", - "SettingsDiagnostics_Description": "Til udviklere", - "SettingsDiagnostics_DiagnosticId_Description": "Del dette ID med udviklerne, når du bliver bedt om det, for at få hjælp til de problemer, du oplever i Wino Mail.", - "SettingsDiagnostics_DiagnosticId_Title": "Diagnostisk ID", - "SettingsDiagnostics_Title": "Diagnosticering", - "SettingsDiscord_Description": "Få regelmæssige udviklingsopdateringer, deltag i køreplansdiskussioner og giv feedback.", - "SettingsDiscord_Title": "Discord Kanal", - "SettingsEditLinkedInbox_Description": "Tilføj / fjern konti, omdøb eller bryd linket mellem konti.", - "SettingsEditLinkedInbox_Title": "Rediger Tilknyttede Indbakke", - "SettingsElementTheme_Description": "Vælg et Windows-tema til Wino", - "SettingsElementTheme_Title": "Elementtema", - "SettingsElementThemeSelectionDisabled": "Valg af elementtema er deaktiveret, når appens tema er sat til andet end Standard.", - "SettingsEnableHoverActions_Title": "Aktiver hover-handlinger", - "SettingsEnableIMAPLogs_Description": "Aktivér dette for at give detaljer om eventuelle problemer med IMAP-forbindelsen under opsætning af IMAP-serveren.", - "SettingsEnableIMAPLogs_Title": "Aktiver IMAP-protokollogger", - "SettingsEnableLogs_Description": "Jeg kan få brug for logfiler ved nedbrud for at diagnosticere problemer, du har åbnet på GitHub. Ingen af logfilerne vil afsløre dine legitimationsoplysninger eller sensitiv information for offentligheden.", - "SettingsEnableLogs_Title": "Aktiver logfiler", - "SettingsEnableSignature": "Aktiver signatur", - "SettingsExpandOnStartup_Description": "Indstil om Wino skal udvide kontoens mapper ved opstart.", - "SettingsExpandOnStartup_Title": "Udvid menu ved opstart", - "SettingsExternalContent_Description": "Administrer indstillinger for eksternt indhold ved gengivelse af mails.", - "SettingsExternalContent_Title": "Eksternt indhold", - "SettingsFocusedInbox_Description": "Angiv, om indbakken skal opdeles i to som Fokuseret - Andet.", - "SettingsFocusedInbox_Title": "Fokuseret Indbakke", - "SettingsFolderMenuStyle_Description": "Vælg om kontomapper skal være indlejret under et konto-menupunkt eller ej. Slå dette fra, hvis du foretrækker det gamle menusystem i Windows Mail.", - "SettingsFolderMenuStyle_Title": "Opret Indlejrede Mapper", - "SettingsFolderOptions_Description": "Skift indstillinger for enkelte mapper, f.eks. aktiver/deaktiver synkronisering eller vis/skjul ulæst-badge.", - "SettingsFolderOptions_Title": "Mappekonfiguration", - "SettingsFolderSync_Description": "Aktiver eller deaktiver specifikke mapper til synkronisering.", - "SettingsFolderSync_Title": "Mappe Synkronisering", - "SettingsFontFamily_Title": "Skrifttype Familie", - "SettingsFontPreview_Title": "Forhåndsvisning", - "SettingsFontSize_Title": "Skriftstørrelse", - "SettingsHoverActionCenter": "Centrer Handling", - "SettingsHoverActionLeft": "Venstre Handling", - "SettingsHoverActionRight": "Højre handling", - "SettingsHoverActions_Description": "Vælg 3 handlinger, der vises, når du holder musen over mails.", - "SettingsHoverActions_Title": "Hover-handlinger", - "SettingsLanguage_Description": "Skift visningssprog for Wino.", - "SettingsLanguage_Title": "Visningssprog", - "SettingsLanguageTime_Description": "Wino visningssprog, foretrukket tidsformat.", - "SettingsLanguageTime_Title": "Sprog & Tid", - "SettingsLinkAccounts_Description": "Flet flere konti til én. Se mails fra en indbakke sammen.", - "SettingsLinkAccounts_Title": "Opret Tilknyttede Konti", - "SettingsLinkedAccountsSave_Description": "Ændre det aktuelle link med de nye konti.", - "SettingsLinkedAccountsSave_Title": "Gem ændringer", - "SettingsLoadImages_Title": "Indlæs billeder automatisk", - "SettingsLoadPlaintextLinks_Title": "Konverter klartekst links til klikbare links", - "SettingsLoadStyles_Title": "Indlæs stilarter automatisk", - "SettingsMailListActionBar_Description": "Skjul/vis handlingsbjælke øverst i meddelelseslisten.", - "SettingsMailListActionBar_Title": "Vis mailliste handlinger", - "SettingsMailSpacing_Description": "Juster afstanden mellem mails i listen.", - "SettingsMailSpacing_Title": "Mail Mellemrum", - "SettingsManageAccountSettings_Description": "Notifikationer, signaturer, synkronisering og andre indstillinger pr. konto.", - "SettingsManageAccountSettings_Title": "Administrer Kontoindstillinger", - "SettingsManageAliases_Description": "Se e-mail-aliaser tildelt for denne konto, opdater eller slette dem.", - "SettingsManageAliases_Title": "Aliaser", - "SettingsEditAccountDetails_Title": "Rediger Kontooplysninger", - "SettingsEditAccountDetails_Description": "Skift kontonavn, afsendernavn og tildel en ny farve, hvis du vil.", - "SettingsManageLink_Description": "Flyt elementer for at tilføje et nyt link eller fjerne et eksisterende link.", - "SettingsManageLink_Title": "Administrer link", - "SettingsMarkAsRead_Description": "Ændre hvad der skal ske med det valgte element.", - "SettingsMarkAsRead_DontChange": "Marker ikke automatisk elementet som læst", - "SettingsMarkAsRead_SecondsToWait": "Sekunder at vente: ", - "SettingsMarkAsRead_Timer": "Når de vises i læserruden", - "SettingsMarkAsRead_Title": "Marker element som læst", - "SettingsMarkAsRead_WhenSelected": "Når valgt", - "SettingsMessageList_Description": "Ændr hvordan dine beskeder skal organiseres i e-mail-listen.", - "SettingsMessageList_Title": "Beskedliste", - "SettingsNoAccountSetupMessage": "Ingen konti er oprettet endnu.", - "SettingsNotifications_Description": "Slå notifikationer til eller fra for denne konto.", - "SettingsNotifications_Title": "Notifikationer", - "SettingsNotificationsAndTaskbar_Description": "Vælg, om notifikationer og proceslinjens badge skal vises for denne konto.", - "SettingsNotificationsAndTaskbar_Title": "Notifikationer & Proceslinje", - "SettingsOptions_Title": "Indstillinger", - "SettingsPaneLengthReset_Description": "Nulstil størrelsen af postlisten til original, hvis du har problemer med den.", - "SettingsPaneLengthReset_Title": "Nulstil størrelsen på maillisten", - "SettingsPaypal_Description": "Vis meget mere kærlighed ❤️ Alle donationer er værdsat.", - "SettingsPaypal_Title": "Doner via PayPal", - "SettingsPersonalization_Description": "Skift udseendet af Wino som du vil.", - "SettingsPersonalization_Title": "Tilpasning", - "SettingsPersonalizationMailDisplayCompactMode": "Kompakt Tilstand", - "SettingsPersonalizationMailDisplayMediumMode": "Mellem Tilstand", - "SettingsPersonalizationMailDisplaySpaciousMode": "Rummelig Tilstand", - "SettingsPrefer24HourClock_Description": "Mail modtagertider vil blive vist i 24 timers format i stedet for 12 (AM/PM)", - "SettingsPrefer24HourClock_Title": "Vis urformat i 24 timer", - "SettingsPrivacyPolicy_Description": "Gennemgå privatlivspolitik.", - "SettingsPrivacyPolicy_Title": "Privatlivspolitik", - "SettingsReadComposePane_Description": "Skrifttyper, eksternt indhold.", - "SettingsReadComposePane_Title": "Læser og beskededitor", - "SettingsReader_Title": "Læser", - "SettingsReaderFont_Title": "Standard skrifttype til læser", - "SettingsReaderFontFamily_Description": "Skift standard skrifttype familie og skrifttype størrelse for læsning af mails.", - "SettingsRenameMergeAccount_Description": "Ændr navnet på de linkede konti.", - "SettingsRenameMergeAccount_Title": "Omdøb", - "SettingsReorderAccounts_Description": "Ændr rækkefølgen af konti i kontolisten.", - "SettingsReorderAccounts_Title": "Omarranger Konti", - "SettingsSemanticZoom_Description": "Dette vil give dig mulighed for at klikke på overskrifterne i listen over beskeder og gå til en bestemt dato", - "SettingsSemanticZoom_Title": "Semantisk forstørrelse for datooverskrifter", - "SettingsShowPreviewText_Description": "Skjul/vis forhåndsvisningsteksten.", - "SettingsShowPreviewText_Title": "Vis Forhåndsvisningstekst", - "SettingsShowSenderPictures_Description": "Vis/skjul miniaturebilleder af afsender", - "SettingsShowSenderPictures_Title": "Vis Afsender-Avatarer", + "SettingsAppPreferences_SearchMode_Title": "Default search mode", + "SettingsAppPreferences_ApplicationMode_Title": "Standard applikationstilstand", + "SettingsAppPreferences_ApplicationMode_Description": "Vælg hvilken tilstand Wino åbner i, når der ikke er angivet nogen aktiveringstype.", + "SettingsAppPreferences_ApplicationMode_Mail": "Mail", + "SettingsAppPreferences_ApplicationMode_Calendar": "Kalender", + "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description": "Wino Mail will keep running in the background. You will be notified as new mails arrive.", + "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title": "Run in the background", + "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description": "Wino Mail will keep running on the system tray. Available to launch by clicking on an icon. You will be notified as new mails arrive.", + "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Title": "Minimize to system tray", + "SettingsAppPreferences_ServerBackgroundingMode_Terminate_Description": "Wino Mail will not keep running anywhere. You will not be notified as new mails arrive. Launch Wino Mail again to continue mail synchronization.", + "SettingsAppPreferences_ServerBackgroundingMode_Terminate_Title": "Terminate", + "SettingsAppPreferences_StartupBehavior_Description": "Allow Wino Mail to launch minimized when Windows starts. Always allow it to receive notifications.", + "SettingsAppPreferences_StartupBehavior_Disable": "Disable", + "SettingsAppPreferences_StartupBehavior_Disabled": "Wino Mail will not be launched on Windows startup. This will cause you to miss notifications when you restart your computer.", + "SettingsAppPreferences_StartupBehavior_DisabledByPolicy": "Your administrator or group policies disabled running applications on startup. Thus, Wino Mail can't be set to launch on Windows startup.", + "SettingsAppPreferences_StartupBehavior_DisabledByUser": "Please go to Task Manager -> Startup tab to allow Wino Mail to launch on Windows startup.", + "SettingsAppPreferences_StartupBehavior_Enable": "Enable", + "SettingsAppPreferences_StartupBehavior_Enabled": "Wino Mail successfully set to be launched in the background on Windows startup.", + "SettingsAppPreferences_StartupBehavior_FatalError": "Fatal error occurred while changing the startup mode for Wino Mail.", + "SettingsAppPreferences_StartupBehavior_Title": "Start minimized on Windows startup", + "SettingsAppPreferences_Title": "App Preferences", + "SettingsAppPreferences_HideWinoAccountButton_Title": "Skjul Wino-konto-knappen i titelbjælken", + "SettingsAppPreferences_HideWinoAccountButton_Description": "Skjul profilknappen i titelbjælken, som åbner Wino-kontoens flyout.", + "SettingsAppPreferences_StoreUpdateNotifications_Title": "Meddelelser om opdateringer i Microsoft Store", + "SettingsAppPreferences_StoreUpdateNotifications_Description": "Vis notifikationer og handlinger i bunden, når der er en opdatering i Microsoft Store tilgængelig.", + "SettingsAppPreferences_AiActions_Title": "AI-handlinger", + "SettingsAppPreferences_AiActions_Description": "Vælg standard AI-sprog og hvor resuméer skal gemmes.", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Title": "Standard oversættelsessprog", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Description": "Vælg det standardmål, der bruges af AI-oversættelseshandlinger.", + "SettingsAppPreferences_AiSummarizeLanguage_Title": "Opsummeringssprog", + "SettingsAppPreferences_AiSummarizeLanguage_Description": "Vælg det foretrukne opsummeringssprog til fremtidige AI-sammendrag.", + "SettingsAppPreferences_AiSummarySavePath_Title": "Standard sti til opsummeringer", + "SettingsAppPreferences_AiSummarySavePath_Description": "Vælg den mappe, Wino som standard bør bruge, når AI-sammendrag gemmes.", + "SettingsAppPreferences_AiSummarySavePath_Placeholder": "Brug systemets standard gemmested.", + "SettingsAppPreferences_AiSummarySavePath_InvalidHint": "Denne mappe findes ikke. Standard gemmested vil blive brugt til opsummeringer.", + "SettingsAutoSelectNextItem_Description": "Select the next item after you delete or move a mail.", + "SettingsAutoSelectNextItem_Title": "Auto select next item", + "SettingsAvailableThemes_Description": "Select a theme from Wino's own collection for your taste or apply your own themes.", + "SettingsAvailableThemes_Title": "Available Themes", + "SettingsCalendarSettings_Description": "Change first day of week, hour cell height and more...", + "SettingsCalendarSettings_Title": "Calendar Settings", + "CalendarSettings_DefaultSnoozeDuration_Header": "Standard udsættelsesvarighed", + "CalendarSettings_DefaultSnoozeDuration_Description": "Indstil en standard udsættelsesvarighed for kalenderpåmindelsesmeddelelser.", + "CalendarSettings_TimedDayHeaderFormat_Header": "Dagsoverskriftsformat i tidsvisningen", + "CalendarSettings_TimedDayHeaderFormat_Description": "Vælg hvordan de øverste dagetiketter vises i dag-, uge- og arbejdsugevisninger. Brug datoformateringstokens som ddd, dd, MMM eller dddd.", + "SettingsComposer_Title": "Composer", + "SettingsComposerFont_Title": "Default Composer Font", + "SettingsComposerFontFamily_Description": "Change the default font family and font size for composing mails.", + "SettingsConfigureSpecialFolders_Description": "Set folders with special functions. Folders such as Archive, Inbox, and Drafts are essential for Wino to function properly.", + "SettingsConfigureSpecialFolders_Title": "Configure System Folders", + "SettingsCustomTheme_Description": "Create your own custom theme with custom wallpaper and accent color.", + "SettingsCustomTheme_Title": "Custom Theme", + "SettingsDeleteAccount_Description": "Delete all e-mails and credentials associated with this account.", + "SettingsDeleteAccount_Title": "Delete this account", + "SettingsDeleteProtection_Description": "Should Wino ask you for confirmation every time you try to permanently delete a mail using Shift + Del keys?", + "SettingsDeleteProtection_Title": "Permanent Delete Protection", + "SettingsDiagnostics_Description": "For developers", + "SettingsDiagnostics_DiagnosticId_Description": "Share this ID with the developers when asked to get help for the issues you experience in Wino Mail.", + "SettingsDiagnostics_DiagnosticId_Title": "Diagnostic ID", + "SettingsDiagnostics_Title": "Diagnostics", + "SettingsDiscord_Description": "Get regular development updates, join roadmap discussions and provide feedback.", + "SettingsDiscord_Title": "Discord Channel", + "SettingsEditLinkedInbox_Description": "Add / remove accounts, rename or break the link between accounts.", + "SettingsEditLinkedInbox_Title": "Edit Linked Inbox", + "SettingsWindowBackdrop_Title": "Vinduesbaggrund", + "SettingsWindowBackdrop_Description": "Vælg en baggrundseffekt til Wino-vinduer.", + "SettingsWindowBackdrop_Disabled": "Valg af vinduesbaggrund er deaktiveret, når app-temaet vælges andet end Standard.", + "SettingsElementTheme_Description": "Select a Windows theme for Wino", + "SettingsElementTheme_Title": "Element Theme", + "SettingsElementThemeSelectionDisabled": "Element theme selection is disabled when application theme is selected other than Default.", + "SettingsEnableHoverActions_Title": "Enable hover actions", + "SettingsEnableIMAPLogs_Description": "Enable this to provide details about IMAP connectivity issuses you had during IMAP server setup.", + "SettingsEnableIMAPLogs_Title": "Enable IMAP Protocol Logs", + "SettingsEnableLogs_Description": "I might need logs for crashes to diagnose issues you have opened in GitHub. None of the logs will expose your credentials or sensetive information to public.", + "SettingsEnableLogs_Title": "Enable Logs", + "SettingsEnableSignature": "Enable Signature", + "SettingsExpandOnStartup_Description": "Set whether Wino should expand this account's folders on startup.", + "SettingsExpandOnStartup_Title": "Expand Menu on Startup", + "SettingsExternalContent_Description": "Manage external content settings when rendering mails.", + "SettingsExternalContent_Title": "External Content", + "SettingsFocusedInbox_Description": "Set whether Inbox should be split into two as Focused - Other.", + "SettingsFocusedInbox_Title": "Focused Inbox", + "SettingsFolderMenuStyle_Description": "Change whether account folders should be nested inside an account menu item or not. Toggle this off if you like the old menu system in Windows Mail", + "SettingsFolderMenuStyle_Title": "Create Nested Folders", + "SettingsFolderOptions_Description": "Change individual folder settings like enable/disable sync or show/hide unread badge.", + "SettingsFolderOptions_Title": "Folder Configuration", + "SettingsFolderSync_Description": "Enable or disable specific folders for synchronization.", + "SettingsFolderSync_Title": "Folder Synchronization", + "SettingsFontFamily_Title": "Font Family", + "SettingsFontPreview_Title": "Preview", + "SettingsFontSize_Title": "Font Size", + "SettingsHoverActionCenter": "Center Action", + "SettingsHoverActionLeft": "Left Action", + "SettingsHoverActionRight": "Right Action", + "SettingsHoverActions_Description": "Select 3 actions to show up when you hover over the mails with cursor.", + "SettingsHoverActions_Title": "Hover Actions", + "SettingsLanguage_Description": "Change display language for Wino.", + "SettingsLanguage_Title": "Display Language", + "SettingsLanguageTime_Description": "Wino display language, preferred time format.", + "SettingsLanguageTime_Title": "Language & Time", + "SettingsLinkAccounts_Description": "Merge multiple accounts into one. See mails from one Inbox together.", + "SettingsLinkAccounts_Title": "Create Linked Accounts", + "SettingsLinkedAccountsSave_Description": "Modify the current link with the new accounts.", + "SettingsLinkedAccountsSave_Title": "Save Changes", + "SettingsLoadImages_Title": "Load images automatically", + "SettingsLoadPlaintextLinks_Title": "Convert plaintext links to clickable links", + "SettingsLoadStyles_Title": "Load styles automatically", + "SettingsMailListActionBar_Description": "Hide/show action bar at top of message list.", + "SettingsMailListActionBar_Title": "Show mail list actions", + "SettingsMailSpacing_Description": "Adjust the spacing for listing mails.", + "SettingsMailSpacing_Title": "Mail Spacing", + "SettingsManageAccountSettings_Description": "Notifications, signatures, synchronization and other settings per account.", + "SettingsManageAccountSettings_Title": "Manage Account Settings", + "SettingsManageAliases_Description": "See e-mail aliases assigned for this account, update or delete them.", + "SettingsManageAliases_Title": "Aliases", + "SettingsEditAccountDetails_Title": "Edit Account Details", + "SettingsEditAccountDetails_Description": "Change account name, sender name and assign a new color if you like.", + "EditAccountDetailsPage_SaveSuccess_Title": "Ændringer gemt", + "EditAccountDetailsPage_SaveSuccess_Message": "Dine kontooplysninger er opdateret.", + "SettingsManageLink_Description": "Move items to add new link or remove existing link.", + "SettingsManageLink_Title": "Manage Link", + "SettingsMarkAsRead_Description": "Change what should happen to the selected item.", + "SettingsMarkAsRead_DontChange": "Don't automatically mark item as read", + "SettingsMarkAsRead_SecondsToWait": "Seconds to wait: ", + "SettingsMarkAsRead_Timer": "When viewed in the reading pane", + "SettingsMarkAsRead_Title": "Mark item as read", + "SettingsMarkAsRead_WhenSelected": "When selected", + "SettingsMessageList_Description": "Change how your messages should be organized in mail list.", + "SettingsMessageList_Title": "Message List", + "SettingsNoAccountSetupMessage": "You didn't setup any accounts yet.", + "SettingsNotifications_Description": "Turn on or off notifications for this account.", + "SettingsNotifications_Title": "Notifications", + "SettingsNotificationsAndTaskbar_Description": "Change whether notifications should be displayed and taskbar badge for this account.", + "SettingsNotificationsAndTaskbar_Title": "Notifications & Taskbar", + "SettingsHome_Title": "Hjem", + "SettingsHome_SearchTitle": "Find en indstilling", + "SettingsHome_SearchDescription": "Søg efter funktion, emne eller nøgleord for at springe direkte til den rigtige indstillingssiden.", + "SettingsHome_SearchPlaceholder": "Søg i indstillinger", + "SettingsHome_SearchExamples": "Prøv: tema, opbevaring, sprog, signatur", + "SettingsHome_QuickLinks_Title": "Hurtige links", + "SettingsHome_QuickLinks_Description": "Gå direkte til de indstillinger, som bruges mest af folk.", + "SettingsHome_StorageCard_Description": "Se hvor meget lokalt MIME-indhold Wino opbevarer på denne enhed, og ryd det op, når det er nødvendigt.", + "SettingsHome_StorageEmptySummary": "Intet cachet MIME-indhold endnu.", + "SettingsHome_StorageLoading": "Kontrollerer lokal MIME-brug...", + "SettingsHome_Tips_Title": "Tips og tricks", + "SettingsHome_Tips_Description": "Et par små ændringer kan få Wino til at føles meget mere personlig.", + "SettingsHome_Tip_Theme": "Ønsker du mørk tilstand eller ændringer af accentfarver? Åbn Personalisering.", + "SettingsHome_Tip_Background": "Brug App-præferencer til at styre opstartsadfærd og baggrundssynkronisering.", + "SettingsHome_Tip_Shortcuts": "Tastaturgenveje hjælper dig med at bevæge dig gennem mails hurtigere.", + "SettingsHome_Resources_Title": "Hjælpelinks", + "SettingsHome_Resources_Description": "Åbn projektressourcer, supportoplysninger og udgivelseskanaler.", + "SettingsOptions_Title": "Settings", + "SettingsOptions_GeneralSection": "Generelt", + "SettingsOptions_MailSection": "Mail", + "SettingsOptions_CalendarSection": "Kalender", + "SettingsOptions_MoreComingSoon": "Flere muligheder kommer snart", + "SettingsOptions_HeroDescription": "Tilpas din Wino Mail-oplevelse.", + "SettingsOptions_AccountsSummary": "{0} konto(er) konfigureret", + "SettingsSearch_ManageAccounts_Keywords": "konto;konti;postkasse;postkasser;alias;aliaser;profil;adresse;adresser", + "SettingsSearch_AppPreferences_Keywords": "opstart;baggrund;start;synkronisering;notifikation;notifikationer;søg;systembakke;standardindstillinger", + "SettingsSearch_LanguageTime_Keywords": "sprog;tid;ur;lokalitet;region;format;24-timers;24t", + "SettingsSearch_Personalization_Keywords": "tema;mørk;lys;udseende;accent;farve;farve;tilstand;layout;densitet", + "SettingsSearch_About_Keywords": "om;version;hjemmeside;privatliv;github;donationer;butik;support", + "SettingsSearch_KeyboardShortcuts_Keywords": "genvej;genveje;hurtigtast;hurtigtaster;tastatur;taster", + "SettingsSearch_MessageList_Keywords": "besked;beskeder;liste;trådvisning;tråde;avatar;forhåndsvisning;afsender", + "SettingsSearch_ReadComposePane_Keywords": "læser;komponér;komponist;skrifttype;skrifttyper;ekstern indhold;visning;læsning", + "SettingsSearch_SignatureAndEncryption_Keywords": "signatur;signaturer;kryptering;certifikat;certifikater;S/MIME;smime;sikkerhed", + "SettingsSearch_Storage_Keywords": "lager;cache;cachelagring;mime;disk;plads;oprydning;ryd op;lokal data", + "SettingsSearch_CalendarSettings_Keywords": "kalender;uge;timer;tidsplan;begivenhed;begivenheder", + "SettingsPaneLengthReset_Description": "Reset the size of the mail list to original if you have issues with it.", + "SettingsPaneLengthReset_Title": "Reset Mail List Size", + "SettingsPaypal_Description": "Show much more love ❤️ All donations are appreciated.", + "SettingsPaypal_Title": "Donate via PayPal", + "SettingsPersonalization_Description": "Change appearance of Wino as you like.", + "SettingsPersonalization_Title": "Personalization", + "SettingsPersonalizationMailDisplayCompactMode": "Compact Mode", + "SettingsPersonalizationMailDisplayMediumMode": "Medium Mode", + "SettingsPersonalizationMailDisplaySpaciousMode": "Spacious Mode", + "SettingsPrefer24HourClock_Description": "Mail recieve times will be displayed in 24 hour format instead of 12 (AM/PM)", + "SettingsPrefer24HourClock_Title": "Display Clock Format in 24 Hours", + "SettingsPrivacyPolicy_Description": "Review privacy policy.", + "SettingsPrivacyPolicy_Title": "Privacy Policy", + "SettingsWebsite_Description": "Åbn Wino Mail-hjemmesiden.", + "SettingsWebsite_Title": "Hjemmeside", + "SettingsReadComposePane_Description": "Fonts, external content.", + "SettingsReadComposePane_Title": "Reader & Composer", + "SettingsReader_Title": "Reader", + "SettingsReaderFont_Title": "Default Reader Font", + "SettingsReaderFontFamily_Description": "Change the default font family and font size for reading mails.", + "SettingsRenameMergeAccount_Description": "Change the display name of the linked accounts.", + "SettingsRenameMergeAccount_Title": "Rename", + "SettingsReorderAccounts_Description": "Change the order of accounts in the account list.", + "SettingsReorderAccounts_Title": "Reorder Accounts", + "SettingsSemanticZoom_Description": "This will allow you to click on the headers in messages list and go to specific date", + "SettingsSemanticZoom_Title": "Semantic Zoom for Date Headers", + "SettingsShowPreviewText_Description": "Hide/show the preview text.", + "SettingsShowPreviewText_Title": "Show Preview Text", + "SettingsShowSenderPictures_Description": "Hide/show the thumbnail sender pictures.", + "SettingsShowSenderPictures_Title": "Show Sender Avatars", + "SettingsEmailTemplates_Title": "E-mail-skabeloner", + "SettingsEmailTemplates_Description": "Administrer e-mail-skabeloner", + "SettingsEmailTemplates_CreatePageTitle": "Ny skabelon", + "SettingsEmailTemplates_EditPageTitle": "Rediger skabelon", + "SettingsEmailTemplates_NewTemplateTitle": "Ny skabelon", + "SettingsEmailTemplates_NewTemplateDescription": "Opret en ny e-mail-skabelon", + "SettingsEmailTemplates_NameTitle": "Navn", + "SettingsEmailTemplates_NamePlaceholder": "Skabelonnavn", + "SettingsEmailTemplates_DescriptionTitle": "Beskrivelse", + "SettingsEmailTemplates_DescriptionPlaceholder": "Valgfri beskrivelse", + "SettingsEmailTemplates_ContentTitle": "Skabelonindhold", + "SettingsEmailTemplates_ContentDescription": "Rediger HTML-indholdet for denne skabelon.", + "SettingsEmailTemplates_NameRequired": "Skabelonnavn er påkrævet.", "SettingsEnableGravatarAvatars_Title": "Gravatar", - "SettingsEnableGravatarAvatars_Description": "Brug gravatar (hvis tilgængelig) som afsenderbillede", - "SettingsEnableFavicons_Title": "Domæneikoner (Favikoner)", - "SettingsEnableFavicons_Description": "Brug favikoner (hvis tilgængelig) som afsenderbillede", - "SettingsMailList_ClearAvatarsCache_Button": "Ryd cachelagrede avatarer", - "SettingsSignature_AddCustomSignature_Button": "Tilføj signatur", - "SettingsSignature_AddCustomSignature_Title": "Tilføj brugerdefineret signatur", - "SettingsSignature_DeleteSignature_Title": "Slet signatur", - "SettingsSignature_Description": "Administrer kontosignaturer", - "SettingsSignature_EditSignature_Title": "Rediger signatur", - "SettingsSignature_ForFollowingMessages_Title": "Til Svar/Vidersendelser", - "SettingsSignature_ForNewMessages_Title": "Til nye beskeder", - "SettingsSignature_NoneSignatureName": "Ingen", - "SettingsSignature_SignatureDefaults": "Standarder for signatur", - "SettingsSignature_Signatures": "Signaturer", - "SettingsSignature_Title": "Signatur", - "SettingsStartupItem_Description": "Primær konto, der skal åbne Indbakke ved opstart.", - "SettingsStartupItem_Title": "Opstartsobjekt", - "SettingsStore_Description": "Vis noget kærlighed ❤️", - "SettingsStore_Title": "Bedøm i butik", - "SettingsTaskbarBadge_Description": "Vis antal ulæste mails på proceslinjeikonet.", - "SettingsTaskbarBadge_Title": "Proceslinjebadge", - "SettingsThreads_Description": "Organiser beskeder i samtaletråde.", - "SettingsThreads_Title": "Samtaletråde", - "SettingsUnlinkAccounts_Description": "Fjern forbindelsen mellem konti. Dette vil ikke slette dine konti.", - "SettingsUnlinkAccounts_Title": "Fjern kontotilknytning", - "SettingsMailRendering_ActionLabels_Title": "Handlingsetiketter", - "SettingsMailRendering_ActionLabels_Description": "Vis handlingsetiketter", - "SignatureDeleteDialog_Message": "Er du sikker på du vil slette \"{0}\" signatur?", - "SignatureDeleteDialog_Title": "Slet signatur", - "SignatureEditorDialog_SignatureName_Placeholder": "Navngiv din signatur", - "SignatureEditorDialog_SignatureName_TitleEdit": "Nuværende signaturnavn: {0}", - "SignatureEditorDialog_SignatureName_TitleNew": "Signaturnavn", - "SignatureEditorDialog_Title": "Signaturedigering", - "SortingOption_Date": "efter dato", - "SortingOption_Name": "efter navn", - "StoreRatingDialog_MessageFirstLine": "Alle feedbacks er værdsat, og de vil gøre meget Wino bedre i fremtiden. Vil du bedømme Wino i Microsoft Store?", - "StoreRatingDialog_MessageSecondLine": "Ønsker du at bedømme Wino Mail i Microsoft Store?", - "StoreRatingDialog_Title": "Nyder du Wino?", - "SynchronizationFolderReport_Failed": "synkronisering fejlede", - "SynchronizationFolderReport_Success": "Opdateret", - "SystemFolderConfigDialog_ArchiveFolderDescription": "Arkiverede beskeder vil blive flyttet hertil.", - "SystemFolderConfigDialog_ArchiveFolderHeader": "Arkiv Mappe", - "SystemFolderConfigDialog_DeletedFolderDescription": "Slettede beskeder vil blive flyttet hertil.", - "SystemFolderConfigDialog_DeletedFolderHeader": "Slettet mappe", - "SystemFolderConfigDialog_DraftFolderDescription": "Nye mails/svar oprettes her.", - "SystemFolderConfigDialog_DraftFolderHeader": "Udkastmappe", - "SystemFolderConfigDialog_JunkFolderDescription": "Alle spam-mails vil være her.", - "SystemFolderConfigDialog_JunkFolderHeader": "Spam-mappe", - "SystemFolderConfigDialog_MessageFirstLine": "Denne IMAP-server understøtter ikke SPECIAL-USE-udvidelsen, derfor kunne Wino ikke konfigurere systemmapperne korrekt.", - "SystemFolderConfigDialog_MessageSecondLine": "Vælg venligst de relevante mapper til specifikke funktioner.", - "SystemFolderConfigDialog_SentFolderDescription": "Mappe, hvor sendte beskeder gemmes.", - "SystemFolderConfigDialog_SentFolderHeader": "Sendt Mappe", - "SystemFolderConfigDialog_Title": "Konfigurer Systemmapper", - "SystemFolderConfigDialogValidation_DuplicateSystemFolders": "Nogle af systemmapperne bruges mere end én gang i konfigurationen.", - "SystemFolderConfigDialogValidation_InboxSelected": "Du kan ikke tildele Indbakke-mappen til en anden systemmappe.", - "SystemFolderConfigSetupSuccess_Message": "Systemmapper er konfigureret.", - "SystemFolderConfigSetupSuccess_Title": "Opsætning Af Systemmapper", - "TestingImapConnectionMessage": "Tester serverforbindelse...", - "TitleBarServerDisconnectedButton_Description": "Wino er afbrudt fra netværket. Klik igen for at genoprette forbindelsen.", - "TitleBarServerDisconnectedButton_Title": "Ingen forbindelse", - "TitleBarServerReconnectButton_Title": "Forbind igen", - "TitleBarServerReconnectingButton_Title": "Opretter forbindelse", - "Today": "I dag", - "UnknownAddress": "Ukendt adresse", - "UnknownDateHeader": "Ukendt dato", - "UnknownGroupAddress": "Ukendt mailgruppeadresse", - "UnknownSender": "Ukendt afsender", - "Unsubscribe": "Afmeld", - "ViewContactDetails": "Vis detaljer", - "WinoUpgradeDescription": "Wino tilbyder 3 gratis konti til at starte med. Hvis du har brug for mere end 3 konti, bedes du opgradere.", - "WinoUpgradeMessage": "Opgrader til ubegrænsede konti", - "WinoUpgradeRemainingAccountsMessage": "{0} ud af {1} gratis konti anvendes.", - "Yesterday": "I går", - "SettingsAppPreferences_EmailSyncInterval_Title": "E-mail synkroniseringsinterval", - "SettingsAppPreferences_EmailSyncInterval_Description": "Automatisk synkroniseringsinterval for mails (minutter). Denne indstilling træder først i kraft efter genstart af Wino Mail." + "SettingsEnableGravatarAvatars_Description": "Use gravatar (if available) as sender picture", + "SettingsEnableFavicons_Title": "Domain icons (Favicons)", + "SettingsEnableFavicons_Description": "Use domain favicons (if available) as sender picture", + "SettingsMailList_ClearAvatarsCache_Button": "Clear cached avatars", + "SettingsSignature_AddCustomSignature_Button": "Add signature", + "SettingsSignature_AddCustomSignature_Title": "Add custom signature", + "SettingsSignature_DeleteSignature_Title": "Delete signature", + "SettingsSignature_Description": "Manage account signatures", + "SettingsSignature_EditSignature_Title": "Edit signature", + "SettingsSignature_ForFollowingMessages_Title": "For Replies/Forwards", + "SettingsSignature_ForNewMessages_Title": "For New Messages", + "SettingsSignature_NoneSignatureName": "None", + "SettingsSignature_SignatureDefaults": "Signature defaults", + "SettingsSignature_Signatures": "Signatures", + "SettingsSignature_Title": "Signature", + "SettingsStartupItem_Description": "Primary account item to load Inbox at startup.", + "SettingsStartupItem_Title": "Startup Item", + "SettingsStore_Description": "Show some love ❤️", + "SettingsStore_Title": "Rate in Store", + "SettingsStorage_Title": "Opbevaring", + "SettingsStorage_Description": "Scan og administrer MIME-cache gemt i din lokale data-mappe.", + "SettingsStorage_ScanFolder": "Scan lokal data-mappe", + "SettingsStorage_NoLocalMimeDataFound": "Ingen lokal MIME-data fundet.", + "SettingsStorage_NoAccountsFound": "Ingen konti fundet.", + "SettingsStorage_TotalUsage": "Samlet lokal MIME-brug: {0}", + "SettingsStorage_AccountUsageDescription": "{0} brugt i lokal MIME-cache", + "SettingsStorage_DeleteAll_Title": "Slet alt MIME-indhold", + "SettingsStorage_DeleteAll_Description": "Slet hele MIME-cache-mappen for denne konto.", + "SettingsStorage_DeleteAll_Button": "Slet alt", + "SettingsStorage_DeleteAll_Confirm_Title": "Slet alt MIME-indhold", + "SettingsStorage_DeleteAll_Confirm_Message": "Slet alle lokale MIME-data for {0}?", + "SettingsStorage_DeleteAll_Success": "Alle MIME-indhold blev slettet.", + "SettingsStorage_DeleteOld_Title": "Slet ældre MIME-indhold", + "SettingsStorage_DeleteOld_Description": "Slet MIME-filer baseret på oprettelsesdatoen for mails i den lokale database.", + "SettingsStorage_DeleteOld_1Month": "> 1 måned", + "SettingsStorage_DeleteOld_3Months": "> 3 måneder", + "SettingsStorage_DeleteOld_6Months": "> 6 måneder", + "SettingsStorage_DeleteOld_1Year": "> 1 år", + "SettingsStorage_DeleteOld_Confirm_Title": "Slet ældre MIME-indhold", + "SettingsStorage_DeleteOld_Confirm_Message": "Slet lokale MIME-data ældre end {0} for {1}?", + "SettingsStorage_DeleteOld_Success": "Slettede {0} MIME-mappe(r) ældre end {1}.", + "SettingsStorage_1Month": "1 måned", + "SettingsStorage_3Months": "3 måneder", + "SettingsStorage_6Months": "6 måneder", + "SettingsStorage_1Year": "1 år", + "SettingsStorage_Months": "{0} måneder", + "SettingsTaskbarBadge_Description": "Include unread mail count in taskbar icon.", + "SettingsTaskbarBadge_Title": "Taskbar Badge", + "SettingsThreads_Description": "Organize messages into conversation threads.", + "SettingsThreads_Title": "Conversation Threading", + "SettingsUnlinkAccounts_Description": "Remove the link between accounts. his will not delete your accounts.", + "SettingsUnlinkAccounts_Title": "Unlink Accounts", + "SettingsMailRendering_ActionLabels_Title": "Action labels", + "SettingsMailRendering_ActionLabels_Description": "Show action labels.", + "SignatureDeleteDialog_Message": "Are you sure you want to delete \"{0}\" signature?", + "SignatureDeleteDialog_Title": "Delete signature", + "SignatureEditorDialog_SignatureName_Placeholder": "Name your signature", + "SignatureEditorDialog_SignatureName_TitleEdit": "Current signature name: {0}", + "SignatureEditorDialog_SignatureName_TitleNew": "Signature name", + "SignatureEditorDialog_Title": "Signature Editor", + "SortingOption_Date": "by date", + "SortingOption_Name": "by name", + "StoreRatingDialog_MessageFirstLine": "All feedbacks are appreciated and they will make much Wino better in the future. Would you like to rate Wino in Microsoft Store?", + "StoreRatingDialog_MessageSecondLine": "Would you like to rate Wino Mail in Microsoft Store?", + "StoreRatingDialog_Title": "Enjoying Wino?", + "SynchronizationFolderReport_Failed": "synchronization is failed", + "SynchronizationFolderReport_Success": "up to date", + "SystemFolderConfigDialog_ArchiveFolderDescription": "Archived messages will be moved to here.", + "SystemFolderConfigDialog_ArchiveFolderHeader": "Archive Folder", + "SystemFolderConfigDialog_DeletedFolderDescription": "Deleted messages will be moved to here.", + "SystemFolderConfigDialog_DeletedFolderHeader": "Deleted Folder", + "SystemFolderConfigDialog_DraftFolderDescription": "New mails/replies will be crafted in here.", + "SystemFolderConfigDialog_DraftFolderHeader": "Draft Folder", + "SystemFolderConfigDialog_JunkFolderDescription": "All spam/junk mails will be here.", + "SystemFolderConfigDialog_JunkFolderHeader": "Junk/Spam Folder", + "SystemFolderConfigDialog_MessageFirstLine": "This IMAP server doesn't support SPECIAL-USE extension hence Wino couldn't setup the system folders properly.", + "SystemFolderConfigDialog_MessageSecondLine": "Please select the appropriate folders for specific functionalities.", + "SystemFolderConfigDialog_SentFolderDescription": "Folder that sent messages will be stored.", + "SystemFolderConfigDialog_SentFolderHeader": "Sent Folder", + "SystemFolderConfigDialog_Title": "Configure System Folders", + "SystemFolderConfigDialogValidation_DuplicateSystemFolders": "Some of the system folders are used more than once in the configuration.", + "SystemFolderConfigDialogValidation_InboxSelected": "You can't assign Inbox folder to any other system folder.", + "SystemFolderConfigSetupSuccess_Message": "System folders are successfully configured.", + "SystemFolderConfigSetupSuccess_Title": "System Folders Setup", + "SystemTrayMenu_ShowWino": "Åbn Wino Mail", + "SystemTrayMenu_ShowWinoCalendar": "Åbn Wino Kalender", + "SystemTrayMenu_ExitWino": "Afslut", + "TestingImapConnectionMessage": "Testing server connection...", + "TitleBarServerDisconnectedButton_Description": "Wino is disconnected from the network. Click reconnect to restore connection.", + "TitleBarServerDisconnectedButton_Title": "no connection", + "TitleBarServerReconnectButton_Title": "reconnect", + "TitleBarServerReconnectingButton_Title": "connecting", + "Today": "Today", + "UnknownAddress": "unknown address", + "UnknownDateHeader": "Unknown Date", + "UnknownGroupAddress": "unknown Mail Group Address", + "UnknownSender": "Unknown Sender", + "Unsubscribe": "Unsubscribe", + "ViewContactDetails": "View Details", + "WinoUpgradeDescription": "Wino offers 3 accounts to start with for free. If you need more than 3 accounts, please upgrade", + "WinoUpgradeMessage": "Upgrade to Unlimited Accounts", + "WinoUpgradeRemainingAccountsMessage": "{0} out of {1} free accounts used.", + "Yesterday": "Yesterday", + "Smime_ImportCertificates_Success": "Certifikater importeret med succes.", + "Smime_ImportCertificates_Error": "Fejl ved import af certifikater: {0}", + "Smime_RemoveCertificates_Confirm": "Vil du virkelig fjerne certifikaterne {0}?", + "Smime_RemoveCertificates_Success": "Certifikater fjernet.", + "Smime_ExportCertificates_Success": "Certifikater eksporteret.", + "Smime_ExportCertificates_Error": "Fejl ved eksport af certifikater.", + "Smime_CertificateDetails": "Emne: {0}\\nUdsteder: {1}\\nGyldig fra: {2}\\nGyldig til: {3}\\nFingeraftryk: {4}", + "Smime_CertificatePassword_Title": "Certifikatpassword påkrævet", + "Smime_CertificatePassword_Placeholder": "Certifikatpassword til {0} (valgfri)", + "Smime_Confirm_Title": "Bekræft", + "Buttons_OK": "OK", + "Buttons_Refresh": "Opdater", + "SettingsSignatureAndEncryption_Title": "Signatur og kryptering", + "SettingsSignatureAndEncryption_Description": "Administrer S/MIME-certifikater til underskrift og kryptering af e-mails.", + "SettingsSignatureAndEncryption_MyCertificatesHeader": "Mine certifikater", + "SettingsSignatureAndEncryption_MyCertificatesDescription": "Personlige certifikater til underskrift og kryptering", + "SettingsSignatureAndEncryption_RecipientCertificatesHeader": "Modtagercertifikater", + "SettingsSignatureAndEncryption_RecipientCertificatesDescription": "Modtagercertifikater til dekryptering", + "SettingsSignatureAndEncryption_NameColumn": "Navn", + "SettingsSignatureAndEncryption_ExpiresColumn": "Udløber den", + "SettingsSignatureAndEncryption_ThumbprintColumn": "Fingeraftryk", + "Buttons_Remove": "Fjern", + "Buttons_Export": "Eksportér", + "Buttons_Import": "Importer", + "SettingsSignatureAndEncryption_SigningCertificate": "S/MIME Signeringcertifikat", + "SettingsSignatureAndEncryption_EncryptionCertificate": "S/MIME-kryptering", + "SettingsSignatureAndEncryption_SigningCertificatePlaceholder": "Ingen", + "SmimeSignaturesInMessage": "Signaturer i denne besked:", + "SmimeSignatureEntry": "• {0} {1} ({2}, gyldig til {3} - {4})", + "SmimeSigningCertificateInfoTitle": "S/MIME Signeringcertifikat-info", + "SmimeCertificateInfoTitle": "S/MIME Certifikat-info", + "SmimeNoCertificateFileFound": "Ingen certifikatfil fundet.", + "SmimeSaveCertificate": "Gem certifikatet...", + "SmimeCertificate": "S/MIME-certifikat", + "SmimeCertificateSavedTo": "Certifikat gemt til {0}", + "SmimeSignedTooltip": "Denne besked er underskrevet med et S/MIME-certifikat. Klik for flere detaljer", + "SmimeEncryptedTooltip": "Denne besked er krypteret med et S/MIME-certifikat.", + "SmimeCertificateFileInfo": "Fil: {0}\\nType: {1}\\nStørrelse: {2:N0} bytes", + "Composer_LightTheme": "Lyst tema", + "Composer_DarkTheme": "Mørkt tema", + "Composer_Outdent": "Fjern indrykning", + "Composer_Indent": "Indryk", + "Composer_BulletList": "Punktliste", + "Composer_OrderedList": "Nummereret liste", + "Composer_Stroke": "Streg", + "Composer_Bold": "Fed", + "Composer_Italic": "Kursiv", + "Composer_Underline": "Understregning", + "Composer_CcBcc": "Cc og Bcc", + "Composer_EnableSmimeSignature": "Aktivér/deaktiver S/MIME-signatur", + "Composer_EnableSmimeEncryption": "Aktivér/deaktiver S/MIME-kryptering", + "Composer_LocalDraftSyncInfo": "Denne kladde er udelukkende lokal. Wino kunne ikke sende den til din mailserver. Klik for at prøve at sende den igen.", + "Composer_CertificateExpires": "Udløber den: ", + "Composer_SmimeSignature": "S/MIME-signatur", + "Composer_SmimeEncryption": "S/MIME-kryptering", + "Composer_EmailTemplatesPlaceholder": "E-mail-skabeloner", + "Composer_AiSummarize": "Sammenfat med AI", + "Composer_AiSummarizeDescription": "Uddrag nøglepunkter, handlingspunkter og beslutninger fra denne e-mail.", + "Composer_AiTranslate": "Oversæt med AI", + "Composer_AiActions": "AI-handlinger", + "Composer_AiRewrite": "Omskriv med AI", + "AiActions_CheckingStatus": "Kontrollerer AI-adgang...", + "AiActions_SignedOutTitle": "Få adgang til Wino AI-pakken.", + "AiActions_SignedOutDescription": "Oversæt, omskriv og opsummer e-mails med AI, når du har logget ind på din Wino-konto og aktiveret AI Pack-tilføjelsen.", + "AiActions_NoPackTitle": "AI-pakken kræves", + "AiActions_NoPackDescription": "Du er logget ind, men AI Pack er ikke aktiveret endnu. Køb den for at bruge Winos AI-oversættelse, omskrivning og sammenfatningsværktøjer.", + "AiActions_UsageSummary": "{0} ud af {1} kreditter brugt denne måned.", + "Composer_AiRewritePolite": "Gør den høflig", + "Composer_AiRewritePoliteDescription": "Gør formuleringen mere høflig uden at ændre intentionen.", + "Composer_AiRewriteAngry": "Gør den vred", + "Composer_AiRewriteAngryDescription": "Bruger en skarpere og mere konfronterende tone.", + "Composer_AiRewriteHappy": "Gør det muntert", + "Composer_AiRewriteHappyDescription": "Tilføjer en mere munter og entusiastisk tone.", + "Composer_AiRewriteFormal": "Gør det formelt", + "Composer_AiRewriteFormalDescription": "Gør beskeden mere professionel og struktureret.", + "Composer_AiRewriteFriendly": "Gør det venligt", + "Composer_AiRewriteFriendlyDescription": "Gør beskeden mere imødekommende.", + "Composer_AiRewriteShorter": "Gør det kortere", + "Composer_AiRewriteShorterDescription": "Trimmer teksten og fjerner unødvendige detaljer.", + "Composer_AiRewriteClearer": "Gør det tydeligere", + "Composer_AiRewriteClearerDescription": "Forbedrer læsbarheden og gør beskeden lettere at følge.", + "Composer_AiRewriteCustom": "Brugerdefineret", + "Composer_AiRewriteCustomDescription": "Beskriv din egen omskrivningsintention.", + "Composer_AiRewriteCustomPlaceholder": "Beskriv hvordan du vil have beskeden omskrevet", + "Composer_AiRewriteMode": "Omskrivning af tone", + "Composer_AiRewriteApply": "Anvend omskrivning", + "Composer_AiTranslateDialogTitle": "Oversæt med AI", + "Composer_AiTranslateDialogDescription": "Indtast målsproget eller kulturkoden, f.eks. en-US, tr-TR, de-DE eller fr-FR.", + "Composer_AiTranslateApply": "Oversæt", + "Composer_AiTranslateLanguage": "Målsprog", + "Composer_AiTranslateCustomPlaceholder": "Indtast kulturkode", + "Composer_AiTranslateLanguageEnglish": "Engelsk (en-US)", + "Composer_AiTranslateLanguageTurkish": "tyrkisk (tr-TR)", + "Composer_AiTranslateLanguageGerman": "Tysk (de-DE)", + "Composer_AiTranslateLanguageFrench": "Fransk (fr-FR)", + "Composer_AiTranslateLanguageSpanish": "Spansk (es-ES)", + "Composer_AiTranslateLanguageItalian": "Italiensk (it-IT)", + "Composer_AiTranslateLanguagePortugueseBrazil": "Portugisisk (Brasilien) (pt-BR)", + "Composer_AiTranslateLanguageDutch": "Nederlandsk (nl-NL)", + "Composer_AiTranslateLanguagePolish": "Polsk (pl-PL)", + "Composer_AiTranslateLanguageRussian": "Russisk (ru-RU)", + "Composer_AiTranslateLanguageJapanese": "Japansk (ja-JP)", + "Composer_AiTranslateLanguageKorean": "Koreansk (ko-KR)", + "Composer_AiTranslateLanguageChineseSimplified": "Kinesisk, forenklet (zh-CN)", + "Composer_AiTranslateLanguageArabic": "Arabisk (ar-SA)", + "Composer_AiTranslateLanguageHindi": "Hindi (hi-IN)", + "Composer_AiTranslateLanguageOther": "Andet...", + "Composer_AiBusyTitle": "AI arbejder allerede", + "Composer_AiBusyMessage": "Vent venligst, indtil den nuværende AI-handling er færdig.", + "Composer_AiSignInRequired": "Log ind på din Wino-konto for at bruge AI-funktioner.", + "Composer_AiMissingHtml": "Der er ikke noget beskedindhold at sende til Wino AI endnu.", + "Composer_AiQuotaUnavailable": "AI-resultatet blev anvendt.", + "Composer_AiAppliedMessage": "AI-resultatet blev anvendt i komponisten. Brug fortryd, hvis du vil fortryde det.", + "Composer_AiSummarizeSuccessTitle": "AI-sammenfatning anvendt", + "Composer_AiTranslateSuccessTitle": "AI-oversættelse anvendt", + "Composer_AiRewriteSuccessTitle": "AI-omskrivning anvendt", + "Composer_AiErrorTitle": "AI-handlingen mislykkedes", + "Reader_AiAppliedMessage": "AI-resultatet vises nu for denne besked. Genåbn beskeden for at se det oprindelige indhold igen.", + "SettingsAppPreferences_EmailSyncInterval_Title": "Email sync interval", + "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail.", + "ContactsPage_Title": "Kontakter", + "ContactsPage_AddContact": "Tilføj kontakt", + "ContactsPage_EditContact": "Rediger kontakt", + "ContactsPage_DeleteContact": "Slet kontakt", + "ContactsPage_SearchPlaceholder": "Søg kontakter...", + "ContactsPage_NoContacts": "Ingen kontakter fundet", + "ContactsPage_ContactsCount": "{0} kontakter", + "ContactsPage_SelectedContactsCount": "{0} valgte", + "ContactsPage_DeleteSelectedContacts": "Slet valgte", + "ContactEditDialog_Title": "Rediger kontakt", + "ContactEditDialog_PhotoSection": "Foto", + "ContactEditDialog_ChoosePhoto": "Vælg foto", + "ContactEditDialog_RemovePhoto": "Fjern foto", + "ContactEditDialog_NameHeader": "Navn", + "ContactEditDialog_NamePlaceholder": "Kontaktnavn", + "ContactEditDialog_EmailHeader": "E-mail adresse", + "ContactEditDialog_EmailPlaceholder": "kontakt@example.com", + "ContactEditDialog_InfoSection": "Kontaktoplysninger", + "ContactEditDialog_RootContactInfo": "Dette er en rodkontakt tilknyttet dine konti og kan ikke slettes.", + "ContactEditDialog_OverriddenContactInfo": "Denne kontakt er manuelt ændret og opdateres ikke under synkronisering.", + "ContactsPage_Subtitle": "Administrer dine e-mailkontakter og deres oplysninger", + "ContactStatus_Account": "Konto", + "ContactStatus_Modified": "Ændret", + "ContactAction_Edit": "Rediger kontakt", + "ContactAction_ChangePhoto": "Skift foto", + "ContactAction_Delete": "Slet kontakt", + "ContactAction_Add": "Tilføj kontakt", + "ContactSelection_Selected": "valgt", + "ContactSelection_SelectAll": "Vælg alle", + "ContactSelection_Clear": "Ryd markering", + "ContactsPage_EmptyState": "Ingen kontakter at vise", + "ContactsPage_AddFirstContact": "Tilføj din første kontakt", + "ContactsPage_ContactsCountSuffix": "kontakter", + "ContactsPane_NewContact": "Ny kontakt", + "ContactsPane_DescriptionTitle": "Administrer dine kontakter", + "ContactsPane_DescriptionBody": "Opret kontakter, omdøb dem, opdater profilbilleder, og hold gemte oplysninger organiseret et sted.", + "ContactEditDialog_AddTitle": "Tilføj kontakt", + "ContactInfoBar_ContactAdded": "Kontakt tilføjet med succes.", + "ContactInfoBar_ContactUpdated": "Kontakt opdateret med succes.", + "ContactInfoBar_ContactsDeleted": "Kontakter slettet med succes.", + "ContactInfoBar_ContactPhotoUpdated": "Kontaktfoto opdateret med succes.", + "ContactInfoBar_FailedToLoadContacts": "Kunne ikke indlæse kontakter: {0}", + "ContactInfoBar_FailedToAddContact": "Kunne ikke tilføje kontakt: {0}", + "ContactInfoBar_FailedToUpdateContact": "Kunne ikke opdatere kontakt: {0}", + "ContactInfoBar_FailedToDeleteContacts": "Kunne ikke slette kontakter: {0}", + "ContactInfoBar_FailedToUpdatePhoto": "Kunne ikke opdatere foto: {0}", + "ContactInfoBar_CannotDeleteRoot": "Rodkontakter kan ikke slettes.", + "ContactConfirmDialog_DeleteTitle": "Slet kontakt", + "ContactConfirmDialog_DeleteMessage": "Er du sikker på, at du vil slette kontakten '{0}'?", + "ContactConfirmDialog_DeleteMultipleMessage": "Er du sikker på, at du vil slette {0} kontakt(er)?", + "ContactConfirmDialog_DeleteButton": "Slet", + "CalendarAccountSettings_Title": "Kalenderkontoindstillinger", + "CalendarAccountSettings_Description": "Administrer kalenderindstillinger for {0}", + "CalendarAccountSettings_AccountColor": "Konto-farve", + "CalendarAccountSettings_AccountColorDescription": "Skift visningsfarven for denne kalenderkonto", + "CalendarAccountSettings_SyncEnabled": "Aktivér synkronisering", + "CalendarAccountSettings_SyncEnabledDescription": "Aktivér eller deaktiver kalender-synkronisering for denne konto.", + "CalendarAccountSettings_DefaultShowAs": "Standard tilgængelighedsstatus", + "CalendarAccountSettings_DefaultShowAsDescription": "Standard tilgængelighedsstatus for nye begivenheder oprettet med denne konto.", + "CalendarAccountSettings_PrimaryCalendar": "Primær kalender", + "CalendarAccountSettings_PrimaryCalendarDescription": "Marker denne kalender som primær kalender for kontoen", + "CalendarSettings_NewEventBehavior_Header": "Opførsel for knappen Ny begivenhed", + "CalendarSettings_NewEventBehavior_Description": "Vælg om knappen Ny begivenhed skal spørge om en kalender hver gang eller altid åbne en specifik kalender.", + "CalendarSettings_NewEventBehavior_AskEachTime": "Spørg hver gang.", + "CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar": "Brug altid en specifik kalender.", + "CalendarSettings_Rendering_Title": "Visning", + "CalendarSettings_Rendering_Description": "Konfigurer kalenderlayout og visningsadfærd.", + "CalendarSettings_Notifications_Title": "Notifikationer", + "CalendarSettings_Notifications_Description": "Vælg standard påmindelses- og udsættelsesadfærd.", + "CalendarSettings_Preferences_Title": "Præferencer", + "CalendarSettings_Preferences_Description": "Indstil hvordan knappen Ny begivenhed opfører sig.", + "WhatIsNew_GetStartedButton": "Kom i gang", + "WhatIsNew_ContinueAnywayButton": "Fortsæt alligevel", + "WhatIsNew_PreparingForNewVersionButton": "Forbereder ny version...", + "WhatIsNew_MigrationPreparing_Title": "Forbereder dine data", + "WhatIsNew_MigrationPreparing_Description": "Wino anvender opdateringsmigrering. Vent venligst, mens vi forbereder dine kontooplysninger til denne udgivelse.", + "WhatIsNew_MigrationFailedMessage": "Anvendelse af migreringer mislykkedes med fejlkode {0}. Du kan fortsætte med at bruge applikationen. Hvis du oplever alvorlige problemer, bedes du geninstallere applikationen.", + "WhatIsNew_MigrationNotification_Title": "Wino Mail opdateret", + "WhatIsNew_MigrationNotification_Message": "Åbn appen for at fuldføre opdateringen og se hvad der er nyt.", + "WelcomeWindow_Title": "Velkommen til Wino Mail", + "WelcomeWindow_Subtitle": "En native Windows-oplevelse til Mail og Kalender.", + "WelcomeWindow_WhatsNewTitle": "Seneste ændringer", + "WelcomeWindow_FeaturesTitle": "Funktioner", + "WelcomeWindow_WhatsNewTab": "Hvad er nyt", + "WelcomeWindow_FeaturesTab": "Funktioner", + "WelcomeWindow_GetStartedButton": "Kom i gang ved at tilføje en konto", + "WelcomeWindow_GetStartedDescription": "Tilføj din Outlook-, Gmail- eller IMAP-konto for at komme i gang med Wino Mail.", + "WelcomeWindow_ImportFromWinoAccount": "Importer fra din Wino-konto", + "WelcomeWindow_ImportInProgress": "Importerere dine synkroniserede præferencer og konti...", + "WelcomeWindow_ImportNoAccountsFound": "Ingen synkroniserede konti blev fundet i din Wino-konto. Hvis præferencer var tilgængelige, blev de gendannet. Brug Kom i gang til manuelt at tilføje en konto.", + "WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} synkroniserede konti er allerede tilgængelige på denne enhed. Brug Kom i gang til manuelt at tilføje en anden konto, hvis det er nødvendigt.", + "WelcomeWindow_SetupTitle": "Opsæt din konto", + "WelcomeWindow_SetupSubtitle": "Vælg din mailudbyder for at komme i gang", + "WelcomeWindow_AddAccountButton": "Tilføj konto", + "WelcomeWindow_SkipForNow": "Spring over for nu — jeg vil konfigurere den senere", + "WelcomeWindow_AppDescription": "En hurtig, fokuseret indbakke — nydesignet til Windows 11.", + "WelcomeWizard_Step1Title": "Velkommen", + "SystemTrayMenu_Open": "Åbn", + "WinoAccount_Titlebar_SyncBenefitTitle": "Synkroniseringsindstillinger", + "WinoAccount_Titlebar_SyncBenefitDescription": "Hold dine Wino-præferencer synkroniseret på tværs af enheder.", + "WinoAccount_Titlebar_AddonsBenefitTitle": "Få adgang til tilføjelser", + "WinoAccount_Titlebar_AddonsBenefitDescription": "Adgang til premiumfunktioner som Wino AI Pack.", + "WinoAccount_Management_Description": "Administrer din Wino-konto, adgang til AI Pack og synkroniserede præferencer og kontooplysninger.", + "WinoAccount_Management_SignedOutTitle": "Log ind på Wino Mail", + "WinoAccount_Management_SignedOutDescription": "Log ind eller opret en konto for at synkronisere din e-mail, få adgang til AI-funktioner og administrere dine indstillinger på tværs af enheder.", + "WinoAccount_Management_ProfileSectionHeader": "Profil", + "WinoAccount_Management_AddOnsSectionHeader": "Wino-tilføjelser", + "WinoAccount_Management_DataSectionHeader": "Data", + "WinoAccount_Management_AccountActionsSectionHeader": "Kontohandlinger", + "WinoAccount_Management_AccountCardTitle": "Konto", + "WinoAccount_Management_AccountCardDescription": "Din Wino-konto e-mailadresse og den aktuelle kontostatus.", + "WinoAccount_Management_AiPackCardTitle": "AI Pack", + "WinoAccount_Management_AiPackCardDescription": "Se om Wino AI Pack er aktiveret, og hvor meget brug der er tilbage.", + "WinoAccount_Management_AiPackActive": "AI Pack er aktiv", + "WinoAccount_Management_AiPackInactive": "AI Pack er ikke aktiv", + "WinoAccount_Management_AiPackUsage": "{0} af {1} anvendelser brugt. {2} tilbage.", + "WinoAccount_Management_AiPackBillingPeriod": "Faktureringsperiode: {0:d} - {1:d}", + "WinoAccount_Management_AiPackUnknownUsage": "Brugsoplysninger er endnu ikke tilgængelige.", + "WinoAccount_Management_AiPackBuyDescription": "Køb Wino AI Pack for at oversætte, omskrive eller opsummere e-mails med AI.", + "WinoAccount_Management_AiPackPromoTitle": "Få adgang til AI Pack", + "WinoAccount_Management_AiPackPromoDescription": "Forbedr din e-mail-arbejdsgang med AI-drevne værktøjer. Oversæt beskeder til mere end 50 sprog, omskriv for klarhed og tone, og få øjeblikkelige sammendrag af lange tråde.", + "WinoAccount_Management_AiPackPromoPrice": "$4.99 / mo", + "WinoAccount_Management_AiPackPromoRequests": "1.000 kreditter", + "WinoAccount_Management_AiPackGetButton": "Få AI Pack", + "WinoAddOn_AI_PACK_Name": "Wino AI Pack", + "WinoAddOn_AI_PACK_Description": "AI-drevne værktøjer til oversættelse, omskrivning og sammenfatning af e-mails i Wino Mail.", + "WinoAddOn_AI_PACK_Keywords": "AI, oversæt, omskriv, opsummer, produktivitet", + "WinoAddOn_UNLIMITED_ACCOUNTS_Name": "Ubegrænsede konti", + "WinoAddOn_UNLIMITED_ACCOUNTS_Description": "Fjern kontoens begrænsning og tilføj så mange mailkonti, som du har brug for.", + "WinoAddOn_UNLIMITED_ACCOUNTS_Keywords": "konti, ubegrænsede, premium, tilføjelsesmodul", + "WinoAccount_Management_PurchaseRequiresSignIn": "Log ind med din Wino-konto for at gennemføre dette køb.", + "WinoAccount_Management_PurchaseStartFailed": "Wino kunne ikke gennemføre dette køb i Microsoft Store.", + "WinoAccount_Management_StoreSyncFailed": "Dit køb er gennemført, men Wino kunne ikke opdatere dine kontoens fordele endnu. Prøv igen om et øjeblik.", + "WinoAccount_Management_AiPackSubscriptionActive": "Dit abonnement er aktivt.", + "WinoAccount_Management_AiPackRenews": "Fornyer {0:d}", + "WinoAccount_Management_AiPackRequestsUsed": "Kreditter brugt denne måned", + "WinoAccount_Management_AiPackResets": "Nulstiller {0:d}", + "WinoAccount_Management_AiPackUsageLoadFailed": "Vi havde problemer med at indlæse din AI-brugsbalance.", + "WinoAccount_Management_AiPackFeatureTranslate": "Oversæt", + "WinoAccount_Management_AiPackFeatureRewrite": "Omskriv", + "WinoAccount_Management_AiPackFeatureSummarize": "Opsummer", + "WinoAccount_Management_AddOnLoadFailed": "Vi havde problemer med at indlæse denne tilføjelse.", + "WinoAccount_Management_SyncPreferencesTitle": "Synkroniser præferencer og konti", + "WinoAccount_Management_SyncPreferencesDescription": "Importer eller eksporter dine Wino-præferencer og postkasseoplysninger på tværs af enheder. Adgangskoder, tokens og andre følsomme oplysninger bliver aldrig synkroniseret.", + "WinoAccount_Management_SignOutTitle": "Log ud", + "WinoAccount_Management_SignOutDescription": "Log ud af din konto på denne enhed", + "WinoAccount_Management_StatusLabel": "Status: {0}", + "WinoAccount_Management_NoRemoteSettings": "Der er endnu ikke gemt synkroniserede data for denne konto.", + "WinoAccount_Management_ExportSucceeded": "Dine valgte Wino-data blev eksporteret med succes.", + "WinoAccount_Management_ExportPreferencesSucceeded": "Dine præferencer blev eksporteret til din Wino-konto.", + "WinoAccount_Management_ExportAccountsSucceeded": "Eksporterede {0} kontooplysninger til din Wino-konto.", + "WinoAccount_Management_ImportSucceeded": "Importerede synkroniserede data fra din Wino-konto.", + "WinoAccount_Management_ImportPreferencesSucceeded": "Anvendte {0} synkroniserede præferencer.", + "WinoAccount_Management_ImportAccountsSucceeded": "Importerede {0} konti.", + "WinoAccount_Management_ImportDuplicateAccountsSkipped": "Springede {0} konti, der allerede findes på denne enhed.", + "WinoAccount_Management_ImportPartial": "Anvendte {0} synkroniserede præferencer. {1} præferencer kunne ikke gendannes.", + "WinoAccount_Management_ImportReloginReminder": "Adgangskoder, tokens og andre følsomme oplysninger blev ikke importeret. Log ind igen for hver konto på denne enhed, før du bruger den.", + "WinoAccount_Management_SerializeFailed": "Wino kunne ikke serialisere dine nuværende præferencer.", + "WinoAccount_Management_EmptyExport": "Der er ingen præferencer at eksportere.", + "WinoAccount_Management_ImportEmpty": "Den synkroniserede datapayload indeholder ikke noget nyt at gendanne.", + "WinoAccount_Management_ExportDialog_Title": "Eksporter til din Wino-konto", + "WinoAccount_Management_ExportDialog_Description": "Vælg, hvad du vil synkronisere til din Wino-konto.", + "WinoAccount_Management_ExportDialog_IncludePreferences": "Præferencer", + "WinoAccount_Management_ExportDialog_IncludeAccounts": "Konti", + "WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Adgangskoder, tokens og andre følsomme oplysninger synkroniseres ikke.", + "WinoAccount_Management_ExportDialog_AccountsRelogin": "Importerede konti på en anden PC kræver stadig, at du logger ind igen, før de kan bruges.", + "WinoAccount_Management_ExportDialog_InProgress": "Eksporterer dine valgte Wino-data...", + "WinoAccount_Management_LoadFailed": "Wino kunne ikke indlæse de seneste oplysninger om Wino-kontoen.", + "WinoAccount_Management_ActionFailed": "Anmodningen til Wino-kontoen kunne ikke gennemføres.", + "WinoAccount_SettingsSection_Title": "Wino-konto", + "WinoAccount_SettingsSection_Description": "Opret eller log ind på en Wino-konto ved hjælp af din localhost-godkendelsestjeneste.", + "WinoAccount_RegisterButton_Title": "Registrer konto", + "WinoAccount_RegisterButton_Description": "Opret en Wino-konto med e-mail og adgangskode.", + "WinoAccount_RegisterButton_Action": "Åbn registrering", + "WinoAccount_LoginButton_Title": "Log ind", + "WinoAccount_LoginButton_Description": "Log ind på en eksisterende Wino-konto med e-mail og adgangskode.", + "WinoAccount_LoginButton_Action": "Åbn login", + "WinoAccount_SignOutButton_Title": "Log ud", + "WinoAccount_SignOutButton_Description": "Fjern den lokalt gemte Wino-konto-session.", + "WinoAccount_SignOutButton_Action": "Log ud", + "WinoAccount_RegisterDialog_Title": "Opret Wino-konto", + "WinoAccount_RegisterDialog_Description": "Opret en Wino-konto for at holde din Wino-oplevelse i synkroniseret og låse op kontobaserede tilføjelser.", + "WinoAccount_RegisterDialog_HeroTitle": "Opret din Wino-konto", + "WinoAccount_RegisterDialog_BenefitsTitle": "Hvorfor oprette en?", + "WinoAccount_RegisterDialog_BenefitSyncTitle": "Importer og eksporter indstillinger på tværs af enheder.", + "WinoAccount_RegisterDialog_BenefitSyncDescription": "Flyt dine Wino-præferencer mellem enheder uden at skulle opbygge dit setup fra bunden.", + "WinoAccount_RegisterDialog_BenefitAiTitle": "Få adgang til eksklusive tilføjelser som Wino AI Pack (betalt)", + "WinoAccount_RegisterDialog_BenefitAiDescription": "Brug én konto til at låse op for Wino-premium-funktioner, efterhånden som de bliver tilgængelige.", + "WinoAccount_RegisterDialog_DifferenceTitle": "Wino-kontoen er adskilt fra dine mailkonti.", + "WinoAccount_RegisterDialog_DifferenceDescription": "Dine Outlook-, Gmail-, IMAP-, eller andre e-mailkonti forbliver præcis som de er. En Wino-konto håndterer udelukkende Wino-specifikke funktioner og kontobaserede tilføjelser.", + "WinoAccount_RegisterDialog_PrimaryButton": "Registrer", + "WinoAccount_RegisterDialog_PrivacyTitle": "Privatliv og API-behandling", + "WinoAccount_RegisterDialog_PrivacyDescription": "Valgfrie tilføjelser som Wino AI Pack kan sende udvalgte e-mail HTML-indhold til Wino API-tjenesten, kun når du bruger disse funktioner.", + "WinoAccount_RegisterDialog_PrivacyLinkText": "Læs privatlivspolitikken", + "WinoAccount_RegisterDialog_PrivacyCheckbox": "Jeg accepterer privatlivspolitikken.", + "WinoAccount_LoginDialog_Title": "Log ind på Wino-konto", + "WinoAccount_LoginDialog_Description": "Log ind på din Wino-konto for at synkronisere din Wino-opsætning og få adgang til kontobaserede funktioner.", + "WinoAccount_LoginDialog_HeroTitle": "Velkommen tilbage.", + "WinoAccount_LoginDialog_BenefitsTitle": "Hvad du får ved at logge ind.", + "WinoAccount_LoginDialog_BenefitsDescription": "Brug din Wino-konto til at fortsætte synkronisering af indstillinger på tværs af enheder og få adgang til betalte tilføjelser som Wino AI Pack.", + "WinoAccount_LoginDialog_DifferenceTitle": "Dette er ikke din e-mailkonto-login.", + "WinoAccount_LoginDialog_DifferenceDescription": "Log ind her tilføjer ikke eller erstatter dine Outlook-, Gmail-, eller IMAP-konti i Wino. Det logger dig kun ind på Wino-specifikke tjenester.", + "WinoAccount_LoginDialog_ForgotPasswordLink": "Glemt adgangskode?", + "WinoAccount_EmailLabel": "E-mail", + "WinoAccount_EmailPlaceholder": "name@example.com", + "WinoAccount_PasswordLabel": "Adgangskode", + "WinoAccount_ConfirmPasswordLabel": "Bekræft adgangskode", + "WinoAccount_ForgotPasswordDialog_Title": "Nulstil din adgangskode.", + "WinoAccount_ForgotPasswordDialog_PrimaryButton": "Send nulstillings-e-mail.", + "WinoAccount_ForgotPasswordDialog_BackToSignIn": "Tilbage til log ind", + "WinoAccount_ForgotPasswordDialog_Description": "Indtast din Wino-konto-e-mailadresse, og vi sender dig et link til nulstilling af adgangskoden, hvis adressen er registreret.", + "WinoAccount_Validation_EmailRequired": "E-mail er påkrævet.", + "WinoAccount_Validation_PasswordRequired": "Adgangskode er påkrævet.", + "WinoAccount_Validation_PasswordMismatch": "Adgangskoderne stemmer ikke overens.", + "WinoAccount_Validation_PrivacyConsentRequired": "Du skal acceptere privatlivspolitikken, før du opretter en Wino-konto.", + "WinoAccount_Error_InvalidCredentials": "E-mailadressen eller adgangskoden er forkert.", + "WinoAccount_Error_AccountLocked": "Denne konto er midlertidigt låst.", + "WinoAccount_Error_AccountBanned": "Denne konto er blevet forbudt.", + "WinoAccount_Error_AccountSuspended": "Denne konto er blevet suspenderet.", + "WinoAccount_Error_EmailNotConfirmed": "Bekræft venligst din e-mailadresse, før du logger ind.", + "WinoAccount_Error_EmailConfirmationRequired": "Bekræft venligst din e-mailadresse, før du logger ind.", + "WinoAccount_Error_EmailConfirmationResendNotAvailable": "En ny bekræftelses-e-mail er endnu ikke tilgængelig.", + "WinoAccount_Error_EmailConfirmationResendInvalid": "Denne bekræftelsesanmodning er ikke længere gyldig. Prøv at logge ind igen.", + "WinoAccount_Error_EmailNotRegistered": "Denne e-mailadresse er ikke registreret.", + "WinoAccount_Error_RefreshTokenInvalid": "Din session er ikke længere gyldig. Log ind igen.", + "WinoAccount_Error_EmailAlreadyRegistered": "Denne e-mailadresse er allerede registreret.", + "WinoAccount_Error_ExternalLoginEmailRequired": "En e-mailadresse er påkrævet for at fuldføre ekstern sign-in.", + "WinoAccount_Error_ExternalLoginInvalid": "Anmodningen om ekstern sign-in er ugyldig.", + "WinoAccount_Error_ExternalAuthStateInvalid": "Den eksterne sign-in-tilstand er ugyldig eller udløbet.", + "WinoAccount_Error_ExternalAuthCodeInvalid": "Den eksterne sign-in-kode er ugyldig eller udløbet.", + "WinoAccount_Error_AiPackRequired": "Et aktivt Wino AI Pack-abonnement er påkrævet for denne handling.", + "WinoAccount_Error_AiQuotaExceeded": "Din AI Pack-brugsgrænse er nået for den aktuelle faktureringsperiode.", + "WinoAccount_Error_AiHtmlEmpty": "Der er ikke noget e-mailindhold at behandle.", + "WinoAccount_Error_AiHtmlTooLarge": "Denne e-mail er for stor til at behandles med Wino AI.", + "WinoAccount_Error_AiUnsupportedLanguage": "Det sprog understøttes ikke. Prøv en gyldig kulturkode som en-US eller tr-TR.", + "WinoAccount_Error_Forbidden": "Du har ikke tilladelse til at udføre denne handling.", + "WinoAccount_Error_ValidationFailed": "Anmodningen er ugyldig. Gennemgå de indtastede værdier.", + "WinoAccount_RegisterSuccessMessage": "Registrering af Wino-kontoen er gennemført for {0}.", + "WinoAccount_LoginSuccessMessage": "Logget ind på Wino-konto som {0}.", + "WinoAccount_EmailConfirmationSentDialog_Title": "Bekræft din e-mailadresse", + "WinoAccount_EmailConfirmationSentDialog_Message": "Vi har sendt en bekræftelses-e-mail til {0}. Bekræft den venligst og prøv at logge ind igen.", + "WinoAccount_EmailConfirmationPendingDialog_Title": "E-mailbekræftelse kræves.", + "WinoAccount_EmailConfirmationPendingDialog_Message": "Vi venter stadig på, at du bekræfter {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ResendButton": "Send bekræftelses-e-mail igen.", + "WinoAccount_EmailConfirmationPendingDialog_Countdown": "Du kan sende bekræftelses-e-mailen igen om {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ReadyToResend": "Du kan nu sende bekræftelses-e-mailen igen.", + "WinoAccount_EmailConfirmationResentDialog_Title": "Bekræftelses-e-mail gensendt.", + "WinoAccount_EmailConfirmationResentDialog_Message": "Vi har sendt endnu en bekræftelses-e-mail til {0}. Bekræft den venligst og prøv at logge ind igen.", + "WinoAccount_ForgotPasswordDialog_SuccessTitle": "Nulstillings-e-mailen er sendt.", + "WinoAccount_ForgotPasswordDialog_SuccessMessage": "Vi har sendt en nulstillings-e-mail til {0}. Åbn den besked for at vælge en ny adgangskode.", + "WinoAccount_ChangePassword_Title": "Skift adgangskode.", + "WinoAccount_ChangePassword_Description": "Send en nulstillings-e-mail til denne Wino-konto.", + "WinoAccount_ChangePassword_Action": "Send nulstillings-e-mail.", + "WinoAccount_ChangePassword_ConfirmationMessage": "Vil du have Wino til at sende en nulstilling af adgangskoden til {0}?", + "WinoAccount_SignOut_SuccessMessage": "Logget ud fra Wino-konto {0}.", + "WinoAccount_SignOut_NoAccountMessage": "Der er ingen aktiv Wino-konto til at logge ud.", + "WinoAccount_Titlebar_SignedOutTitle": "Wino-konto", + "WinoAccount_Titlebar_SignedOutDescription": "Log ind eller opret en Wino-konto for at styre din Wino-session.", + "WinoAccount_Titlebar_SignedInStatus": "Status: {0}", + "WelcomeWizard_Step2Title": "Tilføj konto", + "WelcomeWizard_Step3Title": "Afslut opsætningen", + "ProviderSelection_Title": "Vælg din e-mail-udbyder", + "ProviderSelection_Subtitle": "Vælg en udbyder nedenfor for at tilføje din e-mail-konto til Wino Mail.", + "ProviderSelection_AccountNameHeader": "Konto navn", + "ProviderSelection_AccountNamePlaceholder": "f.eks. Personlig, Arbejde", + "ProviderSelection_DisplayNameHeader": "Vis navn", + "ProviderSelection_DisplayNamePlaceholder": "f.eks. John Doe", + "ProviderSelection_EmailHeader": "E-mailadresse", + "ProviderSelection_EmailPlaceholder": "f.eks. johndoe@example.com", + "ProviderSelection_AppPasswordHeader": "App-specifikt kodeord", + "ProviderSelection_AppPasswordHelp": "Hvordan får jeg et app-specifikt kodeord?", + "ProviderSelection_CalendarModeHeader": "Kalenderintegration", + "ProviderSelection_CalendarMode_DisabledTitle": "Deaktiveret", + "ProviderSelection_CalendarMode_DisabledDescription": "Ingen kalenderintegration", + "ProviderSelection_CalendarMode_CalDavTitle": "CalDAV-synchronisering", + "ProviderSelection_CalendarMode_CalDavDescription_Apple": "Dine kalenderbegivenheder synkroniseres mellem dine enheder til Apples servere.", + "ProviderSelection_CalendarMode_CalDavDescription_Yahoo": "Dine kalenderbegivenheder synkroniseres mellem dine enheder til Yahoo-servere.", + "ProviderSelection_CalendarMode_LocalTitle": "Lokal kalender", + "ProviderSelection_CalendarMode_LocalDescription": "Dine begivenheder gemmes kun på din computer. Ingen forbindelse til servere.", + "ProviderSelection_ClearColor": "Ryd farve", + "ProviderSelection_ContinueButton": "Fortsæt", + "ProviderSelection_SpecialImap_Subtitle": "Indtast dine kontooplysninger for at oprette forbindelse.", + "AccountSetup_Title": "Opsætning af din konto", + "AccountSetup_Step_Authenticating": "Autentificerer med {0}", + "AccountSetup_Step_TestingMailAuth": "Tester mailgodkendelse", + "AccountSetup_Step_SyncingFolders": "Synkroniserer mappemetadata", + "AccountSetup_Step_FetchingProfile": "Henter profiloplysninger", + "AccountSetup_Step_DiscoveringCalDav": "Opdager CalDAV-indstillinger", + "AccountSetup_Step_TestingCalendarAuth": "Tester kalenderautentificering", + "AccountSetup_Step_SavingAccount": "Gemmer kontooplysninger", + "AccountSetup_Step_FetchingCalendarMetadata": "Henter kalendermetadata", + "AccountSetup_Step_SyncingAliases": "Synkroniserer aliaser", + "AccountSetup_Step_Finalizing": "Færdiggør opsætningen", + "AccountSetup_FailureMessage": "Opsætningen mislykkedes. Gå tilbage for at rette dine indstillinger, eller prøv igen senere.", + "AccountSetup_SuccessMessage": "Din konto er oprettet med succes!", + "AccountSetup_GoBackButton": "Gå tilbage", + "AccountSetup_TryAgainButton": "Prøv igen", + "ImapCalDavSettings_AutoDiscoveryFailed": "Automatisk opdagelse mislykkedes. Indtast indstillingerne manuelt i fanen Avanceret." } - - diff --git a/Wino.Core.Domain/Translations/de_DE/resources.json b/Wino.Core.Domain/Translations/de_DE/resources.json index 4473568b..735f28e0 100644 --- a/Wino.Core.Domain/Translations/de_DE/resources.json +++ b/Wino.Core.Domain/Translations/de_DE/resources.json @@ -8,6 +8,7 @@ "AccountCacheReset_Message": "Dieser Account erfordert eine vollständige Neu-Sychronisierung, um fortzufahren. Bitte warten Sie, während Wino Ihre Nachrichten erneut synchronisiert...", "AccountContactNameYou": "Sie", "AccountCreationDialog_Completed": "alles erledigt", + "AccountCreationDialog_FetchingCalendarMetadata": "Kalenderdetails werden abgerufen.", "AccountCreationDialog_FetchingEvents": "Kalenderereignisse werden abgerufen.", "AccountCreationDialog_FetchingProfileInformation": "Lade Profildetails.", "AccountCreationDialog_GoogleAuthHelpClipboardText_Row0": "Falls Ihr Browser nicht automatisch gestartet wurde, um die Authentifizierung abzuschließen:", @@ -17,6 +18,7 @@ "AccountCreationDialog_Initializing": "Initialisierung", "AccountCreationDialog_PreparingFolders": "Es werden Ordner-Informationen gesammelt.", "AccountCreationDialog_SigninIn": "Kontoinformationen wurden gespeichert.", + "Purchased": "Gekauft", "AccountEditDialog_Message": "Konto-Name", "AccountEditDialog_Title": "Konto bearbeiten", "AccountPickerDialog_Title": "Konto auswählen", @@ -26,6 +28,10 @@ "AccountDetailsPage_Description": "Ändern Sie den Namen des Kontos in Wino und setzen Sie den gewünschten Absendernamen.", "AccountDetailsPage_ColorPicker_Title": "Konto-Farbe", "AccountDetailsPage_ColorPicker_Description": "Zuweisen einer neuen Kontofarbe, um das Symbol in der Liste einzufärben.", + "AccountDetailsPage_TabGeneral": "Allgemein", + "AccountDetailsPage_TabMail": "E-Mail", + "AccountDetailsPage_TabCalendar": "Kalender", + "AccountDetailsPage_CalendarListDescription": "Wähle einen Kalender aus, um dessen Einstellungen zu konfigurieren.", "AddHyperlink": "Hinzufügen", "AppCloseBackgroundSynchronizationWarningTitle": "Hintergrundsynchronisierung", "AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Die Anwendung wird beim Start von Windows nicht gestartet.", @@ -47,8 +53,10 @@ "BasicIMAPSetupDialog_Title": "IMAP-Konto", "Busy": "Beschäftigt", "Buttons_AddAccount": "Konto hinzufügen", + "Buttons_FixAccount": "Konto reparieren", "Buttons_AddNewAlias": "Neuen Alias hinzufügen", "Buttons_Allow": "Erlauben", + "Buttons_Apply": "Übernehmen", "Buttons_ApplyTheme": "Thema anwenden", "Buttons_Browse": "Durchsuchen", "Buttons_Cancel": "Abbrechen", @@ -62,6 +70,7 @@ "Buttons_Edit": "Bearbeiten", "Buttons_EnableImageRendering": "Aktivieren", "Buttons_Multiselect": "Mehrfachauswahl", + "Buttons_Manage": "Verwalten", "Buttons_No": "Nein", "Buttons_Open": "Öffnen", "Buttons_Purchase": "Kaufen", @@ -70,15 +79,134 @@ "Buttons_Save": "Speichern", "Buttons_SaveConfiguration": "Einstellungen speichern", "Buttons_Send": "Senden", + "Buttons_SendToServer": "An Server senden", "Buttons_Share": "Teilen", "Buttons_SignIn": "Anmelden", "Buttons_Sync": "Synchronisieren", "Buttons_SyncAliases": "Aliase synchronisieren", "Buttons_TryAgain": "Wiederholen", "Buttons_Yes": "Ja", + "Sync_SynchronizingFolder": "Synchronisiere {0} {1}%", + "Sync_DownloadedMessages": "Heruntergeladene {0} Nachrichten von {1}", + "SyncAction_Archiving": "Archivieren von {0} E-Mail(n)", + "SyncAction_ClearingFlag": "Markierung von {0} E-Mail(n) aufheben", + "SyncAction_CreatingDraft": "Entwurf erstellen", + "SyncAction_CreatingEvent": "Termin erstellen", + "SyncAction_Deleting": "Lösche {0} E-Mail(n)", + "SyncAction_EmptyingFolder": "Ordner leeren", + "SyncAction_MarkingAsRead": "Markiere {0} E-Mail(n) als gelesen", + "SyncAction_MarkingAsUnread": "Markiere {0} E-Mail(n) als ungelesen", + "SyncAction_MarkingFolderAsRead": "Ordner als gelesen markieren", + "SyncAction_Moving": "Verschiebe {0} E-Mail(n)", + "SyncAction_MovingToFocused": "Verschiebe {0} E-Mail(n) in den Fokus-Posteingang", + "SyncAction_RenamingFolder": "Ordner umbenennen", + "SyncAction_SendingMail": "E-Mail wird gesendet", + "SyncAction_SettingFlag": "Flagge setzen für {0} E-Mail(n)", + "SyncAction_SynchronizingAccount": "Synchronisiere {0}", + "SyncAction_SynchronizingAccounts": "Synchronisiere {0} Konto(n)", + "SyncAction_SynchronizingCalendarData": "Synchronisiere Kalenderdaten", + "SyncAction_SynchronizingCalendarEvents": "Synchronisiere Kalenderereignisse", + "SyncAction_SynchronizingCalendarMetadata": "Kalender-Metadaten synchronisieren", + "SyncAction_Unarchiving": "Aus dem Archiv wiederherstellen von {0} E-Mail(n)", "CalendarAllDayEventSummary": "ganztägige Ereignisse", "CalendarDisplayOptions_Color": "Farbe", "CalendarDisplayOptions_Expand": "Ausklappen", + "CalendarEventResponse_Accept": "Annehmen", + "CalendarEventResponse_AcceptedResponse": "Akzeptiert", + "CalendarEventResponse_Decline": "Ablehnen", + "CalendarEventResponse_DeclinedResponse": "Abgelehnt", + "CalendarEventResponse_NotResponded": "Noch nicht beantwortet", + "CalendarEventResponse_Tentative": "Vorläufig", + "CalendarEventResponse_TentativeResponse": "Vorläufig", + "CalendarEventRsvpPanel_Accept": "Annehmen", + "CalendarEventRsvpPanel_AddMessage": "Füge eine Nachricht zu deiner Antwort hinzu... (optional)", + "CalendarEventRsvpPanel_Decline": "Ablehnen", + "CalendarEventRsvpPanel_Message": "Nachricht", + "CalendarEventRsvpPanel_SendReplyMessage": "Antwortnachricht senden", + "CalendarEventRsvpPanel_Tentative": "Vorläufig", + "CalendarEventRsvpPanel_Title": "Antwortoptionen", + "CalendarAttendeeStatus_Accepted": "Akzeptiert", + "CalendarAttendeeStatus_Declined": "Abgelehnt", + "CalendarAttendeeStatus_NeedsAction": "Erfordert Aktion", + "CalendarAttendeeStatus_Tentative": "Vorläufig", + "CalendarEventDetails_Attachments": "Anhänge", + "CalendarEventCompose_AddAttachment": "Anhang hinzufügen", + "CalendarEventCompose_AllDay": "Ganztägig", + "CalendarEventCompose_AttachmentsNotSupportedForCalDav": "Anhänge werden für CalDAV-Kalender nicht unterstützt.", + "CalendarEventCompose_EndDate": "Enddatum", + "CalendarEventCompose_EndTime": "Endzeit", + "CalendarEventCompose_Every": "jeden", + "CalendarEventCompose_ForWeekdays": "für", + "CalendarEventCompose_FrequencyDay": "Tag", + "CalendarEventCompose_FrequencyDayPlural": "Tage", + "CalendarEventCompose_FrequencyMonth": "Monat", + "CalendarEventCompose_FrequencyMonthPlural": "Monate", + "CalendarEventCompose_FrequencyWeek": "Woche", + "CalendarEventCompose_FrequencyWeekPlural": "Wochen", + "CalendarEventCompose_FrequencyYear": "Jahr", + "CalendarEventCompose_FrequencyYearPlural": "Jahre", + "CalendarEventCompose_Location": "Ort", + "CalendarEventCompose_LocationPlaceholder": "Ort hinzufügen", + "CalendarEventCompose_NewEventButton": "Neuer Termin", + "CalendarEventCompose_DefaultCalendarHint": "Sie können in den Kalender-Einstellungen einen Standardkalender für neue Termine auswählen.", + "CalendarEventCompose_DefaultCalendarSettingsLink": "Kalender-Einstellungen öffnen", + "CalendarEventCompose_NoCalendarsMessage": "Für die Termin-Erstellung stehen noch keine Kalender zur Verfügung.", + "CalendarEventCompose_NoCalendarsTitle": "Keine Kalender verfügbar", + "CalendarEventCompose_NoEndDate": "Kein Enddatum", + "CalendarEventCompose_Notes": "Notizen", + "CalendarEventCompose_PickCalendarTitle": "Wähle einen Kalender", + "CalendarEventCompose_Recurring": "Wiederkehrend", + "CalendarEventCompose_RecurringSummary": "Tritt alle {0} {1}{2} {3} in Kraft {4}{5}", + "CalendarEventCompose_RecurringSummarySmart": "Tritt {0}{1} {2} in Kraft {3}{4}", + "CalendarEventCompose_RepeatEvery": "Wiederhole alle", + "CalendarEventCompose_SelectCalendar": "Kalender auswählen", + "CalendarEventCompose_SingleOccurrenceSummary": "Findet statt am {0} {1}", + "CalendarEventCompose_StartDate": "Startdatum", + "CalendarEventCompose_StartTime": "Startzeit", + "CalendarEventCompose_TimeRangeSummary": "von {0} bis {1}", + "CalendarEventCompose_Title": "Veranstaltungstitel", + "CalendarEventCompose_TitlePlaceholder": "Titel hinzufügen", + "CalendarEventCompose_Until": "bis", + "CalendarEventCompose_UntilSummary": " bis {0}", + "CalendarEventCompose_ValidationInvalidAllDayRange": "Das Enddatum eines Ganztags-Ereignisses muss nach dem Startdatum liegen.", + "CalendarEventCompose_ValidationInvalidAttendee": "Ein oder mehrere Teilnehmer haben eine ungültige E-Mail-Adresse.", + "CalendarEventCompose_ValidationInvalidRecurrenceEnd": "Das Enddatum der Wiederholung muss gleich oder nach dem Startdatum des Termins liegen.", + "CalendarEventCompose_ValidationInvalidTimeRange": "Die Endzeit muss später als die Startzeit sein.", + "CalendarEventCompose_ValidationMissingAttachment": "Ein oder mehrere Anhänge sind nicht mehr verfügbar: {0}", + "CalendarEventCompose_ValidationMissingCalendar": "Wähle einen Kalender, bevor du das Ereignis erstellst.", + "CalendarEventCompose_ValidationMissingTitle": "Gib vor dem Erstellen des Termins einen Titel ein.", + "CalendarEventCompose_ValidationTitle": "Terminvalidierung fehlgeschlagen", + "CalendarEventCompose_WeekdaySummary": " am {0}", + "CalendarEventCompose_Weekday_Friday": "Fr", + "CalendarEventCompose_Weekday_Monday": "Mo", + "CalendarEventCompose_Weekday_Saturday": "Sa", + "CalendarEventCompose_Weekday_Sunday": "So", + "CalendarEventCompose_Weekday_Thursday": "Do", + "CalendarEventCompose_Weekday_Tuesday": "Di", + "CalendarEventCompose_Weekday_Wednesday": "Mi", + "CalendarEventDetails_Details": "Details", + "CalendarEventDetails_EditSeries": "Serie bearbeiten", + "CalendarEventDetails_Editing": "Bearbeiten", + "CalendarEventDetails_InviteSomeone": "Jemand einladen", + "CalendarEventDetails_JoinOnline": "Online teilnehmen", + "CalendarEventDetails_Organizer": "Organisator", + "CalendarEventDetails_People": "Personen", + "CalendarEventDetails_ReadOnlyEvent": "Schreibgeschütztes Ereignis", + "CalendarEventDetails_Reminder": "Erinnerung", + "CalendarReminder_StartedHoursAgo": "Vor {0} Stunden gestartet", + "CalendarReminder_StartedMinutesAgo": "Vor {0} Minuten gestartet", + "CalendarReminder_StartedNow": "Gerade gestartet", + "CalendarReminder_StartingNow": "Startet jetzt", + "CalendarReminder_StartsInHours": "Beginnt in {0} Stunden", + "CalendarReminder_StartsInMinutes": "Beginnt in {0} Minuten", + "CalendarReminder_SnoozeAction": "Schlummern", + "CalendarReminder_SnoozeMinutesOption": "{0} Minuten", + "CalendarEventDetails_ShowAs": "Anzeigen als", + "CalendarShowAs_Free": "Verfügbar", + "CalendarShowAs_Tentative": "Vorgemerkt", + "CalendarShowAs_Busy": "Beschäftigt", + "CalendarShowAs_OutOfOffice": "Außer Haus", + "CalendarShowAs_WorkingElsewhere": "Arbeiten woanders", "CalendarItem_DetailsPopup_JoinOnline": "Online beitreten", "CalendarItem_DetailsPopup_ViewEventButton": "Event anzeigen", "CalendarItem_DetailsPopup_ViewSeriesButton": "Serie anzeigen", @@ -88,6 +216,9 @@ "ClipboardTextCopied_Message": "{0} in die Zwischenablage kopiert.", "ClipboardTextCopied_Title": "Kopiert", "ClipboardTextCopyFailed_Message": "{0} konnte nicht in die Zwischenablage kopiert werden.", + "ContactInfoBar_ErrorTitle": "Fehler beim Laden der Kontaktinformationen", + "ContactInfoBar_SuccessTitle": "Kontaktinformationen geladen", + "ContactInfoBar_WarningTitle": "Kontaktinformationen könnten unvollständig sein", "ComingSoon": "Bald verfügbar...", "ComposerAttachmentsDragDropAttach_Message": "Anhängen", "ComposerAttachmentsDropZone_Message": "Dateien hier ablegen", @@ -129,6 +260,10 @@ "DialogMessage_CreateLinkedAccountTitle": "Name des Konto-Links", "DialogMessage_DeleteAccountConfirmationMessage": "{0} löschen?", "DialogMessage_DeleteAccountConfirmationTitle": "Alle mit diesem Konto verknüpften Daten werden dauerhaft von der Festplatte gelöscht.", + "DialogMessage_DeleteEmailTemplateConfirmationMessage": "Vorlage \"{0}\" löschen?", + "DialogMessage_DeleteEmailTemplateConfirmationTitle": "E-Mail-Vorlage löschen", + "DialogMessage_DeleteRecurringSeriesMessage": "Dies wird alle Termine in der Serie löschen. Möchten Sie fortfahren?", + "DialogMessage_DeleteRecurringSeriesTitle": "Wiederkehrende Serie löschen", "DialogMessage_DiscardDraftConfirmationMessage": "Dieser Entwurf wird verworfen. Möchten Sie fortfahren?", "DialogMessage_DiscardDraftConfirmationTitle": "Entwurf verwerfen", "DialogMessage_EmptySubjectConfirmation": "Betreff fehlt", @@ -172,11 +307,18 @@ "ElementTheme_Light": "Heller Modus", "Emoji": "Emoji", "Error_FailedToSetupSystemFolders_Title": "Fehler beim Einrichten der Systemordner", + "Exception_AccountNeedsAttention_Title": "Konto benötigt Aufmerksamkeit", + "Exception_AccountNeedsAttention_Message": "'{0}' erfordert Ihre Aufmerksamkeit, um weiterzuarbeiten.", + "Exception_WebView2RuntimeMissing_Message": "Wino Mail konnte Microsoft Edge WebView2 Runtime nicht finden. Bitte installieren oder reparieren Sie die Laufzeit, damit die Nachrichteninhalte korrekt dargestellt werden.", + "Exception_WebView2RuntimeMissing_Title": "WebView2-Laufzeit erforderlich", "Exception_AuthenticationCanceled": "Authentifizierung abgebrochen", "Exception_CustomThemeExists": "Dieses Thema existiert bereits.", "Exception_CustomThemeMissingName": "Sie müssen einen Namen angeben.", "Exception_CustomThemeMissingWallpaper": "Sie müssen ein eigenes Hintergrundbild angeben.", "Exception_FailedToSynchronizeAliases": "Fehler beim Synchronisieren der Aliase", + "Exception_FailedToSynchronizeCalendarData": "Kalenderdaten konnten nicht synchronisiert werden.", + "Exception_FailedToSynchronizeCalendarEvents": "Kalendereinträge konnten nicht synchronisiert werden.", + "Exception_FailedToSynchronizeCalendarMetadata": "Kalendermetadaten konnten nicht synchronisiert werden.", "Exception_FailedToSynchronizeFolders": "Fehler beim Synchronisieren der Ordner", "Exception_FailedToSynchronizeProfileInformation": "Fehler beim Synchronisieren der Profilinformationen", "Exception_GoogleAuthCallbackNull": "'Callback uri ist 'null' bei der Aktivierung.", @@ -229,6 +371,32 @@ "HoverActionOption_MoveJunk": "In den Papierkorb verschieben", "HoverActionOption_ToggleFlag": "Markieren / Markierung entfernen", "HoverActionOption_ToggleRead": "Lesen / Ungelesen", + "KeyboardShortcuts_FailedToReset": "Tastenkombinationen konnten nicht zurückgesetzt werden.", + "KeyboardShortcuts_FailedToUpdate": "Tastenkombinationen konnten nicht aktualisiert werden.", + "KeyboardShortcuts_MailoperationAction": "Aktion", + "KeyboardShortcuts_Action": "Aktion", + "KeyboardShortcuts_FailedToLoad": "Tastenkombinationen konnten nicht geladen werden.", + "KeyboardShortcuts_EnterKeyForShortcut": "Bitte drücken Sie eine Taste für die Verknüpfung.", + "KeyboardShortcuts_SelectOperationForShortcut": "Bitte wählen Sie eine Aktion für die Verknüpfung.", + "KeyboardShortcuts_EnterKey": "Bitte drücken Sie eine Taste für die Verknüpfung.", + "KeyboardShortcuts_SelectOperation": "Bitte wählen Sie eine Aktion für die Verknüpfung.", + "KeyboardShortcuts_ShortcutInUse": "Diese Verknüpfung wird bereits von einer anderen Verknüpfung verwendet.", + "KeyboardShortcuts_FailedToSave": "Die Verknüpfung konnte nicht gespeichert werden.", + "KeyboardShortcuts_FailedToDelete": "Die Verknüpfung konnte nicht gelöscht werden.", + "KeyboardShortcuts_PageDescription": "Richten Sie Tastenkombinationen für schnelle Mail-Operationen ein. Drücken Sie Tasten, während der Fokus im Tastatureingabefeld liegt, um Verknüpfungen zu erfassen.", + "KeyboardShortcuts_Add": "Verknüpfung hinzufügen", + "KeyboardShortcuts_EditTitle": "Tastenkombination bearbeiten", + "KeyboardShortcuts_ResetToDefaults": "Auf Standardwerte zurücksetzen", + "KeyboardShortcuts_PressKeysHere": "Drücken Sie hier Tasten...", + "KeyboardShortcuts_KeyCombination": "Tastenkombination", + "KeyboardShortcuts_FocusArea": "Fokusfeld oben auswählen und die gewünschte Tastenkombination drücken", + "KeyboardShortcuts_Modifiers": "Modifikatortasten", + "KeyboardShortcuts_Mode": "App-Modus", + "KeyboardShortcuts_ModeMail": "Mail", + "KeyboardShortcuts_ModeCalendar": "Kalender", + "KeyboardShortcuts_ActionToggleReadUnread": "Gelesen/Ungelesen umschalten", + "KeyboardShortcuts_ActionToggleFlag": "Flagge umschalten", + "KeyboardShortcuts_ActionToggleArchive": "Archiv umschalten", "ImageRenderingDisabled": "Bilddarstellung ist für diese Nachricht deaktiviert.", "ImapAdvancedSetupDialog_AuthenticationMethod": "Authentifizierungsmethode", "ImapAdvancedSetupDialog_ConnectionSecurity": "Verbindungssicherheit", @@ -295,12 +463,58 @@ "IMAPSetupDialog_Username": "Benutzername", "IMAPSetupDialog_UsernamePlaceholder": "johndoe, johndoe@fabrikam.com, domain/johndoe", "IMAPSetupDialog_UseSameConfig": "Den gleichen Benutzernamen und das gleiche Passwort für das Senden von E-Mails verwenden", + "ImapCalDavSettingsPage_TitleCreate": "IMAP- und Kalender-Einrichtung", + "ImapCalDavSettingsPage_TitleEdit": "IMAP- und Kalender-Einstellungen bearbeiten", + "ImapCalDavSettingsPage_Subtitle": "Richten Sie IMAP/SMTP und optionale Kalender-Synchronisierung für dieses Konto ein.", + "ImapCalDavSettingsPage_BasicSectionTitle": "Grundkonfiguration", + "ImapCalDavSettingsPage_BasicSectionDescription": "Geben Sie Ihre Identität und Anmeldedaten ein. Wino kann versuchen, die Servereinstellungen automatisch zu erkennen.", + "ImapCalDavSettingsPage_BasicTab": "Grundlegend", + "ImapCalDavSettingsPage_EnableCalendarSupport": "Kalendersupport aktivieren", + "ImapCalDavSettingsPage_AutoDiscoverButton": "Mail-Einstellungen automatisch erkennen", + "ImapCalDavSettingsPage_AutoDiscoverySuccessMessage": "Mail-Einstellungen wurden erkannt und angewendet.", + "ImapCalDavSettingsPage_AdvancedSectionTitle": "Erweiterte Konfiguration", + "ImapCalDavSettingsPage_AdvancedSectionDescription": "Geben Sie Servereinstellungen manuell ein, wenn die automatische Erkennung nicht verfügbar oder falsch ist.", + "ImapCalDavSettingsPage_AdvancedTab": "Erweitert", + "ImapCalDavSettingsPage_CalendarSectionTitle": "Kalender-Einrichtung", + "ImapCalDavSettingsPage_CalendarSectionDescription": "Wählen Sie aus, wie Kalenderdaten für dieses IMAP-Konto funktionieren sollen.", + "ImapCalDavSettingsPage_CalendarModeHeader": "Kalendermodus", + "ImapCalDavSettingsPage_ConnectionSecurityHeader": "Verbindungssicherheit", + "ImapCalDavSettingsPage_AuthenticationMethodHeader": "Authentifizierungsmethode", + "ImapCalDavSettingsPage_CalendarModeDisabled": "Deaktiviert", + "ImapCalDavSettingsPage_CalendarModeCalDav": "CalDAV-Synchronisierung", + "ImapCalDavSettingsPage_CalendarModeLocalOnly": "Nur lokaler Kalender", + "ImapCalDavSettingsPage_CalendarModeDisabledDescription": "Der Kalender ist für dieses Konto deaktiviert.", + "ImapCalDavSettingsPage_CalendarModeCalDavDescription": "Kalendereinträge werden mit Ihrem CalDAV-Server synchronisiert.", + "ImapCalDavSettingsPage_CalendarModeLocalOnlyDescription": "Kalendereinträge werden nur auf diesem Computer gespeichert und nicht mit dem Netzwerk synchronisiert.", + "ImapCalDavSettingsPage_LocalCalendarLearnMore": "Wie der lokale Kalender funktioniert", + "ImapCalDavSettingsPage_LocalCalendarDialogTitle": "Nur lokaler Kalender", + "ImapCalDavSettingsPage_LocalCalendarDialogMessage": "Der lokale Kalender speichert alle Termine nur auf Ihrem Computer. Nichts wird mit iCloud, Yahoo oder einem anderen Anbieter synchronisiert.", + "ImapCalDavSettingsPage_CalDavServiceUrl": "CalDAV-Service-URL", + "ImapCalDavSettingsPage_CalDavUsername": "CalDAV-Benutzername", + "ImapCalDavSettingsPage_CalDavPassword": "CalDAV-Passwort", + "ImapCalDavSettingsPage_CalDavNotRequiredMessage": "CalDAV-Test ist nur erforderlich, wenn der Kalendermodus auf CalDAV-Synchronisierung eingestellt ist.", + "ImapCalDavSettingsPage_CalDavUrlRequired": "CalDAV-Service-URL ist erforderlich.", + "ImapCalDavSettingsPage_CalDavUrlInvalid": "CalDAV-Service-URL muss eine absolute URL sein.", + "ImapCalDavSettingsPage_CalDavUsernameRequired": "CalDAV-Benutzername ist erforderlich.", + "ImapCalDavSettingsPage_CalDavPasswordRequired": "CalDAV-Passwort ist erforderlich.", + "ImapCalDavSettingsPage_TestImapButton": "IMAP-Verbindung testen", + "ImapCalDavSettingsPage_TestCalDavButton": "CalDAV-Verbindung testen", + "ImapCalDavSettingsPage_ImapTestSuccessMessage": "IMAP-Verbindung erfolgreich getestet.", + "ImapCalDavSettingsPage_CalDavTestSuccessMessage": "CalDAV-Verbindung erfolgreich getestet.", + "ImapCalDavSettingsPage_SaveSuccessMessage": "Kontoeinstellungen validiert und gespeichert.", + "ImapCalDavSettingsPage_ICloudHint": "Verwenden Sie ein app-spezifisches Passwort, das in Ihren Apple-Kontoeinstellungen generiert wird.", + "ImapCalDavSettingsPage_YahooHint": "Verwenden Sie ein App-Passwort aus Ihren Yahoo-Kontoeinstellungen.", "Info_AccountCreatedMessage": "{0} wurde erstellt", "Info_AccountCreatedTitle": "Konto-Erstellung", "Info_AccountCreationFailedTitle": "Konto-Erstellung fehlgeschlagen", "Info_AccountDeletedMessage": "{0} wurde erfolgreich gelöscht.", "Info_AccountDeletedTitle": "Konto gelöscht", "Info_AccountIssueFixFailedTitle": "Fehlschlagen", + "Info_AccountIssueFixImapMessage": "Öffnen Sie die IMAP- und Kalendereinstellungen-Seite, um Ihre Serveranmeldeinformationen erneut einzugeben.", + "Info_AccountAttentionRequiredMessage": "Dieses Konto benötigt Ihre Aufmerksamkeit.", + "Info_AccountAttentionRequiredClickableMessage": "Klicken Sie hier, um dieses Konto zu beheben und es erneut zu synchronisieren.", + "Info_AccountAttentionRequiredAction": "Beheben", + "Info_AccountAttentionRequiredActionHint": "Klicken Sie Beheben, um dieses Kontoproblem zu lösen.", "Info_AccountIssueFixSuccessMessage": "Alle Konto-Probleme wurden behoben.", "Info_AccountIssueFixSuccessTitle": "Erfolg", "Info_AttachmentOpenFailedMessage": "Anhang kann nicht geöffnet werden.", @@ -370,6 +584,7 @@ "InfoBarMessage_SynchronizationDisabledFolder": "Dieser Ordner wird nicht synchronisiert.", "InfoBarTitle_SynchronizationDisabledFolder": "Deaktivierter Ordner", "Justify": "Block", + "MenuUpdateAvailable": "Update verfügbar", "Left": "Links", "Link": "Link", "LinkedAccountsCreatePolicyMessage": "Sie müssen mindestens 2 Konten haben, um Link zu erstellen\nLink wird beim Speichern entfernt", @@ -403,6 +618,7 @@ "MailOperation_Unarchive": "Archivierung aufheben", "MailOperation_ViewMessageSource": "Nachrichtenquelle anzeigen", "MailOperation_Zoom": "Zoom", + "MailsDragging": "Verschiebe {0} Element(e)", "MailsSelected": "{0} Element(e) ausgewählt", "MarkFlagUnflag": "Als markiert/nicht markiert setzen", "MarkReadUnread": "Als gelesen/ungelesen markieren", @@ -434,6 +650,8 @@ "Notifications_MultipleNotificationsTitle": "Neue Mail", "Notifications_WinoUpdatedMessage": "Neue Version {0} herunterladen", "Notifications_WinoUpdatedTitle": "Wino Mail wurde aktualisiert.", + "Notifications_StoreUpdateAvailableTitle": "Update verfügbar", + "Notifications_StoreUpdateAvailableMessage": "Eine neuere Version von Wino Mail ist zum Installieren aus dem Microsoft Store bereit.", "OnlineSearchFailed_Message": "Fehler bei der Suche\n{0}\n\nAuflistung von Offline-Mails.", "OnlineSearchTry_Line1": "Finden Sie nicht, was Sie suchen?", "OnlineSearchTry_Line2": "Online-Suche probieren.", @@ -446,7 +664,6 @@ "PaneLengthOption_Small": "Klein", "Photos": "Fotos", "PreparingFoldersMessage": "Ordnervorbereitung", - "ProtocolLogAvailable_Message": "Protokolle zur Diagnose sind verfügbar.", "ProviderDetail_Gmail_Description": "Google-Konto", "ProviderDetail_iCloud_Description": "Apple iCloud-Konto", "ProviderDetail_iCloud_Title": "iCloud", @@ -465,9 +682,14 @@ "SearchBarPlaceholder": "Suche", "SearchingIn": "Suche in", "SearchPivotName": "Ergebnisse", + "Settings_KeyboardShortcuts_Title": "Tastenkombinationen", + "Settings_KeyboardShortcuts_Description": "Verwalten Sie Tastenkombinationen für schnelle Aktionen in Mails.", "SettingConfigureSpecialFolders_Button": "Konfigurieren", "SettingsEditAccountDetails_IMAPConfiguration_Title": "IMAP/SMTP-Konfiguration", "SettingsEditAccountDetails_IMAPConfiguration_Description": "Ändern Sie Ihre eingehenden/ausgehenden Servereinstellungen.", + "SettingsEditAccountDetails_ImapCalDavSettings_Title": "IMAP- und Kalender-Einstellungen", + "SettingsEditAccountDetails_ImapCalDavSettings_Description": "Öffnen Sie die dedizierte IMAP-, SMTP- und CalDAV-Einstellungsseite für dieses Konto.", + "SettingsEditAccountDetails_ImapCalDavSettings_Action": "Einstellungen öffnen", "SettingsAbout_Description": "Erfahren Sie mehr über Wino.", "SettingsAbout_Title": "Über", "SettingsAboutGithub_Description": "Gehen Sie zum Problem-Tracker GitHub-Repository.", @@ -490,6 +712,10 @@ "SettingsAppPreferences_SearchMode_Local": "Lokal", "SettingsAppPreferences_SearchMode_Online": "Online", "SettingsAppPreferences_SearchMode_Title": "Standard-Suchmodus", + "SettingsAppPreferences_ApplicationMode_Title": "Standard-Anwendungsmodus", + "SettingsAppPreferences_ApplicationMode_Description": "Wählen Sie aus, in welchem Modus Wino geöffnet wird, wenn kein Aktivierungstyp explizit festgelegt wird.", + "SettingsAppPreferences_ApplicationMode_Mail": "E-Mail", + "SettingsAppPreferences_ApplicationMode_Calendar": "Kalender", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description": "Wino Mail wird weiterhin im Hintergrund ausgeführt. Sie werden benachrichtigt, sobald neue Mails eintreffen.", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title": "Im Hintergrund ausführen", "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description": "Wino Mail wird weiterhin im System-Tray laufen. Startet beim Klicken des Symbols die Anwendung. Sie werden benachrichtigt, sobald neue Mails eintreffen.", @@ -506,12 +732,30 @@ "SettingsAppPreferences_StartupBehavior_FatalError": "Ein schwerwiegender Fehler ist beim Ändern des Startmodus für Wino Mail aufgetreten.", "SettingsAppPreferences_StartupBehavior_Title": "Beim Start von Windows minimiert starten", "SettingsAppPreferences_Title": "App-Präferenzen", + "SettingsAppPreferences_HideWinoAccountButton_Title": "Wino-Konto-Schaltfläche in der Titelleiste ausblenden", + "SettingsAppPreferences_HideWinoAccountButton_Description": "Profil-Schaltfläche in der Titelleiste ausblenden, die das Wino-Konto-Ausklappmenü öffnet.", + "SettingsAppPreferences_StoreUpdateNotifications_Title": "Store-Update-Benachrichtigungen", + "SettingsAppPreferences_StoreUpdateNotifications_Description": "Zeigen Sie Benachrichtigungen und Fußzeilenaktionen, wenn ein Microsoft Store-Update verfügbar ist.", + "SettingsAppPreferences_AiActions_Title": "KI-Aktionen", + "SettingsAppPreferences_AiActions_Description": "Standard-KI-Sprachen auswählen und Speicherort für Zusammenfassungen festlegen.", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Title": "Standardsprache der Übersetzung", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Description": "Wählen Sie die Standardzielsprache, die von KI-Übersetzungsaktionen verwendet wird.", + "SettingsAppPreferences_AiSummarizeLanguage_Title": "Zusammenfassungssprache", + "SettingsAppPreferences_AiSummarizeLanguage_Description": "Wählen Sie die bevorzugte Sprache für zukünftige KI-Zusammenfassungen.", + "SettingsAppPreferences_AiSummarySavePath_Title": "Standard-Speicherort für Zusammenfassungen", + "SettingsAppPreferences_AiSummarySavePath_Description": "Wählen Sie den Ordner, der standardmäßig zum Speichern von KI-Zusammenfassungen verwendet wird.", + "SettingsAppPreferences_AiSummarySavePath_Placeholder": "Standard-Speicherort des Systems verwenden", + "SettingsAppPreferences_AiSummarySavePath_InvalidHint": "Dieser Ordner existiert nicht. Der Standard-Speicherort wird für Zusammenfassungen verwendet.", "SettingsAutoSelectNextItem_Description": "Das nächste Element auswählen, nachdem Sie eine Mail gelöscht oder verschoben haben.", "SettingsAutoSelectNextItem_Title": "Nächsten Eintrag automatisch auswählen", "SettingsAvailableThemes_Description": "Wählen Sie ein Thema aus der Sammlung von Wino oder verwenden Sie Ihre eigenen Themen.", "SettingsAvailableThemes_Title": "Verfügbare Themen", "SettingsCalendarSettings_Description": "Ändern Sie den ersten Tag der Woche, die Stunden-Zellenhöhe und mehr...", "SettingsCalendarSettings_Title": "Kalendereinstellungen", + "CalendarSettings_DefaultSnoozeDuration_Header": "Standard-Schlummerdauer", + "CalendarSettings_DefaultSnoozeDuration_Description": "Legen Sie eine Standard-Schlummerdauer für Kalender-Erinnerungsbenachrichtigungen fest.", + "CalendarSettings_TimedDayHeaderFormat_Header": "Format der Tagesüberschrift in der Zeitansicht", + "CalendarSettings_TimedDayHeaderFormat_Description": "Wählen Sie aus, wie die oberen Tag-Bezeichnungen in der Tages-, Wochen- und Arbeitswochenansicht angezeigt werden. Verwenden Sie Datumsformat-Token wie ddd, dd, MMM oder dddd.", "SettingsComposer_Title": "Verfasser", "SettingsComposerFont_Title": "Standard Verfasser-Schriftart", "SettingsComposerFontFamily_Description": "Ändern Sie die Standardschriftart und Schriftgröße für das Verfassen von Mails.", @@ -531,6 +775,9 @@ "SettingsDiscord_Title": "Discord Kanal", "SettingsEditLinkedInbox_Description": "Konten hinzufügen/entfernen, umbenennen oder die Verbindung zwischen Konten unterbrechen.", "SettingsEditLinkedInbox_Title": "Verlinkten Posteingang bearbeiten", + "SettingsWindowBackdrop_Title": "Fensterhintergrund", + "SettingsWindowBackdrop_Description": "Wählen Sie einen Hintergrund-Effekt für Wino-Fenster.", + "SettingsWindowBackdrop_Disabled": "Die Fensterhintergrund-Auswahl ist deaktiviert, wenn ein anderes Anwendungs-Theme als Standard ausgewählt ist.", "SettingsElementTheme_Description": "Wählen Sie ein Windows-Thema für Wino", "SettingsElementTheme_Title": "Element-Thema", "SettingsElementThemeSelectionDisabled": "Auswahl des Element-Themas ist deaktiviert, wenn das Anwendungs-Thema nicht als Standard festgelegt ist.", @@ -581,6 +828,8 @@ "SettingsManageAliases_Title": "Aliase", "SettingsEditAccountDetails_Title": "Kontodaten bearbeiten", "SettingsEditAccountDetails_Description": "Ändern Sie den Kontonamen, Absendernamen und weisen Sie eine neue Farbe zu.", + "EditAccountDetailsPage_SaveSuccess_Title": "Änderungen gespeichert", + "EditAccountDetailsPage_SaveSuccess_Message": "Ihre Kontodaten wurden erfolgreich aktualisiert.", "SettingsManageLink_Description": "Elemente verschieben, um neuen Link hinzuzufügen oder bestehenden Link zu entfernen.", "SettingsManageLink_Title": "Link verwalten", "SettingsMarkAsRead_Description": "Ändern Sie, was mit dem ausgewählten Element passieren soll.", @@ -596,7 +845,41 @@ "SettingsNotifications_Title": "Benachrichtigungen", "SettingsNotificationsAndTaskbar_Description": "Passt Benachrichtigungen und das Taskleisten-Symbol für dieses Konto an.", "SettingsNotificationsAndTaskbar_Title": "Benachrichtigungen & Taskleiste", + "SettingsHome_Title": "Startseite", + "SettingsHome_SearchTitle": "Eine Einstellung finden", + "SettingsHome_SearchDescription": "Suchen Sie nach Funktion, Thema oder Stichwort, um direkt zur richtigen Einstellungsseite zu springen.", + "SettingsHome_SearchPlaceholder": "Einstellungen durchsuchen", + "SettingsHome_SearchExamples": "Beispiele: Design, Speicher, Sprache, Signatur", + "SettingsHome_QuickLinks_Title": "Schnellzugriffe", + "SettingsHome_QuickLinks_Description": "Direkt zu den am häufigsten verwendeten Einstellungen springen.", + "SettingsHome_StorageCard_Description": "Sehen Sie, wie viel lokaler MIME-Inhalt Wino auf diesem Gerät speichert, und bereinigen Sie ihn bei Bedarf.", + "SettingsHome_StorageEmptySummary": "Noch kein zwischengespeicherter MIME-Inhalt erkannt.", + "SettingsHome_StorageLoading": "Lokale MIME-Nutzung wird geprüft...", + "SettingsHome_Tips_Title": "Tipps & Tricks", + "SettingsHome_Tips_Description": "Ein paar kleine Änderungen können Wino deutlich persönlicher machen.", + "SettingsHome_Tip_Theme": "Möchten Sie Dunkelmodus oder Akzentänderungen? Öffnen Sie Personalisierung.", + "SettingsHome_Tip_Background": "Verwenden Sie die App-Einstellungen, um das Startverhalten und die Hintergrund-Synchronisierung zu steuern.", + "SettingsHome_Tip_Shortcuts": "Tastenkombinationen helfen Ihnen, sich schneller durch Mails zu bewegen.", + "SettingsHome_Resources_Title": "Nützliche Links", + "SettingsHome_Resources_Description": "Öffnen Sie Projektressourcen, Support-Informationen und Release-Kanäle.", "SettingsOptions_Title": "Einstellungen", + "SettingsOptions_GeneralSection": "Allgemein", + "SettingsOptions_MailSection": "E-Mail", + "SettingsOptions_CalendarSection": "Kalender", + "SettingsOptions_MoreComingSoon": "Weitere Optionen folgen in Kürze", + "SettingsOptions_HeroDescription": "Passen Sie Ihr Wino Mail-Erlebnis an", + "SettingsOptions_AccountsSummary": "{0} Konto(n) konfiguriert", + "SettingsSearch_ManageAccounts_Keywords": "Konto;Konten;Postfach;Postfächer;Alias;Aliases;Profil;Adresse;Adressen", + "SettingsSearch_AppPreferences_Keywords": "Start;Hintergrund;Starten;Synchronisierung;Benachrichtigungen;Suche;Systemablage;Standardeinstellungen", + "SettingsSearch_LanguageTime_Keywords": "Sprache;Zeit;Uhr;Locale;Region;Format;24-Stunden;24h", + "SettingsSearch_Personalization_Keywords": "Design;dunkel;hell;Aussehen;Akzent;Farbe;Farbe;Modus;Layout;Dichte", + "SettingsSearch_About_Keywords": "Über;Version;Website;Datenschutz;GitHub;Spenden;Store;Support", + "SettingsSearch_KeyboardShortcuts_Keywords": "Tastenkürzel;Tastenkürzel;Hotkeys;Hotkeys;Tastatur;Tasten", + "SettingsSearch_MessageList_Keywords": "Nachrichten;Nachrichten;Liste;Threads;Threads;Avatar;Vorschau;Absender", + "SettingsSearch_ReadComposePane_Keywords": "Leser;Verfassen;Verfasser;Schrift;Schriften;Externer Inhalt;Anzeige;Lesen", + "SettingsSearch_SignatureAndEncryption_Keywords": "Signatur;Signaturen;Verschlüsselung;Zertifikat;Zertifikate;S/MIME;SMIME;Sicherheit", + "SettingsSearch_Storage_Keywords": "Speicher;Cache;Caching;Mime;Festplatte;Speicherplatz;Bereinigung;Bereinigen;Lokale Daten", + "SettingsSearch_CalendarSettings_Keywords": "Kalender;Woche;Stunden;Planung;Termin;Termine", "SettingsPaneLengthReset_Description": "Setzen Sie die Größe der Mailliste auf die Originaleinstellung zurück, falls sie Probleme haben.", "SettingsPaneLengthReset_Title": "Größe der Mailliste zurücksetzen", "SettingsPaypal_Description": "Zeigen Sie viel mehr Liebe ❤️ Alle Spenden werden wertgeschätzt.", @@ -610,6 +893,8 @@ "SettingsPrefer24HourClock_Title": "Uhr-Format in 24 Stunden anzeigen", "SettingsPrivacyPolicy_Description": "Datenschutzrichtlinie ansehen.", "SettingsPrivacyPolicy_Title": "Datenschutzerklärung", + "SettingsWebsite_Description": "Öffnen Sie die Wino Mail-Website.", + "SettingsWebsite_Title": "Website", "SettingsReadComposePane_Description": "Schriftarten, externe Inhalte.", "SettingsReadComposePane_Title": "Leseansicht & Verfasser", "SettingsReader_Title": "Leseansicht", @@ -625,6 +910,19 @@ "SettingsShowPreviewText_Title": "Vorschautext anzeigen", "SettingsShowSenderPictures_Description": "Absender-Profilbilder ausblenden/anzeigen.", "SettingsShowSenderPictures_Title": "Absender-Profilbilder anzeigen", + "SettingsEmailTemplates_Title": "E-Mail-Vorlagen", + "SettingsEmailTemplates_Description": "E-Mail-Vorlagen verwalten", + "SettingsEmailTemplates_CreatePageTitle": "Neue Vorlage", + "SettingsEmailTemplates_EditPageTitle": "Vorlage bearbeiten", + "SettingsEmailTemplates_NewTemplateTitle": "Neue Vorlage", + "SettingsEmailTemplates_NewTemplateDescription": "Eine neue E-Mail-Vorlage erstellen", + "SettingsEmailTemplates_NameTitle": "Name", + "SettingsEmailTemplates_NamePlaceholder": "Vorlagenname", + "SettingsEmailTemplates_DescriptionTitle": "Beschreibung", + "SettingsEmailTemplates_DescriptionPlaceholder": "Optionale Beschreibung", + "SettingsEmailTemplates_ContentTitle": "Vorlageninhalt", + "SettingsEmailTemplates_ContentDescription": "Bearbeiten Sie den HTML-Inhalt für diese Vorlage.", + "SettingsEmailTemplates_NameRequired": "Vorlagenname ist erforderlich.", "SettingsEnableGravatarAvatars_Title": "Gravatar", "SettingsEnableGravatarAvatars_Description": "Gravatar (falls verfügbar) als Absenderbild verwenden", "SettingsEnableFavicons_Title": "Domain-Icons (Favicons)", @@ -645,6 +943,33 @@ "SettingsStartupItem_Title": "Startelement", "SettingsStore_Description": "Zeigen Sie etwas Liebe ❤️", "SettingsStore_Title": "Im Store bewerten", + "SettingsStorage_Title": "Speicher", + "SettingsStorage_Description": "Scannen und Verwalten des MIME-Caches, der in Ihrem lokalen Datenordner gespeichert ist.", + "SettingsStorage_ScanFolder": "Lokalen Datenordner scannen", + "SettingsStorage_NoLocalMimeDataFound": "Keine lokalen MIME-Daten gefunden.", + "SettingsStorage_NoAccountsFound": "Keine Konten gefunden.", + "SettingsStorage_TotalUsage": "Gesamter lokaler MIME-Verbrauch: {0}", + "SettingsStorage_AccountUsageDescription": "{0} im lokalen MIME-Cache verwendet", + "SettingsStorage_DeleteAll_Title": "Alle MIME-Inhalte löschen", + "SettingsStorage_DeleteAll_Description": "Diesen Kontos MIME-Cache-Ordner vollständig löschen.", + "SettingsStorage_DeleteAll_Button": "Alle löschen", + "SettingsStorage_DeleteAll_Confirm_Title": "Alle MIME-Inhalte löschen", + "SettingsStorage_DeleteAll_Confirm_Message": "Alle lokalen MIME-Daten für {0} löschen?", + "SettingsStorage_DeleteAll_Success": "Alle MIME-Inhalte wurden gelöscht.", + "SettingsStorage_DeleteOld_Title": "Alte MIME-Inhalte löschen", + "SettingsStorage_DeleteOld_Description": "Lösche MIME-Dateien basierend auf dem Erstellungsdatum der E-Mails in der lokalen Datenbank.", + "SettingsStorage_DeleteOld_1Month": "> 1 Monat", + "SettingsStorage_DeleteOld_3Months": "> 3 Monate", + "SettingsStorage_DeleteOld_6Months": "> 6 Monate", + "SettingsStorage_DeleteOld_1Year": "> 1 Jahr", + "SettingsStorage_DeleteOld_Confirm_Title": "Alte MIME-Inhalte löschen", + "SettingsStorage_DeleteOld_Confirm_Message": "Lokale MIME-Daten, die älter sind als {0}, für {1} löschen?", + "SettingsStorage_DeleteOld_Success": "{0} MIME-Ordner älter als {1} gelöscht.", + "SettingsStorage_1Month": "1 Monat", + "SettingsStorage_3Months": "3 Monate", + "SettingsStorage_6Months": "6 Monate", + "SettingsStorage_1Year": "1 Jahr", + "SettingsStorage_Months": "{0} Monate", "SettingsTaskbarBadge_Description": "Zeigt die Anzahl der ungelesenen Mails auf der Taskleiste an.", "SettingsTaskbarBadge_Title": "Taskleisten-Symbol", "SettingsThreads_Description": "Nachrichten in Unterhaltungsthreads organisieren.", @@ -683,6 +1008,9 @@ "SystemFolderConfigDialogValidation_InboxSelected": "Sie können den Posteingang keinem anderen Systemordner zuordnen.", "SystemFolderConfigSetupSuccess_Message": "Systemordner wurden erfolgreich konfiguriert.", "SystemFolderConfigSetupSuccess_Title": "Systemordner-Einrichtung", + "SystemTrayMenu_ShowWino": "Wino Mail öffnen", + "SystemTrayMenu_ShowWinoCalendar": "Wino Kalender öffnen", + "SystemTrayMenu_ExitWino": "Beenden", "TestingImapConnectionMessage": "Serververbindung wird getestet...", "TitleBarServerDisconnectedButton_Description": "Wino ist vom Netzwerk getrennt. Klicken Sie auf \"Erneut verbinden\", um die Verbindung wiederherzustellen.", "TitleBarServerDisconnectedButton_Title": "Keine Verbindung", @@ -699,8 +1027,422 @@ "WinoUpgradeMessage": "Auf unbegrenzte Konten upgraden", "WinoUpgradeRemainingAccountsMessage": "{0} von {1} kostenlosen Konten verwendet.", "Yesterday": "Gestern", + "Smime_ImportCertificates_Success": "Zertifikate erfolgreich importiert.", + "Smime_ImportCertificates_Error": "Fehler beim Importieren der Zertifikate: {0}", + "Smime_RemoveCertificates_Confirm": "Möchten Sie wirklich die Zertifikate {0} entfernen?", + "Smime_RemoveCertificates_Success": "Zertifikate entfernt.", + "Smime_ExportCertificates_Success": "Zertifikate exportiert.", + "Smime_ExportCertificates_Error": "Fehler beim Exportieren der Zertifikate.", + "Smime_CertificateDetails": "Betreff: {0}\\nAussteller: {1}\\nGültig von: {2}\\nGültig bis: {3}\\nFingerabdruck: {4}", + "Smime_CertificatePassword_Title": "Zertifikatspasswort erforderlich", + "Smime_CertificatePassword_Placeholder": "Zertifikatpasswort für {0} (optional)", + "Smime_Confirm_Title": "Bestätigen", + "Buttons_OK": "OK", + "Buttons_Refresh": "Aktualisieren", + "SettingsSignatureAndEncryption_Title": "Signatur und Verschlüsselung", + "SettingsSignatureAndEncryption_Description": "S/MIME-Zertifikate zum Signieren und Verschlüsseln von E-Mails verwalten.", + "SettingsSignatureAndEncryption_MyCertificatesHeader": "Meine Zertifikate", + "SettingsSignatureAndEncryption_MyCertificatesDescription": "Persönliche Zertifikate zum Signieren und Verschlüsseln", + "SettingsSignatureAndEncryption_RecipientCertificatesHeader": "Zertifikate der Empfänger", + "SettingsSignatureAndEncryption_RecipientCertificatesDescription": "Zertifikate der Empfänger zum Entschlüsseln", + "SettingsSignatureAndEncryption_NameColumn": "Name", + "SettingsSignatureAndEncryption_ExpiresColumn": "Läuft ab am", + "SettingsSignatureAndEncryption_ThumbprintColumn": "Fingerabdruck", + "Buttons_Remove": "Entfernen", + "Buttons_Export": "Exportieren", + "Buttons_Import": "Importieren", + "SettingsSignatureAndEncryption_SigningCertificate": "S/MIME-Signaturzertifikat", + "SettingsSignatureAndEncryption_EncryptionCertificate": "S/MIME-Verschlüsselung", + "SettingsSignatureAndEncryption_SigningCertificatePlaceholder": "Keines", + "SmimeSignaturesInMessage": "Signaturen in dieser Nachricht:", + "SmimeSignatureEntry": "• {0} {1} ({2}, gültig bis {3} - {4})", + "SmimeSigningCertificateInfoTitle": "S/MIME-Signaturzertifikat-Info", + "SmimeCertificateInfoTitle": "S/MIME-Zertifikat-Info", + "SmimeNoCertificateFileFound": "Kein Zertifikatsdatei gefunden", + "SmimeSaveCertificate": "Zertifikat speichern...", + "SmimeCertificate": "S/MIME-Zertifikat", + "SmimeCertificateSavedTo": "Zertifikat gespeichert unter {0}", + "SmimeSignedTooltip": "Diese Nachricht ist mit einem S/MIME-Zertifikat signiert. Zum Anzeigen weiterer Details klicken.", + "SmimeEncryptedTooltip": "Diese Nachricht ist mit einem S/MIME-Zertifikat verschlüsselt.", + "SmimeCertificateFileInfo": "Datei: {0}", + "Composer_LightTheme": "Helles Design", + "Composer_DarkTheme": "Dunkles Design", + "Composer_Outdent": "Einzug verringern", + "Composer_Indent": "Einzug erhöhen", + "Composer_BulletList": "Aufzählungsliste", + "Composer_OrderedList": "Nummerierte Liste", + "Composer_Stroke": "Strich", + "Composer_Bold": "Fett", + "Composer_Italic": "Kursiv", + "Composer_Underline": "Unterstrichen", + "Composer_CcBcc": "Cc & Bcc", + "Composer_EnableSmimeSignature": "S/MIME-Signatur aktivieren/deaktivieren", + "Composer_EnableSmimeEncryption": "S/MIME-Verschlüsselung aktivieren/deaktivieren", + "Composer_LocalDraftSyncInfo": "Dieser Entwurf ist nur lokal. Wino konnte ihn nicht an Ihren Mail-Server senden. Klicken Sie, um den Versand erneut zu versuchen.", + "Composer_CertificateExpires": "Gültig bis: ", + "Composer_SmimeSignature": "S/MIME-Signatur", + "Composer_SmimeEncryption": "S/MIME-Verschlüsselung", + "Composer_EmailTemplatesPlaceholder": "E-Mail-Vorlagen", + "Composer_AiSummarize": "Mit KI zusammenfassen", + "Composer_AiSummarizeDescription": "Extrahiert zentrale Punkte, Maßnahmen und Entscheidungen aus dieser E-Mail.", + "Composer_AiTranslate": "Mit KI übersetzen", + "Composer_AiActions": "KI-Aktionen", + "Composer_AiRewrite": "Mit KI umschreiben", + "AiActions_CheckingStatus": "KI-Zugriff wird geprüft...", + "AiActions_SignedOutTitle": "Wino AI-Paket entsperren", + "AiActions_SignedOutDescription": "Übersetzen, Umschreiben und Zusammenfassen von E-Mails mit KI nach der Anmeldung bei Ihrem Wino-Konto und Aktivierung des AI Pack-Add-ons.", + "AiActions_NoPackTitle": "KI-Paket erforderlich", + "AiActions_NoPackDescription": "Sie sind angemeldet, aber das KI-Paket ist noch nicht aktiv. Kaufen Sie es, um Winos KI-Übersetzung, Umschreiben und Zusammenfassungswerkzeuge zu verwenden.", + "AiActions_UsageSummary": "{0} von {1} Credits in diesem Monat verwendet.", + "Composer_AiRewritePolite": "Höflicher formulieren", + "Composer_AiRewritePoliteDescription": "Formulierung mildern, aber denselben Sinn beibehalten.", + "Composer_AiRewriteAngry": "Wütend formulieren", + "Composer_AiRewriteAngryDescription": "Verwendet einen schärferen und konfrontativeren Ton.", + "Composer_AiRewriteHappy": "Mach es fröhlich", + "Composer_AiRewriteHappyDescription": "Fügt einen lebhafteren und enthusiastischeren Ton hinzu.", + "Composer_AiRewriteFormal": "Mach es formell.", + "Composer_AiRewriteFormalDescription": "Lässt die Nachricht professioneller und strukturierter klingen.", + "Composer_AiRewriteFriendly": "Mach es freundlicher.", + "Composer_AiRewriteFriendlyDescription": "Lässt die Nachricht freundlicher wirken.", + "Composer_AiRewriteShorter": "Mach es kürzer.", + "Composer_AiRewriteShorterDescription": "Strafft den Text und entfernt unnötige Details.", + "Composer_AiRewriteClearer": "Mach es klarer.", + "Composer_AiRewriteClearerDescription": "Verbessert die Lesbarkeit und macht die Nachricht leichter verständlich.", + "Composer_AiRewriteCustom": "Benutzerdefiniert", + "Composer_AiRewriteCustomDescription": "Beschreiben Sie Ihre gewünschte Umformulierung.", + "Composer_AiRewriteCustomPlaceholder": "Beschreiben Sie, wie die Nachricht umformuliert werden soll.", + "Composer_AiRewriteMode": "Ton umschreiben", + "Composer_AiRewriteApply": "Umformulierung anwenden", + "Composer_AiTranslateDialogTitle": "Übersetzen mit KI", + "Composer_AiTranslateDialogDescription": "Geben Sie die Zielsprache oder den Kulturcode ein, z. B. en-US, tr-TR, de-DE oder fr-FR.", + "Composer_AiTranslateApply": "Übersetzen", + "Composer_AiTranslateLanguage": "Zielsprache", + "Composer_AiTranslateCustomPlaceholder": "Geben Sie den Kulturcode ein.", + "Composer_AiTranslateLanguageEnglish": "Englisch (en-US)", + "Composer_AiTranslateLanguageTurkish": "Türkisch (tr-TR)", + "Composer_AiTranslateLanguageGerman": "Deutsch (de-DE)", + "Composer_AiTranslateLanguageFrench": "Französisch (fr-FR)", + "Composer_AiTranslateLanguageSpanish": "Spanisch (es-ES)", + "Composer_AiTranslateLanguageItalian": "Italienisch (it-IT)", + "Composer_AiTranslateLanguagePortugueseBrazil": "Portugiesisch (Brasilien) (pt-BR)", + "Composer_AiTranslateLanguageDutch": "Niederländisch (nl-NL)", + "Composer_AiTranslateLanguagePolish": "Polnisch (pl-PL)", + "Composer_AiTranslateLanguageRussian": "Russisch (ru-RU)", + "Composer_AiTranslateLanguageJapanese": "Japanisch (ja-JP)", + "Composer_AiTranslateLanguageKorean": "Koreanisch (ko-KR)", + "Composer_AiTranslateLanguageChineseSimplified": "Chinesisch, Vereinfachtes (zh-CN)", + "Composer_AiTranslateLanguageArabic": "Arabisch (ar-SA)", + "Composer_AiTranslateLanguageHindi": "Hindi (hi-IN)", + "Composer_AiTranslateLanguageOther": "Andere...", + "Composer_AiBusyTitle": "KI arbeitet bereits", + "Composer_AiBusyMessage": "Bitte warten Sie, bis der aktuelle KI-Vorgang abgeschlossen ist.", + "Composer_AiSignInRequired": "Melden Sie sich bei Ihrem Wino-Konto an, um KI-Funktionen zu verwenden.", + "Composer_AiMissingHtml": "Es gibt noch keinen Nachrichteninhalt, der an Wino-KI gesendet werden kann.", + "Composer_AiQuotaUnavailable": "Das KI-Ergebnis wurde angewendet.", + "Composer_AiAppliedMessage": "Das KI-Ergebnis wurde dem Editor angewendet. Verwenden Sie Rückgängig, wenn Sie es wieder rückgängig machen möchten.", + "Composer_AiSummarizeSuccessTitle": "KI-Zusammenfassung angewendet.", + "Composer_AiTranslateSuccessTitle": "KI-Übersetzung angewendet.", + "Composer_AiRewriteSuccessTitle": "KI-Umformulierung angewendet.", + "Composer_AiErrorTitle": "KI-Aktion fehlgeschlagen.", + "Reader_AiAppliedMessage": "Das KI-Ergebnis wird jetzt für diese Nachricht angezeigt. Öffnen Sie die Nachricht erneut, um den ursprünglichen Inhalt wieder anzuzeigen.", "SettingsAppPreferences_EmailSyncInterval_Title": "Intervall für E-Mail-Synchronisierung", - "SettingsAppPreferences_EmailSyncInterval_Description": "Intervall für die automatisches E-Mail-Synchronisierung (Minuten). Diese Einstellung wird erst nach einem Neustart von Wino Mail angewendet." + "SettingsAppPreferences_EmailSyncInterval_Description": "Intervall für die automatisches E-Mail-Synchronisierung (Minuten). Diese Einstellung wird erst nach einem Neustart von Wino Mail angewendet.", + "ContactsPage_Title": "Kontakte", + "ContactsPage_AddContact": "Kontakt hinzufügen", + "ContactsPage_EditContact": "Kontakt bearbeiten", + "ContactsPage_DeleteContact": "Kontakt löschen", + "ContactsPage_SearchPlaceholder": "Kontakte durchsuchen...", + "ContactsPage_NoContacts": "Keine Kontakte gefunden", + "ContactsPage_ContactsCount": "{0} Kontakte", + "ContactsPage_SelectedContactsCount": "{0} ausgewählt", + "ContactsPage_DeleteSelectedContacts": "Ausgewählte löschen", + "ContactEditDialog_Title": "Kontakt bearbeiten", + "ContactEditDialog_PhotoSection": "Foto", + "ContactEditDialog_ChoosePhoto": "Foto auswählen", + "ContactEditDialog_RemovePhoto": "Foto entfernen", + "ContactEditDialog_NameHeader": "Name", + "ContactEditDialog_NamePlaceholder": "Kontaktname", + "ContactEditDialog_EmailHeader": "E-Mail-Adresse", + "ContactEditDialog_EmailPlaceholder": "contact@example.com", + "ContactEditDialog_InfoSection": "Kontaktinformationen", + "ContactEditDialog_RootContactInfo": "Dies ist ein Hauptkontakt, der mit Ihren Konten verknüpft ist und nicht gelöscht werden kann.", + "ContactEditDialog_OverriddenContactInfo": "Dieser Kontakt wurde manuell bearbeitet und während der Synchronisierung nicht aktualisiert.", + "ContactsPage_Subtitle": "Verwalten Sie Ihre E-Mail-Kontakte und deren Informationen.", + "ContactStatus_Account": "Konto", + "ContactStatus_Modified": "Geändert", + "ContactAction_Edit": "Kontakt bearbeiten", + "ContactAction_ChangePhoto": "Foto ändern", + "ContactAction_Delete": "Kontakt löschen", + "ContactAction_Add": "Kontakt hinzufügen", + "ContactSelection_Selected": "ausgewählt", + "ContactSelection_SelectAll": "Alle auswählen", + "ContactSelection_Clear": "Auswahl löschen", + "ContactsPage_EmptyState": "Keine Kontakte anzuzeigen", + "ContactsPage_AddFirstContact": "Fügen Sie Ihren ersten Kontakt hinzu", + "ContactsPage_ContactsCountSuffix": "Kontakte", + "ContactsPane_NewContact": "Neuer Kontakt", + "ContactsPane_DescriptionTitle": "Verwalten Sie Ihre Kontakte", + "ContactsPane_DescriptionBody": "Erstellen Sie Kontakte, benennen Sie sie um, aktualisieren Sie Profilfotos und halten Sie gespeicherte Details an einem Ort organisiert.", + "ContactEditDialog_AddTitle": "Kontakt hinzufügen", + "ContactInfoBar_ContactAdded": "Kontakt erfolgreich hinzugefügt.", + "ContactInfoBar_ContactUpdated": "Kontakt erfolgreich aktualisiert.", + "ContactInfoBar_ContactsDeleted": "Kontakte erfolgreich gelöscht.", + "ContactInfoBar_ContactPhotoUpdated": "Kontaktfoto erfolgreich aktualisiert.", + "ContactInfoBar_FailedToLoadContacts": "Fehler beim Laden der Kontakte: {0}", + "ContactInfoBar_FailedToAddContact": "Fehler beim Hinzufügen des Kontakts: {0}", + "ContactInfoBar_FailedToUpdateContact": "Fehler beim Aktualisieren des Kontakts: {0}", + "ContactInfoBar_FailedToDeleteContacts": "Fehler beim Löschen der Kontakte: {0}", + "ContactInfoBar_FailedToUpdatePhoto": "Fehler beim Aktualisieren des Fotos: {0}", + "ContactInfoBar_CannotDeleteRoot": "Wurzelkontakte können nicht gelöscht werden.", + "ContactConfirmDialog_DeleteTitle": "Kontakt löschen", + "ContactConfirmDialog_DeleteMessage": "Möchten Sie den Kontakt '{0}' wirklich löschen?", + "ContactConfirmDialog_DeleteMultipleMessage": "Möchten Sie wirklich {0} Kontakt(e) löschen?", + "ContactConfirmDialog_DeleteButton": "Löschen", + "CalendarAccountSettings_Title": "Kalenderkonto-Einstellungen", + "CalendarAccountSettings_Description": "Kalendereinstellungen für {0} verwalten.", + "CalendarAccountSettings_AccountColor": "Kontofarbe", + "CalendarAccountSettings_AccountColorDescription": "Anzeige-Farbe für dieses Kalenderkonto ändern", + "CalendarAccountSettings_SyncEnabled": "Synchronisierung aktivieren", + "CalendarAccountSettings_SyncEnabledDescription": "Kalendersynchronisierung für dieses Konto aktivieren oder deaktivieren", + "CalendarAccountSettings_DefaultShowAs": "Standard-Verfügbarkeitsstatus", + "CalendarAccountSettings_DefaultShowAsDescription": "Standard-Verfügbarkeitsstatus für neue Termine, die mit diesem Konto erstellt werden.", + "CalendarAccountSettings_PrimaryCalendar": "Primärer Kalender", + "CalendarAccountSettings_PrimaryCalendarDescription": "Diesen Kalender als primären Kalender für das Konto festlegen.", + "CalendarSettings_NewEventBehavior_Header": "Verhalten der Schaltfläche Neuer Termin", + "CalendarSettings_NewEventBehavior_Description": "Wählen Sie, ob die Schaltfläche Neuer Termin jedes Mal nach einem Kalender fragen oder immer einen bestimmten Kalender öffnen soll.", + "CalendarSettings_NewEventBehavior_AskEachTime": "Bei jeder Verwendung fragen.", + "CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar": "Immer denselben Kalender verwenden.", + "CalendarSettings_Rendering_Title": "Darstellung", + "CalendarSettings_Rendering_Description": "Kalender-Layout und Anzeigeverhalten konfigurieren.", + "CalendarSettings_Notifications_Title": "Benachrichtigungen", + "CalendarSettings_Notifications_Description": "Standard-Erinnerungs- und Schlummerverhalten festlegen.", + "CalendarSettings_Preferences_Title": "Einstellungen", + "CalendarSettings_Preferences_Description": "Legen Sie fest, wie sich die Schaltfläche Neuer Termin verhält.", + "WhatIsNew_GetStartedButton": "Loslegen", + "WhatIsNew_ContinueAnywayButton": "Trotzdem fortfahren", + "WhatIsNew_PreparingForNewVersionButton": "Auf die neue Version vorbereiten...", + "WhatIsNew_MigrationPreparing_Title": "Daten werden vorbereitet", + "WhatIsNew_MigrationPreparing_Description": "Wino führt Update-Migrationen durch. Bitte warten Sie, während wir Ihre Kontodaten für diese Version vorbereiten.", + "WhatIsNew_MigrationFailedMessage": "Beim Anwenden der Migrationen ist ein Fehler aufgetreten: {0}. Sie können die Anwendung weiterhin verwenden. Falls Sie jedoch schwerwiegende Probleme feststellen, installieren Sie die Anwendung bitte neu.", + "WhatIsNew_MigrationNotification_Title": "Wino Mail aktualisiert", + "WhatIsNew_MigrationNotification_Message": "Öffnen Sie die App, um das Update abzuschließen und Neues zu sehen.", + "WelcomeWindow_Title": "Willkommen bei Wino Mail", + "WelcomeWindow_Subtitle": "Eine native Windows-Erfahrung für Mail und Kalender.", + "WelcomeWindow_WhatsNewTitle": "Neueste Änderungen", + "WelcomeWindow_FeaturesTitle": "Funktionen", + "WelcomeWindow_WhatsNewTab": "Was gibt es Neues", + "WelcomeWindow_FeaturesTab": "Funktionen", + "WelcomeWindow_GetStartedButton": "Konto hinzufügen und loslegen", + "WelcomeWindow_GetStartedDescription": "Fügen Sie Ihr Outlook-, Gmail- oder IMAP-Konto hinzu, um mit Wino Mail zu beginnen.", + "WelcomeWindow_ImportFromWinoAccount": "Aus Ihrem Wino-Konto importieren", + "WelcomeWindow_ImportInProgress": "Import Ihrer synchronisierten Einstellungen und Konten läuft.", + "WelcomeWindow_ImportNoAccountsFound": "Es wurden keine synchronisierten Konten in Ihrem Wino-Konto gefunden. Falls Einstellungen verfügbar waren, wurden sie wiederhergestellt. Verwenden Sie Loslegen, um manuell ein Konto hinzuzufügen.", + "WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} synchronisierte Konten sind auf diesem Gerät bereits verfügbar. Verwenden Sie Loslegen, um bei Bedarf manuell ein weiteres Konto hinzuzufügen.", + "WelcomeWindow_SetupTitle": "Ihr Konto einrichten", + "WelcomeWindow_SetupSubtitle": "Wählen Sie Ihren E-Mail-Anbieter, um loszulegen", + "WelcomeWindow_AddAccountButton": "Konto hinzufügen", + "WelcomeWindow_SkipForNow": "Jetzt überspringen — später einrichten.", + "WelcomeWindow_AppDescription": "Ein schnelles, fokussiertes Postfach – neu gestaltet für Windows 11.", + "WelcomeWizard_Step1Title": "Willkommen", + "SystemTrayMenu_Open": "Öffnen", + "WinoAccount_Titlebar_SyncBenefitTitle": "Synchronisierungseinstellungen", + "WinoAccount_Titlebar_SyncBenefitDescription": "Ihre Wino-Einstellungen geräteübergreifend synchron halten.", + "WinoAccount_Titlebar_AddonsBenefitTitle": "Add-Ons freischalten", + "WinoAccount_Titlebar_AddonsBenefitDescription": "Zugriff auf Premium-Funktionen wie Wino AI Pack.", + "WinoAccount_Management_Description": "Verwalten Sie Ihr Wino-Konto, den Zugriff auf AI Pack sowie synchronisierte Präferenzen und Kontodetails.", + "WinoAccount_Management_SignedOutTitle": "Bei Wino Mail anmelden", + "WinoAccount_Management_SignedOutDescription": "Melden Sie sich an oder erstellen Sie ein Konto, um Ihre E-Mails zu synchronisieren, auf KI-Funktionen zuzugreifen und Ihre Einstellungen über Geräte hinweg zu verwalten.", + "WinoAccount_Management_ProfileSectionHeader": "Profil", + "WinoAccount_Management_AddOnsSectionHeader": "Wino Add-Ons", + "WinoAccount_Management_DataSectionHeader": "Daten", + "WinoAccount_Management_AccountActionsSectionHeader": "Kontenaktionen", + "WinoAccount_Management_AccountCardTitle": "Konto", + "WinoAccount_Management_AccountCardDescription": "Ihre Wino-Konto-E-Mail-Adresse und aktueller Kontostatus.", + "WinoAccount_Management_AiPackCardTitle": "AI Pack", + "WinoAccount_Management_AiPackCardDescription": "Sehen Sie, ob Wino AI Pack aktiv ist und wie viel Nutzung übrig bleibt.", + "WinoAccount_Management_AiPackActive": "AI Pack ist aktiv", + "WinoAccount_Management_AiPackInactive": "AI Pack ist nicht aktiv", + "WinoAccount_Management_AiPackUsage": "{0} von {1} Nutzungen verbraucht. {2} verbleibend.", + "WinoAccount_Management_AiPackBillingPeriod": "Abrechnungszeitraum: {0:d} - {1:d}", + "WinoAccount_Management_AiPackUnknownUsage": "Nutzungsdetails sind noch nicht verfügbar.", + "WinoAccount_Management_AiPackBuyDescription": "Kaufen Sie Wino AI Pack, um E-Mails mithilfe von KI zu übersetzen, umzuschreiben oder zusammenzufassen.", + "WinoAccount_Management_AiPackPromoTitle": "AI Pack freischalten", + "WinoAccount_Management_AiPackPromoDescription": "Beschleunigen Sie Ihren E-Mail-Workflow mit KI-gestützten Tools. Übersetzen Sie Nachrichten in über 50 Sprachen, verbessern Sie Klarheit und Tonfall und erhalten Sie sofortige Zusammenfassungen langer Threads.", + "WinoAccount_Management_AiPackPromoPrice": "$4.99 / mo", + "WinoAccount_Management_AiPackPromoRequests": "1.000 Credits", + "WinoAccount_Management_AiPackGetButton": "AI Pack erhalten", + "WinoAddOn_AI_PACK_Name": "Wino AI Pack", + "WinoAddOn_AI_PACK_Description": "KI-gestützte Tools zum Übersetzen, Umformulieren und Zusammenfassen von Nachrichten in Wino Mail.", + "WinoAddOn_AI_PACK_Keywords": "KI, übersetzen, umschreiben, zusammenfassen, Produktivität", + "WinoAddOn_UNLIMITED_ACCOUNTS_Name": "Unbegrenzte Konten", + "WinoAddOn_UNLIMITED_ACCOUNTS_Description": "Entfernt das Kontolimit und fügt beliebig viele E-Mail-Konten hinzu.", + "WinoAddOn_UNLIMITED_ACCOUNTS_Keywords": "Konten, unbegrenzt, Premium, Add-on", + "WinoAccount_Management_PurchaseRequiresSignIn": "Melden Sie sich mit Ihrem Wino-Konto an, um diesen Kauf abzuschließen.", + "WinoAccount_Management_PurchaseStartFailed": "Wino konnte diesen Kauf im Microsoft Store nicht abschließen.", + "WinoAccount_Management_StoreSyncFailed": "Ihr Kauf ist abgeschlossen, aber Wino konnte Ihre Kontofunktionen noch nicht aktualisieren. Bitte versuchen Sie es in einem Moment erneut.", + "WinoAccount_Management_AiPackSubscriptionActive": "Ihr Abonnement ist aktiv", + "WinoAccount_Management_AiPackRenews": "Verlängert sich {0:d}", + "WinoAccount_Management_AiPackRequestsUsed": "In diesem Monat verwendete Credits", + "WinoAccount_Management_AiPackResets": "Zurücksetzungen {0:d}", + "WinoAccount_Management_AiPackUsageLoadFailed": "Beim Laden Ihres AI-Nutzungsstands gab es Probleme.", + "WinoAccount_Management_AiPackFeatureTranslate": "Übersetzen", + "WinoAccount_Management_AiPackFeatureRewrite": "Umschreiben", + "WinoAccount_Management_AiPackFeatureSummarize": "Zusammenfassen", + "WinoAccount_Management_AddOnLoadFailed": "Beim Laden dieses Add-Ons gab es Probleme.", + "WinoAccount_Management_SyncPreferencesTitle": "Einstellungen und Konten synchronisieren", + "WinoAccount_Management_SyncPreferencesDescription": "Importieren oder exportieren Sie Ihre Wino-Einstellungen und Postfachdaten über Geräte hinweg. Passwörter, Tokens und andere sensible Informationen werden niemals synchronisiert.", + "WinoAccount_Management_SignOutTitle": "Abmelden", + "WinoAccount_Management_SignOutDescription": "Melden Sie sich von Ihrem Konto auf diesem Gerät ab.", + "WinoAccount_Management_StatusLabel": "Status: {0}", + "WinoAccount_Management_NoRemoteSettings": "Es liegen derzeit keine synchronisierten Daten für dieses Konto vor.", + "WinoAccount_Management_ExportSucceeded": "Ihre ausgewählten Wino-Daten wurden erfolgreich exportiert.", + "WinoAccount_Management_ExportPreferencesSucceeded": "Ihre Einstellungen wurden in Ihr Wino-Konto exportiert.", + "WinoAccount_Management_ExportAccountsSucceeded": "Es wurden {0} Kontodaten in Ihr Wino-Konto exportiert.", + "WinoAccount_Management_ImportSucceeded": "Synchronisierte Daten aus Ihrem Wino-Konto importiert.", + "WinoAccount_Management_ImportPreferencesSucceeded": "Es wurden {0} synchronisierte Präferenzen angewendet.", + "WinoAccount_Management_ImportAccountsSucceeded": "Es wurden {0} Konten importiert.", + "WinoAccount_Management_ImportDuplicateAccountsSkipped": "Es wurden {0} Konten übersprungen, die auf diesem Gerät bereits vorhanden sind.", + "WinoAccount_Management_ImportPartial": "Es wurden {0} synchronisierte Einstellungen übernommen. {1} Einstellungen konnten nicht wiederhergestellt werden.", + "WinoAccount_Management_ImportReloginReminder": "Passwörter, Tokens und andere sensible Informationen wurden nicht importiert. Melden Sie sich vor der Verwendung dieses Geräts für jedes Konto erneut an.", + "WinoAccount_Management_SerializeFailed": "Wino konnte Ihre aktuellen Einstellungen nicht serialisieren.", + "WinoAccount_Management_EmptyExport": "Es liegen keine Einstellungen zum Export bereit.", + "WinoAccount_Management_ImportEmpty": "Der synchronisierte Datenpayload enthält keine neuen Daten zur Wiederherstellung.", + "WinoAccount_Management_ExportDialog_Title": "Exportieren Sie in Ihr Wino-Konto.", + "WinoAccount_Management_ExportDialog_Description": "Wählen Sie aus, was Sie mit Ihrem Wino-Konto synchronisieren möchten.", + "WinoAccount_Management_ExportDialog_IncludePreferences": "Einstellungen", + "WinoAccount_Management_ExportDialog_IncludeAccounts": "Konten", + "WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Passwörter, Tokens und andere sensible Informationen werden nicht synchronisiert.", + "WinoAccount_Management_ExportDialog_AccountsRelogin": "Auf einem anderen PC importierte Konten müssen sich weiterhin erneut anmelden, bevor sie verwendet werden können.", + "WinoAccount_Management_ExportDialog_InProgress": "Exportieren Sie Ihre ausgewählten Wino-Daten.", + "WinoAccount_Management_LoadFailed": "Wino konnte die neuesten Informationen zum Wino-Konto nicht laden.", + "WinoAccount_Management_ActionFailed": "Die Wino-Konto-Anfrage konnte nicht abgeschlossen werden.", + "WinoAccount_SettingsSection_Title": "Wino-Konto", + "WinoAccount_SettingsSection_Description": "Erstellen oder melden Sie sich über Ihren lokalen Authentifizierungsdienst bei einem Wino-Konto an.", + "WinoAccount_RegisterButton_Title": "Konto registrieren", + "WinoAccount_RegisterButton_Description": "Erstellen Sie ein Wino-Konto mit E-Mail-Adresse und Passwort.", + "WinoAccount_RegisterButton_Action": "Registrierung öffnen", + "WinoAccount_LoginButton_Title": "Anmelden", + "WinoAccount_LoginButton_Description": "Melden Sie sich mit E-Mail und Passwort bei einem bestehenden Wino-Konto an.", + "WinoAccount_LoginButton_Action": "Anmeldung öffnen", + "WinoAccount_SignOutButton_Title": "Abmelden", + "WinoAccount_SignOutButton_Description": "Die lokal gespeicherte Wino-Konto-Sitzung entfernen.", + "WinoAccount_SignOutButton_Action": "Abmelden", + "WinoAccount_RegisterDialog_Title": "Wino-Konto erstellen", + "WinoAccount_RegisterDialog_Description": "Erstellen Sie ein Wino-Konto, um Ihr Wino-Erlebnis zu synchronisieren und kontobasierte Add-ons freizuschalten.", + "WinoAccount_RegisterDialog_HeroTitle": "Ihr Wino-Konto erstellen", + "WinoAccount_RegisterDialog_BenefitsTitle": "Warum eines erstellen?", + "WinoAccount_RegisterDialog_BenefitSyncTitle": "Einstellungen geräteübergreifend importieren und exportieren", + "WinoAccount_RegisterDialog_BenefitSyncDescription": "Verschieben Sie Ihre Wino-Einstellungen zwischen Geräten, ohne Ihre Einrichtung von Grund auf neu aufbauen zu müssen.", + "WinoAccount_RegisterDialog_BenefitAiTitle": "Zugriff auf exklusive Add-ons wie das Wino AI Pack (kostenpflichtig).", + "WinoAccount_RegisterDialog_BenefitAiDescription": "Verwenden Sie ein Konto, um Premium-Wino-Funktionen freizuschalten, sobald sie verfügbar sind.", + "WinoAccount_RegisterDialog_DifferenceTitle": "Wino-Konto ist von Ihren E-Mail-Konten getrennt", + "WinoAccount_RegisterDialog_DifferenceDescription": "Ihre Outlook-, Gmail-, IMAP- oder anderen E-Mail-Konten bleiben unverändert. Ein Wino-Konto verwaltet nur Wino-spezifische Funktionen und kontoabhängige Add-ons.", + "WinoAccount_RegisterDialog_PrimaryButton": "Registrieren", + "WinoAccount_RegisterDialog_PrivacyTitle": "Datenschutz und API-Verarbeitung", + "WinoAccount_RegisterDialog_PrivacyDescription": "Optionale Add-ons wie das Wino AI Pack können ausgewählte E-Mail-HTML-Inhalte nur dann an den Wino-API-Dienst senden, wenn Sie diese Funktionen verwenden.", + "WinoAccount_RegisterDialog_PrivacyLinkText": "Datenschutzerklärung lesen", + "WinoAccount_RegisterDialog_PrivacyCheckbox": "Ich stimme der Datenschutzerklärung zu.", + "WinoAccount_LoginDialog_Title": "Wino-Konto anmelden", + "WinoAccount_LoginDialog_Description": "Melden Sie sich bei Ihrem Wino-Konto an, um Ihre Wino-Einstellungen zu synchronisieren und auf kontobasierte Funktionen zuzugreifen.", + "WinoAccount_LoginDialog_HeroTitle": "Willkommen zurück", + "WinoAccount_LoginDialog_BenefitsTitle": "Was das Anmelden Ihnen bietet", + "WinoAccount_LoginDialog_BenefitsDescription": "Verwenden Sie Ihr Wino-Konto, um Einstellungen geräteübergreifend weiter zu synchronisieren und auf kostenpflichtige Add-ons wie das Wino AI Pack zuzugreifen.", + "WinoAccount_LoginDialog_DifferenceTitle": "Dies ist nicht die Anmeldung für Ihr E-Mail-Postfach", + "WinoAccount_LoginDialog_DifferenceDescription": "Das Anmelden hier fügt Ihre Outlook-, Gmail- oder IMAP-Konten in Wino weder hinzu noch ersetzt es sie. Sie melden sich ausschließlich bei Wino-spezifischen Diensten an.", + "WinoAccount_LoginDialog_ForgotPasswordLink": "Passwort vergessen?", + "WinoAccount_EmailLabel": "E-Mail", + "WinoAccount_EmailPlaceholder": "name@beispiel.de", + "WinoAccount_PasswordLabel": "Passwort", + "WinoAccount_ConfirmPasswordLabel": "Passwort bestätigen", + "WinoAccount_ForgotPasswordDialog_Title": "Passwort zurücksetzen", + "WinoAccount_ForgotPasswordDialog_PrimaryButton": "E-Mail zum Zurücksetzen senden", + "WinoAccount_ForgotPasswordDialog_BackToSignIn": "Zurück zur Anmeldung", + "WinoAccount_ForgotPasswordDialog_Description": "Geben Sie Ihre Wino-Konto-E-Mail-Adresse ein, und wir senden Ihnen einen Link zum Zurücksetzen des Passworts, falls die Adresse registriert ist.", + "WinoAccount_Validation_EmailRequired": "E-Mail-Adresse ist erforderlich.", + "WinoAccount_Validation_PasswordRequired": "Passwort ist erforderlich.", + "WinoAccount_Validation_PasswordMismatch": "Passwörter stimmen nicht überein.", + "WinoAccount_Validation_PrivacyConsentRequired": "Sie müssen der Datenschutzerklärung zustimmen, bevor Sie ein Wino-Konto erstellen.", + "WinoAccount_Error_InvalidCredentials": "Die E-Mail-Adresse oder das Passwort ist falsch.", + "WinoAccount_Error_AccountLocked": "Dieses Konto ist vorübergehend gesperrt.", + "WinoAccount_Error_AccountBanned": "Dieses Konto wurde gesperrt.", + "WinoAccount_Error_AccountSuspended": "Dieses Konto wurde vorübergehend deaktiviert.", + "WinoAccount_Error_EmailNotConfirmed": "Bitte bestätigen Sie Ihre E-Mail-Adresse, bevor Sie sich anmelden.", + "WinoAccount_Error_EmailConfirmationRequired": "Bitte bestätigen Sie Ihre E-Mail-Adresse, bevor Sie sich anmelden.", + "WinoAccount_Error_EmailConfirmationResendNotAvailable": "Eine neue Bestätigungs-E-Mail ist noch nicht verfügbar.", + "WinoAccount_Error_EmailConfirmationResendInvalid": "Diese Bestätigungsanfrage ist nicht mehr gültig. Bitte versuchen Sie sich erneut anzumelden.", + "WinoAccount_Error_EmailNotRegistered": "Diese E-Mail-Adresse ist nicht registriert.", + "WinoAccount_Error_RefreshTokenInvalid": "Ihre Sitzung ist nicht mehr gültig. Bitte melden Sie sich erneut an.", + "WinoAccount_Error_EmailAlreadyRegistered": "Diese E-Mail-Adresse ist bereits registriert.", + "WinoAccount_Error_ExternalLoginEmailRequired": "Eine E-Mail-Adresse ist erforderlich, um die externe Anmeldung abzuschließen.", + "WinoAccount_Error_ExternalLoginInvalid": "Die externe Anmeldung ist ungültig.", + "WinoAccount_Error_ExternalAuthStateInvalid": "Der Zustand der externen Anmeldung ist ungültig oder abgelaufen.", + "WinoAccount_Error_ExternalAuthCodeInvalid": "Der externe Anmeldecode ist ungültig oder abgelaufen.", + "WinoAccount_Error_AiPackRequired": "Für diese Aktion ist ein aktives Wino AI Pack-Abonnement erforderlich.", + "WinoAccount_Error_AiQuotaExceeded": "Ihr AI Pack-Verbrauchslimit wurde im aktuellen Abrechnungszeitraum erreicht.", + "WinoAccount_Error_AiHtmlEmpty": "Es gibt keinen E-Mail-Inhalt zum Verarbeiten.", + "WinoAccount_Error_AiHtmlTooLarge": "Diese E-Mail ist zu groß, um sie mit Wino AI zu verarbeiten.", + "WinoAccount_Error_AiUnsupportedLanguage": "Diese Sprache wird nicht unterstützt. Versuchen Sie einen gültigen Kulturcode wie en-US oder tr-TR.", + "WinoAccount_Error_Forbidden": "Sie haben nicht die Berechtigung, diese Aktion auszuführen.", + "WinoAccount_Error_ValidationFailed": "Die Anfrage ist ungültig. Bitte überprüfen Sie die eingegebenen Werte.", + "WinoAccount_RegisterSuccessMessage": "Wino-Konto-Registrierung für {0} abgeschlossen.", + "WinoAccount_LoginSuccessMessage": "Sie sind als {0} bei Ihrem Wino-Konto angemeldet.", + "WinoAccount_EmailConfirmationSentDialog_Title": "Bestätigen Sie Ihre E-Mail-Adresse.", + "WinoAccount_EmailConfirmationSentDialog_Message": "Wir haben eine Bestätigungs-E-Mail an {0} gesendet. Bitte bestätigen Sie diese und versuchen Sie sich erneut anzumelden.", + "WinoAccount_EmailConfirmationPendingDialog_Title": "E-Mail-Bestätigung erforderlich", + "WinoAccount_EmailConfirmationPendingDialog_Message": "Wir warten noch darauf, dass Sie {0} bestätigen.", + "WinoAccount_EmailConfirmationPendingDialog_ResendButton": "Bestätigungs-E-Mail erneut senden", + "WinoAccount_EmailConfirmationPendingDialog_Countdown": "Sie können die Bestätigungs-E-Mail in {0} erneut senden.", + "WinoAccount_EmailConfirmationPendingDialog_ReadyToResend": "Sie können die Bestätigungs-E-Mail jetzt erneut senden.", + "WinoAccount_EmailConfirmationResentDialog_Title": "Bestätigungs-E-Mail erneut gesendet", + "WinoAccount_EmailConfirmationResentDialog_Message": "Wir haben eine weitere Bestätigungs-E-Mail an {0} gesendet. Bitte bestätigen Sie diese und versuchen Sie sich erneut anzumelden.", + "WinoAccount_ForgotPasswordDialog_SuccessTitle": "E-Mail zum Zurücksetzen des Passworts gesendet.", + "WinoAccount_ForgotPasswordDialog_SuccessMessage": "Wir haben eine E-Mail zum Zurücksetzen des Passworts an {0} gesendet. Öffnen Sie diese Nachricht, um ein neues Passwort festzulegen.", + "WinoAccount_ChangePassword_Title": "Passwort ändern", + "WinoAccount_ChangePassword_Description": "Senden Sie eine E-Mail zum Zurücksetzen des Passworts an dieses Wino-Konto.", + "WinoAccount_ChangePassword_Action": "E-Mail zum Zurücksetzen senden", + "WinoAccount_ChangePassword_ConfirmationMessage": "Möchten Sie, dass Wino eine Passwort-Zurücksetzungs-E-Mail an {0} sendet?", + "WinoAccount_SignOut_SuccessMessage": "Vom Wino-Konto {0} abgemeldet.", + "WinoAccount_SignOut_NoAccountMessage": "Es gibt kein aktives Wino-Konto zum Abmelden.", + "WinoAccount_Titlebar_SignedOutTitle": "Wino-Konto", + "WinoAccount_Titlebar_SignedOutDescription": "Melden Sie sich an oder erstellen Sie ein Wino-Konto, um Ihre Wino-Sitzung zu verwalten.", + "WinoAccount_Titlebar_SignedInStatus": "Status: {0}", + "WelcomeWizard_Step2Title": "Konto hinzufügen", + "WelcomeWizard_Step3Title": "Einrichtung abschließen", + "ProviderSelection_Title": "E-Mail-Anbieter auswählen", + "ProviderSelection_Subtitle": "Wählen Sie unten einen Anbieter aus, um Ihr E-Mail-Konto zu Wino Mail hinzuzufügen.", + "ProviderSelection_AccountNameHeader": "Kontoname", + "ProviderSelection_AccountNamePlaceholder": "z. B. Privat, Arbeit", + "ProviderSelection_DisplayNameHeader": "Anzeigename", + "ProviderSelection_DisplayNamePlaceholder": "z. B. John Doe", + "ProviderSelection_EmailHeader": "E-Mail-Adresse", + "ProviderSelection_EmailPlaceholder": "z. B. johndoe@example.com", + "ProviderSelection_AppPasswordHeader": "Anwendungsspezifisches Passwort", + "ProviderSelection_AppPasswordHelp": "Wie erhalte ich ein anwendungsspezifisches Passwort?", + "ProviderSelection_CalendarModeHeader": "Kalender-Integration", + "ProviderSelection_CalendarMode_DisabledTitle": "Deaktiviert", + "ProviderSelection_CalendarMode_DisabledDescription": "Keine Kalenderintegration", + "ProviderSelection_CalendarMode_CalDavTitle": "CalDAV-Synchronisierung", + "ProviderSelection_CalendarMode_CalDavDescription_Apple": "Ihre Kalendereinträge werden zwischen Ihren Geräten mit Apple-Servern synchronisiert.", + "ProviderSelection_CalendarMode_CalDavDescription_Yahoo": "Ihre Kalendereinträge werden zwischen Ihren Geräten mit Yahoo-Servern synchronisiert.", + "ProviderSelection_CalendarMode_LocalTitle": "Lokaler Kalender", + "ProviderSelection_CalendarMode_LocalDescription": "Ihre Termine werden nur auf Ihrem Computer gespeichert. Keine Serververbindung.", + "ProviderSelection_ClearColor": "Farbe zurücksetzen", + "ProviderSelection_ContinueButton": "Weiter", + "ProviderSelection_SpecialImap_Subtitle": "Geben Sie Ihre Kontodaten ein, um eine Verbindung herzustellen.", + "AccountSetup_Title": "Konto einrichten", + "AccountSetup_Step_Authenticating": "Authentifizierung mit {0}", + "AccountSetup_Step_TestingMailAuth": "E-Mail-Authentifizierung wird getestet", + "AccountSetup_Step_SyncingFolders": "Ordner-Metadaten werden synchronisiert.", + "AccountSetup_Step_FetchingProfile": "Profildaten werden abgerufen.", + "AccountSetup_Step_DiscoveringCalDav": "CalDAV-Einstellungen werden ermittelt.", + "AccountSetup_Step_TestingCalendarAuth": "Kalender-Authentifizierung wird getestet", + "AccountSetup_Step_SavingAccount": "Kontoinformationen werden gespeichert.", + "AccountSetup_Step_FetchingCalendarMetadata": "Kalender-Metadaten werden abgerufen.", + "AccountSetup_Step_SyncingAliases": "Aliases werden synchronisiert.", + "AccountSetup_Step_Finalizing": "Einrichtung abschließen.", + "AccountSetup_FailureMessage": "Einrichtung fehlgeschlagen. Gehen Sie zurück, um Ihre Einstellungen zu korrigieren, oder versuchen Sie es später erneut.", + "AccountSetup_SuccessMessage": "Ihr Konto wurde erfolgreich eingerichtet!", + "AccountSetup_GoBackButton": "Zurück", + "AccountSetup_TryAgainButton": "Versuchen Sie es erneut.", + "ImapCalDavSettings_AutoDiscoveryFailed": "Automatische Erkennung fehlgeschlagen. Bitte geben Sie die Einstellungen manuell im Reiter Erweitert ein." } - - diff --git a/Wino.Core.Domain/Translations/el_GR/resources.json b/Wino.Core.Domain/Translations/el_GR/resources.json index f2ee62a4..6256152f 100644 --- a/Wino.Core.Domain/Translations/el_GR/resources.json +++ b/Wino.Core.Domain/Translations/el_GR/resources.json @@ -8,6 +8,7 @@ "AccountCacheReset_Message": "Αυτός ο λογαριασμός απαιτεί πλήρη επανασυγχρονισμό για να συνεχίσει να δουλεύει. Παρακαλώ περιμένετε ενώ ο Wino επανασυγχρονίζει τα μηνύματά σας...", "AccountContactNameYou": "Εσείς", "AccountCreationDialog_Completed": "όλα έτοιμα", + "AccountCreationDialog_FetchingCalendarMetadata": "Ανάκτηση λεπτομερειών ημερολογίου.", "AccountCreationDialog_FetchingEvents": "Ανάκτηση συμβάντων ημερολογίου.", "AccountCreationDialog_FetchingProfileInformation": "Λήψη λεπτομερειών προφίλ.", "AccountCreationDialog_GoogleAuthHelpClipboardText_Row0": "Εάν το πρόγραμμα περιήγησής σας δεν εκκινήθηκε αυτόματα για να ολοκληρωθεί ο έλεγχος ταυτότητας:", @@ -17,6 +18,7 @@ "AccountCreationDialog_Initializing": "αρχικοποιείται", "AccountCreationDialog_PreparingFolders": "Προς το παρόν λαμβάνουμε πληροφορίες φακέλων.", "AccountCreationDialog_SigninIn": "Οι πληροφορίες λογαριασμού αποθηκεύονται.", + "Purchased": "Αγορασμένο", "AccountEditDialog_Message": "Όνομα Λογαριασμού", "AccountEditDialog_Title": "Επεξεργασία Λογαριασμού", "AccountPickerDialog_Title": "Επιλέξτε έναν λογαριασμό", @@ -26,6 +28,10 @@ "AccountDetailsPage_Description": "Αλλάξτε το όνομα του λογαριασμού στο Wino και ορίστε το επιθυμητό όνομα αποστολέα.", "AccountDetailsPage_ColorPicker_Title": "Χρώμα λογαριασμού", "AccountDetailsPage_ColorPicker_Description": "Αναθέστε ένα νέο χρώμα λογαριασμού για να χρωματίσετε το σύμβολο του στη λίστα.", + "AccountDetailsPage_TabGeneral": "Γενικά", + "AccountDetailsPage_TabMail": "Ταχυδρομείο", + "AccountDetailsPage_TabCalendar": "Ημερολόγιο", + "AccountDetailsPage_CalendarListDescription": "Επιλέξτε ένα ημερολόγιο για να ορίσετε τις ρυθμίσεις του.", "AddHyperlink": "Προσθήκη", "AppCloseBackgroundSynchronizationWarningTitle": "Συγχρονισμός Παρασκηνίου", "AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Η εφαρμογή δεν έχει οριστεί για εκκίνηση κατά την έναρξη των Windows.", @@ -47,8 +53,10 @@ "BasicIMAPSetupDialog_Title": "Λογαριασμός IMAP", "Busy": "Απασχολημένο", "Buttons_AddAccount": "Προσθήκη Λογαριασμού", + "Buttons_FixAccount": "Διόρθωση λογαριασμού", "Buttons_AddNewAlias": "Προσθήκη Νέου Ψευδώνυμου", "Buttons_Allow": "Αποδοχή", + "Buttons_Apply": "Εφαρμογή", "Buttons_ApplyTheme": "Εφαρμογή Θέματος", "Buttons_Browse": "Περιήγηση", "Buttons_Cancel": "Ακύρωση", @@ -62,6 +70,7 @@ "Buttons_Edit": "Επεξεργασία", "Buttons_EnableImageRendering": "Ενεργοποίηση", "Buttons_Multiselect": "Επιλογή Πολλαπλών", + "Buttons_Manage": "Διαχείριση", "Buttons_No": "Όχι", "Buttons_Open": "Άνοιγμα", "Buttons_Purchase": "Αγορά", @@ -70,15 +79,134 @@ "Buttons_Save": "Αποθήκευση", "Buttons_SaveConfiguration": "Αποθήκευση Διαμόρφωσης", "Buttons_Send": "Αποστολή", + "Buttons_SendToServer": "Αποστολή στον διακομιστή", "Buttons_Share": "Κοινοποίηση", "Buttons_SignIn": "Σύνδεση", "Buttons_Sync": "Συγχρονισμός", "Buttons_SyncAliases": "Συγχρονισμός Ψευδώνυμων", "Buttons_TryAgain": "Προσπαθήστε ξανά", "Buttons_Yes": "Ναι", + "Sync_SynchronizingFolder": "Συγχρονισμός φακέλου {0} {1}%", + "Sync_DownloadedMessages": "Λήφθηκαν {0} μηνύματα από το {1}", + "SyncAction_Archiving": "Αρχειοθέτηση {0} μηνυμάτων", + "SyncAction_ClearingFlag": "Αφαίρεση σήματος από {0} μηνύματα", + "SyncAction_CreatingDraft": "Δημιουργία προσχεδίου", + "SyncAction_CreatingEvent": "Δημιουργία συμβάντος", + "SyncAction_Deleting": "Διαγραφή {0} μηνυμάτων", + "SyncAction_EmptyingFolder": "Άδειασμα φακέλου", + "SyncAction_MarkingAsRead": "Σήμανση {0} μηνυμάτων ως διαβασμένων", + "SyncAction_MarkingAsUnread": "Σήμανση {0} μηνυμάτων ως μη αναγνωσμένων", + "SyncAction_MarkingFolderAsRead": "Σήμανση φακέλου ως διαβασμένου", + "SyncAction_Moving": "Μετακίνηση {0} μηνυμάτων", + "SyncAction_MovingToFocused": "Μετακίνηση {0} μηνυμάτων στα Εστιασμένα", + "SyncAction_RenamingFolder": "Μετονομασία φακέλου", + "SyncAction_SendingMail": "Αποστολή μηνύματος", + "SyncAction_SettingFlag": "Σήμανση {0} μηνυμάτων με σημαία", + "SyncAction_SynchronizingAccount": "Συγχρονισμός {0}", + "SyncAction_SynchronizingAccounts": "Συγχρονισμός {0} λογαριασμών", + "SyncAction_SynchronizingCalendarData": "Συγχρονισμός δεδομένων ημερολογίου", + "SyncAction_SynchronizingCalendarEvents": "Συγχρονισμός συμβάντων ημερολογίου", + "SyncAction_SynchronizingCalendarMetadata": "Συγχρονισμός μεταδεδομένων ημερολογίου", + "SyncAction_Unarchiving": "Αποαρχειοθέτηση {0} μηνυμάτων", "CalendarAllDayEventSummary": "ολοήμερα συμβάντα", "CalendarDisplayOptions_Color": "Χρώμα", "CalendarDisplayOptions_Expand": "Επέκταση", + "CalendarEventResponse_Accept": "Αποδοχή", + "CalendarEventResponse_AcceptedResponse": "Αποδεκτό", + "CalendarEventResponse_Decline": "Απόρριψη", + "CalendarEventResponse_DeclinedResponse": "Απορρίφθηκε", + "CalendarEventResponse_NotResponded": "Δεν έχει απαντηθεί", + "CalendarEventResponse_Tentative": "Ενδεχόμενο", + "CalendarEventResponse_TentativeResponse": "Ενδεχόμενο", + "CalendarEventRsvpPanel_Accept": "Αποδοχή", + "CalendarEventRsvpPanel_AddMessage": "Προσθέστε ένα μήνυμα στην απάντησή σας... (προαιρετικό)", + "CalendarEventRsvpPanel_Decline": "Απόρριψη", + "CalendarEventRsvpPanel_Message": "Μήνυμα", + "CalendarEventRsvpPanel_SendReplyMessage": "Αποστολή απαντητικού μηνύματος", + "CalendarEventRsvpPanel_Tentative": "Ενδεχόμενο", + "CalendarEventRsvpPanel_Title": "Επιλογές απάντησης", + "CalendarAttendeeStatus_Accepted": "Αποδεκτό", + "CalendarAttendeeStatus_Declined": "Απορριφθέν", + "CalendarAttendeeStatus_NeedsAction": "Απαιτεί ενέργεια", + "CalendarAttendeeStatus_Tentative": "Ενδεχόμενο", + "CalendarEventDetails_Attachments": "Συνημμένα", + "CalendarEventCompose_AddAttachment": "Προσθήκη συνημμένου", + "CalendarEventCompose_AllDay": "Ολοήμερο", + "CalendarEventCompose_AttachmentsNotSupportedForCalDav": "Τα συνημμένα δεν υποστηρίζονται για ημερολόγια CalDAV.", + "CalendarEventCompose_EndDate": "Ημερομηνία λήξης", + "CalendarEventCompose_EndTime": "Ώρα λήξης", + "CalendarEventCompose_Every": "κάθε", + "CalendarEventCompose_ForWeekdays": "για", + "CalendarEventCompose_FrequencyDay": "ημέρα", + "CalendarEventCompose_FrequencyDayPlural": "ημέρες", + "CalendarEventCompose_FrequencyMonth": "μήνας", + "CalendarEventCompose_FrequencyMonthPlural": "μήνες", + "CalendarEventCompose_FrequencyWeek": "εβδομάδα", + "CalendarEventCompose_FrequencyWeekPlural": "εβδομάδες", + "CalendarEventCompose_FrequencyYear": "έτος", + "CalendarEventCompose_FrequencyYearPlural": "έτη", + "CalendarEventCompose_Location": "Τοποθεσία", + "CalendarEventCompose_LocationPlaceholder": "Προσθέστε τοποθεσία", + "CalendarEventCompose_NewEventButton": "Νέο γεγονός", + "CalendarEventCompose_DefaultCalendarHint": "Μπορείτε να επιλέξετε ένα προεπιλεγμένο ημερολόγιο για νέα γεγονότα στις ρυθμίσεις Ημερολογίου.", + "CalendarEventCompose_DefaultCalendarSettingsLink": "Άνοιγμα ρυθμίσεων Ημερολογίου", + "CalendarEventCompose_NoCalendarsMessage": "Δεν υπάρχουν ακόμη διαθέσιμα ημερολόγια για τη δημιουργία γεγονότος.", + "CalendarEventCompose_NoCalendarsTitle": "Δεν υπάρχουν διαθέσιμα ημερολόγια", + "CalendarEventCompose_NoEndDate": "Δεν υπάρχει ημερομηνία λήξης", + "CalendarEventCompose_Notes": "Σημειώσεις", + "CalendarEventCompose_PickCalendarTitle": "Επιλέξτε ένα ημερολόγιο", + "CalendarEventCompose_Recurring": "Επαναλαμβανόμενο", + "CalendarEventCompose_RecurringSummary": "Συμβαίνει κάθε {0} {1}{2} {3} με ισχύ {4}{5}", + "CalendarEventCompose_RecurringSummarySmart": "Συμβαίνει {0}{1} {2} με ισχύ {3}{4}", + "CalendarEventCompose_RepeatEvery": "Επανάληψη κάθε", + "CalendarEventCompose_SelectCalendar": "Επιλέξτε ημερολόγιο", + "CalendarEventCompose_SingleOccurrenceSummary": "Συμβαίνει στις {0} {1}", + "CalendarEventCompose_StartDate": "Ημερομηνία έναρξης", + "CalendarEventCompose_StartTime": "Ώρα έναρξης", + "CalendarEventCompose_TimeRangeSummary": "από {0} έως {1}", + "CalendarEventCompose_Title": "Τίτλος γεγονότος", + "CalendarEventCompose_TitlePlaceholder": "Προσθέστε τίτλο", + "CalendarEventCompose_Until": "έως", + "CalendarEventCompose_UntilSummary": " μέχρι {0}", + "CalendarEventCompose_ValidationInvalidAllDayRange": "Η ημερομηνία λήξης ολόήμερου συμβάντος πρέπει να είναι μετά την ημερομηνία έναρξης.", + "CalendarEventCompose_ValidationInvalidAttendee": "Ένας ή περισσότεροι συμμετέχοντες έχουν μη έγκυρη διεύθυνση email.", + "CalendarEventCompose_ValidationInvalidRecurrenceEnd": "Η ημερομηνία λήξης της επανάληψης πρέπει να είναι ίση ή μετά την ημερομηνία έναρξης του γεγονότος.", + "CalendarEventCompose_ValidationInvalidTimeRange": "Η ώρα λήξης πρέπει να είναι αργότερη από την ώρα έναρξης.", + "CalendarEventCompose_ValidationMissingAttachment": "Ένα ή περισσότερα συνημμένα δεν είναι πλέον διαθέσιμα: {0}", + "CalendarEventCompose_ValidationMissingCalendar": "Επιλέξτε ένα ημερολόγιο πριν τη δημιουργία του γεγονότος.", + "CalendarEventCompose_ValidationMissingTitle": "Πληκτρολογήστε έναν τίτλο γεγονότος πριν τη δημιουργία του γεγονότος.", + "CalendarEventCompose_ValidationTitle": "Ο έλεγχος εγκυρότητας του γεγονότος απέτυχε", + "CalendarEventCompose_WeekdaySummary": " στις {0}", + "CalendarEventCompose_Weekday_Friday": "Π", + "CalendarEventCompose_Weekday_Monday": "Δ", + "CalendarEventCompose_Weekday_Saturday": "Σ", + "CalendarEventCompose_Weekday_Sunday": "Κ", + "CalendarEventCompose_Weekday_Thursday": "Π", + "CalendarEventCompose_Weekday_Tuesday": "Τ", + "CalendarEventCompose_Weekday_Wednesday": "Τ", + "CalendarEventDetails_Details": "Λεπτομέρειες", + "CalendarEventDetails_EditSeries": "Επεξεργασία Σειράς", + "CalendarEventDetails_Editing": "Επεξεργασία", + "CalendarEventDetails_InviteSomeone": "Προσκάλεσε κάποιον", + "CalendarEventDetails_JoinOnline": "Συνδεθείτε online", + "CalendarEventDetails_Organizer": "Οργανωτής", + "CalendarEventDetails_People": "Συμμετέχοντες", + "CalendarEventDetails_ReadOnlyEvent": "Γεγονός μόνο ανάγνωσης", + "CalendarEventDetails_Reminder": "Υπενθύμιση", + "CalendarReminder_StartedHoursAgo": "Ξεκίνησε πριν από {0} ώρες", + "CalendarReminder_StartedMinutesAgo": "Ξεκίνησε πριν από {0} λεπτά", + "CalendarReminder_StartedNow": "Ξεκίνησε μόλις τώρα", + "CalendarReminder_StartingNow": "Ξεκινά τώρα", + "CalendarReminder_StartsInHours": "Ξεκινά σε {0} ώρες", + "CalendarReminder_StartsInMinutes": "Ξεκινά σε {0} λεπτά", + "CalendarReminder_SnoozeAction": "Αναβολή", + "CalendarReminder_SnoozeMinutesOption": "{0} λεπτά", + "CalendarEventDetails_ShowAs": "Εμφάνιση ως", + "CalendarShowAs_Free": "Ελεύθερο", + "CalendarShowAs_Tentative": "Ενδεχόμενο", + "CalendarShowAs_Busy": "Απασχολημένο", + "CalendarShowAs_OutOfOffice": "Εκτός Γραφείου", + "CalendarShowAs_WorkingElsewhere": "Εργάζεται αλλού", "CalendarItem_DetailsPopup_JoinOnline": "Συμμετοχή online", "CalendarItem_DetailsPopup_ViewEventButton": "Προβολή συμβάντος", "CalendarItem_DetailsPopup_ViewSeriesButton": "Προβολή σειράς", @@ -88,6 +216,9 @@ "ClipboardTextCopied_Message": "Το \"{0}\" αντιγράφηκε στο πρόχειρο.", "ClipboardTextCopied_Title": "Αντιγράφηκε", "ClipboardTextCopyFailed_Message": "Η αντιγραφή του {0} στο πρόχειρο απέτυχε.", + "ContactInfoBar_ErrorTitle": "Αποτυχία φόρτωσης πληροφοριών επαφής", + "ContactInfoBar_SuccessTitle": "Οι πληροφορίες επικοινωνίας φορτώθηκαν", + "ContactInfoBar_WarningTitle": "Οι πληροφορίες επικοινωνίας ενδέχεται να είναι ελλιπείς", "ComingSoon": "Έρχεται σύντομα...", "ComposerAttachmentsDragDropAttach_Message": "Επισύναψη", "ComposerAttachmentsDropZone_Message": "Τοποθετήστε το αρχείο σας εδώ", @@ -129,6 +260,10 @@ "DialogMessage_CreateLinkedAccountTitle": "Όνομα Συνδέσμου Λογαριασμού", "DialogMessage_DeleteAccountConfirmationMessage": "Διαγραφή {0};", "DialogMessage_DeleteAccountConfirmationTitle": "Όλα τα δεδομένα που σχετίζονται με αυτόν τον λογαριασμό θα διαγραφούν από το δίσκο οριστικά.", + "DialogMessage_DeleteEmailTemplateConfirmationMessage": "Διαγραφή προτύπου \"{0}\"?", + "DialogMessage_DeleteEmailTemplateConfirmationTitle": "Διαγραφή προτύπου ηλεκτρονικού ταχυδρομείου", + "DialogMessage_DeleteRecurringSeriesMessage": "Αυτό θα διαγράψει όλα τα γεγονότα της σειράς. Θέλετε να συνεχίσετε;", + "DialogMessage_DeleteRecurringSeriesTitle": "Διαγραφή Επαναλαμβανόμενης Σειράς", "DialogMessage_DiscardDraftConfirmationMessage": "Αυτό το πρόχειρο θα απορριφθεί. Θέλετε να συνεχίσετε;", "DialogMessage_DiscardDraftConfirmationTitle": "Απόρριψη προχείρου", "DialogMessage_EmptySubjectConfirmation": "Χωρίς Θέμα", @@ -172,11 +307,18 @@ "ElementTheme_Light": "Ανοιχτή λειτουργία", "Emoji": "Emoji", "Error_FailedToSetupSystemFolders_Title": "Αποτυχία ρύθμισης φακέλων συστήματος", + "Exception_AccountNeedsAttention_Title": "Ο λογαριασμός χρειάζεται προσοχή", + "Exception_AccountNeedsAttention_Message": "'{0}' χρειάζεται την προσοχή σας για να συνεχίσετε.", + "Exception_WebView2RuntimeMissing_Message": "Το Wino Mail δεν μπόρεσε να βρει το runtime Microsoft Edge WebView2. Παρακαλούμε εγκαταστήστε ή επιδιορθώστε το runtime ώστε να αποδοθούν σωστά τα περιεχόμενα του μηνύματος.", + "Exception_WebView2RuntimeMissing_Title": "Απαιτείται το WebView2 runtime", "Exception_AuthenticationCanceled": "Η ταυτοποίηση ακυρώθηκε", "Exception_CustomThemeExists": "Αυτό το θέμα υπάρχει ήδη.", "Exception_CustomThemeMissingName": "Πρέπει να δώσετε ένα όνομα.", "Exception_CustomThemeMissingWallpaper": "Πρέπει να δώσετε μια προσαρμοσμένη εικόνα φόντου.", "Exception_FailedToSynchronizeAliases": "Αποτυχία συγχρονισμού ψευδωνύμων", + "Exception_FailedToSynchronizeCalendarData": "Αποτυχία συγχρονισμού δεδομένων ημερολογίου", + "Exception_FailedToSynchronizeCalendarEvents": "Αποτυχία συγχρονισμού συμβάντων ημερολογίου", + "Exception_FailedToSynchronizeCalendarMetadata": "Αποτυχία συγχρονισμού λεπτομερειών ημερολογίου", "Exception_FailedToSynchronizeFolders": "Αποτυχία συγχρονισμού φακέλων", "Exception_FailedToSynchronizeProfileInformation": "Αποτυχία συγχρονισμού πληροφοριών προφίλ", "Exception_GoogleAuthCallbackNull": "Το Callback uri είναι κενό κατά την ενεργοποίηση.", @@ -229,6 +371,32 @@ "HoverActionOption_MoveJunk": "Μετακίνηση στα Ανεπιθύμητα", "HoverActionOption_ToggleFlag": "Προσθήκη/ Αφαίρεση σημαίας", "HoverActionOption_ToggleRead": "Αναγνωσμένο / Μη Αναγνωσμένο", + "KeyboardShortcuts_FailedToReset": "Αποτυχία επαναφοράς συντομεύσεων πληκτρολογίου.", + "KeyboardShortcuts_FailedToUpdate": "Αποτυχία ενημέρωσης συντομεύσεων πληκτρολογίου", + "KeyboardShortcuts_MailoperationAction": "Ενέργεια", + "KeyboardShortcuts_Action": "Ενέργεια", + "KeyboardShortcuts_FailedToLoad": "Αποτυχία φόρτωσης συντομεύσεων πληκτρολογίου.", + "KeyboardShortcuts_EnterKeyForShortcut": "Παρακαλώ εισάγετε ένα πλήκτρο για τη συντόμευση.", + "KeyboardShortcuts_SelectOperationForShortcut": "Παρακαλώ επιλέξτε μια ενέργεια για τη συντόμευση.", + "KeyboardShortcuts_EnterKey": "Παρακαλώ εισάγετε ένα πλήκτρο για τη συντόμευση.", + "KeyboardShortcuts_SelectOperation": "Παρακαλώ επιλέξτε μια ενέργεια για τη συντόμευση.", + "KeyboardShortcuts_ShortcutInUse": "Αυτή η συντόμευση χρησιμοποιείται ήδη από άλλη συντόμευση.", + "KeyboardShortcuts_FailedToSave": "Αποτυχία αποθήκευσης της συντόμευσης.", + "KeyboardShortcuts_FailedToDelete": "Αποτυχία διαγραφής της συντόμευσης.", + "KeyboardShortcuts_PageDescription": "Ρύθμιση συντομεύσεων πληκτρολογίου για γρήγορες ενέργειες ηλεκτρονικού ταχυδρομείου. Πατήστε πλήκτρα όταν το πεδίο εισαγωγής πλήκτρων είναι επιλεγμένο για να καταγραφούν οι συντομεύσεις.", + "KeyboardShortcuts_Add": "Προσθήκη συντόμευσης", + "KeyboardShortcuts_EditTitle": "Επεξεργασία συντόμευσης πληκτρολογίου", + "KeyboardShortcuts_ResetToDefaults": "Επαναφορά στις προεπιλεγμένες ρυθμίσεις", + "KeyboardShortcuts_PressKeysHere": "Πατήστε τα πλήκτρα εδώ...", + "KeyboardShortcuts_KeyCombination": "Συνδυασμός πλήκτρων", + "KeyboardShortcuts_FocusArea": "Εστιάστε στο πεδίο από πάνω και πατήστε τον επιθυμητό συνδυασμό πλήκτρων", + "KeyboardShortcuts_Modifiers": "Πλήκτρα τροποποίησης", + "KeyboardShortcuts_Mode": "Λειτουργία", + "KeyboardShortcuts_ModeMail": "Αλληλογραφία", + "KeyboardShortcuts_ModeCalendar": "Ημερολόγιο", + "KeyboardShortcuts_ActionToggleReadUnread": "Εναλλαγή ανάγνωσης/μη ανάγνωσης", + "KeyboardShortcuts_ActionToggleFlag": "Εναλλαγή σημαίας", + "KeyboardShortcuts_ActionToggleArchive": "Εναλλαγή αρχειοθέτησης/μη αρχειοθέτησης", "ImageRenderingDisabled": "Η αποτύπωση εικόνων είναι απενεργοποιημένη για αυτό το μήνυμα.", "ImapAdvancedSetupDialog_AuthenticationMethod": "Μέθοδος επαλήθευσης", "ImapAdvancedSetupDialog_ConnectionSecurity": "Ασφάλεια σύνδεσης", @@ -295,12 +463,58 @@ "IMAPSetupDialog_Username": "Όνομα χρήστη", "IMAPSetupDialog_UsernamePlaceholder": "johndoe, johndoe@fabrikam.com, domain/johndoe", "IMAPSetupDialog_UseSameConfig": "Χρησιμοποιήστε το ίδιο όνομα χρήστη και κωδικό πρόσβασης για την αποστολή email", + "ImapCalDavSettingsPage_TitleCreate": "Ρύθμιση IMAP και Ημερολογίου", + "ImapCalDavSettingsPage_TitleEdit": "Επεξεργασία ρυθμίσεων IMAP και Ημερολογίου", + "ImapCalDavSettingsPage_Subtitle": "Ρυθμίστε το IMAP/SMTP και προαιρετικό συγχρονισμό ημερολογίου για αυτόν τον λογαριασμό.", + "ImapCalDavSettingsPage_BasicSectionTitle": "Βασική ρύθμιση", + "ImapCalDavSettingsPage_BasicSectionDescription": "Πληκτρολογήστε την ταυτότητα και τα διαπιστευτήριά σας. Το Wino μπορεί να προσπαθήσει να εντοπίσει αυτόματα τις ρυθμίσεις διακομιστή.", + "ImapCalDavSettingsPage_BasicTab": "Βασικό", + "ImapCalDavSettingsPage_EnableCalendarSupport": "Ενεργοποίηση υποστήριξης ημερολογίου", + "ImapCalDavSettingsPage_AutoDiscoverButton": "Αυτόματη εύρεση ρυθμίσεων αλληλογραφίας", + "ImapCalDavSettingsPage_AutoDiscoverySuccessMessage": "Οι ρυθμίσεις αλληλογραφίας εντοπίστηκαν και εφαρμόστηκαν.", + "ImapCalDavSettingsPage_AdvancedSectionTitle": "Προχωρημένες ρυθμίσεις", + "ImapCalDavSettingsPage_AdvancedSectionDescription": "Εισάγετε ρυθμίσεις διακομιστή χειροκίνητα εάν η αυτόματη εύρεση είναι μη διαθέσιμη ή εσφαλμένη.", + "ImapCalDavSettingsPage_AdvancedTab": "Προχωρημένες", + "ImapCalDavSettingsPage_CalendarSectionTitle": "Ρύθμιση ημερολογίου", + "ImapCalDavSettingsPage_CalendarSectionDescription": "Επιλέξτε πώς τα δεδομένα ημερολογίου θα λειτουργούν για αυτόν τον λογαριασμό IMAP.", + "ImapCalDavSettingsPage_CalendarModeHeader": "Λειτουργία ημερολογίου", + "ImapCalDavSettingsPage_ConnectionSecurityHeader": "Ασφάλεια σύνδεσης", + "ImapCalDavSettingsPage_AuthenticationMethodHeader": "Μέθοδος αυθεντικοποίησης", + "ImapCalDavSettingsPage_CalendarModeDisabled": "Απενεργοποιημένη", + "ImapCalDavSettingsPage_CalendarModeCalDav": "Συγχρονισμός CalDAV", + "ImapCalDavSettingsPage_CalendarModeLocalOnly": "Μόνο το τοπικό ημερολόγιο", + "ImapCalDavSettingsPage_CalendarModeDisabledDescription": "Το ημερολόγιο είναι απενεργοποιημένο γι' αυτόν τον λογαριασμό.", + "ImapCalDavSettingsPage_CalendarModeCalDavDescription": "Τα στοιχεία ημερολογίου συγχρονίζονται με τον διακομιστή CalDAV σας.", + "ImapCalDavSettingsPage_CalendarModeLocalOnlyDescription": "Τα στοιχεία ημερολογίου αποθηκεύονται μόνο σε αυτόν τον υπολογιστή και δεν συγχρονίζονται με το δίκτυο.", + "ImapCalDavSettingsPage_LocalCalendarLearnMore": "Πώς λειτουργεί το τοπικό ημερολόγιο", + "ImapCalDavSettingsPage_LocalCalendarDialogTitle": "Μόνο το τοπικό ημερολόγιο", + "ImapCalDavSettingsPage_LocalCalendarDialogMessage": "Το τοπικό ημερολόγιο διατηρεί όλα τα γεγονότα μόνο στον υπολογιστή σας. Τίποτα δεν συγχρονίζεται με το iCloud, Yahoo ή άλλο πάροχο.", + "ImapCalDavSettingsPage_CalDavServiceUrl": "Διεύθυνση URL υπηρεσίας CalDAV", + "ImapCalDavSettingsPage_CalDavUsername": "Όνομα χρήστη CalDAV", + "ImapCalDavSettingsPage_CalDavPassword": "Κωδικός CalDAV", + "ImapCalDavSettingsPage_CalDavNotRequiredMessage": "Η δοκιμή CalDAV απαιτείται μόνο όταν η λειτουργία ημερολογίου έχει οριστεί σε CalDAV συγχρονισμό.", + "ImapCalDavSettingsPage_CalDavUrlRequired": "Απαιτείται διεύθυνση URL υπηρεσίας CalDAV.", + "ImapCalDavSettingsPage_CalDavUrlInvalid": "Η διεύθυνση URL της υπηρεσίας CalDAV πρέπει να είναι απόλυτο URL.", + "ImapCalDavSettingsPage_CalDavUsernameRequired": "Απαιτείται όνομα χρήστη CalDAV.", + "ImapCalDavSettingsPage_CalDavPasswordRequired": "Απαιτείται κωδικός πρόσβασης CalDAV.", + "ImapCalDavSettingsPage_TestImapButton": "Δοκιμή σύνδεσης IMAP", + "ImapCalDavSettingsPage_TestCalDavButton": "Δοκιμή σύνδεσης CalDAV", + "ImapCalDavSettingsPage_ImapTestSuccessMessage": "Η δοκιμή σύνδεσης IMAP ολοκληρώθηκε με επιτυχία.", + "ImapCalDavSettingsPage_CalDavTestSuccessMessage": "Η δοκιμή σύνδεσης CalDAV ολοκληρώθηκε με επιτυχία.", + "ImapCalDavSettingsPage_SaveSuccessMessage": "Οι ρυθμίσεις λογαριασμού επαληθεύτηκαν και αποθηκεύτηκαν.", + "ImapCalDavSettingsPage_ICloudHint": "Χρησιμοποιήστε έναν κωδικό πρόσβασης εφαρμογής που παράγεται από τις ρυθμίσεις λογαριασμού Apple.", + "ImapCalDavSettingsPage_YahooHint": "Χρησιμοποιήστε έναν κωδικό πρόσβασης εφαρμογής από τις ρυθμίσεις ασφάλειας του λογαριασμού σας Yahoo.", "Info_AccountCreatedMessage": "Ο {0} δημιουργήθηκε", "Info_AccountCreatedTitle": "Δημιουργία Λογαριασμού", "Info_AccountCreationFailedTitle": "Αποτυχία Δημιουργίας Λογαριασμού", "Info_AccountDeletedMessage": "Ο {0} διαγράφηκε επιτυχώς.", "Info_AccountDeletedTitle": "Ο Λογαριασμός Διαγράφηκε", "Info_AccountIssueFixFailedTitle": "Αποτυχία", + "Info_AccountIssueFixImapMessage": "Ανοίξτε τη σελίδα ρυθμίσεων IMAP και ημερολογίου για να εισαγάγετε ξανά τα διαπιστευτήριά σας στον διακομιστή.", + "Info_AccountAttentionRequiredMessage": "Αυτός ο λογαριασμός χρειάζεται την προσοχή σας.", + "Info_AccountAttentionRequiredClickableMessage": "Κάντε κλικ για να διορθώσετε αυτόν τον λογαριασμό και να τον επανασυγχρονίσετε.", + "Info_AccountAttentionRequiredAction": "Διόρθωση", + "Info_AccountAttentionRequiredActionHint": "Κάντε κλικ στη Διόρθωση για να επιλύσετε αυτό το ζήτημα του λογαριασμού.", "Info_AccountIssueFixSuccessMessage": "Επιδιορθώθηκαν όλα τα ζητήματα λογαριασμού.", "Info_AccountIssueFixSuccessTitle": "Επιτυχία", "Info_AttachmentOpenFailedMessage": "Αδυναμία ανοίγματος αυτού του συνημμένου.", @@ -370,6 +584,7 @@ "InfoBarMessage_SynchronizationDisabledFolder": "Αυτός ο φάκελος είναι απενεργοποιημένος για συγχρονισμό.", "InfoBarTitle_SynchronizationDisabledFolder": "Απενεργοποιημένος Φάκελος", "Justify": "Στοίχιση", + "MenuUpdateAvailable": "Διαθέσιμη ενημέρωση", "Left": "Αριστερά", "Link": "Σύνδεσμος", "LinkedAccountsCreatePolicyMessage": "πρέπει να έχετε τουλάχιστον 2 λογαριασμούς για να δημιουργήσετε σύνδεσμο\nο σύνδεσμος θα αφαιρεθεί κατά την αποθήκευση", @@ -403,6 +618,7 @@ "MailOperation_Unarchive": "Αφαίρεση από το αρχείο", "MailOperation_ViewMessageSource": "Προβολή προέλευσης μηνύματος", "MailOperation_Zoom": "Μεγέθυνση", + "MailsDragging": "Μετακίνηση {0} αντικειμένου(ων)", "MailsSelected": "Επιλέχθηκαν {0} αντικείμενα/-ο", "MarkFlagUnflag": "Σήμανση ως με/χωρίς επισήμανση", "MarkReadUnread": "Σήμανση ως αναγνωσμένο/μη αναγνωσμένο", @@ -434,6 +650,8 @@ "Notifications_MultipleNotificationsTitle": "Νέα Αλληλογραφία", "Notifications_WinoUpdatedMessage": "Ρίξτε μια ματιά στη νέα έκδοση {0}", "Notifications_WinoUpdatedTitle": "Το Wino Mail έχει ενημερωθεί.", + "Notifications_StoreUpdateAvailableTitle": "Διαθέσιμη ενημέρωση", + "Notifications_StoreUpdateAvailableMessage": "Μια νεότερη έκδοση του Wino Mail είναι έτοιμη για εγκατάσταση από το Microsoft Store.", "OnlineSearchFailed_Message": "Αποτυχία εκτέλεσης αναζήτησης\n{0}\n\nΕμφάνιση μηνυμάτων εκτός σύνδεσης.", "OnlineSearchTry_Line1": "Δεν μπορείτε να βρείτε αυτό που ψάχνετε;", "OnlineSearchTry_Line2": "Δοκιμάστε την online αναζήτηση.", @@ -446,7 +664,6 @@ "PaneLengthOption_Small": "Μικρό", "Photos": "Φωτογραφίες", "PreparingFoldersMessage": "Προετοιμασία φακέλων", - "ProtocolLogAvailable_Message": "Τα αρχεία καταγραφής πρωτοκόλλου είναι διαθέσιμα για διαγνωστικά.", "ProviderDetail_Gmail_Description": "Λογαριασμός Google", "ProviderDetail_iCloud_Description": "Λογαριασμός Apple iCloud", "ProviderDetail_iCloud_Title": "iCloud", @@ -465,9 +682,14 @@ "SearchBarPlaceholder": "Αναζήτηση", "SearchingIn": "Αναζήτηση σε", "SearchPivotName": "Αποτελέσματα", + "Settings_KeyboardShortcuts_Title": "Συντομεύσεις πληκτρολόγιου", + "Settings_KeyboardShortcuts_Description": "Διαχείριση συντομεύσεων πληκτρολογίου για γρήγορες ενέργειες στα μηνύματα.", "SettingConfigureSpecialFolders_Button": "Διαμόρφωση", "SettingsEditAccountDetails_IMAPConfiguration_Title": "Διαμόρφωση IMAP/SMTP", "SettingsEditAccountDetails_IMAPConfiguration_Description": "Αλλάξτε τις ρυθμίσεις του διακομιστή εισερχομένων/εξερχομένων σας.", + "SettingsEditAccountDetails_ImapCalDavSettings_Title": "Ρυθμίσεις IMAP και ημερολογίου", + "SettingsEditAccountDetails_ImapCalDavSettings_Description": "Ανοίξτε τη σελίδα ρυθμίσεων IMAP, SMTP και CalDAV για αυτόν τον λογαριασμό.", + "SettingsEditAccountDetails_ImapCalDavSettings_Action": "Άνοιγμα ρυθμίσεων", "SettingsAbout_Description": "Μάθετε περισσότερα για το Wino.", "SettingsAbout_Title": "Σχετικά", "SettingsAboutGithub_Description": "Μεταβείτε στο αποθετήριο GitHub για τον ανιχνευτή προβλημάτων.", @@ -490,6 +712,10 @@ "SettingsAppPreferences_SearchMode_Local": "Τοπικό", "SettingsAppPreferences_SearchMode_Online": "Online", "SettingsAppPreferences_SearchMode_Title": "Προεπιλεγμένη λειτουργία αναζήτησης", + "SettingsAppPreferences_ApplicationMode_Title": "Προεπιλεγμένη λειτουργία εφαρμογής", + "SettingsAppPreferences_ApplicationMode_Description": "Επιλέξτε σε ποια λειτουργία ανοίγει το Wino όταν δεν έχει οριστεί ρητά τύπος ενεργοποίησης.", + "SettingsAppPreferences_ApplicationMode_Mail": "Αλληλογραφία", + "SettingsAppPreferences_ApplicationMode_Calendar": "Ημερολόγιο", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description": "Το Wino Mail θα συνεχίσει να εκτελείται στο παρασκήνιο. Θα ειδοποιηθείτε καθώς φτάνουν νέα μηνύματα.", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title": "Εκτέλεση στο παρασκήνιο", "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description": "Το Wino Mail θα συνεχίσει να εκτελείται στο συαρτάρι συστήματος. Διαθέσιμο για εκκίνηση κάνοντας κλικ σε ένα εικονίδιο. Θα ειδοποιηθείτε καθώς φτάνουν νέα μηνύματα.", @@ -506,12 +732,30 @@ "SettingsAppPreferences_StartupBehavior_FatalError": "Μοιραίο σφάλμα προέκυψε κατά την αλλαγή της λειτουργίας εκκίνησης για το Wino Mail.", "SettingsAppPreferences_StartupBehavior_Title": "Εκκίνηση σε ελαχιστοποίηση κατά την έναρξη των Windows", "SettingsAppPreferences_Title": "Προτιμήσεις Εφαρμογής", + "SettingsAppPreferences_HideWinoAccountButton_Title": "Απόκρυψη του κουμπιού λογαριασμού Wino στη γραμμή τίτλου", + "SettingsAppPreferences_HideWinoAccountButton_Description": "Απόκρυψη του κουμπιού προφίλ στη γραμμή τίτλου που ανοίγει το μενού λογαριασμού Wino.", + "SettingsAppPreferences_StoreUpdateNotifications_Title": "Ειδοποιήσεις ενημερώσεων Microsoft Store", + "SettingsAppPreferences_StoreUpdateNotifications_Description": "Εμφάνιση ειδοποιήσεων και ενεργειών στο υποσέλιδο όταν υπάρχει διαθέσιμη ενημέρωση του Microsoft Store.", + "SettingsAppPreferences_AiActions_Title": "Ενέργειες AI", + "SettingsAppPreferences_AiActions_Description": "Επιλέξτε προεπιλεγμένες γλώσσες AI και πού θα αποθηκεύονται οι περιλήψεις.", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Title": "Προεπιλεγμένη γλώσσα μετάφρασης", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Description": "Επιλέξτε την προεπιλεγμένη γλώσσα-στόχο που χρησιμοποιούν οι ενέργειες μετάφρασης AI.", + "SettingsAppPreferences_AiSummarizeLanguage_Title": "Γλώσσα περίληψης", + "SettingsAppPreferences_AiSummarizeLanguage_Description": "Επιλέξτε την προτιμώμενη γλώσσα περίληψης για μελλοντικά αποτελέσματα περίληψης AI.", + "SettingsAppPreferences_AiSummarySavePath_Title": "Προεπιλεγμένη διαδρομή αποθήκευσης περιλήψεων", + "SettingsAppPreferences_AiSummarySavePath_Description": "Επιλέξτε τον φάκελο που θα χρησιμοποιεί το Wino προεπιλεγμένα όταν αποθηκεύει AI περιλήψεις.", + "SettingsAppPreferences_AiSummarySavePath_Placeholder": "Χρησιμοποιήστε την προκαθορισμένη τοποθεσία αποθήκευσης του συστήματος", + "SettingsAppPreferences_AiSummarySavePath_InvalidHint": "Αυτός ο φάκελος δεν υπάρχει. Θα χρησιμοποιηθεί η προεπιλεγμένη τοποθεσία αποθήκευσης για τις περιλήψεις.", "SettingsAutoSelectNextItem_Description": "Επιλέξτε το επόμενο στοιχείο μετά τη διαγραφή ή τη μετακίνηση ενός μηνύματος.", "SettingsAutoSelectNextItem_Title": "Αυτόματη επιλογή επόμενου αντικειμένου", "SettingsAvailableThemes_Description": "Επιλέξτε ένα θέμα από τη δική σας συλλογή Wino που σάς αρέσει ή εφαρμόστε τα δικά σας θέματα.", "SettingsAvailableThemes_Title": "Διαθέσιμα Θέματα", "SettingsCalendarSettings_Description": "Αλλαγή της πρώτης ημέρας της εβδομάδας, ύψος κελιού ώρας και περισσότερα...", "SettingsCalendarSettings_Title": "Ρυθμίσεις Ημερολογίου", + "CalendarSettings_DefaultSnoozeDuration_Header": "Προεπιλεγμένη διάρκεια αναβολής υπενθύμισης ημερολογίου", + "CalendarSettings_DefaultSnoozeDuration_Description": "Ορίστε μια προεπιλεγμένη διάρκεια αναβολής υπενθύμισης ημερολογίου.", + "CalendarSettings_TimedDayHeaderFormat_Header": "Μορφή κεφαλίδας ημέρας χρονικής προβολής", + "CalendarSettings_TimedDayHeaderFormat_Description": "Επιλέξτε πώς αποδίδονται οι κορυφαίες ετικέτες ημέρας στις προβολές ημέρας, εβδομάδας και εργάσιμης εβδομάδας. Χρησιμοποιήστε τα tokens μορφής ημερομηνίας όπως ddd, dd, MMM ή dddd.", "SettingsComposer_Title": "Συνθέτης", "SettingsComposerFont_Title": "Προεπιλεγμένη Γραμματοσειρά Συντάκτη", "SettingsComposerFontFamily_Description": "Αλλάξτε την προεπιλεγμένη οικογένεια γραμματοσειρών και το μέγεθος γραμματοσειράς για τη σύνταξη μηνυμάτων.", @@ -531,6 +775,9 @@ "SettingsDiscord_Title": "Κανάλι Discord", "SettingsEditLinkedInbox_Description": "Προσθήκη / κατάργηση λογαριασμών, μετονομασία ή διακοπή του συνδέσμου μεταξύ λογαριασμών.", "SettingsEditLinkedInbox_Title": "Επεξεργασία Συνδεδεμένων Εισερχομένων", + "SettingsWindowBackdrop_Title": "Φόντο παραθύρου", + "SettingsWindowBackdrop_Description": "Επιλέξτε ένα εφέ φόντου για τα παράθυρα του Wino.", + "SettingsWindowBackdrop_Disabled": "Η επιλογή φόντου παραθύρου είναι απενεργοποιημένη όταν το θέμα της εφαρμογής δεν είναι Προεπιλεγμένο.", "SettingsElementTheme_Description": "Επιλέξτε ένα θέμα των Windows για Wino", "SettingsElementTheme_Title": "Θέμα Στοιχείου", "SettingsElementThemeSelectionDisabled": "Η επιλογή θέματος βάσει στοιχείου είναι απενεργοποιημένη όταν το θέμα της εφαρμογής είναι άλλο εκτός του Προεπιλεγμένου.", @@ -581,6 +828,8 @@ "SettingsManageAliases_Title": "Ψευδώνυμα", "SettingsEditAccountDetails_Title": "Επεξεργασία Στοιχείων Λογαριασμού", "SettingsEditAccountDetails_Description": "Αλλάξτε το όνομα λογαριασμού, το όνομα του αποστολέα και ορίστε ένα νέο χρώμα αν θέλετε.", + "EditAccountDetailsPage_SaveSuccess_Title": "Αλλαγές Αποθηκεύτηκαν", + "EditAccountDetailsPage_SaveSuccess_Message": "Τα στοιχεία λογαριασμού σας έχουν ενημερωθεί επιτυχώς.", "SettingsManageLink_Description": "Μετακίνηση αντικειμένων για προσθήκη νέου συνδέσμου ή κατάργηση υπάρχοντος συνδέσμου.", "SettingsManageLink_Title": "Διαχείριση Συνδέσμου", "SettingsMarkAsRead_Description": "Αλλάξτε τι πρέπει να συμβεί στο επιλεγμένο αντικείμενο.", @@ -596,7 +845,41 @@ "SettingsNotifications_Title": "Ειδοποιήσεις", "SettingsNotificationsAndTaskbar_Description": "Αλλάξτε αν θα εμφανίζονται ειδοποιήσεις και σήμα γραμμής εργασιών για αυτόν τον λογαριασμό.", "SettingsNotificationsAndTaskbar_Title": "Ειδοποιήσεις & Γραμμή Εργασιών", + "SettingsHome_Title": "Αρχική", + "SettingsHome_SearchTitle": "Εύρεση ρυθμίσεων", + "SettingsHome_SearchDescription": "Αναζητήστε με λειτουργία, θέμα ή λέξη-κλειδί για να μεταβείτε άμεσα στη σωστή σελίδα ρυθμίσεων.", + "SettingsHome_SearchPlaceholder": "Αναζήτηση ρυθμίσεων", + "SettingsHome_SearchExamples": "Δοκιμάστε: θέμα, αποθήκευση, γλώσσα, υπογραφή", + "SettingsHome_QuickLinks_Title": "Γρήγοροι σύνδεσμοι", + "SettingsHome_QuickLinks_Description": "Μεταβείτε γρήγορα στις ρυθμίσεις που οι άνθρωποι συχνά αναζητούν.", + "SettingsHome_StorageCard_Description": "Δείτε πόσο τοπικό MIME περιεχόμενο διατηρεί το Wino σε αυτή τη συσκευή και καθαρίστε το όταν χρειάζεται.", + "SettingsHome_StorageEmptySummary": "Δεν εντοπίστηκε ακόμη περιεχόμενο MIME στην προσωρινή μνήμη.", + "SettingsHome_StorageLoading": "Ελέγχεται η τοπική χρήση MIME...", + "SettingsHome_Tips_Title": "Συμβουλές & κόλπα", + "SettingsHome_Tips_Description": "Μερικές μικρές αλλαγές μπορούν να κάνουν το Wino πολύ πιο προσωπικό.", + "SettingsHome_Tip_Theme": "Θέλετε σκούρα λειτουργία ή αλλαγές σε έμφαση; Ανοίξτε την Προσωποποίηση.", + "SettingsHome_Tip_Background": "Χρησιμοποιήστε τις Προτιμήσεις Εφαρμογής για να ορίσετε τη συμπεριφορά εκκίνησης και τον συγχρονισμό στο παρασκήνιο.", + "SettingsHome_Tip_Shortcuts": "Οι συντομεύσεις πληκτρολογίου σας βοηθούν να μετακινείστε πιο γρήγορα στα μηνύματα.", + "SettingsHome_Resources_Title": "Χρήσιμοι σύνδεσμοι", + "SettingsHome_Resources_Description": "Άνοιγμα πόρων έργου, πληροφοριών υποστήριξης και καναλιών διάθεσης.", "SettingsOptions_Title": "Ρυθμίσεις", + "SettingsOptions_GeneralSection": "Γενικά", + "SettingsOptions_MailSection": "Αλληλογραφία", + "SettingsOptions_CalendarSection": "Ημερολόγιο", + "SettingsOptions_MoreComingSoon": "Περισσότερες επιλογές σύντομα διαθέσιμες", + "SettingsOptions_HeroDescription": "Διαμορφώστε την εμπειρία Wino Mail", + "SettingsOptions_AccountsSummary": "{0} λογαριασμό(-οι) ρυθμισμένος(-οι)", + "SettingsSearch_ManageAccounts_Keywords": "λογαριασμός;λογαριασμοί;γραμματοκιβώτιο;γραμματοκιβώτια;ψευδώνυμο;ψευδώνυμα;προφίλ;διεύθυνση;διευθύνσεις", + "SettingsSearch_AppPreferences_Keywords": "εκκίνηση;υπόβαθρο;εκκίνηση;συγχρονισμός;ειδοποίηση;ειδοποιήσεις;αναζήτηση;περιοχή ειδοποιήσεων;προεπιλογές", + "SettingsSearch_LanguageTime_Keywords": "γλώσσα;ώρα;ρολόι;τοπικές ρυθμίσεις;περιοχή;μορφή;24 ώρες;24h", + "SettingsSearch_Personalization_Keywords": "θέμα;σκοτεινό;φωτεινό;εμφάνιση;έμφαση;χρωματισμός;λειτουργία;διάταξη;πυκνότητα", + "SettingsSearch_About_Keywords": "περί;έκδοση;ιστοσελίδα;ιδιωτικότητα;github;δωρεά;κατάστημα;υποστήριξη", + "SettingsSearch_KeyboardShortcuts_Keywords": "συντόμευση;συντομεύσεις;hotkey;hotkeys;πληκτρολόγιο;κλειδιά", + "SettingsSearch_MessageList_Keywords": "μήνυμα;μηνύματα;λίστα;νήματα;νήματα;εικόνα προφίλ;προεπισκόπηση;αποστολέας", + "SettingsSearch_ReadComposePane_Keywords": "αναγνώστης;σύνταξη;συνθέτης;γραμματοσειρά;γραμματοσειρές;εξωτερικό περιεχόμενο;εμφάνιση;ανάγνωση", + "SettingsSearch_SignatureAndEncryption_Keywords": "υπογραφή;υπογραφές;κρυπτογράφηση;πιστοποιητικό;πιστοποιητικά;S/MIME;S/MIME;ασφάλεια", + "SettingsSearch_Storage_Keywords": "αποθήκευση;προσωρινή μνήμη;προσωρινή αποθήκευση;MIME περιεχόμενο;δίσκος;χώρος;εκκαθάριση;καθαρισμός;τοπικά δεδομένα", + "SettingsSearch_CalendarSettings_Keywords": "Ημερολόγιο;εβδομάδα;ώρες;πρόγραμμα;γεγονός;γεγονότα", "SettingsPaneLengthReset_Description": "Επαναφέρετε το μέγεθος της λίστας μηνυμάτων στο πρωτότυπο αν έχετε προβλήματα με αυτό.", "SettingsPaneLengthReset_Title": "Επαναφορά Μεγέθους Λίστας Ταχυδρομείου", "SettingsPaypal_Description": "Δείξτε πολύ περισσότερη αγάπη ❤️ Όλες οι δωρεές εκτιμούνται.", @@ -610,6 +893,8 @@ "SettingsPrefer24HourClock_Title": "Εμφάνιση μορφής ρολογιού σε 24ώρο", "SettingsPrivacyPolicy_Description": "Εξέταση πολιτικής απορρήτου.", "SettingsPrivacyPolicy_Title": "Πολιτική Απορρήτου", + "SettingsWebsite_Description": "Ανοίξτε τον ιστότοπο του Wino Mail.", + "SettingsWebsite_Title": "Ιστότοπος", "SettingsReadComposePane_Description": "Γραμματοσειρές, εξωτερικό περιεχόμενο.", "SettingsReadComposePane_Title": "Αναγνώστης & Συνθέτης", "SettingsReader_Title": "Αναγνώστης", @@ -625,6 +910,19 @@ "SettingsShowPreviewText_Title": "Εμφάνιση Κειμένου Προεπισκόπησης", "SettingsShowSenderPictures_Description": "Απόκρυψη/εμφάνιση της μικρογραφίας του αποστολέα.", "SettingsShowSenderPictures_Title": "Εμφάνιση Avatars Αποστολέα", + "SettingsEmailTemplates_Title": "Πρότυπα email", + "SettingsEmailTemplates_Description": "Διαχείριση προτύπων email", + "SettingsEmailTemplates_CreatePageTitle": "Νέο πρότυπο", + "SettingsEmailTemplates_EditPageTitle": "Επεξεργασία προτύπου", + "SettingsEmailTemplates_NewTemplateTitle": "Νέο πρότυπο", + "SettingsEmailTemplates_NewTemplateDescription": "Δημιουργία νέου προτύπου email", + "SettingsEmailTemplates_NameTitle": "Όνομα", + "SettingsEmailTemplates_NamePlaceholder": "Όνομα προτύπου", + "SettingsEmailTemplates_DescriptionTitle": "Περιγραφή", + "SettingsEmailTemplates_DescriptionPlaceholder": "Προαιρετική περιγραφή", + "SettingsEmailTemplates_ContentTitle": "Περιεχόμενο προτύπου", + "SettingsEmailTemplates_ContentDescription": "Επεξεργασία του περιεχομένου HTML για αυτό το πρότυπο.", + "SettingsEmailTemplates_NameRequired": "Απαιτείται το όνομα του προτύπου.", "SettingsEnableGravatarAvatars_Title": "Gravatar", "SettingsEnableGravatarAvatars_Description": "Use gravatar (if available) as sender picture", "SettingsEnableFavicons_Title": "Domain icons (Favicons)", @@ -645,6 +943,33 @@ "SettingsStartupItem_Title": "Αντικείμενο εκκίνησης", "SettingsStore_Description": "Δείξτε λίγη αγάπη ❤️", "SettingsStore_Title": "Αξιολόγηση στο Κατάστημα", + "SettingsStorage_Title": "Αποθήκευση", + "SettingsStorage_Description": "Σάρωση και διαχείριση της προσωρινής μνήμης MIME που αποθηκεύεται στον τοπικό φάκελο δεδομένων.", + "SettingsStorage_ScanFolder": "Σάρωση τοπικού φακέλου δεδομένων", + "SettingsStorage_NoLocalMimeDataFound": "Δεν βρέθηκαν τοπικά δεδομένα MIME.", + "SettingsStorage_NoAccountsFound": "Δεν βρέθηκαν λογαριασμοί.", + "SettingsStorage_TotalUsage": "Συνολική τοπική χρήση MIME: {0}", + "SettingsStorage_AccountUsageDescription": "{0} χρησιμοποιείται στην τοπική μνήμη cache MIME", + "SettingsStorage_DeleteAll_Title": "Διαγραφή όλου του περιεχομένου MIME", + "SettingsStorage_DeleteAll_Description": "Διαγραφή ολόκληρου του φακέλου προσωρινής μνήμης MIME αυτού του λογαριασμού.", + "SettingsStorage_DeleteAll_Button": "Διαγραφή όλων", + "SettingsStorage_DeleteAll_Confirm_Title": "Διαγραφή όλου του περιεχομένου MIME", + "SettingsStorage_DeleteAll_Confirm_Message": "Να διαγραφούν όλα τα τοπικά δεδομένα MIME για {0} ?", + "SettingsStorage_DeleteAll_Success": "Όλο το περιεχόμενο MIME διαγράφηκε.", + "SettingsStorage_DeleteOld_Title": "Διαγραφή παλιότερου περιεχομένου MIME", + "SettingsStorage_DeleteOld_Description": "Διαγραφή αρχείων MIME με βάση την ημερομηνία δημιουργίας μηνύματος στη τοπική βάση δεδομένων.", + "SettingsStorage_DeleteOld_1Month": "> 1 μήνας", + "SettingsStorage_DeleteOld_3Months": "> 3 μήνες", + "SettingsStorage_DeleteOld_6Months": "> 6 μήνες", + "SettingsStorage_DeleteOld_1Year": "> 1 έτος", + "SettingsStorage_DeleteOld_Confirm_Title": "Διαγραφή παλιότερου περιεχομένου MIME", + "SettingsStorage_DeleteOld_Confirm_Message": "Να διαγραφούν τα τοπικά δεδομένα MIME που είναι παλιότερα από {0} για {1} ?", + "SettingsStorage_DeleteOld_Success": "Διαγράφηκαν {0} φάκελος/φάκελοι MIME που είναι παλιότεροι από {1}.", + "SettingsStorage_1Month": "1 μήνας", + "SettingsStorage_3Months": "3 μήνες", + "SettingsStorage_6Months": "6 μήνες", + "SettingsStorage_1Year": "1 έτος", + "SettingsStorage_Months": "{0} μήνες", "SettingsTaskbarBadge_Description": "Συμπερίληψη μη αναγνωσμένου αριθμού αλληλογραφίας στο εικονίδιο της γραμμής εργασιών.", "SettingsTaskbarBadge_Title": "Σήμα Γραμμής Εργασιών", "SettingsThreads_Description": "Οργάνωση μηνυμάτων σε νήματα συνομιλίας.", @@ -683,6 +1008,9 @@ "SystemFolderConfigDialogValidation_InboxSelected": "Δεν μπορείτε να αντιστοιχίσετε τον φάκελο Εισερχομένων σε οποιονδήποτε άλλο φάκελο συστήματος.", "SystemFolderConfigSetupSuccess_Message": "Οι φάκελοι συστήματος έχουν ρυθμιστεί επιτυχώς.", "SystemFolderConfigSetupSuccess_Title": "Ρύθμιση Φακέλων Συστήματος", + "SystemTrayMenu_ShowWino": "Άνοιγμα Wino Mail", + "SystemTrayMenu_ShowWinoCalendar": "Άνοιγμα Wino Ημερολογίου", + "SystemTrayMenu_ExitWino": "Έξοδος", "TestingImapConnectionMessage": "Έλεγχος σύνδεσης διακομιστή...", "TitleBarServerDisconnectedButton_Description": "Το Wino αποσυνδέθηκε από το δίκτυο. Κάντε κλικ στην επιλογή επανασύνδεση για επαναφορά της σύνδεσης.", "TitleBarServerDisconnectedButton_Title": "χωρίς σύνδεση", @@ -699,8 +1027,422 @@ "WinoUpgradeMessage": "Αναβάθμιση σε Απεριόριστους Λογαριασμούς", "WinoUpgradeRemainingAccountsMessage": "Χρησιμοποιούνται {0} από τους {1} δωρεάν λογαριασμούς.", "Yesterday": "Χθες", + "Smime_ImportCertificates_Success": "Τα πιστοποιητικά εισήχθησαν επιτυχώς.", + "Smime_ImportCertificates_Error": "Σφάλμα εισαγωγής πιστοποιητικών: {0}", + "Smime_RemoveCertificates_Confirm": "Θέλετε πραγματικά να αφαιρέσετε τα πιστοποιητικά {0} ?", + "Smime_RemoveCertificates_Success": "Τα πιστοποιητικά αφαιρέθηκαν.", + "Smime_ExportCertificates_Success": "Τα πιστοποιητικά εξήχθησαν.", + "Smime_ExportCertificates_Error": "Σφάλμα εξαγωγής πιστοποιητικών.", + "Smime_CertificateDetails": "Θέμα: {0}\nΕκδότης: {1}\nΙσχύει από: {2}\nΙσχύει έως: {3}\nΔακτυλικό αποτύπωμα: {4}", + "Smime_CertificatePassword_Title": "Απαιτείται κωδικός πρόσβασης πιστοποιητικού", + "Smime_CertificatePassword_Placeholder": "Κωδικός πρόσβασης πιστοποιητικού για {0} (προαιρετικό)", + "Smime_Confirm_Title": "Επιβεβαίωση", + "Buttons_OK": "ΟΚ", + "Buttons_Refresh": "Ανανέωση", + "SettingsSignatureAndEncryption_Title": "Υπογραφή και Κρυπτογράφηση", + "SettingsSignatureAndEncryption_Description": "Διαχείριση πιστοποιητικών S/MIME για την υπογραφή και την κρυπτογράφηση email.", + "SettingsSignatureAndEncryption_MyCertificatesHeader": "Τα πιστοποιητικά μου", + "SettingsSignatureAndEncryption_MyCertificatesDescription": "Προσωπικά πιστοποιητικά για υπογραφή και κρυπτογράφηση", + "SettingsSignatureAndEncryption_RecipientCertificatesHeader": "Πιστοποιητικά αποδέκτη", + "SettingsSignatureAndEncryption_RecipientCertificatesDescription": "Πιστοποιητικά αποδέκτη για αποκρυπτογράφηση", + "SettingsSignatureAndEncryption_NameColumn": "Όνομα", + "SettingsSignatureAndEncryption_ExpiresColumn": "Λήγει στις", + "SettingsSignatureAndEncryption_ThumbprintColumn": "Δακτυλικό αποτύπωμα", + "Buttons_Remove": "Αφαίρεση", + "Buttons_Export": "Εξαγωγή", + "Buttons_Import": "Εισαγωγή", + "SettingsSignatureAndEncryption_SigningCertificate": "Πιστοποιητικό Υπογραφής S/MIME", + "SettingsSignatureAndEncryption_EncryptionCertificate": "Πιστοποιητικό Κρυπτογράφησης S/MIME", + "SettingsSignatureAndEncryption_SigningCertificatePlaceholder": "Κανένα", + "SmimeSignaturesInMessage": "Υπογραφές σε αυτό το μήνυμα:", + "SmimeSignatureEntry": "• {0} {1} ({2}, έγκυρο έως {3} - {4})", + "SmimeSigningCertificateInfoTitle": "Πληροφορίες πιστοποιητικού Υπογραφής S/MIME", + "SmimeCertificateInfoTitle": "Πληροφορίες πιστοποιητικού S/MIME", + "SmimeNoCertificateFileFound": "Δεν βρέθηκε αρχείο πιστοποιητικού", + "SmimeSaveCertificate": "Αποθήκευση πιστοποιητικού...", + "SmimeCertificate": "Πιστοποιητικό S/MIME", + "SmimeCertificateSavedTo": "Το πιστοποιητικό αποθηκεύτηκε στο {0}", + "SmimeSignedTooltip": "Αυτό το μήνυμα έχει υπογραφεί με πιστοποιητικό S/MIME. Κάντε κλικ για περισσότερες λεπτομέρειες", + "SmimeEncryptedTooltip": "Αυτό το μήνυμα είναι κρυπτογραφημένο με πιστοποιητικό S/MIME.", + "SmimeCertificateFileInfo": "Αρχείο: {0}", + "Composer_LightTheme": "Φωτεινό Θέμα", + "Composer_DarkTheme": "Σκοτεινό Θέμα", + "Composer_Outdent": "Αφαίρεση εσοχής", + "Composer_Indent": "Εσοχή", + "Composer_BulletList": "Λίστα κουκκίδων", + "Composer_OrderedList": "Αριθμημένη λίστα", + "Composer_Stroke": "Γραμμή", + "Composer_Bold": "Έντονο", + "Composer_Italic": "Πλάγια", + "Composer_Underline": "Υπογραμμισμένο", + "Composer_CcBcc": "Κοινοποίηση (Cc) & Κρυφή κοινοποίηση (Bcc)", + "Composer_EnableSmimeSignature": "Ενεργοποίηση/ απενεργοποίηση υπογραφής S/MIME", + "Composer_EnableSmimeEncryption": "Ενεργοποίηση/ απενεργοποίηση κρυπτογράφησης S/MIME", + "Composer_LocalDraftSyncInfo": "Αυτό το προσχέδιο είναι τοπικά μόνο. Ο Wino απέτυχε να το στείλει στον διακομιστή ηλεκτρονικού ταχυδρομείου σας. Κάντε κλικ για επανάληψη αποστολής στον διακομιστή.", + "Composer_CertificateExpires": "Λήγει στις: ", + "Composer_SmimeSignature": "Υπογραφή S/MIME", + "Composer_SmimeEncryption": "Κρυπτογράφηση S/MIME", + "Composer_EmailTemplatesPlaceholder": "Πρότυπα ηλεκτρονικού ταχυδρομείου", + "Composer_AiSummarize": "Περίληψη με AI", + "Composer_AiSummarizeDescription": "Εξαγάγετε τα κύρια σημεία, ενέργειες και αποφάσεις από αυτό το email.", + "Composer_AiTranslate": "Μετάφραση με AI", + "Composer_AiActions": "Ενέργειες AI", + "Composer_AiRewrite": "Αναδιατύπωση με AI", + "AiActions_CheckingStatus": "Έλεγχος πρόσβασης AI…", + "AiActions_SignedOutTitle": "Ξεκλειδώστε το Wino AI Pack", + "AiActions_SignedOutDescription": "Μεταφράστε, επαναγράψτε και συνοψίστε τα email με τη χρήση AI αφού συνδεθείτε στον λογαριασμό σας Wino και ενεργοποιήσετε το πρόσθετο AI Pack.", + "AiActions_NoPackTitle": "Απαιτείται το AI Pack", + "AiActions_NoPackDescription": "Έχετε συνδεθεί, αλλά το AI Pack δεν είναι ενεργό ακόμα. Αγοράστε το για να χρησιμοποιήσετε τα εργαλεία μετάφρασης AI, επαναδιατύπωσης και περίληψης του Wino.", + "AiActions_UsageSummary": "{0} από {1} πιστώσεις χρησιμοποιήθηκαν αυτό το μήνα.", + "Composer_AiRewritePolite": "Κάντε το ευγενικό", + "Composer_AiRewritePoliteDescription": "Μαλακώνει τη διατύπωση διατηρώντας τον ίδιο σκοπό.", + "Composer_AiRewriteAngry": "Κάντε το να φανεί οργισμένο", + "Composer_AiRewriteAngryDescription": "Χρησιμοποιεί έναν πιο αιχμηρό και επιθετικό τόνο.", + "Composer_AiRewriteHappy": "Κάνε το χαρούμενο", + "Composer_AiRewriteHappyDescription": "Προσθέτει πιο ζωντανό και ενθουσιώδες τόνο.", + "Composer_AiRewriteFormal": "Κάνε το πιο επίσημο", + "Composer_AiRewriteFormalDescription": "Κάνει το μήνυμα να ακούγεται πιο επαγγελματικό και δομημένο.", + "Composer_AiRewriteFriendly": "Κάνε το πιο φιλικό", + "Composer_AiRewriteFriendlyDescription": "Ζεσταίνει το μήνυμα με έναν πιο προσιτός τόνο.", + "Composer_AiRewriteShorter": "Κάνε το πιο σύντομο", + "Composer_AiRewriteShorterDescription": "Συμπυκνώνει το κείμενο και αφαιρεί περιττές λεπτομέρειες.", + "Composer_AiRewriteClearer": "Κάνε το σαφέστερο", + "Composer_AiRewriteClearerDescription": "Βελτιώνει την αναγνωσιμότητα και κάνει το μήνυμα ευκολότερο να ακολουθηθεί.", + "Composer_AiRewriteCustom": "Προσαρμοσμένο", + "Composer_AiRewriteCustomDescription": "Περιγράψτε τη δική σας πρόθεση επαναδιατύπωσης.", + "Composer_AiRewriteCustomPlaceholder": "Περιγράψτε πώς θέλετε το μήνυμα να ξαναγραφεί", + "Composer_AiRewriteMode": "Τόνος επαναδιατύπωσης", + "Composer_AiRewriteApply": "Εφαρμογή επαναδιατύπωσης", + "Composer_AiTranslateDialogTitle": "Μετάφραση με AI", + "Composer_AiTranslateDialogDescription": "Εισάγετε τη γλώσσα-στόχο ή τον κωδικό κουλτούρας, π.χ. en-US, tr-TR, de-DE ή fr-FR.", + "Composer_AiTranslateApply": "Μετάφρασε", + "Composer_AiTranslateLanguage": "Γλώσσα-στόχος", + "Composer_AiTranslateCustomPlaceholder": "Εισάγετε κωδικό κουλτούρας", + "Composer_AiTranslateLanguageEnglish": "Αγγλικά (en-US)", + "Composer_AiTranslateLanguageTurkish": "Τουρκικά (tr-TR)", + "Composer_AiTranslateLanguageGerman": "Γερμανικά (de-DE)", + "Composer_AiTranslateLanguageFrench": "Γαλλικά (fr-FR)", + "Composer_AiTranslateLanguageSpanish": "Ισπανικά (es-ES)", + "Composer_AiTranslateLanguageItalian": "Ιταλικά (it-IT)", + "Composer_AiTranslateLanguagePortugueseBrazil": "Πορτογαλικά (Βραζιλίας) (pt-BR)", + "Composer_AiTranslateLanguageDutch": "Ολλανδικά (nl-NL)", + "Composer_AiTranslateLanguagePolish": "Πολωνικά (pl-PL)", + "Composer_AiTranslateLanguageRussian": "Ρωσικά (ru-RU)", + "Composer_AiTranslateLanguageJapanese": "Ιαπωνικά (ja-JP)", + "Composer_AiTranslateLanguageKorean": "Κορεάτικά (ko-KR)", + "Composer_AiTranslateLanguageChineseSimplified": "Κινέζικα, Απλοποιημένα (zh-CN)", + "Composer_AiTranslateLanguageArabic": "Αραβικά (ar-SA)", + "Composer_AiTranslateLanguageHindi": "Χίντι (hi-IN)", + "Composer_AiTranslateLanguageOther": "Άλλο...", + "Composer_AiBusyTitle": "AI is already working", + "Composer_AiBusyMessage": "Please wait for the current AI action to finish.", + "Composer_AiSignInRequired": "Sign in to your Wino Account to use AI features.", + "Composer_AiMissingHtml": "There is no message content to send to Wino AI yet.", + "Composer_AiQuotaUnavailable": "The AI result was applied.", + "Composer_AiAppliedMessage": "The AI result was applied to the composer. Use Undo if you want to revert it.", + "Composer_AiSummarizeSuccessTitle": "AI summary applied", + "Composer_AiTranslateSuccessTitle": "AI translation applied", + "Composer_AiRewriteSuccessTitle": "AI rewrite applied", + "Composer_AiErrorTitle": "AI action failed", + "Reader_AiAppliedMessage": "The AI result is now shown for this message. Reopen the message to view the original content again.", "SettingsAppPreferences_EmailSyncInterval_Title": "Email sync interval", - "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail." + "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail.", + "ContactsPage_Title": "Contacts", + "ContactsPage_AddContact": "Add Contact", + "ContactsPage_EditContact": "Edit Contact", + "ContactsPage_DeleteContact": "Delete Contact", + "ContactsPage_SearchPlaceholder": "Search contacts...", + "ContactsPage_NoContacts": "No contacts found", + "ContactsPage_ContactsCount": "{0} contacts", + "ContactsPage_SelectedContactsCount": "{0} selected", + "ContactsPage_DeleteSelectedContacts": "Delete Selected", + "ContactEditDialog_Title": "Edit Contact", + "ContactEditDialog_PhotoSection": "Photo", + "ContactEditDialog_ChoosePhoto": "Choose Photo", + "ContactEditDialog_RemovePhoto": "Remove Photo", + "ContactEditDialog_NameHeader": "Name", + "ContactEditDialog_NamePlaceholder": "Contact name", + "ContactEditDialog_EmailHeader": "Email Address", + "ContactEditDialog_EmailPlaceholder": "contact@example.com", + "ContactEditDialog_InfoSection": "Contact Information", + "ContactEditDialog_RootContactInfo": "This is a root contact associated with your accounts and cannot be deleted.", + "ContactEditDialog_OverriddenContactInfo": "This contact has been manually modified and will not be updated during synchronization.", + "ContactsPage_Subtitle": "Manage your email contacts and their information", + "ContactStatus_Account": "Account", + "ContactStatus_Modified": "Modified", + "ContactAction_Edit": "Edit contact", + "ContactAction_ChangePhoto": "Change photo", + "ContactAction_Delete": "Delete contact", + "ContactAction_Add": "Add Contact", + "ContactSelection_Selected": "selected", + "ContactSelection_SelectAll": "Select All", + "ContactSelection_Clear": "Clear Selection", + "ContactsPage_EmptyState": "No contacts to display", + "ContactsPage_AddFirstContact": "Add your first contact", + "ContactsPage_ContactsCountSuffix": "contacts", + "ContactsPane_NewContact": "New Contact", + "ContactsPane_DescriptionTitle": "Manage your contacts", + "ContactsPane_DescriptionBody": "Create contacts, rename them, update profile pictures, and keep saved details organized in one place.", + "ContactEditDialog_AddTitle": "Add Contact", + "ContactInfoBar_ContactAdded": "Contact added successfully.", + "ContactInfoBar_ContactUpdated": "Contact updated successfully.", + "ContactInfoBar_ContactsDeleted": "Contacts deleted successfully.", + "ContactInfoBar_ContactPhotoUpdated": "Contact photo updated successfully.", + "ContactInfoBar_FailedToLoadContacts": "Failed to load contacts: {0}", + "ContactInfoBar_FailedToAddContact": "Failed to add contact: {0}", + "ContactInfoBar_FailedToUpdateContact": "Failed to update contact: {0}", + "ContactInfoBar_FailedToDeleteContacts": "Failed to delete contacts: {0}", + "ContactInfoBar_FailedToUpdatePhoto": "Failed to update photo: {0}", + "ContactInfoBar_CannotDeleteRoot": "Root contacts cannot be deleted.", + "ContactConfirmDialog_DeleteTitle": "Delete Contact", + "ContactConfirmDialog_DeleteMessage": "Are you sure you want to delete the contact '{0}'?", + "ContactConfirmDialog_DeleteMultipleMessage": "Are you sure you want to delete {0} contact(s)?", + "ContactConfirmDialog_DeleteButton": "Delete", + "CalendarAccountSettings_Title": "Calendar Account Settings", + "CalendarAccountSettings_Description": "Διαχείριση ρυθμίσεων ημερολογίου για το {0}", + "CalendarAccountSettings_AccountColor": "Χρώμα Λογαριασμού", + "CalendarAccountSettings_AccountColorDescription": "Αλλάξτε το χρώμα προβολής για αυτόν τον λογαριασμό ημερολογίου", + "CalendarAccountSettings_SyncEnabled": "Ενεργοποίηση Συγχρονισμού", + "CalendarAccountSettings_SyncEnabledDescription": "Ενεργοποιήστε ή απενεργοποιήστε τον συγχρονισμό ημερολογίου για αυτόν τον λογαριασμό", + "CalendarAccountSettings_DefaultShowAs": "Προεπιλεγμένη Κατάσταση Διαθεσιμότητας", + "CalendarAccountSettings_DefaultShowAsDescription": "Προεπιλεγμένη κατάσταση διαθεσιμότητας για νέα γεγονότα που δημιουργούνται με αυτόν τον λογαριασμό.", + "CalendarAccountSettings_PrimaryCalendar": "Κύριο Ημερολόγιο", + "CalendarAccountSettings_PrimaryCalendarDescription": "Ορίστε αυτό το ημερολόγιο ως κύριο ημερολόγιο για τον λογαριασμό.", + "CalendarSettings_NewEventBehavior_Header": "Συμπεριφορά του Κουμπιού Νέου Γεγονότος", + "CalendarSettings_NewEventBehavior_Description": "Επιλέξτε εάν το κουμπί Νέου Γεγονότος θα ζητά κάθε φορά ένα ημερολόγιο ή θα ανοίγει πάντοτε ένα συγκεκριμένο ημερολόγιο.", + "CalendarSettings_NewEventBehavior_AskEachTime": "Ρωτήστε κάθε φορά.", + "CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar": "Πάντα χρησιμοποιήστε συγκεκριμένο ημερολόγιο.", + "CalendarSettings_Rendering_Title": "Απεικόνιση", + "CalendarSettings_Rendering_Description": "Διαμορφώστε τη διάταξη του ημερολογίου και τη συμπεριφορά προβολής.", + "CalendarSettings_Notifications_Title": "Ειδοποιήσεις", + "CalendarSettings_Notifications_Description": "Επιλέξτε την προεπιλεγμένη υπενθύμιση και τη συμπεριφορά αναβολής.", + "CalendarSettings_Preferences_Title": "Προτιμήσεις", + "CalendarSettings_Preferences_Description": "Ορίστε πώς συμπεριφέρεται το κουμπί Νέου Γεγονότος.", + "WhatIsNew_GetStartedButton": "Ξεκινήστε", + "WhatIsNew_ContinueAnywayButton": "Συνεχίστε ούτως ή άλλως", + "WhatIsNew_PreparingForNewVersionButton": "Ετοιμασία για τη Νέα Έκδοση...", + "WhatIsNew_MigrationPreparing_Title": "Ετοιμάζοντας τα δεδομένα σας", + "WhatIsNew_MigrationPreparing_Description": "Η Wino εφαρμόζει μεταναστεύσεις ενημερώσεων. Περιμένετε καθώς ετοιμάζουμε τα δεδομένα του λογαριασμού σας για αυτήν την έκδοση.", + "WhatIsNew_MigrationFailedMessage": "Η εφαρμογή μεταναστεύσεων απέτυχε με κωδικό σφάλματος {0}. Μπορείτε να συνεχίσετε να χρησιμοποιείτε την εφαρμογή. Ωστόσο, αν παρουσιαστούν σοβαρά προβλήματα, παρακαλούμε επανεγκαταστήστε την εφαρμογή.", + "WhatIsNew_MigrationNotification_Title": "Το Wino Mail Ενημερώθηκε", + "WhatIsNew_MigrationNotification_Message": "Ανοίξτε την εφαρμογή για να ολοκληρώσετε την ενημέρωση και να δείτε τι καινούριο.", + "WelcomeWindow_Title": "Καλώς ορίσατε στο Wino Mail", + "WelcomeWindow_Subtitle": "Μια εγγενής εμπειρία Windows για το Mail και το Ημερολόγιο.", + "WelcomeWindow_WhatsNewTitle": "Τελευταίες αλλαγές", + "WelcomeWindow_FeaturesTitle": "Λειτουργίες", + "WelcomeWindow_WhatsNewTab": "Τι νέο υπάρχει", + "WelcomeWindow_FeaturesTab": "Λειτουργίες", + "WelcomeWindow_GetStartedButton": "Ξεκινήστε προσθέτοντας έναν λογαριασμό", + "WelcomeWindow_GetStartedDescription": "Προσθέστε τον λογαριασμό σας Outlook, Gmail ή IMAP για να ξεκινήσετε με το Wino Mail.", + "WelcomeWindow_ImportFromWinoAccount": "Εισαγωγή από τον λογαριασμό σας Wino", + "WelcomeWindow_ImportInProgress": "Εισαγωγή των συγχρονισμένων προτιμήσεων και λογαριασμών σας...", + "WelcomeWindow_ImportNoAccountsFound": "Δεν βρέθηκαν συγχρονισμένοι λογαριασμοί σε αυτόν τον λογαριασμό Wino. Εάν υπήρχαν προτιμήσεις, επαναφέρθηκαν. Χρησιμοποιήστε το Ξεκινήστε για να προσθέσετε έναν λογαριασμό χειροκίνητα.", + "WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} συγχρονισμένοι λογαριασμοί είναι ήδη διαθέσιμοι σε αυτήν τη συσκευή. Χρησιμοποιήστε το Ξεκινήστε για να προσθέσετε ακόμη έναν λογαριασμό χειροκίνητα εάν χρειάζεται.", + "WelcomeWindow_SetupTitle": "Ρυθμίστε τον λογαριασμό σας", + "WelcomeWindow_SetupSubtitle": "Επιλέξτε τον πάροχο email σας για να ξεκινήσετε", + "WelcomeWindow_AddAccountButton": "Προσθήκη λογαριασμού", + "WelcomeWindow_SkipForNow": "Παράλειψη προς το παρόν — Θα το ρυθμίσω αργότερα", + "WelcomeWindow_AppDescription": "Ένα γρήγορο, εστιασμένο inbox — επανασχεδιασμένο για Windows 11", + "WelcomeWizard_Step1Title": "Καλώς ήρθατε", + "SystemTrayMenu_Open": "Άνοιγμα", + "WinoAccount_Titlebar_SyncBenefitTitle": "Ρυθμίσεις συγχρονισμού", + "WinoAccount_Titlebar_SyncBenefitDescription": "Κρατήστε τις προτιμήσεις Wino συγχρονισμένες σε όλες τις συσκευές.", + "WinoAccount_Titlebar_AddonsBenefitTitle": "Ξεκλειδώστε τα πρόσθετα", + "WinoAccount_Titlebar_AddonsBenefitDescription": "Πρόσβαση σε premium δυνατότητες όπως το Wino AI Pack.", + "WinoAccount_Management_Description": "Διαχειριστείτε τον λογαριασμό Wino, την πρόσβαση στο AI Pack, και τις συγχρονισμένες προτιμήσεις και λεπτομέρειες λογαριασμού.", + "WinoAccount_Management_SignedOutTitle": "Σύνδεση στο Wino Mail", + "WinoAccount_Management_SignedOutDescription": "Συνδεθείτε ή δημιουργήστε λογαριασμό για να συγχρονίσετε το email σας, να έχετε πρόσβαση σε λειτουργίες AI και να διαχειριστείτε τις ρυθμίσεις σας σε όλες τις συσκευές.", + "WinoAccount_Management_ProfileSectionHeader": "Προφίλ", + "WinoAccount_Management_AddOnsSectionHeader": "Πρόσθετα Wino", + "WinoAccount_Management_DataSectionHeader": "Δεδομένα", + "WinoAccount_Management_AccountActionsSectionHeader": "Ενέργειες λογαριασμού", + "WinoAccount_Management_AccountCardTitle": "Λογαριασμός", + "WinoAccount_Management_AccountCardDescription": "Η διεύθυνση email του λογαριασμού Wino σας και η τρέχουσα κατάσταση λογαριασμού.", + "WinoAccount_Management_AiPackCardTitle": "AI Pack", + "WinoAccount_Management_AiPackCardDescription": "Δείτε εάν το Wino AI Pack είναι ενεργό και πόση χρήση απομένει.", + "WinoAccount_Management_AiPackActive": "Το AI Pack είναι ενεργό", + "WinoAccount_Management_AiPackInactive": "Το AI Pack δεν είναι ενεργό", + "WinoAccount_Management_AiPackUsage": "{0} από {1} χρήσεις καταναλώθηκαν. {2} απομένουν.", + "WinoAccount_Management_AiPackBillingPeriod": "Περίοδος χρέωσης: {0:d} - {1:d}", + "WinoAccount_Management_AiPackUnknownUsage": "Οι λεπτομέρειες χρήσης δεν είναι ακόμη διαθέσιμες.", + "WinoAccount_Management_AiPackBuyDescription": "Αγοράστε το Wino AI Pack για μετάφραση, επαναγραφή ή περίληψη emails με AI.", + "WinoAccount_Management_AiPackPromoTitle": "Ξεκλειδώστε το AI Pack", + "WinoAccount_Management_AiPackPromoDescription": "Ενισχύστε την ροή εργασίας email με εργαλεία τεχνητής νοημοσύνης. Μεταφράστε μηνύματα σε 50+ γλώσσες, βελτιώστε τη σαφήνεια και τον τόνο, και λάβετε απευθείας περιλήψεις μακρών συνομιλιών.", + "WinoAccount_Management_AiPackPromoPrice": "4,99 $ / μήνα", + "WinoAccount_Management_AiPackPromoRequests": "1.000 credits", + "WinoAccount_Management_AiPackGetButton": "Λήψη AI Pack", + "WinoAddOn_AI_PACK_Name": "Wino AI Pack", + "WinoAddOn_AI_PACK_Description": "Εργαλεία με τεχνητή νοημοσύνη για μετάφραση, επαναγραφή και περίληψη ενέργειών στο Wino Mail.", + "WinoAddOn_AI_PACK_Keywords": "AI, μετάφραση, επαναγραφή, περίληψη, παραγωγικότητα", + "WinoAddOn_UNLIMITED_ACCOUNTS_Name": "Απεριόριστοι Λογαριασμοί", + "WinoAddOn_UNLIMITED_ACCOUNTS_Description": "Αφαιρέστε τον περιορισμό λογαριασμών και προσθέστε όσους λογαριασμούς email χρειάζεστε.", + "WinoAddOn_UNLIMITED_ACCOUNTS_Keywords": "λογαριασμοί, απεριόριστο, premium, πρόσθετο", + "WinoAccount_Management_PurchaseRequiresSignIn": "Συνδεθείτε με τον λογαριασμό σας Wino για να ολοκληρώσετε την αγορά.", + "WinoAccount_Management_PurchaseStartFailed": "Η αγορά από το Microsoft Store δεν μπόρεσε να ολοκληρωθεί.", + "WinoAccount_Management_StoreSyncFailed": "Η αγορά σας ολοκληρώθηκε, αλλά το Wino δεν μπόρεσε ακόμη να ανανεώσει τα οφέλη του λογαριασμού σας. Προσπαθήστε ξανά σε λίγο.", + "WinoAccount_Management_AiPackSubscriptionActive": "Η συνδρομή σας είναι ενεργή", + "WinoAccount_Management_AiPackRenews": "Ανανεώνει {0:d}", + "WinoAccount_Management_AiPackRequestsUsed": "Χρησιμοποιήθηκαν πιστώσεις αυτόν τον μήνα", + "WinoAccount_Management_AiPackResets": "Επαναφορές {0:d}", + "WinoAccount_Management_AiPackUsageLoadFailed": "Προέκυψαν προβλήματα φόρτωσης του υπολοίπου χρήσης AI.", + "WinoAccount_Management_AiPackFeatureTranslate": "Μετάφραση", + "WinoAccount_Management_AiPackFeatureRewrite": "Επαναγραφή", + "WinoAccount_Management_AiPackFeatureSummarize": "Περίληψη", + "WinoAccount_Management_AddOnLoadFailed": "Προέκυψαν προβλήματα φόρτωσης αυτού του πρόσθετου.", + "WinoAccount_Management_SyncPreferencesTitle": "Συγχρονισμός Προτιμήσεων και Λογαριασμών", + "WinoAccount_Management_SyncPreferencesDescription": "Εισαγάγετε ή εξάγετε τις προτιμήσεις Wino και τις λεπτομέρειες γραμματοκιβωτίου σας σε όλες τις συσκευές. Κωδικοί πρόσβασης, διαπιστευτήρια και άλλες ευαίσθητες πληροφορίες δεν συγχρονίζονται ποτέ.", + "WinoAccount_Management_SignOutTitle": "Αποσύνδεση", + "WinoAccount_Management_SignOutDescription": "Αποσυνδεθείτε από τον λογαριασμό σας σε αυτήν τη συσκευή", + "WinoAccount_Management_StatusLabel": "Κατάσταση: {0}", + "WinoAccount_Management_NoRemoteSettings": "Δεν υπάρχουν ακόμη συγχρονισμένα δεδομένα αποθηκευμένα γι' αυτόν τον λογαριασμό.", + "WinoAccount_Management_ExportSucceeded": "Τα επιλεγμένα δεδομένα Wino εξήχθησαν με επιτυχία.", + "WinoAccount_Management_ExportPreferencesSucceeded": "Οι προτιμήσεις σας εξήχθησαν στο λογαριασμό σας Wino.", + "WinoAccount_Management_ExportAccountsSucceeded": "Εξήχθησαν {0} λεπτομέρειες λογαριασμού στον λογαριασμό σας Wino.", + "WinoAccount_Management_ImportSucceeded": "Εισήχθησαν συγχρονισμένα δεδομένα από τον λογαριασμό σας Wino.", + "WinoAccount_Management_ImportPreferencesSucceeded": "Εφαρμόστηκαν {0} συγχρονισμένες προτιμήσεις.", + "WinoAccount_Management_ImportAccountsSucceeded": "Εισήχθησαν {0} λογαριασμοί.", + "WinoAccount_Management_ImportDuplicateAccountsSkipped": "Απορρίφθηκαν {0} λογαριασμοί που ήδη υπάρχουν σε αυτήν τη συσκευή.", + "WinoAccount_Management_ImportPartial": "Εφαρμόστηκαν {0} συγχρονισμένες προτιμήσεις. {1} προτιμήσεις δεν μπόρεσαν να αποκατασταθούν.", + "WinoAccount_Management_ImportReloginReminder": "Οι κωδικοί πρόσβασης, τα διακριτικά και άλλες ευαίσθητες πληροφορίες δεν εισήχθησαν. Συνδεθείτε ξανά για κάθε λογαριασμό σε αυτήν τη συσκευή πριν τη χρήση.", + "WinoAccount_Management_SerializeFailed": "Το Wino δεν μπόρεσε να σειριαφοποιήσει τις τρέχουσες προτιμήσεις σας.", + "WinoAccount_Management_EmptyExport": "Δεν υπάρχουν τιμές προτιμήσεων προς εξαγωγή.", + "WinoAccount_Management_ImportEmpty": "Το πακέτο συγχρονισμένων δεδομένων δεν περιέχει τίποτα νέο προς επαναφορά.", + "WinoAccount_Management_ExportDialog_Title": "Εξαγωγή στο Λογαριασμό σας Wino", + "WinoAccount_Management_ExportDialog_Description": "Επιλέξτε τι θέλετε να συγχρονίσετε στον Λογαριασμό σας Wino.", + "WinoAccount_Management_ExportDialog_IncludePreferences": "Προτιμήσεις", + "WinoAccount_Management_ExportDialog_IncludeAccounts": "Λογαριασμοί", + "WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Κωδικοί πρόσβασης, διακριτικά και άλλες ευαίσθητες πληροφορίες δεν συγχρονίζονται.", + "WinoAccount_Management_ExportDialog_AccountsRelogin": "Οι εισαγόμενοι λογαριασμοί σε άλλον υπολογιστή θα χρειαστεί να συνδεθείτε ξανά πριν μπορέσουν να χρησιμοποιηθούν.", + "WinoAccount_Management_ExportDialog_InProgress": "Εξάγονται τα επιλεγμένα δεδομένα Wino.", + "WinoAccount_Management_LoadFailed": "Το Wino δεν μπόρεσε να φορτώσει τις τελευταίες πληροφορίες του Λογαριασμού Wino.", + "WinoAccount_Management_ActionFailed": "Το αίτημα Λογαριασμού Wino δεν μπόρεσε να ολοκληρωθεί.", + "WinoAccount_SettingsSection_Title": "Λογαριασμός Wino", + "WinoAccount_SettingsSection_Description": "Δημιουργήστε ή συνδεθείτε σε έναν Λογαριασμό Wino χρησιμοποιώντας την υπηρεσία αυθεντικοποίησης του τοπικού σας υπολογιστή.", + "WinoAccount_RegisterButton_Title": "Εγγραφή λογαριασμού", + "WinoAccount_RegisterButton_Description": "Δημιουργήστε έναν Λογαριασμό Wino με διεύθυνση email και κωδικό πρόσβασης.", + "WinoAccount_RegisterButton_Action": "Άνοιγμα της εγγραφής", + "WinoAccount_LoginButton_Title": "Σύνδεση", + "WinoAccount_LoginButton_Description": "Συνδεθείτε σε υπάρχον Λογαριασμό Wino χρησιμοποιώντας διεύθυνση email και κωδικό πρόσβασης.", + "WinoAccount_LoginButton_Action": "Άνοιγμα σύνδεσης", + "WinoAccount_SignOutButton_Title": "Αποσύνδεση", + "WinoAccount_SignOutButton_Description": "Αφαιρέστε τη συνεδρία του Λογαριασμού Wino που αποθηκεύεται τοπικά.", + "WinoAccount_SignOutButton_Action": "Αποσύνδεση", + "WinoAccount_RegisterDialog_Title": "Δημιουργία Λογαριασμού Wino", + "WinoAccount_RegisterDialog_Description": "Δημιουργήστε Λογαριασμό Wino για να διατηρήσετε την εμπειρία Wino σε συγχρονισμό και να ξεκλειδώσετε τις προσθήκες με βάση τον λογαριασμό.", + "WinoAccount_RegisterDialog_HeroTitle": "Δημιουργήστε τον Λογαριασμό σας στο Wino", + "WinoAccount_RegisterDialog_BenefitsTitle": "Γιατί να δημιουργήσετε έναν;", + "WinoAccount_RegisterDialog_BenefitSyncTitle": "Εισαγωγή και εξαγωγή ρυθμίσεων σε πολλαπλές συσκευές", + "WinoAccount_RegisterDialog_BenefitSyncDescription": "Μετακινήστε τις προτιμήσεις Wino μεταξύ συσκευών χωρίς να χρειάζεται να επαναφέρετε τη ρύθμισή σας από την αρχή.", + "WinoAccount_RegisterDialog_BenefitAiTitle": "Πρόσβαση σε αποκλειστικές προσθήκες όπως το Wino AI Pack (με χρέωση)", + "WinoAccount_RegisterDialog_BenefitAiDescription": "Χρησιμοποιήστε έναν λογαριασμό για να ξεκλειδώσετε τις προχωρημένες δυνατότητες του Wino καθώς γίνονται διαθέσιμες.", + "WinoAccount_RegisterDialog_DifferenceTitle": "Ο Λογαριασμός Wino είναι ξεχωριστός από τους λογαριασμούς email σας", + "WinoAccount_RegisterDialog_DifferenceDescription": "Οι λογαριασμοί email σας όπως Outlook, Gmail, IMAP ή άλλοι παραμένουν ακριβώς όπως είναι. Ένας Λογαριασμός Wino διαχειρίζεται μόνο δυνατότητες που αφορούν το Wino και προσθήκες ανά λογαριασμό.", + "WinoAccount_RegisterDialog_PrimaryButton": "Εγγραφή", + "WinoAccount_RegisterDialog_PrivacyTitle": "Ιδιωτικότητα και επεξεργασία API", + "WinoAccount_RegisterDialog_PrivacyDescription": "Προαιρετικές προσθήκες όπως το Wino AI Pack ενδέχεται να αποστείλουν επιλεγμένο HTML περιεχόμενο ηλεκτρονικού ταχυδρομείου στην υπηρεσία Wino API μόνο όταν χρησιμοποιείτε αυτές τις λειτουργίες.", + "WinoAccount_RegisterDialog_PrivacyLinkText": "Διαβάστε την πολιτική απορρήτου.", + "WinoAccount_RegisterDialog_PrivacyCheckbox": "Συμφωνώ με την πολιτική απορρήτου.", + "WinoAccount_LoginDialog_Title": "Σύνδεση στον Λογαριασμό Wino", + "WinoAccount_LoginDialog_Description": "Συνδεθείτε στον Λογαριασμό Wino σας για να συγχρονίσετε τις ρυθμίσεις Wino και να αποκτήσετε πρόσβαση σε δυνατότητες που βασίζονται σε λογαριασμό.", + "WinoAccount_LoginDialog_HeroTitle": "Καλωσήρθατε ξανά", + "WinoAccount_LoginDialog_BenefitsTitle": "Τι σας προσφέρει η σύνδεση", + "WinoAccount_LoginDialog_BenefitsDescription": "Χρησιμοποιήστε τον Λογαριασμό Wino για να συνεχίσετε τον συγχρονισμό ρυθμίσεων σε πολλές συσκευές και για πρόσβαση σε επί πληρωμή προσθήκες όπως το Wino AI Pack.", + "WinoAccount_LoginDialog_DifferenceTitle": "Αυτό δεν αποτελεί είσοδος στο λογαριασμό email σας", + "WinoAccount_LoginDialog_DifferenceDescription": "Η σύνδεση εδώ δεν προσθέτει ή αντικαθιστά τους λογαριασμούς Outlook, Gmail ή IMAP στο Wino. Συνδεθείτε μόνο σε υπηρεσίες που αφορούν το Wino.", + "WinoAccount_LoginDialog_ForgotPasswordLink": "Ξεχάσατε τον κωδικό σας;", + "WinoAccount_EmailLabel": "Ηλεκτρονική διεύθυνση", + "WinoAccount_EmailPlaceholder": "name@example.com", + "WinoAccount_PasswordLabel": "Κωδικός πρόσβασης", + "WinoAccount_ConfirmPasswordLabel": "Επιβεβαίωση κωδικού", + "WinoAccount_ForgotPasswordDialog_Title": "Επαναφέρετε τον κωδικό σας", + "WinoAccount_ForgotPasswordDialog_PrimaryButton": "Αποστολή email επαναφοράς κωδικού", + "WinoAccount_ForgotPasswordDialog_BackToSignIn": "Επιστροφή στη σύνδεση", + "WinoAccount_ForgotPasswordDialog_Description": "Εισάγετε τη διεύθυνση email του Λογαριασμού Wino και θα σας στείλουμε σύνδεσμο επαναφοράς κωδικού εάν η διεύθυνση έχει εγγραφεί.", + "WinoAccount_Validation_EmailRequired": "Απαιτείται διεύθυνση email.", + "WinoAccount_Validation_PasswordRequired": "Απαιτείται κωδικός πρόσβασης.", + "WinoAccount_Validation_PasswordMismatch": "Οι κωδικοί πρόσβασης δεν ταιριάζουν.", + "WinoAccount_Validation_PrivacyConsentRequired": "Πρέπει να αποδεχτείτε την πολιτική απορρήτου πριν τη δημιουργία Λογαριασμού Wino.", + "WinoAccount_Error_InvalidCredentials": "Η διεύθυνση email ή ο κωδικός πρόσβασης είναι λάθος.", + "WinoAccount_Error_AccountLocked": "Αυτός ο λογαριασμός είναι προσωρινά κλειδωμένος.", + "WinoAccount_Error_AccountBanned": "Αυτός ο λογαριασμός έχει απαγορευτεί.", + "WinoAccount_Error_AccountSuspended": "Αυτός ο λογαριασμός έχει τεθεί σε αναστολή.", + "WinoAccount_Error_EmailNotConfirmed": "Παρακαλώ επιβεβαιώστε τη διεύθυνση email προτού συνδεθείτε.", + "WinoAccount_Error_EmailConfirmationRequired": "Παρακαλώ επιβεβαιώστε τη διεύθυνση email προτού συνδεθείτε.", + "WinoAccount_Error_EmailConfirmationResendNotAvailable": "Ένα νέο email επιβεβαίωσης δεν βρίσκεται ακόμη διαθέσιμο.", + "WinoAccount_Error_EmailConfirmationResendInvalid": "Αυτό το αίτημα επιβεβαίωσης δεν ισχύει πλέον. Παρακαλώ προσπαθήστε να συνδεθείτε ξανά.", + "WinoAccount_Error_EmailNotRegistered": "Αυτή η διεύθυνση email δεν είναι εγγεγραμμένη.", + "WinoAccount_Error_RefreshTokenInvalid": "Η συνεδρία σας δεν είναι πλέον έγκυρη. Παρακαλώ συνδεθείτε ξανά.", + "WinoAccount_Error_EmailAlreadyRegistered": "Αυτή η διεύθυνση email έχει ήδη εγγραφεί.", + "WinoAccount_Error_ExternalLoginEmailRequired": "Απαιτείται διεύθυνση email για να ολοκληρωθεί η εξωτερική σύνδεση.", + "WinoAccount_Error_ExternalLoginInvalid": "Το αίτημα εξωτερικής σύνδεσης είναι άκυρο.", + "WinoAccount_Error_ExternalAuthStateInvalid": "Η κατάσταση εξωτερικής σύνδεσης είναι άκυρη ή έχει λήξει.", + "WinoAccount_Error_ExternalAuthCodeInvalid": "Ο κωδικός εξωτερικής σύνδεσης είναι άκυρος ή έχει λήξει.", + "WinoAccount_Error_AiPackRequired": "Απαιτείται ενεργή συνδρομή Wino AI Pack για αυτήν τη ενέργεια.", + "WinoAccount_Error_AiQuotaExceeded": "Ο περιορισμός χρήσης του Wino AI Pack για την τρέχουσα περίοδο χρέωσης έχει εξαντληθεί.", + "WinoAccount_Error_AiHtmlEmpty": "Δεν υπάρχει περιεχόμενο email προς επεξεργασία.", + "WinoAccount_Error_AiHtmlTooLarge": "Αυτό το email είναι υπερβολικά μεγάλο για επεξεργασία από το Wino AI.", + "WinoAccount_Error_AiUnsupportedLanguage": "Αυτή η γλώσσα δεν υποστηρίζεται. Δοκιμάστε έναν έγκυρο πολιτισμικό κωδικό όπως en-US ή tr-TR.", + "WinoAccount_Error_Forbidden": "Δεν έχετε άδεια να εκτελέσετε αυτήν την ενέργεια.", + "WinoAccount_Error_ValidationFailed": "Το αίτημα δεν είναι έγκυρο. Παρακαλώ ελέγξτε τις καταχωρημένες τιμές.", + "WinoAccount_RegisterSuccessMessage": "Η εγγραφή του Λογαριασμού Wino ολοκληρώθηκε για {0}.", + "WinoAccount_LoginSuccessMessage": "Συνδεθήκατε στον Λογαριασμό Wino ως {0}.", + "WinoAccount_EmailConfirmationSentDialog_Title": "Επιβεβαιώστε τη διεύθυνση email σας", + "WinoAccount_EmailConfirmationSentDialog_Message": "Στείλαμε ένα email επιβεβαίωσης στη διεύθυνση {0}. Παρακαλούμε επιβεβαιώστε το και προσπαθήστε να συνδεθείτε ξανά.", + "WinoAccount_EmailConfirmationPendingDialog_Title": "Απαιτείται επιβεβαίωση email", + "WinoAccount_EmailConfirmationPendingDialog_Message": "Ακόμη περιμένουμε να επιβεβαιώσετε το {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ResendButton": "Αποστολή εκ νέου email επιβεβαίωσης", + "WinoAccount_EmailConfirmationPendingDialog_Countdown": "Μπορείτε να αποστείλετε εκ新的 email επιβεβαίωσης σε {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ReadyToResend": "Μπορείτε να αποστείλετε εκ νέου το email επιβεβαίωσης τώρα.", + "WinoAccount_EmailConfirmationResentDialog_Title": "Το email επιβεβαίωσης εστάλη εκ νέου", + "WinoAccount_EmailConfirmationResentDialog_Message": "Στείλαμε ένα ακόμη email επιβεβαίωσης στη διεύθυνση {0}. Παρακαλώ επιβεβαιώστε το και προσπαθήστε να συνδεθείτε ξανά.", + "WinoAccount_ForgotPasswordDialog_SuccessTitle": "Αποστολή email επαναφοράς κωδικού", + "WinoAccount_ForgotPasswordDialog_SuccessMessage": "Σας έχουμε στείλει ένα μήνυμα επαναφοράς κωδικού στη διεύθυνση {0}. Ανοίξτε το μήνυμα για να ορίσετε έναν νέο κωδικό.", + "WinoAccount_ChangePassword_Title": "Αλλαγή κωδικού", + "WinoAccount_ChangePassword_Description": "Αποστολή email επαναφοράς κωδικού σε αυτόν τον Λογαριασμό Wino.", + "WinoAccount_ChangePassword_Action": "Αποστολή email επαναφοράς", + "WinoAccount_ChangePassword_ConfirmationMessage": "Θέλετε το Wino να στείλει ένα email επαναφοράς κωδικού πρόσβασης στο {0}?", + "WinoAccount_SignOut_SuccessMessage": "Αποσυνδεθήκατε από τον λογαριασμό Wino {0}.", + "WinoAccount_SignOut_NoAccountMessage": "Δεν υπάρχει ενεργός λογαριασμός Wino για αποσύνδεση.", + "WinoAccount_Titlebar_SignedOutTitle": "Λογαριασμός Wino", + "WinoAccount_Titlebar_SignedOutDescription": "Συνδεθείτε ή δημιουργήστε έναν λογαριασμό Wino για να διαχειριστείτε τη συνεδρία σας στο Wino.", + "WinoAccount_Titlebar_SignedInStatus": "Κατάσταση: {0}", + "WelcomeWizard_Step2Title": "Προσθήκη Λογαριασμού", + "WelcomeWizard_Step3Title": "Ολοκλήρωση Ρύθμισης", + "ProviderSelection_Title": "Επιλέξτε τον πάροχο email σας", + "ProviderSelection_Subtitle": "Επιλέξτε έναν πάροχο παρακάτω για να προσθέσετε τον λογαριασμό email σας στο Wino Mail.", + "ProviderSelection_AccountNameHeader": "Όνομα Λογαριασμού", + "ProviderSelection_AccountNamePlaceholder": "π.χ. Προσωπικό, Εργασία", + "ProviderSelection_DisplayNameHeader": "Όνομα εμφάνισης", + "ProviderSelection_DisplayNamePlaceholder": "π.χ. John Doe", + "ProviderSelection_EmailHeader": "Διεύθυνση Ηλεκτρονικού Ταχυδρομείου", + "ProviderSelection_EmailPlaceholder": "π.χ. johndoe@example.com", + "ProviderSelection_AppPasswordHeader": "Κωδικός πρόσβασης εφαρμογής", + "ProviderSelection_AppPasswordHelp": "Πώς μπορώ να πάρω έναν κωδικό πρόσβασης για την εφαρμογή;", + "ProviderSelection_CalendarModeHeader": "Ενσωμάτωση Ημερολογίου", + "ProviderSelection_CalendarMode_DisabledTitle": "Απενεργοποιημένο", + "ProviderSelection_CalendarMode_DisabledDescription": "Δεν υπάρχει ενσωμάτωση ημερολογίου", + "ProviderSelection_CalendarMode_CalDavTitle": "CalDAV Συγχρονισμός", + "ProviderSelection_CalendarMode_CalDavDescription_Apple": "Τα γεγονότα του ημερολογίου σας συγχρονίζονται με τους διακομιστές της Apple μεταξύ των συσκευών σας.", + "ProviderSelection_CalendarMode_CalDavDescription_Yahoo": "Τα γεγονότα του ημερολογίου σας συγχρονίζονται με τους διακομιστές Yahoo ανάμεσα στις συσκευές σας.", + "ProviderSelection_CalendarMode_LocalTitle": "Τοπικό ημερολόγιο", + "ProviderSelection_CalendarMode_LocalDescription": "Τα γεγονότα σας αποθηκεύονται μόνο στον υπολογιστή σας. Δεν υπάρχει σύνδεση με διακομιστές.", + "ProviderSelection_ClearColor": "Εκκαθάριση χρώματος", + "ProviderSelection_ContinueButton": "Συνέχεια", + "ProviderSelection_SpecialImap_Subtitle": "Εισάγετε τα διαπιστευτήρια του λογαριασμού σας για να συνδεθείτε.", + "AccountSetup_Title": "Ρύθμιση του λογαριασμού σας", + "AccountSetup_Step_Authenticating": "Ταυτοποίηση με {0}", + "AccountSetup_Step_TestingMailAuth": "Δοκιμή αυθεντικοποίησης ηλεκτρονικού ταχυδρομείου", + "AccountSetup_Step_SyncingFolders": "Συγχρονισμός μεταδεδομένων φακέλων", + "AccountSetup_Step_FetchingProfile": "Λήψη πληροφοριών προφίλ", + "AccountSetup_Step_DiscoveringCalDav": "Εύρεση ρυθμίσεων CalDAV", + "AccountSetup_Step_TestingCalendarAuth": "Δοκιμή αυθεντικοποίησης ημερολογίου", + "AccountSetup_Step_SavingAccount": "Αποθήκευση πληροφοριών λογαριασμού", + "AccountSetup_Step_FetchingCalendarMetadata": "Λήψη μεταδεδομένων ημερολογίου", + "AccountSetup_Step_SyncingAliases": "Συγχρονισμός ψευδωνύμων", + "AccountSetup_Step_Finalizing": "Ολοκλήρωση ρύθμισης", + "AccountSetup_FailureMessage": "Η ρύθμιση απέτυχε. Μεταβείτε πίσω για να διορθώσετε τις ρυθμίσεις σας ή προσπαθήστε ξανά αργότερα.", + "AccountSetup_SuccessMessage": "Ο λογαριασμός σας έχει ρυθμιστεί με επιτυχία!", + "AccountSetup_GoBackButton": "Πίσω", + "AccountSetup_TryAgainButton": "Προσπαθήστε Ξανά", + "ImapCalDavSettings_AutoDiscoveryFailed": "Αυτόματη εύρεση απέτυχε. Παρακαλούμε εισάγετε χειροκίνητα τις ρυθμίσεις στην καρτέλα Σύνθετες." } - - diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index 8eccf7fa..0d2d0b54 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -1,4 +1,4 @@ -{ +{ "AccountAlias_Column_Alias": "Alias", "AccountAlias_Column_IsPrimaryAlias": "Primary", "AccountAlias_Column_Verified": "Verified", @@ -8,6 +8,7 @@ "AccountCacheReset_Message": "This account requires full re-sychronization to continue working. Please wait while Wino re-synchronizes your messages...", "AccountContactNameYou": "You", "AccountCreationDialog_Completed": "all done", + "AccountCreationDialog_FetchingCalendarMetadata": "Fetching calendar details.", "AccountCreationDialog_FetchingEvents": "Fetching calendar events.", "AccountCreationDialog_FetchingProfileInformation": "Fetching profile details.", "AccountCreationDialog_GoogleAuthHelpClipboardText_Row0": "If your browser did not launch automatically to complete authentication:", @@ -17,6 +18,7 @@ "AccountCreationDialog_Initializing": "initializing", "AccountCreationDialog_PreparingFolders": "We are getting folder information at the moment.", "AccountCreationDialog_SigninIn": "Account information is being saved.", + "Purchased": "Purchased", "AccountEditDialog_Message": "Account Name", "AccountEditDialog_Title": "Edit Account", "AccountPickerDialog_Title": "Pick an account", @@ -26,6 +28,10 @@ "AccountDetailsPage_Description": "Change the name of the account in Wino and set desired sender name.", "AccountDetailsPage_ColorPicker_Title": "Account color", "AccountDetailsPage_ColorPicker_Description": "Assign a new account color to colorize its symbol in the list.", + "AccountDetailsPage_TabGeneral": "General", + "AccountDetailsPage_TabMail": "Mail", + "AccountDetailsPage_TabCalendar": "Calendar", + "AccountDetailsPage_CalendarListDescription": "Select a calendar to configure its settings", "AddHyperlink": "Add", "AppCloseBackgroundSynchronizationWarningTitle": "Background Synchronization", "AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Application has not been set to launch on Windows startup.", @@ -47,8 +53,10 @@ "BasicIMAPSetupDialog_Title": "IMAP Account", "Busy": "Busy", "Buttons_AddAccount": "Add Account", + "Buttons_FixAccount": "Fix Account", "Buttons_AddNewAlias": "Add New Alias", "Buttons_Allow": "Allow", + "Buttons_Apply": "Apply", "Buttons_ApplyTheme": "Apply Theme", "Buttons_Browse": "Browse", "Buttons_Cancel": "Cancel", @@ -62,6 +70,7 @@ "Buttons_Edit": "Edit", "Buttons_EnableImageRendering": "Enable", "Buttons_Multiselect": "Select Multiple", + "Buttons_Manage": "Manage", "Buttons_No": "No", "Buttons_Open": "Open", "Buttons_Purchase": "Purchase", @@ -70,15 +79,134 @@ "Buttons_Save": "Save", "Buttons_SaveConfiguration": "Save Configuration", "Buttons_Send": "Send", + "Buttons_SendToServer": "Send to server", "Buttons_Share": "Share", "Buttons_SignIn": "Sign In", "Buttons_Sync": "Synchronize", "Buttons_SyncAliases": "Synchronize Aliases", "Buttons_TryAgain": "Try Again", "Buttons_Yes": "Yes", + "Sync_SynchronizingFolder": "Synchronizing {0} {1}%", + "Sync_DownloadedMessages": "Downloaded {0} messages from {1}", + "SyncAction_Archiving": "Archiving {0} mail(s)", + "SyncAction_ClearingFlag": "Unflagging {0} mail(s)", + "SyncAction_CreatingDraft": "Creating draft", + "SyncAction_CreatingEvent": "Creating event", + "SyncAction_Deleting": "Deleting {0} mail(s)", + "SyncAction_EmptyingFolder": "Emptying folder", + "SyncAction_MarkingAsRead": "Marking {0} mail(s) as read", + "SyncAction_MarkingAsUnread": "Marking {0} mail(s) as unread", + "SyncAction_MarkingFolderAsRead": "Marking folder as read", + "SyncAction_Moving": "Moving {0} mail(s)", + "SyncAction_MovingToFocused": "Moving {0} mail(s) to Focused", + "SyncAction_RenamingFolder": "Renaming folder", + "SyncAction_SendingMail": "Sending mail", + "SyncAction_SettingFlag": "Flagging {0} mail(s)", + "SyncAction_SynchronizingAccount": "Synchronizing {0}", + "SyncAction_SynchronizingAccounts": "Synchronizing {0} account(s)", + "SyncAction_SynchronizingCalendarData": "Synchronizing calendar data", + "SyncAction_SynchronizingCalendarEvents": "Synchronizing calendar events", + "SyncAction_SynchronizingCalendarMetadata": "Synchronizing calendar metadata", + "SyncAction_Unarchiving": "Unarchiving {0} mail(s)", "CalendarAllDayEventSummary": "all-day events", "CalendarDisplayOptions_Color": "Color", "CalendarDisplayOptions_Expand": "Expand", + "CalendarEventResponse_Accept": "Accept", + "CalendarEventResponse_AcceptedResponse": "Accepted", + "CalendarEventResponse_Decline": "Decline", + "CalendarEventResponse_DeclinedResponse": "Declined", + "CalendarEventResponse_NotResponded": "Not Responded", + "CalendarEventResponse_Tentative": "Tentative", + "CalendarEventResponse_TentativeResponse": "Tentative", + "CalendarEventRsvpPanel_Accept": "Accept", + "CalendarEventRsvpPanel_AddMessage": "Add a message to your response... (optional)", + "CalendarEventRsvpPanel_Decline": "Decline", + "CalendarEventRsvpPanel_Message": "Message", + "CalendarEventRsvpPanel_SendReplyMessage": "Send a reply message", + "CalendarEventRsvpPanel_Tentative": "Tentative", + "CalendarEventRsvpPanel_Title": "Response Options", + "CalendarAttendeeStatus_Accepted": "Accepted", + "CalendarAttendeeStatus_Declined": "Declined", + "CalendarAttendeeStatus_NeedsAction": "Needs Action", + "CalendarAttendeeStatus_Tentative": "Tentative", + "CalendarEventDetails_Attachments": "Attachments", + "CalendarEventCompose_AddAttachment": "Add attachment", + "CalendarEventCompose_AllDay": "All Day", + "CalendarEventCompose_AttachmentsNotSupportedForCalDav": "Attachments are not supported for CalDAV calendars.", + "CalendarEventCompose_EndDate": "End date", + "CalendarEventCompose_EndTime": "End time", + "CalendarEventCompose_Every": "every", + "CalendarEventCompose_ForWeekdays": "for", + "CalendarEventCompose_FrequencyDay": "day", + "CalendarEventCompose_FrequencyDayPlural": "days", + "CalendarEventCompose_FrequencyMonth": "month", + "CalendarEventCompose_FrequencyMonthPlural": "months", + "CalendarEventCompose_FrequencyWeek": "week", + "CalendarEventCompose_FrequencyWeekPlural": "weeks", + "CalendarEventCompose_FrequencyYear": "year", + "CalendarEventCompose_FrequencyYearPlural": "years", + "CalendarEventCompose_Location": "Location", + "CalendarEventCompose_LocationPlaceholder": "Add a location", + "CalendarEventCompose_NewEventButton": "New Event", + "CalendarEventCompose_DefaultCalendarHint": "You can choose a default calendar for new events in Calendar settings.", + "CalendarEventCompose_DefaultCalendarSettingsLink": "Open Calendar settings", + "CalendarEventCompose_NoCalendarsMessage": "There are no calendars available for event creation yet.", + "CalendarEventCompose_NoCalendarsTitle": "No calendars available", + "CalendarEventCompose_NoEndDate": "No end date", + "CalendarEventCompose_Notes": "Notes", + "CalendarEventCompose_PickCalendarTitle": "Pick a calendar", + "CalendarEventCompose_Recurring": "Recurring", + "CalendarEventCompose_RecurringSummary": "Occurs every {0} {1}{2} {3} effective {4}{5}", + "CalendarEventCompose_RecurringSummarySmart": "Occurs {0}{1} {2} effective {3}{4}", + "CalendarEventCompose_RepeatEvery": "Repeat every", + "CalendarEventCompose_SelectCalendar": "Select calendar", + "CalendarEventCompose_SingleOccurrenceSummary": "Occurs on {0} {1}", + "CalendarEventCompose_StartDate": "Start date", + "CalendarEventCompose_StartTime": "Start time", + "CalendarEventCompose_TimeRangeSummary": "from {0} to {1}", + "CalendarEventCompose_Title": "Event title", + "CalendarEventCompose_TitlePlaceholder": "Add a title", + "CalendarEventCompose_Until": "until", + "CalendarEventCompose_UntilSummary": " until {0}", + "CalendarEventCompose_ValidationInvalidAllDayRange": "The all-day end date must be after the start date.", + "CalendarEventCompose_ValidationInvalidAttendee": "One or more attendees have an invalid email address.", + "CalendarEventCompose_ValidationInvalidRecurrenceEnd": "The recurrence end date must be on or after the event start date.", + "CalendarEventCompose_ValidationInvalidTimeRange": "The end time must be later than the start time.", + "CalendarEventCompose_ValidationMissingAttachment": "One or more attachments are no longer available: {0}", + "CalendarEventCompose_ValidationMissingCalendar": "Select a calendar before creating the event.", + "CalendarEventCompose_ValidationMissingTitle": "Enter an event title before creating the event.", + "CalendarEventCompose_ValidationTitle": "Event validation failed", + "CalendarEventCompose_WeekdaySummary": " on {0}", + "CalendarEventCompose_Weekday_Friday": "F", + "CalendarEventCompose_Weekday_Monday": "M", + "CalendarEventCompose_Weekday_Saturday": "S", + "CalendarEventCompose_Weekday_Sunday": "S", + "CalendarEventCompose_Weekday_Thursday": "T", + "CalendarEventCompose_Weekday_Tuesday": "T", + "CalendarEventCompose_Weekday_Wednesday": "W", + "CalendarEventDetails_Details": "Details", + "CalendarEventDetails_EditSeries": "Edit Series", + "CalendarEventDetails_Editing": "Editing", + "CalendarEventDetails_InviteSomeone": "Invite someone", + "CalendarEventDetails_JoinOnline": "Join Online", + "CalendarEventDetails_Organizer": "Organizer", + "CalendarEventDetails_People": "People", + "CalendarEventDetails_ReadOnlyEvent": "Read-only event", + "CalendarEventDetails_Reminder": "Reminder", + "CalendarReminder_StartedHoursAgo": "Started {0} hours ago", + "CalendarReminder_StartedMinutesAgo": "Started {0} minutes ago", + "CalendarReminder_StartedNow": "Started just now", + "CalendarReminder_StartingNow": "Starting now", + "CalendarReminder_StartsInHours": "Starts in {0} hours", + "CalendarReminder_StartsInMinutes": "Starts in {0} minutes", + "CalendarReminder_SnoozeAction": "Snooze", + "CalendarReminder_SnoozeMinutesOption": "{0} minutes", + "CalendarEventDetails_ShowAs": "Show as", + "CalendarShowAs_Free": "Free", + "CalendarShowAs_Tentative": "Tentative", + "CalendarShowAs_Busy": "Busy", + "CalendarShowAs_OutOfOffice": "Out of Office", + "CalendarShowAs_WorkingElsewhere": "Working Elsewhere", "CalendarItem_DetailsPopup_JoinOnline": "Join online", "CalendarItem_DetailsPopup_ViewEventButton": "View event", "CalendarItem_DetailsPopup_ViewSeriesButton": "View series", @@ -88,6 +216,9 @@ "ClipboardTextCopied_Message": "{0} copied to clipboard.", "ClipboardTextCopied_Title": "Copied", "ClipboardTextCopyFailed_Message": "Failed to copy {0} to clipboard.", + "ContactInfoBar_ErrorTitle": "Failed to load contact information", + "ContactInfoBar_SuccessTitle": "Contact information loaded", + "ContactInfoBar_WarningTitle": "Contact information might be incomplete", "ComingSoon": "Coming soon...", "ComposerAttachmentsDragDropAttach_Message": "Attach", "ComposerAttachmentsDropZone_Message": "Drop your files here", @@ -129,6 +260,10 @@ "DialogMessage_CreateLinkedAccountTitle": "Account Link Name", "DialogMessage_DeleteAccountConfirmationMessage": "Delete {0}?", "DialogMessage_DeleteAccountConfirmationTitle": "All data associated with this account will be deleted from disk permanently.", + "DialogMessage_DeleteEmailTemplateConfirmationMessage": "Delete template \"{0}\"?", + "DialogMessage_DeleteEmailTemplateConfirmationTitle": "Delete Email Template", + "DialogMessage_DeleteRecurringSeriesMessage": "This will delete all events in the series. Do you want to continue?", + "DialogMessage_DeleteRecurringSeriesTitle": "Delete Recurring Series", "DialogMessage_DiscardDraftConfirmationMessage": "This draft will be discarded. Do you want to continue?", "DialogMessage_DiscardDraftConfirmationTitle": "Discard Draft", "DialogMessage_EmptySubjectConfirmation": "Missing Subject", @@ -172,11 +307,18 @@ "ElementTheme_Light": "Light mode", "Emoji": "Emoji", "Error_FailedToSetupSystemFolders_Title": "Failed to setup system folders", + "Exception_AccountNeedsAttention_Title": "Account needs attention", + "Exception_AccountNeedsAttention_Message": "'{0}' requires your attention to continue working.", + "Exception_WebView2RuntimeMissing_Message": "Wino Mail could not find Microsoft Edge WebView2 Runtime. Please install or repair the runtime to render message content correctly.", + "Exception_WebView2RuntimeMissing_Title": "WebView2 runtime is required", "Exception_AuthenticationCanceled": "Authentication canceled", "Exception_CustomThemeExists": "This theme already exists.", "Exception_CustomThemeMissingName": "You must provide a name.", "Exception_CustomThemeMissingWallpaper": "You must provide a custom background image.", "Exception_FailedToSynchronizeAliases": "Failed to synchronize aliases", + "Exception_FailedToSynchronizeCalendarData": "Failed to synchronize calendar data", + "Exception_FailedToSynchronizeCalendarEvents": "Failed to synchronize calendar events", + "Exception_FailedToSynchronizeCalendarMetadata": "Failed to synchronize calendar details", "Exception_FailedToSynchronizeFolders": "Failed to synchronize folders", "Exception_FailedToSynchronizeProfileInformation": "Failed to synchronize profile information", "Exception_GoogleAuthCallbackNull": "Callback uri is null on activation.", @@ -229,6 +371,32 @@ "HoverActionOption_MoveJunk": "Move to Junk", "HoverActionOption_ToggleFlag": "Flag / Unflag", "HoverActionOption_ToggleRead": "Read / Unread", + "KeyboardShortcuts_FailedToReset": "Failed to reset keyboard shortcuts.", + "KeyboardShortcuts_FailedToUpdate": "Failed to update keyboard shortcuts", + "KeyboardShortcuts_MailoperationAction": "Action", + "KeyboardShortcuts_Action": "Action", + "KeyboardShortcuts_FailedToLoad": "Failed to load keyboard shortcuts.", + "KeyboardShortcuts_EnterKeyForShortcut": "Please enter a key for the shortcut.", + "KeyboardShortcuts_SelectOperationForShortcut": "Please an action to perform for the shortcut.", + "KeyboardShortcuts_EnterKey": "Please enter a key for the shortcut.", + "KeyboardShortcuts_SelectOperation": "Please select an action for the shortcut.", + "KeyboardShortcuts_ShortcutInUse": "This shortcut is already in use by another s hortcut.", + "KeyboardShortcuts_FailedToSave": "Failed to save the shortcut.", + "KeyboardShortcuts_FailedToDelete": "Failed to delete the shortcut.", + "KeyboardShortcuts_PageDescription": "Set up keyboard shortcuts for quick mail operations. Press keys while focused on the key input field to capture shortcuts.", + "KeyboardShortcuts_Add": "Add shortcut", + "KeyboardShortcuts_EditTitle": "Edit Keyboard Shortcut", + "KeyboardShortcuts_ResetToDefaults": "Reset to Defaults", + "KeyboardShortcuts_PressKeysHere": "Press keys here...", + "KeyboardShortcuts_KeyCombination": "Key Combination", + "KeyboardShortcuts_FocusArea": "Focus the field above and press the desired key combination", + "KeyboardShortcuts_Modifiers": "Modifier Keys", + "KeyboardShortcuts_Mode": "App Mode", + "KeyboardShortcuts_ModeMail": "Mail", + "KeyboardShortcuts_ModeCalendar": "Calendar", + "KeyboardShortcuts_ActionToggleReadUnread": "Toggle read/unread", + "KeyboardShortcuts_ActionToggleFlag": "Toggle flag", + "KeyboardShortcuts_ActionToggleArchive": "Toggle archive/unarchive", "ImageRenderingDisabled": "Image rendering is disabled for this message.", "ImapAdvancedSetupDialog_AuthenticationMethod": "Authentication method", "ImapAdvancedSetupDialog_ConnectionSecurity": "Connection security", @@ -295,12 +463,58 @@ "IMAPSetupDialog_Username": "Username", "IMAPSetupDialog_UsernamePlaceholder": "johndoe, johndoe@fabrikam.com, domain/johndoe", "IMAPSetupDialog_UseSameConfig": "Use the same username and password for sending email", + "ImapCalDavSettingsPage_TitleCreate": "IMAP and Calendar Setup", + "ImapCalDavSettingsPage_TitleEdit": "Edit IMAP and Calendar Settings", + "ImapCalDavSettingsPage_Subtitle": "Configure IMAP/SMTP and optional calendar synchronization for this account.", + "ImapCalDavSettingsPage_BasicSectionTitle": "Basic setup", + "ImapCalDavSettingsPage_BasicSectionDescription": "Enter your identity and credentials. Wino can try to detect server settings automatically.", + "ImapCalDavSettingsPage_BasicTab": "Basic", + "ImapCalDavSettingsPage_EnableCalendarSupport": "Enable calendar support", + "ImapCalDavSettingsPage_AutoDiscoverButton": "Autodiscover mail settings", + "ImapCalDavSettingsPage_AutoDiscoverySuccessMessage": "Mail settings discovered and applied.", + "ImapCalDavSettingsPage_AdvancedSectionTitle": "Advanced configuration", + "ImapCalDavSettingsPage_AdvancedSectionDescription": "Enter server settings manually if autodiscovery is unavailable or incorrect.", + "ImapCalDavSettingsPage_AdvancedTab": "Advanced", + "ImapCalDavSettingsPage_CalendarSectionTitle": "Calendar setup", + "ImapCalDavSettingsPage_CalendarSectionDescription": "Choose how calendar data should work for this IMAP account.", + "ImapCalDavSettingsPage_CalendarModeHeader": "Calendar mode", + "ImapCalDavSettingsPage_ConnectionSecurityHeader": "Connection security", + "ImapCalDavSettingsPage_AuthenticationMethodHeader": "Authentication method", + "ImapCalDavSettingsPage_CalendarModeDisabled": "Disabled", + "ImapCalDavSettingsPage_CalendarModeCalDav": "CalDAV synchronization", + "ImapCalDavSettingsPage_CalendarModeLocalOnly": "Local calendar only", + "ImapCalDavSettingsPage_CalendarModeDisabledDescription": "Calendar is disabled for this account.", + "ImapCalDavSettingsPage_CalendarModeCalDavDescription": "Calendar items are synchronized with your CalDAV server.", + "ImapCalDavSettingsPage_CalendarModeLocalOnlyDescription": "Calendar items are stored only on this computer and are not synchronized to the network.", + "ImapCalDavSettingsPage_LocalCalendarLearnMore": "How local calendar works", + "ImapCalDavSettingsPage_LocalCalendarDialogTitle": "Local calendar only", + "ImapCalDavSettingsPage_LocalCalendarDialogMessage": "Local calendar keeps all events only on your computer. Nothing is synchronized to iCloud, Yahoo, or any other provider.", + "ImapCalDavSettingsPage_CalDavServiceUrl": "CalDAV service URL", + "ImapCalDavSettingsPage_CalDavUsername": "CalDAV username", + "ImapCalDavSettingsPage_CalDavPassword": "CalDAV password", + "ImapCalDavSettingsPage_CalDavNotRequiredMessage": "CalDAV test is only required when calendar mode is set to CalDAV synchronization.", + "ImapCalDavSettingsPage_CalDavUrlRequired": "CalDAV service URL is required.", + "ImapCalDavSettingsPage_CalDavUrlInvalid": "CalDAV service URL must be an absolute URL.", + "ImapCalDavSettingsPage_CalDavUsernameRequired": "CalDAV username is required.", + "ImapCalDavSettingsPage_CalDavPasswordRequired": "CalDAV password is required.", + "ImapCalDavSettingsPage_TestImapButton": "Test IMAP connection", + "ImapCalDavSettingsPage_TestCalDavButton": "Test CalDAV connection", + "ImapCalDavSettingsPage_ImapTestSuccessMessage": "IMAP connection test succeeded.", + "ImapCalDavSettingsPage_CalDavTestSuccessMessage": "CalDAV connection test succeeded.", + "ImapCalDavSettingsPage_SaveSuccessMessage": "Account settings validated and saved.", + "ImapCalDavSettingsPage_ICloudHint": "Use an app-specific password generated from your Apple account settings.", + "ImapCalDavSettingsPage_YahooHint": "Use an app password from your Yahoo account security settings.", "Info_AccountCreatedMessage": "{0} is created", "Info_AccountCreatedTitle": "Account Creation", "Info_AccountCreationFailedTitle": "Account Creation Failed", "Info_AccountDeletedMessage": "{0} is successfuly deleted.", "Info_AccountDeletedTitle": "Account Deleted", "Info_AccountIssueFixFailedTitle": "Failed", + "Info_AccountIssueFixImapMessage": "Open the IMAP and calendar settings page to enter your server credentials again.", + "Info_AccountAttentionRequiredMessage": "This account needs your attention.", + "Info_AccountAttentionRequiredClickableMessage": "Click to fix this account and resynchronize it.", + "Info_AccountAttentionRequiredAction": "Fix", + "Info_AccountAttentionRequiredActionHint": "Click Fix to resolve this account issue.", "Info_AccountIssueFixSuccessMessage": "Fixed all account issues.", "Info_AccountIssueFixSuccessTitle": "Success", "Info_AttachmentOpenFailedMessage": "Can't open this attachment.", @@ -370,6 +584,7 @@ "InfoBarMessage_SynchronizationDisabledFolder": "This folder is disabled for synchronization.", "InfoBarTitle_SynchronizationDisabledFolder": "Disabled Folder", "Justify": "Justify", + "MenuUpdateAvailable": "Update Available", "Left": "Left", "Link": "Link", "LinkedAccountsCreatePolicyMessage": "you must have at least 2 accounts to create link\nlink will be removed on save", @@ -403,6 +618,7 @@ "MailOperation_Unarchive": "Unarchive", "MailOperation_ViewMessageSource": "View message source", "MailOperation_Zoom": "Zoom", + "MailsDragging": "Dragging {0} item(s)", "MailsSelected": "{0} item(s) selected", "MarkFlagUnflag": "Mark as flagged/unflagged", "MarkReadUnread": "Mark as read/unread", @@ -434,6 +650,8 @@ "Notifications_MultipleNotificationsTitle": "New Mail", "Notifications_WinoUpdatedMessage": "Checkout new version {0}", "Notifications_WinoUpdatedTitle": "Wino Mail has been updated.", + "Notifications_StoreUpdateAvailableTitle": "Update available", + "Notifications_StoreUpdateAvailableMessage": "A newer version of Wino Mail is ready to install from Microsoft Store.", "OnlineSearchFailed_Message": "Failed to perform search\n{0}\n\nListing offline mails.", "OnlineSearchTry_Line1": "Can't find what you are looking for?", "OnlineSearchTry_Line2": "Try online search.", @@ -446,7 +664,6 @@ "PaneLengthOption_Small": "Small", "Photos": "Photos", "PreparingFoldersMessage": "Preparing folders", - "ProtocolLogAvailable_Message": "Protocol logs are available for diagnostics.", "ProviderDetail_Gmail_Description": "Google Account", "ProviderDetail_iCloud_Description": "Apple iCloud Account", "ProviderDetail_iCloud_Title": "iCloud", @@ -465,9 +682,14 @@ "SearchBarPlaceholder": "Search", "SearchingIn": "Searching in", "SearchPivotName": "Results", + "Settings_KeyboardShortcuts_Title": "Keyboard Shortcuts", + "Settings_KeyboardShortcuts_Description": "Manage keyboard shortcuts for quick actions on the mails.", "SettingConfigureSpecialFolders_Button": "Configure", "SettingsEditAccountDetails_IMAPConfiguration_Title": "IMAP/SMTP Configuration", "SettingsEditAccountDetails_IMAPConfiguration_Description": "Change your incoming/outgoing server settings.", + "SettingsEditAccountDetails_ImapCalDavSettings_Title": "IMAP and calendar settings", + "SettingsEditAccountDetails_ImapCalDavSettings_Description": "Open the dedicated IMAP, SMTP, and CalDAV settings page for this account.", + "SettingsEditAccountDetails_ImapCalDavSettings_Action": "Open settings", "SettingsAbout_Description": "Learn more about Wino.", "SettingsAbout_Title": "About", "SettingsAboutGithub_Description": "Go to issue tracker GitHub repository.", @@ -490,6 +712,10 @@ "SettingsAppPreferences_SearchMode_Local": "Local", "SettingsAppPreferences_SearchMode_Online": "Online", "SettingsAppPreferences_SearchMode_Title": "Default search mode", + "SettingsAppPreferences_ApplicationMode_Title": "Default application mode", + "SettingsAppPreferences_ApplicationMode_Description": "Choose which mode Wino opens in when no activation type explicitly sets it.", + "SettingsAppPreferences_ApplicationMode_Mail": "Mail", + "SettingsAppPreferences_ApplicationMode_Calendar": "Calendar", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description": "Wino Mail will keep running in the background. You will be notified as new mails arrive.", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title": "Run in the background", "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description": "Wino Mail will keep running on the system tray. Available to launch by clicking on an icon. You will be notified as new mails arrive.", @@ -506,12 +732,30 @@ "SettingsAppPreferences_StartupBehavior_FatalError": "Fatal error occurred while changing the startup mode for Wino Mail.", "SettingsAppPreferences_StartupBehavior_Title": "Start minimized on Windows startup", "SettingsAppPreferences_Title": "App Preferences", + "SettingsAppPreferences_HideWinoAccountButton_Title": "Hide Wino account button in title bar", + "SettingsAppPreferences_HideWinoAccountButton_Description": "Hide the title bar profile button that opens the Wino account flyout.", + "SettingsAppPreferences_StoreUpdateNotifications_Title": "Store update notifications", + "SettingsAppPreferences_StoreUpdateNotifications_Description": "Show notifications and footer actions when a Microsoft Store update is available.", + "SettingsAppPreferences_AiActions_Title": "AI actions", + "SettingsAppPreferences_AiActions_Description": "Choose default AI languages and where summaries should be saved.", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Title": "Default translation language", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Description": "Pick the default target language used by AI translate actions.", + "SettingsAppPreferences_AiSummarizeLanguage_Title": "Summarize language", + "SettingsAppPreferences_AiSummarizeLanguage_Description": "Choose the preferred summarize language for future AI summary output.", + "SettingsAppPreferences_AiSummarySavePath_Title": "Default summary save path", + "SettingsAppPreferences_AiSummarySavePath_Description": "Choose the folder Wino should use by default when saving AI summaries.", + "SettingsAppPreferences_AiSummarySavePath_Placeholder": "Use the system default save location", + "SettingsAppPreferences_AiSummarySavePath_InvalidHint": "This folder does not exist. The default save location will be used for summaries.", "SettingsAutoSelectNextItem_Description": "Select the next item after you delete or move a mail.", "SettingsAutoSelectNextItem_Title": "Auto select next item", "SettingsAvailableThemes_Description": "Select a theme from Wino's own collection for your taste or apply your own themes.", "SettingsAvailableThemes_Title": "Available Themes", "SettingsCalendarSettings_Description": "Change first day of week, hour cell height and more...", "SettingsCalendarSettings_Title": "Calendar Settings", + "CalendarSettings_DefaultSnoozeDuration_Header": "Default snooze duration", + "CalendarSettings_DefaultSnoozeDuration_Description": "Set a default snooze duration for calendar reminder notifications.", + "CalendarSettings_TimedDayHeaderFormat_Header": "Timed view day header format", + "CalendarSettings_TimedDayHeaderFormat_Description": "Choose how the top day labels are rendered in day, week, and work week views. Use date format tokens like ddd, dd, MMM, or dddd.", "SettingsComposer_Title": "Composer", "SettingsComposerFont_Title": "Default Composer Font", "SettingsComposerFontFamily_Description": "Change the default font family and font size for composing mails.", @@ -531,6 +775,9 @@ "SettingsDiscord_Title": "Discord Channel", "SettingsEditLinkedInbox_Description": "Add / remove accounts, rename or break the link between accounts.", "SettingsEditLinkedInbox_Title": "Edit Linked Inbox", + "SettingsWindowBackdrop_Title": "Window Backdrop", + "SettingsWindowBackdrop_Description": "Select a backdrop effect for Wino windows.", + "SettingsWindowBackdrop_Disabled": "Window backdrop selection is disabled when application theme is selected other than Default.", "SettingsElementTheme_Description": "Select a Windows theme for Wino", "SettingsElementTheme_Title": "Element Theme", "SettingsElementThemeSelectionDisabled": "Element theme selection is disabled when application theme is selected other than Default.", @@ -576,11 +823,13 @@ "SettingsMailSpacing_Description": "Adjust the spacing for listing mails.", "SettingsMailSpacing_Title": "Mail Spacing", "SettingsManageAccountSettings_Description": "Notifications, signatures, synchronization and other settings per account.", - "SettingsManageAccountSettings_Title": "Manage Account Settings", + "SettingsManageAccountSettings_Title": "Manage Accounts", "SettingsManageAliases_Description": "See e-mail aliases assigned for this account, update or delete them.", "SettingsManageAliases_Title": "Aliases", "SettingsEditAccountDetails_Title": "Edit Account Details", "SettingsEditAccountDetails_Description": "Change account name, sender name and assign a new color if you like.", + "EditAccountDetailsPage_SaveSuccess_Title": "Changes Saved", + "EditAccountDetailsPage_SaveSuccess_Message": "Your account details have been updated successfully.", "SettingsManageLink_Description": "Move items to add new link or remove existing link.", "SettingsManageLink_Title": "Manage Link", "SettingsMarkAsRead_Description": "Change what should happen to the selected item.", @@ -596,7 +845,41 @@ "SettingsNotifications_Title": "Notifications", "SettingsNotificationsAndTaskbar_Description": "Change whether notifications should be displayed and taskbar badge for this account.", "SettingsNotificationsAndTaskbar_Title": "Notifications & Taskbar", + "SettingsHome_Title": "Home", + "SettingsHome_SearchTitle": "Find a setting", + "SettingsHome_SearchDescription": "Search by feature, topic, or keyword to jump straight to the right settings page.", + "SettingsHome_SearchPlaceholder": "Search settings", + "SettingsHome_SearchExamples": "Try: theme, storage, language, signature", + "SettingsHome_QuickLinks_Title": "Quick links", + "SettingsHome_QuickLinks_Description": "Jump into the settings people reach for most often.", + "SettingsHome_StorageCard_Description": "See how much local MIME content Wino keeps on this device and clean it up when needed.", + "SettingsHome_StorageEmptySummary": "No cached MIME content detected yet.", + "SettingsHome_StorageLoading": "Checking local MIME usage...", + "SettingsHome_Tips_Title": "Tips & tricks", + "SettingsHome_Tips_Description": "A few small changes can make Wino feel much more personal.", + "SettingsHome_Tip_Theme": "Want dark mode or accent changes? Open Personalization.", + "SettingsHome_Tip_Background": "Use App Preferences to control startup behavior and background sync.", + "SettingsHome_Tip_Shortcuts": "Keyboard Shortcuts helps you move through mail faster.", + "SettingsHome_Resources_Title": "Helpful links", + "SettingsHome_Resources_Description": "Open project resources, support info, and release channels.", "SettingsOptions_Title": "Settings", + "SettingsOptions_GeneralSection": "General", + "SettingsOptions_MailSection": "Mail", + "SettingsOptions_CalendarSection": "Calendar", + "SettingsOptions_MoreComingSoon": "More options coming soon", + "SettingsOptions_HeroDescription": "Customize your Wino Mail experience", + "SettingsOptions_AccountsSummary": "{0} account(s) configured", + "SettingsSearch_ManageAccounts_Keywords": "account;accounts;mailbox;mailboxes;alias;aliases;profile;address;addresses", + "SettingsSearch_AppPreferences_Keywords": "startup;background;launch;sync;notification;notifications;search;tray;defaults", + "SettingsSearch_LanguageTime_Keywords": "language;time;clock;locale;region;format;24 hour;24h", + "SettingsSearch_Personalization_Keywords": "theme;dark;light;appearance;accent;color;colour;mode;layout;density", + "SettingsSearch_About_Keywords": "about;version;website;privacy;github;donate;store;support", + "SettingsSearch_KeyboardShortcuts_Keywords": "shortcut;shortcuts;hotkey;hotkeys;keyboard;keys", + "SettingsSearch_MessageList_Keywords": "message;messages;list;threading;threads;avatar;preview;sender", + "SettingsSearch_ReadComposePane_Keywords": "reader;compose;composer;font;fonts;external content;display;reading", + "SettingsSearch_SignatureAndEncryption_Keywords": "signature;signatures;encryption;certificate;certificates;s mime;smime;security", + "SettingsSearch_Storage_Keywords": "storage;cache;caching;mime;disk;space;cleanup;clean up;local data", + "SettingsSearch_CalendarSettings_Keywords": "calendar;week;hours;schedule;event;events", "SettingsPaneLengthReset_Description": "Reset the size of the mail list to original if you have issues with it.", "SettingsPaneLengthReset_Title": "Reset Mail List Size", "SettingsPaypal_Description": "Show much more love ❤️ All donations are appreciated.", @@ -610,6 +893,8 @@ "SettingsPrefer24HourClock_Title": "Display Clock Format in 24 Hours", "SettingsPrivacyPolicy_Description": "Review privacy policy.", "SettingsPrivacyPolicy_Title": "Privacy Policy", + "SettingsWebsite_Description": "Open the Wino Mail website.", + "SettingsWebsite_Title": "Website", "SettingsReadComposePane_Description": "Fonts, external content.", "SettingsReadComposePane_Title": "Reader & Composer", "SettingsReader_Title": "Reader", @@ -625,6 +910,19 @@ "SettingsShowPreviewText_Title": "Show Preview Text", "SettingsShowSenderPictures_Description": "Hide/show the thumbnail sender pictures.", "SettingsShowSenderPictures_Title": "Show Sender Avatars", + "SettingsEmailTemplates_Title": "Email Templates", + "SettingsEmailTemplates_Description": "Manage e-mail templates", + "SettingsEmailTemplates_CreatePageTitle": "New template", + "SettingsEmailTemplates_EditPageTitle": "Edit template", + "SettingsEmailTemplates_NewTemplateTitle": "New template", + "SettingsEmailTemplates_NewTemplateDescription": "Create a new e-mail template", + "SettingsEmailTemplates_NameTitle": "Name", + "SettingsEmailTemplates_NamePlaceholder": "Template name", + "SettingsEmailTemplates_DescriptionTitle": "Description", + "SettingsEmailTemplates_DescriptionPlaceholder": "Optional description", + "SettingsEmailTemplates_ContentTitle": "Template content", + "SettingsEmailTemplates_ContentDescription": "Edit the HTML content for this template.", + "SettingsEmailTemplates_NameRequired": "Template name is required.", "SettingsEnableGravatarAvatars_Title": "Gravatar", "SettingsEnableGravatarAvatars_Description": "Use gravatar (if available) as sender picture", "SettingsEnableFavicons_Title": "Domain icons (Favicons)", @@ -645,6 +943,33 @@ "SettingsStartupItem_Title": "Startup Item", "SettingsStore_Description": "Show some love ❤️", "SettingsStore_Title": "Rate in Store", + "SettingsStorage_Title": "Storage", + "SettingsStorage_Description": "Scan and manage MIME cache stored in your local data folder.", + "SettingsStorage_ScanFolder": "Scan local data folder", + "SettingsStorage_NoLocalMimeDataFound": "No local MIME data found.", + "SettingsStorage_NoAccountsFound": "No accounts found.", + "SettingsStorage_TotalUsage": "Total local MIME usage: {0}", + "SettingsStorage_AccountUsageDescription": "{0} used in local MIME cache", + "SettingsStorage_DeleteAll_Title": "Delete all MIME content", + "SettingsStorage_DeleteAll_Description": "Delete this account's entire MIME cache folder.", + "SettingsStorage_DeleteAll_Button": "Delete all", + "SettingsStorage_DeleteAll_Confirm_Title": "Delete all MIME content", + "SettingsStorage_DeleteAll_Confirm_Message": "Delete all local MIME data for {0}?", + "SettingsStorage_DeleteAll_Success": "All MIME content was deleted.", + "SettingsStorage_DeleteOld_Title": "Delete old MIME content", + "SettingsStorage_DeleteOld_Description": "Delete MIME files based on mail creation date in local database.", + "SettingsStorage_DeleteOld_1Month": "> 1 month", + "SettingsStorage_DeleteOld_3Months": "> 3 months", + "SettingsStorage_DeleteOld_6Months": "> 6 months", + "SettingsStorage_DeleteOld_1Year": "> 1 year", + "SettingsStorage_DeleteOld_Confirm_Title": "Delete old MIME content", + "SettingsStorage_DeleteOld_Confirm_Message": "Delete local MIME data older than {0} for {1}?", + "SettingsStorage_DeleteOld_Success": "Deleted {0} MIME folder(s) older than {1}.", + "SettingsStorage_1Month": "1 month", + "SettingsStorage_3Months": "3 months", + "SettingsStorage_6Months": "6 months", + "SettingsStorage_1Year": "1 year", + "SettingsStorage_Months": "{0} months", "SettingsTaskbarBadge_Description": "Include unread mail count in taskbar icon.", "SettingsTaskbarBadge_Title": "Taskbar Badge", "SettingsThreads_Description": "Organize messages into conversation threads.", @@ -683,6 +1008,9 @@ "SystemFolderConfigDialogValidation_InboxSelected": "You can't assign Inbox folder to any other system folder.", "SystemFolderConfigSetupSuccess_Message": "System folders are successfully configured.", "SystemFolderConfigSetupSuccess_Title": "System Folders Setup", + "SystemTrayMenu_ShowWino": "Open Wino Mail", + "SystemTrayMenu_ShowWinoCalendar": "Open Wino Calendar", + "SystemTrayMenu_ExitWino": "Exit", "TestingImapConnectionMessage": "Testing server connection...", "TitleBarServerDisconnectedButton_Description": "Wino is disconnected from the network. Click reconnect to restore connection.", "TitleBarServerDisconnectedButton_Title": "no connection", @@ -699,8 +1027,422 @@ "WinoUpgradeMessage": "Upgrade to Unlimited Accounts", "WinoUpgradeRemainingAccountsMessage": "{0} out of {1} free accounts used.", "Yesterday": "Yesterday", + "Smime_ImportCertificates_Success": "Certificates imported successfully.", + "Smime_ImportCertificates_Error": "Error importing certificates: {0}", + "Smime_RemoveCertificates_Confirm": "Do you really want to remove the certificates {0}?", + "Smime_RemoveCertificates_Success": "Certificates removed.", + "Smime_ExportCertificates_Success": "Certificates exported.", + "Smime_ExportCertificates_Error": "Error exporting certificates.", + "Smime_CertificateDetails": "Subject: {0}\nIssuer: {1}\nValid from: {2}\nValid to: {3}\nThumbprint: {4}", + "Smime_CertificatePassword_Title": "Certificate password required", + "Smime_CertificatePassword_Placeholder": "Certificate password for {0} (optional)", + "Smime_Confirm_Title": "Confirm", + "Buttons_OK": "OK", + "Buttons_Refresh": "Refresh", + "SettingsSignatureAndEncryption_Title": "Signature and Encryption", + "SettingsSignatureAndEncryption_Description": "Manage S/MIME certificates for signing and encrypting emails.", + "SettingsSignatureAndEncryption_MyCertificatesHeader": "My certificates", + "SettingsSignatureAndEncryption_MyCertificatesDescription": "Personal certificates for signing and encryption", + "SettingsSignatureAndEncryption_RecipientCertificatesHeader": "Recipient certificates", + "SettingsSignatureAndEncryption_RecipientCertificatesDescription": "Recipient certificates for decryption", + "SettingsSignatureAndEncryption_NameColumn": "Name", + "SettingsSignatureAndEncryption_ExpiresColumn": "Expires on", + "SettingsSignatureAndEncryption_ThumbprintColumn": "Thumbprint", + "Buttons_Remove": "Remove", + "Buttons_Export": "Export", + "Buttons_Import": "Import", + "SettingsSignatureAndEncryption_SigningCertificate": "S/Mime Signing Certificate", + "SettingsSignatureAndEncryption_EncryptionCertificate": "S/Mime Encryption", + "SettingsSignatureAndEncryption_SigningCertificatePlaceholder": "None", + "SmimeSignaturesInMessage": "Signatures in this message:", + "SmimeSignatureEntry": "• {0} {1} ({2}, valid through {3} - {4})", + "SmimeSigningCertificateInfoTitle": "S/MIME Signing Certificate Info", + "SmimeCertificateInfoTitle": "S/MIME Certificate Info", + "SmimeNoCertificateFileFound": "No certificate file found", + "SmimeSaveCertificate": "Save certificate...", + "SmimeCertificate": "S/MIME Certificate", + "SmimeCertificateSavedTo": "Certificate saved to {0}", + "SmimeSignedTooltip": "This message is signed with an S/Mime certificate. Click for more details", + "SmimeEncryptedTooltip": "This message is encrypted with an S/Mime certificate.", + "SmimeCertificateFileInfo": "File: {0}\nType: {1}\nSize: {2:N0} bytes", + "Composer_LightTheme": "Light Theme", + "Composer_DarkTheme": "Dark Theme", + "Composer_Outdent": "Outdent", + "Composer_Indent": "Indent", + "Composer_BulletList": "Bullet List", + "Composer_OrderedList": "Ordered List", + "Composer_Stroke": "Stroke", + "Composer_Bold": "Bold", + "Composer_Italic": "Italic", + "Composer_Underline": "Underline", + "Composer_CcBcc": "Cc & Bcc", + "Composer_EnableSmimeSignature": "Enable/disable S/MIME signature", + "Composer_EnableSmimeEncryption": "Enable/disable S/MIME encryption", + "Composer_LocalDraftSyncInfo": "This draft is local only. Wino failed to send it to your mail server. Click to retry sending it to the server.", + "Composer_CertificateExpires": "Expires on: ", + "Composer_SmimeSignature": "S/MIME Signature", + "Composer_SmimeEncryption": "S/MIME Encryption", + "Composer_EmailTemplatesPlaceholder": "E-mail templates", + "Composer_AiSummarize": "Summarize with AI", + "Composer_AiSummarizeDescription": "Extract key points, action items, and decisions from this email.", + "Composer_AiTranslate": "Translate with AI", + "Composer_AiActions": "AI Actions", + "Composer_AiRewrite": "Rewrite with AI", + "AiActions_CheckingStatus": "Checking AI access...", + "AiActions_SignedOutTitle": "Unlock Wino AI Pack", + "AiActions_SignedOutDescription": "Translate, rewrite, and summarize emails with AI after signing in to your Wino Account and activating the AI Pack add-on.", + "AiActions_NoPackTitle": "AI Pack required", + "AiActions_NoPackDescription": "You're signed in, but AI Pack is not active yet. Purchase it to use Wino's AI translation, rewrite, and summarize tools.", + "AiActions_UsageSummary": "{0} of {1} credits used this month.", + "Composer_AiRewritePolite": "Make it polite", + "Composer_AiRewritePoliteDescription": "Softens the wording while keeping the same intent.", + "Composer_AiRewriteAngry": "Make it angry", + "Composer_AiRewriteAngryDescription": "Uses a sharper and more confrontational tone.", + "Composer_AiRewriteHappy": "Make it happy", + "Composer_AiRewriteHappyDescription": "Adds a more upbeat and enthusiastic tone.", + "Composer_AiRewriteFormal": "Make it formal", + "Composer_AiRewriteFormalDescription": "Makes the message sound more professional and structured.", + "Composer_AiRewriteFriendly": "Make it friendly", + "Composer_AiRewriteFriendlyDescription": "Warms up the message with a more approachable tone.", + "Composer_AiRewriteShorter": "Make it shorter", + "Composer_AiRewriteShorterDescription": "Tightens the text and removes unnecessary detail.", + "Composer_AiRewriteClearer": "Make it clearer", + "Composer_AiRewriteClearerDescription": "Improves readability and makes the message easier to follow.", + "Composer_AiRewriteCustom": "Custom", + "Composer_AiRewriteCustomDescription": "Describe your own rewrite intention.", + "Composer_AiRewriteCustomPlaceholder": "Describe how you want the message rewritten", + "Composer_AiRewriteMode": "Rewrite tone", + "Composer_AiRewriteApply": "Apply rewrite", + "Composer_AiTranslateDialogTitle": "Translate with AI", + "Composer_AiTranslateDialogDescription": "Enter the target language or culture code, such as en-US, tr-TR, de-DE, or fr-FR.", + "Composer_AiTranslateApply": "Translate", + "Composer_AiTranslateLanguage": "Target language", + "Composer_AiTranslateCustomPlaceholder": "Enter culture code", + "Composer_AiTranslateLanguageEnglish": "English (en-US)", + "Composer_AiTranslateLanguageTurkish": "Turkish (tr-TR)", + "Composer_AiTranslateLanguageGerman": "German (de-DE)", + "Composer_AiTranslateLanguageFrench": "French (fr-FR)", + "Composer_AiTranslateLanguageSpanish": "Spanish (es-ES)", + "Composer_AiTranslateLanguageItalian": "Italian (it-IT)", + "Composer_AiTranslateLanguagePortugueseBrazil": "Portuguese (Brazil) (pt-BR)", + "Composer_AiTranslateLanguageDutch": "Dutch (nl-NL)", + "Composer_AiTranslateLanguagePolish": "Polish (pl-PL)", + "Composer_AiTranslateLanguageRussian": "Russian (ru-RU)", + "Composer_AiTranslateLanguageJapanese": "Japanese (ja-JP)", + "Composer_AiTranslateLanguageKorean": "Korean (ko-KR)", + "Composer_AiTranslateLanguageChineseSimplified": "Chinese, Simplified (zh-CN)", + "Composer_AiTranslateLanguageArabic": "Arabic (ar-SA)", + "Composer_AiTranslateLanguageHindi": "Hindi (hi-IN)", + "Composer_AiTranslateLanguageOther": "Other...", + "Composer_AiBusyTitle": "AI is already working", + "Composer_AiBusyMessage": "Please wait for the current AI action to finish.", + "Composer_AiSignInRequired": "Sign in to your Wino Account to use AI features.", + "Composer_AiMissingHtml": "There is no message content to send to Wino AI yet.", + "Composer_AiQuotaUnavailable": "The AI result was applied.", + "Composer_AiAppliedMessage": "The AI result was applied to the composer. Use Undo if you want to revert it.", + "Composer_AiSummarizeSuccessTitle": "AI summary applied", + "Composer_AiTranslateSuccessTitle": "AI translation applied", + "Composer_AiRewriteSuccessTitle": "AI rewrite applied", + "Composer_AiErrorTitle": "AI action failed", + "Reader_AiAppliedMessage": "The AI result is now shown for this message. Reopen the message to view the original content again.", "SettingsAppPreferences_EmailSyncInterval_Title": "Email sync interval", - "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail." + "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail.", + "ContactsPage_Title": "Contacts", + "ContactsPage_AddContact": "Add Contact", + "ContactsPage_EditContact": "Edit Contact", + "ContactsPage_DeleteContact": "Delete Contact", + "ContactsPage_SearchPlaceholder": "Search contacts...", + "ContactsPage_NoContacts": "No contacts found", + "ContactsPage_ContactsCount": "{0} contacts", + "ContactsPage_SelectedContactsCount": "{0} selected", + "ContactsPage_DeleteSelectedContacts": "Delete Selected", + "ContactEditDialog_Title": "Edit Contact", + "ContactEditDialog_PhotoSection": "Photo", + "ContactEditDialog_ChoosePhoto": "Choose Photo", + "ContactEditDialog_RemovePhoto": "Remove Photo", + "ContactEditDialog_NameHeader": "Name", + "ContactEditDialog_NamePlaceholder": "Contact name", + "ContactEditDialog_EmailHeader": "Email Address", + "ContactEditDialog_EmailPlaceholder": "contact@example.com", + "ContactEditDialog_InfoSection": "Contact Information", + "ContactEditDialog_RootContactInfo": "This is a root contact associated with your accounts and cannot be deleted.", + "ContactEditDialog_OverriddenContactInfo": "This contact has been manually modified and will not be updated during synchronization.", + "ContactsPage_Subtitle": "Manage your email contacts and their information", + "ContactStatus_Account": "Account", + "ContactStatus_Modified": "Modified", + "ContactAction_Edit": "Edit contact", + "ContactAction_ChangePhoto": "Change photo", + "ContactAction_Delete": "Delete contact", + "ContactAction_Add": "Add Contact", + "ContactSelection_Selected": "selected", + "ContactSelection_SelectAll": "Select All", + "ContactSelection_Clear": "Clear Selection", + "ContactsPage_EmptyState": "No contacts to display", + "ContactsPage_AddFirstContact": "Add your first contact", + "ContactsPage_ContactsCountSuffix": "contacts", + "ContactsPane_NewContact": "New Contact", + "ContactsPane_DescriptionTitle": "Manage your contacts", + "ContactsPane_DescriptionBody": "Create contacts, rename them, update profile pictures, and keep saved details organized in one place.", + "ContactEditDialog_AddTitle": "Add Contact", + "ContactInfoBar_ContactAdded": "Contact added successfully.", + "ContactInfoBar_ContactUpdated": "Contact updated successfully.", + "ContactInfoBar_ContactsDeleted": "Contacts deleted successfully.", + "ContactInfoBar_ContactPhotoUpdated": "Contact photo updated successfully.", + "ContactInfoBar_FailedToLoadContacts": "Failed to load contacts: {0}", + "ContactInfoBar_FailedToAddContact": "Failed to add contact: {0}", + "ContactInfoBar_FailedToUpdateContact": "Failed to update contact: {0}", + "ContactInfoBar_FailedToDeleteContacts": "Failed to delete contacts: {0}", + "ContactInfoBar_FailedToUpdatePhoto": "Failed to update photo: {0}", + "ContactInfoBar_CannotDeleteRoot": "Root contacts cannot be deleted.", + "ContactConfirmDialog_DeleteTitle": "Delete Contact", + "ContactConfirmDialog_DeleteMessage": "Are you sure you want to delete the contact '{0}'?", + "ContactConfirmDialog_DeleteMultipleMessage": "Are you sure you want to delete {0} contact(s)?", + "ContactConfirmDialog_DeleteButton": "Delete", + "CalendarAccountSettings_Title": "Calendar Account Settings", + "CalendarAccountSettings_Description": "Manage calendar settings for {0}", + "CalendarAccountSettings_AccountColor": "Account Color", + "CalendarAccountSettings_AccountColorDescription": "Change the display color for this calendar account", + "CalendarAccountSettings_SyncEnabled": "Enable Synchronization", + "CalendarAccountSettings_SyncEnabledDescription": "Enable or disable calendar synchronization for this account", + "CalendarAccountSettings_DefaultShowAs": "Default Show As Status", + "CalendarAccountSettings_DefaultShowAsDescription": "Default availability status for new events created with this account", + "CalendarAccountSettings_PrimaryCalendar": "Primary Calendar", + "CalendarAccountSettings_PrimaryCalendarDescription": "Mark this calendar as the primary calendar for the account", + "CalendarSettings_NewEventBehavior_Header": "New Event button behavior", + "CalendarSettings_NewEventBehavior_Description": "Choose whether the New Event button should ask for a calendar each time or always open a specific calendar.", + "CalendarSettings_NewEventBehavior_AskEachTime": "Ask each time.", + "CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar": "Always use specific calendar.", + "CalendarSettings_Rendering_Title": "Rendering", + "CalendarSettings_Rendering_Description": "Configure calendar layout and display behavior.", + "CalendarSettings_Notifications_Title": "Notifications", + "CalendarSettings_Notifications_Description": "Choose default reminder and snooze behavior.", + "CalendarSettings_Preferences_Title": "Preferences", + "CalendarSettings_Preferences_Description": "Set how the New Event button behaves.", + "WhatIsNew_GetStartedButton": "Get Started", + "WhatIsNew_ContinueAnywayButton": "Continue anyway", + "WhatIsNew_PreparingForNewVersionButton": "Preparing for new version...", + "WhatIsNew_MigrationPreparing_Title": "Preparing your data", + "WhatIsNew_MigrationPreparing_Description": "Wino is applying update migrations. Please wait while we prepare your account data for this release.", + "WhatIsNew_MigrationFailedMessage": "Applying migrations failed with error code {0}. You may continue to use the application. However, if you encounter serious issues please re-install the application.", + "WhatIsNew_MigrationNotification_Title": "Wino Mail Updated", + "WhatIsNew_MigrationNotification_Message": "Open the app to complete the update and see what's new.", + "WelcomeWindow_Title": "Welcome to Wino Mail", + "WelcomeWindow_Subtitle": "A native Windows experience for Mail and Calendar.", + "WelcomeWindow_WhatsNewTitle": "Latest changes", + "WelcomeWindow_FeaturesTitle": "Features", + "WelcomeWindow_WhatsNewTab": "What's New", + "WelcomeWindow_FeaturesTab": "Features", + "WelcomeWindow_GetStartedButton": "Get started by adding an account", + "WelcomeWindow_GetStartedDescription": "Add your Outlook, Gmail, or IMAP account to get started with Wino Mail.", + "WelcomeWindow_ImportFromWinoAccount": "Import from your Wino Account", + "WelcomeWindow_ImportInProgress": "Importing your synchronized preferences and accounts...", + "WelcomeWindow_ImportNoAccountsFound": "No synced accounts were found in your Wino Account. If preferences were available, they were restored. Use Get started to add an account manually.", + "WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} synced accounts are already available on this device. Use Get started to add another account manually if needed.", + "WelcomeWindow_SetupTitle": "Set up your account", + "WelcomeWindow_SetupSubtitle": "Choose your email provider to get started", + "WelcomeWindow_AddAccountButton": "Add account", + "WelcomeWindow_SkipForNow": "Skip for now — I'll set it up later", + "WelcomeWindow_AppDescription": "A fast, focused inbox — redesigned for Windows 11", + "WelcomeWizard_Step1Title": "Welcome", + "SystemTrayMenu_Open": "Open", + "WinoAccount_Titlebar_SyncBenefitTitle": "Sync settings", + "WinoAccount_Titlebar_SyncBenefitDescription": "Keep your Wino preferences in sync across devices.", + "WinoAccount_Titlebar_AddonsBenefitTitle": "Unlock add-ons", + "WinoAccount_Titlebar_AddonsBenefitDescription": "Access premium features like Wino AI Pack.", + "WinoAccount_Management_Description": "Manage your Wino Account, AI Pack access, and synchronized preferences and account details.", + "WinoAccount_Management_SignedOutTitle": "Sign in to Wino Mail", + "WinoAccount_Management_SignedOutDescription": "Sign in or create an account to sync your email, access AI features, and manage your settings across devices.", + "WinoAccount_Management_ProfileSectionHeader": "Profile", + "WinoAccount_Management_AddOnsSectionHeader": "Wino Add-Ons", + "WinoAccount_Management_DataSectionHeader": "Data", + "WinoAccount_Management_AccountActionsSectionHeader": "Account actions", + "WinoAccount_Management_AccountCardTitle": "Account", + "WinoAccount_Management_AccountCardDescription": "Your Wino Account email address and current account state.", + "WinoAccount_Management_AiPackCardTitle": "AI Pack", + "WinoAccount_Management_AiPackCardDescription": "See whether Wino AI Pack is active and how much usage is left.", + "WinoAccount_Management_AiPackActive": "AI Pack is active", + "WinoAccount_Management_AiPackInactive": "AI Pack is not active", + "WinoAccount_Management_AiPackUsage": "{0} of {1} uses consumed. {2} remaining.", + "WinoAccount_Management_AiPackBillingPeriod": "Billing period: {0:d} - {1:d}", + "WinoAccount_Management_AiPackUnknownUsage": "Usage details are not available yet.", + "WinoAccount_Management_AiPackBuyDescription": "Buy Wino AI Pack to translate, rewrite or summarize emails with AI.", + "WinoAccount_Management_AiPackPromoTitle": "Unlock AI Pack", + "WinoAccount_Management_AiPackPromoDescription": "Supercharge your email workflow with AI-powered tools. Translate messages into 50+ languages, rewrite for clarity and tone, and get instant summaries of long threads.", + "WinoAccount_Management_AiPackPromoPrice": "$4.99 / mo", + "WinoAccount_Management_AiPackPromoRequests": "1,000 credits", + "WinoAccount_Management_AiPackGetButton": "Get AI Pack", + "WinoAddOn_AI_PACK_Name": "Wino AI Pack", + "WinoAddOn_AI_PACK_Description": "AI-powered tools for translate, rewrite, and summarize actions in Wino Mail.", + "WinoAddOn_AI_PACK_Keywords": "AI, translate, rewrite, summarize, productivity", + "WinoAddOn_UNLIMITED_ACCOUNTS_Name": "Unlimited Accounts", + "WinoAddOn_UNLIMITED_ACCOUNTS_Description": "Remove the account limit and add as many mail accounts as you need.", + "WinoAddOn_UNLIMITED_ACCOUNTS_Keywords": "accounts, unlimited, premium, add-on", + "WinoAccount_Management_PurchaseRequiresSignIn": "Sign in with your Wino Account to complete this purchase.", + "WinoAccount_Management_PurchaseStartFailed": "Wino could not complete this Microsoft Store purchase.", + "WinoAccount_Management_StoreSyncFailed": "Your purchase finished, but Wino could not refresh your account benefits yet. Please try again in a moment.", + "WinoAccount_Management_AiPackSubscriptionActive": "Your subscription is active", + "WinoAccount_Management_AiPackRenews": "Renews {0:d}", + "WinoAccount_Management_AiPackRequestsUsed": "Credits used this month", + "WinoAccount_Management_AiPackResets": "Resets {0:d}", + "WinoAccount_Management_AiPackUsageLoadFailed": "We had issues loading your AI usage balance.", + "WinoAccount_Management_AiPackFeatureTranslate": "Translate", + "WinoAccount_Management_AiPackFeatureRewrite": "Rewrite", + "WinoAccount_Management_AiPackFeatureSummarize": "Summarize", + "WinoAccount_Management_AddOnLoadFailed": "We had issues loading this add-on.", + "WinoAccount_Management_SyncPreferencesTitle": "Synchronize Preferences and Accounts", + "WinoAccount_Management_SyncPreferencesDescription": "Import or export your Wino preferences and mailbox details across devices. Passwords, tokens, and other sensitive information are never synced.", + "WinoAccount_Management_SignOutTitle": "Sign out", + "WinoAccount_Management_SignOutDescription": "Sign out of your account on this device", + "WinoAccount_Management_StatusLabel": "Status: {0}", + "WinoAccount_Management_NoRemoteSettings": "There is no synchronized data stored for this account yet.", + "WinoAccount_Management_ExportSucceeded": "Your selected Wino data was exported successfully.", + "WinoAccount_Management_ExportPreferencesSucceeded": "Your preferences were exported to your Wino Account.", + "WinoAccount_Management_ExportAccountsSucceeded": "Exported {0} account details to your Wino Account.", + "WinoAccount_Management_ImportSucceeded": "Imported synchronized data from your Wino Account.", + "WinoAccount_Management_ImportPreferencesSucceeded": "Applied {0} synchronized preferences.", + "WinoAccount_Management_ImportAccountsSucceeded": "Imported {0} accounts.", + "WinoAccount_Management_ImportDuplicateAccountsSkipped": "Skipped {0} accounts that already exist on this device.", + "WinoAccount_Management_ImportPartial": "Applied {0} synchronized preferences. {1} preferences could not be restored.", + "WinoAccount_Management_ImportReloginReminder": "Passwords, tokens, and other sensitive information were not imported. Sign in again for each account on this device before using it.", + "WinoAccount_Management_SerializeFailed": "Wino could not serialize your current preferences.", + "WinoAccount_Management_EmptyExport": "There are no preference values to export.", + "WinoAccount_Management_ImportEmpty": "The synchronized data payload does not contain anything new to restore.", + "WinoAccount_Management_ExportDialog_Title": "Export to your Wino Account", + "WinoAccount_Management_ExportDialog_Description": "Choose what you want to sync to your Wino Account.", + "WinoAccount_Management_ExportDialog_IncludePreferences": "Preferences", + "WinoAccount_Management_ExportDialog_IncludeAccounts": "Accounts", + "WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Passwords, tokens, and other sensitive information are not synced.", + "WinoAccount_Management_ExportDialog_AccountsRelogin": "Imported accounts on another PC will still need you to sign in again before they can be used.", + "WinoAccount_Management_ExportDialog_InProgress": "Exporting your selected Wino data...", + "WinoAccount_Management_LoadFailed": "Wino could not load the latest Wino Account information.", + "WinoAccount_Management_ActionFailed": "The Wino Account request could not be completed.", + "WinoAccount_SettingsSection_Title": "Wino Account", + "WinoAccount_SettingsSection_Description": "Create or sign in to a Wino Account using your localhost auth service.", + "WinoAccount_RegisterButton_Title": "Register account", + "WinoAccount_RegisterButton_Description": "Create a Wino Account with email and password.", + "WinoAccount_RegisterButton_Action": "Open registration", + "WinoAccount_LoginButton_Title": "Sign in", + "WinoAccount_LoginButton_Description": "Sign in to an existing Wino Account with email and password.", + "WinoAccount_LoginButton_Action": "Open login", + "WinoAccount_SignOutButton_Title": "Sign out", + "WinoAccount_SignOutButton_Description": "Remove the locally stored Wino Account session.", + "WinoAccount_SignOutButton_Action": "Sign out", + "WinoAccount_RegisterDialog_Title": "Create Wino Account", + "WinoAccount_RegisterDialog_Description": "Create a Wino Account to keep your Wino experience in sync and unlock account-based add-ons.", + "WinoAccount_RegisterDialog_HeroTitle": "Create your Wino Account", + "WinoAccount_RegisterDialog_BenefitsTitle": "Why create one?", + "WinoAccount_RegisterDialog_BenefitSyncTitle": "Import and export settings across devices", + "WinoAccount_RegisterDialog_BenefitSyncDescription": "Move your Wino preferences between devices without rebuilding your setup from scratch.", + "WinoAccount_RegisterDialog_BenefitAiTitle": "Access exclusive add-ons like Wino AI Pack (paid)", + "WinoAccount_RegisterDialog_BenefitAiDescription": "Use one account to unlock premium Wino features as they become available.", + "WinoAccount_RegisterDialog_DifferenceTitle": "Wino Account is separate from your mail accounts", + "WinoAccount_RegisterDialog_DifferenceDescription": "Your Outlook, Gmail, IMAP, or other email accounts stay exactly as they are. A Wino Account only manages Wino-specific features and account-based add-ons.", + "WinoAccount_RegisterDialog_PrimaryButton": "Register", + "WinoAccount_RegisterDialog_PrivacyTitle": "Privacy and API processing", + "WinoAccount_RegisterDialog_PrivacyDescription": "Optional add-ons such as Wino AI Pack may send selected email HTML content to the Wino API service only when you use those features.", + "WinoAccount_RegisterDialog_PrivacyLinkText": "Read the privacy policy", + "WinoAccount_RegisterDialog_PrivacyCheckbox": "I agree to the privacy policy.", + "WinoAccount_LoginDialog_Title": "Sign In to Wino Account", + "WinoAccount_LoginDialog_Description": "Sign in to your Wino Account to sync your Wino setup and access account-based features.", + "WinoAccount_LoginDialog_HeroTitle": "Welcome back", + "WinoAccount_LoginDialog_BenefitsTitle": "What signing in gives you", + "WinoAccount_LoginDialog_BenefitsDescription": "Use your Wino Account to continue syncing settings across devices and to access paid add-ons such as Wino AI Pack.", + "WinoAccount_LoginDialog_DifferenceTitle": "This is not your email mailbox sign-in", + "WinoAccount_LoginDialog_DifferenceDescription": "Signing in here does not add or replace your Outlook, Gmail, or IMAP accounts in Wino. It only signs you in to Wino-specific services.", + "WinoAccount_LoginDialog_ForgotPasswordLink": "Forgot password?", + "WinoAccount_EmailLabel": "Email", + "WinoAccount_EmailPlaceholder": "name@example.com", + "WinoAccount_PasswordLabel": "Password", + "WinoAccount_ConfirmPasswordLabel": "Confirm password", + "WinoAccount_ForgotPasswordDialog_Title": "Reset your password", + "WinoAccount_ForgotPasswordDialog_PrimaryButton": "Send reset email", + "WinoAccount_ForgotPasswordDialog_BackToSignIn": "Back to sign in", + "WinoAccount_ForgotPasswordDialog_Description": "Enter your Wino Account email address and we will send you a password reset link if the address is registered.", + "WinoAccount_Validation_EmailRequired": "Email is required.", + "WinoAccount_Validation_PasswordRequired": "Password is required.", + "WinoAccount_Validation_PasswordMismatch": "Passwords do not match.", + "WinoAccount_Validation_PrivacyConsentRequired": "You must accept the privacy policy before creating a Wino Account.", + "WinoAccount_Error_InvalidCredentials": "The email address or password is incorrect.", + "WinoAccount_Error_AccountLocked": "This account is temporarily locked.", + "WinoAccount_Error_AccountBanned": "This account has been banned.", + "WinoAccount_Error_AccountSuspended": "This account has been suspended.", + "WinoAccount_Error_EmailNotConfirmed": "Please confirm your email address before signing in.", + "WinoAccount_Error_EmailConfirmationRequired": "Please confirm your email address before signing in.", + "WinoAccount_Error_EmailConfirmationResendNotAvailable": "A new confirmation email is not available yet.", + "WinoAccount_Error_EmailConfirmationResendInvalid": "This confirmation request is no longer valid. Please try signing in again.", + "WinoAccount_Error_EmailNotRegistered": "This email address is not registered.", + "WinoAccount_Error_RefreshTokenInvalid": "Your session is no longer valid. Please sign in again.", + "WinoAccount_Error_EmailAlreadyRegistered": "This email address is already registered.", + "WinoAccount_Error_ExternalLoginEmailRequired": "An email address is required to complete external sign-in.", + "WinoAccount_Error_ExternalLoginInvalid": "The external sign-in request is invalid.", + "WinoAccount_Error_ExternalAuthStateInvalid": "The external sign-in state is invalid or expired.", + "WinoAccount_Error_ExternalAuthCodeInvalid": "The external sign-in code is invalid or expired.", + "WinoAccount_Error_AiPackRequired": "An active Wino AI Pack subscription is required for this action.", + "WinoAccount_Error_AiQuotaExceeded": "Your AI Pack usage limit has been reached for the current billing period.", + "WinoAccount_Error_AiHtmlEmpty": "There is no email content to process.", + "WinoAccount_Error_AiHtmlTooLarge": "This email is too large to process with Wino AI.", + "WinoAccount_Error_AiUnsupportedLanguage": "That language is not supported. Try a valid culture code such as en-US or tr-TR.", + "WinoAccount_Error_Forbidden": "You do not have permission to perform this action.", + "WinoAccount_Error_ValidationFailed": "The request is invalid. Please review the entered values.", + "WinoAccount_RegisterSuccessMessage": "Wino Account registration completed for {0}.", + "WinoAccount_LoginSuccessMessage": "Signed in to Wino Account as {0}.", + "WinoAccount_EmailConfirmationSentDialog_Title": "Confirm your email address", + "WinoAccount_EmailConfirmationSentDialog_Message": "We sent an email confirmation to {0}. Please confirm it and try signing in again.", + "WinoAccount_EmailConfirmationPendingDialog_Title": "Email confirmation required", + "WinoAccount_EmailConfirmationPendingDialog_Message": "We are still waiting for you to confirm {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ResendButton": "Resend confirmation email", + "WinoAccount_EmailConfirmationPendingDialog_Countdown": "You can resend the confirmation email in {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ReadyToResend": "You can resend the confirmation email now.", + "WinoAccount_EmailConfirmationResentDialog_Title": "Confirmation email resent", + "WinoAccount_EmailConfirmationResentDialog_Message": "We sent another confirmation email to {0}. Please confirm it and try signing in again.", + "WinoAccount_ForgotPasswordDialog_SuccessTitle": "Password reset email sent", + "WinoAccount_ForgotPasswordDialog_SuccessMessage": "We sent a password reset email to {0}. Open that message to choose a new password.", + "WinoAccount_ChangePassword_Title": "Change password", + "WinoAccount_ChangePassword_Description": "Send a password reset email to this Wino Account.", + "WinoAccount_ChangePassword_Action": "Send reset email", + "WinoAccount_ChangePassword_ConfirmationMessage": "Do you want Wino to send a password reset email to {0}?", + "WinoAccount_SignOut_SuccessMessage": "Signed out from Wino Account {0}.", + "WinoAccount_SignOut_NoAccountMessage": "There is no active Wino Account to sign out.", + "WinoAccount_Titlebar_SignedOutTitle": "Wino Account", + "WinoAccount_Titlebar_SignedOutDescription": "Sign in or create a Wino Account to manage your Wino session.", + "WinoAccount_Titlebar_SignedInStatus": "Status: {0}", + "WelcomeWizard_Step2Title": "Add Account", + "WelcomeWizard_Step3Title": "Finish Setup", + "ProviderSelection_Title": "Choose your email provider", + "ProviderSelection_Subtitle": "Select a provider below to add your email account to Wino Mail.", + "ProviderSelection_AccountNameHeader": "Account Name", + "ProviderSelection_AccountNamePlaceholder": "e.g. Personal, Work", + "ProviderSelection_DisplayNameHeader": "Display Name", + "ProviderSelection_DisplayNamePlaceholder": "e.g. John Doe", + "ProviderSelection_EmailHeader": "E-mail Address", + "ProviderSelection_EmailPlaceholder": "e.g. johndoe@example.com", + "ProviderSelection_AppPasswordHeader": "App-Specific Password", + "ProviderSelection_AppPasswordHelp": "How do I get an app-specific password?", + "ProviderSelection_CalendarModeHeader": "Calendar Integration", + "ProviderSelection_CalendarMode_DisabledTitle": "Disabled", + "ProviderSelection_CalendarMode_DisabledDescription": "No calendar integration", + "ProviderSelection_CalendarMode_CalDavTitle": "CalDAV Synchronization", + "ProviderSelection_CalendarMode_CalDavDescription_Apple": "Your calendar events are synced to Apple servers between your devices.", + "ProviderSelection_CalendarMode_CalDavDescription_Yahoo": "Your calendar events are synced to Yahoo servers between your devices.", + "ProviderSelection_CalendarMode_LocalTitle": "Local calendar", + "ProviderSelection_CalendarMode_LocalDescription": "Your events are stored only on your computer. No server connectivity.", + "ProviderSelection_ClearColor": "Clear color", + "ProviderSelection_ContinueButton": "Continue", + "ProviderSelection_SpecialImap_Subtitle": "Enter your account credentials to connect.", + "AccountSetup_Title": "Setting up your account", + "AccountSetup_Step_Authenticating": "Authenticating with {0}", + "AccountSetup_Step_TestingMailAuth": "Testing mail authentication", + "AccountSetup_Step_SyncingFolders": "Synchronizing folder metadata", + "AccountSetup_Step_FetchingProfile": "Fetching profile information", + "AccountSetup_Step_DiscoveringCalDav": "Discovering CalDAV settings", + "AccountSetup_Step_TestingCalendarAuth": "Testing calendar authentication", + "AccountSetup_Step_SavingAccount": "Saving account information", + "AccountSetup_Step_FetchingCalendarMetadata": "Fetching calendar metadata", + "AccountSetup_Step_SyncingAliases": "Synchronizing aliases", + "AccountSetup_Step_Finalizing": "Finalizing setup", + "AccountSetup_FailureMessage": "Setup failed. Go back to fix your settings, or try again later.", + "AccountSetup_SuccessMessage": "Your account has been set up successfully!", + "AccountSetup_GoBackButton": "Go Back", + "AccountSetup_TryAgainButton": "Try Again", + "ImapCalDavSettings_AutoDiscoveryFailed": "Auto-discovery failed. Please enter settings manually in the Advanced tab." } - - diff --git a/Wino.Core.Domain/Translations/es_ES/resources.json b/Wino.Core.Domain/Translations/es_ES/resources.json index 6857cbc7..b7a77f3c 100644 --- a/Wino.Core.Domain/Translations/es_ES/resources.json +++ b/Wino.Core.Domain/Translations/es_ES/resources.json @@ -8,6 +8,7 @@ "AccountCacheReset_Message": "Esta cuenta requiere la resincronización completa para continuar trabajando. Por favor, espera mientras Wino sincroniza tus mensajes...", "AccountContactNameYou": "Usted", "AccountCreationDialog_Completed": "todo hecho", + "AccountCreationDialog_FetchingCalendarMetadata": "Obteniendo detalles del calendario.", "AccountCreationDialog_FetchingEvents": "Obteniendo eventos del calendario.", "AccountCreationDialog_FetchingProfileInformation": "Obteniendo información del perfil.", "AccountCreationDialog_GoogleAuthHelpClipboardText_Row0": "Si su navegador no se ha abierto automáticamente para completar la autentificación:", @@ -17,6 +18,7 @@ "AccountCreationDialog_Initializing": "inicializando", "AccountCreationDialog_PreparingFolders": "Estamos obteniendo información de la carpeta en este momento.", "AccountCreationDialog_SigninIn": "La información de la cuenta se está guardando.", + "Purchased": "Comprado", "AccountEditDialog_Message": "Nombre de la Cuenta", "AccountEditDialog_Title": "Editar cuenta", "AccountPickerDialog_Title": "Elija una cuenta", @@ -26,6 +28,10 @@ "AccountDetailsPage_Description": "Cambie el nombre de la cuenta en Wino y establezca el nombre deseado del remitente.", "AccountDetailsPage_ColorPicker_Title": "Color de la cuenta", "AccountDetailsPage_ColorPicker_Description": "Asigna un nuevo color de cuenta para colorear su símbolo en la lista.", + "AccountDetailsPage_TabGeneral": "General", + "AccountDetailsPage_TabMail": "Correo", + "AccountDetailsPage_TabCalendar": "Calendario", + "AccountDetailsPage_CalendarListDescription": "Selecciona un calendario para configurar sus ajustes", "AddHyperlink": "Añadir", "AppCloseBackgroundSynchronizationWarningTitle": "Sincronización en segundo plano", "AppCloseStartupLaunchDisabledWarningMessageFirstLine": "La aplicación no está configurada para iniciarse con Windows.", @@ -47,8 +53,10 @@ "BasicIMAPSetupDialog_Title": "Cuenta IMAP", "Busy": "Ocupado/a", "Buttons_AddAccount": "Añadir Cuenta", + "Buttons_FixAccount": "Reparar cuenta", "Buttons_AddNewAlias": "Añadir nuevo alias", "Buttons_Allow": "Permitir", + "Buttons_Apply": "Aplicar", "Buttons_ApplyTheme": "Aplicar Tema", "Buttons_Browse": "Buscar", "Buttons_Cancel": "Cancelar", @@ -62,6 +70,7 @@ "Buttons_Edit": "Editar", "Buttons_EnableImageRendering": "Activar", "Buttons_Multiselect": "Selección múltiple", + "Buttons_Manage": "Gestionar", "Buttons_No": "No", "Buttons_Open": "Abrir", "Buttons_Purchase": "Comprar", @@ -70,15 +79,134 @@ "Buttons_Save": "Guardar", "Buttons_SaveConfiguration": "Guardar Configuración", "Buttons_Send": "Enviar", + "Buttons_SendToServer": "Enviar al servidor", "Buttons_Share": "Compartir", "Buttons_SignIn": "Iniciar Sesión", "Buttons_Sync": "Sincronizar", "Buttons_SyncAliases": "Sincronizar alias", "Buttons_TryAgain": "Inténtalo de nuevo", "Buttons_Yes": "Sí", + "Sync_SynchronizingFolder": "Sincronizando {0} {1}%", + "Sync_DownloadedMessages": "Descargados {0} mensajes de {1}", + "SyncAction_Archiving": "Archivando {0} correo(s)", + "SyncAction_ClearingFlag": "Desmarcando {0} correo(s)", + "SyncAction_CreatingDraft": "Creando borrador", + "SyncAction_CreatingEvent": "Creando evento", + "SyncAction_Deleting": "Eliminando {0} correo(s)", + "SyncAction_EmptyingFolder": "Vaciando la carpeta", + "SyncAction_MarkingAsRead": "Marcando {0} correo(s) como leído", + "SyncAction_MarkingAsUnread": "Marcando {0} correo(s) como no leído", + "SyncAction_MarkingFolderAsRead": "Marcando la carpeta como leída", + "SyncAction_Moving": "Moviendo {0} correo(s)", + "SyncAction_MovingToFocused": "Moviendo {0} correo(s) a Enfocados", + "SyncAction_RenamingFolder": "Renombrando carpeta", + "SyncAction_SendingMail": "Enviando correo", + "SyncAction_SettingFlag": "Marcando {0} correo(s) con bandera", + "SyncAction_SynchronizingAccount": "Sincronizando {0}", + "SyncAction_SynchronizingAccounts": "Sincronizando {0} cuenta(s)", + "SyncAction_SynchronizingCalendarData": "Sincronizando datos del calendario", + "SyncAction_SynchronizingCalendarEvents": "Sincronizando eventos del calendario", + "SyncAction_SynchronizingCalendarMetadata": "Sincronizando metadatos del calendario", + "SyncAction_Unarchiving": "Desarchivando {0} correo(s)", "CalendarAllDayEventSummary": "eventos de todo el día", "CalendarDisplayOptions_Color": "Color", "CalendarDisplayOptions_Expand": "Expandir", + "CalendarEventResponse_Accept": "Aceptar", + "CalendarEventResponse_AcceptedResponse": "Aceptado", + "CalendarEventResponse_Decline": "Declinar", + "CalendarEventResponse_DeclinedResponse": "Declinado", + "CalendarEventResponse_NotResponded": "Sin responder", + "CalendarEventResponse_Tentative": "Provisional", + "CalendarEventResponse_TentativeResponse": "Provisional", + "CalendarEventRsvpPanel_Accept": "Aceptar", + "CalendarEventRsvpPanel_AddMessage": "Añadir un mensaje a tu respuesta... (opcional)", + "CalendarEventRsvpPanel_Decline": "Declinar", + "CalendarEventRsvpPanel_Message": "Mensaje", + "CalendarEventRsvpPanel_SendReplyMessage": "Enviar mensaje de respuesta", + "CalendarEventRsvpPanel_Tentative": "Provisional", + "CalendarEventRsvpPanel_Title": "Opciones de respuesta", + "CalendarAttendeeStatus_Accepted": "Aceptado", + "CalendarAttendeeStatus_Declined": "Rechazado", + "CalendarAttendeeStatus_NeedsAction": "Necesita acción", + "CalendarAttendeeStatus_Tentative": "Provisional", + "CalendarEventDetails_Attachments": "Adjuntos", + "CalendarEventCompose_AddAttachment": "Añadir adjunto", + "CalendarEventCompose_AllDay": "Todo el día", + "CalendarEventCompose_AttachmentsNotSupportedForCalDav": "Los adjuntos no son compatibles con calendarios CalDAV.", + "CalendarEventCompose_EndDate": "Fecha de finalización", + "CalendarEventCompose_EndTime": "Hora de finalización", + "CalendarEventCompose_Every": "cada", + "CalendarEventCompose_ForWeekdays": "para", + "CalendarEventCompose_FrequencyDay": "día", + "CalendarEventCompose_FrequencyDayPlural": "días", + "CalendarEventCompose_FrequencyMonth": "mes", + "CalendarEventCompose_FrequencyMonthPlural": "meses", + "CalendarEventCompose_FrequencyWeek": "semana", + "CalendarEventCompose_FrequencyWeekPlural": "semanas", + "CalendarEventCompose_FrequencyYear": "año", + "CalendarEventCompose_FrequencyYearPlural": "años", + "CalendarEventCompose_Location": "Ubicación", + "CalendarEventCompose_LocationPlaceholder": "Añadir ubicación", + "CalendarEventCompose_NewEventButton": "Nuevo evento", + "CalendarEventCompose_DefaultCalendarHint": "Puedes seleccionar un calendario predeterminado para los nuevos eventos en la configuración del calendario.", + "CalendarEventCompose_DefaultCalendarSettingsLink": "Abrir la configuración del calendario", + "CalendarEventCompose_NoCalendarsMessage": "Aún no hay calendarios disponibles para crear un evento.", + "CalendarEventCompose_NoCalendarsTitle": "No hay calendarios disponibles", + "CalendarEventCompose_NoEndDate": "Sin fecha de finalización", + "CalendarEventCompose_Notes": "Notas", + "CalendarEventCompose_PickCalendarTitle": "Elegir un calendario", + "CalendarEventCompose_Recurring": "Recurrente", + "CalendarEventCompose_RecurringSummary": "Ocurre cada {0} {1}{2} {3} efectivo {4}{5}", + "CalendarEventCompose_RecurringSummarySmart": "Ocurre {0}{1} {2} efectivo {3}{4}", + "CalendarEventCompose_RepeatEvery": "Repite cada", + "CalendarEventCompose_SelectCalendar": "Seleccionar calendario", + "CalendarEventCompose_SingleOccurrenceSummary": "Ocurre el {0} {1}", + "CalendarEventCompose_StartDate": "Fecha de inicio", + "CalendarEventCompose_StartTime": "Hora de inicio", + "CalendarEventCompose_TimeRangeSummary": "desde {0} hasta {1}", + "CalendarEventCompose_Title": "Título del evento", + "CalendarEventCompose_TitlePlaceholder": "Añadir un título", + "CalendarEventCompose_Until": "hasta", + "CalendarEventCompose_UntilSummary": " hasta {0}", + "CalendarEventCompose_ValidationInvalidAllDayRange": "La fecha de finalización para todo el día debe ser posterior a la fecha de inicio.", + "CalendarEventCompose_ValidationInvalidAttendee": "Uno o más asistentes tienen una dirección de correo electrónico no válida.", + "CalendarEventCompose_ValidationInvalidRecurrenceEnd": "La fecha de finalización de la recurrencia debe ser igual o posterior a la fecha de inicio del evento.", + "CalendarEventCompose_ValidationInvalidTimeRange": "La hora de finalización debe ser posterior a la hora de inicio.", + "CalendarEventCompose_ValidationMissingAttachment": "Uno o más adjuntos ya no están disponibles: {0}", + "CalendarEventCompose_ValidationMissingCalendar": "Selecciona un calendario antes de crear el evento.", + "CalendarEventCompose_ValidationMissingTitle": "Introduce un título del evento antes de crear el evento.", + "CalendarEventCompose_ValidationTitle": "La validación del evento falló", + "CalendarEventCompose_WeekdaySummary": " en {0}", + "CalendarEventCompose_Weekday_Friday": "V", + "CalendarEventCompose_Weekday_Monday": "L", + "CalendarEventCompose_Weekday_Saturday": "S", + "CalendarEventCompose_Weekday_Sunday": "D", + "CalendarEventCompose_Weekday_Thursday": "J", + "CalendarEventCompose_Weekday_Tuesday": "M", + "CalendarEventCompose_Weekday_Wednesday": "X", + "CalendarEventDetails_Details": "Detalles", + "CalendarEventDetails_EditSeries": "Editar serie", + "CalendarEventDetails_Editing": "Editando", + "CalendarEventDetails_InviteSomeone": "Invitar a alguien", + "CalendarEventDetails_JoinOnline": "Unirse en línea", + "CalendarEventDetails_Organizer": "Organizador", + "CalendarEventDetails_People": "Personas", + "CalendarEventDetails_ReadOnlyEvent": "Evento de solo lectura", + "CalendarEventDetails_Reminder": "Recordatorio", + "CalendarReminder_StartedHoursAgo": "Hace {0} horas", + "CalendarReminder_StartedMinutesAgo": "Hace {0} minutos", + "CalendarReminder_StartedNow": "Acaba de empezar", + "CalendarReminder_StartingNow": "Comienza ahora", + "CalendarReminder_StartsInHours": "Comienza en {0} horas", + "CalendarReminder_StartsInMinutes": "Comienza en {0} minutos", + "CalendarReminder_SnoozeAction": "Posponer", + "CalendarReminder_SnoozeMinutesOption": "{0} minutos", + "CalendarEventDetails_ShowAs": "Mostrar como", + "CalendarShowAs_Free": "Libre", + "CalendarShowAs_Tentative": "Provisional", + "CalendarShowAs_Busy": "Ocupado", + "CalendarShowAs_OutOfOffice": "Fuera de la oficina", + "CalendarShowAs_WorkingElsewhere": "Trabajando en otro lugar", "CalendarItem_DetailsPopup_JoinOnline": "Unirse en línea", "CalendarItem_DetailsPopup_ViewEventButton": "Ver evento", "CalendarItem_DetailsPopup_ViewSeriesButton": "Ver serie", @@ -88,6 +216,9 @@ "ClipboardTextCopied_Message": "{0} copiado al portapapeles.", "ClipboardTextCopied_Title": "Copiado", "ClipboardTextCopyFailed_Message": "Error al copiar {0} al portapapeles.", + "ContactInfoBar_ErrorTitle": "Error al cargar la información de contacto", + "ContactInfoBar_SuccessTitle": "La información de contacto se ha cargado", + "ContactInfoBar_WarningTitle": "La información de contacto podría estar incompleta", "ComingSoon": "Próximamente...", "ComposerAttachmentsDragDropAttach_Message": "Adjuntar", "ComposerAttachmentsDropZone_Message": "Suelta tus archivos aquí", @@ -129,6 +260,10 @@ "DialogMessage_CreateLinkedAccountTitle": "Nombre del enlace de cuenta", "DialogMessage_DeleteAccountConfirmationMessage": "¿Eliminar {0}?", "DialogMessage_DeleteAccountConfirmationTitle": "Todos los datos asociados a esta cuenta se eliminarán del disco permanentemente.", + "DialogMessage_DeleteEmailTemplateConfirmationMessage": "Eliminar la plantilla \"{0}\"?", + "DialogMessage_DeleteEmailTemplateConfirmationTitle": "Eliminar plantilla de correo", + "DialogMessage_DeleteRecurringSeriesMessage": "Esto eliminará todos los eventos de la serie. ¿Desea continuar?", + "DialogMessage_DeleteRecurringSeriesTitle": "Eliminar serie recurrente", "DialogMessage_DiscardDraftConfirmationMessage": "Este borrador se descartará. ¿Desea continuar?", "DialogMessage_DiscardDraftConfirmationTitle": "Descartar borrador", "DialogMessage_EmptySubjectConfirmation": "Sin asunto", @@ -172,11 +307,18 @@ "ElementTheme_Light": "Modo claro", "Emoji": "Emoji", "Error_FailedToSetupSystemFolders_Title": "Error al configurar las carpetas del sistema", + "Exception_AccountNeedsAttention_Title": "La cuenta necesita atención", + "Exception_AccountNeedsAttention_Message": "'{0}' requiere su atención para continuar trabajando.", + "Exception_WebView2RuntimeMissing_Message": "Wino Mail no pudo encontrar Microsoft Edge WebView2 Runtime. Por favor, instale o repare el tiempo de ejecución para renderizar correctamente el contenido de los mensajes.", + "Exception_WebView2RuntimeMissing_Title": "Se requiere WebView2 runtime", "Exception_AuthenticationCanceled": "Autenticación cancelada", "Exception_CustomThemeExists": "Este tema ya existe.", "Exception_CustomThemeMissingName": "Debe proporcionar un nombre.", "Exception_CustomThemeMissingWallpaper": "Debe proporcionar una imagen de fondo personalizada.", "Exception_FailedToSynchronizeAliases": "Error al sincronizar los alias", + "Exception_FailedToSynchronizeCalendarData": "No se pudo sincronizar los datos del calendario.", + "Exception_FailedToSynchronizeCalendarEvents": "No se pudieron sincronizar los eventos del calendario.", + "Exception_FailedToSynchronizeCalendarMetadata": "No se pudieron sincronizar los detalles del calendario.", "Exception_FailedToSynchronizeFolders": "Error al sincronizar carpetas", "Exception_FailedToSynchronizeProfileInformation": "Error al sincronizar la información del perfil", "Exception_GoogleAuthCallbackNull": "Callback uri nulo al activarse.", @@ -229,6 +371,32 @@ "HoverActionOption_MoveJunk": "Mover a Correo no deseado", "HoverActionOption_ToggleFlag": "Marcar / Desmarcar", "HoverActionOption_ToggleRead": "Leídos / Sin leer", + "KeyboardShortcuts_FailedToReset": "No se pudieron restablecer los atajos de teclado.", + "KeyboardShortcuts_FailedToUpdate": "No se pudieron actualizar los atajos de teclado.", + "KeyboardShortcuts_MailoperationAction": "Acción", + "KeyboardShortcuts_Action": "Acción", + "KeyboardShortcuts_FailedToLoad": "No se pudieron cargar los atajos de teclado.", + "KeyboardShortcuts_EnterKeyForShortcut": "Por favor, introduzca una tecla para el atajo.", + "KeyboardShortcuts_SelectOperationForShortcut": "Por favor, seleccione una acción para el atajo.", + "KeyboardShortcuts_EnterKey": "Por favor, introduzca una tecla para el atajo.", + "KeyboardShortcuts_SelectOperation": "Por favor, seleccione una acción para el atajo.", + "KeyboardShortcuts_ShortcutInUse": "Este atajo ya está en uso por otro atajo.", + "KeyboardShortcuts_FailedToSave": "No se pudo guardar el atajo.", + "KeyboardShortcuts_FailedToDelete": "No se pudo eliminar el atajo.", + "KeyboardShortcuts_PageDescription": "Configurar atajos de teclado para operaciones rápidas de correo. Presione las teclas mientras el foco está en el campo de entrada de teclas para capturar atajos.", + "KeyboardShortcuts_Add": "Añadir atajo", + "KeyboardShortcuts_EditTitle": "Editar atajo de teclado", + "KeyboardShortcuts_ResetToDefaults": "Restablecer a valores predeterminados", + "KeyboardShortcuts_PressKeysHere": "Presione las teclas aquí...", + "KeyboardShortcuts_KeyCombination": "Combinación de teclas", + "KeyboardShortcuts_FocusArea": "Coloque el foco en el campo de arriba y presione la combinación de teclas deseada", + "KeyboardShortcuts_Modifiers": "Teclas modificadoras", + "KeyboardShortcuts_Mode": "Modo de la aplicación", + "KeyboardShortcuts_ModeMail": "Correo", + "KeyboardShortcuts_ModeCalendar": "Calendario", + "KeyboardShortcuts_ActionToggleReadUnread": "Alternar leído/no leído", + "KeyboardShortcuts_ActionToggleFlag": "Alternar bandera", + "KeyboardShortcuts_ActionToggleArchive": "Alternar archivar/desarchivar", "ImageRenderingDisabled": "El procesamiento de imágenes está desactivado para este mensaje.", "ImapAdvancedSetupDialog_AuthenticationMethod": "Método de autenticación", "ImapAdvancedSetupDialog_ConnectionSecurity": "Seguridad de la conexión", @@ -295,12 +463,58 @@ "IMAPSetupDialog_Username": "Nombre de usuario", "IMAPSetupDialog_UsernamePlaceholder": "fulanomengano, fulanomengano@fabrikam.com, dominio/fulanomengano", "IMAPSetupDialog_UseSameConfig": "Usar el mismo correo y contraseña para enviar correos", + "ImapCalDavSettingsPage_TitleCreate": "Configuración de IMAP y Calendario", + "ImapCalDavSettingsPage_TitleEdit": "Editar configuración de IMAP y Calendario", + "ImapCalDavSettingsPage_Subtitle": "Configurar IMAP/SMTP y la sincronización de calendario opcional para esta cuenta.", + "ImapCalDavSettingsPage_BasicSectionTitle": "Configuración básica", + "ImapCalDavSettingsPage_BasicSectionDescription": "Introduce tu identidad y credenciales. Wino puede intentar detectar la configuración del servidor automáticamente.", + "ImapCalDavSettingsPage_BasicTab": "Básico", + "ImapCalDavSettingsPage_EnableCalendarSupport": "Habilitar soporte de calendario", + "ImapCalDavSettingsPage_AutoDiscoverButton": "Descubrir automáticamente la configuración de correo", + "ImapCalDavSettingsPage_AutoDiscoverySuccessMessage": "La configuración de correo se ha detectado y aplicado.", + "ImapCalDavSettingsPage_AdvancedSectionTitle": "Configuración avanzada", + "ImapCalDavSettingsPage_AdvancedSectionDescription": "Introduce manualmente la configuración del servidor si la detección automática no está disponible o es incorrecta.", + "ImapCalDavSettingsPage_AdvancedTab": "Avanzado", + "ImapCalDavSettingsPage_CalendarSectionTitle": "Configuración del calendario", + "ImapCalDavSettingsPage_CalendarSectionDescription": "Elige cómo deben funcionar los datos del calendario para esta cuenta IMAP.", + "ImapCalDavSettingsPage_CalendarModeHeader": "Modo de calendario", + "ImapCalDavSettingsPage_ConnectionSecurityHeader": "Seguridad de la conexión", + "ImapCalDavSettingsPage_AuthenticationMethodHeader": "Método de autenticación", + "ImapCalDavSettingsPage_CalendarModeDisabled": "Desactivado", + "ImapCalDavSettingsPage_CalendarModeCalDav": "Sincronización CalDAV", + "ImapCalDavSettingsPage_CalendarModeLocalOnly": "Sólo calendario local", + "ImapCalDavSettingsPage_CalendarModeDisabledDescription": "El calendario está desactivado para esta cuenta.", + "ImapCalDavSettingsPage_CalendarModeCalDavDescription": "Los elementos del calendario se sincronizan con su servidor CalDAV.", + "ImapCalDavSettingsPage_CalendarModeLocalOnlyDescription": "Los elementos del calendario se almacenan solo en este equipo y no se sincronizan con la red.", + "ImapCalDavSettingsPage_LocalCalendarLearnMore": "Cómo funciona el calendario local", + "ImapCalDavSettingsPage_LocalCalendarDialogTitle": "Solo calendario local", + "ImapCalDavSettingsPage_LocalCalendarDialogMessage": "El calendario local mantiene todos los eventos solo en su ordenador. No se sincroniza con iCloud, Yahoo ni con ningún otro proveedor.", + "ImapCalDavSettingsPage_CalDavServiceUrl": "URL del servicio CalDAV", + "ImapCalDavSettingsPage_CalDavUsername": "Nombre de usuario CalDAV", + "ImapCalDavSettingsPage_CalDavPassword": "Contraseña CalDAV", + "ImapCalDavSettingsPage_CalDavNotRequiredMessage": "La prueba de CalDAV solo es necesaria cuando el modo de calendario está configurado en sincronización CalDAV.", + "ImapCalDavSettingsPage_CalDavUrlRequired": "Se requiere la URL del servicio CalDAV.", + "ImapCalDavSettingsPage_CalDavUrlInvalid": "La URL del servicio CalDAV debe ser una URL absoluta.", + "ImapCalDavSettingsPage_CalDavUsernameRequired": "Se requiere el nombre de usuario de CalDAV.", + "ImapCalDavSettingsPage_CalDavPasswordRequired": "Se requiere la contraseña de CalDAV.", + "ImapCalDavSettingsPage_TestImapButton": "Probar conexión IMAP", + "ImapCalDavSettingsPage_TestCalDavButton": "Probar conexión CalDAV", + "ImapCalDavSettingsPage_ImapTestSuccessMessage": "La prueba de conexión IMAP se realizó con éxito.", + "ImapCalDavSettingsPage_CalDavTestSuccessMessage": "La prueba de conexión CalDAV se realizó con éxito.", + "ImapCalDavSettingsPage_SaveSuccessMessage": "La configuración de la cuenta se ha validado y guardado.", + "ImapCalDavSettingsPage_ICloudHint": "Utilice una contraseña específica de la aplicación generada desde la configuración de su cuenta de Apple.", + "ImapCalDavSettingsPage_YahooHint": "Utilice una contraseña de aplicación desde la configuración de seguridad de su cuenta de Yahoo.", "Info_AccountCreatedMessage": "{0} se ha creado", "Info_AccountCreatedTitle": "Creación de una cuenta", "Info_AccountCreationFailedTitle": "Ocurrió un error al crear la cuenta", "Info_AccountDeletedMessage": "{0} eliminado correctamente.", "Info_AccountDeletedTitle": "Cuenta eliminada", "Info_AccountIssueFixFailedTitle": "Fallido", + "Info_AccountIssueFixImapMessage": "Abra la página de configuración de IMAP y calendario para volver a introducir las credenciales de su servidor.", + "Info_AccountAttentionRequiredMessage": "Esta cuenta necesita atención.", + "Info_AccountAttentionRequiredClickableMessage": "Haga clic para solucionar esta cuenta y volver a sincronizarla.", + "Info_AccountAttentionRequiredAction": "Solucionar", + "Info_AccountAttentionRequiredActionHint": "Haga clic en Solucionar para resolver este problema de la cuenta.", "Info_AccountIssueFixSuccessMessage": "Se han corregido todos los problemas de la cuenta.", "Info_AccountIssueFixSuccessTitle": "Correcto", "Info_AttachmentOpenFailedMessage": "No se puede abrir este adjunto.", @@ -370,6 +584,7 @@ "InfoBarMessage_SynchronizationDisabledFolder": "Esta carpeta está desactivada para la sincronización.", "InfoBarTitle_SynchronizationDisabledFolder": "Carpeta desactivada", "Justify": "Justificar", + "MenuUpdateAvailable": "Actualización disponible", "Left": "Izquierda", "Link": "Enlace", "LinkedAccountsCreatePolicyMessage": "debes tener al menos 2 cuentas para crear el enlace\nenlace se eliminará al guardar", @@ -403,6 +618,7 @@ "MailOperation_Unarchive": "Desarchivar", "MailOperation_ViewMessageSource": "Ver fuente del mensaje", "MailOperation_Zoom": "Zoom", + "MailsDragging": "Arrastrando {0} elemento(s)", "MailsSelected": "{0} artículo(s) seleccionado(s)", "MarkFlagUnflag": "Marcar como marcado/desmarcado", "MarkReadUnread": "Marcar como leído/no leído", @@ -434,6 +650,8 @@ "Notifications_MultipleNotificationsTitle": "Mensajes nuevos", "Notifications_WinoUpdatedMessage": "Comprobar nueva versión {0}", "Notifications_WinoUpdatedTitle": "Wino Mail ha sido actualizado.", + "Notifications_StoreUpdateAvailableTitle": "Actualización disponible", + "Notifications_StoreUpdateAvailableMessage": "Una versión más reciente de Wino Mail está lista para instalar desde Microsoft Store.", "OnlineSearchFailed_Message": "Error al realizar la búsqueda\n{0}\n\nLista de correos sin conexión.", "OnlineSearchTry_Line1": "¿No encuentra lo que busca?", "OnlineSearchTry_Line2": "Prueba la búsqueda en línea.", @@ -446,7 +664,6 @@ "PaneLengthOption_Small": "Pequeño", "Photos": "Fotos", "PreparingFoldersMessage": "Preparando carpetas", - "ProtocolLogAvailable_Message": "Los registros de protocolo están disponibles para el diagnóstico.", "ProviderDetail_Gmail_Description": "Cuenta de Google", "ProviderDetail_iCloud_Description": "Cuenta de Apple iCloud", "ProviderDetail_iCloud_Title": "iCloud", @@ -465,9 +682,14 @@ "SearchBarPlaceholder": "Buscar", "SearchingIn": "Buscando en", "SearchPivotName": "Resultados", + "Settings_KeyboardShortcuts_Title": "Atajos de teclado", + "Settings_KeyboardShortcuts_Description": "Gestione los atajos de teclado para acciones rápidas en los correos.", "SettingConfigureSpecialFolders_Button": "Configurar", "SettingsEditAccountDetails_IMAPConfiguration_Title": "Configuración IMAP/SMTP", "SettingsEditAccountDetails_IMAPConfiguration_Description": "Cambie la configuración del servidor entrante/saliente.", + "SettingsEditAccountDetails_ImapCalDavSettings_Title": "Configuración de IMAP y calendario", + "SettingsEditAccountDetails_ImapCalDavSettings_Description": "Abra la página de configuración dedicada de IMAP, SMTP y CalDAV para esta cuenta.", + "SettingsEditAccountDetails_ImapCalDavSettings_Action": "Abrir configuración", "SettingsAbout_Description": "Conoce más sobre Wino.", "SettingsAbout_Title": "Acerca de", "SettingsAboutGithub_Description": "Ir al rastreador de problemas en el repositorio de GitHub.", @@ -490,6 +712,10 @@ "SettingsAppPreferences_SearchMode_Local": "Local", "SettingsAppPreferences_SearchMode_Online": "Conectado", "SettingsAppPreferences_SearchMode_Title": "Modo de búsqueda predeterminado", + "SettingsAppPreferences_ApplicationMode_Title": "Modo de aplicación predeterminado", + "SettingsAppPreferences_ApplicationMode_Description": "Elija en qué modo se abre Wino cuando no se establece explícitamente un tipo de activación.", + "SettingsAppPreferences_ApplicationMode_Mail": "Correo", + "SettingsAppPreferences_ApplicationMode_Calendar": "Calendario", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description": "Wino Mail seguirá funcionando en segundo plano. Se le notificará cuando lleguen nuevos correos.", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title": "Ejecutarse en segundo plano", "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description": "Wino Mail funcionará en la bandeja del sistema. Se lanzará haciendo clic en un icono. Se notificará como llegan nuevos correos.", @@ -506,12 +732,30 @@ "SettingsAppPreferences_StartupBehavior_FatalError": "Error fatal al cambiar el modo de inicio para Wino Mail.", "SettingsAppPreferences_StartupBehavior_Title": "Ejecutar minimizado al iniciar Windows", "SettingsAppPreferences_Title": "Configuración de la aplicación", + "SettingsAppPreferences_HideWinoAccountButton_Title": "Ocultar el botón de la cuenta de Wino en la barra de título", + "SettingsAppPreferences_HideWinoAccountButton_Description": "Ocultar el botón de perfil en la barra de título que abre el panel desplegable de la cuenta de Wino.", + "SettingsAppPreferences_StoreUpdateNotifications_Title": "Notificaciones de actualizaciones de Microsoft Store", + "SettingsAppPreferences_StoreUpdateNotifications_Description": "Mostrar notificaciones y acciones en el pie de página cuando haya una actualización de Microsoft Store disponible.", + "SettingsAppPreferences_AiActions_Title": "Acciones de IA", + "SettingsAppPreferences_AiActions_Description": "Elegir los idiomas de IA predeterminados y dónde guardar los resúmenes.", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Title": "Idioma de traducción predeterminado", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Description": "Seleccione el lenguaje de destino predeterminado utilizado por las acciones de traducción IA.", + "SettingsAppPreferences_AiSummarizeLanguage_Title": "Idioma de resumen", + "SettingsAppPreferences_AiSummarizeLanguage_Description": "Elija el idioma de resumen preferido para futuros resúmenes generados por IA.", + "SettingsAppPreferences_AiSummarySavePath_Title": "Ruta de guardado de resúmenes predeterminada", + "SettingsAppPreferences_AiSummarySavePath_Description": "Elija la carpeta que Wino debe usar por defecto al guardar resúmenes de IA.", + "SettingsAppPreferences_AiSummarySavePath_Placeholder": "Usar la ubicación de guardado predeterminada del sistema", + "SettingsAppPreferences_AiSummarySavePath_InvalidHint": "Esta carpeta no existe. Se utilizará la ubicación de guardado predeterminada para los resúmenes.", "SettingsAutoSelectNextItem_Description": "Seleccione el siguiente elemento después de eliminar o mover un correo.", "SettingsAutoSelectNextItem_Title": "Auto seleccionar siguiente elemento", "SettingsAvailableThemes_Description": "Escoge un tema que te agrade desde la colección de Wino o aplica tus propios temas.", "SettingsAvailableThemes_Title": "Temas Disponibles", "SettingsCalendarSettings_Description": "Modifica el primer día de la semana, tamaño de la hora y más.", "SettingsCalendarSettings_Title": "Ajustes del calendario", + "CalendarSettings_DefaultSnoozeDuration_Header": "Duración predeterminada de posponer", + "CalendarSettings_DefaultSnoozeDuration_Description": "Establezca una duración predeterminada de posponer para las notificaciones de recordatorio del calendario.", + "CalendarSettings_TimedDayHeaderFormat_Header": "Formato de cabecera de día en la vista con horario", + "CalendarSettings_TimedDayHeaderFormat_Description": "Elija cómo se renderizan las etiquetas de los días en las vistas de día, semana y semana laboral. Use tokens de formato de fecha como ddd, dd, MMM o dddd.", "SettingsComposer_Title": "Editor", "SettingsComposerFont_Title": "Fuente por defecto para Escribir", "SettingsComposerFontFamily_Description": "Cambie el tamaño por defecto de la familia de fuentes y del tipo de letra para escribir correos.", @@ -531,6 +775,9 @@ "SettingsDiscord_Title": "Canal de Discord", "SettingsEditLinkedInbox_Description": "Añadir / quitar cuentas, renombrar o quitar vínculos entre cuentas.", "SettingsEditLinkedInbox_Title": "Editar bandeja de entrada vinculada", + "SettingsWindowBackdrop_Title": "Fondo de la ventana", + "SettingsWindowBackdrop_Description": "Seleccione un efecto de fondo para las ventanas de Wino.", + "SettingsWindowBackdrop_Disabled": "La selección de fondo de la ventana está deshabilitada cuando el tema de la aplicación sea diferente de Predeterminado.", "SettingsElementTheme_Description": "Escoge un tema de Windows para Wino", "SettingsElementTheme_Title": "Tema de elemento", "SettingsElementThemeSelectionDisabled": "La selección de elementos del tema se desactiva cuando el tema de la aplicación se selecciona de forma predeterminada.", @@ -581,6 +828,8 @@ "SettingsManageAliases_Title": "Alias", "SettingsEditAccountDetails_Title": "Editar Detalles de Cuenta", "SettingsEditAccountDetails_Description": "Cambie el nombre de la cuenta, nombre del remitente y asigne un nuevo color si lo desea.", + "EditAccountDetailsPage_SaveSuccess_Title": "Cambios guardados", + "EditAccountDetailsPage_SaveSuccess_Message": "Los detalles de la cuenta se han actualizado correctamente.", "SettingsManageLink_Description": "Mover elementos para añadir un nuevo enlace o eliminar el enlace existente.", "SettingsManageLink_Title": "Administrar enlaces", "SettingsMarkAsRead_Description": "Cambiar lo que debería pasar con el elemento seleccionado.", @@ -596,7 +845,41 @@ "SettingsNotifications_Title": "Notificaciones", "SettingsNotificationsAndTaskbar_Description": "Cambiar si las notificaciones deben mostrarse y la insignia de la barra de tareas para esta cuenta.", "SettingsNotificationsAndTaskbar_Title": "Notificaciones y Barra de tareas", + "SettingsHome_Title": "Inicio", + "SettingsHome_SearchTitle": "Buscar una configuración", + "SettingsHome_SearchDescription": "Busque por función, tema o palabra clave para ir directamente a la página de configuración adecuada.", + "SettingsHome_SearchPlaceholder": "Buscar configuraciones", + "SettingsHome_SearchExamples": "Ejemplos: tema, almacenamiento, idioma, firma", + "SettingsHome_QuickLinks_Title": "Enlaces rápidos", + "SettingsHome_QuickLinks_Description": "Acceda directamente a las configuraciones a las que se accede con más frecuencia.", + "SettingsHome_StorageCard_Description": "Vea cuánto contenido MIME local guarda Wino en este dispositivo y límpielo cuando sea necesario.", + "SettingsHome_StorageEmptySummary": "Aún no se detecta contenido MIME en caché.", + "SettingsHome_StorageLoading": "Comprobando el uso de MIME local...", + "SettingsHome_Tips_Title": "Consejos y trucos", + "SettingsHome_Tips_Description": "Unos pequeños cambios pueden hacer que Wino se sienta mucho más personal.", + "SettingsHome_Tip_Theme": "¿Quieres modo oscuro o cambios de acento? Abre Personalización.", + "SettingsHome_Tip_Background": "Utilice Preferencias de la aplicación para controlar el inicio y la sincronización en segundo plano.", + "SettingsHome_Tip_Shortcuts": "Los atajos de teclado te permiten moverte por el correo más rápido.", + "SettingsHome_Resources_Title": "Enlaces útiles", + "SettingsHome_Resources_Description": "Abra recursos del proyecto, información de soporte y canales de lanzamiento.", "SettingsOptions_Title": "Ajustes", + "SettingsOptions_GeneralSection": "General", + "SettingsOptions_MailSection": "Correo", + "SettingsOptions_CalendarSection": "Calendario", + "SettingsOptions_MoreComingSoon": "Más opciones próximamente", + "SettingsOptions_HeroDescription": "Personalice su experiencia con Wino Mail.", + "SettingsOptions_AccountsSummary": "{0} cuenta(s) configurada(s)", + "SettingsSearch_ManageAccounts_Keywords": "cuenta;cuentas;buzón;buzones;alias;alias(es);perfil;dirección;direcciones", + "SettingsSearch_AppPreferences_Keywords": "inicio;fondo;arranque;sincronización;notificación;notificaciones;búsqueda;bandeja;predeterminados", + "SettingsSearch_LanguageTime_Keywords": "idioma;hora;reloj;localidad;región;formato;24 horas;24h", + "SettingsSearch_Personalization_Keywords": "tema;oscuro;claro;apariencia;acento;color;color;modo;disposición;densidad", + "SettingsSearch_About_Keywords": "acerca;versión;sitio web;privacidad;github;donar;tienda;soporte", + "SettingsSearch_KeyboardShortcuts_Keywords": "atajo;atajos;tecla;teclas;teclado;teclas", + "SettingsSearch_MessageList_Keywords": "mensaje;mensajes;lista;hilo;hilos;avatar;vista previa;remitente", + "SettingsSearch_ReadComposePane_Keywords": "lector;redactar;redactor;fuente;fuentes;contenido externo;visualización;lectura", + "SettingsSearch_SignatureAndEncryption_Keywords": "firma;firmas;encriptación;certificado;certificados;S/MIME;SMIME;seguridad", + "SettingsSearch_Storage_Keywords": "almacenamiento;caché;caché;mime;disco;espacio;limpieza;limpiar;datos locales", + "SettingsSearch_CalendarSettings_Keywords": "calendario;semana;horas;programación;evento;eventos", "SettingsPaneLengthReset_Description": "Restablecer el tamaño de la lista de correo a original si tiene problemas.", "SettingsPaneLengthReset_Title": "Resetear tamaño de lista de correo", "SettingsPaypal_Description": "Muestre mucho más amor ❤️ Todas las donaciones se agradecen.", @@ -610,6 +893,8 @@ "SettingsPrefer24HourClock_Title": "Mostrar formato de reloj en 24 horas", "SettingsPrivacyPolicy_Description": "Revisar la Política de Privacidad.", "SettingsPrivacyPolicy_Title": "Políticas de privacidad", + "SettingsWebsite_Description": "Abrir el sitio web de Wino Mail.", + "SettingsWebsite_Title": "Sitio web", "SettingsReadComposePane_Description": "Fuentes, contenido externo.", "SettingsReadComposePane_Title": "Lector y Editor", "SettingsReader_Title": "Lector", @@ -625,6 +910,19 @@ "SettingsShowPreviewText_Title": "Mostrar texto de vista previa", "SettingsShowSenderPictures_Description": "Ocultar/mostrar imágenes del remitente en miniatura.", "SettingsShowSenderPictures_Title": "Mostrar Avatares de Remitente", + "SettingsEmailTemplates_Title": "Plantillas de correo", + "SettingsEmailTemplates_Description": "Administrar plantillas de correo electrónico", + "SettingsEmailTemplates_CreatePageTitle": "Nueva plantilla", + "SettingsEmailTemplates_EditPageTitle": "Editar plantilla", + "SettingsEmailTemplates_NewTemplateTitle": "Nueva plantilla", + "SettingsEmailTemplates_NewTemplateDescription": "Crear una nueva plantilla de correo electrónico", + "SettingsEmailTemplates_NameTitle": "Nombre", + "SettingsEmailTemplates_NamePlaceholder": "Nombre de la plantilla", + "SettingsEmailTemplates_DescriptionTitle": "Descripción", + "SettingsEmailTemplates_DescriptionPlaceholder": "Descripción opcional", + "SettingsEmailTemplates_ContentTitle": "Contenido de la plantilla", + "SettingsEmailTemplates_ContentDescription": "Editar el contenido HTML de esta plantilla.", + "SettingsEmailTemplates_NameRequired": "Se requiere un nombre de plantilla.", "SettingsEnableGravatarAvatars_Title": "Gravatar", "SettingsEnableGravatarAvatars_Description": "Usar gravatar (si está disponible) como imagen del remitente", "SettingsEnableFavicons_Title": "Iconos de dominio (Faviconos)", @@ -645,6 +943,33 @@ "SettingsStartupItem_Title": "Elemento de Inicio", "SettingsStore_Description": "Mostrar un poco de amor ❤️", "SettingsStore_Title": "Valorar en la tienda", + "SettingsStorage_Title": "Almacenamiento", + "SettingsStorage_Description": "Escanear y gestionar la caché MIME almacenada en su carpeta de datos locales.", + "SettingsStorage_ScanFolder": "Escanear la carpeta de datos locales", + "SettingsStorage_NoLocalMimeDataFound": "No se encontraron datos MIME locales.", + "SettingsStorage_NoAccountsFound": "No se encontraron cuentas.", + "SettingsStorage_TotalUsage": "Uso total local de MIME: {0}", + "SettingsStorage_AccountUsageDescription": "{0} utilizado en la caché MIME local", + "SettingsStorage_DeleteAll_Title": "Eliminar todo el contenido MIME", + "SettingsStorage_DeleteAll_Description": "Eliminar la carpeta completa de caché MIME de esta cuenta.", + "SettingsStorage_DeleteAll_Button": "Eliminar todo", + "SettingsStorage_DeleteAll_Confirm_Title": "Eliminar todo el contenido MIME", + "SettingsStorage_DeleteAll_Confirm_Message": "¿Eliminar todos los datos MIME locales para {0}?", + "SettingsStorage_DeleteAll_Success": "Todo el contenido MIME ha sido eliminado.", + "SettingsStorage_DeleteOld_Title": "Eliminar contenido MIME antiguo", + "SettingsStorage_DeleteOld_Description": "Eliminar archivos MIME basados en la fecha de creación del correo en la base de datos local.", + "SettingsStorage_DeleteOld_1Month": "> 1 mes", + "SettingsStorage_DeleteOld_3Months": "> 3 meses", + "SettingsStorage_DeleteOld_6Months": "> 6 meses", + "SettingsStorage_DeleteOld_1Year": "> 1 año", + "SettingsStorage_DeleteOld_Confirm_Title": "Eliminar contenido MIME antiguo", + "SettingsStorage_DeleteOld_Confirm_Message": "¿Eliminar datos MIME locales anteriores a {0} para {1}?", + "SettingsStorage_DeleteOld_Success": "Se eliminaron {0} carpeta(s) MIME anteriores a {1}.", + "SettingsStorage_1Month": "1 mes", + "SettingsStorage_3Months": "3 meses", + "SettingsStorage_6Months": "6 meses", + "SettingsStorage_1Year": "1 año", + "SettingsStorage_Months": "{0} meses", "SettingsTaskbarBadge_Description": "Incluye conteo de correo no leído en el icono de la barra de tareas.", "SettingsTaskbarBadge_Title": "Insignia de la barra de tareas", "SettingsThreads_Description": "Organizar mensajes en hilos de conversación.", @@ -683,6 +1008,9 @@ "SystemFolderConfigDialogValidation_InboxSelected": "No puede asignar la bandeja de entrada a ninguna otra carpeta del sistema.", "SystemFolderConfigSetupSuccess_Message": "Carpetas del sistema configuradas correctamente.", "SystemFolderConfigSetupSuccess_Title": "Configurar Carpetas del Sistema", + "SystemTrayMenu_ShowWino": "Abrir Wino Mail", + "SystemTrayMenu_ShowWinoCalendar": "Abrir Wino Calendario", + "SystemTrayMenu_ExitWino": "Salir", "TestingImapConnectionMessage": "Probando conexión con el servidor...", "TitleBarServerDisconnectedButton_Description": "Wino está desconectado de la red. Haga clic en reconectar para restaurar la conexión.", "TitleBarServerDisconnectedButton_Title": "sin conexión", @@ -699,8 +1027,422 @@ "WinoUpgradeMessage": "Actualizar a Cuentas Ilimitadas", "WinoUpgradeRemainingAccountsMessage": "{0} de {1} cuentas gratuitas usadas.", "Yesterday": "Ayer", - "SettingsAppPreferences_EmailSyncInterval_Title": "Intervalo de sincronización de email", - "SettingsAppPreferences_EmailSyncInterval_Description": "Intervalo de sincronización automática de correo electrónico (minutos). Esta configuración se aplicará sólo después de reiniciar Wino Mail." + "Smime_ImportCertificates_Success": "Certificados importados con éxito.", + "Smime_ImportCertificates_Error": "Error al importar certificados: {0}", + "Smime_RemoveCertificates_Confirm": "¿Realmente desea quitar los certificados {0}?", + "Smime_RemoveCertificates_Success": "Certificados eliminados.", + "Smime_ExportCertificates_Success": "Certificados exportados.", + "Smime_ExportCertificates_Error": "Error al exportar certificados.", + "Smime_CertificateDetails": "Sujeto: {0}\\nEmisor: {1}\\nVálido desde: {2}\\nVálido hasta: {3}\\nHuella digital: {4}", + "Smime_CertificatePassword_Title": "Se requiere contraseña del certificado", + "Smime_CertificatePassword_Placeholder": "Contraseña del certificado para {0} (opcional)", + "Smime_Confirm_Title": "Confirmar", + "Buttons_OK": "Aceptar", + "Buttons_Refresh": "Actualizar", + "SettingsSignatureAndEncryption_Title": "Firma y cifrado", + "SettingsSignatureAndEncryption_Description": "Gestiona los certificados S/MIME para firmar y cifrar correos.", + "SettingsSignatureAndEncryption_MyCertificatesHeader": "Mis certificados", + "SettingsSignatureAndEncryption_MyCertificatesDescription": "Certificados personales para firmar y cifrar", + "SettingsSignatureAndEncryption_RecipientCertificatesHeader": "Certificados del destinatario", + "SettingsSignatureAndEncryption_RecipientCertificatesDescription": "Certificados del destinatario para descifrado", + "SettingsSignatureAndEncryption_NameColumn": "Nombre", + "SettingsSignatureAndEncryption_ExpiresColumn": "Expira el", + "SettingsSignatureAndEncryption_ThumbprintColumn": "Huella digital", + "Buttons_Remove": "Eliminar", + "Buttons_Export": "Exportar", + "Buttons_Import": "Importar", + "SettingsSignatureAndEncryption_SigningCertificate": "Certificado de firma S/MIME", + "SettingsSignatureAndEncryption_EncryptionCertificate": "Certificado de cifrado S/MIME", + "SettingsSignatureAndEncryption_SigningCertificatePlaceholder": "Ninguno", + "SmimeSignaturesInMessage": "Firmas en este mensaje:", + "SmimeSignatureEntry": "• {0} {1} ({2}, válido hasta {3} - {4})", + "SmimeSigningCertificateInfoTitle": "Información del certificado de firma S/MIME", + "SmimeCertificateInfoTitle": "Información del certificado S/MIME", + "SmimeNoCertificateFileFound": "No se encontró ningún archivo de certificado", + "SmimeSaveCertificate": "Guardar certificado...", + "SmimeCertificate": "Certificado S/MIME", + "SmimeCertificateSavedTo": "Certificado guardado en {0}", + "SmimeSignedTooltip": "Este mensaje está firmado con un certificado S/MIME. Haga clic para ver más detalles", + "SmimeEncryptedTooltip": "Este mensaje está cifrado con un certificado S/MIME.", + "SmimeCertificateFileInfo": "Archivo: {0}\\nTipo: {1}\\nTamaño: {2:N0} bytes", + "Composer_LightTheme": "Tema claro", + "Composer_DarkTheme": "Tema oscuro", + "Composer_Outdent": "Quitar sangría", + "Composer_Indent": "Aumentar sangría", + "Composer_BulletList": "Lista con viñetas", + "Composer_OrderedList": "Lista numerada", + "Composer_Stroke": "Trazo", + "Composer_Bold": "Negrita", + "Composer_Italic": "Cursiva", + "Composer_Underline": "Subrayado", + "Composer_CcBcc": "CC y CCO", + "Composer_EnableSmimeSignature": "Activar/desactivar firma S/MIME", + "Composer_EnableSmimeEncryption": "Activar/desactivar cifrado S/MIME", + "Composer_LocalDraftSyncInfo": "Este borrador es local. Wino no pudo enviarlo a su servidor de correo. Haga clic para volver a intentarlo enviarlo al servidor.", + "Composer_CertificateExpires": "Expira el: ", + "Composer_SmimeSignature": "Firma S/MIME", + "Composer_SmimeEncryption": "Cifrado S/MIME", + "Composer_EmailTemplatesPlaceholder": "Plantillas de correo", + "Composer_AiSummarize": "Resumir con IA", + "Composer_AiSummarizeDescription": "Extrae puntos clave, acciones y decisiones de este correo.", + "Composer_AiTranslate": "Traducir con IA", + "Composer_AiActions": "Acciones de IA", + "Composer_AiRewrite": "Reescribir con IA", + "AiActions_CheckingStatus": "Comprobando acceso a IA...", + "AiActions_SignedOutTitle": "Desbloquear el paquete de IA de Wino", + "AiActions_SignedOutDescription": "Traduce, reescribe y resume correos con IA después de iniciar sesión en tu cuenta de Wino y activar el paquete IA.", + "AiActions_NoPackTitle": "Se requiere paquete IA", + "AiActions_NoPackDescription": "Ya has iniciado sesión, pero el paquete IA no está activo. Adquiérelo para usar las herramientas de traducción, reescritura y resumen con IA de Wino.", + "AiActions_UsageSummary": "{0} de {1} créditos usados este mes.", + "Composer_AiRewritePolite": "Hazlo más cortés", + "Composer_AiRewritePoliteDescription": "Suaviza el tono manteniendo la misma intención.", + "Composer_AiRewriteAngry": "Hazlo más enfadado", + "Composer_AiRewriteAngryDescription": "Utiliza un tono más áspero y confrontacional.", + "Composer_AiRewriteHappy": "Hazlo más alegre", + "Composer_AiRewriteHappyDescription": "Añade un tono más optimista y entusiasta.", + "Composer_AiRewriteFormal": "Hazlo formal", + "Composer_AiRewriteFormalDescription": "Hace que el mensaje suene más profesional y estructurado.", + "Composer_AiRewriteFriendly": "Hazlo más amigable", + "Composer_AiRewriteFriendlyDescription": "Dale al mensaje un tono más cercano.", + "Composer_AiRewriteShorter": "Hazlo más corto", + "Composer_AiRewriteShorterDescription": "Acorta el texto y elimina detalles innecesarios.", + "Composer_AiRewriteClearer": "Hazlo más claro", + "Composer_AiRewriteClearerDescription": "Mejora la legibilidad y facilita la comprensión del mensaje.", + "Composer_AiRewriteCustom": "Personalizado", + "Composer_AiRewriteCustomDescription": "Describe tu objetivo de reescritura.", + "Composer_AiRewriteCustomPlaceholder": "Describe cómo quieres que se reescriba el mensaje", + "Composer_AiRewriteMode": "Tono de reescritura", + "Composer_AiRewriteApply": "Aplicar reescritura", + "Composer_AiTranslateDialogTitle": "Traducir con IA", + "Composer_AiTranslateDialogDescription": "Introduce el código de idioma o cultura de destino, como en-US, tr-TR, de-DE o fr-FR.", + "Composer_AiTranslateApply": "Traducir", + "Composer_AiTranslateLanguage": "Idioma de destino", + "Composer_AiTranslateCustomPlaceholder": "Introduce el código de cultura", + "Composer_AiTranslateLanguageEnglish": "Inglés (en-US)", + "Composer_AiTranslateLanguageTurkish": "Turco (tr-TR)", + "Composer_AiTranslateLanguageGerman": "Alemán (de-DE)", + "Composer_AiTranslateLanguageFrench": "Francés (fr-FR)", + "Composer_AiTranslateLanguageSpanish": "Español (es-ES)", + "Composer_AiTranslateLanguageItalian": "Italiano (it-IT)", + "Composer_AiTranslateLanguagePortugueseBrazil": "Portugués (Brasil) (pt-BR)", + "Composer_AiTranslateLanguageDutch": "Neerlandés (nl-NL)", + "Composer_AiTranslateLanguagePolish": "Polaco (pl-PL)", + "Composer_AiTranslateLanguageRussian": "Ruso (ru-RU)", + "Composer_AiTranslateLanguageJapanese": "Japonés (ja-JP)", + "Composer_AiTranslateLanguageKorean": "Coreano (ko-KR)", + "Composer_AiTranslateLanguageChineseSimplified": "Chino simplificado (zh-CN)", + "Composer_AiTranslateLanguageArabic": "Árabe (ar-SA)", + "Composer_AiTranslateLanguageHindi": "Hindi (hi-IN)", + "Composer_AiTranslateLanguageOther": "Otro...", + "Composer_AiBusyTitle": "La IA ya está trabajando", + "Composer_AiBusyMessage": "Por favor, espera a que termine la acción de IA actual.", + "Composer_AiSignInRequired": "Inicia sesión en tu cuenta de Wino para usar las funciones de IA.", + "Composer_AiMissingHtml": "Aún no hay contenido de mensaje para enviar a Wino AI.", + "Composer_AiQuotaUnavailable": "El resultado de IA fue aplicado.", + "Composer_AiAppliedMessage": "El resultado de IA se ha aplicado al compositor. Usa Deshacer si quieres revertirlo.", + "Composer_AiSummarizeSuccessTitle": "Resumen de IA aplicado", + "Composer_AiTranslateSuccessTitle": "Traducción de IA aplicada", + "Composer_AiRewriteSuccessTitle": "Reescritura con IA aplicada", + "Composer_AiErrorTitle": "La acción de IA falló", + "Reader_AiAppliedMessage": "El resultado de IA se muestra ahora para este mensaje. Reabre el mensaje para ver de nuevo el contenido original.", + "SettingsAppPreferences_EmailSyncInterval_Title": "Email sync interval", + "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail.", + "ContactsPage_Title": "Contactos", + "ContactsPage_AddContact": "Añadir contacto", + "ContactsPage_EditContact": "Editar contacto", + "ContactsPage_DeleteContact": "Eliminar contacto", + "ContactsPage_SearchPlaceholder": "Buscar contactos...", + "ContactsPage_NoContacts": "No se han encontrado contactos", + "ContactsPage_ContactsCount": "{0} contactos", + "ContactsPage_SelectedContactsCount": "{0} seleccionados", + "ContactsPage_DeleteSelectedContacts": "Eliminar seleccionados", + "ContactEditDialog_Title": "Editar contacto", + "ContactEditDialog_PhotoSection": "Foto", + "ContactEditDialog_ChoosePhoto": "Elegir foto", + "ContactEditDialog_RemovePhoto": "Quitar foto", + "ContactEditDialog_NameHeader": "Nombre", + "ContactEditDialog_NamePlaceholder": "Nombre de contacto", + "ContactEditDialog_EmailHeader": "Correo electrónico", + "ContactEditDialog_EmailPlaceholder": "contact@example.com", + "ContactEditDialog_InfoSection": "Información de contacto", + "ContactEditDialog_RootContactInfo": "Este es un contacto raíz asociado a tus cuentas y no puede ser eliminado.", + "ContactEditDialog_OverriddenContactInfo": "Este contacto ha sido modificado manualmente y no se actualizará durante la sincronización.", + "ContactsPage_Subtitle": "Gestiona tus contactos de correo electrónico y su información", + "ContactStatus_Account": "Cuenta", + "ContactStatus_Modified": "Modificado", + "ContactAction_Edit": "Editar contacto", + "ContactAction_ChangePhoto": "Cambiar foto", + "ContactAction_Delete": "Eliminar contacto", + "ContactAction_Add": "Añadir contacto", + "ContactSelection_Selected": "seleccionado", + "ContactSelection_SelectAll": "Seleccionar todo", + "ContactSelection_Clear": "Borrar selección", + "ContactsPage_EmptyState": "No hay contactos para mostrar", + "ContactsPage_AddFirstContact": "Añade tu primer contacto", + "ContactsPage_ContactsCountSuffix": "contactos", + "ContactsPane_NewContact": "Nuevo contacto", + "ContactsPane_DescriptionTitle": "Gestiona tus contactos", + "ContactsPane_DescriptionBody": "Crea contactos, cámbiales el nombre, actualiza sus fotos de perfil y mantiene la información guardada organizada en un solo lugar.", + "ContactEditDialog_AddTitle": "Añadir contacto", + "ContactInfoBar_ContactAdded": "Contacto añadido con éxito.", + "ContactInfoBar_ContactUpdated": "Contacto actualizado con éxito.", + "ContactInfoBar_ContactsDeleted": "Contactos eliminados con éxito.", + "ContactInfoBar_ContactPhotoUpdated": "Foto de contacto actualizada con éxito.", + "ContactInfoBar_FailedToLoadContacts": "Error al cargar contactos: {0}", + "ContactInfoBar_FailedToAddContact": "Error al añadir contacto: {0}", + "ContactInfoBar_FailedToUpdateContact": "Error al actualizar contacto: {0}", + "ContactInfoBar_FailedToDeleteContacts": "Error al eliminar contactos: {0}", + "ContactInfoBar_FailedToUpdatePhoto": "Error al actualizar la foto: {0}", + "ContactInfoBar_CannotDeleteRoot": "No se pueden eliminar contactos raíz.", + "ContactConfirmDialog_DeleteTitle": "Eliminar contacto", + "ContactConfirmDialog_DeleteMessage": "¿Estás seguro de que quieres eliminar el contacto '{0}'?", + "ContactConfirmDialog_DeleteMultipleMessage": "¿Seguro que quieres eliminar {0} contacto(s)?", + "ContactConfirmDialog_DeleteButton": "Eliminar", + "CalendarAccountSettings_Title": "Configuración de la cuenta de calendario", + "CalendarAccountSettings_Description": "Gestiona la configuración de calendario para {0}", + "CalendarAccountSettings_AccountColor": "Color de la cuenta", + "CalendarAccountSettings_AccountColorDescription": "Cambiar el color de visualización para esta cuenta de calendario", + "CalendarAccountSettings_SyncEnabled": "Habilitar sincronización", + "CalendarAccountSettings_SyncEnabledDescription": "Activar o desactivar la sincronización del calendario para esta cuenta", + "CalendarAccountSettings_DefaultShowAs": "Estado de Disponibilidad Predeterminado", + "CalendarAccountSettings_DefaultShowAsDescription": "Estado de disponibilidad predeterminado para los nuevos eventos creados con esta cuenta", + "CalendarAccountSettings_PrimaryCalendar": "Calendario principal", + "CalendarAccountSettings_PrimaryCalendarDescription": "Marcar este calendario como el calendario principal para la cuenta", + "CalendarSettings_NewEventBehavior_Header": "Comportamiento del botón de Nuevo Evento", + "CalendarSettings_NewEventBehavior_Description": "Elija si el botón Nuevo Evento debe pedir un calendario cada vez o abrir siempre un calendario específico.", + "CalendarSettings_NewEventBehavior_AskEachTime": "Pedir cada vez.", + "CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar": "Usar siempre un calendario específico.", + "CalendarSettings_Rendering_Title": "Renderizado", + "CalendarSettings_Rendering_Description": "Configurar la distribución del calendario y el comportamiento de visualización.", + "CalendarSettings_Notifications_Title": "Notificaciones", + "CalendarSettings_Notifications_Description": "Elegir la notificación predeterminada y el comportamiento de posposición.", + "CalendarSettings_Preferences_Title": "Preferencias", + "CalendarSettings_Preferences_Description": "Configurar el comportamiento del botón Nuevo Evento.", + "WhatIsNew_GetStartedButton": "Comenzar", + "WhatIsNew_ContinueAnywayButton": "Continuar de todas formas", + "WhatIsNew_PreparingForNewVersionButton": "Preparando para la nueva versión...", + "WhatIsNew_MigrationPreparing_Title": "Preparando sus datos", + "WhatIsNew_MigrationPreparing_Description": "Wino está aplicando migraciones de actualización. Por favor espere mientras preparamos los datos de su cuenta para esta versión.", + "WhatIsNew_MigrationFailedMessage": "La aplicación de migraciones falló con el código de error {0}. Puede seguir usando la aplicación. Sin embargo, si encuentra problemas graves, por favor vuelva a instalar la aplicación.", + "WhatIsNew_MigrationNotification_Title": "Wino Mail actualizado", + "WhatIsNew_MigrationNotification_Message": "Abra la aplicación para completar la actualización y ver las novedades.", + "WelcomeWindow_Title": "Bienvenido a Wino Mail", + "WelcomeWindow_Subtitle": "Una experiencia nativa de Windows para Correo y Calendario.", + "WelcomeWindow_WhatsNewTitle": "Últimos cambios", + "WelcomeWindow_FeaturesTitle": "Funciones", + "WelcomeWindow_WhatsNewTab": "Novedades", + "WelcomeWindow_FeaturesTab": "Funciones", + "WelcomeWindow_GetStartedButton": "Comienza añadiendo una cuenta", + "WelcomeWindow_GetStartedDescription": "Agrega tu cuenta de Outlook, Gmail o IMAP para empezar a usar Wino Mail.", + "WelcomeWindow_ImportFromWinoAccount": "Importar desde tu cuenta de Wino", + "WelcomeWindow_ImportInProgress": "Importando tus preferencias y cuentas sincronizadas...", + "WelcomeWindow_ImportNoAccountsFound": "No se encontraron cuentas sincronizadas en su Cuenta de Wino. Si las preferencias estaban disponibles, se restauraron. Usa Comenzar para añadir una cuenta manualmente.", + "WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} cuentas sincronizadas ya están disponibles en este dispositivo. Usa Comenzar para añadir otra cuenta manualmente si es necesario.", + "WelcomeWindow_SetupTitle": "Configura tu cuenta", + "WelcomeWindow_SetupSubtitle": "Elige tu proveedor de correo para empezar", + "WelcomeWindow_AddAccountButton": "Añadir cuenta", + "WelcomeWindow_SkipForNow": "Omitir por ahora — lo configuraré más tarde.", + "WelcomeWindow_AppDescription": "Una bandeja de entrada rápida y enfocada, rediseñada para Windows 11.", + "WelcomeWizard_Step1Title": "Bienvenido", + "SystemTrayMenu_Open": "Abrir", + "WinoAccount_Titlebar_SyncBenefitTitle": "Configuración de sincronización", + "WinoAccount_Titlebar_SyncBenefitDescription": "Mantén tus preferencias de Wino sincronizadas entre dispositivos.", + "WinoAccount_Titlebar_AddonsBenefitTitle": "Desbloquear complementos", + "WinoAccount_Titlebar_AddonsBenefitDescription": "Accede a funciones premium como Wino AI Pack.", + "WinoAccount_Management_Description": "Gestiona tu Cuenta de Wino, acceso a AI Pack y preferencias sincronizadas y detalles de la cuenta.", + "WinoAccount_Management_SignedOutTitle": "Inicia sesión en Wino Mail", + "WinoAccount_Management_SignedOutDescription": "Inicia sesión o crea una cuenta para sincronizar tu correo, acceder a funciones de IA y gestionar tus ajustes entre dispositivos.", + "WinoAccount_Management_ProfileSectionHeader": "Perfil", + "WinoAccount_Management_AddOnsSectionHeader": "Complementos de Wino", + "WinoAccount_Management_DataSectionHeader": "Datos", + "WinoAccount_Management_AccountActionsSectionHeader": "Acciones de cuenta", + "WinoAccount_Management_AccountCardTitle": "Cuenta", + "WinoAccount_Management_AccountCardDescription": "La dirección de correo de tu cuenta de Wino y el estado actual de la cuenta.", + "WinoAccount_Management_AiPackCardTitle": "AI Pack", + "WinoAccount_Management_AiPackCardDescription": "Ver si Wino AI Pack está activo y cuánta utilización queda.", + "WinoAccount_Management_AiPackActive": "AI Pack está activo", + "WinoAccount_Management_AiPackInactive": "AI Pack no está activo", + "WinoAccount_Management_AiPackUsage": "{0} de {1} usos consumidos. Quedan {2}.", + "WinoAccount_Management_AiPackBillingPeriod": "Periodo de facturación: {0:d} - {1:d}", + "WinoAccount_Management_AiPackUnknownUsage": "Los detalles de uso aún no están disponibles.", + "WinoAccount_Management_AiPackBuyDescription": "Compra Wino AI Pack para traducir, reescribir o resumir correos con IA.", + "WinoAccount_Management_AiPackPromoTitle": "Desbloquear AI Pack", + "WinoAccount_Management_AiPackPromoDescription": "Potencia tu flujo de correo con herramientas impulsadas por IA. Traduce mensajes a más de 50 idiomas, reescribe para claridad y tono, y obtén resúmenes instantáneos de hilos largos.", + "WinoAccount_Management_AiPackPromoPrice": "$4,99 / mes", + "WinoAccount_Management_AiPackPromoRequests": "1.000 créditos", + "WinoAccount_Management_AiPackGetButton": "Obtener AI Pack", + "WinoAddOn_AI_PACK_Name": "Wino AI Pack", + "WinoAddOn_AI_PACK_Description": "Herramientas impulsadas por IA para traducir, reescribir y resumir acciones en Wino Mail.", + "WinoAddOn_AI_PACK_Keywords": "IA, traducir, reescribir, resumir, productividad", + "WinoAddOn_UNLIMITED_ACCOUNTS_Name": "Cuentas ilimitadas", + "WinoAddOn_UNLIMITED_ACCOUNTS_Description": "Elimina el límite de cuentas y añade tantas cuentas de correo como necesites.", + "WinoAddOn_UNLIMITED_ACCOUNTS_Keywords": "cuentas, ilimitadas, premium, complemento", + "WinoAccount_Management_PurchaseRequiresSignIn": "Inicia sesión con tu Cuenta de Wino para completar esta compra.", + "WinoAccount_Management_PurchaseStartFailed": "Wino no pudo completar esta compra en Microsoft Store.", + "WinoAccount_Management_StoreSyncFailed": "La compra terminó, pero Wino no pudo actualizar aún los beneficios de su cuenta. Por favor, inténtelo de nuevo en un momento.", + "WinoAccount_Management_AiPackSubscriptionActive": "Tu suscripción está activa.", + "WinoAccount_Management_AiPackRenews": "Renueva {0:d}", + "WinoAccount_Management_AiPackRequestsUsed": "Créditos usados este mes", + "WinoAccount_Management_AiPackResets": "Reinicios {0:d}", + "WinoAccount_Management_AiPackUsageLoadFailed": "Tuvimos problemas al cargar tu saldo de uso de IA.", + "WinoAccount_Management_AiPackFeatureTranslate": "Traducir", + "WinoAccount_Management_AiPackFeatureRewrite": "Reescribir", + "WinoAccount_Management_AiPackFeatureSummarize": "Resumir", + "WinoAccount_Management_AddOnLoadFailed": "Tuvimos problemas al cargar este complemento.", + "WinoAccount_Management_SyncPreferencesTitle": "Sincronizar Preferencias y Cuentas", + "WinoAccount_Management_SyncPreferencesDescription": "Importa o exporta tus preferencias de Wino y los detalles de la bandeja de entrada entre dispositivos. Las contraseñas, tokens y otra información sensible nunca se sincronizan.", + "WinoAccount_Management_SignOutTitle": "Cerrar sesión", + "WinoAccount_Management_SignOutDescription": "Cerrar sesión de tu cuenta en este dispositivo", + "WinoAccount_Management_StatusLabel": "Estado: {0}", + "WinoAccount_Management_NoRemoteSettings": "Todavía no hay datos sincronizados almacenados para esta cuenta.", + "WinoAccount_Management_ExportSucceeded": "Los datos de Wino seleccionados se exportaron correctamente.", + "WinoAccount_Management_ExportPreferencesSucceeded": "Tus preferencias se exportaron a tu Cuenta de Wino.", + "WinoAccount_Management_ExportAccountsSucceeded": "Se exportaron {0} detalles de la cuenta a tu Cuenta de Wino.", + "WinoAccount_Management_ImportSucceeded": "Se importaron los datos sincronizados desde tu Cuenta de Wino.", + "WinoAccount_Management_ImportPreferencesSucceeded": "Se aplicaron {0} preferencias sincronizadas.", + "WinoAccount_Management_ImportAccountsSucceeded": "Se importaron {0} cuentas.", + "WinoAccount_Management_ImportDuplicateAccountsSkipped": "Se omitieron {0} cuentas que ya existen en este dispositivo.", + "WinoAccount_Management_ImportPartial": "Se aplicaron {0} preferencias sincronizadas. No se pudieron restaurar {1} preferencias.", + "WinoAccount_Management_ImportReloginReminder": "Las contraseñas, tokens y otra información sensible no se importaron. Inicia sesión de nuevo en cada cuenta en este dispositivo antes de usarla.", + "WinoAccount_Management_SerializeFailed": "No se pudieron serializar tus preferencias actuales.", + "WinoAccount_Management_EmptyExport": "No hay valores de preferencias para exportar.", + "WinoAccount_Management_ImportEmpty": "La carga de datos sincronizados no contiene nada nuevo para restaurar.", + "WinoAccount_Management_ExportDialog_Title": "Exportar a tu cuenta de Wino", + "WinoAccount_Management_ExportDialog_Description": "Elige qué quieres sincronizar con tu cuenta de Wino.", + "WinoAccount_Management_ExportDialog_IncludePreferences": "Preferencias", + "WinoAccount_Management_ExportDialog_IncludeAccounts": "Cuentas", + "WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Las contraseñas, los tokens y otra información sensible no se sincronizan.", + "WinoAccount_Management_ExportDialog_AccountsRelogin": "Las cuentas importadas en otro PC seguirán requiriéndote iniciar sesión de nuevo antes de poder usarlas.", + "WinoAccount_Management_ExportDialog_InProgress": "Exportando tus datos de Wino seleccionados...", + "WinoAccount_Management_LoadFailed": "No se pudo cargar la información más reciente de la cuenta de Wino.", + "WinoAccount_Management_ActionFailed": "No se pudo completar la solicitud de la cuenta de Wino.", + "WinoAccount_SettingsSection_Title": "Cuenta de Wino", + "WinoAccount_SettingsSection_Description": "Crea o inicia sesión en una cuenta de Wino usando tu servicio de autenticación local.", + "WinoAccount_RegisterButton_Title": "Registrar cuenta", + "WinoAccount_RegisterButton_Description": "Crea una cuenta de Wino con correo electrónico y contraseña.", + "WinoAccount_RegisterButton_Action": "Abrir registro", + "WinoAccount_LoginButton_Title": "Iniciar sesión", + "WinoAccount_LoginButton_Description": "Inicia sesión en una cuenta de Wino existente con correo electrónico y contraseña.", + "WinoAccount_LoginButton_Action": "Abrir inicio de sesión", + "WinoAccount_SignOutButton_Title": "Cerrar sesión", + "WinoAccount_SignOutButton_Description": "Eliminar la sesión de la cuenta de Wino almacenada localmente.", + "WinoAccount_SignOutButton_Action": "Cerrar sesión", + "WinoAccount_RegisterDialog_Title": "Crear cuenta de Wino", + "WinoAccount_RegisterDialog_Description": "Crea una cuenta de Wino para mantener tu experiencia de Wino sincronizada y desbloquear complementos basados en la cuenta.", + "WinoAccount_RegisterDialog_HeroTitle": "Crea tu cuenta de Wino", + "WinoAccount_RegisterDialog_BenefitsTitle": "¿Por qué crear una?", + "WinoAccount_RegisterDialog_BenefitSyncTitle": "Importar y exportar configuraciones entre dispositivos", + "WinoAccount_RegisterDialog_BenefitSyncDescription": "Mueve tus preferencias de Wino entre dispositivos sin tener que reconstruir tu configuración desde cero.", + "WinoAccount_RegisterDialog_BenefitAiTitle": "Accede a complementos exclusivos como Wino AI Pack (de pago).", + "WinoAccount_RegisterDialog_BenefitAiDescription": "Utiliza una sola cuenta para desbloquear funciones premium de Wino a medida que estén disponibles.", + "WinoAccount_RegisterDialog_DifferenceTitle": "La cuenta de Wino es independiente de tus cuentas de correo.", + "WinoAccount_RegisterDialog_DifferenceDescription": "Tus cuentas de Outlook, Gmail, IMAP u otras cuentas de correo permanecen tal como están. Una cuenta de Wino solo gestiona funciones específicas de Wino y complementos basados en la cuenta.", + "WinoAccount_RegisterDialog_PrimaryButton": "Registrar", + "WinoAccount_RegisterDialog_PrivacyTitle": "Privacidad y procesamiento de API", + "WinoAccount_RegisterDialog_PrivacyDescription": "Los complementos opcionales, como Wino AI Pack, pueden enviar contenido HTML de correo seleccionado al servicio API de Wino solo cuando use esas funciones.", + "WinoAccount_RegisterDialog_PrivacyLinkText": "Leer la política de privacidad", + "WinoAccount_RegisterDialog_PrivacyCheckbox": "Acepto la política de privacidad.", + "WinoAccount_LoginDialog_Title": "Inicia sesión en la cuenta de Wino", + "WinoAccount_LoginDialog_Description": "Inicia sesión en tu cuenta de Wino para sincronizar tu configuración de Wino y acceder a funciones basadas en la cuenta.", + "WinoAccount_LoginDialog_HeroTitle": "Bienvenido de nuevo", + "WinoAccount_LoginDialog_BenefitsTitle": "Qué te ofrece iniciar sesión", + "WinoAccount_LoginDialog_BenefitsDescription": "Utiliza tu cuenta de Wino para seguir sincronizando la configuración entre dispositivos y acceder a complementos de pago como Wino AI Pack.", + "WinoAccount_LoginDialog_DifferenceTitle": "Este no es el inicio de sesión de tu buzón de correo", + "WinoAccount_LoginDialog_DifferenceDescription": "Iniciar sesión aquí no añade ni reemplaza tus cuentas de Outlook, Gmail o IMAP en Wino. Solo te permite iniciar sesión en servicios específicos de Wino.", + "WinoAccount_LoginDialog_ForgotPasswordLink": "¿Olvidaste la contraseña?", + "WinoAccount_EmailLabel": "Correo electrónico", + "WinoAccount_EmailPlaceholder": "name@example.com", + "WinoAccount_PasswordLabel": "Contraseña", + "WinoAccount_ConfirmPasswordLabel": "Confirmar contraseña", + "WinoAccount_ForgotPasswordDialog_Title": "Restablecer la contraseña", + "WinoAccount_ForgotPasswordDialog_PrimaryButton": "Enviar correo de restablecimiento", + "WinoAccount_ForgotPasswordDialog_BackToSignIn": "Volver a iniciar sesión", + "WinoAccount_ForgotPasswordDialog_Description": "Introduce la dirección de correo de tu cuenta de Wino y te enviaremos un enlace para restablecer la contraseña si la dirección está registrada.", + "WinoAccount_Validation_EmailRequired": "Se requiere un correo electrónico.", + "WinoAccount_Validation_PasswordRequired": "Se requiere una contraseña.", + "WinoAccount_Validation_PasswordMismatch": "Las contraseñas no coinciden.", + "WinoAccount_Validation_PrivacyConsentRequired": "Debes aceptar la política de privacidad antes de crear una cuenta de Wino.", + "WinoAccount_Error_InvalidCredentials": "La dirección de correo o la contraseña son incorrectas.", + "WinoAccount_Error_AccountLocked": "Esta cuenta está temporalmente bloqueada.", + "WinoAccount_Error_AccountBanned": "Esta cuenta ha sido baneada.", + "WinoAccount_Error_AccountSuspended": "Esta cuenta ha sido suspendida.", + "WinoAccount_Error_EmailNotConfirmed": "Por favor confirma tu dirección de correo electrónico antes de iniciar sesión.", + "WinoAccount_Error_EmailConfirmationRequired": "Por favor confirma tu dirección de correo electrónico antes de iniciar sesión.", + "WinoAccount_Error_EmailConfirmationResendNotAvailable": "Aún no está disponible un nuevo correo de confirmación.", + "WinoAccount_Error_EmailConfirmationResendInvalid": "Esta solicitud de confirmación ya no es válida. Por favor, intenta iniciar sesión de nuevo.", + "WinoAccount_Error_EmailNotRegistered": "Esta dirección de correo no está registrada.", + "WinoAccount_Error_RefreshTokenInvalid": "Tu sesión ya no es válida. Por favor, inicia sesión de nuevo.", + "WinoAccount_Error_EmailAlreadyRegistered": "Esta dirección de correo ya está registrada.", + "WinoAccount_Error_ExternalLoginEmailRequired": "Se necesita una dirección de correo para completar el inicio de sesión externo.", + "WinoAccount_Error_ExternalLoginInvalid": "La solicitud de inicio de sesión externo no es válida.", + "WinoAccount_Error_ExternalAuthStateInvalid": "El estado de inicio de sesión externo no es válido o ha caducado.", + "WinoAccount_Error_ExternalAuthCodeInvalid": "El código de inicio de sesión externo es inválido o ha caducado.", + "WinoAccount_Error_AiPackRequired": "Se requiere una suscripción activa a Wino AI Pack para realizar esta acción.", + "WinoAccount_Error_AiQuotaExceeded": "Se ha alcanzado el límite de uso de AI Pack para el periodo de facturación actual.", + "WinoAccount_Error_AiHtmlEmpty": "No hay contenido de correo para procesar.", + "WinoAccount_Error_AiHtmlTooLarge": "Este correo es demasiado grande para procesarlo con Wino AI.", + "WinoAccount_Error_AiUnsupportedLanguage": "Ese idioma no es compatible. Prueba un código de cultura válido, como en-US o tr-TR.", + "WinoAccount_Error_Forbidden": "No tienes permiso para realizar esta acción.", + "WinoAccount_Error_ValidationFailed": "La solicitud es inválida. Por favor revisa los valores introducidos.", + "WinoAccount_RegisterSuccessMessage": "Registro de la cuenta de Wino completado para {0}.", + "WinoAccount_LoginSuccessMessage": "Has iniciado sesión en la cuenta de Wino como {0}.", + "WinoAccount_EmailConfirmationSentDialog_Title": "Confirma tu dirección de correo electrónico", + "WinoAccount_EmailConfirmationSentDialog_Message": "Enviamos una confirmación por correo a {0}. Por favor, confírmala e intenta iniciar sesión de nuevo.", + "WinoAccount_EmailConfirmationPendingDialog_Title": "Se requiere confirmación de correo", + "WinoAccount_EmailConfirmationPendingDialog_Message": "Aún estamos esperando a que confirmes {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ResendButton": "Reenviar correo de confirmación", + "WinoAccount_EmailConfirmationPendingDialog_Countdown": "Puedes volver a enviar el correo de confirmación en {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ReadyToResend": "Ya puedes volver a enviar el correo de confirmación.", + "WinoAccount_EmailConfirmationResentDialog_Title": "Correo de confirmación reenviado", + "WinoAccount_EmailConfirmationResentDialog_Message": "Enviamos otro correo de confirmación a {0}. Por favor, confírmalo e intenta iniciar sesión de nuevo.", + "WinoAccount_ForgotPasswordDialog_SuccessTitle": "Correo de restablecimiento de contraseña enviado", + "WinoAccount_ForgotPasswordDialog_SuccessMessage": "Hemos enviado un correo de restablecimiento de contraseña a {0}. Abre ese mensaje para elegir una nueva contraseña.", + "WinoAccount_ChangePassword_Title": "Cambiar contraseña", + "WinoAccount_ChangePassword_Description": "Enviar un correo de restablecimiento de contraseña a esta cuenta de Wino.", + "WinoAccount_ChangePassword_Action": "Enviar correo de restablecimiento", + "WinoAccount_ChangePassword_ConfirmationMessage": "¿Quieres que Wino envíe un correo electrónico para restablecer la contraseña a {0}?", + "WinoAccount_SignOut_SuccessMessage": "Has cerrado sesión en la cuenta de Wino {0}.", + "WinoAccount_SignOut_NoAccountMessage": "No hay ninguna cuenta de Wino activa para cerrar sesión.", + "WinoAccount_Titlebar_SignedOutTitle": "Cuenta de Wino", + "WinoAccount_Titlebar_SignedOutDescription": "Inicia sesión o crea una cuenta de Wino para gestionar tu sesión de Wino.", + "WinoAccount_Titlebar_SignedInStatus": "Estado: {0}", + "WelcomeWizard_Step2Title": "Añadir cuenta", + "WelcomeWizard_Step3Title": "Finalizar configuración", + "ProviderSelection_Title": "Selecciona tu proveedor de correo", + "ProviderSelection_Subtitle": "Selecciona un proveedor a continuación para añadir tu cuenta de correo a Wino Mail.", + "ProviderSelection_AccountNameHeader": "Nombre de la cuenta", + "ProviderSelection_AccountNamePlaceholder": "p. ej. Personal, Trabajo", + "ProviderSelection_DisplayNameHeader": "Nombre para mostrar", + "ProviderSelection_DisplayNamePlaceholder": "p. ej. John Doe", + "ProviderSelection_EmailHeader": "Correo electrónico", + "ProviderSelection_EmailPlaceholder": "p. ej. johndoe@example.com", + "ProviderSelection_AppPasswordHeader": "Contraseña específica de la aplicación", + "ProviderSelection_AppPasswordHelp": "¿Cómo obtengo una contraseña específica para la aplicación?", + "ProviderSelection_CalendarModeHeader": "Integración de calendario", + "ProviderSelection_CalendarMode_DisabledTitle": "Deshabilitado", + "ProviderSelection_CalendarMode_DisabledDescription": "Sin integración de calendario", + "ProviderSelection_CalendarMode_CalDavTitle": "Sincronización CalDAV", + "ProviderSelection_CalendarMode_CalDavDescription_Apple": "Tus eventos del calendario se sincronizan entre tus dispositivos con los servidores de Apple.", + "ProviderSelection_CalendarMode_CalDavDescription_Yahoo": "Tus eventos del calendario se sincronizan entre tus dispositivos con los servidores de Yahoo.", + "ProviderSelection_CalendarMode_LocalTitle": "Calendario local", + "ProviderSelection_CalendarMode_LocalDescription": "Tus eventos se almacenan solo en tu ordenador. No hay conectividad con el servidor.", + "ProviderSelection_ClearColor": "Borrar color", + "ProviderSelection_ContinueButton": "Continuar", + "ProviderSelection_SpecialImap_Subtitle": "Introduce las credenciales de tu cuenta para conectar.", + "AccountSetup_Title": "Configurando tu cuenta", + "AccountSetup_Step_Authenticating": "Autenticando con {0}", + "AccountSetup_Step_TestingMailAuth": "Probando la autenticación del correo", + "AccountSetup_Step_SyncingFolders": "Sincronizando metadatos de carpetas", + "AccountSetup_Step_FetchingProfile": "Recuperando información del perfil", + "AccountSetup_Step_DiscoveringCalDav": "Descubriendo la configuración de CalDAV", + "AccountSetup_Step_TestingCalendarAuth": "Probando la autenticación del calendario", + "AccountSetup_Step_SavingAccount": "Guardando la información de la cuenta", + "AccountSetup_Step_FetchingCalendarMetadata": "Recuperando metadatos del calendario", + "AccountSetup_Step_SyncingAliases": "Sincronizando alias", + "AccountSetup_Step_Finalizing": "Finalizando la configuración", + "AccountSetup_FailureMessage": "La configuración ha fallado. Regresa para corregir tus ajustes o inténtalo de nuevo más tarde.", + "AccountSetup_SuccessMessage": "¡Tu cuenta se ha configurado correctamente!", + "AccountSetup_GoBackButton": "Atrás", + "AccountSetup_TryAgainButton": "Inténtalo de nuevo", + "ImapCalDavSettings_AutoDiscoveryFailed": "El autodescubrimiento ha fallado. Por favor, introduce la configuración manualmente en la pestaña Avanzada." } - - diff --git a/Wino.Core.Domain/Translations/fi_FI/resources.json b/Wino.Core.Domain/Translations/fi_FI/resources.json index 238c42fe..47d6fa2f 100644 --- a/Wino.Core.Domain/Translations/fi_FI/resources.json +++ b/Wino.Core.Domain/Translations/fi_FI/resources.json @@ -8,6 +8,7 @@ "AccountCacheReset_Message": "This account requires full re-sychronization to continue working. Please wait while Wino re-synchronizes your messages...", "AccountContactNameYou": "You", "AccountCreationDialog_Completed": "Valmis", + "AccountCreationDialog_FetchingCalendarMetadata": "Haetaan kalenterin tietoja.", "AccountCreationDialog_FetchingEvents": "Fetching calendar events.", "AccountCreationDialog_FetchingProfileInformation": "Fetching profile details.", "AccountCreationDialog_GoogleAuthHelpClipboardText_Row0": "If your browser did not launch automatically to complete authentication:", @@ -17,6 +18,7 @@ "AccountCreationDialog_Initializing": "valmistellaan", "AccountCreationDialog_PreparingFolders": "Valmistellaan kansioita", "AccountCreationDialog_SigninIn": "Tallennetaan tilin tietoja.", + "Purchased": "Ostettu", "AccountEditDialog_Message": "Tilin nimi", "AccountEditDialog_Title": "Muokkaa tiliä", "AccountPickerDialog_Title": "Valitse tili", @@ -26,6 +28,10 @@ "AccountDetailsPage_Description": "Change the name of the account in Wino and set desired sender name.", "AccountDetailsPage_ColorPicker_Title": "Account color", "AccountDetailsPage_ColorPicker_Description": "Assign a new account color to colorize its symbol in the list.", + "AccountDetailsPage_TabGeneral": "Yleinen", + "AccountDetailsPage_TabMail": "Sähköposti", + "AccountDetailsPage_TabCalendar": "Kalenteri", + "AccountDetailsPage_CalendarListDescription": "Valitse kalenteri määrittääksesi sen asetukset", "AddHyperlink": "Lisää", "AppCloseBackgroundSynchronizationWarningTitle": "Background Synchronization", "AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Application has not been set to launch on Windows startup.", @@ -47,8 +53,10 @@ "BasicIMAPSetupDialog_Title": "IMAP-tili", "Busy": "Busy", "Buttons_AddAccount": "Lisää tili", + "Buttons_FixAccount": "Korjaa tili", "Buttons_AddNewAlias": "Add New Alias", "Buttons_Allow": "Allow", + "Buttons_Apply": "Käytä", "Buttons_ApplyTheme": "Vaihda teemaa", "Buttons_Browse": "Selaa", "Buttons_Cancel": "Peruuta", @@ -62,6 +70,7 @@ "Buttons_Edit": "Muokkaa", "Buttons_EnableImageRendering": "Ota käyttöön", "Buttons_Multiselect": "Select Multiple", + "Buttons_Manage": "Hallitse", "Buttons_No": "Ei", "Buttons_Open": "Avaa", "Buttons_Purchase": "Osta", @@ -70,15 +79,134 @@ "Buttons_Save": "Tallenna", "Buttons_SaveConfiguration": "Tallenna asetukset", "Buttons_Send": "Send", + "Buttons_SendToServer": "Lähetä palvelimelle", "Buttons_Share": "Jaa", "Buttons_SignIn": "Kirjaudu sisään", "Buttons_Sync": "Synchronize", "Buttons_SyncAliases": "Synchronize Aliases", "Buttons_TryAgain": "Yritä uudelleen", "Buttons_Yes": "Kyllä", + "Sync_SynchronizingFolder": "Synkronoidaan {0} {1}%", + "Sync_DownloadedMessages": "Ladattiin {0} viestiä kohteesta {1}", + "SyncAction_Archiving": "Arkistoidaan {0} sähköpostia", + "SyncAction_ClearingFlag": "Poistetaan lippumerkintä {0} sähköpostista", + "SyncAction_CreatingDraft": "Luodaan luonnos", + "SyncAction_CreatingEvent": "Luodaan tapahtuma", + "SyncAction_Deleting": "Poistetaan {0} sähköpostia", + "SyncAction_EmptyingFolder": "Tyhjennetään kansiota", + "SyncAction_MarkingAsRead": "Merkataan {0} sähköpostia luetuiksi", + "SyncAction_MarkingAsUnread": "Merkataan {0} sähköpostia lukemattomiksi", + "SyncAction_MarkingFolderAsRead": "Merkitään kansio luetuksi", + "SyncAction_Moving": "Siirretään {0} sähköpostia", + "SyncAction_MovingToFocused": "Siirretään {0} sähköpostia Focused-kansioon", + "SyncAction_RenamingFolder": "Kansion nimeäminen", + "SyncAction_SendingMail": "Lähetetään sähköpostia", + "SyncAction_SettingFlag": "Merkitään {0} sähköpostia lipulla", + "SyncAction_SynchronizingAccount": "Synkronoidaan {0}", + "SyncAction_SynchronizingAccounts": "Synkronoidaan {0} tiliä", + "SyncAction_SynchronizingCalendarData": "Synkronoidaan kalenteritietoja", + "SyncAction_SynchronizingCalendarEvents": "Synkronoidaan kalenteritapahtumia", + "SyncAction_SynchronizingCalendarMetadata": "Synkronoidaan kalenterimetatiedot", + "SyncAction_Unarchiving": "Palautetaan {0} sähköpostia arkistosta", "CalendarAllDayEventSummary": "all-day events", "CalendarDisplayOptions_Color": "Color", "CalendarDisplayOptions_Expand": "Expand", + "CalendarEventResponse_Accept": "Hyväksy", + "CalendarEventResponse_AcceptedResponse": "Hyväksytty", + "CalendarEventResponse_Decline": "Hylkää", + "CalendarEventResponse_DeclinedResponse": "Hylätty", + "CalendarEventResponse_NotResponded": "Ei vastattu", + "CalendarEventResponse_Tentative": "Ehkä", + "CalendarEventResponse_TentativeResponse": "Ehkä", + "CalendarEventRsvpPanel_Accept": "Hyväksy", + "CalendarEventRsvpPanel_AddMessage": "Lisää viesti vastaukseen... (valinnainen)", + "CalendarEventRsvpPanel_Decline": "Hylkää", + "CalendarEventRsvpPanel_Message": "Viesti", + "CalendarEventRsvpPanel_SendReplyMessage": "Lähetä vastausviesti", + "CalendarEventRsvpPanel_Tentative": "Ehkä", + "CalendarEventRsvpPanel_Title": "Vastausvaihtoehdot", + "CalendarAttendeeStatus_Accepted": "Hyväksytty", + "CalendarAttendeeStatus_Declined": "Hylätty", + "CalendarAttendeeStatus_NeedsAction": "Toimintaa tarvitaan", + "CalendarAttendeeStatus_Tentative": "Ehkä", + "CalendarEventDetails_Attachments": "Liitteet", + "CalendarEventCompose_AddAttachment": "Lisää liite", + "CalendarEventCompose_AllDay": "Koko päivä", + "CalendarEventCompose_AttachmentsNotSupportedForCalDav": "Liitteitä ei tueta CalDAV-kalentereille.", + "CalendarEventCompose_EndDate": "Loppupäivä", + "CalendarEventCompose_EndTime": "Loppuaika", + "CalendarEventCompose_Every": "joka", + "CalendarEventCompose_ForWeekdays": "päiville", + "CalendarEventCompose_FrequencyDay": "päivä", + "CalendarEventCompose_FrequencyDayPlural": "päivät", + "CalendarEventCompose_FrequencyMonth": "kuukausi", + "CalendarEventCompose_FrequencyMonthPlural": "kuukaudet", + "CalendarEventCompose_FrequencyWeek": "viikko", + "CalendarEventCompose_FrequencyWeekPlural": "viikot", + "CalendarEventCompose_FrequencyYear": "vuosi", + "CalendarEventCompose_FrequencyYearPlural": "vuodet", + "CalendarEventCompose_Location": "Sijainti", + "CalendarEventCompose_LocationPlaceholder": "Lisää sijainti", + "CalendarEventCompose_NewEventButton": "Uusi tapahtuma", + "CalendarEventCompose_DefaultCalendarHint": "Voit valita oletkalenterin uusille tapahtumille Kalenteriasetuksissa.", + "CalendarEventCompose_DefaultCalendarSettingsLink": "Avaa Kalenteriasetukset", + "CalendarEventCompose_NoCalendarsMessage": "Tällä hetkellä tapahtuman luomiseen ei ole käytettävissä kalentereita.", + "CalendarEventCompose_NoCalendarsTitle": "Ei käytettävissä olevia kalentereita", + "CalendarEventCompose_NoEndDate": "Ei päättymispäivää", + "CalendarEventCompose_Notes": "Muistiinpanot", + "CalendarEventCompose_PickCalendarTitle": "Valitse kalenteri", + "CalendarEventCompose_Recurring": "Toistuva", + "CalendarEventCompose_RecurringSummary": "Tapahtuu {0} {1}{2} {3} voimassa {4}{5}", + "CalendarEventCompose_RecurringSummarySmart": "Tapahtuu {0}{1} {2} voimassa {3}{4}", + "CalendarEventCompose_RepeatEvery": "Toista joka", + "CalendarEventCompose_SelectCalendar": "Valitse kalenteri", + "CalendarEventCompose_SingleOccurrenceSummary": "Tapahtuu {0} {1}", + "CalendarEventCompose_StartDate": "Alkupäivä", + "CalendarEventCompose_StartTime": "Alkamisaika", + "CalendarEventCompose_TimeRangeSummary": "alkaen {0} - {1}", + "CalendarEventCompose_Title": "Tapahtuman otsikko", + "CalendarEventCompose_TitlePlaceholder": "Lisää otsikko", + "CalendarEventCompose_Until": "asti", + "CalendarEventCompose_UntilSummary": "asti {0}", + "CalendarEventCompose_ValidationInvalidAllDayRange": "Koko päivän lopetuspäivän on oltava aloituspäivän jälkeen.", + "CalendarEventCompose_ValidationInvalidAttendee": "Yksi tai useampi osallistuja on virheellinen sähköpostiosoite.", + "CalendarEventCompose_ValidationInvalidRecurrenceEnd": "Toistojen päättymispäivämäärän on oltava sama tai myöhemmin kuin tapahtuman aloituspäivä.", + "CalendarEventCompose_ValidationInvalidTimeRange": "Loppuajan on oltava myöhemmin kuin alkamisaika.", + "CalendarEventCompose_ValidationMissingAttachment": "Yksi tai useampi liite ei ole enää käytettävissä: {0}", + "CalendarEventCompose_ValidationMissingCalendar": "Valitse kalenteri ennen tapahtuman luomista.", + "CalendarEventCompose_ValidationMissingTitle": "Syötä tapahtuman otsikko ennen tapahtuman luomista.", + "CalendarEventCompose_ValidationTitle": "Tapahtuman vahvistus epäonnistui", + "CalendarEventCompose_WeekdaySummary": " {0}", + "CalendarEventCompose_Weekday_Friday": "P", + "CalendarEventCompose_Weekday_Monday": "M", + "CalendarEventCompose_Weekday_Saturday": "La", + "CalendarEventCompose_Weekday_Sunday": "Su", + "CalendarEventCompose_Weekday_Thursday": "To", + "CalendarEventCompose_Weekday_Tuesday": "Ti", + "CalendarEventCompose_Weekday_Wednesday": "Ke", + "CalendarEventDetails_Details": "Yksityiskohdat", + "CalendarEventDetails_EditSeries": "Muokkaa sarjaa", + "CalendarEventDetails_Editing": "Muokataan", + "CalendarEventDetails_InviteSomeone": "Kutsu joku", + "CalendarEventDetails_JoinOnline": "Liity verkossa", + "CalendarEventDetails_Organizer": "Järjestäjä", + "CalendarEventDetails_People": "Henkilöt", + "CalendarEventDetails_ReadOnlyEvent": "Vain katsottava tapahtuma", + "CalendarEventDetails_Reminder": "Muistutus", + "CalendarReminder_StartedHoursAgo": "Aloitettu {0} tuntia sitten", + "CalendarReminder_StartedMinutesAgo": "Aloitettu {0} minuuttia sitten", + "CalendarReminder_StartedNow": "Aloitettu juuri nyt", + "CalendarReminder_StartingNow": "Käynnistyy nyt", + "CalendarReminder_StartsInHours": "Alkaa {0} tunnin kuluttua", + "CalendarReminder_StartsInMinutes": "Alkaa {0} minuutin kuluttua", + "CalendarReminder_SnoozeAction": "Myöhäistä", + "CalendarReminder_SnoozeMinutesOption": "{0} minuuttia", + "CalendarEventDetails_ShowAs": "Näytä tilana", + "CalendarShowAs_Free": "Vapaa", + "CalendarShowAs_Tentative": "Ehdotettu", + "CalendarShowAs_Busy": "Varattu", + "CalendarShowAs_OutOfOffice": "Poissa toimistosta", + "CalendarShowAs_WorkingElsewhere": "Työssä muualla", "CalendarItem_DetailsPopup_JoinOnline": "Join online", "CalendarItem_DetailsPopup_ViewEventButton": "View event", "CalendarItem_DetailsPopup_ViewSeriesButton": "View series", @@ -88,6 +216,9 @@ "ClipboardTextCopied_Message": "{0} kopioitu leikepöydälle.", "ClipboardTextCopied_Title": "Kopioitu", "ClipboardTextCopyFailed_Message": "{0} kopioiminen leikepöydälle epäonnistui.", + "ContactInfoBar_ErrorTitle": "Yhteystietojen lataus epäonnistui", + "ContactInfoBar_SuccessTitle": "Yhteystiedot ladattiin", + "ContactInfoBar_WarningTitle": "Yhteystiedot saattavat olla puutteelliset", "ComingSoon": "Tulossa pian...", "ComposerAttachmentsDragDropAttach_Message": "Liitä", "ComposerAttachmentsDropZone_Message": "Raahaa tiedostosi tähän", @@ -129,6 +260,10 @@ "DialogMessage_CreateLinkedAccountTitle": "Linkitettyjen tilien nimi", "DialogMessage_DeleteAccountConfirmationMessage": "Poistetaanko {0}?", "DialogMessage_DeleteAccountConfirmationTitle": "Kaikki tämän tilin tiedot poistetaan pysyvästi.", + "DialogMessage_DeleteEmailTemplateConfirmationMessage": "Poistetaanko sähköpostimalli \"{0}\"?", + "DialogMessage_DeleteEmailTemplateConfirmationTitle": "Poista sähköpostimalli", + "DialogMessage_DeleteRecurringSeriesMessage": "Tämä poistaa sarjan kaikki tapahtumat. Haluatko jatkaa?", + "DialogMessage_DeleteRecurringSeriesTitle": "Poista toistuva sarja", "DialogMessage_DiscardDraftConfirmationMessage": "Luonnos hylätään. Haluatko jatkaa?", "DialogMessage_DiscardDraftConfirmationTitle": "Hylkää luonnos", "DialogMessage_EmptySubjectConfirmation": "Aihe puuttuu", @@ -172,11 +307,18 @@ "ElementTheme_Light": "Vaalea teema", "Emoji": "Emoji", "Error_FailedToSetupSystemFolders_Title": "Failed to setup system folders", + "Exception_AccountNeedsAttention_Title": "Tili vaatii huomiota", + "Exception_AccountNeedsAttention_Message": "'{0}' vaatii huomiotasi jatkaaksesi.", + "Exception_WebView2RuntimeMissing_Message": "Wino Mail ei löytänyt Microsoft Edge WebView2 -ajonaikaa. Asenna tai korjaa ajonaikaa, jotta viestin sisältö näkyy oikein.", + "Exception_WebView2RuntimeMissing_Title": "WebView2-ajonaika vaaditaan", "Exception_AuthenticationCanceled": "Tunnistautuminen peruutettu", "Exception_CustomThemeExists": "Teema on jo olemassa.", "Exception_CustomThemeMissingName": "Sinun on annettava nimi.", "Exception_CustomThemeMissingWallpaper": "Sinun on valittava taustakuva.", "Exception_FailedToSynchronizeAliases": "Failed to synchronize aliases", + "Exception_FailedToSynchronizeCalendarData": "Kalenteritietojen synkronointi epäonnistui", + "Exception_FailedToSynchronizeCalendarEvents": "Kalenteritapahtumien synkronointi epäonnistui", + "Exception_FailedToSynchronizeCalendarMetadata": "Kalenteritietojen synkronointi epäonnistui", "Exception_FailedToSynchronizeFolders": "Kansioiden synkronointi epäonnistui", "Exception_FailedToSynchronizeProfileInformation": "Failed to synchronize profile information", "Exception_GoogleAuthCallbackNull": "Callback uri is null on activation.", @@ -229,6 +371,32 @@ "HoverActionOption_MoveJunk": "Siirrä roskakoriin", "HoverActionOption_ToggleFlag": "Merkitse / Poista merkintä", "HoverActionOption_ToggleRead": "Luettu / Lukematon", + "KeyboardShortcuts_FailedToReset": "Pikanäppäinten palauttaminen oletusasetuksiin epäonnistui", + "KeyboardShortcuts_FailedToUpdate": "Pikanäppäinten päivittäminen epäonnistui", + "KeyboardShortcuts_MailoperationAction": "Toiminto", + "KeyboardShortcuts_Action": "Toiminto", + "KeyboardShortcuts_FailedToLoad": "Pikanäppäinten lataaminen epäonnistui", + "KeyboardShortcuts_EnterKeyForShortcut": "Syötä näppäin pikanäppäintä varten", + "KeyboardShortcuts_SelectOperationForShortcut": "Valitse toiminto, joka suoritetaan pikanäppäimellä", + "KeyboardShortcuts_EnterKey": "Syötä näppäin pikanäppäintä varten", + "KeyboardShortcuts_SelectOperation": "Valitse pikanäppäimelle suoritettava toiminto", + "KeyboardShortcuts_ShortcutInUse": "Tämä pikanäppäin on jo käytössä toisen pikanäppäimen toimesta", + "KeyboardShortcuts_FailedToSave": "Pikanäppäimen tallennus epäonnistui", + "KeyboardShortcuts_FailedToDelete": "Pikanäppäimen poistaminen epäonnistui", + "KeyboardShortcuts_PageDescription": "Aseta pikanäppäimet nopeisiin sähköpostitoimintoihin. Paina näppäimiä, kun kohdistin on avaimen syöttökentässä, tallentaaksesi pikanäppäimet.", + "KeyboardShortcuts_Add": "Lisää pikanäppäin", + "KeyboardShortcuts_EditTitle": "Muokkaa pikanäppäintä", + "KeyboardShortcuts_ResetToDefaults": "Palauta oletusasetukset", + "KeyboardShortcuts_PressKeysHere": "Paina tässä näppäimiä...", + "KeyboardShortcuts_KeyCombination": "Näppäinyhdistelmä", + "KeyboardShortcuts_FocusArea": "Aseta kohdistus yllä olevaan kenttään ja paina haluttu näppäinyhdistelmä", + "KeyboardShortcuts_Modifiers": "Muut näppäimet", + "KeyboardShortcuts_Mode": "Sovellustila", + "KeyboardShortcuts_ModeMail": "Sähköposti", + "KeyboardShortcuts_ModeCalendar": "Kalenteri", + "KeyboardShortcuts_ActionToggleReadUnread": "Vaihda luetuksi/lukemattomaksi", + "KeyboardShortcuts_ActionToggleFlag": "Vaihda merkintä", + "KeyboardShortcuts_ActionToggleArchive": "Vaihda arkistointi/poista arkisto", "ImageRenderingDisabled": "Image rendering is disabled for this message.", "ImapAdvancedSetupDialog_AuthenticationMethod": "Authentication method", "ImapAdvancedSetupDialog_ConnectionSecurity": "Connection security", @@ -295,12 +463,58 @@ "IMAPSetupDialog_Username": "Username", "IMAPSetupDialog_UsernamePlaceholder": "johndoe, johndoe@fabrikam.com, domain/johndoe", "IMAPSetupDialog_UseSameConfig": "Use the same username and password for sending email", + "ImapCalDavSettingsPage_TitleCreate": "IMAP- ja Kalenteriasetukset", + "ImapCalDavSettingsPage_TitleEdit": "Muokkaa IMAP- ja Kalenteriasetuksia", + "ImapCalDavSettingsPage_Subtitle": "Määritä tälle tilille IMAP/SMTP ja mahdollinen kalenterin synkronointi.", + "ImapCalDavSettingsPage_BasicSectionTitle": "Perusasetukset", + "ImapCalDavSettingsPage_BasicSectionDescription": "Anna henkilöllisyytesi ja tunnistetiedot. Wino voi yrittää tunnistaa palvelinasetukset automaattisesti.", + "ImapCalDavSettingsPage_BasicTab": "Perus", + "ImapCalDavSettingsPage_EnableCalendarSupport": "Ota kalenterituki käyttöön", + "ImapCalDavSettingsPage_AutoDiscoverButton": "Sähköpostiasetusten automaattinen haku", + "ImapCalDavSettingsPage_AutoDiscoverySuccessMessage": "Sähköpostiasetukset on löydetty ja otettu käyttöön.", + "ImapCalDavSettingsPage_AdvancedSectionTitle": "Edistyneet asetukset", + "ImapCalDavSettingsPage_AdvancedSectionDescription": "Syötä palvelinasetukset manuaalisesti, jos autodiscovery ei ole käytettävissä tai on virheellinen.", + "ImapCalDavSettingsPage_AdvancedTab": "Edistyneet", + "ImapCalDavSettingsPage_CalendarSectionTitle": "Kalenteriasetukset", + "ImapCalDavSettingsPage_CalendarSectionDescription": "Valitse, miten kalenteritiedot tulisi toimia tämän IMAP-tilin kanssa.", + "ImapCalDavSettingsPage_CalendarModeHeader": "Kalenterin tila", + "ImapCalDavSettingsPage_ConnectionSecurityHeader": "Yhteyden suojaus", + "ImapCalDavSettingsPage_AuthenticationMethodHeader": "Tunnistustapa", + "ImapCalDavSettingsPage_CalendarModeDisabled": "Pois käytöstä", + "ImapCalDavSettingsPage_CalendarModeCalDav": "CalDAV-synkronointi", + "ImapCalDavSettingsPage_CalendarModeLocalOnly": "Vain paikallinen kalenteri", + "ImapCalDavSettingsPage_CalendarModeDisabledDescription": "Kalenteri on pois käytöstä tältä tililtä.", + "ImapCalDavSettingsPage_CalendarModeCalDavDescription": "Kalenteri-tapahtumat synkronoidaan CalDAV-palvelimen kanssa.", + "ImapCalDavSettingsPage_CalendarModeLocalOnlyDescription": "Kalenteri-tapahtumat tallennetaan vain tähän tietokoneeseen eikä niitä synkronoida verkkoon.", + "ImapCalDavSettingsPage_LocalCalendarLearnMore": "Miten paikallinen kalenteri toimii", + "ImapCalDavSettingsPage_LocalCalendarDialogTitle": "Vain paikallinen kalenteri", + "ImapCalDavSettingsPage_LocalCalendarDialogMessage": "Paikallinen kalenteri tallentaa kaikki tapahtumat vain tähän tietokoneeseen. Mitään ei synkronoida iCloudiin, Yahooon tai muihin palveluntarjoajiin.", + "ImapCalDavSettingsPage_CalDavServiceUrl": "CalDAV-palvelun URL", + "ImapCalDavSettingsPage_CalDavUsername": "CalDAV-käyttäjänimi", + "ImapCalDavSettingsPage_CalDavPassword": "CalDAV-salasana", + "ImapCalDavSettingsPage_CalDavNotRequiredMessage": "CalDAV-testi on tarpeellinen vain, kun kalenteritila on CalDAV-synkronointi.", + "ImapCalDavSettingsPage_CalDavUrlRequired": "CalDAV-palvelun URL on pakollinen.", + "ImapCalDavSettingsPage_CalDavUrlInvalid": "CalDAV-palvelun URL on oltava absoluuttinen URL.", + "ImapCalDavSettingsPage_CalDavUsernameRequired": "CalDAV-käyttäjätunnus on pakollinen.", + "ImapCalDavSettingsPage_CalDavPasswordRequired": "CalDAV-salasana on pakollinen.", + "ImapCalDavSettingsPage_TestImapButton": "Testaa IMAP-yhteys", + "ImapCalDavSettingsPage_TestCalDavButton": "Testaa CalDAV-yhteys", + "ImapCalDavSettingsPage_ImapTestSuccessMessage": "IMAP-yhteyden testaus onnistui.", + "ImapCalDavSettingsPage_CalDavTestSuccessMessage": "CalDAV-yhteyden testaus onnistui.", + "ImapCalDavSettingsPage_SaveSuccessMessage": "Tilin asetukset vahvistettiin ja tallennettiin.", + "ImapCalDavSettingsPage_ICloudHint": "Käytä sovelluskohtaisesti luotua salasanaa Apple-tilisi asetuksista.", + "ImapCalDavSettingsPage_YahooHint": "Käytä sovelluskohtaisesti luotua salasanaa Yahoo-tilisi suojausasetuksista.", "Info_AccountCreatedMessage": "{0} is created", "Info_AccountCreatedTitle": "Tilin luominen", "Info_AccountCreationFailedTitle": "Tilin luominen epäonnistui", "Info_AccountDeletedMessage": "{0} poistettiin onnistuneesti.", "Info_AccountDeletedTitle": "Tili poistettu", "Info_AccountIssueFixFailedTitle": "Epäonnistui", + "Info_AccountIssueFixImapMessage": "Avaa IMAP- ja kalenteriasetussivun syöttääksesi palvelimesi tunnistetiedot uudelleen.", + "Info_AccountAttentionRequiredMessage": "Tämä tili vaatii huomiota.", + "Info_AccountAttentionRequiredClickableMessage": "Napsauta korjataksesi tämän tilin ja synkronoi se uudelleen.", + "Info_AccountAttentionRequiredAction": "Korjaa", + "Info_AccountAttentionRequiredActionHint": "Napsauta Korjaa ratkaistaksesi tämän tilin ongelman.", "Info_AccountIssueFixSuccessMessage": "Kaikki tiliongelmat korjattu.", "Info_AccountIssueFixSuccessTitle": "Onnistui", "Info_AttachmentOpenFailedMessage": "Tätä liitettä ei voi avata.", @@ -370,6 +584,7 @@ "InfoBarMessage_SynchronizationDisabledFolder": "This folder is disabled for synchronization.", "InfoBarTitle_SynchronizationDisabledFolder": "Disabled Folder", "Justify": "Justify", + "MenuUpdateAvailable": "Päivitys saatavilla", "Left": "Left", "Link": "Link", "LinkedAccountsCreatePolicyMessage": "you must have at least 2 accounts to create link\nlink will be removed on save", @@ -403,6 +618,7 @@ "MailOperation_Unarchive": "Poista arkistointi", "MailOperation_ViewMessageSource": "View message source", "MailOperation_Zoom": "Zoomaa", + "MailsDragging": "Vedetään {0} kohdetta", "MailsSelected": "{0} kohta(a) valittu", "MarkFlagUnflag": "Merkitse tai poista merkintä", "MarkReadUnread": "Merkitse luetuksi / lukemattomaksi", @@ -434,6 +650,8 @@ "Notifications_MultipleNotificationsTitle": "New Mail", "Notifications_WinoUpdatedMessage": "Tarkasta uusi versio {0}", "Notifications_WinoUpdatedTitle": "Wino Mail on päivitetty.", + "Notifications_StoreUpdateAvailableTitle": "Päivitys saatavilla", + "Notifications_StoreUpdateAvailableMessage": "Uusin Wino Mail -versio on valmis asennettavaksi Microsoft Storesta.", "OnlineSearchFailed_Message": "Failed to perform search\n{0}\n\nListing offline mails.", "OnlineSearchTry_Line1": "Can't find what you are looking for?", "OnlineSearchTry_Line2": "Try online search.", @@ -446,7 +664,6 @@ "PaneLengthOption_Small": "Pieni", "Photos": "Kuvat", "PreparingFoldersMessage": "Valmistellaan kansioita", - "ProtocolLogAvailable_Message": "Protokolla lokit ovat saatavilla diagnostiikkaan.", "ProviderDetail_Gmail_Description": "Google-tili", "ProviderDetail_iCloud_Description": "Apple iCloud Account", "ProviderDetail_iCloud_Title": "iCloud", @@ -465,9 +682,14 @@ "SearchBarPlaceholder": "Search", "SearchingIn": "Searching in", "SearchPivotName": "Results", + "Settings_KeyboardShortcuts_Title": "Pikanäppäimet", + "Settings_KeyboardShortcuts_Description": "Hallitse pikanäppäimiä nopeisiin toimiin sähköposteissa.", "SettingConfigureSpecialFolders_Button": "Configure", "SettingsEditAccountDetails_IMAPConfiguration_Title": "IMAP/SMTP Configuration", "SettingsEditAccountDetails_IMAPConfiguration_Description": "Change your incoming/outgoing server settings.", + "SettingsEditAccountDetails_ImapCalDavSettings_Title": "IMAP- ja kalenteriasetukset", + "SettingsEditAccountDetails_ImapCalDavSettings_Description": "Avaa tämän tilin erillinen IMAP-, SMTP- ja CalDAV-asetussivu.", + "SettingsEditAccountDetails_ImapCalDavSettings_Action": "Avaa asetukset", "SettingsAbout_Description": "Learn more about Wino.", "SettingsAbout_Title": "About", "SettingsAboutGithub_Description": "Go to issue tracker GitHub repository.", @@ -490,6 +712,10 @@ "SettingsAppPreferences_SearchMode_Local": "Local", "SettingsAppPreferences_SearchMode_Online": "Online", "SettingsAppPreferences_SearchMode_Title": "Default search mode", + "SettingsAppPreferences_ApplicationMode_Title": "Sovelluksen oletustila", + "SettingsAppPreferences_ApplicationMode_Description": "Valitse, millä tilalla Wino avautuu, kun aktivointityyppiä ei ole määritetty.", + "SettingsAppPreferences_ApplicationMode_Mail": "Sähköposti", + "SettingsAppPreferences_ApplicationMode_Calendar": "Kalenteri", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description": "Wino Mail will keep running in the background. You will be notified as new mails arrive.", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title": "Run in the background", "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description": "Wino Mail will keep running on the system tray. Available to launch by clicking on an icon. You will be notified as new mails arrive.", @@ -506,12 +732,30 @@ "SettingsAppPreferences_StartupBehavior_FatalError": "Fatal error occurred while changing the startup mode for Wino Mail.", "SettingsAppPreferences_StartupBehavior_Title": "Start minimized on Windows startup", "SettingsAppPreferences_Title": "App Preferences", + "SettingsAppPreferences_HideWinoAccountButton_Title": "Piilota otsikkorivillä oleva Wino-tilin profiilipainike.", + "SettingsAppPreferences_HideWinoAccountButton_Description": "Piilota otsikkorivillä oleva profiilipainike, joka avaa Wino-tilin valikon.", + "SettingsAppPreferences_StoreUpdateNotifications_Title": "Store-päivitysilmoitukset", + "SettingsAppPreferences_StoreUpdateNotifications_Description": "Näytä ilmoitukset ja alatunnisteen toiminnot, kun Microsoft Store -päivitys on saatavilla.", + "SettingsAppPreferences_AiActions_Title": "Tekoälytoiminnot", + "SettingsAppPreferences_AiActions_Description": "Valitse tekoälyn oletuskielet ja mihin yhteenvetoja tallennetaan.", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Title": "Oletuskäännöksen kieli", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Description": "Valitse tekoälykäännösten oletuskieli.", + "SettingsAppPreferences_AiSummarizeLanguage_Title": "Tiivistämisen kieli", + "SettingsAppPreferences_AiSummarizeLanguage_Description": "Valitse tulevia AI-tiivistystuloksia varten haluamasi tiivistyskieli.", + "SettingsAppPreferences_AiSummarySavePath_Title": "Oletus tiivistelmien tallennuspaikka", + "SettingsAppPreferences_AiSummarySavePath_Description": "Valitse kansio, jota Wino käyttää oletuksena AI-tiivistelmien tallennuksessa.", + "SettingsAppPreferences_AiSummarySavePath_Placeholder": "Käytä järjestelmän oletustallennuspaikkaa", + "SettingsAppPreferences_AiSummarySavePath_InvalidHint": "Tätä kansiota ei ole olemassa. Tiivistelmien tallennuspaikkaa käytetään oletuksena.", "SettingsAutoSelectNextItem_Description": "Select the next item after you delete or move a mail.", "SettingsAutoSelectNextItem_Title": "Auto select next item", "SettingsAvailableThemes_Description": "Select a theme from Wino's own collection for your taste or apply your own themes.", "SettingsAvailableThemes_Title": "Available Themes", "SettingsCalendarSettings_Description": "Change first day of week, hour cell height and more...", "SettingsCalendarSettings_Title": "Calendar Settings", + "CalendarSettings_DefaultSnoozeDuration_Header": "Oletussnooze-kesto", + "CalendarSettings_DefaultSnoozeDuration_Description": "Aseta oletusviive kalenterimuistutusten siirtotoiminnolle.", + "CalendarSettings_TimedDayHeaderFormat_Header": "Ajastetun näkymän päivän otsikoiden muoto", + "CalendarSettings_TimedDayHeaderFormat_Description": "Valitse, miten päivän yläotsikot renderöidään päivän-, viikon- ja työviikkonäytöissä. Käytä päivämäärämuotojen tunnuksia kuten ddd, dd, MMM tai dddd.", "SettingsComposer_Title": "Composer", "SettingsComposerFont_Title": "Default Composer Font", "SettingsComposerFontFamily_Description": "Change the default font family and font size for composing mails.", @@ -531,6 +775,9 @@ "SettingsDiscord_Title": "Discord Channel", "SettingsEditLinkedInbox_Description": "Add / remove accounts, rename or break the link between accounts.", "SettingsEditLinkedInbox_Title": "Edit Linked Inbox", + "SettingsWindowBackdrop_Title": "Ikkunan tausta", + "SettingsWindowBackdrop_Description": "Valitse Wino-ikkunoiden taustaefekti.", + "SettingsWindowBackdrop_Disabled": "Ikkunan taustavaikutuksen valinta on poistettu käytöstä, kun sovelluksen teema on muu kuin Oletus.", "SettingsElementTheme_Description": "Select a Windows theme for Wino", "SettingsElementTheme_Title": "Element Theme", "SettingsElementThemeSelectionDisabled": "Element theme selection is disabled when application theme is selected other than Default.", @@ -581,6 +828,8 @@ "SettingsManageAliases_Title": "Aliases", "SettingsEditAccountDetails_Title": "Edit Account Details", "SettingsEditAccountDetails_Description": "Change account name, sender name and assign a new color if you like.", + "EditAccountDetailsPage_SaveSuccess_Title": "Muutokset tallennettu", + "EditAccountDetailsPage_SaveSuccess_Message": "Tilin tiedot on päivitetty onnistuneesti.", "SettingsManageLink_Description": "Move items to add new link or remove existing link.", "SettingsManageLink_Title": "Manage Link", "SettingsMarkAsRead_Description": "Change what should happen to the selected item.", @@ -596,7 +845,41 @@ "SettingsNotifications_Title": "Notifications", "SettingsNotificationsAndTaskbar_Description": "Change whether notifications should be displayed and taskbar badge for this account.", "SettingsNotificationsAndTaskbar_Title": "Notifications & Taskbar", + "SettingsHome_Title": "Koti", + "SettingsHome_SearchTitle": "Etsi asetus", + "SettingsHome_SearchDescription": "Hae ominaisuuden, aiheen tai avainsanan perusteella ja siirry suoraan oikealle asetussivulle.", + "SettingsHome_SearchPlaceholder": "Hae asetuksista", + "SettingsHome_SearchExamples": "Esimerkki: teema, tallennus, kieli, allekirjoitus", + "SettingsHome_QuickLinks_Title": "Nopeat linkit", + "SettingsHome_QuickLinks_Description": "Siirry nopeasti niihin asetuksiin, joita ihmiset käyttävät eniten.", + "SettingsHome_StorageCard_Description": "Näe, kuinka paljon paikallista MIME-sisältöä Wino pitää tässä laitteessa ja puhdista sitä tarvittaessa.", + "SettingsHome_StorageEmptySummary": "Ei välimuistin MIME-sisältöä havaittu vielä.", + "SettingsHome_StorageLoading": "Tarkistetaan paikallisen MIME-käytön määrää...", + "SettingsHome_Tips_Title": "Vinkit ja niksit", + "SettingsHome_Tips_Description": "Muutama pieni muutos voi tehdä Winosta paljon henkilökohtaisemman.", + "SettingsHome_Tip_Theme": "Haluatko tumma-tilan tai korostusvaihtoehtoja? Avaa Personointi.", + "SettingsHome_Tip_Background": "Käytä sovellusasetuksia hallitaksesi käynnistymistä ja taustasynkronointia.", + "SettingsHome_Tip_Shortcuts": "Pikanäppäimet auttavat liikkumaan sähköpostin läpi nopeammin.", + "SettingsHome_Resources_Title": "Hyödylliset linkit", + "SettingsHome_Resources_Description": "Avaa projektin resurssit, tuki-informaatio ja julkaisukanavat.", "SettingsOptions_Title": "Settings", + "SettingsOptions_GeneralSection": "Yleiset", + "SettingsOptions_MailSection": "Sähköposti", + "SettingsOptions_CalendarSection": "Kalenteri", + "SettingsOptions_MoreComingSoon": "Lisäominaisuuksia tulossa pian", + "SettingsOptions_HeroDescription": "Mukauta Wino Mail -kokemustasi.", + "SettingsOptions_AccountsSummary": "{0} tiliä on määritetty", + "SettingsSearch_ManageAccounts_Keywords": "tili;tilit;postilaatikko;postilaatikot;aliaksia;profiili;osoite;osoitteet", + "SettingsSearch_AppPreferences_Keywords": "käynnistys;tausta;käynnistys;synkronointi;ilmoitukset;ilmoitukset;haku;järjestelmäpalkki;oletusarvot", + "SettingsSearch_LanguageTime_Keywords": "kieli;aika;kello;paikkakunta;alue;muoto;24 tuntia;24h", + "SettingsSearch_Personalization_Keywords": "teema;tumma;vaalea;ulkoasu;korostus;väri;väri;tila;asettelu;tiheys", + "SettingsSearch_About_Keywords": "tietoja;versio;verkkosivusto;tietosuoja;github;lahjoita;kauppa;tuki", + "SettingsSearch_KeyboardShortcuts_Keywords": "pikakuvake;pikakuvakkeet;pikanäppäin;pikanäppäimet;näppäin;näppäimet", + "SettingsSearch_MessageList_Keywords": "viesti;viestit;luettelo;keskusteluketjut;ketjut;avatar;esikatselu;lähettäjä", + "SettingsSearch_ReadComposePane_Keywords": "lukija;laadi;laadi;fontti;fontit;ulkoisen sisällön;näyttö;luku", + "SettingsSearch_SignatureAndEncryption_Keywords": "allekirjoitus;allekirjoitukset;salaus;sertifikaatti;sertifikaatit;S-MIME;turvallisuus", + "SettingsSearch_Storage_Keywords": "tallennustila;välimuisti;välimuistitus;mime;levy;tilaa;siivous;paikallinen data", + "SettingsSearch_CalendarSettings_Keywords": "kalenteri;viikko;tunnit;aikataulu;tapahtuma;tapahtumat", "SettingsPaneLengthReset_Description": "Reset the size of the mail list to original if you have issues with it.", "SettingsPaneLengthReset_Title": "Reset Mail List Size", "SettingsPaypal_Description": "Show much more love ❤️ All donations are appreciated.", @@ -610,6 +893,8 @@ "SettingsPrefer24HourClock_Title": "Display Clock Format in 24 Hours", "SettingsPrivacyPolicy_Description": "Review privacy policy.", "SettingsPrivacyPolicy_Title": "Privacy Policy", + "SettingsWebsite_Description": "Avaa Wino Mail -verkkosivusto.", + "SettingsWebsite_Title": "Verkkosivusto", "SettingsReadComposePane_Description": "Fonts, external content.", "SettingsReadComposePane_Title": "Reader & Composer", "SettingsReader_Title": "Reader", @@ -625,6 +910,19 @@ "SettingsShowPreviewText_Title": "Show Preview Text", "SettingsShowSenderPictures_Description": "Hide/show the thumbnail sender pictures.", "SettingsShowSenderPictures_Title": "Show Sender Avatars", + "SettingsEmailTemplates_Title": "Sähköpostimallit", + "SettingsEmailTemplates_Description": "Hallitse sähköpostimallipohjia", + "SettingsEmailTemplates_CreatePageTitle": "Uusi malli", + "SettingsEmailTemplates_EditPageTitle": "Muokkaa mallia", + "SettingsEmailTemplates_NewTemplateTitle": "Uusi malli", + "SettingsEmailTemplates_NewTemplateDescription": "Luo uusi sähköpostimalli", + "SettingsEmailTemplates_NameTitle": "Nimi", + "SettingsEmailTemplates_NamePlaceholder": "Mallin nimi", + "SettingsEmailTemplates_DescriptionTitle": "Kuvaus", + "SettingsEmailTemplates_DescriptionPlaceholder": "Valinnainen kuvaus", + "SettingsEmailTemplates_ContentTitle": "Mallin sisältö", + "SettingsEmailTemplates_ContentDescription": "Muokkaa tämän mallin HTML-sisältöä.", + "SettingsEmailTemplates_NameRequired": "Mallin nimi on pakollinen.", "SettingsEnableGravatarAvatars_Title": "Gravatar", "SettingsEnableGravatarAvatars_Description": "Use gravatar (if available) as sender picture", "SettingsEnableFavicons_Title": "Domain icons (Favicons)", @@ -645,6 +943,33 @@ "SettingsStartupItem_Title": "Startup Item", "SettingsStore_Description": "Show some love ❤️", "SettingsStore_Title": "Rate in Store", + "SettingsStorage_Title": "Tallennus", + "SettingsStorage_Description": "Skannaa ja hallitse paikalliseen data-kansioon tallennettu MIME-välimuisti.", + "SettingsStorage_ScanFolder": "Skannaa paikallisen data-kansion", + "SettingsStorage_NoLocalMimeDataFound": "Paikallista MIME-tietoa ei löytynyt.", + "SettingsStorage_NoAccountsFound": "Tilit eivät löytyneet.", + "SettingsStorage_TotalUsage": "Paikallinen MIME-käyttö yhteensä: {0}", + "SettingsStorage_AccountUsageDescription": "{0} käytetty paikallisessa MIME-välimuistissa", + "SettingsStorage_DeleteAll_Title": "Poista kaikki MIME-sisällöt", + "SettingsStorage_DeleteAll_Description": "Poista tämän tilin koko MIME-välimuistihakemisto.", + "SettingsStorage_DeleteAll_Button": "Poista kaikki", + "SettingsStorage_DeleteAll_Confirm_Title": "Poista kaikki MIME-sisällöt", + "SettingsStorage_DeleteAll_Confirm_Message": "Poista kaikki paikalliset MIME-tiedot tilille {0}?", + "SettingsStorage_DeleteAll_Success": "Kaikki MIME-sisällöt poistettiin.", + "SettingsStorage_DeleteOld_Title": "Poista vanhat MIME-sisällöt", + "SettingsStorage_DeleteOld_Description": "Poista MIME-tiedostoja sähköpostin luontipäivämäärän perusteella paikallisessa tietokannassa.", + "SettingsStorage_DeleteOld_1Month": "Yli 1 kuukausi", + "SettingsStorage_DeleteOld_3Months": "Yli 3 kuukautta", + "SettingsStorage_DeleteOld_6Months": "Yli 6 kuukautta", + "SettingsStorage_DeleteOld_1Year": "Yli 1 vuosi", + "SettingsStorage_DeleteOld_Confirm_Title": "Poista vanhat MIME-sisällöt", + "SettingsStorage_DeleteOld_Confirm_Message": "Poista paikalliset MIME-tiedot, jotka ovat vanhempia kuin {0} tilillä {1}?", + "SettingsStorage_DeleteOld_Success": "Poistettu {0} MIME-kansiota vanhempia kuin {1}.", + "SettingsStorage_1Month": "1 kuukausi", + "SettingsStorage_3Months": "3 kuukautta", + "SettingsStorage_6Months": "6 kuukautta", + "SettingsStorage_1Year": "1 vuosi", + "SettingsStorage_Months": "{0} kuukautta", "SettingsTaskbarBadge_Description": "Include unread mail count in taskbar icon.", "SettingsTaskbarBadge_Title": "Taskbar Badge", "SettingsThreads_Description": "Organize messages into conversation threads.", @@ -683,6 +1008,9 @@ "SystemFolderConfigDialogValidation_InboxSelected": "You can't assign Inbox folder to any other system folder.", "SystemFolderConfigSetupSuccess_Message": "System folders are successfully configured.", "SystemFolderConfigSetupSuccess_Title": "System Folders Setup", + "SystemTrayMenu_ShowWino": "Avaa Wino Mail", + "SystemTrayMenu_ShowWinoCalendar": "Avaa Wino Kalenteri", + "SystemTrayMenu_ExitWino": "Poistu", "TestingImapConnectionMessage": "Testing server connection...", "TitleBarServerDisconnectedButton_Description": "Wino is disconnected from the network. Click reconnect to restore connection.", "TitleBarServerDisconnectedButton_Title": "no connection", @@ -699,8 +1027,422 @@ "WinoUpgradeMessage": "Upgrade to Unlimited Accounts", "WinoUpgradeRemainingAccountsMessage": "{0} out of {1} free accounts used.", "Yesterday": "Yesterday", + "Smime_ImportCertificates_Success": "Sertifikaatit tuotiin onnistuneesti.", + "Smime_ImportCertificates_Error": "Virhe sertifikaattien tuonnissa: {0}", + "Smime_RemoveCertificates_Confirm": "Haluatko todella poistaa sertifikaatit {0}?", + "Smime_RemoveCertificates_Success": "Sertifikaatit poistettu.", + "Smime_ExportCertificates_Success": "Sertifikaatit viety ulos.", + "Smime_ExportCertificates_Error": "Virhe sertifikaattien viennissä.", + "Smime_CertificateDetails": "Aihe: {0}\\nMyöntäjä: {1}\\nVoimassa alkaen: {2}\\nVoimassa asti: {3}\\nSormenjälki: {4}", + "Smime_CertificatePassword_Title": "Sertifikaatin salasana vaaditaan", + "Smime_CertificatePassword_Placeholder": "Sertifikaatin salasana kohteelle {0} (valinnainen)", + "Smime_Confirm_Title": "Vahvista", + "Buttons_OK": "OK", + "Buttons_Refresh": "Päivitä", + "SettingsSignatureAndEncryption_Title": "Allekirjoitus ja salaus", + "SettingsSignatureAndEncryption_Description": "Hallitse S/MIME-sertifikaatteja sähköpostien allekirjoittamiseen ja salaukseen.", + "SettingsSignatureAndEncryption_MyCertificatesHeader": "Omat sertifikaatit", + "SettingsSignatureAndEncryption_MyCertificatesDescription": "Henkilökohtaiset sertifikaatit allekirjoittamiseen ja salaukseen", + "SettingsSignatureAndEncryption_RecipientCertificatesHeader": "Vastaanottajien sertifikaatit", + "SettingsSignatureAndEncryption_RecipientCertificatesDescription": "Vastaanottajien sertifikaatit salauksen purkuun", + "SettingsSignatureAndEncryption_NameColumn": "Nimi", + "SettingsSignatureAndEncryption_ExpiresColumn": "Vanhentuu", + "SettingsSignatureAndEncryption_ThumbprintColumn": "Sormenjälki", + "Buttons_Remove": "Poista", + "Buttons_Export": "Vie", + "Buttons_Import": "Tuo", + "SettingsSignatureAndEncryption_SigningCertificate": "S/MIME-allekirjoitussertifikaatti", + "SettingsSignatureAndEncryption_EncryptionCertificate": "S/MIME-salaussertifikaatti", + "SettingsSignatureAndEncryption_SigningCertificatePlaceholder": "Ei mitään", + "SmimeSignaturesInMessage": "Tämän viestin allekirjoitukset:", + "SmimeSignatureEntry": "• {0} {1} ({2}, voimassa {3} - {4})", + "SmimeSigningCertificateInfoTitle": "S/MIME-allekirjoitussertifikaatin tiedot", + "SmimeCertificateInfoTitle": "S/MIME-sertifikaatin tiedot", + "SmimeNoCertificateFileFound": "Sertifikaatitiedostoa ei löytynyt", + "SmimeSaveCertificate": "Tallenna sertifikaatti...", + "SmimeCertificate": "S/MIME-sertifikaatti", + "SmimeCertificateSavedTo": "Sertifikaatti tallennettu kohteeseen {0}", + "SmimeSignedTooltip": "Tämä viesti on allekirjoitettu S/MIME-sertifikaatilla. Napsauta lisätietoja.", + "SmimeEncryptedTooltip": "Tämä viesti on salattu S/MIME-sertifikaatin avulla.", + "SmimeCertificateFileInfo": "Tiedosto: {0}", + "Composer_LightTheme": "Kevyt teema", + "Composer_DarkTheme": "Tumma teema", + "Composer_Outdent": "Poista sisennys", + "Composer_Indent": "Sisennä", + "Composer_BulletList": "Luettelomerkit", + "Composer_OrderedList": "Numeroitu lista", + "Composer_Stroke": "Viiva", + "Composer_Bold": "Lihavoitu", + "Composer_Italic": "Kursivoitu", + "Composer_Underline": "Alleviivaus", + "Composer_CcBcc": "Cc ja Bcc", + "Composer_EnableSmimeSignature": "Ota S/MIME-allekirjoitus käyttöön/poista käytöstä", + "Composer_EnableSmimeEncryption": "Ota S/MIME-salaus käyttöön/poista käytöstä", + "Composer_LocalDraftSyncInfo": "Tämä luonnos on vain paikallinen. Wino ei onnistunut lähettämään sitä sähköpostipalvelimellesi. Napsauta uudelleenlähettääksesi sen palvelimelle.", + "Composer_CertificateExpires": "Vanhentuu:", + "Composer_SmimeSignature": "S/MIME-allekirjoitus", + "Composer_SmimeEncryption": "S/MIME-salaus", + "Composer_EmailTemplatesPlaceholder": "Sähköpostimallit", + "Composer_AiSummarize": "Tiivistä tekoälyn avulla", + "Composer_AiSummarizeDescription": "Tuo tämän sähköpostin keskeiset kohdat, toimenpiteet ja päätökset.", + "Composer_AiTranslate": "Käännä tekoälyn avulla", + "Composer_AiActions": "Tekoälyn toimet", + "Composer_AiRewrite": "Uudelleenkirjoita tekoälyn avulla", + "AiActions_CheckingStatus": "Tarkistetaan tekoälyyn pääsyä...", + "AiActions_SignedOutTitle": "Poista Wino AI Packin lukitus", + "AiActions_SignedOutDescription": "Käännä, kirjoita ja tiivistä sähköposteja tekoälyn avulla kirjautumalla Wino-tilillesi ja aktivoimalla AI Pack -lisäosan.", + "AiActions_NoPackTitle": "AI Pack vaaditaan", + "AiActions_NoPackDescription": "Olet kirjautuneena sisään, mutta AI Pack ei ole vielä käytössä. Osta se käyttääksesi Wino'n tekoälyn käännöksiä, uudelleenkirjoitusta ja tiivistämistyökaluja.", + "AiActions_UsageSummary": "Tässä kuussa käytettyjen krediittien määrä: {0} / {1}", + "Composer_AiRewritePolite": "Tee siitä kohteliaampi", + "Composer_AiRewritePoliteDescription": "Lievittää sanamuotoa samalla säilyttäen saman tarkoituksen.", + "Composer_AiRewriteAngry": "Tee siitä vihaisen", + "Composer_AiRewriteAngryDescription": "Käyttää terävämpää ja konfrontatiivisempaa sävyä.", + "Composer_AiRewriteHappy": "Tee siitä iloinen", + "Composer_AiRewriteHappyDescription": "Lisää iloisemman ja innostuneemman sävyn.", + "Composer_AiRewriteFormal": "Tee siitä muodollinen", + "Composer_AiRewriteFormalDescription": "Saa viestin kuulostamaan ammattimaisemmalta ja jäsennellymmältä.", + "Composer_AiRewriteFriendly": "Tee siitä ystävällinen", + "Composer_AiRewriteFriendlyDescription": "Lämmittää viestiä lähestyttävämmällä sävyllä.", + "Composer_AiRewriteShorter": "Tee siitä lyhyempi", + "Composer_AiRewriteShorterDescription": "Tiivistää tekstin ja poistaa tarpeettomat yksityiskohdat.", + "Composer_AiRewriteClearer": "Tee siitä selkeämpi", + "Composer_AiRewriteClearerDescription": "Parantaa luettavuutta ja tekee viestistä helpommin seurattavan.", + "Composer_AiRewriteCustom": "Mukautettu", + "Composer_AiRewriteCustomDescription": "Kuvaile haluamasi uudelleenkirjoituksen tarkoitus.", + "Composer_AiRewriteCustomPlaceholder": "Kuvaile, miten haluat viestin kirjoitettavan uudelleen", + "Composer_AiRewriteMode": "Uudelleenkirjoituksen sävy", + "Composer_AiRewriteApply": "Käytä uudelleenkirjoitusta", + "Composer_AiTranslateDialogTitle": "Käännä tekoälyn avulla", + "Composer_AiTranslateDialogDescription": "Syötä kohdekieli tai kulttuurikoodi, kuten en-US, tr-TR, de-DE tai fr-FR.", + "Composer_AiTranslateApply": "Käännä", + "Composer_AiTranslateLanguage": "Kohdekieli", + "Composer_AiTranslateCustomPlaceholder": "Syötä kulttuurikoodi", + "Composer_AiTranslateLanguageEnglish": "Englanti (en-US)", + "Composer_AiTranslateLanguageTurkish": "Turkki (tr-TR)", + "Composer_AiTranslateLanguageGerman": "Saksa (de-DE)", + "Composer_AiTranslateLanguageFrench": "Ranska (fr-FR)", + "Composer_AiTranslateLanguageSpanish": "Espanja (es-ES)", + "Composer_AiTranslateLanguageItalian": "Italia (it-IT)", + "Composer_AiTranslateLanguagePortugueseBrazil": "Portugali (Brasília) (pt-BR)", + "Composer_AiTranslateLanguageDutch": "Hollanti (nl-NL)", + "Composer_AiTranslateLanguagePolish": "Puola (pl-PL)", + "Composer_AiTranslateLanguageRussian": "Venäjä (ru-RU)", + "Composer_AiTranslateLanguageJapanese": "Japani (ja-JP)", + "Composer_AiTranslateLanguageKorean": "Korea (ko-KR)", + "Composer_AiTranslateLanguageChineseSimplified": "Kiina, yksinkertaistettu (zh-CN)", + "Composer_AiTranslateLanguageArabic": "Arabia (ar-SA)", + "Composer_AiTranslateLanguageHindi": "Hindi (hi-IN)", + "Composer_AiTranslateLanguageOther": "Muu...", + "Composer_AiBusyTitle": "AI on jo käynnissä", + "Composer_AiBusyMessage": "Odota, kunnes nykyinen AI-toiminto valmistuu.", + "Composer_AiSignInRequired": "Kirjaudu sisään Wino-tilillesi käyttääksesi AI-ominaisuuksia.", + "Composer_AiMissingHtml": "Wino AI:lle ei ole vielä viestisisältöä.", + "Composer_AiQuotaUnavailable": "AI-tulos on käytetty.", + "Composer_AiAppliedMessage": "AI-tulos lisättiin kirjoittimeen. Jos haluat kumota sen, käytä Kumoa.", + "Composer_AiSummarizeSuccessTitle": "AI-yhteenveto otettu käyttöön.", + "Composer_AiTranslateSuccessTitle": "AI-käännös otettu käyttöön.", + "Composer_AiRewriteSuccessTitle": "AI-uudelleenkirjoitus otettu käyttöön.", + "Composer_AiErrorTitle": "AI-toiminto epäonnistui.", + "Reader_AiAppliedMessage": "AI-tulos on nyt näytetty tässä viestissä. Avaa viesti uudelleen nähdäksesi alkuperäisen sisällön.", "SettingsAppPreferences_EmailSyncInterval_Title": "Email sync interval", - "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail." + "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail.", + "ContactsPage_Title": "Yhteystiedot", + "ContactsPage_AddContact": "Lisää yhteystieto", + "ContactsPage_EditContact": "Muokkaa yhteystietoa", + "ContactsPage_DeleteContact": "Poista yhteystieto", + "ContactsPage_SearchPlaceholder": "Etsi yhteystietoja...", + "ContactsPage_NoContacts": "Yhteystietoja ei löytynyt", + "ContactsPage_ContactsCount": "{0} yhteystietoa", + "ContactsPage_SelectedContactsCount": "{0} valittua yhteystietoa", + "ContactsPage_DeleteSelectedContacts": "Poista valitut", + "ContactEditDialog_Title": "Muokkaa yhteystietoa", + "ContactEditDialog_PhotoSection": "Kuva", + "ContactEditDialog_ChoosePhoto": "Valitse kuva", + "ContactEditDialog_RemovePhoto": "Poista kuva", + "ContactEditDialog_NameHeader": "Nimi", + "ContactEditDialog_NamePlaceholder": "Yhteystiedon nimi", + "ContactEditDialog_EmailHeader": "Sähköpostiosoite", + "ContactEditDialog_EmailPlaceholder": "contact@example.com", + "ContactEditDialog_InfoSection": "Yhteystiedot", + "ContactEditDialog_RootContactInfo": "Tämä on tilillesi liittyvä pääyhteystieto, eikä sitä voi poistaa.", + "ContactEditDialog_OverriddenContactInfo": "Tätä yhteystietoa on muokattu manuaalisesti eikä sitä päivitetä synkronoinnin aikana.", + "ContactsPage_Subtitle": "Hallitse sähköpostiyhteystietojasi ja niiden tietoja", + "ContactStatus_Account": "Tili", + "ContactStatus_Modified": "Muokattu", + "ContactAction_Edit": "Muokkaa yhteystietoa", + "ContactAction_ChangePhoto": "Vaihda kuva", + "ContactAction_Delete": "Poista yhteystieto", + "ContactAction_Add": "Lisää yhteystieto", + "ContactSelection_Selected": "valittu", + "ContactSelection_SelectAll": "Valitse kaikki", + "ContactSelection_Clear": "Tyhjennä valinta", + "ContactsPage_EmptyState": "Näytettävissä ei ole yhteystietoja.", + "ContactsPage_AddFirstContact": "Lisää ensimmäinen yhteystietosi", + "ContactsPage_ContactsCountSuffix": "yhteystietoja", + "ContactsPane_NewContact": "Uusi yhteystieto", + "ContactsPane_DescriptionTitle": "Hallitse yhteystietojasi", + "ContactsPane_DescriptionBody": "Luo yhteystietoja, nimeä ne uudelleen, päivitä profiilikuvat ja pidä tallennetut tiedot järjestyksessä yhdessä paikassa.", + "ContactEditDialog_AddTitle": "Lisää yhteystieto", + "ContactInfoBar_ContactAdded": "Yhteystieto lisätty onnistuneesti.", + "ContactInfoBar_ContactUpdated": "Yhteystieto päivitetty onnistuneesti.", + "ContactInfoBar_ContactsDeleted": "Yhteystiedot poistettu onnistuneesti.", + "ContactInfoBar_ContactPhotoUpdated": "Yhteystietokuvan päivittäminen onnistui.", + "ContactInfoBar_FailedToLoadContacts": "Yhteystietojen lataaminen epäonnistui: {0}", + "ContactInfoBar_FailedToAddContact": "Yhteystiedon lisääminen epäonnistui: {0}", + "ContactInfoBar_FailedToUpdateContact": "Yhteystiedon päivittäminen epäonnistui: {0}", + "ContactInfoBar_FailedToDeleteContacts": "Yhteystietojen poistaminen epäonnistui: {0}", + "ContactInfoBar_FailedToUpdatePhoto": "Kuvan päivittäminen epäonnistui: {0}", + "ContactInfoBar_CannotDeleteRoot": "Juuriyhteystietoja ei voi poistaa.", + "ContactConfirmDialog_DeleteTitle": "Poista yhteystieto", + "ContactConfirmDialog_DeleteMessage": "Oletko varma, että haluat poistaa yhteystiedon '{0}'?", + "ContactConfirmDialog_DeleteMultipleMessage": "Oletko varma, että haluat poistaa {0} yhteystieto(a)?", + "ContactConfirmDialog_DeleteButton": "Poista", + "CalendarAccountSettings_Title": "Kalenteritilin asetukset", + "CalendarAccountSettings_Description": "Hallitse tilin {0} kalenteriasetuksia.", + "CalendarAccountSettings_AccountColor": "Tilin väri", + "CalendarAccountSettings_AccountColorDescription": "Muuta tämän kalenteritilin näyttöväriä.", + "CalendarAccountSettings_SyncEnabled": "Ota synkronointi käyttöön", + "CalendarAccountSettings_SyncEnabledDescription": "Ota kalenterin synkronointi käyttöön tai poista se käytöstä tälle tilille.", + "CalendarAccountSettings_DefaultShowAs": "Oletus Näytä tila", + "CalendarAccountSettings_DefaultShowAsDescription": "Uusien tapahtumien oletusarvoinen saatavuustila tämän tilin kanssa.", + "CalendarAccountSettings_PrimaryCalendar": "Ensisijainen kalenteri", + "CalendarAccountSettings_PrimaryCalendarDescription": "Merkitse tämä kalenteri tilin ensisijaiseksi kalenteriksi.", + "CalendarSettings_NewEventBehavior_Header": "Uuden tapahtuman painikkeen käyttäytyminen", + "CalendarSettings_NewEventBehavior_Description": "Valitse, pitäisikö Uuden tapahtuman -painikkeen kysyä kalenteria joka kerta vai avata aina tietyn kalenterin.", + "CalendarSettings_NewEventBehavior_AskEachTime": "Kysy joka kerta.", + "CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar": "Käytä aina tiettyä kalenteria.", + "CalendarSettings_Rendering_Title": "Renderointi", + "CalendarSettings_Rendering_Description": "Määritä kalenterin asettelu ja näyttökäyttäytyminen.", + "CalendarSettings_Notifications_Title": "Ilmoitukset", + "CalendarSettings_Notifications_Description": "Valitse oletusmuistutusten ja torkutusten toimintatapa.", + "CalendarSettings_Preferences_Title": "Asetukset", + "CalendarSettings_Preferences_Description": "Määritä, miten Uuden tapahtuman -painike toimii.", + "WhatIsNew_GetStartedButton": "Aloita", + "WhatIsNew_ContinueAnywayButton": "Jatka silti", + "WhatIsNew_PreparingForNewVersionButton": "Valmistelemme uutta versiota...", + "WhatIsNew_MigrationPreparing_Title": "Tietojesi valmistelu", + "WhatIsNew_MigrationPreparing_Description": "Wino soveltaa päivitysmigraatioita. Odota, kun valmistelemme tilisi tiedot tämän version julkaisua varten.", + "WhatIsNew_MigrationFailedMessage": "Migraatioiden soveltaminen epäonnistui virhekoodilla {0}. Voit jatkaa sovelluksen käyttöä. Mikäli ilmenee vakavia ongelmia, asenna sovellus uudelleen.", + "WhatIsNew_MigrationNotification_Title": "Wino Mail Päivitetty", + "WhatIsNew_MigrationNotification_Message": "Avaa sovellus päivittääksesi sen loppuun ja nähdäksesi, mitä uutta on.", + "WelcomeWindow_Title": "Tervetuloa Wino Mailiin", + "WelcomeWindow_Subtitle": "Natiivi Windows-kokemus Mailille ja Kalenterille.", + "WelcomeWindow_WhatsNewTitle": "Viimeisimmät muutokset", + "WelcomeWindow_FeaturesTitle": "Ominaisuudet", + "WelcomeWindow_WhatsNewTab": "Mitä uutta", + "WelcomeWindow_FeaturesTab": "Ominaisuudet", + "WelcomeWindow_GetStartedButton": "Aloita lisäämällä tilin", + "WelcomeWindow_GetStartedDescription": "Lisää Outlook-, Gmail- tai IMAP-tili aloittaaksesi Wino Mailin käytön.", + "WelcomeWindow_ImportFromWinoAccount": "Tuo Wino-tililtäsi", + "WelcomeWindow_ImportInProgress": "Tuodaan synkronoitujen asetusten ja tilien tiedot...", + "WelcomeWindow_ImportNoAccountsFound": "Wino-tililläsi ei löytynyt synkronoituja tilejä. Jos asetukset olivat käytettävissä, ne palautettiin. Käytä Aloita tilin lisäämisen manuaalisesti.", + "WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} synkronoitua tiliä on jo käytettävissä tässä laitteessa. Käytä Aloita tilin lisääminen manuaalisesti, jos tarvitset toisen tilin.", + "WelcomeWindow_SetupTitle": "Ota tilisi käyttöön", + "WelcomeWindow_SetupSubtitle": "Valitse sähköpostipalvelusi aloittaaksesi.", + "WelcomeWindow_AddAccountButton": "Lisää tili", + "WelcomeWindow_SkipForNow": "Hyppää hetkeksi — asennan sen myöhemmin.", + "WelcomeWindow_AppDescription": "Nopea, keskittynyt postilaatikko – uudelleen suunniteltu Windows 11:lle", + "WelcomeWizard_Step1Title": "Tervetuloa", + "SystemTrayMenu_Open": "Avaa", + "WinoAccount_Titlebar_SyncBenefitTitle": "Synkronointiasetukset", + "WinoAccount_Titlebar_SyncBenefitDescription": "Pidä Wino-asetuksesi synkronoituina useilla laitteilla.", + "WinoAccount_Titlebar_AddonsBenefitTitle": "Ota lisäosat käyttöön", + "WinoAccount_Titlebar_AddonsBenefitDescription": "Pääsy premium-ominaisuuksiin, kuten Wino AI Pack.", + "WinoAccount_Management_Description": "Hallitse Wino-tiliä, AI Pack -käyttöoikeuksia sekä synkronoitujen asetusten ja tilitietojen hallintaa.", + "WinoAccount_Management_SignedOutTitle": "Kirjaudu sisään Wino Mailiin", + "WinoAccount_Management_SignedOutDescription": "Kirjaudu sisään tai luo tili synkronoidaaksesi sähköpostisi, käyttääksesi AI-ominaisuuksia ja hallinnoidaksesi asetuksiasi useilla laitteilla.", + "WinoAccount_Management_ProfileSectionHeader": "Profiili", + "WinoAccount_Management_AddOnsSectionHeader": "Wino-lisäosat", + "WinoAccount_Management_DataSectionHeader": "Tiedot", + "WinoAccount_Management_AccountActionsSectionHeader": "Tilin toiminnot", + "WinoAccount_Management_AccountCardTitle": "Tili", + "WinoAccount_Management_AccountCardDescription": "Wino-tilisi sähköpostiosoite ja nykyinen tilitila.", + "WinoAccount_Management_AiPackCardTitle": "AI Pack", + "WinoAccount_Management_AiPackCardDescription": "Näet, onko Wino AI Pack aktiivinen ja kuinka paljon käyttöä on jäljellä.", + "WinoAccount_Management_AiPackActive": "AI Pack on käytössä", + "WinoAccount_Management_AiPackInactive": "AI Pack ei ole käytössä", + "WinoAccount_Management_AiPackUsage": "{0} / {1} käyttökertaa käytetty. {2} jäljellä.", + "WinoAccount_Management_AiPackBillingPeriod": "Laskutusjakso: {0:d} - {1:d}", + "WinoAccount_Management_AiPackUnknownUsage": "Käyttötietoja ei ole vielä saatavilla.", + "WinoAccount_Management_AiPackBuyDescription": "Osta Wino AI Pack kääntääksesi, uudelleenkirjoittaaksesi tai tiivistääksesi sähköposteja tekoälyn avulla.", + "WinoAccount_Management_AiPackPromoTitle": "Ota AI Pack käyttöön", + "WinoAccount_Management_AiPackPromoDescription": "Vahvista sähköpostityöskentelyä tekoälypohjaisilla työkaluilla. Käännä viestit yli 50 kielelle, kirjoita uudelleen selkeyden ja sävyn parantamiseksi sekä saat välittömät yhteenvetot pitkistä keskusteluista.", + "WinoAccount_Management_AiPackPromoPrice": "$4.99 / kk", + "WinoAccount_Management_AiPackPromoRequests": "1 000 krediittiä", + "WinoAccount_Management_AiPackGetButton": "Hae AI Pack", + "WinoAddOn_AI_PACK_Name": "Wino AI Pack", + "WinoAddOn_AI_PACK_Description": "AI-pohjaisia työkaluja kääntä-, uudelleenkirjoittamis- ja tiivistystoimintoja Wino Mailiin.", + "WinoAddOn_AI_PACK_Keywords": "AI, kääntäminen, uudelleenkirjoittaminen, tiivistäminen, tuottavuus", + "WinoAddOn_UNLIMITED_ACCOUNTS_Name": "Rajoittamattomat tilit", + "WinoAddOn_UNLIMITED_ACCOUNTS_Description": "Poista tilirajoitus ja lisää haluamasi määrän sähköpostitilejä.", + "WinoAddOn_UNLIMITED_ACCOUNTS_Keywords": "tilit, rajoittamattomat, premium, lisäosa", + "WinoAccount_Management_PurchaseRequiresSignIn": "Kirjaudu sisään Wino-tililläsi tämän oston suorittamiseksi.", + "WinoAccount_Management_PurchaseStartFailed": "Wino ei voinut suorittaa tätä Microsoft Store -ostoa.", + "WinoAccount_Management_StoreSyncFailed": "Ostoksesi on valmis, mutta Wino ei voinut päivittää tilisi etuja vielä. Yritä uudelleen hetken kuluttua.", + "WinoAccount_Management_AiPackSubscriptionActive": "Tilaus on aktiivinen", + "WinoAccount_Management_AiPackRenews": "Uudistuu {0:d}", + "WinoAccount_Management_AiPackRequestsUsed": "Tässä kuussa käytetyt krediitit", + "WinoAccount_Management_AiPackResets": "Nollaukset {0:d}", + "WinoAccount_Management_AiPackUsageLoadFailed": "Emme voineet ladata AI-käyttösaldoasi.", + "WinoAccount_Management_AiPackFeatureTranslate": "Käännä", + "WinoAccount_Management_AiPackFeatureRewrite": "Uudelleenkirjoita", + "WinoAccount_Management_AiPackFeatureSummarize": "Tiivistä", + "WinoAccount_Management_AddOnLoadFailed": "Meillä ilmeni ongelmia tämän lisäosan lataamisessa.", + "WinoAccount_Management_SyncPreferencesTitle": "Synkronoi asetukset ja tilit", + "WinoAccount_Management_SyncPreferencesDescription": "Tuo tai vie Wino-asetuksesi ja postilaatikon tiedot eri laitteiden välillä. Salasanat, tokenit ja muut herkät tiedot eivät koskaan synkronoidu.", + "WinoAccount_Management_SignOutTitle": "Kirjaudu ulos", + "WinoAccount_Management_SignOutDescription": "Kirjaudu ulos tililtäsi tästä laitteesta", + "WinoAccount_Management_StatusLabel": "Tila: {0}", + "WinoAccount_Management_NoRemoteSettings": "Tälle tilille ei ole vielä tallennettu synkronoituja tietoja.", + "WinoAccount_Management_ExportSucceeded": "Valitut Wino-tietosi vietiin menestyksekkäällä.", + "WinoAccount_Management_ExportPreferencesSucceeded": "Asetuksesi vietiin Wino-tilillesi.", + "WinoAccount_Management_ExportAccountsSucceeded": "Viedyt {0} tilin tiedot Wino-tilillesi.", + "WinoAccount_Management_ImportSucceeded": "Tuotiin synkronoituja tietoja Wino-tililtäsi.", + "WinoAccount_Management_ImportPreferencesSucceeded": "Käytettiin {0} synkronoitua asetusta.", + "WinoAccount_Management_ImportAccountsSucceeded": "Tuotiin {0} tiliä.", + "WinoAccount_Management_ImportDuplicateAccountsSkipped": "Ohitettiin {0} tiliä, jotka ovat jo olemassa tässä laitteessa.", + "WinoAccount_Management_ImportPartial": "Sovellettu {0} synkronoituja asetuksia. {1} asetusta ei voitu palauttaa.", + "WinoAccount_Management_ImportReloginReminder": "Salasanat, tunnukset ja muut arkaluonteiset tiedot eivät synkronoidu. Kirjaudu uudelleen jokaiselle tilille tässä laitteessa ennen käyttöä.", + "WinoAccount_Management_SerializeFailed": "Wino ei voinut sarjoittaa nykyisiä asetuksiasi.", + "WinoAccount_Management_EmptyExport": "Ei siirrettäviä asetuksia.", + "WinoAccount_Management_ImportEmpty": "Synkronoidussa tiedoissa ei ole mitään palautettavaa.", + "WinoAccount_Management_ExportDialog_Title": "Vie tiedot Wino-tilillesi", + "WinoAccount_Management_ExportDialog_Description": "Valitse, mitä haluat synkronoida Wino-tilillesi.", + "WinoAccount_Management_ExportDialog_IncludePreferences": "Asetukset", + "WinoAccount_Management_ExportDialog_IncludeAccounts": "Tilit", + "WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Salasanat, tunnukset ja muut arkaluonteiset tiedot eivät synkronoidu.", + "WinoAccount_Management_ExportDialog_AccountsRelogin": "Toisella tietokoneella tuodut tilit vaativat vielä uudelleen kirjautumisen ennen kuin niitä voidaan käyttää.", + "WinoAccount_Management_ExportDialog_InProgress": "Viedään valitut Wino-tiedot...", + "WinoAccount_Management_LoadFailed": "Wino ei voinut ladata uusinta Wino-tilin tietoa.", + "WinoAccount_Management_ActionFailed": "Wino-tilin pyyntöä ei voitu suorittaa.", + "WinoAccount_SettingsSection_Title": "Wino-tili", + "WinoAccount_SettingsSection_Description": "Luo Wino-tili tai kirjaudu sisään käyttämällä paikallista todentamispalvelua.", + "WinoAccount_RegisterButton_Title": "Rekisteröi tili", + "WinoAccount_RegisterButton_Description": "Luo Wino-tili sähköpostiosoitteella ja salasanalla.", + "WinoAccount_RegisterButton_Action": "Avaa rekisteröinti", + "WinoAccount_LoginButton_Title": "Kirjaudu sisään", + "WinoAccount_LoginButton_Description": "Kirjaudu sisään olemassa olevaan Wino-tiliin sähköpostiosoitteella ja salasanalla.", + "WinoAccount_LoginButton_Action": "Avaa kirjautuminen", + "WinoAccount_SignOutButton_Title": "Kirjaudu ulos", + "WinoAccount_SignOutButton_Description": "Poista paikallisesti tallennettu Wino-tilin istunto.", + "WinoAccount_SignOutButton_Action": "Kirjaudu ulos", + "WinoAccount_RegisterDialog_Title": "Luo Wino-tili", + "WinoAccount_RegisterDialog_Description": "Luo Wino-tili, jotta Wino-kokemuksesi pysyy synkronoituna ja avaa tilikohtaiset lisäosat.", + "WinoAccount_RegisterDialog_HeroTitle": "Luo Wino-tilisi", + "WinoAccount_RegisterDialog_BenefitsTitle": "Miksi luoda tilin?", + "WinoAccount_RegisterDialog_BenefitSyncTitle": "Tuo ja vie asetukset laitteiden välillä", + "WinoAccount_RegisterDialog_BenefitSyncDescription": "Siirrä Wino-asetuksesi laitteiden välillä ilman että joudut aloittamaan asennuksen alusta.", + "WinoAccount_RegisterDialog_BenefitAiTitle": "Pääsy eksklusiivisiin lisäosiin, kuten Wino AI Pack (maksullinen)", + "WinoAccount_RegisterDialog_BenefitAiDescription": "Käytä yhtä tiliä ottaaksesi käyttöön premium-Wino-ominaisuudet sitä mukaa kun ne tulevat saataville.", + "WinoAccount_RegisterDialog_DifferenceTitle": "Wino-tili on erillinen sähköpostitileistäsi", + "WinoAccount_RegisterDialog_DifferenceDescription": "Outlook-, Gmail-, IMAP- tai muut sähköpostitilisi pysyvät sellaisina kuin ne ovat. Wino-tili hallinnoi vain Winoon liittyviä ominaisuuksia ja tilikohtaisia lisäosia.", + "WinoAccount_RegisterDialog_PrimaryButton": "Rekisteröidy", + "WinoAccount_RegisterDialog_PrivacyTitle": "Tietosuoja ja API-käsittely", + "WinoAccount_RegisterDialog_PrivacyDescription": "Valinnaiset lisäosat, kuten Wino AI Pack, voivat lähettää valitun sähköpostin HTML-sisällön Wino API -palveluun vain käytettäessä näitä ominaisuuksia.", + "WinoAccount_RegisterDialog_PrivacyLinkText": "Lue tietosuojakäytäntö", + "WinoAccount_RegisterDialog_PrivacyCheckbox": "Hyväksyn tietosuojakäytännön.", + "WinoAccount_LoginDialog_Title": "Kirjaudu Wino-tiliin", + "WinoAccount_LoginDialog_Description": "Kirjaudu Wino-tilillesi, jotta asetuksesi pysyvät synkronoituina ja voit käyttää tiliin perustuvia ominaisuuksia.", + "WinoAccount_LoginDialog_HeroTitle": "Tervetuloa takaisin", + "WinoAccount_LoginDialog_BenefitsTitle": "Mitä sisäänkirjautuminen antaa sinulle", + "WinoAccount_LoginDialog_BenefitsDescription": "Käytä Wino-tiliäsi jatkaaksesi asetusten synkronointia laitteiden välillä ja saadaksesi pääsyn maksullisiin lisäosiin, kuten Wino AI Pack.", + "WinoAccount_LoginDialog_DifferenceTitle": "Tämä ei ole sähköpostilaatikkosi tilin sisäänkirjautuminen", + "WinoAccount_LoginDialog_DifferenceDescription": "Tässä kirjautuminen ei lisää eikä korvaa Outlook-, Gmail- tai IMAP-tilejä Winoon. Se kirjaa sinut vain Wino-spesifisiin palveluihin.", + "WinoAccount_LoginDialog_ForgotPasswordLink": "Unohditko salasanan?", + "WinoAccount_EmailLabel": "Sähköpostiosoite", + "WinoAccount_EmailPlaceholder": "name@example.com", + "WinoAccount_PasswordLabel": "Salasana", + "WinoAccount_ConfirmPasswordLabel": "Vahvista salasana", + "WinoAccount_ForgotPasswordDialog_Title": "Nollaa salasanasi", + "WinoAccount_ForgotPasswordDialog_PrimaryButton": "Lähetä palautussähköposti", + "WinoAccount_ForgotPasswordDialog_BackToSignIn": "Takaisin sisäänkirjautumiseen", + "WinoAccount_ForgotPasswordDialog_Description": "Syötä Wino-tilisi sähköpostiosoite, ja lähetämme salasanan palautuslinkin, jos osoite on rekisteröity.", + "WinoAccount_Validation_EmailRequired": "Sähköpostiosoite on pakollinen.", + "WinoAccount_Validation_PasswordRequired": "Salasana on pakollinen.", + "WinoAccount_Validation_PasswordMismatch": "Salasanat eivät täsmää.", + "WinoAccount_Validation_PrivacyConsentRequired": "Sinun on hyväksyttävä tietosuojakäytäntö ennen Wino-tilin luomista.", + "WinoAccount_Error_InvalidCredentials": "Sähköpostiosoite tai salasana on virheellinen.", + "WinoAccount_Error_AccountLocked": "Tili on tilapäisesti lukittu.", + "WinoAccount_Error_AccountBanned": "Tili on estetty.", + "WinoAccount_Error_AccountSuspended": "Tili on keskeytetty.", + "WinoAccount_Error_EmailNotConfirmed": "Vahvista sähköpostiosoitteesi ennen sisäänkirjautumista.", + "WinoAccount_Error_EmailConfirmationRequired": "Vahvista sähköpostiosoitteesi ennen sisäänkirjautumista.", + "WinoAccount_Error_EmailConfirmationResendNotAvailable": "Uusi vahvistussähköposti ei ole vielä saatavilla.", + "WinoAccount_Error_EmailConfirmationResendInvalid": "Tämä vahvistuspyyntö ei ole enää voimassa. Yritä kirjautua sisään uudelleen.", + "WinoAccount_Error_EmailNotRegistered": "Tämä sähköpostiosoite ei ole rekisteröity.", + "WinoAccount_Error_RefreshTokenInvalid": "Istuntosi ei ole enää voimassa. Kirjaudu uudelleen.", + "WinoAccount_Error_EmailAlreadyRegistered": "Tämä sähköpostiosoite on jo rekisteröity.", + "WinoAccount_Error_ExternalLoginEmailRequired": "Ulkoisen sisäänkirjautumisen toteuttamiseksi vaaditaan sähköpostiosoite.", + "WinoAccount_Error_ExternalLoginInvalid": "Ulkoisen sisäänkirjautumisen pyyntö on virheellinen.", + "WinoAccount_Error_ExternalAuthStateInvalid": "Ulkoisen sisäänkirjautumistilan arvo on virheellinen tai vanhentunut.", + "WinoAccount_Error_ExternalAuthCodeInvalid": "Ulkoisen sisäänkirjautumisen koodi on virheellinen tai vanhentunut.", + "WinoAccount_Error_AiPackRequired": "Tähän toimenpiteeseen vaaditaan voimassa oleva Wino AI Pack -tilaus.", + "WinoAccount_Error_AiQuotaExceeded": "Nykyisen laskutusjakson AI Packin käyttöraja on ylitetty.", + "WinoAccount_Error_AiHtmlEmpty": "Käsiteltävissä olevaa sähköpostisisältöä ei ole.", + "WinoAccount_Error_AiHtmlTooLarge": "Tämä sähköposti on liian suuri käsitellä Wino AI:lla.", + "WinoAccount_Error_AiUnsupportedLanguage": "Tätä kieltä ei tueta. Kokeile kelvollista kulttuurikoodia kuten en-US tai tr-TR.", + "WinoAccount_Error_Forbidden": "Sinulla ei ole oikeuksia suorittaa tätä toimintoa.", + "WinoAccount_Error_ValidationFailed": "Pyyntö on virheellinen. Tarkista syötetyt arvot.", + "WinoAccount_RegisterSuccessMessage": "Wino-tilin rekisteröinti suoritettu tilille {0}.", + "WinoAccount_LoginSuccessMessage": "Kirjauduttu Wino-tiliin käyttäjänä {0}.", + "WinoAccount_EmailConfirmationSentDialog_Title": "Vahvista sähköpostiosoitteesi", + "WinoAccount_EmailConfirmationSentDialog_Message": "Lähetimme sähköpostivahvistuksen osoitteeseen {0}. Vahvista se ja yritä kirjautua sisään uudelleen.", + "WinoAccount_EmailConfirmationPendingDialog_Title": "Sähköpostivahvistus vaaditaan", + "WinoAccount_EmailConfirmationPendingDialog_Message": "Odotamme edelleen, että vahvistat {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ResendButton": "Lähetä vahvistusviesti uudelleen", + "WinoAccount_EmailConfirmationPendingDialog_Countdown": "Vahvistusviestin uudelleenlähetys on mahdollista {0} kuluttua.", + "WinoAccount_EmailConfirmationPendingDialog_ReadyToResend": "Nyt voit lähettää vahvistusviestin uudelleen.", + "WinoAccount_EmailConfirmationResentDialog_Title": "Vahvistusviesti lähetetty uudelleen", + "WinoAccount_EmailConfirmationResentDialog_Message": "Lähetimme toisen vahvistusviestin osoitteeseen {0}. Vahvista se ja yritä kirjautua sisään uudelleen.", + "WinoAccount_ForgotPasswordDialog_SuccessTitle": "Salasanan palautusviesti lähetetty.", + "WinoAccount_ForgotPasswordDialog_SuccessMessage": "Lähetimme salasanan palautussähköpostin osoitteeseen {0}. Avaa viesti valitaksesi uuden salasanan.", + "WinoAccount_ChangePassword_Title": "Vaihda salasana", + "WinoAccount_ChangePassword_Description": "Lähetä salasanan palautussähköposti tälle Wino-tilille.", + "WinoAccount_ChangePassword_Action": "Lähetä palautussähköposti", + "WinoAccount_ChangePassword_ConfirmationMessage": "Haluatko, että Wino lähettää salasanan palautussähköpostin osoitteeseen {0}?", + "WinoAccount_SignOut_SuccessMessage": "Olet kirjautunut ulos Wino-tililtä {0}.", + "WinoAccount_SignOut_NoAccountMessage": "Ei ole aktiivista Wino-tiliä, josta kirjautua ulos.", + "WinoAccount_Titlebar_SignedOutTitle": "Wino-tili", + "WinoAccount_Titlebar_SignedOutDescription": "Kirjaudu sisään tai luo Wino-tili hallinnoidaksesi Wino-istuntoasi.", + "WinoAccount_Titlebar_SignedInStatus": "Tila: {0}", + "WelcomeWizard_Step2Title": "Lisää tili", + "WelcomeWizard_Step3Title": "Viimeistele asennus", + "ProviderSelection_Title": "Valitse sähköpostipalveluntarjoajasi", + "ProviderSelection_Subtitle": "Valitse alla oleva palveluntarjoaja lisätäksesi sähköpostitilisi Wino Mailiin.", + "ProviderSelection_AccountNameHeader": "Tilin nimi", + "ProviderSelection_AccountNamePlaceholder": "esim. Henkilökohtainen, Työ", + "ProviderSelection_DisplayNameHeader": "Näytettävä nimi", + "ProviderSelection_DisplayNamePlaceholder": "esim. John Doe", + "ProviderSelection_EmailHeader": "Sähköpostiosoite", + "ProviderSelection_EmailPlaceholder": "esim. johndoe@example.com", + "ProviderSelection_AppPasswordHeader": "Sovelluskohtainen salasana", + "ProviderSelection_AppPasswordHelp": "Kuinka saan sovelluskohtaisen salasanan?", + "ProviderSelection_CalendarModeHeader": "Kalenterin integrointi", + "ProviderSelection_CalendarMode_DisabledTitle": "Pois käytöstä", + "ProviderSelection_CalendarMode_DisabledDescription": "Kalenterin integrointi ei ole käytössä.", + "ProviderSelection_CalendarMode_CalDavTitle": "CalDAV-synkronointi", + "ProviderSelection_CalendarMode_CalDavDescription_Apple": "Kalenteritapahtumasi synkronoidaan Apple-palvelinten välillä laitteidesi välillä.", + "ProviderSelection_CalendarMode_CalDavDescription_Yahoo": "Kalenteritapahtumasi synkronoidaan Yahoo-palvelimien välillä laitteidesi välillä.", + "ProviderSelection_CalendarMode_LocalTitle": "Paikallinen kalenteri", + "ProviderSelection_CalendarMode_LocalDescription": "Tapahtumasi tallennetaan vain tietokoneellesi. Ei palvelinyhteyttä.", + "ProviderSelection_ClearColor": "Tyhjennä väri", + "ProviderSelection_ContinueButton": "Jatka", + "ProviderSelection_SpecialImap_Subtitle": "Anna tilin kirjautumistiedot yhdistääksesi sen.", + "AccountSetup_Title": "Tilin määrittäminen", + "AccountSetup_Step_Authenticating": "Vahvistetaan {0}:n kanssa", + "AccountSetup_Step_TestingMailAuth": "Tarkistetaan sähköpostin todentaminen", + "AccountSetup_Step_SyncingFolders": "Kansioiden metatietojen synkronointi", + "AccountSetup_Step_FetchingProfile": "Hakee profiilitietoja", + "AccountSetup_Step_DiscoveringCalDav": "CalDAV-asetusten selvittäminen", + "AccountSetup_Step_TestingCalendarAuth": "Testataan kalenterin todennusta", + "AccountSetup_Step_SavingAccount": "Tallennetaan tilitiedot", + "AccountSetup_Step_FetchingCalendarMetadata": "Haetaan kalenterimetatietoja", + "AccountSetup_Step_SyncingAliases": "Aliasien synkronointi", + "AccountSetup_Step_Finalizing": "Asetusten viimeistely", + "AccountSetup_FailureMessage": "Asetus epäonnistui. Palaa takaisin korjataksesi asetuksesi tai yritä uudelleen myöhemmin.", + "AccountSetup_SuccessMessage": "Tilisi on luotu onnistuneesti!", + "AccountSetup_GoBackButton": "Palaa takaisin", + "AccountSetup_TryAgainButton": "Yritä uudelleen", + "ImapCalDavSettings_AutoDiscoveryFailed": "Automaattinen havaitseminen epäonnistui. Anna asetukset manuaalisesti Lisäasetukset-välilehdellä." } - - diff --git a/Wino.Core.Domain/Translations/fr_FR/resources.json b/Wino.Core.Domain/Translations/fr_FR/resources.json index 427d902c..16b955db 100644 --- a/Wino.Core.Domain/Translations/fr_FR/resources.json +++ b/Wino.Core.Domain/Translations/fr_FR/resources.json @@ -8,6 +8,7 @@ "AccountCacheReset_Message": "Ce compte nécessite une resynchronisation complète pour continuer à fonctionner. Veuillez patienter pendant que Wino synchronise à nouveau vos messages...", "AccountContactNameYou": "Vous", "AccountCreationDialog_Completed": "tout est prêt", + "AccountCreationDialog_FetchingCalendarMetadata": "Récupération des détails du calendrier.", "AccountCreationDialog_FetchingEvents": "Récupération des événements de l'agenda.", "AccountCreationDialog_FetchingProfileInformation": "Récupération des détails du profil.", "AccountCreationDialog_GoogleAuthHelpClipboardText_Row0": "Si votre navigateur ne se lance pas automatiquement pour terminer l'authentification :", @@ -17,6 +18,7 @@ "AccountCreationDialog_Initializing": "initialisation", "AccountCreationDialog_PreparingFolders": "Récupération des informations du dossier.", "AccountCreationDialog_SigninIn": "Enregistrement des informations du compte.", + "Purchased": "Acheté", "AccountEditDialog_Message": "Nom du compte", "AccountEditDialog_Title": "Modifier le compte", "AccountPickerDialog_Title": "Choisir un compte", @@ -26,6 +28,10 @@ "AccountDetailsPage_Description": "Modifiez le nom du compte dans Wino et définissez le nom de l'expéditeur désiré.", "AccountDetailsPage_ColorPicker_Title": "Couleur du compte", "AccountDetailsPage_ColorPicker_Description": "Attribuez une nouvelle couleur de compte pour coloriser son symbole dans la liste.", + "AccountDetailsPage_TabGeneral": "Général", + "AccountDetailsPage_TabMail": "Courrier", + "AccountDetailsPage_TabCalendar": "Calendrier", + "AccountDetailsPage_CalendarListDescription": "Sélectionnez un calendrier pour configurer ses paramètres.", "AddHyperlink": "Ajouter", "AppCloseBackgroundSynchronizationWarningTitle": "Synchronisation en arrière-plan", "AppCloseStartupLaunchDisabledWarningMessageFirstLine": "L'application n'a pas été configurée pour être lancée au démarrage de Windows.", @@ -47,8 +53,10 @@ "BasicIMAPSetupDialog_Title": "Compte IMAP", "Busy": "Occupé", "Buttons_AddAccount": "Ajouter un compte", + "Buttons_FixAccount": "Réparer le compte", "Buttons_AddNewAlias": "Ajouter un nouvel alias", "Buttons_Allow": "Autoriser", + "Buttons_Apply": "Appliquer", "Buttons_ApplyTheme": "Appliquer le thème", "Buttons_Browse": "Parcourir", "Buttons_Cancel": "Annuler", @@ -62,6 +70,7 @@ "Buttons_Edit": "Éditer", "Buttons_EnableImageRendering": "Activer", "Buttons_Multiselect": "Sélectionner", + "Buttons_Manage": "Gérer", "Buttons_No": "Non", "Buttons_Open": "Ouvrir", "Buttons_Purchase": "Acheter", @@ -70,15 +79,134 @@ "Buttons_Save": "Enregistrer", "Buttons_SaveConfiguration": "Enregistrer la configuration", "Buttons_Send": "Envoyer", + "Buttons_SendToServer": "Envoyer au serveur", "Buttons_Share": "Partager", "Buttons_SignIn": "Connexion", "Buttons_Sync": "Synchroniser", "Buttons_SyncAliases": "Synchroniser les alias", "Buttons_TryAgain": "Réessayer", "Buttons_Yes": "Oui", + "Sync_SynchronizingFolder": "Synchronisation {0} {1}%", + "Sync_DownloadedMessages": "Messages téléchargés {0} de {1}", + "SyncAction_Archiving": "Archivage de {0} courrier(s)", + "SyncAction_ClearingFlag": "Suppression du drapeau pour {0} courrier(s)", + "SyncAction_CreatingDraft": "Création d'un brouillon", + "SyncAction_CreatingEvent": "Création d'un événement", + "SyncAction_Deleting": "Suppression de {0} courrier(s)", + "SyncAction_EmptyingFolder": "Vider le dossier", + "SyncAction_MarkingAsRead": "Marquage de {0} courrier(s) comme lu", + "SyncAction_MarkingAsUnread": "Marquage de {0} courrier(s) comme non lu", + "SyncAction_MarkingFolderAsRead": "Marquage du dossier comme lu", + "SyncAction_Moving": "Déplacement de {0} courrier(s)", + "SyncAction_MovingToFocused": "Déplacement de {0} courrier(s) vers Focused", + "SyncAction_RenamingFolder": "Renommer le dossier", + "SyncAction_SendingMail": "Envoi du courrier", + "SyncAction_SettingFlag": "Marquage du drapeau pour {0} courrier(s)", + "SyncAction_SynchronizingAccount": "Synchronisation de {0}", + "SyncAction_SynchronizingAccounts": "Synchronisation de {0} compte(s)", + "SyncAction_SynchronizingCalendarData": "Synchronisation des données du calendrier", + "SyncAction_SynchronizingCalendarEvents": "Synchronisation des événements du calendrier", + "SyncAction_SynchronizingCalendarMetadata": "Synchronisation des métadonnées du calendrier", + "SyncAction_Unarchiving": "Désarchivage de {0} courrier(s)", "CalendarAllDayEventSummary": "événements d'une journée entière", "CalendarDisplayOptions_Color": "Couleur ", "CalendarDisplayOptions_Expand": "Agrandir", + "CalendarEventResponse_Accept": "Accepter", + "CalendarEventResponse_AcceptedResponse": "Accepté", + "CalendarEventResponse_Decline": "Refuser", + "CalendarEventResponse_DeclinedResponse": "Refusé", + "CalendarEventResponse_NotResponded": "Non répondu", + "CalendarEventResponse_Tentative": "Provisoire", + "CalendarEventResponse_TentativeResponse": "Provisoire", + "CalendarEventRsvpPanel_Accept": "Accepter", + "CalendarEventRsvpPanel_AddMessage": "Ajouter un message à votre réponse... (optionnel)", + "CalendarEventRsvpPanel_Decline": "Refuser", + "CalendarEventRsvpPanel_Message": "Message", + "CalendarEventRsvpPanel_SendReplyMessage": "Envoyer un message de réponse", + "CalendarEventRsvpPanel_Tentative": "Provisoire", + "CalendarEventRsvpPanel_Title": "Options de réponse", + "CalendarAttendeeStatus_Accepted": "Accepté", + "CalendarAttendeeStatus_Declined": "Refusé", + "CalendarAttendeeStatus_NeedsAction": "Action requise", + "CalendarAttendeeStatus_Tentative": "Provisoire", + "CalendarEventDetails_Attachments": "Pièces jointes", + "CalendarEventCompose_AddAttachment": "Ajouter une pièce jointe", + "CalendarEventCompose_AllDay": "Toute la journée", + "CalendarEventCompose_AttachmentsNotSupportedForCalDav": "Les pièces jointes ne sont pas prises en charge pour les calendriers CalDAV.", + "CalendarEventCompose_EndDate": "Date de fin", + "CalendarEventCompose_EndTime": "Heure de fin", + "CalendarEventCompose_Every": "chaque", + "CalendarEventCompose_ForWeekdays": "pour", + "CalendarEventCompose_FrequencyDay": "jour", + "CalendarEventCompose_FrequencyDayPlural": "jours", + "CalendarEventCompose_FrequencyMonth": "mois", + "CalendarEventCompose_FrequencyMonthPlural": "mois", + "CalendarEventCompose_FrequencyWeek": "semaine", + "CalendarEventCompose_FrequencyWeekPlural": "semaines", + "CalendarEventCompose_FrequencyYear": "an", + "CalendarEventCompose_FrequencyYearPlural": "ans", + "CalendarEventCompose_Location": "Lieu", + "CalendarEventCompose_LocationPlaceholder": "Ajouter un lieu", + "CalendarEventCompose_NewEventButton": "Nouvel Événement", + "CalendarEventCompose_DefaultCalendarHint": "Vous pouvez choisir un calendrier par défaut pour les nouveaux événements dans les paramètres du calendrier.", + "CalendarEventCompose_DefaultCalendarSettingsLink": "Ouvrir les paramètres du calendrier", + "CalendarEventCompose_NoCalendarsMessage": "Aucun calendrier n'est disponible pour la création d'événements pour le moment.", + "CalendarEventCompose_NoCalendarsTitle": "Aucun calendrier disponible", + "CalendarEventCompose_NoEndDate": "Pas de date de fin", + "CalendarEventCompose_Notes": "Notes", + "CalendarEventCompose_PickCalendarTitle": "Choisir un calendrier", + "CalendarEventCompose_Recurring": "Récurrent", + "CalendarEventCompose_RecurringSummary": "Se produit tous les {0} {1}{2} {3} effectif {4}{5}", + "CalendarEventCompose_RecurringSummarySmart": "Se produit {0}{1} {2} effectif {3}{4}", + "CalendarEventCompose_RepeatEvery": "Répéter tous les", + "CalendarEventCompose_SelectCalendar": "Sélectionner le calendrier", + "CalendarEventCompose_SingleOccurrenceSummary": "A lieu le {0} {1}", + "CalendarEventCompose_StartDate": "Date de début", + "CalendarEventCompose_StartTime": "Heure de début", + "CalendarEventCompose_TimeRangeSummary": "de {0} à {1}", + "CalendarEventCompose_Title": "Titre de l'événement", + "CalendarEventCompose_TitlePlaceholder": "Ajouter un titre", + "CalendarEventCompose_Until": "jusqu'à", + "CalendarEventCompose_UntilSummary": " jusqu'à {0}", + "CalendarEventCompose_ValidationInvalidAllDayRange": "La date de fin de l'événement en plein jour doit être postérieure à la date de début.", + "CalendarEventCompose_ValidationInvalidAttendee": "Une ou plusieurs adresses e-mail des participants sont invalides.", + "CalendarEventCompose_ValidationInvalidRecurrenceEnd": "La date de fin de récurrence doit être égale ou postérieure à la date de début de l'événement.", + "CalendarEventCompose_ValidationInvalidTimeRange": "L'heure de fin doit être postérieure à l'heure de début.", + "CalendarEventCompose_ValidationMissingAttachment": "Une ou plusieurs pièces jointes ne sont plus disponibles : {0}", + "CalendarEventCompose_ValidationMissingCalendar": "Sélectionnez un calendrier avant de créer l'événement.", + "CalendarEventCompose_ValidationMissingTitle": "Saisissez un titre d'événement avant de créer l'événement.", + "CalendarEventCompose_ValidationTitle": "Échec de la validation de l'événement", + "CalendarEventCompose_WeekdaySummary": " le {0}", + "CalendarEventCompose_Weekday_Friday": "Vendredi", + "CalendarEventCompose_Weekday_Monday": "Lundi", + "CalendarEventCompose_Weekday_Saturday": "S", + "CalendarEventCompose_Weekday_Sunday": "S", + "CalendarEventCompose_Weekday_Thursday": "T", + "CalendarEventCompose_Weekday_Tuesday": "T", + "CalendarEventCompose_Weekday_Wednesday": "W", + "CalendarEventDetails_Details": "Détails", + "CalendarEventDetails_EditSeries": "Modifier la série", + "CalendarEventDetails_Editing": "Modification en cours", + "CalendarEventDetails_InviteSomeone": "Inviter quelqu'un", + "CalendarEventDetails_JoinOnline": "Rejoindre en ligne", + "CalendarEventDetails_Organizer": "Organisateur", + "CalendarEventDetails_People": "Participants", + "CalendarEventDetails_ReadOnlyEvent": "Événement en lecture seule", + "CalendarEventDetails_Reminder": "Rappel", + "CalendarReminder_StartedHoursAgo": "Démarré il y a {0} heures", + "CalendarReminder_StartedMinutesAgo": "Démarré il y a {0} minutes", + "CalendarReminder_StartedNow": "Démarré tout juste maintenant", + "CalendarReminder_StartingNow": "Démarrage en cours", + "CalendarReminder_StartsInHours": "Démarre dans {0} heures", + "CalendarReminder_StartsInMinutes": "Démarre dans {0} minutes", + "CalendarReminder_SnoozeAction": "Rappeler plus tard", + "CalendarReminder_SnoozeMinutesOption": "{0} minutes", + "CalendarEventDetails_ShowAs": "Afficher comme", + "CalendarShowAs_Free": "Libre", + "CalendarShowAs_Tentative": "Provisoire", + "CalendarShowAs_Busy": "Occupé", + "CalendarShowAs_OutOfOffice": "Hors du bureau", + "CalendarShowAs_WorkingElsewhere": "Travaillant ailleurs", "CalendarItem_DetailsPopup_JoinOnline": "Rejoindre en ligne", "CalendarItem_DetailsPopup_ViewEventButton": "Afficher l'événement", "CalendarItem_DetailsPopup_ViewSeriesButton": "Afficher les séries", @@ -88,6 +216,9 @@ "ClipboardTextCopied_Message": "{0} copié dans le presse-papiers.", "ClipboardTextCopied_Title": "Copié", "ClipboardTextCopyFailed_Message": "Échec de la copie de {0} dans le presse-papiers.", + "ContactInfoBar_ErrorTitle": "Échec du chargement des informations de contact", + "ContactInfoBar_SuccessTitle": "Informations de contact chargées", + "ContactInfoBar_WarningTitle": "Les informations de contact pourraient être incomplètes", "ComingSoon": "Bientôt disponible...", "ComposerAttachmentsDragDropAttach_Message": "Attacher", "ComposerAttachmentsDropZone_Message": "Déposez vos fichiers ici", @@ -129,6 +260,10 @@ "DialogMessage_CreateLinkedAccountTitle": "Nom du lien des comptes liés", "DialogMessage_DeleteAccountConfirmationMessage": "Supprimer {0} ?", "DialogMessage_DeleteAccountConfirmationTitle": "Toutes les données associées à ce compte seront définitivement supprimées du disque.", + "DialogMessage_DeleteEmailTemplateConfirmationMessage": "Supprimer le modèle \\\"{0}\\\" ?", + "DialogMessage_DeleteEmailTemplateConfirmationTitle": "Supprimer le modèle d’e-mail", + "DialogMessage_DeleteRecurringSeriesMessage": "Cette action supprimera tous les événements de la série. Voulez-vous continuer ?", + "DialogMessage_DeleteRecurringSeriesTitle": "Supprimer la série récurrente", "DialogMessage_DiscardDraftConfirmationMessage": "Ce brouillon sera supprimé. Voulez-vous continuer ?", "DialogMessage_DiscardDraftConfirmationTitle": "Supprimer le brouillon", "DialogMessage_EmptySubjectConfirmation": "Objet manquant", @@ -172,11 +307,18 @@ "ElementTheme_Light": "Mode clair", "Emoji": "Emoji", "Error_FailedToSetupSystemFolders_Title": "Impossible de configurer les dossiers système", + "Exception_AccountNeedsAttention_Title": "Le compte nécessite une attention", + "Exception_AccountNeedsAttention_Message": "'{0}' nécessite votre attention pour continuer à travailler.", + "Exception_WebView2RuntimeMissing_Message": "Wino Mail n'a pas pu trouver le runtime WebView2 de Microsoft Edge. Veuillez installer ou réparer le runtime pour afficher correctement le contenu des messages.", + "Exception_WebView2RuntimeMissing_Title": "Le runtime WebView2 est requis", "Exception_AuthenticationCanceled": "Authentification annulée", "Exception_CustomThemeExists": "Ce thème existe déjà.", "Exception_CustomThemeMissingName": "Vous devez indiquer un nom.", "Exception_CustomThemeMissingWallpaper": "Vous devez fournir une image d'arrière-plan personnalisée.", "Exception_FailedToSynchronizeAliases": "Impossible de synchroniser les alias", + "Exception_FailedToSynchronizeCalendarData": "Échec de la synchronisation des données du calendrier", + "Exception_FailedToSynchronizeCalendarEvents": "Échec de la synchronisation des événements du calendrier", + "Exception_FailedToSynchronizeCalendarMetadata": "Échec de la synchronisation des métadonnées du calendrier", "Exception_FailedToSynchronizeFolders": "Échec de la synchronisation des dossiers", "Exception_FailedToSynchronizeProfileInformation": "Impossible de synchroniser les informations de profil", "Exception_GoogleAuthCallbackNull": "L'URL de rappel est nulle lors de l'activation.", @@ -229,6 +371,32 @@ "HoverActionOption_MoveJunk": "Déplacer vers courriers indésirables", "HoverActionOption_ToggleFlag": "Marquer / Démarquer", "HoverActionOption_ToggleRead": "Lu / Non lu", + "KeyboardShortcuts_FailedToReset": "Échec de la réinitialisation des raccourcis clavier.", + "KeyboardShortcuts_FailedToUpdate": "Échec de la mise à jour des raccourcis clavier.", + "KeyboardShortcuts_MailoperationAction": "Action", + "KeyboardShortcuts_Action": "Action", + "KeyboardShortcuts_FailedToLoad": "Échec du chargement des raccourcis clavier.", + "KeyboardShortcuts_EnterKeyForShortcut": "Veuillez saisir une touche pour le raccourci.", + "KeyboardShortcuts_SelectOperationForShortcut": "Veuillez sélectionner une action à effectuer pour le raccourci.", + "KeyboardShortcuts_EnterKey": "Veuillez saisir une touche pour le raccourci.", + "KeyboardShortcuts_SelectOperation": "Veuillez sélectionner une action pour le raccourci.", + "KeyboardShortcuts_ShortcutInUse": "Ce raccourci est déjà utilisé par un autre raccourci.", + "KeyboardShortcuts_FailedToSave": "Échec de la sauvegarde du raccourci.", + "KeyboardShortcuts_FailedToDelete": "Échec de la suppression du raccourci.", + "KeyboardShortcuts_PageDescription": "Configurez des raccourcis clavier pour les opérations rapides de messagerie. Appuyez sur les touches lorsque le champ de saisie est actif pour capturer les raccourcis.", + "KeyboardShortcuts_Add": "Ajouter un raccourci", + "KeyboardShortcuts_EditTitle": "Modifier le raccourci clavier", + "KeyboardShortcuts_ResetToDefaults": "Réinitialiser les paramètres par défaut", + "KeyboardShortcuts_PressKeysHere": "Appuyez sur les touches ici...", + "KeyboardShortcuts_KeyCombination": "Combinaison de touches", + "KeyboardShortcuts_FocusArea": "Placez le curseur dans le champ ci-dessus et appuyez sur la combinaison de touches souhaitée", + "KeyboardShortcuts_Modifiers": "Touches modificateurs", + "KeyboardShortcuts_Mode": "Mode de l’application", + "KeyboardShortcuts_ModeMail": "Messagerie", + "KeyboardShortcuts_ModeCalendar": "Calendrier", + "KeyboardShortcuts_ActionToggleReadUnread": "Basculer lu/non lu", + "KeyboardShortcuts_ActionToggleFlag": "Basculer le drapeau", + "KeyboardShortcuts_ActionToggleArchive": "Basculer l’archivage/désarchivage", "ImageRenderingDisabled": "L'affichage d'image est désactivé pour ce message.", "ImapAdvancedSetupDialog_AuthenticationMethod": "Méthode d’authentification", "ImapAdvancedSetupDialog_ConnectionSecurity": "Sécurité de la connexion", @@ -295,12 +463,58 @@ "IMAPSetupDialog_Username": "Nom d’utilisateur", "IMAPSetupDialog_UsernamePlaceholder": "jeandupont, jeandupont@exemple.fr, domaine/jeandupont", "IMAPSetupDialog_UseSameConfig": "Utiliser le même nom d'utilisateur et mot de passe pour envoyer un e-mail", + "ImapCalDavSettingsPage_TitleCreate": "Configuration IMAP et Calendrier", + "ImapCalDavSettingsPage_TitleEdit": "Modifier les paramètres IMAP et Calendrier", + "ImapCalDavSettingsPage_Subtitle": "Configurer IMAP/SMTP et la synchronisation optionnelle du calendrier pour ce compte.", + "ImapCalDavSettingsPage_BasicSectionTitle": "Configuration de base", + "ImapCalDavSettingsPage_BasicSectionDescription": "Saisissez votre identité et vos identifiants. Wino peut tenter de détecter automatiquement les paramètres du serveur.", + "ImapCalDavSettingsPage_BasicTab": "Basique", + "ImapCalDavSettingsPage_EnableCalendarSupport": "Activer la prise en charge du calendrier", + "ImapCalDavSettingsPage_AutoDiscoverButton": "Découvrir automatiquement les paramètres du courrier", + "ImapCalDavSettingsPage_AutoDiscoverySuccessMessage": "Paramètres du courrier détectés et appliqués.", + "ImapCalDavSettingsPage_AdvancedSectionTitle": "Configuration avancée", + "ImapCalDavSettingsPage_AdvancedSectionDescription": "Saisissez manuellement les paramètres du serveur si la découverte automatique est indisponible ou incorrecte.", + "ImapCalDavSettingsPage_AdvancedTab": "Avancé", + "ImapCalDavSettingsPage_CalendarSectionTitle": "Configuration du calendrier", + "ImapCalDavSettingsPage_CalendarSectionDescription": "Choisissez comment les données du calendrier doivent fonctionner pour ce compte IMAP.", + "ImapCalDavSettingsPage_CalendarModeHeader": "Mode du calendrier", + "ImapCalDavSettingsPage_ConnectionSecurityHeader": "Sécurité de la connexion", + "ImapCalDavSettingsPage_AuthenticationMethodHeader": "Méthode d'authentification", + "ImapCalDavSettingsPage_CalendarModeDisabled": "Désactivé", + "ImapCalDavSettingsPage_CalendarModeCalDav": "Synchronisation CalDAV", + "ImapCalDavSettingsPage_CalendarModeLocalOnly": "Calendrier local uniquement", + "ImapCalDavSettingsPage_CalendarModeDisabledDescription": "Le calendrier est désactivé pour ce compte.", + "ImapCalDavSettingsPage_CalendarModeCalDavDescription": "Les éléments du calendrier sont synchronisés avec votre serveur CalDAV.", + "ImapCalDavSettingsPage_CalendarModeLocalOnlyDescription": "Les éléments du calendrier ne sont stockés que sur cet ordinateur et ne sont pas synchronisés sur le réseau.", + "ImapCalDavSettingsPage_LocalCalendarLearnMore": "Comment fonctionne le calendrier local", + "ImapCalDavSettingsPage_LocalCalendarDialogTitle": "Calendrier local uniquement", + "ImapCalDavSettingsPage_LocalCalendarDialogMessage": "Le calendrier local conserve tous les événements uniquement sur votre ordinateur. Rien n'est synchronisé vers iCloud, Yahoo ou tout autre fournisseur.", + "ImapCalDavSettingsPage_CalDavServiceUrl": "URL du service CalDAV", + "ImapCalDavSettingsPage_CalDavUsername": "Nom d'utilisateur CalDAV", + "ImapCalDavSettingsPage_CalDavPassword": "Mot de passe CalDAV", + "ImapCalDavSettingsPage_CalDavNotRequiredMessage": "Le test CalDAV n'est nécessaire que lorsque le mode calendrier est défini sur la synchronisation CalDAV.", + "ImapCalDavSettingsPage_CalDavUrlRequired": "L'URL du service CalDAV est requise.", + "ImapCalDavSettingsPage_CalDavUrlInvalid": "L'URL du service CalDAV doit être une URL absolue.", + "ImapCalDavSettingsPage_CalDavUsernameRequired": "Le nom d'utilisateur CalDAV est requis.", + "ImapCalDavSettingsPage_CalDavPasswordRequired": "Le mot de passe CalDAV est requis.", + "ImapCalDavSettingsPage_TestImapButton": "Tester la connexion IMAP", + "ImapCalDavSettingsPage_TestCalDavButton": "Tester la connexion CalDAV", + "ImapCalDavSettingsPage_ImapTestSuccessMessage": "Le test de connexion IMAP a réussi.", + "ImapCalDavSettingsPage_CalDavTestSuccessMessage": "Le test de connexion CalDAV a réussi.", + "ImapCalDavSettingsPage_SaveSuccessMessage": "Les paramètres du compte ont été validés et enregistrés.", + "ImapCalDavSettingsPage_ICloudHint": "Utilisez un mot de passe d’application généré à partir des paramètres de votre compte Apple.", + "ImapCalDavSettingsPage_YahooHint": "Utilisez un mot de passe d’application issu des paramètres de sécurité de votre compte Yahoo.", "Info_AccountCreatedMessage": "{0} a été créé", "Info_AccountCreatedTitle": "Création de compte", "Info_AccountCreationFailedTitle": "Échec de la création du compte", "Info_AccountDeletedMessage": "{0} a été supprimé avec succès.", "Info_AccountDeletedTitle": "Compte supprimé", "Info_AccountIssueFixFailedTitle": "Échec", + "Info_AccountIssueFixImapMessage": "Ouvrez la page des paramètres IMAP et calendrier pour saisir à nouveau vos identifiants de serveur.", + "Info_AccountAttentionRequiredMessage": "Ce compte nécessite votre attention.", + "Info_AccountAttentionRequiredClickableMessage": "Cliquez pour corriger ce compte et le resynchroniser.", + "Info_AccountAttentionRequiredAction": "Corriger", + "Info_AccountAttentionRequiredActionHint": "Cliquez sur Corriger pour résoudre ce problème de compte.", "Info_AccountIssueFixSuccessMessage": "Tous les problèmes de compte ont étés corrigés", "Info_AccountIssueFixSuccessTitle": "Succès", "Info_AttachmentOpenFailedMessage": "Impossible d'ouvrir la pièce-jointe.", @@ -370,6 +584,7 @@ "InfoBarMessage_SynchronizationDisabledFolder": "Ce dossier est désactivé pour la synchronisation.", "InfoBarTitle_SynchronizationDisabledFolder": "Dossier désactivé", "Justify": "Justifier", + "MenuUpdateAvailable": "Mise à jour disponible", "Left": "Gauche", "Link": "Lien", "LinkedAccountsCreatePolicyMessage": "vous devez avoir au moins 2 comptes pour lier des comptes\nle lien sera supprimé lors de la sauvegarde", @@ -403,6 +618,7 @@ "MailOperation_Unarchive": "Désarchiver", "MailOperation_ViewMessageSource": "Voir la source du message", "MailOperation_Zoom": "Zoom", + "MailsDragging": "Déplacement de {0} élément(s)", "MailsSelected": "{0} élément(s) sélectionné(s)", "MarkFlagUnflag": "Marquer ou Démarquer ce message", "MarkReadUnread": "Marquer comme lu/non lu", @@ -434,6 +650,8 @@ "Notifications_MultipleNotificationsTitle": "Nouveau message", "Notifications_WinoUpdatedMessage": "Vérifier la nouvelle version {0}", "Notifications_WinoUpdatedTitle": "Wino Mail a été mis à jour.", + "Notifications_StoreUpdateAvailableTitle": "Mise à jour disponible", + "Notifications_StoreUpdateAvailableMessage": "Une version plus récente de Wino Mail est prête à être installée depuis le Microsoft Store.", "OnlineSearchFailed_Message": "Impossible d'effectuer la recherche\n{0}\n\nListe des e-mails hors ligne.", "OnlineSearchTry_Line1": "Vous ne trouvez pas ce que vous cherchez ?", "OnlineSearchTry_Line2": "Essayez une recherche en ligne.", @@ -446,7 +664,6 @@ "PaneLengthOption_Small": "Petit", "Photos": "Images", "PreparingFoldersMessage": "Préparation des dossiers", - "ProtocolLogAvailable_Message": "Les journaux de protocole sont disponibles pour les diagnostics.", "ProviderDetail_Gmail_Description": "Compte Google", "ProviderDetail_iCloud_Description": "Compte iCloud d'Apple", "ProviderDetail_iCloud_Title": "iCloud", @@ -465,9 +682,14 @@ "SearchBarPlaceholder": "Rechercher", "SearchingIn": "Recherche dans", "SearchPivotName": "Résultats", + "Settings_KeyboardShortcuts_Title": "Raccourcis clavier", + "Settings_KeyboardShortcuts_Description": "Gérer les raccourcis clavier pour les actions rapides sur les mails.", "SettingConfigureSpecialFolders_Button": "Configurer", "SettingsEditAccountDetails_IMAPConfiguration_Title": "Configuration IMAP/SMTP", "SettingsEditAccountDetails_IMAPConfiguration_Description": "Modifiez vos paramètres de serveur entrant/sortant.", + "SettingsEditAccountDetails_ImapCalDavSettings_Title": "Paramètres IMAP et calendrier", + "SettingsEditAccountDetails_ImapCalDavSettings_Description": "Ouvrez la page de paramètres dédiée IMAP, SMTP et CalDAV pour ce compte.", + "SettingsEditAccountDetails_ImapCalDavSettings_Action": "Ouvrir les paramètres", "SettingsAbout_Description": "En savoir plus sur Wino.", "SettingsAbout_Title": "À propos", "SettingsAboutGithub_Description": "Ouvrir le gestionnaire de tickets du dépôt GitHub.", @@ -490,6 +712,10 @@ "SettingsAppPreferences_SearchMode_Local": "Local", "SettingsAppPreferences_SearchMode_Online": "En ligne", "SettingsAppPreferences_SearchMode_Title": "Mode de recherche par défaut", + "SettingsAppPreferences_ApplicationMode_Title": "Mode d'application par défaut", + "SettingsAppPreferences_ApplicationMode_Description": "Choisissez le mode d'ouverture par défaut de Wino lorsque aucun type d'activation n'est explicitement défini.", + "SettingsAppPreferences_ApplicationMode_Mail": "Courriel", + "SettingsAppPreferences_ApplicationMode_Calendar": "Calendrier", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description": "Wino Mail continuera à fonctionner en arrière-plan. Vous serez informé de l'arrivée de nouveaux courriers.", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title": "Exécuter en arrière-plan", "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description": "Wino Mail fonctionne en permanence dans la barre d'état système. Il est possible de le lancer en cliquant sur une icône. Vous serez informé de l'arrivée de nouveaux messages.", @@ -506,12 +732,30 @@ "SettingsAppPreferences_StartupBehavior_FatalError": "Une erreur fatale s'est produite lors de la modification du mode de démarrage de Wino Mail.", "SettingsAppPreferences_StartupBehavior_Title": "Démarrage minimisé au démarrage de Windows", "SettingsAppPreferences_Title": "Préférences de l'application", + "SettingsAppPreferences_HideWinoAccountButton_Title": "Masquer le bouton de profil dans la barre de titre", + "SettingsAppPreferences_HideWinoAccountButton_Description": "Masquer le bouton de profil dans la barre de titre qui ouvre le volet du compte Wino.", + "SettingsAppPreferences_StoreUpdateNotifications_Title": "Notifications de mise à jour du Store", + "SettingsAppPreferences_StoreUpdateNotifications_Description": "Afficher les notifications et les actions de bas de page lorsqu'une mise à jour du Microsoft Store est disponible.", + "SettingsAppPreferences_AiActions_Title": "Actions IA", + "SettingsAppPreferences_AiActions_Description": "Choisissez les langues IA par défaut et où les résumés doivent être enregistrés.", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Title": "Langue de traduction par défaut", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Description": "Choisissez la langue cible par défaut utilisée par les actions de traduction IA.", + "SettingsAppPreferences_AiSummarizeLanguage_Title": "Langue de résumé", + "SettingsAppPreferences_AiSummarizeLanguage_Description": "Choisissez la langue de résumé préférée pour les futures sorties de résumé IA.", + "SettingsAppPreferences_AiSummarySavePath_Title": "Chemin par défaut pour l'enregistrement des résumés", + "SettingsAppPreferences_AiSummarySavePath_Description": "Choisissez le dossier que Wino doit utiliser par défaut lors de l'enregistrement des résumés IA.", + "SettingsAppPreferences_AiSummarySavePath_Placeholder": "Utiliser l'emplacement d'enregistrement système par défaut", + "SettingsAppPreferences_AiSummarySavePath_InvalidHint": "Ce dossier n'existe pas. L'emplacement d'enregistrement par défaut sera utilisé pour les résumés.", "SettingsAutoSelectNextItem_Description": "Sélectionnez l'élément suivant après avoir supprimé ou déplacé un courrier.", "SettingsAutoSelectNextItem_Title": "Sélection automatique de l'élément suivant", "SettingsAvailableThemes_Description": "Sélectionnez un thème de la collection de Wino selon vos goûts ou appliquez vos propres thèmes.", "SettingsAvailableThemes_Title": "Thèmes disponibles", "SettingsCalendarSettings_Description": "Changez le premier jour de la semaine, la hauteur des cellules...", "SettingsCalendarSettings_Title": "Paramètres de l'agenda", + "CalendarSettings_DefaultSnoozeDuration_Header": "Durée de rappel par défaut", + "CalendarSettings_DefaultSnoozeDuration_Description": "Définissez une durée de rappel par défaut pour les notifications de rappel du calendrier.", + "CalendarSettings_TimedDayHeaderFormat_Header": "Format de l'en-tête jour en vue horaire", + "CalendarSettings_TimedDayHeaderFormat_Description": "Choisissez comment les libellés du jour en haut sont affichés dans les vues jour, semaine et semaine de travail. Utilisez des jetons de format de date tels que ddd, dd, MMM ou dddd.", "SettingsComposer_Title": "Éditeur", "SettingsComposerFont_Title": "Police par défaut du compositeur", "SettingsComposerFontFamily_Description": "Modifier la police et la taille par défaut lors de la composition des mails.", @@ -531,6 +775,9 @@ "SettingsDiscord_Title": "Canal Discord", "SettingsEditLinkedInbox_Description": "Ajouter / supprimer des comptes, renommer ou rompre le lien entre les comptes.", "SettingsEditLinkedInbox_Title": "Modifier la boîte de réception liée", + "SettingsWindowBackdrop_Title": "Arrière-plan de la fenêtre", + "SettingsWindowBackdrop_Description": "Sélectionnez un effet d'arrière-plan pour les fenêtres Wino.", + "SettingsWindowBackdrop_Disabled": "La sélection de l'arrière-plan des fenêtres est désactivée lorsque le thème de l'application est autre que Défaut.", "SettingsElementTheme_Description": "Sélectionnez un thème Windows pour Wino", "SettingsElementTheme_Title": "Thème de l'élément", "SettingsElementThemeSelectionDisabled": "La sélection du thème de l'élément est désactivée lorsque le thème de l'application sélectionné n'est pas le thème par défaut.", @@ -581,6 +828,8 @@ "SettingsManageAliases_Title": "Alias", "SettingsEditAccountDetails_Title": "Modifier les détails du compte", "SettingsEditAccountDetails_Description": "Changez le nom du compte, le nom de l'expéditeur et attribuez une nouvelle couleur si vous le souhaitez.", + "EditAccountDetailsPage_SaveSuccess_Title": "Modifications enregistrées", + "EditAccountDetailsPage_SaveSuccess_Message": "Les détails de votre compte ont été mis à jour avec succès.", "SettingsManageLink_Description": "Déplacer des éléments pour ajouter un nouveau lien ou supprimer un lien existant.", "SettingsManageLink_Title": "Gérer le lien", "SettingsMarkAsRead_Description": "Modifier ce qui doit arriver à l'élément sélectionné.", @@ -596,7 +845,41 @@ "SettingsNotifications_Title": "Notifications", "SettingsNotificationsAndTaskbar_Description": "Choisir si les notifications et le badge de la barre des tâches doivent être affichées pour ce compte.", "SettingsNotificationsAndTaskbar_Title": "Notifications & Barre de tâches", + "SettingsHome_Title": "Accueil", + "SettingsHome_SearchTitle": "Trouver un paramètre", + "SettingsHome_SearchDescription": "Recherchez par fonctionnalité, sujet ou mot-clé pour accéder directement à la bonne page de paramètres.", + "SettingsHome_SearchPlaceholder": "Rechercher des paramètres", + "SettingsHome_SearchExamples": "Exemples: thème, stockage, langue, signature", + "SettingsHome_QuickLinks_Title": "Liens rapides", + "SettingsHome_QuickLinks_Description": "Accédez rapidement aux paramètres les plus utilisés.", + "SettingsHome_StorageCard_Description": "Vérifiez combien de contenu MIME local Wino conserve sur cet appareil et nettoyez-le si nécessaire.", + "SettingsHome_StorageEmptySummary": "Aucun contenu MIME mis en cache détecté pour le moment.", + "SettingsHome_StorageLoading": "Vérification de l'utilisation MIME locale...", + "SettingsHome_Tips_Title": "Astuces", + "SettingsHome_Tips_Description": "Quelques petits changements peuvent rendre Wino beaucoup plus personnel.", + "SettingsHome_Tip_Theme": "Vous voulez le mode sombre ou des changements d'accentuation ? Ouvrez Personnalisation.", + "SettingsHome_Tip_Background": "Utilisez les Préférences de l'application pour contrôler le comportement de démarrage et la synchronisation en arrière-plan.", + "SettingsHome_Tip_Shortcuts": "Les raccourcis clavier vous permettent de parcourir les mails plus rapidement.", + "SettingsHome_Resources_Title": "Liens utiles", + "SettingsHome_Resources_Description": "Ouvrez les ressources du projet, les informations de support et les canaux de publication.", "SettingsOptions_Title": "Paramètres", + "SettingsOptions_GeneralSection": "Général", + "SettingsOptions_MailSection": "Courrier", + "SettingsOptions_CalendarSection": "Calendrier", + "SettingsOptions_MoreComingSoon": "Plus d'options à venir", + "SettingsOptions_HeroDescription": "Personnalisez votre expérience Wino Mail.", + "SettingsOptions_AccountsSummary": "{0} compte(s) configuré(s)", + "SettingsSearch_ManageAccounts_Keywords": "compte;comptes;Boîte aux lettres;Boîtes aux lettres;alias;aliases;profil;adresse;adresses", + "SettingsSearch_AppPreferences_Keywords": "démarrage;arrière-plan;lancement;synchronisation;notifications;recherche;plateau;paramètres par défaut", + "SettingsSearch_LanguageTime_Keywords": "langue;heure;horloge;paramètres régionaux;région;format;24 heures;24h", + "SettingsSearch_Personalization_Keywords": "thème;sombre;clair;apparence;accent;couleur;couleur;mode;disposition;densité", + "SettingsSearch_About_Keywords": "à propos;version;site web;confidentialité;github;faire un don;magasin;assistance", + "SettingsSearch_KeyboardShortcuts_Keywords": "raccourci;raccourcis;raccourci clavier;raccourcis clavier;clavier;touches", + "SettingsSearch_MessageList_Keywords": "message;messages;liste;fil de discussion;fils de discussion;avatar;aperçu;expéditeur", + "SettingsSearch_ReadComposePane_Keywords": "lecteur;composer;éditeur;police;polices;contenu externe;affichage;lecture", + "SettingsSearch_SignatureAndEncryption_Keywords": "signature;signatures;chiffrement;certificat;certificats;S/MIME;SMIME;sécurité", + "SettingsSearch_Storage_Keywords": "stockage;cache;mise en cache;MIME;disque;espace;nettoyage;nettoyer;données locales", + "SettingsSearch_CalendarSettings_Keywords": "calendrier;semaine;heures;planning;événement;événements", "SettingsPaneLengthReset_Description": "Réinitialisez la taille de la liste de courrier à sa valeur d'origine si vous rencontrez des problèmes.", "SettingsPaneLengthReset_Title": "Réinitialiser la taille de la liste des messages", "SettingsPaypal_Description": "Montrez-nous plus d'amour ❤️ Tous les dons sont appréciés.", @@ -610,6 +893,8 @@ "SettingsPrefer24HourClock_Title": "Afficher le format horloge en 24 heures", "SettingsPrivacyPolicy_Description": "Examinez la politique de confidentialité.", "SettingsPrivacyPolicy_Title": "Politique de confidentialité", + "SettingsWebsite_Description": "Ouvrez le site Web de Wino Mail.", + "SettingsWebsite_Title": "Site Web", "SettingsReadComposePane_Description": "Polices, contenu externe.", "SettingsReadComposePane_Title": "Lecteur & Éditeur", "SettingsReader_Title": "Lecteur", @@ -625,6 +910,19 @@ "SettingsShowPreviewText_Title": "Afficher l'aperçu du texte", "SettingsShowSenderPictures_Description": "Masquer/afficher les vignettes des images de l'expéditeur.", "SettingsShowSenderPictures_Title": "Afficher l'avatar de l'expéditeur", + "SettingsEmailTemplates_Title": "Modèles d'e-mails", + "SettingsEmailTemplates_Description": "Gérer les modèles d'e-mails", + "SettingsEmailTemplates_CreatePageTitle": "Nouveau modèle", + "SettingsEmailTemplates_EditPageTitle": "Modifier le modèle", + "SettingsEmailTemplates_NewTemplateTitle": "Nouveau modèle", + "SettingsEmailTemplates_NewTemplateDescription": "Créer un nouveau modèle d’e-mail", + "SettingsEmailTemplates_NameTitle": "Nom", + "SettingsEmailTemplates_NamePlaceholder": "Nom du modèle", + "SettingsEmailTemplates_DescriptionTitle": "Description", + "SettingsEmailTemplates_DescriptionPlaceholder": "Description optionnelle", + "SettingsEmailTemplates_ContentTitle": "Contenu du modèle", + "SettingsEmailTemplates_ContentDescription": "Modifier le contenu HTML de ce modèle.", + "SettingsEmailTemplates_NameRequired": "Le nom du modèle est requis.", "SettingsEnableGravatarAvatars_Title": "Gravatar", "SettingsEnableGravatarAvatars_Description": "Use gravatar (if available) as sender picture", "SettingsEnableFavicons_Title": "Domain icons (Favicons)", @@ -645,6 +943,33 @@ "SettingsStartupItem_Title": "Élément de démarrage", "SettingsStore_Description": "Montrer nous un peu d'amour ❤️", "SettingsStore_Title": "Évaluez l'application sur le store", + "SettingsStorage_Title": "Stockage", + "SettingsStorage_Description": "Analyser et gérer le cache MIME stocké dans votre dossier de données local.", + "SettingsStorage_ScanFolder": "Analyser le dossier de données local", + "SettingsStorage_NoLocalMimeDataFound": "Aucune donnée MIME locale trouvée.", + "SettingsStorage_NoAccountsFound": "Aucun compte trouvé.", + "SettingsStorage_TotalUsage": "Utilisation totale locale du cache MIME : {0}", + "SettingsStorage_AccountUsageDescription": "{0} utilisé dans le cache MIME local", + "SettingsStorage_DeleteAll_Title": "Supprimer tout le contenu MIME", + "SettingsStorage_DeleteAll_Description": "Supprimer l'intégralité du dossier de cache MIME de ce compte.", + "SettingsStorage_DeleteAll_Button": "Supprimer tout", + "SettingsStorage_DeleteAll_Confirm_Title": "Supprimer tout le contenu MIME", + "SettingsStorage_DeleteAll_Confirm_Message": "Supprimer toutes les données MIME locales pour {0} ?", + "SettingsStorage_DeleteAll_Success": "Tout le contenu MIME a été supprimé.", + "SettingsStorage_DeleteOld_Title": "Supprimer le contenu MIME ancien", + "SettingsStorage_DeleteOld_Description": "Supprimer les fichiers MIME en fonction de la date de création des e-mails dans la base de données locale.", + "SettingsStorage_DeleteOld_1Month": "> 1 mois", + "SettingsStorage_DeleteOld_3Months": "> 3 mois", + "SettingsStorage_DeleteOld_6Months": "> 6 mois", + "SettingsStorage_DeleteOld_1Year": "> 1 an", + "SettingsStorage_DeleteOld_Confirm_Title": "Supprimer le contenu MIME ancien", + "SettingsStorage_DeleteOld_Confirm_Message": "Supprimer les données MIME locales plus âgées que {0} pour {1} ?", + "SettingsStorage_DeleteOld_Success": "Dossier MIME {0} plus ancien que {1} supprimé.", + "SettingsStorage_1Month": "1 mois", + "SettingsStorage_3Months": "3 mois", + "SettingsStorage_6Months": "6 mois", + "SettingsStorage_1Year": "1 an", + "SettingsStorage_Months": "{0} mois", "SettingsTaskbarBadge_Description": "Inclure le nombre de messages non lus dans l'icône de la barre des tâches.", "SettingsTaskbarBadge_Title": "Badge de la barre de tâches", "SettingsThreads_Description": "Organiser les messages en fils de conversation.", @@ -683,6 +1008,9 @@ "SystemFolderConfigDialogValidation_InboxSelected": "Vous ne pouvez pas assigner de dossier de boîte de réception à un autre dossier système.", "SystemFolderConfigSetupSuccess_Message": "Les dossiers système sont configurés avec succès.", "SystemFolderConfigSetupSuccess_Title": "Configuration des dossiers système", + "SystemTrayMenu_ShowWino": "Ouvrir Wino Mail", + "SystemTrayMenu_ShowWinoCalendar": "Ouvrir Wino Calendar", + "SystemTrayMenu_ExitWino": "Quitter", "TestingImapConnectionMessage": "Test de la connexion au serveur...", "TitleBarServerDisconnectedButton_Description": "Wino est déconnecté du réseau. Cliquez sur reconnecter pour rétablir la connexion.", "TitleBarServerDisconnectedButton_Title": "pas de connexion", @@ -699,8 +1027,422 @@ "WinoUpgradeMessage": "Mettre à niveau vers des comptes illimités", "WinoUpgradeRemainingAccountsMessage": "{0} comptes gratuits utilisés sur {1}.", "Yesterday": "Hier", + "Smime_ImportCertificates_Success": "Certificats importés avec succès.", + "Smime_ImportCertificates_Error": "Erreur lors de l'importation des certificats : {0}", + "Smime_RemoveCertificates_Confirm": "Voulez-vous vraiment supprimer les certificats {0} ?", + "Smime_RemoveCertificates_Success": "Certificats supprimés.", + "Smime_ExportCertificates_Success": "Certificats exportés.", + "Smime_ExportCertificates_Error": "Erreur lors de l'exportation des certificats.", + "Smime_CertificateDetails": "Sujet : {0}\nÉmetteur : {1}\nValide à partir de : {2}\nValide jusqu'au : {3}\nEmpreinte : {4}", + "Smime_CertificatePassword_Title": "Mot de passe du certificat requis", + "Smime_CertificatePassword_Placeholder": "Mot de passe du certificat pour {0} (facultatif)", + "Smime_Confirm_Title": "Confirmer", + "Buttons_OK": "OK", + "Buttons_Refresh": "Actualiser", + "SettingsSignatureAndEncryption_Title": "Signature et chiffrement", + "SettingsSignatureAndEncryption_Description": "Gérer les certificats S/MIME pour la signature et le chiffrement des e-mails.", + "SettingsSignatureAndEncryption_MyCertificatesHeader": "Mes certificats", + "SettingsSignatureAndEncryption_MyCertificatesDescription": "Certificats personnels pour la signature et le chiffrement", + "SettingsSignatureAndEncryption_RecipientCertificatesHeader": "Certificats du destinataire", + "SettingsSignatureAndEncryption_RecipientCertificatesDescription": "Certificats du destinataire pour le décryptage", + "SettingsSignatureAndEncryption_NameColumn": "Nom", + "SettingsSignatureAndEncryption_ExpiresColumn": "Expire le", + "SettingsSignatureAndEncryption_ThumbprintColumn": "Empreinte", + "Buttons_Remove": "Supprimer", + "Buttons_Export": "Exporter", + "Buttons_Import": "Importer", + "SettingsSignatureAndEncryption_SigningCertificate": "Certificat de signature S/MIME", + "SettingsSignatureAndEncryption_EncryptionCertificate": "Certificat de chiffrement S/MIME", + "SettingsSignatureAndEncryption_SigningCertificatePlaceholder": "Aucun", + "SmimeSignaturesInMessage": "Signatures dans ce message :", + "SmimeSignatureEntry": "• {0} {1} ({2}, valide jusqu'au {3} - {4})", + "SmimeSigningCertificateInfoTitle": "Info du certificat de signature S/MIME", + "SmimeCertificateInfoTitle": "Info du certificat S/MIME", + "SmimeNoCertificateFileFound": "Aucun fichier de certificat trouvé", + "SmimeSaveCertificate": "Enregistrer le certificat...", + "SmimeCertificate": "Certificat S/MIME", + "SmimeCertificateSavedTo": "Certificat enregistré dans {0}", + "SmimeSignedTooltip": "Ce message est signé avec un certificat S/MIME. Cliquez pour plus de détails", + "SmimeEncryptedTooltip": "Ce message est chiffré avec un certificat S/MIME.", + "SmimeCertificateFileInfo": "Fichier : {0}", + "Composer_LightTheme": "Thème clair", + "Composer_DarkTheme": "Thème sombre", + "Composer_Outdent": "Réduire le retrait", + "Composer_Indent": "Ajouter un retrait", + "Composer_BulletList": "Liste à puces", + "Composer_OrderedList": "Liste numérotée", + "Composer_Stroke": "Trait", + "Composer_Bold": "Gras", + "Composer_Italic": "Italique", + "Composer_Underline": "Souligner", + "Composer_CcBcc": "Cc et Cci", + "Composer_EnableSmimeSignature": "Activer/désactiver la signature S/MIME", + "Composer_EnableSmimeEncryption": "Activer/désactiver le chiffrement S/MIME", + "Composer_LocalDraftSyncInfo": "Ce brouillon est uniquement local. Wino n'a pas pu l'envoyer à votre serveur de messagerie. Cliquez pour réessayer de l'envoyer au serveur.", + "Composer_CertificateExpires": "Expire le : ", + "Composer_SmimeSignature": "Signature S/MIME", + "Composer_SmimeEncryption": "Chiffrement S/MIME", + "Composer_EmailTemplatesPlaceholder": "Modèles d'e-mails", + "Composer_AiSummarize": "Résumer avec l'IA", + "Composer_AiSummarizeDescription": "Extraire les points clés, les éléments d'action et les décisions de ce courriel.", + "Composer_AiTranslate": "Traduire avec l'IA", + "Composer_AiActions": "Actions IA", + "Composer_AiRewrite": "Réécrire avec l'IA", + "AiActions_CheckingStatus": "Vérification de l'accès à l'IA...", + "AiActions_SignedOutTitle": "Déverrouiller le pack IA Wino", + "AiActions_SignedOutDescription": "Traduire, réécrire et résumer les e-mails avec l'IA après vous être connecté à votre compte Wino et avoir activé l'extension AI Pack.", + "AiActions_NoPackTitle": "Pack IA requis", + "AiActions_NoPackDescription": "Vous êtes connecté, mais le pack IA n'est pas encore actif. Achetez-le pour utiliser les outils de traduction, réécriture et résumé de Wino.", + "AiActions_UsageSummary": "{0} sur {1} crédits utilisés ce mois-ci.", + "Composer_AiRewritePolite": "Rendre plus poli", + "Composer_AiRewritePoliteDescription": "Adoucit le ton tout en conservant le même sens.", + "Composer_AiRewriteAngry": "Rendre plus agressif", + "Composer_AiRewriteAngryDescription": "Adopte un ton plus tranchant et plus agressif.", + "Composer_AiRewriteHappy": "Rendez-le heureux", + "Composer_AiRewriteHappyDescription": "Ajoute un ton plus optimiste et enthousiaste.", + "Composer_AiRewriteFormal": "Rendez-le formel.", + "Composer_AiRewriteFormalDescription": "Donne au message un ton plus professionnel et structuré.", + "Composer_AiRewriteFriendly": "Rendez-le plus convivial.", + "Composer_AiRewriteFriendlyDescription": "Rends le message plus chaleureux et accessible.", + "Composer_AiRewriteShorter": "Rendez-le plus court.", + "Composer_AiRewriteShorterDescription": "Raccourcit le texte et supprime les détails inutiles.", + "Composer_AiRewriteClearer": "Rendez-le plus clair.", + "Composer_AiRewriteClearerDescription": "Améliore la lisibilité et rend le message plus facile à suivre.", + "Composer_AiRewriteCustom": "Personnalisé", + "Composer_AiRewriteCustomDescription": "Décrivez votre intention de réécriture.", + "Composer_AiRewriteCustomPlaceholder": "Décrivez comment vous souhaitez que le message soit réécrit", + "Composer_AiRewriteMode": "Ton de réécriture", + "Composer_AiRewriteApply": "Appliquer la réécriture", + "Composer_AiTranslateDialogTitle": "Traduire avec l'IA", + "Composer_AiTranslateDialogDescription": "Saisissez la langue cible ou le code de culture, par exemple en-US, tr-TR, de-DE, ou fr-FR.", + "Composer_AiTranslateApply": "Traduire", + "Composer_AiTranslateLanguage": "Langue cible", + "Composer_AiTranslateCustomPlaceholder": "Entrez le code de culture", + "Composer_AiTranslateLanguageEnglish": "Anglais (en-US)", + "Composer_AiTranslateLanguageTurkish": "Turc (tr-TR)", + "Composer_AiTranslateLanguageGerman": "Allemand (de-DE)", + "Composer_AiTranslateLanguageFrench": "Français (fr-FR)", + "Composer_AiTranslateLanguageSpanish": "Espagnol (es-ES)", + "Composer_AiTranslateLanguageItalian": "Italien (it-IT)", + "Composer_AiTranslateLanguagePortugueseBrazil": "Portugais (Brésil) (pt-BR)", + "Composer_AiTranslateLanguageDutch": "Néerlandais (nl-NL)", + "Composer_AiTranslateLanguagePolish": "Polonais (pl-PL)", + "Composer_AiTranslateLanguageRussian": "Russe (ru-RU)", + "Composer_AiTranslateLanguageJapanese": "Japonais (ja-JP)", + "Composer_AiTranslateLanguageKorean": "Coréen (ko-KR)", + "Composer_AiTranslateLanguageChineseSimplified": "Chinois simplifié (zh-CN)", + "Composer_AiTranslateLanguageArabic": "Arabe (ar-SA)", + "Composer_AiTranslateLanguageHindi": "Hindi (hi-IN)", + "Composer_AiTranslateLanguageOther": "Autres...", + "Composer_AiBusyTitle": "L'IA est déjà en cours d'exécution.", + "Composer_AiBusyMessage": "Veuillez patienter pendant que l'action IA en cours se termine.", + "Composer_AiSignInRequired": "Connectez-vous à votre compte Wino pour utiliser les fonctionnalités IA.", + "Composer_AiMissingHtml": "Il n'y a pas de contenu de message à envoyer à Wino IA pour le moment.", + "Composer_AiQuotaUnavailable": "Le résultat IA a été appliqué.", + "Composer_AiAppliedMessage": "Le résultat de l'IA a été appliqué au rédacteur. Utilisez Annuler si vous souhaitez revenir en arrière.", + "Composer_AiSummarizeSuccessTitle": "Résumé IA appliqué", + "Composer_AiTranslateSuccessTitle": "Traduction IA appliquée", + "Composer_AiRewriteSuccessTitle": "Réécriture IA appliquée", + "Composer_AiErrorTitle": "L'action IA a échoué.", + "Reader_AiAppliedMessage": "Le résultat IA est désormais affiché pour ce message. Réouvrez le message pour voir à nouveau le contenu original.", "SettingsAppPreferences_EmailSyncInterval_Title": "Email sync interval", - "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail." + "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail.", + "ContactsPage_Title": "Contacts", + "ContactsPage_AddContact": "Ajouter un contact", + "ContactsPage_EditContact": "Modifier le contact", + "ContactsPage_DeleteContact": "Supprimer le contact", + "ContactsPage_SearchPlaceholder": "Rechercher des contacts...", + "ContactsPage_NoContacts": "Aucun contact trouvé", + "ContactsPage_ContactsCount": "{0} contacts", + "ContactsPage_SelectedContactsCount": "{0} sélectionné", + "ContactsPage_DeleteSelectedContacts": "Supprimer les contacts sélectionnés", + "ContactEditDialog_Title": "Modifier le contact", + "ContactEditDialog_PhotoSection": "Photo", + "ContactEditDialog_ChoosePhoto": "Choisir une photo", + "ContactEditDialog_RemovePhoto": "Supprimer la photo", + "ContactEditDialog_NameHeader": "Nom", + "ContactEditDialog_NamePlaceholder": "Nom du contact", + "ContactEditDialog_EmailHeader": "Adresse e-mail", + "ContactEditDialog_EmailPlaceholder": "contact@example.com", + "ContactEditDialog_InfoSection": "Informations de contact", + "ContactEditDialog_RootContactInfo": "Ceci est un contact racine associé à vos comptes et ne peut pas être supprimé.", + "ContactEditDialog_OverriddenContactInfo": "Ce contact a été modifié manuellement et ne sera pas mis à jour lors de la synchronisation.", + "ContactsPage_Subtitle": "Gérez vos contacts e-mail et leurs informations.", + "ContactStatus_Account": "Compte", + "ContactStatus_Modified": "Modifié", + "ContactAction_Edit": "Modifier le contact", + "ContactAction_ChangePhoto": "Changer la photo", + "ContactAction_Delete": "Supprimer le contact", + "ContactAction_Add": "Ajouter un contact", + "ContactSelection_Selected": "sélectionné", + "ContactSelection_SelectAll": "Tout sélectionner", + "ContactSelection_Clear": "Effacer la sélection", + "ContactsPage_EmptyState": "Aucun contact à afficher", + "ContactsPage_AddFirstContact": "Ajouter votre premier contact", + "ContactsPage_ContactsCountSuffix": "contacts", + "ContactsPane_NewContact": "Nouveau contact", + "ContactsPane_DescriptionTitle": "Gérez vos contacts", + "ContactsPane_DescriptionBody": "Créez des contacts, renommez-les, mettez à jour les photos de profil et gardez les informations enregistrées organisées en un seul endroit.", + "ContactEditDialog_AddTitle": "Ajouter un contact", + "ContactInfoBar_ContactAdded": "Contact ajouté avec succès.", + "ContactInfoBar_ContactUpdated": "Contact mis à jour avec succès.", + "ContactInfoBar_ContactsDeleted": "Contacts supprimés avec succès.", + "ContactInfoBar_ContactPhotoUpdated": "Photo de contact mise à jour avec succès.", + "ContactInfoBar_FailedToLoadContacts": "Échec du chargement des contacts : {0}", + "ContactInfoBar_FailedToAddContact": "Échec de l'ajout du contact : {0}", + "ContactInfoBar_FailedToUpdateContact": "Échec de la mise à jour du contact : {0}", + "ContactInfoBar_FailedToDeleteContacts": "Échec de la suppression des contacts : {0}", + "ContactInfoBar_FailedToUpdatePhoto": "Échec de la mise à jour de la photo : {0}", + "ContactInfoBar_CannotDeleteRoot": "Les contacts racine ne peuvent pas être supprimés.", + "ContactConfirmDialog_DeleteTitle": "Supprimer le contact", + "ContactConfirmDialog_DeleteMessage": "Êtes-vous sûr de vouloir supprimer le contact '{0}' ?", + "ContactConfirmDialog_DeleteMultipleMessage": "Êtes-vous sûr de vouloir supprimer {0} contact(s) ?", + "ContactConfirmDialog_DeleteButton": "Supprimer", + "CalendarAccountSettings_Title": "Paramètres du compte du calendrier", + "CalendarAccountSettings_Description": "Gérer les paramètres du calendrier pour {0}", + "CalendarAccountSettings_AccountColor": "Couleur du compte", + "CalendarAccountSettings_AccountColorDescription": "Changer la couleur d'affichage de ce compte de calendrier.", + "CalendarAccountSettings_SyncEnabled": "Activer la synchronisation", + "CalendarAccountSettings_SyncEnabledDescription": "Activer ou désactiver la synchronisation du calendrier pour ce compte", + "CalendarAccountSettings_DefaultShowAs": "État affiché par défaut", + "CalendarAccountSettings_DefaultShowAsDescription": "État de disponibilité par défaut pour les nouveaux événements créés avec ce compte", + "CalendarAccountSettings_PrimaryCalendar": "Calendrier principal", + "CalendarAccountSettings_PrimaryCalendarDescription": "Marquer ce calendrier comme calendrier principal pour le compte", + "CalendarSettings_NewEventBehavior_Header": "Comportement du bouton Nouveau Événement", + "CalendarSettings_NewEventBehavior_Description": "Choisissez si le bouton Nouveau Événement doit demander quel calendrier choisir à chaque fois ou ouvrir systématiquement un calendrier spécifique.", + "CalendarSettings_NewEventBehavior_AskEachTime": "Demander à chaque fois.", + "CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar": "Toujours utiliser un calendrier spécifique.", + "CalendarSettings_Rendering_Title": "Affichage", + "CalendarSettings_Rendering_Description": "Configurer la mise en page du calendrier et le comportement d'affichage.", + "CalendarSettings_Notifications_Title": "Notifications", + "CalendarSettings_Notifications_Description": "Choisir le rappel par défaut et le comportement de remise à plus tard.", + "CalendarSettings_Preferences_Title": "Préférences", + "CalendarSettings_Preferences_Description": "Définir le comportement du bouton Nouveau Événement.", + "WhatIsNew_GetStartedButton": "Commencer", + "WhatIsNew_ContinueAnywayButton": "Continuer quand même", + "WhatIsNew_PreparingForNewVersionButton": "Préparation de la nouvelle version...", + "WhatIsNew_MigrationPreparing_Title": "Préparation de vos données", + "WhatIsNew_MigrationPreparing_Description": "Wino applique les migrations de mise à jour. Veuillez patienter pendant que nous préparons vos données de compte pour cette version.", + "WhatIsNew_MigrationFailedMessage": "L'application des migrations a échoué avec le code d'erreur {0}. Vous pouvez continuer à utiliser l'application. Cependant, si vous rencontrez des problèmes graves, réinstallez l'application.", + "WhatIsNew_MigrationNotification_Title": "Wino Mail mis à jour", + "WhatIsNew_MigrationNotification_Message": "Ouvrez l'application pour terminer la mise à jour et voir les nouveautés.", + "WelcomeWindow_Title": "Bienvenue sur Wino Mail", + "WelcomeWindow_Subtitle": "Une expérience Windows native pour Mail et Calendrier.", + "WelcomeWindow_WhatsNewTitle": "Dernières modifications", + "WelcomeWindow_FeaturesTitle": "Fonctionnalités", + "WelcomeWindow_WhatsNewTab": "Quoi de neuf", + "WelcomeWindow_FeaturesTab": "Fonctionnalités", + "WelcomeWindow_GetStartedButton": "Commencer en ajoutant un compte", + "WelcomeWindow_GetStartedDescription": "Ajoutez votre compte Outlook, Gmail ou IMAP pour commencer avec Wino Mail.", + "WelcomeWindow_ImportFromWinoAccount": "Importer depuis votre compte Wino", + "WelcomeWindow_ImportInProgress": "Importation de vos préférences et comptes synchronisés...", + "WelcomeWindow_ImportNoAccountsFound": "Aucun compte synchronisé n'a été trouvé dans votre compte Wino. Si des préférences étaient disponibles, elles ont été restaurées. Utilisez Commencer pour ajouter manuellement un compte.", + "WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} comptes synchronisés sont déjà disponibles sur cet appareil. Utilisez Commencer pour en ajouter un autre manuellement si nécessaire.", + "WelcomeWindow_SetupTitle": "Configurer votre compte", + "WelcomeWindow_SetupSubtitle": "Choisissez votre fournisseur de messagerie pour commencer", + "WelcomeWindow_AddAccountButton": "Ajouter un compte", + "WelcomeWindow_SkipForNow": "Passer pour le moment — je le configurerai plus tard", + "WelcomeWindow_AppDescription": "Une boîte de réception rapide et ciblée — repensée pour Windows 11", + "WelcomeWizard_Step1Title": "Bienvenue", + "SystemTrayMenu_Open": "Ouvrir", + "WinoAccount_Titlebar_SyncBenefitTitle": "Paramètres de synchronisation", + "WinoAccount_Titlebar_SyncBenefitDescription": "Conservez vos préférences Wino synchronisées sur vos appareils.", + "WinoAccount_Titlebar_AddonsBenefitTitle": "Extensions Wino", + "WinoAccount_Titlebar_AddonsBenefitDescription": "Accédez à des fonctionnalités premium comme Wino AI Pack.", + "WinoAccount_Management_Description": "Gérez votre compte Wino, l'accès à AI Pack et les préférences synchronisées ainsi que les détails du compte.", + "WinoAccount_Management_SignedOutTitle": "Connectez-vous à Wino Mail", + "WinoAccount_Management_SignedOutDescription": "Connectez-vous ou créez un compte pour synchroniser vos e-mails, accéder aux fonctionnalités d'IA et gérer vos paramètres sur tous vos appareils.", + "WinoAccount_Management_ProfileSectionHeader": "Profil", + "WinoAccount_Management_AddOnsSectionHeader": "Extensions Wino", + "WinoAccount_Management_DataSectionHeader": "Données", + "WinoAccount_Management_AccountActionsSectionHeader": "Actions du compte", + "WinoAccount_Management_AccountCardTitle": "Compte", + "WinoAccount_Management_AccountCardDescription": "Adresse e-mail de votre compte Wino et état actuel du compte.", + "WinoAccount_Management_AiPackCardTitle": "AI Pack", + "WinoAccount_Management_AiPackCardDescription": "Voir si Wino AI Pack est actif et combien d'utilisation il reste.", + "WinoAccount_Management_AiPackActive": "AI Pack est actif", + "WinoAccount_Management_AiPackInactive": "AI Pack n'est pas actif", + "WinoAccount_Management_AiPackUsage": "{0} sur {1} utilisations consommées. {2} restantes.", + "WinoAccount_Management_AiPackBillingPeriod": "Période de facturation : {0:d} - {1:d}", + "WinoAccount_Management_AiPackUnknownUsage": "Les détails d'utilisation ne sont pas encore disponibles.", + "WinoAccount_Management_AiPackBuyDescription": "Achetez Wino AI Pack pour traduire, réécrire ou résumer les e-mails avec l'IA.", + "WinoAccount_Management_AiPackPromoTitle": "Déverrouiller AI Pack", + "WinoAccount_Management_AiPackPromoDescription": "Boostez votre flux de travail des e-mails avec des outils alimentés par l'IA. Traduisez les messages en plus de 50 langues, réécrivez-les pour plus de clarté et de tonalité, et obtenez des résumés instantanés des longs fils de discussion.", + "WinoAccount_Management_AiPackPromoPrice": "4,99 $ / mois", + "WinoAccount_Management_AiPackPromoRequests": "1 000 crédits", + "WinoAccount_Management_AiPackGetButton": "Obtenir AI Pack", + "WinoAddOn_AI_PACK_Name": "Wino AI Pack", + "WinoAddOn_AI_PACK_Description": "Outils IA pour traduire, réécrire et résumer les actions dans Wino Mail.", + "WinoAddOn_AI_PACK_Keywords": "IA, traduire, réécrire, résumer, productivité", + "WinoAddOn_UNLIMITED_ACCOUNTS_Name": "Comptes illimités", + "WinoAddOn_UNLIMITED_ACCOUNTS_Description": "Supprimer la limite de comptes et ajouter autant de comptes de messagerie que nécessaire.", + "WinoAddOn_UNLIMITED_ACCOUNTS_Keywords": "comptes, illimités, premium, module complémentaire", + "WinoAccount_Management_PurchaseRequiresSignIn": "Connectez-vous avec votre compte Wino pour terminer cet achat.", + "WinoAccount_Management_PurchaseStartFailed": "Wino n'a pas pu terminer cet achat sur le Microsoft Store.", + "WinoAccount_Management_StoreSyncFailed": "Votre achat est terminé, mais Wino n'a pas encore pu actualiser vos avantages de compte. Réessayez dans un instant.", + "WinoAccount_Management_AiPackSubscriptionActive": "Votre abonnement est actif", + "WinoAccount_Management_AiPackRenews": "Renouvelle {0:d}", + "WinoAccount_Management_AiPackRequestsUsed": "Crédits utilisés ce mois-ci", + "WinoAccount_Management_AiPackResets": "Réinitialisations {0:d}", + "WinoAccount_Management_AiPackUsageLoadFailed": "Nous avons rencontré des problèmes lors du chargement de votre solde d'utilisation IA.", + "WinoAccount_Management_AiPackFeatureTranslate": "Traduire", + "WinoAccount_Management_AiPackFeatureRewrite": "Réécrire", + "WinoAccount_Management_AiPackFeatureSummarize": "Résumer", + "WinoAccount_Management_AddOnLoadFailed": "Nous avons rencontré des problèmes lors du chargement de ce module complémentaire.", + "WinoAccount_Management_SyncPreferencesTitle": "Synchroniser les préférences et les comptes", + "WinoAccount_Management_SyncPreferencesDescription": "Importer ou exporter vos préférences Wino et les détails de votre boîte aux lettres sur plusieurs appareils. Les mots de passe, les jetons et d'autres informations sensibles ne sont jamais synchronisés.", + "WinoAccount_Management_SignOutTitle": "Se déconnecter", + "WinoAccount_Management_SignOutDescription": "Déconnectez-vous de votre compte sur cet appareil", + "WinoAccount_Management_StatusLabel": "Statut : {0}", + "WinoAccount_Management_NoRemoteSettings": "Il n'y a pas encore de données synchronisées stockées pour ce compte.", + "WinoAccount_Management_ExportSucceeded": "Les données Wino sélectionnées ont été exportées avec succès.", + "WinoAccount_Management_ExportPreferencesSucceeded": "Vos préférences ont été exportées vers votre compte Wino.", + "WinoAccount_Management_ExportAccountsSucceeded": "Détails de {0} compte exportés vers votre compte Wino.", + "WinoAccount_Management_ImportSucceeded": "Données synchronisées importées depuis votre compte Wino.", + "WinoAccount_Management_ImportPreferencesSucceeded": "{0} préférences synchronisées appliquées.", + "WinoAccount_Management_ImportAccountsSucceeded": "Importé {0} comptes.", + "WinoAccount_Management_ImportDuplicateAccountsSkipped": "Ignorés {0} comptes qui existent déjà sur cet appareil.", + "WinoAccount_Management_ImportPartial": "Préférences synchronisées appliquées : {0}. {1} préférences n'ont pas pu être restaurées.", + "WinoAccount_Management_ImportReloginReminder": "Les mots de passe, jetons et d'autres informations sensibles n'ont pas été importés. Veuillez vous reconnecter pour chaque compte sur cet appareil avant de l'utiliser.", + "WinoAccount_Management_SerializeFailed": "Wino n'a pas pu sérialiser vos préférences actuelles.", + "WinoAccount_Management_EmptyExport": "Il n'y a aucune valeur de préférence à exporter.", + "WinoAccount_Management_ImportEmpty": "La charge utile de données synchronisées ne contient rien de nouveau à restaurer.", + "WinoAccount_Management_ExportDialog_Title": "Exporter vers votre compte Wino", + "WinoAccount_Management_ExportDialog_Description": "Choisissez ce que vous souhaitez synchroniser vers votre compte Wino.", + "WinoAccount_Management_ExportDialog_IncludePreferences": "Préférences", + "WinoAccount_Management_ExportDialog_IncludeAccounts": "Comptes", + "WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Les mots de passe, jetons et autres informations sensibles ne sont pas synchronisés.", + "WinoAccount_Management_ExportDialog_AccountsRelogin": "Les comptes importés sur un autre PC devront quand même être reconnectés avant de pouvoir être utilisés.", + "WinoAccount_Management_ExportDialog_InProgress": "Exportation de vos données Wino sélectionnées...", + "WinoAccount_Management_LoadFailed": "Wino n'a pas pu charger les dernières informations du compte Wino.", + "WinoAccount_Management_ActionFailed": "La requête du compte Wino n'a pas pu être complétée.", + "WinoAccount_SettingsSection_Title": "Compte Wino", + "WinoAccount_SettingsSection_Description": "Créez ou connectez-vous à un compte Wino en utilisant votre service d'authentification local.", + "WinoAccount_RegisterButton_Title": "Créer un compte", + "WinoAccount_RegisterButton_Description": "Créez un compte Wino avec un e-mail et un mot de passe.", + "WinoAccount_RegisterButton_Action": "Ouvrir l'inscription", + "WinoAccount_LoginButton_Title": "Se connecter", + "WinoAccount_LoginButton_Description": "Connectez‑vous à un compte Wino existant avec votre e-mail et votre mot de passe.", + "WinoAccount_LoginButton_Action": "Ouvrir la connexion", + "WinoAccount_SignOutButton_Title": "Se déconnecter", + "WinoAccount_SignOutButton_Description": "Supprimer la session de compte Wino stockée localement.", + "WinoAccount_SignOutButton_Action": "Se déconnecter", + "WinoAccount_RegisterDialog_Title": "Créer un compte Wino", + "WinoAccount_RegisterDialog_Description": "Créez un compte Wino pour synchroniser votre expérience Wino et débloquer les modules complémentaires basés sur le compte.", + "WinoAccount_RegisterDialog_HeroTitle": "Créez votre compte Wino", + "WinoAccount_RegisterDialog_BenefitsTitle": "Pourquoi en créer un ?", + "WinoAccount_RegisterDialog_BenefitSyncTitle": "Importer et exporter les paramètres sur différents appareils", + "WinoAccount_RegisterDialog_BenefitSyncDescription": "Déplacez vos préférences Wino entre les appareils sans avoir à reconstruire votre configuration à partir de zéro.", + "WinoAccount_RegisterDialog_BenefitAiTitle": "Accédez à des add-ons exclusifs tels que Wino AI Pack (payant)", + "WinoAccount_RegisterDialog_BenefitAiDescription": "Utilisez un seul compte pour déverrouiller les fonctionnalités premium de Wino au fur et à mesure de leur disponibilité.", + "WinoAccount_RegisterDialog_DifferenceTitle": "Le compte Wino est distinct de vos comptes de messagerie", + "WinoAccount_RegisterDialog_DifferenceDescription": "Vos comptes de messagerie Outlook, Gmail, IMAP ou autres restent tels quels. Un compte Wino gère uniquement les fonctionnalités spécifiques à Wino et les modules complémentaires basés sur le compte.", + "WinoAccount_RegisterDialog_PrimaryButton": "S'enregistrer", + "WinoAccount_RegisterDialog_PrivacyTitle": "Confidentialité et traitement API", + "WinoAccount_RegisterDialog_PrivacyDescription": "Des modules complémentaires optionnels tels que Wino AI Pack peuvent envoyer le contenu HTML d'e-mails sélectionné au service API Wino uniquement lorsque vous utilisez ces fonctionnalités.", + "WinoAccount_RegisterDialog_PrivacyLinkText": "Lire la politique de confidentialité", + "WinoAccount_RegisterDialog_PrivacyCheckbox": "J'accepte la politique de confidentialité.", + "WinoAccount_LoginDialog_Title": "Se connecter au compte Wino", + "WinoAccount_LoginDialog_Description": "Connectez-vous à votre compte Wino pour synchroniser votre configuration Wino et accéder aux fonctionnalités basées sur le compte.", + "WinoAccount_LoginDialog_HeroTitle": "Bon retour", + "WinoAccount_LoginDialog_BenefitsTitle": "Ce que la connexion vous apporte", + "WinoAccount_LoginDialog_BenefitsDescription": "Utilisez votre compte Wino pour continuer à synchroniser les paramètres entre les appareils et accéder à des modules complémentaires payants tels que Wino AI Pack.", + "WinoAccount_LoginDialog_DifferenceTitle": "Ce n'est pas la connexion à votre boîte mail", + "WinoAccount_LoginDialog_DifferenceDescription": "La connexion ici n'ajoute ni ne remplace vos comptes Outlook, Gmail ou IMAP dans Wino. Elle vous connecte uniquement aux services propres à Wino.", + "WinoAccount_LoginDialog_ForgotPasswordLink": "Mot de passe oublié ?", + "WinoAccount_EmailLabel": "E-mail", + "WinoAccount_EmailPlaceholder": "nom@example.com", + "WinoAccount_PasswordLabel": "Mot de passe", + "WinoAccount_ConfirmPasswordLabel": "Confirmer le mot de passe", + "WinoAccount_ForgotPasswordDialog_Title": "Réinitialiser votre mot de passe", + "WinoAccount_ForgotPasswordDialog_PrimaryButton": "Envoyer l'e-mail de réinitialisation", + "WinoAccount_ForgotPasswordDialog_BackToSignIn": "Retour à la connexion", + "WinoAccount_ForgotPasswordDialog_Description": "Saisissez l'adresse e-mail de votre compte Wino et nous vous enverrons un lien de réinitialisation du mot de passe si l'adresse est enregistrée.", + "WinoAccount_Validation_EmailRequired": "L'adresse e-mail est requise.", + "WinoAccount_Validation_PasswordRequired": "Le mot de passe est requis.", + "WinoAccount_Validation_PasswordMismatch": "Les mots de passe ne correspondent pas.", + "WinoAccount_Validation_PrivacyConsentRequired": "Vous devez accepter la politique de confidentialité avant de créer un compte Wino.", + "WinoAccount_Error_InvalidCredentials": "L'adresse e-mail ou le mot de passe est incorrect.", + "WinoAccount_Error_AccountLocked": "Ce compte est temporairement bloqué.", + "WinoAccount_Error_AccountBanned": "Ce compte a été banni.", + "WinoAccount_Error_AccountSuspended": "Ce compte a été suspendu.", + "WinoAccount_Error_EmailNotConfirmed": "Veuillez confirmer votre adresse e-mail avant de vous connecter.", + "WinoAccount_Error_EmailConfirmationRequired": "Veuillez confirmer votre adresse e-mail avant de vous connecter.", + "WinoAccount_Error_EmailConfirmationResendNotAvailable": "Un nouvel e-mail de confirmation n'est pas encore disponible.", + "WinoAccount_Error_EmailConfirmationResendInvalid": "Cette demande de confirmation n'est plus valable. Veuillez réessayer de vous connecter.", + "WinoAccount_Error_EmailNotRegistered": "Cette adresse e-mail n'est pas enregistrée.", + "WinoAccount_Error_RefreshTokenInvalid": "Votre session n'est plus valide. Veuillez vous reconnecter.", + "WinoAccount_Error_EmailAlreadyRegistered": "Cette adresse e-mail est déjà enregistrée.", + "WinoAccount_Error_ExternalLoginEmailRequired": "Une adresse e-mail est requise pour terminer la connexion externe.", + "WinoAccount_Error_ExternalLoginInvalid": "La demande de connexion externe est invalide.", + "WinoAccount_Error_ExternalAuthStateInvalid": "L'état de la connexion externe est invalide ou expiré.", + "WinoAccount_Error_ExternalAuthCodeInvalid": "Le code de connexion externe est invalide ou expiré.", + "WinoAccount_Error_AiPackRequired": "Un abonnement actif au Wino AI Pack est requis pour cette action.", + "WinoAccount_Error_AiQuotaExceeded": "Votre limite d'utilisation du AI Pack a été atteinte pour la période de facturation en cours.", + "WinoAccount_Error_AiHtmlEmpty": "Il n'y a aucun contenu d'e-mail à traiter.", + "WinoAccount_Error_AiHtmlTooLarge": "Cet e-mail est trop volumineux pour être traité par Wino AI.", + "WinoAccount_Error_AiUnsupportedLanguage": "Cette langue n'est pas prise en charge. Essayez un code de culture valide tel que en-US ou tr-TR.", + "WinoAccount_Error_Forbidden": "Vous n'avez pas la permission d'effectuer cette action.", + "WinoAccount_Error_ValidationFailed": "La requête est invalide. Veuillez vérifier les valeurs saisies.", + "WinoAccount_RegisterSuccessMessage": "Enregistrement du compte Wino terminé pour {0}.", + "WinoAccount_LoginSuccessMessage": "Connecté au compte Wino en tant que {0}.", + "WinoAccount_EmailConfirmationSentDialog_Title": "Confirmer votre adresse e-mail", + "WinoAccount_EmailConfirmationSentDialog_Message": "Nous avons envoyé une confirmation par e-mail à {0}. Veuillez la confirmer et essayer de vous connecter à nouveau.", + "WinoAccount_EmailConfirmationPendingDialog_Title": "Confirmation de l'e-mail requise", + "WinoAccount_EmailConfirmationPendingDialog_Message": "Nous vous attendons encore pour confirmer {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ResendButton": "Renvoyer l'e-mail de confirmation", + "WinoAccount_EmailConfirmationPendingDialog_Countdown": "Vous pouvez renvoyer l'e-mail de confirmation dans {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ReadyToResend": "Vous pouvez renvoyer l'e-mail de confirmation dès maintenant.", + "WinoAccount_EmailConfirmationResentDialog_Title": "E-mail de confirmation renvoyé", + "WinoAccount_EmailConfirmationResentDialog_Message": "Nous avons envoyé un autre e-mail de confirmation à {0}. Veuillez le confirmer et réessayer de vous connecter.", + "WinoAccount_ForgotPasswordDialog_SuccessTitle": "E-mail de réinitialisation du mot de passe envoyé", + "WinoAccount_ForgotPasswordDialog_SuccessMessage": "Nous avons envoyé un e-mail de réinitialisation du mot de passe à {0}. Ouvrez ce message pour choisir un nouveau mot de passe.", + "WinoAccount_ChangePassword_Title": "Modifier le mot de passe", + "WinoAccount_ChangePassword_Description": "Envoyez un e-mail de réinitialisation du mot de passe à ce compte Wino.", + "WinoAccount_ChangePassword_Action": "Envoyer l'e-mail de réinitialisation", + "WinoAccount_ChangePassword_ConfirmationMessage": "Voulez-vous que Wino envoie un e-mail de réinitialisation du mot de passe à {0} ?", + "WinoAccount_SignOut_SuccessMessage": "Déconnecté du compte Wino {0}.", + "WinoAccount_SignOut_NoAccountMessage": "Aucun compte Wino actif à déconnecter.", + "WinoAccount_Titlebar_SignedOutTitle": "Compte Wino", + "WinoAccount_Titlebar_SignedOutDescription": "Connectez-vous ou créez un compte Wino pour gérer votre session Wino.", + "WinoAccount_Titlebar_SignedInStatus": "Statut : {0}", + "WelcomeWizard_Step2Title": "Ajouter un compte", + "WelcomeWizard_Step3Title": "Terminer la configuration", + "ProviderSelection_Title": "Choisissez votre fournisseur de messagerie", + "ProviderSelection_Subtitle": "Sélectionnez un fournisseur ci-dessous pour ajouter votre compte de messagerie à Wino Mail.", + "ProviderSelection_AccountNameHeader": "Nom du compte", + "ProviderSelection_AccountNamePlaceholder": "par ex. Personnel, Professionnel", + "ProviderSelection_DisplayNameHeader": "Nom affiché", + "ProviderSelection_DisplayNamePlaceholder": "par ex. John Doe", + "ProviderSelection_EmailHeader": "Adresse e-mail", + "ProviderSelection_EmailPlaceholder": "par ex. johndoe@example.com", + "ProviderSelection_AppPasswordHeader": "Mot de passe spécifique à l’application", + "ProviderSelection_AppPasswordHelp": "Comment obtenir un mot de passe spécifique à l’application ?", + "ProviderSelection_CalendarModeHeader": "Intégration du calendrier", + "ProviderSelection_CalendarMode_DisabledTitle": "Désactivé", + "ProviderSelection_CalendarMode_DisabledDescription": "Aucune intégration de calendrier.", + "ProviderSelection_CalendarMode_CalDavTitle": "Synchronisation CalDAV", + "ProviderSelection_CalendarMode_CalDavDescription_Apple": "Vos événements de calendrier sont synchronisés avec les serveurs Apple entre vos appareils.", + "ProviderSelection_CalendarMode_CalDavDescription_Yahoo": "Vos événements de calendrier sont synchronisés avec les serveurs Yahoo entre vos appareils.", + "ProviderSelection_CalendarMode_LocalTitle": "Calendrier local", + "ProviderSelection_CalendarMode_LocalDescription": "Vos événements sont stockés uniquement sur votre ordinateur. Pas de connexion au serveur.", + "ProviderSelection_ClearColor": "Effacer la couleur", + "ProviderSelection_ContinueButton": "Continuer", + "ProviderSelection_SpecialImap_Subtitle": "Saisissez vos identifiants de compte pour vous connecter.", + "AccountSetup_Title": "Configuration de votre compte", + "AccountSetup_Step_Authenticating": "Authentification avec {0}", + "AccountSetup_Step_TestingMailAuth": "Test d’authentification du courrier", + "AccountSetup_Step_SyncingFolders": "Synchronisation des métadonnées des dossiers", + "AccountSetup_Step_FetchingProfile": "Récupération des informations de profil", + "AccountSetup_Step_DiscoveringCalDav": "Découverte des paramètres CalDAV", + "AccountSetup_Step_TestingCalendarAuth": "Test d’authentification du calendrier", + "AccountSetup_Step_SavingAccount": "Enregistrement des informations du compte", + "AccountSetup_Step_FetchingCalendarMetadata": "Récupération des métadonnées du calendrier", + "AccountSetup_Step_SyncingAliases": "Synchronisation des alias", + "AccountSetup_Step_Finalizing": "Finalisation de la configuration", + "AccountSetup_FailureMessage": "Échec de l’installation. Revenez en arrière pour corriger vos paramètres, ou réessayez plus tard.", + "AccountSetup_SuccessMessage": "Votre compte a été configuré avec succès.", + "AccountSetup_GoBackButton": "Retour", + "AccountSetup_TryAgainButton": "Réessayer", + "ImapCalDavSettings_AutoDiscoveryFailed": "La détection automatique a échoué. Veuillez saisir les paramètres manuellement dans l’onglet Avancé." } - - diff --git a/Wino.Core.Domain/Translations/gl_ES/resources.json b/Wino.Core.Domain/Translations/gl_ES/resources.json index 5a036ca6..516c5a15 100644 --- a/Wino.Core.Domain/Translations/gl_ES/resources.json +++ b/Wino.Core.Domain/Translations/gl_ES/resources.json @@ -8,6 +8,7 @@ "AccountCacheReset_Message": "This account requires full re-sychronization to continue working. Please wait while Wino re-synchronizes your messages...", "AccountContactNameYou": "You", "AccountCreationDialog_Completed": "todo feito", + "AccountCreationDialog_FetchingCalendarMetadata": "Obtendo datos do calendario.", "AccountCreationDialog_FetchingEvents": "Cargando os eventos do calendario.", "AccountCreationDialog_FetchingProfileInformation": "Cargando os detalles do perfil.", "AccountCreationDialog_GoogleAuthHelpClipboardText_Row0": "Se o navegador non se iniciou automaticamente, complete a autenticación:", @@ -17,6 +18,7 @@ "AccountCreationDialog_Initializing": "iniciando", "AccountCreationDialog_PreparingFolders": "Neste intre estamos a obter información dos cartafois.", "AccountCreationDialog_SigninIn": "A información da conta estase a gardar.", + "Purchased": "Adquirido", "AccountEditDialog_Message": "Nome da conta", "AccountEditDialog_Title": "Editar conta", "AccountPickerDialog_Title": "Escolla unha conta", @@ -26,6 +28,10 @@ "AccountDetailsPage_Description": "Change the name of the account in Wino and set desired sender name.", "AccountDetailsPage_ColorPicker_Title": "Account color", "AccountDetailsPage_ColorPicker_Description": "Assign a new account color to colorize its symbol in the list.", + "AccountDetailsPage_TabGeneral": "Xeral", + "AccountDetailsPage_TabMail": "Correo", + "AccountDetailsPage_TabCalendar": "Calendario", + "AccountDetailsPage_CalendarListDescription": "Selecciona un calendario para configurar as súas configuracións.", "AddHyperlink": "Engadir", "AppCloseBackgroundSynchronizationWarningTitle": "Sincronización en segundo plano", "AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Application has not been set to launch on Windows startup.", @@ -47,8 +53,10 @@ "BasicIMAPSetupDialog_Title": "Conta IMAP", "Busy": "Busy", "Buttons_AddAccount": "Engadir conta", + "Buttons_FixAccount": "Arreglar conta", "Buttons_AddNewAlias": "Add New Alias", "Buttons_Allow": "Allow", + "Buttons_Apply": "Aplicar", "Buttons_ApplyTheme": "Aplicar tema", "Buttons_Browse": "Procurar", "Buttons_Cancel": "Anular", @@ -62,6 +70,7 @@ "Buttons_Edit": "Editar", "Buttons_EnableImageRendering": "Activar", "Buttons_Multiselect": "Select Multiple", + "Buttons_Manage": "Xestionar", "Buttons_No": "Non", "Buttons_Open": "Abrir", "Buttons_Purchase": "Mercar", @@ -70,15 +79,134 @@ "Buttons_Save": "Gardar", "Buttons_SaveConfiguration": "Gardar configuración", "Buttons_Send": "Send", + "Buttons_SendToServer": "Enviar ao servidor", "Buttons_Share": "Compartir", "Buttons_SignIn": "Iniciar sesión", "Buttons_Sync": "Synchronize", "Buttons_SyncAliases": "Synchronize Aliases", "Buttons_TryAgain": "Téntao de novo", "Buttons_Yes": "Si", + "Sync_SynchronizingFolder": "Sincronizando {0} {1}%", + "Sync_DownloadedMessages": "Descargadas {0} mensaxes de {1}", + "SyncAction_Archiving": "Archivando {0} correo(s)", + "SyncAction_ClearingFlag": "Eliminando a marca de {0} correo(s)", + "SyncAction_CreatingDraft": "Creando un rascunho", + "SyncAction_CreatingEvent": "Creando evento", + "SyncAction_Deleting": "Eliminando {0} correo(s)", + "SyncAction_EmptyingFolder": "Vaciando a carpeta", + "SyncAction_MarkingAsRead": "Marcando {0} correo(s) como lido", + "SyncAction_MarkingAsUnread": "Marcando {0} correo(s) como non lido", + "SyncAction_MarkingFolderAsRead": "Marcando a carpeta como lida", + "SyncAction_Moving": "Movendo {0} correo(s)", + "SyncAction_MovingToFocused": "Movendo {0} correo(s) á Carpeta Enfocada", + "SyncAction_RenamingFolder": "Renomeando a carpeta", + "SyncAction_SendingMail": "Enviando correo", + "SyncAction_SettingFlag": "Marcando {0} correo(s) coa marca", + "SyncAction_SynchronizingAccount": "Sincronizando {0}", + "SyncAction_SynchronizingAccounts": "Sincronizando {0} conta(s)", + "SyncAction_SynchronizingCalendarData": "Sincronizando datos do calendario", + "SyncAction_SynchronizingCalendarEvents": "Sincronizando eventos do calendario", + "SyncAction_SynchronizingCalendarMetadata": "Sincronizando metadatos do calendario", + "SyncAction_Unarchiving": "Desarquivando {0} correo(s)", "CalendarAllDayEventSummary": "all-day events", "CalendarDisplayOptions_Color": "Color", "CalendarDisplayOptions_Expand": "Expand", + "CalendarEventResponse_Accept": "Aceptar", + "CalendarEventResponse_AcceptedResponse": "Aceptado", + "CalendarEventResponse_Decline": "Rexeitar", + "CalendarEventResponse_DeclinedResponse": "Rexeitado", + "CalendarEventResponse_NotResponded": "Non respondido", + "CalendarEventResponse_Tentative": "Provisional", + "CalendarEventResponse_TentativeResponse": "Provisional", + "CalendarEventRsvpPanel_Accept": "Aceptar", + "CalendarEventRsvpPanel_AddMessage": "Engade unha mensaxe á túa resposta... (opcional)", + "CalendarEventRsvpPanel_Decline": "Rexeitar", + "CalendarEventRsvpPanel_Message": "Mensaxe", + "CalendarEventRsvpPanel_SendReplyMessage": "Enviar unha mensaxe de resposta", + "CalendarEventRsvpPanel_Tentative": "Provisional", + "CalendarEventRsvpPanel_Title": "Opcións de resposta", + "CalendarAttendeeStatus_Accepted": "Aceptado", + "CalendarAttendeeStatus_Declined": "Rexeitado", + "CalendarAttendeeStatus_NeedsAction": "Precisa acción", + "CalendarAttendeeStatus_Tentative": "Provisional", + "CalendarEventDetails_Attachments": "Anexos", + "CalendarEventCompose_AddAttachment": "Engadir anexo", + "CalendarEventCompose_AllDay": "Todo o día", + "CalendarEventCompose_AttachmentsNotSupportedForCalDav": "Os anexos non son compatibles cos calendarios CalDAV.", + "CalendarEventCompose_EndDate": "Data de fin", + "CalendarEventCompose_EndTime": "Hora de fin", + "CalendarEventCompose_Every": "cada", + "CalendarEventCompose_ForWeekdays": "para", + "CalendarEventCompose_FrequencyDay": "día", + "CalendarEventCompose_FrequencyDayPlural": "días", + "CalendarEventCompose_FrequencyMonth": "mes", + "CalendarEventCompose_FrequencyMonthPlural": "meses", + "CalendarEventCompose_FrequencyWeek": "semana", + "CalendarEventCompose_FrequencyWeekPlural": "semanas", + "CalendarEventCompose_FrequencyYear": "ano", + "CalendarEventCompose_FrequencyYearPlural": "anos", + "CalendarEventCompose_Location": "Lugar", + "CalendarEventCompose_LocationPlaceholder": "Engadir un lugar", + "CalendarEventCompose_NewEventButton": "Novo Evento", + "CalendarEventCompose_DefaultCalendarHint": "Podes escoller un calendario por defecto para novos eventos nas configuracións do Calendario.", + "CalendarEventCompose_DefaultCalendarSettingsLink": "Abrir as configuracións do Calendario", + "CalendarEventCompose_NoCalendarsMessage": "Aínda non hai calendarios dispoñibles para crear eventos.", + "CalendarEventCompose_NoCalendarsTitle": "Non hai calendarios dispoñibles", + "CalendarEventCompose_NoEndDate": "Sen data de fin", + "CalendarEventCompose_Notes": "Notas", + "CalendarEventCompose_PickCalendarTitle": "Elixe un calendario", + "CalendarEventCompose_Recurring": "Recurrente", + "CalendarEventCompose_RecurringSummary": "Ocorre cada {0} {1}{2} {3} efectivo {4}{5}", + "CalendarEventCompose_RecurringSummarySmart": "Ocorre {0}{1} {2} efectivo {3}{4}", + "CalendarEventCompose_RepeatEvery": "Repetir cada", + "CalendarEventCompose_SelectCalendar": "Seleccionar calendario", + "CalendarEventCompose_SingleOccurrenceSummary": "Ocorre en {0} {1}", + "CalendarEventCompose_StartDate": "Data de inicio", + "CalendarEventCompose_StartTime": "Hora de inicio", + "CalendarEventCompose_TimeRangeSummary": "de {0} a {1}", + "CalendarEventCompose_Title": "Título do evento", + "CalendarEventCompose_TitlePlaceholder": "Engadir un título", + "CalendarEventCompose_Until": "ata", + "CalendarEventCompose_UntilSummary": " ata {0}", + "CalendarEventCompose_ValidationInvalidAllDayRange": "A data de finalización de todo o día debe ser posterior á data de inicio.", + "CalendarEventCompose_ValidationInvalidAttendee": "Un ou máis asistentes teñen unha dirección de correo electrónico non válida.", + "CalendarEventCompose_ValidationInvalidRecurrenceEnd": "A data de finalización da recorrencia debe ser igual ou posterior á data de inicio do evento.", + "CalendarEventCompose_ValidationInvalidTimeRange": "A hora de finalización debe ser posterior á hora de inicio.", + "CalendarEventCompose_ValidationMissingAttachment": "Unha ou máis anexos xa non están dispoñibles: {0}", + "CalendarEventCompose_ValidationMissingCalendar": "Selecciona un calendario antes de crear o evento.", + "CalendarEventCompose_ValidationMissingTitle": "Introduce o título do evento antes de crear o evento.", + "CalendarEventCompose_ValidationTitle": "A validación do evento fallou", + "CalendarEventCompose_WeekdaySummary": " en {0}", + "CalendarEventCompose_Weekday_Friday": "V", + "CalendarEventCompose_Weekday_Monday": "L", + "CalendarEventCompose_Weekday_Saturday": "S", + "CalendarEventCompose_Weekday_Sunday": "S", + "CalendarEventCompose_Weekday_Thursday": "T", + "CalendarEventCompose_Weekday_Tuesday": "T", + "CalendarEventCompose_Weekday_Wednesday": "W", + "CalendarEventDetails_Details": "Detalles", + "CalendarEventDetails_EditSeries": "Editar Séries", + "CalendarEventDetails_Editing": "Editando", + "CalendarEventDetails_InviteSomeone": "Invitar a alguén", + "CalendarEventDetails_JoinOnline": "Unirse en liña", + "CalendarEventDetails_Organizer": "Organizador", + "CalendarEventDetails_People": "Persoas", + "CalendarEventDetails_ReadOnlyEvent": "Evento de só lectura", + "CalendarEventDetails_Reminder": "Recordatorio", + "CalendarReminder_StartedHoursAgo": "Comezou hai {0} horas", + "CalendarReminder_StartedMinutesAgo": "Comezou hai {0} minutos", + "CalendarReminder_StartedNow": "Acaba de comezar", + "CalendarReminder_StartingNow": "Comeza agora", + "CalendarReminder_StartsInHours": "Comeza en {0} horas", + "CalendarReminder_StartsInMinutes": "Comeza en {0} minutos", + "CalendarReminder_SnoozeAction": "Sonear", + "CalendarReminder_SnoozeMinutesOption": "{0} minutos", + "CalendarEventDetails_ShowAs": "Mostrar como", + "CalendarShowAs_Free": "Livre", + "CalendarShowAs_Tentative": "Tentativo", + "CalendarShowAs_Busy": "Ocupado", + "CalendarShowAs_OutOfOffice": "Fóra da oficina", + "CalendarShowAs_WorkingElsewhere": "Traballando noutro lugar", "CalendarItem_DetailsPopup_JoinOnline": "Join online", "CalendarItem_DetailsPopup_ViewEventButton": "View event", "CalendarItem_DetailsPopup_ViewSeriesButton": "View series", @@ -88,6 +216,9 @@ "ClipboardTextCopied_Message": "{0} copiado ó portapapeis.", "ClipboardTextCopied_Title": "Copiado", "ClipboardTextCopyFailed_Message": "Produciuse un erro ao copiar {0} no portapapeis.", + "ContactInfoBar_ErrorTitle": "Error ao cargar a información de contacto", + "ContactInfoBar_SuccessTitle": "Información de contacto cargada", + "ContactInfoBar_WarningTitle": "A información de contacto pode estar incompleta", "ComingSoon": "Proximamente...", "ComposerAttachmentsDragDropAttach_Message": "Adxuntar", "ComposerAttachmentsDropZone_Message": "Solte os ficheiros aquí", @@ -129,6 +260,10 @@ "DialogMessage_CreateLinkedAccountTitle": "Nome da conta vinculada", "DialogMessage_DeleteAccountConfirmationMessage": "Borrar {0}?", "DialogMessage_DeleteAccountConfirmationTitle": "Todos os datos asociados con esta conta serán borrados do disco permanentemente.", + "DialogMessage_DeleteEmailTemplateConfirmationMessage": "Eliminar a plantilla \"{0}\"?", + "DialogMessage_DeleteEmailTemplateConfirmationTitle": "Eliminar plantilla de correo", + "DialogMessage_DeleteRecurringSeriesMessage": "Isto eliminará todos os eventos da serie. Queres continuar?", + "DialogMessage_DeleteRecurringSeriesTitle": "Eliminar serie recurrente", "DialogMessage_DiscardDraftConfirmationMessage": "O borrador será eliminado. Quere continuar?", "DialogMessage_DiscardDraftConfirmationTitle": "Eliminar borrador", "DialogMessage_EmptySubjectConfirmation": "Missing Subject", @@ -172,11 +307,18 @@ "ElementTheme_Light": "Modo claro", "Emoji": "Emoticonas", "Error_FailedToSetupSystemFolders_Title": "Failed to setup system folders", + "Exception_AccountNeedsAttention_Title": "A conta precisa atención", + "Exception_AccountNeedsAttention_Message": "'{0}' require a túa atención para continuar a funcionar.", + "Exception_WebView2RuntimeMissing_Message": "Wino Mail non pode localizar o runtime WebView2 de Microsoft Edge. Por favor instale ou repare o runtime para renderizar o contido dos mensaxes correctamente.", + "Exception_WebView2RuntimeMissing_Title": "É necesario o runtime WebView2", "Exception_AuthenticationCanceled": "Cancelouse a autenticación", "Exception_CustomThemeExists": "Este tema xa existe.", "Exception_CustomThemeMissingName": "Debes proporcionar un nome.", "Exception_CustomThemeMissingWallpaper": "Debes proporcionar unha imaxe de fondo personalizada.", "Exception_FailedToSynchronizeAliases": "Failed to synchronize aliases", + "Exception_FailedToSynchronizeCalendarData": "Falhou a sincronización dos datos do calendario", + "Exception_FailedToSynchronizeCalendarEvents": "Falhou a sincronización dos eventos do calendario", + "Exception_FailedToSynchronizeCalendarMetadata": "Falhou a sincronización dos metadatos do calendario", "Exception_FailedToSynchronizeFolders": "Produciuse un erro ao sincronizar as carpetas", "Exception_FailedToSynchronizeProfileInformation": "Failed to synchronize profile information", "Exception_GoogleAuthCallbackNull": "A uri de retrochamada é nula na activación.", @@ -229,6 +371,32 @@ "HoverActionOption_MoveJunk": "Mover a correo lixo", "HoverActionOption_ToggleFlag": "Marcar / Desmarcar", "HoverActionOption_ToggleRead": "Ler / Desler", + "KeyboardShortcuts_FailedToReset": "Fallou ao restablecer os atallos de teclado.", + "KeyboardShortcuts_FailedToUpdate": "Fallou a actualización dos atallos de teclado.", + "KeyboardShortcuts_MailoperationAction": "Acción", + "KeyboardShortcuts_Action": "Acción", + "KeyboardShortcuts_FailedToLoad": "Fallou ao cargar os atallos de teclado.", + "KeyboardShortcuts_EnterKeyForShortcut": "Por favor, introduce unha tecla para o atallo.", + "KeyboardShortcuts_SelectOperationForShortcut": "Por favor, selecciona unha acción para o atallo.", + "KeyboardShortcuts_EnterKey": "Por favor, introduce unha tecla para o atallo.", + "KeyboardShortcuts_SelectOperation": "Por favor, selecciona unha acción para o atallo.", + "KeyboardShortcuts_ShortcutInUse": "Este atallo xa está en uso por outro atallo.", + "KeyboardShortcuts_FailedToSave": "Fallou ao gardar o atallo.", + "KeyboardShortcuts_FailedToDelete": "Fallou ao eliminar o atallo.", + "KeyboardShortcuts_PageDescription": "Configura os atallos de teclado para operacións rápidas de correo. Pulsa as teclas mentres o foco está no campo de entrada da tecla para capturar os atallos.", + "KeyboardShortcuts_Add": "Engadir atallo", + "KeyboardShortcuts_EditTitle": "Editar atallo de teclado", + "KeyboardShortcuts_ResetToDefaults": "Restablecer aos valores por defecto", + "KeyboardShortcuts_PressKeysHere": "Pulsa aquí as teclas...", + "KeyboardShortcuts_KeyCombination": "Combinación de teclas", + "KeyboardShortcuts_FocusArea": "Foca o campo superior e pulsa a combinación de teclas desexada.", + "KeyboardShortcuts_Modifiers": "Teclas modificadoras", + "KeyboardShortcuts_Mode": "Modo da aplicación", + "KeyboardShortcuts_ModeMail": "Correo", + "KeyboardShortcuts_ModeCalendar": "Calendario", + "KeyboardShortcuts_ActionToggleReadUnread": "Alternar lido/non lido", + "KeyboardShortcuts_ActionToggleFlag": "Alternar bandeira", + "KeyboardShortcuts_ActionToggleArchive": "Alternar arquivar/desarquivar", "ImageRenderingDisabled": "A mostra de imaxes está desactivada para esta mensaxe.", "ImapAdvancedSetupDialog_AuthenticationMethod": "Authentication method", "ImapAdvancedSetupDialog_ConnectionSecurity": "Connection security", @@ -295,12 +463,58 @@ "IMAPSetupDialog_Username": "Nome de usuario", "IMAPSetupDialog_UsernamePlaceholder": "vicenterisco, vicenterisco@dominio.com, dominio/vicenterisco", "IMAPSetupDialog_UseSameConfig": "Usa o mesmo usuario e contrasinal para enviar correo", + "ImapCalDavSettingsPage_TitleCreate": "Configuración de IMAP e Calendario", + "ImapCalDavSettingsPage_TitleEdit": "Editar configuración de IMAP e Calendario", + "ImapCalDavSettingsPage_Subtitle": "Configura IMAP/SMTP e a sincronización de calendario opcional para esta conta.", + "ImapCalDavSettingsPage_BasicSectionTitle": "Configuración básica", + "ImapCalDavSettingsPage_BasicSectionDescription": "Introduce a túa identidade e credenciais. Wino pode tentar detectar automaticamente a configuración do servidor.", + "ImapCalDavSettingsPage_BasicTab": "Básico", + "ImapCalDavSettingsPage_EnableCalendarSupport": "Habilitar o soporte do calendario", + "ImapCalDavSettingsPage_AutoDiscoverButton": "Descubrir automaticamente a configuración de correo", + "ImapCalDavSettingsPage_AutoDiscoverySuccessMessage": "Configuración de correo descuberta e aplicada.", + "ImapCalDavSettingsPage_AdvancedSectionTitle": "Configuración avanzada", + "ImapCalDavSettingsPage_AdvancedSectionDescription": "Introduce a configuración do servidor manualmente se a detección automática non está dispoñible ou é incorrecta.", + "ImapCalDavSettingsPage_AdvancedTab": "Avanzado", + "ImapCalDavSettingsPage_CalendarSectionTitle": "Configuración do calendario", + "ImapCalDavSettingsPage_CalendarSectionDescription": "Elixa como deberían funcionar os datos do calendario para esta conta IMAP.", + "ImapCalDavSettingsPage_CalendarModeHeader": "Modo do calendario", + "ImapCalDavSettingsPage_ConnectionSecurityHeader": "Seguridade da conexión", + "ImapCalDavSettingsPage_AuthenticationMethodHeader": "Método de autenticación", + "ImapCalDavSettingsPage_CalendarModeDisabled": "Desactivado", + "ImapCalDavSettingsPage_CalendarModeCalDav": "Sincronización CalDAV", + "ImapCalDavSettingsPage_CalendarModeLocalOnly": "Só calendario local", + "ImapCalDavSettingsPage_CalendarModeDisabledDescription": "O calendario está desactivado para esta conta.", + "ImapCalDavSettingsPage_CalendarModeCalDavDescription": "Os elementos do calendario sincronízanse co teu servidor CalDAV.", + "ImapCalDavSettingsPage_CalendarModeLocalOnlyDescription": "Os elementos do calendario só se gardan neste ordenador e non se sincronizan na rede.", + "ImapCalDavSettingsPage_LocalCalendarLearnMore": "Como funciona o calendario local", + "ImapCalDavSettingsPage_LocalCalendarDialogTitle": "Só calendario local", + "ImapCalDavSettingsPage_LocalCalendarDialogMessage": "O calendario local garda todos os eventos só no teu ordenador. Non se sincroniza con iCloud, Yahoo nin calquera outro proveedor.", + "ImapCalDavSettingsPage_CalDavServiceUrl": "URL do servizo CalDAV", + "ImapCalDavSettingsPage_CalDavUsername": "Nome de usuario CalDAV", + "ImapCalDavSettingsPage_CalDavPassword": "Contrasinal CalDAV", + "ImapCalDavSettingsPage_CalDavNotRequiredMessage": "A proba de CalDAV só é necesaria cando o modo de calendario está establecido en sincronización CalDAV.", + "ImapCalDavSettingsPage_CalDavUrlRequired": "O URL do servizo CalDAV é obligatorio.", + "ImapCalDavSettingsPage_CalDavUrlInvalid": "O URL do servizo CalDAV debe ser un URL absoluto.", + "ImapCalDavSettingsPage_CalDavUsernameRequired": "O nome de usuario CalDAV é obrigatorio.", + "ImapCalDavSettingsPage_CalDavPasswordRequired": "A contrasinal CalDAV é obrigatorio.", + "ImapCalDavSettingsPage_TestImapButton": "Probar conexión IMAP", + "ImapCalDavSettingsPage_TestCalDavButton": "Probar conexión CalDAV", + "ImapCalDavSettingsPage_ImapTestSuccessMessage": "Conexión IMAP probada con éxito.", + "ImapCalDavSettingsPage_CalDavTestSuccessMessage": "Conexión CalDAV probada con éxito.", + "ImapCalDavSettingsPage_SaveSuccessMessage": "Axustes da conta validados e gardados.", + "ImapCalDavSettingsPage_ICloudHint": "Use unha contrasinal de aplicación xerado nas configuracións da súa conta de Apple.", + "ImapCalDavSettingsPage_YahooHint": "Use unha contrasinal de aplicación das configuracións de seguridade da súa conta de Yahoo.", "Info_AccountCreatedMessage": "{0} creouse", "Info_AccountCreatedTitle": "Creación de contas", "Info_AccountCreationFailedTitle": "A creación da conta fallou", "Info_AccountDeletedMessage": "{0} borrouse correctamente.", "Info_AccountDeletedTitle": "A conta borrouse", "Info_AccountIssueFixFailedTitle": "Fallou", + "Info_AccountIssueFixImapMessage": "Abra a páxina de configuración de IMAP e calendario para volver introducir as credenciais do servidor.", + "Info_AccountAttentionRequiredMessage": "Esta conta necesita a súa atención.", + "Info_AccountAttentionRequiredClickableMessage": "Prema para solucionar esta conta e re-sincronizala.", + "Info_AccountAttentionRequiredAction": "Solucionar", + "Info_AccountAttentionRequiredActionHint": "Prema Solucionar para resolver o problema desta conta.", "Info_AccountIssueFixSuccessMessage": "Solucionados todos os erros da conta.", "Info_AccountIssueFixSuccessTitle": "Éxito", "Info_AttachmentOpenFailedMessage": "Non se pode abrir este ficheiro adxunto.", @@ -370,6 +584,7 @@ "InfoBarMessage_SynchronizationDisabledFolder": "A sincronización está desactivada neste cartafol.", "InfoBarTitle_SynchronizationDisabledFolder": "Cartafol desactivado", "Justify": "Justify", + "MenuUpdateAvailable": "Actualización dispoñible", "Left": "Left", "Link": "Link", "LinkedAccountsCreatePolicyMessage": "you must have at least 2 accounts to create link\nlink will be removed on save", @@ -403,6 +618,7 @@ "MailOperation_Unarchive": "Unarchive", "MailOperation_ViewMessageSource": "View message source", "MailOperation_Zoom": "Zoom", + "MailsDragging": "Arrastrando {0} elemento(s)", "MailsSelected": "{0} item(s) selected", "MarkFlagUnflag": "Mark as flagged/unflagged", "MarkReadUnread": "Mark as read/unread", @@ -434,6 +650,8 @@ "Notifications_MultipleNotificationsTitle": "New Mail", "Notifications_WinoUpdatedMessage": "Checkout new version {0}", "Notifications_WinoUpdatedTitle": "Wino Mail has been updated.", + "Notifications_StoreUpdateAvailableTitle": "Actualización dispoñible", + "Notifications_StoreUpdateAvailableMessage": "Hai unha versión máis recente de Wino Mail lista para instalar desde Microsoft Store.", "OnlineSearchFailed_Message": "Failed to perform search\n{0}\n\nListing offline mails.", "OnlineSearchTry_Line1": "Can't find what you are looking for?", "OnlineSearchTry_Line2": "Try online search.", @@ -446,7 +664,6 @@ "PaneLengthOption_Small": "Small", "Photos": "Photos", "PreparingFoldersMessage": "Preparing folders", - "ProtocolLogAvailable_Message": "Protocol logs are available for diagnostics.", "ProviderDetail_Gmail_Description": "Google Account", "ProviderDetail_iCloud_Description": "Apple iCloud Account", "ProviderDetail_iCloud_Title": "iCloud", @@ -465,9 +682,14 @@ "SearchBarPlaceholder": "Search", "SearchingIn": "Searching in", "SearchPivotName": "Results", + "Settings_KeyboardShortcuts_Title": "Atallos de teclado", + "Settings_KeyboardShortcuts_Description": "Xestiona as teclas de atallo para accións rápidas nos correos.", "SettingConfigureSpecialFolders_Button": "Configure", "SettingsEditAccountDetails_IMAPConfiguration_Title": "IMAP/SMTP Configuration", "SettingsEditAccountDetails_IMAPConfiguration_Description": "Change your incoming/outgoing server settings.", + "SettingsEditAccountDetails_ImapCalDavSettings_Title": "Axustes de IMAP e calendario.", + "SettingsEditAccountDetails_ImapCalDavSettings_Description": "Abrir a páxina de axustes dedicada de IMAP, SMTP e CalDAV para esta conta.", + "SettingsEditAccountDetails_ImapCalDavSettings_Action": "Abrir axustes", "SettingsAbout_Description": "Learn more about Wino.", "SettingsAbout_Title": "About", "SettingsAboutGithub_Description": "Go to issue tracker GitHub repository.", @@ -490,6 +712,10 @@ "SettingsAppPreferences_SearchMode_Local": "Local", "SettingsAppPreferences_SearchMode_Online": "Online", "SettingsAppPreferences_SearchMode_Title": "Default search mode", + "SettingsAppPreferences_ApplicationMode_Title": "Modo da aplicación por defecto.", + "SettingsAppPreferences_ApplicationMode_Description": "Elixa o modo de apertura predeterminado de Wino cando non se define un tipo de activación.", + "SettingsAppPreferences_ApplicationMode_Mail": "Correo", + "SettingsAppPreferences_ApplicationMode_Calendar": "Calendario", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description": "Wino Mail will keep running in the background. You will be notified as new mails arrive.", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title": "Run in the background", "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description": "Wino Mail will keep running on the system tray. Available to launch by clicking on an icon. You will be notified as new mails arrive.", @@ -506,12 +732,30 @@ "SettingsAppPreferences_StartupBehavior_FatalError": "Fatal error occurred while changing the startup mode for Wino Mail.", "SettingsAppPreferences_StartupBehavior_Title": "Start minimized on Windows startup", "SettingsAppPreferences_Title": "App Preferences", + "SettingsAppPreferences_HideWinoAccountButton_Title": "Ocultar o botón da conta de Wino na barra de título.", + "SettingsAppPreferences_HideWinoAccountButton_Description": "Ocultar o botón de perfil na barra de título que abre o panel da conta de Wino.", + "SettingsAppPreferences_StoreUpdateNotifications_Title": "Notificacións de actualización da Microsoft Store.", + "SettingsAppPreferences_StoreUpdateNotifications_Description": "Mostrar notificacións e accións no rodapé cando haxa unha actualización da Microsoft Store disponível.", + "SettingsAppPreferences_AiActions_Title": "Accións de IA", + "SettingsAppPreferences_AiActions_Description": "Elixa os idiomas de IA predeterminados e onde se deben gardar os resumos.", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Title": "Idioma de tradución por defecto.", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Description": "Selecciona o idioma de destino predeterminado utilizado polas accións de tradución da IA.", + "SettingsAppPreferences_AiSummarizeLanguage_Title": "Idioma de resumo.", + "SettingsAppPreferences_AiSummarizeLanguage_Description": "Selecciona o idioma de resumo preferido para os futuros resumos da IA.", + "SettingsAppPreferences_AiSummarySavePath_Title": "Ruta de almacenamento de resumos por defecto.", + "SettingsAppPreferences_AiSummarySavePath_Description": "Elixa a carpeta que Wino debe usar por defecto ao gardar os resumos da IA.", + "SettingsAppPreferences_AiSummarySavePath_Placeholder": "Usar a localización de almacenamento por defecto do sistema.", + "SettingsAppPreferences_AiSummarySavePath_InvalidHint": "Esta carpeta non existe. Empregarase a ubicación de almacenamento por defecto para os resumos.", "SettingsAutoSelectNextItem_Description": "Select the next item after you delete or move a mail.", "SettingsAutoSelectNextItem_Title": "Auto select next item", "SettingsAvailableThemes_Description": "Select a theme from Wino's own collection for your taste or apply your own themes.", "SettingsAvailableThemes_Title": "Available Themes", "SettingsCalendarSettings_Description": "Change first day of week, hour cell height and more...", "SettingsCalendarSettings_Title": "Calendar Settings", + "CalendarSettings_DefaultSnoozeDuration_Header": "Duración predeterminada de adiamento.", + "CalendarSettings_DefaultSnoozeDuration_Description": "Define unha duración predeterminada de adiamento para as notificacións de recordatorio do calendario.", + "CalendarSettings_TimedDayHeaderFormat_Header": "Formato da cabecera do día na vista temporal.", + "CalendarSettings_TimedDayHeaderFormat_Description": "Elixa como se renderizan as etiquetas do día nas vistas de día, semana e semana laboral. Usa tokens de formato de data como ddd, dd, MMM ou dddd.", "SettingsComposer_Title": "Composer", "SettingsComposerFont_Title": "Default Composer Font", "SettingsComposerFontFamily_Description": "Change the default font family and font size for composing mails.", @@ -531,6 +775,9 @@ "SettingsDiscord_Title": "Discord Channel", "SettingsEditLinkedInbox_Description": "Add / remove accounts, rename or break the link between accounts.", "SettingsEditLinkedInbox_Title": "Edit Linked Inbox", + "SettingsWindowBackdrop_Title": "Fondo da xanela", + "SettingsWindowBackdrop_Description": "Selecciona un efecto de fondo para as ventás de Wino.", + "SettingsWindowBackdrop_Disabled": "A selección do fondo da ventá está desactivada cando o tema da aplicación é diferente de Predeterminado.", "SettingsElementTheme_Description": "Select a Windows theme for Wino", "SettingsElementTheme_Title": "Element Theme", "SettingsElementThemeSelectionDisabled": "Element theme selection is disabled when application theme is selected other than Default.", @@ -581,6 +828,8 @@ "SettingsManageAliases_Title": "Aliases", "SettingsEditAccountDetails_Title": "Edit Account Details", "SettingsEditAccountDetails_Description": "Change account name, sender name and assign a new color if you like.", + "EditAccountDetailsPage_SaveSuccess_Title": "Cambios gardados.", + "EditAccountDetailsPage_SaveSuccess_Message": "Os datos da súa conta foron actualizados correctamente.", "SettingsManageLink_Description": "Move items to add new link or remove existing link.", "SettingsManageLink_Title": "Manage Link", "SettingsMarkAsRead_Description": "Change what should happen to the selected item.", @@ -596,7 +845,41 @@ "SettingsNotifications_Title": "Notifications", "SettingsNotificationsAndTaskbar_Description": "Change whether notifications should be displayed and taskbar badge for this account.", "SettingsNotificationsAndTaskbar_Title": "Notifications & Taskbar", + "SettingsHome_Title": "Inicio", + "SettingsHome_SearchTitle": "Atopa unha configuración", + "SettingsHome_SearchDescription": "Busca por función, tema ou palabra clave para ir directamente á páxina de axustes correspondente.", + "SettingsHome_SearchPlaceholder": "Buscar configuracións", + "SettingsHome_SearchExamples": "Proba: tema, almacenamento, idioma, sinatura", + "SettingsHome_QuickLinks_Title": "Enlaces rápidos", + "SettingsHome_QuickLinks_Description": "Accede ás configuracións ás que se accode con maior frecuencia.", + "SettingsHome_StorageCard_Description": "Vexa canto contido MIME local se conserva neste dispositivo e elimínao cando sexa necesario.", + "SettingsHome_StorageEmptySummary": "Aínda non se detectou contido MIME almacenado en caché.", + "SettingsHome_StorageLoading": "Comprobando o uso local de MIME...", + "SettingsHome_Tips_Title": "Consellos e truques", + "SettingsHome_Tips_Description": "Algúns cambios pequenos poden facer que Wino sexa máis persoal.", + "SettingsHome_Tip_Theme": "Queres modo escuro ou cambios de acento? Abre a Personalización.", + "SettingsHome_Tip_Background": "Utilice Preferencias da Aplicación para controlar o comportamento de inicio e a sincronización en segundo plano.", + "SettingsHome_Tip_Shortcuts": "As teclas de atallo axúdanche a navegar polo correo máis rápido.", + "SettingsHome_Resources_Title": "Enlaces útiles", + "SettingsHome_Resources_Description": "Abrir recursos do proxecto, información de soporte e canles de lanzamento.", "SettingsOptions_Title": "Settings", + "SettingsOptions_GeneralSection": "Xeral", + "SettingsOptions_MailSection": "Correo", + "SettingsOptions_CalendarSection": "Calendario", + "SettingsOptions_MoreComingSoon": "Máis opcións próximamente.", + "SettingsOptions_HeroDescription": "Personaliza a experiencia de Wino Mail.", + "SettingsOptions_AccountsSummary": "{0} conta(s) configurada(s)", + "SettingsSearch_ManageAccounts_Keywords": "conta;contas;caixa de correo;caixas de correo;apelido;apelidos;perfil;enderezo;enderezos", + "SettingsSearch_AppPreferences_Keywords": "inicio;fondo;iniciar;sincronizar;notificación;notificacións;busca;bandeiña do sistema;predeterminados", + "SettingsSearch_LanguageTime_Keywords": "lingua;hora;relox;localidade;rexión;formato;24 horas;24h", + "SettingsSearch_Personalization_Keywords": "tema;escuro;claro;aparencia;acento;cor;cor;modo;deseño;densidade", + "SettingsSearch_About_Keywords": "acerca de;versión;páxina web;privacidade;GitHub;doar;tenda;axuda", + "SettingsSearch_KeyboardShortcuts_Keywords": "atajo;atalos;tecla rápida;teclas rápidas;teclado;teclas", + "SettingsSearch_MessageList_Keywords": "mensaxe;mensaxes;lista;fíos;fíos;avatar;vista previa;remitente", + "SettingsSearch_ReadComposePane_Keywords": "lector;redactar;redactor;tipo de letra;tipos de letra;contido externo;visualización;lectura", + "SettingsSearch_SignatureAndEncryption_Keywords": "sinatura;sinaturas;cifrado;certificado;certificados;s mime;smime;seguridade", + "SettingsSearch_Storage_Keywords": "armazenamento;cache;caché;MIME;disco;espazo;limpeza;limpar;datos locais", + "SettingsSearch_CalendarSettings_Keywords": "calendario;semana;horas;horario;evento;eventos", "SettingsPaneLengthReset_Description": "Reset the size of the mail list to original if you have issues with it.", "SettingsPaneLengthReset_Title": "Reset Mail List Size", "SettingsPaypal_Description": "Show much more love ❤️ All donations are appreciated.", @@ -610,6 +893,8 @@ "SettingsPrefer24HourClock_Title": "Display Clock Format in 24 Hours", "SettingsPrivacyPolicy_Description": "Review privacy policy.", "SettingsPrivacyPolicy_Title": "Privacy Policy", + "SettingsWebsite_Description": "Abrir o sitio web de Wino Mail.", + "SettingsWebsite_Title": "Sitio web", "SettingsReadComposePane_Description": "Fonts, external content.", "SettingsReadComposePane_Title": "Reader & Composer", "SettingsReader_Title": "Reader", @@ -625,6 +910,19 @@ "SettingsShowPreviewText_Title": "Show Preview Text", "SettingsShowSenderPictures_Description": "Hide/show the thumbnail sender pictures.", "SettingsShowSenderPictures_Title": "Show Sender Avatars", + "SettingsEmailTemplates_Title": "Plantillas de correo electrónico", + "SettingsEmailTemplates_Description": "Xestiona as plantillas de correo electrónico.", + "SettingsEmailTemplates_CreatePageTitle": "Nova plantilla", + "SettingsEmailTemplates_EditPageTitle": "Editar plantilla", + "SettingsEmailTemplates_NewTemplateTitle": "Nova plantilla", + "SettingsEmailTemplates_NewTemplateDescription": "Crea unha nova plantilla de correo", + "SettingsEmailTemplates_NameTitle": "Nome", + "SettingsEmailTemplates_NamePlaceholder": "Nome da plantilla", + "SettingsEmailTemplates_DescriptionTitle": "Descripión", + "SettingsEmailTemplates_DescriptionPlaceholder": "Descripión opcional", + "SettingsEmailTemplates_ContentTitle": "Contido da plantilla", + "SettingsEmailTemplates_ContentDescription": "Edita o contido HTML desta plantilla.", + "SettingsEmailTemplates_NameRequired": "O nome da plantilla é obrigatorio.", "SettingsEnableGravatarAvatars_Title": "Gravatar", "SettingsEnableGravatarAvatars_Description": "Use gravatar (if available) as sender picture", "SettingsEnableFavicons_Title": "Domain icons (Favicons)", @@ -645,6 +943,33 @@ "SettingsStartupItem_Title": "Startup Item", "SettingsStore_Description": "Show some love ❤️", "SettingsStore_Title": "Rate in Store", + "SettingsStorage_Title": "Armazenamento", + "SettingsStorage_Description": "Escanear e xestionar o caché MIME almacenado no cartafol de datos locais.", + "SettingsStorage_ScanFolder": "Escanea o cartafol de datos locais.", + "SettingsStorage_NoLocalMimeDataFound": "Non se atoparon datos MIME locais.", + "SettingsStorage_NoAccountsFound": "Non se atoparon contas.", + "SettingsStorage_TotalUsage": "Uso total local de MIME: {0}", + "SettingsStorage_AccountUsageDescription": "{0} usado no caché MIME local.", + "SettingsStorage_DeleteAll_Title": "Eliminar todo o contido MIME.", + "SettingsStorage_DeleteAll_Description": "Eliminar a carpeta completa do caché MIME desta conta.", + "SettingsStorage_DeleteAll_Button": "Eliminar todo", + "SettingsStorage_DeleteAll_Confirm_Title": "Eliminar todo o contido MIME.", + "SettingsStorage_DeleteAll_Confirm_Message": "Eliminar todos os datos MIME locais para {0}?", + "SettingsStorage_DeleteAll_Success": "Todo o contido MIME foi eliminado.", + "SettingsStorage_DeleteOld_Title": "Eliminar contido MIME antigo.", + "SettingsStorage_DeleteOld_Description": "Eliminar ficheiros MIME baseados na data de creación dos correos na base de datos local.", + "SettingsStorage_DeleteOld_1Month": "> 1 mes", + "SettingsStorage_DeleteOld_3Months": "> 3 meses", + "SettingsStorage_DeleteOld_6Months": "> 6 meses", + "SettingsStorage_DeleteOld_1Year": "> 1 ano", + "SettingsStorage_DeleteOld_Confirm_Title": "Eliminar contido MIME antigo.", + "SettingsStorage_DeleteOld_Confirm_Message": "Eliminar datos MIME locais máis antigos que {0} para {1}?", + "SettingsStorage_DeleteOld_Success": "Eliminadas {0} carpetas MIME máis antigas que {1}.", + "SettingsStorage_1Month": "1 mes", + "SettingsStorage_3Months": "3 meses", + "SettingsStorage_6Months": "6 meses", + "SettingsStorage_1Year": "1 ano", + "SettingsStorage_Months": "{0} meses", "SettingsTaskbarBadge_Description": "Include unread mail count in taskbar icon.", "SettingsTaskbarBadge_Title": "Taskbar Badge", "SettingsThreads_Description": "Organize messages into conversation threads.", @@ -683,6 +1008,9 @@ "SystemFolderConfigDialogValidation_InboxSelected": "You can't assign Inbox folder to any other system folder.", "SystemFolderConfigSetupSuccess_Message": "System folders are successfully configured.", "SystemFolderConfigSetupSuccess_Title": "System Folders Setup", + "SystemTrayMenu_ShowWino": "Abrir Wino Mail", + "SystemTrayMenu_ShowWinoCalendar": "Abrir Wino Calendar", + "SystemTrayMenu_ExitWino": "Saír", "TestingImapConnectionMessage": "Testing server connection...", "TitleBarServerDisconnectedButton_Description": "Wino is disconnected from the network. Click reconnect to restore connection.", "TitleBarServerDisconnectedButton_Title": "no connection", @@ -699,8 +1027,422 @@ "WinoUpgradeMessage": "Upgrade to Unlimited Accounts", "WinoUpgradeRemainingAccountsMessage": "{0} out of {1} free accounts used.", "Yesterday": "Yesterday", + "Smime_ImportCertificates_Success": "Certificados importados con éxito.", + "Smime_ImportCertificates_Error": "Erro ao importar certificados: {0}", + "Smime_RemoveCertificates_Confirm": "Quere realmente eliminar os certificados {0}?", + "Smime_RemoveCertificates_Success": "Certificados eliminados.", + "Smime_ExportCertificates_Success": "Certificados exportados.", + "Smime_ExportCertificates_Error": "Erro ao exportar certificados.", + "Smime_CertificateDetails": "Suxeito: {0}\\nEmisor: {1}\\nVálido desde: {2}\\nVálido ata: {3}\\nHuella dactilar: {4}", + "Smime_CertificatePassword_Title": "Contrasinal do certificado obrigatorio", + "Smime_CertificatePassword_Placeholder": "Contrasinal do certificado para {0} (opcional)", + "Smime_Confirm_Title": "Confirmar", + "Buttons_OK": "Aceptar", + "Buttons_Refresh": "Actualizar", + "SettingsSignatureAndEncryption_Title": "Sinatura e cifrado", + "SettingsSignatureAndEncryption_Description": "Xestiona certificados S/MIME para asinatura e cifrado de correos.", + "SettingsSignatureAndEncryption_MyCertificatesHeader": "Os meus certificados", + "SettingsSignatureAndEncryption_MyCertificatesDescription": "Certificados persoais para assinatura e cifrado.", + "SettingsSignatureAndEncryption_RecipientCertificatesHeader": "Certificados dos destinatarios", + "SettingsSignatureAndEncryption_RecipientCertificatesDescription": "Certificados dos destinatarios para descifrado.", + "SettingsSignatureAndEncryption_NameColumn": "Nome", + "SettingsSignatureAndEncryption_ExpiresColumn": "Expira en", + "SettingsSignatureAndEncryption_ThumbprintColumn": "Huella dactilar", + "Buttons_Remove": "Eliminar", + "Buttons_Export": "Exportar", + "Buttons_Import": "Importar", + "SettingsSignatureAndEncryption_SigningCertificate": "Certificado de assinatura S/MIME", + "SettingsSignatureAndEncryption_EncryptionCertificate": "Certificado de cifrado S/MIME", + "SettingsSignatureAndEncryption_SigningCertificatePlaceholder": "Ningún", + "SmimeSignaturesInMessage": "Sinaturas neste mensaxe:", + "SmimeSignatureEntry": "• {0} {1} ({2}, válido ata {3} - {4})", + "SmimeSigningCertificateInfoTitle": "Información do certificado de assinatura S/MIME", + "SmimeCertificateInfoTitle": "Información do certificado S/MIME", + "SmimeNoCertificateFileFound": "Non se atopou ningún ficheiro de certificado.", + "SmimeSaveCertificate": "Gardar certificado...", + "SmimeCertificate": "Certificado S/MIME", + "SmimeCertificateSavedTo": "O certificado gardouse en {0}", + "SmimeSignedTooltip": "Este mensaxe está asinado con un certificado S/MIME. Clic para máis detalles", + "SmimeEncryptedTooltip": "Este mensaxe está cifrado con un certificado S/MIME.", + "SmimeCertificateFileInfo": "Ficheiro: {0}", + "Composer_LightTheme": "Tema claro", + "Composer_DarkTheme": "Tema escuro", + "Composer_Outdent": "Reducir sangría", + "Composer_Indent": "Aumentar sangría", + "Composer_BulletList": "Lista de viñetas", + "Composer_OrderedList": "Lista numerada", + "Composer_Stroke": "Trazo", + "Composer_Bold": "Negrita", + "Composer_Italic": "Cursiva", + "Composer_Underline": "Subliñado", + "Composer_CcBcc": "Cc e Bcc", + "Composer_EnableSmimeSignature": "Habilitar/ deshabilitar a sinatura S/MIME", + "Composer_EnableSmimeEncryption": "Habilitar/ deshabilitar o cifrado S/MIME", + "Composer_LocalDraftSyncInfo": "Este borrador é só local. O Wino non conseguiu envialo ao teu servidor de correo. Clica para tentar envialo de novo ao servidor.", + "Composer_CertificateExpires": "Expira en: ", + "Composer_SmimeSignature": "Assinatura S/MIME", + "Composer_SmimeEncryption": "Cifrado S/MIME", + "Composer_EmailTemplatesPlaceholder": "Plantillas de correo", + "Composer_AiSummarize": "Resumir con IA", + "Composer_AiSummarizeDescription": "Extrae puntos clave, accións e decisións deste correo.", + "Composer_AiTranslate": "Traducir con IA", + "Composer_AiActions": "Accións IA", + "Composer_AiRewrite": "Reescribe con IA", + "AiActions_CheckingStatus": "Comprobando o acceso á IA...", + "AiActions_SignedOutTitle": "Desbloquear o paquete AI de Wino", + "AiActions_SignedOutDescription": "Traducir, reescribir e resumir correos con IA despois de iniciar sesión na túa Conta Wino e activar o complemento AI Pack.", + "AiActions_NoPackTitle": "Precísase AI Pack", + "AiActions_NoPackDescription": "Iniciaste sesión, pero o AI Pack non está activo aínda. Cómprao para usar as ferramentas de tradución, reescritura e resumo de IA de Wino.", + "AiActions_UsageSummary": "{0} de {1} créditos usados este mes.", + "Composer_AiRewritePolite": "Faino máis educado", + "Composer_AiRewritePoliteDescription": "Ablanda a redacción mantendo o mesmo propósito.", + "Composer_AiRewriteAngry": "Faino enfadado", + "Composer_AiRewriteAngryDescription": "Emprega un ton máis agudo e confrontativo.", + "Composer_AiRewriteHappy": "Fai que sexa feliz", + "Composer_AiRewriteHappyDescription": "Engade un ton máis optimista e entusiástico.", + "Composer_AiRewriteFormal": "Fai que sexa formal", + "Composer_AiRewriteFormalDescription": "Fai que a mensaxe soe máis profesional e estruturada.", + "Composer_AiRewriteFriendly": "Fai que sexa amigable", + "Composer_AiRewriteFriendlyDescription": "Aporta un ton máis accesible á mensaxe.", + "Composer_AiRewriteShorter": "Fai que sexa máis curto", + "Composer_AiRewriteShorterDescription": "Condensa o texto e elimina detalles innecesarios.", + "Composer_AiRewriteClearer": "Fai que sexa máis claro", + "Composer_AiRewriteClearerDescription": "Melora a legibilidade e facilita a lectura da mensaxe.", + "Composer_AiRewriteCustom": "Personalizado", + "Composer_AiRewriteCustomDescription": "Describe a túa propia intención de reescritura.", + "Composer_AiRewriteCustomPlaceholder": "Describe como queres que a mensaxe sexa reescrita", + "Composer_AiRewriteMode": "Tono de reescritura", + "Composer_AiRewriteApply": "Aplicar a reescritura", + "Composer_AiTranslateDialogTitle": "Traducir con IA", + "Composer_AiTranslateDialogDescription": "Introduce a lingua ou código de cultura de destino, por exemplo, en-US, tr-TR, de-DE ou fr-FR.", + "Composer_AiTranslateApply": "Traducir", + "Composer_AiTranslateLanguage": "Idioma de destino", + "Composer_AiTranslateCustomPlaceholder": "Introduce o código de cultura", + "Composer_AiTranslateLanguageEnglish": "Inglés (en-US)", + "Composer_AiTranslateLanguageTurkish": "Turco (tr-TR)", + "Composer_AiTranslateLanguageGerman": "Alemán (de-DE)", + "Composer_AiTranslateLanguageFrench": "Francés (fr-FR)", + "Composer_AiTranslateLanguageSpanish": "Español (es-ES)", + "Composer_AiTranslateLanguageItalian": "Italiano (it-IT)", + "Composer_AiTranslateLanguagePortugueseBrazil": "Portugués (Brasil) (pt-BR)", + "Composer_AiTranslateLanguageDutch": "Holandés (nl-NL)", + "Composer_AiTranslateLanguagePolish": "Polaco (pl-PL)", + "Composer_AiTranslateLanguageRussian": "Ruso (ru-RU)", + "Composer_AiTranslateLanguageJapanese": "Xaponés (ja-JP)", + "Composer_AiTranslateLanguageKorean": "Coreano (ko-KR)", + "Composer_AiTranslateLanguageChineseSimplified": "Chinés, Simplificado (zh-CN)", + "Composer_AiTranslateLanguageArabic": "Árabe (ar-SA)", + "Composer_AiTranslateLanguageHindi": "Hindi (hi-IN)", + "Composer_AiTranslateLanguageOther": "Outro...", + "Composer_AiBusyTitle": "AIA xa está a funcionar", + "Composer_AiBusyMessage": "Por favor agarda a que remate a acción actual de IA.", + "Composer_AiSignInRequired": "Inicia sesión na túa conta Wino para usar as funcións de IA.", + "Composer_AiMissingHtml": "Ainda non hai contido de mensaxe para enviar á IA de Wino.", + "Composer_AiQuotaUnavailable": "O resultado da IA foi aplicado.", + "Composer_AiAppliedMessage": "O resultado da IA aplicouse ao compositor. Usa Desfacer se queres revertelo.", + "Composer_AiSummarizeSuccessTitle": "Resumo da IA aplicado.", + "Composer_AiTranslateSuccessTitle": "Tradución da IA aplicada.", + "Composer_AiRewriteSuccessTitle": "Reescritura da IA aplicada.", + "Composer_AiErrorTitle": "A acción da IA fallou.", + "Reader_AiAppliedMessage": "O resultado da IA móstrase agora nesta mensaxe. Reabre a mensaxe para ver de novo o contido orixinal.", "SettingsAppPreferences_EmailSyncInterval_Title": "Email sync interval", - "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail." + "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail.", + "ContactsPage_Title": "Contactos", + "ContactsPage_AddContact": "Engadir Contacto", + "ContactsPage_EditContact": "Editar Contacto", + "ContactsPage_DeleteContact": "Borrar Contacto", + "ContactsPage_SearchPlaceholder": "Buscar contactos...", + "ContactsPage_NoContacts": "Non se atoparon contactos", + "ContactsPage_ContactsCount": "{0} contactos", + "ContactsPage_SelectedContactsCount": "{0} seleccionados", + "ContactsPage_DeleteSelectedContacts": "Borrar seleccionados", + "ContactEditDialog_Title": "Editar Contacto", + "ContactEditDialog_PhotoSection": "Foto", + "ContactEditDialog_ChoosePhoto": "Seleccionar Foto", + "ContactEditDialog_RemovePhoto": "Eliminar Foto", + "ContactEditDialog_NameHeader": "Nome", + "ContactEditDialog_NamePlaceholder": "Nome do contacto", + "ContactEditDialog_EmailHeader": "Enderezo de correo electrónico", + "ContactEditDialog_EmailPlaceholder": "contact@example.com", + "ContactEditDialog_InfoSection": "Información do contacto", + "ContactEditDialog_RootContactInfo": "Este é un contacto raíz asociado ás túas contas e non se pode eliminar.", + "ContactEditDialog_OverriddenContactInfo": "Este contacto foi modificado manualmente e non se actualizará durante a sincronización.", + "ContactsPage_Subtitle": "Xestiona os teus contactos de correo e a súa información.", + "ContactStatus_Account": "Conta", + "ContactStatus_Modified": "Modificado", + "ContactAction_Edit": "Editar contacto", + "ContactAction_ChangePhoto": "Cambiar foto", + "ContactAction_Delete": "Eliminar contacto", + "ContactAction_Add": "Engadir contacto", + "ContactSelection_Selected": "seleccionado", + "ContactSelection_SelectAll": "Seleccionar todo", + "ContactSelection_Clear": "Borrar selección", + "ContactsPage_EmptyState": "Non hai contactos para amosar", + "ContactsPage_AddFirstContact": "Engade o teu primeiro contacto", + "ContactsPage_ContactsCountSuffix": "contactos", + "ContactsPane_NewContact": "Novo contacto", + "ContactsPane_DescriptionTitle": "Xestiona os teus contactos", + "ContactsPane_DescriptionBody": "Crea contactos, renóminalos, actualiza as fotos de perfil e mantén organizados os datos gardados nun só lugar.", + "ContactEditDialog_AddTitle": "Engadir contacto", + "ContactInfoBar_ContactAdded": "Contacto engadido correctamente.", + "ContactInfoBar_ContactUpdated": "Contacto actualizado correctamente.", + "ContactInfoBar_ContactsDeleted": "Contactos eliminados correctamente.", + "ContactInfoBar_ContactPhotoUpdated": "Foto do contacto actualizada correctamente.", + "ContactInfoBar_FailedToLoadContacts": "Erro ao cargar os contactos: {0}", + "ContactInfoBar_FailedToAddContact": "Erro ao engadir contacto: {0}", + "ContactInfoBar_FailedToUpdateContact": "Erro ao actualizar o contacto: {0}", + "ContactInfoBar_FailedToDeleteContacts": "Erro ao borrar os contactos: {0}", + "ContactInfoBar_FailedToUpdatePhoto": "Erro ao actualizar a foto: {0}", + "ContactInfoBar_CannotDeleteRoot": "Non se poden eliminar os contactos raíz.", + "ContactConfirmDialog_DeleteTitle": "Eliminar contacto", + "ContactConfirmDialog_DeleteMessage": "Está seguro de que quere eliminar o contacto '{0}'?", + "ContactConfirmDialog_DeleteMultipleMessage": "Está seguro de que quere eliminar {0} contacto(s)?", + "ContactConfirmDialog_DeleteButton": "Eliminar", + "CalendarAccountSettings_Title": "Configuración da conta de calendario", + "CalendarAccountSettings_Description": "Xestiona a configuración do calendario para {0}", + "CalendarAccountSettings_AccountColor": "Cor da conta", + "CalendarAccountSettings_AccountColorDescription": "Mudar a cor de visualización desta conta de calendario", + "CalendarAccountSettings_SyncEnabled": "Habilitar a sincronización", + "CalendarAccountSettings_SyncEnabledDescription": "Habilitar ou deshabilitar a sincronización do calendario para esta conta", + "CalendarAccountSettings_DefaultShowAs": "Estado por defecto de Mostrar como", + "CalendarAccountSettings_DefaultShowAsDescription": "Estado de dispoñibilidade por defecto para novos eventos creados con esta conta", + "CalendarAccountSettings_PrimaryCalendar": "Calendario principal", + "CalendarAccountSettings_PrimaryCalendarDescription": "Marcar este calendario como o calendario principal da conta", + "CalendarSettings_NewEventBehavior_Header": "Comportamento do botón Novo evento", + "CalendarSettings_NewEventBehavior_Description": "Elixe se o botón Novo evento debe pedir un calendario cada vez ou abrir sempre un calendario específico", + "CalendarSettings_NewEventBehavior_AskEachTime": "Preguntar cada vez", + "CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar": "Sempre usar un calendario específico", + "CalendarSettings_Rendering_Title": "Renderizado", + "CalendarSettings_Rendering_Description": "Configurar o deseño do calendario e o comportamento de visualización", + "CalendarSettings_Notifications_Title": "Notificacións", + "CalendarSettings_Notifications_Description": "Elixe o comportamento por defecto para recordatorios e para o adiamento", + "CalendarSettings_Preferences_Title": "Preferencias", + "CalendarSettings_Preferences_Description": "Define como funciona o botón Novo evento", + "WhatIsNew_GetStartedButton": "Comeza", + "WhatIsNew_ContinueAnywayButton": "Continuar de todas as formas", + "WhatIsNew_PreparingForNewVersionButton": "Preparándose para a nova versión...", + "WhatIsNew_MigrationPreparing_Title": "Preparando os teus dados", + "WhatIsNew_MigrationPreparing_Description": "Wino está a aplicar migracións de actualización. Por favor, agarda mentres preparamos os teus datos da conta para esta versión", + "WhatIsNew_MigrationFailedMessage": "Ao aplicar as migracións fallou co código de erro {0}. Pode continuar a usar a aplicación. Non obstante, se atopas problemas graves, por favor reinstale a aplicación", + "WhatIsNew_MigrationNotification_Title": "Wino Mail actualizado", + "WhatIsNew_MigrationNotification_Message": "Abre a aplicación para completar a actualización e ver as novidades", + "WelcomeWindow_Title": "Benvido a Wino Mail", + "WelcomeWindow_Subtitle": "Una experiencia nativa de Windows para Mail e Calendario", + "WelcomeWindow_WhatsNewTitle": "Últimos cambios", + "WelcomeWindow_FeaturesTitle": "Características", + "WelcomeWindow_WhatsNewTab": "Novidades", + "WelcomeWindow_FeaturesTab": "Características", + "WelcomeWindow_GetStartedButton": "Comeza engadindo unha conta", + "WelcomeWindow_GetStartedDescription": "Engade a túa conta de Outlook, Gmail ou IMAP para comezar a usar Wino Mail", + "WelcomeWindow_ImportFromWinoAccount": "Importar desde a túa Conta Wino", + "WelcomeWindow_ImportInProgress": "Importando as túas preferencias e contas sincronizadas...", + "WelcomeWindow_ImportNoAccountsFound": "Non se atoparon contas sincronizadas na túa Conta Wino. Se as preferencias estaban dispoñibles, foron restauradas. Usa Comeza para engadir unha conta manualmente", + "WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} contas sincronizadas xa están dispoñibles neste dispositivo. Usa Comeza para engadir outra conta manualmente se é necesario", + "WelcomeWindow_SetupTitle": "Configura a túa conta", + "WelcomeWindow_SetupSubtitle": "Elixe o teu provedor de correo para comezar", + "WelcomeWindow_AddAccountButton": "Engadir conta", + "WelcomeWindow_SkipForNow": "Omitir por agora — configúrolo máis tarde", + "WelcomeWindow_AppDescription": "Una caixa de entrada rápida e focalizada — redeseñada para Windows 11", + "WelcomeWizard_Step1Title": "Benvido", + "SystemTrayMenu_Open": "Abrir", + "WinoAccount_Titlebar_SyncBenefitTitle": "Configuración de sincronización", + "WinoAccount_Titlebar_SyncBenefitDescription": "Mantén as túas preferencias de Wino en sincronía entre dispositivos", + "WinoAccount_Titlebar_AddonsBenefitTitle": "Desbloquear complementos", + "WinoAccount_Titlebar_AddonsBenefitDescription": "Accede ás funcións premium como o Pacote IA de Wino", + "WinoAccount_Management_Description": "Xestiona a túa Conta Wino, o acceso ao Pacote IA e as preferencias sincronizadas e os detalles da conta", + "WinoAccount_Management_SignedOutTitle": "Inicia sesión en Wino Mail", + "WinoAccount_Management_SignedOutDescription": "Inicia sesión ou crea unha conta para sincronizar o teu correo, acceder ás funcións de IA e xestionar as túas configuracións entre dispositivos", + "WinoAccount_Management_ProfileSectionHeader": "Perfil", + "WinoAccount_Management_AddOnsSectionHeader": "Complementos Wino", + "WinoAccount_Management_DataSectionHeader": "Datos", + "WinoAccount_Management_AccountActionsSectionHeader": "Accións da conta", + "WinoAccount_Management_AccountCardTitle": "Conta", + "WinoAccount_Management_AccountCardDescription": "O teu enderezo de correo da Conta Wino e o estado actual da conta", + "WinoAccount_Management_AiPackCardTitle": "Pacote IA", + "WinoAccount_Management_AiPackCardDescription": "Comprobe se o Pacote IA de Wino está activo e cantas utilizacións quedan", + "WinoAccount_Management_AiPackActive": "O Pacote IA está activo", + "WinoAccount_Management_AiPackInactive": "O Pacote IA non está activo", + "WinoAccount_Management_AiPackUsage": "{0} de {1} usos consumidos. Quedan {2}.", + "WinoAccount_Management_AiPackBillingPeriod": "Período de facturación: {0:d} - {1:d}", + "WinoAccount_Management_AiPackUnknownUsage": "Os detalles de uso aínda non están dispoñibles", + "WinoAccount_Management_AiPackBuyDescription": "Compre o Pacote IA de Wino para traducir, reescribir e resumir correos con IA", + "WinoAccount_Management_AiPackPromoTitle": "Desbloquear Pacote IA", + "WinoAccount_Management_AiPackPromoDescription": "Impulsa o teu fluxo de correo con ferramentas potenciadas por IA. Traducir mensaxes a máis de 50 idiomas, reescribir para maior claridade e tonalidade, e obter resumos instantáneos de fíos longos", + "WinoAccount_Management_AiPackPromoPrice": "$4.99 / mes", + "WinoAccount_Management_AiPackPromoRequests": "1,000 créditos", + "WinoAccount_Management_AiPackGetButton": "Obter Pacote IA", + "WinoAddOn_AI_PACK_Name": "Pacote IA de Wino", + "WinoAddOn_AI_PACK_Description": "Ferramentas alimentadas por IA para traducir, reescribir e resumir accións en Wino Mail", + "WinoAddOn_AI_PACK_Keywords": "IA, traducir, reescribir, resumir, produtividade", + "WinoAddOn_UNLIMITED_ACCOUNTS_Name": "Contas ilimitadas", + "WinoAddOn_UNLIMITED_ACCOUNTS_Description": "Quita o límite de contas e engade as contas de correo que precises", + "WinoAddOn_UNLIMITED_ACCOUNTS_Keywords": "contas, ilimitadas, premium, complemento", + "WinoAccount_Management_PurchaseRequiresSignIn": "Inicia sesión coa túa Conta Wino para completar esta compra", + "WinoAccount_Management_PurchaseStartFailed": "Wino non puido completar esta compra da Microsoft Store", + "WinoAccount_Management_StoreSyncFailed": "A compra finalizou, pero Wino non puido actualizar os beneficios da túa conta aínda. Por favor, tenta de novo nun momento", + "WinoAccount_Management_AiPackSubscriptionActive": "A túa subscrición está activa", + "WinoAccount_Management_AiPackRenews": "Renova {0:d}", + "WinoAccount_Management_AiPackRequestsUsed": "Créditos usados este mes", + "WinoAccount_Management_AiPackResets": "Reinicios {0:d}", + "WinoAccount_Management_AiPackUsageLoadFailed": "Tivemos problemas ao cargar o saldo de uso de IA", + "WinoAccount_Management_AiPackFeatureTranslate": "Traducir", + "WinoAccount_Management_AiPackFeatureRewrite": "Reescribir", + "WinoAccount_Management_AiPackFeatureSummarize": "Resumir", + "WinoAccount_Management_AddOnLoadFailed": "Tivemos problemas ao cargar este complemento", + "WinoAccount_Management_SyncPreferencesTitle": "Sincronizar Preferencias e Contas", + "WinoAccount_Management_SyncPreferencesDescription": "Importa ou exporta as túas preferencias de Wino e os detalles da caixa de correo entre dispositivos. Contrasinais, tokens e outra información sensible nunca se sincronizan", + "WinoAccount_Management_SignOutTitle": "Pechar sesión", + "WinoAccount_Management_SignOutDescription": "Pecha a sesión da túa conta neste dispositivo", + "WinoAccount_Management_StatusLabel": "Estado: {0}", + "WinoAccount_Management_NoRemoteSettings": "Ainda non hai datos sincronizados almacenados para esta conta", + "WinoAccount_Management_ExportSucceeded": "Os datos de Wino seleccionados exportáronse con éxito", + "WinoAccount_Management_ExportPreferencesSucceeded": "As túas preferencias exportáronse para a túa Conta Wino", + "WinoAccount_Management_ExportAccountsSucceeded": "Exportados {0} detalles de conta á túa Conta Wino", + "WinoAccount_Management_ImportSucceeded": "Datos sincronizados importados desde a túa Conta Wino", + "WinoAccount_Management_ImportPreferencesSucceeded": "Aplicadas {0} preferencias sincronizadas", + "WinoAccount_Management_ImportAccountsSucceeded": "Importáronse {0} contas.", + "WinoAccount_Management_ImportDuplicateAccountsSkipped": "Ignoráronse {0} contas que xa existen neste dispositivo.", + "WinoAccount_Management_ImportPartial": "Aplicáronse {0} preferencias sincronizadas. {1} preferencias non se puideron restaurar.", + "WinoAccount_Management_ImportReloginReminder": "Contrasinais, tokens e outra información sensíbel non foron importados. Inicie sesión de novo para cada conta neste dispositivo antes de usala.", + "WinoAccount_Management_SerializeFailed": "Wino non conseguiu serializar as túas preferencias actuais.", + "WinoAccount_Management_EmptyExport": "Non hai valores de preferencias para exportar.", + "WinoAccount_Management_ImportEmpty": "O paquete de datos sincronizados non contén nada novo para restaurar.", + "WinoAccount_Management_ExportDialog_Title": "Exportar á túa Conta Wino", + "WinoAccount_Management_ExportDialog_Description": "Escolle o que desexes sincronizar coa túa Conta Wino", + "WinoAccount_Management_ExportDialog_IncludePreferences": "Preferencias", + "WinoAccount_Management_ExportDialog_IncludeAccounts": "Contas", + "WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Contrasinais, tokens e outra información sensible non se sincronizan.", + "WinoAccount_Management_ExportDialog_AccountsRelogin": "As contas importadas noutro PC seguirán a necesitar que inicies sesión de novo antes de poder usalas.", + "WinoAccount_Management_ExportDialog_InProgress": "Exportando os datos de Wino seleccionados", + "WinoAccount_Management_LoadFailed": "Wino non conseguiu carregar a información máis recente da Conta Wino.", + "WinoAccount_Management_ActionFailed": "A solicitude da Conta Wino non puido completarse.", + "WinoAccount_SettingsSection_Title": "Conta Wino", + "WinoAccount_SettingsSection_Description": "Crea ou inicia sesión nunha Conta Wino usando o teu servizo de autentificación local.", + "WinoAccount_RegisterButton_Title": "Rexistrar conta", + "WinoAccount_RegisterButton_Description": "Crea unha Conta Wino cun correo electrónico e contrasinal.", + "WinoAccount_RegisterButton_Action": "Abrir rexistro", + "WinoAccount_LoginButton_Title": "Iniciar sesión", + "WinoAccount_LoginButton_Description": "Inicia sesión nunha Conta Wino existente cun correo electrónico e contrasinal.", + "WinoAccount_LoginButton_Action": "Abrir sesión", + "WinoAccount_SignOutButton_Title": "Cerrar sesión", + "WinoAccount_SignOutButton_Description": "Eliminar a sesión da Conta Wino almacenada localmente.", + "WinoAccount_SignOutButton_Action": "Cerrar sesión", + "WinoAccount_RegisterDialog_Title": "Crear Conta Wino", + "WinoAccount_RegisterDialog_Description": "Crea unha Conta Wino para manter a túa experiencia Wino sincronizada e desbloquear complementos baseados na conta.", + "WinoAccount_RegisterDialog_HeroTitle": "Crea a túa Conta Wino", + "WinoAccount_RegisterDialog_BenefitsTitle": "Por que crear unha?", + "WinoAccount_RegisterDialog_BenefitSyncTitle": "Importa e exporta configuración entre dispositivos", + "WinoAccount_RegisterDialog_BenefitSyncDescription": "Mova as túas preferencias de Wino entre dispositivos sen ter que volver a configurar todo desde cero.", + "WinoAccount_RegisterDialog_BenefitAiTitle": "Accede a complementos exclusivos como Wino AI Pack (pagado).", + "WinoAccount_RegisterDialog_BenefitAiDescription": "Utiliza unha conta para desbloquear as características premium de Wino conforme estean dispoñibles.", + "WinoAccount_RegisterDialog_DifferenceTitle": "A Conta Wino é independente das túas contas de correo.", + "WinoAccount_RegisterDialog_DifferenceDescription": "As túas contas de Outlook, Gmail, IMAP ou outras contas de correo permanecen exactamente como están. Unha Conta Wino só xestiona características específicas de Wino e complementos baseados na conta.", + "WinoAccount_RegisterDialog_PrimaryButton": "Rexistrar", + "WinoAccount_RegisterDialog_PrivacyTitle": "Privacidade e procesamento da API", + "WinoAccount_RegisterDialog_PrivacyDescription": "Complementos opcionais, como Wino AI Pack, poden enviar o contido HTML seleccionado de correo electrónico ao servizo Wino API só cando uses esas funcións.", + "WinoAccount_RegisterDialog_PrivacyLinkText": "Ler a política de privacidade", + "WinoAccount_RegisterDialog_PrivacyCheckbox": "Acepto a política de privacidade", + "WinoAccount_LoginDialog_Title": "Inicia sesión na Conta Wino", + "WinoAccount_LoginDialog_Description": "Inicia sesión na túa Conta Wino para sincronizar a túa configuración de Wino e acceder ás características baseadas na conta.", + "WinoAccount_LoginDialog_HeroTitle": "Benvido de novo", + "WinoAccount_LoginDialog_BenefitsTitle": "O que che proporciona iniciar sesión", + "WinoAccount_LoginDialog_BenefitsDescription": "Utiliza a túa Conta Wino para seguir a sincronización das configuracións entre dispositivos e acceder a complementos pagados como Wino AI Pack.", + "WinoAccount_LoginDialog_DifferenceTitle": "Este inicio de sesión non é o da túa caixa de correo", + "WinoAccount_LoginDialog_DifferenceDescription": "Ao iniciar sesión aquí, non engádese nin substitúense as túas contas de Outlook, Gmail, IMAP ou outras no Wino. Só inicia sesión nos servizos específicos de Wino.", + "WinoAccount_LoginDialog_ForgotPasswordLink": "¿Esqueciches o contrasinal?", + "WinoAccount_EmailLabel": "Correo electrónico", + "WinoAccount_EmailPlaceholder": "name@example.com", + "WinoAccount_PasswordLabel": "Contrasinal", + "WinoAccount_ConfirmPasswordLabel": "Confirmar contrasinal", + "WinoAccount_ForgotPasswordDialog_Title": "Restablece o teu contrasinal", + "WinoAccount_ForgotPasswordDialog_PrimaryButton": "Enviar correo de restablecemento", + "WinoAccount_ForgotPasswordDialog_BackToSignIn": "Volver a iniciar sesión", + "WinoAccount_ForgotPasswordDialog_Description": "Introduce o correo da túa Conta Wino e enviaremos unha ligazón de restablecemento de contrasinal se a dirección está rexistrada.", + "WinoAccount_Validation_EmailRequired": "O correo electrónico é obrigatorio.", + "WinoAccount_Validation_PasswordRequired": "O contrasinal é obrigatorio.", + "WinoAccount_Validation_PasswordMismatch": "As contrasinais non coinciden.", + "WinoAccount_Validation_PrivacyConsentRequired": "Debe aceptar a política de privacidade antes de crear unha Conta Wino.", + "WinoAccount_Error_InvalidCredentials": "O enderezo de correo electrónico ou o contrasinal son incorrectos.", + "WinoAccount_Error_AccountLocked": "Esta conta está bloqueada temporalmente.", + "WinoAccount_Error_AccountBanned": "Esta conta foi banida.", + "WinoAccount_Error_AccountSuspended": "Esta conta foi suspendida.", + "WinoAccount_Error_EmailNotConfirmed": "Por favor, confirma o teu enderezo de correo antes de iniciar sesión.", + "WinoAccount_Error_EmailConfirmationRequired": "Por favor, confirma o teu enderezo de correo antes de iniciar sesión.", + "WinoAccount_Error_EmailConfirmationResendNotAvailable": "Aínda non está dispoñible un novo correo de confirmación.", + "WinoAccount_Error_EmailConfirmationResendInvalid": "Esta solicitude de confirmación xa non é válida. Por favor, inténtao iniciar sesión de novo.", + "WinoAccount_Error_EmailNotRegistered": "Este enderezo de correo non está rexistrado.", + "WinoAccount_Error_RefreshTokenInvalid": "A túa sesión xa non é válida. Por favor, inicia sesión de novo.", + "WinoAccount_Error_EmailAlreadyRegistered": "Este enderezo de correo xa está rexistrado.", + "WinoAccount_Error_ExternalLoginEmailRequired": "É necesario un enderezo de correo para completar o inicio de sesión externo.", + "WinoAccount_Error_ExternalLoginInvalid": "A solicitude de inicio de sesión externo é inválida.", + "WinoAccount_Error_ExternalAuthStateInvalid": "O estado do inicio de sesión externo é inválido ou expirou.", + "WinoAccount_Error_ExternalAuthCodeInvalid": "O código de inicio de sesión externo é inválido ou expirou.", + "WinoAccount_Error_AiPackRequired": "É necesaria unha suscrición activa de Wino AI Pack para esta acción.", + "WinoAccount_Error_AiQuotaExceeded": "O límite de uso de Wino AI Pack foi alcanzado para o actual período de facturación.", + "WinoAccount_Error_AiHtmlEmpty": "Non hai contido de correo para procesar.", + "WinoAccount_Error_AiHtmlTooLarge": "Este correo é demasiado grande para ser procesado polo Wino AI.", + "WinoAccount_Error_AiUnsupportedLanguage": "Ese idioma non é compatible. Proba cun código de cultura válido como en-US ou tr-TR.", + "WinoAccount_Error_Forbidden": "Non ten permiso para realizar esta acción.", + "WinoAccount_Error_ValidationFailed": "A solicitude é inválida. Por favor, revisa os valores introducidos.", + "WinoAccount_RegisterSuccessMessage": "O rexistro da Conta Wino completouse para {0}.", + "WinoAccount_LoginSuccessMessage": "Iniciaste sesión na Conta Wino como {0}.", + "WinoAccount_EmailConfirmationSentDialog_Title": "Confirma o teu enderezo de correo", + "WinoAccount_EmailConfirmationSentDialog_Message": "Enviamos unha confirmación por correo a {0}. Por favor confírmalo e inténtao iniciar sesión de novo.", + "WinoAccount_EmailConfirmationPendingDialog_Title": "Se require confirmación de correo", + "WinoAccount_EmailConfirmationPendingDialog_Message": "Seguimos a aguardar a que confirmes {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ResendButton": "Reenviar correo de confirmación", + "WinoAccount_EmailConfirmationPendingDialog_Countdown": "Podes reenviar o correo de confirmación en {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ReadyToResend": "Podes reenviar o correo de confirmación agora.", + "WinoAccount_EmailConfirmationResentDialog_Title": "Correo de confirmación reenviado", + "WinoAccount_EmailConfirmationResentDialog_Message": "Enviamos unha confirmación por correo a {0}. Por favor confírmalo e inténtao iniciar sesión de novo.", + "WinoAccount_ForgotPasswordDialog_SuccessTitle": "Correo de restablecemento de contrasinal enviado", + "WinoAccount_ForgotPasswordDialog_SuccessMessage": "Enviámosche un correo de restablecemento de contrasinal a {0}. Abre ese correo para escoller unha nova contrasinal.", + "WinoAccount_ChangePassword_Title": "Cambiar contrasinal", + "WinoAccount_ChangePassword_Description": "Enviar un correo de restablecemento de contrasinal a esta Conta Wino.", + "WinoAccount_ChangePassword_Action": "Enviar correo de restablecemento.", + "WinoAccount_ChangePassword_ConfirmationMessage": "¿Queres que Wino envíe un correo de restablecemento da contrasinal a {0}?", + "WinoAccount_SignOut_SuccessMessage": "Saiu da Conta Wino {0}.", + "WinoAccount_SignOut_NoAccountMessage": "Non hai unha Conta Wino activa para pechar sesión.", + "WinoAccount_Titlebar_SignedOutTitle": "Conta Wino", + "WinoAccount_Titlebar_SignedOutDescription": "Inicia sesión ou crea unha Conta Wino para xestionar a túa sesión de Wino.", + "WinoAccount_Titlebar_SignedInStatus": "Estado: {0}", + "WelcomeWizard_Step2Title": "Engadir conta", + "WelcomeWizard_Step3Title": "Rematar a configuración", + "ProviderSelection_Title": "Elixe o teu provedor de correo", + "ProviderSelection_Subtitle": "Selecciona un provedor a continuación para engadir a túa conta de correo a Wino Mail.", + "ProviderSelection_AccountNameHeader": "Nome da conta", + "ProviderSelection_AccountNamePlaceholder": "p. ex. Persoal, Laboral", + "ProviderSelection_DisplayNameHeader": "Nome para amosar", + "ProviderSelection_DisplayNamePlaceholder": "p. ex. John Doe", + "ProviderSelection_EmailHeader": "Enderezo de correo electrónico", + "ProviderSelection_EmailPlaceholder": "p. ex. johndoe@example.com", + "ProviderSelection_AppPasswordHeader": "Contraseña específica da aplicación", + "ProviderSelection_AppPasswordHelp": "Como podo obter unha contrasinal específica da aplicación?", + "ProviderSelection_CalendarModeHeader": "Integración do calendario", + "ProviderSelection_CalendarMode_DisabledTitle": "Desactivado", + "ProviderSelection_CalendarMode_DisabledDescription": "Sen integración de calendario", + "ProviderSelection_CalendarMode_CalDavTitle": "Sincronización CalDAV", + "ProviderSelection_CalendarMode_CalDavDescription_Apple": "Os teus eventos de calendario están sincronizados cos servidores de Apple entre os teus dispositivos.", + "ProviderSelection_CalendarMode_CalDavDescription_Yahoo": "Os teus eventos de calendario están sincronizados cos servidores de Yahoo entre os teus dispositivos.", + "ProviderSelection_CalendarMode_LocalTitle": "Calendario local", + "ProviderSelection_CalendarMode_LocalDescription": "Os teus eventos só se gardan no teu ordenador. Non hai conexión co servidor.", + "ProviderSelection_ClearColor": "Cor limpa", + "ProviderSelection_ContinueButton": "Continuar", + "ProviderSelection_SpecialImap_Subtitle": "Introduce as credenciais da túa conta para conectarte.", + "AccountSetup_Title": "Configurando a túa conta", + "AccountSetup_Step_Authenticating": "Autenticando con {0}", + "AccountSetup_Step_TestingMailAuth": "Probas de autenticación de correo", + "AccountSetup_Step_SyncingFolders": "Sincronizando metadatos de carpetas", + "AccountSetup_Step_FetchingProfile": "Obtendo información do perfil", + "AccountSetup_Step_DiscoveringCalDav": "Descubrindo as configuración de CalDAV", + "AccountSetup_Step_TestingCalendarAuth": "Probas de autenticación do calendario", + "AccountSetup_Step_SavingAccount": "Gardar a información da conta", + "AccountSetup_Step_FetchingCalendarMetadata": "Obtendo metadatos do calendario", + "AccountSetup_Step_SyncingAliases": "Sincronizando alias", + "AccountSetup_Step_Finalizing": "Rematando a configuración", + "AccountSetup_FailureMessage": "A configuración fallou. Volva atrás para corrixir os seus axustes, ou inténtao de novo máis tarde.", + "AccountSetup_SuccessMessage": "A túa conta está configurada con éxito!", + "AccountSetup_GoBackButton": "Volver", + "AccountSetup_TryAgainButton": "Proba de novo", + "ImapCalDavSettings_AutoDiscoveryFailed": "Falhou o autodescubrimento. Introduza axustes manualmente na pestana Avanzadas." } - - diff --git a/Wino.Core.Domain/Translations/id_ID/resources.json b/Wino.Core.Domain/Translations/id_ID/resources.json index 36e07978..a6e79adc 100644 --- a/Wino.Core.Domain/Translations/id_ID/resources.json +++ b/Wino.Core.Domain/Translations/id_ID/resources.json @@ -4,10 +4,11 @@ "AccountAlias_Column_Verified": "Terverifikasi", "AccountAlias_Disclaimer_FirstLine": "Wino hanya dapat mengimpor alias untuk akun Gmail Anda.", "AccountAlias_Disclaimer_SecondLine": "Jika Anda ingin menggunakan alias untuk akun Outlook atau IMAP, mohon tambahkan sendiri.", - "AccountCacheReset_Title": "Setel Ulang Tembolok Akun", - "AccountCacheReset_Message": "Akun ini memerlukan penyelarasan ulang keseluruhan untuk dapat lanjut berjalan. Mohon menunggu selagi Wino menyelaraskan pesan Anda...", + "AccountCacheReset_Title": "Account Cache Reset", + "AccountCacheReset_Message": "This account requires full re-sychronization to continue working. Please wait while Wino re-synchronizes your messages...", "AccountContactNameYou": "Anda", "AccountCreationDialog_Completed": "semua selesai", + "AccountCreationDialog_FetchingCalendarMetadata": "Mengambil metadata kalender.", "AccountCreationDialog_FetchingEvents": "Mengambil acara kalender.", "AccountCreationDialog_FetchingProfileInformation": "Mengambil rincian profil.", "AccountCreationDialog_GoogleAuthHelpClipboardText_Row0": "Jika peramban web Anda tidak terbuka secara otomatis:", @@ -17,15 +18,20 @@ "AccountCreationDialog_Initializing": "memulai", "AccountCreationDialog_PreparingFolders": "Kami sedang mendapatkan informasi folder.", "AccountCreationDialog_SigninIn": "Informasi akun sedang disimpan.", + "Purchased": "Dibeli", "AccountEditDialog_Message": "Nama Akun", "AccountEditDialog_Title": "Sunting Akun", "AccountPickerDialog_Title": "Pilih akun", "AccountSettingsDialog_AccountName": "Nama Tampilan Pengirim", "AccountSettingsDialog_AccountNamePlaceholder": "misal: Budi Susilo", - "AccountDetailsPage_Title": "Info akun", - "AccountDetailsPage_Description": "Ubah nama akun di Wino dan atur nama pengirim yang diinkan.", - "AccountDetailsPage_ColorPicker_Title": "Warna akun", - "AccountDetailsPage_ColorPicker_Description": "Berikan warna untuk akun agar mudah ditemukan di daftar.", + "AccountDetailsPage_Title": "Account info", + "AccountDetailsPage_Description": "Change the name of the account in Wino and set desired sender name.", + "AccountDetailsPage_ColorPicker_Title": "Account color", + "AccountDetailsPage_ColorPicker_Description": "Assign a new account color to colorize its symbol in the list.", + "AccountDetailsPage_TabGeneral": "Umum", + "AccountDetailsPage_TabMail": "Surel", + "AccountDetailsPage_TabCalendar": "Kalender", + "AccountDetailsPage_CalendarListDescription": "Pilih kalender untuk mengonfigurasi pengaturannya", "AddHyperlink": "Tambahkan", "AppCloseBackgroundSynchronizationWarningTitle": "Penyelarasan Latar Belakang", "AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Aplikasi tidak diatur untuk berjalan saat Windows dimulai.", @@ -47,8 +53,10 @@ "BasicIMAPSetupDialog_Title": "Akun IMAP", "Busy": "Sibuk", "Buttons_AddAccount": "Tambahkan Akun", + "Buttons_FixAccount": "Perbaiki Akun", "Buttons_AddNewAlias": "Tambahkan Alias Baru", "Buttons_Allow": "Izinkan", + "Buttons_Apply": "Terapkan", "Buttons_ApplyTheme": "Terapkan Tema", "Buttons_Browse": "Telusuri", "Buttons_Cancel": "Batal", @@ -62,6 +70,7 @@ "Buttons_Edit": "Ubah", "Buttons_EnableImageRendering": "Nyalakan", "Buttons_Multiselect": "Pilih Beberapa", + "Buttons_Manage": "Kelola", "Buttons_No": "Tidak", "Buttons_Open": "Buka", "Buttons_Purchase": "Beli", @@ -70,15 +79,134 @@ "Buttons_Save": "Simpan", "Buttons_SaveConfiguration": "Simpan Pengaturan", "Buttons_Send": "Kirim", + "Buttons_SendToServer": "Kirim ke server", "Buttons_Share": "Bagikan", "Buttons_SignIn": "Masuk", "Buttons_Sync": "Selaraskan", "Buttons_SyncAliases": "Selaraskan Alias", "Buttons_TryAgain": "Coba Lagi", "Buttons_Yes": "Ya", + "Sync_SynchronizingFolder": "Sinkronisasi {0} {1}%", + "Sync_DownloadedMessages": "{0} pesan telah diunduh dari {1}", + "SyncAction_Archiving": "Mengarsipkan {0} surel", + "SyncAction_ClearingFlag": "Menghapus penanda pada {0} surel", + "SyncAction_CreatingDraft": "Membuat draf", + "SyncAction_CreatingEvent": "Membuat acara", + "SyncAction_Deleting": "Menghapus {0} surel", + "SyncAction_EmptyingFolder": "Mengosongkan folder", + "SyncAction_MarkingAsRead": "Menandai {0} surel sebagai telah dibaca", + "SyncAction_MarkingAsUnread": "Menandai {0} surel sebagai belum dibaca", + "SyncAction_MarkingFolderAsRead": "Menandai folder sebagai telah dibaca", + "SyncAction_Moving": "Memindahkan {0} surel", + "SyncAction_MovingToFocused": "Memindahkan {0} surel ke Fokus", + "SyncAction_RenamingFolder": "Mengganti nama folder", + "SyncAction_SendingMail": "Mengirim surel", + "SyncAction_SettingFlag": "Menandai {0} surel dengan bendera", + "SyncAction_SynchronizingAccount": "Menyinkronkan {0}", + "SyncAction_SynchronizingAccounts": "Menyinkronkan {0} akun", + "SyncAction_SynchronizingCalendarData": "Menyinkronkan data kalender", + "SyncAction_SynchronizingCalendarEvents": "Menyinkronkan acara kalender", + "SyncAction_SynchronizingCalendarMetadata": "Menyinkronkan metadata kalender", + "SyncAction_Unarchiving": "Mengembalikan {0} surel dari arsip", "CalendarAllDayEventSummary": "acara sepanjang hari", "CalendarDisplayOptions_Color": "Warna", "CalendarDisplayOptions_Expand": "Perluas", + "CalendarEventResponse_Accept": "Terima", + "CalendarEventResponse_AcceptedResponse": "Diterima", + "CalendarEventResponse_Decline": "Tolak", + "CalendarEventResponse_DeclinedResponse": "Ditolak", + "CalendarEventResponse_NotResponded": "Belum menanggapi", + "CalendarEventResponse_Tentative": "Tentatif", + "CalendarEventResponse_TentativeResponse": "Tentatif", + "CalendarEventRsvpPanel_Accept": "Terima", + "CalendarEventRsvpPanel_AddMessage": "Tambahkan pesan ke tanggapan Anda... (opsional)", + "CalendarEventRsvpPanel_Decline": "Tolak", + "CalendarEventRsvpPanel_Message": "Pesan", + "CalendarEventRsvpPanel_SendReplyMessage": "Kirim balasan pesan", + "CalendarEventRsvpPanel_Tentative": "Tentatif", + "CalendarEventRsvpPanel_Title": "Pilihan Tanggapan", + "CalendarAttendeeStatus_Accepted": "Diterima", + "CalendarAttendeeStatus_Declined": "Ditolak", + "CalendarAttendeeStatus_NeedsAction": "Perlu tindakan", + "CalendarAttendeeStatus_Tentative": "Tentatif", + "CalendarEventDetails_Attachments": "Lampiran", + "CalendarEventCompose_AddAttachment": "Tambahkan lampiran", + "CalendarEventCompose_AllDay": "Sepanjang hari", + "CalendarEventCompose_AttachmentsNotSupportedForCalDav": "Lampiran tidak didukung untuk kalender CalDAV.", + "CalendarEventCompose_EndDate": "Tanggal selesai", + "CalendarEventCompose_EndTime": "Waktu selesai", + "CalendarEventCompose_Every": "setiap", + "CalendarEventCompose_ForWeekdays": "untuk", + "CalendarEventCompose_FrequencyDay": "hari", + "CalendarEventCompose_FrequencyDayPlural": "hari", + "CalendarEventCompose_FrequencyMonth": "bulan", + "CalendarEventCompose_FrequencyMonthPlural": "bulan", + "CalendarEventCompose_FrequencyWeek": "minggu", + "CalendarEventCompose_FrequencyWeekPlural": "minggu", + "CalendarEventCompose_FrequencyYear": "tahun", + "CalendarEventCompose_FrequencyYearPlural": "tahun", + "CalendarEventCompose_Location": "Lokasi", + "CalendarEventCompose_LocationPlaceholder": "Tambahkan lokasi", + "CalendarEventCompose_NewEventButton": "Acara Baru", + "CalendarEventCompose_DefaultCalendarHint": "Anda dapat memilih kalender default untuk acara baru di Pengaturan Kalender.", + "CalendarEventCompose_DefaultCalendarSettingsLink": "Buka Pengaturan Kalender", + "CalendarEventCompose_NoCalendarsMessage": "Belum ada kalender yang tersedia untuk membuat acara.", + "CalendarEventCompose_NoCalendarsTitle": "Tidak ada kalender tersedia", + "CalendarEventCompose_NoEndDate": "Tidak ada tanggal selesai", + "CalendarEventCompose_Notes": "Catatan", + "CalendarEventCompose_PickCalendarTitle": "Pilih kalender", + "CalendarEventCompose_Recurring": "Berulang", + "CalendarEventCompose_RecurringSummary": "Terjadi setiap {0} {1}{2} {3} berlaku {4}{5}", + "CalendarEventCompose_RecurringSummarySmart": "Terjadi {0}{1} {2} berlaku {3}{4}", + "CalendarEventCompose_RepeatEvery": "Ulangi setiap", + "CalendarEventCompose_SelectCalendar": "Pilih kalender", + "CalendarEventCompose_SingleOccurrenceSummary": "Terjadi pada {0} {1}", + "CalendarEventCompose_StartDate": "Tanggal mulai", + "CalendarEventCompose_StartTime": "Waktu mulai", + "CalendarEventCompose_TimeRangeSummary": "dari {0} hingga {1}", + "CalendarEventCompose_Title": "Judul acara", + "CalendarEventCompose_TitlePlaceholder": "Tambahkan judul", + "CalendarEventCompose_Until": "sampai", + "CalendarEventCompose_UntilSummary": " hingga {0}", + "CalendarEventCompose_ValidationInvalidAllDayRange": "Tanggal akhir untuk acara sepanjang hari harus setelah tanggal mulai.", + "CalendarEventCompose_ValidationInvalidAttendee": "Salah satu atau lebih peserta memiliki alamat email yang tidak valid.", + "CalendarEventCompose_ValidationInvalidRecurrenceEnd": "Tanggal akhir pengulangan harus sama dengan atau setelah tanggal mulai acara.", + "CalendarEventCompose_ValidationInvalidTimeRange": "Waktu selesai harus lebih lama daripada waktu mulai.", + "CalendarEventCompose_ValidationMissingAttachment": "Satu atau lebih lampiran tidak lagi tersedia: {0}", + "CalendarEventCompose_ValidationMissingCalendar": "Pilih kalender sebelum membuat acara.", + "CalendarEventCompose_ValidationMissingTitle": "Masukkan judul acara sebelum membuat acara.", + "CalendarEventCompose_ValidationTitle": "Validasi acara gagal", + "CalendarEventCompose_WeekdaySummary": " pada {0}", + "CalendarEventCompose_Weekday_Friday": "F", + "CalendarEventCompose_Weekday_Monday": "M", + "CalendarEventCompose_Weekday_Saturday": "Sab", + "CalendarEventCompose_Weekday_Sunday": "Min", + "CalendarEventCompose_Weekday_Thursday": "Kam", + "CalendarEventCompose_Weekday_Tuesday": "Sel", + "CalendarEventCompose_Weekday_Wednesday": "Rab", + "CalendarEventDetails_Details": "Rincian", + "CalendarEventDetails_EditSeries": "Ubah Seri", + "CalendarEventDetails_Editing": "Menyunting", + "CalendarEventDetails_InviteSomeone": "Undang orang lain", + "CalendarEventDetails_JoinOnline": "Gabung Online", + "CalendarEventDetails_Organizer": "Penyelenggara", + "CalendarEventDetails_People": "Orang", + "CalendarEventDetails_ReadOnlyEvent": "Acara hanya baca", + "CalendarEventDetails_Reminder": "Pengingat", + "CalendarReminder_StartedHoursAgo": "Dimulai {0} jam yang lalu", + "CalendarReminder_StartedMinutesAgo": "Dimulai {0} menit yang lalu", + "CalendarReminder_StartedNow": "Baru saja dimulai", + "CalendarReminder_StartingNow": "Mulai sekarang", + "CalendarReminder_StartsInHours": "Dimulai dalam {0} jam", + "CalendarReminder_StartsInMinutes": "Dimulai dalam {0} menit", + "CalendarReminder_SnoozeAction": "Tunda", + "CalendarReminder_SnoozeMinutesOption": "{0} menit", + "CalendarEventDetails_ShowAs": "Tampilkan sebagai", + "CalendarShowAs_Free": "Tersedia", + "CalendarShowAs_Tentative": "Sementara", + "CalendarShowAs_Busy": "Sibuk", + "CalendarShowAs_OutOfOffice": "Di luar kantor", + "CalendarShowAs_WorkingElsewhere": "Bekerja di tempat lain", "CalendarItem_DetailsPopup_JoinOnline": "Bergabung secara daring", "CalendarItem_DetailsPopup_ViewEventButton": "Lihat acara", "CalendarItem_DetailsPopup_ViewSeriesButton": "Lihat seri", @@ -88,6 +216,9 @@ "ClipboardTextCopied_Message": "{0} disalin ke papan klip.", "ClipboardTextCopied_Title": "Disalin", "ClipboardTextCopyFailed_Message": "Gagal menyalin {0} ke papan klip.", + "ContactInfoBar_ErrorTitle": "Gagal memuat informasi kontak", + "ContactInfoBar_SuccessTitle": "Informasi kontak dimuat", + "ContactInfoBar_WarningTitle": "Informasi kontak mungkin tidak lengkap", "ComingSoon": "Segera datang...", "ComposerAttachmentsDragDropAttach_Message": "Lampirkan", "ComposerAttachmentsDropZone_Message": "Jatuhkan berkas Anda di sini", @@ -129,6 +260,10 @@ "DialogMessage_CreateLinkedAccountTitle": "Nama Tautan Akun", "DialogMessage_DeleteAccountConfirmationMessage": "Hapus {0}?", "DialogMessage_DeleteAccountConfirmationTitle": "Semua data terkait akun ini akan dihapus secara permanen dari penyimpanan.", + "DialogMessage_DeleteEmailTemplateConfirmationMessage": "Hapus template \"{0}\"?", + "DialogMessage_DeleteEmailTemplateConfirmationTitle": "Hapus Email Template", + "DialogMessage_DeleteRecurringSeriesMessage": "Ini akan menghapus semua acara dalam seri ini. Anda ingin melanjutkan?", + "DialogMessage_DeleteRecurringSeriesTitle": "Hapus Seri Berulang", "DialogMessage_DiscardDraftConfirmationMessage": "Konsep ini akan dibuang. Apakah Anda ingin melanjutkan?", "DialogMessage_DiscardDraftConfirmationTitle": "Buang Konsep", "DialogMessage_EmptySubjectConfirmation": "Tidak Ada Perihal", @@ -172,11 +307,18 @@ "ElementTheme_Light": "Mode terang", "Emoji": "Emoji", "Error_FailedToSetupSystemFolders_Title": "Gagal menyiapkan folder sistem", + "Exception_AccountNeedsAttention_Title": "Akun membutuhkan perhatian", + "Exception_AccountNeedsAttention_Message": "'{0}' memerlukan perhatian Anda untuk melanjutkan.", + "Exception_WebView2RuntimeMissing_Message": "Wino Mail tidak dapat menemukan runtime Microsoft Edge WebView2. Harap pasang atau perbaiki runtime untuk menampilkan isi pesan dengan benar.", + "Exception_WebView2RuntimeMissing_Title": "Runtime WebView2 diperlukan", "Exception_AuthenticationCanceled": "Otentikasi dibatalkan", "Exception_CustomThemeExists": "Tema ini sudah ada.", "Exception_CustomThemeMissingName": "Anda harus memberikan nama.", "Exception_CustomThemeMissingWallpaper": "Anda harus memberikan gambar latar Anda sendiri.", "Exception_FailedToSynchronizeAliases": "Gagal menyelaraskan alias", + "Exception_FailedToSynchronizeCalendarData": "Gagal menyinkronkan data kalender", + "Exception_FailedToSynchronizeCalendarEvents": "Gagal menyinkronkan peristiwa kalender", + "Exception_FailedToSynchronizeCalendarMetadata": "Gagal menyinkronkan detail kalender", "Exception_FailedToSynchronizeFolders": "Gagal menyelaraskan folder", "Exception_FailedToSynchronizeProfileInformation": "Gagal menyelaraskan informasi profil", "Exception_GoogleAuthCallbackNull": "Alamat panggilan menghasilkan balasan kosong.", @@ -188,7 +330,7 @@ "Exception_ImapClientPoolFailed": "Gagal IMAP Client Pool.", "Exception_InboxNotAvailable": "Gagal menyiapkan folder akun.", "Exception_InvalidSystemFolderConfiguration": "Pengaturan folder sistem tidak valid. Mohon periksa pengaturan dan coba lagi.", - "Exception_InvalidMultiAccountMoveTarget": "Anda tidak dapat memindahkan item yang berada di akun yang berbeda di akun tertaut.", + "Exception_InvalidMultiAccountMoveTarget": "You can't move multiple items that belong to different accounts in linked account.", "Exception_MailProcessing": "Surel ini sedang diproses. Harap coba lagi nanti.", "Exception_MissingAlias": "Alias utama tidak ada untuk akun ini. Gagal membuat draf.", "Exception_NullAssignedAccount": "Akun yang diberikan kosong", @@ -221,33 +363,59 @@ "GeneralTitle_Error": "Galat", "GeneralTitle_Info": "Informasi", "GeneralTitle_Warning": "Peringatan", - "GmailServiceDisabled_Title": "Galat Gmail", - "GmailServiceDisabled_Message": "Akun Google Workspace Anda sepertinya tidak mengizinkan Gmail. Mohon hubungi administrator akun Google Workspace Anda untuk mengizinkan Gmail.", - "GmailArchiveFolderNameOverride": "Asip", + "GmailServiceDisabled_Title": "Gmail Error", + "GmailServiceDisabled_Message": "Your Google Workspace account seems to be disabled for Gmail service. Please contact your administrator to enable Gmail service for your account.", + "GmailArchiveFolderNameOverride": "Archive", "HoverActionOption_Archive": "Arsipkan", "HoverActionOption_Delete": "Hapus", "HoverActionOption_MoveJunk": "Pindahkan ke Sampah", "HoverActionOption_ToggleFlag": "Tandai / Hapus Tanda", "HoverActionOption_ToggleRead": "Baca / Belum Dibaca", + "KeyboardShortcuts_FailedToReset": "Gagal mereset pintasan keyboard.", + "KeyboardShortcuts_FailedToUpdate": "Gagal memperbarui pintasan keyboard", + "KeyboardShortcuts_MailoperationAction": "Aksi", + "KeyboardShortcuts_Action": "Aksi", + "KeyboardShortcuts_FailedToLoad": "Gagal memuat pintasan keyboard.", + "KeyboardShortcuts_EnterKeyForShortcut": "Harap masukkan tombol untuk pintasan.", + "KeyboardShortcuts_SelectOperationForShortcut": "Pilih sebuah aksi untuk pintasan.", + "KeyboardShortcuts_EnterKey": "Harap masukkan tombol untuk pintasan.", + "KeyboardShortcuts_SelectOperation": "Pilih sebuah aksi untuk pintasan.", + "KeyboardShortcuts_ShortcutInUse": "Pintasan ini sudah digunakan oleh pintasan lain.", + "KeyboardShortcuts_FailedToSave": "Gagal menyimpan pintasan.", + "KeyboardShortcuts_FailedToDelete": "Gagal menghapus pintasan.", + "KeyboardShortcuts_PageDescription": "Mengatur pintasan keyboard untuk operasi email cepat. Tekan tombol saat fokus pada bidang input tombol untuk menangkap pintasan.", + "KeyboardShortcuts_Add": "Tambahkan pintasan", + "KeyboardShortcuts_EditTitle": "Ubah Pintasan Keyboard", + "KeyboardShortcuts_ResetToDefaults": "Kembalikan ke Default", + "KeyboardShortcuts_PressKeysHere": "Tekan tombol di sini...", + "KeyboardShortcuts_KeyCombination": "Kombinasi tombol", + "KeyboardShortcuts_FocusArea": "Fokuskan bidang di atas dan tekan kombinasi tombol yang diinginkan", + "KeyboardShortcuts_Modifiers": "Tombol modifier", + "KeyboardShortcuts_Mode": "Mode Aplikasi", + "KeyboardShortcuts_ModeMail": "Email", + "KeyboardShortcuts_ModeCalendar": "Kalender", + "KeyboardShortcuts_ActionToggleReadUnread": "Ganti telah dibaca/belum dibaca", + "KeyboardShortcuts_ActionToggleFlag": "Ganti tanda", + "KeyboardShortcuts_ActionToggleArchive": "Ganti arsipkan/batalkan arsip", "ImageRenderingDisabled": "Pemuatan gambar dimatikan untuk pesan ini.", "ImapAdvancedSetupDialog_AuthenticationMethod": "Metode otentikasi", "ImapAdvancedSetupDialog_ConnectionSecurity": "Keamanan sambungan", - "IMAPAdvancedSetupDialog_ValidationAuthMethodRequired": "Metode otentikasi diperlukan", - "IMAPAdvancedSetupDialog_ValidationConnectionSecurityRequired": "Jenis keamanan sambungan diperlukan", - "IMAPAdvancedSetupDialog_ValidationDisplayNameRequired": "Nama tampilan diperlukan", - "IMAPAdvancedSetupDialog_ValidationEmailInvalid": "Mohon masukkan alamat surel yang benar", - "IMAPAdvancedSetupDialog_ValidationEmailRequired": "Alamat surel diperlukan", - "IMAPAdvancedSetupDialog_ValidationErrorTitle": "Mohon periksa:", - "IMAPAdvancedSetupDialog_ValidationIncomingPortInvalid": "Port masuk harus antara 1-65535", - "IMAPAdvancedSetupDialog_ValidationIncomingPortRequired": "Port peladen masuk diperlukan", - "IMAPAdvancedSetupDialog_ValidationIncomingServerRequired": "Alamat peladen masuk diperlukan", - "IMAPAdvancedSetupDialog_ValidationOutgoingPasswordRequired": "Sandi peladen keluar diperlukan", - "IMAPAdvancedSetupDialog_ValidationOutgoingPortInvalid": "Port keluar harus antara 1-65535", - "IMAPAdvancedSetupDialog_ValidationOutgoingPortRequired": "Port peladen keluar diperlukan", - "IMAPAdvancedSetupDialog_ValidationOutgoingServerRequired": "Alamat peladen keluar diperlukan", - "IMAPAdvancedSetupDialog_ValidationOutgoingUsernameRequired": "Nama pengguna peladen keluar diperlukan", - "IMAPAdvancedSetupDialog_ValidationPasswordRequired": "Sandi diperlukan", - "IMAPAdvancedSetupDialog_ValidationUsernameRequired": "Nama pengguna diperlukan", + "IMAPAdvancedSetupDialog_ValidationAuthMethodRequired": "Authentication method is required", + "IMAPAdvancedSetupDialog_ValidationConnectionSecurityRequired": "Connection security type is required", + "IMAPAdvancedSetupDialog_ValidationDisplayNameRequired": "Display name is required", + "IMAPAdvancedSetupDialog_ValidationEmailInvalid": "Please enter a valid email address", + "IMAPAdvancedSetupDialog_ValidationEmailRequired": "Email address is required", + "IMAPAdvancedSetupDialog_ValidationErrorTitle": "Please check the following:", + "IMAPAdvancedSetupDialog_ValidationIncomingPortInvalid": "Incoming port must be between 1-65535", + "IMAPAdvancedSetupDialog_ValidationIncomingPortRequired": "Incoming server port is required", + "IMAPAdvancedSetupDialog_ValidationIncomingServerRequired": "Incoming server address is required", + "IMAPAdvancedSetupDialog_ValidationOutgoingPasswordRequired": "Outgoing server password is required", + "IMAPAdvancedSetupDialog_ValidationOutgoingPortInvalid": "Outgoing port must be between 1-65535", + "IMAPAdvancedSetupDialog_ValidationOutgoingPortRequired": "Outgoing server port is required", + "IMAPAdvancedSetupDialog_ValidationOutgoingServerRequired": "Outgoing server address is required", + "IMAPAdvancedSetupDialog_ValidationOutgoingUsernameRequired": "Outgoing server username is required", + "IMAPAdvancedSetupDialog_ValidationPasswordRequired": "Password is required", + "IMAPAdvancedSetupDialog_ValidationUsernameRequired": "Username is required", "ImapAuthenticationMethod_Auto": "Otomatis", "ImapAuthenticationMethod_CramMD5": "CRAM-MD5", "ImapAuthenticationMethod_DigestMD5": "DIGEST-MD5", @@ -260,11 +428,11 @@ "ImapConnectionSecurity_SslTls": "SSL/TLS", "ImapConnectionSecurity_StartTls": "STARTTLS", "IMAPSetupDialog_AccountType": "Jenis akun", - "IMAPSetupDialog_ValidationSuccess_Title": "Berhasil", - "IMAPSetupDialog_ValidationSuccess_Message": "Validasi berhasil", - "IMAPSetupDialog_SaveImapSuccess_Title": "Berhasil", - "IMAPSetupDialog_SaveImapSuccess_Message": "Pengaturan IMAP berhasil disimpan.", - "IMAPSetupDialog_ValidationFailed_Title": "Validasi peladen IMAP gagal.", + "IMAPSetupDialog_ValidationSuccess_Title": "Success", + "IMAPSetupDialog_ValidationSuccess_Message": "Validation successful", + "IMAPSetupDialog_SaveImapSuccess_Title": "Success", + "IMAPSetupDialog_SaveImapSuccess_Message": "IMAP settings saved successfuly.", + "IMAPSetupDialog_ValidationFailed_Title": "IMAP Server validation failed.", "IMAPSetupDialog_CertificateAllowanceRequired_Row0": "Peladen ini meminta pertukaran SSL untuk melanjutkan. Mohon pastikan rincian sertifikat di bawah ini.", "IMAPSetupDialog_CertificateAllowanceRequired_Row1": "Izinkan pertukaran untuk melanjutkan mengatur akun Anda.", "IMAPSetupDialog_CertificateDenied": "Pengguna tidak mengotorisasi pertukaran sertifikat.", @@ -279,8 +447,8 @@ "IMAPSetupDialog_DisplayNamePlaceholder": "misal: Budi Susilo", "IMAPSetupDialog_IncomingMailServer": "Peladen email masuk", "IMAPSetupDialog_IncomingMailServerPort": "Port", - "IMAPSetupDialog_IMAPSettings": "Pengaturan Peladen IMAP", - "IMAPSetupDialog_SMTPSettings": "Pengaturan Peladen SMTP", + "IMAPSetupDialog_IMAPSettings": "IMAP Server Settings", + "IMAPSetupDialog_SMTPSettings": "SMTP Server Settings", "IMAPSetupDialog_MailAddress": "Alamat surel", "IMAPSetupDialog_MailAddressPlaceholder": "nama@contoh.com", "IMAPSetupDialog_OutgoingMailServer": "Peladen surel keluar (SMTP)", @@ -295,12 +463,58 @@ "IMAPSetupDialog_Username": "Nama Pengguna", "IMAPSetupDialog_UsernamePlaceholder": "budisusilo, budisusilo@contoh.com, domain/budisusilo", "IMAPSetupDialog_UseSameConfig": "Gunakan nama pengguna dan kata sandi yang sama untuk mengirim surel", + "ImapCalDavSettingsPage_TitleCreate": "Pengaturan IMAP dan Kalender", + "ImapCalDavSettingsPage_TitleEdit": "Ubah Pengaturan IMAP dan Kalender", + "ImapCalDavSettingsPage_Subtitle": "Atur IMAP/SMTP dan sinkronisasi kalender opsional untuk akun ini.", + "ImapCalDavSettingsPage_BasicSectionTitle": "Pengaturan dasar", + "ImapCalDavSettingsPage_BasicSectionDescription": "Masukkan identitas dan kredensial Anda. Wino dapat mencoba mendeteksi pengaturan server secara otomatis.", + "ImapCalDavSettingsPage_BasicTab": "Dasar", + "ImapCalDavSettingsPage_EnableCalendarSupport": "Aktifkan dukungan kalender", + "ImapCalDavSettingsPage_AutoDiscoverButton": "Cari otomatis pengaturan email", + "ImapCalDavSettingsPage_AutoDiscoverySuccessMessage": "Pengaturan email ditemukan dan diterapkan.", + "ImapCalDavSettingsPage_AdvancedSectionTitle": "Konfigurasi lanjutan", + "ImapCalDavSettingsPage_AdvancedSectionDescription": "Masukkan pengaturan server secara manual jika autodiscovery tidak tersedia atau tidak benar.", + "ImapCalDavSettingsPage_AdvancedTab": "Lanjutan", + "ImapCalDavSettingsPage_CalendarSectionTitle": "Pengaturan Kalender", + "ImapCalDavSettingsPage_CalendarSectionDescription": "Pilih bagaimana data kalender seharusnya bekerja untuk akun IMAP ini.", + "ImapCalDavSettingsPage_CalendarModeHeader": "Mode Kalender", + "ImapCalDavSettingsPage_ConnectionSecurityHeader": "Keamanan koneksi", + "ImapCalDavSettingsPage_AuthenticationMethodHeader": "Metode otentikasi", + "ImapCalDavSettingsPage_CalendarModeDisabled": "Dinonaktifkan", + "ImapCalDavSettingsPage_CalendarModeCalDav": "Sinkronisasi CalDAV", + "ImapCalDavSettingsPage_CalendarModeLocalOnly": "Hanya kalender lokal", + "ImapCalDavSettingsPage_CalendarModeDisabledDescription": "Kalender dinonaktifkan untuk akun ini.", + "ImapCalDavSettingsPage_CalendarModeCalDavDescription": "Item kalender disinkronkan dengan server CalDAV Anda.", + "ImapCalDavSettingsPage_CalendarModeLocalOnlyDescription": "Item kalender disimpan hanya di komputer ini dan tidak disinkronkan ke jaringan.", + "ImapCalDavSettingsPage_LocalCalendarLearnMore": "Cara kerja kalender lokal", + "ImapCalDavSettingsPage_LocalCalendarDialogTitle": "Kalender lokal saja", + "ImapCalDavSettingsPage_LocalCalendarDialogMessage": "Kalender lokal menyimpan semua acara hanya di komputer Anda. Tidak ada yang disinkronkan ke iCloud, Yahoo, atau penyedia lainnya.", + "ImapCalDavSettingsPage_CalDavServiceUrl": "URL layanan CalDAV", + "ImapCalDavSettingsPage_CalDavUsername": "Nama pengguna CalDAV", + "ImapCalDavSettingsPage_CalDavPassword": "Kata sandi CalDAV", + "ImapCalDavSettingsPage_CalDavNotRequiredMessage": "Pengujian CalDAV diperlukan hanya jika mode kalender diatur ke sinkronisasi CalDAV.", + "ImapCalDavSettingsPage_CalDavUrlRequired": "URL layanan CalDAV diperlukan.", + "ImapCalDavSettingsPage_CalDavUrlInvalid": "URL layanan CalDAV harus berupa URL absolut.", + "ImapCalDavSettingsPage_CalDavUsernameRequired": "Nama pengguna CalDAV diperlukan.", + "ImapCalDavSettingsPage_CalDavPasswordRequired": "Kata sandi CalDAV diperlukan.", + "ImapCalDavSettingsPage_TestImapButton": "Uji koneksi IMAP", + "ImapCalDavSettingsPage_TestCalDavButton": "Uji koneksi CalDAV", + "ImapCalDavSettingsPage_ImapTestSuccessMessage": "Pengujian koneksi IMAP berhasil.", + "ImapCalDavSettingsPage_CalDavTestSuccessMessage": "Pengujian koneksi CalDAV berhasil.", + "ImapCalDavSettingsPage_SaveSuccessMessage": "Pengaturan akun telah divalidasi dan disimpan.", + "ImapCalDavSettingsPage_ICloudHint": "Gunakan kata sandi khusus aplikasi yang dihasilkan dari pengaturan akun Apple Anda.", + "ImapCalDavSettingsPage_YahooHint": "Gunakan kata sandi aplikasi dari pengaturan keamanan akun Yahoo Anda.", "Info_AccountCreatedMessage": "{0} telah dibuat", "Info_AccountCreatedTitle": "Buat Akun", "Info_AccountCreationFailedTitle": "Gagal Membuat Akun", "Info_AccountDeletedMessage": "{0} berhasil dihapus.", "Info_AccountDeletedTitle": "Akun Dihapus", "Info_AccountIssueFixFailedTitle": "Gagal", + "Info_AccountIssueFixImapMessage": "Buka halaman pengaturan IMAP dan kalender untuk memasukkan kredensial server Anda lagi.", + "Info_AccountAttentionRequiredMessage": "Akun ini memerlukan perhatian Anda.", + "Info_AccountAttentionRequiredClickableMessage": "Klik untuk memperbaiki akun ini dan melakukan sinkronisasi ulang.", + "Info_AccountAttentionRequiredAction": "Perbaiki", + "Info_AccountAttentionRequiredActionHint": "Klik Perbaiki untuk menyelesaikan masalah akun ini.", "Info_AccountIssueFixSuccessMessage": "Semua masalah akun diperbaiki.", "Info_AccountIssueFixSuccessTitle": "Berhasil", "Info_AttachmentOpenFailedMessage": "Tidak dapat membuka lampiran ini.", @@ -370,6 +584,7 @@ "InfoBarMessage_SynchronizationDisabledFolder": "Penyelarasan folder ini dimatikan.", "InfoBarTitle_SynchronizationDisabledFolder": "Folder yang Dimatikan", "Justify": "Rata kiri kanan", + "MenuUpdateAvailable": "Pembaruan Tersedia", "Left": "Kiri", "Link": "Tautan", "LinkedAccountsCreatePolicyMessage": "Anda harus memiliki paling sedikit 2 akun untuk membuat tautan\nTautan akan dihapus", @@ -403,6 +618,7 @@ "MailOperation_Unarchive": "Batalkan arsip", "MailOperation_ViewMessageSource": "Lihat sumber pesan", "MailOperation_Zoom": "Perbesaran", + "MailsDragging": "Menggeser {0} item", "MailsSelected": "{0} item dipilih", "MarkFlagUnflag": "Berikan/batal berikan tanda", "MarkReadUnread": "Tandai sudah/belum dibaca", @@ -434,9 +650,11 @@ "Notifications_MultipleNotificationsTitle": "Pesan Baru", "Notifications_WinoUpdatedMessage": "Versi baru terpasang {0}", "Notifications_WinoUpdatedTitle": "Wino Mail telah diperbarui.", - "OnlineSearchFailed_Message": "Gagal mencari\n{0}\n\nSurel luring ditampilkan.", - "OnlineSearchTry_Line1": "Tidak dapat menemukan yang Anda cari?", - "OnlineSearchTry_Line2": "Coba pencarian daring.", + "Notifications_StoreUpdateAvailableTitle": "Pembaruan Tersedia", + "Notifications_StoreUpdateAvailableMessage": "Versi Wino Mail yang lebih baru siap diinstal dari Microsoft Store.", + "OnlineSearchFailed_Message": "Failed to perform search\n{0}\n\nListing offline mails.", + "OnlineSearchTry_Line1": "Can't find what you are looking for?", + "OnlineSearchTry_Line2": "Try online search.", "Other": "Lainnya", "PaneLengthOption_Default": "Bawaan", "PaneLengthOption_ExtraLarge": "Ekstra Besar", @@ -446,7 +664,6 @@ "PaneLengthOption_Small": "Kecil", "Photos": "Foto", "PreparingFoldersMessage": "Menyiapkan folder", - "ProtocolLogAvailable_Message": "Catatan log protokol kini tersedia untuk pengawakutuan.", "ProviderDetail_Gmail_Description": "Akun Google", "ProviderDetail_iCloud_Description": "Akun iCloud Apple", "ProviderDetail_iCloud_Title": "iCloud", @@ -465,9 +682,14 @@ "SearchBarPlaceholder": "Cari", "SearchingIn": "Mencari di", "SearchPivotName": "Hasil", + "Settings_KeyboardShortcuts_Title": "Pintasan Keyboard", + "Settings_KeyboardShortcuts_Description": "Kelola pintasan keyboard untuk tindakan cepat pada surel.", "SettingConfigureSpecialFolders_Button": "Atur", - "SettingsEditAccountDetails_IMAPConfiguration_Title": "Pengaturan IMAP/SMTP", - "SettingsEditAccountDetails_IMAPConfiguration_Description": "Ubah pengaturan peladen masuk/keluar.", + "SettingsEditAccountDetails_IMAPConfiguration_Title": "IMAP/SMTP Configuration", + "SettingsEditAccountDetails_IMAPConfiguration_Description": "Change your incoming/outgoing server settings.", + "SettingsEditAccountDetails_ImapCalDavSettings_Title": "Pengaturan IMAP dan kalender", + "SettingsEditAccountDetails_ImapCalDavSettings_Description": "Buka halaman pengaturan khusus IMAP, SMTP, dan CalDAV untuk akun ini.", + "SettingsEditAccountDetails_ImapCalDavSettings_Action": "Buka pengaturan", "SettingsAbout_Description": "Lebih lanjut tentang Wino.", "SettingsAbout_Title": "Tentang", "SettingsAboutGithub_Description": "Pergi ke pelacak masalah di repository GitHub.", @@ -486,10 +708,14 @@ "SettingsAppPreferences_CloseBehavior_Description": "Apa yang harus terjadi ketika Anda menutup aplikasi?", "SettingsAppPreferences_CloseBehavior_Title": "Perilaku saat menutup aplikasi", "SettingsAppPreferences_Description": "Pengaturan umum untuk Wino Mail.", - "SettingsAppPreferences_SearchMode_Description": "Atur apakah Winno harus mencari di tembolok secara luring atau daring. Pencarian luring selalu lebih cepat. Anda dapat melakukan pencarian daring jika surel yang Anda cari tidak muncul di hasil pencarian.", - "SettingsAppPreferences_SearchMode_Local": "Luring", - "SettingsAppPreferences_SearchMode_Online": "Daring", - "SettingsAppPreferences_SearchMode_Title": "Modus pencarian bawaan", + "SettingsAppPreferences_SearchMode_Description": "Set whether Wino should check fetched mails first while doing a search or ask your mail server online. Local search is always faster and you can always do an online search if your mail is not in the results.", + "SettingsAppPreferences_SearchMode_Local": "Local", + "SettingsAppPreferences_SearchMode_Online": "Online", + "SettingsAppPreferences_SearchMode_Title": "Default search mode", + "SettingsAppPreferences_ApplicationMode_Title": "Mode aplikasi bawaan", + "SettingsAppPreferences_ApplicationMode_Description": "Pilih mode aplikasi bawaan saat tidak ada tipe aktivasi yang ditetapkan secara eksplisit.", + "SettingsAppPreferences_ApplicationMode_Mail": "Surel", + "SettingsAppPreferences_ApplicationMode_Calendar": "Kalender", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description": "Wino Mail akan tetap berjalan di latar belakang. Pemberitahuan surel baru akan tetap berfungsi.", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title": "Jalankan di latar belakang", "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description": "Wino Mail akan tetap berjalan di baki sistem. Anda dapat membuka Wino Mail dengan mengeklik ikon. Pemberitahuan surel baru akan tetap berfungsi.", @@ -506,12 +732,30 @@ "SettingsAppPreferences_StartupBehavior_FatalError": "Galat kritis terjadi saat mengubah modus mulai untuk Wino Mail.", "SettingsAppPreferences_StartupBehavior_Title": "Mulai dan minimalkan saat menyalakan Windows", "SettingsAppPreferences_Title": "Pengaturan Aplikasi", + "SettingsAppPreferences_HideWinoAccountButton_Title": "Sembunyikan tombol akun Wino di bilah judul", + "SettingsAppPreferences_HideWinoAccountButton_Description": "Sembunyikan tombol profil di bilah judul yang membuka flyout akun Wino.", + "SettingsAppPreferences_StoreUpdateNotifications_Title": "Pemberitahuan pembaruan toko", + "SettingsAppPreferences_StoreUpdateNotifications_Description": "Tampilkan pemberitahuan dan tindakan footer saat pembaruan Microsoft Store tersedia.", + "SettingsAppPreferences_AiActions_Title": "Tindakan AI", + "SettingsAppPreferences_AiActions_Description": "Pilih bahasa AI bawaan dan tempat ringkasan disimpan.", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Title": "Bahasa terjemahan bawaan", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Description": "Pilih bahasa target default yang digunakan oleh tindakan terjemahan AI.", + "SettingsAppPreferences_AiSummarizeLanguage_Title": "Bahasa ringkasan", + "SettingsAppPreferences_AiSummarizeLanguage_Description": "Pilih bahasa yang akan digunakan untuk ringkasan keluaran AI di masa mendatang.", + "SettingsAppPreferences_AiSummarySavePath_Title": "Lokasi penyimpanan ringkasan bawaan", + "SettingsAppPreferences_AiSummarySavePath_Description": "Pilih folder yang akan digunakan Wino secara default saat menyimpan ringkasan AI.", + "SettingsAppPreferences_AiSummarySavePath_Placeholder": "Gunakan lokasi penyimpanan default sistem", + "SettingsAppPreferences_AiSummarySavePath_InvalidHint": "Folder ini tidak ada. Lokasi penyimpanan default akan digunakan untuk ringkasan.", "SettingsAutoSelectNextItem_Description": "Pilih item berikutnya setelah Anda menghapus atau memindahkan surel.", "SettingsAutoSelectNextItem_Title": "Pilih item berikutnya secara otomatis", "SettingsAvailableThemes_Description": "Pilih sebuah tema atau buat tema Anda sendiri.", "SettingsAvailableThemes_Title": "Tema yang Tersedia", "SettingsCalendarSettings_Description": "Ubah hari pertama dalam pekan, tinggi kotak jam, dan lain-lain...", "SettingsCalendarSettings_Title": "Pengaturan Kalender", + "CalendarSettings_DefaultSnoozeDuration_Header": "Durasi snooze bawaan", + "CalendarSettings_DefaultSnoozeDuration_Description": "Atur durasi snooze bawaan untuk pemberitahuan pengingat kalender.", + "CalendarSettings_TimedDayHeaderFormat_Header": "Format header hari tampilan terjadwal", + "CalendarSettings_TimedDayHeaderFormat_Description": "Pilih bagaimana label hari teratas dirender pada tampilan hari, minggu, dan minggu kerja. Gunakan token format tanggal seperti ddd, dd, MMM, atau dddd.", "SettingsComposer_Title": "Bidang penulisan", "SettingsComposerFont_Title": "Fon Bawaan", "SettingsComposerFontFamily_Description": "Ubah fon bawaan dan ukuran fon saat menulis surel.", @@ -521,16 +765,19 @@ "SettingsCustomTheme_Title": "Tema Khusus", "SettingsDeleteAccount_Description": "Hapus semua surel dan informasi tentang akun ini.", "SettingsDeleteAccount_Title": "Hapus akun ini", - "SettingsDeleteProtection_Description": "Haruskah Wino selalu meminta persetujuan setiap kali Anda menghapus permanen surel dengan tombol Shift + Del?", + "SettingsDeleteProtection_Description": "Should Wino ask you for confirmation every time you try to permanently delete a mail using Shift + Del keys?", "SettingsDeleteProtection_Title": "Perlindungan Hapus Permanen", "SettingsDiagnostics_Description": "Untuk pengembang", - "SettingsDiagnostics_DiagnosticId_Description": "Bagikan ID ini ke para pengembang saat diminta untuk mendapatkan bantuan masalah yang dialami di Wino Mail.", - "SettingsDiagnostics_DiagnosticId_Title": "ID Pengawakutuan", + "SettingsDiagnostics_DiagnosticId_Description": "Share this ID with the developers when asked to get help for the issues you experience in Wino Mail.", + "SettingsDiagnostics_DiagnosticId_Title": "Diagnostic ID", "SettingsDiagnostics_Title": "Diagnostik", "SettingsDiscord_Description": "Dapatkan pemberuan pengembangan, gabung dengan obrolan pengembangan dan berikan masukan.", "SettingsDiscord_Title": "Saluran Discord", "SettingsEditLinkedInbox_Description": "Tambah/hapus akun, ubah nama, atau hapus tautan antara akun.", "SettingsEditLinkedInbox_Title": "Sunting Kotak Masuk Tertaut", + "SettingsWindowBackdrop_Title": "Latar belakang jendela", + "SettingsWindowBackdrop_Description": "Pilih efek latar belakang untuk jendela Wino.", + "SettingsWindowBackdrop_Disabled": "Pemilihan latar belakang jendela dinonaktifkan saat tema aplikasi dipilih selain Default.", "SettingsElementTheme_Description": "Pilih tema Windows untuk Wino", "SettingsElementTheme_Title": "Tema Elemen", "SettingsElementThemeSelectionDisabled": "Pilihan tema elemen dimatikan saat tema aplikasi bawaan tidak dipilih.", @@ -579,8 +826,10 @@ "SettingsManageAccountSettings_Title": "Kelole Pengaturan Akun", "SettingsManageAliases_Description": "Lihat, perbarui, dan hapus alias untuk akun ini.", "SettingsManageAliases_Title": "Alias", - "SettingsEditAccountDetails_Title": "Sunting rincian akun", - "SettingsEditAccountDetails_Description": "Ubah nama akun, nama pengirim, dan atur warna akun.", + "SettingsEditAccountDetails_Title": "Edit Account Details", + "SettingsEditAccountDetails_Description": "Change account name, sender name and assign a new color if you like.", + "EditAccountDetailsPage_SaveSuccess_Title": "Perubahan Disimpan", + "EditAccountDetailsPage_SaveSuccess_Message": "Rincian akun Anda telah berhasil diperbarui.", "SettingsManageLink_Description": "Pindahkan item untuk menambahkan tautan baru atau hapus tautan yang sudah ada.", "SettingsManageLink_Title": "Kelola Tautan", "SettingsMarkAsRead_Description": "Ubah apa yang terjadi ke item yang dipilih.", @@ -596,7 +845,41 @@ "SettingsNotifications_Title": "Pemberitahuan", "SettingsNotificationsAndTaskbar_Description": "Ubah apakah pemberitahuan dan lencana bilah tugas harus ditampilkan untuk akun ini.", "SettingsNotificationsAndTaskbar_Title": "Pemberitahuan & Bilah Tugas", + "SettingsHome_Title": "Beranda", + "SettingsHome_SearchTitle": "Cari pengaturan", + "SettingsHome_SearchDescription": "Cari berdasarkan fitur, topik, atau kata kunci untuk langsung menuju halaman pengaturan yang tepat.", + "SettingsHome_SearchPlaceholder": "Cari pengaturan", + "SettingsHome_SearchExamples": "Coba: tema, penyimpanan, bahasa, tanda tangan", + "SettingsHome_QuickLinks_Title": "Tautan cepat", + "SettingsHome_QuickLinks_Description": "Langsung ke pengaturan yang paling sering diakses.", + "SettingsHome_StorageCard_Description": "Lihat berapa banyak konten MIME lokal yang disimpan Wino di perangkat ini dan bersihkan jika diperlukan.", + "SettingsHome_StorageEmptySummary": "Belum ada konten MIME yang di-cache.", + "SettingsHome_StorageLoading": "Memeriksa penggunaan MIME lokal...", + "SettingsHome_Tips_Title": "Tips & Trik", + "SettingsHome_Tips_Description": "Beberapa perubahan kecil dapat membuat Wino terasa jauh lebih personal.", + "SettingsHome_Tip_Theme": "Ingin mode gelap atau perubahan aksen? Buka Personalisasi.", + "SettingsHome_Tip_Background": "Gunakan Preferensi Aplikasi untuk mengontrol perilaku startup dan sinkronisasi latar belakang.", + "SettingsHome_Tip_Shortcuts": "Pintasan keyboard membantu Anda menavigasi surel lebih cepat.", + "SettingsHome_Resources_Title": "Tautan yang berguna", + "SettingsHome_Resources_Description": "Buka sumber daya proyek, info dukungan, dan saluran rilis.", "SettingsOptions_Title": "Pengaturan", + "SettingsOptions_GeneralSection": "Umum", + "SettingsOptions_MailSection": "Surel", + "SettingsOptions_CalendarSection": "Kalender", + "SettingsOptions_MoreComingSoon": "Opsi lain akan datang segera", + "SettingsOptions_HeroDescription": "Sesuaikan pengalaman Wino Mail Anda.", + "SettingsOptions_AccountsSummary": "{0} akun yang dikonfigurasi", + "SettingsSearch_ManageAccounts_Keywords": "akun;akun;kotak surat;kotak surat;alias;alias;profil;alamat;alamat", + "SettingsSearch_AppPreferences_Keywords": "awal;latar belakang;peluncuran;sinkronisasi;notifikasi;notifikasi;pencarian;ikon baki;bawaan", + "SettingsSearch_LanguageTime_Keywords": "bahasa;waktu;jam;locale;wilayah;format;24 jam;24 jam", + "SettingsSearch_Personalization_Keywords": "tema;gelap;terang;penampilan;aksen;warna;warna;mode;tata letak;kepadatan", + "SettingsSearch_About_Keywords": "tentang;versi;situs web;privasi;github;donasi;toko;dukungan", + "SettingsSearch_KeyboardShortcuts_Keywords": "pintasan;pintasan;tombol pintas;tombol pintas;papan ketik;tombol", + "SettingsSearch_MessageList_Keywords": "pesan;pesan;daftar;percabangan;utas;avatar;pratinjau;pengirim", + "SettingsSearch_ReadComposePane_Keywords": "pembaca;menulis;penyusun;font;font;konten eksternal;tampilan;membaca", + "SettingsSearch_SignatureAndEncryption_Keywords": "tanda tangan;tanda tangan;enkripsi;sertifikat;sertifikat;s mime;smime;keamanan", + "SettingsSearch_Storage_Keywords": "penyimpanan;cache;caching;mime;disk;ruang;pembersihan;bersihkan;data lokal", + "SettingsSearch_CalendarSettings_Keywords": "kalender;minggu;jam;jadwal;acara;acara", "SettingsPaneLengthReset_Description": "Atur ukuran daftar email me bawaan.", "SettingsPaneLengthReset_Title": "Atur Ulang Ukuran Daftar Surel", "SettingsPaypal_Description": "Berikan lebih banyak cinta ❤️ Semua sumbangan sangat kami hargai.", @@ -610,6 +893,8 @@ "SettingsPrefer24HourClock_Title": "Gunakan Format 24 Jam", "SettingsPrivacyPolicy_Description": "Tinjau kebijakan privasi.", "SettingsPrivacyPolicy_Title": "Kebijakan Privasi", + "SettingsWebsite_Description": "Buka situs Wino Mail.", + "SettingsWebsite_Title": "Situs", "SettingsReadComposePane_Description": "Fon, konten eksternal.", "SettingsReadComposePane_Title": "Pembaca dan Bidang Penulisan", "SettingsReader_Title": "Pembaca", @@ -625,11 +910,24 @@ "SettingsShowPreviewText_Title": "Tampilkan Teks Pratinjau", "SettingsShowSenderPictures_Description": "Sembunyikan/tampilkan foto pengirim.", "SettingsShowSenderPictures_Title": "Tampilkan Foto Pengirim", + "SettingsEmailTemplates_Title": "Template Email", + "SettingsEmailTemplates_Description": "Kelola template email", + "SettingsEmailTemplates_CreatePageTitle": "Template baru", + "SettingsEmailTemplates_EditPageTitle": "Edit template", + "SettingsEmailTemplates_NewTemplateTitle": "Template baru", + "SettingsEmailTemplates_NewTemplateDescription": "Buat template email baru", + "SettingsEmailTemplates_NameTitle": "Nama", + "SettingsEmailTemplates_NamePlaceholder": "Nama template", + "SettingsEmailTemplates_DescriptionTitle": "Deskripsi", + "SettingsEmailTemplates_DescriptionPlaceholder": "Deskripsi opsional", + "SettingsEmailTemplates_ContentTitle": "Konten template", + "SettingsEmailTemplates_ContentDescription": "Edit konten HTML untuk template ini.", + "SettingsEmailTemplates_NameRequired": "Nama template diperlukan.", "SettingsEnableGravatarAvatars_Title": "Gravatar", - "SettingsEnableGravatarAvatars_Description": "Gunakan gravatar sebagai foto pengirim jika tersedia", - "SettingsEnableFavicons_Title": "Ikon Domain (Favicons)", - "SettingsEnableFavicons_Description": "Gunakan favicon domain sebagai foto pengirim jika tersedia", - "SettingsMailList_ClearAvatarsCache_Button": "Hapus avatar di tembolok", + "SettingsEnableGravatarAvatars_Description": "Use gravatar (if available) as sender picture", + "SettingsEnableFavicons_Title": "Domain icons (Favicons)", + "SettingsEnableFavicons_Description": "Use domain favicons (if available) as sender picture", + "SettingsMailList_ClearAvatarsCache_Button": "Clear cached avatars", "SettingsSignature_AddCustomSignature_Button": "Tambah tanda tangan", "SettingsSignature_AddCustomSignature_Title": "Tambahkan tanda tangan khusus", "SettingsSignature_DeleteSignature_Title": "Hapus tanda tangan", @@ -645,14 +943,41 @@ "SettingsStartupItem_Title": "Akun Utama", "SettingsStore_Description": "Berikan sedikit cinta ❤️", "SettingsStore_Title": "Nilai di Microsoft Store", + "SettingsStorage_Title": "Penyimpanan", + "SettingsStorage_Description": "Pindai dan kelola cache MIME yang disimpan di folder data lokal Anda.", + "SettingsStorage_ScanFolder": "Pindai folder data lokal", + "SettingsStorage_NoLocalMimeDataFound": "Tidak ada data MIME lokal yang ditemukan.", + "SettingsStorage_NoAccountsFound": "Tidak ada akun yang ditemukan.", + "SettingsStorage_TotalUsage": "Total penggunaan MIME lokal: {0}", + "SettingsStorage_AccountUsageDescription": "{0} digunakan dalam cache MIME lokal", + "SettingsStorage_DeleteAll_Title": "Hapus semua konten MIME", + "SettingsStorage_DeleteAll_Description": "Hapus seluruh folder cache MIME untuk akun ini.", + "SettingsStorage_DeleteAll_Button": "Hapus semua", + "SettingsStorage_DeleteAll_Confirm_Title": "Hapus semua konten MIME", + "SettingsStorage_DeleteAll_Confirm_Message": "Hapus semua data MIME lokal untuk {0}?", + "SettingsStorage_DeleteAll_Success": "Semua konten MIME telah dihapus.", + "SettingsStorage_DeleteOld_Title": "Hapus konten MIME lama", + "SettingsStorage_DeleteOld_Description": "Hapus berkas MIME berdasarkan tanggal pembuatan email di basis data lokal.", + "SettingsStorage_DeleteOld_1Month": "> 1 bulan", + "SettingsStorage_DeleteOld_3Months": "> 3 bulan", + "SettingsStorage_DeleteOld_6Months": "> 6 bulan", + "SettingsStorage_DeleteOld_1Year": "> 1 tahun", + "SettingsStorage_DeleteOld_Confirm_Title": "Hapus konten MIME lama", + "SettingsStorage_DeleteOld_Confirm_Message": "Hapus data MIME lokal yang lebih lama dari {0} untuk {1}?", + "SettingsStorage_DeleteOld_Success": "Berhasil menghapus {0} folder MIME yang lebih lama dari {1}.", + "SettingsStorage_1Month": "1 bulan", + "SettingsStorage_3Months": "3 bulan", + "SettingsStorage_6Months": "6 bulan", + "SettingsStorage_1Year": "1 tahun", + "SettingsStorage_Months": "{0} bulan", "SettingsTaskbarBadge_Description": "Tampilkan jumlah surel belum dibaca di ikon bilah tugas.", "SettingsTaskbarBadge_Title": "Lencana Bilah Tugas", "SettingsThreads_Description": "Kelompokkan pesan menjadi utas.", "SettingsThreads_Title": "Utas Percakapan", "SettingsUnlinkAccounts_Description": "Hapus tautan antara akun. Ini tidak akan menghapus akun Anda.", "SettingsUnlinkAccounts_Title": "Lepaskan Tautan Akun", - "SettingsMailRendering_ActionLabels_Title": "Label aksi", - "SettingsMailRendering_ActionLabels_Description": "Tampilkan label aksi.", + "SettingsMailRendering_ActionLabels_Title": "Action labels", + "SettingsMailRendering_ActionLabels_Description": "Show action labels.", "SignatureDeleteDialog_Message": "Apakah Anda yakin ingin menghapus tanda tangan \"{0}\"?", "SignatureDeleteDialog_Title": "Hapus tanda tangan", "SignatureEditorDialog_SignatureName_Placeholder": "Beri nama tanda tangan", @@ -683,6 +1008,9 @@ "SystemFolderConfigDialogValidation_InboxSelected": "Anda tidak dapat menetapkan folder Kotak Masuk ke folder sistem yang lain.", "SystemFolderConfigSetupSuccess_Message": "Folder sistem berhasil diatur.", "SystemFolderConfigSetupSuccess_Title": "Persiapan Folder Sistem", + "SystemTrayMenu_ShowWino": "Buka Wino Mail", + "SystemTrayMenu_ShowWinoCalendar": "Buka Wino Kalender", + "SystemTrayMenu_ExitWino": "Keluar", "TestingImapConnectionMessage": "Menguji sambungan peladen...", "TitleBarServerDisconnectedButton_Description": "Wino tidak tersambung ke jaringan. Klik sambungkan kembali untuk mencoba melanjutkan sambungan.", "TitleBarServerDisconnectedButton_Title": "tiada sambungan", @@ -699,8 +1027,422 @@ "WinoUpgradeMessage": "Tingkatkan ke Akun Tak Terbatas", "WinoUpgradeRemainingAccountsMessage": "{0} dari {1} akun gratis digunakan.", "Yesterday": "Kemarin", - "SettingsAppPreferences_EmailSyncInterval_Title": "Jangka waktu penyelarasan surel", - "SettingsAppPreferences_EmailSyncInterval_Description": "Jangka waktu penyelarasan surel dalam menit. Pengaturan ini akan diterapkan setelah memulai ulang Wino mail." + "Smime_ImportCertificates_Success": "Sertifikat berhasil diimpor.", + "Smime_ImportCertificates_Error": "Gagal mengimpor sertifikat: {0}", + "Smime_RemoveCertificates_Confirm": "Apakah Anda benar-benar ingin menghapus sertifikat {0}?", + "Smime_RemoveCertificates_Success": "Sertifikat dihapus.", + "Smime_ExportCertificates_Success": "Sertifikat berhasil diekspor.", + "Smime_ExportCertificates_Error": "Gagal mengekspor sertifikat.", + "Smime_CertificateDetails": "Subjek: {0}\nPenerbit: {1}\nBerlaku sejak: {2}\nBerlaku hingga: {3}\nJejak Sidik Jari: {4}", + "Smime_CertificatePassword_Title": "Kata sandi sertifikat diperlukan", + "Smime_CertificatePassword_Placeholder": "Kata sandi sertifikat untuk {0} (opsional)", + "Smime_Confirm_Title": "Konfirmasi", + "Buttons_OK": "OK", + "Buttons_Refresh": "Segarkan", + "SettingsSignatureAndEncryption_Title": "Tanda Tangan dan Enkripsi", + "SettingsSignatureAndEncryption_Description": "Kelola sertifikat S/MIME untuk menandatangani dan mengenkripsi email.", + "SettingsSignatureAndEncryption_MyCertificatesHeader": "Sertifikat saya", + "SettingsSignatureAndEncryption_MyCertificatesDescription": "Sertifikat pribadi untuk penandatanganan dan enkripsi", + "SettingsSignatureAndEncryption_RecipientCertificatesHeader": "Sertifikat Penerima", + "SettingsSignatureAndEncryption_RecipientCertificatesDescription": "Sertifikat penerima untuk dekripsi", + "SettingsSignatureAndEncryption_NameColumn": "Nama", + "SettingsSignatureAndEncryption_ExpiresColumn": "Kadaluarsa pada", + "SettingsSignatureAndEncryption_ThumbprintColumn": "Jejak Sidik Jari", + "Buttons_Remove": "Hapus", + "Buttons_Export": "Ekspor", + "Buttons_Import": "Impor", + "SettingsSignatureAndEncryption_SigningCertificate": "Sertifikat Penandatangan S/MIME", + "SettingsSignatureAndEncryption_EncryptionCertificate": "Sertifikat Enkripsi S/MIME", + "SettingsSignatureAndEncryption_SigningCertificatePlaceholder": "Tidak ada", + "SmimeSignaturesInMessage": "Tanda tangan dalam pesan ini:", + "SmimeSignatureEntry": "• {0} {1} ({2}, berlaku hingga {3} - {4})", + "SmimeSigningCertificateInfoTitle": "Info Sertifikat Penandatangan S/MIME", + "SmimeCertificateInfoTitle": "Informasi Sertifikat S/MIME", + "SmimeNoCertificateFileFound": "Tidak ditemukan berkas sertifikat", + "SmimeSaveCertificate": "Simpan sertifikat...", + "SmimeCertificate": "S/MIME Sertifikat", + "SmimeCertificateSavedTo": "Sertifikat disimpan ke {0}", + "SmimeSignedTooltip": "Pesan ini ditandatangani dengan sertifikat S/MIME. Klik untuk detail lebih lanjut", + "SmimeEncryptedTooltip": "Pesan ini dienkripsi dengan sertifikat S/MIME.", + "SmimeCertificateFileInfo": "Berkas: {0}", + "Composer_LightTheme": "Tema Terang", + "Composer_DarkTheme": "Tema Gelap", + "Composer_Outdent": "Kurangi Indentasi", + "Composer_Indent": "Indentasi", + "Composer_BulletList": "Daftar Peluru", + "Composer_OrderedList": "Daftar Terurut", + "Composer_Stroke": "Garis", + "Composer_Bold": "Tebal", + "Composer_Italic": "Miring", + "Composer_Underline": "Garis bawah", + "Composer_CcBcc": "CC & BCC", + "Composer_EnableSmimeSignature": "Aktifkan/matikan tanda tangan S/MIME", + "Composer_EnableSmimeEncryption": "Aktifkan/matikan enkripsi S/MIME", + "Composer_LocalDraftSyncInfo": "Draf ini hanya lokal. Wino gagal mengirimkannya ke server email Anda. Klik untuk mencoba mengirimkannya ke server.", + "Composer_CertificateExpires": "Kadaluarsa pada: ", + "Composer_SmimeSignature": "Tanda tangan S/MIME", + "Composer_SmimeEncryption": "Enkripsi S/MIME", + "Composer_EmailTemplatesPlaceholder": "Template email", + "Composer_AiSummarize": "Ringkas dengan AI", + "Composer_AiSummarizeDescription": "Ekstrak poin-poin utama, item tindakan, dan keputusan dari email ini.", + "Composer_AiTranslate": "Terjemahkan dengan AI", + "Composer_AiActions": "Tindakan AI", + "Composer_AiRewrite": "Tulis ulang dengan AI", + "AiActions_CheckingStatus": "Memeriksa akses AI...", + "AiActions_SignedOutTitle": "Buka Paket AI Wino", + "AiActions_SignedOutDescription": "Terjemahkan, tulis ulang, dan rangkum email menggunakan AI setelah masuk ke Akun Wino Anda dan mengaktifkan add-on Paket AI.", + "AiActions_NoPackTitle": "Diperlukan Paket AI", + "AiActions_NoPackDescription": "Anda telah masuk, tetapi Paket AI belum aktif. Beli paketnya untuk menggunakan alat terjemahan AI, penulisan ulang, dan rangkuman AI Wino.", + "AiActions_UsageSummary": "{0} dari {1} kredit telah digunakan bulan ini.", + "Composer_AiRewritePolite": "Jadikan lebih sopan", + "Composer_AiRewritePoliteDescription": "Membuat kata-kata lebih halus tanpa mengubah maksudnya.", + "Composer_AiRewriteAngry": "Buat marah", + "Composer_AiRewriteAngryDescription": "Menggunakan nada yang lebih tajam dan konfrontatif.", + "Composer_AiRewriteHappy": "Ubah menjadi ceria", + "Composer_AiRewriteHappyDescription": "Menambahkan nada yang lebih ceria dan antusias.", + "Composer_AiRewriteFormal": "Ubah menjadi formal", + "Composer_AiRewriteFormalDescription": "Membuat pesan terdengar lebih profesional dan terstruktur.", + "Composer_AiRewriteFriendly": "Ubah menjadi ramah", + "Composer_AiRewriteFriendlyDescription": "Membuat pesan terdengar lebih ramah dan mudah didekati.", + "Composer_AiRewriteShorter": "Ubah menjadi lebih singkat", + "Composer_AiRewriteShorterDescription": "Memadatkan teks dan menghilangkan detail yang tidak perlu.", + "Composer_AiRewriteClearer": "Jadikan lebih jelas", + "Composer_AiRewriteClearerDescription": "Meningkatkan keterbacaan dan membuat pesan lebih mudah diikuti.", + "Composer_AiRewriteCustom": "Kustom", + "Composer_AiRewriteCustomDescription": "Gambarkan maksud penulisan ulang Anda sendiri.", + "Composer_AiRewriteCustomPlaceholder": "Gambarkan bagaimana Anda ingin pesan ditulis ulang", + "Composer_AiRewriteMode": "Nada penulisan ulang", + "Composer_AiRewriteApply": "Terapkan penulisan ulang", + "Composer_AiTranslateDialogTitle": "Terjemahkan dengan AI", + "Composer_AiTranslateDialogDescription": "Masukkan bahasa target atau kode budaya, seperti en-US, tr-TR, de-DE, atau fr-FR.", + "Composer_AiTranslateApply": "Terjemahkan", + "Composer_AiTranslateLanguage": "Bahasa target", + "Composer_AiTranslateCustomPlaceholder": "Masukkan kode budaya", + "Composer_AiTranslateLanguageEnglish": "Inggris (en-US)", + "Composer_AiTranslateLanguageTurkish": "Turki (tr-TR)", + "Composer_AiTranslateLanguageGerman": "Jerman (de-DE)", + "Composer_AiTranslateLanguageFrench": "Prancis (fr-FR)", + "Composer_AiTranslateLanguageSpanish": "Spanyol (es-ES)", + "Composer_AiTranslateLanguageItalian": "Italia (it-IT)", + "Composer_AiTranslateLanguagePortugueseBrazil": "Portugis Brasil (pt-BR)", + "Composer_AiTranslateLanguageDutch": "Belanda (nl-NL)", + "Composer_AiTranslateLanguagePolish": "Polandia (pl-PL)", + "Composer_AiTranslateLanguageRussian": "Rusia (ru-RU)", + "Composer_AiTranslateLanguageJapanese": "Jepang (ja-JP)", + "Composer_AiTranslateLanguageKorean": "Korea (ko-KR)", + "Composer_AiTranslateLanguageChineseSimplified": "Cina Sederhana (zh-CN)", + "Composer_AiTranslateLanguageArabic": "Arab (ar-SA)", + "Composer_AiTranslateLanguageHindi": "Hindi (hi-IN)", + "Composer_AiTranslateLanguageOther": "Lainnya...", + "Composer_AiBusyTitle": "AI sedang bekerja", + "Composer_AiBusyMessage": "Harap tunggu hingga aksi AI saat ini selesai.", + "Composer_AiSignInRequired": "Masuk ke akun Wino Anda untuk menggunakan fitur AI.", + "Composer_AiMissingHtml": "Belum ada konten pesan untuk dikirim ke Wino AI.", + "Composer_AiQuotaUnavailable": "Hasil AI telah diterapkan.", + "Composer_AiAppliedMessage": "Hasil AI telah diterapkan pada penyusun. Gunakan Undo jika Anda ingin membatalkan.", + "Composer_AiSummarizeSuccessTitle": "Ringkasan AI diterapkan", + "Composer_AiTranslateSuccessTitle": "Terjemahan AI diterapkan", + "Composer_AiRewriteSuccessTitle": "Penulisan ulang AI diterapkan", + "Composer_AiErrorTitle": "Aksi AI gagal", + "Reader_AiAppliedMessage": "Hasil AI sekarang ditampilkan untuk pesan ini. Buka kembali pesan tersebut untuk melihat konten aslinya lagi.", + "SettingsAppPreferences_EmailSyncInterval_Title": "Email sync interval", + "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail.", + "ContactsPage_Title": "Kontak", + "ContactsPage_AddContact": "Tambahkan Kontak", + "ContactsPage_EditContact": "Ubah Kontak", + "ContactsPage_DeleteContact": "Hapus Kontak", + "ContactsPage_SearchPlaceholder": "Cari kontak...", + "ContactsPage_NoContacts": "Tidak ada kontak yang ditemukan", + "ContactsPage_ContactsCount": "{0} kontak", + "ContactsPage_SelectedContactsCount": "{0} terpilih", + "ContactsPage_DeleteSelectedContacts": "Hapus yang Dipilih", + "ContactEditDialog_Title": "Ubah Kontak", + "ContactEditDialog_PhotoSection": "Foto", + "ContactEditDialog_ChoosePhoto": "Pilih Foto", + "ContactEditDialog_RemovePhoto": "Hapus Foto", + "ContactEditDialog_NameHeader": "Nama", + "ContactEditDialog_NamePlaceholder": "Nama kontak", + "ContactEditDialog_EmailHeader": "Alamat Email", + "ContactEditDialog_EmailPlaceholder": "contoh@domain.com", + "ContactEditDialog_InfoSection": "Informasi Kontak", + "ContactEditDialog_RootContactInfo": "Kontak akar ini terkait dengan akun Anda dan tidak dapat dihapus.", + "ContactEditDialog_OverriddenContactInfo": "Kontak ini telah dimodifikasi secara manual dan tidak akan diperbarui selama sinkronisasi.", + "ContactsPage_Subtitle": "Kelola kontak email Anda dan informasinya", + "ContactStatus_Account": "Akun", + "ContactStatus_Modified": "Dimodifikasi", + "ContactAction_Edit": "Ubah kontak", + "ContactAction_ChangePhoto": "Ubah foto", + "ContactAction_Delete": "Hapus kontak", + "ContactAction_Add": "Tambahkan Kontak", + "ContactSelection_Selected": "terpilih", + "ContactSelection_SelectAll": "Pilih semua", + "ContactSelection_Clear": "Kosongkan Pemilihan", + "ContactsPage_EmptyState": "Tidak ada kontak untuk ditampilkan", + "ContactsPage_AddFirstContact": "Tambahkan kontak pertama Anda", + "ContactsPage_ContactsCountSuffix": "kontak", + "ContactsPane_NewContact": "Kontak Baru", + "ContactsPane_DescriptionTitle": "Kelola kontak Anda", + "ContactsPane_DescriptionBody": "Buat kontak, ganti namanya, perbarui foto profil, dan atur detail yang tersimpan di satu tempat.", + "ContactEditDialog_AddTitle": "Tambahkan Kontak", + "ContactInfoBar_ContactAdded": "Kontak berhasil ditambahkan.", + "ContactInfoBar_ContactUpdated": "Kontak berhasil diperbarui.", + "ContactInfoBar_ContactsDeleted": "Kontak berhasil dihapus.", + "ContactInfoBar_ContactPhotoUpdated": "Foto kontak berhasil diperbarui.", + "ContactInfoBar_FailedToLoadContacts": "Gagal memuat kontak: {0}", + "ContactInfoBar_FailedToAddContact": "Gagal menambahkan kontak: {0}", + "ContactInfoBar_FailedToUpdateContact": "Gagal memperbarui kontak: {0}", + "ContactInfoBar_FailedToDeleteContacts": "Gagal menghapus kontak: {0}", + "ContactInfoBar_FailedToUpdatePhoto": "Gagal memperbarui foto: {0}", + "ContactInfoBar_CannotDeleteRoot": "Kontak akar tidak dapat dihapus.", + "ContactConfirmDialog_DeleteTitle": "Hapus Kontak", + "ContactConfirmDialog_DeleteMessage": "Anda yakin ingin menghapus kontak '{0}'?", + "ContactConfirmDialog_DeleteMultipleMessage": "Anda yakin ingin menghapus {0} kontak?", + "ContactConfirmDialog_DeleteButton": "Hapus", + "CalendarAccountSettings_Title": "Pengaturan Akun Kalender", + "CalendarAccountSettings_Description": "Kelola pengaturan kalender untuk {0}", + "CalendarAccountSettings_AccountColor": "Warna Akun", + "CalendarAccountSettings_AccountColorDescription": "Ubah warna tampilan untuk akun kalender ini", + "CalendarAccountSettings_SyncEnabled": "Aktifkan Sinkronisasi", + "CalendarAccountSettings_SyncEnabledDescription": "Aktifkan atau nonaktifkan sinkronisasi kalender untuk akun ini", + "CalendarAccountSettings_DefaultShowAs": "Status Tampilkan Secara Default", + "CalendarAccountSettings_DefaultShowAsDescription": "Status ketersediaan default untuk acara baru yang dibuat dengan akun ini", + "CalendarAccountSettings_PrimaryCalendar": "Kalender Utama", + "CalendarAccountSettings_PrimaryCalendarDescription": "Tandai kalender ini sebagai kalender utama untuk akun tersebut.", + "CalendarSettings_NewEventBehavior_Header": "Perilaku tombol Acara Baru", + "CalendarSettings_NewEventBehavior_Description": "Pilih apakah tombol Acara Baru harus menanyakan kalender setiap kali atau selalu membuka kalender tertentu.", + "CalendarSettings_NewEventBehavior_AskEachTime": "Tanyakan setiap kali.", + "CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar": "Selalu gunakan kalender tertentu.", + "CalendarSettings_Rendering_Title": "Penataan", + "CalendarSettings_Rendering_Description": "Atur tata letak kalender dan perilaku tampilan.", + "CalendarSettings_Notifications_Title": "Pemberitahuan", + "CalendarSettings_Notifications_Description": "Pilih pengingat default dan perilaku tunda.", + "CalendarSettings_Preferences_Title": "Preferensi", + "CalendarSettings_Preferences_Description": "Atur bagaimana tombol Acara Baru berperilaku.", + "WhatIsNew_GetStartedButton": "Mulai", + "WhatIsNew_ContinueAnywayButton": "Lanjutkan saja", + "WhatIsNew_PreparingForNewVersionButton": "Sedang mempersiapkan versi baru...", + "WhatIsNew_MigrationPreparing_Title": "Menyiapkan data Anda", + "WhatIsNew_MigrationPreparing_Description": "Wino sedang menerapkan migrasi pembaruan. Harap tunggu sambil kami menyiapkan data akun Anda untuk rilis ini.", + "WhatIsNew_MigrationFailedMessage": "Gagal menerapkan migrasi dengan kode galat {0}. Anda dapat terus menggunakan aplikasi tersebut. Namun, jika Anda mengalami masalah serius silakan instal ulang aplikasi.", + "WhatIsNew_MigrationNotification_Title": "Wino Mail Diperbarui", + "WhatIsNew_MigrationNotification_Message": "Buka aplikasi untuk menyelesaikan pembaruan dan melihat apa yang baru.", + "WelcomeWindow_Title": "Selamat datang di Wino Mail", + "WelcomeWindow_Subtitle": "Pengalaman asli Windows untuk Mail dan Kalender.", + "WelcomeWindow_WhatsNewTitle": "Perubahan Terbaru", + "WelcomeWindow_FeaturesTitle": "Fitur", + "WelcomeWindow_WhatsNewTab": "Apa yang Baru", + "WelcomeWindow_FeaturesTab": "Fitur", + "WelcomeWindow_GetStartedButton": "Mulai dengan menambahkan akun", + "WelcomeWindow_GetStartedDescription": "Tambahkan akun Outlook, Gmail, atau IMAP Anda untuk memulai dengan Wino Mail.", + "WelcomeWindow_ImportFromWinoAccount": "Impor dari Akun Wino Anda", + "WelcomeWindow_ImportInProgress": "Mengimpor preferensi dan akun yang tersinkronkan...", + "WelcomeWindow_ImportNoAccountsFound": "Tidak ada akun yang tersinkronisasi ditemukan di Akun Wino Anda. Jika preferensi tersedia, mereka telah dipulihkan. Gunakan Mulai untuk menambahkan akun secara manual.", + "WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} akun yang tersinkronisasi sudah tersedia di perangkat ini. Gunakan Mulai untuk menambahkan akun lain secara manual jika diperlukan.", + "WelcomeWindow_SetupTitle": "Siapkan akun Anda", + "WelcomeWindow_SetupSubtitle": "Pilih penyedia email Anda untuk memulai", + "WelcomeWindow_AddAccountButton": "Tambahkan akun", + "WelcomeWindow_SkipForNow": "Lewati untuk sekarang — saya akan mengaturnya nanti", + "WelcomeWindow_AppDescription": "Kotak masuk yang cepat dan fokus — dirancang ulang untuk Windows 11", + "WelcomeWizard_Step1Title": "Selamat datang", + "SystemTrayMenu_Open": "Buka", + "WinoAccount_Titlebar_SyncBenefitTitle": "Pengaturan Sinkronisasi", + "WinoAccount_Titlebar_SyncBenefitDescription": "Jaga preferensi Wino Anda tetap sinkron di berbagai perangkat.", + "WinoAccount_Titlebar_AddonsBenefitTitle": "Aktifkan Add-ons", + "WinoAccount_Titlebar_AddonsBenefitDescription": "Akses fitur premium seperti Wino AI Pack.", + "WinoAccount_Management_Description": "Kelola Akun Wino Anda, akses AI Pack, serta preferensi yang tersinkron dan detail akun.", + "WinoAccount_Management_SignedOutTitle": "Masuk ke Wino Mail", + "WinoAccount_Management_SignedOutDescription": "Masuk atau buat akun untuk menyinkronkan email Anda, mengakses fitur AI, dan mengelola pengaturan di beberapa perangkat.", + "WinoAccount_Management_ProfileSectionHeader": "Profil", + "WinoAccount_Management_AddOnsSectionHeader": "Add-Ons Wino", + "WinoAccount_Management_DataSectionHeader": "Data", + "WinoAccount_Management_AccountActionsSectionHeader": "Aksi Akun", + "WinoAccount_Management_AccountCardTitle": "Akun", + "WinoAccount_Management_AccountCardDescription": "Alamat email Akun Wino Anda dan status akun saat ini.", + "WinoAccount_Management_AiPackCardTitle": "Paket AI", + "WinoAccount_Management_AiPackCardDescription": "Lihat apakah Paket AI Wino aktif dan berapa banyak penggunaan yang tersisa.", + "WinoAccount_Management_AiPackActive": "Paket AI aktif", + "WinoAccount_Management_AiPackInactive": "Paket AI tidak aktif", + "WinoAccount_Management_AiPackUsage": "{0} dari {1} penggunaan telah terpakai. {2} tersisa.", + "WinoAccount_Management_AiPackBillingPeriod": "Periode penagihan: {0:d} - {1:d}", + "WinoAccount_Management_AiPackUnknownUsage": "Rincian penggunaan belum tersedia.", + "WinoAccount_Management_AiPackBuyDescription": "Beli Paket AI Wino untuk menerjemahkan, menulis ulang, atau merangkum email menggunakan AI.", + "WinoAccount_Management_AiPackPromoTitle": "Aktifkan Paket AI", + "WinoAccount_Management_AiPackPromoDescription": "Tingkatkan alur kerja email Anda dengan alat bertenaga AI. Terjemahkan pesan ke lebih dari 50 bahasa, tulis ulang untuk kejelasan dan nada, dan dapatkan ringkasan instan dari utasan panjang.", + "WinoAccount_Management_AiPackPromoPrice": "$4,99 / bln", + "WinoAccount_Management_AiPackPromoRequests": "1.000 kredit", + "WinoAccount_Management_AiPackGetButton": "Dapatkan Paket AI", + "WinoAddOn_AI_PACK_Name": "Wino AI Pack", + "WinoAddOn_AI_PACK_Description": "Alat bertenaga AI untuk menerjemahkan, menulis ulang, dan merangkum tindakan di Wino Mail.", + "WinoAddOn_AI_PACK_Keywords": "AI, terjemahan, menulis ulang, merangkum, produktivitas", + "WinoAddOn_UNLIMITED_ACCOUNTS_Name": "Akun Tak Terbatas", + "WinoAddOn_UNLIMITED_ACCOUNTS_Description": "Hapus batas akun dan tambahkan sebanyak mungkin akun email yang Anda perlukan.", + "WinoAddOn_UNLIMITED_ACCOUNTS_Keywords": "akun, tak terbatas, premium, add-on", + "WinoAccount_Management_PurchaseRequiresSignIn": "Masuk dengan Akun Wino Anda untuk menyelesaikan pembelian ini.", + "WinoAccount_Management_PurchaseStartFailed": "Wino tidak dapat menyelesaikan pembelian ini di Microsoft Store.", + "WinoAccount_Management_StoreSyncFailed": "Pembelian Anda selesai, tetapi Wino belum dapat menyegarkan manfaat akun Anda. Silakan coba lagi sebentar.", + "WinoAccount_Management_AiPackSubscriptionActive": "Langganan Anda aktif", + "WinoAccount_Management_AiPackRenews": "Pembaruan {0:d}", + "WinoAccount_Management_AiPackRequestsUsed": "Kredit digunakan bulan ini", + "WinoAccount_Management_AiPackResets": "Pengaturan ulang {0:d}", + "WinoAccount_Management_AiPackUsageLoadFailed": "Kami mengalami masalah memuat saldo penggunaan AI Anda.", + "WinoAccount_Management_AiPackFeatureTranslate": "Terjemahkan", + "WinoAccount_Management_AiPackFeatureRewrite": "Tulis Ulang", + "WinoAccount_Management_AiPackFeatureSummarize": "Ringkas", + "WinoAccount_Management_AddOnLoadFailed": "Kami mengalami masalah memuat add-on ini.", + "WinoAccount_Management_SyncPreferencesTitle": "Sinkronkan Preferensi dan Akun", + "WinoAccount_Management_SyncPreferencesDescription": "Impor atau ekspor preferensi Wino Anda dan detail kotak masuk di berbagai perangkat. Kata sandi, token, dan informasi sensitif lainnya tidak pernah disinkronkan.", + "WinoAccount_Management_SignOutTitle": "Keluar", + "WinoAccount_Management_SignOutDescription": "Keluar dari akun Anda di perangkat ini", + "WinoAccount_Management_StatusLabel": "Status: {0}", + "WinoAccount_Management_NoRemoteSettings": "Belum ada data tersinkron untuk akun ini.", + "WinoAccount_Management_ExportSucceeded": "Data Wino yang Anda pilih berhasil diekspor.", + "WinoAccount_Management_ExportPreferencesSucceeded": "Preferensi Anda telah diekspor ke Akun Wino Anda.", + "WinoAccount_Management_ExportAccountsSucceeded": "Mengekspor detail akun {0} ke Akun Wino Anda.", + "WinoAccount_Management_ImportSucceeded": "Data tersinkron yang diimpor dari Akun Wino Anda.", + "WinoAccount_Management_ImportPreferencesSucceeded": "Berhasil menerapkan {0} preferensi tersinkron.", + "WinoAccount_Management_ImportAccountsSucceeded": "Berhasil mengimpor {0} akun.", + "WinoAccount_Management_ImportDuplicateAccountsSkipped": "Mengabaikan {0} akun yang sudah ada di perangkat ini.", + "WinoAccount_Management_ImportPartial": "Menerapkan {0} preferensi yang tersinkronisasi. {1} preferensi tidak dapat dipulihkan.", + "WinoAccount_Management_ImportReloginReminder": "Kata sandi, token, dan informasi sensitif lainnya tidak diimpor. Masuk lagi untuk setiap akun di perangkat ini sebelum menggunakannya.", + "WinoAccount_Management_SerializeFailed": "Wino tidak dapat menyerialisasikan preferensi Anda saat ini.", + "WinoAccount_Management_EmptyExport": "Tidak ada nilai preferensi yang dapat diekspor.", + "WinoAccount_Management_ImportEmpty": "Payload data yang disinkronkan tidak berisi apa pun yang baru untuk dipulihkan.", + "WinoAccount_Management_ExportDialog_Title": "Ekspor ke Akun Wino Anda", + "WinoAccount_Management_ExportDialog_Description": "Pilih apa yang ingin Anda sinkronkan ke Akun Wino Anda.", + "WinoAccount_Management_ExportDialog_IncludePreferences": "Preferensi", + "WinoAccount_Management_ExportDialog_IncludeAccounts": "Akun", + "WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Kata sandi, token, dan informasi sensitif lainnya tidak disinkronkan.", + "WinoAccount_Management_ExportDialog_AccountsRelogin": "Akun yang diimpor di PC lain masih perlu Anda masuk lagi sebelum dapat digunakan.", + "WinoAccount_Management_ExportDialog_InProgress": "Sedang mengekspor data Wino yang Anda pilih...", + "WinoAccount_Management_LoadFailed": "Wino tidak dapat memuat informasi Akun Wino terbaru.", + "WinoAccount_Management_ActionFailed": "Permintaan Akun Wino tidak dapat diselesaikan.", + "WinoAccount_SettingsSection_Title": "Akun Wino", + "WinoAccount_SettingsSection_Description": "Buat atau masuk ke Akun Wino menggunakan layanan otentikasi localhost Anda.", + "WinoAccount_RegisterButton_Title": "Daftarkan akun", + "WinoAccount_RegisterButton_Description": "Buat Akun Wino dengan email dan kata sandi.", + "WinoAccount_RegisterButton_Action": "Buka pendaftaran", + "WinoAccount_LoginButton_Title": "Masuk", + "WinoAccount_LoginButton_Description": "Masuk ke Akun Wino yang sudah ada dengan email dan kata sandi.", + "WinoAccount_LoginButton_Action": "Buka masuk", + "WinoAccount_SignOutButton_Title": "Keluar", + "WinoAccount_SignOutButton_Description": "Hapus sesi Akun Wino yang tersimpan secara lokal.", + "WinoAccount_SignOutButton_Action": "Keluar", + "WinoAccount_RegisterDialog_Title": "Buat Akun Wino", + "WinoAccount_RegisterDialog_Description": "Buat Akun Wino untuk menjaga sinkronisasi pengalaman Wino Anda dan membuka add-on berbasis akun.", + "WinoAccount_RegisterDialog_HeroTitle": "Buat Akun Wino Anda", + "WinoAccount_RegisterDialog_BenefitsTitle": "Mengapa membuatnya?", + "WinoAccount_RegisterDialog_BenefitSyncTitle": "Impor dan ekspor pengaturan antar perangkat", + "WinoAccount_RegisterDialog_BenefitSyncDescription": "Pindahkan preferensi Wino Anda antar perangkat tanpa membangun ulang pengaturan Anda dari awal.", + "WinoAccount_RegisterDialog_BenefitAiTitle": "Akses add-on eksklusif seperti Wino AI Pack (berbayar)", + "WinoAccount_RegisterDialog_BenefitAiDescription": "Gunakan satu akun untuk membuka fitur premium Wino saat tersedia.", + "WinoAccount_RegisterDialog_DifferenceTitle": "Akun Wino terpisah dari akun email Anda", + "WinoAccount_RegisterDialog_DifferenceDescription": "Outlook, Gmail, IMAP, atau akun email lainnya tetap seperti adanya. Akun Wino hanya mengelola fitur khusus Wino dan add-on berbasis akun.", + "WinoAccount_RegisterDialog_PrimaryButton": "Daftar", + "WinoAccount_RegisterDialog_PrivacyTitle": "Privasi dan pemrosesan API", + "WinoAccount_RegisterDialog_PrivacyDescription": "Add-on opsional seperti Wino AI Pack mungkin mengirim konten HTML email terpilih ke layanan API Wino hanya saat Anda menggunakan fitur tersebut.", + "WinoAccount_RegisterDialog_PrivacyLinkText": "Baca kebijakan privasi", + "WinoAccount_RegisterDialog_PrivacyCheckbox": "Saya setuju dengan kebijakan privasi.", + "WinoAccount_LoginDialog_Title": "Masuk ke Akun Wino", + "WinoAccount_LoginDialog_Description": "Masuk ke Akun Wino Anda untuk menyinkronkan pengaturan Wino Anda dan mengakses fitur berbasis akun.", + "WinoAccount_LoginDialog_HeroTitle": "Selamat datang kembali", + "WinoAccount_LoginDialog_BenefitsTitle": "Apa yang didapat saat masuk", + "WinoAccount_LoginDialog_BenefitsDescription": "Gunakan Akun Wino Anda untuk terus menyinkronkan pengaturan antar perangkat dan mengakses add-on berbayar seperti Wino AI Pack.", + "WinoAccount_LoginDialog_DifferenceTitle": "Ini bukan masuk ke akun email Anda", + "WinoAccount_LoginDialog_DifferenceDescription": "Masuk di sini tidak menambahkan atau menggantikan akun Outlook, Gmail, atau IMAP Anda di Wino. Ini hanya membuat Anda masuk ke layanan khusus Wino.", + "WinoAccount_LoginDialog_ForgotPasswordLink": "Lupa kata sandi?", + "WinoAccount_EmailLabel": "Email", + "WinoAccount_EmailPlaceholder": "name@example.com", + "WinoAccount_PasswordLabel": "Kata sandi", + "WinoAccount_ConfirmPasswordLabel": "Konfirmasi kata sandi", + "WinoAccount_ForgotPasswordDialog_Title": "Atur ulang kata sandi Anda", + "WinoAccount_ForgotPasswordDialog_PrimaryButton": "Kirim email reset", + "WinoAccount_ForgotPasswordDialog_BackToSignIn": "Kembali ke masuk", + "WinoAccount_ForgotPasswordDialog_Description": "Masukkan alamat email Akun Wino Anda dan kami akan mengirimkan tautan reset kata sandi jika alamatnya terdaftar.", + "WinoAccount_Validation_EmailRequired": "Email diperlukan.", + "WinoAccount_Validation_PasswordRequired": "Kata sandi diperlukan.", + "WinoAccount_Validation_PasswordMismatch": "Kata sandi tidak cocok.", + "WinoAccount_Validation_PrivacyConsentRequired": "Anda harus menerima kebijakan privasi sebelum membuat Akun Wino.", + "WinoAccount_Error_InvalidCredentials": "Alamat email atau kata sandi tidak benar.", + "WinoAccount_Error_AccountLocked": "Akun ini dikunci sementara.", + "WinoAccount_Error_AccountBanned": "Akun ini telah diblokir.", + "WinoAccount_Error_AccountSuspended": "Akun ini telah ditangguhkan.", + "WinoAccount_Error_EmailNotConfirmed": "Harap konfirmasi alamat email Anda sebelum masuk.", + "WinoAccount_Error_EmailConfirmationRequired": "Harap konfirmasi alamat email Anda sebelum masuk.", + "WinoAccount_Error_EmailConfirmationResendNotAvailable": "Email konfirmasi baru belum tersedia.", + "WinoAccount_Error_EmailConfirmationResendInvalid": "Permintaan konfirmasi ini tidak lagi valid. Silakan coba masuk kembali.", + "WinoAccount_Error_EmailNotRegistered": "Alamat email ini belum terdaftar.", + "WinoAccount_Error_RefreshTokenInvalid": "Sesi Anda tidak lagi valid. Silakan masuk lagi.", + "WinoAccount_Error_EmailAlreadyRegistered": "Alamat email ini sudah terdaftar.", + "WinoAccount_Error_ExternalLoginEmailRequired": "Sebuah alamat email diperlukan untuk melengkapi masuk eksternal.", + "WinoAccount_Error_ExternalLoginInvalid": "Permintaan masuk eksternal tidak valid.", + "WinoAccount_Error_ExternalAuthStateInvalid": "Status masuk eksternal tidak valid atau kedaluwarsa.", + "WinoAccount_Error_ExternalAuthCodeInvalid": "Kode masuk eksternal tidak valid atau kedaluwarsa.", + "WinoAccount_Error_AiPackRequired": "langganan Wino AI Pack aktif diperlukan untuk tindakan ini.", + "WinoAccount_Error_AiQuotaExceeded": "Batas penggunaan AI Pack Anda telah tercapai untuk periode penagihan saat ini.", + "WinoAccount_Error_AiHtmlEmpty": "Tidak ada isi email yang akan diproses.", + "WinoAccount_Error_AiHtmlTooLarge": "Email ini terlalu besar untuk diproses dengan Wino AI.", + "WinoAccount_Error_AiUnsupportedLanguage": "Bahasa tersebut tidak didukung. Coba kode budaya yang valid seperti en-US atau tr-TR.", + "WinoAccount_Error_Forbidden": "Anda tidak memiliki izin untuk melakukan tindakan ini.", + "WinoAccount_Error_ValidationFailed": "Permintaan tidak valid. Silakan tinjau nilai yang dimasukkan.", + "WinoAccount_RegisterSuccessMessage": "Pendaftaran Akun Wino selesai untuk {0}.", + "WinoAccount_LoginSuccessMessage": "Masuk ke Akun Wino sebagai {0}.", + "WinoAccount_EmailConfirmationSentDialog_Title": "Konfirmasi alamat email Anda", + "WinoAccount_EmailConfirmationSentDialog_Message": "Kami telah mengirim konfirmasi email ke {0}. Harap konfirmasi itu dan coba masuk kembali.", + "WinoAccount_EmailConfirmationPendingDialog_Title": "Diperlukan konfirmasi email", + "WinoAccount_EmailConfirmationPendingDialog_Message": "Kami masih menunggu Anda untuk mengonfirmasi {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ResendButton": "Kirim ulang email konfirmasi", + "WinoAccount_EmailConfirmationPendingDialog_Countdown": "Anda bisa mengirim ulang email konfirmasi dalam {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ReadyToResend": "Anda sekarang bisa mengirim ulang email konfirmasi.", + "WinoAccount_EmailConfirmationResentDialog_Title": "Email konfirmasi telah terkirim ulang", + "WinoAccount_EmailConfirmationResentDialog_Message": "Kami mengirim email konfirmasi lagi ke {0}. Harap konfirmasi itu dan coba masuk kembali.", + "WinoAccount_ForgotPasswordDialog_SuccessTitle": "Email reset kata sandi telah dikirim", + "WinoAccount_ForgotPasswordDialog_SuccessMessage": "Kami telah mengirim email reset kata sandi ke {0}. Buka pesan itu untuk memilih kata sandi baru.", + "WinoAccount_ChangePassword_Title": "Ubah kata sandi", + "WinoAccount_ChangePassword_Description": "Kirim email reset kata sandi ke Akun Wino ini.", + "WinoAccount_ChangePassword_Action": "Kirim email reset", + "WinoAccount_ChangePassword_ConfirmationMessage": "Apakah Anda ingin Wino mengirimkan email reset kata sandi ke {0}?", + "WinoAccount_SignOut_SuccessMessage": "Anda telah keluar dari Akun Wino {0}.", + "WinoAccount_SignOut_NoAccountMessage": "Tidak ada Akun Wino aktif untuk keluar.", + "WinoAccount_Titlebar_SignedOutTitle": "Akun Wino", + "WinoAccount_Titlebar_SignedOutDescription": "Masuk atau buat Akun Wino untuk mengelola sesi Wino Anda.", + "WinoAccount_Titlebar_SignedInStatus": "Status: {0}", + "WelcomeWizard_Step2Title": "Tambahkan Akun", + "WelcomeWizard_Step3Title": "Selesaikan Pengaturan", + "ProviderSelection_Title": "Pilih penyedia email Anda", + "ProviderSelection_Subtitle": "Pilih penyedia di bawah ini untuk menambahkan akun email Anda ke Wino Mail.", + "ProviderSelection_AccountNameHeader": "Nama Akun", + "ProviderSelection_AccountNamePlaceholder": "contoh: Pribadi, Kerja", + "ProviderSelection_DisplayNameHeader": "Nama Tampilan", + "ProviderSelection_DisplayNamePlaceholder": "contoh: John Doe", + "ProviderSelection_EmailHeader": "Alamat Email", + "ProviderSelection_EmailPlaceholder": "contoh: johndoe@example.com", + "ProviderSelection_AppPasswordHeader": "Kata Sandi Khusus Aplikasi", + "ProviderSelection_AppPasswordHelp": "Bagaimana cara mendapatkan kata sandi khusus aplikasi?", + "ProviderSelection_CalendarModeHeader": "Integrasi Kalender", + "ProviderSelection_CalendarMode_DisabledTitle": "Dinonaktifkan", + "ProviderSelection_CalendarMode_DisabledDescription": "Tidak ada integrasi kalender", + "ProviderSelection_CalendarMode_CalDavTitle": "Sinkronisasi CalDAV", + "ProviderSelection_CalendarMode_CalDavDescription_Apple": "Peristiwa kalender Anda disinkronkan ke server Apple di antara perangkat Anda.", + "ProviderSelection_CalendarMode_CalDavDescription_Yahoo": "Peristiwa kalender Anda disinkronkan ke server Yahoo di antara perangkat Anda.", + "ProviderSelection_CalendarMode_LocalTitle": "Kalender lokal", + "ProviderSelection_CalendarMode_LocalDescription": "Acara Anda disimpan hanya di komputer Anda. Tidak ada koneksi ke server.", + "ProviderSelection_ClearColor": "Hapus warna", + "ProviderSelection_ContinueButton": "Lanjutkan", + "ProviderSelection_SpecialImap_Subtitle": "Masukkan kredensial akun Anda untuk terhubung.", + "AccountSetup_Title": "Mengatur Akun Anda", + "AccountSetup_Step_Authenticating": "Mengotentikasi dengan {0}", + "AccountSetup_Step_TestingMailAuth": "Menguji autentikasi email", + "AccountSetup_Step_SyncingFolders": "Sinkronisasi metadata folder", + "AccountSetup_Step_FetchingProfile": "Mengambil informasi profil", + "AccountSetup_Step_DiscoveringCalDav": "Menemukan pengaturan CalDAV", + "AccountSetup_Step_TestingCalendarAuth": "Menguji autentikasi kalender", + "AccountSetup_Step_SavingAccount": "Menyimpan informasi akun", + "AccountSetup_Step_FetchingCalendarMetadata": "Mengambil metadata kalender", + "AccountSetup_Step_SyncingAliases": "Sinkronisasi alias", + "AccountSetup_Step_Finalizing": "Menyelesaikan pengaturan", + "AccountSetup_FailureMessage": "Pengaturan gagal. Kembali ke sebelumnya untuk memperbaiki pengaturan Anda, atau coba lagi nanti.", + "AccountSetup_SuccessMessage": "Akun Anda telah berhasil diatur!", + "AccountSetup_GoBackButton": "Kembali", + "AccountSetup_TryAgainButton": "Coba Lagi", + "ImapCalDavSettings_AutoDiscoveryFailed": "Penemuan otomatis gagal. Silakan masukkan pengaturan secara manual di tab Lanjutan." } - - diff --git a/Wino.Core.Domain/Translations/it_IT/resources.json b/Wino.Core.Domain/Translations/it_IT/resources.json index 2bc3c650..12080a54 100644 --- a/Wino.Core.Domain/Translations/it_IT/resources.json +++ b/Wino.Core.Domain/Translations/it_IT/resources.json @@ -6,26 +6,32 @@ "AccountAlias_Disclaimer_SecondLine": "Se vuoi usare alias per il tuo account Outlook o IMAP, aggiungili da te.", "AccountCacheReset_Title": "Ripristino cache account", "AccountCacheReset_Message": "Questo account richiede la ri-sincronizzazione completa per continuare a funzionare. Attendere mentre Wino ri-sincronizza i messaggi…", - "AccountContactNameYou": "Te", - "AccountCreationDialog_Completed": "tutto fatto", - "AccountCreationDialog_FetchingEvents": "Eventi calendario in recupero.", - "AccountCreationDialog_FetchingProfileInformation": "Dati del profilo in recupero.", + "AccountContactNameYou": "Tu", + "AccountCreationDialog_Completed": "fatto", + "AccountCreationDialog_FetchingCalendarMetadata": "Recupero dettagli del calendario.", + "AccountCreationDialog_FetchingEvents": "Recupero eventi calendario.", + "AccountCreationDialog_FetchingProfileInformation": "Recupero dettagli profilo.", "AccountCreationDialog_GoogleAuthHelpClipboardText_Row0": "Se il browser non è stato avviato automaticamente per completare l'autenticazione:", "AccountCreationDialog_GoogleAuthHelpClipboardText_Row1": "1) Clicca il pulsante qui sotto per copiare l'indirizzo di autenticazione", - "AccountCreationDialog_GoogleAuthHelpClipboardText_Row2": "2) Avvia il tuo browser web (Edge, Chrome, Firefox ecc…)", + "AccountCreationDialog_GoogleAuthHelpClipboardText_Row2": "2) Avvia il tuo browser web (Edge, Chrome, Firefox ecc...)", "AccountCreationDialog_GoogleAuthHelpClipboardText_Row3": "3) Incolla l'indirizzo copiato e vai al sito web per completare l'autenticazione manualmente.", "AccountCreationDialog_Initializing": "inizializzazione", "AccountCreationDialog_PreparingFolders": "Stiamo ricevendo informazioni sulle cartelle al momento.", - "AccountCreationDialog_SigninIn": "Informazioni dell'account in salvataggio.", + "AccountCreationDialog_SigninIn": "Le informazioni dell'account sono in fase di salvataggio.", + "Purchased": "Acquistato", "AccountEditDialog_Message": "Nome account", "AccountEditDialog_Title": "Modifica account", "AccountPickerDialog_Title": "Scegli un account", - "AccountSettingsDialog_AccountName": "Nome visualizzato del mittente", + "AccountSettingsDialog_AccountName": "Nome visualizzato mittente", "AccountSettingsDialog_AccountNamePlaceholder": "es. Mario Rossi", "AccountDetailsPage_Title": "Informazioni account", "AccountDetailsPage_Description": "Modifica il nome dell'account in Wino e imposta il nome del mittente desiderato.", "AccountDetailsPage_ColorPicker_Title": "Colore account", "AccountDetailsPage_ColorPicker_Description": "Assegna un nuovo colore all'account per colorare il suo simbolo nella lista.", + "AccountDetailsPage_TabGeneral": "Generale", + "AccountDetailsPage_TabMail": "Posta", + "AccountDetailsPage_TabCalendar": "Calendario", + "AccountDetailsPage_CalendarListDescription": "Seleziona un calendario per configurarne le impostazioni.", "AddHyperlink": "Aggiungi", "AppCloseBackgroundSynchronizationWarningTitle": "Sincronizzazione dietro le quinte", "AppCloseStartupLaunchDisabledWarningMessageFirstLine": "L'applicazione non è stata impostata per avviarsi all'avvio di Windows.", @@ -33,22 +39,24 @@ "AppCloseStartupLaunchDisabledWarningMessageThirdLine": "Vuoi andare alla pagina Preferenze dell'app per abilitarla?", "AppCloseTerminateBehaviorWarningMessageFirstLine": "Stai terminando Wino Mail e il comportamento di chiusura dell'app è impostato su 'Termina'.", "AppCloseTerminateBehaviorWarningMessageSecondLine": "Questo fermerà tutte le sincronizzazioni e le notifiche dietro le quinte.", - "AppCloseTerminateBehaviorWarningMessageThirdLine": "Vuoi andare alle preferenze dell'app per impostare Wino Mail per essere eseguito minimizzato o dietro le quinte?", - "AutoDiscoveryProgressMessage": "Ricerca impostazioni posta…", + "AppCloseTerminateBehaviorWarningMessageThirdLine": "Vuoi andare alle preferenze dell'app per impostare Wino Mail da eseguire minimizzato o dietro le quinte?", + "AutoDiscoveryProgressMessage": "Ricerca delle impostazioni della posta...", "BasicIMAPSetupDialog_AdvancedConfiguration": "Configurazione avanzata", - "BasicIMAPSetupDialog_CredentialLocalMessage": "Le tue credenziali saranno memorizzate solo localmente nel tuo computer.", + "BasicIMAPSetupDialog_CredentialLocalMessage": "Le tue credenziali verranno memorizzate solo localmente sul tuo computer.", "BasicIMAPSetupDialog_Description": "Alcuni account richiedono fasi aggiuntive per accedere", "BasicIMAPSetupDialog_DisplayName": "Nome visualizzato", "BasicIMAPSetupDialog_DisplayNamePlaceholder": "es. Mario Rossi", "BasicIMAPSetupDialog_LearnMore": "Scopri di più", "BasicIMAPSetupDialog_MailAddress": "Indirizzo e-mail", - "BasicIMAPSetupDialog_MailAddressPlaceholder": "mariorossi@esempio.com", + "BasicIMAPSetupDialog_MailAddressPlaceholder": "johndoe@fabrikam.com", "BasicIMAPSetupDialog_Password": "Parola d'accesso", "BasicIMAPSetupDialog_Title": "Account IMAP", "Busy": "Occupato", "Buttons_AddAccount": "Aggiungi account", + "Buttons_FixAccount": "Ripara account", "Buttons_AddNewAlias": "Aggiungi nuovo alias", "Buttons_Allow": "Consenti", + "Buttons_Apply": "Applica", "Buttons_ApplyTheme": "Applica tema", "Buttons_Browse": "Sfoglia", "Buttons_Cancel": "Annulla", @@ -60,8 +68,9 @@ "Buttons_Deny": "Nega", "Buttons_Discard": "Scarta", "Buttons_Edit": "Modifica", - "Buttons_EnableImageRendering": "Abilita", + "Buttons_EnableImageRendering": "Attiva", "Buttons_Multiselect": "Seleziona multipli", + "Buttons_Manage": "Gestisci", "Buttons_No": "No", "Buttons_Open": "Apri", "Buttons_Purchase": "Acquista", @@ -70,46 +79,168 @@ "Buttons_Save": "Salva", "Buttons_SaveConfiguration": "Salva configurazione", "Buttons_Send": "Invia", + "Buttons_SendToServer": "Invia al server", "Buttons_Share": "Condividi", "Buttons_SignIn": "Accedi", "Buttons_Sync": "Sincronizza", "Buttons_SyncAliases": "Sincronizza alias", - "Buttons_TryAgain": "Tenta ancora", + "Buttons_TryAgain": "Riprova", "Buttons_Yes": "Sì", + "Sync_SynchronizingFolder": "Sincronizzazione della cartella {0} {1}%", + "Sync_DownloadedMessages": "Scaricati {0} messaggi da {1}", + "SyncAction_Archiving": "Archiviazione di {0} messaggi", + "SyncAction_ClearingFlag": "Rimuovere flag da {0} messaggi", + "SyncAction_CreatingDraft": "Creazione bozza", + "SyncAction_CreatingEvent": "Creazione evento", + "SyncAction_Deleting": "Eliminazione di {0} messaggi", + "SyncAction_EmptyingFolder": "Svuotamento della cartella", + "SyncAction_MarkingAsRead": "Contrassegnando {0} messaggi come letti", + "SyncAction_MarkingAsUnread": "Contrassegnando {0} messaggi come non letti", + "SyncAction_MarkingFolderAsRead": "Contrassegnando la cartella come letta", + "SyncAction_Moving": "Spostamento di {0} messaggi", + "SyncAction_MovingToFocused": "Spostamento di {0} messaggi in Concentrato", + "SyncAction_RenamingFolder": "Rinomina cartella", + "SyncAction_SendingMail": "Invio di posta", + "SyncAction_SettingFlag": "Contrassegno di {0} messaggi", + "SyncAction_SynchronizingAccount": "Sincronizzazione di {0}", + "SyncAction_SynchronizingAccounts": "Sincronizzazione di {0} account", + "SyncAction_SynchronizingCalendarData": "Sincronizzazione dei dati del calendario", + "SyncAction_SynchronizingCalendarEvents": "Sincronizzazione degli eventi del calendario", + "SyncAction_SynchronizingCalendarMetadata": "Sincronizzazione dei metadati del calendario", + "SyncAction_Unarchiving": "Estrazione dall'archivio di {0} messaggi", "CalendarAllDayEventSummary": "eventi giornalieri", "CalendarDisplayOptions_Color": "Colore", "CalendarDisplayOptions_Expand": "Espandi", + "CalendarEventResponse_Accept": "Accetta", + "CalendarEventResponse_AcceptedResponse": "Accettato", + "CalendarEventResponse_Decline": "Rifiuta", + "CalendarEventResponse_DeclinedResponse": "Rifiutato", + "CalendarEventResponse_NotResponded": "Non risposto", + "CalendarEventResponse_Tentative": "Provvisorio", + "CalendarEventResponse_TentativeResponse": "Provvisorio", + "CalendarEventRsvpPanel_Accept": "Accetta", + "CalendarEventRsvpPanel_AddMessage": "Aggiungi un messaggio alla tua risposta... (facoltativo)", + "CalendarEventRsvpPanel_Decline": "Rifiuta", + "CalendarEventRsvpPanel_Message": "Messaggio", + "CalendarEventRsvpPanel_SendReplyMessage": "Invia una risposta", + "CalendarEventRsvpPanel_Tentative": "Provvisorio", + "CalendarEventRsvpPanel_Title": "Opzioni di risposta", + "CalendarAttendeeStatus_Accepted": "Accettato", + "CalendarAttendeeStatus_Declined": "Rifiutato", + "CalendarAttendeeStatus_NeedsAction": "Richiede azione", + "CalendarAttendeeStatus_Tentative": "Provvisorio", + "CalendarEventDetails_Attachments": "Allegati", + "CalendarEventCompose_AddAttachment": "Aggiungi allegato", + "CalendarEventCompose_AllDay": "Tutto il giorno", + "CalendarEventCompose_AttachmentsNotSupportedForCalDav": "Gli allegati non sono supportati per i calendari CalDAV.", + "CalendarEventCompose_EndDate": "Data di fine", + "CalendarEventCompose_EndTime": "Orario di fine", + "CalendarEventCompose_Every": "ogni", + "CalendarEventCompose_ForWeekdays": "per", + "CalendarEventCompose_FrequencyDay": "giorno", + "CalendarEventCompose_FrequencyDayPlural": "giorni", + "CalendarEventCompose_FrequencyMonth": "mese", + "CalendarEventCompose_FrequencyMonthPlural": "mesi", + "CalendarEventCompose_FrequencyWeek": "settimana", + "CalendarEventCompose_FrequencyWeekPlural": "settimane", + "CalendarEventCompose_FrequencyYear": "anno", + "CalendarEventCompose_FrequencyYearPlural": "anni", + "CalendarEventCompose_Location": "Luogo", + "CalendarEventCompose_LocationPlaceholder": "Aggiungi luogo", + "CalendarEventCompose_NewEventButton": "Nuovo evento", + "CalendarEventCompose_DefaultCalendarHint": "Puoi scegliere un calendario predefinito per i nuovi eventi nelle impostazioni del Calendario.", + "CalendarEventCompose_DefaultCalendarSettingsLink": "Apri impostazioni del Calendario", + "CalendarEventCompose_NoCalendarsMessage": "Non ci sono calendari disponibili per la creazione di eventi al momento.", + "CalendarEventCompose_NoCalendarsTitle": "Nessun calendario disponibile", + "CalendarEventCompose_NoEndDate": "Nessuna data di fine", + "CalendarEventCompose_Notes": "Note", + "CalendarEventCompose_PickCalendarTitle": "Scegli un calendario", + "CalendarEventCompose_Recurring": "Ricorrente", + "CalendarEventCompose_RecurringSummary": "Si verifica ogni {0} {1}{2} {3} effettivo {4}{5}", + "CalendarEventCompose_RecurringSummarySmart": "Si verifica {0}{1} {2} effettivo {3}{4}", + "CalendarEventCompose_RepeatEvery": "Ripeti ogni", + "CalendarEventCompose_SelectCalendar": "Seleziona calendario", + "CalendarEventCompose_SingleOccurrenceSummary": "Si verifica il {0} {1}", + "CalendarEventCompose_StartDate": "Data di inizio", + "CalendarEventCompose_StartTime": "Orario di inizio", + "CalendarEventCompose_TimeRangeSummary": "da {0} a {1}", + "CalendarEventCompose_Title": "Titolo evento", + "CalendarEventCompose_TitlePlaceholder": "Aggiungi un titolo", + "CalendarEventCompose_Until": "fino a", + "CalendarEventCompose_UntilSummary": " fino a {0}", + "CalendarEventCompose_ValidationInvalidAllDayRange": "La data di fine dell'intera giornata deve essere successiva a quella di inizio.", + "CalendarEventCompose_ValidationInvalidAttendee": "Uno o più partecipanti hanno un indirizzo email non valido.", + "CalendarEventCompose_ValidationInvalidRecurrenceEnd": "La data di fine della ricorrenza deve essere uguale o successiva alla data di inizio dell'evento.", + "CalendarEventCompose_ValidationInvalidTimeRange": "L'orario di fine deve essere successivo all'orario di inizio.", + "CalendarEventCompose_ValidationMissingAttachment": "Uno o più allegati non sono più disponibili: {0}", + "CalendarEventCompose_ValidationMissingCalendar": "Seleziona un calendario prima di creare l'evento.", + "CalendarEventCompose_ValidationMissingTitle": "Inserisci un titolo per l'evento prima di crearlo.", + "CalendarEventCompose_ValidationTitle": "Validazione dell'evento fallita", + "CalendarEventCompose_WeekdaySummary": " il {0}", + "CalendarEventCompose_Weekday_Friday": "V", + "CalendarEventCompose_Weekday_Monday": "L", + "CalendarEventCompose_Weekday_Saturday": "Sab", + "CalendarEventCompose_Weekday_Sunday": "Dom", + "CalendarEventCompose_Weekday_Thursday": "Gio", + "CalendarEventCompose_Weekday_Tuesday": "Mar", + "CalendarEventCompose_Weekday_Wednesday": "Mer", + "CalendarEventDetails_Details": "Dettagli", + "CalendarEventDetails_EditSeries": "Modifica serie", + "CalendarEventDetails_Editing": "Modifica in corso", + "CalendarEventDetails_InviteSomeone": "Invita qualcuno", + "CalendarEventDetails_JoinOnline": "Partecipa online", + "CalendarEventDetails_Organizer": "Organizzatore", + "CalendarEventDetails_People": "Persone", + "CalendarEventDetails_ReadOnlyEvent": "Evento di sola lettura", + "CalendarEventDetails_Reminder": "Promemoria", + "CalendarReminder_StartedHoursAgo": "Iniziato {0} ore fa", + "CalendarReminder_StartedMinutesAgo": "Iniziato {0} minuti fa", + "CalendarReminder_StartedNow": "Iniziato ora", + "CalendarReminder_StartingNow": "Inizio ora", + "CalendarReminder_StartsInHours": "Inizia tra {0} ore", + "CalendarReminder_StartsInMinutes": "Inizia tra {0} minuti", + "CalendarReminder_SnoozeAction": "Rimanda", + "CalendarReminder_SnoozeMinutesOption": "{0} minuti", + "CalendarEventDetails_ShowAs": "Mostra come", + "CalendarShowAs_Free": "Disponibile", + "CalendarShowAs_Tentative": "Provvisorio", + "CalendarShowAs_Busy": "Occupato", + "CalendarShowAs_OutOfOffice": "Fuori dall'ufficio", + "CalendarShowAs_WorkingElsewhere": "Lavorando altrove", "CalendarItem_DetailsPopup_JoinOnline": "Unisciti in rete", "CalendarItem_DetailsPopup_ViewEventButton": "Vedi evento", "CalendarItem_DetailsPopup_ViewSeriesButton": "Vedi serie", - "CalendarItemAllDay": "in giornata", + "CalendarItemAllDay": "la giornata", "CategoriesFolderNameOverride": "Categorie", - "Center": "Centro", + "Center": "Centra", "ClipboardTextCopied_Message": "{0} copiato negli Appunti.", "ClipboardTextCopied_Title": "Copiato", - "ClipboardTextCopyFailed_Message": "Impossibile copiare {0} negli Appunti.", - "ComingSoon": "In arrivo…", + "ClipboardTextCopyFailed_Message": "Impossibile copiare {0} negli Aappunti.", + "ContactInfoBar_ErrorTitle": "Impossibile caricare le informazioni di contatto", + "ContactInfoBar_SuccessTitle": "Informazioni di contatto caricate", + "ContactInfoBar_WarningTitle": "Le informazioni di contatto potrebbero essere incomplete", + "ComingSoon": "Prossimamente...", "ComposerAttachmentsDragDropAttach_Message": "Allega", - "ComposerAttachmentsDropZone_Message": "Rilascia i tuoi file qui", + "ComposerAttachmentsDropZone_Message": "Trascina qui i tuoi file", "ComposerFrom": "Da: ", - "ComposerImagesDropZone_Message": "Rilascia le tue immagini qui", + "ComposerImagesDropZone_Message": "Trascina qui le tue immagini", "ComposerSubject": "Oggetto: ", "ComposerTo": "A: ", - "ComposerToPlaceholder": "batti Invio per immettere gli indirizzi", + "ComposerToPlaceholder": "clicca Invio per inserire gli indirizzi", "CreateAccountAliasDialog_AliasAddress": "Indirizzo", "CreateAccountAliasDialog_AliasAddressPlaceholder": "es. support@miodominio.com", - "CreateAccountAliasDialog_Description": "Assicurati che il tuo server in uscita permetta l'invio di e-mail da questo alias.", + "CreateAccountAliasDialog_Description": "Assicurati che il tuo server in uscita permetta di inviare e-mail da questo alias.", "CreateAccountAliasDialog_ReplyToAddress": "Indirizzo per le risposte", "CreateAccountAliasDialog_ReplyToAddressPlaceholder": "admin@miodominio.com", "CreateAccountAliasDialog_Title": "Crea alias dell'account", "CustomThemeBuilder_AccentColorDescription": "Imposta un colore principale personalizzato, se vuoi. Non selezionando un colore, sarà usato il colore principale di Windows.", "CustomThemeBuilder_AccentColorTitle": "Colore principale", "CustomThemeBuilder_PickColor": "Scegli", - "CustomThemeBuilder_ThemeNameDescription": "Nome unico del tuo tema personale.", + "CustomThemeBuilder_ThemeNameDescription": "Nome unico del tuo tema personalizzato.", "CustomThemeBuilder_ThemeNameTitle": "Nome tema", "CustomThemeBuilder_Title": "Creatore temi personalizzati", - "CustomThemeBuilder_WallpaperDescription": "Imposta uno sfondo personale per Wino", - "CustomThemeBuilder_WallpaperTitle": "Imposta sfondo personale", + "CustomThemeBuilder_WallpaperDescription": "Imposta uno sfondo personalizzato per Wino", + "CustomThemeBuilder_WallpaperTitle": "Imposta sfondo personalizzato", "Dialog_DontAskAgain": "Non chiedere più", "DialogMessage_AccountLimitMessage": "Hai raggiunto il limite di creazione degli account.\nVuoi acquistare il componente aggiuntivo 'Account illimitati' per continuare?", "DialogMessage_AccountLimitTitle": "Raggiunto limite account", @@ -121,7 +252,7 @@ "DialogMessage_AliasNotSelectedTitle": "Alias mancante", "DialogMessage_CantDeleteRootAliasMessage": "L'alias radice non può essere eliminato. Questa è la tua identità principale associata alla configurazione del tuo account.", "DialogMessage_CantDeleteRootAliasTitle": "Impossibile eliminare alias", - "DialogMessage_CleanupFolderMessage": "Vuoi eliminare definitivamente tutte le e-mail in questa cartella?", + "DialogMessage_CleanupFolderMessage": "Vuoi eliminare definitivamente tutti i messaggi in questa cartella?", "DialogMessage_CleanupFolderTitle": "Svuota cartella", "DialogMessage_ComposerMissingRecipientMessage": "Il messaggio non ha destinatari.", "DialogMessage_ComposerValidationFailedTitle": "Convalida non riuscita", @@ -129,23 +260,27 @@ "DialogMessage_CreateLinkedAccountTitle": "Nome account collegato", "DialogMessage_DeleteAccountConfirmationMessage": "Eliminare {0}?", "DialogMessage_DeleteAccountConfirmationTitle": "I dati associati a questo account saranno permanentemente eliminati dal disco.", + "DialogMessage_DeleteEmailTemplateConfirmationMessage": "Elimina il modello '{0}'?", + "DialogMessage_DeleteEmailTemplateConfirmationTitle": "Elimina modello di email", + "DialogMessage_DeleteRecurringSeriesMessage": "Questo eliminerà tutti gli eventi della serie. Vuoi continuare?", + "DialogMessage_DeleteRecurringSeriesTitle": "Elimina serie ricorrente", "DialogMessage_DiscardDraftConfirmationMessage": "Questa bozza sarà scartata. Vuoi continuare?", "DialogMessage_DiscardDraftConfirmationTitle": "Scarta bozza", "DialogMessage_EmptySubjectConfirmation": "Oggetto mancante", "DialogMessage_EmptySubjectConfirmationMessage": "Il messaggio non ha oggetto. Vuoi continuare?", - "DialogMessage_EnableStartupLaunchDeniedMessage": "È possibile abilitare l'auto-avvio da Impostazioni -> Preferenze app.", + "DialogMessage_EnableStartupLaunchDeniedMessage": "È possibile abilitare l'avvio da Impostazioni -> Preferenze app.", "DialogMessage_EnableStartupLaunchMessage": "Permetti a Wino Mail di lanciarsi automaticamente minimizzato all'avvio di Windows per non perdere alcuna notifica.\n\nVuoi abilitare il lancio all'avvio?", "DialogMessage_EnableStartupLaunchTitle": "Abilita auto-avvio", "DialogMessage_HardDeleteConfirmationMessage": "Eliminazione permanente", "DialogMessage_HardDeleteConfirmationTitle": "I messaggi saranno eliminati definitivamente. Vuoi continuare?", "DialogMessage_InvalidAliasMessage": "Questo alias non è valido. Assicurati che tutti gli indirizzi dell'alias siano indirizzi e-mail validi.", "DialogMessage_InvalidAliasTitle": "Alias non valido", - "DialogMessage_NoAccountsForCreateMailMessage": "Non hai alcun account dal quale creare messaggi.", + "DialogMessage_NoAccountsForCreateMailMessage": "Non hai alcun account per creare messaggi.", "DialogMessage_NoAccountsForCreateMailTitle": "Account mancante", - "DialogMessage_PrintingFailedMessage": "Impossibile stampare di quest'e-mail. Risultato: {0}", + "DialogMessage_PrintingFailedMessage": "Stampa di questa e-mail non riuscita. Risultato: {0}", "DialogMessage_PrintingFailedTitle": "Non riuscita", "DialogMessage_PrintingSuccessMessage": "La posta è inviata alla stampante.", - "DialogMessage_PrintingSuccessTitle": "Riuscito", + "DialogMessage_PrintingSuccessTitle": "Fatto", "DialogMessage_RenameFolderMessage": "Inserisci un nuovo nome per questa cartella", "DialogMessage_RenameFolderTitle": "Rinomina cartella", "DialogMessage_RenameLinkedAccountsMessage": "Inserisci un nuovo nome per l'account collegato", @@ -153,7 +288,7 @@ "DialogMessage_UnlinkAccountsConfirmationMessage": "Questa operazione non eliminerà i tuoi account, ma interromperà solo il collegamento verso le connessioni alla cartella condivisa. Vuoi continuare?", "DialogMessage_UnlinkAccountsConfirmationTitle": "Scollega account", "DialogMessage_UnsubscribeConfirmationGoToWebsiteConfirmButton": "Vai al sito", - "DialogMessage_UnsubscribeConfirmationGoToWebsiteMessage": "Per smettere di ricevere messaggi da {0}, vai al sito web per disiscriverti.", + "DialogMessage_UnsubscribeConfirmationGoToWebsiteMessage": "Per smettere di ricevere messaggi da {0}, vai sul loro sito web per disiscriverti.", "DialogMessage_UnsubscribeConfirmationMailtoMessage": "Vuoi smettere di ricevere messaggi da {0}? Wino annullerà la tua iscrizione inviando una e-mail dal tuo account e-mail a {1}.", "DialogMessage_UnsubscribeConfirmationOneClickMessage": "Vuoi smettere di ricevere messaggi da {0}?", "DialogMessage_UnsubscribeConfirmationTitle": "Disiscriviti", @@ -167,16 +302,23 @@ "EditorToolbarOption_None": "Nessuno", "EditorToolbarOption_Options": "Opzioni", "EditorTooltip_WebViewEditor": "Usa editor di visualizzazione web", - "ElementTheme_Dark": "Modalità scura", + "ElementTheme_Dark": "Tema scuro", "ElementTheme_Default": "Usa le impostazioni di sistema", - "ElementTheme_Light": "Modalità chiara", + "ElementTheme_Light": "Tema chiaro", "Emoji": "Emoji", "Error_FailedToSetupSystemFolders_Title": "Impossibile configurare le cartelle di sistema", + "Exception_AccountNeedsAttention_Title": "L'account richiede attenzione", + "Exception_AccountNeedsAttention_Message": "'{0}' richiede la tua attenzione per continuare a lavorare.", + "Exception_WebView2RuntimeMissing_Message": "Wino Mail non è riuscito a trovare Microsoft Edge WebView2 Runtime. Si prega di installare o riparare il runtime per rendere correttamente il contenuto dei messaggi.", + "Exception_WebView2RuntimeMissing_Title": "Il runtime WebView2 è richiesto", "Exception_AuthenticationCanceled": "Autenticazione annullata", "Exception_CustomThemeExists": "Questo tema esiste già.", "Exception_CustomThemeMissingName": "Devi fornire un nome.", - "Exception_CustomThemeMissingWallpaper": "Devi fornire un'immagine di sfondo personale.", + "Exception_CustomThemeMissingWallpaper": "Devi fornire un'immagine di sfondo personalizzata.", "Exception_FailedToSynchronizeAliases": "Impossibile sincronizzare gli alias", + "Exception_FailedToSynchronizeCalendarData": "Impossibile sincronizzare i dati del calendario", + "Exception_FailedToSynchronizeCalendarEvents": "Impossibile sincronizzare gli eventi del calendario", + "Exception_FailedToSynchronizeCalendarMetadata": "Impossibile sincronizzare i metadati del calendario", "Exception_FailedToSynchronizeFolders": "Sincronizzazione cartelle non riuscita", "Exception_FailedToSynchronizeProfileInformation": "Impossibile sincronizzare le informazioni del profilo", "Exception_GoogleAuthCallbackNull": "Il Callback uri è nullo all'attivazione.", @@ -229,6 +371,32 @@ "HoverActionOption_MoveJunk": "Sposta in Indesiderata", "HoverActionOption_ToggleFlag": "Contrassegno sì/no", "HoverActionOption_ToggleRead": "Già letto / Da leggere", + "KeyboardShortcuts_FailedToReset": "Impossibile reimpostare le scorciatoie da tastiera.", + "KeyboardShortcuts_FailedToUpdate": "Impossibile aggiornare le scorciatoie da tastiera", + "KeyboardShortcuts_MailoperationAction": "Azione", + "KeyboardShortcuts_Action": "Azione", + "KeyboardShortcuts_FailedToLoad": "Impossibile caricare le scorciatoie da tastiera.", + "KeyboardShortcuts_EnterKeyForShortcut": "Inserisci una combinazione di tasti per la scorciatoia.", + "KeyboardShortcuts_SelectOperationForShortcut": "Seleziona un'azione da associare alla scorciatoia.", + "KeyboardShortcuts_EnterKey": "Premi un tasto per la scorciatoia.", + "KeyboardShortcuts_SelectOperation": "Seleziona un'azione per la scorciatoia.", + "KeyboardShortcuts_ShortcutInUse": "Questa scorciatoia è già in uso da un'altra scorciatoia.", + "KeyboardShortcuts_FailedToSave": "Impossibile salvare la scorciatoia.", + "KeyboardShortcuts_FailedToDelete": "Impossibile eliminare la scorciatoia.", + "KeyboardShortcuts_PageDescription": "Configura scorciatoie da tastiera per operazioni rapide sulla posta. Premi i tasti mentre è focalizzato sul campo di input della scorciatoia per catturare le combinazioni.", + "KeyboardShortcuts_Add": "Aggiungi scorciatoia", + "KeyboardShortcuts_EditTitle": "Modifica scorciatoia da tastiera", + "KeyboardShortcuts_ResetToDefaults": "Ripristina impostazioni predefinite", + "KeyboardShortcuts_PressKeysHere": "Premi qui i tasti...", + "KeyboardShortcuts_KeyCombination": "Combinazione di tasti", + "KeyboardShortcuts_FocusArea": "Porta il focus sul campo qui sopra e premi la combinazione di tasti desiderata", + "KeyboardShortcuts_Modifiers": "Tasti modificatori", + "KeyboardShortcuts_Mode": "Modalità dell'app", + "KeyboardShortcuts_ModeMail": "Posta", + "KeyboardShortcuts_ModeCalendar": "Calendario", + "KeyboardShortcuts_ActionToggleReadUnread": "Alterna letto/non letto", + "KeyboardShortcuts_ActionToggleFlag": "Alterna flag", + "KeyboardShortcuts_ActionToggleArchive": "Archivia/disarchivia", "ImageRenderingDisabled": "La visualizzazione dell'immagine è disabilitata per questo messaggio.", "ImapAdvancedSetupDialog_AuthenticationMethod": "Metodo di autenticazione", "ImapAdvancedSetupDialog_ConnectionSecurity": "Sicurezza della connessione", @@ -295,12 +463,58 @@ "IMAPSetupDialog_Username": "Nome utente", "IMAPSetupDialog_UsernamePlaceholder": "johndoe, johndoe@fabrikam.com, domain/johndoe", "IMAPSetupDialog_UseSameConfig": "Usa lo stesso nome utente e la stessa parola d'accesso per l'invio di e-mail", + "ImapCalDavSettingsPage_TitleCreate": "IMAP e Calendario", + "ImapCalDavSettingsPage_TitleEdit": "Modifica impostazioni IMAP e calendario", + "ImapCalDavSettingsPage_Subtitle": "Configura IMAP/SMTP e la sincronizzazione opzionale del calendario per questo account.", + "ImapCalDavSettingsPage_BasicSectionTitle": "Impostazioni di base", + "ImapCalDavSettingsPage_BasicSectionDescription": "Inserisci la tua identità e le credenziali. Wino può tentare di rilevare automaticamente le impostazioni del server.", + "ImapCalDavSettingsPage_BasicTab": "Base", + "ImapCalDavSettingsPage_EnableCalendarSupport": "Attiva supporto calendario", + "ImapCalDavSettingsPage_AutoDiscoverButton": "Scoperta automatica delle impostazioni di posta", + "ImapCalDavSettingsPage_AutoDiscoverySuccessMessage": "Impostazioni della posta rilevate e applicate.", + "ImapCalDavSettingsPage_AdvancedSectionTitle": "Configurazione avanzata", + "ImapCalDavSettingsPage_AdvancedSectionDescription": "Inserire manualmente le impostazioni del server se la scoperta automatica non è disponibile o è errata.", + "ImapCalDavSettingsPage_AdvancedTab": "Avanzate", + "ImapCalDavSettingsPage_CalendarSectionTitle": "Configurazione calendario", + "ImapCalDavSettingsPage_CalendarSectionDescription": "Scegli come i dati del calendario dovrebbero operare per questo account IMAP.", + "ImapCalDavSettingsPage_CalendarModeHeader": "Modalità calendario", + "ImapCalDavSettingsPage_ConnectionSecurityHeader": "Sicurezza connessione", + "ImapCalDavSettingsPage_AuthenticationMethodHeader": "Metodo di autenticazione", + "ImapCalDavSettingsPage_CalendarModeDisabled": "Disabilitato", + "ImapCalDavSettingsPage_CalendarModeCalDav": "Sincronizzazione CalDAV", + "ImapCalDavSettingsPage_CalendarModeLocalOnly": "Solo calendario locale", + "ImapCalDavSettingsPage_CalendarModeDisabledDescription": "Il calendario è disabilitato per questo account.", + "ImapCalDavSettingsPage_CalendarModeCalDavDescription": "Gli elementi del calendario sono sincronizzati con il tuo server CalDAV.", + "ImapCalDavSettingsPage_CalendarModeLocalOnlyDescription": "Gli elementi del calendario sono archiviati solo sul tuo computer e non sono sincronizzati in rete.", + "ImapCalDavSettingsPage_LocalCalendarLearnMore": "Come funziona il calendario locale", + "ImapCalDavSettingsPage_LocalCalendarDialogTitle": "Solo calendario locale", + "ImapCalDavSettingsPage_LocalCalendarDialogMessage": "Il calendario locale mantiene tutti gli eventi solo sul tuo computer. Nulla viene sincronizzato con iCloud, Yahoo o altri fornitori.", + "ImapCalDavSettingsPage_CalDavServiceUrl": "URL del servizio CalDAV", + "ImapCalDavSettingsPage_CalDavUsername": "Nome utente CalDAV", + "ImapCalDavSettingsPage_CalDavPassword": "Password CalDAV", + "ImapCalDavSettingsPage_CalDavNotRequiredMessage": "Il test CalDAV è richiesto solo quando la modalità calendario è impostata su sincronizzazione CalDAV.", + "ImapCalDavSettingsPage_CalDavUrlRequired": "È richiesto l'URL del servizio CalDAV.", + "ImapCalDavSettingsPage_CalDavUrlInvalid": "L'URL del servizio CalDAV deve essere un URL assoluto.", + "ImapCalDavSettingsPage_CalDavUsernameRequired": "Il nome utente CalDAV è obbligatorio.", + "ImapCalDavSettingsPage_CalDavPasswordRequired": "La password CalDAV è obbligatoria.", + "ImapCalDavSettingsPage_TestImapButton": "Test connessione IMAP", + "ImapCalDavSettingsPage_TestCalDavButton": "Test connessione CalDAV", + "ImapCalDavSettingsPage_ImapTestSuccessMessage": "Il test della connessione IMAP è riuscito.", + "ImapCalDavSettingsPage_CalDavTestSuccessMessage": "Il test della connessione CalDAV è riuscito.", + "ImapCalDavSettingsPage_SaveSuccessMessage": "Impostazioni dell'account verificate e salvate.", + "ImapCalDavSettingsPage_ICloudHint": "Usa una password specifica per l'app generata dalle impostazioni del tuo account Apple.", + "ImapCalDavSettingsPage_YahooHint": "Usa una password per l'app dalle impostazioni di sicurezza del tuo account Yahoo.", "Info_AccountCreatedMessage": "{0} è stato creato", "Info_AccountCreatedTitle": "Creazione account", "Info_AccountCreationFailedTitle": "Creazione account non riuscita", "Info_AccountDeletedMessage": "{0} è stato eliminato correttamente.", "Info_AccountDeletedTitle": "Account eliminato", "Info_AccountIssueFixFailedTitle": "Non riuscito", + "Info_AccountIssueFixImapMessage": "Apri la pagina delle impostazioni IMAP e calendario per inserire nuovamente le credenziali del server.", + "Info_AccountAttentionRequiredMessage": "Questo account necessita della tua attenzione.", + "Info_AccountAttentionRequiredClickableMessage": "Fai clic per risolvere questo account e sincronizzarlo di nuovo.", + "Info_AccountAttentionRequiredAction": "Correggi", + "Info_AccountAttentionRequiredActionHint": "Fai clic su Correggi per risolvere questo problema dell'account.", "Info_AccountIssueFixSuccessMessage": "Risolti tutti i problemi dell'account.", "Info_AccountIssueFixSuccessTitle": "Fatto", "Info_AttachmentOpenFailedMessage": "Non è possibile aprire questo allegato.", @@ -362,7 +576,7 @@ "Info_SyncFailedTitle": "Sincronizzazione non riuscita", "Info_UnsubscribeErrorMessage": "Impossibile annullare l'iscrizione", "Info_UnsubscribeLinkInvalidMessage": "Questo collegamento di disiscrizione non è valido. Impossibile disiscriversi dalla lista.", - "Info_UnsubscribeLinkInvalidTitle": "URI disiscrizione non valido", + "Info_UnsubscribeLinkInvalidTitle": "URL disiscrizione non valido", "Info_UnsubscribeSuccessMessage": "Iscrizione a {0} annullata con successo.", "Info_UnsupportedFunctionalityDescription": "Questa funzionalità non è stata ancora implementata.", "Info_UnsupportedFunctionalityTitle": "Non supportato", @@ -370,6 +584,7 @@ "InfoBarMessage_SynchronizationDisabledFolder": "Questa cartella è disabilitata per la sincronizzazione.", "InfoBarTitle_SynchronizationDisabledFolder": "Cartella disabilitata", "Justify": "Giustifica", + "MenuUpdateAvailable": "Aggiornamento disponibile", "Left": "Sinistra", "Link": "Collegamento", "LinkedAccountsCreatePolicyMessage": "devi avere almeno 2 account per creare il collegamento\nil collegamento sarà rimosso al salvataggio", @@ -403,6 +618,7 @@ "MailOperation_Unarchive": "Sposta da archivio", "MailOperation_ViewMessageSource": "Vedi sorgente messaggio", "MailOperation_Zoom": "Zoom", + "MailsDragging": "Trascinando {0} elemento/i", "MailsSelected": "{0} elemento/i selezionato/i", "MarkFlagUnflag": "Segna come contrassegnato/non contrassegnato", "MarkReadUnread": "Segna come letto/da leggere", @@ -434,6 +650,8 @@ "Notifications_MultipleNotificationsTitle": "Nuova posta", "Notifications_WinoUpdatedMessage": "Controlla la nuova versione {0}", "Notifications_WinoUpdatedTitle": "Wino Mail è stato aggiornato.", + "Notifications_StoreUpdateAvailableTitle": "Aggiornamento disponibile", + "Notifications_StoreUpdateAvailableMessage": "È disponibile una versione più recente di Wino Mail, pronta per l'installazione dal Microsoft Store.", "OnlineSearchFailed_Message": "Esecuzione della ricerca non riuscita\n{0}\n\nnell'elenco delle e-mail fuori rete.", "OnlineSearchTry_Line1": "Nessuna risposta adeguata trovata?", "OnlineSearchTry_Line2": "Prova la ricerca in rete.", @@ -446,7 +664,6 @@ "PaneLengthOption_Small": "Piccolo", "Photos": "Foto", "PreparingFoldersMessage": "Preparazione cartelle", - "ProtocolLogAvailable_Message": "I registri del protocollo sono disponibili per la diagnostica.", "ProviderDetail_Gmail_Description": "Account Google", "ProviderDetail_iCloud_Description": "Account iCloud Apple", "ProviderDetail_iCloud_Title": "iCloud", @@ -465,9 +682,14 @@ "SearchBarPlaceholder": "Cerca", "SearchingIn": "Ricerca in", "SearchPivotName": "Risultati", + "Settings_KeyboardShortcuts_Title": "Scorciatoie da tastiera", + "Settings_KeyboardShortcuts_Description": "Gestisci le scorciatoie da tastiera per azioni rapide sulle email.", "SettingConfigureSpecialFolders_Button": "Configura", "SettingsEditAccountDetails_IMAPConfiguration_Title": "Configurazione IMAP/SMTP", "SettingsEditAccountDetails_IMAPConfiguration_Description": "Cambia le impostazioni del server in entrata/uscita.", + "SettingsEditAccountDetails_ImapCalDavSettings_Title": "Impostazioni IMAP e calendario", + "SettingsEditAccountDetails_ImapCalDavSettings_Description": "Apri la pagina dedicata alle impostazioni IMAP, SMTP e CalDAV per questo account.", + "SettingsEditAccountDetails_ImapCalDavSettings_Action": "Apri impostazioni", "SettingsAbout_Description": "Scopri di più su Wino.", "SettingsAbout_Title": "Informaz.", "SettingsAboutGithub_Description": "Vai all'issue tracker del repository GitHub.", @@ -490,6 +712,10 @@ "SettingsAppPreferences_SearchMode_Local": "Locale", "SettingsAppPreferences_SearchMode_Online": "In rete", "SettingsAppPreferences_SearchMode_Title": "Modalità di ricerca predefinita", + "SettingsAppPreferences_ApplicationMode_Title": "Modalità predefinita dell'applicazione", + "SettingsAppPreferences_ApplicationMode_Description": "Scegli in quale modalità si aprirà Wino quando non è specificato un tipo di attivazione.", + "SettingsAppPreferences_ApplicationMode_Mail": "Posta", + "SettingsAppPreferences_ApplicationMode_Calendar": "Calendario", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description": "Wino Mail continuerà a essere in esecuzione dietro le quinte. Sarai avvisato quando arrivano nuovi messaggi.", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title": "Esegui dietro le quinte", "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description": "Wino Mail continuerà a funzionare nell'area di notifica. Disponibile per il lancio cliccando un'icona. Sarai avvisato quando arrivano nuovi messaggi.", @@ -506,12 +732,30 @@ "SettingsAppPreferences_StartupBehavior_FatalError": "Si è verificato un errore fatale modificando la modalità di avvio per Wino Mail.", "SettingsAppPreferences_StartupBehavior_Title": "Avvia minimizzato all'avvio di Windows", "SettingsAppPreferences_Title": "Preferenze dell'app", + "SettingsAppPreferences_HideWinoAccountButton_Title": "Nascondi il pulsante dell'account Wino nella barra del titolo", + "SettingsAppPreferences_HideWinoAccountButton_Description": "Nascondi il pulsante profilo nella barra del titolo che apre il menu dell'account Wino.", + "SettingsAppPreferences_StoreUpdateNotifications_Title": "Notifiche di aggiornamento del Microsoft Store.", + "SettingsAppPreferences_StoreUpdateNotifications_Description": "Mostra notifiche e azioni a piè di pagina quando è disponibile un aggiornamento dal Microsoft Store.", + "SettingsAppPreferences_AiActions_Title": "Azioni AI", + "SettingsAppPreferences_AiActions_Description": "Scegli le lingue predefinite per l'IA e dove dovrebbero essere salvati i riassunti.", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Title": "Lingua di traduzione predefinita", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Description": "Seleziona la lingua di destinazione predefinita utilizzata dalle azioni di traduzione AI.", + "SettingsAppPreferences_AiSummarizeLanguage_Title": "Lingua di riepilogo", + "SettingsAppPreferences_AiSummarizeLanguage_Description": "Seleziona la lingua preferita per i riassunti futuri generati dall'IA.", + "SettingsAppPreferences_AiSummarySavePath_Title": "Percorso di salvataggio predefinito per i riassunti", + "SettingsAppPreferences_AiSummarySavePath_Description": "Scegli la cartella che Wino dovrebbe utilizzare per impostazione predefinita quando salva i riassunti generati dall'IA.", + "SettingsAppPreferences_AiSummarySavePath_Placeholder": "Usa la posizione di salvataggio di sistema predefinita", + "SettingsAppPreferences_AiSummarySavePath_InvalidHint": "Questa cartella non esiste. Verrà utilizzata la posizione di salvataggio predefinita per i riassunti.", "SettingsAutoSelectNextItem_Description": "Seleziona l'elemento successivo dopo aver eliminato o spostato un messaggio.", "SettingsAutoSelectNextItem_Title": "Auto-seleziona l'elemento successivo", "SettingsAvailableThemes_Description": "Seleziona un tema dalla collezione di Wino di tuo gusto o applica i tuoi temi.", "SettingsAvailableThemes_Title": "Temi disponibili", "SettingsCalendarSettings_Description": "Cambia primo giorno della settimana, altezza della cella dell'ora e altro…", "SettingsCalendarSettings_Title": "Impostazioni calendario", + "CalendarSettings_DefaultSnoozeDuration_Header": "Durata predefinita del posticipo", + "CalendarSettings_DefaultSnoozeDuration_Description": "Imposta una durata predefinita per il posticipo delle notifiche promemoria del calendario.", + "CalendarSettings_TimedDayHeaderFormat_Header": "Formato intestazione giorno vista temporizzata", + "CalendarSettings_TimedDayHeaderFormat_Description": "Scegli come le etichette dei giorni in alto vengono visualizzate nelle viste giorno, settimana e settimana lavorativa. Usa token di formato data come ddd, dd, MMM o dddd.", "SettingsComposer_Title": "Compositore", "SettingsComposerFont_Title": "Carattere predefinito compositore", "SettingsComposerFontFamily_Description": "Cambia famiglia caratteri e dimensione carattere predefiniti per la scrittura dei messaggi.", @@ -531,6 +775,9 @@ "SettingsDiscord_Title": "Canale Discord", "SettingsEditLinkedInbox_Description": "Aggiungi/rimuovi account, rinomina o interrompi il collegamento tra gli account.", "SettingsEditLinkedInbox_Title": "Modifica In arrivo collegata", + "SettingsWindowBackdrop_Title": "Sfondo della finestra", + "SettingsWindowBackdrop_Description": "Seleziona un effetto di sfondo per le finestre di Wino.", + "SettingsWindowBackdrop_Disabled": "La selezione dello sfondo della finestra è disabilitata quando il tema dell'applicazione è diverso da Predefinito.", "SettingsElementTheme_Description": "Seleziona un tema di Windows per Wino", "SettingsElementTheme_Title": "Tema elemento", "SettingsElementThemeSelectionDisabled": "La selezione del tema dell'elemento è disabilitata quando un tema dell'applicazione diverso da quello predefinito è selezionato.", @@ -581,6 +828,8 @@ "SettingsManageAliases_Title": "Alias", "SettingsEditAccountDetails_Title": "Modifica dettagli account", "SettingsEditAccountDetails_Description": "Cambia il nome dell'account, il nome del mittente e - se vuoi - assegnagli un nuovo colore.", + "EditAccountDetailsPage_SaveSuccess_Title": "Modifiche salvate", + "EditAccountDetailsPage_SaveSuccess_Message": "I dettagli dell'account sono stati aggiornati con successo.", "SettingsManageLink_Description": "Sposta gli elementi per aggiungere un nuovo collegamento o rimuovi un collegamento esistente.", "SettingsManageLink_Title": "Gestisci collegamento", "SettingsMarkAsRead_Description": "Cambia cosa dovrebbe accadere all'elemento selezionato.", @@ -596,7 +845,41 @@ "SettingsNotifications_Title": "Notifiche", "SettingsNotificationsAndTaskbar_Description": "Cambia se le notifiche devono essere visualizzate e distintivo della barra delle attività per questo account.", "SettingsNotificationsAndTaskbar_Title": "Notifiche e barra delle attività", + "SettingsHome_Title": "Home", + "SettingsHome_SearchTitle": "Trova un'impostazione", + "SettingsHome_SearchDescription": "Cerca per funzione, argomento o parola chiave per andare direttamente alla pagina delle impostazioni corretta.", + "SettingsHome_SearchPlaceholder": "Cerca impostazioni", + "SettingsHome_SearchExamples": "Prova: tema, archiviazione, lingua, firma", + "SettingsHome_QuickLinks_Title": "Collegamenti rapidi", + "SettingsHome_QuickLinks_Description": "Accedi rapidamente alle impostazioni che le persone cercano più spesso.", + "SettingsHome_StorageCard_Description": "Vedi quanta contenuto MIME locale Wino conserva su questo dispositivo e puliscilo quando necessario.", + "SettingsHome_StorageEmptySummary": "Nessun contenuto MIME memorizzato nella cache rilevato finora.", + "SettingsHome_StorageLoading": "Verificando l'utilizzo MIME locale...", + "SettingsHome_Tips_Title": "Consigli e trucchi", + "SettingsHome_Tips_Description": "Qualche piccola modifica può rendere Wino molto più personale.", + "SettingsHome_Tip_Theme": "Vuoi modalità scura o cambiamenti di accento? Apri Personalizzazione.", + "SettingsHome_Tip_Background": "Usa Preferenze dell'app per controllare l'avvio e la sincronizzazione in background.", + "SettingsHome_Tip_Shortcuts": "Le scorciatoie da tastiera ti aiutano a muoverti tra le email più rapidamente.", + "SettingsHome_Resources_Title": "Collegamenti utili", + "SettingsHome_Resources_Description": "Apri risorse del progetto, informazioni di supporto e canali di rilascio.", "SettingsOptions_Title": "Impostazioni", + "SettingsOptions_GeneralSection": "Generale", + "SettingsOptions_MailSection": "Posta", + "SettingsOptions_CalendarSection": "Calendario", + "SettingsOptions_MoreComingSoon": "Altre opzioni in arrivo", + "SettingsOptions_HeroDescription": "Personalizza la tua esperienza con Wino Mail.", + "SettingsOptions_AccountsSummary": "{0} account configurati", + "SettingsSearch_ManageAccounts_Keywords": "account;account;casella di posta;caselle di posta;alias;alias;profilo;indirizzo;indirizzi", + "SettingsSearch_AppPreferences_Keywords": "avvio;in background;lancio;sincronizzazione;notifiche;notifiche;ricerca;area di notifica;predefiniti", + "SettingsSearch_LanguageTime_Keywords": "lingua;tempo;orologio;locale;regione;formato;24 ore;24h", + "SettingsSearch_Personalization_Keywords": "tema;scuro;chiaro;aspetto;accento;colore;colore;modalità;layout;densità", + "SettingsSearch_About_Keywords": "informazioni;versione;sito web;privacy;github;donazioni;negozio;supporto", + "SettingsSearch_KeyboardShortcuts_Keywords": "scorciatoia;scorciatoie;tasti rapidi;tasti rapidi;tastiera;tasti", + "SettingsSearch_MessageList_Keywords": "messaggio;messaggi;elenco;conversazioni;conversazioni;avatar;anteprima;mittente", + "SettingsSearch_ReadComposePane_Keywords": "lettore;componi;compositore;carattere;caratteri;contenuto esterno;visualizzazione;lettura", + "SettingsSearch_SignatureAndEncryption_Keywords": "firma;firme;crittografia;certificato;certificati;S/MIME;S/MIME;sicurezza", + "SettingsSearch_Storage_Keywords": "archiviazione;cache;memorizzazione nella cache;MIME;disco;spazio;pulizia;ripulire;dati locali", + "SettingsSearch_CalendarSettings_Keywords": "calendario;settimana;ore;orario;evento;eventi", "SettingsPaneLengthReset_Description": "Reimposta la dimensione dell'elenco di posta all'originale se hai problemi con esso.", "SettingsPaneLengthReset_Title": "Reimposta dimensione elenco posta", "SettingsPaypal_Description": "Mostra molto più amore ❤️ Tutte le donazioni sono apprezzate.", @@ -610,6 +893,8 @@ "SettingsPrefer24HourClock_Title": "Mostra orologio in formato 24 ore", "SettingsPrivacyPolicy_Description": "Verifica la politica di riservatezza.", "SettingsPrivacyPolicy_Title": "Politica di riservatezza", + "SettingsWebsite_Description": "Apri il sito web di Wino Mail.", + "SettingsWebsite_Title": "Sito web", "SettingsReadComposePane_Description": "Caratteri, contenuto esterno.", "SettingsReadComposePane_Title": "Lettore e compositore", "SettingsReader_Title": "Lettore", @@ -625,6 +910,19 @@ "SettingsShowPreviewText_Title": "Mostra testo di anteprima", "SettingsShowSenderPictures_Description": "Mostra/nascondi le miniature delle immagini del mittente.", "SettingsShowSenderPictures_Title": "Mostra avatar mittente", + "SettingsEmailTemplates_Title": "Modelli di email", + "SettingsEmailTemplates_Description": "Gestisci i modelli di e-mail", + "SettingsEmailTemplates_CreatePageTitle": "Nuovo modello", + "SettingsEmailTemplates_EditPageTitle": "Modifica modello", + "SettingsEmailTemplates_NewTemplateTitle": "Nuovo modello", + "SettingsEmailTemplates_NewTemplateDescription": "Crea un nuovo modello di e-mail", + "SettingsEmailTemplates_NameTitle": "Nome", + "SettingsEmailTemplates_NamePlaceholder": "Nome del modello", + "SettingsEmailTemplates_DescriptionTitle": "Descrizione", + "SettingsEmailTemplates_DescriptionPlaceholder": "Descrizione opzionale", + "SettingsEmailTemplates_ContentTitle": "Contenuto del modello", + "SettingsEmailTemplates_ContentDescription": "Modifica il contenuto HTML di questo modello.", + "SettingsEmailTemplates_NameRequired": "Il nome del modello è obbligatorio.", "SettingsEnableGravatarAvatars_Title": "Personificatore", "SettingsEnableGravatarAvatars_Description": "Usa il personificatore (se disponibile) come immagine del mittente", "SettingsEnableFavicons_Title": "Icone del dominio (Favicons)", @@ -645,6 +943,33 @@ "SettingsStartupItem_Title": "Elemento Iniziale", "SettingsStore_Description": "Mostra un po' di amore ❤️", "SettingsStore_Title": "Valuta nel negozio", + "SettingsStorage_Title": "Archiviazione", + "SettingsStorage_Description": "Scansiona e gestisci la cache MIME memorizzata nella tua cartella dati locale.", + "SettingsStorage_ScanFolder": "Scansiona la cartella dati locale", + "SettingsStorage_NoLocalMimeDataFound": "Nessun dato MIME locale trovato.", + "SettingsStorage_NoAccountsFound": "Nessun account trovato.", + "SettingsStorage_TotalUsage": "Uso totale della cache MIME locale: {0}", + "SettingsStorage_AccountUsageDescription": "{0} utilizzati nella cache MIME locale.", + "SettingsStorage_DeleteAll_Title": "Elimina tutto il contenuto MIME", + "SettingsStorage_DeleteAll_Description": "Elimina l'intera cartella cache MIME di questo account.", + "SettingsStorage_DeleteAll_Button": "Elimina tutto", + "SettingsStorage_DeleteAll_Confirm_Title": "Elimina tutto il contenuto MIME", + "SettingsStorage_DeleteAll_Confirm_Message": "Eliminare tutti i dati MIME locali per {0}?", + "SettingsStorage_DeleteAll_Success": "Tutti i contenuti MIME sono stati eliminati.", + "SettingsStorage_DeleteOld_Title": "Elimina contenuti MIME vecchi", + "SettingsStorage_DeleteOld_Description": "Elimina i file MIME in base alla data di creazione dell'e-mail nel database locale.", + "SettingsStorage_DeleteOld_1Month": "> 1 mese", + "SettingsStorage_DeleteOld_3Months": "> 3 mesi", + "SettingsStorage_DeleteOld_6Months": "> 6 mesi", + "SettingsStorage_DeleteOld_1Year": "> 1 anno", + "SettingsStorage_DeleteOld_Confirm_Title": "Elimina contenuti MIME vecchi", + "SettingsStorage_DeleteOld_Confirm_Message": "Eliminare i dati MIME locali più vecchi di {0} per {1}?", + "SettingsStorage_DeleteOld_Success": "Eliminate {0} cartella MIME più vecchie di {1}.", + "SettingsStorage_1Month": "1 mese", + "SettingsStorage_3Months": "3 mesi", + "SettingsStorage_6Months": "6 mesi", + "SettingsStorage_1Year": "1 anno", + "SettingsStorage_Months": "{0} mesi", "SettingsTaskbarBadge_Description": "Includi conteggio dei messaggi non letti nell'icona della barra delle attività.", "SettingsTaskbarBadge_Title": "In barra attività come", "SettingsThreads_Description": "Organizza i messaggi in gruppi di conversazione.", @@ -683,6 +1008,9 @@ "SystemFolderConfigDialogValidation_InboxSelected": "Non puoi assegnare la cartella In arrivo a nessun'altra cartella di sistema.", "SystemFolderConfigSetupSuccess_Message": "Le cartelle di sistema sono configurate correttamente.", "SystemFolderConfigSetupSuccess_Title": "Configurazione cartelle di sistema", + "SystemTrayMenu_ShowWino": "Apri Wino Mail", + "SystemTrayMenu_ShowWinoCalendar": "Apri Wino Calendario", + "SystemTrayMenu_ExitWino": "Esci", "TestingImapConnectionMessage": "Verifica connessione del server...", "TitleBarServerDisconnectedButton_Description": "Wino è disconnesso dalla rete. Clicca di nuovo per ripristinare la connessione.", "TitleBarServerDisconnectedButton_Title": "nessuna connessione", @@ -699,8 +1027,422 @@ "WinoUpgradeMessage": "Aggiorna ad account illimitati", "WinoUpgradeRemainingAccountsMessage": "{0} di {1} account gratuiti usati.", "Yesterday": "Ieri", + "Smime_ImportCertificates_Success": "Certificati importati correttamente.", + "Smime_ImportCertificates_Error": "Errore durante l'importazione dei certificati: {0}", + "Smime_RemoveCertificates_Confirm": "Vuoi davvero rimuovere i certificati {0}?", + "Smime_RemoveCertificates_Success": "Certificati rimossi.", + "Smime_ExportCertificates_Success": "Certificati esportati.", + "Smime_ExportCertificates_Error": "Errore durante l'esportazione dei certificati.", + "Smime_CertificateDetails": "Soggetto: {0}\nEmittente: {1}\nValido da: {2}\nValido fino a: {3}\nImpronta: {4}", + "Smime_CertificatePassword_Title": "Password del certificato obbligatoria", + "Smime_CertificatePassword_Placeholder": "Password del certificato per {0} (opzionale)", + "Smime_Confirm_Title": "Conferma", + "Buttons_OK": "OK", + "Buttons_Refresh": "Aggiorna", + "SettingsSignatureAndEncryption_Title": "Firma e Crittografia", + "SettingsSignatureAndEncryption_Description": "Gestisci certificati S/MIME per firmare e cifrare le email.", + "SettingsSignatureAndEncryption_MyCertificatesHeader": "I miei certificati", + "SettingsSignatureAndEncryption_MyCertificatesDescription": "Certificati personali per la firma e la cifratura", + "SettingsSignatureAndEncryption_RecipientCertificatesHeader": "Certificati del destinatario", + "SettingsSignatureAndEncryption_RecipientCertificatesDescription": "Certificati dei destinatari per decrittazione", + "SettingsSignatureAndEncryption_NameColumn": "Nome", + "SettingsSignatureAndEncryption_ExpiresColumn": "Scadenza", + "SettingsSignatureAndEncryption_ThumbprintColumn": "Impronta digitale", + "Buttons_Remove": "Rimuovi", + "Buttons_Export": "Esporta", + "Buttons_Import": "Importa", + "SettingsSignatureAndEncryption_SigningCertificate": "Certificato di firma S/MIME", + "SettingsSignatureAndEncryption_EncryptionCertificate": "Crittografia S/MIME", + "SettingsSignatureAndEncryption_SigningCertificatePlaceholder": "Nessuno", + "SmimeSignaturesInMessage": "Firme in questo messaggio:", + "SmimeSignatureEntry": "• {0} {1} ({2}, valido dal {3} - {4})", + "SmimeSigningCertificateInfoTitle": "Informazioni sul certificato di firma S/MIME", + "SmimeCertificateInfoTitle": "Informazioni sul certificato S/MIME", + "SmimeNoCertificateFileFound": "Nessun file di certificato trovato", + "SmimeSaveCertificate": "Salva certificato...", + "SmimeCertificate": "Certificato S/MIME", + "SmimeCertificateSavedTo": "Certificato salvato in {0}", + "SmimeSignedTooltip": "Questo messaggio è firmato con un certificato S/MIME. Clicca per ulteriori dettagli", + "SmimeEncryptedTooltip": "Questo messaggio è cifrato con un certificato S/MIME.", + "SmimeCertificateFileInfo": "File: {0}", + "Composer_LightTheme": "Tema Chiaro", + "Composer_DarkTheme": "Tema Scuro", + "Composer_Outdent": "Riduci rientro", + "Composer_Indent": "Aumenta rientro", + "Composer_BulletList": "Elenco puntato", + "Composer_OrderedList": "Elenco numerato", + "Composer_Stroke": "Tratto", + "Composer_Bold": "Grassetto", + "Composer_Italic": "Corsivo", + "Composer_Underline": "Sottolinea", + "Composer_CcBcc": "Cc e Bcc", + "Composer_EnableSmimeSignature": "Attiva/disattiva firma S/MIME", + "Composer_EnableSmimeEncryption": "Attiva/disattiva cifratura S/MIME", + "Composer_LocalDraftSyncInfo": "Questa bozza è locale. Wino non è riuscito a inviarla al server di posta. Fare clic per ritentare l'invio al server.", + "Composer_CertificateExpires": "Scadenza: ", + "Composer_SmimeSignature": "Firma S/MIME", + "Composer_SmimeEncryption": "Crittografia S/MIME", + "Composer_EmailTemplatesPlaceholder": "Modelli di e-mail", + "Composer_AiSummarize": "Riepiloga con l'IA", + "Composer_AiSummarizeDescription": "Estrai i punti chiave, le azioni e le decisioni da questa email.", + "Composer_AiTranslate": "Traduci con l'IA", + "Composer_AiActions": "Azioni IA", + "Composer_AiRewrite": "Riformula con l'IA", + "AiActions_CheckingStatus": "Verifica l'accesso all'IA...", + "AiActions_SignedOutTitle": "Sblocca Wino AI Pack", + "AiActions_SignedOutDescription": "Traduci, riscrivi e riassumi le email con l'IA dopo aver effettuato l'accesso al tuo account Wino e attivato l'add-on AI Pack.", + "AiActions_NoPackTitle": "AI Pack richiesto", + "AiActions_NoPackDescription": "Hai effettuato l'accesso, ma l'AI Pack non è attivo ancora. Acquistalo per utilizzare gli strumenti di traduzione, riscrittura e riassunto con IA di Wino.", + "AiActions_UsageSummary": "{0} di {1} crediti usati questo mese.", + "Composer_AiRewritePolite": "Rendilo più cortese", + "Composer_AiRewritePoliteDescription": "Rende la formulazione meno rigida mantenendo lo stesso intento.", + "Composer_AiRewriteAngry": "Rendilo arrabbiato", + "Composer_AiRewriteAngryDescription": "Usa un tono più tagliante e conflittuale.", + "Composer_AiRewriteHappy": "Rendilo felice", + "Composer_AiRewriteHappyDescription": "Aggiunge un tono più vivace ed entusiasta.", + "Composer_AiRewriteFormal": "Rendi formale", + "Composer_AiRewriteFormalDescription": "Rende il messaggio più professionale e strutturato.", + "Composer_AiRewriteFriendly": "Rendi amichevole", + "Composer_AiRewriteFriendlyDescription": "Rende il messaggio più accessibile e cordiale.", + "Composer_AiRewriteShorter": "Rendi più breve", + "Composer_AiRewriteShorterDescription": "Accorcia il testo e rimuove dettagli superflui.", + "Composer_AiRewriteClearer": "Rendi più chiaro", + "Composer_AiRewriteClearerDescription": "Migliora la leggibilità e rende il messaggio più facile da seguire.", + "Composer_AiRewriteCustom": "Personalizzato", + "Composer_AiRewriteCustomDescription": "Descrivi la tua intenzione di riscrittura.", + "Composer_AiRewriteCustomPlaceholder": "Descrivi come vuoi che il messaggio venga riscritto", + "Composer_AiRewriteMode": "Riformula il tono", + "Composer_AiRewriteApply": "Applica la riscrittura", + "Composer_AiTranslateDialogTitle": "Traduci con l'IA", + "Composer_AiTranslateDialogDescription": "Inserisci la lingua di destinazione o il codice culturale, come en-US, tr-TR, de-DE o fr-FR.", + "Composer_AiTranslateApply": "Traduci", + "Composer_AiTranslateLanguage": "Lingua di destinazione", + "Composer_AiTranslateCustomPlaceholder": "Inserisci il codice culturale", + "Composer_AiTranslateLanguageEnglish": "Inglese (en-US)", + "Composer_AiTranslateLanguageTurkish": "Turco (tr-TR)", + "Composer_AiTranslateLanguageGerman": "Tedesco (de-DE)", + "Composer_AiTranslateLanguageFrench": "Francese (fr-FR)", + "Composer_AiTranslateLanguageSpanish": "Spagnolo (es-ES)", + "Composer_AiTranslateLanguageItalian": "Italiano (it-IT)", + "Composer_AiTranslateLanguagePortugueseBrazil": "Portoghese (Brasile) (pt-BR)", + "Composer_AiTranslateLanguageDutch": "Olandese (nl-NL)", + "Composer_AiTranslateLanguagePolish": "Polacco (pl-PL)", + "Composer_AiTranslateLanguageRussian": "Russo (ru-RU)", + "Composer_AiTranslateLanguageJapanese": "Giapponese (ja-JP)", + "Composer_AiTranslateLanguageKorean": "Coreano (ko-KR)", + "Composer_AiTranslateLanguageChineseSimplified": "Cinese semplificato (zh-CN)", + "Composer_AiTranslateLanguageArabic": "Arabo (ar-SA)", + "Composer_AiTranslateLanguageHindi": "Hindi (hi-IN)", + "Composer_AiTranslateLanguageOther": "Altro...", + "Composer_AiBusyTitle": "L'IA è già in funzione", + "Composer_AiBusyMessage": "Attendere il completamento dell'azione IA in corso.", + "Composer_AiSignInRequired": "Accedi al tuo account Wino per utilizzare le funzionalità IA.", + "Composer_AiMissingHtml": "Non ci sono contenuti nel messaggio da inviare a Wino IA.", + "Composer_AiQuotaUnavailable": "Il risultato IA è stato applicato.", + "Composer_AiAppliedMessage": "Il risultato dell'IA è stato applicato al composer. Usa Annulla se vuoi ripristinarlo.", + "Composer_AiSummarizeSuccessTitle": "Riepilogo IA applicato", + "Composer_AiTranslateSuccessTitle": "Traduzione IA applicata", + "Composer_AiRewriteSuccessTitle": "Riformulazione IA applicata", + "Composer_AiErrorTitle": "Azione IA non riuscita", + "Reader_AiAppliedMessage": "Il risultato IA è ora mostrato per questo messaggio. Riapri il messaggio per visualizzare nuovamente il contenuto originale.", "SettingsAppPreferences_EmailSyncInterval_Title": "Intervallo sincronizzazione e-mail", - "SettingsAppPreferences_EmailSyncInterval_Description": "Intervallo di sincronizzazione automatica delle e-mail (minuti). Questa impostazione sarà applicata solo dopo il riavvio di Wino Mail." + "SettingsAppPreferences_EmailSyncInterval_Description": "Intervallo di sincronizzazione automatica delle e-mail (minuti). Questa impostazione sarà applicata solo dopo il riavvio di Wino Mail.", + "ContactsPage_Title": "Contatti", + "ContactsPage_AddContact": "Aggiungi contatto", + "ContactsPage_EditContact": "Modifica contatto", + "ContactsPage_DeleteContact": "Elimina contatto", + "ContactsPage_SearchPlaceholder": "Cerca contatti...", + "ContactsPage_NoContacts": "Nessun contatto trovato", + "ContactsPage_ContactsCount": "{0} contatti", + "ContactsPage_SelectedContactsCount": "{0} selezionati", + "ContactsPage_DeleteSelectedContacts": "Elimina selezionati", + "ContactEditDialog_Title": "Modifica contatto", + "ContactEditDialog_PhotoSection": "Foto", + "ContactEditDialog_ChoosePhoto": "Scegli foto", + "ContactEditDialog_RemovePhoto": "Rimuovi foto", + "ContactEditDialog_NameHeader": "Nome", + "ContactEditDialog_NamePlaceholder": "Nome contatto", + "ContactEditDialog_EmailHeader": "Indirizzo email", + "ContactEditDialog_EmailPlaceholder": "contatto@example.com", + "ContactEditDialog_InfoSection": "Informazioni sul contatto", + "ContactEditDialog_RootContactInfo": "Questo è un contatto principale associato ai tuoi account e non può essere eliminato.", + "ContactEditDialog_OverriddenContactInfo": "Questo contatto è stato modificato manualmente e non verrà aggiornato durante la sincronizzazione.", + "ContactsPage_Subtitle": "Gestisci i tuoi contatti email e le relative informazioni.", + "ContactStatus_Account": "Account", + "ContactStatus_Modified": "Modificato", + "ContactAction_Edit": "Modifica contatto", + "ContactAction_ChangePhoto": "Cambia foto", + "ContactAction_Delete": "Elimina contatto", + "ContactAction_Add": "Aggiungi contatto", + "ContactSelection_Selected": "selezionato", + "ContactSelection_SelectAll": "Seleziona tutto", + "ContactSelection_Clear": "Cancella selezione", + "ContactsPage_EmptyState": "Nessun contatto da visualizzare", + "ContactsPage_AddFirstContact": "Aggiungi il tuo primo contatto", + "ContactsPage_ContactsCountSuffix": "contatti", + "ContactsPane_NewContact": "Nuovo contatto", + "ContactsPane_DescriptionTitle": "Gestisci i tuoi contatti", + "ContactsPane_DescriptionBody": "Crea contatti, rinominali, aggiorna le foto profilo e conserva in un unico posto i dettagli salvati.", + "ContactEditDialog_AddTitle": "Aggiungi contatto", + "ContactInfoBar_ContactAdded": "Contatto aggiunto con successo.", + "ContactInfoBar_ContactUpdated": "Contatto aggiornato con successo.", + "ContactInfoBar_ContactsDeleted": "Contatti eliminati con successo.", + "ContactInfoBar_ContactPhotoUpdated": "Foto del contatto aggiornata con successo.", + "ContactInfoBar_FailedToLoadContacts": "Impossibile caricare i contatti: {0}", + "ContactInfoBar_FailedToAddContact": "Impossibile aggiungere il contatto: {0}", + "ContactInfoBar_FailedToUpdateContact": "Impossibile aggiornare il contatto: {0}", + "ContactInfoBar_FailedToDeleteContacts": "Impossibile eliminare i contatti: {0}", + "ContactInfoBar_FailedToUpdatePhoto": "Impossibile aggiornare la foto: {0}", + "ContactInfoBar_CannotDeleteRoot": "I contatti principali non possono essere eliminati.", + "ContactConfirmDialog_DeleteTitle": "Elimina contatto", + "ContactConfirmDialog_DeleteMessage": "Sei sicuro di voler eliminare il contatto '{0}'?", + "ContactConfirmDialog_DeleteMultipleMessage": "Sei sicuro di voler eliminare {0} contatto(i)?", + "ContactConfirmDialog_DeleteButton": "Elimina", + "CalendarAccountSettings_Title": "Impostazioni account calendario", + "CalendarAccountSettings_Description": "Gestisci le impostazioni del calendario per {0}", + "CalendarAccountSettings_AccountColor": "Colore dell'account", + "CalendarAccountSettings_AccountColorDescription": "Modifica il colore di visualizzazione per questo account calendario", + "CalendarAccountSettings_SyncEnabled": "Abilita la sincronizzazione", + "CalendarAccountSettings_SyncEnabledDescription": "Abilita o disabilita la sincronizzazione del calendario per questo account", + "CalendarAccountSettings_DefaultShowAs": "Disponibilità predefinita", + "CalendarAccountSettings_DefaultShowAsDescription": "Stato di disponibilità predefinito per i nuovi eventi creati con questo account", + "CalendarAccountSettings_PrimaryCalendar": "Calendario principale", + "CalendarAccountSettings_PrimaryCalendarDescription": "Segna questo calendario come calendario principale per l'account", + "CalendarSettings_NewEventBehavior_Header": "Comportamento del pulsante Nuovo Evento", + "CalendarSettings_NewEventBehavior_Description": "Scegli se il pulsante Nuovo Evento deve chiedere quale calendario utilizzare ogni volta o aprire sempre un calendario specifico.", + "CalendarSettings_NewEventBehavior_AskEachTime": "Chiedi ogni volta.", + "CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar": "Usa sempre un calendario specifico.", + "CalendarSettings_Rendering_Title": "Visualizzazione", + "CalendarSettings_Rendering_Description": "Configura la disposizione del calendario e il comportamento di visualizzazione.", + "CalendarSettings_Notifications_Title": "Notifiche", + "CalendarSettings_Notifications_Description": "Imposta promemoria e comportamento di posticipo predefiniti.", + "CalendarSettings_Preferences_Title": "Preferenze", + "CalendarSettings_Preferences_Description": "Imposta il comportamento del pulsante Nuovo Evento.", + "WhatIsNew_GetStartedButton": "Inizia", + "WhatIsNew_ContinueAnywayButton": "Continua comunque", + "WhatIsNew_PreparingForNewVersionButton": "Preparazione per una nuova versione...", + "WhatIsNew_MigrationPreparing_Title": "Preparazione dei tuoi dati", + "WhatIsNew_MigrationPreparing_Description": "Wino sta applicando migrazioni di aggiornamento. Attendere mentre prepariamo i dati del tuo account per questa versione.", + "WhatIsNew_MigrationFailedMessage": "L'applicazione delle migrazioni è fallita con il codice di errore {0}. Puoi continuare a utilizzare l'applicazione. Tuttavia, se incontri problemi gravi, reinstalla l'app.", + "WhatIsNew_MigrationNotification_Title": "Wino Mail Aggiornato", + "WhatIsNew_MigrationNotification_Message": "Apri l'app per completare l'aggiornamento e vedere cosa c'è di nuovo.", + "WelcomeWindow_Title": "Benvenuto in Wino Mail", + "WelcomeWindow_Subtitle": "Un'esperienza nativa di Windows per Posta e Calendario.", + "WelcomeWindow_WhatsNewTitle": "Ultime modifiche", + "WelcomeWindow_FeaturesTitle": "Funzionalità", + "WelcomeWindow_WhatsNewTab": "Novità", + "WelcomeWindow_FeaturesTab": "Funzionalità", + "WelcomeWindow_GetStartedButton": "Inizia aggiungendo un account", + "WelcomeWindow_GetStartedDescription": "Aggiungi il tuo account Outlook, Gmail o IMAP per iniziare a usare Wino Mail.", + "WelcomeWindow_ImportFromWinoAccount": "Importa dal tuo account Wino", + "WelcomeWindow_ImportInProgress": "Importazione delle preferenze e degli account sincronizzati in corso...", + "WelcomeWindow_ImportNoAccountsFound": "Nessun account sincronizzato è stato trovato nel tuo account Wino. Se erano disponibili le preferenze, sono state ripristinate. Usa Inizia per aggiungere manualmente un account.", + "WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} account sincronizzati sono già disponibili su questo dispositivo. Usa Inizia per aggiungere manualmente un altro account se necessario.", + "WelcomeWindow_SetupTitle": "Configura il tuo account", + "WelcomeWindow_SetupSubtitle": "Scegli il tuo provider di posta per iniziare", + "WelcomeWindow_AddAccountButton": "Aggiungi account", + "WelcomeWindow_SkipForNow": "Salta per ora — lo configurerò più tardi", + "WelcomeWindow_AppDescription": "Una posta in arrivo veloce e focalizzata — riprogettata per Windows 11", + "WelcomeWizard_Step1Title": "Benvenuto", + "SystemTrayMenu_Open": "Apri", + "WinoAccount_Titlebar_SyncBenefitTitle": "Sincronizza impostazioni", + "WinoAccount_Titlebar_SyncBenefitDescription": "Mantieni le preferenze di Wino sincronizzate su più dispositivi.", + "WinoAccount_Titlebar_AddonsBenefitTitle": "Sblocca componenti aggiuntivi", + "WinoAccount_Titlebar_AddonsBenefitDescription": "Accedi a funzionalità premium come Wino AI Pack.", + "WinoAccount_Management_Description": "Gestisci il tuo account Wino, l'accesso all'AI Pack e le preferenze sincronizzate e i dettagli dell'account.", + "WinoAccount_Management_SignedOutTitle": "Accedi a Wino Mail", + "WinoAccount_Management_SignedOutDescription": "Accedi o crea un account per sincronizzare la tua posta, accedere alle funzionalità AI e gestire le impostazioni su più dispositivi.", + "WinoAccount_Management_ProfileSectionHeader": "Profilo", + "WinoAccount_Management_AddOnsSectionHeader": "Componenti aggiuntivi Wino", + "WinoAccount_Management_DataSectionHeader": "Dati", + "WinoAccount_Management_AccountActionsSectionHeader": "Azioni dell'account", + "WinoAccount_Management_AccountCardTitle": "Account", + "WinoAccount_Management_AccountCardDescription": "Indirizzo email dell'account Wino e stato attuale dell'account.", + "WinoAccount_Management_AiPackCardTitle": "AI Pack", + "WinoAccount_Management_AiPackCardDescription": "Vedi se Wino AI Pack è attivo e quanto utilizzo è rimasto.", + "WinoAccount_Management_AiPackActive": "AI Pack è attivo", + "WinoAccount_Management_AiPackInactive": "AI Pack non è attivo", + "WinoAccount_Management_AiPackUsage": "{0} di {1} utilizzi consumati. Rimangono {2}.", + "WinoAccount_Management_AiPackBillingPeriod": "Periodo di fatturazione: {0:d} - {1:d}", + "WinoAccount_Management_AiPackUnknownUsage": "I dettagli sull'utilizzo non sono ancora disponibili.", + "WinoAccount_Management_AiPackBuyDescription": "Acquista Wino AI Pack per tradurre, riscrivere o riassumere le email con l'IA.", + "WinoAccount_Management_AiPackPromoTitle": "Sblocca AI Pack", + "WinoAccount_Management_AiPackPromoDescription": "Potenzia notevolmente il flusso di lavoro delle email con strumenti basati sull'IA. Traduci i messaggi in oltre 50 lingue, riscrivi per chiarezza e tono e ottieni sommari istantanei di lunghe conversazioni.", + "WinoAccount_Management_AiPackPromoPrice": "$4,99 / mese", + "WinoAccount_Management_AiPackPromoRequests": "1.000 crediti", + "WinoAccount_Management_AiPackGetButton": "Ottieni AI Pack", + "WinoAddOn_AI_PACK_Name": "Wino AI Pack", + "WinoAddOn_AI_PACK_Description": "Strumenti basati sull'IA per tradurre, riscrivere e riassumere azioni in Wino Mail.", + "WinoAddOn_AI_PACK_Keywords": "IA, traduci, riscrivi, riassumi, produttività", + "WinoAddOn_UNLIMITED_ACCOUNTS_Name": "Account illimitati", + "WinoAddOn_UNLIMITED_ACCOUNTS_Description": "Rimuovi il limite di account e aggiungi quanti account di posta ti servono.", + "WinoAddOn_UNLIMITED_ACCOUNTS_Keywords": "account, illimitato, premium, componente aggiuntivo", + "WinoAccount_Management_PurchaseRequiresSignIn": "Accedi con il tuo account Wino per completare questo acquisto.", + "WinoAccount_Management_PurchaseStartFailed": "Wino non è riuscito a completare questo acquisto sul Microsoft Store.", + "WinoAccount_Management_StoreSyncFailed": "L'acquisto è stato completato, ma Wino non è riuscito a aggiornare i benefici dell'account. Prova di nuovo tra un momento.", + "WinoAccount_Management_AiPackSubscriptionActive": "La tua sottoscrizione è attiva", + "WinoAccount_Management_AiPackRenews": "Si rinnova {0:d}", + "WinoAccount_Management_AiPackRequestsUsed": "Crediti utilizzati questo mese", + "WinoAccount_Management_AiPackResets": "Ripristini {0:d}", + "WinoAccount_Management_AiPackUsageLoadFailed": "Si è verificato un problema nel caricamento del tuo saldo di utilizzo IA.", + "WinoAccount_Management_AiPackFeatureTranslate": "Traduci", + "WinoAccount_Management_AiPackFeatureRewrite": "Riformula", + "WinoAccount_Management_AiPackFeatureSummarize": "Riepiloga", + "WinoAccount_Management_AddOnLoadFailed": "Si sono verificati problemi nel caricamento di questo componente aggiuntivo.", + "WinoAccount_Management_SyncPreferencesTitle": "Sincronizza Preferenze e Account", + "WinoAccount_Management_SyncPreferencesDescription": "Importa o esporta le tue preferenze Wino e i dettagli della casella di posta su più dispositivi. Password, token e altre informazioni sensibili non vengono mai sincronizzate.", + "WinoAccount_Management_SignOutTitle": "Esci", + "WinoAccount_Management_SignOutDescription": "Esci dal tuo account su questo dispositivo", + "WinoAccount_Management_StatusLabel": "Stato: {0}", + "WinoAccount_Management_NoRemoteSettings": "Non sono presenti dati sincronizzati per questo account.", + "WinoAccount_Management_ExportSucceeded": "I dati Wino selezionati sono stati esportati con successo.", + "WinoAccount_Management_ExportPreferencesSucceeded": "Le tue preferenze sono state esportate nel tuo account Wino.", + "WinoAccount_Management_ExportAccountsSucceeded": "Esportati {0} dettagli dell'account nel tuo Wino Account.", + "WinoAccount_Management_ImportSucceeded": "Dati sincronizzati importati dal tuo account Wino.", + "WinoAccount_Management_ImportPreferencesSucceeded": "Applicate {0} preferenze sincronizzate.", + "WinoAccount_Management_ImportAccountsSucceeded": "Importati {0} account.", + "WinoAccount_Management_ImportDuplicateAccountsSkipped": "Sono stati saltati {0} account già presenti su questo dispositivo.", + "WinoAccount_Management_ImportPartial": "Applicate {0} preferenze sincronizzate. {1} preferenze non sono state ripristinate.", + "WinoAccount_Management_ImportReloginReminder": "Le password, i token e altre informazioni sensibili non sono state importate. Effettua l'accesso di nuovo per ogni account su questo dispositivo prima di utilizzarlo.", + "WinoAccount_Management_SerializeFailed": "Wino non è riuscito a serializzare le tue preferenze correnti.", + "WinoAccount_Management_EmptyExport": "Non ci sono valori di preferenza da esportare.", + "WinoAccount_Management_ImportEmpty": "Il payload dei dati sincronizzati non contiene nulla di nuovo da ripristinare.", + "WinoAccount_Management_ExportDialog_Title": "Esporta nel tuo account Wino", + "WinoAccount_Management_ExportDialog_Description": "Scegli cosa vuoi sincronizzare nel tuo account Wino.", + "WinoAccount_Management_ExportDialog_IncludePreferences": "Preferenze", + "WinoAccount_Management_ExportDialog_IncludeAccounts": "Account", + "WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Le password, i token e altre informazioni sensibili non sono sincronizzati.", + "WinoAccount_Management_ExportDialog_AccountsRelogin": "Gli account importati su un altro PC richiederanno comunque di effettuare nuovamente l'accesso prima che possano essere utilizzati.", + "WinoAccount_Management_ExportDialog_InProgress": "Esportazione dei dati Wino selezionati in corso...", + "WinoAccount_Management_LoadFailed": "Impossibile caricare le ultime informazioni dell'Account Wino.", + "WinoAccount_Management_ActionFailed": "La richiesta dell'Account Wino non può essere completata.", + "WinoAccount_SettingsSection_Title": "Account Wino", + "WinoAccount_SettingsSection_Description": "Crea o accedi a un account Wino utilizzando il tuo servizio di autenticazione locale.", + "WinoAccount_RegisterButton_Title": "Registra account", + "WinoAccount_RegisterButton_Description": "Crea un account Wino con email e password.", + "WinoAccount_RegisterButton_Action": "Apri registrazione", + "WinoAccount_LoginButton_Title": "Accedi", + "WinoAccount_LoginButton_Description": "Accedi a un account Wino esistente con email e password.", + "WinoAccount_LoginButton_Action": "Apri accesso", + "WinoAccount_SignOutButton_Title": "Esci", + "WinoAccount_SignOutButton_Description": "Rimuovi la sessione dell'account Wino memorizzata localmente.", + "WinoAccount_SignOutButton_Action": "Disconnetti", + "WinoAccount_RegisterDialog_Title": "Crea account Wino", + "WinoAccount_RegisterDialog_Description": "Crea un account Wino per mantenere sincronizzata la tua esperienza Wino e sbloccare componenti aggiuntivi basati sull'account.", + "WinoAccount_RegisterDialog_HeroTitle": "Crea il tuo account Wino", + "WinoAccount_RegisterDialog_BenefitsTitle": "Perché crearne uno?", + "WinoAccount_RegisterDialog_BenefitSyncTitle": "Importa ed esporta le impostazioni tra dispositivi", + "WinoAccount_RegisterDialog_BenefitSyncDescription": "Sposta le preferenze Wino tra dispositivi senza dover ricreare la configurazione da zero.", + "WinoAccount_RegisterDialog_BenefitAiTitle": "Accedi a componenti aggiuntivi esclusivi come Wino AI Pack (a pagamento)", + "WinoAccount_RegisterDialog_BenefitAiDescription": "Usa un account per sbloccare le funzionalità premium di Wino man mano che diventano disponibili.", + "WinoAccount_RegisterDialog_DifferenceTitle": "L'account Wino è separato dai tuoi account di posta", + "WinoAccount_RegisterDialog_DifferenceDescription": "I tuoi account Outlook, Gmail, IMAP o altri account di posta restano esattamente come sono. Un account Wino gestisce solo funzionalità specifiche di Wino e componenti aggiuntivi basati sull'account.", + "WinoAccount_RegisterDialog_PrimaryButton": "Registrati", + "WinoAccount_RegisterDialog_PrivacyTitle": "Privacy e elaborazione API", + "WinoAccount_RegisterDialog_PrivacyDescription": "Componenti aggiuntivi opzionali come Wino AI Pack possono inviare contenuti HTML selezionati delle email al servizio API di Wino solo quando usi tali funzionalità.", + "WinoAccount_RegisterDialog_PrivacyLinkText": "Leggi l'informativa sulla privacy", + "WinoAccount_RegisterDialog_PrivacyCheckbox": "Accetto l'informativa sulla privacy.", + "WinoAccount_LoginDialog_Title": "Accedi a Wino Account", + "WinoAccount_LoginDialog_Description": "Accedi al tuo account Wino per sincronizzare la tua configurazione Wino e accedere a funzionalità basate sull'account.", + "WinoAccount_LoginDialog_HeroTitle": "Bentornato", + "WinoAccount_LoginDialog_BenefitsTitle": "Cosa ti offre l'accesso", + "WinoAccount_LoginDialog_BenefitsDescription": "Usa il tuo account Wino per continuare a sincronizzare le impostazioni tra i dispositivi e per accedere a componenti aggiuntivi a pagamento come Wino AI Pack.", + "WinoAccount_LoginDialog_DifferenceTitle": "Questo non è l'accesso alla tua casella di posta", + "WinoAccount_LoginDialog_DifferenceDescription": "L'accesso qui non aggiunge né sostituisce i tuoi account Outlook, Gmail o IMAP in Wino. Ti autentica solo per i servizi specifici di Wino.", + "WinoAccount_LoginDialog_ForgotPasswordLink": "Password dimenticata?", + "WinoAccount_EmailLabel": "Email", + "WinoAccount_EmailPlaceholder": "name@example.com", + "WinoAccount_PasswordLabel": "Password", + "WinoAccount_ConfirmPasswordLabel": "Conferma password", + "WinoAccount_ForgotPasswordDialog_Title": "Reimposta la password", + "WinoAccount_ForgotPasswordDialog_PrimaryButton": "Invia email di reimpostazione", + "WinoAccount_ForgotPasswordDialog_BackToSignIn": "Torna all'accesso", + "WinoAccount_ForgotPasswordDialog_Description": "Inserisci l'indirizzo email del tuo account Wino e ti invieremo un link per reimpostare la password se l'indirizzo è registrato.", + "WinoAccount_Validation_EmailRequired": "Email obbligatoria.", + "WinoAccount_Validation_PasswordRequired": "La password è obbligatoria.", + "WinoAccount_Validation_PasswordMismatch": "Le password non corrispondono.", + "WinoAccount_Validation_PrivacyConsentRequired": "È necessario accettare l'informativa sulla privacy prima di creare un account Wino.", + "WinoAccount_Error_InvalidCredentials": "L'indirizzo email o la password non sono corretti.", + "WinoAccount_Error_AccountLocked": "Questo account è temporaneamente bloccato.", + "WinoAccount_Error_AccountBanned": "Questo account è stato bannato.", + "WinoAccount_Error_AccountSuspended": "Questo account è stato sospeso.", + "WinoAccount_Error_EmailNotConfirmed": "Si prega di confermare l'indirizzo email prima di accedere.", + "WinoAccount_Error_EmailConfirmationRequired": "Si prega di confermare l'indirizzo email prima di accedere.", + "WinoAccount_Error_EmailConfirmationResendNotAvailable": "Una nuova email di conferma non è ancora disponibile.", + "WinoAccount_Error_EmailConfirmationResendInvalid": "Questa richiesta di conferma non è più valida. Prova ad accedere di nuovo.", + "WinoAccount_Error_EmailNotRegistered": "Questo indirizzo email non è registrato.", + "WinoAccount_Error_RefreshTokenInvalid": "La tua sessione non è più valida. Effettua nuovamente l'accesso.", + "WinoAccount_Error_EmailAlreadyRegistered": "Questo indirizzo email è già registrato.", + "WinoAccount_Error_ExternalLoginEmailRequired": "È necessario un indirizzo email per completare l'accesso esterno.", + "WinoAccount_Error_ExternalLoginInvalid": "La richiesta di accesso esterno non è valida.", + "WinoAccount_Error_ExternalAuthStateInvalid": "Lo stato di autenticazione esterna non è valido o scaduto.", + "WinoAccount_Error_ExternalAuthCodeInvalid": "Il codice di autenticazione esterna non è valido o è scaduto.", + "WinoAccount_Error_AiPackRequired": "È prevista una sottoscrizione attiva a Wino AI Pack per questa azione.", + "WinoAccount_Error_AiQuotaExceeded": "Hai raggiunto il limite di utilizzo di AI Pack per il periodo di fatturazione corrente.", + "WinoAccount_Error_AiHtmlEmpty": "Non c'è contenuto email da elaborare.", + "WinoAccount_Error_AiHtmlTooLarge": "Questa email è troppo grande per essere processata con Wino AI.", + "WinoAccount_Error_AiUnsupportedLanguage": "Questa lingua non è supportata. Prova un codice culturale valido come en-US o tr-TR.", + "WinoAccount_Error_Forbidden": "Non hai l'autorizzazione per eseguire questa azione.", + "WinoAccount_Error_ValidationFailed": "La richiesta non è valida. Si prega di controllare i valori inseriti.", + "WinoAccount_RegisterSuccessMessage": "Registrazione dell'account Wino completata per {0}.", + "WinoAccount_LoginSuccessMessage": "Hai effettuato l'accesso a Wino Account come {0}.", + "WinoAccount_EmailConfirmationSentDialog_Title": "Conferma il tuo indirizzo email", + "WinoAccount_EmailConfirmationSentDialog_Message": "Abbiamo inviato una conferma via email a {0}. Confermala e prova ad accedere di nuovo.", + "WinoAccount_EmailConfirmationPendingDialog_Title": "Conferma email richiesta", + "WinoAccount_EmailConfirmationPendingDialog_Message": "Stiamo ancora aspettando che tu confermi {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ResendButton": "Riprova invio email di conferma", + "WinoAccount_EmailConfirmationPendingDialog_Countdown": "Puoi rinviare l'email di conferma tra {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ReadyToResend": "Ora puoi inviare nuovamente l'email di conferma.", + "WinoAccount_EmailConfirmationResentDialog_Title": "Email di conferma inviata nuovamente", + "WinoAccount_EmailConfirmationResentDialog_Message": "Abbiamo inviato un'altra email di conferma a {0}. Confermala e prova ad accedere di nuovo.", + "WinoAccount_ForgotPasswordDialog_SuccessTitle": "Email per reimpostare la password inviata", + "WinoAccount_ForgotPasswordDialog_SuccessMessage": "Abbiamo inviato un'email per reimpostare la password a {0}. Apri quel messaggio per scegliere una nuova password.", + "WinoAccount_ChangePassword_Title": "Cambia password", + "WinoAccount_ChangePassword_Description": "Invia un'email di reimpostazione della password a questo account Wino.", + "WinoAccount_ChangePassword_Action": "Invia email di reimpostazione", + "WinoAccount_ChangePassword_ConfirmationMessage": "Vuoi che Wino invii un'email di reimpostazione della password a {0}?", + "WinoAccount_SignOut_SuccessMessage": "Sei uscito dall'account Wino {0}.", + "WinoAccount_SignOut_NoAccountMessage": "Non esiste alcun account Wino attivo da disconnettere.", + "WinoAccount_Titlebar_SignedOutTitle": "Account Wino", + "WinoAccount_Titlebar_SignedOutDescription": "Accedi o crea un account Wino per gestire la tua sessione Wino.", + "WinoAccount_Titlebar_SignedInStatus": "Stato: {0}", + "WelcomeWizard_Step2Title": "Aggiungi account", + "WelcomeWizard_Step3Title": "Completa la configurazione", + "ProviderSelection_Title": "Scegli il provider di posta elettronica", + "ProviderSelection_Subtitle": "Seleziona un provider qui sotto per aggiungere il tuo account di posta a Wino Mail.", + "ProviderSelection_AccountNameHeader": "Nome account", + "ProviderSelection_AccountNamePlaceholder": "ad es. Personale, Lavoro", + "ProviderSelection_DisplayNameHeader": "Nome visualizzato", + "ProviderSelection_DisplayNamePlaceholder": "es. John Doe", + "ProviderSelection_EmailHeader": "Indirizzo e-mail", + "ProviderSelection_EmailPlaceholder": "es. johndoe@example.com", + "ProviderSelection_AppPasswordHeader": "Password specifica per l'app", + "ProviderSelection_AppPasswordHelp": "Come posso ottenere una password specifica per l'app?", + "ProviderSelection_CalendarModeHeader": "Integrazione del calendario", + "ProviderSelection_CalendarMode_DisabledTitle": "Disattivato", + "ProviderSelection_CalendarMode_DisabledDescription": "Nessuna integrazione del calendario", + "ProviderSelection_CalendarMode_CalDavTitle": "Sincronizzazione CalDAV", + "ProviderSelection_CalendarMode_CalDavDescription_Apple": "Gli eventi del tuo calendario sono sincronizzati sui server Apple tra i tuoi dispositivi.", + "ProviderSelection_CalendarMode_CalDavDescription_Yahoo": "Gli eventi del tuo calendario sono sincronizzati sui server Yahoo tra i tuoi dispositivi.", + "ProviderSelection_CalendarMode_LocalTitle": "Calendario locale", + "ProviderSelection_CalendarMode_LocalDescription": "I tuoi eventi sono memorizzati solo sul tuo computer. Nessuna connessione al server.", + "ProviderSelection_ClearColor": "Cancella colore", + "ProviderSelection_ContinueButton": "Continua", + "ProviderSelection_SpecialImap_Subtitle": "Inserisci le credenziali del tuo account per connetterti.", + "AccountSetup_Title": "Configurazione del tuo account", + "AccountSetup_Step_Authenticating": "Autenticazione in corso con {0}", + "AccountSetup_Step_TestingMailAuth": "Test dell'autenticazione della posta", + "AccountSetup_Step_SyncingFolders": "Sincronizzazione dei metadati delle cartelle", + "AccountSetup_Step_FetchingProfile": "Recupero delle informazioni del profilo", + "AccountSetup_Step_DiscoveringCalDav": "Scoperta delle impostazioni CalDAV", + "AccountSetup_Step_TestingCalendarAuth": "Test dell'autenticazione del calendario", + "AccountSetup_Step_SavingAccount": "Salvataggio delle informazioni dell'account", + "AccountSetup_Step_FetchingCalendarMetadata": "Recupero dei metadati del calendario", + "AccountSetup_Step_SyncingAliases": "Sincronizzazione degli alias", + "AccountSetup_Step_Finalizing": "Finalizzazione della configurazione", + "AccountSetup_FailureMessage": "Configurazione non riuscita. Torna indietro per correggere le impostazioni o riprova più tardi.", + "AccountSetup_SuccessMessage": "Il tuo account è stato configurato con successo!", + "AccountSetup_GoBackButton": "Torna indietro", + "AccountSetup_TryAgainButton": "Riprova", + "ImapCalDavSettings_AutoDiscoveryFailed": "Rilevamento automatico non riuscito. Si prega di inserire manualmente le impostazioni nella scheda Avanzate." } - - diff --git a/Wino.Core.Domain/Translations/ja_JP/resources.json b/Wino.Core.Domain/Translations/ja_JP/resources.json index 11497101..cae70cf4 100644 --- a/Wino.Core.Domain/Translations/ja_JP/resources.json +++ b/Wino.Core.Domain/Translations/ja_JP/resources.json @@ -8,6 +8,7 @@ "AccountCacheReset_Message": "This account requires full re-sychronization to continue working. Please wait while Wino re-synchronizes your messages...", "AccountContactNameYou": "You", "AccountCreationDialog_Completed": "完了", + "AccountCreationDialog_FetchingCalendarMetadata": "カレンダーの詳細を取得しています。", "AccountCreationDialog_FetchingEvents": "Fetching calendar events.", "AccountCreationDialog_FetchingProfileInformation": "Fetching profile details.", "AccountCreationDialog_GoogleAuthHelpClipboardText_Row0": "If your browser did not launch automatically to complete authentication:", @@ -17,6 +18,7 @@ "AccountCreationDialog_Initializing": "初期化中", "AccountCreationDialog_PreparingFolders": "現在フォルダ情報を取得中です。", "AccountCreationDialog_SigninIn": "アカウント情報を保存しています。", + "Purchased": "購入済み", "AccountEditDialog_Message": "アカウント名", "AccountEditDialog_Title": "アカウントの編集", "AccountPickerDialog_Title": "アカウントを選択", @@ -26,6 +28,10 @@ "AccountDetailsPage_Description": "Change the name of the account in Wino and set desired sender name.", "AccountDetailsPage_ColorPicker_Title": "Account color", "AccountDetailsPage_ColorPicker_Description": "Assign a new account color to colorize its symbol in the list.", + "AccountDetailsPage_TabGeneral": "一般", + "AccountDetailsPage_TabMail": "メール", + "AccountDetailsPage_TabCalendar": "カレンダー", + "AccountDetailsPage_CalendarListDescription": "設定を構成するカレンダーを選択してください。", "AddHyperlink": "追加", "AppCloseBackgroundSynchronizationWarningTitle": "Background Synchronization", "AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Application has not been set to launch on Windows startup.", @@ -47,8 +53,10 @@ "BasicIMAPSetupDialog_Title": "IMAPアカウント", "Busy": "Busy", "Buttons_AddAccount": "アカウントを追加", + "Buttons_FixAccount": "アカウントを修正", "Buttons_AddNewAlias": "Add New Alias", "Buttons_Allow": "Allow", + "Buttons_Apply": "適用", "Buttons_ApplyTheme": "テーマを適用", "Buttons_Browse": "閲覧", "Buttons_Cancel": "キャンセル", @@ -62,6 +70,7 @@ "Buttons_Edit": "編集", "Buttons_EnableImageRendering": "有効化", "Buttons_Multiselect": "複数選択", + "Buttons_Manage": "管理", "Buttons_No": "いいえ", "Buttons_Open": "開く", "Buttons_Purchase": "購入", @@ -70,15 +79,134 @@ "Buttons_Save": "保存", "Buttons_SaveConfiguration": "設定を保存", "Buttons_Send": "送信", + "Buttons_SendToServer": "サーバーに送信", "Buttons_Share": "シェアする", "Buttons_SignIn": "サインイン", "Buttons_Sync": "Synchronize", "Buttons_SyncAliases": "Synchronize Aliases", "Buttons_TryAgain": "やり直す", "Buttons_Yes": "はい", + "Sync_SynchronizingFolder": "同期中 {0} {1}", + "Sync_DownloadedMessages": "ダウンロード済み {0} 件のメッセージから {1}", + "SyncAction_Archiving": "{0} 通のメールをアーカイブしています", + "SyncAction_ClearingFlag": "{0} 通のメールのフラグを解除しています", + "SyncAction_CreatingDraft": "下書きを作成しています", + "SyncAction_CreatingEvent": "イベントを作成しています", + "SyncAction_Deleting": "{0} 通のメールを削除しています", + "SyncAction_EmptyingFolder": "フォルダーを空にしています", + "SyncAction_MarkingAsRead": "{0} 通のメールを既読にしています", + "SyncAction_MarkingAsUnread": "{0} 通のメールを未読にしています", + "SyncAction_MarkingFolderAsRead": "フォルダーを既読としてマークしています", + "SyncAction_Moving": "{0} 通のメールを移動しています", + "SyncAction_MovingToFocused": "{0} 通のメールを Focused に移動しています", + "SyncAction_RenamingFolder": "フォルダー名を変更しています", + "SyncAction_SendingMail": "メールを送信しています", + "SyncAction_SettingFlag": "{0} 通のメールにフラグを設定しています", + "SyncAction_SynchronizingAccount": "{0} を同期しています", + "SyncAction_SynchronizingAccounts": "{0} アカウントを同期しています", + "SyncAction_SynchronizingCalendarData": "カレンダーデータを同期しています", + "SyncAction_SynchronizingCalendarEvents": "カレンダーイベントを同期しています", + "SyncAction_SynchronizingCalendarMetadata": "カレンダーのメタデータを同期しています", + "SyncAction_Unarchiving": "{0} 通のメールを復元しています", "CalendarAllDayEventSummary": "all-day events", "CalendarDisplayOptions_Color": "Color", "CalendarDisplayOptions_Expand": "Expand", + "CalendarEventResponse_Accept": "承諾", + "CalendarEventResponse_AcceptedResponse": "承諾しました", + "CalendarEventResponse_Decline": "辞退", + "CalendarEventResponse_DeclinedResponse": "辞退しました", + "CalendarEventResponse_NotResponded": "未回答", + "CalendarEventResponse_Tentative": "仮承諾", + "CalendarEventResponse_TentativeResponse": "仮承諾", + "CalendarEventRsvpPanel_Accept": "承諾", + "CalendarEventRsvpPanel_AddMessage": "返答にメッセージを追加...(任意)", + "CalendarEventRsvpPanel_Decline": "辞退", + "CalendarEventRsvpPanel_Message": "メッセージ", + "CalendarEventRsvpPanel_SendReplyMessage": "返信メッセージを送信", + "CalendarEventRsvpPanel_Tentative": "仮承諾", + "CalendarEventRsvpPanel_Title": "回答オプション", + "CalendarAttendeeStatus_Accepted": "承諾済み", + "CalendarAttendeeStatus_Declined": "辞退済み", + "CalendarAttendeeStatus_NeedsAction": "対応が必要", + "CalendarAttendeeStatus_Tentative": "仮承諾", + "CalendarEventDetails_Attachments": "添付ファイル", + "CalendarEventCompose_AddAttachment": "添付ファイルを追加", + "CalendarEventCompose_AllDay": "終日", + "CalendarEventCompose_AttachmentsNotSupportedForCalDav": "CalDAV カレンダーでは添付ファイルはサポートされていません。", + "CalendarEventCompose_EndDate": "終了日", + "CalendarEventCompose_EndTime": "終了時刻", + "CalendarEventCompose_Every": "毎", + "CalendarEventCompose_ForWeekdays": "平日", + "CalendarEventCompose_FrequencyDay": "日", + "CalendarEventCompose_FrequencyDayPlural": "日", + "CalendarEventCompose_FrequencyMonth": "月", + "CalendarEventCompose_FrequencyMonthPlural": "ヶ月", + "CalendarEventCompose_FrequencyWeek": "週", + "CalendarEventCompose_FrequencyWeekPlural": "週間", + "CalendarEventCompose_FrequencyYear": "年", + "CalendarEventCompose_FrequencyYearPlural": "年", + "CalendarEventCompose_Location": "場所", + "CalendarEventCompose_LocationPlaceholder": "場所を追加", + "CalendarEventCompose_NewEventButton": "新規イベント", + "CalendarEventCompose_DefaultCalendarHint": "カレンダー設定で新規イベントのデフォルト カレンダーを選択できます。", + "CalendarEventCompose_DefaultCalendarSettingsLink": "カレンダー設定を開く", + "CalendarEventCompose_NoCalendarsMessage": "イベント作成のためのカレンダーはまだありません。", + "CalendarEventCompose_NoCalendarsTitle": "利用可能なカレンダーがありません", + "CalendarEventCompose_NoEndDate": "終了日なし", + "CalendarEventCompose_Notes": "メモ", + "CalendarEventCompose_PickCalendarTitle": "カレンダーを選択", + "CalendarEventCompose_Recurring": "繰り返し", + "CalendarEventCompose_RecurringSummary": "毎 {0} {1}{2} {3} 有効 {4}{5}", + "CalendarEventCompose_RecurringSummarySmart": "発生します {0}{1} {2} 有効 {3}{4}", + "CalendarEventCompose_RepeatEvery": "毎回", + "CalendarEventCompose_SelectCalendar": "カレンダーを選択", + "CalendarEventCompose_SingleOccurrenceSummary": " {0} {1} に発生します", + "CalendarEventCompose_StartDate": "開始日", + "CalendarEventCompose_StartTime": "開始時刻", + "CalendarEventCompose_TimeRangeSummary": " {0} から {1}", + "CalendarEventCompose_Title": "イベント名", + "CalendarEventCompose_TitlePlaceholder": "タイトルを追加", + "CalendarEventCompose_Until": "until", + "CalendarEventCompose_UntilSummary": " まで {0}", + "CalendarEventCompose_ValidationInvalidAllDayRange": "終日イベントの終了日は開始日より後でなければなりません。", + "CalendarEventCompose_ValidationInvalidAttendee": "1名以上の出席者のメールアドレスが無効です。", + "CalendarEventCompose_ValidationInvalidRecurrenceEnd": "繰り返しの終了日はイベント開始日と同日またはそれ以降でなければなりません。", + "CalendarEventCompose_ValidationInvalidTimeRange": "終了時刻は開始時刻より後でなければなりません。", + "CalendarEventCompose_ValidationMissingAttachment": "1つ以上の添付ファイルが利用できなくなっています: {0}", + "CalendarEventCompose_ValidationMissingCalendar": "イベントを作成する前にカレンダーを選択してください。", + "CalendarEventCompose_ValidationMissingTitle": "イベントを作成する前にイベントタイトルを入力してください。", + "CalendarEventCompose_ValidationTitle": "イベントの検証に失敗しました", + "CalendarEventCompose_WeekdaySummary": " on {0}", + "CalendarEventCompose_Weekday_Friday": "金", + "CalendarEventCompose_Weekday_Monday": "月", + "CalendarEventCompose_Weekday_Saturday": "土", + "CalendarEventCompose_Weekday_Sunday": "日", + "CalendarEventCompose_Weekday_Thursday": "木", + "CalendarEventCompose_Weekday_Tuesday": "火", + "CalendarEventCompose_Weekday_Wednesday": "水", + "CalendarEventDetails_Details": "詳細", + "CalendarEventDetails_EditSeries": "シリーズを編集", + "CalendarEventDetails_Editing": "編集中", + "CalendarEventDetails_InviteSomeone": "招待する", + "CalendarEventDetails_JoinOnline": "オンラインで参加", + "CalendarEventDetails_Organizer": "主催者", + "CalendarEventDetails_People": "参加者", + "CalendarEventDetails_ReadOnlyEvent": "読み取り専用イベント", + "CalendarEventDetails_Reminder": "リマインダー", + "CalendarReminder_StartedHoursAgo": "{0}時間前に開始しました", + "CalendarReminder_StartedMinutesAgo": "{0}分前に開始しました", + "CalendarReminder_StartedNow": "今しがた開始しました", + "CalendarReminder_StartingNow": "今、開始しています", + "CalendarReminder_StartsInHours": "{0}時間後に開始します", + "CalendarReminder_StartsInMinutes": "{0}分後に開始します", + "CalendarReminder_SnoozeAction": "スヌーズ", + "CalendarReminder_SnoozeMinutesOption": "{0}分", + "CalendarEventDetails_ShowAs": "表示方法", + "CalendarShowAs_Free": "空き", + "CalendarShowAs_Tentative": "暫定", + "CalendarShowAs_Busy": "予定あり", + "CalendarShowAs_OutOfOffice": "不在", + "CalendarShowAs_WorkingElsewhere": "別の場所で作業中", "CalendarItem_DetailsPopup_JoinOnline": "Join online", "CalendarItem_DetailsPopup_ViewEventButton": "View event", "CalendarItem_DetailsPopup_ViewSeriesButton": "View series", @@ -88,6 +216,9 @@ "ClipboardTextCopied_Message": "{0} copied to clipboard.", "ClipboardTextCopied_Title": "Copied", "ClipboardTextCopyFailed_Message": "Failed to copy {0} to clipboard.", + "ContactInfoBar_ErrorTitle": "連絡先情報の読み込みに失敗しました", + "ContactInfoBar_SuccessTitle": "連絡先情報を読み込みました", + "ContactInfoBar_WarningTitle": "連絡先情報が不完全の場合があります", "ComingSoon": "近日公開", "ComposerAttachmentsDragDropAttach_Message": "Attach", "ComposerAttachmentsDropZone_Message": "Drop your files here", @@ -129,6 +260,10 @@ "DialogMessage_CreateLinkedAccountTitle": "アカウントリンク名", "DialogMessage_DeleteAccountConfirmationMessage": "{0} を削除しますか?", "DialogMessage_DeleteAccountConfirmationTitle": "このアカウントに関連付けられたすべてのデータは永久にディスクから削除されます。", + "DialogMessage_DeleteEmailTemplateConfirmationMessage": "テンプレート \"{0}\" を削除しますか?", + "DialogMessage_DeleteEmailTemplateConfirmationTitle": "電子メールテンプレートを削除", + "DialogMessage_DeleteRecurringSeriesMessage": "これによりシリーズのすべてのイベントが削除されます。続行しますか?", + "DialogMessage_DeleteRecurringSeriesTitle": "繰り返しシリーズを削除", "DialogMessage_DiscardDraftConfirmationMessage": "この下書きは破棄されます。続行しますか?", "DialogMessage_DiscardDraftConfirmationTitle": "下書きを破棄", "DialogMessage_EmptySubjectConfirmation": "Missing Subject", @@ -172,11 +307,18 @@ "ElementTheme_Light": "ライトモード", "Emoji": "Emoji", "Error_FailedToSetupSystemFolders_Title": "Failed to setup system folders", + "Exception_AccountNeedsAttention_Title": "アカウントの対応が必要です", + "Exception_AccountNeedsAttention_Message": "'{0}' の対応が必要です。作業を続行するには対応してください。", + "Exception_WebView2RuntimeMissing_Message": "Wino Mail は Microsoft Edge WebView2 ランタイムを見つけられませんでした。メッセージ内容を正しく表示するには、ランタイムをインストールまたは修復してください。", + "Exception_WebView2RuntimeMissing_Title": "WebView2 ランタイムが必要です", "Exception_AuthenticationCanceled": "Authentication canceled", "Exception_CustomThemeExists": "This theme already exists.", "Exception_CustomThemeMissingName": "You must provide a name.", "Exception_CustomThemeMissingWallpaper": "You must provide a custom background image.", "Exception_FailedToSynchronizeAliases": "Failed to synchronize aliases", + "Exception_FailedToSynchronizeCalendarData": "カレンダーデータの同期に失敗しました", + "Exception_FailedToSynchronizeCalendarEvents": "カレンダーイベントの同期に失敗しました", + "Exception_FailedToSynchronizeCalendarMetadata": "カレンダーの詳細の同期に失敗しました", "Exception_FailedToSynchronizeFolders": "Failed to synchronize folders", "Exception_FailedToSynchronizeProfileInformation": "Failed to synchronize profile information", "Exception_GoogleAuthCallbackNull": "Callback uri is null on activation.", @@ -229,6 +371,32 @@ "HoverActionOption_MoveJunk": "Move to Junk", "HoverActionOption_ToggleFlag": "Flag / Unflag", "HoverActionOption_ToggleRead": "Read / Unread", + "KeyboardShortcuts_FailedToReset": "キーボードショートカットのリセットに失敗しました。", + "KeyboardShortcuts_FailedToUpdate": "キーボードショートカットの更新に失敗しました", + "KeyboardShortcuts_MailoperationAction": "アクション", + "KeyboardShortcuts_Action": "アクション", + "KeyboardShortcuts_FailedToLoad": "キーボードショートカットの読み込みに失敗しました。", + "KeyboardShortcuts_EnterKeyForShortcut": "ショートカットのキーを入力してください。", + "KeyboardShortcuts_SelectOperationForShortcut": "ショートカットで実行するアクションを選択してください。", + "KeyboardShortcuts_EnterKey": "ショートカットのキーを入力してください。", + "KeyboardShortcuts_SelectOperation": "ショートカットのアクションを選択してください。", + "KeyboardShortcuts_ShortcutInUse": "このショートカットはすでに別のショートカットで使用されています。", + "KeyboardShortcuts_FailedToSave": "ショートカットの保存に失敗しました。", + "KeyboardShortcuts_FailedToDelete": "ショートカットの削除に失敗しました。", + "KeyboardShortcuts_PageDescription": "迅速なメール操作のためのキーボードショートカットを設定します。キー入力フィールドにフォーカスしている間にキーを押すとショートカットを取得します。", + "KeyboardShortcuts_Add": "ショートカットを追加", + "KeyboardShortcuts_EditTitle": "キーボードショートカットを編集", + "KeyboardShortcuts_ResetToDefaults": "デフォルトにリセット", + "KeyboardShortcuts_PressKeysHere": "ここにキーを押してください...", + "KeyboardShortcuts_KeyCombination": "キーの組み合わせ", + "KeyboardShortcuts_FocusArea": "上のフィールドにフォーカスして、希望のキーの組み合わせを押してください", + "KeyboardShortcuts_Modifiers": "修飾キー", + "KeyboardShortcuts_Mode": "アプリモード", + "KeyboardShortcuts_ModeMail": "メール", + "KeyboardShortcuts_ModeCalendar": "カレンダー", + "KeyboardShortcuts_ActionToggleReadUnread": "既読/未読を切り替え", + "KeyboardShortcuts_ActionToggleFlag": "フラグを切り替え", + "KeyboardShortcuts_ActionToggleArchive": "アーカイブ/復元を切り替え", "ImageRenderingDisabled": "Image rendering is disabled for this message.", "ImapAdvancedSetupDialog_AuthenticationMethod": "Authentication method", "ImapAdvancedSetupDialog_ConnectionSecurity": "Connection security", @@ -295,12 +463,58 @@ "IMAPSetupDialog_Username": "Username", "IMAPSetupDialog_UsernamePlaceholder": "johndoe, johndoe@fabrikam.com, domain/johndoe", "IMAPSetupDialog_UseSameConfig": "Use the same username and password for sending email", + "ImapCalDavSettingsPage_TitleCreate": "IMAPとカレンダーの設定", + "ImapCalDavSettingsPage_TitleEdit": "IMAPとカレンダーの設定を編集", + "ImapCalDavSettingsPage_Subtitle": "このアカウントのIMAP/SMTPと任意のカレンダー同期を設定します。", + "ImapCalDavSettingsPage_BasicSectionTitle": "基本設定", + "ImapCalDavSettingsPage_BasicSectionDescription": "アイデンティティと資格情報を入力してください。Winoはサーバ設定を自動的に検出しようとします。", + "ImapCalDavSettingsPage_BasicTab": "基本", + "ImapCalDavSettingsPage_EnableCalendarSupport": "カレンダー機能を有効にする", + "ImapCalDavSettingsPage_AutoDiscoverButton": "メール設定を自動検出", + "ImapCalDavSettingsPage_AutoDiscoverySuccessMessage": "メール設定が検出され、適用されました。", + "ImapCalDavSettingsPage_AdvancedSectionTitle": "高度な設定", + "ImapCalDavSettingsPage_AdvancedSectionDescription": "自動検出が利用できない場合や正しくない場合は、サーバ設定を手動で入力してください。", + "ImapCalDavSettingsPage_AdvancedTab": "上級", + "ImapCalDavSettingsPage_CalendarSectionTitle": "カレンダー設定", + "ImapCalDavSettingsPage_CalendarSectionDescription": "このIMAPアカウントのカレンダーデータの扱いを選択してください。", + "ImapCalDavSettingsPage_CalendarModeHeader": "カレンダーモード", + "ImapCalDavSettingsPage_ConnectionSecurityHeader": "接続のセキュリティ", + "ImapCalDavSettingsPage_AuthenticationMethodHeader": "認証方法", + "ImapCalDavSettingsPage_CalendarModeDisabled": "無効", + "ImapCalDavSettingsPage_CalendarModeCalDav": "CalDAV 同期", + "ImapCalDavSettingsPage_CalendarModeLocalOnly": "ローカルカレンダーのみ", + "ImapCalDavSettingsPage_CalendarModeDisabledDescription": "このアカウントではカレンダーは無効です。", + "ImapCalDavSettingsPage_CalendarModeCalDavDescription": "カレンダー項目はCalDAVサーバーと同期されます。", + "ImapCalDavSettingsPage_CalendarModeLocalOnlyDescription": "カレンダー項目はこのコンピュータのみに保存され、ネットワークへは同期されません。", + "ImapCalDavSettingsPage_LocalCalendarLearnMore": "ローカルカレンダーの仕組み", + "ImapCalDavSettingsPage_LocalCalendarDialogTitle": "ローカルカレンダーのみ", + "ImapCalDavSettingsPage_LocalCalendarDialogMessage": "ローカルカレンダーはすべてのイベントをコンピュータのみに保存します。iCloud、Yahoo、その他の提供者へは同期されません。", + "ImapCalDavSettingsPage_CalDavServiceUrl": "CalDAVサービスURL", + "ImapCalDavSettingsPage_CalDavUsername": "CalDAV ユーザー名", + "ImapCalDavSettingsPage_CalDavPassword": "CalDAV パスワード", + "ImapCalDavSettingsPage_CalDavNotRequiredMessage": "CalDAVのテストは、カレンダーモードがCalDAV同期に設定されている場合にのみ必要です。", + "ImapCalDavSettingsPage_CalDavUrlRequired": "CalDAVサービスURLが必要です。", + "ImapCalDavSettingsPage_CalDavUrlInvalid": "CalDAVサービスURLは絶対URLである必要があります。", + "ImapCalDavSettingsPage_CalDavUsernameRequired": "CalDAV ユーザー名が必須です。", + "ImapCalDavSettingsPage_CalDavPasswordRequired": "CalDAV パスワードが必須です。", + "ImapCalDavSettingsPage_TestImapButton": "IMAP 接続をテスト", + "ImapCalDavSettingsPage_TestCalDavButton": "CalDAV 接続をテスト", + "ImapCalDavSettingsPage_ImapTestSuccessMessage": "IMAP 接続のテストに成功しました。", + "ImapCalDavSettingsPage_CalDavTestSuccessMessage": "CalDAV 接続のテストに成功しました。", + "ImapCalDavSettingsPage_SaveSuccessMessage": "アカウント設定を検証して保存しました。", + "ImapCalDavSettingsPage_ICloudHint": "Apple アカウント設定で生成したアプリ用パスワードを使用してください。", + "ImapCalDavSettingsPage_YahooHint": "Yahoo アカウントのセキュリティ設定からアプリ用パスワードを使用してください。", "Info_AccountCreatedMessage": "{0} is created", "Info_AccountCreatedTitle": "Account Creation", "Info_AccountCreationFailedTitle": "Account Creation Failed", "Info_AccountDeletedMessage": "{0} is successfuly deleted.", "Info_AccountDeletedTitle": "Account Deleted", "Info_AccountIssueFixFailedTitle": "Failed", + "Info_AccountIssueFixImapMessage": "IMAP およびカレンダーの設定ページを開き、再度サーバーの認証情報を入力してください。", + "Info_AccountAttentionRequiredMessage": "このアカウントは対応が必要です。", + "Info_AccountAttentionRequiredClickableMessage": "このアカウントを修正して再同期するにはクリックしてください。", + "Info_AccountAttentionRequiredAction": "修正", + "Info_AccountAttentionRequiredActionHint": "このアカウントの問題を解決するには、修正をクリックしてください。", "Info_AccountIssueFixSuccessMessage": "Fixed all account issues.", "Info_AccountIssueFixSuccessTitle": "Success", "Info_AttachmentOpenFailedMessage": "Can't open this attachment.", @@ -370,6 +584,7 @@ "InfoBarMessage_SynchronizationDisabledFolder": "This folder is disabled for synchronization.", "InfoBarTitle_SynchronizationDisabledFolder": "Disabled Folder", "Justify": "Justify", + "MenuUpdateAvailable": "更新が利用可能です", "Left": "Left", "Link": "Link", "LinkedAccountsCreatePolicyMessage": "you must have at least 2 accounts to create link\nlink will be removed on save", @@ -403,6 +618,7 @@ "MailOperation_Unarchive": "Unarchive", "MailOperation_ViewMessageSource": "View message source", "MailOperation_Zoom": "Zoom", + "MailsDragging": "{0} 件をドラッグ中", "MailsSelected": "{0} item(s) selected", "MarkFlagUnflag": "Mark as flagged/unflagged", "MarkReadUnread": "Mark as read/unread", @@ -434,6 +650,8 @@ "Notifications_MultipleNotificationsTitle": "New Mail", "Notifications_WinoUpdatedMessage": "Checkout new version {0}", "Notifications_WinoUpdatedTitle": "Wino Mail has been updated.", + "Notifications_StoreUpdateAvailableTitle": "更新が利用可能です", + "Notifications_StoreUpdateAvailableMessage": "Microsoft Store から新しいバージョンの Wino Mail をインストールできます。", "OnlineSearchFailed_Message": "Failed to perform search\n{0}\n\nListing offline mails.", "OnlineSearchTry_Line1": "Can't find what you are looking for?", "OnlineSearchTry_Line2": "Try online search.", @@ -446,7 +664,6 @@ "PaneLengthOption_Small": "Small", "Photos": "Photos", "PreparingFoldersMessage": "Preparing folders", - "ProtocolLogAvailable_Message": "Protocol logs are available for diagnostics.", "ProviderDetail_Gmail_Description": "Google Account", "ProviderDetail_iCloud_Description": "Apple iCloud Account", "ProviderDetail_iCloud_Title": "iCloud", @@ -465,9 +682,14 @@ "SearchBarPlaceholder": "Search", "SearchingIn": "Searching in", "SearchPivotName": "Results", + "Settings_KeyboardShortcuts_Title": "キーボードショートカット", + "Settings_KeyboardShortcuts_Description": "メールのクイック操作用キーボードショートカットを管理します。", "SettingConfigureSpecialFolders_Button": "Configure", "SettingsEditAccountDetails_IMAPConfiguration_Title": "IMAP/SMTP Configuration", "SettingsEditAccountDetails_IMAPConfiguration_Description": "Change your incoming/outgoing server settings.", + "SettingsEditAccountDetails_ImapCalDavSettings_Title": "IMAP およびカレンダー設定", + "SettingsEditAccountDetails_ImapCalDavSettings_Description": "このアカウント用の専用の IMAP、SMTP、CalDAV 設定ページを開きます。", + "SettingsEditAccountDetails_ImapCalDavSettings_Action": "設定を開く", "SettingsAbout_Description": "Learn more about Wino.", "SettingsAbout_Title": "About", "SettingsAboutGithub_Description": "Go to issue tracker GitHub repository.", @@ -490,6 +712,10 @@ "SettingsAppPreferences_SearchMode_Local": "Local", "SettingsAppPreferences_SearchMode_Online": "Online", "SettingsAppPreferences_SearchMode_Title": "Default search mode", + "SettingsAppPreferences_ApplicationMode_Title": "デフォルトのアプリケーションモード", + "SettingsAppPreferences_ApplicationMode_Description": "アクティベーションのタイプが明示的に設定されていない場合に、Wino が開くモードを選択します。", + "SettingsAppPreferences_ApplicationMode_Mail": "メール", + "SettingsAppPreferences_ApplicationMode_Calendar": "カレンダー", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description": "Wino Mail will keep running in the background. You will be notified as new mails arrive.", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title": "Run in the background", "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description": "Wino Mail will keep running on the system tray. Available to launch by clicking on an icon. You will be notified as new mails arrive.", @@ -506,12 +732,30 @@ "SettingsAppPreferences_StartupBehavior_FatalError": "Fatal error occurred while changing the startup mode for Wino Mail.", "SettingsAppPreferences_StartupBehavior_Title": "Start minimized on Windows startup", "SettingsAppPreferences_Title": "App Preferences", + "SettingsAppPreferences_HideWinoAccountButton_Title": "タイトルバーの Wino アカウントボタンを非表示にする", + "SettingsAppPreferences_HideWinoAccountButton_Description": "Wino アカウントのドロップダウンを開くタイトルバーのプロフィールボタンを非表示にします。", + "SettingsAppPreferences_StoreUpdateNotifications_Title": "ストアの更新通知", + "SettingsAppPreferences_StoreUpdateNotifications_Description": "Microsoft Store の更新が利用可能なときに通知とフッターのアクションを表示します。", + "SettingsAppPreferences_AiActions_Title": "AI アクション", + "SettingsAppPreferences_AiActions_Description": "デフォルトの AI 言語と要約を保存する場所を選択します。", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Title": "デフォルトの翻訳言語", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Description": "AI 翻訳アクションで使用されるデフォルトのターゲット言語を選択します。", + "SettingsAppPreferences_AiSummarizeLanguage_Title": "要約言語", + "SettingsAppPreferences_AiSummarizeLanguage_Description": "今後の AI 要約出力に使用する推奨要約言語を選択します。", + "SettingsAppPreferences_AiSummarySavePath_Title": "デフォルトの要約保存先", + "SettingsAppPreferences_AiSummarySavePath_Description": "AI 要約を保存する際に Wino がデフォルトで使用するフォルダを選択します。", + "SettingsAppPreferences_AiSummarySavePath_Placeholder": "システムの既定の保存場所を使用", + "SettingsAppPreferences_AiSummarySavePath_InvalidHint": "このフォルダは存在しません。要約のデフォルト保存場所を使用します。", "SettingsAutoSelectNextItem_Description": "Select the next item after you delete or move a mail.", "SettingsAutoSelectNextItem_Title": "Auto select next item", "SettingsAvailableThemes_Description": "Select a theme from Wino's own collection for your taste or apply your own themes.", "SettingsAvailableThemes_Title": "Available Themes", "SettingsCalendarSettings_Description": "Change first day of week, hour cell height and more...", "SettingsCalendarSettings_Title": "Calendar Settings", + "CalendarSettings_DefaultSnoozeDuration_Header": "デフォルトのスヌーズ期間", + "CalendarSettings_DefaultSnoozeDuration_Description": "カレンダーリマインダー通知のデフォルトのスヌーズ期間を設定します。", + "CalendarSettings_TimedDayHeaderFormat_Header": "時間表示日ヘッダの形式", + "CalendarSettings_TimedDayHeaderFormat_Description": "日表示、週表示、勤務週表示で上部の日ラベルの表示方法を選択します。ddd、dd、MMM、dddd などの日付フォーマットトークンを使用します。", "SettingsComposer_Title": "Composer", "SettingsComposerFont_Title": "Default Composer Font", "SettingsComposerFontFamily_Description": "Change the default font family and font size for composing mails.", @@ -531,6 +775,9 @@ "SettingsDiscord_Title": "Discord Channel", "SettingsEditLinkedInbox_Description": "Add / remove accounts, rename or break the link between accounts.", "SettingsEditLinkedInbox_Title": "Edit Linked Inbox", + "SettingsWindowBackdrop_Title": "ウィンドウの背景", + "SettingsWindowBackdrop_Description": "Wino ウィンドウに適用する背景効果を選択します。", + "SettingsWindowBackdrop_Disabled": "デフォルト以外のアプリテーマが選択されている場合、ウィンドウの背景の選択は無効です。", "SettingsElementTheme_Description": "Select a Windows theme for Wino", "SettingsElementTheme_Title": "Element Theme", "SettingsElementThemeSelectionDisabled": "Element theme selection is disabled when application theme is selected other than Default.", @@ -581,6 +828,8 @@ "SettingsManageAliases_Title": "Aliases", "SettingsEditAccountDetails_Title": "Edit Account Details", "SettingsEditAccountDetails_Description": "Change account name, sender name and assign a new color if you like.", + "EditAccountDetailsPage_SaveSuccess_Title": "変更を保存しました", + "EditAccountDetailsPage_SaveSuccess_Message": "アカウントの詳細が正常に更新されました。", "SettingsManageLink_Description": "Move items to add new link or remove existing link.", "SettingsManageLink_Title": "Manage Link", "SettingsMarkAsRead_Description": "Change what should happen to the selected item.", @@ -596,7 +845,41 @@ "SettingsNotifications_Title": "Notifications", "SettingsNotificationsAndTaskbar_Description": "Change whether notifications should be displayed and taskbar badge for this account.", "SettingsNotificationsAndTaskbar_Title": "Notifications & Taskbar", + "SettingsHome_Title": "ホーム", + "SettingsHome_SearchTitle": "設定を探す", + "SettingsHome_SearchDescription": "機能、トピック、キーワードで検索して、適切な設定ページへ直接ジャンプします。", + "SettingsHome_SearchPlaceholder": "設定を検索", + "SettingsHome_SearchExamples": "例: テーマ、ストレージ、言語、署名", + "SettingsHome_QuickLinks_Title": "クイックリンク", + "SettingsHome_QuickLinks_Description": "よく使われる設定へすぐ移動します。", + "SettingsHome_StorageCard_Description": "このデバイスにローカルに保存されている MIME コンテンツの量を確認し、必要に応じて整理します。", + "SettingsHome_StorageEmptySummary": "まだキャッシュした MIME コンテンツは検出されていません。", + "SettingsHome_StorageLoading": "ローカル MIME 使用状況を確認しています...", + "SettingsHome_Tips_Title": "ヒントとコツ", + "SettingsHome_Tips_Description": "いくつかの小さな変更で Wino をよりパーソナルに感じられるようになります。", + "SettingsHome_Tip_Theme": "ダークモードやアクセントの変更をしますか? パーソナライズを開いてください。", + "SettingsHome_Tip_Background": "起動時の動作とバックグラウンド同期を制御するには、アプリ設定を使用してください。", + "SettingsHome_Tip_Shortcuts": "キーボードショートカットを使うと、メールの操作がより速くなります。", + "SettingsHome_Resources_Title": "役立つリンク", + "SettingsHome_Resources_Description": "プロジェクトのリソース、サポート情報、リリースチャンネルを開きます。", "SettingsOptions_Title": "Settings", + "SettingsOptions_GeneralSection": "一般", + "SettingsOptions_MailSection": "メール", + "SettingsOptions_CalendarSection": "カレンダー", + "SettingsOptions_MoreComingSoon": "追加のオプションは近日公開予定", + "SettingsOptions_HeroDescription": "Wino Mail の体験をカスタマイズ", + "SettingsOptions_AccountsSummary": "{0} アカウントが設定されています。", + "SettingsSearch_ManageAccounts_Keywords": "アカウント;アカウント;メールボックス;メールボックス;エイリアス;エイリアス;プロフィール;アドレス;アドレス", + "SettingsSearch_AppPreferences_Keywords": "起動;バックグラウンド;同期;通知;通知;検索;トレイ;デフォルト", + "SettingsSearch_LanguageTime_Keywords": "言語;時刻;時計;ロケール;地域;形式;24時間;24h", + "SettingsSearch_Personalization_Keywords": "テーマ;ダーク;ライト;外観;アクセント;カラー;モード;レイアウト;密度", + "SettingsSearch_About_Keywords": "情報;バージョン;ウェブサイト;プライバシー;GitHub;寄付;ストア;サポート", + "SettingsSearch_KeyboardShortcuts_Keywords": "ショートカット;ショートカット;ホットキー;ホットキー;キーボード;キー", + "SettingsSearch_MessageList_Keywords": "メッセージ;メッセージ;リスト;スレッド;スレッド;アバター;プレビュー;差出人", + "SettingsSearch_ReadComposePane_Keywords": "リーダー;作成;作成者;フォント;フォント;外部コンテンツ;表示;読み取り", + "SettingsSearch_SignatureAndEncryption_Keywords": "署名;署名;暗号化;証明書;証明書;S/MIME;SMIME;セキュリティ", + "SettingsSearch_Storage_Keywords": "ストレージ;キャッシュ;キャッシュ;MIME;ディスク;容量;クリーンアップ;クリーンアップ;ローカルデータ", + "SettingsSearch_CalendarSettings_Keywords": "カレンダー;週;時間;予定;イベント;イベント", "SettingsPaneLengthReset_Description": "Reset the size of the mail list to original if you have issues with it.", "SettingsPaneLengthReset_Title": "Reset Mail List Size", "SettingsPaypal_Description": "Show much more love ❤️ All donations are appreciated.", @@ -610,6 +893,8 @@ "SettingsPrefer24HourClock_Title": "Display Clock Format in 24 Hours", "SettingsPrivacyPolicy_Description": "Review privacy policy.", "SettingsPrivacyPolicy_Title": "Privacy Policy", + "SettingsWebsite_Description": "Wino Mail のウェブサイトを開きます。", + "SettingsWebsite_Title": "ウェブサイト", "SettingsReadComposePane_Description": "Fonts, external content.", "SettingsReadComposePane_Title": "Reader & Composer", "SettingsReader_Title": "Reader", @@ -625,6 +910,19 @@ "SettingsShowPreviewText_Title": "Show Preview Text", "SettingsShowSenderPictures_Description": "Hide/show the thumbnail sender pictures.", "SettingsShowSenderPictures_Title": "Show Sender Avatars", + "SettingsEmailTemplates_Title": "メールテンプレート", + "SettingsEmailTemplates_Description": "メールテンプレートを管理", + "SettingsEmailTemplates_CreatePageTitle": "新しいテンプレート", + "SettingsEmailTemplates_EditPageTitle": "テンプレートを編集", + "SettingsEmailTemplates_NewTemplateTitle": "新しいテンプレート", + "SettingsEmailTemplates_NewTemplateDescription": "新しいメール テンプレートを作成", + "SettingsEmailTemplates_NameTitle": "名前", + "SettingsEmailTemplates_NamePlaceholder": "テンプレート名", + "SettingsEmailTemplates_DescriptionTitle": "説明", + "SettingsEmailTemplates_DescriptionPlaceholder": "任意の説明", + "SettingsEmailTemplates_ContentTitle": "テンプレート内容", + "SettingsEmailTemplates_ContentDescription": "このテンプレートの HTML コンテンツを編集", + "SettingsEmailTemplates_NameRequired": "テンプレート名は必須です。", "SettingsEnableGravatarAvatars_Title": "Gravatar", "SettingsEnableGravatarAvatars_Description": "Use gravatar (if available) as sender picture", "SettingsEnableFavicons_Title": "Domain icons (Favicons)", @@ -645,6 +943,33 @@ "SettingsStartupItem_Title": "Startup Item", "SettingsStore_Description": "Show some love ❤️", "SettingsStore_Title": "Rate in Store", + "SettingsStorage_Title": "ストレージ", + "SettingsStorage_Description": "ローカルデータフォルダに格納された MIME キャッシュをスキャンして管理します。", + "SettingsStorage_ScanFolder": "ローカルデータフォルダをスキャン", + "SettingsStorage_NoLocalMimeDataFound": "ローカル MIME データが見つかりません。", + "SettingsStorage_NoAccountsFound": "アカウントが見つかりません。", + "SettingsStorage_TotalUsage": "ローカル MIME 使用量の合計: {0}", + "SettingsStorage_AccountUsageDescription": "{0} がローカル MIME キャッシュで使用されています", + "SettingsStorage_DeleteAll_Title": "すべての MIME コンテンツを削除", + "SettingsStorage_DeleteAll_Description": "このアカウントの MIME キャッシュフォルダー全体を削除します。", + "SettingsStorage_DeleteAll_Button": "すべて削除", + "SettingsStorage_DeleteAll_Confirm_Title": "すべての MIME コンテンツを削除", + "SettingsStorage_DeleteAll_Confirm_Message": "{0} のローカル MIME データをすべて削除しますか?", + "SettingsStorage_DeleteAll_Success": "すべての MIME コンテンツを削除しました。", + "SettingsStorage_DeleteOld_Title": "古い MIME コンテンツを削除", + "SettingsStorage_DeleteOld_Description": "ローカルデータベースのメール作成日に基づいて MIME ファイルを削除します。", + "SettingsStorage_DeleteOld_1Month": "1か月以上", + "SettingsStorage_DeleteOld_3Months": "3か月以上", + "SettingsStorage_DeleteOld_6Months": "6か月以上", + "SettingsStorage_DeleteOld_1Year": "1年以上", + "SettingsStorage_DeleteOld_Confirm_Title": "古い MIME コンテンツを削除", + "SettingsStorage_DeleteOld_Confirm_Message": "{1} のローカル MIME データを {0} より古いものとして削除しますか?", + "SettingsStorage_DeleteOld_Success": "{1} より古い {0} 個の MIME フォルダーを削除しました。", + "SettingsStorage_1Month": "1か月", + "SettingsStorage_3Months": "3か月", + "SettingsStorage_6Months": "6か月", + "SettingsStorage_1Year": "1年", + "SettingsStorage_Months": "{0} か月", "SettingsTaskbarBadge_Description": "Include unread mail count in taskbar icon.", "SettingsTaskbarBadge_Title": "Taskbar Badge", "SettingsThreads_Description": "Organize messages into conversation threads.", @@ -683,6 +1008,9 @@ "SystemFolderConfigDialogValidation_InboxSelected": "You can't assign Inbox folder to any other system folder.", "SystemFolderConfigSetupSuccess_Message": "System folders are successfully configured.", "SystemFolderConfigSetupSuccess_Title": "System Folders Setup", + "SystemTrayMenu_ShowWino": "Wino Mail を開く", + "SystemTrayMenu_ShowWinoCalendar": "Wino カレンダーを開く", + "SystemTrayMenu_ExitWino": "終了", "TestingImapConnectionMessage": "Testing server connection...", "TitleBarServerDisconnectedButton_Description": "Wino is disconnected from the network. Click reconnect to restore connection.", "TitleBarServerDisconnectedButton_Title": "no connection", @@ -699,8 +1027,422 @@ "WinoUpgradeMessage": "Upgrade to Unlimited Accounts", "WinoUpgradeRemainingAccountsMessage": "{0} out of {1} free accounts used.", "Yesterday": "Yesterday", + "Smime_ImportCertificates_Success": "証明書のインポートに成功しました。", + "Smime_ImportCertificates_Error": "証明書のインポート中にエラーが発生しました: {0}", + "Smime_RemoveCertificates_Confirm": "{0} の証明書を本当に削除しますか?", + "Smime_RemoveCertificates_Success": "証明書を削除しました。", + "Smime_ExportCertificates_Success": "証明書をエクスポートしました。", + "Smime_ExportCertificates_Error": "証明書のエクスポート中にエラーが発生しました。", + "Smime_CertificateDetails": "件名: {0}\\n発行者: {1}\\n有効開始: {2}\\n有効期限: {3}\\nサムプリント: {4}", + "Smime_CertificatePassword_Title": "証明書のパスワードが必要です", + "Smime_CertificatePassword_Placeholder": "{0} の証明書のパスワード(任意)", + "Smime_Confirm_Title": "確認", + "Buttons_OK": "OK", + "Buttons_Refresh": "更新", + "SettingsSignatureAndEncryption_Title": "署名と暗号化", + "SettingsSignatureAndEncryption_Description": "メールの署名と暗号化のための S/MIME 証明書を管理します。", + "SettingsSignatureAndEncryption_MyCertificatesHeader": "自分の証明書", + "SettingsSignatureAndEncryption_MyCertificatesDescription": "署名と暗号化の個人証明書", + "SettingsSignatureAndEncryption_RecipientCertificatesHeader": "受信者の証明書", + "SettingsSignatureAndEncryption_RecipientCertificatesDescription": "復号用の受信者証明書", + "SettingsSignatureAndEncryption_NameColumn": "名前", + "SettingsSignatureAndEncryption_ExpiresColumn": "有効期限", + "SettingsSignatureAndEncryption_ThumbprintColumn": "サムプリント", + "Buttons_Remove": "削除", + "Buttons_Export": "エクスポート", + "Buttons_Import": "インポート", + "SettingsSignatureAndEncryption_SigningCertificate": "S/MIME 署名証明書", + "SettingsSignatureAndEncryption_EncryptionCertificate": "S/MIME 暗号化", + "SettingsSignatureAndEncryption_SigningCertificatePlaceholder": "なし", + "SmimeSignaturesInMessage": "このメッセージの署名:", + "SmimeSignatureEntry": "• {0} {1}({2}、有効期限 {3} - {4})", + "SmimeSigningCertificateInfoTitle": "S/MIME 署名証明書情報", + "SmimeCertificateInfoTitle": "S/MIME 証明書情報", + "SmimeNoCertificateFileFound": "証明書ファイルが見つかりません", + "SmimeSaveCertificate": "証明書を保存...", + "SmimeCertificate": "S/MIME 証明書", + "SmimeCertificateSavedTo": "証明書は {0} に保存されました", + "SmimeSignedTooltip": "このメッセージは S/MIME 証明書で署名されています。詳細を見るにはクリックしてください", + "SmimeEncryptedTooltip": "このメッセージは S/MIME 証明書で暗号化されています。", + "SmimeCertificateFileInfo": "ファイル: {0}", + "Composer_LightTheme": "ライトテーマ", + "Composer_DarkTheme": "ダークテーマ", + "Composer_Outdent": "インデントを減らす", + "Composer_Indent": "インデントを追加", + "Composer_BulletList": "箇条書き", + "Composer_OrderedList": "番号付きリスト", + "Composer_Stroke": "ストローク", + "Composer_Bold": "太字", + "Composer_Italic": "斜体", + "Composer_Underline": "下線", + "Composer_CcBcc": "Cc と Bcc", + "Composer_EnableSmimeSignature": "S/MIME 署名の有効化/無効化", + "Composer_EnableSmimeEncryption": "S/MIME 暗号化の有効化/無効化", + "Composer_LocalDraftSyncInfo": "この下書きはローカルのみです。Wino がこの下書きをメールサーバーへ送信できませんでした。サーバーへ再送信するにはクリックしてください。", + "Composer_CertificateExpires": "有効期限: ", + "Composer_SmimeSignature": "S/MIME 署名", + "Composer_SmimeEncryption": "S/MIME 暗号化", + "Composer_EmailTemplatesPlaceholder": "メール テンプレート", + "Composer_AiSummarize": "AI で要約", + "Composer_AiSummarizeDescription": "このメールから要点、アクション項目、決定事項を抽出します。", + "Composer_AiTranslate": "AI で翻訳", + "Composer_AiActions": "AI アクション", + "Composer_AiRewrite": "AI で言い換え", + "AiActions_CheckingStatus": "AI アクセスを確認しています...", + "AiActions_SignedOutTitle": "Wino AI パックのロックを解除", + "AiActions_SignedOutDescription": "Wino ア카ウントにサインインし、AI パックのアドオンを有効にした後、AI を使用してメールを翻訳・リライト・要約します。", + "AiActions_NoPackTitle": "AI パックが必要です", + "AiActions_NoPackDescription": "サインインしていますが、AI パックはまだ有効になっていません。Wino の AI 翻訳、リライト、要約機能を使用するには購入してください。", + "AiActions_UsageSummary": "今月は {0} / {1} クレジットを使用しました。", + "Composer_AiRewritePolite": "丁寧に書き直す", + "Composer_AiRewritePoliteDescription": "意図を変えず、表現を柔らかくします。", + "Composer_AiRewriteAngry": "怒りっぽく書く", + "Composer_AiRewriteAngryDescription": "より鋭く、対立的な口調を使用します。", + "Composer_AiRewriteHappy": "明るいトーンにする", + "Composer_AiRewriteHappyDescription": "より明るく、熱意のあるトーンを追加します", + "Composer_AiRewriteFormal": "フォーマルな表現にする", + "Composer_AiRewriteFormalDescription": "メッセージをより専門的で整理された表現にします", + "Composer_AiRewriteFriendly": "親しみやすいトーンにする", + "Composer_AiRewriteFriendlyDescription": "より親しみやすいトーンでメッセージを温かくします", + "Composer_AiRewriteShorter": "短くする", + "Composer_AiRewriteShorterDescription": "文章を簡潔にし、不要な詳細を削除します", + "Composer_AiRewriteClearer": "よりわかりやすくする", + "Composer_AiRewriteClearerDescription": "読みやすさを高め、メッセージの理解をより容易にします", + "Composer_AiRewriteCustom": "カスタム", + "Composer_AiRewriteCustomDescription": "自分の書き換え意図を説明してください", + "Composer_AiRewriteCustomPlaceholder": "メッセージをどのように書き換えたいかを説明してください", + "Composer_AiRewriteMode": "トーンの書き換え", + "Composer_AiRewriteApply": "書き換えを適用", + "Composer_AiTranslateDialogTitle": "AIで翻訳", + "Composer_AiTranslateDialogDescription": "ターゲット言語またはカルチャコードを en-US、tr-TR、de-DE、fr-FR などの形式で入力してください", + "Composer_AiTranslateApply": "翻訳", + "Composer_AiTranslateLanguage": "対象言語", + "Composer_AiTranslateCustomPlaceholder": "カルチャコードを入力してください", + "Composer_AiTranslateLanguageEnglish": "英語 (en-US)", + "Composer_AiTranslateLanguageTurkish": "トルコ語 (tr-TR)", + "Composer_AiTranslateLanguageGerman": "ドイツ語 (de-DE)", + "Composer_AiTranslateLanguageFrench": "フランス語 (fr-FR)", + "Composer_AiTranslateLanguageSpanish": "スペイン語 (es-ES)", + "Composer_AiTranslateLanguageItalian": "イタリア語 (it-IT)", + "Composer_AiTranslateLanguagePortugueseBrazil": "ポルトガル語(ブラジル) (pt-BR)", + "Composer_AiTranslateLanguageDutch": "オランダ語 (nl-NL)", + "Composer_AiTranslateLanguagePolish": "ポーランド語 (pl-PL)", + "Composer_AiTranslateLanguageRussian": "ロシア語 (ru-RU)", + "Composer_AiTranslateLanguageJapanese": "日本語 (ja-JP)", + "Composer_AiTranslateLanguageKorean": "韓国語 (ko-KR)", + "Composer_AiTranslateLanguageChineseSimplified": "中国語(簡体字)(zh-CN)", + "Composer_AiTranslateLanguageArabic": "アラビア語 (ar-SA)", + "Composer_AiTranslateLanguageHindi": "ヒンディー語 (hi-IN)", + "Composer_AiTranslateLanguageOther": "その他...", + "Composer_AiBusyTitle": "AIはすでに処理中です", + "Composer_AiBusyMessage": "現在のAI処理が完了するのを待ってください", + "Composer_AiSignInRequired": "AI機能を利用するには、Winoアカウントにサインインしてください", + "Composer_AiMissingHtml": "Wino AIに送信するメッセージ内容がまだありません", + "Composer_AiQuotaUnavailable": "AIの結果が適用されました", + "Composer_AiAppliedMessage": "AIの結果が作成エディタに適用されました。元に戻すには取り消しを使ってください", + "Composer_AiSummarizeSuccessTitle": "AI要約を適用しました", + "Composer_AiTranslateSuccessTitle": "AI翻訳を適用しました", + "Composer_AiRewriteSuccessTitle": "AI書き換えを適用しました", + "Composer_AiErrorTitle": "AIの処理に失敗しました", + "Reader_AiAppliedMessage": "このメッセージにAIの結果が表示されました。元の内容を再度表示するには、メッセージを再度開いてください", "SettingsAppPreferences_EmailSyncInterval_Title": "Email sync interval", - "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail." + "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail.", + "ContactsPage_Title": "連絡先", + "ContactsPage_AddContact": "連絡先を追加", + "ContactsPage_EditContact": "連絡先を編集", + "ContactsPage_DeleteContact": "連絡先を削除", + "ContactsPage_SearchPlaceholder": "連絡先を検索...", + "ContactsPage_NoContacts": "連絡先が見つかりません", + "ContactsPage_ContactsCount": "{0} 件の連絡先", + "ContactsPage_SelectedContactsCount": "{0}件が選択されています", + "ContactsPage_DeleteSelectedContacts": "選択した連絡先を削除", + "ContactEditDialog_Title": "連絡先を編集", + "ContactEditDialog_PhotoSection": "写真", + "ContactEditDialog_ChoosePhoto": "写真を選択", + "ContactEditDialog_RemovePhoto": "写真を削除", + "ContactEditDialog_NameHeader": "名前", + "ContactEditDialog_NamePlaceholder": "連絡先名", + "ContactEditDialog_EmailHeader": "メールアドレス", + "ContactEditDialog_EmailPlaceholder": "contact@example.com", + "ContactEditDialog_InfoSection": "連絡先情報", + "ContactEditDialog_RootContactInfo": "これはあなたのアカウントに関連付けられたルート連絡先です。削除できません", + "ContactEditDialog_OverriddenContactInfo": "この連絡先は手動で変更されており、同期時には更新されません", + "ContactsPage_Subtitle": "メールの連絡先と情報を管理します", + "ContactStatus_Account": "アカウント", + "ContactStatus_Modified": "変更済み", + "ContactAction_Edit": "連絡先を編集", + "ContactAction_ChangePhoto": "写真を変更", + "ContactAction_Delete": "連絡先を削除", + "ContactAction_Add": "連絡先を追加", + "ContactSelection_Selected": "選択済み", + "ContactSelection_SelectAll": "すべて選択", + "ContactSelection_Clear": "選択をクリア", + "ContactsPage_EmptyState": "表示する連絡先がありません", + "ContactsPage_AddFirstContact": "最初の連絡先を追加", + "ContactsPage_ContactsCountSuffix": "件", + "ContactsPane_NewContact": "新しい連絡先", + "ContactsPane_DescriptionTitle": "連絡先を管理する", + "ContactsPane_DescriptionBody": "連絡先を作成し、名前を変更し、プロフィール写真を更新し、保存済みの詳細を1か所に整理します", + "ContactEditDialog_AddTitle": "連絡先を追加", + "ContactInfoBar_ContactAdded": "連絡先を追加しました", + "ContactInfoBar_ContactUpdated": "連絡先を更新しました", + "ContactInfoBar_ContactsDeleted": "連絡先を削除しました", + "ContactInfoBar_ContactPhotoUpdated": "連絡先の写真を更新しました", + "ContactInfoBar_FailedToLoadContacts": "連絡先の読み込みに失敗しました: {0}", + "ContactInfoBar_FailedToAddContact": "連絡先の追加に失敗しました: {0}", + "ContactInfoBar_FailedToUpdateContact": "連絡先の更新に失敗しました: {0}", + "ContactInfoBar_FailedToDeleteContacts": "連絡先の削除に失敗しました: {0}", + "ContactInfoBar_FailedToUpdatePhoto": "写真の更新に失敗しました: {0}", + "ContactInfoBar_CannotDeleteRoot": "ルート連絡先は削除できません", + "ContactConfirmDialog_DeleteTitle": "連絡先を削除", + "ContactConfirmDialog_DeleteMessage": "以下の連絡先 '{0}' を削除してもよろしいですか?", + "ContactConfirmDialog_DeleteMultipleMessage": "{0} 件の連絡先を削除してもよろしいですか?", + "ContactConfirmDialog_DeleteButton": "削除", + "CalendarAccountSettings_Title": "カレンダーアカウント設定", + "CalendarAccountSettings_Description": "{0} のカレンダー設定を管理します", + "CalendarAccountSettings_AccountColor": "アカウントの色", + "CalendarAccountSettings_AccountColorDescription": "このカレンダーアカウントの表示色を変更します。", + "CalendarAccountSettings_SyncEnabled": "同期を有効にする", + "CalendarAccountSettings_SyncEnabledDescription": "このアカウントのカレンダー同期を有効または無効にします。", + "CalendarAccountSettings_DefaultShowAs": "デフォルト表示ステータス", + "CalendarAccountSettings_DefaultShowAsDescription": "このアカウントで作成される新しいイベントのデフォルト可用性ステータス。", + "CalendarAccountSettings_PrimaryCalendar": "メインカレンダー", + "CalendarAccountSettings_PrimaryCalendarDescription": "このカレンダーをアカウントのメインカレンダーとして設定します。", + "CalendarSettings_NewEventBehavior_Header": "新規イベントボタンの動作", + "CalendarSettings_NewEventBehavior_Description": "新規イベントボタンを押すと毎回カレンダーを選択するか、特定のカレンダーを常に開くかを選択します。", + "CalendarSettings_NewEventBehavior_AskEachTime": "都度確認する", + "CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar": "常に特定のカレンダーを使用する。", + "CalendarSettings_Rendering_Title": "レンダリング", + "CalendarSettings_Rendering_Description": "カレンダーのレイアウトと表示動作を設定します。", + "CalendarSettings_Notifications_Title": "通知", + "CalendarSettings_Notifications_Description": "デフォルトのリマインダーとスヌーズ動作を選択します。", + "CalendarSettings_Preferences_Title": "設定", + "CalendarSettings_Preferences_Description": "新規イベントボタンの動作を設定します。", + "WhatIsNew_GetStartedButton": "はじめる", + "WhatIsNew_ContinueAnywayButton": "とにかく続行", + "WhatIsNew_PreparingForNewVersionButton": "新しいバージョンの準備中...", + "WhatIsNew_MigrationPreparing_Title": "データを準備しています", + "WhatIsNew_MigrationPreparing_Description": "Wino は更新のマイグレーションを適用しています。今回のリリースの準備のため、アカウントデータの準備が完了するまでお待ちください。", + "WhatIsNew_MigrationFailedMessage": "マイグレーションの適用がエラーコード {0} で失敗しました。引き続きアプリケーションを使用できます。ただし重大な問題が発生した場合は、アプリを再インストールしてください。", + "WhatIsNew_MigrationNotification_Title": "Wino Mail が更新されました", + "WhatIsNew_MigrationNotification_Message": "アップデートを完了して新機能を確認するには、アプリを開いてください。", + "WelcomeWindow_Title": "Wino Mail へようこそ", + "WelcomeWindow_Subtitle": "メールとカレンダーのネイティブなWindows体験。", + "WelcomeWindow_WhatsNewTitle": "最新の変更点", + "WelcomeWindow_FeaturesTitle": "機能", + "WelcomeWindow_WhatsNewTab": "新機能", + "WelcomeWindow_FeaturesTab": "機能", + "WelcomeWindow_GetStartedButton": "アカウントを追加して始める", + "WelcomeWindow_GetStartedDescription": "Wino Mail を始めるには、Outlook、Gmail、または IMAP アカウントを追加してください。", + "WelcomeWindow_ImportFromWinoAccount": "Wino アカウントからインポート", + "WelcomeWindow_ImportInProgress": "同期された設定とアカウントをインポートしています...", + "WelcomeWindow_ImportNoAccountsFound": "Wino アカウントに同期されたアカウントは見つかりませんでした。設定が利用可能だった場合、それらは復元されました。アカウントを手動で追加するには、はじめるを使ってください。", + "WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} 個の同期済みアカウントはすでにこのデバイスにあります。必要に応じて、別のアカウントを手動で追加するにははじめるを使用してください。", + "WelcomeWindow_SetupTitle": "アカウントの設定", + "WelcomeWindow_SetupSubtitle": "開始するにはメールプロバイダを選択してください", + "WelcomeWindow_AddAccountButton": "アカウントを追加", + "WelcomeWindow_SkipForNow": "とりあえずスキップ — 後で設定します", + "WelcomeWindow_AppDescription": "高速で集中できる受信箱 — Windows 11 向けに再設計されています。", + "WelcomeWizard_Step1Title": "ようこそ", + "SystemTrayMenu_Open": "開く", + "WinoAccount_Titlebar_SyncBenefitTitle": "同期設定", + "WinoAccount_Titlebar_SyncBenefitDescription": "Wino の設定をデバイス間で同期します。", + "WinoAccount_Titlebar_AddonsBenefitTitle": "アドオンを有効化", + "WinoAccount_Titlebar_AddonsBenefitDescription": "Wino AI Pack のようなプレミアム機能にアクセスできます。", + "WinoAccount_Management_Description": "Wino アカウント、AI Pack へのアクセス、同期設定とアカウントの詳細を管理します。", + "WinoAccount_Management_SignedOutTitle": "Wino Mail にサインイン", + "WinoAccount_Management_SignedOutDescription": "メールを同期し、AI機能にアクセスし、デバイス間で設定を管理するにはサインインするかアカウントを作成してください。", + "WinoAccount_Management_ProfileSectionHeader": "プロフィール", + "WinoAccount_Management_AddOnsSectionHeader": "Wino アドオン", + "WinoAccount_Management_DataSectionHeader": "データ", + "WinoAccount_Management_AccountActionsSectionHeader": "アカウント操作", + "WinoAccount_Management_AccountCardTitle": "アカウント", + "WinoAccount_Management_AccountCardDescription": "Wino アカウントのメールアドレスと現在のアカウント状態。", + "WinoAccount_Management_AiPackCardTitle": "AIパック", + "WinoAccount_Management_AiPackCardDescription": "Wino AI Pack が有効かどうかと残りの使用量を確認します。", + "WinoAccount_Management_AiPackActive": "AIパックは有効です", + "WinoAccount_Management_AiPackInactive": "AIパックは有効ではありません", + "WinoAccount_Management_AiPackUsage": "{1} のうち {0} 使用済み。残り {2}。", + "WinoAccount_Management_AiPackBillingPeriod": "請求期間: {0:d} - {1:d}", + "WinoAccount_Management_AiPackUnknownUsage": "使用量の詳細はまだ利用できません。", + "WinoAccount_Management_AiPackBuyDescription": "AI を使ってメールを翻訳・書き換え・要約するには、Wino AI Pack を購入してください。", + "WinoAccount_Management_AiPackPromoTitle": "AI Pack をアンロック", + "WinoAccount_Management_AiPackPromoDescription": "AI 機能を搭載したツールでメール作業を強化します。50言語以上への翻訳、明確さとトーンのための書換え、長いスレッドの即時要約を提供します。", + "WinoAccount_Management_AiPackPromoPrice": "$4.99/月", + "WinoAccount_Management_AiPackPromoRequests": "1,000 クレジット", + "WinoAccount_Management_AiPackGetButton": "AI Packを入手", + "WinoAddOn_AI_PACK_Name": "Wino AI Pack", + "WinoAddOn_AI_PACK_Description": "Wino Mail で翻訳、書き換え、要約を行うAI搭載ツールです。", + "WinoAddOn_AI_PACK_Keywords": "AI、翻訳、書き換え、要約、生産性", + "WinoAddOn_UNLIMITED_ACCOUNTS_Name": "無制限アカウント", + "WinoAddOn_UNLIMITED_ACCOUNTS_Description": "アカウントの上限を解除し、必要なだけメールアカウントを追加できます。", + "WinoAddOn_UNLIMITED_ACCOUNTS_Keywords": "アカウント、無制限、プレミアム、アドオン", + "WinoAccount_Management_PurchaseRequiresSignIn": "この購入を完了するには Wino アカウントにサインインしてください。", + "WinoAccount_Management_PurchaseStartFailed": "Wino はこの Microsoft Store の購入を完了できませんでした。", + "WinoAccount_Management_StoreSyncFailed": "購入は完了しましたが、Wino はまだアカウントの特典を更新できませんでした。少ししてからもう一度お試しください。", + "WinoAccount_Management_AiPackSubscriptionActive": "サブスクリプションは有効です", + "WinoAccount_Management_AiPackRenews": "更新日: {0:d}", + "WinoAccount_Management_AiPackRequestsUsed": "今月使用済みクレジット", + "WinoAccount_Management_AiPackResets": "リセット {0:d}", + "WinoAccount_Management_AiPackUsageLoadFailed": "AI 使用量の残高を読み込む際に問題が発生しました。", + "WinoAccount_Management_AiPackFeatureTranslate": "翻訳", + "WinoAccount_Management_AiPackFeatureRewrite": "書き換え", + "WinoAccount_Management_AiPackFeatureSummarize": "要約", + "WinoAccount_Management_AddOnLoadFailed": "このアドオンの読み込みに問題が発生しました。", + "WinoAccount_Management_SyncPreferencesTitle": "設定とアカウントの同期", + "WinoAccount_Management_SyncPreferencesDescription": "Wino の設定とメールボックスの詳細をデバイス間でインポートまたはエクスポートします。パスワード、トークン、その他の機密情報は同期されません。", + "WinoAccount_Management_SignOutTitle": "サインアウト", + "WinoAccount_Management_SignOutDescription": "このデバイスでアカウントからサインアウトします", + "WinoAccount_Management_StatusLabel": "ステータス: {0}", + "WinoAccount_Management_NoRemoteSettings": "このアカウントにはまだ同期されたデータが保存されていません。", + "WinoAccount_Management_ExportSucceeded": "選択した Wino データが正常にエクスポートされました。", + "WinoAccount_Management_ExportPreferencesSucceeded": "設定が Wino アカウントにエクスポートされました。", + "WinoAccount_Management_ExportAccountsSucceeded": "{0} 件のアカウントの詳細を Wino アカウントにエクスポートしました。", + "WinoAccount_Management_ImportSucceeded": "Wino アカウントから同期データをインポートしました。", + "WinoAccount_Management_ImportPreferencesSucceeded": "{0} 個の同期済み設定を適用しました。", + "WinoAccount_Management_ImportAccountsSucceeded": "{0} 件のアカウントをインポートしました。", + "WinoAccount_Management_ImportDuplicateAccountsSkipped": "このデバイスですでに存在する {0} 件のアカウントをスキップしました。", + "WinoAccount_Management_ImportPartial": "{0} 件の同期設定を適用しました。{1} 件の設定は復元できませんでした。", + "WinoAccount_Management_ImportReloginReminder": "パスワード、トークン、その他の機密情報はインポートされませんでした。このデバイス上の各アカウントについて、使用前に再度サインインしてください。", + "WinoAccount_Management_SerializeFailed": "Wino は現在の設定をシリアライズできませんでした。", + "WinoAccount_Management_EmptyExport": "エクスポートする設定値がありません。", + "WinoAccount_Management_ImportEmpty": "同期データのペイロードには復元する新しい情報が含まれていません。", + "WinoAccount_Management_ExportDialog_Title": "Wino アカウントへエクスポート", + "WinoAccount_Management_ExportDialog_Description": "Wino アカウントに同期する内容を選択してください。", + "WinoAccount_Management_ExportDialog_IncludePreferences": "設定", + "WinoAccount_Management_ExportDialog_IncludeAccounts": "アカウント", + "WinoAccount_Management_ExportDialog_AccountsDisclaimer": "パスワード、トークン、その他の機密情報は同期されません。", + "WinoAccount_Management_ExportDialog_AccountsRelogin": "他のPCでインポートされたアカウントを使用するには、再度サインインする必要があります。", + "WinoAccount_Management_ExportDialog_InProgress": "選択した Wino データをエクスポートしています...", + "WinoAccount_Management_LoadFailed": "Wino は最新の Wino アカウント情報を読み込めませんでした。", + "WinoAccount_Management_ActionFailed": "Wino アカウントのリクエストを完了できませんでした。", + "WinoAccount_SettingsSection_Title": "Wino アカウント", + "WinoAccount_SettingsSection_Description": "ローカルホストの認証サービスを使用して、Wino アカウントを作成またはサインインします。", + "WinoAccount_RegisterButton_Title": "アカウントを登録", + "WinoAccount_RegisterButton_Description": "メールアドレスとパスワードで Wino アカウントを作成します。", + "WinoAccount_RegisterButton_Action": "登録画面を開く", + "WinoAccount_LoginButton_Title": "サインイン", + "WinoAccount_LoginButton_Description": "メールアドレスとパスワードで既存の Wino アカウントにサインインします。", + "WinoAccount_LoginButton_Action": "ログイン画面を開く", + "WinoAccount_SignOutButton_Title": "サインアウト", + "WinoAccount_SignOutButton_Description": "ローカルに保存された Wino アカウントセッションを削除します。", + "WinoAccount_SignOutButton_Action": "サインアウト", + "WinoAccount_RegisterDialog_Title": "Wino アカウントを作成", + "WinoAccount_RegisterDialog_Description": "Wino の体験を同期させ、アカウントベースのアドオンを利用するために Wino アカウントを作成します。", + "WinoAccount_RegisterDialog_HeroTitle": "Wino アカウントを作成しましょう", + "WinoAccount_RegisterDialog_BenefitsTitle": "なぜ作成するのですか?", + "WinoAccount_RegisterDialog_BenefitSyncTitle": "デバイス間で設定をインポート・エクスポート", + "WinoAccount_RegisterDialog_BenefitSyncDescription": "セットアップを一から再構築することなく、Wino の設定をデバイス間で移動します。", + "WinoAccount_RegisterDialog_BenefitAiTitle": "Wino AI Pack などの独占アドオンにアクセスできます(有料)", + "WinoAccount_RegisterDialog_BenefitAiDescription": "1 つのアカウントでプレミアム機能をアンロックします。", + "WinoAccount_RegisterDialog_DifferenceTitle": "Wino アカウントはメールアカウントとは別のものです", + "WinoAccount_RegisterDialog_DifferenceDescription": "Outlook、Gmail、IMAP、その他のメールアカウントはそのままです。Wino アカウントは Wino 固有の機能とアカウントベースのアドオンのみを管理します。", + "WinoAccount_RegisterDialog_PrimaryButton": "登録", + "WinoAccount_RegisterDialog_PrivacyTitle": "プライバシーと API 処理", + "WinoAccount_RegisterDialog_PrivacyDescription": "Wino AI Pack などのオプションのアドオンは、これらの機能を使用する場合にのみ、選択されたメール HTML コンテンツを Wino API サービスへ送信することがあります。", + "WinoAccount_RegisterDialog_PrivacyLinkText": "プライバシーポリシーを読む", + "WinoAccount_RegisterDialog_PrivacyCheckbox": "プライバシーポリシーに同意します。", + "WinoAccount_LoginDialog_Title": "Wino アカウントにサインイン", + "WinoAccount_LoginDialog_Description": "Wino アカウントにサインインして、Wino の設定を同期し、アカウントベースの機能にアクセスします。", + "WinoAccount_LoginDialog_HeroTitle": "おかえりなさい", + "WinoAccount_LoginDialog_BenefitsTitle": "サインインで得られる機能", + "WinoAccount_LoginDialog_BenefitsDescription": "Wino アカウントを使用して、デバイス間の設定を引き続き同期し、Wino AI Pack のような有料アドオンにアクセスします。", + "WinoAccount_LoginDialog_DifferenceTitle": "これはメールボックスのサインインではありません。", + "WinoAccount_LoginDialog_DifferenceDescription": "ここでサインインしても、Outlook、Gmail、IMAP のアカウントを Wino に追加または置換することはありません。Wino 専用のサービスにサインインするだけです。", + "WinoAccount_LoginDialog_ForgotPasswordLink": "パスワードをお忘れですか?", + "WinoAccount_EmailLabel": "メールアドレス", + "WinoAccount_EmailPlaceholder": "name@example.com", + "WinoAccount_PasswordLabel": "パスワード", + "WinoAccount_ConfirmPasswordLabel": "パスワードの確認", + "WinoAccount_ForgotPasswordDialog_Title": "パスワードをリセット", + "WinoAccount_ForgotPasswordDialog_PrimaryButton": "リセット用のメールを送信", + "WinoAccount_ForgotPasswordDialog_BackToSignIn": "サインイン画面へ戻る", + "WinoAccount_ForgotPasswordDialog_Description": "Wino アカウントのメールアドレスを入力すると、登録済みのアドレスであればパスワードリセットリンクを送信します。", + "WinoAccount_Validation_EmailRequired": "メールアドレスが必要です。", + "WinoAccount_Validation_PasswordRequired": "パスワードが必要です。", + "WinoAccount_Validation_PasswordMismatch": "パスワードが一致しません。", + "WinoAccount_Validation_PrivacyConsentRequired": "Wino アカウントを作成する前に、プライバシーポリシーに同意する必要があります。", + "WinoAccount_Error_InvalidCredentials": "メールアドレスまたはパスワードが正しくありません。", + "WinoAccount_Error_AccountLocked": "このアカウントは一時的にロックされています。", + "WinoAccount_Error_AccountBanned": "このアカウントは利用停止となっています。", + "WinoAccount_Error_AccountSuspended": "このアカウントは一時停止されています。", + "WinoAccount_Error_EmailNotConfirmed": "サインインする前にメールアドレスを確認してください。", + "WinoAccount_Error_EmailConfirmationRequired": "サインインする前にメールアドレスの確認をしてください。", + "WinoAccount_Error_EmailConfirmationResendNotAvailable": "新しい確認メールはまだ利用できません。", + "WinoAccount_Error_EmailConfirmationResendInvalid": "この確認リクエストは有効ではありません。再度サインインをお試しください。", + "WinoAccount_Error_EmailNotRegistered": "このメールアドレスは登録されていません。", + "WinoAccount_Error_RefreshTokenInvalid": "セッションが有効ではなくなりました。もう一度サインインしてください。", + "WinoAccount_Error_EmailAlreadyRegistered": "このメールアドレスはすでに登録されています。", + "WinoAccount_Error_ExternalLoginEmailRequired": "外部サインインを完了するにはメールアドレスが必要です。", + "WinoAccount_Error_ExternalLoginInvalid": "外部サインインのリクエストが無効です。", + "WinoAccount_Error_ExternalAuthStateInvalid": "外部サインインの状態が無効または期限切れです。", + "WinoAccount_Error_ExternalAuthCodeInvalid": "外部サインインのコードが無効または期限切れです。", + "WinoAccount_Error_AiPackRequired": "この操作には有効な Wino AI Pack の購読が必要です。", + "WinoAccount_Error_AiQuotaExceeded": "現在の請求期間の AI Pack 使用制限を超えました。", + "WinoAccount_Error_AiHtmlEmpty": "処理するメールHTMLコンテンツがありません。", + "WinoAccount_Error_AiHtmlTooLarge": "このメールは Wino AI で処理するには大きすぎます。", + "WinoAccount_Error_AiUnsupportedLanguage": "その言語はサポートされていません。有効なカルチャコードとして en-US や tr-TR を試してください。", + "WinoAccount_Error_Forbidden": "この操作を実行する権限がありません。", + "WinoAccount_Error_ValidationFailed": "リクエストが無効です。入力した値を確認してください。", + "WinoAccount_RegisterSuccessMessage": "{0} の Wino アカウント登録が完了しました。", + "WinoAccount_LoginSuccessMessage": "{0} として Wino アカウントにサインインしました。", + "WinoAccount_EmailConfirmationSentDialog_Title": "メールアドレスを確認してください", + "WinoAccount_EmailConfirmationSentDialog_Message": "私たちは {0} 宛てにメール確認を送りました。確認を完了してから、再度サインインしてください。", + "WinoAccount_EmailConfirmationPendingDialog_Title": "メールアドレスの確認が必要です。", + "WinoAccount_EmailConfirmationPendingDialog_Message": "まだ {0} の確認を完了していません。", + "WinoAccount_EmailConfirmationPendingDialog_ResendButton": "確認メールを再送信", + "WinoAccount_EmailConfirmationPendingDialog_Countdown": "{0} 後に確認メールを再送信できます。", + "WinoAccount_EmailConfirmationPendingDialog_ReadyToResend": "今すぐ確認メールを再送信できます。", + "WinoAccount_EmailConfirmationResentDialog_Title": "確認メールを再送信しました", + "WinoAccount_EmailConfirmationResentDialog_Message": "再度確認メールを {0} 宛てに送信しました。確認を完了してから、再度サインインしてください。", + "WinoAccount_ForgotPasswordDialog_SuccessTitle": "パスワードリセットメールを送信しました", + "WinoAccount_ForgotPasswordDialog_SuccessMessage": "{0} 宛にパスワードリセットメールを送りました。そのメッセージを開いて新しいパスワードを選択してください。", + "WinoAccount_ChangePassword_Title": "パスワードを変更", + "WinoAccount_ChangePassword_Description": "この Wino アカウントにパスワードリセットメールを送信します。", + "WinoAccount_ChangePassword_Action": "リセットメールを送信", + "WinoAccount_ChangePassword_ConfirmationMessage": "Wino から {0} にパスワードリセットのメールを送信しますか?", + "WinoAccount_SignOut_SuccessMessage": "Wino アカウント {0} からサインアウトしました。", + "WinoAccount_SignOut_NoAccountMessage": "サインアウトするアカウントが現在アクティブではありません。", + "WinoAccount_Titlebar_SignedOutTitle": "Wino アカウント", + "WinoAccount_Titlebar_SignedOutDescription": "Wino セッションを管理するには、サインインするか Wino アカウントを作成してください。", + "WinoAccount_Titlebar_SignedInStatus": "ステータス: {0}", + "WelcomeWizard_Step2Title": "アカウントの追加", + "WelcomeWizard_Step3Title": "設定を完了", + "ProviderSelection_Title": "メールプロバイダを選択", + "ProviderSelection_Subtitle": "以下のプロバイダを選択して、Wino Mail にメールアカウントを追加します。", + "ProviderSelection_AccountNameHeader": "アカウント名", + "ProviderSelection_AccountNamePlaceholder": "例: Personal、Work", + "ProviderSelection_DisplayNameHeader": "表示名", + "ProviderSelection_DisplayNamePlaceholder": "例: John Doe", + "ProviderSelection_EmailHeader": "メールアドレス", + "ProviderSelection_EmailPlaceholder": "例: johndoe@example.com", + "ProviderSelection_AppPasswordHeader": "アプリ専用パスワード", + "ProviderSelection_AppPasswordHelp": "アプリ専用パスワードはどうやって取得しますか?", + "ProviderSelection_CalendarModeHeader": "カレンダー統合", + "ProviderSelection_CalendarMode_DisabledTitle": "無効", + "ProviderSelection_CalendarMode_DisabledDescription": "カレンダー統合はありません", + "ProviderSelection_CalendarMode_CalDavTitle": "CalDAV 同期", + "ProviderSelection_CalendarMode_CalDavDescription_Apple": "カレンダーのイベントはデバイス間で Apple のサーバーと同期されます。", + "ProviderSelection_CalendarMode_CalDavDescription_Yahoo": "カレンダーのイベントはデバイス間で Yahoo のサーバーと同期されます。", + "ProviderSelection_CalendarMode_LocalTitle": "ローカルカレンダー", + "ProviderSelection_CalendarMode_LocalDescription": "イベントはあなたのコンピューターのみに保存されます。サーバー接続はありません。", + "ProviderSelection_ClearColor": "色をクリア", + "ProviderSelection_ContinueButton": "次へ", + "ProviderSelection_SpecialImap_Subtitle": "接続するには、アカウントの認証情報を入力してください。", + "AccountSetup_Title": "アカウントを設定しています", + "AccountSetup_Step_Authenticating": "{0} で認証中", + "AccountSetup_Step_TestingMailAuth": "メール認証をテストしています", + "AccountSetup_Step_SyncingFolders": "フォルダのメタデータを同期しています", + "AccountSetup_Step_FetchingProfile": "プロフィール情報を取得しています", + "AccountSetup_Step_DiscoveringCalDav": "CalDAV 設定を検出しています", + "AccountSetup_Step_TestingCalendarAuth": "カレンダー認証をテストしています", + "AccountSetup_Step_SavingAccount": "アカウント情報を保存しています", + "AccountSetup_Step_FetchingCalendarMetadata": "カレンダーのメタデータを取得しています", + "AccountSetup_Step_SyncingAliases": "エイリアスを同期しています", + "AccountSetup_Step_Finalizing": "設定を完了しています", + "AccountSetup_FailureMessage": "設定の適用に失敗しました。設定を修正するには戻るか、後でもう一度お試しください。", + "AccountSetup_SuccessMessage": "アカウントが正常に設定されました!", + "AccountSetup_GoBackButton": "戻る", + "AccountSetup_TryAgainButton": "再試行", + "ImapCalDavSettings_AutoDiscoveryFailed": "自動検出に失敗しました。詳細設定タブで手動で設定を入力してください。" } - - diff --git a/Wino.Core.Domain/Translations/lt_LT/resources.json b/Wino.Core.Domain/Translations/lt_LT/resources.json index 8eccf7fa..dea35f3c 100644 --- a/Wino.Core.Domain/Translations/lt_LT/resources.json +++ b/Wino.Core.Domain/Translations/lt_LT/resources.json @@ -8,6 +8,7 @@ "AccountCacheReset_Message": "This account requires full re-sychronization to continue working. Please wait while Wino re-synchronizes your messages...", "AccountContactNameYou": "You", "AccountCreationDialog_Completed": "all done", + "AccountCreationDialog_FetchingCalendarMetadata": "Gaunama kalendoriaus informacija.", "AccountCreationDialog_FetchingEvents": "Fetching calendar events.", "AccountCreationDialog_FetchingProfileInformation": "Fetching profile details.", "AccountCreationDialog_GoogleAuthHelpClipboardText_Row0": "If your browser did not launch automatically to complete authentication:", @@ -17,6 +18,7 @@ "AccountCreationDialog_Initializing": "initializing", "AccountCreationDialog_PreparingFolders": "We are getting folder information at the moment.", "AccountCreationDialog_SigninIn": "Account information is being saved.", + "Purchased": "Įsigyta", "AccountEditDialog_Message": "Account Name", "AccountEditDialog_Title": "Edit Account", "AccountPickerDialog_Title": "Pick an account", @@ -26,6 +28,10 @@ "AccountDetailsPage_Description": "Change the name of the account in Wino and set desired sender name.", "AccountDetailsPage_ColorPicker_Title": "Account color", "AccountDetailsPage_ColorPicker_Description": "Assign a new account color to colorize its symbol in the list.", + "AccountDetailsPage_TabGeneral": "Bendri", + "AccountDetailsPage_TabMail": "Paštas", + "AccountDetailsPage_TabCalendar": "Kalendorius", + "AccountDetailsPage_CalendarListDescription": "Pasirinkite kalendorių, kad sukonfigūruotumėte jo nustatymus.", "AddHyperlink": "Add", "AppCloseBackgroundSynchronizationWarningTitle": "Background Synchronization", "AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Application has not been set to launch on Windows startup.", @@ -47,8 +53,10 @@ "BasicIMAPSetupDialog_Title": "IMAP Account", "Busy": "Busy", "Buttons_AddAccount": "Add Account", + "Buttons_FixAccount": "Pataisyti paskyrą", "Buttons_AddNewAlias": "Add New Alias", "Buttons_Allow": "Allow", + "Buttons_Apply": "Taikyti", "Buttons_ApplyTheme": "Apply Theme", "Buttons_Browse": "Browse", "Buttons_Cancel": "Cancel", @@ -62,6 +70,7 @@ "Buttons_Edit": "Edit", "Buttons_EnableImageRendering": "Enable", "Buttons_Multiselect": "Select Multiple", + "Buttons_Manage": "Tvarkyti", "Buttons_No": "No", "Buttons_Open": "Open", "Buttons_Purchase": "Purchase", @@ -70,15 +79,134 @@ "Buttons_Save": "Save", "Buttons_SaveConfiguration": "Save Configuration", "Buttons_Send": "Send", + "Buttons_SendToServer": "Siųsti į serverį", "Buttons_Share": "Share", "Buttons_SignIn": "Sign In", "Buttons_Sync": "Synchronize", "Buttons_SyncAliases": "Synchronize Aliases", "Buttons_TryAgain": "Try Again", "Buttons_Yes": "Yes", + "Sync_SynchronizingFolder": "Sinchronizuojama {0} {1}%", + "Sync_DownloadedMessages": "Atsisiųsta {0} pranešimų iš {1}", + "SyncAction_Archiving": "Archyvuojama {0} laiškų", + "SyncAction_ClearingFlag": "Šalinama vėliava iš {0} laiškų", + "SyncAction_CreatingDraft": "Kuriamas juodraštis", + "SyncAction_CreatingEvent": "Kuriamas įvykis", + "SyncAction_Deleting": "Ištrinami {0} laiškai", + "SyncAction_EmptyingFolder": "Ištuštinamas aplankas", + "SyncAction_MarkingAsRead": "Žymima kaip skaityta {0} laiškų", + "SyncAction_MarkingAsUnread": "Žymima kaip neperskaitytas {0} laiškų", + "SyncAction_MarkingFolderAsRead": "Žymimas aplankas kaip skaitytas", + "SyncAction_Moving": "Perkeliama {0} laiškų", + "SyncAction_MovingToFocused": "Perkeliama {0} laiškų į Focused", + "SyncAction_RenamingFolder": "Aplanko pavadinimo keitimas", + "SyncAction_SendingMail": "Laiškai siunčiami", + "SyncAction_SettingFlag": "Žymima {0} laiškų vėliava", + "SyncAction_SynchronizingAccount": "Sinchronizuojama {0}", + "SyncAction_SynchronizingAccounts": "Sinchronizuojama {0} paskyra(-ų)", + "SyncAction_SynchronizingCalendarData": "Sinchronizuojami kalendoriaus duomenys", + "SyncAction_SynchronizingCalendarEvents": "Sinchronizuojami kalendoriaus įvykiai", + "SyncAction_SynchronizingCalendarMetadata": "Sinchronizuojami kalendoriaus metaduomenys", + "SyncAction_Unarchiving": "Išarchyvuojama {0} laiškų", "CalendarAllDayEventSummary": "all-day events", "CalendarDisplayOptions_Color": "Color", "CalendarDisplayOptions_Expand": "Expand", + "CalendarEventResponse_Accept": "Priimti", + "CalendarEventResponse_AcceptedResponse": "Priimta", + "CalendarEventResponse_Decline": "Atmesti", + "CalendarEventResponse_DeclinedResponse": "Atmesta", + "CalendarEventResponse_NotResponded": "Neatsakyta", + "CalendarEventResponse_Tentative": "Laikinas", + "CalendarEventResponse_TentativeResponse": "Laikinas atsakymas", + "CalendarEventRsvpPanel_Accept": "Priimti", + "CalendarEventRsvpPanel_AddMessage": "Pridėkite atsakymui pranešimą... (neprivalomas)", + "CalendarEventRsvpPanel_Decline": "Atmesti", + "CalendarEventRsvpPanel_Message": "Pranešimas", + "CalendarEventRsvpPanel_SendReplyMessage": "Siųsti atsakymo žinutę", + "CalendarEventRsvpPanel_Tentative": "Laikinas", + "CalendarEventRsvpPanel_Title": "Atsakymo parinktys", + "CalendarAttendeeStatus_Accepted": "Priimtas", + "CalendarAttendeeStatus_Declined": "Atmestas", + "CalendarAttendeeStatus_NeedsAction": "Reikia veiksmo", + "CalendarAttendeeStatus_Tentative": "Laikinas", + "CalendarEventDetails_Attachments": "Priedai", + "CalendarEventCompose_AddAttachment": "Pridėti priedą", + "CalendarEventCompose_AllDay": "Visą dieną", + "CalendarEventCompose_AttachmentsNotSupportedForCalDav": "Priedai nepalaikomi CalDAV kalendoriuose.", + "CalendarEventCompose_EndDate": "Pabaigos data", + "CalendarEventCompose_EndTime": "Pabaigos laikas", + "CalendarEventCompose_Every": "kas", + "CalendarEventCompose_ForWeekdays": "už", + "CalendarEventCompose_FrequencyDay": "diena", + "CalendarEventCompose_FrequencyDayPlural": "dienos", + "CalendarEventCompose_FrequencyMonth": "mėnuo", + "CalendarEventCompose_FrequencyMonthPlural": "mėnesiai", + "CalendarEventCompose_FrequencyWeek": "savaitė", + "CalendarEventCompose_FrequencyWeekPlural": "savaitės", + "CalendarEventCompose_FrequencyYear": "metai", + "CalendarEventCompose_FrequencyYearPlural": "metai", + "CalendarEventCompose_Location": "Vieta", + "CalendarEventCompose_LocationPlaceholder": "Pridėkite vietą", + "CalendarEventCompose_NewEventButton": "Naujas įvykis", + "CalendarEventCompose_DefaultCalendarHint": "Galite pasirinkti numatytąjį kalendorių naujiems įvykiams Kalendoriaus nustatymuose.", + "CalendarEventCompose_DefaultCalendarSettingsLink": "Atidaryti Kalendoriaus nustatymus", + "CalendarEventCompose_NoCalendarsMessage": "Kol kas nėra kalendorių, skirtų įvykių kūrimui.", + "CalendarEventCompose_NoCalendarsTitle": "Nėra prieinamų kalendorių", + "CalendarEventCompose_NoEndDate": "Nėra pabaigos datos", + "CalendarEventCompose_Notes": "Pastabos", + "CalendarEventCompose_PickCalendarTitle": "Pasirinkite kalendorių", + "CalendarEventCompose_Recurring": "Pasikartojantis", + "CalendarEventCompose_RecurringSummary": "Pasikartojantis įvykis įvyksta kas {0} {1}{2} {3} veiksmingas {4}{5}", + "CalendarEventCompose_RecurringSummarySmart": "Vyksta {0}{1} {2} veiksmingas {3}{4}", + "CalendarEventCompose_RepeatEvery": "Kartoti kas", + "CalendarEventCompose_SelectCalendar": "Pasirinkite kalendorių", + "CalendarEventCompose_SingleOccurrenceSummary": "Įvyksta {0} {1}", + "CalendarEventCompose_StartDate": "Pradžios data", + "CalendarEventCompose_StartTime": "Pradžios laikas", + "CalendarEventCompose_TimeRangeSummary": "nuo {0} iki {1}", + "CalendarEventCompose_Title": "Įvykio pavadinimas", + "CalendarEventCompose_TitlePlaceholder": "Pridėti pavadinimą", + "CalendarEventCompose_Until": "iki", + "CalendarEventCompose_UntilSummary": " iki {0}", + "CalendarEventCompose_ValidationInvalidAllDayRange": "Visos dienos pabaigos data turi būti vėlesnė už pradžios datą.", + "CalendarEventCompose_ValidationInvalidAttendee": "Vienas arba keli dalyviai turi neteisingą el. pašto adresą.", + "CalendarEventCompose_ValidationInvalidRecurrenceEnd": "Pasikartojimo pabaigos data turi būti lygi arba vėlesnė už įvykio pradžios datą.", + "CalendarEventCompose_ValidationInvalidTimeRange": "Pabaigos laikas turi būti vėlesnis už pradžios laiką.", + "CalendarEventCompose_ValidationMissingAttachment": "Vienas ar daugiau priedų nebėra pasiekiami: {0}", + "CalendarEventCompose_ValidationMissingCalendar": "Prieš kuriant įvykį, pasirinkite kalendorių.", + "CalendarEventCompose_ValidationMissingTitle": "Prieš kuriant įvykį įveskite įvykio pavadinimą.", + "CalendarEventCompose_ValidationTitle": "Įvykio patvirtinimas nepavyko", + "CalendarEventCompose_WeekdaySummary": " {0}", + "CalendarEventCompose_Weekday_Friday": "Penktadienis", + "CalendarEventCompose_Weekday_Monday": "Pirmadienis", + "CalendarEventCompose_Weekday_Saturday": "Šeštadienis", + "CalendarEventCompose_Weekday_Sunday": "Sekmadienis", + "CalendarEventCompose_Weekday_Thursday": "Ketvirtadienis", + "CalendarEventCompose_Weekday_Tuesday": "Antradienis", + "CalendarEventCompose_Weekday_Wednesday": "Trečiadienis", + "CalendarEventDetails_Details": "Detalės", + "CalendarEventDetails_EditSeries": "Redaguoti seriją", + "CalendarEventDetails_Editing": "Redaguojama", + "CalendarEventDetails_InviteSomeone": "Pakvieskite ką nors", + "CalendarEventDetails_JoinOnline": "Prisijungti internetu", + "CalendarEventDetails_Organizer": "Organizatorius", + "CalendarEventDetails_People": "Žmonės", + "CalendarEventDetails_ReadOnlyEvent": "Tik skaitomas įvykis", + "CalendarEventDetails_Reminder": "Priminimas", + "CalendarReminder_StartedHoursAgo": "Prasidėjo prieš {0} valandų", + "CalendarReminder_StartedMinutesAgo": "Prasidėjo prieš {0} minučių", + "CalendarReminder_StartedNow": "Prasidėjo dabar", + "CalendarReminder_StartingNow": "Pradedama dabar", + "CalendarReminder_StartsInHours": "Pradės veikti po {0} valandų", + "CalendarReminder_StartsInMinutes": "Pradės veikti po {0} minučių", + "CalendarReminder_SnoozeAction": "Atidėti", + "CalendarReminder_SnoozeMinutesOption": "{0} minučių", + "CalendarEventDetails_ShowAs": "Rodyti kaip", + "CalendarShowAs_Free": "Laisvas", + "CalendarShowAs_Tentative": "Laikinas", + "CalendarShowAs_Busy": "Užimtas", + "CalendarShowAs_OutOfOffice": "Iš biuro", + "CalendarShowAs_WorkingElsewhere": "Dirba kitur", "CalendarItem_DetailsPopup_JoinOnline": "Join online", "CalendarItem_DetailsPopup_ViewEventButton": "View event", "CalendarItem_DetailsPopup_ViewSeriesButton": "View series", @@ -88,6 +216,9 @@ "ClipboardTextCopied_Message": "{0} copied to clipboard.", "ClipboardTextCopied_Title": "Copied", "ClipboardTextCopyFailed_Message": "Failed to copy {0} to clipboard.", + "ContactInfoBar_ErrorTitle": "Nepavyko įkelti kontaktų informacijos", + "ContactInfoBar_SuccessTitle": "Kontaktų informacija įkelta", + "ContactInfoBar_WarningTitle": "Kontaktų informacija gali būti nepilna", "ComingSoon": "Coming soon...", "ComposerAttachmentsDragDropAttach_Message": "Attach", "ComposerAttachmentsDropZone_Message": "Drop your files here", @@ -129,6 +260,10 @@ "DialogMessage_CreateLinkedAccountTitle": "Account Link Name", "DialogMessage_DeleteAccountConfirmationMessage": "Delete {0}?", "DialogMessage_DeleteAccountConfirmationTitle": "All data associated with this account will be deleted from disk permanently.", + "DialogMessage_DeleteEmailTemplateConfirmationMessage": "Ar ištrinti šabloną \"{0}\"?", + "DialogMessage_DeleteEmailTemplateConfirmationTitle": "Ištrinti el. pašto šabloną", + "DialogMessage_DeleteRecurringSeriesMessage": "Tai ištrins visus įvykius serijoje. Ar norite tęsti?", + "DialogMessage_DeleteRecurringSeriesTitle": "Ištrinti pakartojančią seriją", "DialogMessage_DiscardDraftConfirmationMessage": "This draft will be discarded. Do you want to continue?", "DialogMessage_DiscardDraftConfirmationTitle": "Discard Draft", "DialogMessage_EmptySubjectConfirmation": "Missing Subject", @@ -172,11 +307,18 @@ "ElementTheme_Light": "Light mode", "Emoji": "Emoji", "Error_FailedToSetupSystemFolders_Title": "Failed to setup system folders", + "Exception_AccountNeedsAttention_Title": "Paskyra reikalauja dėmesio", + "Exception_AccountNeedsAttention_Message": "'{0}' reikia jūsų dėmesio, kad galėtumėte tęsti darbą.", + "Exception_WebView2RuntimeMissing_Message": "Wino Mail nepavyko rasti Microsoft Edge WebView2 Runtime. Prašome įdiegti arba atnaujinti vykdymo laiką, kad pranešimų turinys būtų teisingai atvaizduojamas.", + "Exception_WebView2RuntimeMissing_Title": "Reikalingas WebView2 vykdymo laikas", "Exception_AuthenticationCanceled": "Authentication canceled", "Exception_CustomThemeExists": "This theme already exists.", "Exception_CustomThemeMissingName": "You must provide a name.", "Exception_CustomThemeMissingWallpaper": "You must provide a custom background image.", "Exception_FailedToSynchronizeAliases": "Failed to synchronize aliases", + "Exception_FailedToSynchronizeCalendarData": "Nepavyko sinchronizuoti kalendoriaus duomenų", + "Exception_FailedToSynchronizeCalendarEvents": "Nepavyko sinchronizuoti kalendoriaus įvykių", + "Exception_FailedToSynchronizeCalendarMetadata": "Nepavyko sinchronizuoti kalendoriaus detalių", "Exception_FailedToSynchronizeFolders": "Failed to synchronize folders", "Exception_FailedToSynchronizeProfileInformation": "Failed to synchronize profile information", "Exception_GoogleAuthCallbackNull": "Callback uri is null on activation.", @@ -229,6 +371,32 @@ "HoverActionOption_MoveJunk": "Move to Junk", "HoverActionOption_ToggleFlag": "Flag / Unflag", "HoverActionOption_ToggleRead": "Read / Unread", + "KeyboardShortcuts_FailedToReset": "Nepavyko atstatyti klavišų trumpinių.", + "KeyboardShortcuts_FailedToUpdate": "Nepavyko atnaujinti klavišų trumpinių", + "KeyboardShortcuts_MailoperationAction": "Veiksmas", + "KeyboardShortcuts_Action": "Veiksmas", + "KeyboardShortcuts_FailedToLoad": "Nepavyko užkrauti klavišų trumpinių.", + "KeyboardShortcuts_EnterKeyForShortcut": "Įveskite klavišą trumpiniui.", + "KeyboardShortcuts_SelectOperationForShortcut": "Prašome pasirinkti veiksmą, kurį atlikti trumpiniui.", + "KeyboardShortcuts_EnterKey": "Įveskite klavišą trumpiniui.", + "KeyboardShortcuts_SelectOperation": "Prašome pasirinkti veiksmą trumpiniui.", + "KeyboardShortcuts_ShortcutInUse": "Šis trumpinys jau naudojamas kitu trumpiniu.", + "KeyboardShortcuts_FailedToSave": "Nepavyko išsaugoti trumpinio.", + "KeyboardShortcuts_FailedToDelete": "Nepavyko ištrinti trumpinio.", + "KeyboardShortcuts_PageDescription": "Nustatykite klavišų trumpinius greitiems pašto veiksmams. Paspauskite klavišus, kai fokusuojate į lauką klavišo įvedimui, kad įrašytumėte trumpinius.", + "KeyboardShortcuts_Add": "Pridėti trumpinį", + "KeyboardShortcuts_EditTitle": "Redaguoti klavišų trumpinį", + "KeyboardShortcuts_ResetToDefaults": "Atstatyti numatytąsias reikšmes", + "KeyboardShortcuts_PressKeysHere": "Spauskite čia klavišus...", + "KeyboardShortcuts_KeyCombination": "Klavišų derinys", + "KeyboardShortcuts_FocusArea": "Sukelkite fokusavimą į viršuje esantį lauką ir paspauskite norimą klavišų derinį", + "KeyboardShortcuts_Modifiers": "Modifikavimo klavišai", + "KeyboardShortcuts_Mode": "Programos režimas", + "KeyboardShortcuts_ModeMail": "Paštas", + "KeyboardShortcuts_ModeCalendar": "Kalendorius", + "KeyboardShortcuts_ActionToggleReadUnread": "Perjungti skaitytą/neskaitytą", + "KeyboardShortcuts_ActionToggleFlag": "Perjungti žymę", + "KeyboardShortcuts_ActionToggleArchive": "Archyvuoti / Iš archyvo", "ImageRenderingDisabled": "Image rendering is disabled for this message.", "ImapAdvancedSetupDialog_AuthenticationMethod": "Authentication method", "ImapAdvancedSetupDialog_ConnectionSecurity": "Connection security", @@ -295,12 +463,58 @@ "IMAPSetupDialog_Username": "Username", "IMAPSetupDialog_UsernamePlaceholder": "johndoe, johndoe@fabrikam.com, domain/johndoe", "IMAPSetupDialog_UseSameConfig": "Use the same username and password for sending email", + "ImapCalDavSettingsPage_TitleCreate": "IMAP ir kalendoriaus nustatymas", + "ImapCalDavSettingsPage_TitleEdit": "Redaguoti IMAP ir Kalendoriaus nustatymus", + "ImapCalDavSettingsPage_Subtitle": "Konfigūruokite IMAP/SMTP ir pasirenkamą kalendoriaus sinchronizaciją šiai paskyrai.", + "ImapCalDavSettingsPage_BasicSectionTitle": "Pagrindinis nustatymas", + "ImapCalDavSettingsPage_BasicSectionDescription": "Įveskite savo tapatybę ir prisijungimo duomenis. Wino gali bandyti automatiškai nustatyti serverio nustatymus.", + "ImapCalDavSettingsPage_BasicTab": "Pagrindinis", + "ImapCalDavSettingsPage_EnableCalendarSupport": "Įgalinti kalendoriaus palaikymą", + "ImapCalDavSettingsPage_AutoDiscoverButton": "Automatinis pašto nustatymų nustatymas", + "ImapCalDavSettingsPage_AutoDiscoverySuccessMessage": "Pašto nustatymai buvo atrasti ir pritaikyti.", + "ImapCalDavSettingsPage_AdvancedSectionTitle": "Išplėstinis nustatymas", + "ImapCalDavSettingsPage_AdvancedSectionDescription": "Rankiniu būdu įveskite serverio nustatymus, jei automatinis nustatymas nėra pasiekiamas arba neteisingas.", + "ImapCalDavSettingsPage_AdvancedTab": "Išplėstinis", + "ImapCalDavSettingsPage_CalendarSectionTitle": "Kalendoriaus nustatymai", + "ImapCalDavSettingsPage_CalendarSectionDescription": "Pasirinkite, kaip kalendoriaus duomenys turėtų veikti šiai IMAP paskyrai.", + "ImapCalDavSettingsPage_CalendarModeHeader": "Kalendoriaus režimas", + "ImapCalDavSettingsPage_ConnectionSecurityHeader": "Ryšio saugumas", + "ImapCalDavSettingsPage_AuthenticationMethodHeader": "Autentifikavimo metodas", + "ImapCalDavSettingsPage_CalendarModeDisabled": "Išjungta", + "ImapCalDavSettingsPage_CalendarModeCalDav": "CalDAV sinchronizacija", + "ImapCalDavSettingsPage_CalendarModeLocalOnly": "Tik vietinis kalendorius", + "ImapCalDavSettingsPage_CalendarModeDisabledDescription": "Kalendorius išjungtas šiai paskyrai.", + "ImapCalDavSettingsPage_CalendarModeCalDavDescription": "Kalendoriaus įrašai sinchronizuojami su jūsų CalDAV serveriu.", + "ImapCalDavSettingsPage_CalendarModeLocalOnlyDescription": "Kalendoriaus įrašai saugomi tik šiame kompiuteryje ir nėra sinchronizuojami su tinklu.", + "ImapCalDavSettingsPage_LocalCalendarLearnMore": "Kaip veikia vietinis kalendorius", + "ImapCalDavSettingsPage_LocalCalendarDialogTitle": "Tik vietinis kalendorius", + "ImapCalDavSettingsPage_LocalCalendarDialogMessage": "Vietinis kalendorius saugo visus įvykius tik jūsų kompiuteryje. Nieko nėra sinchronizuojama su iCloud, Yahoo ar bet kuriuo kitu teikėju.", + "ImapCalDavSettingsPage_CalDavServiceUrl": "CalDAV paslaugos URL", + "ImapCalDavSettingsPage_CalDavUsername": "CalDAV vartotojo vardas", + "ImapCalDavSettingsPage_CalDavPassword": "CalDAV slaptažodis", + "ImapCalDavSettingsPage_CalDavNotRequiredMessage": "CalDAV testavimas reikalingas tik tuo atveju, kai kalendoriaus režimas nustatytas į CalDAV sinchronizaciją.", + "ImapCalDavSettingsPage_CalDavUrlRequired": "CalDAV paslaugos URL yra privalomas.", + "ImapCalDavSettingsPage_CalDavUrlInvalid": "CalDAV paslaugos URL turi būti absoliutus URL.", + "ImapCalDavSettingsPage_CalDavUsernameRequired": "CalDAV naudotojo vardas yra privalomas.", + "ImapCalDavSettingsPage_CalDavPasswordRequired": "CalDAV slaptažodis yra privalomas.", + "ImapCalDavSettingsPage_TestImapButton": "Išbandyti IMAP ryšį", + "ImapCalDavSettingsPage_TestCalDavButton": "Išbandyti CalDAV ryšį", + "ImapCalDavSettingsPage_ImapTestSuccessMessage": "IMAP ryšio patikrinimas sėkmingas.", + "ImapCalDavSettingsPage_CalDavTestSuccessMessage": "CalDAV ryšio patikrinimas sėkmingas.", + "ImapCalDavSettingsPage_SaveSuccessMessage": "Paskyros nustatymai patvirtinti ir išsaugoti.", + "ImapCalDavSettingsPage_ICloudHint": "Naudokite programos specifinį slaptažodį, sugeneruotą jūsų Apple paskyros nustatymuose.", + "ImapCalDavSettingsPage_YahooHint": "Naudokite programos slaptažodį iš Yahoo paskyros saugumo nustatymų.", "Info_AccountCreatedMessage": "{0} is created", "Info_AccountCreatedTitle": "Account Creation", "Info_AccountCreationFailedTitle": "Account Creation Failed", "Info_AccountDeletedMessage": "{0} is successfuly deleted.", "Info_AccountDeletedTitle": "Account Deleted", "Info_AccountIssueFixFailedTitle": "Failed", + "Info_AccountIssueFixImapMessage": "Atidarykite IMAP ir kalendoriaus nustatymų puslapį ir dar kartą įveskite serverio prisijungimo duomenis.", + "Info_AccountAttentionRequiredMessage": "Šiai paskyrai reikia dėmesio.", + "Info_AccountAttentionRequiredClickableMessage": "Spustelėkite, kad ištaisytumėte šią paskyrą ir vėl ją sinchronizuotumėte.", + "Info_AccountAttentionRequiredAction": "Ištaisyti", + "Info_AccountAttentionRequiredActionHint": "Spustelėkite Ištaisyti, kad išspręstumėte šią paskyros problemą.", "Info_AccountIssueFixSuccessMessage": "Fixed all account issues.", "Info_AccountIssueFixSuccessTitle": "Success", "Info_AttachmentOpenFailedMessage": "Can't open this attachment.", @@ -370,6 +584,7 @@ "InfoBarMessage_SynchronizationDisabledFolder": "This folder is disabled for synchronization.", "InfoBarTitle_SynchronizationDisabledFolder": "Disabled Folder", "Justify": "Justify", + "MenuUpdateAvailable": "Atnaujinimas prieinamas.", "Left": "Left", "Link": "Link", "LinkedAccountsCreatePolicyMessage": "you must have at least 2 accounts to create link\nlink will be removed on save", @@ -403,6 +618,7 @@ "MailOperation_Unarchive": "Unarchive", "MailOperation_ViewMessageSource": "View message source", "MailOperation_Zoom": "Zoom", + "MailsDragging": "Vilkimas {0} elemento(-ų)", "MailsSelected": "{0} item(s) selected", "MarkFlagUnflag": "Mark as flagged/unflagged", "MarkReadUnread": "Mark as read/unread", @@ -434,6 +650,8 @@ "Notifications_MultipleNotificationsTitle": "New Mail", "Notifications_WinoUpdatedMessage": "Checkout new version {0}", "Notifications_WinoUpdatedTitle": "Wino Mail has been updated.", + "Notifications_StoreUpdateAvailableTitle": "Parduotuvės atnaujinimas prieinamas.", + "Notifications_StoreUpdateAvailableMessage": "Naujesnė Wino Mail versija paruošta įdiegti per Microsoft Store.", "OnlineSearchFailed_Message": "Failed to perform search\n{0}\n\nListing offline mails.", "OnlineSearchTry_Line1": "Can't find what you are looking for?", "OnlineSearchTry_Line2": "Try online search.", @@ -446,7 +664,6 @@ "PaneLengthOption_Small": "Small", "Photos": "Photos", "PreparingFoldersMessage": "Preparing folders", - "ProtocolLogAvailable_Message": "Protocol logs are available for diagnostics.", "ProviderDetail_Gmail_Description": "Google Account", "ProviderDetail_iCloud_Description": "Apple iCloud Account", "ProviderDetail_iCloud_Title": "iCloud", @@ -465,9 +682,14 @@ "SearchBarPlaceholder": "Search", "SearchingIn": "Searching in", "SearchPivotName": "Results", + "Settings_KeyboardShortcuts_Title": "Klaviatūros trumpiniai", + "Settings_KeyboardShortcuts_Description": "Tvarkykite klaviatūros trumpinius greitiems veiksmams el. laiškuose.", "SettingConfigureSpecialFolders_Button": "Configure", "SettingsEditAccountDetails_IMAPConfiguration_Title": "IMAP/SMTP Configuration", "SettingsEditAccountDetails_IMAPConfiguration_Description": "Change your incoming/outgoing server settings.", + "SettingsEditAccountDetails_ImapCalDavSettings_Title": "IMAP ir kalendoriaus nustatymai", + "SettingsEditAccountDetails_ImapCalDavSettings_Description": "Atidarykite šiai paskyrai skirtą IMAP, SMTP ir CalDAV nustatymų puslapį.", + "SettingsEditAccountDetails_ImapCalDavSettings_Action": "Atidaryti nustatymus", "SettingsAbout_Description": "Learn more about Wino.", "SettingsAbout_Title": "About", "SettingsAboutGithub_Description": "Go to issue tracker GitHub repository.", @@ -490,6 +712,10 @@ "SettingsAppPreferences_SearchMode_Local": "Local", "SettingsAppPreferences_SearchMode_Online": "Online", "SettingsAppPreferences_SearchMode_Title": "Default search mode", + "SettingsAppPreferences_ApplicationMode_Title": "Numatytasis programos režimas", + "SettingsAppPreferences_ApplicationMode_Description": "Pasirinkite, kuriame režime Wino atsidarys, kai aiškiai nebus nustatytas aktyvacijos tipas.", + "SettingsAppPreferences_ApplicationMode_Mail": "Paštas", + "SettingsAppPreferences_ApplicationMode_Calendar": "Kalendorius", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description": "Wino Mail will keep running in the background. You will be notified as new mails arrive.", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title": "Run in the background", "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description": "Wino Mail will keep running on the system tray. Available to launch by clicking on an icon. You will be notified as new mails arrive.", @@ -506,12 +732,30 @@ "SettingsAppPreferences_StartupBehavior_FatalError": "Fatal error occurred while changing the startup mode for Wino Mail.", "SettingsAppPreferences_StartupBehavior_Title": "Start minimized on Windows startup", "SettingsAppPreferences_Title": "App Preferences", + "SettingsAppPreferences_HideWinoAccountButton_Title": "Slėpti pavadinimo juostos profilio mygtuką", + "SettingsAppPreferences_HideWinoAccountButton_Description": "Slėpti pavadinimo juostos profilio mygtuką, kuris atveria Wino paskyros išskleidžiamąjį langą.", + "SettingsAppPreferences_StoreUpdateNotifications_Title": "Parduotuvės atnaujinimų pranešimai", + "SettingsAppPreferences_StoreUpdateNotifications_Description": "Rodyti pranešimus ir veiksmus apačioje, kai Microsoft Store bus prieinamas atnaujinimas.", + "SettingsAppPreferences_AiActions_Title": "Dirbtinio intelekto veiksmai", + "SettingsAppPreferences_AiActions_Description": "Pasirinkite numatytas AI kalbas ir kur turėtų būti saugomos santraukos.", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Title": "Numatomoji vertimo kalba", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Description": "Pasirinkite numatytąją tikslinę kalbą, kuri bus naudojama AI vertimo veiksmuose.", + "SettingsAppPreferences_AiSummarizeLanguage_Title": "Santraukos kalba", + "SettingsAppPreferences_AiSummarizeLanguage_Description": "Pasirinkite pageidaujamą santraukos kalbą ateities AI santraukų išvestims.", + "SettingsAppPreferences_AiSummarySavePath_Title": "Numatytasis santraukų saugojimo kelias", + "SettingsAppPreferences_AiSummarySavePath_Description": "Pasirinkite aplanką, kurį Wino naudos kaip numatytąjį saugojant AI santraukas.", + "SettingsAppPreferences_AiSummarySavePath_Placeholder": "Naudokite sistemos numatytąją saugojimo vietą.", + "SettingsAppPreferences_AiSummarySavePath_InvalidHint": "Šis aplankas neegzistuoja. Santraukoms bus naudojama numatytoji saugojimo vieta.", "SettingsAutoSelectNextItem_Description": "Select the next item after you delete or move a mail.", "SettingsAutoSelectNextItem_Title": "Auto select next item", "SettingsAvailableThemes_Description": "Select a theme from Wino's own collection for your taste or apply your own themes.", "SettingsAvailableThemes_Title": "Available Themes", "SettingsCalendarSettings_Description": "Change first day of week, hour cell height and more...", "SettingsCalendarSettings_Title": "Calendar Settings", + "CalendarSettings_DefaultSnoozeDuration_Header": "Numatytoji užmiego trukmė", + "CalendarSettings_DefaultSnoozeDuration_Description": "Nustatykite numatytąją užmiego trukmę priminimams apie įvykius.", + "CalendarSettings_TimedDayHeaderFormat_Header": "Laiko rodymo dienos antraštės formatas", + "CalendarSettings_TimedDayHeaderFormat_Description": "Pasirinkite, kaip viršutinės dienos žymės rodomos dienos, savaitės ir darbo savaitės peržiūrose. Naudokite datos formato ženklus, tokius kaip ddd, dd, MMM arba dddd.", "SettingsComposer_Title": "Composer", "SettingsComposerFont_Title": "Default Composer Font", "SettingsComposerFontFamily_Description": "Change the default font family and font size for composing mails.", @@ -531,6 +775,9 @@ "SettingsDiscord_Title": "Discord Channel", "SettingsEditLinkedInbox_Description": "Add / remove accounts, rename or break the link between accounts.", "SettingsEditLinkedInbox_Title": "Edit Linked Inbox", + "SettingsWindowBackdrop_Title": "Langų fono efektas", + "SettingsWindowBackdrop_Description": "Pasirinkite Wino langų fono efektą.", + "SettingsWindowBackdrop_Disabled": "Langų fono pasirinkimas išjungtas, kai programa tema yra pasirinkta kita nei Numatytoji.", "SettingsElementTheme_Description": "Select a Windows theme for Wino", "SettingsElementTheme_Title": "Element Theme", "SettingsElementThemeSelectionDisabled": "Element theme selection is disabled when application theme is selected other than Default.", @@ -581,6 +828,8 @@ "SettingsManageAliases_Title": "Aliases", "SettingsEditAccountDetails_Title": "Edit Account Details", "SettingsEditAccountDetails_Description": "Change account name, sender name and assign a new color if you like.", + "EditAccountDetailsPage_SaveSuccess_Title": "Pakeitimai išsaugoti", + "EditAccountDetailsPage_SaveSuccess_Message": "Jūsų paskyros duomenys sėkmingai atnaujinti.", "SettingsManageLink_Description": "Move items to add new link or remove existing link.", "SettingsManageLink_Title": "Manage Link", "SettingsMarkAsRead_Description": "Change what should happen to the selected item.", @@ -596,7 +845,41 @@ "SettingsNotifications_Title": "Notifications", "SettingsNotificationsAndTaskbar_Description": "Change whether notifications should be displayed and taskbar badge for this account.", "SettingsNotificationsAndTaskbar_Title": "Notifications & Taskbar", + "SettingsHome_Title": "Pagrindinis", + "SettingsHome_SearchTitle": "Raskite nustatymą", + "SettingsHome_SearchDescription": "Ieškokite pagal funkciją, temą ar raktažodį ir tiesiog pereikite į tinkamą nustatymų puslapį.", + "SettingsHome_SearchPlaceholder": "Ieškoti nustatymų", + "SettingsHome_SearchExamples": "Pavyzdžiui: tema, saugykla, kalba, parašas", + "SettingsHome_QuickLinks_Title": "Greitos nuorodos", + "SettingsHome_QuickLinks_Description": "Pereikite prie nustatymų, kuriuos žmonės dažniausiai pasiekia.", + "SettingsHome_StorageCard_Description": "Sužinokite, kiek vietinio MIME turinio Wino saugo šiame įrenginyje, ir, prireikus, išvalykite jį.", + "SettingsHome_StorageEmptySummary": "Šiuo metu nerandama jokio talpykloje saugomo MIME turinio.", + "SettingsHome_StorageLoading": "Tikrinama vietinio MIME naudojimo būsena...", + "SettingsHome_Tips_Title": "Patarimai ir gudrybės", + "SettingsHome_Tips_Description": "Keletas nedidelių pakeitimų gali padaryti, kad Wino jaustųsi daug asmeniškesnė.", + "SettingsHome_Tip_Theme": "Norite tamsaus režimo ar akcentų pakeitimų? Atidarykite Personalizavimą.", + "SettingsHome_Tip_Background": "Naudokite programos nuostatas, kad valdytumėte paleidimo elgseną ir fono sinchronizaciją.", + "SettingsHome_Tip_Shortcuts": "Klaviatūros trumpiniai padeda greičiau judėti tarp el. laiškų.", + "SettingsHome_Resources_Title": "Naudingos nuorodos", + "SettingsHome_Resources_Description": "Atidarykite projekto išteklius, palaikymo informaciją ir leidimo kanalus.", "SettingsOptions_Title": "Settings", + "SettingsOptions_GeneralSection": "Bendra", + "SettingsOptions_MailSection": "Paštas", + "SettingsOptions_CalendarSection": "Kalendorius", + "SettingsOptions_MoreComingSoon": "Daugiau parinkčių bus netrukus.", + "SettingsOptions_HeroDescription": "Pritaikykite Wino Mail patirtį.", + "SettingsOptions_AccountsSummary": "{0} paskyra(-ų) sukonfigūruota", + "SettingsSearch_ManageAccounts_Keywords": "paskyra;paskyrų;pašto dėžutė;pašto dėžutės;slapyvardis;slapyvardžiai;profilis;adresas;adresai", + "SettingsSearch_AppPreferences_Keywords": "pradinis paleidimas; fonas; paleidimas; sinchronizavimas; pranešimas; pranešimai; paieška; dėklas; numatytieji nustatymai", + "SettingsSearch_LanguageTime_Keywords": "kalba;laikas;laikrodis;lokalė;regionas;formatas;24 val.;24val.", + "SettingsSearch_Personalization_Keywords": "tema;tamsus;šviesus;išvaizda;akcentas;spalva;spalva;režimas;išdėstymas;tankis", + "SettingsSearch_About_Keywords": "apie;versija;svetainė;privatumas;GitHub;paaukokite;parduotuvė;pagalba", + "SettingsSearch_KeyboardShortcuts_Keywords": "trumpinys;trumpiniai;mygtukas;mygtukai;klavišas;klavišai", + "SettingsSearch_MessageList_Keywords": "žinutė;žinutės;sąrašas;gijos;gijos;avataras;peržiūra;siuntėjas", + "SettingsSearch_ReadComposePane_Keywords": "skaitytojas;kurti;redaktorius;šriftas;šriftai;išorinis turinys;atvaizdavimas;skaitymas", + "SettingsSearch_SignatureAndEncryption_Keywords": "parašas;parašai;šifravimas;sertifikatas;sertifikatai;S/MIME;S/MIME;saugumas", + "SettingsSearch_Storage_Keywords": "saugykla;talpykla;talpyklavimas;MIME;disko;vietos;valymas;valyti;vietiniai duomenys", + "SettingsSearch_CalendarSettings_Keywords": "kalendorius;savaitė;valandos;tvarkaraštis;įvykis;įvykiai", "SettingsPaneLengthReset_Description": "Reset the size of the mail list to original if you have issues with it.", "SettingsPaneLengthReset_Title": "Reset Mail List Size", "SettingsPaypal_Description": "Show much more love ❤️ All donations are appreciated.", @@ -610,6 +893,8 @@ "SettingsPrefer24HourClock_Title": "Display Clock Format in 24 Hours", "SettingsPrivacyPolicy_Description": "Review privacy policy.", "SettingsPrivacyPolicy_Title": "Privacy Policy", + "SettingsWebsite_Description": "Atidarykite Wino Mail svetainę.", + "SettingsWebsite_Title": "Svetainė", "SettingsReadComposePane_Description": "Fonts, external content.", "SettingsReadComposePane_Title": "Reader & Composer", "SettingsReader_Title": "Reader", @@ -625,6 +910,19 @@ "SettingsShowPreviewText_Title": "Show Preview Text", "SettingsShowSenderPictures_Description": "Hide/show the thumbnail sender pictures.", "SettingsShowSenderPictures_Title": "Show Sender Avatars", + "SettingsEmailTemplates_Title": "El. pašto šablonai", + "SettingsEmailTemplates_Description": "Tvarkykite el. pašto šablonus", + "SettingsEmailTemplates_CreatePageTitle": "Naujas šablonas", + "SettingsEmailTemplates_EditPageTitle": "Redaguoti šabloną", + "SettingsEmailTemplates_NewTemplateTitle": "Naujas šablonas", + "SettingsEmailTemplates_NewTemplateDescription": "Sukurkite naują el. pašto šabloną", + "SettingsEmailTemplates_NameTitle": "Pavadinimas", + "SettingsEmailTemplates_NamePlaceholder": "Šablono pavadinimas", + "SettingsEmailTemplates_DescriptionTitle": "Aprašymas", + "SettingsEmailTemplates_DescriptionPlaceholder": "Nebūtinas aprašas", + "SettingsEmailTemplates_ContentTitle": "Šablono turinys", + "SettingsEmailTemplates_ContentDescription": "Redaguokite HTML turinį šio šablono.", + "SettingsEmailTemplates_NameRequired": "Šablono pavadinimas yra privalomas.", "SettingsEnableGravatarAvatars_Title": "Gravatar", "SettingsEnableGravatarAvatars_Description": "Use gravatar (if available) as sender picture", "SettingsEnableFavicons_Title": "Domain icons (Favicons)", @@ -645,6 +943,33 @@ "SettingsStartupItem_Title": "Startup Item", "SettingsStore_Description": "Show some love ❤️", "SettingsStore_Title": "Rate in Store", + "SettingsStorage_Title": "Saugykla", + "SettingsStorage_Description": "Skenuokite ir valdykite MIME talpyklą, saugomą jūsų vietiniame duomenų aplanke.", + "SettingsStorage_ScanFolder": "Skenuokite vietinį duomenų aplanką", + "SettingsStorage_NoLocalMimeDataFound": "Nerasta vietinių MIME duomenų.", + "SettingsStorage_NoAccountsFound": "Nerasta paskyrų.", + "SettingsStorage_TotalUsage": "Viso vietinio MIME naudojimo: {0}", + "SettingsStorage_AccountUsageDescription": "{0} naudojama vietinėje MIME talpyloje", + "SettingsStorage_DeleteAll_Title": "Ištrinti visą MIME turinį", + "SettingsStorage_DeleteAll_Description": "Ištrinti šios paskyros visą MIME talpyklos aplanką.", + "SettingsStorage_DeleteAll_Button": "Ištrinti viską", + "SettingsStorage_DeleteAll_Confirm_Title": "Ištrinti visą MIME turinį", + "SettingsStorage_DeleteAll_Confirm_Message": "Ar tikrai norite ištrinti visus vietinius MIME duomenis {0}?", + "SettingsStorage_DeleteAll_Success": "Visas MIME turinys buvo ištrintas.", + "SettingsStorage_DeleteOld_Title": "Ištrinti senus MIME duomenis", + "SettingsStorage_DeleteOld_Description": "Ištrinti MIME failus pagal laiško kūrimo datą vietinėje duomenų bazėje.", + "SettingsStorage_DeleteOld_1Month": "> 1 mėnuo", + "SettingsStorage_DeleteOld_3Months": "> 3 mėnesiai", + "SettingsStorage_DeleteOld_6Months": "> 6 mėnesiai", + "SettingsStorage_DeleteOld_1Year": "> 1 metai", + "SettingsStorage_DeleteOld_Confirm_Title": "Ištrinti senus MIME duomenis", + "SettingsStorage_DeleteOld_Confirm_Message": "Ar tikrai norite ištrinti vietinius MIME duomenis, senesnius nei {0} paskyrai {1}?", + "SettingsStorage_DeleteOld_Success": "Ištrinta {0} MIME aplankas(-ų), senesnių nei {1}.", + "SettingsStorage_1Month": "1 mėnuo", + "SettingsStorage_3Months": "3 mėnesiai", + "SettingsStorage_6Months": "6 mėnesiai", + "SettingsStorage_1Year": "1 metai", + "SettingsStorage_Months": "{0} mėnesiai", "SettingsTaskbarBadge_Description": "Include unread mail count in taskbar icon.", "SettingsTaskbarBadge_Title": "Taskbar Badge", "SettingsThreads_Description": "Organize messages into conversation threads.", @@ -683,6 +1008,9 @@ "SystemFolderConfigDialogValidation_InboxSelected": "You can't assign Inbox folder to any other system folder.", "SystemFolderConfigSetupSuccess_Message": "System folders are successfully configured.", "SystemFolderConfigSetupSuccess_Title": "System Folders Setup", + "SystemTrayMenu_ShowWino": "Atidaryti Wino Mail", + "SystemTrayMenu_ShowWinoCalendar": "Atidaryti Wino Calendar", + "SystemTrayMenu_ExitWino": "Išjungti", "TestingImapConnectionMessage": "Testing server connection...", "TitleBarServerDisconnectedButton_Description": "Wino is disconnected from the network. Click reconnect to restore connection.", "TitleBarServerDisconnectedButton_Title": "no connection", @@ -699,8 +1027,422 @@ "WinoUpgradeMessage": "Upgrade to Unlimited Accounts", "WinoUpgradeRemainingAccountsMessage": "{0} out of {1} free accounts used.", "Yesterday": "Yesterday", + "Smime_ImportCertificates_Success": "Sertifikatai sėkmingai importuoti.", + "Smime_ImportCertificates_Error": "Klaida importuojant sertifikatus: {0}", + "Smime_RemoveCertificates_Confirm": "Ar tikrai norite pašalinti sertifikatus {0}?", + "Smime_RemoveCertificates_Success": "Sertifikatai pašalinti.", + "Smime_ExportCertificates_Success": "Sertifikatai eksportuoti.", + "Smime_ExportCertificates_Error": "Klaida eksportuojant sertifikatus.", + "Smime_CertificateDetails": "Tema: {0}\nIšdavėjas: {1}\nGalioja nuo: {2}\nGalioja iki: {3}\nPiršto atspaudas: {4}", + "Smime_CertificatePassword_Title": "Sertifikato slaptažodis reikalingas", + "Smime_CertificatePassword_Placeholder": "Sertifikato slaptažodis {0} (nebūtina)", + "Smime_Confirm_Title": "Patvirtinti", + "Buttons_OK": "Gerai", + "Buttons_Refresh": "Atnaujinti", + "SettingsSignatureAndEncryption_Title": "Parašas ir šifravimas", + "SettingsSignatureAndEncryption_Description": "Tvarkykite S/MIME sertifikatus el. paštu parašams ir šifravimui.", + "SettingsSignatureAndEncryption_MyCertificatesHeader": "Mano sertifikatai", + "SettingsSignatureAndEncryption_MyCertificatesDescription": "Asmeniniai sertifikatai parašams ir šifravimui", + "SettingsSignatureAndEncryption_RecipientCertificatesHeader": "Gavėjo sertifikatai", + "SettingsSignatureAndEncryption_RecipientCertificatesDescription": "Gavėjo sertifikatai dešifravimui", + "SettingsSignatureAndEncryption_NameColumn": "Pavadinimas", + "SettingsSignatureAndEncryption_ExpiresColumn": "Galioja iki", + "SettingsSignatureAndEncryption_ThumbprintColumn": "Piršto atspaudas", + "Buttons_Remove": "Ištrinti", + "Buttons_Export": "Eksportuoti", + "Buttons_Import": "Importuoti", + "SettingsSignatureAndEncryption_SigningCertificate": "S/MIME parašymo sertifikatas", + "SettingsSignatureAndEncryption_EncryptionCertificate": "S/MIME šifravimo sertifikatas", + "SettingsSignatureAndEncryption_SigningCertificatePlaceholder": "Nėra", + "SmimeSignaturesInMessage": "Šio pranešimo parašai:", + "SmimeSignatureEntry": "• {0} {1} ({2}, galioja iki {3} - {4})", + "SmimeSigningCertificateInfoTitle": "S/MIME parašymo sertifikato informacija", + "SmimeCertificateInfoTitle": "S/MIME sertifikato informacija", + "SmimeNoCertificateFileFound": "Nerastas sertifikato failas", + "SmimeSaveCertificate": "Išsaugoti sertifikatą...", + "SmimeCertificate": "S/MIME sertifikatas", + "SmimeCertificateSavedTo": "Sertifikatas išsaugotas į {0}", + "SmimeSignedTooltip": "Šis pranešimas yra pasirašytas S/MIME sertifikatu. Spustelėkite norėdami gauti daugiau informacijos", + "SmimeEncryptedTooltip": "Šis pranešimas yra užšifruotas naudojant S/MIME sertifikatą.", + "SmimeCertificateFileInfo": "Failas: {0}", + "Composer_LightTheme": "Šviesi tema", + "Composer_DarkTheme": "Tamsi tema", + "Composer_Outdent": "Išlyginti įtrauką", + "Composer_Indent": "Padidinti įtrauką", + "Composer_BulletList": "Žymėjimo sąrašas", + "Composer_OrderedList": "Numeruotas sąrašas", + "Composer_Stroke": "Brūkšnys", + "Composer_Bold": "Paryškintas", + "Composer_Italic": "Kursyvas", + "Composer_Underline": "Pabraukta", + "Composer_CcBcc": "Cc ir Bcc", + "Composer_EnableSmimeSignature": "Įjungti/Išjungti S/MIME parašą", + "Composer_EnableSmimeEncryption": "Įjungti/Išjungti S/MIME šifravimą", + "Composer_LocalDraftSyncInfo": "Šis juodraštis yra tik vietinis. Wino nepavyko išsiųsti jo į jūsų pašto serverį. Spustelėkite, kad išsiųstumėte į serverį iš naujo.", + "Composer_CertificateExpires": "Galioja iki: ", + "Composer_SmimeSignature": "S/MIME parašas", + "Composer_SmimeEncryption": "S/MIME šifravimas", + "Composer_EmailTemplatesPlaceholder": "El. pašto šablonai", + "Composer_AiSummarize": "Apibendrinti su AI", + "Composer_AiSummarizeDescription": "Ištraukite pagrindines mintis, veiksmų punktus ir sprendimus iš šio el. laiško.", + "Composer_AiTranslate": "Išversti su AI", + "Composer_AiActions": "AI veiksmai", + "Composer_AiRewrite": "Perrašyti su AI", + "AiActions_CheckingStatus": "Tikrinama prieiga prie AI...", + "AiActions_SignedOutTitle": "Atrakinkite Wino AI paketą", + "AiActions_SignedOutDescription": "Išverskite, perrašykite ir apibendrinkite el. laiškus naudodami AI prisijungę prie savo Wino paskyros ir aktyvavę AI Pack papildinį.", + "AiActions_NoPackTitle": "Reikia AI paketo", + "AiActions_NoPackDescription": "Jūs esate prisijungęs, tačiau AI paketas dar neveikia. Įsigykite jį, kad naudotumėte Wino AI vertimo, perrašymo ir santraukos įrankius.", + "AiActions_UsageSummary": "{0} iš {1} kreditų šį mėnesį panaudota.", + "Composer_AiRewritePolite": "Padarykite mandagų toną", + "Composer_AiRewritePoliteDescription": "Švelnina formuluotę išlaikant tą patį tikslą.", + "Composer_AiRewriteAngry": "Padarykite piktą toną", + "Composer_AiRewriteAngryDescription": "Naudojamas aštresnis ir konfrontacinis tonas.", + "Composer_AiRewriteHappy": "Padarykite jį laimingą", + "Composer_AiRewriteHappyDescription": "Prideda optimistiškesnį ir entuziastingesnį toną.", + "Composer_AiRewriteFormal": "Paverskite toną oficialiu.", + "Composer_AiRewriteFormalDescription": "Paverskite žinutę profesionaliau ir struktūriškai.", + "Composer_AiRewriteFriendly": "Paverskite toną draugiškesniu.", + "Composer_AiRewriteFriendlyDescription": "Prideda šiltą, draugiškesnį toną.", + "Composer_AiRewriteShorter": "Trumpinkite.", + "Composer_AiRewriteShorterDescription": "Supaprastina tekstą ir pašalina nereikalingas detales.", + "Composer_AiRewriteClearer": "Padarykite aiškesnį.", + "Composer_AiRewriteClearerDescription": "Pagerina skaitomumą ir padaro žinutę lengviau suprantamą.", + "Composer_AiRewriteCustom": "Individualus", + "Composer_AiRewriteCustomDescription": "Apibūdinkite savo perrašymo tikslą.", + "Composer_AiRewriteCustomPlaceholder": "Apibūdinkite, kaip norite, kad žinutė būtų perrašyta", + "Composer_AiRewriteMode": "Toną perrašyti", + "Composer_AiRewriteApply": "Pritaikyti perrašymą.", + "Composer_AiTranslateDialogTitle": "Išversti su AI", + "Composer_AiTranslateDialogDescription": "Įveskite tikslinę kalbą arba kultūros kodą, pvz., en-US, tr-TR, de-DE ar fr-FR.", + "Composer_AiTranslateApply": "Išversti", + "Composer_AiTranslateLanguage": "Tikslinė kalba", + "Composer_AiTranslateCustomPlaceholder": "Įveskite kultūros kodą", + "Composer_AiTranslateLanguageEnglish": "Anglų (en-US)", + "Composer_AiTranslateLanguageTurkish": "Turkų (tr-TR)", + "Composer_AiTranslateLanguageGerman": "Vokiečių (de-DE)", + "Composer_AiTranslateLanguageFrench": "Prancūzų (fr-FR)", + "Composer_AiTranslateLanguageSpanish": "Ispanų (es-ES)", + "Composer_AiTranslateLanguageItalian": "Italų (it-IT)", + "Composer_AiTranslateLanguagePortugueseBrazil": "Brazilijos portugalų (pt-BR)", + "Composer_AiTranslateLanguageDutch": "Olandų (nl-NL)", + "Composer_AiTranslateLanguagePolish": "Lenkų (pl-PL)", + "Composer_AiTranslateLanguageRussian": "Rusų (ru-RU)", + "Composer_AiTranslateLanguageJapanese": "Japonų (ja-JP)", + "Composer_AiTranslateLanguageKorean": "Korėjiečių (ko-KR)", + "Composer_AiTranslateLanguageChineseSimplified": "Kinų (supaprastinta, zh-CN)", + "Composer_AiTranslateLanguageArabic": "Arabų (ar-SA)", + "Composer_AiTranslateLanguageHindi": "Hindi (hi-IN)", + "Composer_AiTranslateLanguageOther": "Kita...", + "Composer_AiBusyTitle": "Dirbtinis intelektas jau veikia.", + "Composer_AiBusyMessage": "Palaukite, kol baigsis dabartinis AI veiksmas.", + "Composer_AiSignInRequired": "Prisijunkite prie savo Wino paskyros, kad naudotumėte AI funkcijas.", + "Composer_AiMissingHtml": "Dar nėra žinutės turinio, kurį būtų galima siųsti Wino AI.", + "Composer_AiQuotaUnavailable": "AI rezultatas buvo pritaikytas.", + "Composer_AiAppliedMessage": "AI rezultatas buvo pritaikytas kompozitoriui. Jei norite grąžinti pakeitimus, naudokite Atšaukti.", + "Composer_AiSummarizeSuccessTitle": "AI santrauka pritaikyta.", + "Composer_AiTranslateSuccessTitle": "AI vertimas pritaikytas.", + "Composer_AiRewriteSuccessTitle": "AI perrašymas pritaikytas.", + "Composer_AiErrorTitle": "AI veiksmas nepavyko.", + "Reader_AiAppliedMessage": "AI rezultatas dabar rodomas šiame pranešime. Iš naujo atidarykite pranešimą, kad pamatytumėte originalų turinį.", "SettingsAppPreferences_EmailSyncInterval_Title": "Email sync interval", - "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail." + "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail.", + "ContactsPage_Title": "Kontaktai", + "ContactsPage_AddContact": "Pridėti kontaktą", + "ContactsPage_EditContact": "Redaguoti kontaktą", + "ContactsPage_DeleteContact": "Ištrinti kontaktą", + "ContactsPage_SearchPlaceholder": "Ieškoti kontaktų...", + "ContactsPage_NoContacts": "Kontaktų nėra.", + "ContactsPage_ContactsCount": "{0} kontaktai", + "ContactsPage_SelectedContactsCount": "{0} pasirinktas", + "ContactsPage_DeleteSelectedContacts": "Ištrinti pasirinktus kontaktus", + "ContactEditDialog_Title": "Redaguoti kontaktą", + "ContactEditDialog_PhotoSection": "Nuotrauka", + "ContactEditDialog_ChoosePhoto": "Pasirinkti nuotrauką", + "ContactEditDialog_RemovePhoto": "Ištrinti nuotrauką", + "ContactEditDialog_NameHeader": "Vardas", + "ContactEditDialog_NamePlaceholder": "Kontakto vardas", + "ContactEditDialog_EmailHeader": "El. pašto adresas", + "ContactEditDialog_EmailPlaceholder": "contact@example.com", + "ContactEditDialog_InfoSection": "Kontaktinė informacija", + "ContactEditDialog_RootContactInfo": "Tai yra pagrindinis kontaktas, susijęs su jūsų paskyromis, kurio ištrinti negalima.", + "ContactEditDialog_OverriddenContactInfo": "Šis kontaktas buvo rankiniu būdu pakeistas ir sinchronizacijos metu nebus atnaujinamas.", + "ContactsPage_Subtitle": "Tvarkykite savo el. pašto kontaktus ir jų informaciją", + "ContactStatus_Account": "Paskyra", + "ContactStatus_Modified": "Redaguotas", + "ContactAction_Edit": "Redaguoti kontaktą", + "ContactAction_ChangePhoto": "Pakeisti nuotrauką", + "ContactAction_Delete": "Ištrinti kontaktą", + "ContactAction_Add": "Pridėti kontaktą", + "ContactSelection_Selected": "pasirinktas", + "ContactSelection_SelectAll": "Pasirinkti visus", + "ContactSelection_Clear": "Išvalyti pasirinkimą", + "ContactsPage_EmptyState": "Kontaktų nėra.", + "ContactsPage_AddFirstContact": "Pridėkite savo pirmąjį kontaktą.", + "ContactsPage_ContactsCountSuffix": "kontaktai", + "ContactsPane_NewContact": "Naujas kontaktas", + "ContactsPane_DescriptionTitle": "Tvarkykite savo kontaktus", + "ContactsPane_DescriptionBody": "Kurkite kontaktus, pervardykite juos, atnaujinkite profilio nuotraukas ir visus išsaugotus duomenis laikykite vienoje vietoje.", + "ContactEditDialog_AddTitle": "Pridėti kontaktą", + "ContactInfoBar_ContactAdded": "Kontaktas sėkmingai pridėtas.", + "ContactInfoBar_ContactUpdated": "Kontaktas sėkmingai atnaujintas.", + "ContactInfoBar_ContactsDeleted": "Kontaktai sėkmingai ištrinti.", + "ContactInfoBar_ContactPhotoUpdated": "Kontaktinė nuotrauka sėkmingai atnaujinta.", + "ContactInfoBar_FailedToLoadContacts": "Nepavyko įkelti kontaktų: {0}", + "ContactInfoBar_FailedToAddContact": "Nepavyko pridėti kontakto: {0}", + "ContactInfoBar_FailedToUpdateContact": "Nepavyko atnaujinti kontakto: {0}", + "ContactInfoBar_FailedToDeleteContacts": "Nepavyko ištrinti kontaktų: {0}", + "ContactInfoBar_FailedToUpdatePhoto": "Nepavyko atnaujinti nuotraukos: {0}", + "ContactInfoBar_CannotDeleteRoot": "Pagrindiniai kontaktai negali būti ištrinti.", + "ContactConfirmDialog_DeleteTitle": "Ištrinti kontaktą", + "ContactConfirmDialog_DeleteMessage": "Ar tikrai norite ištrinti kontaktą '{0}'?", + "ContactConfirmDialog_DeleteMultipleMessage": "Ar tikrai norite ištrinti {0} kontaktą(-ų)?", + "ContactConfirmDialog_DeleteButton": "Ištrinti", + "CalendarAccountSettings_Title": "Kalendoriaus paskyros nustatymai", + "CalendarAccountSettings_Description": "Valdykite kalendoriaus nustatymus {0}.", + "CalendarAccountSettings_AccountColor": "Kalendoriaus paskyros spalva", + "CalendarAccountSettings_AccountColorDescription": "Keiskite šios kalendoriaus paskyros rodymo spalvą", + "CalendarAccountSettings_SyncEnabled": "Įgalinti sinchronizaciją", + "CalendarAccountSettings_SyncEnabledDescription": "Įgalinti arba išjungti šios paskyros kalendoriaus sinchronizavimą", + "CalendarAccountSettings_DefaultShowAs": "Numatytas rodomas statusas", + "CalendarAccountSettings_DefaultShowAsDescription": "Numatytas prieinamumo statusas naujiems įvykiams, kuriuos kuriate su šia paskyra.", + "CalendarAccountSettings_PrimaryCalendar": "Pagrindinis kalendorius", + "CalendarAccountSettings_PrimaryCalendarDescription": "Pažymėkite šį kalendorių kaip pagrindinį paskyros kalendorių", + "CalendarSettings_NewEventBehavior_Header": "Naujo įvykio mygtuko elgsena", + "CalendarSettings_NewEventBehavior_Description": "Pasirinkite, ar mygtukas Naujasis įvykis turėtų prašyti kalendoriaus kiekvieną kartą, ar visada atidaryti konkretų kalendorių.", + "CalendarSettings_NewEventBehavior_AskEachTime": "Klausti kiekvieną kartą.", + "CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar": "Visada naudoti konkretų kalendorių.", + "CalendarSettings_Rendering_Title": "Atvaizdavimas", + "CalendarSettings_Rendering_Description": "Nustatykite kalendoriaus išdėstymo ir rodymo elgesį.", + "CalendarSettings_Notifications_Title": "Pranešimai", + "CalendarSettings_Notifications_Description": "Pasirinkite numatytą priminimo ir atidėjimo elgesį.", + "CalendarSettings_Preferences_Title": "Nustatymai", + "CalendarSettings_Preferences_Description": "Nustatykite, kaip veiks Naujo įvykio mygtukas.", + "WhatIsNew_GetStartedButton": "Pradėti", + "WhatIsNew_ContinueAnywayButton": "Tęsti bet kokiu atveju", + "WhatIsNew_PreparingForNewVersionButton": "Ruošiama naujai versijai...", + "WhatIsNew_MigrationPreparing_Title": "Paruošiami jūsų duomenys", + "WhatIsNew_MigrationPreparing_Description": "Wino taiko atnaujinimo migracijas. Palaukite, kol paruošime jūsų paskyros duomenis šiai versijai.", + "WhatIsNew_MigrationFailedMessage": "Migracijų taikymas nepavyko su klaidos kodu {0}. Galite toliau naudoti programą. Tačiau jei kiltų rimtų problemų, prašome iš naujo įdiegti programą.", + "WhatIsNew_MigrationNotification_Title": "Wino Mail atnaujinta", + "WhatIsNew_MigrationNotification_Message": "Atidarykite programą, kad užbaigtumėte atnaujinimą ir pamatytumėte naujienas.", + "WelcomeWindow_Title": "Sveiki atvykę į Wino Mail", + "WelcomeWindow_Subtitle": "Natyvi Windows patirtis paštui ir kalendoriui.", + "WelcomeWindow_WhatsNewTitle": "Naujienos", + "WelcomeWindow_FeaturesTitle": "Funkcijos", + "WelcomeWindow_WhatsNewTab": "Naujienos", + "WelcomeWindow_FeaturesTab": "Funkcijos", + "WelcomeWindow_GetStartedButton": "Pradėti pridėdami paskyrą", + "WelcomeWindow_GetStartedDescription": "Pridėkite paskyrą Outlook, Gmail arba IMAP, kad pradėtumėte naudotis Wino Mail.", + "WelcomeWindow_ImportFromWinoAccount": "Importuoti iš jūsų Wino paskyros", + "WelcomeWindow_ImportInProgress": "Importuojami jūsų sinchronizuoti nustatymai ir paskyros...", + "WelcomeWindow_ImportNoAccountsFound": "Nerasta sinchronizuotų paskyrų jūsų Wino paskyroje. Jei buvo nustatymų, jie buvo atstatyti. Naudokite Pradėti, kad pridėtumėte paskyrą rankiniu būdu.", + "WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} sinchronizuotų paskyrų jau yra šiame įrenginyje. Jei reikia, naudokite Pradėti, kad pridėtumėte dar vieną paskyrą rankiniu būdu.", + "WelcomeWindow_SetupTitle": "Nustatykite savo paskyrą", + "WelcomeWindow_SetupSubtitle": "Pasirinkite el. pašto teikėją, kad pradėtumėte", + "WelcomeWindow_AddAccountButton": "Pridėti paskyrą", + "WelcomeWindow_SkipForNow": "Praleisti dabar — sukonfigūruosiu vėliau", + "WelcomeWindow_AppDescription": "Greita, sutelkta pašto dėžutė — pritaikyta Windows 11", + "WelcomeWizard_Step1Title": "Sveiki", + "SystemTrayMenu_Open": "Atidaryti", + "WinoAccount_Titlebar_SyncBenefitTitle": "Sinchronizavimo nustatymai", + "WinoAccount_Titlebar_SyncBenefitDescription": "Laikykite savo Wino nustatymus sinchronizuotus tarp įrenginių.", + "WinoAccount_Titlebar_AddonsBenefitTitle": "Atrakinti papildinius", + "WinoAccount_Titlebar_AddonsBenefitDescription": "Gaukite prieigą prie premium funkcijų, pvz., Wino AI Pack.", + "WinoAccount_Management_Description": "Tvarkykite savo Wino paskyrą, AI Pack prieigą ir sinchronizuotus nustatymus bei paskyros duomenis.", + "WinoAccount_Management_SignedOutTitle": "Prisijunkite prie Wino Mail", + "WinoAccount_Management_SignedOutDescription": "Prisijunkite arba sukurkite paskyrą, kad sinchronizuotumėte el. laiškus, gautumėte prieigą prie AI funkcijų ir valdytumėte nustatymus tarp įrenginių.", + "WinoAccount_Management_ProfileSectionHeader": "Profilis", + "WinoAccount_Management_AddOnsSectionHeader": "Wino papildiniai", + "WinoAccount_Management_DataSectionHeader": "Duomenys", + "WinoAccount_Management_AccountActionsSectionHeader": "Paskyros veiksmai", + "WinoAccount_Management_AccountCardTitle": "Paskyra", + "WinoAccount_Management_AccountCardDescription": "Jūsų Wino paskyros el. pašto adresas ir esama paskyros būsena.", + "WinoAccount_Management_AiPackCardTitle": "AI Paketas", + "WinoAccount_Management_AiPackCardDescription": "Pasižiūrėkite, ar Wino AI Paketas yra aktyvus ir kiek liko naudojimo.", + "WinoAccount_Management_AiPackActive": "AI Paketas yra aktyvus", + "WinoAccount_Management_AiPackInactive": "AI Paketas nėra aktyvus", + "WinoAccount_Management_AiPackUsage": "{0} iš {1} naudojimų panaudota. Liko {2}.", + "WinoAccount_Management_AiPackBillingPeriod": "Apmokėjimo periodas: {0:d} - {1:d}", + "WinoAccount_Management_AiPackUnknownUsage": "Naudojimo duomenys dar nėra prieinami.", + "WinoAccount_Management_AiPackBuyDescription": "Pirkite Wino AI Pack, kad galėtumėte versti, perrašyti ar santraukti el. laiškus su AI.", + "WinoAccount_Management_AiPackPromoTitle": "Atrakinti AI Paketą", + "WinoAccount_Management_AiPackPromoDescription": "Pagerinkite el. pašto darbą su AI įrankiais. Išversti žinutes į daugiau nei 50 kalbų, perrašyti jas aiškiai ir tinkamai tonui, ir gauti trumpas santraukas iš ilgesnių pokalbių.", + "WinoAccount_Management_AiPackPromoPrice": "$4,99 / mėn", + "WinoAccount_Management_AiPackPromoRequests": "1 000 kreditų", + "WinoAccount_Management_AiPackGetButton": "Gauti AI paketą", + "WinoAddOn_AI_PACK_Name": "Wino AI Pack", + "WinoAddOn_AI_PACK_Description": "AI pagrindu veikiantys įrankiai vertimams, perrašymams ir santraukoms Wino Mail.", + "WinoAddOn_AI_PACK_Keywords": "AI, vertimas, perrašymas, santraukos, produktyvumas", + "WinoAddOn_UNLIMITED_ACCOUNTS_Name": "Neribotos paskyros", + "WinoAddOn_UNLIMITED_ACCOUNTS_Description": "Šalina paskyrų apribojimą ir leidžia pridėti tiek el. pašto paskyrų, kiek reikia.", + "WinoAddOn_UNLIMITED_ACCOUNTS_Keywords": "paskyros, neribotos, premium, priedas", + "WinoAccount_Management_PurchaseRequiresSignIn": "Prisijunkite prie savo Wino paskyros, kad užbaigtumėte šią pirkimo operaciją.", + "WinoAccount_Management_PurchaseStartFailed": "Wino negalėjo užbaigti šios Microsoft Store pirkimo operacijos.", + "WinoAccount_Management_StoreSyncFailed": "Pirkimas baigėsi, bet Wino dar negali atnaujinti jūsų paskyros naudų. Bandykite dar kartą po akimirkos.", + "WinoAccount_Management_AiPackSubscriptionActive": "Jūsų prenumerata veikia", + "WinoAccount_Management_AiPackRenews": "Atnaujinama {0:d}", + "WinoAccount_Management_AiPackRequestsUsed": "Šį mėnesį panaudoti kreditai", + "WinoAccount_Management_AiPackResets": "Atstatymai {0:d}", + "WinoAccount_Management_AiPackUsageLoadFailed": "Kilo problemų įkeliant jūsų AI naudojimo balansą.", + "WinoAccount_Management_AiPackFeatureTranslate": "Išversti", + "WinoAccount_Management_AiPackFeatureRewrite": "Perrašyti", + "WinoAccount_Management_AiPackFeatureSummarize": "Santraukuoti", + "WinoAccount_Management_AddOnLoadFailed": "Kilo problemų įkeliant šį papildinį.", + "WinoAccount_Management_SyncPreferencesTitle": "Sinchronizuoti Nustatymus ir Paskyras", + "WinoAccount_Management_SyncPreferencesDescription": "Importuokite arba eksportuokite savo Wino nustatymus ir pašto dėžutės duomenis tarp įrenginių. Slaptažodžiai, tokenai ir kita jautri informacija niekada nesinchronizuojama.", + "WinoAccount_Management_SignOutTitle": "Atsijungti", + "WinoAccount_Management_SignOutDescription": "Atsijunkite nuo savo paskyros šiame įrenginyje", + "WinoAccount_Management_StatusLabel": "Būsena: {0}", + "WinoAccount_Management_NoRemoteSettings": "Šiai paskyrai dar nėra saugomų sinchronizuotų duomenų.", + "WinoAccount_Management_ExportSucceeded": "Pasirinkti Wino duomenys buvo sėkmingai eksportuoti.", + "WinoAccount_Management_ExportPreferencesSucceeded": "Jūsų nustatymai buvo eksportuoti į jūsų Wino paskyrą.", + "WinoAccount_Management_ExportAccountsSucceeded": "Eksportuota {0} paskyros informacija į jūsų Wino paskyrą.", + "WinoAccount_Management_ImportSucceeded": "Importuoti sinchronizuoti duomenys iš jūsų Wino paskyros.", + "WinoAccount_Management_ImportPreferencesSucceeded": "Pritaikyta {0} sinchronizuotų nustatymų.", + "WinoAccount_Management_ImportAccountsSucceeded": "Importuotos {0} paskyros.", + "WinoAccount_Management_ImportDuplicateAccountsSkipped": "Praleista {0} paskyrų, kurios jau egzistuoja šiame įrenginyje.", + "WinoAccount_Management_ImportPartial": "Pritaikytos {0} sinchronizuotos nuostatos. {1} nuostatos negalėjo būti atkurtos.", + "WinoAccount_Management_ImportReloginReminder": "Slaptažodžiai, tokenai ir kita jautri informacija nebuvo importuota. Prisijunkite iš naujo prie kiekvienos paskyros šiame įrenginyje prieš naudodamiesi.", + "WinoAccount_Management_SerializeFailed": "Wino negalėjo serializuoti jūsų dabartinių nuostatų.", + "WinoAccount_Management_EmptyExport": "Nėra nuostatų reikšmių eksportuoti.", + "WinoAccount_Management_ImportEmpty": "Sinchronizuotas duomenų paketas neturi nieko naujo, ką atkurti.", + "WinoAccount_Management_ExportDialog_Title": "Eksportuoti į savo Wino paskyrą.", + "WinoAccount_Management_ExportDialog_Description": "Pasirinkite, ką norite sinchronizuoti su savo Wino paskyra.", + "WinoAccount_Management_ExportDialog_IncludePreferences": "Nuostatos", + "WinoAccount_Management_ExportDialog_IncludeAccounts": "Paskyros", + "WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Slaptažodžiai, tokenai ir kita jautri informacija nėra sinchronizuojama.", + "WinoAccount_Management_ExportDialog_AccountsRelogin": "Importuotos paskyros kituose kompiuteriuose vis tiek reikės prisijungti iš naujo prieš jų naudojimą.", + "WinoAccount_Management_ExportDialog_InProgress": "Eksportuojami jūsų pasirinkti Wino duomenys...", + "WinoAccount_Management_LoadFailed": "Wino negalėjo įkelti naujausios Wino paskyros informacijos.", + "WinoAccount_Management_ActionFailed": "Prašymas Wino paskyrai negalėjo būti užbaigtas.", + "WinoAccount_SettingsSection_Title": "Wino paskyra", + "WinoAccount_SettingsSection_Description": "Sukurkite arba prisijunkite prie Wino paskyros naudodami savo vietinį autentifikavimo paslaugą.", + "WinoAccount_RegisterButton_Title": "Registruoti paskyrą", + "WinoAccount_RegisterButton_Description": "Sukurkite Wino paskyrą su el. pašto adresu ir slaptažodžiu.", + "WinoAccount_RegisterButton_Action": "Atidaryti registraciją", + "WinoAccount_LoginButton_Title": "Prisijungti", + "WinoAccount_LoginButton_Description": "Prisijunkite prie esamos Wino paskyros naudodami el. paštą ir slaptažodį.", + "WinoAccount_LoginButton_Action": "Atidaryti prisijungimą", + "WinoAccount_SignOutButton_Title": "Atsijungti", + "WinoAccount_SignOutButton_Description": "Ištrinti vietoje saugomą Wino paskyros seansą.", + "WinoAccount_SignOutButton_Action": "Atsijungti", + "WinoAccount_RegisterDialog_Title": "Sukurti Wino paskyrą", + "WinoAccount_RegisterDialog_Description": "Sukurkite Wino paskyrą, kad jūsų Wino patirtis būtų sinchronizuota ir būtų įjungti paskyros pagrindu veikiantys papildiniai.", + "WinoAccount_RegisterDialog_HeroTitle": "Sukurti savo Wino paskyrą", + "WinoAccount_RegisterDialog_BenefitsTitle": "Kodėl sukurti vieną paskyrą?", + "WinoAccount_RegisterDialog_BenefitSyncTitle": "Importavimas ir eksportavimas tarp įrenginių", + "WinoAccount_RegisterDialog_BenefitSyncDescription": "Perkelkite savo Wino nuostatas tarp įrenginių be naujo konfigūravimo nuo nulio.", + "WinoAccount_RegisterDialog_BenefitAiTitle": "Gaukite prieigą prie išskirtinių papildinių, pvz., Wino AI Pack (mokama).", + "WinoAccount_RegisterDialog_BenefitAiDescription": "Naudokite vieną paskyrą, kad atrakintumėte premium Wino funkcijas jų atsiradimo metu.", + "WinoAccount_RegisterDialog_DifferenceTitle": "Wino paskyra yra atskira nuo jūsų pašto paskyrų", + "WinoAccount_RegisterDialog_DifferenceDescription": "Jūsų Outlook, Gmail, IMAP ar kitos el. pašto paskyros lieka tokios, kokios yra. Wino paskyra valdo tik Wino specifines funkcijas ir paskyros pagrindu veikiančius papildinius.", + "WinoAccount_RegisterDialog_PrimaryButton": "Registruotis", + "WinoAccount_RegisterDialog_PrivacyTitle": "Privatumas ir API duomenų apdorojimas", + "WinoAccount_RegisterDialog_PrivacyDescription": "Pasirenkami papildiniai, tokie kaip Wino AI Pack, gali siųsti pasirinktą el. pašto HTML turinį į Wino API paslaugą tik naudodamiesi šiomis funkcijomis.", + "WinoAccount_RegisterDialog_PrivacyLinkText": "Skaityti privatumo politiką", + "WinoAccount_RegisterDialog_PrivacyCheckbox": "Aš sutinku su privatumo politika.", + "WinoAccount_LoginDialog_Title": "Prisijungti prie Wino paskyros", + "WinoAccount_LoginDialog_Description": "Prisijunkite prie Wino paskyros, kad sinchronizuotumėte savo Wino nustatymus ir gautumėte prieigą prie paskyros pagrindu veikiančių funkcijų.", + "WinoAccount_LoginDialog_HeroTitle": "Sveiki sugrįžę", + "WinoAccount_LoginDialog_BenefitsTitle": "Ką suteikia prisijungimas", + "WinoAccount_LoginDialog_BenefitsDescription": "Naudokite savo Wino paskyrą, kad toliau sinchronizuotumėte nustatymus tarp įrenginių ir gautumėte prieigą prie mokamų papildinių, tokių kaip Wino AI Pack.", + "WinoAccount_LoginDialog_DifferenceTitle": "Tai nėra prisijungimas prie jūsų el. pašto dėžutės", + "WinoAccount_LoginDialog_DifferenceDescription": "Prisijungimas čia neprideda ar nepakeičia jūsų Outlook, Gmail ar IMAP paskyrų Wino. Jis tik prisijungia prie Wino specifinių paslaugų.", + "WinoAccount_LoginDialog_ForgotPasswordLink": "Pamiršote slaptažodį?", + "WinoAccount_EmailLabel": "El. paštas", + "WinoAccount_EmailPlaceholder": "vardas@example.com", + "WinoAccount_PasswordLabel": "Slaptažodis", + "WinoAccount_ConfirmPasswordLabel": "Patvirtinti slaptažodį", + "WinoAccount_ForgotPasswordDialog_Title": "Atstatyti slaptažodį", + "WinoAccount_ForgotPasswordDialog_PrimaryButton": "Išsiųsti slaptažodžio atstatymo el. laišką", + "WinoAccount_ForgotPasswordDialog_BackToSignIn": "Grįžti prie prisijungimo", + "WinoAccount_ForgotPasswordDialog_Description": "Įveskite savo Wino paskyros el. pašto adresą, o mes išsiųsime slaptažodžio atstatymo nuorodą, jeigu adresas yra užregistruotas.", + "WinoAccount_Validation_EmailRequired": "El. paštas yra privalomas.", + "WinoAccount_Validation_PasswordRequired": "Slaptažodis yra privalomas.", + "WinoAccount_Validation_PasswordMismatch": "Slaptažodžiai nesutampa.", + "WinoAccount_Validation_PrivacyConsentRequired": "Prieš kuriant Wino paskyrą turite sutikti su privatumo politika.", + "WinoAccount_Error_InvalidCredentials": "El. pašto adresas arba slaptažodis yra neteisingi.", + "WinoAccount_Error_AccountLocked": "Ši paskyra laikina užblokuota.", + "WinoAccount_Error_AccountBanned": "Ši paskyra buvo uždrausta.", + "WinoAccount_Error_AccountSuspended": "Ši paskyra buvo sustabdyta.", + "WinoAccount_Error_EmailNotConfirmed": "Prašome patvirtinti el. pašto adresą prieš prisijungiant.", + "WinoAccount_Error_EmailConfirmationRequired": "Prašome patvirtinti el. pašto adresą prieš prisijungiant.", + "WinoAccount_Error_EmailConfirmationResendNotAvailable": "Naujas patvirtinimo el. laiškas dar nėra prieinamas.", + "WinoAccount_Error_EmailConfirmationResendInvalid": "Šis patvirtinimo prašymas jau nebegalioja. Bandykite prisijungti iš naujo.", + "WinoAccount_Error_EmailNotRegistered": "Šis el. pašto adresas nėra užregistruotas.", + "WinoAccount_Error_RefreshTokenInvalid": "Jūsų sesija jau nebegalioja. Prašome prisijungti iš naujo.", + "WinoAccount_Error_EmailAlreadyRegistered": "Šis el. pašto adresas jau yra užregistruotas.", + "WinoAccount_Error_ExternalLoginEmailRequired": "Norint užbaigti išorinį prisijungimą, reikia el. pašto adreso.", + "WinoAccount_Error_ExternalLoginInvalid": "Išorinis prisijungimo prašymas yra neteisingas.", + "WinoAccount_Error_ExternalAuthStateInvalid": "Išorinė prisijungimo būsena yra neteisinga arba pasibaigusi.", + "WinoAccount_Error_ExternalAuthCodeInvalid": "Išorinis prisijungimo kodas yra neteisingas arba pasibaigęs.", + "WinoAccount_Error_AiPackRequired": "Šiai veiksmui reikalinga aktyvi Wino AI Pack prenumerata.", + "WinoAccount_Error_AiQuotaExceeded": "Jūsų AI Pack naudojimo limitas šiam apmokėjimo laikotarpiui yra išnaudotas.", + "WinoAccount_Error_AiHtmlEmpty": "Nėra el. pašto turinio, kurį apdoroti.", + "WinoAccount_Error_AiHtmlTooLarge": "Šis el. laiškas per didelis, kad būtų apdorotas su Wino AI.", + "WinoAccount_Error_AiUnsupportedLanguage": "Ši kalba nepalaikoma. Pabandykite tinkamą kultūros kodą, pvz., en-US arba tr-TR.", + "WinoAccount_Error_Forbidden": "Neturite teisės atlikti šį veiksmą.", + "WinoAccount_Error_ValidationFailed": "Prašymas yra neteisingas. Prašome peržiūrėti įvestas reikšmes.", + "WinoAccount_RegisterSuccessMessage": "Wino paskyros registracija atlikta {0}.", + "WinoAccount_LoginSuccessMessage": "Prisijungta prie Wino paskyros kaip {0}.", + "WinoAccount_EmailConfirmationSentDialog_Title": "Patvirtinkite savo el. pašto adresą", + "WinoAccount_EmailConfirmationSentDialog_Message": "Mes išsiuntėme el. pašto patvirtinimą į {0}. Prašome patvirtinti jį ir bandykite prisijungti iš naujo.", + "WinoAccount_EmailConfirmationPendingDialog_Title": "Reikia el. pašto patvirtinimo.", + "WinoAccount_EmailConfirmationPendingDialog_Message": "Laukiame, kol patvirtinsite {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ResendButton": "Išsiųsti patvirtinimo el. laišką iš naujo", + "WinoAccount_EmailConfirmationPendingDialog_Countdown": "Patvirtinimo el. laišką galite išsiųsti po {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ReadyToResend": "Dabar galite išsiųsti patvirtinimo el. laišką iš naujo.", + "WinoAccount_EmailConfirmationResentDialog_Title": "Patvirtinimo el. laiškas išsiųstas iš naujo.", + "WinoAccount_EmailConfirmationResentDialog_Message": "Mes išsiuntėme dar vieną patvirtinimo el. laišką į {0}. Prašome patvirtinti jį ir bandykite prisijungti iš naujo.", + "WinoAccount_ForgotPasswordDialog_SuccessTitle": "Slaptažodžio atstatymo el. laiškas išsiųstas", + "WinoAccount_ForgotPasswordDialog_SuccessMessage": "Mes išsiuntėme slaptažodžio atstatymo el. laišką į {0}. Atidarykite tą pranešimą ir pasirinkite naują slaptažodį.", + "WinoAccount_ChangePassword_Title": "Pakeisti slaptažodį", + "WinoAccount_ChangePassword_Description": "Siųsti slaptažodžio atstatymo el. laišką šiai Wino paskyrai.", + "WinoAccount_ChangePassword_Action": "Išsiųsti atstatymo el. laišką", + "WinoAccount_ChangePassword_ConfirmationMessage": "Ar norite, kad Wino išsiųstų slaptažodžio atkūrimo el. laišką adresu {0}?", + "WinoAccount_SignOut_SuccessMessage": "Atsijungta nuo Wino paskyros {0}.", + "WinoAccount_SignOut_NoAccountMessage": "Nėra aktyvios Wino paskyros, iš kurios atsijungti.", + "WinoAccount_Titlebar_SignedOutTitle": "Wino paskyra", + "WinoAccount_Titlebar_SignedOutDescription": "Prisijunkite arba sukurkite Wino paskyrą, kad valdytumėte savo Wino sesiją.", + "WinoAccount_Titlebar_SignedInStatus": "Būsena: {0}", + "WelcomeWizard_Step2Title": "Pridėti paskyrą", + "WelcomeWizard_Step3Title": "Nustatymo užbaigimas", + "ProviderSelection_Title": "Pasirinkite savo el. pašto teikėją", + "ProviderSelection_Subtitle": "Pasirinkite teikėją žemiau, kad pridėtumėte savo el. pašto paskyrą prie Wino Mail.", + "ProviderSelection_AccountNameHeader": "Paskyros pavadinimas", + "ProviderSelection_AccountNamePlaceholder": "pvz. Asmeninė, Darbo", + "ProviderSelection_DisplayNameHeader": "Rodomas vardas", + "ProviderSelection_DisplayNamePlaceholder": "pvz. Jonas Doe", + "ProviderSelection_EmailHeader": "El. pašto adresas", + "ProviderSelection_EmailPlaceholder": "pvz. johndoe@example.com", + "ProviderSelection_AppPasswordHeader": "Programos specifinis slaptažodis", + "ProviderSelection_AppPasswordHelp": "Kaip gauti programos specifinį slaptažodį?", + "ProviderSelection_CalendarModeHeader": "Kalendoriaus integracija", + "ProviderSelection_CalendarMode_DisabledTitle": "Išjungta", + "ProviderSelection_CalendarMode_DisabledDescription": "Nėra kalendoriaus integracijos", + "ProviderSelection_CalendarMode_CalDavTitle": "CalDAV sinchronizavimas", + "ProviderSelection_CalendarMode_CalDavDescription_Apple": "Jūsų kalendoriaus įvykiai sinchronizuojami su Apple serveriais tarp įrenginių.", + "ProviderSelection_CalendarMode_CalDavDescription_Yahoo": "Jūsų kalendoriaus įvykiai sinchronizuojami su Yahoo serveriais tarp įrenginių.", + "ProviderSelection_CalendarMode_LocalTitle": "Vietinis kalendorius", + "ProviderSelection_CalendarMode_LocalDescription": "Jūsų įvykiai saugomi tik jūsų kompiuteryje. Nėra ryšio su serveriais.", + "ProviderSelection_ClearColor": "Išvalyti spalvą", + "ProviderSelection_ContinueButton": "Tęsti", + "ProviderSelection_SpecialImap_Subtitle": "Įveskite savo paskyros prisijungimo duomenis, kad prisijungtumėte.", + "AccountSetup_Title": "Jūsų paskyros nustatymas", + "AccountSetup_Step_Authenticating": "Autentifikuojama su {0}", + "AccountSetup_Step_TestingMailAuth": "Testuojama el. pašto autentifikacija", + "AccountSetup_Step_SyncingFolders": "Sinchronizuojami aplankų metaduomenys", + "AccountSetup_Step_FetchingProfile": "Gaunama profilio informacija", + "AccountSetup_Step_DiscoveringCalDav": "CalDAV nustatymų aptikimas", + "AccountSetup_Step_TestingCalendarAuth": "Kalendoriaus autentifikacija testuojama", + "AccountSetup_Step_SavingAccount": "Išsaugoma paskyros informacija", + "AccountSetup_Step_FetchingCalendarMetadata": "Gaunami kalendoriaus metaduomenys", + "AccountSetup_Step_SyncingAliases": "Aliasai sinchronizuojami", + "AccountSetup_Step_Finalizing": "Nustatymo užbaigimas", + "AccountSetup_FailureMessage": "Nustatymas nepavyko. Grįžkite atgal, kad ištaisytumėte nustatymus, arba bandykite vėliau.", + "AccountSetup_SuccessMessage": "Jūsų paskyra sėkmingai sukurta!", + "AccountSetup_GoBackButton": "Grįžti", + "AccountSetup_TryAgainButton": "Bandykite dar kartą", + "ImapCalDavSettings_AutoDiscoveryFailed": "Automatinis aptikimas nepavyko. Įveskite nustatymus rankiniu būdu Išplėstiniame skirtuke." } - - diff --git a/Wino.Core.Domain/Translations/nl_NL/resources.json b/Wino.Core.Domain/Translations/nl_NL/resources.json index 6c7c8f1a..f6480a62 100644 --- a/Wino.Core.Domain/Translations/nl_NL/resources.json +++ b/Wino.Core.Domain/Translations/nl_NL/resources.json @@ -8,6 +8,7 @@ "AccountCacheReset_Message": "Dit account moet opnieuw gesynchroniseerd worden om te blijven werken. Wacht totdat Wino uw berichten opnieuw heeft gesynchroniseerd...", "AccountContactNameYou": "U", "AccountCreationDialog_Completed": "Alles is gereed", + "AccountCreationDialog_FetchingCalendarMetadata": "Kalendergegevens ophalen.", "AccountCreationDialog_FetchingEvents": "Agenda gebeurtenissen aan het ophalen.", "AccountCreationDialog_FetchingProfileInformation": "Profielgegevens aan het ophalen.", "AccountCreationDialog_GoogleAuthHelpClipboardText_Row0": "Indien uw browser niet automatisch opstart om de authenticatie te voltooien:", @@ -17,6 +18,7 @@ "AccountCreationDialog_Initializing": "Bezig met initialiseren", "AccountCreationDialog_PreparingFolders": "Map gegevens worden momenteel ontvangen.", "AccountCreationDialog_SigninIn": "Accountinformatie wordt opgeslagen.", + "Purchased": "Aangekocht", "AccountEditDialog_Message": "Accountnaam", "AccountEditDialog_Title": "Account bewerken", "AccountPickerDialog_Title": "Kies een account", @@ -26,6 +28,10 @@ "AccountDetailsPage_Description": "Wijzig de accountnaam in Wino en stel de gewenste naam van de afzender in.", "AccountDetailsPage_ColorPicker_Title": "Account kleur", "AccountDetailsPage_ColorPicker_Description": "Wijs een nieuwe account kleur toe om het bijbehorende symbool in de lijst in te kleuren.", + "AccountDetailsPage_TabGeneral": "Algemeen", + "AccountDetailsPage_TabMail": "E-mail", + "AccountDetailsPage_TabCalendar": "Kalender", + "AccountDetailsPage_CalendarListDescription": "Selecteer een kalender om de instellingen ervan te configureren.", "AddHyperlink": "Toevoegen", "AppCloseBackgroundSynchronizationWarningTitle": "Achtergrondsynchronisatie", "AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Applicatie is niet ingesteld om te laden bij het opstarten van Windows.", @@ -47,8 +53,10 @@ "BasicIMAPSetupDialog_Title": "IMAP-account", "Busy": "Bezig", "Buttons_AddAccount": "Account toevoegen", + "Buttons_FixAccount": "Account repareren", "Buttons_AddNewAlias": "Alias toevoegen", "Buttons_Allow": "Toestaan", + "Buttons_Apply": "Toepassen", "Buttons_ApplyTheme": "Thema toepassen", "Buttons_Browse": "Bladeren", "Buttons_Cancel": "Annuleren", @@ -62,6 +70,7 @@ "Buttons_Edit": "Bewerken", "Buttons_EnableImageRendering": "Inschakelen", "Buttons_Multiselect": "Meerdere selecteren", + "Buttons_Manage": "Beheren", "Buttons_No": "Nee", "Buttons_Open": "Openen", "Buttons_Purchase": "Aanschaffen", @@ -70,15 +79,134 @@ "Buttons_Save": "Opslaan", "Buttons_SaveConfiguration": "Configuratie opslaan", "Buttons_Send": "Verzenden", + "Buttons_SendToServer": "Naar server verzenden", "Buttons_Share": "Delen", "Buttons_SignIn": "Aanmelden", "Buttons_Sync": "Synchroniseren", "Buttons_SyncAliases": "Aliassen synchroniseren", "Buttons_TryAgain": "Probeer opnieuw", "Buttons_Yes": "Ja", + "Sync_SynchronizingFolder": "Synchroniseren van {0} {1}%", + "Sync_DownloadedMessages": "Gedownloade {0} berichten van {1}", + "SyncAction_Archiving": "Archiveren van {0} e-mail(s)", + "SyncAction_ClearingFlag": "Vlag verwijderen voor {0} e-mail(s)", + "SyncAction_CreatingDraft": "Concept aanmaken", + "SyncAction_CreatingEvent": "Evenement aanmaken", + "SyncAction_Deleting": "Verwijderen van {0} e-mail(s)", + "SyncAction_EmptyingFolder": "Map legen", + "SyncAction_MarkingAsRead": "Markeren van {0} e-mail(s) als gelezen", + "SyncAction_MarkingAsUnread": "Markeren van {0} e-mail(s) als ongelezen", + "SyncAction_MarkingFolderAsRead": "Map als gelezen markeren", + "SyncAction_Moving": "Verplaatsen van {0} e-mail(s)", + "SyncAction_MovingToFocused": "Verplaatsen van {0} e-mail(s) naar Focused", + "SyncAction_RenamingFolder": "Map hernoemen", + "SyncAction_SendingMail": "Mail verzenden", + "SyncAction_SettingFlag": "Vlag toewijzen aan {0} e-mail(s)", + "SyncAction_SynchronizingAccount": "Synchroniseren van {0}", + "SyncAction_SynchronizingAccounts": "Synchroniseren van {0} account(en)", + "SyncAction_SynchronizingCalendarData": "Synchroniseren van kalendergegevens", + "SyncAction_SynchronizingCalendarEvents": "Synchroniseren van kalendergebeurtenissen", + "SyncAction_SynchronizingCalendarMetadata": "Synchroniseren van kalendermetadata", + "SyncAction_Unarchiving": "Uitpakken van {0} e-mail(s)", "CalendarAllDayEventSummary": "Gebeurtenissen die de hele dag duren", "CalendarDisplayOptions_Color": "Kleur", "CalendarDisplayOptions_Expand": "Uitklappen", + "CalendarEventResponse_Accept": "Accepteren", + "CalendarEventResponse_AcceptedResponse": "Geaccepteerd", + "CalendarEventResponse_Decline": "Afwijzen", + "CalendarEventResponse_DeclinedResponse": "Afgewezen", + "CalendarEventResponse_NotResponded": "Nog niet gereageerd", + "CalendarEventResponse_Tentative": "Tentatief", + "CalendarEventResponse_TentativeResponse": "Tentatief", + "CalendarEventRsvpPanel_Accept": "Accepteren", + "CalendarEventRsvpPanel_AddMessage": "Voeg een bericht toe aan uw antwoord... (optioneel)", + "CalendarEventRsvpPanel_Decline": "Afwijzen", + "CalendarEventRsvpPanel_Message": "Bericht", + "CalendarEventRsvpPanel_SendReplyMessage": "Antwoordbericht verzenden", + "CalendarEventRsvpPanel_Tentative": "Tentatief", + "CalendarEventRsvpPanel_Title": "Antwoordopties", + "CalendarAttendeeStatus_Accepted": "Geaccepteerd", + "CalendarAttendeeStatus_Declined": "Afgewezen", + "CalendarAttendeeStatus_NeedsAction": "Actie vereist", + "CalendarAttendeeStatus_Tentative": "Tentatief", + "CalendarEventDetails_Attachments": "Bijlagen", + "CalendarEventCompose_AddAttachment": "Bijlage toevoegen", + "CalendarEventCompose_AllDay": "Hele dag", + "CalendarEventCompose_AttachmentsNotSupportedForCalDav": "Bijlagen worden niet ondersteund voor CalDAV-kalenders.", + "CalendarEventCompose_EndDate": "Einddatum", + "CalendarEventCompose_EndTime": "Eindtijd", + "CalendarEventCompose_Every": "elke", + "CalendarEventCompose_ForWeekdays": "voor", + "CalendarEventCompose_FrequencyDay": "dag", + "CalendarEventCompose_FrequencyDayPlural": "dagen", + "CalendarEventCompose_FrequencyMonth": "maand", + "CalendarEventCompose_FrequencyMonthPlural": "maanden", + "CalendarEventCompose_FrequencyWeek": "week", + "CalendarEventCompose_FrequencyWeekPlural": "weken", + "CalendarEventCompose_FrequencyYear": "jaar", + "CalendarEventCompose_FrequencyYearPlural": "jaren", + "CalendarEventCompose_Location": "Locatie", + "CalendarEventCompose_LocationPlaceholder": "Voeg een locatie toe", + "CalendarEventCompose_NewEventButton": "Nieuw evenement", + "CalendarEventCompose_DefaultCalendarHint": "U kunt een standaardkalender kiezen voor nieuwe gebeurtenissen in de Kalenderinstellingen.", + "CalendarEventCompose_DefaultCalendarSettingsLink": "Kalenderinstellingen openen", + "CalendarEventCompose_NoCalendarsMessage": "Er zijn nog geen kalenders beschikbaar voor het aanmaken van een gebeurtenis.", + "CalendarEventCompose_NoCalendarsTitle": "Geen kalenders beschikbaar", + "CalendarEventCompose_NoEndDate": "Geen einddatum", + "CalendarEventCompose_Notes": "Notities", + "CalendarEventCompose_PickCalendarTitle": "Kies een kalender", + "CalendarEventCompose_Recurring": "Terugkerend", + "CalendarEventCompose_RecurringSummary": "Komt elke {0} {1}{2} {3} van kracht {4}{5}", + "CalendarEventCompose_RecurringSummarySmart": "Komt {0}{1} {2} van kracht {3}{4}", + "CalendarEventCompose_RepeatEvery": "Herhaal elke", + "CalendarEventCompose_SelectCalendar": "Kalender selecteren", + "CalendarEventCompose_SingleOccurrenceSummary": "Komt op {0} {1}", + "CalendarEventCompose_StartDate": "Startdatum", + "CalendarEventCompose_StartTime": "Starttijd", + "CalendarEventCompose_TimeRangeSummary": "van {0} tot {1}", + "CalendarEventCompose_Title": "Evenementtitel", + "CalendarEventCompose_TitlePlaceholder": "Voeg een titel toe", + "CalendarEventCompose_Until": "tot", + "CalendarEventCompose_UntilSummary": " tot {0}", + "CalendarEventCompose_ValidationInvalidAllDayRange": "De einddatum voor de hele dag moet na de startdatum liggen.", + "CalendarEventCompose_ValidationInvalidAttendee": "Een of meer genodigden hebben een ongeldig e-mailadres.", + "CalendarEventCompose_ValidationInvalidRecurrenceEnd": "De einddatum van de herhaling moet gelijk aan of groter dan de startdatum van het evenement.", + "CalendarEventCompose_ValidationInvalidTimeRange": "De eindtijd moet later zijn dan de starttijd.", + "CalendarEventCompose_ValidationMissingAttachment": "Een of meer bijlagen zijn niet langer beschikbaar: {0}", + "CalendarEventCompose_ValidationMissingCalendar": "Selecteer een kalender voordat u het evenement aanmaakt.", + "CalendarEventCompose_ValidationMissingTitle": "Voer een evenementtitel in voordat u het evenement aanmaakt.", + "CalendarEventCompose_ValidationTitle": "Evenementvalidatie mislukt", + "CalendarEventCompose_WeekdaySummary": " op {0}", + "CalendarEventCompose_Weekday_Friday": "V", + "CalendarEventCompose_Weekday_Monday": "M", + "CalendarEventCompose_Weekday_Saturday": "Za", + "CalendarEventCompose_Weekday_Sunday": "Zo", + "CalendarEventCompose_Weekday_Thursday": "Do", + "CalendarEventCompose_Weekday_Tuesday": "Di", + "CalendarEventCompose_Weekday_Wednesday": "Wo", + "CalendarEventDetails_Details": "Details", + "CalendarEventDetails_EditSeries": "Serie bewerken", + "CalendarEventDetails_Editing": "Bewerken", + "CalendarEventDetails_InviteSomeone": "Iemand uitnodigen", + "CalendarEventDetails_JoinOnline": "Online deelnemen", + "CalendarEventDetails_Organizer": "Organisator", + "CalendarEventDetails_People": "Personen", + "CalendarEventDetails_ReadOnlyEvent": "Alleen-lezen evenement", + "CalendarEventDetails_Reminder": "Herinnering", + "CalendarReminder_StartedHoursAgo": "{0} uur geleden gestart", + "CalendarReminder_StartedMinutesAgo": "{0} minuten geleden gestart", + "CalendarReminder_StartedNow": "Zojuist gestart", + "CalendarReminder_StartingNow": "Wordt nu gestart.", + "CalendarReminder_StartsInHours": "Start in {0} uur", + "CalendarReminder_StartsInMinutes": "Start in {0} minuten", + "CalendarReminder_SnoozeAction": "Uitstellen", + "CalendarReminder_SnoozeMinutesOption": "{0} minuten", + "CalendarEventDetails_ShowAs": "Weergeven als", + "CalendarShowAs_Free": "Vrij", + "CalendarShowAs_Tentative": "Voorlopig", + "CalendarShowAs_Busy": "Bezet", + "CalendarShowAs_OutOfOffice": "Buiten kantoor", + "CalendarShowAs_WorkingElsewhere": "Werkend op een andere locatie", "CalendarItem_DetailsPopup_JoinOnline": "Online aanmelden", "CalendarItem_DetailsPopup_ViewEventButton": "Gebeurtenis weergeven", "CalendarItem_DetailsPopup_ViewSeriesButton": "Series weergeven", @@ -88,6 +216,9 @@ "ClipboardTextCopied_Message": "{0} is naar het klembord gekopieerd.", "ClipboardTextCopied_Title": "Gekopieerd", "ClipboardTextCopyFailed_Message": "Kopiëren van {0} naar het klembord mislukt.", + "ContactInfoBar_ErrorTitle": "Laden van contactgegevens mislukt", + "ContactInfoBar_SuccessTitle": "Contactgegevens geladen", + "ContactInfoBar_WarningTitle": "Contactgegevens kunnen onvolledig zijn", "ComingSoon": "Binnenkort beschikbaar...", "ComposerAttachmentsDragDropAttach_Message": "Bijlage toevoegen", "ComposerAttachmentsDropZone_Message": "Sleep uw bestanden hier naartoe", @@ -129,6 +260,10 @@ "DialogMessage_CreateLinkedAccountTitle": "Naam van accountkoppeling", "DialogMessage_DeleteAccountConfirmationMessage": "{0} verwijderen?", "DialogMessage_DeleteAccountConfirmationTitle": "Alle gegevens die gekoppeld zijn aan dit account worden permanent verwijderd.", + "DialogMessage_DeleteEmailTemplateConfirmationMessage": "Sjabloon \"{0}\" verwijderen?", + "DialogMessage_DeleteEmailTemplateConfirmationTitle": "E-mail-sjabloon verwijderen", + "DialogMessage_DeleteRecurringSeriesMessage": "Dit verwijdert alle gebeurtenissen uit de serie. Wilt u doorgaan?", + "DialogMessage_DeleteRecurringSeriesTitle": "Doorlopende serie verwijderen", "DialogMessage_DiscardDraftConfirmationMessage": "Dit concept zal worden verwijderd. Wilt u doorgaan?", "DialogMessage_DiscardDraftConfirmationTitle": "Concept verwijderen", "DialogMessage_EmptySubjectConfirmation": "Onderwerp ontbreekt", @@ -172,11 +307,18 @@ "ElementTheme_Light": "Lichte modus", "Emoji": "Emoji", "Error_FailedToSetupSystemFolders_Title": "Instellen van systeemmappen mislukt", + "Exception_AccountNeedsAttention_Title": "Account vereist aandacht", + "Exception_AccountNeedsAttention_Message": "'{0}' vereist uw aandacht om verder te werken.", + "Exception_WebView2RuntimeMissing_Message": "Wino Mail kon de Microsoft Edge WebView2-runtime niet vinden. Installeer of herstel de runtime zodat de berichtinhoud correct wordt weergegeven.", + "Exception_WebView2RuntimeMissing_Title": "WebView2-runtime is vereist", "Exception_AuthenticationCanceled": "Authenticatie geannuleerd", "Exception_CustomThemeExists": "Dit thema bestaat al.", "Exception_CustomThemeMissingName": "U moet een naam invullen.", "Exception_CustomThemeMissingWallpaper": "U moet een aangepaste achtergrondafbeelding invoeren.", "Exception_FailedToSynchronizeAliases": "Synchroniseren van aliassen mislukt", + "Exception_FailedToSynchronizeCalendarData": "Kan kalendergegevens niet synchroniseren", + "Exception_FailedToSynchronizeCalendarEvents": "Kan kalendergebeurtenissen niet synchroniseren", + "Exception_FailedToSynchronizeCalendarMetadata": "Kan kalenderdetails niet synchroniseren", "Exception_FailedToSynchronizeFolders": "Synchroniseren van mappen mislukt", "Exception_FailedToSynchronizeProfileInformation": "Synchroniseren van profielinformatie mislukt", "Exception_GoogleAuthCallbackNull": "Callback uri is null bij het activeren.", @@ -229,6 +371,32 @@ "HoverActionOption_MoveJunk": "Verplaats naar Ongewenst", "HoverActionOption_ToggleFlag": "Markeren / Niet markeren", "HoverActionOption_ToggleRead": "Gelezen / Ongelezen", + "KeyboardShortcuts_FailedToReset": "Kan sneltoetsen niet terugzetten.", + "KeyboardShortcuts_FailedToUpdate": "Kan sneltoetsen niet bijwerken", + "KeyboardShortcuts_MailoperationAction": "Actie", + "KeyboardShortcuts_Action": "Actie", + "KeyboardShortcuts_FailedToLoad": "Kan sneltoetsen niet laden.", + "KeyboardShortcuts_EnterKeyForShortcut": "Voer alstublieft een toets in voor de sneltoets.", + "KeyboardShortcuts_SelectOperationForShortcut": "Kies alstublieft een actie voor de sneltoets.", + "KeyboardShortcuts_EnterKey": "Voer alstublieft een toets in voor de sneltoets.", + "KeyboardShortcuts_SelectOperation": "Selecteer alstublieft een actie voor de sneltoets.", + "KeyboardShortcuts_ShortcutInUse": "Deze snelkoppeling wordt al gebruikt door een andere snelkoppeling.", + "KeyboardShortcuts_FailedToSave": "Kan de sneltoets niet opslaan.", + "KeyboardShortcuts_FailedToDelete": "Kan de sneltoets niet verwijderen.", + "KeyboardShortcuts_PageDescription": "Stel sneltoetsen in voor snelle e-mailbewerkingen. Druk op toetsen terwijl het invoerveld voor de toets ingedrukt is om sneltoetsen vast te leggen.", + "KeyboardShortcuts_Add": "Sneltoets toevoegen", + "KeyboardShortcuts_EditTitle": "Sneltoets bewerken", + "KeyboardShortcuts_ResetToDefaults": "Reset naar standaardwaarden", + "KeyboardShortcuts_PressKeysHere": "Druk hier op toetsen...", + "KeyboardShortcuts_KeyCombination": "Toetscombinatie", + "KeyboardShortcuts_FocusArea": "Zet de focus op het bovenstaande veld en druk op de gewenste toetsencombinatie", + "KeyboardShortcuts_Modifiers": "Modifiertoetsen", + "KeyboardShortcuts_Mode": "App-modus", + "KeyboardShortcuts_ModeMail": "E-mail", + "KeyboardShortcuts_ModeCalendar": "Agenda", + "KeyboardShortcuts_ActionToggleReadUnread": "Gelezen/ongelezen wisselen", + "KeyboardShortcuts_ActionToggleFlag": "Vlag schakelen", + "KeyboardShortcuts_ActionToggleArchive": "Archief aan/uitzetten", "ImageRenderingDisabled": "Afbeeldingsweergave is voor dit bericht uitgeschakeld.", "ImapAdvancedSetupDialog_AuthenticationMethod": "Authenticatiemethode", "ImapAdvancedSetupDialog_ConnectionSecurity": "Beveiliging van verbinding", @@ -262,8 +430,8 @@ "IMAPSetupDialog_AccountType": "Accounttype", "IMAPSetupDialog_ValidationSuccess_Title": "Voltooid", "IMAPSetupDialog_ValidationSuccess_Message": "Validatie voltooid", - "IMAPSetupDialog_SaveImapSuccess_Title": "Voltooid", - "IMAPSetupDialog_SaveImapSuccess_Message": "IMAP-instellingen zijn succesvol opgeslagen.", + "IMAPSetupDialog_SaveImapSuccess_Title": "Success", + "IMAPSetupDialog_SaveImapSuccess_Message": "IMAP settings saved successfuly.", "IMAPSetupDialog_ValidationFailed_Title": "Validatie van IMAP-server is mislukt.", "IMAPSetupDialog_CertificateAllowanceRequired_Row0": "Deze server vraagt naar een SSL-verificatie om door te kunnen gaan. Bevestig de certificaatdetails hieronder.", "IMAPSetupDialog_CertificateAllowanceRequired_Row1": "Verificatie toestaan om door te gaan met het instellen van uw account.", @@ -295,12 +463,58 @@ "IMAPSetupDialog_Username": "Gebruikersnaam", "IMAPSetupDialog_UsernamePlaceholder": "jansmit, jansmit@fabrikam.com, domein/jansmit", "IMAPSetupDialog_UseSameConfig": "Dezelfde gebruikersnaam en wachtwoord gebruiken voor het verzenden van e-mail", + "ImapCalDavSettingsPage_TitleCreate": "IMAP- en Kalenderinstellingen", + "ImapCalDavSettingsPage_TitleEdit": "IMAP- en Kalenderinstellingen bewerken", + "ImapCalDavSettingsPage_Subtitle": "IMAP/SMTP en optionele kalender-synchronisatie voor dit account configureren.", + "ImapCalDavSettingsPage_BasicSectionTitle": "Basisinstellingen", + "ImapCalDavSettingsPage_BasicSectionDescription": "Voer uw identiteit en inloggegevens in. Wino kan proberen serverinstellingen automatisch te detecteren.", + "ImapCalDavSettingsPage_BasicTab": "Basis", + "ImapCalDavSettingsPage_EnableCalendarSupport": "Kalenderondersteuning inschakelen", + "ImapCalDavSettingsPage_AutoDiscoverButton": "Mailinstellingen automatisch ontdekken", + "ImapCalDavSettingsPage_AutoDiscoverySuccessMessage": "Mailinstellingen gevonden en toegepast.", + "ImapCalDavSettingsPage_AdvancedSectionTitle": "Geavanceerde configuratie", + "ImapCalDavSettingsPage_AdvancedSectionDescription": "Voer handmatig serverinstellingen in als autodiscovery niet beschikbaar of onjuist is.", + "ImapCalDavSettingsPage_AdvancedTab": "Geavanceerd", + "ImapCalDavSettingsPage_CalendarSectionTitle": "Kalenderconfiguratie", + "ImapCalDavSettingsPage_CalendarSectionDescription": "Kies hoe kalendergegevens moeten werken voor dit IMAP-account.", + "ImapCalDavSettingsPage_CalendarModeHeader": "Kalendermodus", + "ImapCalDavSettingsPage_ConnectionSecurityHeader": "Verbindingsbeveiliging", + "ImapCalDavSettingsPage_AuthenticationMethodHeader": "Authenticatiemethode", + "ImapCalDavSettingsPage_CalendarModeDisabled": "Uitgeschakeld", + "ImapCalDavSettingsPage_CalendarModeCalDav": "CalDAV-synchronisatie", + "ImapCalDavSettingsPage_CalendarModeLocalOnly": "Alleen lokale kalender", + "ImapCalDavSettingsPage_CalendarModeDisabledDescription": "Kalender is uitgeschakeld voor dit account.", + "ImapCalDavSettingsPage_CalendarModeCalDavDescription": "Kalenderitems worden gesynchroniseerd met uw CalDAV-server.", + "ImapCalDavSettingsPage_CalendarModeLocalOnlyDescription": "Kalenderitems worden alleen op deze computer opgeslagen en worden niet gesynchroniseerd met het netwerk.", + "ImapCalDavSettingsPage_LocalCalendarLearnMore": "Hoe de lokale kalender werkt", + "ImapCalDavSettingsPage_LocalCalendarDialogTitle": "Alleen lokale kalender", + "ImapCalDavSettingsPage_LocalCalendarDialogMessage": "De lokale kalender bewaart alle gebeurtenissen uitsluitend op uw computer. Niets wordt gesynchroniseerd met iCloud, Yahoo of een andere provider.", + "ImapCalDavSettingsPage_CalDavServiceUrl": "CalDAV-service-URL", + "ImapCalDavSettingsPage_CalDavUsername": "CalDAV-gebruikersnaam", + "ImapCalDavSettingsPage_CalDavPassword": "CalDAV-wachtwoord", + "ImapCalDavSettingsPage_CalDavNotRequiredMessage": "CalDAV-test is alleen vereist wanneer Kalendermodus is ingesteld op CalDAV-synchronisatie.", + "ImapCalDavSettingsPage_CalDavUrlRequired": "CalDAV-service-URL is vereist.", + "ImapCalDavSettingsPage_CalDavUrlInvalid": "CalDAV-service-URL moet een absolute URL zijn.", + "ImapCalDavSettingsPage_CalDavUsernameRequired": "CalDAV-gebruikersnaam is vereist.", + "ImapCalDavSettingsPage_CalDavPasswordRequired": "CalDAV-wachtwoord is vereist.", + "ImapCalDavSettingsPage_TestImapButton": "IMAP-verbinding testen", + "ImapCalDavSettingsPage_TestCalDavButton": "CalDAV-verbinding testen", + "ImapCalDavSettingsPage_ImapTestSuccessMessage": "IMAP-verbinding is geslaagd.", + "ImapCalDavSettingsPage_CalDavTestSuccessMessage": "CalDAV-verbinding is geslaagd.", + "ImapCalDavSettingsPage_SaveSuccessMessage": "Accountinstellingen gevalideerd en opgeslagen.", + "ImapCalDavSettingsPage_ICloudHint": "Gebruik een app-wachtwoord dat is gegenereerd via uw Apple-accountinstellingen.", + "ImapCalDavSettingsPage_YahooHint": "Gebruik een app-wachtwoord uit de beveiligingsinstellingen van uw Yahoo-account.", "Info_AccountCreatedMessage": "{0} is aangemaakt", "Info_AccountCreatedTitle": "Account aanmaken", "Info_AccountCreationFailedTitle": "Aanmaken van account is mislukt", "Info_AccountDeletedMessage": "{0} is succesvol verwijderd.", "Info_AccountDeletedTitle": "Account verwijderd", "Info_AccountIssueFixFailedTitle": "Mislukt", + "Info_AccountIssueFixImapMessage": "Open de IMAP- en kalenderinstellingenpagina om uw serverinloggegevens opnieuw in te voeren.", + "Info_AccountAttentionRequiredMessage": "Deze account heeft uw aandacht nodig.", + "Info_AccountAttentionRequiredClickableMessage": "Klik om dit account te herstellen en opnieuw te synchroniseren.", + "Info_AccountAttentionRequiredAction": "Oplossen", + "Info_AccountAttentionRequiredActionHint": "Klik op Oplossen om dit accountprobleem op te lossen.", "Info_AccountIssueFixSuccessMessage": "Alle accountproblemen zijn opgelost.", "Info_AccountIssueFixSuccessTitle": "Gelukt", "Info_AttachmentOpenFailedMessage": "Deze bijlage kan niet geopend worden.", @@ -354,9 +568,9 @@ "Info_ReviewUnknownErrorTitle": "Onbekende fout", "Info_ReviewUpdatedMessage": "Hartelijk dank voor de bijgewerkte beoordeling.", "Info_SignatureDisabledMessage": "Handtekening voor dit account uitschakelen", - "Info_SignatureDisabledTitle": "Voltooid", + "Info_SignatureDisabledTitle": "Gelukt", "Info_SignatureSavedMessage": "Nieuwe handtekening is opgeslagen", - "Info_SignatureSavedTitle": "Voltooid", + "Info_SignatureSavedTitle": "Gelukt", "Info_SyncCanceledMessage": "Geannuleerd", "Info_SyncCanceledTitle": "Synchronisatie", "Info_SyncFailedTitle": "Synchronisatie is mislukt", @@ -370,6 +584,7 @@ "InfoBarMessage_SynchronizationDisabledFolder": "Synchronisatie is uitgeschakeld voor deze map.", "InfoBarTitle_SynchronizationDisabledFolder": "Uitgeschakelde map", "Justify": "Uitvullen", + "MenuUpdateAvailable": "Update beschikbaar", "Left": "Links", "Link": "Link", "LinkedAccountsCreatePolicyMessage": "U moet tenminste 2 accounts hebben om een koppeling te maken.\nDe koppeling zal verwijderd worden bij het opslaan.", @@ -403,6 +618,7 @@ "MailOperation_Unarchive": "Niet meer archiveren", "MailOperation_ViewMessageSource": "Berichtbron weergeven", "MailOperation_Zoom": "Zoom", + "MailsDragging": "Bezig met het verslepen van {0} item(s)", "MailsSelected": "{0} item(s) geselecteerd", "MarkFlagUnflag": "Vlag toevoegen/verwijderen", "MarkReadUnread": "Markeren als gelezen/ongelezen", @@ -434,6 +650,8 @@ "Notifications_MultipleNotificationsTitle": "Nieuwe berichten", "Notifications_WinoUpdatedMessage": "Bekijk de nieuwe versie {0}", "Notifications_WinoUpdatedTitle": "Wino-Mail is bijgewerkt.", + "Notifications_StoreUpdateAvailableTitle": "Update beschikbaar", + "Notifications_StoreUpdateAvailableMessage": "Een nieuwere versie van Wino Mail is klaar om te installeren vanaf de Microsoft Store.", "OnlineSearchFailed_Message": "{0} zoeken is mislukt\n\nOffline e-mails worden weergegeven.", "OnlineSearchTry_Line1": "Kunt u niet vinden wat u zoekt?", "OnlineSearchTry_Line2": "Probeer online zoeken.", @@ -446,7 +664,6 @@ "PaneLengthOption_Small": "Klein", "Photos": "Foto’s", "PreparingFoldersMessage": "Mappen voorbereiden", - "ProtocolLogAvailable_Message": "Protocol logs zijn beschikbaar voor diagnose.", "ProviderDetail_Gmail_Description": "Google Account", "ProviderDetail_iCloud_Description": "Apple iCloud Account", "ProviderDetail_iCloud_Title": "iCloud", @@ -465,9 +682,14 @@ "SearchBarPlaceholder": "Zoeken", "SearchingIn": "Zoeken in", "SearchPivotName": "Resultaten", + "Settings_KeyboardShortcuts_Title": "Sneltoetsen", + "Settings_KeyboardShortcuts_Description": "Beheer sneltoetsen voor snelle acties in de e-mails.", "SettingConfigureSpecialFolders_Button": "Configureer", "SettingsEditAccountDetails_IMAPConfiguration_Title": "IMAP/SMPT Configuratie", "SettingsEditAccountDetails_IMAPConfiguration_Description": "Wijzig uw inkomende/uitgaande serverinstellingen.", + "SettingsEditAccountDetails_ImapCalDavSettings_Title": "IMAP- en kalenderinstellingen", + "SettingsEditAccountDetails_ImapCalDavSettings_Description": "Open de speciale IMAP-, SMTP- en CalDAV-instellingenpagina voor dit account.", + "SettingsEditAccountDetails_ImapCalDavSettings_Action": "Instellingen openen", "SettingsAbout_Description": "Meer informatie over Wino.", "SettingsAbout_Title": "Over", "SettingsAboutGithub_Description": "Ga naar de Issue Tracker in de GitHub-repository.", @@ -490,6 +712,10 @@ "SettingsAppPreferences_SearchMode_Local": "Lokaal", "SettingsAppPreferences_SearchMode_Online": "Online", "SettingsAppPreferences_SearchMode_Title": "Standaard zoekmethode", + "SettingsAppPreferences_ApplicationMode_Title": "Standaardtoepassingsmodus", + "SettingsAppPreferences_ApplicationMode_Description": "Kies in welke modus Wino opent wanneer geen activatiemodus expliciet is ingesteld.", + "SettingsAppPreferences_ApplicationMode_Mail": "E-mail", + "SettingsAppPreferences_ApplicationMode_Calendar": "Kalender", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description": "Wino Mail zal op de achtergrond blijven draaien. U wordt op de hoogte gebracht wanneer er nieuwe e-mails binnenkomen.", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title": "Op de achtergrond uitvoeren", "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description": "Wino Mail zal in het systeemvak blijven draaien en laden als u op het icoon klikt. U wordt op de hoogte gebracht wanneer er nieuwe e-mails binnenkomen.", @@ -506,12 +732,30 @@ "SettingsAppPreferences_StartupBehavior_FatalError": "Fatale fout tijdens het wijzigen van de opstartmethode voor Wino Mail.", "SettingsAppPreferences_StartupBehavior_Title": "Geminimaliseerd starten bij opstarten van Windows", "SettingsAppPreferences_Title": "App voorkeuren", + "SettingsAppPreferences_HideWinoAccountButton_Title": "Verberg Wino-accountknop in de titelbalk.", + "SettingsAppPreferences_HideWinoAccountButton_Description": "Verberg de profielknop in de titelbalk die het Wino-account-uitklapmenu opent.", + "SettingsAppPreferences_StoreUpdateNotifications_Title": "Store-updatemeldingen", + "SettingsAppPreferences_StoreUpdateNotifications_Description": "Toon meldingen en footer-acties wanneer er een Microsoft Store-update beschikbaar is.", + "SettingsAppPreferences_AiActions_Title": "AI-acties", + "SettingsAppPreferences_AiActions_Description": "Kies de standaard AI-talen en waar samenvattingen moeten worden opgeslagen.", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Title": "Standaard doeltaal", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Description": "Kies de standaard doeltaal die door AI-vertalingacties wordt gebruikt.", + "SettingsAppPreferences_AiSummarizeLanguage_Title": "Samenvatteltaal", + "SettingsAppPreferences_AiSummarizeLanguage_Description": "Kies de gewenste samenvatteltaal voor toekomstige AI-samenvattingen.", + "SettingsAppPreferences_AiSummarySavePath_Title": "Standaard opslagpad voor samenvattingen", + "SettingsAppPreferences_AiSummarySavePath_Description": "Kies de map die Wino standaard moet gebruiken bij het opslaan van AI-samenvattingen.", + "SettingsAppPreferences_AiSummarySavePath_Placeholder": "Gebruik de standaardlocatie van het systeem.", + "SettingsAppPreferences_AiSummarySavePath_InvalidHint": "Deze map bestaat niet. De standaardopslaglocatie wordt gebruikt voor samenvattingen.", "SettingsAutoSelectNextItem_Description": "Selecteer het volgende item nadat u een e-mail hebt verwijderd of verplaatst.", "SettingsAutoSelectNextItem_Title": "Volgende item automatisch selecteren", "SettingsAvailableThemes_Description": "Selecteer een thema uit Wino’s collectie of pas uw eigen thema's toe.", "SettingsAvailableThemes_Title": "Beschikbare thema's", "SettingsCalendarSettings_Description": "Wijzig de eerste weekdag, vakhoogte voor uren en meer...", "SettingsCalendarSettings_Title": "Agenda instellingen", + "CalendarSettings_DefaultSnoozeDuration_Header": "Standaard snooze-duur", + "CalendarSettings_DefaultSnoozeDuration_Description": "Stel een standaard snooze-duur in voor kalenderherinneringsmeldingen.", + "CalendarSettings_TimedDayHeaderFormat_Header": "Formaat van de dagkop in de tijdweergave", + "CalendarSettings_TimedDayHeaderFormat_Description": "Kies hoe de bovenste daglabels worden weergegeven in dag-, week- en werkweekweergaven. Gebruik datumopmaaktekens zoals ddd, dd, MMM of dddd.", "SettingsComposer_Title": "Opsteller", "SettingsComposerFont_Title": "Standaard lettertype voor opstellen", "SettingsComposerFontFamily_Description": "Wijzig de standaard lettertype en lettergrootte voor het opstellen van e-mails.", @@ -531,6 +775,9 @@ "SettingsDiscord_Title": "Discord-kanaal", "SettingsEditLinkedInbox_Description": "Accounts toevoegen, verwijderen, hernoemen of de koppeling tussen accounts verbreken.", "SettingsEditLinkedInbox_Title": "Gekoppelde Inbox bewerken", + "SettingsWindowBackdrop_Title": "Vensterachtergrond", + "SettingsWindowBackdrop_Description": "Kies een achtergrondeffect voor Wino-vensters.", + "SettingsWindowBackdrop_Disabled": "Het kiezen van de vensterachtergrond is uitgeschakeld wanneer een applicatiethema anders dan Standaard is geselecteerd.", "SettingsElementTheme_Description": "Selecteer een Windows-thema voor Wino", "SettingsElementTheme_Title": "Thema modus", "SettingsElementThemeSelectionDisabled": "Thema modus selectie is uitgeschakeld wanneer een niet-standaard thema is geselecteerd.", @@ -581,6 +828,8 @@ "SettingsManageAliases_Title": "Aliassen", "SettingsEditAccountDetails_Title": "Bewerk accountdetails", "SettingsEditAccountDetails_Description": "Wijzig accountnaam, naam van afzender en wijs een kleur toe indien gewenst.", + "EditAccountDetailsPage_SaveSuccess_Title": "Wijzigingen opgeslagen", + "EditAccountDetailsPage_SaveSuccess_Message": "Uw accountgegevens zijn succesvol bijgewerkt.", "SettingsManageLink_Description": "Verplaats items naar een nieuwe koppeling of verwijder een bestaande koppeling.", "SettingsManageLink_Title": "Koppeling beheren", "SettingsMarkAsRead_Description": "Wijzig wat er met geselecteerde items moet gebeuren.", @@ -596,7 +845,41 @@ "SettingsNotifications_Title": "Meldingen", "SettingsNotificationsAndTaskbar_Description": "Bepaal of meldingen en de taakbalk badge weergegeven moeten worden voor dit account.", "SettingsNotificationsAndTaskbar_Title": "Meldingen en taakbalk", + "SettingsHome_Title": "Start", + "SettingsHome_SearchTitle": "Zoek een instelling", + "SettingsHome_SearchDescription": "Zoek op functie, onderwerp of trefwoord om direct naar de juiste instellingenpagina te springen.", + "SettingsHome_SearchPlaceholder": "Zoek instellingen", + "SettingsHome_SearchExamples": "Probeer: thema, opslag, taal, handtekening", + "SettingsHome_QuickLinks_Title": "Snelle links", + "SettingsHome_QuickLinks_Description": "Ga rechtstreeks naar de instellingen die het vaakst worden gebruikt.", + "SettingsHome_StorageCard_Description": "Bekijk hoeveel lokale MIME-inhoud Wino op dit apparaat bewaart en ruim het op indien nodig.", + "SettingsHome_StorageEmptySummary": "Nog geen gecacheerde MIME-inhoud gedetecteerd.", + "SettingsHome_StorageLoading": "Lokaal MIME-gebruik wordt gecontroleerd...", + "SettingsHome_Tips_Title": "Tips en trucs", + "SettingsHome_Tips_Description": "Een paar kleine wijzigingen kunnen Wino veel persoonlijker laten aanvoelen.", + "SettingsHome_Tip_Theme": "Wil je de donkere modus of accentkleuren wijzigen? Open Personalisatie.", + "SettingsHome_Tip_Background": "Gebruik App-voorkeuren om het opstartgedrag en de achtergrondsynchronisatie te regelen.", + "SettingsHome_Tip_Shortcuts": "Sneltoetsen helpen je sneller door e-mails te navigeren.", + "SettingsHome_Resources_Title": "Handige links", + "SettingsHome_Resources_Description": "Open projectbronnen, ondersteuningsinformatie en releasekanalen.", "SettingsOptions_Title": "Instellingen", + "SettingsOptions_GeneralSection": "Algemeen", + "SettingsOptions_MailSection": "E-mail", + "SettingsOptions_CalendarSection": "Kalender", + "SettingsOptions_MoreComingSoon": "Meer opties komen binnenkort beschikbaar.", + "SettingsOptions_HeroDescription": "Pas uw Wino Mail-ervaring aan.", + "SettingsOptions_AccountsSummary": "{0} account(en) geconfigureerd", + "SettingsSearch_ManageAccounts_Keywords": "account;accounts;postvak;postvakken;alias;aliassen;profiel;adres;adressen", + "SettingsSearch_AppPreferences_Keywords": "opstart;achtergrond;start;synchronisatie;melding;meldingen;zoeken;systeemvak;standaardinstellingen", + "SettingsSearch_LanguageTime_Keywords": "taal;tijd;klok;locale;regio;formaat;24-uurs;24u", + "SettingsSearch_Personalization_Keywords": "thema;donker;licht;uiterlijk;accent;kleur;kleur;modus;indeling;dichtheid", + "SettingsSearch_About_Keywords": "over;versie;website;privacy;github;doneren;winkel;ondersteuning", + "SettingsSearch_KeyboardShortcuts_Keywords": "toets;toetsen;hotkey;hotkeys;toetsenbord;sleutels", + "SettingsSearch_MessageList_Keywords": "bericht;berichten;lijst;gesprekken;conversaties;avatar;voorbeeld;afzender", + "SettingsSearch_ReadComposePane_Keywords": "lezer;opstellen;opsteller;lettertype;lettertypen;externe inhoud;weergave;lezen", + "SettingsSearch_SignatureAndEncryption_Keywords": "handtekening;handtekeningen;versleuteling;certificaat;certificaten;smime;beveiliging", + "SettingsSearch_Storage_Keywords": "opslag;cache;caching;mime;schijfruimte;ruimte;opruimen;opschonen;lokale gegevens", + "SettingsSearch_CalendarSettings_Keywords": "kalender;week;uren;planning;evenement;evenementen", "SettingsPaneLengthReset_Description": "Reset de grootte van de e-maillijst naar de originele staat als u problemen hiermee ondervindt.", "SettingsPaneLengthReset_Title": "Grootte van e-maillijst herstellen", "SettingsPaypal_Description": "Toon veel meer liefde ❤️ Alle donaties worden gewaardeerd.", @@ -610,6 +893,8 @@ "SettingsPrefer24HourClock_Title": "Tijd in 24-uursnotatie weergeven", "SettingsPrivacyPolicy_Description": "Privacybeleid bekijken", "SettingsPrivacyPolicy_Title": "Privacybeleid", + "SettingsWebsite_Description": "Open de Wino Mail-website.", + "SettingsWebsite_Title": "Website", "SettingsReadComposePane_Description": "Lettertypen, externe inhoud.", "SettingsReadComposePane_Title": "Lezer & Opsteller", "SettingsReader_Title": "Lezer", @@ -625,6 +910,19 @@ "SettingsShowPreviewText_Title": "Voorbeeldtekst weergeven", "SettingsShowSenderPictures_Description": "Verberg/toon miniatuurafbeeldingen van afzender.", "SettingsShowSenderPictures_Title": "Avatars van afzenders tonen", + "SettingsEmailTemplates_Title": "E-mailtemplates", + "SettingsEmailTemplates_Description": "Beheer e-mailtemplates", + "SettingsEmailTemplates_CreatePageTitle": "Nieuw sjabloon", + "SettingsEmailTemplates_EditPageTitle": "Sjabloon bewerken", + "SettingsEmailTemplates_NewTemplateTitle": "Nieuw sjabloon", + "SettingsEmailTemplates_NewTemplateDescription": "Maak een nieuw e-mailsjabloon.", + "SettingsEmailTemplates_NameTitle": "Naam", + "SettingsEmailTemplates_NamePlaceholder": "Naam van het sjabloon", + "SettingsEmailTemplates_DescriptionTitle": "Beschrijving", + "SettingsEmailTemplates_DescriptionPlaceholder": "Optionele beschrijving", + "SettingsEmailTemplates_ContentTitle": "Sjablooninhoud", + "SettingsEmailTemplates_ContentDescription": "Bewerk de HTML-inhoud voor dit sjabloon.", + "SettingsEmailTemplates_NameRequired": "Naam van sjabloon is vereist.", "SettingsEnableGravatarAvatars_Title": "Gravatar", "SettingsEnableGravatarAvatars_Description": "Gebruik gravatar als foto voor afzender (indien beschikbaar)", "SettingsEnableFavicons_Title": "Domein iconen (Favicons)", @@ -645,6 +943,33 @@ "SettingsStartupItem_Title": "Opstart account", "SettingsStore_Description": "Toon wat liefde ❤️", "SettingsStore_Title": "Beoordeel in Store", + "SettingsStorage_Title": "Opslag", + "SettingsStorage_Description": "Scan en beheer MIME-cache die is opgeslagen in uw lokale gegevensmap.", + "SettingsStorage_ScanFolder": "Scan lokale gegevensmap", + "SettingsStorage_NoLocalMimeDataFound": "Geen lokale MIME-gegevens gevonden.", + "SettingsStorage_NoAccountsFound": "Geen accounts gevonden.", + "SettingsStorage_TotalUsage": "Totaal lokaal MIME-gebruik: {0}", + "SettingsStorage_AccountUsageDescription": "{0} gebruikt in de lokale MIME-cache", + "SettingsStorage_DeleteAll_Title": "Verwijder alle MIME-inhoud", + "SettingsStorage_DeleteAll_Description": "Verwijder de hele MIME-cache-map van dit account.", + "SettingsStorage_DeleteAll_Button": "Alle verwijderen", + "SettingsStorage_DeleteAll_Confirm_Title": "Alle MIME-inhoud verwijderen", + "SettingsStorage_DeleteAll_Confirm_Message": "Verwijder alle lokale MIME-gegevens voor {0}?", + "SettingsStorage_DeleteAll_Success": "Alle MIME-inhoud is verwijderd.", + "SettingsStorage_DeleteOld_Title": "Verwijder oud MIME-inhoud", + "SettingsStorage_DeleteOld_Description": "Verwijder MIME-bestanden op basis van de aanmaakdatum van e-mail in de lokale database.", + "SettingsStorage_DeleteOld_1Month": "> 1 maand", + "SettingsStorage_DeleteOld_3Months": "> 3 maanden", + "SettingsStorage_DeleteOld_6Months": "> 6 maanden", + "SettingsStorage_DeleteOld_1Year": "> 1 jaar", + "SettingsStorage_DeleteOld_Confirm_Title": "Verwijder oud MIME-inhoud", + "SettingsStorage_DeleteOld_Confirm_Message": "Verwijder lokale MIME-gegevens ouder dan {0} voor {1}?", + "SettingsStorage_DeleteOld_Success": "Verwijderd {0} MIME-map(pen) ouder dan {1}.", + "SettingsStorage_1Month": "1 maand", + "SettingsStorage_3Months": "3 maanden", + "SettingsStorage_6Months": "6 maanden", + "SettingsStorage_1Year": "1 jaar", + "SettingsStorage_Months": "{0} maanden", "SettingsTaskbarBadge_Description": "Includeer aantal ongelezen e-mails in taakbalkpictogram.", "SettingsTaskbarBadge_Title": "Taakbalk badge", "SettingsThreads_Description": "Voeg berichten samen tot gesprekken.", @@ -683,6 +1008,9 @@ "SystemFolderConfigDialogValidation_InboxSelected": "U kunt de map Inbox niet aan een andere systeemmap toewijzen.", "SystemFolderConfigSetupSuccess_Message": "Systeemmappen zijn succesvol geconfigureerd.", "SystemFolderConfigSetupSuccess_Title": "Systeemmappen instellen", + "SystemTrayMenu_ShowWino": "Wino Mail openen", + "SystemTrayMenu_ShowWinoCalendar": "Wino Calendar openen", + "SystemTrayMenu_ExitWino": "Afsluiten", "TestingImapConnectionMessage": "Bezig met testen van serververbinding...", "TitleBarServerDisconnectedButton_Description": "Wino is van de netwerkverbinding verbroken. Klik op opnieuw verbinden om de verbinding te herstellen.", "TitleBarServerDisconnectedButton_Title": "Geen verbinding", @@ -699,8 +1027,422 @@ "WinoUpgradeMessage": "Upgraden naar Onbeperkte aantal accounts", "WinoUpgradeRemainingAccountsMessage": "{0} van de {1} gratis accounts gebruikt.", "Yesterday": "Gisteren", - "SettingsAppPreferences_EmailSyncInterval_Title": "E-mail synchronisatie-interval", - "SettingsAppPreferences_EmailSyncInterval_Description": "Automatische e-mail synchronisatie-interval (minuten). Deze instelling zal alleen toegepast worden na herstarten van Wino Mail." + "Smime_ImportCertificates_Success": "Certificaten succesvol geïmporteerd.", + "Smime_ImportCertificates_Error": "Fout bij het importeren van certificaten: {0}", + "Smime_RemoveCertificates_Confirm": "Wilt u werkelijk de certificaten {0} verwijderen?", + "Smime_RemoveCertificates_Success": "Certificaten verwijderd.", + "Smime_ExportCertificates_Success": "Certificaten geëxporteerd.", + "Smime_ExportCertificates_Error": "Fout bij het exporteren van certificaten.", + "Smime_CertificateDetails": "Onderwerp: {0}\\nUitgevende instantie: {1}\\nGeldig vanaf: {2}\\nGeldig tot: {3}\\nVingerafdruk: {4}", + "Smime_CertificatePassword_Title": "Certificaatwachtwoord vereist", + "Smime_CertificatePassword_Placeholder": "Certificaatwachtwoord voor {0} (optioneel)", + "Smime_Confirm_Title": "Bevestigen", + "Buttons_OK": "OK", + "Buttons_Refresh": "Vernieuwen", + "SettingsSignatureAndEncryption_Title": "Handtekening en Versleuteling", + "SettingsSignatureAndEncryption_Description": "Beheer S/MIME-certificaten voor het ondertekenen en versleutelen van e-mails.", + "SettingsSignatureAndEncryption_MyCertificatesHeader": "Mijn certificaten", + "SettingsSignatureAndEncryption_MyCertificatesDescription": "Persoonlijke certificaten voor ondertekenen en versleutelen", + "SettingsSignatureAndEncryption_RecipientCertificatesHeader": "Certificaten van ontvangers", + "SettingsSignatureAndEncryption_RecipientCertificatesDescription": "Certificaten van ontvangers voor ontcijfering", + "SettingsSignatureAndEncryption_NameColumn": "Naam", + "SettingsSignatureAndEncryption_ExpiresColumn": "Verloopt op", + "SettingsSignatureAndEncryption_ThumbprintColumn": "Vingerafdruk", + "Buttons_Remove": "Verwijderen", + "Buttons_Export": "Exporteer", + "Buttons_Import": "Importeren", + "SettingsSignatureAndEncryption_SigningCertificate": "S/MIME-handtekeningcertificaat", + "SettingsSignatureAndEncryption_EncryptionCertificate": "S/MIME-encryptiecertificaat", + "SettingsSignatureAndEncryption_SigningCertificatePlaceholder": "Geen", + "SmimeSignaturesInMessage": "Handtekeningen in dit bericht:", + "SmimeSignatureEntry": "• {0} {1} ({2}, geldig tot {3} - {4})", + "SmimeSigningCertificateInfoTitle": "Informatie over S/MIME-handtekeningcertificaat", + "SmimeCertificateInfoTitle": "Informatie over S/MIME-certificaat", + "SmimeNoCertificateFileFound": "Geen certificaatbestand gevonden", + "SmimeSaveCertificate": "Bewaar certificaat...", + "SmimeCertificate": "S/MIME-certificaat", + "SmimeCertificateSavedTo": "Certificaat opgeslagen naar {0}", + "SmimeSignedTooltip": "Dit bericht is ondertekend met een S/MIME-certificaat. Klik voor meer details", + "SmimeEncryptedTooltip": "Dit bericht is versleuteld met een S/MIME-certificaat.", + "SmimeCertificateFileInfo": "Bestand: {0}\\nType: {1}\\nGrootte: {2:N0} bytes", + "Composer_LightTheme": "Licht Thema", + "Composer_DarkTheme": "Donker Thema", + "Composer_Outdent": "Uitinspringen", + "Composer_Indent": "Inspringen", + "Composer_BulletList": "Opsommingstekens", + "Composer_OrderedList": "Genummerde lijst", + "Composer_Stroke": "Lijn", + "Composer_Bold": "Vet", + "Composer_Italic": "Cursief", + "Composer_Underline": "Onderstrepen", + "Composer_CcBcc": "Cc en Bcc", + "Composer_EnableSmimeSignature": "S/MIME-handtekening in- of uitschakelen", + "Composer_EnableSmimeEncryption": "S/MIME-versleuteling in- of uitschakelen", + "Composer_LocalDraftSyncInfo": "Dit concept is lokaal. Wino kon het niet naar uw mailserver verzenden. Klik om het opnieuw te verzenden.", + "Composer_CertificateExpires": "Verloopt op: ", + "Composer_SmimeSignature": "S/MIME-handtekening", + "Composer_SmimeEncryption": "S/MIME-versleuteling", + "Composer_EmailTemplatesPlaceholder": "E-mail-sjablonen", + "Composer_AiSummarize": "Samenvatten met AI", + "Composer_AiSummarizeDescription": "Haal belangrijke punten, actiepunten en beslissingen uit deze e-mail.", + "Composer_AiTranslate": "Vertaal met AI", + "Composer_AiActions": "AI-acties", + "Composer_AiRewrite": "Herschrijven met AI", + "AiActions_CheckingStatus": "AI-toegang controleren...", + "AiActions_SignedOutTitle": "Wino AI-pack ontgrendelen", + "AiActions_SignedOutDescription": "Vertaal, herschrijf en vat e-mails samen met AI nadat u bent ingelogd op uw Wino-account en de AI Pack-add-on hebt geactiveerd.", + "AiActions_NoPackTitle": "AI Pack vereist", + "AiActions_NoPackDescription": "U bent ingelogd, maar AI Pack is nog niet actief. Koop het om de AI-vertaling, herschrijf- en samenvattingshulpmiddelen van Wino te gebruiken.", + "AiActions_UsageSummary": "{0} van {1} credits deze maand gebruikt.", + "Composer_AiRewritePolite": "Maak het beleefder", + "Composer_AiRewritePoliteDescription": "Verzacht de formulering terwijl de bedoeling hetzelfde blijft.", + "Composer_AiRewriteAngry": "Maak het boos", + "Composer_AiRewriteAngryDescription": "Gebruikt een scherpere en confronterende toon.", + "Composer_AiRewriteHappy": "Maak het vrolijk", + "Composer_AiRewriteHappyDescription": "Voegt een vrolijkere en enthousiastere toon toe.", + "Composer_AiRewriteFormal": "Maak het formeel", + "Composer_AiRewriteFormalDescription": "Geeft het bericht een professionelere en gestructureerdere toon.", + "Composer_AiRewriteFriendly": "Maak het vriendelijk", + "Composer_AiRewriteFriendlyDescription": "Maakt de boodschap warmer en toegankelijker.", + "Composer_AiRewriteShorter": "Maak het korter", + "Composer_AiRewriteShorterDescription": "Verkort de tekst en verwijdert onnodige details.", + "Composer_AiRewriteClearer": "Maak het duidelijker", + "Composer_AiRewriteClearerDescription": "Verbetert de leesbaarheid en maakt het bericht gemakkelijker te volgen.", + "Composer_AiRewriteCustom": "Aangepast", + "Composer_AiRewriteCustomDescription": "Beschrijf je eigen herschrijvingsdoel.", + "Composer_AiRewriteCustomPlaceholder": "Beschrijf hoe je wilt dat het bericht herschreven wordt", + "Composer_AiRewriteMode": "Herschrijf toon", + "Composer_AiRewriteApply": "Herschrijf toepassen", + "Composer_AiTranslateDialogTitle": "Vertalen met AI", + "Composer_AiTranslateDialogDescription": "Voer de doeltaal of cultuurcode in, zoals en-US, tr-TR, de-DE of fr-FR.", + "Composer_AiTranslateApply": "Vertalen", + "Composer_AiTranslateLanguage": "Doeltaal", + "Composer_AiTranslateCustomPlaceholder": "Voer cultuurcode in", + "Composer_AiTranslateLanguageEnglish": "Engels (en-US)", + "Composer_AiTranslateLanguageTurkish": "Turks (tr-TR)", + "Composer_AiTranslateLanguageGerman": "Duits (de-DE)", + "Composer_AiTranslateLanguageFrench": "Frans (fr-FR)", + "Composer_AiTranslateLanguageSpanish": "Spaans (es-ES)", + "Composer_AiTranslateLanguageItalian": "Italiaans (it-IT)", + "Composer_AiTranslateLanguagePortugueseBrazil": "Portugees (Brazilië) (pt-BR)", + "Composer_AiTranslateLanguageDutch": "Nederlands (nl-NL)", + "Composer_AiTranslateLanguagePolish": "Pools (pl-PL)", + "Composer_AiTranslateLanguageRussian": "Russisch (ru-RU)", + "Composer_AiTranslateLanguageJapanese": "Japans (ja-JP)", + "Composer_AiTranslateLanguageKorean": "Koreaans (ko-KR)", + "Composer_AiTranslateLanguageChineseSimplified": "Chinees, Vereenvoudigd (zh-CN)", + "Composer_AiTranslateLanguageArabic": "Arabisch (ar-SA)", + "Composer_AiTranslateLanguageHindi": "Hindi (hi-IN)", + "Composer_AiTranslateLanguageOther": "Overig...", + "Composer_AiBusyTitle": "AI is al bezig", + "Composer_AiBusyMessage": "Wacht alstublieft tot de huidige AI-actie is voltooid.", + "Composer_AiSignInRequired": "Meld u aan bij uw Wino-account om AI-functies te gebruiken.", + "Composer_AiMissingHtml": "Er is nog geen berichtinhoud om naar Wino AI te sturen.", + "Composer_AiQuotaUnavailable": "Het AI-resultaat is toegepast.", + "Composer_AiAppliedMessage": "Het AI-resultaat is toegepast op de composer. Gebruik Ongedaan maken als u het wilt terugdraaien.", + "Composer_AiSummarizeSuccessTitle": "AI-samenvatting toegepast", + "Composer_AiTranslateSuccessTitle": "AI-vertaling toegepast", + "Composer_AiRewriteSuccessTitle": "AI-herschrijving toegepast", + "Composer_AiErrorTitle": "AI-actie mislukt", + "Reader_AiAppliedMessage": "Het AI-resultaat wordt nu weergegeven voor dit bericht. Open het bericht opnieuw om de oorspronkelijke inhoud weer te geven.", + "SettingsAppPreferences_EmailSyncInterval_Title": "Email sync interval", + "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail.", + "ContactsPage_Title": "Contactpersonen", + "ContactsPage_AddContact": "Contact toevoegen", + "ContactsPage_EditContact": "Contact bewerken", + "ContactsPage_DeleteContact": "Contact verwijderen", + "ContactsPage_SearchPlaceholder": "Zoek contactpersonen...", + "ContactsPage_NoContacts": "Geen contactpersonen gevonden", + "ContactsPage_ContactsCount": "{0} contactpersonen", + "ContactsPage_SelectedContactsCount": "{0} geselecteerd", + "ContactsPage_DeleteSelectedContacts": "Geselecteerde verwijderen", + "ContactEditDialog_Title": "Contact bewerken", + "ContactEditDialog_PhotoSection": "Foto", + "ContactEditDialog_ChoosePhoto": "Foto kiezen", + "ContactEditDialog_RemovePhoto": "Foto verwijderen", + "ContactEditDialog_NameHeader": "Naam", + "ContactEditDialog_NamePlaceholder": "Contactnaam", + "ContactEditDialog_EmailHeader": "E-mailadres", + "ContactEditDialog_EmailPlaceholder": "contact@example.com", + "ContactEditDialog_InfoSection": "Contactinformatie", + "ContactEditDialog_RootContactInfo": "Dit is een hoofdcontact dat aan uw accounts is gekoppeld en kan niet worden verwijderd.", + "ContactEditDialog_OverriddenContactInfo": "Dit contact is handmatig gewijzigd en zal tijdens synchronisatie niet worden bijgewerkt.", + "ContactsPage_Subtitle": "Beheer uw e-mailcontactpersonen en hun informatie", + "ContactStatus_Account": "Account", + "ContactStatus_Modified": "Aangepast", + "ContactAction_Edit": "Contact bewerken", + "ContactAction_ChangePhoto": "Foto wijzigen", + "ContactAction_Delete": "Contact verwijderen", + "ContactAction_Add": "Contact toevoegen", + "ContactSelection_Selected": "geselecteerd", + "ContactSelection_SelectAll": "Alles selecteren", + "ContactSelection_Clear": "Selectie wissen", + "ContactsPage_EmptyState": "Geen contactpersonen om weer te geven", + "ContactsPage_AddFirstContact": "Voeg uw eerste contact toe", + "ContactsPage_ContactsCountSuffix": "contactpersonen", + "ContactsPane_NewContact": "Nieuw contact", + "ContactsPane_DescriptionTitle": "Beheer uw contacten", + "ContactsPane_DescriptionBody": "Maak contacten aan, hernoem ze, werk profielfoto's bij en houd opgeslagen details georganiseerd op één plek.", + "ContactEditDialog_AddTitle": "Contact toevoegen", + "ContactInfoBar_ContactAdded": "Contact succesvol toegevoegd.", + "ContactInfoBar_ContactUpdated": "Contact succesvol bijgewerkt.", + "ContactInfoBar_ContactsDeleted": "Contactpersonen succesvol verwijderd.", + "ContactInfoBar_ContactPhotoUpdated": "Contactfoto succesvol bijgewerkt.", + "ContactInfoBar_FailedToLoadContacts": "Mislukt om contactpersonen te laden: {0}", + "ContactInfoBar_FailedToAddContact": "Fout bij het toevoegen van contact: {0}", + "ContactInfoBar_FailedToUpdateContact": "Fout bij het bijwerken van contact: {0}", + "ContactInfoBar_FailedToDeleteContacts": "Fout bij het verwijderen van contactpersonen: {0}", + "ContactInfoBar_FailedToUpdatePhoto": "Fout bij het bijwerken van foto: {0}", + "ContactInfoBar_CannotDeleteRoot": "Hoofdcontacten kunnen niet worden verwijderd.", + "ContactConfirmDialog_DeleteTitle": "Contact verwijderen", + "ContactConfirmDialog_DeleteMessage": "Weet u zeker dat u het contact '{0}' wilt verwijderen?", + "ContactConfirmDialog_DeleteMultipleMessage": "Weet u zeker dat u {0} contactpersoon(en) wilt verwijderen?", + "ContactConfirmDialog_DeleteButton": "Verwijderen", + "CalendarAccountSettings_Title": "Kalenderaccountinstellingen", + "CalendarAccountSettings_Description": "Beheer kalenderinstellingen voor {0}", + "CalendarAccountSettings_AccountColor": "Accountkleur", + "CalendarAccountSettings_AccountColorDescription": "Wijzig de weergavekleur voor dit kalenderaccount", + "CalendarAccountSettings_SyncEnabled": "Synchronisatie inschakelen", + "CalendarAccountSettings_SyncEnabledDescription": "Schakel kalendersynchronisatie in of uit voor dit account.", + "CalendarAccountSettings_DefaultShowAs": "Standaard Beschikbaarheidsstatus", + "CalendarAccountSettings_DefaultShowAsDescription": "Standaard beschikbaarheidsstatus voor nieuwe gebeurtenissen die met dit account zijn aangemaakt.", + "CalendarAccountSettings_PrimaryCalendar": "Primaire kalender", + "CalendarAccountSettings_PrimaryCalendarDescription": "Markeer deze kalender als de primaire kalender voor het account.", + "CalendarSettings_NewEventBehavior_Header": "Gedrag van de knop Nieuwe gebeurtenis", + "CalendarSettings_NewEventBehavior_Description": "Kies of de knop Nieuwe gebeurtenis bij elke klik om een kalender moet vragen of altijd een specifieke kalender moet openen.", + "CalendarSettings_NewEventBehavior_AskEachTime": "Vraag elke keer.", + "CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar": "Gebruik altijd een specifieke kalender.", + "CalendarSettings_Rendering_Title": "Weergave", + "CalendarSettings_Rendering_Description": "Kalenderindeling en weergave-instellingen configureren.", + "CalendarSettings_Notifications_Title": "Notificaties", + "CalendarSettings_Notifications_Description": "Kies standaard herinnerings- en uitstelgedrag.", + "CalendarSettings_Preferences_Title": "Voorkeuren", + "CalendarSettings_Preferences_Description": "Stel in hoe de Nieuwe gebeurtenis-knop zich gedraagt.", + "WhatIsNew_GetStartedButton": "Beginnen met het toevoegen van een account.", + "WhatIsNew_ContinueAnywayButton": "Toch doorgaan", + "WhatIsNew_PreparingForNewVersionButton": "Voorbereiden op nieuwe versie...", + "WhatIsNew_MigrationPreparing_Title": "Uw gegevens aan het voorbereiden", + "WhatIsNew_MigrationPreparing_Description": "Wino past update-migraties toe. Even geduld terwijl we uw accountgegevens voor deze release voorbereiden.", + "WhatIsNew_MigrationFailedMessage": "Het toepassen van migraties is mislukt met foutcode {0}. U kunt de applicatie blijven gebruiken. Als u echter ernstige problemen ondervindt, installeer de toepassing opnieuw.", + "WhatIsNew_MigrationNotification_Title": "Wino Mail is bijgewerkt", + "WhatIsNew_MigrationNotification_Message": "Open de app om de update te voltooien en te zien wat er nieuw is.", + "WelcomeWindow_Title": "Welkom bij Wino Mail", + "WelcomeWindow_Subtitle": "Een native Windows-ervaring voor Mail en Kalender.", + "WelcomeWindow_WhatsNewTitle": "Laatste wijzigingen", + "WelcomeWindow_FeaturesTitle": "Functies", + "WelcomeWindow_WhatsNewTab": "Wat is nieuw", + "WelcomeWindow_FeaturesTab": "Functies", + "WelcomeWindow_GetStartedButton": "Beginnen met het toevoegen van een account.", + "WelcomeWindow_GetStartedDescription": "Voeg uw Outlook-, Gmail- of IMAP-account toe om aan de slag te gaan met Wino Mail.", + "WelcomeWindow_ImportFromWinoAccount": "Importeren vanuit uw Wino-account", + "WelcomeWindow_ImportInProgress": "Uw gesynchroniseerde voorkeuren en accounts worden geïmporteerd...", + "WelcomeWindow_ImportNoAccountsFound": "Er zijn geen gesynchroniseerde accounts gevonden in uw Wino-account. Als er voorkeuren beschikbaar waren, zijn ze hersteld. Gebruik Aan de slag om handmatig een account toe te voegen.", + "WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} gesynchroniseerde accounts zijn al beschikbaar op dit apparaat. Gebruik Aan de slag om indien nodig handmatig nog een account toe te voegen.", + "WelcomeWindow_SetupTitle": "Uw account instellen.", + "WelcomeWindow_SetupSubtitle": "Kies uw e-mailprovider om aan de slag te gaan.", + "WelcomeWindow_AddAccountButton": "Account toevoegen", + "WelcomeWindow_SkipForNow": "Voor nu overslaan — ik stel het later in.", + "WelcomeWindow_AppDescription": "Een snelle, gefocuste inbox — opnieuw ontworpen voor Windows 11", + "WelcomeWizard_Step1Title": "Welkom", + "SystemTrayMenu_Open": "Open", + "WinoAccount_Titlebar_SyncBenefitTitle": "Synchronisatie-instellingen", + "WinoAccount_Titlebar_SyncBenefitDescription": "Houd uw Wino-voorkeuren gesynchroniseerd tussen apparaten.", + "WinoAccount_Titlebar_AddonsBenefitTitle": "Add-ons ontgrendelen", + "WinoAccount_Titlebar_AddonsBenefitDescription": "Toegang tot premiumfuncties zoals Wino AI Pack.", + "WinoAccount_Management_Description": "Beheer uw Wino-account, AI Pack-toegang, en gesynchroniseerde voorkeuren en accountgegevens.", + "WinoAccount_Management_SignedOutTitle": "Inloggen bij Wino Mail", + "WinoAccount_Management_SignedOutDescription": "Log in of maak een account aan om uw e-mail te synchroniseren, toegang te krijgen tot AI-functies en uw instellingen tussen apparaten te beheren.", + "WinoAccount_Management_ProfileSectionHeader": "Profiel", + "WinoAccount_Management_AddOnsSectionHeader": "Wino Add-ons", + "WinoAccount_Management_DataSectionHeader": "Gegevens", + "WinoAccount_Management_AccountActionsSectionHeader": "Accountacties", + "WinoAccount_Management_AccountCardTitle": "Account", + "WinoAccount_Management_AccountCardDescription": "Uw Wino-account e-mailadres en huidige accountstatus.", + "WinoAccount_Management_AiPackCardTitle": "AI Pack", + "WinoAccount_Management_AiPackCardDescription": "Zie of Wino AI Pack actief is en hoeveel gebruik er nog over is.", + "WinoAccount_Management_AiPackActive": "AI Pack is actief", + "WinoAccount_Management_AiPackInactive": "AI Pack is niet actief", + "WinoAccount_Management_AiPackUsage": "{0} van {1} gebruiken verbruikt. {2} resterend.", + "WinoAccount_Management_AiPackBillingPeriod": "Factureringsperiode: {0:d} - {1:d}", + "WinoAccount_Management_AiPackUnknownUsage": "Gegevens over het gebruik zijn nog niet beschikbaar.", + "WinoAccount_Management_AiPackBuyDescription": "Koop Wino AI Pack om e-mails te vertalen, te herschrijven of samen te vatten met AI.", + "WinoAccount_Management_AiPackPromoTitle": "AI Pack ontgrendelen", + "WinoAccount_Management_AiPackPromoDescription": "Versnel uw e-mailworkflow met AI-gedreven hulpmiddelen. Vertaal berichten naar meer dan 50 talen, herschrijf voor duidelijkheid en toon, en krijg directe samenvattingen van lange draadjes.", + "WinoAccount_Management_AiPackPromoPrice": "$4,99 / mnd", + "WinoAccount_Management_AiPackPromoRequests": "1.000 credits", + "WinoAccount_Management_AiPackGetButton": "AI Pack verkrijgen", + "WinoAddOn_AI_PACK_Name": "Wino AI Pack", + "WinoAddOn_AI_PACK_Description": "AI-gedreven tools voor vertalen, herschrijven en samenvatten acties in Wino Mail.", + "WinoAddOn_AI_PACK_Keywords": "AI, vertalen, herschrijven, samenvatten, productiviteit", + "WinoAddOn_UNLIMITED_ACCOUNTS_Name": "Onbeperkte accounts", + "WinoAddOn_UNLIMITED_ACCOUNTS_Description": "Verwijder de accountlimiet en voeg zoveel e-mailaccounts toe als je nodig hebt.", + "WinoAddOn_UNLIMITED_ACCOUNTS_Keywords": "accounts, onbeperkt, premium, add-on", + "WinoAccount_Management_PurchaseRequiresSignIn": "Meld u aan met uw Wino-account om deze aankoop te voltooien.", + "WinoAccount_Management_PurchaseStartFailed": "Wino kon deze aankoop via Microsoft Store niet voltooien.", + "WinoAccount_Management_StoreSyncFailed": "Uw aankoop is voltooid, maar Wino kon uw accountvoordelen nog niet vernieuwen. Probeer het over een moment opnieuw.", + "WinoAccount_Management_AiPackSubscriptionActive": "Uw abonnement is actief", + "WinoAccount_Management_AiPackRenews": "Verlengt {0:d}", + "WinoAccount_Management_AiPackRequestsUsed": "Credits deze maand gebruikt", + "WinoAccount_Management_AiPackResets": "Reset {0:d}", + "WinoAccount_Management_AiPackUsageLoadFailed": "We hebben problemen bij het laden van uw AI-gebruikbalans.", + "WinoAccount_Management_AiPackFeatureTranslate": "Vertalen", + "WinoAccount_Management_AiPackFeatureRewrite": "Herschrijven", + "WinoAccount_Management_AiPackFeatureSummarize": "Samenvatten", + "WinoAccount_Management_AddOnLoadFailed": "We hebben problemen bij het laden van deze add-on.", + "WinoAccount_Management_SyncPreferencesTitle": "Synchroniseer Voorkeuren en Accounts", + "WinoAccount_Management_SyncPreferencesDescription": "Importeer of exporteer uw Wino-voorkeuren en mailboxgegevens over apparaten. Wachtwoorden, tokens en andere gevoelige informatie worden nooit gesynchroniseerd.", + "WinoAccount_Management_SignOutTitle": "Uitloggen", + "WinoAccount_Management_SignOutDescription": "Uitloggen van uw account op dit apparaat", + "WinoAccount_Management_StatusLabel": "Status: {0}", + "WinoAccount_Management_NoRemoteSettings": "Er zijn nog geen gesynchroniseerde gegevens opgeslagen voor dit account.", + "WinoAccount_Management_ExportSucceeded": "Uw geselecteerde Wino-gegevens zijn succesvol geëxporteerd.", + "WinoAccount_Management_ExportPreferencesSucceeded": "Uw voorkeuren zijn geëxporteerd naar uw Wino-account.", + "WinoAccount_Management_ExportAccountsSucceeded": "Exporteerde {0} accountgegevens naar uw Wino-account.", + "WinoAccount_Management_ImportSucceeded": "Gesynchroniseerde gegevens uit uw Wino-account geïmporteerd.", + "WinoAccount_Management_ImportPreferencesSucceeded": "Toegepaste {0} gesynchroniseerde voorkeuren.", + "WinoAccount_Management_ImportAccountsSucceeded": "Geïmporteerde {0} accounts.", + "WinoAccount_Management_ImportDuplicateAccountsSkipped": "Overgeslagen {0} accounts die al op dit apparaat bestaan.", + "WinoAccount_Management_ImportPartial": "Toegepaste {0} gesynchroniseerde voorkeuren. {1} voorkeuren konden niet worden hersteld.", + "WinoAccount_Management_ImportReloginReminder": "Wachtwoorden, tokens en andere gevoelige informatie zijn niet geïmporteerd. Log opnieuw in voor elk account op dit apparaat voordat u het gebruikt.", + "WinoAccount_Management_SerializeFailed": "Wino kon uw huidige voorkeuren niet serialiseren.", + "WinoAccount_Management_EmptyExport": "Er zijn geen voorkeuren om te exporteren.", + "WinoAccount_Management_ImportEmpty": "De gesynchroniseerde gegevenspayload bevat geen nieuwe items om te herstellen.", + "WinoAccount_Management_ExportDialog_Title": "Exporteer naar uw Wino-account", + "WinoAccount_Management_ExportDialog_Description": "Kies wat u wilt synchroniseren met uw Wino-account.", + "WinoAccount_Management_ExportDialog_IncludePreferences": "Voorkeuren", + "WinoAccount_Management_ExportDialog_IncludeAccounts": "Accounts", + "WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Wachtwoorden, tokens en andere gevoelige informatie worden niet gesynchroniseerd.", + "WinoAccount_Management_ExportDialog_AccountsRelogin": "Geïmporteerde accounts op een andere pc vereisen nog steeds dat u zich opnieuw aanmeldt voordat ze kunnen worden gebruikt.", + "WinoAccount_Management_ExportDialog_InProgress": "Uw geselecteerde Wino-gegevens worden geëxporteerd...", + "WinoAccount_Management_LoadFailed": "Wino kon de nieuwste Wino Account-informatie niet laden.", + "WinoAccount_Management_ActionFailed": "Het Wino Account-verzoek kon niet worden voltooid.", + "WinoAccount_SettingsSection_Title": "Wino-account", + "WinoAccount_SettingsSection_Description": "Maak een Wino-account aan of meld u aan bij een Wino-account met uw localhost-authenticatieservice.", + "WinoAccount_RegisterButton_Title": "Account registreren", + "WinoAccount_RegisterButton_Description": "Maak een Wino-account aan met e-mail en wachtwoord.", + "WinoAccount_RegisterButton_Action": "Registratie openen", + "WinoAccount_LoginButton_Title": "Inloggen", + "WinoAccount_LoginButton_Description": "Meld u aan bij uw Wino-account om uw Wino-setup te synchroniseren en toegang te krijgen tot accountgebonden functies.", + "WinoAccount_LoginButton_Action": "Inloggen openen", + "WinoAccount_SignOutButton_Title": "Uitloggen", + "WinoAccount_SignOutButton_Description": "Verwijder de lokaal opgeslagen Wino-account-sessie.", + "WinoAccount_SignOutButton_Action": "Uitloggen", + "WinoAccount_RegisterDialog_Title": "Wino-account aanmaken", + "WinoAccount_RegisterDialog_Description": "Maak een Wino-account aan om uw Wino-ervaring gesynchroniseerd te houden en accountgebonden add-ons te ontgrendelen.", + "WinoAccount_RegisterDialog_HeroTitle": "Maak uw Wino-account aan", + "WinoAccount_RegisterDialog_BenefitsTitle": "Waarom er één aanmaken?", + "WinoAccount_RegisterDialog_BenefitSyncTitle": "Instellingen tussen apparaten importeren en exporteren.", + "WinoAccount_RegisterDialog_BenefitSyncDescription": "Verplaats uw Wino-voorkeuren tussen apparaten zonder uw installatie vanaf nul opnieuw te hoeven opbouwen.", + "WinoAccount_RegisterDialog_BenefitAiTitle": "Toegang tot exclusieve add-ons zoals Wino AI Pack (betaald)", + "WinoAccount_RegisterDialog_BenefitAiDescription": "Gebruik één account om premiumfuncties van Wino te ontgrendelen zodra ze beschikbaar komen.", + "WinoAccount_RegisterDialog_DifferenceTitle": "Wino-account is gescheiden van uw e-mailaccounts.", + "WinoAccount_RegisterDialog_DifferenceDescription": "Uw Outlook-, Gmail-, IMAP-, of andere e-mailaccounts blijven zoals ze zijn. Een Wino-account beheert alleen Wino-specifieke functies en accountgebonden add-ons.", + "WinoAccount_RegisterDialog_PrimaryButton": "Registreren", + "WinoAccount_RegisterDialog_PrivacyTitle": "Privacy en API-verwerking", + "WinoAccount_RegisterDialog_PrivacyDescription": "Optionele add-ons zoals Wino AI Pack kunnen geselecteerde HTML-inhoud van e-mails verzenden naar de Wino API-service wanneer u deze functies gebruikt.", + "WinoAccount_RegisterDialog_PrivacyLinkText": "Lees het privacybeleid", + "WinoAccount_RegisterDialog_PrivacyCheckbox": "Ik ga akkoord met het privacybeleid.", + "WinoAccount_LoginDialog_Title": "Inloggen bij Wino-account", + "WinoAccount_LoginDialog_Description": "Meld u aan bij uw Wino-account om uw Wino-setup te synchroniseren en toegang te krijgen tot accountgebonden functies.", + "WinoAccount_LoginDialog_HeroTitle": "Welkom terug", + "WinoAccount_LoginDialog_BenefitsTitle": "Wat aanmelden u oplevert.", + "WinoAccount_LoginDialog_BenefitsDescription": "Gebruik uw Wino-account om instellingen tussen apparaten te blijven synchroniseren en toegang te krijgen tot betaalde add-ons zoals Wino AI Pack.", + "WinoAccount_LoginDialog_DifferenceTitle": "Dit is niet de aanmelding voor uw e-mailaccount.", + "WinoAccount_LoginDialog_DifferenceDescription": "Inloggen hier voegt uw Outlook-, Gmail-, of IMAP-accounts niet toe of vervangt ze in Wino. U meldt zich hiermee alleen aan bij Wino-specifieke services.", + "WinoAccount_LoginDialog_ForgotPasswordLink": "Wachtwoord vergeten?", + "WinoAccount_EmailLabel": "E-mailadres", + "WinoAccount_EmailPlaceholder": "naam@voorbeeld.nl", + "WinoAccount_PasswordLabel": "Wachtwoord", + "WinoAccount_ConfirmPasswordLabel": "Wachtwoord bevestigen", + "WinoAccount_ForgotPasswordDialog_Title": "Wachtwoord opnieuw instellen", + "WinoAccount_ForgotPasswordDialog_PrimaryButton": "Wachtwoordherstelfe-mail verzenden", + "WinoAccount_ForgotPasswordDialog_BackToSignIn": "Terug naar aanmelden", + "WinoAccount_ForgotPasswordDialog_Description": "Voer het Wino-account-e-mailadres in en wij sturen u een wachtwoordherstellink als het adres is geregistreerd.", + "WinoAccount_Validation_EmailRequired": "E-mail is vereist.", + "WinoAccount_Validation_PasswordRequired": "Wachtwoord is vereist.", + "WinoAccount_Validation_PasswordMismatch": "Wachtwoorden komen niet overeen.", + "WinoAccount_Validation_PrivacyConsentRequired": "U moet het privacybeleid accepteren voordat u een Wino-account aanmaakt.", + "WinoAccount_Error_InvalidCredentials": "Het e-mailadres of wachtwoord is onjuist.", + "WinoAccount_Error_AccountLocked": "Dit account is tijdelijk geblokkeerd.", + "WinoAccount_Error_AccountBanned": "Dit account is verbannen.", + "WinoAccount_Error_AccountSuspended": "Dit account is geschorst.", + "WinoAccount_Error_EmailNotConfirmed": "Bevestig uw e-mailadres voordat u zich aanmeldt.", + "WinoAccount_Error_EmailConfirmationRequired": "Bevestig uw e-mailadres voordat u zich aanmeldt.", + "WinoAccount_Error_EmailConfirmationResendNotAvailable": "Een nieuwe bevestigingsmail is nog niet beschikbaar.", + "WinoAccount_Error_EmailConfirmationResendInvalid": "Deze bevestigingsverzoek is niet meer geldig. Probeer opnieuw aan te melden.", + "WinoAccount_Error_EmailNotRegistered": "Dit e-mailadres is niet geregistreerd.", + "WinoAccount_Error_RefreshTokenInvalid": "Uw sessie is niet langer geldig. Meld u opnieuw aan.", + "WinoAccount_Error_EmailAlreadyRegistered": "Dit e-mailadres is al geregistreerd.", + "WinoAccount_Error_ExternalLoginEmailRequired": "Een e-mailadres is vereist om externe aanmelding te voltooien.", + "WinoAccount_Error_ExternalLoginInvalid": "Het externe aanmeldingsverzoek is ongeldig.", + "WinoAccount_Error_ExternalAuthStateInvalid": "De externe aanmeldingstoestand is ongeldig of verlopen.", + "WinoAccount_Error_ExternalAuthCodeInvalid": "De externe aanmeldcode is ongeldig of verlopen.", + "WinoAccount_Error_AiPackRequired": "Voor deze actie is een actief Wino AI Pack-abonnement vereist.", + "WinoAccount_Error_AiQuotaExceeded": "Uw AI Pack-gebruikslimiet is bereikt voor de huidige factureringsperiode.", + "WinoAccount_Error_AiHtmlEmpty": "Er is geen e-mailinhoud om te verwerken.", + "WinoAccount_Error_AiHtmlTooLarge": "Deze e-mail is te groot om met Wino AI te verwerken.", + "WinoAccount_Error_AiUnsupportedLanguage": "Die taal wordt niet ondersteund. Probeer een geldige cultuurcode zoals en-US of tr-TR.", + "WinoAccount_Error_Forbidden": "U heeft geen toestemming om deze handeling uit te voeren.", + "WinoAccount_Error_ValidationFailed": "Het verzoek is ongeldig. Controleer de ingevoerde waarden.", + "WinoAccount_RegisterSuccessMessage": "Wino-accountregistratie voltooid voor {0}.", + "WinoAccount_LoginSuccessMessage": "Ingelogd bij Wino-account als {0}.", + "WinoAccount_EmailConfirmationSentDialog_Title": "Bevestig uw e-mailadres", + "WinoAccount_EmailConfirmationSentDialog_Message": "We hebben een e-mailbevestiging verzonden naar {0}. Bevestig deze en probeer u opnieuw aan te melden.", + "WinoAccount_EmailConfirmationPendingDialog_Title": "E-mailbevestiging vereist", + "WinoAccount_EmailConfirmationPendingDialog_Message": "We wachten nog op bevestiging van {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ResendButton": "Bevestigingsmail opnieuw verzenden", + "WinoAccount_EmailConfirmationPendingDialog_Countdown": "U kunt de bevestigingsmail opnieuw verzenden in {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ReadyToResend": "U kunt nu de bevestigingsmail opnieuw verzenden.", + "WinoAccount_EmailConfirmationResentDialog_Title": "Bevestigingsmail opnieuw verzonden", + "WinoAccount_EmailConfirmationResentDialog_Message": "We hebben nog een bevestigingsmail verzonden naar {0}. Bevestig deze en probeer u zich opnieuw aan te melden.", + "WinoAccount_ForgotPasswordDialog_SuccessTitle": "Wachtwoordherstelfe-mail verzonden", + "WinoAccount_ForgotPasswordDialog_SuccessMessage": "We hebben een wachtwoordherstelfe-mail verzonden naar {0}. Open dat bericht om een nieuw wachtwoord te kiezen.", + "WinoAccount_ChangePassword_Title": "Wachtwoord wijzigen", + "WinoAccount_ChangePassword_Description": "Stuur een wachtwoordherstelfe-mail naar dit Wino-account.", + "WinoAccount_ChangePassword_Action": "Reset-e-mail verzenden", + "WinoAccount_ChangePassword_ConfirmationMessage": "Wilt u Wino een wachtwoordreset-e-mail sturen naar {0}?", + "WinoAccount_SignOut_SuccessMessage": "Uitgelogd bij Wino Account {0}.", + "WinoAccount_SignOut_NoAccountMessage": "Er is geen actieve Wino Account om af te melden.", + "WinoAccount_Titlebar_SignedOutTitle": "Wino Account", + "WinoAccount_Titlebar_SignedOutDescription": "Inloggen of maak een Wino Account aan om uw Wino-sessie te beheren.", + "WinoAccount_Titlebar_SignedInStatus": "Status: {0}", + "WelcomeWizard_Step2Title": "Account toevoegen", + "WelcomeWizard_Step3Title": "Configuratie voltooien", + "ProviderSelection_Title": "Kies uw e-mailprovider", + "ProviderSelection_Subtitle": "Selecteer hieronder een provider om uw e-mailaccount toe te voegen aan Wino Mail.", + "ProviderSelection_AccountNameHeader": "Accountnaam", + "ProviderSelection_AccountNamePlaceholder": "bijv. Persoonlijk, Werk", + "ProviderSelection_DisplayNameHeader": "Weergavenaam", + "ProviderSelection_DisplayNamePlaceholder": "bijv. John Doe", + "ProviderSelection_EmailHeader": "E-mailadres", + "ProviderSelection_EmailPlaceholder": "bijv. johndoe@example.com", + "ProviderSelection_AppPasswordHeader": "App-specifiek wachtwoord", + "ProviderSelection_AppPasswordHelp": "Hoe krijg ik een app-specifiek wachtwoord?", + "ProviderSelection_CalendarModeHeader": "Kalenderintegratie", + "ProviderSelection_CalendarMode_DisabledTitle": "Uitgeschakeld", + "ProviderSelection_CalendarMode_DisabledDescription": "Geen kalenderintegratie", + "ProviderSelection_CalendarMode_CalDavTitle": "CalDAV-synchronisatie", + "ProviderSelection_CalendarMode_CalDavDescription_Apple": "Uw agenda-evenementen worden tussen uw apparaten gesynchroniseerd met Apple-servers.", + "ProviderSelection_CalendarMode_CalDavDescription_Yahoo": "Uw agenda-evenementen worden tussen uw apparaten gesynchroniseerd met Yahoo-servers.", + "ProviderSelection_CalendarMode_LocalTitle": "Lokale kalender", + "ProviderSelection_CalendarMode_LocalDescription": "Uw afspraken worden alleen op uw computer opgeslagen. Geen verbinding met een server.", + "ProviderSelection_ClearColor": "Kleur wissen", + "ProviderSelection_ContinueButton": "Doorgaan", + "ProviderSelection_SpecialImap_Subtitle": "Voer uw accountgegevens in om verbinding te maken.", + "AccountSetup_Title": "Uw account wordt ingesteld.", + "AccountSetup_Step_Authenticating": "Authenticatie met {0}", + "AccountSetup_Step_TestingMailAuth": "Mailauthenticatie testen", + "AccountSetup_Step_SyncingFolders": "Mapmetagegevens synchroniseren", + "AccountSetup_Step_FetchingProfile": "Profielinformatie ophalen", + "AccountSetup_Step_DiscoveringCalDav": "CalDAV-instellingen ontdekken", + "AccountSetup_Step_TestingCalendarAuth": "Kalenderauthenticatie testen", + "AccountSetup_Step_SavingAccount": "Accountinformatie opslaan", + "AccountSetup_Step_FetchingCalendarMetadata": "Kalendermetadata ophalen", + "AccountSetup_Step_SyncingAliases": "Aliassen synchroniseren", + "AccountSetup_Step_Finalizing": "Configuratie afronden", + "AccountSetup_FailureMessage": "Het instellen van uw account is mislukt. Ga terug om uw instellingen te corrigeren, of probeer het later opnieuw.", + "AccountSetup_SuccessMessage": "Uw account is succesvol ingesteld!", + "AccountSetup_GoBackButton": "Ga terug", + "AccountSetup_TryAgainButton": "Opnieuw proberen", + "ImapCalDavSettings_AutoDiscoveryFailed": "Automatische detectie mislukt. Voer de instellingen handmatig in op het tabblad Geavanceerd." } - - diff --git a/Wino.Core.Domain/Translations/pl_PL/resources.json b/Wino.Core.Domain/Translations/pl_PL/resources.json index 412ebbab..5e150da5 100644 --- a/Wino.Core.Domain/Translations/pl_PL/resources.json +++ b/Wino.Core.Domain/Translations/pl_PL/resources.json @@ -8,6 +8,7 @@ "AccountCacheReset_Message": "To konto wymaga ponownej pełnej synchronizacji, aby kontynuować pracę. Poczekaj, aż Wino zsynchronizuje ponownie Twoje wiadomości...", "AccountContactNameYou": "You", "AccountCreationDialog_Completed": "gotowe", + "AccountCreationDialog_FetchingCalendarMetadata": "Pobieranie informacji o kalendarzu.", "AccountCreationDialog_FetchingEvents": "Fetching calendar events.", "AccountCreationDialog_FetchingProfileInformation": "Pobieranie danych profilu.", "AccountCreationDialog_GoogleAuthHelpClipboardText_Row0": "Jeśli Twoja przeglądarka nie uruchomiła się automatycznie, aby ukończyć uwierzytelnienie:", @@ -17,6 +18,7 @@ "AccountCreationDialog_Initializing": "inicjowanie", "AccountCreationDialog_PreparingFolders": "Trwa pobieranie informacji o folderach.", "AccountCreationDialog_SigninIn": "Zapisywanie danych konta.", + "Purchased": "Kupione", "AccountEditDialog_Message": "Nazwa konta", "AccountEditDialog_Title": "Edytuj konto", "AccountPickerDialog_Title": "Wybierz konto", @@ -26,6 +28,10 @@ "AccountDetailsPage_Description": "Change the name of the account in Wino and set desired sender name.", "AccountDetailsPage_ColorPicker_Title": "Account color", "AccountDetailsPage_ColorPicker_Description": "Assign a new account color to colorize its symbol in the list.", + "AccountDetailsPage_TabGeneral": "Ogólne", + "AccountDetailsPage_TabMail": "Poczta", + "AccountDetailsPage_TabCalendar": "Kalendarz", + "AccountDetailsPage_CalendarListDescription": "Wybierz kalendarz, aby skonfigurować jego ustawienia.", "AddHyperlink": "Dodaj", "AppCloseBackgroundSynchronizationWarningTitle": "Synchronizacja w tle", "AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Aplikacja nie została ustawiona do uruchamiania przy starcie systemu Windows.", @@ -47,8 +53,10 @@ "BasicIMAPSetupDialog_Title": "Konto IMAP", "Busy": "Zajęty", "Buttons_AddAccount": "Dodaj konto", + "Buttons_FixAccount": "Napraw konto", "Buttons_AddNewAlias": "Dodaj nowy alias", "Buttons_Allow": "Zezwól", + "Buttons_Apply": "Zastosuj", "Buttons_ApplyTheme": "Zastosuj motyw", "Buttons_Browse": "Przeglądaj", "Buttons_Cancel": "Anuluj", @@ -62,6 +70,7 @@ "Buttons_Edit": "Edytuj", "Buttons_EnableImageRendering": "Włącz", "Buttons_Multiselect": "Wybierz wielokrotnie", + "Buttons_Manage": "Zarządzaj", "Buttons_No": "Nie", "Buttons_Open": "Otwórz", "Buttons_Purchase": "Kup", @@ -70,15 +79,134 @@ "Buttons_Save": "Zapisz", "Buttons_SaveConfiguration": "Zapisz konfigurację", "Buttons_Send": "Wyślij", + "Buttons_SendToServer": "Wyślij do serwera", "Buttons_Share": "Udostępnij", "Buttons_SignIn": "Zaloguj się", "Buttons_Sync": "Synchronizuj", "Buttons_SyncAliases": "Synchronizuj aliasy", "Buttons_TryAgain": "Spróbuj ponownie", "Buttons_Yes": "Tak", + "Sync_SynchronizingFolder": "Trwa synchronizacja {0} {1}%", + "Sync_DownloadedMessages": "Pobrano {0} wiadomości z {1}", + "SyncAction_Archiving": "Archiwizowanie {0} wiadomości e-mail", + "SyncAction_ClearingFlag": "Cofanie flagi dla {0} wiadomości", + "SyncAction_CreatingDraft": "Tworzenie szkicu", + "SyncAction_CreatingEvent": "Tworzenie wydarzenia", + "SyncAction_Deleting": "Usuwanie {0} wiadomości", + "SyncAction_EmptyingFolder": "Opróżnianie folderu", + "SyncAction_MarkingAsRead": "Oznaczanie {0} wiadomości jako przeczytanych", + "SyncAction_MarkingAsUnread": "Oznaczanie {0} wiadomości jako nieprzeczytanych", + "SyncAction_MarkingFolderAsRead": "Oznaczanie folderu jako przeczytanego", + "SyncAction_Moving": "Przenoszenie {0} wiadomości", + "SyncAction_MovingToFocused": "Przenoszenie {0} wiadomości do Zakładki Skupione", + "SyncAction_RenamingFolder": "Zmienianie nazwy folderu", + "SyncAction_SendingMail": "Wysyłanie wiadomości e-mail", + "SyncAction_SettingFlag": "Ustawianie flagi {0} wiadomości", + "SyncAction_SynchronizingAccount": "Synchronizowanie {0}", + "SyncAction_SynchronizingAccounts": "Synchronizowanie {0} kont", + "SyncAction_SynchronizingCalendarData": "Synchronizowanie danych kalendarza", + "SyncAction_SynchronizingCalendarEvents": "Synchronizowanie wydarzeń kalendarza", + "SyncAction_SynchronizingCalendarMetadata": "Synchronizowanie metadanych kalendarza", + "SyncAction_Unarchiving": "Przywracanie {0} wiadomości z archiwum", "CalendarAllDayEventSummary": "all-day events", "CalendarDisplayOptions_Color": "Color", "CalendarDisplayOptions_Expand": "Expand", + "CalendarEventResponse_Accept": "Akceptuj", + "CalendarEventResponse_AcceptedResponse": "Zaakceptowano", + "CalendarEventResponse_Decline": "Odrzuć", + "CalendarEventResponse_DeclinedResponse": "Odrzucono", + "CalendarEventResponse_NotResponded": "Brak odpowiedzi", + "CalendarEventResponse_Tentative": "Wstępny", + "CalendarEventResponse_TentativeResponse": "Wstępny", + "CalendarEventRsvpPanel_Accept": "Akceptuj", + "CalendarEventRsvpPanel_AddMessage": "Dodaj wiadomość do odpowiedzi... (opcjonalnie)", + "CalendarEventRsvpPanel_Decline": "Odrzuć", + "CalendarEventRsvpPanel_Message": "Wiadomość", + "CalendarEventRsvpPanel_SendReplyMessage": "Wyślij wiadomość odpowiedzi", + "CalendarEventRsvpPanel_Tentative": "Wstępny", + "CalendarEventRsvpPanel_Title": "Opcje odpowiedzi", + "CalendarAttendeeStatus_Accepted": "Zaakceptowano", + "CalendarAttendeeStatus_Declined": "Odrzucono", + "CalendarAttendeeStatus_NeedsAction": "Wymaga akcji", + "CalendarAttendeeStatus_Tentative": "Wstępny", + "CalendarEventDetails_Attachments": "Załączniki", + "CalendarEventCompose_AddAttachment": "Dodaj załącznik", + "CalendarEventCompose_AllDay": "Całodniowe", + "CalendarEventCompose_AttachmentsNotSupportedForCalDav": "Załączniki nie są obsługiwane w kalendarzach CalDAV.", + "CalendarEventCompose_EndDate": "Data zakończenia", + "CalendarEventCompose_EndTime": "Czas zakończenia", + "CalendarEventCompose_Every": "co", + "CalendarEventCompose_ForWeekdays": "dla", + "CalendarEventCompose_FrequencyDay": "dzień", + "CalendarEventCompose_FrequencyDayPlural": "dni", + "CalendarEventCompose_FrequencyMonth": "miesiąc", + "CalendarEventCompose_FrequencyMonthPlural": "miesięcy", + "CalendarEventCompose_FrequencyWeek": "tydzień", + "CalendarEventCompose_FrequencyWeekPlural": "tygodni", + "CalendarEventCompose_FrequencyYear": "rok", + "CalendarEventCompose_FrequencyYearPlural": "lat", + "CalendarEventCompose_Location": "Lokalizacja", + "CalendarEventCompose_LocationPlaceholder": "Dodaj lokalizację", + "CalendarEventCompose_NewEventButton": "Nowe wydarzenie", + "CalendarEventCompose_DefaultCalendarHint": "Możesz wybrać domyślny kalendarz dla nowych wydarzeń w ustawieniach kalendarza.", + "CalendarEventCompose_DefaultCalendarSettingsLink": "Otwórz ustawienia kalendarza", + "CalendarEventCompose_NoCalendarsMessage": "Na razie nie ma dostępnych kalendarzy do tworzenia wydarzeń.", + "CalendarEventCompose_NoCalendarsTitle": "Brak dostępnych kalendarzy", + "CalendarEventCompose_NoEndDate": "Brak daty zakończenia", + "CalendarEventCompose_Notes": "Notatki", + "CalendarEventCompose_PickCalendarTitle": "Wybierz kalendarz", + "CalendarEventCompose_Recurring": "Powtarzające się", + "CalendarEventCompose_RecurringSummary": "Występuje co {0} {1}{2} {3} skuteczne {4}{5}", + "CalendarEventCompose_RecurringSummarySmart": "Występuje {0}{1} {2} skuteczne {3}{4}", + "CalendarEventCompose_RepeatEvery": "Powtarzaj co", + "CalendarEventCompose_SelectCalendar": "Wybierz kalendarz", + "CalendarEventCompose_SingleOccurrenceSummary": "Występuje w {0} {1}", + "CalendarEventCompose_StartDate": "Data rozpoczęcia", + "CalendarEventCompose_StartTime": "Czas rozpoczęcia", + "CalendarEventCompose_TimeRangeSummary": "od {0} do {1}", + "CalendarEventCompose_Title": "Tytuł wydarzenia", + "CalendarEventCompose_TitlePlaceholder": "Dodaj tytuł", + "CalendarEventCompose_Until": "do", + "CalendarEventCompose_UntilSummary": " do {0}", + "CalendarEventCompose_ValidationInvalidAllDayRange": "Data zakończenia dnia całodniowego musi być późniejsza niż data rozpoczęcia.", + "CalendarEventCompose_ValidationInvalidAttendee": "Co najmniej jeden uczestnik ma nieprawidłowy adres e-mail.", + "CalendarEventCompose_ValidationInvalidRecurrenceEnd": "Data zakończenia powtarzania musi być równa lub późniejsza niż data rozpoczęcia wydarzenia.", + "CalendarEventCompose_ValidationInvalidTimeRange": "Czas zakończenia musi być późniejszy niż czas rozpoczęcia.", + "CalendarEventCompose_ValidationMissingAttachment": "Jedna lub więcej załączników nie jest już dostępnych: {0}", + "CalendarEventCompose_ValidationMissingCalendar": "Wybierz kalendarz przed utworzeniem wydarzenia.", + "CalendarEventCompose_ValidationMissingTitle": "Wprowadź tytuł wydarzenia przed utworzeniem wydarzenia.", + "CalendarEventCompose_ValidationTitle": "Walidacja tytułu nie powiodła się", + "CalendarEventCompose_WeekdaySummary": " w {0}", + "CalendarEventCompose_Weekday_Friday": "Pt", + "CalendarEventCompose_Weekday_Monday": "Pn", + "CalendarEventCompose_Weekday_Saturday": "So", + "CalendarEventCompose_Weekday_Sunday": "Nd", + "CalendarEventCompose_Weekday_Thursday": "Cz", + "CalendarEventCompose_Weekday_Tuesday": "Wt", + "CalendarEventCompose_Weekday_Wednesday": "Śr", + "CalendarEventDetails_Details": "Szczegóły", + "CalendarEventDetails_EditSeries": "Edytuj serię", + "CalendarEventDetails_Editing": "Edytowanie", + "CalendarEventDetails_InviteSomeone": "Zaprosić kogoś", + "CalendarEventDetails_JoinOnline": "Dołącz online", + "CalendarEventDetails_Organizer": "Organizator", + "CalendarEventDetails_People": "Osoby", + "CalendarEventDetails_ReadOnlyEvent": "Wydarzenie tylko do odczytu", + "CalendarEventDetails_Reminder": "Przypomnienie", + "CalendarReminder_StartedHoursAgo": "Rozpoczęto {0} godzin temu", + "CalendarReminder_StartedMinutesAgo": "Rozpoczęto {0} minut temu", + "CalendarReminder_StartedNow": "Właśnie rozpoczęto", + "CalendarReminder_StartingNow": "Rozpoczyna się teraz", + "CalendarReminder_StartsInHours": "Zaczyna się za {0} godzin", + "CalendarReminder_StartsInMinutes": "Zaczyna się za {0} minut", + "CalendarReminder_SnoozeAction": "Przypomnij później", + "CalendarReminder_SnoozeMinutesOption": "{0} minut", + "CalendarEventDetails_ShowAs": "Pokaż jako", + "CalendarShowAs_Free": "Wolny", + "CalendarShowAs_Tentative": "Wstępny", + "CalendarShowAs_Busy": "Zajęty", + "CalendarShowAs_OutOfOffice": "Poza biurem", + "CalendarShowAs_WorkingElsewhere": "Pracuję w innym miejscu", "CalendarItem_DetailsPopup_JoinOnline": "Join online", "CalendarItem_DetailsPopup_ViewEventButton": "View event", "CalendarItem_DetailsPopup_ViewSeriesButton": "View series", @@ -88,6 +216,9 @@ "ClipboardTextCopied_Message": "{0} skopiowano do schowka.", "ClipboardTextCopied_Title": "Skopiowane", "ClipboardTextCopyFailed_Message": "Nie udało się skopiować {0} do schowka.", + "ContactInfoBar_ErrorTitle": "Nie udało się załadować informacji kontaktowych", + "ContactInfoBar_SuccessTitle": "Informacje kontaktowe załadowane", + "ContactInfoBar_WarningTitle": "Informacje kontaktowe mogą być niekompletne", "ComingSoon": "Już wkrótce...", "ComposerAttachmentsDragDropAttach_Message": "Załącz", "ComposerAttachmentsDropZone_Message": "Upuść pliki tutaj", @@ -129,6 +260,10 @@ "DialogMessage_CreateLinkedAccountTitle": "Nazwa połączonego konta", "DialogMessage_DeleteAccountConfirmationMessage": "Usunąć {0}?", "DialogMessage_DeleteAccountConfirmationTitle": "Wszystkie dane powiązane z tym kontem zostaną trwale usunięte z dysku.", + "DialogMessage_DeleteEmailTemplateConfirmationMessage": "Usuń szablon \"{0}\"?", + "DialogMessage_DeleteEmailTemplateConfirmationTitle": "Usuń szablon e-mail", + "DialogMessage_DeleteRecurringSeriesMessage": "To usunie wszystkie zdarzenia z serii. Czy chcesz kontynuować?", + "DialogMessage_DeleteRecurringSeriesTitle": "Usuń serię cykliczną", "DialogMessage_DiscardDraftConfirmationMessage": "Ta wersja robocza zostanie odrzucona. Czy chcesz kontynuować?", "DialogMessage_DiscardDraftConfirmationTitle": "Porzuć wersje roboczą", "DialogMessage_EmptySubjectConfirmation": "Brak tematu", @@ -172,11 +307,18 @@ "ElementTheme_Light": "Tryb jasny", "Emoji": "Emotikony", "Error_FailedToSetupSystemFolders_Title": "Nie udało się skonfigurować folderów systemowych", + "Exception_AccountNeedsAttention_Title": "Konto wymaga uwagi", + "Exception_AccountNeedsAttention_Message": "'{0}' wymaga Twojej uwagi, aby kontynuować pracę.", + "Exception_WebView2RuntimeMissing_Message": "Wino Mail nie mógł odnaleźć środowiska WebView2 firmy Microsoft Edge. Proszę zainstalować lub naprawić to środowisko, aby prawidłowo renderować treść wiadomości.", + "Exception_WebView2RuntimeMissing_Title": "Wymagane środowisko WebView2", "Exception_AuthenticationCanceled": "Anulowano uwierzytelnianie", "Exception_CustomThemeExists": "Ten motyw już istnieje.", "Exception_CustomThemeMissingName": "Musisz podać nazwę.", "Exception_CustomThemeMissingWallpaper": "Musisz podać niestandardowy obraz tła.", "Exception_FailedToSynchronizeAliases": "Nie udało się zsynchronizować aliasów", + "Exception_FailedToSynchronizeCalendarData": "Nie udało się zsynchronizować danych kalendarza", + "Exception_FailedToSynchronizeCalendarEvents": "Nie udało się zsynchronizować zdarzeń kalendarza", + "Exception_FailedToSynchronizeCalendarMetadata": "Nie udało się zsynchronizować szczegółów kalendarza", "Exception_FailedToSynchronizeFolders": "Nie udało się zsynchronizować folderów", "Exception_FailedToSynchronizeProfileInformation": "Nie udało się zsynchronizować informacji o profilu", "Exception_GoogleAuthCallbackNull": "Adres uri wywołania zwrotnego jest pusty przy aktywacji.", @@ -229,6 +371,32 @@ "HoverActionOption_MoveJunk": "Przenieś do wiadomości-śmieci", "HoverActionOption_ToggleFlag": "Oflaguj / Usuń flagę", "HoverActionOption_ToggleRead": "Przeczytane / Nieprzeczytane", + "KeyboardShortcuts_FailedToReset": "Nie udało się zresetować skrótów klawiaturowych.", + "KeyboardShortcuts_FailedToUpdate": "Nie udało się zaktualizować skrótów klawiaturowych.", + "KeyboardShortcuts_MailoperationAction": "Akcja", + "KeyboardShortcuts_Action": "Akcja", + "KeyboardShortcuts_FailedToLoad": "Nie udało się załadować skrótów klawiaturowych.", + "KeyboardShortcuts_EnterKeyForShortcut": "Proszę wprowadzić klawisz dla skrótu.", + "KeyboardShortcuts_SelectOperationForShortcut": "Wybierz akcję do wykonania dla skrótu.", + "KeyboardShortcuts_EnterKey": "Proszę wprowadzić klawisz dla skrótu.", + "KeyboardShortcuts_SelectOperation": "Proszę wybrać akcję dla skrótu.", + "KeyboardShortcuts_ShortcutInUse": "Ten skrót jest już używany przez inny skrót.", + "KeyboardShortcuts_FailedToSave": "Nie udało się zapisać skrótu.", + "KeyboardShortcuts_FailedToDelete": "Nie udało się usunąć skrótu.", + "KeyboardShortcuts_PageDescription": "Skonfiguruj skróty klawiaturowe dla szybkich operacji poczty. Naciśnij klawisze, gdy fokus znajduje się w polu wejścia klawisza, aby przechwycić skróty.", + "KeyboardShortcuts_Add": "Dodaj skrót", + "KeyboardShortcuts_EditTitle": "Edytuj skrót klawiaturowy", + "KeyboardShortcuts_ResetToDefaults": "Przywróć domyślne", + "KeyboardShortcuts_PressKeysHere": "Naciśnij tutaj klawisze...", + "KeyboardShortcuts_KeyCombination": "Kombinacja klawiszy", + "KeyboardShortcuts_FocusArea": "Skup fokus na polu powyżej i naciśnij żądaną kombinację klawiszy", + "KeyboardShortcuts_Modifiers": "Klawisze modyfikujące", + "KeyboardShortcuts_Mode": "Tryb aplikacji", + "KeyboardShortcuts_ModeMail": "Poczta", + "KeyboardShortcuts_ModeCalendar": "Kalendarz", + "KeyboardShortcuts_ActionToggleReadUnread": "Przełączanie między przeczytanym a nieprzeczytanym", + "KeyboardShortcuts_ActionToggleFlag": "Przełącz flagę", + "KeyboardShortcuts_ActionToggleArchive": "Przełącz archiwizację/wyarchiwizuj", "ImageRenderingDisabled": "Wyświetlanie obrazów jest wyłączone dla tej wiadomości.", "ImapAdvancedSetupDialog_AuthenticationMethod": "Metoda autoryzacji", "ImapAdvancedSetupDialog_ConnectionSecurity": "Zabezpieczenia dot. połączenia", @@ -295,12 +463,58 @@ "IMAPSetupDialog_Username": "Nazwa użytkownika", "IMAPSetupDialog_UsernamePlaceholder": "jankowalski, jankowalski@jakasdomena.com, domena/jankowalski", "IMAPSetupDialog_UseSameConfig": "Użyj tej samej nazwy użytkownika i hasła dla poczty wychodzącej", + "ImapCalDavSettingsPage_TitleCreate": "Konfiguracja IMAP i Kalendarza", + "ImapCalDavSettingsPage_TitleEdit": "Edytuj ustawienia IMAP i kalendarza", + "ImapCalDavSettingsPage_Subtitle": "Skonfiguruj IMAP/SMTP i opcjonalną synchronizację kalendarza dla tego konta.", + "ImapCalDavSettingsPage_BasicSectionTitle": "Podstawowa konfiguracja", + "ImapCalDavSettingsPage_BasicSectionDescription": "Wprowadź swoją tożsamość i dane uwierzytelniające. Wino może spróbować automatycznie wykryć ustawienia serwera.", + "ImapCalDavSettingsPage_BasicTab": "Podstawowy", + "ImapCalDavSettingsPage_EnableCalendarSupport": "Włącz obsługę kalendarza", + "ImapCalDavSettingsPage_AutoDiscoverButton": "Automatyczne wykrywanie ustawień poczty", + "ImapCalDavSettingsPage_AutoDiscoverySuccessMessage": "Ustawienia poczty zostały wykryte i zastosowane.", + "ImapCalDavSettingsPage_AdvancedSectionTitle": "Zaawansowana konfiguracja", + "ImapCalDavSettingsPage_AdvancedSectionDescription": "Wprowadź ręcznie ustawienia serwera, jeśli automatyczne wykrywanie nie jest dostępne lub nieprawidłowe.", + "ImapCalDavSettingsPage_AdvancedTab": "Zaawansowany", + "ImapCalDavSettingsPage_CalendarSectionTitle": "Ustawienia kalendarza", + "ImapCalDavSettingsPage_CalendarSectionDescription": "Wybierz, jak dane kalendarza będą działać dla tego konta IMAP.", + "ImapCalDavSettingsPage_CalendarModeHeader": "Tryb kalendarza", + "ImapCalDavSettingsPage_ConnectionSecurityHeader": "Bezpieczeństwo połączenia", + "ImapCalDavSettingsPage_AuthenticationMethodHeader": "Metoda uwierzytelniania", + "ImapCalDavSettingsPage_CalendarModeDisabled": "Wyłączony", + "ImapCalDavSettingsPage_CalendarModeCalDav": "Synchronizacja CalDAV", + "ImapCalDavSettingsPage_CalendarModeLocalOnly": "Tylko kalendarz lokalny", + "ImapCalDavSettingsPage_CalendarModeDisabledDescription": "Kalendarz jest wyłączony dla tego konta.", + "ImapCalDavSettingsPage_CalendarModeCalDavDescription": "Elementy kalendarza są synchronizowane z Twoim serwerem CalDAV.", + "ImapCalDavSettingsPage_CalendarModeLocalOnlyDescription": "Elementy kalendarza są przechowywane wyłącznie na tym komputerze i nie są synchronizowane z siecią.", + "ImapCalDavSettingsPage_LocalCalendarLearnMore": "Jak działa kalendarz lokalny", + "ImapCalDavSettingsPage_LocalCalendarDialogTitle": "Tylko kalendarz lokalny", + "ImapCalDavSettingsPage_LocalCalendarDialogMessage": "Kalendarz lokalny przechowuje wszystkie zdarzenia wyłącznie na Twoim komputerze. Nic nie synchronizuje się z iCloud, Yahoo ani innym dostawcą.", + "ImapCalDavSettingsPage_CalDavServiceUrl": "URL usługi CalDAV", + "ImapCalDavSettingsPage_CalDavUsername": "Nazwa użytkownika CalDAV", + "ImapCalDavSettingsPage_CalDavPassword": "Hasło CalDAV", + "ImapCalDavSettingsPage_CalDavNotRequiredMessage": "Test CalDAV jest wymagany tylko wtedy, gdy tryb kalendarza ustawiony jest na synchronizację CalDAV.", + "ImapCalDavSettingsPage_CalDavUrlRequired": "Wymagany jest URL usługi CalDAV.", + "ImapCalDavSettingsPage_CalDavUrlInvalid": "URL usługi CalDAV musi być bezwzględny.", + "ImapCalDavSettingsPage_CalDavUsernameRequired": "Wymagana jest nazwa użytkownika CalDAV.", + "ImapCalDavSettingsPage_CalDavPasswordRequired": "Wymagane jest hasło CalDAV.", + "ImapCalDavSettingsPage_TestImapButton": "Testuj połączenie IMAP", + "ImapCalDavSettingsPage_TestCalDavButton": "Testuj połączenie CalDAV", + "ImapCalDavSettingsPage_ImapTestSuccessMessage": "Test połączenia IMAP zakończony pomyślnie.", + "ImapCalDavSettingsPage_CalDavTestSuccessMessage": "Test połączenia CalDAV zakończony pomyślnie.", + "ImapCalDavSettingsPage_SaveSuccessMessage": "Ustawienia konta zweryfikowane i zapisane.", + "ImapCalDavSettingsPage_ICloudHint": "Użyj hasła specyficznego dla aplikacji wygenerowanego w ustawieniach konta Apple.", + "ImapCalDavSettingsPage_YahooHint": "Użyj hasła aplikacji z ustawień zabezpieczeń konta Yahoo.", "Info_AccountCreatedMessage": "Utworzono {0}", "Info_AccountCreatedTitle": "Tworzenie konta", "Info_AccountCreationFailedTitle": "Tworzenie konta nie powiodło się", "Info_AccountDeletedMessage": "Usunięto {0}.", "Info_AccountDeletedTitle": "Konto usunięte", "Info_AccountIssueFixFailedTitle": "Nie powiodło się", + "Info_AccountIssueFixImapMessage": "Otwórz stronę ustawień IMAP i kalendarza, aby ponownie wprowadzić dane serwera.", + "Info_AccountAttentionRequiredMessage": "To konto wymaga Twojej uwagi.", + "Info_AccountAttentionRequiredClickableMessage": "Kliknij, aby naprawić to konto i ponownie zsynchronizować.", + "Info_AccountAttentionRequiredAction": "Napraw", + "Info_AccountAttentionRequiredActionHint": "Kliknij Napraw, aby rozwiązać ten problem z kontem.", "Info_AccountIssueFixSuccessMessage": "Naprawiono wszystkie problemy z kontem.", "Info_AccountIssueFixSuccessTitle": "Zakończono pomyślnie", "Info_AttachmentOpenFailedMessage": "Nie można otworzyć tego załącznika.", @@ -370,6 +584,7 @@ "InfoBarMessage_SynchronizationDisabledFolder": "Ten folder jest wyłączony z synchronizacji.", "InfoBarTitle_SynchronizationDisabledFolder": "Katalog niesynchronizowany", "Justify": "Wyjustuj", + "MenuUpdateAvailable": "Dostępna aktualizacja", "Left": "Wyrównaj do lewej", "Link": "Odnośnik", "LinkedAccountsCreatePolicyMessage": "musisz mieć co najmniej 2 konta, aby je połączyć\npołączenie zostanie usunięte przy zapisywaniu", @@ -403,6 +618,7 @@ "MailOperation_Unarchive": "Odarchiwizuj", "MailOperation_ViewMessageSource": "View message source", "MailOperation_Zoom": "Powiększ", + "MailsDragging": "Przenoszenie {0} elementu(-ów)", "MailsSelected": "{0} zaznaczonych elementów", "MarkFlagUnflag": "Oznacz jako oflagowane/nieoflagowane", "MarkReadUnread": "Oznacz jako przeczytane/nieprzeczytane", @@ -431,10 +647,12 @@ "NoMessageCrieteria": "Żadne wiadomości nie spełniają kryteriów wyszukiwania", "NoMessageEmptyFolder": "Ten katalog jest pusty", "Notifications_MultipleNotificationsMessage": "Masz {0} nowych wiadomości.", - "Notifications_MultipleNotificationsTitle": "Nowa wiadomość", + "Notifications_MultipleNotificationsTitle": "New Mail", "Notifications_WinoUpdatedMessage": "Sprawdź nową wersję {0}", "Notifications_WinoUpdatedTitle": "Wino Mail został zaktualizowany.", - "OnlineSearchFailed_Message": "Nie udało się wykonać wyszukiwania\n{0}\n\nWyświetlanie wiadomości offline.", + "Notifications_StoreUpdateAvailableTitle": "Aktualizacja dostępna", + "Notifications_StoreUpdateAvailableMessage": "Nowsza wersja Wino Mail jest gotowa do zainstalowania w Microsoft Store.", + "OnlineSearchFailed_Message": "Failed to perform search\n{0}\n\nListing offline mails.", "OnlineSearchTry_Line1": "Can't find what you are looking for?", "OnlineSearchTry_Line2": "Try online search.", "Other": "Inne", @@ -446,28 +664,32 @@ "PaneLengthOption_Small": "Mała", "Photos": "Zdjęcia", "PreparingFoldersMessage": "Przygotowywanie folderów", - "ProtocolLogAvailable_Message": "Rejestry protokołów są dostępne do diagnostyki.", "ProviderDetail_Gmail_Description": "Konto Google", - "ProviderDetail_iCloud_Description": "Konto Apple iCloud", + "ProviderDetail_iCloud_Description": "Apple iCloud Account", "ProviderDetail_iCloud_Title": "iCloud", "ProviderDetail_IMAP_Description": "Serwer IMAP/SMTP", "ProviderDetail_IMAP_Title": "Serwer IMAP", - "ProviderDetail_Yahoo_Description": "Konto Yahoo", + "ProviderDetail_Yahoo_Description": "Yahoo Account", "ProviderDetail_Yahoo_Title": "Yahoo Mail", - "QuickEventDialog_EventName": "Nazwa wydarzenia", - "QuickEventDialog_IsAllDay": "Cały dzień", - "QuickEventDialog_Location": "Miejsce", - "QuickEventDialog_RemindMe": "Przypomnij mi", - "QuickEventDialogMoreDetailsButtonText": "Szczegóły", + "QuickEventDialog_EventName": "Event name", + "QuickEventDialog_IsAllDay": "All day", + "QuickEventDialog_Location": "Location", + "QuickEventDialog_RemindMe": "Remind me", + "QuickEventDialogMoreDetailsButtonText": "More details", "Reader_SaveAllAttachmentButtonText": "Zapisz wszystkie załączniki", "Results": "Wyniki", "Right": "Wyrównaj do prawej", "SearchBarPlaceholder": "Szukaj", "SearchingIn": "Szukanie w", "SearchPivotName": "Wyniki", + "Settings_KeyboardShortcuts_Title": "Skróty klawiszowe", + "Settings_KeyboardShortcuts_Description": "Zarządzaj skrótami klawiszowymi do szybkich akcji w wiadomościach.", "SettingConfigureSpecialFolders_Button": "Konfiguruj", - "SettingsEditAccountDetails_IMAPConfiguration_Title": "Ustawienia IMAP/SMTP", - "SettingsEditAccountDetails_IMAPConfiguration_Description": "Zmień ustawienia serwera przychodzenia/wychodzącego.", + "SettingsEditAccountDetails_IMAPConfiguration_Title": "IMAP/SMTP Configuration", + "SettingsEditAccountDetails_IMAPConfiguration_Description": "Change your incoming/outgoing server settings.", + "SettingsEditAccountDetails_ImapCalDavSettings_Title": "Ustawienia IMAP i kalendarza", + "SettingsEditAccountDetails_ImapCalDavSettings_Description": "Otwórz dedykowaną stronę ustawień IMAP, SMTP i CalDAV dla tego konta.", + "SettingsEditAccountDetails_ImapCalDavSettings_Action": "Otwórz ustawienia", "SettingsAbout_Description": "Dowiedz się więcej o Wino.", "SettingsAbout_Title": "O aplikacji", "SettingsAboutGithub_Description": "Przejdź do repozytorium trackera zgłoszeń GitHub.", @@ -486,10 +708,14 @@ "SettingsAppPreferences_CloseBehavior_Description": "Co powinno się wydarzyć po zamknięciu aplikacji?", "SettingsAppPreferences_CloseBehavior_Title": "Zachowanie zamknięcia aplikacji", "SettingsAppPreferences_Description": "Ustawienia ogólne / preferencje dla Wino Mail.", - "SettingsAppPreferences_SearchMode_Description": "Ustaw, czy Wino ma najpierw sprawdzać pobrane wiadomości podczas wyszukiwania, czy też zapytać o nie bezpośrednio serwer pocztowy online. Wyszukiwanie lokalne jest zawsze szybsze, a w razie potrzeby możesz wykonać wyszukiwanie online, jeśli Twoja wiadomość nie pojawi się w wynikach.", - "SettingsAppPreferences_SearchMode_Local": "Na tym urządzeniu", + "SettingsAppPreferences_SearchMode_Description": "Set whether Wino should check fetched mails first while doing a search or ask your mail server online. Local search is always faster and you can always do an online search if your mail is not in the results.", + "SettingsAppPreferences_SearchMode_Local": "Local", "SettingsAppPreferences_SearchMode_Online": "Online", - "SettingsAppPreferences_SearchMode_Title": "Domyślny tryb wyszukiwania", + "SettingsAppPreferences_SearchMode_Title": "Default search mode", + "SettingsAppPreferences_ApplicationMode_Title": "Domyślny tryb aplikacji", + "SettingsAppPreferences_ApplicationMode_Description": "Wybierz tryb uruchamiania Wino, gdy żaden typ aktywacji nie jest jawnie ustawiony.", + "SettingsAppPreferences_ApplicationMode_Mail": "Poczta", + "SettingsAppPreferences_ApplicationMode_Calendar": "Kalendarz", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description": "Wino Mail będzie nadal działać w tle. Zostaniesz powiadomiony, gdy pojawią się nowe wiadomości.", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title": "Działanie w tle", "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description": "Wino Mail będzie nadal działać w zasobniku systemowym. Aplikacja będzie dostępna do uruchomienia klikając na ikonę. Zostaniesz powiadomiony o pojawieniu się nowych wiadomości.", @@ -506,12 +732,30 @@ "SettingsAppPreferences_StartupBehavior_FatalError": "Wystąpił błąd krytyczny podczas zmiany trybu uruchamiania Wino Mail.", "SettingsAppPreferences_StartupBehavior_Title": "Uruchom zminimalizowany przy starcie Windowsa", "SettingsAppPreferences_Title": "Preferencje Aplikacji", + "SettingsAppPreferences_HideWinoAccountButton_Title": "Ukryj przycisk konta Wino na pasku tytułu", + "SettingsAppPreferences_HideWinoAccountButton_Description": "Ukryj przycisk profilu na pasku tytułu, który otwiera wysuwane menu konta Wino.", + "SettingsAppPreferences_StoreUpdateNotifications_Title": "Powiadomienia o aktualizacjach sklepu", + "SettingsAppPreferences_StoreUpdateNotifications_Description": "Pokaż powiadomienia i akcje w stopce, gdy dostępna jest aktualizacja Microsoft Store.", + "SettingsAppPreferences_AiActions_Title": "Działania AI", + "SettingsAppPreferences_AiActions_Description": "Wybierz domyślne języki AI i miejsce zapisywania streszczeń.", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Title": "Domyślny język tłumaczenia", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Description": "Wybierz domyślny język docelowy używany przez akcje tłumaczenia AI.", + "SettingsAppPreferences_AiSummarizeLanguage_Title": "Język podsumowania", + "SettingsAppPreferences_AiSummarizeLanguage_Description": "Wybierz preferowany język podsumowań dla przyszłych wyników AI.", + "SettingsAppPreferences_AiSummarySavePath_Title": "Domyślna ścieżka zapisywania streszczeń", + "SettingsAppPreferences_AiSummarySavePath_Description": "Wybierz folder, który Wino ma używać domyślnie do zapisywania streszczeń AI.", + "SettingsAppPreferences_AiSummarySavePath_Placeholder": "Użyj domyślnej lokalizacji zapisu systemu", + "SettingsAppPreferences_AiSummarySavePath_InvalidHint": "Ten folder nie istnieje. Zostanie użyta domyślna lokalizacja zapisu dla streszczeń.", "SettingsAutoSelectNextItem_Description": "Wybierz następny element po usunięciu lub przesunięciu wiadomości.", "SettingsAutoSelectNextItem_Title": "Automatycznie wybierz następny element", "SettingsAvailableThemes_Description": "Wybierz motyw z kolekcji Wino wedle uznania lub zastosuj własne motywy.", "SettingsAvailableThemes_Title": "Dostępne motywy", - "SettingsCalendarSettings_Description": "Zmień pierwszy dzień tygodnia, wysokość komórki godziny i inne...", - "SettingsCalendarSettings_Title": "Ustawienia kalendarza", + "SettingsCalendarSettings_Description": "Change first day of week, hour cell height and more...", + "SettingsCalendarSettings_Title": "Calendar Settings", + "CalendarSettings_DefaultSnoozeDuration_Header": "Domyślny czas odroczenia", + "CalendarSettings_DefaultSnoozeDuration_Description": "Ustaw domyślny czas odroczenia powiadomień przypomnień kalendarza.", + "CalendarSettings_TimedDayHeaderFormat_Header": "Format nagłówka dnia w widoku z czasem", + "CalendarSettings_TimedDayHeaderFormat_Description": "Wybierz, jak będą renderowane górne etykiety dni w widokach dzień, tydzień i tydzień roboczy. Używaj tokenów formatu daty takich jak ddd, dd, MMM lub dddd.", "SettingsComposer_Title": "Edytor", "SettingsComposerFont_Title": "Domyślna czcionka przy pisaniu wiadomości", "SettingsComposerFontFamily_Description": "Zmień domyślny rozmiar czcionki i czcionki dla pisanej wiadomości.", @@ -521,16 +765,19 @@ "SettingsCustomTheme_Title": "Własny motyw", "SettingsDeleteAccount_Description": "Usuń wszystkie e-maile i dane uwierzytelniające powiązane z tym kontem.", "SettingsDeleteAccount_Title": "Usuń to konto", - "SettingsDeleteProtection_Description": "Czy Wino powinno poprosić Cię o potwierdzenie za każdym razem, gdy próbujesz trwale usunąć wiadomość używając klawiszy Shift + Del?", + "SettingsDeleteProtection_Description": "Should Wino ask you for confirmation every time you try to permanently delete a mail using Shift + Del keys?", "SettingsDeleteProtection_Title": "Ochrona przed trwałym usunięciem", "SettingsDiagnostics_Description": "Dla programistów", - "SettingsDiagnostics_DiagnosticId_Description": "Udostępnij ten identyfikator deweloperom, gdy zostaniesz poproszony o pomoc w rozwiązaniu problemów, które napotykasz w Wino Mail.", - "SettingsDiagnostics_DiagnosticId_Title": "ID diagnostyczne", + "SettingsDiagnostics_DiagnosticId_Description": "Share this ID with the developers when asked to get help for the issues you experience in Wino Mail.", + "SettingsDiagnostics_DiagnosticId_Title": "Diagnostic ID", "SettingsDiagnostics_Title": "Diagnostyka", "SettingsDiscord_Description": "Otrzymuj regularne informacje dotyczące rozwoju aplikacji, dołącz do dyskusji o roadmapie i przedstawiaj opinie.", "SettingsDiscord_Title": "Kanał Discord", "SettingsEditLinkedInbox_Description": "Dodaj / usuń konta, zmień nazwę lub zerwij powiązanie między kontami.", "SettingsEditLinkedInbox_Title": "Edytuj połączoną skrzynkę odbiorczą", + "SettingsWindowBackdrop_Title": "Tło okna", + "SettingsWindowBackdrop_Description": "Wybierz efekt tła dla okien Wino.", + "SettingsWindowBackdrop_Disabled": "Wybór tła okna jest wyłączony, gdy motyw aplikacji nie jest ustawiony na Domyślny.", "SettingsElementTheme_Description": "Wybierz motyw Windows dla Wino", "SettingsElementTheme_Title": "Motyw elementu", "SettingsElementThemeSelectionDisabled": "Wybór motywu elementu jest wyłączony, gdy wybrany motyw aplikacji jest inny niż Domyślny.", @@ -569,7 +816,7 @@ "SettingsLinkedAccountsSave_Description": "Modyfikuj bieżące połączenie z nowymi kontami.", "SettingsLinkedAccountsSave_Title": "Zapisz zmiany", "SettingsLoadImages_Title": "Automatycznie wczytuj obrazy", - "SettingsLoadPlaintextLinks_Title": "Konwertuj zwykłe (tekstowe) linki na klikalne linki", + "SettingsLoadPlaintextLinks_Title": "Convert plaintext links to clickable links", "SettingsLoadStyles_Title": "Automatycznie wczytuj style", "SettingsMailListActionBar_Description": "Ukryj/pokaż pasek akcji na górze listy wiadomości.", "SettingsMailListActionBar_Title": "Pokaż akcje listy maili", @@ -579,8 +826,10 @@ "SettingsManageAccountSettings_Title": "Zarządzaj ustawieniami konta", "SettingsManageAliases_Description": "Zobacz aliasy e-mail przypisane do tego konta, zaktualizuj lub usuń je.", "SettingsManageAliases_Title": "Aliasy", - "SettingsEditAccountDetails_Title": "Edytuj szczegóły konta", - "SettingsEditAccountDetails_Description": "Zmień nazwę konta, nazwę nadawcy i przypisz nowy kolor, jeśli chcesz.", + "SettingsEditAccountDetails_Title": "Edit Account Details", + "SettingsEditAccountDetails_Description": "Change account name, sender name and assign a new color if you like.", + "EditAccountDetailsPage_SaveSuccess_Title": "Zmiany zapisane", + "EditAccountDetailsPage_SaveSuccess_Message": "Dane konta zostały pomyślnie zaktualizowane.", "SettingsManageLink_Description": "Przenieś elementy, aby dodać nowe połączenie lub usunąć istniejące połączenie.", "SettingsManageLink_Title": "Zarządzaj połączeniem kont", "SettingsMarkAsRead_Description": "Zmień, co powinno się stać z zaznaczonym elementem.", @@ -594,9 +843,43 @@ "SettingsNoAccountSetupMessage": "Nie skonfigurowano jeszcze żadnych kont.", "SettingsNotifications_Description": "Włącz lub wyłącz powiadomienia dla tego konta.", "SettingsNotifications_Title": "Powiadomienia", - "SettingsNotificationsAndTaskbar_Description": "Zmień, czy powiadomienia mają być wyświetlane oraz czy ma być pokazywana plakietka na pasku zadań dla tego konta.", - "SettingsNotificationsAndTaskbar_Title": "Powiadomienia i Pasek zadań", + "SettingsNotificationsAndTaskbar_Description": "Change whether notifications should be displayed and taskbar badge for this account.", + "SettingsNotificationsAndTaskbar_Title": "Notifications & Taskbar", + "SettingsHome_Title": "Strona główna", + "SettingsHome_SearchTitle": "Znajdź ustawienie", + "SettingsHome_SearchDescription": "Wyszukaj według funkcji, tematu lub słowa kluczowego, aby przejść bezpośrednio do właściwej strony ustawień.", + "SettingsHome_SearchPlaceholder": "Szukaj ustawień", + "SettingsHome_SearchExamples": "Przykłady: motyw, przechowywanie, język, podpis", + "SettingsHome_QuickLinks_Title": "Szybkie linki", + "SettingsHome_QuickLinks_Description": "Szybki dostęp do ustawień, do których ludzie najczęściej sięgają.", + "SettingsHome_StorageCard_Description": "Zobacz, ile lokalnych treści MIME gromadzi Wino na tym urządzeniu i oczyść je w razie potrzeby.", + "SettingsHome_StorageEmptySummary": "Na razie nie wykryto żadnych danych MIME w pamięci podręcznej.", + "SettingsHome_StorageLoading": "Sprawdzanie lokalnego użycia MIME...", + "SettingsHome_Tips_Title": "Wskazówki i sztuczki", + "SettingsHome_Tips_Description": "Kilka drobnych zmian może sprawić, że Wino stanie się bardziej osobiste.", + "SettingsHome_Tip_Theme": "Chcesz tryb ciemny lub zmiany akcentów? Otwórz Personalizację.", + "SettingsHome_Tip_Background": "Użyj preferencji aplikacji, aby kontrolować uruchamianie i synchronizację w tle.", + "SettingsHome_Tip_Shortcuts": "Skróty klawiszowe pomagają szybciej przeglądać wiadomości.", + "SettingsHome_Resources_Title": "Przydatne linki", + "SettingsHome_Resources_Description": "Otwórz zasoby projektu, informacje wsparcia i kanały wydań.", "SettingsOptions_Title": "Ustawienia", + "SettingsOptions_GeneralSection": "Ogólne", + "SettingsOptions_MailSection": "Poczta", + "SettingsOptions_CalendarSection": "Kalendarz", + "SettingsOptions_MoreComingSoon": "Więcej opcji wkrótce", + "SettingsOptions_HeroDescription": "Dostosuj swoje doświadczenie z Wino Mail.", + "SettingsOptions_AccountsSummary": "{0} konto(-ów) skonfigurowane", + "SettingsSearch_ManageAccounts_Keywords": "konto;konta;skrzynka;skrzynki;alias;aliasy;profil;adres;adresy", + "SettingsSearch_AppPreferences_Keywords": "uruchamianie;tło;uruchamianie;synchronizacja;powiadomienie;powiadomienia;wyszukiwanie;tray;domyślne", + "SettingsSearch_LanguageTime_Keywords": "język;czas;zegar;lokalizacja;region;format;24 godziny;24h", + "SettingsSearch_Personalization_Keywords": "motyw;ciemny;jasny;wygląd;akcent;kolor;kolor;tryb;układ;gęstość", + "SettingsSearch_About_Keywords": "o;wersja;strona;prywatność;GitHub;dotacje;sklep;wsparcie", + "SettingsSearch_KeyboardShortcuts_Keywords": "skrót;skróty;hotkey;hotkeys;klawisz;klawisze", + "SettingsSearch_MessageList_Keywords": "wiadomość;wiadomości;lista;wątki;wątki;avatar;podgląd;nadawca", + "SettingsSearch_ReadComposePane_Keywords": "czytnik;komponowanie;kompozytor;czcionka;czcionki;zawartość zewnętrzna;wyświetlanie;czytanie", + "SettingsSearch_SignatureAndEncryption_Keywords": "podpis;podpisy;szyfrowanie;certyfikat;certyfikaty;S/MIME;S/MIME;bezpieczeństwo", + "SettingsSearch_Storage_Keywords": "przechowywanie;pamięć podręczna;buforowanie;MIME;dysk;przestrzeń;czyszczenie;wyczyść;dane lokalne", + "SettingsSearch_CalendarSettings_Keywords": "kalendarz;tydzień;godziny;harmonogram;wydarzenie;wydarzenia", "SettingsPaneLengthReset_Description": "Zresetuj rozmiar listy mailowej do oryginalnego, jeśli masz problemy z nim.", "SettingsPaneLengthReset_Title": "Zresetuj rozmiar listy pocztowej", "SettingsPaypal_Description": "Okaż dużo więcej miłości ❤️ Za wszystkie darowizny dziękuję.", @@ -610,6 +893,8 @@ "SettingsPrefer24HourClock_Title": "Wyświetl czas w formacie 24-godzinnym", "SettingsPrivacyPolicy_Description": "Zapoznaj się z Polityką prywatności.", "SettingsPrivacyPolicy_Title": "Politykę prywatności", + "SettingsWebsite_Description": "Otwórz stronę Wino Mail.", + "SettingsWebsite_Title": "Strona internetowa", "SettingsReadComposePane_Description": "Czcionki, treści zewnętrzne.", "SettingsReadComposePane_Title": "Czytelnik i edytor", "SettingsReader_Title": "Czytelnik", @@ -625,11 +910,24 @@ "SettingsShowPreviewText_Title": "Pokaż podgląd tekstu", "SettingsShowSenderPictures_Description": "Ukryj/pokaż miniaturki zdjęć nadawcy.", "SettingsShowSenderPictures_Title": "Pokaż awatary nadawcy", + "SettingsEmailTemplates_Title": "Szablony e-mail", + "SettingsEmailTemplates_Description": "Zarządzaj szablonami wiadomości e-mail.", + "SettingsEmailTemplates_CreatePageTitle": "Nowy szablon", + "SettingsEmailTemplates_EditPageTitle": "Edytuj szablon", + "SettingsEmailTemplates_NewTemplateTitle": "Nowy szablon", + "SettingsEmailTemplates_NewTemplateDescription": "Utwórz nowy szablon e-mail", + "SettingsEmailTemplates_NameTitle": "Nazwa", + "SettingsEmailTemplates_NamePlaceholder": "Nazwa szablonu", + "SettingsEmailTemplates_DescriptionTitle": "Opis", + "SettingsEmailTemplates_DescriptionPlaceholder": "Opcjonalny opis", + "SettingsEmailTemplates_ContentTitle": "Treść szablonu", + "SettingsEmailTemplates_ContentDescription": "Edytuj zawartość HTML tego szablonu.", + "SettingsEmailTemplates_NameRequired": "Nazwa szablonu jest wymagana.", "SettingsEnableGravatarAvatars_Title": "Gravatar", - "SettingsEnableGravatarAvatars_Description": "Użyj gravatara (jeśli dostępny) jako zdjęcia nadawcy", - "SettingsEnableFavicons_Title": "Ikony domen (Favicons)", - "SettingsEnableFavicons_Description": "Użyj gravatara (jeśli dostępny) jako zdjęcia nadawcy", - "SettingsMailList_ClearAvatarsCache_Button": "Wyczyść cache awatarów", + "SettingsEnableGravatarAvatars_Description": "Use gravatar (if available) as sender picture", + "SettingsEnableFavicons_Title": "Domain icons (Favicons)", + "SettingsEnableFavicons_Description": "Use domain favicons (if available) as sender picture", + "SettingsMailList_ClearAvatarsCache_Button": "Clear cached avatars", "SettingsSignature_AddCustomSignature_Button": "Dodaj podpis", "SettingsSignature_AddCustomSignature_Title": "Dodaj własny podpis", "SettingsSignature_DeleteSignature_Title": "Usuń podpis", @@ -645,14 +943,41 @@ "SettingsStartupItem_Title": "Obiekt startowy", "SettingsStore_Description": "Okaż trochę miłości ❤️", "SettingsStore_Title": "Oceń w sklepie", - "SettingsTaskbarBadge_Description": "Dołącz licznik nieprzeczytanych wiadomości do paska zadań.", + "SettingsStorage_Title": "Przechowywanie", + "SettingsStorage_Description": "Skanuj i zarządzaj pamięcią podręczną MIME przechowywaną w lokalnym folderze danych.", + "SettingsStorage_ScanFolder": "Skanuj lokalny folder danych", + "SettingsStorage_NoLocalMimeDataFound": "Nie znaleziono lokalnych danych MIME.", + "SettingsStorage_NoAccountsFound": "Nie znaleziono kont.", + "SettingsStorage_TotalUsage": "Całkowite użycie MIME lokalnie: {0}", + "SettingsStorage_AccountUsageDescription": "{0} użyte w lokalnej pamięci podręcznej MIME", + "SettingsStorage_DeleteAll_Title": "Usuń całą zawartość MIME", + "SettingsStorage_DeleteAll_Description": "Usuń cały folder pamięci podręcznej MIME dla tego konta.", + "SettingsStorage_DeleteAll_Button": "Usuń wszystkie", + "SettingsStorage_DeleteAll_Confirm_Title": "Usuń całą zawartość MIME", + "SettingsStorage_DeleteAll_Confirm_Message": "Czy usunąć wszystkie lokalne dane MIME dla {0}?", + "SettingsStorage_DeleteAll_Success": "Wszystkie treści MIME zostały usunięte.", + "SettingsStorage_DeleteOld_Title": "Usuń stare zawartości MIME", + "SettingsStorage_DeleteOld_Description": "Usuń pliki MIME na podstawie daty utworzenia wiadomości w lokalnej bazie danych.", + "SettingsStorage_DeleteOld_1Month": "> 1 miesiąc", + "SettingsStorage_DeleteOld_3Months": "> 3 miesiące", + "SettingsStorage_DeleteOld_6Months": "> 6 miesięcy", + "SettingsStorage_DeleteOld_1Year": "> 1 rok", + "SettingsStorage_DeleteOld_Confirm_Title": "Usuń stare zawartości MIME", + "SettingsStorage_DeleteOld_Confirm_Message": "Czy usunąć lokalne dane MIME starsze niż {0} dla {1}?", + "SettingsStorage_DeleteOld_Success": "Usunięto {0} folder(y) MIME starszych niż {1}.", + "SettingsStorage_1Month": "1 miesiąc", + "SettingsStorage_3Months": "3 miesiące", + "SettingsStorage_6Months": "6 miesięcy", + "SettingsStorage_1Year": "1 rok", + "SettingsStorage_Months": "{0} miesięcy", + "SettingsTaskbarBadge_Description": "Include unread mail count in taskbar icon.", "SettingsTaskbarBadge_Title": "Ikona paska zadań", "SettingsThreads_Description": "Organizuj wiadomości w wątki konwersacji.", "SettingsThreads_Title": "Wątkowanie konwersacji", "SettingsUnlinkAccounts_Description": "Usuń łącze pomiędzy kontami. To nie spowoduje usunięcie Twoich kont.", "SettingsUnlinkAccounts_Title": "Odłącz konto", - "SettingsMailRendering_ActionLabels_Title": "Etykiety działań", - "SettingsMailRendering_ActionLabels_Description": "Pokaż etykiety działań.", + "SettingsMailRendering_ActionLabels_Title": "Action labels", + "SettingsMailRendering_ActionLabels_Description": "Show action labels.", "SignatureDeleteDialog_Message": "Czy na pewno chcesz usunąć podpis \"{0}\"?", "SignatureDeleteDialog_Title": "Usuń podpis", "SignatureEditorDialog_SignatureName_Placeholder": "Nazwij swój podpis", @@ -683,6 +1008,9 @@ "SystemFolderConfigDialogValidation_InboxSelected": "Nie można przypisać folderu skrzynki odbiorczej do innego folderu systemowego.", "SystemFolderConfigSetupSuccess_Message": "Foldery systemowe zostały pomyślnie skonfigurowane.", "SystemFolderConfigSetupSuccess_Title": "Konfiguracja folderów systemowych", + "SystemTrayMenu_ShowWino": "Otwórz Wino Mail", + "SystemTrayMenu_ShowWinoCalendar": "Otwórz Kalendarz Wino", + "SystemTrayMenu_ExitWino": "Zamknij", "TestingImapConnectionMessage": "Testowanie połączenia z serwerem...", "TitleBarServerDisconnectedButton_Description": "Wino jest odłączone od sieci. Kliknij 'połącz ponownie', aby przywrócić połączenie.", "TitleBarServerDisconnectedButton_Title": "brak połączenia", @@ -699,8 +1027,422 @@ "WinoUpgradeMessage": "Ulepsz do nielimitowanych kont", "WinoUpgradeRemainingAccountsMessage": "Wykorzystano {0} z {1} darmowych kont.", "Yesterday": "Wczoraj", - "SettingsAppPreferences_EmailSyncInterval_Title": "Interwał synchronizacji poczty", - "SettingsAppPreferences_EmailSyncInterval_Description": "Interwał automatycznej synchronizacji wiadomości e-mail (minuty). To ustawienie zostanie zastosowane dopiero po ponownym uruchomieniu aplikacji Wino Mail." + "Smime_ImportCertificates_Success": "Certyfikaty zaimportowano pomyślnie.", + "Smime_ImportCertificates_Error": "Błąd przy imporcie certyfikatów: {0}", + "Smime_RemoveCertificates_Confirm": "Czy na pewno chcesz usunąć certyfikaty {0}?", + "Smime_RemoveCertificates_Success": "Certyfikaty zostały usunięte.", + "Smime_ExportCertificates_Success": "Certyfikaty wyeksportowano.", + "Smime_ExportCertificates_Error": "Błąd eksportowania certyfikatów.", + "Smime_CertificateDetails": "Podmiot: {0}\nWystawca: {1}\nWażny od: {2}\nWażny do: {3}\nOdcisk palca: {4}", + "Smime_CertificatePassword_Title": "Wymagane hasło do certyfikatu", + "Smime_CertificatePassword_Placeholder": "Hasło certyfikatu dla {0} (opcjonalnie)", + "Smime_Confirm_Title": "Potwierdź", + "Buttons_OK": "OK", + "Buttons_Refresh": "Odśwież", + "SettingsSignatureAndEncryption_Title": "Podpisywanie i szyfrowanie", + "SettingsSignatureAndEncryption_Description": "Zarządzaj certyfikatami S/MIME do podpisywania i szyfrowania wiadomości e-mail.", + "SettingsSignatureAndEncryption_MyCertificatesHeader": "Moje certyfikaty", + "SettingsSignatureAndEncryption_MyCertificatesDescription": "Certyfikaty osobiste do podpisywania i szyfrowania", + "SettingsSignatureAndEncryption_RecipientCertificatesHeader": "Certyfikaty odbiorców", + "SettingsSignatureAndEncryption_RecipientCertificatesDescription": "Certyfikaty odbiorców do odszyfrowywania", + "SettingsSignatureAndEncryption_NameColumn": "Nazwa", + "SettingsSignatureAndEncryption_ExpiresColumn": "Wygasa dnia", + "SettingsSignatureAndEncryption_ThumbprintColumn": "Odcisk palca", + "Buttons_Remove": "Usuń", + "Buttons_Export": "Eksportuj", + "Buttons_Import": "Importuj", + "SettingsSignatureAndEncryption_SigningCertificate": "Certyfikat podpisu S/MIME", + "SettingsSignatureAndEncryption_EncryptionCertificate": "Certyfikat szyfrowania S/MIME", + "SettingsSignatureAndEncryption_SigningCertificatePlaceholder": "Brak", + "SmimeSignaturesInMessage": "Podpisy w tej wiadomości:", + "SmimeSignatureEntry": "• {0} {1} ({2}, ważny do {3} - {4})", + "SmimeSigningCertificateInfoTitle": "Informacje o certyfikacie podpisu S/MIME", + "SmimeCertificateInfoTitle": "Informacje o certyfikacie S/MIME", + "SmimeNoCertificateFileFound": "Nie znaleziono pliku certyfikatu", + "SmimeSaveCertificate": "Zapisz certyfikat...", + "SmimeCertificate": "Certyfikat S/MIME", + "SmimeCertificateSavedTo": "Certyfikat zapisany w {0}", + "SmimeSignedTooltip": "Ta wiadomość jest podpisana certyfikatem S/MIME. Kliknij, aby uzyskać więcej szczegółów", + "SmimeEncryptedTooltip": "Ta wiadomość jest zaszyfrowana certyfikatem S/MIME.", + "SmimeCertificateFileInfo": "Plik: {0}\nTyp: {1}\nRozmiar: {2:N0} bajtów", + "Composer_LightTheme": "Jasny motyw", + "Composer_DarkTheme": "Ciemny motyw", + "Composer_Outdent": "Cofnij wcięcie", + "Composer_Indent": "Wcięcie", + "Composer_BulletList": "Listy punktowane", + "Composer_OrderedList": "Listy numerowane", + "Composer_Stroke": "Obrys", + "Composer_Bold": "Pogrubienie", + "Composer_Italic": "Kursywa", + "Composer_Underline": "Podkreślenie", + "Composer_CcBcc": "DW i UDW", + "Composer_EnableSmimeSignature": "Włącz/wyłącz podpis S/MIME", + "Composer_EnableSmimeEncryption": "Włącz/wyłącz szyfrowanie S/MIME", + "Composer_LocalDraftSyncInfo": "Ten szkic jest wyłącznie lokalny. Wino nie udało się go wysłać na serwer pocztowy. Kliknij, aby ponownie spróbować wysłać go na serwer.", + "Composer_CertificateExpires": "Wygasa:", + "Composer_SmimeSignature": "Podpis S/MIME", + "Composer_SmimeEncryption": "Szyfrowanie S/MIME", + "Composer_EmailTemplatesPlaceholder": "Szablony wiadomości e-mail", + "Composer_AiSummarize": "Streszczaj AI", + "Composer_AiSummarizeDescription": "Wyodrębnia kluczowe punkty, zadania do wykonania i decyzje z tej wiadomości.", + "Composer_AiTranslate": "Tłumacz za pomocą AI", + "Composer_AiActions": "Działania AI", + "Composer_AiRewrite": "Przepisz za pomocą AI", + "AiActions_CheckingStatus": "Sprawdzanie dostępu do AI...", + "AiActions_SignedOutTitle": "Odblokuj pakiet Wino AI", + "AiActions_SignedOutDescription": "Tłumaczenie, przepisywanie i streszczanie wiadomości za pomocą AI po zalogowaniu do konta Wino i aktywacji dodatku AI Pack.", + "AiActions_NoPackTitle": "Wymagany pakiet AI", + "AiActions_NoPackDescription": "Jesteś zalogowany, ale pakiet AI nie jest jeszcze aktywny. Wykup go, aby korzystać z tłumaczenia, przepisywania i streszczania AI w Wino.", + "AiActions_UsageSummary": "{0} z {1} kredytów wykorzystano w tym miesiącu.", + "Composer_AiRewritePolite": "Uczyń to grzeczniejszym", + "Composer_AiRewritePoliteDescription": "Złagodza ton, zachowując ten sam sens.", + "Composer_AiRewriteAngry": "Uczyń to agresywnym", + "Composer_AiRewriteAngryDescription": "Używa ostrzejszego i bardziej konfrontacyjnego tonu.", + "Composer_AiRewriteHappy": "Spraw, by to było radosne.", + "Composer_AiRewriteHappyDescription": "Dodaje ton bardziej pogodny i entuzjastyczny.", + "Composer_AiRewriteFormal": "Uczyń to formalnym", + "Composer_AiRewriteFormalDescription": "Sprawia, że wiadomość brzmi bardziej profesjonalnie i jest bardziej uporządkowana.", + "Composer_AiRewriteFriendly": "Spraw, by to było przyjazne.", + "Composer_AiRewriteFriendlyDescription": "Sprawia, że wiadomość staje się bardziej przyjazna.", + "Composer_AiRewriteShorter": "Uczyń to krótsze", + "Composer_AiRewriteShorterDescription": "Skraca tekst i usuwa zbędne szczegóły.", + "Composer_AiRewriteClearer": "Spraw, by to było jaśniejsze.", + "Composer_AiRewriteClearerDescription": "Poprawia czytelność i sprawia, że wiadomość jest łatwiejsza do zrozumienia.", + "Composer_AiRewriteCustom": "Własne", + "Composer_AiRewriteCustomDescription": "Opisz własny cel przepisu.", + "Composer_AiRewriteCustomPlaceholder": "Opisz, jak chcesz, aby wiadomość została przepisana", + "Composer_AiRewriteMode": "Ton przepisu", + "Composer_AiRewriteApply": "Zastosuj przepisanie", + "Composer_AiTranslateDialogTitle": "Tłumacz z AI", + "Composer_AiTranslateDialogDescription": "Wprowadź docelowy język lub kod kultury, np. en-US, tr-TR, de-DE lub fr-FR.", + "Composer_AiTranslateApply": "Przetłumacz", + "Composer_AiTranslateLanguage": "Język docelowy", + "Composer_AiTranslateCustomPlaceholder": "Wprowadź kod kultury", + "Composer_AiTranslateLanguageEnglish": "Angielski (en-US)", + "Composer_AiTranslateLanguageTurkish": "Turecki (tr-TR)", + "Composer_AiTranslateLanguageGerman": "Niemiecki (de-DE)", + "Composer_AiTranslateLanguageFrench": "Francuski (fr-FR)", + "Composer_AiTranslateLanguageSpanish": "Hiszpański (es-ES)", + "Composer_AiTranslateLanguageItalian": "Włoski (it-IT)", + "Composer_AiTranslateLanguagePortugueseBrazil": "Portugalski (pt-BR)", + "Composer_AiTranslateLanguageDutch": "Niderlandzki (nl-NL)", + "Composer_AiTranslateLanguagePolish": "Polski (pl-PL)", + "Composer_AiTranslateLanguageRussian": "Rosyjski (ru-RU)", + "Composer_AiTranslateLanguageJapanese": "Japoński (ja-JP)", + "Composer_AiTranslateLanguageKorean": "Koreański (ko-KR)", + "Composer_AiTranslateLanguageChineseSimplified": "Chiński, uproszczony (zh-CN)", + "Composer_AiTranslateLanguageArabic": "Arabski (ar-SA)", + "Composer_AiTranslateLanguageHindi": "Hindi (hi-IN)", + "Composer_AiTranslateLanguageOther": "Inne...", + "Composer_AiBusyTitle": "AI już działa", + "Composer_AiBusyMessage": "Proszę poczekać, aż zakończy się bieżąca operacja AI.", + "Composer_AiSignInRequired": "Zaloguj się na konto Wino, aby korzystać z funkcji AI.", + "Composer_AiMissingHtml": "Nie ma jeszcze treści wiadomości do wysłania do Wino AI.", + "Composer_AiQuotaUnavailable": "Wynik AI został zastosowany.", + "Composer_AiAppliedMessage": "Wynik AI został zastosowany w komponencie. Użyj Cofnij, jeśli chcesz cofnąć zmianę.", + "Composer_AiSummarizeSuccessTitle": "Podsumowanie AI zostało zastosowane.", + "Composer_AiTranslateSuccessTitle": "Tłumaczenie AI zostało zastosowane.", + "Composer_AiRewriteSuccessTitle": "Przepis AI został zastosowany.", + "Composer_AiErrorTitle": "Akcja AI nie powiodła się.", + "Reader_AiAppliedMessage": "Wynik AI jest teraz wyświetlany dla tej wiadomości. Otwórz ponownie wiadomość, aby ponownie zobaczyć oryginalną treść.", + "SettingsAppPreferences_EmailSyncInterval_Title": "Email sync interval", + "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail.", + "ContactsPage_Title": "Kontakty", + "ContactsPage_AddContact": "Dodaj kontakt", + "ContactsPage_EditContact": "Edytuj kontakt", + "ContactsPage_DeleteContact": "Usuń kontakt", + "ContactsPage_SearchPlaceholder": "Wyszukaj kontakty...", + "ContactsPage_NoContacts": "Nie znaleziono kontaktów", + "ContactsPage_ContactsCount": "{0} kontaktów", + "ContactsPage_SelectedContactsCount": "{0} wybranych", + "ContactsPage_DeleteSelectedContacts": "Usuń wybrane", + "ContactEditDialog_Title": "Edytuj kontakt", + "ContactEditDialog_PhotoSection": "Zdjęcie", + "ContactEditDialog_ChoosePhoto": "Wybierz zdjęcie", + "ContactEditDialog_RemovePhoto": "Usuń zdjęcie", + "ContactEditDialog_NameHeader": "Nazwa", + "ContactEditDialog_NamePlaceholder": "Nazwa kontaktu", + "ContactEditDialog_EmailHeader": "Adres e-mail", + "ContactEditDialog_EmailPlaceholder": "kontakt@example.com", + "ContactEditDialog_InfoSection": "Informacje kontaktowe", + "ContactEditDialog_RootContactInfo": "To jest główny kontakt powiązany z twoimi kontami i nie może być usunięty.", + "ContactEditDialog_OverriddenContactInfo": "Ten kontakt został ręcznie zmodyfikowany i nie będzie aktualizowany podczas synchronizacji.", + "ContactsPage_Subtitle": "Zarządzaj kontaktami e-mail i ich informacjami", + "ContactStatus_Account": "Konto", + "ContactStatus_Modified": "Zmodyfikowano", + "ContactAction_Edit": "Edytuj kontakt", + "ContactAction_ChangePhoto": "Zmień zdjęcie", + "ContactAction_Delete": "Usuń kontakt", + "ContactAction_Add": "Dodaj kontakt", + "ContactSelection_Selected": "wybrane", + "ContactSelection_SelectAll": "Zaznacz wszystko", + "ContactSelection_Clear": "Wyczyść zaznaczenie", + "ContactsPage_EmptyState": "Brak kontaktów do wyświetlenia", + "ContactsPage_AddFirstContact": "Dodaj swój pierwszy kontakt", + "ContactsPage_ContactsCountSuffix": "kontakty", + "ContactsPane_NewContact": "Nowy kontakt", + "ContactsPane_DescriptionTitle": "Zarządzaj swoimi kontaktami", + "ContactsPane_DescriptionBody": "Utwórz kontakty, nadaj im nazwy, zaktualizuj zdjęcia profilowe i przechowuj zapisane dane w jednym miejscu.", + "ContactEditDialog_AddTitle": "Dodaj kontakt", + "ContactInfoBar_ContactAdded": "Kontakt dodano pomyślnie.", + "ContactInfoBar_ContactUpdated": "Kontakt zaktualizowano pomyślnie.", + "ContactInfoBar_ContactsDeleted": "Kontakty zostały usunięte pomyślnie.", + "ContactInfoBar_ContactPhotoUpdated": "Zdjęcie kontaktu zostało pomyślnie zaktualizowane.", + "ContactInfoBar_FailedToLoadContacts": "Nie udało się załadować kontaktów: {0}", + "ContactInfoBar_FailedToAddContact": "Nie udało się dodać kontaktu: {0}", + "ContactInfoBar_FailedToUpdateContact": "Nie udało się zaktualizować kontaktu: {0}", + "ContactInfoBar_FailedToDeleteContacts": "Nie udało się usunąć kontaktów: {0}", + "ContactInfoBar_FailedToUpdatePhoto": "Nie udało się zaktualizować zdjęcia: {0}", + "ContactInfoBar_CannotDeleteRoot": "Kontakty główne nie mogą zostać usunięte.", + "ContactConfirmDialog_DeleteTitle": "Usuń kontakt", + "ContactConfirmDialog_DeleteMessage": "Czy na pewno chcesz usunąć kontakt '{0}'?", + "ContactConfirmDialog_DeleteMultipleMessage": "Czy na pewno chcesz usunąć {0} kontaktów?", + "ContactConfirmDialog_DeleteButton": "Usuń", + "CalendarAccountSettings_Title": "Ustawienia konta kalendarza", + "CalendarAccountSettings_Description": "Zarządzaj ustawieniami kalendarza dla {0}", + "CalendarAccountSettings_AccountColor": "Kolor konta", + "CalendarAccountSettings_AccountColorDescription": "Zmień kolor wyświetlania dla tego konta kalendarza", + "CalendarAccountSettings_SyncEnabled": "Włącz synchronizację", + "CalendarAccountSettings_SyncEnabledDescription": "Włącz lub wyłącz synchronizację kalendarza dla tego konta", + "CalendarAccountSettings_DefaultShowAs": "Domyślny status dostępności", + "CalendarAccountSettings_DefaultShowAsDescription": "Domyślny status dostępności dla nowych wydarzeń utworzonych tym kontem", + "CalendarAccountSettings_PrimaryCalendar": "Kalendarz główny", + "CalendarAccountSettings_PrimaryCalendarDescription": "Ustaw ten kalendarz jako główny kalendarz dla konta", + "CalendarSettings_NewEventBehavior_Header": "Zachowanie przycisku Nowe wydarzenie", + "CalendarSettings_NewEventBehavior_Description": "Wybierz, czy przycisk Nowe wydarzenie powinien pytać o kalendarz za każdym razem, czy zawsze otwierać określony kalendarz.", + "CalendarSettings_NewEventBehavior_AskEachTime": "Pytaj za każdym razem.", + "CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar": "Zawsze używaj określonego kalendarza.", + "CalendarSettings_Rendering_Title": "Renderowanie", + "CalendarSettings_Rendering_Description": "Skonfiguruj układ kalendarza i sposób wyświetlania.", + "CalendarSettings_Notifications_Title": "Powiadomienia", + "CalendarSettings_Notifications_Description": "Wybierz domyślne przypomnienie i sposób odraczania.", + "CalendarSettings_Preferences_Title": "Preferencje", + "CalendarSettings_Preferences_Description": "Ustaw, jak działa przycisk Nowe wydarzenie.", + "WhatIsNew_GetStartedButton": "Zacznij", + "WhatIsNew_ContinueAnywayButton": "Kontynuuj mimo wszystko", + "WhatIsNew_PreparingForNewVersionButton": "Przygotowywanie nowej wersji...", + "WhatIsNew_MigrationPreparing_Title": "Przygotowywanie danych", + "WhatIsNew_MigrationPreparing_Description": "Wino stosuje migracje aktualizacji. Proszę poczekać, aż przygotujemy dane Twojego konta na tę wersję.", + "WhatIsNew_MigrationFailedMessage": "Wykonanie migracji zakończyło się błędem kod {0}. Możesz kontynuować korzystanie z aplikacji. Jeśli napotkasz poważne problemy, ponownie zainstaluj aplikację.", + "WhatIsNew_MigrationNotification_Title": "Wino Mail Zaktualizowany", + "WhatIsNew_MigrationNotification_Message": "Otwórz aplikację, aby zakończyć aktualizację i zobaczyć nowości.", + "WelcomeWindow_Title": "Witamy w Wino Mail", + "WelcomeWindow_Subtitle": "Natywne doświadczenie Windows dla Poczty i Kalendarza.", + "WelcomeWindow_WhatsNewTitle": "Najnowsze zmiany", + "WelcomeWindow_FeaturesTitle": "Funkcje", + "WelcomeWindow_WhatsNewTab": "Co nowego", + "WelcomeWindow_FeaturesTab": "Funkcje", + "WelcomeWindow_GetStartedButton": "Rozpocznij od dodania konta", + "WelcomeWindow_GetStartedDescription": "Dodaj swoje konta Outlook, Gmail lub IMAP, aby rozpocząć z Wino Mail.", + "WelcomeWindow_ImportFromWinoAccount": "Importuj z konta Wino", + "WelcomeWindow_ImportInProgress": "Importowanie zsynchronizowanych ustawień i kont...", + "WelcomeWindow_ImportNoAccountsFound": "Nie znaleziono zsynchronizowanych kont w Twoim koncie Wino. Jeśli ustawienia były dostępne, zostały przywrócone. Aby dodać konto ręcznie, użyj Rozpocznij.", + "WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} zsynchronizowanych kont są już dostępne na tym urządzeniu. Aby dodać inne konto ręcznie, jeśli to potrzebne, użyj Rozpocznij.", + "WelcomeWindow_SetupTitle": "Skonfiguruj swoje konto", + "WelcomeWindow_SetupSubtitle": "Wybierz dostawcę poczty e-mail, aby rozpocząć", + "WelcomeWindow_AddAccountButton": "Dodaj konto", + "WelcomeWindow_SkipForNow": "Później pominę — skonfiguruję to później", + "WelcomeWindow_AppDescription": "Szybka i skupiona skrzynka odbiorcza — przeprojektowana dla Windows 11", + "WelcomeWizard_Step1Title": "Witaj", + "SystemTrayMenu_Open": "Otwórz", + "WinoAccount_Titlebar_SyncBenefitTitle": "Ustawienia synchronizacji", + "WinoAccount_Titlebar_SyncBenefitDescription": "Utrzymuj preferencje Wino w synchronizacji między urządzeniami.", + "WinoAccount_Titlebar_AddonsBenefitTitle": "Odblokuj dodatki", + "WinoAccount_Titlebar_AddonsBenefitDescription": "Uzyskaj dostęp do funkcji premium, takich jak Pakiet AI Wino.", + "WinoAccount_Management_Description": "Zarządzaj kontem Wino, dostępem do Pakietu AI oraz zsynchronizowanymi preferencjami i szczegółami konta.", + "WinoAccount_Management_SignedOutTitle": "Zaloguj się do Wino Mail", + "WinoAccount_Management_SignedOutDescription": "Zaloguj się lub utwórz konto, aby synchronizować e-maile, uzyskać dostęp do funkcji AI i zarządzać ustawieniami między urządzeniami.", + "WinoAccount_Management_ProfileSectionHeader": "Profil", + "WinoAccount_Management_AddOnsSectionHeader": "Dodatki Wino", + "WinoAccount_Management_DataSectionHeader": "Dane", + "WinoAccount_Management_AccountActionsSectionHeader": "Działania konta", + "WinoAccount_Management_AccountCardTitle": "Konto", + "WinoAccount_Management_AccountCardDescription": "Adres e-mail konta Wino i bieżący stan konta.", + "WinoAccount_Management_AiPackCardTitle": "Pakiet AI", + "WinoAccount_Management_AiPackCardDescription": "Sprawdź, czy Pakiet AI Wino jest aktywny i ile pozostało użycia.", + "WinoAccount_Management_AiPackActive": "Pakiet AI Wino jest aktywny", + "WinoAccount_Management_AiPackInactive": "Pakiet AI Wino nie jest aktywny", + "WinoAccount_Management_AiPackUsage": "{0} z {1} użyć zużyto. {2} pozostało.", + "WinoAccount_Management_AiPackBillingPeriod": "Okres rozliczeniowy: {0:d} - {1:d}", + "WinoAccount_Management_AiPackUnknownUsage": "Szczegóły użycia nie są jeszcze dostępne.", + "WinoAccount_Management_AiPackBuyDescription": "Kup Pakiet AI Wino, aby tłumaczyć, przepisywać lub streszczać e-maile za pomocą AI.", + "WinoAccount_Management_AiPackPromoTitle": "Odblokuj Pakiet AI", + "WinoAccount_Management_AiPackPromoDescription": "Przyspiesz swój przepływ pracy z e-mailami dzięki narzędziom opartym na AI. Tłumacz wiadomości na ponad 50 języków, przepisuj dla jasności i tonu, oraz uzyskuj natychmiastowe streszczenia długich wątków.", + "WinoAccount_Management_AiPackPromoPrice": "$4,99 / mies.", + "WinoAccount_Management_AiPackPromoRequests": "1 000 kredytów", + "WinoAccount_Management_AiPackGetButton": "Pobierz Pakiet AI", + "WinoAddOn_AI_PACK_Name": "Pakiet AI Wino", + "WinoAddOn_AI_PACK_Description": "Narzędzia z AI do tłumaczenia, przepisywania i streszczania wiadomości w Wino Mail.", + "WinoAddOn_AI_PACK_Keywords": "AI, tłumaczenie, przepisywanie, streszczanie, produktywność", + "WinoAddOn_UNLIMITED_ACCOUNTS_Name": "Nielimitowane konta", + "WinoAddOn_UNLIMITED_ACCOUNTS_Description": "Usuń ograniczenie kont i dodawaj dowolną liczbę kont pocztowych.", + "WinoAddOn_UNLIMITED_ACCOUNTS_Keywords": "konta, nieograniczony, premium, dodatek", + "WinoAccount_Management_PurchaseRequiresSignIn": "Zaloguj się na swoje konto Wino, aby ukończyć ten zakup.", + "WinoAccount_Management_PurchaseStartFailed": "Wino nie mógł zakończyć tego zakupu w Microsoft Store.", + "WinoAccount_Management_StoreSyncFailed": "Zakup zakończony, ale Wino nie mógł odświeżyć korzyści konta. Spróbuj ponownie za chwilę.", + "WinoAccount_Management_AiPackSubscriptionActive": "Twoja subskrypcja jest aktywna", + "WinoAccount_Management_AiPackRenews": "Odnowienie {0:d}", + "WinoAccount_Management_AiPackRequestsUsed": "Wykorzystane kredyty w tym miesiącu", + "WinoAccount_Management_AiPackResets": "Reset {0:d}", + "WinoAccount_Management_AiPackUsageLoadFailed": "Wystąpiły problemy z wczytywaniem salda użycia AI.", + "WinoAccount_Management_AiPackFeatureTranslate": "Tłumaczenie", + "WinoAccount_Management_AiPackFeatureRewrite": "Przepisanie", + "WinoAccount_Management_AiPackFeatureSummarize": "Streszczenie", + "WinoAccount_Management_AddOnLoadFailed": "Wystąpiły problemy z wczytywaniem tego dodatku.", + "WinoAccount_Management_SyncPreferencesTitle": "Synchronizuj Preferencje i Konta", + "WinoAccount_Management_SyncPreferencesDescription": "Importuj lub eksportuj swoje preferencje Wino i dane skrzynki pocztowej między urządzeniami. Hasła, tokeny i inne wrażliwe informacje nigdy nie są synchronizowane.", + "WinoAccount_Management_SignOutTitle": "Wyloguj się", + "WinoAccount_Management_SignOutDescription": "Wyloguj się ze swojego konta na tym urządzeniu", + "WinoAccount_Management_StatusLabel": "Status: {0}", + "WinoAccount_Management_NoRemoteSettings": "Na razie nie przechowujemy zsynchronizowanych danych dla tego konta.", + "WinoAccount_Management_ExportSucceeded": "Wybrane dane Wino zostały pomyślnie wyeksportowane.", + "WinoAccount_Management_ExportPreferencesSucceeded": "Twoje preferencje zostały wyeksportowane do konta Wino.", + "WinoAccount_Management_ExportAccountsSucceeded": "Wyeksportowano {0} szczegółów konta do Twojego konta Wino.", + "WinoAccount_Management_ImportSucceeded": "Zaimportowano zsynchronizowane dane z konta Wino.", + "WinoAccount_Management_ImportPreferencesSucceeded": "Zastosowano {0} zsynchronizowanych preferencji.", + "WinoAccount_Management_ImportAccountsSucceeded": "Zaimportowano {0} kont.", + "WinoAccount_Management_ImportDuplicateAccountsSkipped": "Pominięto {0} kont, które już istnieją na tym urządzeniu.", + "WinoAccount_Management_ImportPartial": "Zastosowano {0} zsynchronizowanych preferencji. {1} ustawień nie udało się przywrócić.", + "WinoAccount_Management_ImportReloginReminder": "Hasła, tokeny i inne wrażliwe dane nie zostały zaimportowane. Zaloguj się ponownie dla każdego konta na tym urządzeniu przed użyciem.", + "WinoAccount_Management_SerializeFailed": "Wino nie mógł zapisać Twoich bieżących ustawień.", + "WinoAccount_Management_EmptyExport": "Brak wartości preferencji do eksportu.", + "WinoAccount_Management_ImportEmpty": "Przesył zsynchronizowanych danych nie zawiera nic nowego do przywrócenia.", + "WinoAccount_Management_ExportDialog_Title": "Eksport do konta Wino.", + "WinoAccount_Management_ExportDialog_Description": "Wybierz, co chcesz zsynchronizować z kontem Wino.", + "WinoAccount_Management_ExportDialog_IncludePreferences": "Preferencje", + "WinoAccount_Management_ExportDialog_IncludeAccounts": "Konta", + "WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Hasła, tokeny i inne wrażliwe dane nie są synchronizowane.", + "WinoAccount_Management_ExportDialog_AccountsRelogin": "Zaimportowane konta na innym komputerze będą nadal wymagały ponownego zalogowania przed użyciem.", + "WinoAccount_Management_ExportDialog_InProgress": "Eksportowanie wybranych danych Wino...", + "WinoAccount_Management_LoadFailed": "Nie udało się załadować najnowszych informacji o koncie Wino.", + "WinoAccount_Management_ActionFailed": "Żądanie konta Wino nie mogło zostać zakończone.", + "WinoAccount_SettingsSection_Title": "Konto Wino", + "WinoAccount_SettingsSection_Description": "Utwórz lub zaloguj się na konto Wino, używając usługi uwierzytelniania na lokalnym hoście.", + "WinoAccount_RegisterButton_Title": "Zarejestruj konto", + "WinoAccount_RegisterButton_Description": "Utwórz konto Wino za pomocą adresu e-mail i hasła.", + "WinoAccount_RegisterButton_Action": "Otwórz rejestrację", + "WinoAccount_LoginButton_Title": "Zaloguj się", + "WinoAccount_LoginButton_Description": "Zaloguj się na istniejące konto Wino za pomocą adresu e-mail i hasła.", + "WinoAccount_LoginButton_Action": "Otwórz logowanie", + "WinoAccount_SignOutButton_Title": "Wyloguj się", + "WinoAccount_SignOutButton_Description": "Usuń lokalnie zapisaną sesję konta Wino.", + "WinoAccount_SignOutButton_Action": "Wyloguj się", + "WinoAccount_RegisterDialog_Title": "Utwórz konto Wino", + "WinoAccount_RegisterDialog_Description": "Utwórz konto Wino, aby utrzymać synchronizację środowiska Wino i uzyskać dostęp do dodatków opartych na kontach.", + "WinoAccount_RegisterDialog_HeroTitle": "Utwórz swoje konto Wino", + "WinoAccount_RegisterDialog_BenefitsTitle": "Dlaczego warto je utworzyć?", + "WinoAccount_RegisterDialog_BenefitSyncTitle": "Importuj i eksportuj ustawienia między urządzeniami", + "WinoAccount_RegisterDialog_BenefitSyncDescription": "Przenieś swoje preferencje Wino między urządzeniami bez konieczności przebudowywania konfiguracji od zera.", + "WinoAccount_RegisterDialog_BenefitAiTitle": "Dostęp do ekskluzywnych dodatków, takich jak Wino AI Pack (płatny).", + "WinoAccount_RegisterDialog_BenefitAiDescription": "Użyj jednego konta, aby odblokować funkcje premium Wino, gdy będą dostępne.", + "WinoAccount_RegisterDialog_DifferenceTitle": "Konto Wino jest oddzielne od Twoich kont e-mail.", + "WinoAccount_RegisterDialog_DifferenceDescription": "Twoje konta Outlook, Gmail, IMAP lub inne konta e-mail pozostają takie, jakie są. Konto Wino obsługuje wyłącznie funkcje Wino i dodatki oparte na kontach.", + "WinoAccount_RegisterDialog_PrimaryButton": "Zarejestruj", + "WinoAccount_RegisterDialog_PrivacyTitle": "Prywatność i przetwarzanie danych API", + "WinoAccount_RegisterDialog_PrivacyDescription": "Opcjonalne dodatki, takie jak Wino AI Pack, mogą wysyłać wybrane treści HTML wiadomości e-mail do usługi Wino API tylko wtedy, gdy korzystasz z tych funkcji.", + "WinoAccount_RegisterDialog_PrivacyLinkText": "Przeczytaj politykę prywatności.", + "WinoAccount_RegisterDialog_PrivacyCheckbox": "Zgadzam się na politykę prywatności.", + "WinoAccount_LoginDialog_Title": "Zaloguj się do konta Wino", + "WinoAccount_LoginDialog_Description": "Zaloguj się na swoje konto Wino, aby zsynchronizować ustawienia Wino i uzyskać dostęp do funkcji opartych na koncie.", + "WinoAccount_LoginDialog_HeroTitle": "Witaj ponownie", + "WinoAccount_LoginDialog_BenefitsTitle": "Co daje logowanie", + "WinoAccount_LoginDialog_BenefitsDescription": "Użyj konta Wino, aby kontynuować synchronizację ustawień między urządzeniami i uzyskać dostęp do płatnych dodatków, takich jak Wino AI Pack.", + "WinoAccount_LoginDialog_DifferenceTitle": "To nie jest logowanie do Twojej skrzynki e-mail.", + "WinoAccount_LoginDialog_DifferenceDescription": "Logowanie tutaj nie dodaje ani nie zastępuje Twoich kont Outlook, Gmail lub IMAP w Wino. Służy wyłącznie do zalogowania Cię do usług specyficznych dla Wino.", + "WinoAccount_LoginDialog_ForgotPasswordLink": "Zapomniałeś hasła?", + "WinoAccount_EmailLabel": "E-mail", + "WinoAccount_EmailPlaceholder": "name@example.com", + "WinoAccount_PasswordLabel": "Hasło", + "WinoAccount_ConfirmPasswordLabel": "Potwierdź hasło", + "WinoAccount_ForgotPasswordDialog_Title": "Zresetuj hasło.", + "WinoAccount_ForgotPasswordDialog_PrimaryButton": "Wyślij e-mail z resetem hasła.", + "WinoAccount_ForgotPasswordDialog_BackToSignIn": "Wróć do logowania", + "WinoAccount_ForgotPasswordDialog_Description": "Wprowadź adres e-mail konta Wino, a my wyślemy Ci link do zresetowania hasła, jeśli adres jest zarejestrowany.", + "WinoAccount_Validation_EmailRequired": "Wymagany jest adres e-mail.", + "WinoAccount_Validation_PasswordRequired": "Wymagane jest hasło.", + "WinoAccount_Validation_PasswordMismatch": "Hasła nie pasują.", + "WinoAccount_Validation_PrivacyConsentRequired": "Musisz zaakceptować politykę prywatności, zanim utworzysz konto Wino.", + "WinoAccount_Error_InvalidCredentials": "Adres e-mail lub hasło jest nieprawidłowe.", + "WinoAccount_Error_AccountLocked": "Konto jest tymczasowo zablokowane.", + "WinoAccount_Error_AccountBanned": "Konto zostało zablokowane.", + "WinoAccount_Error_AccountSuspended": "Konto zostało zawieszone.", + "WinoAccount_Error_EmailNotConfirmed": "Proszę potwierdzić adres e-mail przed zalogowaniem.", + "WinoAccount_Error_EmailConfirmationRequired": "Proszę potwierdzić adres e-mail przed zalogowaniem.", + "WinoAccount_Error_EmailConfirmationResendNotAvailable": "Nowy e-mail potwierdzający nie jest jeszcze dostępny.", + "WinoAccount_Error_EmailConfirmationResendInvalid": "To żądanie potwierdzające nie jest już ważne. Spróbuj ponownie zalogować się.", + "WinoAccount_Error_EmailNotRegistered": "Ten adres e-mail nie jest zarejestrowany.", + "WinoAccount_Error_RefreshTokenInvalid": "Twoja sesja nie jest już ważna. Proszę zaloguj się ponownie.", + "WinoAccount_Error_EmailAlreadyRegistered": "Ten adres e-mail jest już zarejestrowany.", + "WinoAccount_Error_ExternalLoginEmailRequired": "Wymagany jest adres e-mail do zakończenia zewnętrznego logowania.", + "WinoAccount_Error_ExternalLoginInvalid": "Żądanie zewnętrznego logowania jest nieprawidłowe.", + "WinoAccount_Error_ExternalAuthStateInvalid": "Stan zewnętrznego logowania jest nieprawidłowy lub wygasł.", + "WinoAccount_Error_ExternalAuthCodeInvalid": "Kod zewnętrznego logowania jest nieprawidłowy lub wygasł.", + "WinoAccount_Error_AiPackRequired": "Wymagana jest aktywna subskrypcja Wino AI Pack do wykonania tej czynności.", + "WinoAccount_Error_AiQuotaExceeded": "Limit użycia Wino AI Pack w bieżącym okresie rozliczeniowym został wyczerpany.", + "WinoAccount_Error_AiHtmlEmpty": "Brak treści e-mail do przetworzenia.", + "WinoAccount_Error_AiHtmlTooLarge": "Ten e-mail jest zbyt duży, aby go przetworzyć za pomocą Wino AI.", + "WinoAccount_Error_AiUnsupportedLanguage": "Ten język nie jest obsługiwany. Spróbuj użyć prawidłowego kodu kultury, na przykład en-US lub tr-TR.", + "WinoAccount_Error_Forbidden": "Nie masz uprawnień do wykonania tej czynności.", + "WinoAccount_Error_ValidationFailed": "Żądanie jest nieprawidłowe. Sprawdź wprowadzone wartości.", + "WinoAccount_RegisterSuccessMessage": "Rejestracja konta Wino zakończona dla {0}.", + "WinoAccount_LoginSuccessMessage": "Zalogowano do konta Wino jako {0}.", + "WinoAccount_EmailConfirmationSentDialog_Title": "Potwierdź swój adres e-mail.", + "WinoAccount_EmailConfirmationSentDialog_Message": "Wysłano potwierdzenie e-maila na {0}. Proszę potwierdzić je i spróbować ponownie się zalogować.", + "WinoAccount_EmailConfirmationPendingDialog_Title": "Wymagane potwierdzenie adresu e-mail", + "WinoAccount_EmailConfirmationPendingDialog_Message": "Wciąż czekamy na potwierdzenie {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ResendButton": "Wyślij ponownie e-mail potwierdzający.", + "WinoAccount_EmailConfirmationPendingDialog_Countdown": "Możesz ponownie wysłać e-mail potwierdzający za {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ReadyToResend": "Możesz teraz ponownie wysłać e-mail potwierdzający.", + "WinoAccount_EmailConfirmationResentDialog_Title": "E-mail potwierdzający wysłany ponownie.", + "WinoAccount_EmailConfirmationResentDialog_Message": "Wysłano kolejny e-mail potwierdzający na {0}. Proszę potwierdzić go i spróbować ponownie się zalogować.", + "WinoAccount_ForgotPasswordDialog_SuccessTitle": "Wysłano e-mail z resetem hasła.", + "WinoAccount_ForgotPasswordDialog_SuccessMessage": "Wysłaliśmy e-mail z resetem hasła na {0}. Otwórz tę wiadomość, aby wybrać nowe hasło.", + "WinoAccount_ChangePassword_Title": "Zmień hasło", + "WinoAccount_ChangePassword_Description": "Wyślij e-mail z resetem hasła na to konto Wino.", + "WinoAccount_ChangePassword_Action": "Wyślij e-mail z resetem hasła.", + "WinoAccount_ChangePassword_ConfirmationMessage": "Czy Wino ma wysłać wiadomość e-mail z resetowaniem hasła na {0}?", + "WinoAccount_SignOut_SuccessMessage": "Wylogowano z konta Wino {0}.", + "WinoAccount_SignOut_NoAccountMessage": "Nie ma aktywnego konta Wino, z którego można się wylogować.", + "WinoAccount_Titlebar_SignedOutTitle": "Konto Wino", + "WinoAccount_Titlebar_SignedOutDescription": "Zaloguj się lub utwórz konto Wino, aby zarządzać sesją Wino.", + "WinoAccount_Titlebar_SignedInStatus": "Status: {0}", + "WelcomeWizard_Step2Title": "Dodaj konto", + "WelcomeWizard_Step3Title": "Zakończ konfigurację", + "ProviderSelection_Title": "Wybierz dostawcę poczty e-mail", + "ProviderSelection_Subtitle": "Wybierz dostawcę poniżej, aby dodać swoje konto e-mail do Wino Mail.", + "ProviderSelection_AccountNameHeader": "Nazwa konta", + "ProviderSelection_AccountNamePlaceholder": "np. Osobiste, Służbowe", + "ProviderSelection_DisplayNameHeader": "Nazwa wyświetlana", + "ProviderSelection_DisplayNamePlaceholder": "np. Jan Kowalski", + "ProviderSelection_EmailHeader": "Adres e-mail", + "ProviderSelection_EmailPlaceholder": "np. johndoe@example.com", + "ProviderSelection_AppPasswordHeader": "Hasło specyficzne dla aplikacji", + "ProviderSelection_AppPasswordHelp": "Jak uzyskać hasło specyficzne dla aplikacji?", + "ProviderSelection_CalendarModeHeader": "Integracja kalendarza", + "ProviderSelection_CalendarMode_DisabledTitle": "Wyłączone", + "ProviderSelection_CalendarMode_DisabledDescription": "Brak integracji kalendarza", + "ProviderSelection_CalendarMode_CalDavTitle": "Synchronizacja CalDAV", + "ProviderSelection_CalendarMode_CalDavDescription_Apple": "Twoje wydarzenia kalendarza są synchronizowane z serwerami Apple między Twoimi urządzeniami.", + "ProviderSelection_CalendarMode_CalDavDescription_Yahoo": "Twoje wydarzenia kalendarza są synchronizowane z serwerami Yahoo między Twoimi urządzeniami.", + "ProviderSelection_CalendarMode_LocalTitle": "Lokalny kalendarz", + "ProviderSelection_CalendarMode_LocalDescription": "Twoje wydarzenia są przechowywane wyłącznie na tym komputerze. Brak połączenia z serwerem.", + "ProviderSelection_ClearColor": "Wyczyść kolor", + "ProviderSelection_ContinueButton": "Kontynuuj", + "ProviderSelection_SpecialImap_Subtitle": "Wprowadź dane konta, aby połączyć.", + "AccountSetup_Title": "Konfigurowanie konta", + "AccountSetup_Step_Authenticating": "Uwierzytelnianie z {0}", + "AccountSetup_Step_TestingMailAuth": "Testowanie uwierzytelniania poczty", + "AccountSetup_Step_SyncingFolders": "Synchronizowanie metadanych folderów", + "AccountSetup_Step_FetchingProfile": "Pobieranie informacji o profilu", + "AccountSetup_Step_DiscoveringCalDav": "Wykrywanie ustawień CalDAV", + "AccountSetup_Step_TestingCalendarAuth": "Testowanie uwierzytelniania kalendarza", + "AccountSetup_Step_SavingAccount": "Zapisywanie informacji o koncie", + "AccountSetup_Step_FetchingCalendarMetadata": "Pobieranie metadanych kalendarza", + "AccountSetup_Step_SyncingAliases": "Synchronizowanie aliasów", + "AccountSetup_Step_Finalizing": "Finalizowanie konfiguracji", + "AccountSetup_FailureMessage": "Konfiguracja nie powiodła się. Wróć, aby naprawić ustawienia, albo spróbuj ponownie później.", + "AccountSetup_SuccessMessage": "Twoje konto zostało pomyślnie skonfigurowane!", + "AccountSetup_GoBackButton": "Wstecz", + "AccountSetup_TryAgainButton": "Spróbuj ponownie", + "ImapCalDavSettings_AutoDiscoveryFailed": "Wykrywanie automatyczne nie powiodło się. Wprowadź ustawienia ręcznie w zakładce Zaawansowane." } - - diff --git a/Wino.Core.Domain/Translations/pt_BR/resources.json b/Wino.Core.Domain/Translations/pt_BR/resources.json index 43dc9c08..5c9bfeef 100644 --- a/Wino.Core.Domain/Translations/pt_BR/resources.json +++ b/Wino.Core.Domain/Translations/pt_BR/resources.json @@ -8,6 +8,7 @@ "AccountCacheReset_Message": "Esta conta requer re-sincronização completa para continuar funcionando. Por favor, aguarde enquanto o Wino re-sincroniza suas mensagens...", "AccountContactNameYou": "Você", "AccountCreationDialog_Completed": "Tudo pronto", + "AccountCreationDialog_FetchingCalendarMetadata": "Obtendo detalhes do calendário.", "AccountCreationDialog_FetchingEvents": "Buscando eventos na agenda.", "AccountCreationDialog_FetchingProfileInformation": "Obtendo detalhes do perfil.", "AccountCreationDialog_GoogleAuthHelpClipboardText_Row0": "Se seu navegador não foi iniciado automaticamente para concluir a autenticação:", @@ -17,6 +18,7 @@ "AccountCreationDialog_Initializing": "iniciando", "AccountCreationDialog_PreparingFolders": "Estamos recebendo informações sobre as pastas no momento.", "AccountCreationDialog_SigninIn": "Informações da conta estão sendo salvas.", + "Purchased": "Comprado", "AccountEditDialog_Message": "Nome da Conta", "AccountEditDialog_Title": "Editar Conta", "AccountPickerDialog_Title": "Escolha uma conta", @@ -26,6 +28,10 @@ "AccountDetailsPage_Description": "Altere o nome da conta no Wino e defina o nome do remetente desejado.", "AccountDetailsPage_ColorPicker_Title": "Cor da conta", "AccountDetailsPage_ColorPicker_Description": "Atribua uma nova cor da conta para colorir seu símbolo na lista.", + "AccountDetailsPage_TabGeneral": "Geral", + "AccountDetailsPage_TabMail": "Email", + "AccountDetailsPage_TabCalendar": "Calendário", + "AccountDetailsPage_CalendarListDescription": "Selecione um calendário para configurar suas configurações.", "AddHyperlink": "Adicionar", "AppCloseBackgroundSynchronizationWarningTitle": "Sincronização em segundo plano", "AppCloseStartupLaunchDisabledWarningMessageFirstLine": "O aplicativo não foi configurado para iniciar com o Windows.", @@ -47,8 +53,10 @@ "BasicIMAPSetupDialog_Title": "Conta IMAP", "Busy": "Ocupado(a)", "Buttons_AddAccount": "Adicionar Conta", + "Buttons_FixAccount": "Corrigir Conta", "Buttons_AddNewAlias": "Adicionar Novo Apelido", "Buttons_Allow": "Permitir", + "Buttons_Apply": "Aplicar", "Buttons_ApplyTheme": "Aplicar Tema", "Buttons_Browse": "Navegar", "Buttons_Cancel": "Cancelar", @@ -62,6 +70,7 @@ "Buttons_Edit": "Editar", "Buttons_EnableImageRendering": "Habilitar", "Buttons_Multiselect": "Selecionar Vários", + "Buttons_Manage": "Gerenciar", "Buttons_No": "Não", "Buttons_Open": "Abrir", "Buttons_Purchase": "Comprar", @@ -70,15 +79,134 @@ "Buttons_Save": "Salvar", "Buttons_SaveConfiguration": "Salvar Configuração", "Buttons_Send": "Enviar", + "Buttons_SendToServer": "Enviar para o servidor", "Buttons_Share": "Compartilhar", "Buttons_SignIn": "Entrar", "Buttons_Sync": "Sincronizar", "Buttons_SyncAliases": "Sincronizar Apelidos", "Buttons_TryAgain": "Tentar Novamente", "Buttons_Yes": "Sim", + "Sync_SynchronizingFolder": "Sincronizando {0} {1}%", + "Sync_DownloadedMessages": "Baixadas {0} mensagens de {1}", + "SyncAction_Archiving": "Arquivando {0} e-mail(s)", + "SyncAction_ClearingFlag": "Desmarcando a bandeira de {0} e-mail(s)", + "SyncAction_CreatingDraft": "Criando rascunho", + "SyncAction_CreatingEvent": "Criando evento", + "SyncAction_Deleting": "Excluindo {0} e-mail(s)", + "SyncAction_EmptyingFolder": "Esvaziando pasta", + "SyncAction_MarkingAsRead": "Marcando {0} e-mail(s) como lidos", + "SyncAction_MarkingAsUnread": "Marcando {0} e-mail(s) como não lidos", + "SyncAction_MarkingFolderAsRead": "Marcando pasta como lida", + "SyncAction_Moving": "Movendo {0} e-mail(s)", + "SyncAction_MovingToFocused": "Movendo {0} e-mail(s) para Focused", + "SyncAction_RenamingFolder": "Renomeando pasta", + "SyncAction_SendingMail": "Enviando e-mail", + "SyncAction_SettingFlag": "Sinalizando {0} e-mail(s)", + "SyncAction_SynchronizingAccount": "Sincronizando {0}", + "SyncAction_SynchronizingAccounts": "Sincronizando {0} conta(s)", + "SyncAction_SynchronizingCalendarData": "Sincronizando dados do calendário", + "SyncAction_SynchronizingCalendarEvents": "Sincronizando eventos do calendário", + "SyncAction_SynchronizingCalendarMetadata": "Sincronizando metadados do calendário", + "SyncAction_Unarchiving": "Desarquivando {0} e-mail(s)", "CalendarAllDayEventSummary": "eventos do dia todo", "CalendarDisplayOptions_Color": "Cor", "CalendarDisplayOptions_Expand": "Expandir", + "CalendarEventResponse_Accept": "Aceitar", + "CalendarEventResponse_AcceptedResponse": "Aceito", + "CalendarEventResponse_Decline": "Recusar", + "CalendarEventResponse_DeclinedResponse": "Recusado", + "CalendarEventResponse_NotResponded": "Ainda não respondido", + "CalendarEventResponse_Tentative": "Provisório", + "CalendarEventResponse_TentativeResponse": "Provisório", + "CalendarEventRsvpPanel_Accept": "Aceitar", + "CalendarEventRsvpPanel_AddMessage": "Adicionar uma mensagem à sua resposta... (opcional)", + "CalendarEventRsvpPanel_Decline": "Recusar", + "CalendarEventRsvpPanel_Message": "Mensagem", + "CalendarEventRsvpPanel_SendReplyMessage": "Enviar mensagem de resposta", + "CalendarEventRsvpPanel_Tentative": "Provisório", + "CalendarEventRsvpPanel_Title": "Opções de resposta", + "CalendarAttendeeStatus_Accepted": "Aceito", + "CalendarAttendeeStatus_Declined": "Recusado", + "CalendarAttendeeStatus_NeedsAction": "Requer ação", + "CalendarAttendeeStatus_Tentative": "Provisório", + "CalendarEventDetails_Attachments": "Anexos", + "CalendarEventCompose_AddAttachment": "Adicionar anexo", + "CalendarEventCompose_AllDay": "O dia todo", + "CalendarEventCompose_AttachmentsNotSupportedForCalDav": "Anexos não são suportados para calendários CalDAV.", + "CalendarEventCompose_EndDate": "Data de término", + "CalendarEventCompose_EndTime": "Hora de término", + "CalendarEventCompose_Every": "a cada", + "CalendarEventCompose_ForWeekdays": "para", + "CalendarEventCompose_FrequencyDay": "dia", + "CalendarEventCompose_FrequencyDayPlural": "dias", + "CalendarEventCompose_FrequencyMonth": "mês", + "CalendarEventCompose_FrequencyMonthPlural": "meses", + "CalendarEventCompose_FrequencyWeek": "semana", + "CalendarEventCompose_FrequencyWeekPlural": "semanas", + "CalendarEventCompose_FrequencyYear": "ano", + "CalendarEventCompose_FrequencyYearPlural": "anos", + "CalendarEventCompose_Location": "Local", + "CalendarEventCompose_LocationPlaceholder": "Adicionar um local", + "CalendarEventCompose_NewEventButton": "Novo evento", + "CalendarEventCompose_DefaultCalendarHint": "Você pode escolher um calendário padrão para novos eventos nas configurações do Calendário.", + "CalendarEventCompose_DefaultCalendarSettingsLink": "Abrir configurações do Calendário", + "CalendarEventCompose_NoCalendarsMessage": "Ainda não há calendários disponíveis para a criação de eventos.", + "CalendarEventCompose_NoCalendarsTitle": "Nenhum calendário disponível", + "CalendarEventCompose_NoEndDate": "Sem data de término", + "CalendarEventCompose_Notes": "Notas", + "CalendarEventCompose_PickCalendarTitle": "Escolha um calendário", + "CalendarEventCompose_Recurring": "Recorrente", + "CalendarEventCompose_RecurringSummary": "Ocorre a cada {0} {1}{2} {3} efetivo {4}{5}", + "CalendarEventCompose_RecurringSummarySmart": "Ocorre {0}{1} {2} efetivo {3}{4}", + "CalendarEventCompose_RepeatEvery": "Repita a cada", + "CalendarEventCompose_SelectCalendar": "Selecionar calendário", + "CalendarEventCompose_SingleOccurrenceSummary": "Ocorre em {0} {1}", + "CalendarEventCompose_StartDate": "Data de início", + "CalendarEventCompose_StartTime": "Hora de início", + "CalendarEventCompose_TimeRangeSummary": "de {0} até {1}", + "CalendarEventCompose_Title": "Título do evento", + "CalendarEventCompose_TitlePlaceholder": "Adicionar um título", + "CalendarEventCompose_Until": "até", + "CalendarEventCompose_UntilSummary": " até {0}", + "CalendarEventCompose_ValidationInvalidAllDayRange": "A data de término do dia inteiro deve ser posterior à data de início.", + "CalendarEventCompose_ValidationInvalidAttendee": "Um ou mais participantes têm um endereço de e-mail inválido.", + "CalendarEventCompose_ValidationInvalidRecurrenceEnd": "A data de término da recorrência deve ser igual ou posterior à data de início do evento.", + "CalendarEventCompose_ValidationInvalidTimeRange": "A hora de término deve ser posterior à hora de início.", + "CalendarEventCompose_ValidationMissingAttachment": "Um ou mais anexos não estão mais disponíveis: {0}", + "CalendarEventCompose_ValidationMissingCalendar": "Selecione um calendário antes de criar o evento.", + "CalendarEventCompose_ValidationMissingTitle": "Digite um título de evento antes de criar o evento.", + "CalendarEventCompose_ValidationTitle": "Validação do evento falhou", + "CalendarEventCompose_WeekdaySummary": " em {0}", + "CalendarEventCompose_Weekday_Friday": "Sexta-feira", + "CalendarEventCompose_Weekday_Monday": "Segunda-feira", + "CalendarEventCompose_Weekday_Saturday": "Sáb", + "CalendarEventCompose_Weekday_Sunday": "Dom", + "CalendarEventCompose_Weekday_Thursday": "Qui", + "CalendarEventCompose_Weekday_Tuesday": "Ter", + "CalendarEventCompose_Weekday_Wednesday": "Qua", + "CalendarEventDetails_Details": "Detalhes", + "CalendarEventDetails_EditSeries": "Editar Série", + "CalendarEventDetails_Editing": "Editando", + "CalendarEventDetails_InviteSomeone": "Convidar alguém", + "CalendarEventDetails_JoinOnline": "Entrar online", + "CalendarEventDetails_Organizer": "Organizador", + "CalendarEventDetails_People": "Pessoas", + "CalendarEventDetails_ReadOnlyEvent": "Evento somente leitura", + "CalendarEventDetails_Reminder": "Lembrete", + "CalendarReminder_StartedHoursAgo": "Iniciado há {0} horas", + "CalendarReminder_StartedMinutesAgo": "Iniciado há {0} minutos", + "CalendarReminder_StartedNow": "Iniciado agora", + "CalendarReminder_StartingNow": "Iniciando agora", + "CalendarReminder_StartsInHours": "Começa em {0} horas", + "CalendarReminder_StartsInMinutes": "Começa em {0} minutos", + "CalendarReminder_SnoozeAction": "Adiar", + "CalendarReminder_SnoozeMinutesOption": "{0} minutos", + "CalendarEventDetails_ShowAs": "Mostrar como", + "CalendarShowAs_Free": "Livre", + "CalendarShowAs_Tentative": "Provisório", + "CalendarShowAs_Busy": "Ocupado", + "CalendarShowAs_OutOfOffice": "Fora do escritório", + "CalendarShowAs_WorkingElsewhere": "Trabalhando em outro local", "CalendarItem_DetailsPopup_JoinOnline": "Participar online", "CalendarItem_DetailsPopup_ViewEventButton": "Ver evento", "CalendarItem_DetailsPopup_ViewSeriesButton": "Ver serie", @@ -88,6 +216,9 @@ "ClipboardTextCopied_Message": "{0} Copiado para a área de transferência.", "ClipboardTextCopied_Title": "Copiado", "ClipboardTextCopyFailed_Message": "Falhou ao copiar {0} para a área de transferência.", + "ContactInfoBar_ErrorTitle": "Falha ao carregar informações de contato", + "ContactInfoBar_SuccessTitle": "Informações de contato carregadas com sucesso", + "ContactInfoBar_WarningTitle": "As informações de contato podem estar incompletas", "ComingSoon": "Em breve...", "ComposerAttachmentsDragDropAttach_Message": "Anexar", "ComposerAttachmentsDropZone_Message": "Solte seus arquivos aqui", @@ -129,6 +260,10 @@ "DialogMessage_CreateLinkedAccountTitle": "Nome do vínculo da conta", "DialogMessage_DeleteAccountConfirmationMessage": "Excluir {0}?", "DialogMessage_DeleteAccountConfirmationTitle": "Todos os dados associados a esta conta serão excluídos do disco permanentemente.", + "DialogMessage_DeleteEmailTemplateConfirmationMessage": "Excluir modelo \\\"{0}\\\"?", + "DialogMessage_DeleteEmailTemplateConfirmationTitle": "Excluir Modelo de Email", + "DialogMessage_DeleteRecurringSeriesMessage": "Isso excluirá todos os eventos da série. Deseja continuar?", + "DialogMessage_DeleteRecurringSeriesTitle": "Excluir Série Recorrente", "DialogMessage_DiscardDraftConfirmationMessage": "Este rascunho será descartado. Você quer continuar?", "DialogMessage_DiscardDraftConfirmationTitle": "Descartar Rascunho", "DialogMessage_EmptySubjectConfirmation": "Faltando Assunto", @@ -172,11 +307,18 @@ "ElementTheme_Light": "Modo claro", "Emoji": "Emoji", "Error_FailedToSetupSystemFolders_Title": "Falha ao configurar pastas de sistema", + "Exception_AccountNeedsAttention_Title": "Conta requer atenção", + "Exception_AccountNeedsAttention_Message": "'{0}' requer sua atenção para continuar funcionando.", + "Exception_WebView2RuntimeMissing_Message": "O Wino Mail não conseguiu localizar o runtime WebView2 do Microsoft Edge. Por favor, instale ou repare o runtime para renderizar o conteúdo das mensagens corretamente.", + "Exception_WebView2RuntimeMissing_Title": "É necessário o runtime WebView2", "Exception_AuthenticationCanceled": "Autenticação cancelada", "Exception_CustomThemeExists": "Este tema já existe.", "Exception_CustomThemeMissingName": "Você deve escrever um nome.", "Exception_CustomThemeMissingWallpaper": "Você deve fornecer uma imagem de fundo personalizada.", "Exception_FailedToSynchronizeAliases": "Falha ao sincronizar apelidos", + "Exception_FailedToSynchronizeCalendarData": "Falha ao sincronizar dados do calendário", + "Exception_FailedToSynchronizeCalendarEvents": "Falha ao sincronizar eventos do calendário", + "Exception_FailedToSynchronizeCalendarMetadata": "Falha ao sincronizar os metadados do calendário", "Exception_FailedToSynchronizeFolders": "Erro ao sincronizar pastas", "Exception_FailedToSynchronizeProfileInformation": "Falha ao sincronizar as informações do perfil", "Exception_GoogleAuthCallbackNull": "Callback uri é 'null' na ativação.", @@ -229,6 +371,32 @@ "HoverActionOption_MoveJunk": "Mover para Lixeira", "HoverActionOption_ToggleFlag": "Sinalizar / Não sinalizar", "HoverActionOption_ToggleRead": "Lido / Não lido", + "KeyboardShortcuts_FailedToReset": "Falha ao redefinir atalhos de teclado.", + "KeyboardShortcuts_FailedToUpdate": "Falha ao atualizar atalhos de teclado", + "KeyboardShortcuts_MailoperationAction": "Ação", + "KeyboardShortcuts_Action": "Ação", + "KeyboardShortcuts_FailedToLoad": "Falha ao carregar atalhos de teclado.", + "KeyboardShortcuts_EnterKeyForShortcut": "Por favor, digite uma tecla para o atalho.", + "KeyboardShortcuts_SelectOperationForShortcut": "Por favor, selecione uma ação para o atalho.", + "KeyboardShortcuts_EnterKey": "Por favor, digite uma tecla para o atalho.", + "KeyboardShortcuts_SelectOperation": "Por favor, selecione uma ação para o atalho.", + "KeyboardShortcuts_ShortcutInUse": "Este atalho já está em uso por outro atalho.", + "KeyboardShortcuts_FailedToSave": "Falha ao salvar o atalho.", + "KeyboardShortcuts_FailedToDelete": "Falha ao excluir o atalho.", + "KeyboardShortcuts_PageDescription": "Configure atalhos de teclado para operações rápidas de e-mail. Pressione teclas enquanto o foco estiver no campo de entrada da tecla para capturar os atalhos.", + "KeyboardShortcuts_Add": "Adicionar atalho", + "KeyboardShortcuts_EditTitle": "Editar Atalho de Teclado", + "KeyboardShortcuts_ResetToDefaults": "Redefinir para Padrões", + "KeyboardShortcuts_PressKeysHere": "Pressione as teclas aqui...", + "KeyboardShortcuts_KeyCombination": "Combinação de teclas", + "KeyboardShortcuts_FocusArea": "Foque no campo acima e pressione a combinação de teclas desejada", + "KeyboardShortcuts_Modifiers": "Teclas modificadoras", + "KeyboardShortcuts_Mode": "Modo do aplicativo", + "KeyboardShortcuts_ModeMail": "E-mail", + "KeyboardShortcuts_ModeCalendar": "Calendário", + "KeyboardShortcuts_ActionToggleReadUnread": "Alternar lido/não lido", + "KeyboardShortcuts_ActionToggleFlag": "Alternar sinalizador", + "KeyboardShortcuts_ActionToggleArchive": "Alternar arquivar/desarquivar", "ImageRenderingDisabled": "A renderização de imagem está desabilitada para esta mensagem.", "ImapAdvancedSetupDialog_AuthenticationMethod": "Método de autenticação", "ImapAdvancedSetupDialog_ConnectionSecurity": "Segurança de Conexão", @@ -295,12 +463,58 @@ "IMAPSetupDialog_Username": "Nome de Usuário", "IMAPSetupDialog_UsernamePlaceholder": "joaosilva, joaosilva@fabrikam.com, dominio/johndoe", "IMAPSetupDialog_UseSameConfig": "Use o mesmo nome de usuário e senha para enviar e-mail", + "ImapCalDavSettingsPage_TitleCreate": "Configuração de IMAP e Calendário", + "ImapCalDavSettingsPage_TitleEdit": "Editar Configurações de IMAP e Calendário", + "ImapCalDavSettingsPage_Subtitle": "Configure IMAP/SMTP e a sincronização opcional do calendário para esta conta.", + "ImapCalDavSettingsPage_BasicSectionTitle": "Configuração básica", + "ImapCalDavSettingsPage_BasicSectionDescription": "Digite sua identidade e credenciais. O Wino pode tentar detectar as configurações do servidor automaticamente.", + "ImapCalDavSettingsPage_BasicTab": "Básico", + "ImapCalDavSettingsPage_EnableCalendarSupport": "Habilitar suporte ao calendário", + "ImapCalDavSettingsPage_AutoDiscoverButton": "Descoberta automática de configurações de e-mail", + "ImapCalDavSettingsPage_AutoDiscoverySuccessMessage": "Configurações de e-mail encontradas e aplicadas.", + "ImapCalDavSettingsPage_AdvancedSectionTitle": "Configuração Avançada", + "ImapCalDavSettingsPage_AdvancedSectionDescription": "Digite as configurações do servidor manualmente se a descoberta automática não estiver disponível ou estiver incorreta.", + "ImapCalDavSettingsPage_AdvancedTab": "Avançado", + "ImapCalDavSettingsPage_CalendarSectionTitle": "Configuração do calendário", + "ImapCalDavSettingsPage_CalendarSectionDescription": "Escolha como os dados do calendário devem funcionar para esta conta IMAP.", + "ImapCalDavSettingsPage_CalendarModeHeader": "Modo do calendário", + "ImapCalDavSettingsPage_ConnectionSecurityHeader": "Segurança da conexão", + "ImapCalDavSettingsPage_AuthenticationMethodHeader": "Método de autenticação", + "ImapCalDavSettingsPage_CalendarModeDisabled": "Desativado", + "ImapCalDavSettingsPage_CalendarModeCalDav": "Sincronização CalDAV", + "ImapCalDavSettingsPage_CalendarModeLocalOnly": "Somente calendário local", + "ImapCalDavSettingsPage_CalendarModeDisabledDescription": "O calendário está desativado para esta conta.", + "ImapCalDavSettingsPage_CalendarModeCalDavDescription": "Os itens do calendário são sincronizados com seu servidor CalDAV.", + "ImapCalDavSettingsPage_CalendarModeLocalOnlyDescription": "Os itens do calendário são armazenados apenas neste computador e não são sincronizados com a rede.", + "ImapCalDavSettingsPage_LocalCalendarLearnMore": "Como funciona o calendário local", + "ImapCalDavSettingsPage_LocalCalendarDialogTitle": "Apenas calendário local", + "ImapCalDavSettingsPage_LocalCalendarDialogMessage": "O calendário local mantém todos os eventos apenas no seu computador. Nada é sincronizado com iCloud, Yahoo ou qualquer outro provedor.", + "ImapCalDavSettingsPage_CalDavServiceUrl": "URL do serviço CalDAV", + "ImapCalDavSettingsPage_CalDavUsername": "Nome de usuário CalDAV", + "ImapCalDavSettingsPage_CalDavPassword": "Senha CalDAV", + "ImapCalDavSettingsPage_CalDavNotRequiredMessage": "O teste CalDAV é necessário apenas quando o modo de calendário estiver configurado para sincronização CalDAV.", + "ImapCalDavSettingsPage_CalDavUrlRequired": "URL do serviço CalDAV é obrigatório.", + "ImapCalDavSettingsPage_CalDavUrlInvalid": "A URL do serviço CalDAV deve ser uma URL absoluta.", + "ImapCalDavSettingsPage_CalDavUsernameRequired": "O nome de usuário do CalDAV é obrigatório.", + "ImapCalDavSettingsPage_CalDavPasswordRequired": "A senha do CalDAV é obrigatória.", + "ImapCalDavSettingsPage_TestImapButton": "Testar conexão IMAP", + "ImapCalDavSettingsPage_TestCalDavButton": "Testar conexão CalDAV", + "ImapCalDavSettingsPage_ImapTestSuccessMessage": "Teste de conexão IMAP bem-sucedido.", + "ImapCalDavSettingsPage_CalDavTestSuccessMessage": "Teste de conexão CalDAV bem-sucedido.", + "ImapCalDavSettingsPage_SaveSuccessMessage": "Configurações da conta validadas e salvas.", + "ImapCalDavSettingsPage_ICloudHint": "Use uma senha específica de aplicativo gerada nas configurações da sua conta Apple.", + "ImapCalDavSettingsPage_YahooHint": "Use uma senha de aplicativo nas configurações de segurança da sua conta Yahoo.", "Info_AccountCreatedMessage": "{0} foi criado", "Info_AccountCreatedTitle": "Criação de Conta", "Info_AccountCreationFailedTitle": "Erro na criação da conta", "Info_AccountDeletedMessage": "{0} foi excluído com sucesso.", "Info_AccountDeletedTitle": "Conta excluída", "Info_AccountIssueFixFailedTitle": "Falhou", + "Info_AccountIssueFixImapMessage": "Abra a página de configurações de IMAP e calendário para inserir novamente as credenciais do servidor.", + "Info_AccountAttentionRequiredMessage": "Esta conta requer sua atenção.", + "Info_AccountAttentionRequiredClickableMessage": "Clique para corrigir esta conta e ressincronizá-la.", + "Info_AccountAttentionRequiredAction": "Corrigir", + "Info_AccountAttentionRequiredActionHint": "Clique em Corrigir para resolver este problema da conta.", "Info_AccountIssueFixSuccessMessage": "Corrigido todos os problemas da conta.", "Info_AccountIssueFixSuccessTitle": "Sucesso", "Info_AttachmentOpenFailedMessage": "Não foi possível abrir este anexo.", @@ -370,6 +584,7 @@ "InfoBarMessage_SynchronizationDisabledFolder": "Esta pasta está desabilitada para sincronização.", "InfoBarTitle_SynchronizationDisabledFolder": "Pasta desabilitada", "Justify": "Justificado", + "MenuUpdateAvailable": "Atualização disponível", "Left": "Esquerda", "Link": "Link", "LinkedAccountsCreatePolicyMessage": "você precisa ter pelo menos 2 contas para criar o vinculo\no vinculo será removido ao salvar", @@ -403,6 +618,7 @@ "MailOperation_Unarchive": "Desarquivar", "MailOperation_ViewMessageSource": "Ver fonte da mensagem", "MailOperation_Zoom": "Ampliar/Reduzir", + "MailsDragging": "Arrastando {0} item(s)", "MailsSelected": "{0} item(s) selecionado(s)", "MarkFlagUnflag": "Marcar como sinalizado/não sinalizado", "MarkReadUnread": "Marcar como lido/não lido", @@ -434,6 +650,8 @@ "Notifications_MultipleNotificationsTitle": "Novo E-mail", "Notifications_WinoUpdatedMessage": "Veja a nova versão {0}", "Notifications_WinoUpdatedTitle": "O Wino Mail foi atualizado.", + "Notifications_StoreUpdateAvailableTitle": "Atualização disponível", + "Notifications_StoreUpdateAvailableMessage": "Uma versão mais recente do Wino Mail está pronta para instalar na Microsoft Store.", "OnlineSearchFailed_Message": "Falha ao realizar a pesquisa\n{0}\n\nlistando e-mails offline.", "OnlineSearchTry_Line1": "Não conseguiu encontrar o que estava procurando?", "OnlineSearchTry_Line2": "Experimente a pesquisa online.", @@ -446,7 +664,6 @@ "PaneLengthOption_Small": "Pequeno", "Photos": "Fotos", "PreparingFoldersMessage": "Preparando pastas", - "ProtocolLogAvailable_Message": "Os registros do protocolo estão disponíveis para diagnósticos.", "ProviderDetail_Gmail_Description": "Conta do Google", "ProviderDetail_iCloud_Description": "Conta Apple iCloud", "ProviderDetail_iCloud_Title": "iCloud", @@ -465,9 +682,14 @@ "SearchBarPlaceholder": "Pesquisar", "SearchingIn": "Pesquisar em", "SearchPivotName": "Resultados", + "Settings_KeyboardShortcuts_Title": "Atalhos de Teclado", + "Settings_KeyboardShortcuts_Description": "Gerencie atalhos de teclado para ações rápidas nas mensagens.", "SettingConfigureSpecialFolders_Button": "Configurar", "SettingsEditAccountDetails_IMAPConfiguration_Title": "Configuração IMAP/SMTP", "SettingsEditAccountDetails_IMAPConfiguration_Description": "Altere suas configurações do servidor de entrada/saída.", + "SettingsEditAccountDetails_ImapCalDavSettings_Title": "Configurações de IMAP e CalDAV", + "SettingsEditAccountDetails_ImapCalDavSettings_Description": "Abra a página de configurações dedicada de IMAP, SMTP e CalDAV para esta conta.", + "SettingsEditAccountDetails_ImapCalDavSettings_Action": "Abrir configurações", "SettingsAbout_Description": "Saiba mais sobre o Wino.", "SettingsAbout_Title": "Sobre", "SettingsAboutGithub_Description": "Ir para o repositório GitHub do rastreador de problemas.", @@ -490,6 +712,10 @@ "SettingsAppPreferences_SearchMode_Local": "Local", "SettingsAppPreferences_SearchMode_Online": "Online", "SettingsAppPreferences_SearchMode_Title": "Modo de busca padrão", + "SettingsAppPreferences_ApplicationMode_Title": "Modo padrão do aplicativo", + "SettingsAppPreferences_ApplicationMode_Description": "Escolha em que modo o Wino abre quando nenhum tipo de ativação estiver explicitamente definido.", + "SettingsAppPreferences_ApplicationMode_Mail": "E-mail", + "SettingsAppPreferences_ApplicationMode_Calendar": "Calendário", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description": "O Wino Mail continuará em execução em segundo plano. Você será notificado quando chegarem novos e-mails.", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title": "Executar em segundo plano", "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description": "O Wino Mail continuará em execução na bandeja do sistema. Disponível para iniciar clicando no ícone. Você será notificado assim que novos e-mails chegaram.", @@ -506,12 +732,30 @@ "SettingsAppPreferences_StartupBehavior_FatalError": "Ocorreu um erro fatal ao alterar o modo de inicialização do Wino Mail.", "SettingsAppPreferences_StartupBehavior_Title": "Iniciar minimizado na inicialização do Windows", "SettingsAppPreferences_Title": "Preferências do Aplicativo", + "SettingsAppPreferences_HideWinoAccountButton_Title": "Ocultar o botão do perfil na barra de título", + "SettingsAppPreferences_HideWinoAccountButton_Description": "Ocultar o botão de perfil na barra de título que abre o menu suspenso da conta do Wino.", + "SettingsAppPreferences_StoreUpdateNotifications_Title": "Notificações de atualização da Microsoft Store", + "SettingsAppPreferences_StoreUpdateNotifications_Description": "Exibir notificações e ações no rodapé quando houver uma atualização disponível da Microsoft Store.", + "SettingsAppPreferences_AiActions_Title": "Ações de IA", + "SettingsAppPreferences_AiActions_Description": "Escolha os idiomas padrão da IA e onde os resumos devem ser salvos.", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Title": "Idioma de tradução padrão", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Description": "Selecione o idioma de destino padrão usado pelas ações de tradução por IA.", + "SettingsAppPreferences_AiSummarizeLanguage_Title": "Idioma de resumo", + "SettingsAppPreferences_AiSummarizeLanguage_Description": "Escolha o idioma de resumo preferido para os resumos gerados pela IA.", + "SettingsAppPreferences_AiSummarySavePath_Title": "Caminho padrão para salvar resumos", + "SettingsAppPreferences_AiSummarySavePath_Description": "Escolha a pasta que o Wino deve usar como padrão ao salvar resumos de IA.", + "SettingsAppPreferences_AiSummarySavePath_Placeholder": "Usar o local de salvamento padrão do sistema", + "SettingsAppPreferences_AiSummarySavePath_InvalidHint": "Essa pasta não existe. O local de salvamento padrão será usado para os resumos.", "SettingsAutoSelectNextItem_Description": "Selecione o próximo item depois de excluir ou mover um e-mail.", "SettingsAutoSelectNextItem_Title": "Selecionar automaticamente o próximo item", "SettingsAvailableThemes_Description": "Selecione um tema da própria coleção do Wino para o seu gosto ou aplique seus próprios temas.", "SettingsAvailableThemes_Title": "Temas Disponíveis", "SettingsCalendarSettings_Description": "Alterar o primeiro dia da semana, altura da célula da hora e mais...", "SettingsCalendarSettings_Title": "Configurações do calendário", + "CalendarSettings_DefaultSnoozeDuration_Header": "Duração padrão do soneca", + "CalendarSettings_DefaultSnoozeDuration_Description": "Defina uma duração padrão de soneca para as notificações de lembrete do calendário.", + "CalendarSettings_TimedDayHeaderFormat_Header": "Formato do cabeçalho do dia na visualização com horário", + "CalendarSettings_TimedDayHeaderFormat_Description": "Escolha como os rótulos dos dias no topo são renderizados nas visualizações de dia, semana e semana de trabalho. Use tokens de formato de data como ddd, dd, MMM ou dddd.", "SettingsComposer_Title": "Compositor", "SettingsComposerFont_Title": "Fonte padrão do compositor", "SettingsComposerFontFamily_Description": "Altere a família de fontes padrão e o tamanho da fonte para escrever e-mails.", @@ -531,6 +775,9 @@ "SettingsDiscord_Title": "Canal do Discord", "SettingsEditLinkedInbox_Description": "Adicionar/remover contas, renomear ou quebrar o vínculo entre contas.", "SettingsEditLinkedInbox_Title": "Editar caixa de entrada vinculada", + "SettingsWindowBackdrop_Title": "Plano de Fundo da Janela", + "SettingsWindowBackdrop_Description": "Selecione um efeito de plano de fundo para as janelas do Wino.", + "SettingsWindowBackdrop_Disabled": "A seleção de plano de fundo da janela está desativada quando o tema da aplicação for diferente de Padrão.", "SettingsElementTheme_Description": "Selecione um tema do Windows para o Wino", "SettingsElementTheme_Title": "Tema do Elemento", "SettingsElementThemeSelectionDisabled": "Seleção de tema de elemento está desabilitada quando o tema da aplicação é selecionado diferente do padrão.", @@ -581,6 +828,8 @@ "SettingsManageAliases_Title": "Apelidos", "SettingsEditAccountDetails_Title": "Editar dados da conta", "SettingsEditAccountDetails_Description": "Alterar nome da conta, nome do remetente e atribuir uma nova cor, se quiser.", + "EditAccountDetailsPage_SaveSuccess_Title": "Alterações salvas", + "EditAccountDetailsPage_SaveSuccess_Message": "Os detalhes da sua conta foram atualizados com sucesso.", "SettingsManageLink_Description": "Mova os itens para adicionar um novo vínculo ou remover o vínculo existente.", "SettingsManageLink_Title": "Gerenciar Vinculo", "SettingsMarkAsRead_Description": "Altere o que deve acontecer com o item selecionado.", @@ -596,7 +845,41 @@ "SettingsNotifications_Title": "Notificações", "SettingsNotificationsAndTaskbar_Description": "Alterar se as notificações devem ser exibidas e ícone na barra de tarefas para esta conta.", "SettingsNotificationsAndTaskbar_Title": "Notificações & Barra de Tarefas", + "SettingsHome_Title": "Início", + "SettingsHome_SearchTitle": "Encontre uma configuração", + "SettingsHome_SearchDescription": "Pesquise por recurso, tópico ou palavra-chave para ir direto à página de configurações correta.", + "SettingsHome_SearchPlaceholder": "Pesquisar configurações", + "SettingsHome_SearchExamples": "Exemplos: tema, armazenamento, idioma, assinatura", + "SettingsHome_QuickLinks_Title": "Links rápidos", + "SettingsHome_QuickLinks_Description": "Acesse rapidamente as configurações que as pessoas mais usam.", + "SettingsHome_StorageCard_Description": "Veja quanto conteúdo MIME local o Wino mantém neste dispositivo e limpe-o quando necessário.", + "SettingsHome_StorageEmptySummary": "Nenhum conteúdo MIME em cache detectado ainda.", + "SettingsHome_StorageLoading": "Verificando o uso local de MIME...", + "SettingsHome_Tips_Title": "Dicas e truques", + "SettingsHome_Tips_Description": "Algumas pequenas mudanças podem tornar o Wino muito mais pessoal.", + "SettingsHome_Tip_Theme": "Quer modo escuro ou alterações de acento? Abra Personalização.", + "SettingsHome_Tip_Background": "Use as Preferências do Aplicativo para controlar o comportamento de inicialização e a sincronização em segundo plano.", + "SettingsHome_Tip_Shortcuts": "Atalhos de teclado ajudam você a navegar pelo e-mail mais rapidamente.", + "SettingsHome_Resources_Title": "Links úteis", + "SettingsHome_Resources_Description": "Abra recursos do projeto, informações de suporte e canais de lançamento.", "SettingsOptions_Title": "Configurações", + "SettingsOptions_GeneralSection": "Geral", + "SettingsOptions_MailSection": "E-mail", + "SettingsOptions_CalendarSection": "Calendário", + "SettingsOptions_MoreComingSoon": "Mais opções chegando em breve", + "SettingsOptions_HeroDescription": "Personalize sua experiência com o Wino Mail.", + "SettingsOptions_AccountsSummary": "{0} conta(s) configurada(s)", + "SettingsSearch_ManageAccounts_Keywords": "conta;contas;caixa de correio;caixas de correio;apelido;apelidos;perfil;endereço;endereços", + "SettingsSearch_AppPreferences_Keywords": "inicialização;fundo;iniciar;sincronização;notificação;notificações;pesquisa;bandeja;padrões", + "SettingsSearch_LanguageTime_Keywords": "idioma;hora;horário;localidade;região;formato;24 horas;24h", + "SettingsSearch_Personalization_Keywords": "tema;escuro;claro;aparência;cor de destaque;cor;cores;layout;densidade", + "SettingsSearch_About_Keywords": "sobre;versão;site;privacidade;GitHub;doar;loja;suporte", + "SettingsSearch_KeyboardShortcuts_Keywords": "atalho;atalhos;atalho;atalhos;teclado;teclas", + "SettingsSearch_MessageList_Keywords": "mensagem;mensagens;lista;encadeamento;conversas;avatar;pré-visualização;remetente", + "SettingsSearch_ReadComposePane_Keywords": "leitor;compor;redator;fonte;fontes;conteúdo externo;exibição;leitura", + "SettingsSearch_SignatureAndEncryption_Keywords": "assinatura;assinaturas;criptografia;certificado;certificados;S/MIME;S/MIME;segurança", + "SettingsSearch_Storage_Keywords": "armazenamento;cache;cache;mime;disco;espaço;limpeza;limpar;dados locais", + "SettingsSearch_CalendarSettings_Keywords": "calendário;semana;horas;programação;evento;eventos", "SettingsPaneLengthReset_Description": "Redefina o tamanho da lista de e-mails para o padrão se você estiver com problemas.", "SettingsPaneLengthReset_Title": "Redefinir tamanho da lista de e-mails", "SettingsPaypal_Description": "Mostre muito mais amor ❤️ Todas as doações são apreciadas.", @@ -610,6 +893,8 @@ "SettingsPrefer24HourClock_Title": "Exibir o formato do relógio em 24 horas", "SettingsPrivacyPolicy_Description": "Revise a política de privacidade.", "SettingsPrivacyPolicy_Title": "Política de Privacidade", + "SettingsWebsite_Description": "Abrir o site do Wino Mail.", + "SettingsWebsite_Title": "Site", "SettingsReadComposePane_Description": "Fontes, conteúdo externo.", "SettingsReadComposePane_Title": "Leitor & Compositor", "SettingsReader_Title": "Leitor", @@ -625,6 +910,19 @@ "SettingsShowPreviewText_Title": "Mostrar Pré-visualização de Texto", "SettingsShowSenderPictures_Description": "Ocultar/exibir imagens do remetente da miniatura.", "SettingsShowSenderPictures_Title": "Mostrar Avatares de Remetentes", + "SettingsEmailTemplates_Title": "Modelos de e-mail", + "SettingsEmailTemplates_Description": "Gerenciar modelos de e-mail", + "SettingsEmailTemplates_CreatePageTitle": "Novo modelo", + "SettingsEmailTemplates_EditPageTitle": "Editar modelo", + "SettingsEmailTemplates_NewTemplateTitle": "Novo modelo", + "SettingsEmailTemplates_NewTemplateDescription": "Criar um novo modelo de e-mail", + "SettingsEmailTemplates_NameTitle": "Nome", + "SettingsEmailTemplates_NamePlaceholder": "Nome do modelo", + "SettingsEmailTemplates_DescriptionTitle": "Descrição", + "SettingsEmailTemplates_DescriptionPlaceholder": "Descrição opcional", + "SettingsEmailTemplates_ContentTitle": "Conteúdo do modelo", + "SettingsEmailTemplates_ContentDescription": "Edite o conteúdo HTML deste modelo.", + "SettingsEmailTemplates_NameRequired": "O nome do modelo é obrigatório.", "SettingsEnableGravatarAvatars_Title": "Gravatar", "SettingsEnableGravatarAvatars_Description": "Usar gravatar (se disponível) como imagem do remetente", "SettingsEnableFavicons_Title": "Ícones de domínio (Favicons)", @@ -645,6 +943,33 @@ "SettingsStartupItem_Title": "Item de inicialização", "SettingsStore_Description": "Mostre um pouco de amor ❤️", "SettingsStore_Title": "Avalie na Loja", + "SettingsStorage_Title": "Armazenamento", + "SettingsStorage_Description": "Digitalizar e gerenciar o cache MIME armazenado na sua pasta de dados local.", + "SettingsStorage_ScanFolder": "Digitalizar a pasta de dados locais", + "SettingsStorage_NoLocalMimeDataFound": "Nenhum dado MIME local encontrado.", + "SettingsStorage_NoAccountsFound": "Nenhuma conta encontrada.", + "SettingsStorage_TotalUsage": "Uso total local de MIME: {0}", + "SettingsStorage_AccountUsageDescription": "{0} utilizado(s) no cache MIME local", + "SettingsStorage_DeleteAll_Title": "Excluir todo o conteúdo MIME", + "SettingsStorage_DeleteAll_Description": "Excluir toda a pasta de cache MIME desta conta.", + "SettingsStorage_DeleteAll_Button": "Excluir tudo", + "SettingsStorage_DeleteAll_Confirm_Title": "Excluir todo o conteúdo MIME", + "SettingsStorage_DeleteAll_Confirm_Message": "Excluir todos os dados MIME locais para {0}?", + "SettingsStorage_DeleteAll_Success": "Todo o conteúdo MIME foi excluído.", + "SettingsStorage_DeleteOld_Title": "Excluir conteúdo MIME antigo", + "SettingsStorage_DeleteOld_Description": "Excluir arquivos MIME com base na data de criação dos e-mails no banco de dados local.", + "SettingsStorage_DeleteOld_1Month": "> 1 mês", + "SettingsStorage_DeleteOld_3Months": "> 3 meses", + "SettingsStorage_DeleteOld_6Months": "> 6 meses", + "SettingsStorage_DeleteOld_1Year": "> 1 ano", + "SettingsStorage_DeleteOld_Confirm_Title": "Excluir conteúdo MIME antigo", + "SettingsStorage_DeleteOld_Confirm_Message": "Excluir dados MIME locais com mais de {0} para {1}?", + "SettingsStorage_DeleteOld_Success": "Excluídas {0} pasta(s) MIME mais antigas que {1}.", + "SettingsStorage_1Month": "1 mês", + "SettingsStorage_3Months": "3 meses", + "SettingsStorage_6Months": "6 meses", + "SettingsStorage_1Year": "1 ano", + "SettingsStorage_Months": "{0} meses", "SettingsTaskbarBadge_Description": "Incluir contagem de emails não lidos no ícone da barra de tarefas.", "SettingsTaskbarBadge_Title": "Ícone da Barra de Tarefas", "SettingsThreads_Description": "Organizar mensagens em tópicos de conversa.", @@ -683,6 +1008,9 @@ "SystemFolderConfigDialogValidation_InboxSelected": "Você não pode atribuir a pasta Caixa de entrada para outra pasta do sistema.", "SystemFolderConfigSetupSuccess_Message": "Pastas do sistema configuradas com sucesso.", "SystemFolderConfigSetupSuccess_Title": "Configuração de pastas do sistema", + "SystemTrayMenu_ShowWino": "Abrir Wino Mail", + "SystemTrayMenu_ShowWinoCalendar": "Abrir Wino Calendário", + "SystemTrayMenu_ExitWino": "Sair", "TestingImapConnectionMessage": "Testando conexão do servidor...", "TitleBarServerDisconnectedButton_Description": "Wino está desconectado da rede. Clique em reconectar para restaurar a conexão.", "TitleBarServerDisconnectedButton_Title": "Sem conexão", @@ -699,8 +1027,422 @@ "WinoUpgradeMessage": "Atualizar para Contas Ilimitadas", "WinoUpgradeRemainingAccountsMessage": "{0} de {1} contas gratuitas usadas.", "Yesterday": "Ontem", + "Smime_ImportCertificates_Success": "Certificados importados com sucesso.", + "Smime_ImportCertificates_Error": "Erro ao importar certificados: {0}", + "Smime_RemoveCertificates_Confirm": "Você realmente deseja remover os certificados {0}?", + "Smime_RemoveCertificates_Success": "Certificados removidos.", + "Smime_ExportCertificates_Success": "Certificados exportados.", + "Smime_ExportCertificates_Error": "Erro ao exportar certificados.", + "Smime_CertificateDetails": "Assunto: {0}\\nEmissor: {1}\\nVálido de: {2}\\nVálido até: {3}\\nImpressão digital: {4}", + "Smime_CertificatePassword_Title": "Senha do certificado obrigatória", + "Smime_CertificatePassword_Placeholder": "Senha do certificado para {0} (opcional)", + "Smime_Confirm_Title": "Confirmar", + "Buttons_OK": "OK", + "Buttons_Refresh": "Atualizar", + "SettingsSignatureAndEncryption_Title": "Assinatura e Criptografia", + "SettingsSignatureAndEncryption_Description": "Gerenciar certificados S/MIME para assinar e criptografar e-mails.", + "SettingsSignatureAndEncryption_MyCertificatesHeader": "Meus certificados", + "SettingsSignatureAndEncryption_MyCertificatesDescription": "Certificados pessoais para assinatura e criptografia", + "SettingsSignatureAndEncryption_RecipientCertificatesHeader": "Certificados do destinatário", + "SettingsSignatureAndEncryption_RecipientCertificatesDescription": "Certificados de destinatário para descriptografia", + "SettingsSignatureAndEncryption_NameColumn": "Nome", + "SettingsSignatureAndEncryption_ExpiresColumn": "Vence em", + "SettingsSignatureAndEncryption_ThumbprintColumn": "Impressão digital", + "Buttons_Remove": "Remover", + "Buttons_Export": "Exportar", + "Buttons_Import": "Importar", + "SettingsSignatureAndEncryption_SigningCertificate": "Certificado de assinatura S/MIME", + "SettingsSignatureAndEncryption_EncryptionCertificate": "Criptografia S/MIME", + "SettingsSignatureAndEncryption_SigningCertificatePlaceholder": "Nenhum", + "SmimeSignaturesInMessage": "Assinaturas nesta mensagem:", + "SmimeSignatureEntry": "• {0} {1} ({2}, válido até {3} - {4})", + "SmimeSigningCertificateInfoTitle": "Informação do Certificado de Assinatura S/MIME", + "SmimeCertificateInfoTitle": "Informação do Certificado S/MIME", + "SmimeNoCertificateFileFound": "Nenhum arquivo de certificado encontrado", + "SmimeSaveCertificate": "Salvar certificado...", + "SmimeCertificate": "Certificado S/MIME", + "SmimeCertificateSavedTo": "Certificado salvo em {0}", + "SmimeSignedTooltip": "Esta mensagem está assinada com um certificado S/MIME. Clique para ver mais detalhes", + "SmimeEncryptedTooltip": "Esta mensagem está criptografada com um certificado S/MIME.", + "SmimeCertificateFileInfo": "Arquivo: {0}\\nTipo: {1}\\nTamanho: {2:N0} bytes", + "Composer_LightTheme": "Tema Claro", + "Composer_DarkTheme": "Tema Escuro", + "Composer_Outdent": "Diminuir recuo", + "Composer_Indent": "Aumentar recuo", + "Composer_BulletList": "Lista com marcadores", + "Composer_OrderedList": "Lista numerada", + "Composer_Stroke": "Traço", + "Composer_Bold": "Negrito", + "Composer_Italic": "Itálico", + "Composer_Underline": "Sublinhado", + "Composer_CcBcc": "Cc e Cco", + "Composer_EnableSmimeSignature": "Ativar/desativar assinatura S/MIME", + "Composer_EnableSmimeEncryption": "Ativar/desativar criptografia S/MIME", + "Composer_LocalDraftSyncInfo": "Este rascunho é apenas local. O Wino não conseguiu enviá-lo para o servidor de e-mail. Clique para tentar enviá-lo novamente ao servidor.", + "Composer_CertificateExpires": "Vence em: ", + "Composer_SmimeSignature": "Assinatura S/MIME", + "Composer_SmimeEncryption": "Criptografia S/MIME", + "Composer_EmailTemplatesPlaceholder": "Modelos de e-mail", + "Composer_AiSummarize": "Resumir com IA", + "Composer_AiSummarizeDescription": "Extrair pontos-chave, itens de ação e decisões deste e-mail.", + "Composer_AiTranslate": "Traduzir com IA", + "Composer_AiActions": "Ações de IA", + "Composer_AiRewrite": "Reescrever com IA", + "AiActions_CheckingStatus": "Verificando o acesso à IA...", + "AiActions_SignedOutTitle": "Desbloquear o Pacote de IA do Wino", + "AiActions_SignedOutDescription": "Traduza, reescreva e resuma e-mails com IA após fazer login em sua Conta Wino e ativar o complemento Pacote de IA.", + "AiActions_NoPackTitle": "Pacote de IA necessário", + "AiActions_NoPackDescription": "Você está conectado, mas o Pacote de IA ainda não está ativo. Compre-o para usar as ferramentas de tradução, reescrita e resumos com IA do Wino.", + "AiActions_UsageSummary": "{0} de {1} créditos usados neste mês.", + "Composer_AiRewritePolite": "Tornar mais polido", + "Composer_AiRewritePoliteDescription": "Suaviza a redação mantendo a mesma intenção.", + "Composer_AiRewriteAngry": "Tornar mais agressivo", + "Composer_AiRewriteAngryDescription": "Usa um tom mais áspero e confrontacional.", + "Composer_AiRewriteHappy": "Torne-o feliz", + "Composer_AiRewriteHappyDescription": "Adiciona um tom mais otimista e entusiasmado.", + "Composer_AiRewriteFormal": "Torne-o formal", + "Composer_AiRewriteFormalDescription": "Faz com que a mensagem soe mais profissional e estruturada.", + "Composer_AiRewriteFriendly": "Torne-o mais amigável", + "Composer_AiRewriteFriendlyDescription": "Aquece a mensagem com um tom mais acessível.", + "Composer_AiRewriteShorter": "Torne-o mais curto", + "Composer_AiRewriteShorterDescription": "Torna o texto mais curto e remove detalhes desnecessários.", + "Composer_AiRewriteClearer": "Torne-o mais claro", + "Composer_AiRewriteClearerDescription": "Melhora a legibilidade e torna a mensagem mais fácil de entender.", + "Composer_AiRewriteCustom": "Personalizado", + "Composer_AiRewriteCustomDescription": "Descreva sua intenção de reescrita.", + "Composer_AiRewriteCustomPlaceholder": "Descreva como você quer que a mensagem seja reescrita", + "Composer_AiRewriteMode": "Tom da reescrita", + "Composer_AiRewriteApply": "Aplicar reescrita", + "Composer_AiTranslateDialogTitle": "Traduzir com IA", + "Composer_AiTranslateDialogDescription": "Digite o idioma-alvo ou código de cultura, como en-US, tr-TR, de-DE ou fr-FR.", + "Composer_AiTranslateApply": "Traduzir", + "Composer_AiTranslateLanguage": "Idioma-alvo", + "Composer_AiTranslateCustomPlaceholder": "Digite o código de cultura", + "Composer_AiTranslateLanguageEnglish": "Inglês (en-US)", + "Composer_AiTranslateLanguageTurkish": "Turco (tr-TR)", + "Composer_AiTranslateLanguageGerman": "Alemão (de-DE)", + "Composer_AiTranslateLanguageFrench": "Francês (fr-FR)", + "Composer_AiTranslateLanguageSpanish": "Espanhol (es-ES)", + "Composer_AiTranslateLanguageItalian": "Italiano (it-IT)", + "Composer_AiTranslateLanguagePortugueseBrazil": "Português (Brasil) (pt-BR)", + "Composer_AiTranslateLanguageDutch": "Holandês (nl-NL)", + "Composer_AiTranslateLanguagePolish": "Polonês (pl-PL)", + "Composer_AiTranslateLanguageRussian": "Russo (ru-RU)", + "Composer_AiTranslateLanguageJapanese": "Japonês (ja-JP)", + "Composer_AiTranslateLanguageKorean": "Coreano (ko-KR)", + "Composer_AiTranslateLanguageChineseSimplified": "Chinês, Simplificado (zh-CN)", + "Composer_AiTranslateLanguageArabic": "Árabe (ar-SA)", + "Composer_AiTranslateLanguageHindi": "Hindi (hi-IN)", + "Composer_AiTranslateLanguageOther": "Outro...", + "Composer_AiBusyTitle": "A IA já está em funcionamento.", + "Composer_AiBusyMessage": "Por favor, aguarde a conclusão da ação atual da IA.", + "Composer_AiSignInRequired": "Faça login na sua Conta Wino para usar os recursos de IA.", + "Composer_AiMissingHtml": "Ainda não há conteúdo de mensagem para enviar para a IA do Wino.", + "Composer_AiQuotaUnavailable": "O resultado da IA foi aplicado.", + "Composer_AiAppliedMessage": "O resultado da IA foi aplicado ao compositor. Use Desfazer se quiser reverter.", + "Composer_AiSummarizeSuccessTitle": "Resumo da IA aplicado", + "Composer_AiTranslateSuccessTitle": "Tradução da IA aplicada", + "Composer_AiRewriteSuccessTitle": "Reescrita da IA aplicada", + "Composer_AiErrorTitle": "Ação da IA falhou", + "Reader_AiAppliedMessage": "O resultado da IA é exibido para esta mensagem. Reabra a mensagem para ver o conteúdo original novamente.", "SettingsAppPreferences_EmailSyncInterval_Title": "Intervalo de sincronização de e-mail", - "SettingsAppPreferences_EmailSyncInterval_Description": "Intervalo de sincronização automática de e-mails (minutos). Esta configuração será aplicada apenas após reiniciar o Wino Mail." + "SettingsAppPreferences_EmailSyncInterval_Description": "Intervalo de sincronização automática de e-mails (minutos). Esta configuração será aplicada apenas após reiniciar o Wino Mail.", + "ContactsPage_Title": "Contatos", + "ContactsPage_AddContact": "Adicionar Contato", + "ContactsPage_EditContact": "Editar Contato", + "ContactsPage_DeleteContact": "Excluir Contato", + "ContactsPage_SearchPlaceholder": "Pesquisar contatos...", + "ContactsPage_NoContacts": "Nenhum contato encontrado", + "ContactsPage_ContactsCount": "{0} contatos", + "ContactsPage_SelectedContactsCount": "{0} selecionado(s)", + "ContactsPage_DeleteSelectedContacts": "Excluir Selecionados", + "ContactEditDialog_Title": "Editar Contato", + "ContactEditDialog_PhotoSection": "Foto", + "ContactEditDialog_ChoosePhoto": "Escolher Foto", + "ContactEditDialog_RemovePhoto": "Remover Foto", + "ContactEditDialog_NameHeader": "Nome", + "ContactEditDialog_NamePlaceholder": "Nome do contato", + "ContactEditDialog_EmailHeader": "Endereço de E-mail", + "ContactEditDialog_EmailPlaceholder": "contato@exemplo.com", + "ContactEditDialog_InfoSection": "Informações de Contato", + "ContactEditDialog_RootContactInfo": "Este é um contato raiz associado às suas contas e não pode ser excluído.", + "ContactEditDialog_OverriddenContactInfo": "Este contato foi modificado manualmente e não será atualizado durante a sincronização.", + "ContactsPage_Subtitle": "Gerencie seus contatos de e-mail e as informações deles.", + "ContactStatus_Account": "Conta", + "ContactStatus_Modified": "Modificado", + "ContactAction_Edit": "Editar contato", + "ContactAction_ChangePhoto": "Alterar foto", + "ContactAction_Delete": "Excluir contato", + "ContactAction_Add": "Adicionar Contato", + "ContactSelection_Selected": "Selecionado", + "ContactSelection_SelectAll": "Selecionar Tudo", + "ContactSelection_Clear": "Limpar Seleção", + "ContactsPage_EmptyState": "Nenhum contato para exibir", + "ContactsPage_AddFirstContact": "Adicione seu primeiro contato", + "ContactsPage_ContactsCountSuffix": "contatos", + "ContactsPane_NewContact": "Novo Contato", + "ContactsPane_DescriptionTitle": "Gerencie seus contatos", + "ContactsPane_DescriptionBody": "Crie contatos, renomeie-os, atualize fotos de perfil e mantenha dados salvos organizados em um só local.", + "ContactEditDialog_AddTitle": "Adicionar Contato", + "ContactInfoBar_ContactAdded": "Contato adicionado com sucesso.", + "ContactInfoBar_ContactUpdated": "Contato atualizado com sucesso.", + "ContactInfoBar_ContactsDeleted": "Contatos excluídos com sucesso.", + "ContactInfoBar_ContactPhotoUpdated": "Foto de contato atualizada com sucesso.", + "ContactInfoBar_FailedToLoadContacts": "Falha ao carregar contatos: {0}", + "ContactInfoBar_FailedToAddContact": "Falha ao adicionar contato: {0}", + "ContactInfoBar_FailedToUpdateContact": "Falha ao atualizar contato: {0}", + "ContactInfoBar_FailedToDeleteContacts": "Falha ao excluir contatos: {0}", + "ContactInfoBar_FailedToUpdatePhoto": "Falha ao atualizar a foto: {0}", + "ContactInfoBar_CannotDeleteRoot": "Contatos raiz não podem ser excluídos.", + "ContactConfirmDialog_DeleteTitle": "Excluir Contato", + "ContactConfirmDialog_DeleteMessage": "Tem certeza de que deseja excluir o contato '{0}'?", + "ContactConfirmDialog_DeleteMultipleMessage": "Tem certeza de que deseja excluir {0} contato(s)?", + "ContactConfirmDialog_DeleteButton": "Excluir", + "CalendarAccountSettings_Title": "Configurações da Conta de Calendário", + "CalendarAccountSettings_Description": "Gerencie as configurações de calendário para {0}", + "CalendarAccountSettings_AccountColor": "Cor da Conta", + "CalendarAccountSettings_AccountColorDescription": "Altere a cor de exibição desta conta de calendário.", + "CalendarAccountSettings_SyncEnabled": "Ativar sincronização", + "CalendarAccountSettings_SyncEnabledDescription": "Ative ou desative a sincronização do calendário para esta conta.", + "CalendarAccountSettings_DefaultShowAs": "Estado de disponibilidade padrão", + "CalendarAccountSettings_DefaultShowAsDescription": "Estado de disponibilidade padrão para novos eventos criados com esta conta.", + "CalendarAccountSettings_PrimaryCalendar": "Calendário principal", + "CalendarAccountSettings_PrimaryCalendarDescription": "Marque este calendário como o calendário principal para a conta.", + "CalendarSettings_NewEventBehavior_Header": "Comportamento do botão Novo Evento", + "CalendarSettings_NewEventBehavior_Description": "Escolha se o botão Novo Evento deve pedir um calendário a cada vez ou abrir sempre um calendário específico.", + "CalendarSettings_NewEventBehavior_AskEachTime": "Perguntar a cada vez.", + "CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar": "Sempre usar calendário específico.", + "CalendarSettings_Rendering_Title": "Renderização", + "CalendarSettings_Rendering_Description": "Configure o layout do calendário e o comportamento de exibição.", + "CalendarSettings_Notifications_Title": "Notificações", + "CalendarSettings_Notifications_Description": "Escolha o lembrete padrão e o comportamento de adiar.", + "CalendarSettings_Preferences_Title": "Preferências", + "CalendarSettings_Preferences_Description": "Defina como o botão Novo Evento se comporta.", + "WhatIsNew_GetStartedButton": "Começar", + "WhatIsNew_ContinueAnywayButton": "Continuar mesmo assim.", + "WhatIsNew_PreparingForNewVersionButton": "Preparando para a nova versão...", + "WhatIsNew_MigrationPreparing_Title": "Preparando seus dados", + "WhatIsNew_MigrationPreparing_Description": "O Wino está aplicando migrações de atualização. Por favor, aguarde enquanto preparamos os dados da sua conta para esta versão.", + "WhatIsNew_MigrationFailedMessage": "Falha ao aplicar migrações com o código de erro {0}. Você pode continuar usando o aplicativo. No entanto, se encontrar problemas sérios, reinstale o aplicativo.", + "WhatIsNew_MigrationNotification_Title": "Wino Mail Atualizado", + "WhatIsNew_MigrationNotification_Message": "Abra o aplicativo para concluir a atualização e ver as novidades.", + "WelcomeWindow_Title": "Bem-vindo ao Wino Mail", + "WelcomeWindow_Subtitle": "Uma experiência nativa do Windows para Mail e Calendário.", + "WelcomeWindow_WhatsNewTitle": "Últimas alterações", + "WelcomeWindow_FeaturesTitle": "Recursos", + "WelcomeWindow_WhatsNewTab": "Novidades", + "WelcomeWindow_FeaturesTab": "Recursos", + "WelcomeWindow_GetStartedButton": "Comece adicionando uma conta", + "WelcomeWindow_GetStartedDescription": "Adicione sua conta do Outlook, Gmail ou IMAP para começar com o Wino Mail.", + "WelcomeWindow_ImportFromWinoAccount": "Importar da sua Conta Wino", + "WelcomeWindow_ImportInProgress": "Importando suas preferências e contas sincronizadas...", + "WelcomeWindow_ImportNoAccountsFound": "Nenhuma conta sincronizada foi encontrada na sua Conta Wino. Se havia preferências disponíveis, elas foram restauradas. Use Começar para adicionar uma conta manualmente.", + "WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} contas sincronizadas já estão disponíveis neste dispositivo. Use Começar para adicionar outra conta manualmente, se necessário.", + "WelcomeWindow_SetupTitle": "Configurar sua conta", + "WelcomeWindow_SetupSubtitle": "Escolha seu provedor de e-mail para começar", + "WelcomeWindow_AddAccountButton": "Adicionar conta", + "WelcomeWindow_SkipForNow": "Pular por enquanto — vou configurar mais tarde", + "WelcomeWindow_AppDescription": "Uma caixa de entrada rápida e focada — redesenhada para o Windows 11", + "WelcomeWizard_Step1Title": "Bem-vindo", + "SystemTrayMenu_Open": "Abrir", + "WinoAccount_Titlebar_SyncBenefitTitle": "Configurações de sincronização", + "WinoAccount_Titlebar_SyncBenefitDescription": "Mantenha suas preferências do Wino sincronizadas entre dispositivos.", + "WinoAccount_Titlebar_AddonsBenefitTitle": "Desbloquear complementos", + "WinoAccount_Titlebar_AddonsBenefitDescription": "Acesse recursos premium como o Wino AI Pack.", + "WinoAccount_Management_Description": "Gerencie sua Conta Wino, acesso ao AI Pack e preferências sincronizadas e detalhes da conta.", + "WinoAccount_Management_SignedOutTitle": "Entrar no Wino Mail", + "WinoAccount_Management_SignedOutDescription": "Faça login ou crie uma conta para sincronizar seus e-mails, acessar recursos de IA e gerenciar suas configurações entre dispositivos.", + "WinoAccount_Management_ProfileSectionHeader": "Perfil", + "WinoAccount_Management_AddOnsSectionHeader": "Complementos Wino", + "WinoAccount_Management_DataSectionHeader": "Dados", + "WinoAccount_Management_AccountActionsSectionHeader": "Ações da conta", + "WinoAccount_Management_AccountCardTitle": "Conta", + "WinoAccount_Management_AccountCardDescription": "Endereço de e-mail da Conta Wino e o estado atual da conta.", + "WinoAccount_Management_AiPackCardTitle": "AI Pack", + "WinoAccount_Management_AiPackCardDescription": "Veja se o Wino AI Pack está ativo e quanto uso resta.", + "WinoAccount_Management_AiPackActive": "AI Pack está ativo.", + "WinoAccount_Management_AiPackInactive": "AI Pack não está ativo.", + "WinoAccount_Management_AiPackUsage": "{0} de {1} usos consumidos. {2} restantes.", + "WinoAccount_Management_AiPackBillingPeriod": "Período de cobrança: {0:d} - {1:d}", + "WinoAccount_Management_AiPackUnknownUsage": "Os detalhes de uso ainda não estão disponíveis.", + "WinoAccount_Management_AiPackBuyDescription": "Compre o Wino AI Pack para traduzir, reescrever ou resumir e-mails com IA.", + "WinoAccount_Management_AiPackPromoTitle": "Desbloquear AI Pack", + "WinoAccount_Management_AiPackPromoDescription": "Acelere seu fluxo de e-mails com ferramentas alimentadas por IA. Traduza mensagens para 50+ idiomas, reescreva para clareza e tom, e obtenha resumos instantâneos de longas conversas.", + "WinoAccount_Management_AiPackPromoPrice": "$4,99 / mês", + "WinoAccount_Management_AiPackPromoRequests": "1.000 créditos", + "WinoAccount_Management_AiPackGetButton": "Obter AI Pack", + "WinoAddOn_AI_PACK_Name": "Wino AI Pack", + "WinoAddOn_AI_PACK_Description": "Ferramentas alimentadas por IA para traduzir, reescrever e resumir ações no Wino Mail.", + "WinoAddOn_AI_PACK_Keywords": "IA, traduzir, reescrever, resumir, produtividade", + "WinoAddOn_UNLIMITED_ACCOUNTS_Name": "Contas ilimitadas", + "WinoAddOn_UNLIMITED_ACCOUNTS_Description": "Remover o limite de contas e adicionar quantas contas de e-mail você precisar.", + "WinoAddOn_UNLIMITED_ACCOUNTS_Keywords": "contas, ilimitadas, premium, complemento", + "WinoAccount_Management_PurchaseRequiresSignIn": "Faça login com sua Conta Wino para concluir esta compra.", + "WinoAccount_Management_PurchaseStartFailed": "O Wino não pôde concluir esta compra na Microsoft Store.", + "WinoAccount_Management_StoreSyncFailed": "Sua compra foi concluída, mas o Wino não conseguiu atualizar seus benefícios de conta ainda. Tente novamente em instantes.", + "WinoAccount_Management_AiPackSubscriptionActive": "Sua assinatura está ativa", + "WinoAccount_Management_AiPackRenews": "Renova em {0:d}", + "WinoAccount_Management_AiPackRequestsUsed": "Créditos usados neste mês", + "WinoAccount_Management_AiPackResets": "Reinicializações {0:d}", + "WinoAccount_Management_AiPackUsageLoadFailed": "Houve problemas ao carregar o saldo de uso de IA.", + "WinoAccount_Management_AiPackFeatureTranslate": "Traduzir", + "WinoAccount_Management_AiPackFeatureRewrite": "Reescrever", + "WinoAccount_Management_AiPackFeatureSummarize": "Resumir", + "WinoAccount_Management_AddOnLoadFailed": "Tivemos problemas ao carregar este complemento.", + "WinoAccount_Management_SyncPreferencesTitle": "Sincronizar Preferências e Contas", + "WinoAccount_Management_SyncPreferencesDescription": "Importe ou exporte suas preferências Wino e detalhes da caixa de correio entre dispositivos. Senhas, tokens e outras informações sensíveis nunca são sincronizados.", + "WinoAccount_Management_SignOutTitle": "Sair", + "WinoAccount_Management_SignOutDescription": "Sair da sua conta neste dispositivo", + "WinoAccount_Management_StatusLabel": "Status: {0}", + "WinoAccount_Management_NoRemoteSettings": "Ainda não há dados sincronizados armazenados para esta conta.", + "WinoAccount_Management_ExportSucceeded": "Seus dados Wino selecionados foram exportados com sucesso.", + "WinoAccount_Management_ExportPreferencesSucceeded": "Suas preferências foram exportadas para sua Conta Wino.", + "WinoAccount_Management_ExportAccountsSucceeded": "Foram exportados {0} detalhes de conta para a sua Conta Wino.", + "WinoAccount_Management_ImportSucceeded": "Dados sincronizados importados da sua Conta Wino.", + "WinoAccount_Management_ImportPreferencesSucceeded": "Foram aplicadas {0} preferências sincronizadas.", + "WinoAccount_Management_ImportAccountsSucceeded": "Foram importadas {0} contas.", + "WinoAccount_Management_ImportDuplicateAccountsSkipped": "Foram puladas {0} contas que já existem neste dispositivo.", + "WinoAccount_Management_ImportPartial": "Foram aplicadas {0} preferências sincronizadas. {1} preferências não puderam ser restauradas.", + "WinoAccount_Management_ImportReloginReminder": "Senhas, tokens e outras informações sensíveis não foram importadas. Faça login novamente para cada conta neste dispositivo antes de usá-las.", + "WinoAccount_Management_SerializeFailed": "O Wino não pôde serializar suas preferências atuais.", + "WinoAccount_Management_EmptyExport": "Não há valores de preferência para exportar.", + "WinoAccount_Management_ImportEmpty": "A carga de dados sincronizados não contém nada novo para restaurar.", + "WinoAccount_Management_ExportDialog_Title": "Exportar para a sua Conta Wino", + "WinoAccount_Management_ExportDialog_Description": "Escolha o que deseja sincronizar com a sua Conta Wino.", + "WinoAccount_Management_ExportDialog_IncludePreferences": "Preferências", + "WinoAccount_Management_ExportDialog_IncludeAccounts": "Contas", + "WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Senhas, tokens e outras informações sensíveis não são sincronizados.", + "WinoAccount_Management_ExportDialog_AccountsRelogin": "Contas importadas em outro PC ainda precisarão que você faça login novamente antes que possam ser usadas.", + "WinoAccount_Management_ExportDialog_InProgress": "Exportando seus dados Wino selecionados...", + "WinoAccount_Management_LoadFailed": "Não foi possível carregar as informações mais recentes da Conta Wino.", + "WinoAccount_Management_ActionFailed": "A solicitação da Conta Wino não pôde ser concluída.", + "WinoAccount_SettingsSection_Title": "Conta Wino", + "WinoAccount_SettingsSection_Description": "Crie ou faça login em uma Conta Wino usando seu serviço de autenticação local.", + "WinoAccount_RegisterButton_Title": "Cadastrar conta", + "WinoAccount_RegisterButton_Description": "Crie uma Conta Wino com e-mail e senha.", + "WinoAccount_RegisterButton_Action": "Abrir cadastro", + "WinoAccount_LoginButton_Title": "Entrar", + "WinoAccount_LoginButton_Description": "Faça login em uma Conta Wino existente com e-mail e senha.", + "WinoAccount_LoginButton_Action": "Abrir login", + "WinoAccount_SignOutButton_Title": "Sair", + "WinoAccount_SignOutButton_Description": "Remover a sessão da Conta Wino armazenada localmente.", + "WinoAccount_SignOutButton_Action": "Sair", + "WinoAccount_RegisterDialog_Title": "Criar Conta Wino", + "WinoAccount_RegisterDialog_Description": "Crie uma Conta Wino para manter sua experiência Wino sincronizada e desbloquear complementos baseados em conta.", + "WinoAccount_RegisterDialog_HeroTitle": "Crie sua Conta Wino", + "WinoAccount_RegisterDialog_BenefitsTitle": "Por que criar uma?", + "WinoAccount_RegisterDialog_BenefitSyncTitle": "Importar e exportar configurações entre dispositivos", + "WinoAccount_RegisterDialog_BenefitSyncDescription": "Transfira suas preferências do Wino entre dispositivos sem precisar reconstruir sua configuração do zero.", + "WinoAccount_RegisterDialog_BenefitAiTitle": "Acesse extensões exclusivas, como o Wino AI Pack (pago)", + "WinoAccount_RegisterDialog_BenefitAiDescription": "Use uma Conta para desbloquear recursos premium do Wino conforme ficarem disponíveis.", + "WinoAccount_RegisterDialog_DifferenceTitle": "A Conta Wino é separada de suas contas de e-mail", + "WinoAccount_RegisterDialog_DifferenceDescription": "Suas contas de e-mail, como Outlook, Gmail, IMAP ou outras, permanecem exatamente como estão. Uma Conta Wino gerencia apenas recursos específicos do Wino e extensões baseadas em conta.", + "WinoAccount_RegisterDialog_PrimaryButton": "Cadastrar", + "WinoAccount_RegisterDialog_PrivacyTitle": "Privacidade e processamento de API", + "WinoAccount_RegisterDialog_PrivacyDescription": "Complementos opcionais, como o Wino AI Pack, podem enviar conteúdo HTML de e-mails selecionados para o serviço de API do Wino apenas quando você usar esses recursos.", + "WinoAccount_RegisterDialog_PrivacyLinkText": "Leia a política de privacidade", + "WinoAccount_RegisterDialog_PrivacyCheckbox": "Concordo com a política de privacidade.", + "WinoAccount_LoginDialog_Title": "Entrar na Conta Wino", + "WinoAccount_LoginDialog_Description": "Faça login na sua Conta Wino para sincronizar sua configuração do Wino e acessar recursos baseados em conta.", + "WinoAccount_LoginDialog_HeroTitle": "Bem-vindo de volta", + "WinoAccount_LoginDialog_BenefitsTitle": "O que o login oferece", + "WinoAccount_LoginDialog_BenefitsDescription": "Use sua Conta Wino para continuar sincronizando as configurações entre dispositivos e acessar extensões pagas, como o Wino AI Pack.", + "WinoAccount_LoginDialog_DifferenceTitle": "Este não é o login da sua caixa de e-mail", + "WinoAccount_LoginDialog_DifferenceDescription": "Entrar aqui não adiciona nem substitui suas contas do Outlook, Gmail ou IMAP no Wino. Ele apenas faz você entrar em serviços específicos do Wino.", + "WinoAccount_LoginDialog_ForgotPasswordLink": "Esqueceu a senha?", + "WinoAccount_EmailLabel": "E-mail", + "WinoAccount_EmailPlaceholder": "name@example.com", + "WinoAccount_PasswordLabel": "Senha", + "WinoAccount_ConfirmPasswordLabel": "Confirmar senha", + "WinoAccount_ForgotPasswordDialog_Title": "Redefinir sua senha", + "WinoAccount_ForgotPasswordDialog_PrimaryButton": "Enviar e-mail de redefinição", + "WinoAccount_ForgotPasswordDialog_BackToSignIn": "Voltar ao login", + "WinoAccount_ForgotPasswordDialog_Description": "Digite o endereço de e-mail da sua Conta Wino e enviaremos um link de redefinição de senha se o endereço estiver registrado.", + "WinoAccount_Validation_EmailRequired": "O e-mail é obrigatório.", + "WinoAccount_Validation_PasswordRequired": "A senha é obrigatória.", + "WinoAccount_Validation_PasswordMismatch": "As senhas não correspondem.", + "WinoAccount_Validation_PrivacyConsentRequired": "Você precisa aceitar a política de privacidade antes de criar uma Conta Wino.", + "WinoAccount_Error_InvalidCredentials": "O endereço de e-mail ou a senha estão incorretos.", + "WinoAccount_Error_AccountLocked": "Esta conta está temporariamente bloqueada.", + "WinoAccount_Error_AccountBanned": "Esta conta foi banida.", + "WinoAccount_Error_AccountSuspended": "Esta conta foi suspensa.", + "WinoAccount_Error_EmailNotConfirmed": "Por favor, confirme seu endereço de e-mail antes de entrar.", + "WinoAccount_Error_EmailConfirmationRequired": "Por favor, confirme seu endereço de e-mail antes de entrar.", + "WinoAccount_Error_EmailConfirmationResendNotAvailable": "Um novo e-mail de confirmação ainda não está disponível.", + "WinoAccount_Error_EmailConfirmationResendInvalid": "Esta solicitação de confirmação não é mais válida. Por favor, tente entrar novamente.", + "WinoAccount_Error_EmailNotRegistered": "Este endereço de e-mail não está registrado.", + "WinoAccount_Error_RefreshTokenInvalid": "Sua sessão não é mais válida. Faça login novamente.", + "WinoAccount_Error_EmailAlreadyRegistered": "Este endereço de e-mail já está cadastrado.", + "WinoAccount_Error_ExternalLoginEmailRequired": "Um endereço de e-mail é necessário para concluir o login externo.", + "WinoAccount_Error_ExternalLoginInvalid": "A solicitação de login externo é inválida.", + "WinoAccount_Error_ExternalAuthStateInvalid": "O estado de autenticação externa é inválido ou expirado.", + "WinoAccount_Error_ExternalAuthCodeInvalid": "O código de autenticação externo é inválido ou expirado.", + "WinoAccount_Error_AiPackRequired": "Uma assinatura ativa do Wino AI Pack é necessária para esta ação.", + "WinoAccount_Error_AiQuotaExceeded": "Seu limite de uso do Wino AI Pack foi atingido no período de cobrança atual.", + "WinoAccount_Error_AiHtmlEmpty": "Não há conteúdo de e-mail para processar.", + "WinoAccount_Error_AiHtmlTooLarge": "Este e-mail é grande demais para ser processado pelo Wino AI.", + "WinoAccount_Error_AiUnsupportedLanguage": "Esse idioma não é suportado. Tente um código de cultura válido, como en-US ou tr-TR.", + "WinoAccount_Error_Forbidden": "Você não tem permissão para realizar esta ação.", + "WinoAccount_Error_ValidationFailed": "A requisição é inválida. Por favor, revise os valores inseridos.", + "WinoAccount_RegisterSuccessMessage": "O registro da Conta Wino foi concluído para {0}.", + "WinoAccount_LoginSuccessMessage": "Conectado à Conta Wino como {0}.", + "WinoAccount_EmailConfirmationSentDialog_Title": "Confirme seu endereço de e-mail", + "WinoAccount_EmailConfirmationSentDialog_Message": "Enviamos uma confirmação por e-mail para {0}. Por favor confirme-a e tente fazer login novamente.", + "WinoAccount_EmailConfirmationPendingDialog_Title": "Confirmação de e-mail necessária", + "WinoAccount_EmailConfirmationPendingDialog_Message": "Ainda estamos aguardando você confirmar {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ResendButton": "Reenviar e-mail de confirmação", + "WinoAccount_EmailConfirmationPendingDialog_Countdown": "Você pode reenviar o e-mail de confirmação em {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ReadyToResend": "Você pode reenviar o e-mail de confirmação agora.", + "WinoAccount_EmailConfirmationResentDialog_Title": "E-mail de confirmação reenviado", + "WinoAccount_EmailConfirmationResentDialog_Message": "Enviamos outro e-mail de confirmação para {0}. Por favor confirme-o e tente fazer login novamente.", + "WinoAccount_ForgotPasswordDialog_SuccessTitle": "E-mail de redefinição de senha enviado", + "WinoAccount_ForgotPasswordDialog_SuccessMessage": "Enviamos um e-mail de redefinição de senha para {0}. Abra essa mensagem para escolher uma nova senha.", + "WinoAccount_ChangePassword_Title": "Alterar senha", + "WinoAccount_ChangePassword_Description": "Envie um e-mail de redefinição de senha para esta Conta Wino.", + "WinoAccount_ChangePassword_Action": "Enviar e-mail de redefinição", + "WinoAccount_ChangePassword_ConfirmationMessage": "Você quer que o Wino envie um e-mail de redefinição de senha para {0}?", + "WinoAccount_SignOut_SuccessMessage": "Desconectado da Conta Wino {0}.", + "WinoAccount_SignOut_NoAccountMessage": "Não há nenhuma Conta Wino ativa para sair.", + "WinoAccount_Titlebar_SignedOutTitle": "Conta Wino", + "WinoAccount_Titlebar_SignedOutDescription": "Faça login ou crie uma Conta Wino para gerenciar sua sessão do Wino.", + "WinoAccount_Titlebar_SignedInStatus": "Status: {0}", + "WelcomeWizard_Step2Title": "Adicionar Conta", + "WelcomeWizard_Step3Title": "Concluir Configuração", + "ProviderSelection_Title": "Escolha seu provedor de e-mail", + "ProviderSelection_Subtitle": "Selecione um provedor abaixo para adicionar sua conta de e-mail ao Wino Mail.", + "ProviderSelection_AccountNameHeader": "Nome da Conta", + "ProviderSelection_AccountNamePlaceholder": "ex.: Pessoal, Trabalho", + "ProviderSelection_DisplayNameHeader": "Nome para exibição", + "ProviderSelection_DisplayNamePlaceholder": "ex.: João da Silva", + "ProviderSelection_EmailHeader": "Endereço de E-mail", + "ProviderSelection_EmailPlaceholder": "ex.: joaodoe@exemplo.com", + "ProviderSelection_AppPasswordHeader": "Senha de aplicativo", + "ProviderSelection_AppPasswordHelp": "Como obtenho uma senha de aplicativo?", + "ProviderSelection_CalendarModeHeader": "Integração de Calendário", + "ProviderSelection_CalendarMode_DisabledTitle": "Desativado", + "ProviderSelection_CalendarMode_DisabledDescription": "Sem integração de calendário", + "ProviderSelection_CalendarMode_CalDavTitle": "Sincronização CalDAV", + "ProviderSelection_CalendarMode_CalDavDescription_Apple": "Seus eventos de calendário são sincronizados com os servidores da Apple entre seus dispositivos.", + "ProviderSelection_CalendarMode_CalDavDescription_Yahoo": "Seus eventos de calendário são sincronizados com os servidores Yahoo entre seus dispositivos.", + "ProviderSelection_CalendarMode_LocalTitle": "Calendário local", + "ProviderSelection_CalendarMode_LocalDescription": "Seus eventos ficam armazenados apenas no seu computador. Sem conectividade com o servidor.", + "ProviderSelection_ClearColor": "Limpar cor", + "ProviderSelection_ContinueButton": "Continuar", + "ProviderSelection_SpecialImap_Subtitle": "Informe as credenciais da sua conta para conectar.", + "AccountSetup_Title": "Configurando sua conta", + "AccountSetup_Step_Authenticating": "Autenticando com {0}", + "AccountSetup_Step_TestingMailAuth": "Testando autenticação de e-mail", + "AccountSetup_Step_SyncingFolders": "Sincronizando metadados de pastas", + "AccountSetup_Step_FetchingProfile": "Buscando informações de perfil", + "AccountSetup_Step_DiscoveringCalDav": "Descobrindo configurações do CalDAV", + "AccountSetup_Step_TestingCalendarAuth": "Testando autenticação de calendário", + "AccountSetup_Step_SavingAccount": "Salvando informações da conta", + "AccountSetup_Step_FetchingCalendarMetadata": "Buscando metadados do calendário", + "AccountSetup_Step_SyncingAliases": "Sincronizando aliases", + "AccountSetup_Step_Finalizing": "Finalizando configuração", + "AccountSetup_FailureMessage": "Falha na configuração. Volte para corrigir suas configurações ou tente novamente mais tarde.", + "AccountSetup_SuccessMessage": "Sua conta foi configurada com sucesso!", + "AccountSetup_GoBackButton": "Voltar", + "AccountSetup_TryAgainButton": "Tentar novamente", + "ImapCalDavSettings_AutoDiscoveryFailed": "A descoberta automática falhou. Por favor, insira as configurações manualmente na guia Avançado." } - - diff --git a/Wino.Core.Domain/Translations/ro_RO/resources.json b/Wino.Core.Domain/Translations/ro_RO/resources.json index e9f62ce5..aaafc202 100644 --- a/Wino.Core.Domain/Translations/ro_RO/resources.json +++ b/Wino.Core.Domain/Translations/ro_RO/resources.json @@ -8,6 +8,7 @@ "AccountCacheReset_Message": "Acest cont necesită resincronizare completă pentru a continua să funcționeze. Vă rugăm să așteptați în timp ce Wino vă resincronizează mesajele...", "AccountContactNameYou": "Dvs.", "AccountCreationDialog_Completed": "toate terminate", + "AccountCreationDialog_FetchingCalendarMetadata": "Se preiau detalii despre calendar.", "AccountCreationDialog_FetchingEvents": "Preluare evenimente calendar.", "AccountCreationDialog_FetchingProfileInformation": "Preluare detalii de profil.", "AccountCreationDialog_GoogleAuthHelpClipboardText_Row0": "În cazul în care browser-ul dvs. nu a fost lansat automat pentru a finaliza autentificarea:", @@ -17,6 +18,7 @@ "AccountCreationDialog_Initializing": "se inițializează", "AccountCreationDialog_PreparingFolders": "Obținem informații despre folder în acest moment.", "AccountCreationDialog_SigninIn": "Informațiile contului sunt salvate.", + "Purchased": "Achiziționat", "AccountEditDialog_Message": "Nume Cont", "AccountEditDialog_Title": "Editare Cont", "AccountPickerDialog_Title": "Alegeți un cont", @@ -26,6 +28,10 @@ "AccountDetailsPage_Description": "Schimbați numele contului în Wino și setați numele dorit al expeditorului.", "AccountDetailsPage_ColorPicker_Title": "Culoarea contului", "AccountDetailsPage_ColorPicker_Description": "Atribuiți o nouă culoare a contului pentru a-i colora simbolul în listă.", + "AccountDetailsPage_TabGeneral": "General", + "AccountDetailsPage_TabMail": "Email", + "AccountDetailsPage_TabCalendar": "Calendar", + "AccountDetailsPage_CalendarListDescription": "Selectați un calendar pentru a-i configura setările.", "AddHyperlink": "Adăugare", "AppCloseBackgroundSynchronizationWarningTitle": "Sincronizare în Fundal", "AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Aplicația nu a fost setată să pornească la pornirea Windows.", @@ -47,8 +53,10 @@ "BasicIMAPSetupDialog_Title": "Cont IMAP", "Busy": "Ocupat", "Buttons_AddAccount": "Adăugare Cont", + "Buttons_FixAccount": "Remediază contul", "Buttons_AddNewAlias": "Adăugare Alias Nou", "Buttons_Allow": "Permite", + "Buttons_Apply": "Aplică", "Buttons_ApplyTheme": "Aplicați Tema", "Buttons_Browse": "Răsfoire", "Buttons_Cancel": "Anulare", @@ -62,6 +70,7 @@ "Buttons_Edit": "Editare", "Buttons_EnableImageRendering": "Activare", "Buttons_Multiselect": "Selectare Multiplă", + "Buttons_Manage": "Gestionează", "Buttons_No": "Nu", "Buttons_Open": "Deschidere", "Buttons_Purchase": "Achiziționare", @@ -70,15 +79,134 @@ "Buttons_Save": "Salvare", "Buttons_SaveConfiguration": "Salvare Configurație", "Buttons_Send": "Trimitere", + "Buttons_SendToServer": "Trimite către server", "Buttons_Share": "Partajare", "Buttons_SignIn": "Conectare", "Buttons_Sync": "Sincronizare", "Buttons_SyncAliases": "Sincronizați Aliasurile", "Buttons_TryAgain": "Încercați din Nou", "Buttons_Yes": "Da", + "Sync_SynchronizingFolder": "Se sincronizează {0} {1}%", + "Sync_DownloadedMessages": "Au fost descărcate {0} mesaje din {1}", + "SyncAction_Archiving": "Arhivare {0} mesaj(e)", + "SyncAction_ClearingFlag": "Eliminarea steagului {0} mesaj(e)", + "SyncAction_CreatingDraft": "Creare schiță", + "SyncAction_CreatingEvent": "Creare eveniment", + "SyncAction_Deleting": "Ștergerea {0} mesaj(e)", + "SyncAction_EmptyingFolder": "Golește folderul", + "SyncAction_MarkingAsRead": "Marcare {0} mesaj(e) ca citite", + "SyncAction_MarkingAsUnread": "Marcare {0} mesaj(e) ca necitite", + "SyncAction_MarkingFolderAsRead": "Marcare folderului ca citit", + "SyncAction_Moving": "Mutarea {0} mesaj(e)", + "SyncAction_MovingToFocused": "Mutarea {0} mesaj(e) către Focused", + "SyncAction_RenamingFolder": "Redenumire folder", + "SyncAction_SendingMail": "Trimitere e-mail", + "SyncAction_SettingFlag": "Atribuirea steagului {0} mesaj(e)", + "SyncAction_SynchronizingAccount": "Se sincronizează {0}", + "SyncAction_SynchronizingAccounts": "Se sincronizează {0} cont(e)", + "SyncAction_SynchronizingCalendarData": "Se sincronizează datele calendarului", + "SyncAction_SynchronizingCalendarEvents": "Se sincronizează evenimentele din calendar", + "SyncAction_SynchronizingCalendarMetadata": "Se sincronizează metadatele calendarului", + "SyncAction_Unarchiving": "Dezarchivarea {0} mesaj(e)", "CalendarAllDayEventSummary": "evenimente pe tot parcursul zilei", "CalendarDisplayOptions_Color": "Culoare", "CalendarDisplayOptions_Expand": "Extindere", + "CalendarEventResponse_Accept": "Acceptă", + "CalendarEventResponse_AcceptedResponse": "Acceptat", + "CalendarEventResponse_Decline": "Refuză", + "CalendarEventResponse_DeclinedResponse": "Refuzat", + "CalendarEventResponse_NotResponded": "Nu a răspuns", + "CalendarEventResponse_Tentative": "Tentativ", + "CalendarEventResponse_TentativeResponse": "Tentativ", + "CalendarEventRsvpPanel_Accept": "Acceptă", + "CalendarEventRsvpPanel_AddMessage": "Adaugă un mesaj la răspunsul tău... (opțional)", + "CalendarEventRsvpPanel_Decline": "Refuză", + "CalendarEventRsvpPanel_Message": "Mesaj", + "CalendarEventRsvpPanel_SendReplyMessage": "Trimite un mesaj de răspuns", + "CalendarEventRsvpPanel_Tentative": "Tentativ", + "CalendarEventRsvpPanel_Title": "Opțiuni de răspuns", + "CalendarAttendeeStatus_Accepted": "Acceptat", + "CalendarAttendeeStatus_Declined": "Refuzat", + "CalendarAttendeeStatus_NeedsAction": "Are nevoie de acțiune", + "CalendarAttendeeStatus_Tentative": "Tentativ", + "CalendarEventDetails_Attachments": "Atașamente", + "CalendarEventCompose_AddAttachment": "Adaugă atașament", + "CalendarEventCompose_AllDay": "Toată ziua", + "CalendarEventCompose_AttachmentsNotSupportedForCalDav": "Atașamentele nu sunt suportate pentru calendarele CalDAV.", + "CalendarEventCompose_EndDate": "Data de încheiere", + "CalendarEventCompose_EndTime": "Ora de încheiere", + "CalendarEventCompose_Every": "fiecare", + "CalendarEventCompose_ForWeekdays": "pentru", + "CalendarEventCompose_FrequencyDay": "zi", + "CalendarEventCompose_FrequencyDayPlural": "zile", + "CalendarEventCompose_FrequencyMonth": "lună", + "CalendarEventCompose_FrequencyMonthPlural": "luni", + "CalendarEventCompose_FrequencyWeek": "săptămână", + "CalendarEventCompose_FrequencyWeekPlural": "săptămâni", + "CalendarEventCompose_FrequencyYear": "an", + "CalendarEventCompose_FrequencyYearPlural": "ani", + "CalendarEventCompose_Location": "Locație", + "CalendarEventCompose_LocationPlaceholder": "Adaugă o locație", + "CalendarEventCompose_NewEventButton": "Eveniment nou", + "CalendarEventCompose_DefaultCalendarHint": "Puteți alege un calendar implicit pentru evenimentele noi în setările Calendarului.", + "CalendarEventCompose_DefaultCalendarSettingsLink": "Deschide setările Calendarului", + "CalendarEventCompose_NoCalendarsMessage": "Nu există calendare disponibile pentru crearea unui eveniment.", + "CalendarEventCompose_NoCalendarsTitle": "Niciun calendar disponibil", + "CalendarEventCompose_NoEndDate": "Fără dată de încheiere", + "CalendarEventCompose_Notes": "Notițe", + "CalendarEventCompose_PickCalendarTitle": "Alege un calendar", + "CalendarEventCompose_Recurring": "Recurent", + "CalendarEventCompose_RecurringSummary": "Are loc la fiecare {0} {1}{2} {3} în vigoare {4}{5}", + "CalendarEventCompose_RecurringSummarySmart": "Are loc {0}{1} {2} în vigoare {3}{4}", + "CalendarEventCompose_RepeatEvery": "Repetă la fiecare", + "CalendarEventCompose_SelectCalendar": "Selectează calendarul", + "CalendarEventCompose_SingleOccurrenceSummary": "Are loc pe {0} {1}", + "CalendarEventCompose_StartDate": "Data de începere", + "CalendarEventCompose_StartTime": "Ora de început", + "CalendarEventCompose_TimeRangeSummary": "de la {0} până la {1}", + "CalendarEventCompose_Title": "Titlul evenimentului", + "CalendarEventCompose_TitlePlaceholder": "Adaugă un titlu", + "CalendarEventCompose_Until": "până", + "CalendarEventCompose_UntilSummary": " până {0}", + "CalendarEventCompose_ValidationInvalidAllDayRange": "Data de încheiere pentru intervalul de o zi întreagă trebuie să fie după data de început.", + "CalendarEventCompose_ValidationInvalidAttendee": "Unul sau mai mulți participanți au o adresă de email invalidă.", + "CalendarEventCompose_ValidationInvalidRecurrenceEnd": "Data de încheiere a repetării trebuie să fie egală sau după data de început a evenimentului.", + "CalendarEventCompose_ValidationInvalidTimeRange": "Ora de încheiere trebuie să fie mai târziu decât ora de început.", + "CalendarEventCompose_ValidationMissingAttachment": "Unul sau mai multe atașamente nu mai sunt disponibile: {0}", + "CalendarEventCompose_ValidationMissingCalendar": "Selectați un calendar înainte de a crea evenimentul.", + "CalendarEventCompose_ValidationMissingTitle": "Introduceți un titlu pentru eveniment înainte de a crea evenimentul.", + "CalendarEventCompose_ValidationTitle": "Validarea evenimentului a eșuat.", + "CalendarEventCompose_WeekdaySummary": " pe {0}", + "CalendarEventCompose_Weekday_Friday": "V", + "CalendarEventCompose_Weekday_Monday": "L", + "CalendarEventCompose_Weekday_Saturday": "S", + "CalendarEventCompose_Weekday_Sunday": "D", + "CalendarEventCompose_Weekday_Thursday": "J", + "CalendarEventCompose_Weekday_Tuesday": "M", + "CalendarEventCompose_Weekday_Wednesday": "M", + "CalendarEventDetails_Details": "Detalii", + "CalendarEventDetails_EditSeries": "Editează seria", + "CalendarEventDetails_Editing": "Editare", + "CalendarEventDetails_InviteSomeone": "Invită pe cineva", + "CalendarEventDetails_JoinOnline": "Conectează-te online", + "CalendarEventDetails_Organizer": "Organizator", + "CalendarEventDetails_People": "Persoane", + "CalendarEventDetails_ReadOnlyEvent": "Eveniment doar pentru citire", + "CalendarEventDetails_Reminder": "Notificare", + "CalendarReminder_StartedHoursAgo": "Au trecut {0} ore", + "CalendarReminder_StartedMinutesAgo": "Au trecut {0} minute", + "CalendarReminder_StartedNow": "A început chiar acum", + "CalendarReminder_StartingNow": "Începe acum", + "CalendarReminder_StartsInHours": "Începe în {0} ore", + "CalendarReminder_StartsInMinutes": "Începe în {0} minute", + "CalendarReminder_SnoozeAction": "Amână", + "CalendarReminder_SnoozeMinutesOption": "{0} minute", + "CalendarEventDetails_ShowAs": "Afișează ca", + "CalendarShowAs_Free": "Liber", + "CalendarShowAs_Tentative": "Tentativ", + "CalendarShowAs_Busy": "Ocupat", + "CalendarShowAs_OutOfOffice": "În afara biroului", + "CalendarShowAs_WorkingElsewhere": "Lucrez în altă locație", "CalendarItem_DetailsPopup_JoinOnline": "Alăturați-vă online", "CalendarItem_DetailsPopup_ViewEventButton": "Vizualizare eveniment", "CalendarItem_DetailsPopup_ViewSeriesButton": "Vizualizare serie", @@ -88,6 +216,9 @@ "ClipboardTextCopied_Message": "{0} copiat în clipboard.", "ClipboardTextCopied_Title": "Copiat", "ClipboardTextCopyFailed_Message": "Nu s-a putut copia {0} în clipboard.", + "ContactInfoBar_ErrorTitle": "Nu s-a putut încărca informațiile de contact", + "ContactInfoBar_SuccessTitle": "Informațiile de contact au fost încărcate", + "ContactInfoBar_WarningTitle": "Informațiile de contact ar putea fi incomplete", "ComingSoon": "În curând...", "ComposerAttachmentsDragDropAttach_Message": "Atașare", "ComposerAttachmentsDropZone_Message": "Plasați-vă fișierele aici", @@ -129,6 +260,10 @@ "DialogMessage_CreateLinkedAccountTitle": "Nume Link Cont", "DialogMessage_DeleteAccountConfirmationMessage": "Ștergeți {0}?", "DialogMessage_DeleteAccountConfirmationTitle": "Toate datele asociate acestui cont vor fi șterse definitiv de pe disc.", + "DialogMessage_DeleteEmailTemplateConfirmationMessage": "Ștergeți șablonul \"{0}\"?", + "DialogMessage_DeleteEmailTemplateConfirmationTitle": "Ștergeți șablonul de e-mail", + "DialogMessage_DeleteRecurringSeriesMessage": "Aceasta va șterge toate evenimentele din seria respectivă. Doriți să continuați?", + "DialogMessage_DeleteRecurringSeriesTitle": "Șterge seria repetitivă", "DialogMessage_DiscardDraftConfirmationMessage": "Această schiță va fi eliminat. Doriți să continuați?", "DialogMessage_DiscardDraftConfirmationTitle": "Eliminare Schiță", "DialogMessage_EmptySubjectConfirmation": "Subiect Lipsă", @@ -172,11 +307,18 @@ "ElementTheme_Light": "Mod luminos", "Emoji": "Emoji", "Error_FailedToSetupSystemFolders_Title": "Configurarea folderelor de sistem a eșuat", + "Exception_AccountNeedsAttention_Title": "Contul are nevoie de atenție", + "Exception_AccountNeedsAttention_Message": "'{0}' necesită atenția dumneavoastră pentru a continua lucrul.", + "Exception_WebView2RuntimeMissing_Message": "Wino Mail nu a putut găsi WebView2 Runtime-ul Microsoft Edge. Vă rugăm să instalați sau să reparați runtime-ul pentru a reda conținutul mesajului corect.", + "Exception_WebView2RuntimeMissing_Title": "Este necesar WebView2 Runtime", "Exception_AuthenticationCanceled": "Autentificare anulată", "Exception_CustomThemeExists": "Această temă există deja.", "Exception_CustomThemeMissingName": "Trebuie să furnizați un nume.", "Exception_CustomThemeMissingWallpaper": "Trebuie să furnizați o imagine de fundal personalizată.", "Exception_FailedToSynchronizeAliases": "Sincronizarea aliasurilor a eșuat", + "Exception_FailedToSynchronizeCalendarData": "Nu s-a reușit sincronizarea datelor calendarului", + "Exception_FailedToSynchronizeCalendarEvents": "Nu s-a reușit sincronizarea evenimentelor din calendar", + "Exception_FailedToSynchronizeCalendarMetadata": "Nu s-a reușit sincronizarea detaliilor calendarului", "Exception_FailedToSynchronizeFolders": "Sincronizarea folderelor a eșuat", "Exception_FailedToSynchronizeProfileInformation": "Sincronizarea informațiilor de profil a eșuat", "Exception_GoogleAuthCallbackNull": "Callback uri este nul la activare.", @@ -229,6 +371,32 @@ "HoverActionOption_MoveJunk": "Mutare în Gunoi", "HoverActionOption_ToggleFlag": "Semnalizare / Desemnalizare", "HoverActionOption_ToggleRead": "Citit / Necitit", + "KeyboardShortcuts_FailedToReset": "Nu s-a putut resetarea scurtăturilor de la tastatură.", + "KeyboardShortcuts_FailedToUpdate": "Nu s-a putut actualizarea scurtăturilor de la tastatură.", + "KeyboardShortcuts_MailoperationAction": "Acțiune", + "KeyboardShortcuts_Action": "Acțiune", + "KeyboardShortcuts_FailedToLoad": "Nu s-a putut încărcarea scurtăturilor de la tastatură.", + "KeyboardShortcuts_EnterKeyForShortcut": "Vă rugăm să introduceți o tastă pentru scurtătură.", + "KeyboardShortcuts_SelectOperationForShortcut": "Vă rugăm să selectați o acțiune pentru scurtătură.", + "KeyboardShortcuts_EnterKey": "Vă rugăm să introduceți o tastă pentru scurtătură.", + "KeyboardShortcuts_SelectOperation": "Vă rugăm să selectați o acțiune pentru scurtătură.", + "KeyboardShortcuts_ShortcutInUse": "Această scurtătură este deja utilizată de o altă scurtătură.", + "KeyboardShortcuts_FailedToSave": "Nu s-a putut salva scurtătura.", + "KeyboardShortcuts_FailedToDelete": "Nu s-a putut șterge scurtătura.", + "KeyboardShortcuts_PageDescription": "Configurează scurtăturile de tastatură pentru operațiile rapide cu e-mail. Apăsați tastele în timp ce câmpul de introducere a tastei este focalizat pentru a captura scurtăturile.", + "KeyboardShortcuts_Add": "Adaugă scurtătură", + "KeyboardShortcuts_EditTitle": "Editează scurtătura de tastatură", + "KeyboardShortcuts_ResetToDefaults": "Resetează la valorile implicite", + "KeyboardShortcuts_PressKeysHere": "Apăsați tastele aici...", + "KeyboardShortcuts_KeyCombination": "Combinație de taste", + "KeyboardShortcuts_FocusArea": "Focalizați câmpul de mai sus și apăsați combinația de taste dorită", + "KeyboardShortcuts_Modifiers": "Taste modificatoare", + "KeyboardShortcuts_Mode": "Modul aplicație", + "KeyboardShortcuts_ModeMail": "E-mail", + "KeyboardShortcuts_ModeCalendar": "Calendar", + "KeyboardShortcuts_ActionToggleReadUnread": "Comută citit/necitit", + "KeyboardShortcuts_ActionToggleFlag": "Comută steag", + "KeyboardShortcuts_ActionToggleArchive": "Arhivează/Dezarhivează", "ImageRenderingDisabled": "Randarea imaginilor este dezactivată pentru acest mesaj.", "ImapAdvancedSetupDialog_AuthenticationMethod": "Metodă de autentificare", "ImapAdvancedSetupDialog_ConnectionSecurity": "Securitatea conexiunii", @@ -262,8 +430,8 @@ "IMAPSetupDialog_AccountType": "Tip cont", "IMAPSetupDialog_ValidationSuccess_Title": "Succes", "IMAPSetupDialog_ValidationSuccess_Message": "Validare reușită", - "IMAPSetupDialog_SaveImapSuccess_Title": "Succes", - "IMAPSetupDialog_SaveImapSuccess_Message": "Setări IMAP salvate cu succes.", + "IMAPSetupDialog_SaveImapSuccess_Title": "Success", + "IMAPSetupDialog_SaveImapSuccess_Message": "IMAP settings saved successfuly.", "IMAPSetupDialog_ValidationFailed_Title": "Validarea serverului IMAP a eșuat.", "IMAPSetupDialog_CertificateAllowanceRequired_Row0": "Acest server solicită un SSL handshake pentru a continua. Vă rugăm să confirmați detaliile certificatului de mai jos.", "IMAPSetupDialog_CertificateAllowanceRequired_Row1": "Permiteți handshake-ului să continue configurarea contului.", @@ -295,12 +463,58 @@ "IMAPSetupDialog_Username": "Nume de utilizator", "IMAPSetupDialog_UsernamePlaceholder": "andreipopescu, andreipopescu@fabikam.com, domeniu/andreipopescu", "IMAPSetupDialog_UseSameConfig": "Utilizați același nume de utilizator și aceeași parolă pentru trimiterea de e-mail", + "ImapCalDavSettingsPage_TitleCreate": "Configurare IMAP și Calendar", + "ImapCalDavSettingsPage_TitleEdit": "Editează setările IMAP și Calendar", + "ImapCalDavSettingsPage_Subtitle": "Configurează IMAP/SMTP și sincronizarea calendarului pentru acest cont.", + "ImapCalDavSettingsPage_BasicSectionTitle": "Setări de bază", + "ImapCalDavSettingsPage_BasicSectionDescription": "Introduceți identitatea și acreditările dvs. Wino poate încerca să detecteze automat setările serverului.", + "ImapCalDavSettingsPage_BasicTab": "De bază", + "ImapCalDavSettingsPage_EnableCalendarSupport": "Activează suportul pentru calendar", + "ImapCalDavSettingsPage_AutoDiscoverButton": "Autodetectează setările de e-mail", + "ImapCalDavSettingsPage_AutoDiscoverySuccessMessage": "Setările de e-mail au fost descoperite și aplicate.", + "ImapCalDavSettingsPage_AdvancedSectionTitle": "Configurare avansată", + "ImapCalDavSettingsPage_AdvancedSectionDescription": "Introduceți manual setările serverului dacă detectarea automată nu este disponibilă sau este incorectă.", + "ImapCalDavSettingsPage_AdvancedTab": "Avansat", + "ImapCalDavSettingsPage_CalendarSectionTitle": "Configurare calendar", + "ImapCalDavSettingsPage_CalendarSectionDescription": "Alegeți cum ar trebui să funcționeze datele calendarului pentru acest cont IMAP.", + "ImapCalDavSettingsPage_CalendarModeHeader": "Mod calendar", + "ImapCalDavSettingsPage_ConnectionSecurityHeader": "Securitatea conexiunii", + "ImapCalDavSettingsPage_AuthenticationMethodHeader": "Metoda de autentificare", + "ImapCalDavSettingsPage_CalendarModeDisabled": "Dezactivat", + "ImapCalDavSettingsPage_CalendarModeCalDav": "Sincronizarea CalDAV", + "ImapCalDavSettingsPage_CalendarModeLocalOnly": "Doar calendar local", + "ImapCalDavSettingsPage_CalendarModeDisabledDescription": "Calendarul este dezactivat pentru acest cont.", + "ImapCalDavSettingsPage_CalendarModeCalDavDescription": "Elementele calendarului sunt sincronizate cu serverul CalDAV.", + "ImapCalDavSettingsPage_CalendarModeLocalOnlyDescription": "Elementele calendarului sunt stocate doar pe acest calculator și nu sunt sincronizate în rețea.", + "ImapCalDavSettingsPage_LocalCalendarLearnMore": "Cum funcționează calendarul local", + "ImapCalDavSettingsPage_LocalCalendarDialogTitle": "Doar calendar local", + "ImapCalDavSettingsPage_LocalCalendarDialogMessage": "Calendarul local păstrează toate evenimentele doar pe calculatorul dvs. Niciunul dintre ele nu este sincronizat cu iCloud, Yahoo sau alt furnizor.", + "ImapCalDavSettingsPage_CalDavServiceUrl": "URL-ul serviciului CalDAV", + "ImapCalDavSettingsPage_CalDavUsername": "Numele de utilizator CalDAV", + "ImapCalDavSettingsPage_CalDavPassword": "Parola CalDAV", + "ImapCalDavSettingsPage_CalDavNotRequiredMessage": "Testul CalDAV este necesar doar atunci când modul calendar este setat la sincronizarea CalDAV.", + "ImapCalDavSettingsPage_CalDavUrlRequired": "URL-ul serviciului CalDAV este necesar.", + "ImapCalDavSettingsPage_CalDavUrlInvalid": "URL-ul serviciului CalDAV trebuie să fie un URL absolut.", + "ImapCalDavSettingsPage_CalDavUsernameRequired": "Numele de utilizator CalDAV este obligatoriu.", + "ImapCalDavSettingsPage_CalDavPasswordRequired": "Parola CalDAV este obligatorie.", + "ImapCalDavSettingsPage_TestImapButton": "Testează conexiunea IMAP", + "ImapCalDavSettingsPage_TestCalDavButton": "Testează conexiunea CalDAV", + "ImapCalDavSettingsPage_ImapTestSuccessMessage": "Testul conexiunii IMAP a reușit.", + "ImapCalDavSettingsPage_CalDavTestSuccessMessage": "Testul conexiunii CalDAV a reușit.", + "ImapCalDavSettingsPage_SaveSuccessMessage": "Setările contului au fost validate și salvate.", + "ImapCalDavSettingsPage_ICloudHint": "Folosiți o parolă de aplicație generată din setările contului dvs. Apple.", + "ImapCalDavSettingsPage_YahooHint": "Folosiți o parolă de aplicație din setările de securitate ale contului Yahoo.", "Info_AccountCreatedMessage": "{0} este creat", "Info_AccountCreatedTitle": "Creare Cont", "Info_AccountCreationFailedTitle": "Crearea contului a eșuat", "Info_AccountDeletedMessage": "{0} a fost șters cu succes.", "Info_AccountDeletedTitle": "Cont Șters", "Info_AccountIssueFixFailedTitle": "Eșuat", + "Info_AccountIssueFixImapMessage": "Deschideți pagina de setări IMAP și calendar pentru a introduce din nou acreditările serverului.", + "Info_AccountAttentionRequiredMessage": "Acest cont necesită atenția dumneavoastră.", + "Info_AccountAttentionRequiredClickableMessage": "Faceți clic pentru a repara acest cont și a-l resincroniza.", + "Info_AccountAttentionRequiredAction": "Remediază", + "Info_AccountAttentionRequiredActionHint": "Faceți clic pe Remediază pentru a rezolva această problemă a contului.", "Info_AccountIssueFixSuccessMessage": "Au fost rezolvate toate problemele contului.", "Info_AccountIssueFixSuccessTitle": "Succes", "Info_AttachmentOpenFailedMessage": "Nu se poate deschide acest atașament.", @@ -370,6 +584,7 @@ "InfoBarMessage_SynchronizationDisabledFolder": "Acest folder este dezactivat pentru sincronizare.", "InfoBarTitle_SynchronizationDisabledFolder": "Folder Dezactivat", "Justify": "Justificare", + "MenuUpdateAvailable": "Actualizare disponibilă", "Left": "Stânga", "Link": "Link", "LinkedAccountsCreatePolicyMessage": "trebuie să aveți cel puțin 2 conturi pentru a crea un link\nlink-ul va fi eliminat la salvare", @@ -403,6 +618,7 @@ "MailOperation_Unarchive": "Dezarhivare", "MailOperation_ViewMessageSource": "Vizualizați sursa mesajului", "MailOperation_Zoom": "Mărește", + "MailsDragging": "Se trag {0} element(e)", "MailsSelected": "{0} element(e) selectate", "MarkFlagUnflag": "Marcare ca semnazizat/nesemnalizat", "MarkReadUnread": "Marcare ca citit/necitit", @@ -434,6 +650,8 @@ "Notifications_MultipleNotificationsTitle": "Mesaj E-Mail Nou", "Notifications_WinoUpdatedMessage": "Verificați versiunea nouă {0}", "Notifications_WinoUpdatedTitle": "Wino Mail a fost actualizat.", + "Notifications_StoreUpdateAvailableTitle": "Actualizare disponibilă", + "Notifications_StoreUpdateAvailableMessage": "O versiune mai nouă de Wino Mail este pregătită pentru instalare din Microsoft Store.", "OnlineSearchFailed_Message": "Nu s-a reușit căutarea\n{0}\n\nSe listează e-mail-urile offline.", "OnlineSearchTry_Line1": "Nu găsiți ceea ce căutați?", "OnlineSearchTry_Line2": "Încercați căutarea online.", @@ -446,7 +664,6 @@ "PaneLengthOption_Small": "Mic", "Photos": "Fotografii", "PreparingFoldersMessage": "Se pregătesc folderele", - "ProtocolLogAvailable_Message": "Jurnalele de protocol sunt disponibile pentru diagnostice.", "ProviderDetail_Gmail_Description": "Cont Google", "ProviderDetail_iCloud_Description": "Cont Apple iCloud", "ProviderDetail_iCloud_Title": "iCloud", @@ -465,9 +682,14 @@ "SearchBarPlaceholder": "Căutare", "SearchingIn": "Se caută în", "SearchPivotName": "Rezultate", + "Settings_KeyboardShortcuts_Title": "Scurtături de tastatură", + "Settings_KeyboardShortcuts_Description": "Gestionează scurtăturile de tastatură pentru acțiuni rapide în e-mailuri.", "SettingConfigureSpecialFolders_Button": "Configurare", "SettingsEditAccountDetails_IMAPConfiguration_Title": "Configurație IMAP/SMTP", "SettingsEditAccountDetails_IMAPConfiguration_Description": "Modificați setările serverului de intrare/ieșire.", + "SettingsEditAccountDetails_ImapCalDavSettings_Title": "Setări IMAP și calendar", + "SettingsEditAccountDetails_ImapCalDavSettings_Description": "Deschideți pagina dedicată de setări IMAP, SMTP și CalDAV pentru acest cont.", + "SettingsEditAccountDetails_ImapCalDavSettings_Action": "Deschideți setările", "SettingsAbout_Description": "Aflați mai multe despre Wino.", "SettingsAbout_Title": "Despre", "SettingsAboutGithub_Description": "Mergeți la tracker-ul de probleme al repozitoriului GitHub.", @@ -490,6 +712,10 @@ "SettingsAppPreferences_SearchMode_Local": "Local", "SettingsAppPreferences_SearchMode_Online": "Online", "SettingsAppPreferences_SearchMode_Title": "Modul de căutare implicit", + "SettingsAppPreferences_ApplicationMode_Title": "Mod implicit al aplicației", + "SettingsAppPreferences_ApplicationMode_Description": "Alegeți modul în care Wino se deschide atunci când nu este specificat un tip de activare.", + "SettingsAppPreferences_ApplicationMode_Mail": "E-mail", + "SettingsAppPreferences_ApplicationMode_Calendar": "Calendar", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description": "Wino Mail va continua să ruleze în fundal. Veți fi anunțat pe măsură ce sosesc e-mailuri noi.", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title": "Rulare în fundal", "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description": "Wino Mail va continua să ruleze în zona de notificări. Disponibil pentru lansare făcând clic pe o pictogramă. Veți fi anunțat pe măsură ce sosesc e-mailuri noi.", @@ -506,12 +732,30 @@ "SettingsAppPreferences_StartupBehavior_FatalError": "A apărut o eroare fatală la schimbarea modului de pornire pentru Wino Mail.", "SettingsAppPreferences_StartupBehavior_Title": "Pornire minimizată la pornirea Windows", "SettingsAppPreferences_Title": "Preferințe Aplicație", + "SettingsAppPreferences_HideWinoAccountButton_Title": "Ascundeți butonul contului Wino din bara de titlu", + "SettingsAppPreferences_HideWinoAccountButton_Description": "Ascundeți butonul de profil din bara de titlu care deschide meniul contului Wino.", + "SettingsAppPreferences_StoreUpdateNotifications_Title": "Notificări despre actualizările din Microsoft Store", + "SettingsAppPreferences_StoreUpdateNotifications_Description": "Afișează notificări și acțiuni în subsol atunci când este disponibilă o actualizare din Microsoft Store.", + "SettingsAppPreferences_AiActions_Title": "Acțiuni AI", + "SettingsAppPreferences_AiActions_Description": "Alegeți limbile AI implicite și locul în care rezumatele ar trebui să fie salvate.", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Title": "Limba de traducere implicită", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Description": "Alegeți limba țintă implicită utilizată de acțiunile de traducere AI.", + "SettingsAppPreferences_AiSummarizeLanguage_Title": "Limba rezumării", + "SettingsAppPreferences_AiSummarizeLanguage_Description": "Alegeți limba de rezumat preferată pentru ieșirea viitoare a rezumatelor AI.", + "SettingsAppPreferences_AiSummarySavePath_Title": "Calea implicită de salvare a rezumatelor", + "SettingsAppPreferences_AiSummarySavePath_Description": "Alegeți directorul implicit pe care Wino ar trebui să-l folosească la salvarea rezumatelor AI.", + "SettingsAppPreferences_AiSummarySavePath_Placeholder": "Folosiți locația implicită de salvare a sistemului", + "SettingsAppPreferences_AiSummarySavePath_InvalidHint": "Acest folder nu există. Pentru rezumate va fi folosită locația implicită de salvare.", "SettingsAutoSelectNextItem_Description": "Selectați următorul element după ce ștergeți sau mutați un e-mail.", "SettingsAutoSelectNextItem_Title": "Selectați automat următorul element", "SettingsAvailableThemes_Description": "Selectați o temă din colecția Wino pentru gustul dvs. sau aplicați propriile teme.", "SettingsAvailableThemes_Title": "Teme Disponibile", "SettingsCalendarSettings_Description": "Modificați prima zi a săptămânii, înălțimea celulei de oră și multe altele...", "SettingsCalendarSettings_Title": "Setări Calendar", + "CalendarSettings_DefaultSnoozeDuration_Header": "Durata implicită de amânare", + "CalendarSettings_DefaultSnoozeDuration_Description": "Stabiliți o durată implicită de amânare pentru notificările de reamintire din calendar.", + "CalendarSettings_TimedDayHeaderFormat_Header": "Formatul antetului zilei în vizualizarea temporizată.", + "CalendarSettings_TimedDayHeaderFormat_Description": "Alegeți cum sunt afișate etichetele zilei în vizualizările pentru zi, săptămână și săptămâna de lucru. Folosiți tokenuri de format ale datei precum ddd, dd, MMM sau dddd.", "SettingsComposer_Title": "Compozitor", "SettingsComposerFont_Title": "Font Compozitor Implicit", "SettingsComposerFontFamily_Description": "Modificați familia de fonturi implicite și dimensiunea fontului pentru compunerea e-mail-urilor.", @@ -521,7 +765,7 @@ "SettingsCustomTheme_Title": "Temă Personalizată", "SettingsDeleteAccount_Description": "Ștergeți toate e-mail-urile și acreditările asociate cu acest cont.", "SettingsDeleteAccount_Title": "Ștergeți acest cont", - "SettingsDeleteProtection_Description": "Ar trebui ca Wino să vă ceară o confirmare de fiecare dată când încercați să ștergeți definitiv un e-mail folosind tastele Shift + Del?", + "SettingsDeleteProtection_Description": "Should Wino ask you for confirmation every time you try to permanently delete a mail using Shift + Del keys?", "SettingsDeleteProtection_Title": "Protecție Ștergere Permanentă", "SettingsDiagnostics_Description": "Pentru dezvoltatori", "SettingsDiagnostics_DiagnosticId_Description": "Partajați acest ID cu dezvoltatorii atunci când le cereți ajutor pentru problemele pe care le întâmpinați în Wino Mail.", @@ -531,6 +775,9 @@ "SettingsDiscord_Title": "Canal Discord", "SettingsEditLinkedInbox_Description": "Adăugați/eliminați conturi, redenumiți sau rupeți legătura dintre conturi.", "SettingsEditLinkedInbox_Title": "Editați Inboxul Asociat", + "SettingsWindowBackdrop_Title": "Fundalul ferestrei", + "SettingsWindowBackdrop_Description": "Alegeți un efect de fundal pentru ferestrele Wino.", + "SettingsWindowBackdrop_Disabled": "Selecția fundalului ferestrei este dezactivată dacă tema aplicației este setată la ceva altceva decât Implicit.", "SettingsElementTheme_Description": "Selectați o temă Windows pentru Wino", "SettingsElementTheme_Title": "Temă Element", "SettingsElementThemeSelectionDisabled": "Selectarea temei elementului este dezactivată atunci când tema aplicației este selectată diferită de Implicită.", @@ -581,6 +828,8 @@ "SettingsManageAliases_Title": "Aliasuri", "SettingsEditAccountDetails_Title": "Editați detaliile contului", "SettingsEditAccountDetails_Description": "Schimbați numele contului, numele expeditorului și atribuiți o nouă culoare dacă doriți.", + "EditAccountDetailsPage_SaveSuccess_Title": "Modificările au fost salvate.", + "EditAccountDetailsPage_SaveSuccess_Message": "Detaliile contului dvs. au fost actualizate cu succes.", "SettingsManageLink_Description": "Mutați elementele pentru a adăuga un link nou sau pentru a elimina link-ul existent.", "SettingsManageLink_Title": "Gestionare Link", "SettingsMarkAsRead_Description": "Schimbați ceea ce ar trebui să se întâmple cu elementul selectat.", @@ -596,7 +845,41 @@ "SettingsNotifications_Title": "Notificări", "SettingsNotificationsAndTaskbar_Description": "Modificați dacă notificările trebuie afișate și insigna din bara de activități pentru acest cont.", "SettingsNotificationsAndTaskbar_Title": "Notificări și Bara de activități", + "SettingsHome_Title": "Acasă", + "SettingsHome_SearchTitle": "Găsește o setare", + "SettingsHome_SearchDescription": "Căutați după funcționalitate, subiect sau cuvânt cheie pentru a naviga direct la pagina corectă de setări.", + "SettingsHome_SearchPlaceholder": "Caută setări", + "SettingsHome_SearchExamples": "Exemple: temă, stocare, limbă, semnătură", + "SettingsHome_QuickLinks_Title": "Linkuri rapide", + "SettingsHome_QuickLinks_Description": "Accesați rapid setările cele mai utilizate.", + "SettingsHome_StorageCard_Description": "Vedeți cât conținut MIME local reține Wino pe acest dispozitiv și curățați-l când este necesar.", + "SettingsHome_StorageEmptySummary": "Încă nu a fost detectat conținut MIME în cache.", + "SettingsHome_StorageLoading": "Se verifică utilizarea MIME locală...", + "SettingsHome_Tips_Title": "Sfaturi și trucuri", + "SettingsHome_Tips_Description": "Câteva modificări mări pot face ca Wino să se simtă mult mai personal.", + "SettingsHome_Tip_Theme": "Doriți modul întunecat sau schimbări de nuanțe? Deschideți Personalizarea.", + "SettingsHome_Tip_Background": "Folosiți Preferințele aplicației pentru a controla comportamentul de pornire și sincronizarea în fundal.", + "SettingsHome_Tip_Shortcuts": "Scurtăturile de la tastatură vă ajută să navigați prin e-mail mai rapid.", + "SettingsHome_Resources_Title": "Linkuri utile", + "SettingsHome_Resources_Description": "Deschideți resursele proiectului, informații de suport și canale de lansare.", "SettingsOptions_Title": "Setări", + "SettingsOptions_GeneralSection": "General", + "SettingsOptions_MailSection": "E-mail", + "SettingsOptions_CalendarSection": "Calendar", + "SettingsOptions_MoreComingSoon": "Mai multe opțiuni în curând", + "SettingsOptions_HeroDescription": "Personalizați experiența dvs. Wino Mail", + "SettingsOptions_AccountsSummary": "{0} cont configurat(e)", + "SettingsSearch_ManageAccounts_Keywords": "cont;conturi;cutii poștale;cutii poștale;alias;aliasuri;profil;adresă;adrese", + "SettingsSearch_AppPreferences_Keywords": "pornire;fundal;lansare;sincronizare;notificări;căutare;zonă de notificări;implicit", + "SettingsSearch_LanguageTime_Keywords": "limbă;timp;ceas;locală;regiune;format;24 de ore;24h", + "SettingsSearch_Personalization_Keywords": "temă;întunecat;deschis;aspect;accent;culoare;mod;aranjament;densitate", + "SettingsSearch_About_Keywords": "despre;versiune;site;confidențialitate;github;donează;magazin;suport", + "SettingsSearch_KeyboardShortcuts_Keywords": "scurtătură;scurtături;tastă rapidă;tastatură;taste;hotkey;hotkeys", + "SettingsSearch_MessageList_Keywords": "mesaj;mesaje;listă;șir de mesaje;șiruri;avatar;previzualizare;expeditor", + "SettingsSearch_ReadComposePane_Keywords": "cititor;compunere;compozitor;font;fonturi;conținut extern;afișare;lectură", + "SettingsSearch_SignatureAndEncryption_Keywords": "semnătură;semnături;criptare;certificat;certificate;S/MIME;smime;securitate", + "SettingsSearch_Storage_Keywords": "stocare;cache;caching;mime;disc;spațiu;curățare;curățați;date locale", + "SettingsSearch_CalendarSettings_Keywords": "calendar;săptămână;ore;program;eveniment;evenimente", "SettingsPaneLengthReset_Description": "Resetați dimensiunea listei de e-mail la dimensiunea originală dacă aveți probleme cu aceasta.", "SettingsPaneLengthReset_Title": "Resetați Dimensiunea Listei de E-Mail", "SettingsPaypal_Description": "Arătați mult mai multă dragoste ❤️ Toate donațiile sunt apreciate.", @@ -610,6 +893,8 @@ "SettingsPrefer24HourClock_Title": "Afișează formatul ceasului în 24 de ore", "SettingsPrivacyPolicy_Description": "Consultați politica de confidențialitate.", "SettingsPrivacyPolicy_Title": "Politica de confidențialitate", + "SettingsWebsite_Description": "Deschideți site-ul Wino Mail.", + "SettingsWebsite_Title": "Site", "SettingsReadComposePane_Description": "Fonturi, conținut extern.", "SettingsReadComposePane_Title": "Cititor și Compozitor", "SettingsReader_Title": "Cititor", @@ -625,11 +910,24 @@ "SettingsShowPreviewText_Title": "Afișează textul de previzualizare", "SettingsShowSenderPictures_Description": "Ascundeți/afișați miniaturi imagini expeditor.", "SettingsShowSenderPictures_Title": "Afișați avatarele expeditorului", + "SettingsEmailTemplates_Title": "Șabloane de e-mail", + "SettingsEmailTemplates_Description": "Gestionează șabloanele de e-mail", + "SettingsEmailTemplates_CreatePageTitle": "Șablon nou", + "SettingsEmailTemplates_EditPageTitle": "Editează șablonul", + "SettingsEmailTemplates_NewTemplateTitle": "Șablon nou", + "SettingsEmailTemplates_NewTemplateDescription": "Creează un nou șablon de e-mail", + "SettingsEmailTemplates_NameTitle": "Nume", + "SettingsEmailTemplates_NamePlaceholder": "Nume șablon", + "SettingsEmailTemplates_DescriptionTitle": "Descriere", + "SettingsEmailTemplates_DescriptionPlaceholder": "Descriere opțională", + "SettingsEmailTemplates_ContentTitle": "Conținutul șablonului", + "SettingsEmailTemplates_ContentDescription": "Modificați conținutul HTML pentru acest șablon.", + "SettingsEmailTemplates_NameRequired": "Numele șablonului este obligatoriu.", "SettingsEnableGravatarAvatars_Title": "Gravatar", - "SettingsEnableGravatarAvatars_Description": "Folosiți gravatar (dacă este disponibil) ca imagine a expeditorului", - "SettingsEnableFavicons_Title": "Pictograme domeniu (Favicons)", - "SettingsEnableFavicons_Description": "Folosiți favicon-uri de domeniu (dacă sunt disponibile) ca imagine a expeditorului", - "SettingsMailList_ClearAvatarsCache_Button": "Șterge avatarele din cache", + "SettingsEnableGravatarAvatars_Description": "Use gravatar (if available) as sender picture", + "SettingsEnableFavicons_Title": "Domain icons (Favicons)", + "SettingsEnableFavicons_Description": "Use domain favicons (if available) as sender picture", + "SettingsMailList_ClearAvatarsCache_Button": "Clear cached avatars", "SettingsSignature_AddCustomSignature_Button": "Adăugare semnătură", "SettingsSignature_AddCustomSignature_Title": "Adăugare semnătură personalizată", "SettingsSignature_DeleteSignature_Title": "Ștergere semnătură", @@ -645,14 +943,41 @@ "SettingsStartupItem_Title": "Element de Pornire", "SettingsStore_Description": "Arătați puțină dragoste ❤️", "SettingsStore_Title": "Evaluați în Store", + "SettingsStorage_Title": "Stocare", + "SettingsStorage_Description": "Scanează și gestionează cache-ul MIME stocat în folderul local de date.", + "SettingsStorage_ScanFolder": "Scanează folderul local de date", + "SettingsStorage_NoLocalMimeDataFound": "Nu s-au găsit date MIME locale.", + "SettingsStorage_NoAccountsFound": "Nu s-au găsit conturi.", + "SettingsStorage_TotalUsage": "Utilizare totală MIME locală: {0}", + "SettingsStorage_AccountUsageDescription": "{0} folosit în cache-ul MIME local", + "SettingsStorage_DeleteAll_Title": "Ștergeți întregul conținut MIME", + "SettingsStorage_DeleteAll_Description": "Ștergeți întregul dosar cache MIME al acestui cont.", + "SettingsStorage_DeleteAll_Button": "Ștergeți tot", + "SettingsStorage_DeleteAll_Confirm_Title": "Ștergeți întregul conținut MIME", + "SettingsStorage_DeleteAll_Confirm_Message": "Ștergeți toate datele MIME locale pentru {0}?", + "SettingsStorage_DeleteAll_Success": "Toate conținuturile MIME au fost șterse.", + "SettingsStorage_DeleteOld_Title": "Ștergeți conținut MIME vechi", + "SettingsStorage_DeleteOld_Description": "Ștergeți fișiere MIME în funcție de data de creare a mesajelor din baza locală de date.", + "SettingsStorage_DeleteOld_1Month": "> 1 lună", + "SettingsStorage_DeleteOld_3Months": "> 3 luni", + "SettingsStorage_DeleteOld_6Months": "> 6 luni", + "SettingsStorage_DeleteOld_1Year": "> 1 an", + "SettingsStorage_DeleteOld_Confirm_Title": "Ștergeți conținut MIME vechi", + "SettingsStorage_DeleteOld_Confirm_Message": "Ștergeți datele MIME locale mai vechi decât {0} pentru {1}?", + "SettingsStorage_DeleteOld_Success": "Au fost șterse {0} foldere MIME mai vechi decât {1}.", + "SettingsStorage_1Month": "1 lună", + "SettingsStorage_3Months": "3 luni", + "SettingsStorage_6Months": "6 luni", + "SettingsStorage_1Year": "1 an", + "SettingsStorage_Months": "{0} luni", "SettingsTaskbarBadge_Description": "Include numărul de email-uri necitite în pictograma de pe bara de activități.", "SettingsTaskbarBadge_Title": "Insignă Bară de activități", "SettingsThreads_Description": "Organizați mesajele în fire de conversație.", "SettingsThreads_Title": "Aranjare mesaje în conversație", "SettingsUnlinkAccounts_Description": "Eliminați legătura dintre conturi. Acest lucru nu va șterge conturile dvs.", "SettingsUnlinkAccounts_Title": "Deasociere Conturi", - "SettingsMailRendering_ActionLabels_Title": "Etichete de acțiune", - "SettingsMailRendering_ActionLabels_Description": "Afișare etichete de acțiune.", + "SettingsMailRendering_ActionLabels_Title": "Action labels", + "SettingsMailRendering_ActionLabels_Description": "Show action labels.", "SignatureDeleteDialog_Message": "Sigur doriți să ștergeți semnătura „{0}”?", "SignatureDeleteDialog_Title": "Ștergeți semnătura", "SignatureEditorDialog_SignatureName_Placeholder": "Numiți-vă semnătura", @@ -683,6 +1008,9 @@ "SystemFolderConfigDialogValidation_InboxSelected": "Nu puteți atribui folderul Inbox oricărui alt folder de sistem.", "SystemFolderConfigSetupSuccess_Message": "Folderele de sistem sunt configurate cu succes.", "SystemFolderConfigSetupSuccess_Title": "Configurare Fosare de Sistem", + "SystemTrayMenu_ShowWino": "Deschide Wino Mail", + "SystemTrayMenu_ShowWinoCalendar": "Deschide calendarul Wino", + "SystemTrayMenu_ExitWino": "Ieșiți", "TestingImapConnectionMessage": "Se testează conexiunea la server...", "TitleBarServerDisconnectedButton_Description": "Wino este deconectat de la rețea. Faceți clic pe reconectare pentru a restabili conexiunea.", "TitleBarServerDisconnectedButton_Title": "nicio conexiune", @@ -699,8 +1027,422 @@ "WinoUpgradeMessage": "Upgrade la Conturi Nelimitate", "WinoUpgradeRemainingAccountsMessage": "{0} din {1} conturi gratuite folosite.", "Yesterday": "Ieri", - "SettingsAppPreferences_EmailSyncInterval_Title": "Interval sincronizare e-mail", - "SettingsAppPreferences_EmailSyncInterval_Description": "Interval automat de sincronizare e-mail (minute). Această setare va fi aplicată numai după repornirea Wino Mail." + "Smime_ImportCertificates_Success": "Certificatele au fost importate cu succes.", + "Smime_ImportCertificates_Error": "Eroare la importarea certificatelor: {0}", + "Smime_RemoveCertificates_Confirm": "Chiar doriți să eliminați certificatele {0}?", + "Smime_RemoveCertificates_Success": "Certificatele au fost eliminate.", + "Smime_ExportCertificates_Success": "Certificatele au fost exportate.", + "Smime_ExportCertificates_Error": "Eroare la exportarea certificatelor.", + "Smime_CertificateDetails": "Subiect: {0}\nEmitent: {1}\nValabil de la: {2}\nValabil până la: {3}\nAmprentă: {4}", + "Smime_CertificatePassword_Title": "Parola certificatului este obligatorie", + "Smime_CertificatePassword_Placeholder": "Parola certificatului pentru {0} (opțional)", + "Smime_Confirm_Title": "Confirmare", + "Buttons_OK": "OK", + "Buttons_Refresh": "Actualizează", + "SettingsSignatureAndEncryption_Title": "Semnătură și criptare", + "SettingsSignatureAndEncryption_Description": "Gestionează certificate S/MIME pentru semnarea și criptarea e-mailurilor.", + "SettingsSignatureAndEncryption_MyCertificatesHeader": "Certificatele mele", + "SettingsSignatureAndEncryption_MyCertificatesDescription": "Certificate personale pentru semnare și criptare", + "SettingsSignatureAndEncryption_RecipientCertificatesHeader": "Certificatele destinatarilor", + "SettingsSignatureAndEncryption_RecipientCertificatesDescription": "Certificatele destinatarilor pentru decriptare", + "SettingsSignatureAndEncryption_NameColumn": "Nume", + "SettingsSignatureAndEncryption_ExpiresColumn": "Expiră la", + "SettingsSignatureAndEncryption_ThumbprintColumn": "Amprentă", + "Buttons_Remove": "Șterge", + "Buttons_Export": "Exportă", + "Buttons_Import": "Importă", + "SettingsSignatureAndEncryption_SigningCertificate": "Certificat de semnătură S/MIME", + "SettingsSignatureAndEncryption_EncryptionCertificate": "Certificat de criptare S/MIME", + "SettingsSignatureAndEncryption_SigningCertificatePlaceholder": "Niciunul", + "SmimeSignaturesInMessage": "Semnăturile din acest mesaj:", + "SmimeSignatureEntry": "• {0} {1} ({2}, valabil până la {3} - {4})", + "SmimeSigningCertificateInfoTitle": "Informații despre certificatul de semnătură S/MIME", + "SmimeCertificateInfoTitle": "Informații despre certificatul S/MIME", + "SmimeNoCertificateFileFound": "Nu a fost găsit niciun fișier de certificat", + "SmimeSaveCertificate": "Salvează certificatul...", + "SmimeCertificate": "Certificat S/MIME", + "SmimeCertificateSavedTo": "Certificatul a fost salvat în {0}", + "SmimeSignedTooltip": "Acest mesaj este semnat cu un certificat S/MIME. Faceți clic pentru mai multe detalii", + "SmimeEncryptedTooltip": "Acest mesaj este criptat cu un certificat S/MIME.", + "SmimeCertificateFileInfo": "Fișier: {0}", + "Composer_LightTheme": "Temă deschisă", + "Composer_DarkTheme": "Temă Întunecată", + "Composer_Outdent": "Scade indentarea", + "Composer_Indent": "Indentează", + "Composer_BulletList": "Listă cu buline", + "Composer_OrderedList": "Listă numerotată", + "Composer_Stroke": "Tăiere cu linie", + "Composer_Bold": "Îngroșat", + "Composer_Italic": "Cursiv", + "Composer_Underline": "Subliniază", + "Composer_CcBcc": "Cc & Bcc", + "Composer_EnableSmimeSignature": "Activează/ Dezactivează semnătura S/MIME", + "Composer_EnableSmimeEncryption": "Activează/ Dezactivează criptarea S/MIME", + "Composer_LocalDraftSyncInfo": "Acest draft este local doar. Wino nu a reușit să-l trimită către serverul tău de e-mail. Fă clic pentru a încerca din nou trimiterea către server.", + "Composer_CertificateExpires": "Expiră la: ", + "Composer_SmimeSignature": "Semnătură S/MIME", + "Composer_SmimeEncryption": "Criptare S/MIME", + "Composer_EmailTemplatesPlaceholder": "Șabloane de e-mail", + "Composer_AiSummarize": "Rezumă cu AI", + "Composer_AiSummarizeDescription": "Extrage ideile principale, pașii de acțiune și deciziile din acest e-mail.", + "Composer_AiTranslate": "Translatează cu AI", + "Composer_AiActions": "Acțiuni AI", + "Composer_AiRewrite": "Rescrie cu AI", + "AiActions_CheckingStatus": "Se verifică accesul AI...", + "AiActions_SignedOutTitle": "Deblochează pachetul Wino AI", + "AiActions_SignedOutDescription": "Translatează, rescrie și rezumă e-mailuri cu AI după ce te autentifici în contul tău Wino și activezi pachetul AI.", + "AiActions_NoPackTitle": "Pachetul AI este necesar", + "AiActions_NoPackDescription": "Ești conectat, dar pachetul AI nu este încă activ. Achiziționează-l pentru a folosi instrumentele AI de traducere, rescriere și rezumare ale Wino.", + "AiActions_UsageSummary": "{0} din {1} credite utilizate în această lună.", + "Composer_AiRewritePolite": "Fă-l politicos", + "Composer_AiRewritePoliteDescription": "Îmblânzește formularea păstrând același scop.", + "Composer_AiRewriteAngry": "Fă-l furios", + "Composer_AiRewriteAngryDescription": "Folosește un ton mai dur și mai confruntațional.", + "Composer_AiRewriteHappy": "Fă-l să fie fericit", + "Composer_AiRewriteHappyDescription": "Adaugă un ton mai optimist și mai entuziast.", + "Composer_AiRewriteFormal": "Fă-l să fie formal", + "Composer_AiRewriteFormalDescription": "Face mesajul să pară mai profesional și mai structurat.", + "Composer_AiRewriteFriendly": "Fă-l să fie prietenos", + "Composer_AiRewriteFriendlyDescription": "Încălzește mesajul cu un ton mai accesibil.", + "Composer_AiRewriteShorter": "Fă-l să fie mai scurt", + "Composer_AiRewriteShorterDescription": "Îmbunătățește concizia textului și elimină detaliile inutile.", + "Composer_AiRewriteClearer": "Fă-l să fie mai clar", + "Composer_AiRewriteClearerDescription": "Îmbunătățește lizibilitatea și face mesajul mai ușor de urmărit.", + "Composer_AiRewriteCustom": "Personalizat", + "Composer_AiRewriteCustomDescription": "Descrie scopul tău pentru rescriere.", + "Composer_AiRewriteCustomPlaceholder": "Descrie cum vrei să fie rescris mesajul.", + "Composer_AiRewriteMode": "Ton de rescriere", + "Composer_AiRewriteApply": "Aplică rescrierea", + "Composer_AiTranslateDialogTitle": "Traducere cu AI", + "Composer_AiTranslateDialogDescription": "Introduceți limba țintă sau codul de cultură, de exemplu en-US, tr-TR, de-DE sau fr-FR.", + "Composer_AiTranslateApply": "Tradu", + "Composer_AiTranslateLanguage": "Limba țintă", + "Composer_AiTranslateCustomPlaceholder": "Introduceți codul de cultură", + "Composer_AiTranslateLanguageEnglish": "Engleză (en-US)", + "Composer_AiTranslateLanguageTurkish": "Turcă (tr-TR)", + "Composer_AiTranslateLanguageGerman": "Germană (de-DE)", + "Composer_AiTranslateLanguageFrench": "Franceză (fr-FR)", + "Composer_AiTranslateLanguageSpanish": "Spaniolă (es-ES)", + "Composer_AiTranslateLanguageItalian": "Italiană (it-IT)", + "Composer_AiTranslateLanguagePortugueseBrazil": "Portugheză (Brazilia) (pt-BR)", + "Composer_AiTranslateLanguageDutch": "Olandeză (nl-NL)", + "Composer_AiTranslateLanguagePolish": "Poloneză (pl-PL)", + "Composer_AiTranslateLanguageRussian": "Rusă (ru-RU)", + "Composer_AiTranslateLanguageJapanese": "Japoneză (ja-JP)", + "Composer_AiTranslateLanguageKorean": "Coreeană (ko-KR)", + "Composer_AiTranslateLanguageChineseSimplified": "Chineză, Simplificată (zh-CN)", + "Composer_AiTranslateLanguageArabic": "Arabă (ar-SA)", + "Composer_AiTranslateLanguageHindi": "Hindi (hi-IN)", + "Composer_AiTranslateLanguageOther": "Altele...", + "Composer_AiBusyTitle": "AI este deja în lucru", + "Composer_AiBusyMessage": "Vă rugăm să așteptați finalizarea acțiunii curente AI.", + "Composer_AiSignInRequired": "Conectați-vă la contul Wino pentru a utiliza funcțiile AI.", + "Composer_AiMissingHtml": "Încă nu există conținut de mesaj de trimis către Wino AI.", + "Composer_AiQuotaUnavailable": "Rezultatul AI a fost aplicat.", + "Composer_AiAppliedMessage": "Rezultatul AI a fost aplicat în editorul de compoziție. Folosiți Anulează dacă doriți să reveniți.", + "Composer_AiSummarizeSuccessTitle": "Rezumat AI aplicat", + "Composer_AiTranslateSuccessTitle": "Traducere AI aplicată", + "Composer_AiRewriteSuccessTitle": "Rescriere AI aplicată", + "Composer_AiErrorTitle": "Acțiunea AI a eșuat.", + "Reader_AiAppliedMessage": "Rezultatul AI este acum afișat pentru acest mesaj. Redeschideți mesajul pentru a vizualiza din nou conținutul original.", + "SettingsAppPreferences_EmailSyncInterval_Title": "Email sync interval", + "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail.", + "ContactsPage_Title": "Contacte", + "ContactsPage_AddContact": "Adaugă contact", + "ContactsPage_EditContact": "Editează contactul", + "ContactsPage_DeleteContact": "Șterge contactul", + "ContactsPage_SearchPlaceholder": "Caută contacte...", + "ContactsPage_NoContacts": "Nu există contacte de afișat.", + "ContactsPage_ContactsCount": "{0} contacte", + "ContactsPage_SelectedContactsCount": "{0} contacte selectate", + "ContactsPage_DeleteSelectedContacts": "Șterge contactele selectate", + "ContactEditDialog_Title": "Editează contactul", + "ContactEditDialog_PhotoSection": "Poză", + "ContactEditDialog_ChoosePhoto": "Alege poza", + "ContactEditDialog_RemovePhoto": "Șterge poza", + "ContactEditDialog_NameHeader": "Nume", + "ContactEditDialog_NamePlaceholder": "Numele contactului", + "ContactEditDialog_EmailHeader": "Adresă de e-mail", + "ContactEditDialog_EmailPlaceholder": "contact@example.com", + "ContactEditDialog_InfoSection": "Informații de contact", + "ContactEditDialog_RootContactInfo": "Acest contact este asociat cu conturile dvs. și nu poate fi șters.", + "ContactEditDialog_OverriddenContactInfo": "Acest contact a fost modificat manual și nu va fi actualizat în timpul sincronizării.", + "ContactsPage_Subtitle": "Gestionează contactele dvs. de e-mail și informațiile lor.", + "ContactStatus_Account": "Cont", + "ContactStatus_Modified": "Modificat", + "ContactAction_Edit": "Editează contactul", + "ContactAction_ChangePhoto": "Schimbă poza", + "ContactAction_Delete": "Șterge contactul", + "ContactAction_Add": "Adaugă contact", + "ContactSelection_Selected": "selectat", + "ContactSelection_SelectAll": "Selectează toate", + "ContactSelection_Clear": "Golește selecția", + "ContactsPage_EmptyState": "Nu există contacte de afișat.", + "ContactsPage_AddFirstContact": "Adaugă primul contact", + "ContactsPage_ContactsCountSuffix": "contacte", + "ContactsPane_NewContact": "Contact nou", + "ContactsPane_DescriptionTitle": "Gestionează contactele tale.", + "ContactsPane_DescriptionBody": "Creați contacte, redenumiți-le, actualizați imaginile de profil și păstrați detaliile salvate organizate într-un singur loc.", + "ContactEditDialog_AddTitle": "Adaugă contact", + "ContactInfoBar_ContactAdded": "Contactul a fost adăugat cu succes.", + "ContactInfoBar_ContactUpdated": "Contactul a fost actualizat cu succes.", + "ContactInfoBar_ContactsDeleted": "Contactele au fost șterse cu succes.", + "ContactInfoBar_ContactPhotoUpdated": "Fotografia contactului a fost actualizată cu succes.", + "ContactInfoBar_FailedToLoadContacts": "Încărcarea contactelor a eșuat: {0}", + "ContactInfoBar_FailedToAddContact": "Nu s-a putut adăuga contactul: {0}", + "ContactInfoBar_FailedToUpdateContact": "Nu s-a putut actualiza contactul: {0}", + "ContactInfoBar_FailedToDeleteContacts": "Nu s-au putut șterge contactele: {0}", + "ContactInfoBar_FailedToUpdatePhoto": "Nu s-a putut actualiza poza: {0}", + "ContactInfoBar_CannotDeleteRoot": "Contactele principale nu pot fi șterse.", + "ContactConfirmDialog_DeleteTitle": "Șterge contactul", + "ContactConfirmDialog_DeleteMessage": "Sigur doriți să ștergeți contactul '{0}'?", + "ContactConfirmDialog_DeleteMultipleMessage": "Sigur doriți să ștergeți {0} contacte?", + "ContactConfirmDialog_DeleteButton": "Șterge", + "CalendarAccountSettings_Title": "Setări cont calendar", + "CalendarAccountSettings_Description": "Gestionează setările calendarului pentru {0}", + "CalendarAccountSettings_AccountColor": "Culoare contului", + "CalendarAccountSettings_AccountColorDescription": "Schimbă culoarea de afișare a acestui cont de calendar.", + "CalendarAccountSettings_SyncEnabled": "Activează sincronizarea", + "CalendarAccountSettings_SyncEnabledDescription": "Activează sau dezactivează sincronizarea calendarului pentru acest cont.", + "CalendarAccountSettings_DefaultShowAs": "Stare implicită de afișare", + "CalendarAccountSettings_DefaultShowAsDescription": "Stare de disponibilitate implicită pentru noile evenimente create cu acest cont.", + "CalendarAccountSettings_PrimaryCalendar": "Calendar principal", + "CalendarAccountSettings_PrimaryCalendarDescription": "Marchează acest calendar ca calendarul principal pentru cont.", + "CalendarSettings_NewEventBehavior_Header": "Comportamentul butonului Eveniment nou", + "CalendarSettings_NewEventBehavior_Description": "Alege dacă butonul 'Eveniment nou' ar trebui să ceară un calendar de fiecare dată sau să deschidă întotdeauna un calendar specific.", + "CalendarSettings_NewEventBehavior_AskEachTime": "Întreabă de fiecare dată.", + "CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar": "Întotdeauna folosește un calendar specific.", + "CalendarSettings_Rendering_Title": "Redare", + "CalendarSettings_Rendering_Description": "Configurează aspectul calendarului și comportamentul de afișare.", + "CalendarSettings_Notifications_Title": "Notificări", + "CalendarSettings_Notifications_Description": "Alege comportamentul implicit pentru mementouri și amânări.", + "CalendarSettings_Preferences_Title": "Preferințe", + "CalendarSettings_Preferences_Description": "Setează cum se comportă butonul 'Eveniment nou'.", + "WhatIsNew_GetStartedButton": "Începe", + "WhatIsNew_ContinueAnywayButton": "Continuă oricum", + "WhatIsNew_PreparingForNewVersionButton": "Se pregătește pentru noua versiune...", + "WhatIsNew_MigrationPreparing_Title": "Pregătirea datelor", + "WhatIsNew_MigrationPreparing_Description": "Wino aplică migrațiile de actualizare. Vă rugăm să așteptați în timp ce pregătim datele contului dvs. pentru această versiune.", + "WhatIsNew_MigrationFailedMessage": "Aplicarea migrațiilor a eșuat cu codul de eroare {0}. Puteți continua să folosiți aplicația. Totuși, dacă întâmpinați probleme grave, vă rugăm să reinstalați aplicația.", + "WhatIsNew_MigrationNotification_Title": "Wino Mail Actualizat", + "WhatIsNew_MigrationNotification_Message": "Deschideți aplicația pentru a finaliza actualizarea și a vedea noutățile.", + "WelcomeWindow_Title": "Bine ați venit la Wino Mail", + "WelcomeWindow_Subtitle": "O experiență nativă Windows pentru Mail și Calendar.", + "WelcomeWindow_WhatsNewTitle": "Ultimele modificări", + "WelcomeWindow_FeaturesTitle": "Funcționalități", + "WelcomeWindow_WhatsNewTab": "Ce e nou", + "WelcomeWindow_FeaturesTab": "Caracteristici", + "WelcomeWindow_GetStartedButton": "Începe", + "WelcomeWindow_GetStartedDescription": "Adăugați contul dvs. Outlook, Gmail sau IMAP pentru a începe cu Wino Mail.", + "WelcomeWindow_ImportFromWinoAccount": "Importă din contul tău Wino", + "WelcomeWindow_ImportInProgress": "Se importă preferințele și conturile sincronizate...", + "WelcomeWindow_ImportNoAccountsFound": "Nu au fost găsite conturi sincronizate în contul tău Wino. Dacă preferințele erau disponibile, acestea au fost restaurate. Folosește Începe pentru a adăuga un cont manual.", + "WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} conturi sincronizate sunt deja disponibile pe acest dispozitiv. Folosește Începe pentru a adăuga manual un alt cont, dacă este necesar.", + "WelcomeWindow_SetupTitle": "Configurarea contului tău", + "WelcomeWindow_SetupSubtitle": "Alege-ți furnizorul de e-mail pentru a începe.", + "WelcomeWindow_AddAccountButton": "Adaugă cont", + "WelcomeWindow_SkipForNow": "Sari pentru moment — Îl voi configura mai târziu.", + "WelcomeWindow_AppDescription": "O căsuță de e-mail agilă și concentrată — reproiectată pentru Windows 11.", + "WelcomeWizard_Step1Title": "Bine ați venit", + "SystemTrayMenu_Open": "Deschide", + "WinoAccount_Titlebar_SyncBenefitTitle": "Setări de sincronizare", + "WinoAccount_Titlebar_SyncBenefitDescription": "Mențineți preferințele Wino sincronizate pe toate dispozitivele.", + "WinoAccount_Titlebar_AddonsBenefitTitle": "Deblochează add-ons", + "WinoAccount_Titlebar_AddonsBenefitDescription": "Accesează funcții premium precum Wino AI Pack.", + "WinoAccount_Management_Description": "Gestionează Contul Wino, accesul la AI Pack și preferințele sincronizate și detaliile contului.", + "WinoAccount_Management_SignedOutTitle": "Conectează-te la Wino Mail", + "WinoAccount_Management_SignedOutDescription": "Conectează-te sau creează un cont pentru a sincroniza emailul tău, a accesa funcțiile AI și a-ți gestiona setările între dispozitive.", + "WinoAccount_Management_ProfileSectionHeader": "Profil", + "WinoAccount_Management_AddOnsSectionHeader": "Extensii Wino", + "WinoAccount_Management_DataSectionHeader": "Date", + "WinoAccount_Management_AccountActionsSectionHeader": "Acțiuni ale contului", + "WinoAccount_Management_AccountCardTitle": "Cont", + "WinoAccount_Management_AccountCardDescription": "Adresa de email a contului Wino și starea curentă a contului.", + "WinoAccount_Management_AiPackCardTitle": "Pachet AI", + "WinoAccount_Management_AiPackCardDescription": "Vezi dacă Pachet AI Wino este activ și cât mai este disponibil din utilizare.", + "WinoAccount_Management_AiPackActive": "Pachet AI este activ", + "WinoAccount_Management_AiPackInactive": "Pachet AI nu este activ", + "WinoAccount_Management_AiPackUsage": "{0} din {1} utilizări consumate. {2} rămase.", + "WinoAccount_Management_AiPackBillingPeriod": "Perioadă de facturare: {0:d} - {1:d}", + "WinoAccount_Management_AiPackUnknownUsage": "Detalii despre utilizare nu sunt disponibile încă.", + "WinoAccount_Management_AiPackBuyDescription": "Cumpără Wino AI Pack pentru a traduce, rescrie sau rezuma emailuri cu ajutorul AI.", + "WinoAccount_Management_AiPackPromoTitle": "Deblochează AI Pack", + "WinoAccount_Management_AiPackPromoDescription": "Accelerează fluxul de lucru al e-mailurilor cu uneltele AI. Tradu mesajele în peste 50 de limbi, rescrie pentru claritate și tonalitate, și obține rezumate instantanee ale firurilor lungi.", + "WinoAccount_Management_AiPackPromoPrice": "$4.99 / lună", + "WinoAccount_Management_AiPackPromoRequests": "1.000 credite", + "WinoAccount_Management_AiPackGetButton": "Obține AI Pack", + "WinoAddOn_AI_PACK_Name": "Wino AI Pack", + "WinoAddOn_AI_PACK_Description": "Unelte alimentate de AI pentru traducere, rescriere și rezumare acțiuni în Wino Mail.", + "WinoAddOn_AI_PACK_Keywords": "AI, traducere, rescrie, rezumă, productivitate", + "WinoAddOn_UNLIMITED_ACCOUNTS_Name": "Conturi nelimitate", + "WinoAddOn_UNLIMITED_ACCOUNTS_Description": "Elimină limita de conturi și adaugă cât de multe conturi de e-mail ai nevoie.", + "WinoAddOn_UNLIMITED_ACCOUNTS_Keywords": "conturi, nelimitate, premium, extensie", + "WinoAccount_Management_PurchaseRequiresSignIn": "Conectează-te cu Contul Wino pentru a finaliza această achiziție.", + "WinoAccount_Management_PurchaseStartFailed": "Wino nu a putut finaliza această achiziție în Microsoft Store.", + "WinoAccount_Management_StoreSyncFailed": "Achiziția ta s-a finalizat, dar Wino nu a putut să actualizeze încă beneficiile contului tău. Te rugăm să încerci din nou într-un moment.", + "WinoAccount_Management_AiPackSubscriptionActive": "Abonamentul tău este activ", + "WinoAccount_Management_AiPackRenews": "Se reînnoiește {0:d}", + "WinoAccount_Management_AiPackRequestsUsed": "Credite utilizate în această lună", + "WinoAccount_Management_AiPackResets": "Resetează {0:d}", + "WinoAccount_Management_AiPackUsageLoadFailed": "Au apărut probleme la încărcarea soldului de utilizare AI.", + "WinoAccount_Management_AiPackFeatureTranslate": "Traducere", + "WinoAccount_Management_AiPackFeatureRewrite": "Rescriere", + "WinoAccount_Management_AiPackFeatureSummarize": "Rezumat", + "WinoAccount_Management_AddOnLoadFailed": "Am întâmpinat probleme la încărcarea acestui add-on.", + "WinoAccount_Management_SyncPreferencesTitle": "Sincronizează Preferințe și Conturi", + "WinoAccount_Management_SyncPreferencesDescription": "Importă sau exportă preferințele Wino și detaliile căsuței de email între dispozitive. Parolele, tokenurile și alte informații sensibile nu sunt sincronizate niciodată.", + "WinoAccount_Management_SignOutTitle": "Deconectare", + "WinoAccount_Management_SignOutDescription": "Deconectează-te de la contul tău pe acest dispozitiv", + "WinoAccount_Management_StatusLabel": "Stare: {0}", + "WinoAccount_Management_NoRemoteSettings": "Încă nu există date sincronizate stocate pentru acest cont.", + "WinoAccount_Management_ExportSucceeded": "Datele Wino selectate au fost exportate cu succes.", + "WinoAccount_Management_ExportPreferencesSucceeded": "Preferințele tale au fost exportate în contul tău Wino.", + "WinoAccount_Management_ExportAccountsSucceeded": "Detalii de cont exportate în contul tău Wino.", + "WinoAccount_Management_ImportSucceeded": "Datele sincronizate din contul tău Wino au fost importate.", + "WinoAccount_Management_ImportPreferencesSucceeded": "Au fost aplicate {0} preferințe sincronizate.", + "WinoAccount_Management_ImportAccountsSucceeded": "/** placeholder **/", + "WinoAccount_Management_ImportDuplicateAccountsSkipped": "/** placeholder **/", + "WinoAccount_Management_ImportPartial": "/** placeholder **/", + "WinoAccount_Management_ImportReloginReminder": "/** placeholder **/", + "WinoAccount_Management_SerializeFailed": "/** placeholder **/", + "WinoAccount_Management_EmptyExport": "/** placeholder **/", + "WinoAccount_Management_ImportEmpty": "/** placeholder **/", + "WinoAccount_Management_ExportDialog_Title": "/** placeholder **/", + "WinoAccount_Management_ExportDialog_Description": "/** placeholder **/", + "WinoAccount_Management_ExportDialog_IncludePreferences": "/** placeholder **/", + "WinoAccount_Management_ExportDialog_IncludeAccounts": "/** placeholder **/", + "WinoAccount_Management_ExportDialog_AccountsDisclaimer": "/** placeholder **/", + "WinoAccount_Management_ExportDialog_AccountsRelogin": "/** placeholder **/", + "WinoAccount_Management_ExportDialog_InProgress": "/** placeholder **/", + "WinoAccount_Management_LoadFailed": "/** placeholder **/", + "WinoAccount_Management_ActionFailed": "/** placeholder **/", + "WinoAccount_SettingsSection_Title": "/** placeholder **/", + "WinoAccount_SettingsSection_Description": "/** placeholder **/", + "WinoAccount_RegisterButton_Title": "/** placeholder **/", + "WinoAccount_RegisterButton_Description": "/** placeholder **/", + "WinoAccount_RegisterButton_Action": "/** placeholder **/", + "WinoAccount_LoginButton_Title": "/** placeholder **/", + "WinoAccount_LoginButton_Description": "/** placeholder **/", + "WinoAccount_LoginButton_Action": "/** placeholder **/", + "WinoAccount_SignOutButton_Title": "/** placeholder **/", + "WinoAccount_SignOutButton_Description": "/** placeholder **/", + "WinoAccount_SignOutButton_Action": "/** placeholder **/", + "WinoAccount_RegisterDialog_Title": "/** placeholder **/", + "WinoAccount_RegisterDialog_Description": "/** placeholder **/", + "WinoAccount_RegisterDialog_HeroTitle": "/** placeholder **/", + "WinoAccount_RegisterDialog_BenefitsTitle": "/** placeholder **/", + "WinoAccount_RegisterDialog_BenefitSyncTitle": "/** placeholder **/", + "WinoAccount_RegisterDialog_BenefitSyncDescription": "/** placeholder **/", + "WinoAccount_RegisterDialog_BenefitAiTitle": "/** placeholder **/", + "WinoAccount_RegisterDialog_BenefitAiDescription": "/** placeholder **/", + "WinoAccount_RegisterDialog_DifferenceTitle": "/** placeholder **/", + "WinoAccount_RegisterDialog_DifferenceDescription": "/** placeholder **/", + "WinoAccount_RegisterDialog_PrimaryButton": "/** placeholder **/", + "WinoAccount_RegisterDialog_PrivacyTitle": "/** placeholder **/", + "WinoAccount_RegisterDialog_PrivacyDescription": "/** placeholder **/", + "WinoAccount_RegisterDialog_PrivacyLinkText": "/** placeholder **/", + "WinoAccount_RegisterDialog_PrivacyCheckbox": "/** placeholder **/", + "WinoAccount_LoginDialog_Title": "/** placeholder **/", + "WinoAccount_LoginDialog_Description": "/** placeholder **/", + "WinoAccount_LoginDialog_HeroTitle": "/** placeholder **/", + "WinoAccount_LoginDialog_BenefitsTitle": "/** placeholder **/", + "WinoAccount_LoginDialog_BenefitsDescription": "/** placeholder **/", + "WinoAccount_LoginDialog_DifferenceTitle": "/** placeholder **/", + "WinoAccount_LoginDialog_DifferenceDescription": "/** placeholder **/", + "WinoAccount_LoginDialog_ForgotPasswordLink": "/** placeholder **/", + "WinoAccount_EmailLabel": "/** placeholder **/", + "WinoAccount_EmailPlaceholder": "/** placeholder **/", + "WinoAccount_PasswordLabel": "/** placeholder **/", + "WinoAccount_ConfirmPasswordLabel": "/** placeholder **/", + "WinoAccount_ForgotPasswordDialog_Title": "/** placeholder **/", + "WinoAccount_ForgotPasswordDialog_PrimaryButton": "/** placeholder **/", + "WinoAccount_ForgotPasswordDialog_BackToSignIn": "/** placeholder **/", + "WinoAccount_ForgotPasswordDialog_Description": "/** placeholder **/", + "WinoAccount_Validation_EmailRequired": "/** placeholder **/", + "WinoAccount_Validation_PasswordRequired": "/** placeholder **/", + "WinoAccount_Validation_PasswordMismatch": "/** placeholder **/", + "WinoAccount_Validation_PrivacyConsentRequired": "/** placeholder **/", + "WinoAccount_Error_InvalidCredentials": "/** placeholder **/", + "WinoAccount_Error_AccountLocked": "/** placeholder **/", + "WinoAccount_Error_AccountBanned": "/** placeholder **/", + "WinoAccount_Error_AccountSuspended": "/** placeholder **/", + "WinoAccount_Error_EmailNotConfirmed": "/** placeholder **/", + "WinoAccount_Error_EmailConfirmationRequired": "/** placeholder **/", + "WinoAccount_Error_EmailConfirmationResendNotAvailable": "/** placeholder **/", + "WinoAccount_Error_EmailConfirmationResendInvalid": "/** placeholder **/", + "WinoAccount_Error_EmailNotRegistered": "/** placeholder **/", + "WinoAccount_Error_RefreshTokenInvalid": "/** placeholder **/", + "WinoAccount_Error_EmailAlreadyRegistered": "/** placeholder **/", + "WinoAccount_Error_ExternalLoginEmailRequired": "/** placeholder **/", + "WinoAccount_Error_ExternalLoginInvalid": "/** placeholder **/", + "WinoAccount_Error_ExternalAuthStateInvalid": "/** placeholder **/", + "WinoAccount_Error_ExternalAuthCodeInvalid": "/** placeholder **/", + "WinoAccount_Error_AiPackRequired": "/** placeholder **/", + "WinoAccount_Error_AiQuotaExceeded": "/** placeholder **/", + "WinoAccount_Error_AiHtmlEmpty": "/** placeholder **/", + "WinoAccount_Error_AiHtmlTooLarge": "/** placeholder **/", + "WinoAccount_Error_AiUnsupportedLanguage": "/** placeholder **/", + "WinoAccount_Error_Forbidden": "/** placeholder **/", + "WinoAccount_Error_ValidationFailed": "/** placeholder **/", + "WinoAccount_RegisterSuccessMessage": "/** placeholder **/", + "WinoAccount_LoginSuccessMessage": "/** placeholder **/", + "WinoAccount_EmailConfirmationSentDialog_Title": "/** placeholder **/", + "WinoAccount_EmailConfirmationSentDialog_Message": "/** placeholder **/", + "WinoAccount_EmailConfirmationPendingDialog_Title": "/** placeholder **/", + "WinoAccount_EmailConfirmationPendingDialog_Message": "/** placeholder **/", + "WinoAccount_EmailConfirmationPendingDialog_ResendButton": "/** placeholder **/", + "WinoAccount_EmailConfirmationPendingDialog_Countdown": "/** placeholder **/", + "WinoAccount_EmailConfirmationPendingDialog_ReadyToResend": "/** placeholder **/", + "WinoAccount_EmailConfirmationResentDialog_Title": "/** placeholder **/", + "WinoAccount_EmailConfirmationResentDialog_Message": "/** placeholder **/", + "WinoAccount_ForgotPasswordDialog_SuccessTitle": "/** placeholder **/", + "WinoAccount_ForgotPasswordDialog_SuccessMessage": "/** placeholder **/", + "WinoAccount_ChangePassword_Title": "/** placeholder **/", + "WinoAccount_ChangePassword_Description": "/** placeholder **/", + "WinoAccount_ChangePassword_Action": "/** placeholder **/", + "WinoAccount_ChangePassword_ConfirmationMessage": "Doriți ca Wino să trimită un e-mail de resetare a parolei către {0}?", + "WinoAccount_SignOut_SuccessMessage": "Deconectat de la contul Wino {0}.", + "WinoAccount_SignOut_NoAccountMessage": "Nu există niciun cont Wino activ pentru a vă deconecta.", + "WinoAccount_Titlebar_SignedOutTitle": "Contul Wino", + "WinoAccount_Titlebar_SignedOutDescription": "Conectați-vă sau creați un Cont Wino pentru a gestiona sesiunea Wino.", + "WinoAccount_Titlebar_SignedInStatus": "Stare: {0}", + "WelcomeWizard_Step2Title": "Adăugați cont", + "WelcomeWizard_Step3Title": "Finalizați configurarea", + "ProviderSelection_Title": "Alegeți furnizorul de e-mail", + "ProviderSelection_Subtitle": "Selectați un furnizor mai jos pentru a adăuga contul dvs. de e-mail în Wino Mail.", + "ProviderSelection_AccountNameHeader": "Nume cont", + "ProviderSelection_AccountNamePlaceholder": "ex. Personal, Birou", + "ProviderSelection_DisplayNameHeader": "Nume afișat", + "ProviderSelection_DisplayNamePlaceholder": "ex. John Doe", + "ProviderSelection_EmailHeader": "Adresă de e-mail", + "ProviderSelection_EmailPlaceholder": "ex. johndoe@example.com", + "ProviderSelection_AppPasswordHeader": "Parolă specifică aplicației", + "ProviderSelection_AppPasswordHelp": "Cum pot obține o parolă specifică aplicației?", + "ProviderSelection_CalendarModeHeader": "Integrare calendar", + "ProviderSelection_CalendarMode_DisabledTitle": "Dezactivat", + "ProviderSelection_CalendarMode_DisabledDescription": "Nicio integrare a calendarului", + "ProviderSelection_CalendarMode_CalDavTitle": "Sincronizare CalDAV", + "ProviderSelection_CalendarMode_CalDavDescription_Apple": "Evenimentele din calendar sunt sincronizate cu serverele Apple între dispozitivele dvs.", + "ProviderSelection_CalendarMode_CalDavDescription_Yahoo": "Evenimentele din calendar sunt sincronizate cu serverele Yahoo între dispozitivele dvs.", + "ProviderSelection_CalendarMode_LocalTitle": "Calendar local", + "ProviderSelection_CalendarMode_LocalDescription": "Evenimentele dvs. sunt stocate doar pe calculatorul dvs. Fără conectivitate la server.", + "ProviderSelection_ClearColor": "Șterge culoarea", + "ProviderSelection_ContinueButton": "Continuă", + "ProviderSelection_SpecialImap_Subtitle": "Introduceți acreditările contului dvs. pentru a vă conecta.", + "AccountSetup_Title": "Configurarea contului", + "AccountSetup_Step_Authenticating": "Autentificare cu {0}", + "AccountSetup_Step_TestingMailAuth": "Se testează autentificarea e-mailului", + "AccountSetup_Step_SyncingFolders": "Sincronizarea metadatelor dosarelor", + "AccountSetup_Step_FetchingProfile": "Preluarea informațiilor despre profil", + "AccountSetup_Step_DiscoveringCalDav": "Descoperirea setărilor CalDAV", + "AccountSetup_Step_TestingCalendarAuth": "Testarea autentificării calendarului", + "AccountSetup_Step_SavingAccount": "Salvarea informațiilor despre cont", + "AccountSetup_Step_FetchingCalendarMetadata": "Preluarea metadatelor calendarului", + "AccountSetup_Step_SyncingAliases": "Sincronizarea aliasurilor", + "AccountSetup_Step_Finalizing": "Finalizarea configurării", + "AccountSetup_FailureMessage": "Configurarea a eșuat. Întoarceți-vă pentru a modifica setările sau încercați din nou mai târziu.", + "AccountSetup_SuccessMessage": "Contul dvs. a fost configurat cu succes!", + "AccountSetup_GoBackButton": "Înapoi", + "AccountSetup_TryAgainButton": "Încercați din nou", + "ImapCalDavSettings_AutoDiscoveryFailed": "Descoperirea automată a eșuat. Vă rugăm să introduceți manual setările în fila Avansată." } - - diff --git a/Wino.Core.Domain/Translations/ru_RU/resources.json b/Wino.Core.Domain/Translations/ru_RU/resources.json index 529b8fbe..a3f17863 100644 --- a/Wino.Core.Domain/Translations/ru_RU/resources.json +++ b/Wino.Core.Domain/Translations/ru_RU/resources.json @@ -8,6 +8,7 @@ "AccountCacheReset_Message": "This account requires full re-sychronization to continue working. Please wait while Wino re-synchronizes your messages...", "AccountContactNameYou": "You", "AccountCreationDialog_Completed": "все готово", + "AccountCreationDialog_FetchingCalendarMetadata": "Получение сведений о календаре.", "AccountCreationDialog_FetchingEvents": "Fetching calendar events.", "AccountCreationDialog_FetchingProfileInformation": "Fetching profile details.", "AccountCreationDialog_GoogleAuthHelpClipboardText_Row0": "If your browser did not launch automatically to complete authentication:", @@ -17,6 +18,7 @@ "AccountCreationDialog_Initializing": "инициализация", "AccountCreationDialog_PreparingFolders": "На данный момент мы получаем информацию о папках.", "AccountCreationDialog_SigninIn": "Данные учетной записи сохраняются.", + "Purchased": "Куплено", "AccountEditDialog_Message": "Имя пользователя", "AccountEditDialog_Title": "Редактировать учетную запись", "AccountPickerDialog_Title": "Выберите учетную запись", @@ -26,6 +28,10 @@ "AccountDetailsPage_Description": "Change the name of the account in Wino and set desired sender name.", "AccountDetailsPage_ColorPicker_Title": "Account color", "AccountDetailsPage_ColorPicker_Description": "Assign a new account color to colorize its symbol in the list.", + "AccountDetailsPage_TabGeneral": "Общие", + "AccountDetailsPage_TabMail": "Почта", + "AccountDetailsPage_TabCalendar": "Календарь", + "AccountDetailsPage_CalendarListDescription": "Выберите календарь, чтобы настроить его параметры.", "AddHyperlink": "Добавить", "AppCloseBackgroundSynchronizationWarningTitle": "Background Synchronization", "AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Application has not been set to launch on Windows startup.", @@ -47,8 +53,10 @@ "BasicIMAPSetupDialog_Title": "Учетная запись IMAP", "Busy": "Busy", "Buttons_AddAccount": "Добавить учетную запись", + "Buttons_FixAccount": "Исправить учетную запись", "Buttons_AddNewAlias": "Add New Alias", "Buttons_Allow": "Allow", + "Buttons_Apply": "Применить", "Buttons_ApplyTheme": "Применить тему", "Buttons_Browse": "Обзор", "Buttons_Cancel": "Отмена", @@ -62,6 +70,7 @@ "Buttons_Edit": "Редактировать", "Buttons_EnableImageRendering": "Включить", "Buttons_Multiselect": "Select Multiple", + "Buttons_Manage": "Управлять", "Buttons_No": "Нет", "Buttons_Open": "Открыть", "Buttons_Purchase": "Купить", @@ -70,15 +79,134 @@ "Buttons_Save": "Сохранить", "Buttons_SaveConfiguration": "Сохранить конфигурацию", "Buttons_Send": "Send", + "Buttons_SendToServer": "Отправить на сервер", "Buttons_Share": "Поделиться", "Buttons_SignIn": "Войти", "Buttons_Sync": "Synchronize", "Buttons_SyncAliases": "Synchronize Aliases", "Buttons_TryAgain": "Повторить", "Buttons_Yes": "Да", + "Sync_SynchronizingFolder": "Синхронизация {0} {1}%", + "Sync_DownloadedMessages": "Загружено {0} сообщений из {1}", + "SyncAction_Archiving": "Архивирование {0} писем", + "SyncAction_ClearingFlag": "Снятие флага с {0} писем", + "SyncAction_CreatingDraft": "Создание черновика", + "SyncAction_CreatingEvent": "Создание события", + "SyncAction_Deleting": "Удаление {0} писем", + "SyncAction_EmptyingFolder": "Очистка папки", + "SyncAction_MarkingAsRead": "Пометка {0} писем как прочитанных", + "SyncAction_MarkingAsUnread": "Пометка {0} писем как непрочитанных", + "SyncAction_MarkingFolderAsRead": "Пометка папки как прочитанной", + "SyncAction_Moving": "Перемещение {0} писем", + "SyncAction_MovingToFocused": "Перемещение {0} писем в Focused", + "SyncAction_RenamingFolder": "Переименование папки", + "SyncAction_SendingMail": "Отправка письма", + "SyncAction_SettingFlag": "Установка флага на {0} писем", + "SyncAction_SynchronizingAccount": "Синхронизация {0}", + "SyncAction_SynchronizingAccounts": "Синхронизация {0} учетной записи(ей)", + "SyncAction_SynchronizingCalendarData": "Синхронизация данных календаря", + "SyncAction_SynchronizingCalendarEvents": "Синхронизация событий календаря", + "SyncAction_SynchronizingCalendarMetadata": "Синхронизация метаданных календаря", + "SyncAction_Unarchiving": "Разархивирование {0} писем", "CalendarAllDayEventSummary": "all-day events", "CalendarDisplayOptions_Color": "Color", "CalendarDisplayOptions_Expand": "Expand", + "CalendarEventResponse_Accept": "Принять", + "CalendarEventResponse_AcceptedResponse": "Принято", + "CalendarEventResponse_Decline": "Отклонить", + "CalendarEventResponse_DeclinedResponse": "Отклонено", + "CalendarEventResponse_NotResponded": "Без ответа", + "CalendarEventResponse_Tentative": "Предварительный", + "CalendarEventResponse_TentativeResponse": "Предварительный", + "CalendarEventRsvpPanel_Accept": "Принять", + "CalendarEventRsvpPanel_AddMessage": "Добавить сообщение к вашему ответу... (необязательно)", + "CalendarEventRsvpPanel_Decline": "Отклонить", + "CalendarEventRsvpPanel_Message": "Сообщение", + "CalendarEventRsvpPanel_SendReplyMessage": "Отправить ответное сообщение", + "CalendarEventRsvpPanel_Tentative": "Предварительный", + "CalendarEventRsvpPanel_Title": "Варианты ответа", + "CalendarAttendeeStatus_Accepted": "Принято", + "CalendarAttendeeStatus_Declined": "Отклонено", + "CalendarAttendeeStatus_NeedsAction": "Требуется действие", + "CalendarAttendeeStatus_Tentative": "Предварительный", + "CalendarEventDetails_Attachments": "Вложения", + "CalendarEventCompose_AddAttachment": "Добавить вложение", + "CalendarEventCompose_AllDay": "Весь день", + "CalendarEventCompose_AttachmentsNotSupportedForCalDav": "Вложения не поддерживаются для календарей CalDAV.", + "CalendarEventCompose_EndDate": "Дата окончания", + "CalendarEventCompose_EndTime": "Время окончания", + "CalendarEventCompose_Every": "каждый", + "CalendarEventCompose_ForWeekdays": "для", + "CalendarEventCompose_FrequencyDay": "день", + "CalendarEventCompose_FrequencyDayPlural": "дней", + "CalendarEventCompose_FrequencyMonth": "месяц", + "CalendarEventCompose_FrequencyMonthPlural": "месяцев", + "CalendarEventCompose_FrequencyWeek": "неделя", + "CalendarEventCompose_FrequencyWeekPlural": "недель", + "CalendarEventCompose_FrequencyYear": "год", + "CalendarEventCompose_FrequencyYearPlural": "лет", + "CalendarEventCompose_Location": "Местоположение", + "CalendarEventCompose_LocationPlaceholder": "Добавить место", + "CalendarEventCompose_NewEventButton": "Новое событие", + "CalendarEventCompose_DefaultCalendarHint": "Вы можете выбрать календарь по умолчанию для новых событий в настройках календаря.", + "CalendarEventCompose_DefaultCalendarSettingsLink": "Открыть настройки календаря", + "CalendarEventCompose_NoCalendarsMessage": "Пока нет доступных календарей для создания события.", + "CalendarEventCompose_NoCalendarsTitle": "Нет доступных календарей", + "CalendarEventCompose_NoEndDate": "Нет даты окончания", + "CalendarEventCompose_Notes": "Заметки", + "CalendarEventCompose_PickCalendarTitle": "Выберите календарь", + "CalendarEventCompose_Recurring": "Повторяющееся", + "CalendarEventCompose_RecurringSummary": "Происходит каждые {0} {1}{2} {3}, действует {4}{5}", + "CalendarEventCompose_RecurringSummarySmart": "Происходит {0}{1} {2} эффективно {3}{4}", + "CalendarEventCompose_RepeatEvery": "Повторять каждые", + "CalendarEventCompose_SelectCalendar": "Выберите календарь", + "CalendarEventCompose_SingleOccurrenceSummary": "Происходит в {0} {1}", + "CalendarEventCompose_StartDate": "Дата начала", + "CalendarEventCompose_StartTime": "Время начала", + "CalendarEventCompose_TimeRangeSummary": "с {0} по {1}", + "CalendarEventCompose_Title": "Заголовок события", + "CalendarEventCompose_TitlePlaceholder": "Добавьте заголовок", + "CalendarEventCompose_Until": "до", + "CalendarEventCompose_UntilSummary": " до {0}", + "CalendarEventCompose_ValidationInvalidAllDayRange": "Дата окончания события на весь день должна быть позже даты начала.", + "CalendarEventCompose_ValidationInvalidAttendee": "У одного или нескольких участников неверный адрес электронной почты.", + "CalendarEventCompose_ValidationInvalidRecurrenceEnd": "Дата окончания повторения должна быть равна или позже даты начала события.", + "CalendarEventCompose_ValidationInvalidTimeRange": "Время окончания должно быть позже времени начала.", + "CalendarEventCompose_ValidationMissingAttachment": "Одно или несколько вложений больше не доступны: {0}", + "CalendarEventCompose_ValidationMissingCalendar": "Выберите календарь перед созданием события.", + "CalendarEventCompose_ValidationMissingTitle": "Введите заголовок события перед созданием события.", + "CalendarEventCompose_ValidationTitle": "Проверка заголовка события не выполнена.", + "CalendarEventCompose_WeekdaySummary": " на {0}", + "CalendarEventCompose_Weekday_Friday": "Пт", + "CalendarEventCompose_Weekday_Monday": "Пн", + "CalendarEventCompose_Weekday_Saturday": "Сб", + "CalendarEventCompose_Weekday_Sunday": "Вс", + "CalendarEventCompose_Weekday_Thursday": "Чт", + "CalendarEventCompose_Weekday_Tuesday": "Вт", + "CalendarEventCompose_Weekday_Wednesday": "Ср", + "CalendarEventDetails_Details": "Детали", + "CalendarEventDetails_EditSeries": "Редактировать серию", + "CalendarEventDetails_Editing": "Редактирование", + "CalendarEventDetails_InviteSomeone": "Пригласить кого-нибудь", + "CalendarEventDetails_JoinOnline": "Присоединиться онлайн", + "CalendarEventDetails_Organizer": "Организатор", + "CalendarEventDetails_People": "Люди", + "CalendarEventDetails_ReadOnlyEvent": "Событие только для чтения", + "CalendarEventDetails_Reminder": "Напоминание", + "CalendarReminder_StartedHoursAgo": "Началось {0} часов назад", + "CalendarReminder_StartedMinutesAgo": "Началось {0} минут назад", + "CalendarReminder_StartedNow": "Началось прямо сейчас", + "CalendarReminder_StartingNow": "Начинается сейчас", + "CalendarReminder_StartsInHours": "Начинается через {0} часов", + "CalendarReminder_StartsInMinutes": "Начинается через {0} минут", + "CalendarReminder_SnoozeAction": "Отложить", + "CalendarReminder_SnoozeMinutesOption": "{0} минут", + "CalendarEventDetails_ShowAs": "Показывать как", + "CalendarShowAs_Free": "Свободно", + "CalendarShowAs_Tentative": "Возможно", + "CalendarShowAs_Busy": "Занято", + "CalendarShowAs_OutOfOffice": "Вне офиса", + "CalendarShowAs_WorkingElsewhere": "Работает в другом месте", "CalendarItem_DetailsPopup_JoinOnline": "Join online", "CalendarItem_DetailsPopup_ViewEventButton": "View event", "CalendarItem_DetailsPopup_ViewSeriesButton": "View series", @@ -88,6 +216,9 @@ "ClipboardTextCopied_Message": "{0} скопировано в буфер обмена.", "ClipboardTextCopied_Title": "Скопировано", "ClipboardTextCopyFailed_Message": "Не удалось скопировать {0} в буфер обмена.", + "ContactInfoBar_ErrorTitle": "Не удалось загрузить контактную информацию", + "ContactInfoBar_SuccessTitle": "Контактная информация загружена", + "ContactInfoBar_WarningTitle": "Контактная информация может быть неполной", "ComingSoon": "Скоро...", "ComposerAttachmentsDragDropAttach_Message": "Вложить", "ComposerAttachmentsDropZone_Message": "Вы можете просто перетащить файл сюда", @@ -129,6 +260,10 @@ "DialogMessage_CreateLinkedAccountTitle": "Название связки учетных записей", "DialogMessage_DeleteAccountConfirmationMessage": "Удалить {0}?", "DialogMessage_DeleteAccountConfirmationTitle": "Все данные, связанные с этой учетной записью, будут окончательно удалены с диска.", + "DialogMessage_DeleteEmailTemplateConfirmationMessage": "Удалить шаблон \"{0}\"?", + "DialogMessage_DeleteEmailTemplateConfirmationTitle": "Удалить шаблон электронной почты", + "DialogMessage_DeleteRecurringSeriesMessage": "Это удалит все события в серии. Продолжить?", + "DialogMessage_DeleteRecurringSeriesTitle": "Удалить повторяющуюся серию", "DialogMessage_DiscardDraftConfirmationMessage": "Этот черновик будет удален. Вы хотите продолжить?", "DialogMessage_DiscardDraftConfirmationTitle": "Удалить черновик", "DialogMessage_EmptySubjectConfirmation": "Missing Subject", @@ -172,11 +307,18 @@ "ElementTheme_Light": "Светлая тема", "Emoji": "Эмодзи", "Error_FailedToSetupSystemFolders_Title": "Не удалось настроить системные папки", + "Exception_AccountNeedsAttention_Title": "Аккаунту требуется внимание", + "Exception_AccountNeedsAttention_Message": "'{0}' требует вашего внимания, чтобы продолжить работу.", + "Exception_WebView2RuntimeMissing_Message": "Wino Mail не удалось найти рантайм Microsoft Edge WebView2. Пожалуйста, установите или исправьте рантайм, чтобы корректно отображать содержимое сообщения.", + "Exception_WebView2RuntimeMissing_Title": "Требуется рантайм WebView2", "Exception_AuthenticationCanceled": "Аутентификация отменена", "Exception_CustomThemeExists": "Такая тема уже существует.", "Exception_CustomThemeMissingName": "Необходимо указать название.", "Exception_CustomThemeMissingWallpaper": "Необходимо предоставить пользовательское фоновое изображение.", "Exception_FailedToSynchronizeAliases": "Failed to synchronize aliases", + "Exception_FailedToSynchronizeCalendarData": "Не удалось синхронизировать данные календаря", + "Exception_FailedToSynchronizeCalendarEvents": "Не удалось синхронизировать события календаря", + "Exception_FailedToSynchronizeCalendarMetadata": "Не удалось синхронизировать детали календаря", "Exception_FailedToSynchronizeFolders": "Не удалось синхронизировать папки", "Exception_FailedToSynchronizeProfileInformation": "Failed to synchronize profile information", "Exception_GoogleAuthCallbackNull": "Callback uri пустой при активации.", @@ -229,6 +371,32 @@ "HoverActionOption_MoveJunk": "Переместить в мусор", "HoverActionOption_ToggleFlag": "Флаг / Убрать флаг", "HoverActionOption_ToggleRead": "Прочитанное / Непрочитанное", + "KeyboardShortcuts_FailedToReset": "Не удалось сбросить сочетания клавиш.", + "KeyboardShortcuts_FailedToUpdate": "Не удалось обновить сочетания клавиш", + "KeyboardShortcuts_MailoperationAction": "Действие", + "KeyboardShortcuts_Action": "Действие", + "KeyboardShortcuts_FailedToLoad": "Не удалось загрузить сочетания клавиш.", + "KeyboardShortcuts_EnterKeyForShortcut": "Пожалуйста, введите клавишу для сочетания.", + "KeyboardShortcuts_SelectOperationForShortcut": "Пожалуйста, выберите действие для сочетания.", + "KeyboardShortcuts_EnterKey": "Пожалуйста, введите клавишу для сочетания.", + "KeyboardShortcuts_SelectOperation": "Пожалуйста, выберите действие для сочетания.", + "KeyboardShortcuts_ShortcutInUse": "Это сочетание клавиш уже используется другим сочетанием.", + "KeyboardShortcuts_FailedToSave": "Не удалось сохранить сочетание клавиш.", + "KeyboardShortcuts_FailedToDelete": "Не удалось удалить сочетание клавиш.", + "KeyboardShortcuts_PageDescription": "Настройка сочетаний клавиш для быстрого выполнения операций с почтой. Нажимайте клавиши, когда фокус в поле ввода клавиши, чтобы зафиксировать сочетания.", + "KeyboardShortcuts_Add": "Добавить сочетание клавиш", + "KeyboardShortcuts_EditTitle": "Редактировать сочетание клавиш", + "KeyboardShortcuts_ResetToDefaults": "Сбросить до значений по умолчанию", + "KeyboardShortcuts_PressKeysHere": "Нажмите здесь клавиши...", + "KeyboardShortcuts_KeyCombination": "Комбинация клавиш", + "KeyboardShortcuts_FocusArea": "Переключите фокус на поле выше и нажмите требуемую комбинацию клавиш", + "KeyboardShortcuts_Modifiers": "Клавиши-модификаторы", + "KeyboardShortcuts_Mode": "Режим приложения", + "KeyboardShortcuts_ModeMail": "Почта", + "KeyboardShortcuts_ModeCalendar": "Календарь", + "KeyboardShortcuts_ActionToggleReadUnread": "Переключать прочитано/не прочитано", + "KeyboardShortcuts_ActionToggleFlag": "Переключить флаг", + "KeyboardShortcuts_ActionToggleArchive": "Архивировать/Разархивировать", "ImageRenderingDisabled": "Отображение изображений отключено для этого сообщения.", "ImapAdvancedSetupDialog_AuthenticationMethod": "Метод авторизации", "ImapAdvancedSetupDialog_ConnectionSecurity": "Безопасность соединения", @@ -295,12 +463,58 @@ "IMAPSetupDialog_Username": "Имя пользователя", "IMAPSetupDialog_UsernamePlaceholder": "иваниванов, ivanivanov@fabrikam.com, домен/иваниванов", "IMAPSetupDialog_UseSameConfig": "Используйте то же имя пользователя и пароль для отправки электронной почты", + "ImapCalDavSettingsPage_TitleCreate": "Настройка IMAP и календаря", + "ImapCalDavSettingsPage_TitleEdit": "Редактировать настройки IMAP и календаря", + "ImapCalDavSettingsPage_Subtitle": "Настроить IMAP/SMTP и необязательную синхронизацию календаря для этой учетной записи.", + "ImapCalDavSettingsPage_BasicSectionTitle": "Базовая настройка", + "ImapCalDavSettingsPage_BasicSectionDescription": "Введите свои данные и учетные данные. Wino может попробовать автоматически определить параметры сервера.", + "ImapCalDavSettingsPage_BasicTab": "Основное", + "ImapCalDavSettingsPage_EnableCalendarSupport": "Включить поддержку календаря", + "ImapCalDavSettingsPage_AutoDiscoverButton": "Автонастройка параметров почты", + "ImapCalDavSettingsPage_AutoDiscoverySuccessMessage": "Параметры почты найдены и применены.", + "ImapCalDavSettingsPage_AdvancedSectionTitle": "Расширенная конфигурация", + "ImapCalDavSettingsPage_AdvancedSectionDescription": "Введите параметры сервера вручную, если автообнаружение недоступно или неверно.", + "ImapCalDavSettingsPage_AdvancedTab": "Расширенные", + "ImapCalDavSettingsPage_CalendarSectionTitle": "Настройка календаря", + "ImapCalDavSettingsPage_CalendarSectionDescription": "Выберите, как данные календаря должны работать для этой учетной записи IMAP.", + "ImapCalDavSettingsPage_CalendarModeHeader": "Режим календаря", + "ImapCalDavSettingsPage_ConnectionSecurityHeader": "Безопасность соединения", + "ImapCalDavSettingsPage_AuthenticationMethodHeader": "Метод аутентификации", + "ImapCalDavSettingsPage_CalendarModeDisabled": "Отключено", + "ImapCalDavSettingsPage_CalendarModeCalDav": "Синхронизация CalDAV", + "ImapCalDavSettingsPage_CalendarModeLocalOnly": "Только локальный календарь", + "ImapCalDavSettingsPage_CalendarModeDisabledDescription": "Календарь отключен для этой учетной записи.", + "ImapCalDavSettingsPage_CalendarModeCalDavDescription": "Элементы календаря синхронизируются с вашим сервером CalDAV.", + "ImapCalDavSettingsPage_CalendarModeLocalOnlyDescription": "Элементы календаря хранятся только на этом компьютере и не синхронизируются с сетью.", + "ImapCalDavSettingsPage_LocalCalendarLearnMore": "Как работает локальный календарь", + "ImapCalDavSettingsPage_LocalCalendarDialogTitle": "Только локальный календарь", + "ImapCalDavSettingsPage_LocalCalendarDialogMessage": "Локальный календарь хранит все события только на вашем компьютере. Ничего не синхронизируется с iCloud, Yahoo или любым другим провайдером.", + "ImapCalDavSettingsPage_CalDavServiceUrl": "URL сервиса CalDAV", + "ImapCalDavSettingsPage_CalDavUsername": "Имя пользователя CalDAV", + "ImapCalDavSettingsPage_CalDavPassword": "Пароль CalDAV", + "ImapCalDavSettingsPage_CalDavNotRequiredMessage": "Проверка CalDAV требуется только если режим календаря установлен на синхронизацию CalDAV.", + "ImapCalDavSettingsPage_CalDavUrlRequired": "URL сервиса CalDAV обязателен.", + "ImapCalDavSettingsPage_CalDavUrlInvalid": "URL сервиса CalDAV должен быть абсолютным.", + "ImapCalDavSettingsPage_CalDavUsernameRequired": "Имя пользователя CalDAV обязательно.", + "ImapCalDavSettingsPage_CalDavPasswordRequired": "Пароль CalDAV обязателен.", + "ImapCalDavSettingsPage_TestImapButton": "Проверить соединение IMAP", + "ImapCalDavSettingsPage_TestCalDavButton": "Проверить соединение CalDAV", + "ImapCalDavSettingsPage_ImapTestSuccessMessage": "Проверка соединения IMAP выполнена успешно.", + "ImapCalDavSettingsPage_CalDavTestSuccessMessage": "Проверка соединения CalDAV выполнена успешно.", + "ImapCalDavSettingsPage_SaveSuccessMessage": "Настройки учетной записи проверены и сохранены.", + "ImapCalDavSettingsPage_ICloudHint": "Используйте пароль для приложений, созданный в настройках вашей учетной записи Apple.", + "ImapCalDavSettingsPage_YahooHint": "Используйте пароль для приложений из настроек безопасности учетной записи Yahoo.", "Info_AccountCreatedMessage": "{0} создано", "Info_AccountCreatedTitle": "Создание учетной записи", "Info_AccountCreationFailedTitle": "Не удалось создать учетную запись", "Info_AccountDeletedMessage": "{0} успешно удалён.", "Info_AccountDeletedTitle": "Учетная запись удалена", "Info_AccountIssueFixFailedTitle": "Ошибка", + "Info_AccountIssueFixImapMessage": "Откройте страницу настроек IMAP и календаря и повторно введите данные сервера.", + "Info_AccountAttentionRequiredMessage": "Эта учетная запись требует внимания.", + "Info_AccountAttentionRequiredClickableMessage": "Нажмите, чтобы исправить эту учетную запись и повторно синхронизировать её.", + "Info_AccountAttentionRequiredAction": "Исправить", + "Info_AccountAttentionRequiredActionHint": "Нажмите «Исправить», чтобы устранить проблему этой учетной записи.", "Info_AccountIssueFixSuccessMessage": "Исправлены все проблемы с учетной записью.", "Info_AccountIssueFixSuccessTitle": "Успешно", "Info_AttachmentOpenFailedMessage": "Не удается открыть это вложение.", @@ -370,6 +584,7 @@ "InfoBarMessage_SynchronizationDisabledFolder": "Эта папка отключена для синхронизации.", "InfoBarTitle_SynchronizationDisabledFolder": "Папка отключена", "Justify": "Выравнять", + "MenuUpdateAvailable": "Обновление доступно.", "Left": "Слева", "Link": "Связать", "LinkedAccountsCreatePolicyMessage": "Для создания связки у вас должно быть не менее 2 учетных записей\nсвязка будет удалена при сохранении", @@ -403,6 +618,7 @@ "MailOperation_Unarchive": "Вернуть из архива", "MailOperation_ViewMessageSource": "View message source", "MailOperation_Zoom": "Масштаб", + "MailsDragging": "Перетаскивание {0} элемента(ов)", "MailsSelected": "Выбрано бесед: {0}", "MarkFlagUnflag": "Пометить как помеченное/неотмеченное", "MarkReadUnread": "Пометить как прочитанное/непрочитанное", @@ -434,6 +650,8 @@ "Notifications_MultipleNotificationsTitle": "New Mail", "Notifications_WinoUpdatedMessage": "Ознакомьтесь с новой версией {0}", "Notifications_WinoUpdatedTitle": "Почта Wino обновлена.", + "Notifications_StoreUpdateAvailableTitle": "Обновление доступно.", + "Notifications_StoreUpdateAvailableMessage": "Новая версия Wino Mail готова к установке в Microsoft Store.", "OnlineSearchFailed_Message": "Failed to perform search\n{0}\n\nListing offline mails.", "OnlineSearchTry_Line1": "Can't find what you are looking for?", "OnlineSearchTry_Line2": "Try online search.", @@ -446,7 +664,6 @@ "PaneLengthOption_Small": "Маленькая", "Photos": "Фотографии", "PreparingFoldersMessage": "Подготовка папок", - "ProtocolLogAvailable_Message": "Протокольные журналы доступны для диагностики.", "ProviderDetail_Gmail_Description": "Учетная запись Google", "ProviderDetail_iCloud_Description": "Apple iCloud Account", "ProviderDetail_iCloud_Title": "iCloud", @@ -465,9 +682,14 @@ "SearchBarPlaceholder": "Поиск", "SearchingIn": "Поиск в", "SearchPivotName": "Результаты", + "Settings_KeyboardShortcuts_Title": "Горячие клавиши", + "Settings_KeyboardShortcuts_Description": "Настройте сочетания клавиш для быстрых действий над письмами.", "SettingConfigureSpecialFolders_Button": "Настроить", "SettingsEditAccountDetails_IMAPConfiguration_Title": "IMAP/SMTP Configuration", "SettingsEditAccountDetails_IMAPConfiguration_Description": "Change your incoming/outgoing server settings.", + "SettingsEditAccountDetails_ImapCalDavSettings_Title": "Настройки IMAP и CalDAV", + "SettingsEditAccountDetails_ImapCalDavSettings_Description": "Откройте страницу настроек IMAP, SMTP и CalDAV для этой учетной записи.", + "SettingsEditAccountDetails_ImapCalDavSettings_Action": "Открыть настройки", "SettingsAbout_Description": "Узнать больше о Wino.", "SettingsAbout_Title": "О программе", "SettingsAboutGithub_Description": "Перейти к трекеру задач репозитория GitHub.", @@ -490,6 +712,10 @@ "SettingsAppPreferences_SearchMode_Local": "Local", "SettingsAppPreferences_SearchMode_Online": "Online", "SettingsAppPreferences_SearchMode_Title": "Default search mode", + "SettingsAppPreferences_ApplicationMode_Title": "Режим приложения по умолчанию", + "SettingsAppPreferences_ApplicationMode_Description": "Выберите режим запуска Wino по умолчанию, если явный тип активации не задан.", + "SettingsAppPreferences_ApplicationMode_Mail": "Почта", + "SettingsAppPreferences_ApplicationMode_Calendar": "Календарь", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description": "Wino Mail продолжит работу в фоновом режиме. При поступлении новых сообщений вы будете уведомлены.", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title": "Run in the background", "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description": "Wino Mail will keep running on the system tray. Available to launch by clicking on an icon. You will be notified as new mails arrive.", @@ -506,12 +732,30 @@ "SettingsAppPreferences_StartupBehavior_FatalError": "Fatal error occurred while changing the startup mode for Wino Mail.", "SettingsAppPreferences_StartupBehavior_Title": "Start minimized on Windows startup", "SettingsAppPreferences_Title": "App Preferences", + "SettingsAppPreferences_HideWinoAccountButton_Title": "Скрыть кнопку аккаунта Wino в панели заголовка", + "SettingsAppPreferences_HideWinoAccountButton_Description": "Скрыть кнопку профиля в панели заголовка, которая открывает выпадающее меню учетной записи Wino.", + "SettingsAppPreferences_StoreUpdateNotifications_Title": "Уведомления об обновлениях магазина", + "SettingsAppPreferences_StoreUpdateNotifications_Description": "Показывать уведомления и действия в нижнем колонтитуле, когда доступно обновление Microsoft Store.", + "SettingsAppPreferences_AiActions_Title": "Действия ИИ", + "SettingsAppPreferences_AiActions_Description": "Выберите языки ИИ по умолчанию и место сохранения сводок.", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Title": "Язык перевода по умолчанию", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Description": "Выберите языки перевода по умолчанию, которые будут использоваться в действиях перевода ИИ.", + "SettingsAppPreferences_AiSummarizeLanguage_Title": "Язык суммирования", + "SettingsAppPreferences_AiSummarizeLanguage_Description": "Выберите предпочитаемый язык суммирования для будущего вывода сводок ИИ.", + "SettingsAppPreferences_AiSummarySavePath_Title": "Путь сохранения сводок по умолчанию.", + "SettingsAppPreferences_AiSummarySavePath_Description": "Выберите папку, которую Wino будет использовать по умолчанию для сохранения сводок ИИ.", + "SettingsAppPreferences_AiSummarySavePath_Placeholder": "Использовать место сохранения по умолчанию в системе", + "SettingsAppPreferences_AiSummarySavePath_InvalidHint": "Эта папка не существует. По умолчанию будет использоваться место сохранения для сводок.", "SettingsAutoSelectNextItem_Description": "Выбирать следующий элемент после удаления или перемещения письма.", "SettingsAutoSelectNextItem_Title": "Автовыбор следующего элемента", "SettingsAvailableThemes_Description": "Выберите тему из коллекции Wino на свой вкус или используйте свои собственные темы.", "SettingsAvailableThemes_Title": "Доступные темы", "SettingsCalendarSettings_Description": "Change first day of week, hour cell height and more...", "SettingsCalendarSettings_Title": "Calendar Settings", + "CalendarSettings_DefaultSnoozeDuration_Header": "Продолжительность повторного откладывания по умолчанию.", + "CalendarSettings_DefaultSnoozeDuration_Description": "Установите продолжительность повторного отложенного напоминания по умолчанию.", + "CalendarSettings_TimedDayHeaderFormat_Header": "Формат заголовка дня во временном виде.", + "CalendarSettings_TimedDayHeaderFormat_Description": "Выберите, как будут отображаться верхние ярлыки дней в режимах дневного, недельного и рабочей недели. Используйте токены формата даты, такие как ddd, dd, MMM или dddd.", "SettingsComposer_Title": "Composer", "SettingsComposerFont_Title": "Шрифт редактора по умолчанию", "SettingsComposerFontFamily_Description": "Измените семейство и размер шрифта по умолчанию при написании писем.", @@ -531,6 +775,9 @@ "SettingsDiscord_Title": "Канал Discord", "SettingsEditLinkedInbox_Description": "Добавляйте/удаляйте учетные записи, переименовывайте или разрывайте связь между ними.", "SettingsEditLinkedInbox_Title": "Редактировать связанную папку \"Входящие\"", + "SettingsWindowBackdrop_Title": "Фон окна", + "SettingsWindowBackdrop_Description": "Выберите эффект фона для окон Wino.", + "SettingsWindowBackdrop_Disabled": "Выбор фона окна отключён, если тема приложения выбрана не по умолчанию.", "SettingsElementTheme_Description": "Выберите тему Windows для Wino", "SettingsElementTheme_Title": "Режим темы", "SettingsElementThemeSelectionDisabled": "Выбор режима темы не работает, если выбрана тема приложения, отличная от темы \"По умолчанию\".", @@ -581,6 +828,8 @@ "SettingsManageAliases_Title": "Aliases", "SettingsEditAccountDetails_Title": "Edit Account Details", "SettingsEditAccountDetails_Description": "Change account name, sender name and assign a new color if you like.", + "EditAccountDetailsPage_SaveSuccess_Title": "Изменения сохранены.", + "EditAccountDetailsPage_SaveSuccess_Message": "Данные вашей учетной записи успешно обновлены.", "SettingsManageLink_Description": "Перемещайте элементы, чтобы добавить новую или удалить существующую привязку.", "SettingsManageLink_Title": "Управление привязкой", "SettingsMarkAsRead_Description": "Выберите, что должно произойти с выбранным элементом.", @@ -596,7 +845,41 @@ "SettingsNotifications_Title": "Уведомления", "SettingsNotificationsAndTaskbar_Description": "Change whether notifications should be displayed and taskbar badge for this account.", "SettingsNotificationsAndTaskbar_Title": "Notifications & Taskbar", + "SettingsHome_Title": "Главная", + "SettingsHome_SearchTitle": "Найти настройку", + "SettingsHome_SearchDescription": "Поиск по функции, теме или ключевому слову для быстрого перехода к нужной странице настроек.", + "SettingsHome_SearchPlaceholder": "Поиск настроек", + "SettingsHome_SearchExamples": "Попробуйте: тема, хранилище, язык, подпись", + "SettingsHome_QuickLinks_Title": "Быстрые ссылки", + "SettingsHome_QuickLinks_Description": "Перейдите к настройкам, к которым чаще всего обращаются пользователи.", + "SettingsHome_StorageCard_Description": "Посмотрите, сколько локального MIME-контента хранит Wino на этом устройстве, и при необходимости очистите его.", + "SettingsHome_StorageEmptySummary": "Кэшированного MIME-контента пока не обнаружено.", + "SettingsHome_StorageLoading": "Проверяем использование локального MIME...", + "SettingsHome_Tips_Title": "Советы и подсказки", + "SettingsHome_Tips_Description": "Небольшие изменения могут сделать использование Wino намного более персональным.", + "SettingsHome_Tip_Theme": "Хотите темный режим или изменения акцентов? Откройте Персонализация.", + "SettingsHome_Tip_Background": "Используйте настройки приложения для управления запуском и фоновой синхронизацией.", + "SettingsHome_Tip_Shortcuts": "Горячие клавиши помогут быстрее работать с почтой.", + "SettingsHome_Resources_Title": "Полезные ссылки", + "SettingsHome_Resources_Description": "Откройте ресурсы проекта, справку и каналы выпуска.", "SettingsOptions_Title": "Параметры", + "SettingsOptions_GeneralSection": "Общие", + "SettingsOptions_MailSection": "Почта", + "SettingsOptions_CalendarSection": "Календарь", + "SettingsOptions_MoreComingSoon": "Скоро будут доступны дополнительные параметры.", + "SettingsOptions_HeroDescription": "Настройте свой опыт использования Wino Mail.", + "SettingsOptions_AccountsSummary": "{0} учетная запись(и) настроена(ы).", + "SettingsSearch_ManageAccounts_Keywords": "учётная запись;учётные записи;почтовый ящик;почтовые ящики;псевдоним;псевдонимы;профиль;адрес;адреса", + "SettingsSearch_AppPreferences_Keywords": "запуск;фон;запуск;синхронизация;уведомления;уведомления;поиск;системный трей;значения по умолчанию", + "SettingsSearch_LanguageTime_Keywords": "язык;время;часы;локаль;регион;формат;24 часа;24ч", + "SettingsSearch_Personalization_Keywords": "тема;темный;светлый;внешний вид;акцент;цвет;цвет;режим;раскладка;плотность", + "SettingsSearch_About_Keywords": "о;версия;веб-сайт;конфиденциальность;github;пожертвовать;магазин;поддержка", + "SettingsSearch_KeyboardShortcuts_Keywords": "ярлык;ярлыки;горячий ярлык;горячие клавиши;клавиатура;клавиши", + "SettingsSearch_MessageList_Keywords": "сообщение;сообщения;список;обсуждения;ветви;аватар;предпросмотр;отправитель", + "SettingsSearch_ReadComposePane_Keywords": "просмотр;составление;составитель;шрифт;шрифты;внешний контент;отображение;чтение", + "SettingsSearch_SignatureAndEncryption_Keywords": "подпись;подписи;шифрование;сертификат;сертификаты;S/MIME;S/MIME;безопасность", + "SettingsSearch_Storage_Keywords": "хранение;кэш;кэширование;MIME;диск;место на диске;очистка;очистить;локальные данные", + "SettingsSearch_CalendarSettings_Keywords": "календарь;неделя;часы;расписание;событие;события", "SettingsPaneLengthReset_Description": "Reset the size of the mail list to original if you have issues with it.", "SettingsPaneLengthReset_Title": "Reset Mail List Size", "SettingsPaypal_Description": "Проявите больше любви ❤️ Все пожертвования высоко ценятся.", @@ -610,6 +893,8 @@ "SettingsPrefer24HourClock_Title": "24-часовой формат отображения времени", "SettingsPrivacyPolicy_Description": "Ознакомьтесь с политикой конфиденциальности.", "SettingsPrivacyPolicy_Title": "Политика конфиденциальности", + "SettingsWebsite_Description": "Откройте сайт Wino Mail.", + "SettingsWebsite_Title": "Сайт", "SettingsReadComposePane_Description": "Fonts, external content.", "SettingsReadComposePane_Title": "Reader & Composer", "SettingsReader_Title": "Reader", @@ -625,6 +910,19 @@ "SettingsShowPreviewText_Title": "Показать текст предпросмотра", "SettingsShowSenderPictures_Description": "Скрыть/показать миниатюру изображения отправителя.", "SettingsShowSenderPictures_Title": "Показывать аватары отправителя", + "SettingsEmailTemplates_Title": "Шаблоны электронной почты", + "SettingsEmailTemplates_Description": "Управление шаблонами электронной почты.", + "SettingsEmailTemplates_CreatePageTitle": "Новый шаблон", + "SettingsEmailTemplates_EditPageTitle": "Редактировать шаблон", + "SettingsEmailTemplates_NewTemplateTitle": "Новый шаблон", + "SettingsEmailTemplates_NewTemplateDescription": "Создайте новый шаблон электронной почты", + "SettingsEmailTemplates_NameTitle": "Название", + "SettingsEmailTemplates_NamePlaceholder": "Название шаблона", + "SettingsEmailTemplates_DescriptionTitle": "Описание", + "SettingsEmailTemplates_DescriptionPlaceholder": "Дополнительное описание (необязательно).", + "SettingsEmailTemplates_ContentTitle": "Содержимое шаблона", + "SettingsEmailTemplates_ContentDescription": "Измените HTML-содержимое этого шаблона.", + "SettingsEmailTemplates_NameRequired": "Требуется имя шаблона.", "SettingsEnableGravatarAvatars_Title": "Gravatar", "SettingsEnableGravatarAvatars_Description": "Use gravatar (if available) as sender picture", "SettingsEnableFavicons_Title": "Domain icons (Favicons)", @@ -645,6 +943,33 @@ "SettingsStartupItem_Title": "Элемент при запуске", "SettingsStore_Description": "Проявите любовь ❤️", "SettingsStore_Title": "Оценить в магазине приложений", + "SettingsStorage_Title": "Хранение", + "SettingsStorage_Description": "Сканировать и управлять кэшом MIME, сохраненным в вашей локальной папке данных.", + "SettingsStorage_ScanFolder": "Сканировать локальную папку данных", + "SettingsStorage_NoLocalMimeDataFound": "Локальные MIME-данные не найдены.", + "SettingsStorage_NoAccountsFound": "Учетные записи не найдены.", + "SettingsStorage_TotalUsage": "Общее использование локального MIME: {0}", + "SettingsStorage_AccountUsageDescription": "{0} используется в локальном кэше MIME.", + "SettingsStorage_DeleteAll_Title": "Удалить весь MIME-контент", + "SettingsStorage_DeleteAll_Description": "Удалить всю папку кэша MIME этой учетной записи.", + "SettingsStorage_DeleteAll_Button": "Удалить всё", + "SettingsStorage_DeleteAll_Confirm_Title": "Удалить весь MIME-контент", + "SettingsStorage_DeleteAll_Confirm_Message": "Удалить локальные MIME-данные для {0}?", + "SettingsStorage_DeleteAll_Success": "Все MIME-данные были удалены.", + "SettingsStorage_DeleteOld_Title": "Удалить старый MIME-контент", + "SettingsStorage_DeleteOld_Description": "Удалить MIME-файлы на основе даты создания письма в локальной базе данных.", + "SettingsStorage_DeleteOld_1Month": "> 1 месяц", + "SettingsStorage_DeleteOld_3Months": "> 3 месяца", + "SettingsStorage_DeleteOld_6Months": "> 6 месяцев", + "SettingsStorage_DeleteOld_1Year": "> 1 год", + "SettingsStorage_DeleteOld_Confirm_Title": "Удалить старый MIME-контент", + "SettingsStorage_DeleteOld_Confirm_Message": "Удалить локальные MIME-данные старше {0} для {1}?", + "SettingsStorage_DeleteOld_Success": "Удалено {0} папок MIME, старше {1}.", + "SettingsStorage_1Month": "1 месяц", + "SettingsStorage_3Months": "3 месяца", + "SettingsStorage_6Months": "6 месяцев", + "SettingsStorage_1Year": "1 год", + "SettingsStorage_Months": "{0} месяцев", "SettingsTaskbarBadge_Description": "Include unread mail count in taskbar icon.", "SettingsTaskbarBadge_Title": "Taskbar Badge", "SettingsThreads_Description": "Организуйте сообщения в беседы.", @@ -683,6 +1008,9 @@ "SystemFolderConfigDialogValidation_InboxSelected": "Вы не можете назначить папку \"Входящие\" другой системной папке.", "SystemFolderConfigSetupSuccess_Message": "Системные папки успешно настроены.", "SystemFolderConfigSetupSuccess_Title": "Настройка системных папок", + "SystemTrayMenu_ShowWino": "Открыть Wino Mail", + "SystemTrayMenu_ShowWinoCalendar": "Открыть Wino Calendar", + "SystemTrayMenu_ExitWino": "Выход", "TestingImapConnectionMessage": "Проверка соединения с сервером...", "TitleBarServerDisconnectedButton_Description": "Wino отключён от сети. Нажмите \"переподключиться\" для восстановления соединения.", "TitleBarServerDisconnectedButton_Title": "нет соединения", @@ -699,8 +1027,422 @@ "WinoUpgradeMessage": "Улучшить до неограниченного количества учетных записей", "WinoUpgradeRemainingAccountsMessage": "Использовано {0} из {1} бесплатных учетных записей.", "Yesterday": "Вчера", + "Smime_ImportCertificates_Success": "Сертификаты успешно импортированы.", + "Smime_ImportCertificates_Error": "Ошибка импорта сертификатов: {0}", + "Smime_RemoveCertificates_Confirm": "Вы действительно хотите удалить сертификаты {0}?", + "Smime_RemoveCertificates_Success": "Сертификаты удалены.", + "Smime_ExportCertificates_Success": "Сертификаты экспортированы.", + "Smime_ExportCertificates_Error": "Ошибка экспорта сертификатов.", + "Smime_CertificateDetails": "Субъект: {0}\nИздатель: {1}\nДействителен с: {2}\nДействителен по: {3}\nОтпечаток: {4}", + "Smime_CertificatePassword_Title": "Требуется пароль к сертификату", + "Smime_CertificatePassword_Placeholder": "Пароль к сертификату для {0} (необязательно)", + "Smime_Confirm_Title": "Подтвердить", + "Buttons_OK": "ОК", + "Buttons_Refresh": "Обновить", + "SettingsSignatureAndEncryption_Title": "Подпись и шифрование", + "SettingsSignatureAndEncryption_Description": "Управление сертификатами S/MIME для подписания и шифрования электронной почты.", + "SettingsSignatureAndEncryption_MyCertificatesHeader": "Мои сертификаты", + "SettingsSignatureAndEncryption_MyCertificatesDescription": "Личные сертификаты для подписания и шифрования", + "SettingsSignatureAndEncryption_RecipientCertificatesHeader": "Сертификаты получателей", + "SettingsSignatureAndEncryption_RecipientCertificatesDescription": "Сертификаты получателей для расшифрования", + "SettingsSignatureAndEncryption_NameColumn": "Имя", + "SettingsSignatureAndEncryption_ExpiresColumn": "Истекает", + "SettingsSignatureAndEncryption_ThumbprintColumn": "Отпечаток", + "Buttons_Remove": "Удалить", + "Buttons_Export": "Экспорт", + "Buttons_Import": "Импорт", + "SettingsSignatureAndEncryption_SigningCertificate": "Сертификат подписи S/MIME", + "SettingsSignatureAndEncryption_EncryptionCertificate": "Сертификат шифрования S/MIME", + "SettingsSignatureAndEncryption_SigningCertificatePlaceholder": "Нет", + "SmimeSignaturesInMessage": "Подписи в этом сообщении:", + "SmimeSignatureEntry": "• {0} {1} ({2}, срок действия: {3} - {4})", + "SmimeSigningCertificateInfoTitle": "Информация о сертификате подписи S/MIME", + "SmimeCertificateInfoTitle": "Информация о сертификате S/MIME", + "SmimeNoCertificateFileFound": "Файл сертификата не найден", + "SmimeSaveCertificate": "Сохранить сертификат...", + "SmimeCertificate": "Сертификат S/MIME", + "SmimeCertificateSavedTo": "Сертификат сохранен в {0}", + "SmimeSignedTooltip": "Это сообщение подписано сертификатом S/MIME. Нажмите для получения дополнительных сведений.", + "SmimeEncryptedTooltip": "Это сообщение зашифровано сертификатом S/MIME.", + "SmimeCertificateFileInfo": "Файл: {0}", + "Composer_LightTheme": "Светлая тема", + "Composer_DarkTheme": "Тёмная тема", + "Composer_Outdent": "Уменьшить отступ", + "Composer_Indent": "Увеличить отступ", + "Composer_BulletList": "Маркированный список", + "Composer_OrderedList": "Нумерованный список", + "Composer_Stroke": "Линия", + "Composer_Bold": "Жирный", + "Composer_Italic": "Курсив", + "Composer_Underline": "Подчеркивание", + "Composer_CcBcc": "Копия и скрытая копия", + "Composer_EnableSmimeSignature": "Включить/выключить подпись S/MIME", + "Composer_EnableSmimeEncryption": "Включить/выключить шифрование S/MIME", + "Composer_LocalDraftSyncInfo": "Этот черновик локальный. Wino не смог отправить его на ваш почтовый сервер. Нажмите, чтобы повторно отправить его на сервер.", + "Composer_CertificateExpires": "Истекает: ", + "Composer_SmimeSignature": "Подпись S/MIME", + "Composer_SmimeEncryption": "Шифрование S/MIME", + "Composer_EmailTemplatesPlaceholder": "Шаблоны электронной почты", + "Composer_AiSummarize": "Сформировать резюме с помощью ИИ", + "Composer_AiSummarizeDescription": "Извлечь ключевые моменты, задачи и принятые решения из этого письма.", + "Composer_AiTranslate": "Перевести с помощью ИИ", + "Composer_AiActions": "Действия ИИ", + "Composer_AiRewrite": "Переписать с помощью ИИ", + "AiActions_CheckingStatus": "Проверяем доступ к ИИ...", + "AiActions_SignedOutTitle": "Разблокировать пакет Wino AI", + "AiActions_SignedOutDescription": "Перевод, переработка и резюмирование писем с помощью искусственного интеллекта после входа в учетную запись Wino и активации дополнения AI Pack.", + "AiActions_NoPackTitle": "Требуется пакет AI", + "AiActions_NoPackDescription": "Вы вошли в систему, но пакет AI не активен. Приобретите его, чтобы использовать инструменты перевода, переработки и резюмирования Wino с помощью ИИ.", + "AiActions_UsageSummary": "{0} из {1} кредитов потрачено в этом месяце.", + "Composer_AiRewritePolite": "Сделать вежливым", + "Composer_AiRewritePoliteDescription": "Смягчает формулировку, сохраняя тот же смысл.", + "Composer_AiRewriteAngry": "Сделать агрессивным", + "Composer_AiRewriteAngryDescription": "Использует более резкий и конфронтационный тон.", + "Composer_AiRewriteHappy": "Сделайте это более радостным", + "Composer_AiRewriteHappyDescription": "Добавляет более радостный и воодушевляющий тон.", + "Composer_AiRewriteFormal": "Сделайте это формальным.", + "Composer_AiRewriteFormalDescription": "Придаёт сообщению более профессиональный и структурированный вид.", + "Composer_AiRewriteFriendly": "Сделайте это дружелюбным.", + "Composer_AiRewriteFriendlyDescription": "Придаёт сообщению более дружелюбный и доступный тон.", + "Composer_AiRewriteShorter": "Сделайте это короче.", + "Composer_AiRewriteShorterDescription": "Укорачивает текст и удаляет лишние детали.", + "Composer_AiRewriteClearer": "Сделайте это яснее.", + "Composer_AiRewriteClearerDescription": "Улучшает читаемость и делает сообщение более понятным.", + "Composer_AiRewriteCustom": "Пользовательский", + "Composer_AiRewriteCustomDescription": "Опишите, как вы хотите переписать текст.", + "Composer_AiRewriteCustomPlaceholder": "Опишите, как вы хотите переписать сообщение.", + "Composer_AiRewriteMode": "Переписать тон", + "Composer_AiRewriteApply": "Применить переформулировку.", + "Composer_AiTranslateDialogTitle": "Перевод с помощью ИИ", + "Composer_AiTranslateDialogDescription": "Введите целевой язык или код культуры, например en-US, tr-TR, de-DE или fr-FR.", + "Composer_AiTranslateApply": "Перевести", + "Composer_AiTranslateLanguage": "Целевой язык", + "Composer_AiTranslateCustomPlaceholder": "Введите код культуры", + "Composer_AiTranslateLanguageEnglish": "Английский (en-US)", + "Composer_AiTranslateLanguageTurkish": "Турецкий (tr-TR)", + "Composer_AiTranslateLanguageGerman": "Немецкий (de-DE)", + "Composer_AiTranslateLanguageFrench": "Французский (fr-FR)", + "Composer_AiTranslateLanguageSpanish": "Испанский (es-ES)", + "Composer_AiTranslateLanguageItalian": "Итальянский (it-IT)", + "Composer_AiTranslateLanguagePortugueseBrazil": "Португальский (Бразилия) (pt-BR)", + "Composer_AiTranslateLanguageDutch": "Нидерландский (nl-NL)", + "Composer_AiTranslateLanguagePolish": "Польский (pl-PL)", + "Composer_AiTranslateLanguageRussian": "Русский (ru-RU)", + "Composer_AiTranslateLanguageJapanese": "Японский (ja-JP)", + "Composer_AiTranslateLanguageKorean": "Корейский (ko-KR)", + "Composer_AiTranslateLanguageChineseSimplified": "Китайский упрощенный (zh-CN)", + "Composer_AiTranslateLanguageArabic": "Арабский (ar-SA)", + "Composer_AiTranslateLanguageHindi": "Хинди (hi-IN)", + "Composer_AiTranslateLanguageOther": "Другое...", + "Composer_AiBusyTitle": "ИИ уже работает", + "Composer_AiBusyMessage": "Пожалуйста, подождите завершения текущего действия ИИ.", + "Composer_AiSignInRequired": "Войдите в свою учетную запись Wino, чтобы использовать функции ИИ.", + "Composer_AiMissingHtml": "Пока нет содержимого сообщения для отправки в Wino AI.", + "Composer_AiQuotaUnavailable": "Результат ИИ применён.", + "Composer_AiAppliedMessage": "Результат ИИ применён к редактору письма. Используйте Отмену, чтобы вернуть исходное состояние.", + "Composer_AiSummarizeSuccessTitle": "Сводка ИИ применена.", + "Composer_AiTranslateSuccessTitle": "Перевод ИИ применён.", + "Composer_AiRewriteSuccessTitle": "Переписывание ИИ применено.", + "Composer_AiErrorTitle": "Ошибка выполнения действия ИИ.", + "Reader_AiAppliedMessage": "Результат ИИ теперь отображается для этого сообщения. Откройте сообщение заново, чтобы увидеть исходное содержимое.", "SettingsAppPreferences_EmailSyncInterval_Title": "Email sync interval", - "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail." + "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail.", + "ContactsPage_Title": "Контакты", + "ContactsPage_AddContact": "Добавить контакт", + "ContactsPage_EditContact": "Редактировать контакт", + "ContactsPage_DeleteContact": "Удалить контакт", + "ContactsPage_SearchPlaceholder": "Поиск контактов...", + "ContactsPage_NoContacts": "Контакты не найдены.", + "ContactsPage_ContactsCount": "{0} контактов", + "ContactsPage_SelectedContactsCount": "Выбрано {0}", + "ContactsPage_DeleteSelectedContacts": "Удалить выбранные", + "ContactEditDialog_Title": "Редактировать контакт", + "ContactEditDialog_PhotoSection": "Фото", + "ContactEditDialog_ChoosePhoto": "Выбрать фото", + "ContactEditDialog_RemovePhoto": "Удалить фото", + "ContactEditDialog_NameHeader": "Имя", + "ContactEditDialog_NamePlaceholder": "Имя контакта", + "ContactEditDialog_EmailHeader": "Адрес электронной почты", + "ContactEditDialog_EmailPlaceholder": "contact@example.com", + "ContactEditDialog_InfoSection": "Контактная информация", + "ContactEditDialog_RootContactInfo": "Это корневой контакт, связанный с вашими учетными записями, и его нельзя удалить.", + "ContactEditDialog_OverriddenContactInfo": "Этот контакт был вручную изменён и не будет обновляться во время синхронизации.", + "ContactsPage_Subtitle": "Управляйте своими контактами электронной почты и их информацией.", + "ContactStatus_Account": "Аккаунт", + "ContactStatus_Modified": "Изменён", + "ContactAction_Edit": "Редактировать контакт", + "ContactAction_ChangePhoto": "Изменить фото", + "ContactAction_Delete": "Удалить контакт", + "ContactAction_Add": "Добавить контакт", + "ContactSelection_Selected": "Выбрано", + "ContactSelection_SelectAll": "Выбрать все", + "ContactSelection_Clear": "Очистить", + "ContactsPage_EmptyState": "Нет контактов для отображения.", + "ContactsPage_AddFirstContact": "Добавьте ваш первый контакт.", + "ContactsPage_ContactsCountSuffix": "контактов", + "ContactsPane_NewContact": "Новый контакт", + "ContactsPane_DescriptionTitle": "Управляйте своими контактами", + "ContactsPane_DescriptionBody": "Создавайте контакты, переименовывайте их, обновляйте фотографии профиля и держите сохраненные данные в одном месте.", + "ContactEditDialog_AddTitle": "Добавить контакт", + "ContactInfoBar_ContactAdded": "Контакт успешно добавлен.", + "ContactInfoBar_ContactUpdated": "Контакт успешно обновлён.", + "ContactInfoBar_ContactsDeleted": "Контакты успешно удалены.", + "ContactInfoBar_ContactPhotoUpdated": "Фотография контакта успешно обновлена.", + "ContactInfoBar_FailedToLoadContacts": "Не удалось загрузить контакты: {0}", + "ContactInfoBar_FailedToAddContact": "Не удалось добавить контакт: {0}", + "ContactInfoBar_FailedToUpdateContact": "Не удалось обновить контакт: {0}", + "ContactInfoBar_FailedToDeleteContacts": "Не удалось удалить контакты: {0}", + "ContactInfoBar_FailedToUpdatePhoto": "Не удалось обновить фото: {0}", + "ContactInfoBar_CannotDeleteRoot": "Корневые контакты нельзя удалить.", + "ContactConfirmDialog_DeleteTitle": "Удалить контакт", + "ContactConfirmDialog_DeleteMessage": "Вы уверены, что хотите удалить контакт '{0}'?", + "ContactConfirmDialog_DeleteMultipleMessage": "Вы уверены, что хотите удалить {0} контакт(ов)?", + "ContactConfirmDialog_DeleteButton": "Удалить", + "CalendarAccountSettings_Title": "Настройки учетной записи календаря", + "CalendarAccountSettings_Description": "Управляйте настройками календаря для {0}", + "CalendarAccountSettings_AccountColor": "Цвет учетной записи", + "CalendarAccountSettings_AccountColorDescription": "Изменить цвет отображения для этой учетной записи календаря.", + "CalendarAccountSettings_SyncEnabled": "Включить синхронизацию", + "CalendarAccountSettings_SyncEnabledDescription": "Включить или отключить синхронизацию календаря для этой учетной записи", + "CalendarAccountSettings_DefaultShowAs": "Статус доступности по умолчанию", + "CalendarAccountSettings_DefaultShowAsDescription": "Статус доступности по умолчанию для новых событий, создаваемых этой учетной записью", + "CalendarAccountSettings_PrimaryCalendar": "Основной календарь", + "CalendarAccountSettings_PrimaryCalendarDescription": "Пометить этот календарь как основной календарь для учетной записи", + "CalendarSettings_NewEventBehavior_Header": "Поведение кнопки Новое событие", + "CalendarSettings_NewEventBehavior_Description": "Выберите, должна ли кнопка Новое событие запрашивать календарь каждый раз или всегда открывать конкретный календарь.", + "CalendarSettings_NewEventBehavior_AskEachTime": "Спрашивать каждый раз.", + "CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar": "Всегда использовать конкретный календарь.", + "CalendarSettings_Rendering_Title": "Отображение", + "CalendarSettings_Rendering_Description": "Настройте разметку календаря и поведение отображения.", + "CalendarSettings_Notifications_Title": "Уведомления", + "CalendarSettings_Notifications_Description": "Выберите поведение напоминаний и откладывания по умолчанию.", + "CalendarSettings_Preferences_Title": "Настройки", + "CalendarSettings_Preferences_Description": "Укажите поведение кнопки Новое событие.", + "WhatIsNew_GetStartedButton": "Начать", + "WhatIsNew_ContinueAnywayButton": "Продолжить все равно", + "WhatIsNew_PreparingForNewVersionButton": "Подготовка новой версии...", + "WhatIsNew_MigrationPreparing_Title": "Подготовка ваших данных", + "WhatIsNew_MigrationPreparing_Description": "Wino применяет миграции обновления. Пожалуйста, подождите, пока мы подготовим данные вашей учетной записи к этому выпуску.", + "WhatIsNew_MigrationFailedMessage": "Не удалось применить миграции. Код ошибки {0}. Вы всё ещё можете использовать приложение. Однако если вы столкнетесь с серьезными проблемами, переустановите приложение.", + "WhatIsNew_MigrationNotification_Title": "Wino Mail обновлено", + "WhatIsNew_MigrationNotification_Message": "Откройте приложение, чтобы завершить обновление и увидеть, что нового.", + "WelcomeWindow_Title": "Добро пожаловать в Wino Mail", + "WelcomeWindow_Subtitle": "Нативный опыт Mail и календаря на Windows.", + "WelcomeWindow_WhatsNewTitle": "Последние изменения", + "WelcomeWindow_FeaturesTitle": "Функции", + "WelcomeWindow_WhatsNewTab": "Что нового", + "WelcomeWindow_FeaturesTab": "Функции", + "WelcomeWindow_GetStartedButton": "Начать, добавив учетную запись", + "WelcomeWindow_GetStartedDescription": "Добавьте учетную запись Outlook, Gmail или IMAP, чтобы начать работу с Wino Mail.", + "WelcomeWindow_ImportFromWinoAccount": "Импорт из вашей учетной записи Wino", + "WelcomeWindow_ImportInProgress": "Импорт синхронизированных настроек и учетных записей...", + "WelcomeWindow_ImportNoAccountsFound": "В вашей учетной записи Wino не найдено синхронизированных учетных записей. Если настройки были доступны, они были восстановлены. Используйте Начать, чтобы добавить учетную запись вручную.", + "WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} синхронизированных учетных записей уже доступны на этом устройстве. При необходимости используйте Начать, чтобы вручную добавить еще одну учетную запись.", + "WelcomeWindow_SetupTitle": "Настройте свою учетную запись", + "WelcomeWindow_SetupSubtitle": "Выберите вашего поставщика электронной почты, чтобы начать.", + "WelcomeWindow_AddAccountButton": "Добавить учетную запись", + "WelcomeWindow_SkipForNow": "Пропустить пока — настрою позже", + "WelcomeWindow_AppDescription": "Быстрый, сфокусированный почтовый ящик — переработан для Windows 11.", + "WelcomeWizard_Step1Title": "Добро пожаловать", + "SystemTrayMenu_Open": "Открыть", + "WinoAccount_Titlebar_SyncBenefitTitle": "Синхронизация настроек", + "WinoAccount_Titlebar_SyncBenefitDescription": "Сохраняйте настройки Wino синхронизированными между устройствами.", + "WinoAccount_Titlebar_AddonsBenefitTitle": "Разблокировать дополнения", + "WinoAccount_Titlebar_AddonsBenefitDescription": "Получайте доступ к премиум-функциям, таким как Wino AI Pack.", + "WinoAccount_Management_Description": "Управляйте своей учетной записью Wino, доступом к AI Pack и синхронизированными настройками и данными учетной записи.", + "WinoAccount_Management_SignedOutTitle": "Войдите в Wino Mail", + "WinoAccount_Management_SignedOutDescription": "Войдите в учетную запись или создайте её, чтобы синхронизировать почту, получить доступ к функциям AI и управлять настройками на разных устройствах.", + "WinoAccount_Management_ProfileSectionHeader": "Профиль", + "WinoAccount_Management_AddOnsSectionHeader": "Дополнения Wino", + "WinoAccount_Management_DataSectionHeader": "Данные", + "WinoAccount_Management_AccountActionsSectionHeader": "Действия с учетной записью", + "WinoAccount_Management_AccountCardTitle": "Учетная запись", + "WinoAccount_Management_AccountCardDescription": "Адрес электронной почты вашей учетной записи Wino и текущее состояние учетной записи.", + "WinoAccount_Management_AiPackCardTitle": "AI Pack", + "WinoAccount_Management_AiPackCardDescription": "Проверьте, активен ли Wino AI Pack и сколько осталось использования.", + "WinoAccount_Management_AiPackActive": "Wino AI Pack активен", + "WinoAccount_Management_AiPackInactive": "Wino AI Pack не активен", + "WinoAccount_Management_AiPackUsage": "{0} из {1} использовано. Осталось {2}.", + "WinoAccount_Management_AiPackBillingPeriod": "Период оплаты: {0:d} - {1:d}", + "WinoAccount_Management_AiPackUnknownUsage": "Данные об использовании еще недоступны.", + "WinoAccount_Management_AiPackBuyDescription": "Купите Wino AI Pack, чтобы переводить, перерабатывать или суммировать письма с помощью AI.", + "WinoAccount_Management_AiPackPromoTitle": "Разблокировать AI Pack", + "WinoAccount_Management_AiPackPromoDescription": "Ускорьте работу с почтой с помощью инструментов на базе AI. Переводите сообщения на более чем 50 языков, перерабатывайте текст для ясности и тона, и получайте мгновенные конспекты длинных переписок.", + "WinoAccount_Management_AiPackPromoPrice": "$4.99 / мес", + "WinoAccount_Management_AiPackPromoRequests": "1 000 кредитов", + "WinoAccount_Management_AiPackGetButton": "Получить AI Pack", + "WinoAddOn_AI_PACK_Name": "Wino AI Pack", + "WinoAddOn_AI_PACK_Description": "AI-powered tools for translate, rewrite, and summarize actions in Wino Mail.", + "WinoAddOn_AI_PACK_Keywords": "AI, translate, rewrite, summarize, productivity", + "WinoAddOn_UNLIMITED_ACCOUNTS_Name": "Unlimited Accounts", + "WinoAddOn_UNLIMITED_ACCOUNTS_Description": "Remove the account limit and add as many mail accounts as you need.", + "WinoAddOn_UNLIMITED_ACCOUNTS_Keywords": "accounts, unlimited, premium, add-on", + "WinoAccount_Management_PurchaseRequiresSignIn": "Sign in with your Wino Account to complete this purchase.", + "WinoAccount_Management_PurchaseStartFailed": "Wino не смог завершить эту покупку в Microsoft Store.", + "WinoAccount_Management_StoreSyncFailed": "Покупка завершена, но Wino не может обновить преимущества вашей учетной записи. Попробуйте еще раз через мгновение.", + "WinoAccount_Management_AiPackSubscriptionActive": "Ваша подписка активна", + "WinoAccount_Management_AiPackRenews": "Продлевается {0:d}", + "WinoAccount_Management_AiPackRequestsUsed": "Использовано кредитов в этом месяце", + "WinoAccount_Management_AiPackResets": "Сбросы {0:d}", + "WinoAccount_Management_AiPackUsageLoadFailed": "Не удалось загрузить баланс использования AI.", + "WinoAccount_Management_AiPackFeatureTranslate": "Перевод", + "WinoAccount_Management_AiPackFeatureRewrite": "Переписать", + "WinoAccount_Management_AiPackFeatureSummarize": "Суммировать", + "WinoAccount_Management_AddOnLoadFailed": "Не удалось загрузить это дополнение.", + "WinoAccount_Management_SyncPreferencesTitle": "Синхронизировать настройки и учетные записи", + "WinoAccount_Management_SyncPreferencesDescription": "Импортируйте или экспортируйте ваши настройки Wino и данные почтового ящика на разных устройствах. Пароли, токены и другая конфиденциальная информация никогда не синхронизируются.", + "WinoAccount_Management_SignOutTitle": "Выйти", + "WinoAccount_Management_SignOutDescription": "Выйдите из своей учетной записи на этом устройстве", + "WinoAccount_Management_StatusLabel": "Статус: {0}", + "WinoAccount_Management_NoRemoteSettings": "Для этой учетной записи синхронизированные данные еще не сохранены.", + "WinoAccount_Management_ExportSucceeded": "Выбранные данные Wino успешно экспортированы.", + "WinoAccount_Management_ExportPreferencesSucceeded": "Ваши настройки экспортированы в вашу учетную запись Wino.", + "WinoAccount_Management_ExportAccountsSucceeded": "Экспортировано {0} сведений об учетной записи в вашу учетную запись Wino.", + "WinoAccount_Management_ImportSucceeded": "Импортированы синхронизированные данные из вашей учетной записи Wino.", + "WinoAccount_Management_ImportPreferencesSucceeded": "Применено {0} синхронизированных настроек.", + "WinoAccount_Management_ImportAccountsSucceeded": "Импортировано {0} учетных записей.", + "WinoAccount_Management_ImportDuplicateAccountsSkipped": "Пропущено {0} учетных записей, которые уже существуют на этом устройстве.", + "WinoAccount_Management_ImportPartial": "Применено {0} синхронизированных настроек. Не удалось восстановить {1} настроек.", + "WinoAccount_Management_ImportReloginReminder": "Пароли, токены и другая конфиденциальная информация не были импортированы. Войдите снова для каждой учетной записи на этом устройстве перед использованием.", + "WinoAccount_Management_SerializeFailed": "Wino не смог сериализовать ваши текущие настройки.", + "WinoAccount_Management_EmptyExport": "Нет значений настроек, которые можно экспортировать.", + "WinoAccount_Management_ImportEmpty": "Пакет синхронизованных данных не содержит ничего нового для восстановления.", + "WinoAccount_Management_ExportDialog_Title": "Экспорт в ваш аккаунт Wino.", + "WinoAccount_Management_ExportDialog_Description": "Выберите, что вы хотите синхронизировать с вашим аккаунтом Wino.", + "WinoAccount_Management_ExportDialog_IncludePreferences": "Настройки", + "WinoAccount_Management_ExportDialog_IncludeAccounts": "Учетные записи", + "WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Пароли, токены и другие конфиденциальные данные не синхронизируются.", + "WinoAccount_Management_ExportDialog_AccountsRelogin": "Импортированные на другом ПК учетные записи по-прежнему потребуют повторного входа в систему перед использованием.", + "WinoAccount_Management_ExportDialog_InProgress": "Выполняется экспорт выбранных данных Wino...", + "WinoAccount_Management_LoadFailed": "Не удалось загрузить последнюю информацию об учетной записи Wino.", + "WinoAccount_Management_ActionFailed": "Запрос к учетной записи Wino не удалось выполнить.", + "WinoAccount_SettingsSection_Title": "Учетная запись Wino", + "WinoAccount_SettingsSection_Description": "Создайте учетную запись Wino или войдите в неё, используя ваш локальный сервис аутентификации.", + "WinoAccount_RegisterButton_Title": "Зарегистрировать учетную запись", + "WinoAccount_RegisterButton_Description": "Создайте учетную запись Wino с электронной почтой и паролем.", + "WinoAccount_RegisterButton_Action": "Открыть регистрацию", + "WinoAccount_LoginButton_Title": "Войти", + "WinoAccount_LoginButton_Description": "Войдите в существующую учетную запись Wino с электронной почтой и паролем.", + "WinoAccount_LoginButton_Action": "Открыть вход", + "WinoAccount_SignOutButton_Title": "Выйти", + "WinoAccount_SignOutButton_Description": "Удалить локально сохраненную сессию учетной записи Wino.", + "WinoAccount_SignOutButton_Action": "Выйти", + "WinoAccount_RegisterDialog_Title": "Создать учетную запись Wino", + "WinoAccount_RegisterDialog_Description": "Создайте учетную запись Wino, чтобы синхронизировать ваш опыт использования Wino и разблокировать дополнения, зависящие от учетной записи.", + "WinoAccount_RegisterDialog_HeroTitle": "Создайте учетную запись Wino", + "WinoAccount_RegisterDialog_BenefitsTitle": "Зачем создавать учетную запись?", + "WinoAccount_RegisterDialog_BenefitSyncTitle": "Импорт и экспорт настроек между устройствами", + "WinoAccount_RegisterDialog_BenefitSyncDescription": "Переносите настройки Wino между устройствами, не восстанавливая конфигурацию с нуля.", + "WinoAccount_RegisterDialog_BenefitAiTitle": "Доступ к эксклюзивным дополнениям, таким как Wino AI Pack (платно)", + "WinoAccount_RegisterDialog_BenefitAiDescription": "Используйте одну учетную запись, чтобы разблокировать премиум-функции Wino по мере их появления.", + "WinoAccount_RegisterDialog_DifferenceTitle": "Учетная запись Wino отделена от ваших почтовых учетных записей", + "WinoAccount_RegisterDialog_DifferenceDescription": "Ваши учетные записи Outlook, Gmail, IMAP и другие почтовые учетные записи останутся без изменений. Учетная запись Wino управляет только функциями Wino и дополнениями, привязанными к учетной записи.", + "WinoAccount_RegisterDialog_PrimaryButton": "Зарегистрироваться", + "WinoAccount_RegisterDialog_PrivacyTitle": "Конфиденциальность и обработка API", + "WinoAccount_RegisterDialog_PrivacyDescription": "Необязательные дополнения, такие как Wino AI Pack, могут отправлять выбранное HTML-содержимое писем в сервис Wino API только при использовании этих функций.", + "WinoAccount_RegisterDialog_PrivacyLinkText": "Прочитайте политику конфиденциальности.", + "WinoAccount_RegisterDialog_PrivacyCheckbox": "Я согласен с политикой конфиденциальности.", + "WinoAccount_LoginDialog_Title": "Войдите в учетную запись Wino", + "WinoAccount_LoginDialog_Description": "Войдите в свою учетную запись Wino, чтобы синхронизировать настройки Wino и получить доступ к функциям, зависящим от учетной записи.", + "WinoAccount_LoginDialog_HeroTitle": "С возвращением", + "WinoAccount_LoginDialog_BenefitsTitle": "Преимущества входа", + "WinoAccount_LoginDialog_BenefitsDescription": "Используйте учетную запись Wino, чтобы продолжать синхронизацию настроек между устройствами и иметь доступ к платным дополнениям, таким как Wino AI Pack.", + "WinoAccount_LoginDialog_DifferenceTitle": "Это не вход в вашу электронную почтовую учетную запись", + "WinoAccount_LoginDialog_DifferenceDescription": "Вход здесь не добавляет и не заменяет ваши учетные записи Outlook, Gmail, IMAP и другие в Wino. Вход здесь выполняет вход только в сервисы, связанные с учетной записью Wino.", + "WinoAccount_LoginDialog_ForgotPasswordLink": "Забыли пароль?", + "WinoAccount_EmailLabel": "Электронная почта", + "WinoAccount_EmailPlaceholder": "name@example.com", + "WinoAccount_PasswordLabel": "Пароль", + "WinoAccount_ConfirmPasswordLabel": "Подтвердите пароль", + "WinoAccount_ForgotPasswordDialog_Title": "Сброс пароля", + "WinoAccount_ForgotPasswordDialog_PrimaryButton": "Отправить письмо для сброса пароля", + "WinoAccount_ForgotPasswordDialog_BackToSignIn": "Вернуться к входу", + "WinoAccount_ForgotPasswordDialog_Description": "Введите адрес электронной почты вашей учетной записи Wino, и мы отправим вам ссылку для сброса пароля, если адрес зарегистрирован.", + "WinoAccount_Validation_EmailRequired": "Электронная почта обязательна.", + "WinoAccount_Validation_PasswordRequired": "Пароль обязателен.", + "WinoAccount_Validation_PasswordMismatch": "Пароли не совпадают.", + "WinoAccount_Validation_PrivacyConsentRequired": "Перед созданием учетной записи Wino вы должны принять политику конфиденциальности.", + "WinoAccount_Error_InvalidCredentials": "Адрес электронной почты или пароль неверны.", + "WinoAccount_Error_AccountLocked": "Эта учетная запись временно заблокирована.", + "WinoAccount_Error_AccountBanned": "Эта учетная запись заблокирована.", + "WinoAccount_Error_AccountSuspended": "Эта учетная запись приостановлена.", + "WinoAccount_Error_EmailNotConfirmed": "Пожалуйста, подтвердите адрес электронной почты перед входом.", + "WinoAccount_Error_EmailConfirmationRequired": "Необходимо подтвердить адрес электронной почты перед входом.", + "WinoAccount_Error_EmailConfirmationResendNotAvailable": "Новое письмо для подтверждения пока недоступно.", + "WinoAccount_Error_EmailConfirmationResendInvalid": "Этот запрос на подтверждение больше не действителен. Пожалуйста, попробуйте войти снова.", + "WinoAccount_Error_EmailNotRegistered": "Этот адрес электронной почты не зарегистрирован.", + "WinoAccount_Error_RefreshTokenInvalid": "Ваша сессия больше не действительна. Пожалуйста, войдите снова.", + "WinoAccount_Error_EmailAlreadyRegistered": "Этот адрес электронной почты уже зарегистрирован.", + "WinoAccount_Error_ExternalLoginEmailRequired": "Для завершения внешней аутентификации требуется адрес электронной почты.", + "WinoAccount_Error_ExternalLoginInvalid": "Запрос внешней аутентификации недействителен.", + "WinoAccount_Error_ExternalAuthStateInvalid": "Состояние внешней аутентификации недействительно или истекло.", + "WinoAccount_Error_ExternalAuthCodeInvalid": "Код внешней аутентификации недействителен или истек.", + "WinoAccount_Error_AiPackRequired": "Требуется активная подписка на Wino AI Pack для выполнения этого действия.", + "WinoAccount_Error_AiQuotaExceeded": "Ваш лимит использования Wino AI Pack достигнут за текущий платежный период.", + "WinoAccount_Error_AiHtmlEmpty": "Нет содержимого письма электронной почты для обработки.", + "WinoAccount_Error_AiHtmlTooLarge": "Это письмо слишком большое для обработки с помощью Wino AI.", + "WinoAccount_Error_AiUnsupportedLanguage": "Этот язык не поддерживается. Попробуйте действительный код культуры, например en-US или tr-TR.", + "WinoAccount_Error_Forbidden": "У вас нет разрешения на выполнение этого действия.", + "WinoAccount_Error_ValidationFailed": "Запрос недействителен. Пожалуйста, проверьте введенные значения.", + "WinoAccount_RegisterSuccessMessage": "Регистрация учетной записи Wino завершена для {0}.", + "WinoAccount_LoginSuccessMessage": "Вы вошли в учетную запись Wino как {0}.", + "WinoAccount_EmailConfirmationSentDialog_Title": "Подтвердите адрес электронной почты.", + "WinoAccount_EmailConfirmationSentDialog_Message": "Мы отправили подтверждение на электронную почту {0}. Подтвердите его и попробуйте войти снова.", + "WinoAccount_EmailConfirmationPendingDialog_Title": "Необходимо подтверждение электронной почты.", + "WinoAccount_EmailConfirmationPendingDialog_Message": "Мы всё ещё ожидаем подтверждения {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ResendButton": "Выслать повторно письмо для подтверждения.", + "WinoAccount_EmailConfirmationPendingDialog_Countdown": "Вы можете повторно отправить письмо для подтверждения через {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ReadyToResend": "Вы можете отправить письмо для подтверждения прямо сейчас.", + "WinoAccount_EmailConfirmationResentDialog_Title": "Письмо для подтверждения отправлено повторно.", + "WinoAccount_EmailConfirmationResentDialog_Message": "Мы отправили ещё одно письмо для подтверждения на {0}. Подтвердите его и попробуйте войти снова.", + "WinoAccount_ForgotPasswordDialog_SuccessTitle": "Письмо для сброса пароля отправлено.", + "WinoAccount_ForgotPasswordDialog_SuccessMessage": "Мы отправили письмо для сброса пароля на {0}. Откройте его, чтобы установить новый пароль.", + "WinoAccount_ChangePassword_Title": "Изменить пароль", + "WinoAccount_ChangePassword_Description": "Отправить письмо для сброса пароля на эту учетную запись Wino.", + "WinoAccount_ChangePassword_Action": "Отправить письмо для сброса пароля.", + "WinoAccount_ChangePassword_ConfirmationMessage": "Вы хотите, чтобы Wino отправил письмо для сброса пароля на {0}?", + "WinoAccount_SignOut_SuccessMessage": "Вы вышли из учетной записи Wino {0}.", + "WinoAccount_SignOut_NoAccountMessage": "Нет активной учетной записи Wino, из которой можно выйти.", + "WinoAccount_Titlebar_SignedOutTitle": "Учетная запись Wino", + "WinoAccount_Titlebar_SignedOutDescription": "Войдите в учетную запись Wino или создайте её, чтобы управлять вашей сессией Wino.", + "WinoAccount_Titlebar_SignedInStatus": "Статус: {0}", + "WelcomeWizard_Step2Title": "Добавить учетную запись", + "WelcomeWizard_Step3Title": "Завершение настройки", + "ProviderSelection_Title": "Выберите поставщика электронной почты", + "ProviderSelection_Subtitle": "Выберите ниже поставщика, чтобы добавить вашу учетную запись электронной почты в Wino Mail.", + "ProviderSelection_AccountNameHeader": "Имя учетной записи", + "ProviderSelection_AccountNamePlaceholder": "например, Личное, Рабочее", + "ProviderSelection_DisplayNameHeader": "Отображаемое имя", + "ProviderSelection_DisplayNamePlaceholder": "например, Иван Иванов", + "ProviderSelection_EmailHeader": "Адрес электронной почты", + "ProviderSelection_EmailPlaceholder": "например, johndoe@example.com", + "ProviderSelection_AppPasswordHeader": "Пароль приложения", + "ProviderSelection_AppPasswordHelp": "Как получить пароль приложения?", + "ProviderSelection_CalendarModeHeader": "Интеграция календаря", + "ProviderSelection_CalendarMode_DisabledTitle": "Отключено", + "ProviderSelection_CalendarMode_DisabledDescription": "Нет интеграции календаря", + "ProviderSelection_CalendarMode_CalDavTitle": "Синхронизация CalDAV", + "ProviderSelection_CalendarMode_CalDavDescription_Apple": "Ваши события календаря синхронизируются с серверами Apple между вашими устройствами.", + "ProviderSelection_CalendarMode_CalDavDescription_Yahoo": "Ваши события календаря синхронизируются с серверами Yahoo между вашими устройствами.", + "ProviderSelection_CalendarMode_LocalTitle": "Локальный календарь", + "ProviderSelection_CalendarMode_LocalDescription": "Ваши события хранятся только на вашем компьютере. Соединение с сервером отсутствует.", + "ProviderSelection_ClearColor": "Очистить цвет", + "ProviderSelection_ContinueButton": "Продолжить", + "ProviderSelection_SpecialImap_Subtitle": "Введите данные учетной записи, чтобы подключиться.", + "AccountSetup_Title": "Настройка вашей учетной записи", + "AccountSetup_Step_Authenticating": "Аутентификация с {0}", + "AccountSetup_Step_TestingMailAuth": "Тестирование аутентификации почты", + "AccountSetup_Step_SyncingFolders": "Синхронизация метаданных папок", + "AccountSetup_Step_FetchingProfile": "Получение информации о профиле", + "AccountSetup_Step_DiscoveringCalDav": "Обнаружение настроек CalDAV", + "AccountSetup_Step_TestingCalendarAuth": "Тестирование аутентификации календаря", + "AccountSetup_Step_SavingAccount": "Сохранение информации об учетной записи", + "AccountSetup_Step_FetchingCalendarMetadata": "Получение метаданных календаря", + "AccountSetup_Step_SyncingAliases": "Синхронизация алиасов", + "AccountSetup_Step_Finalizing": "Завершение настройки", + "AccountSetup_FailureMessage": "Не удалось настроить. Вернитесь назад, чтобы исправить настройки, или попробуйте позже.", + "AccountSetup_SuccessMessage": "Ваша учетная запись успешно настроена!", + "AccountSetup_GoBackButton": "Назад", + "AccountSetup_TryAgainButton": "Попробовать ещё раз", + "ImapCalDavSettings_AutoDiscoveryFailed": "Не удалось автоматически обнаружить настройки. Пожалуйста, введите настройки вручную во вкладке Расширенные." } - - diff --git a/Wino.Core.Domain/Translations/sk_SK/resources.json b/Wino.Core.Domain/Translations/sk_SK/resources.json index c3b5bc54..917ed1ec 100644 --- a/Wino.Core.Domain/Translations/sk_SK/resources.json +++ b/Wino.Core.Domain/Translations/sk_SK/resources.json @@ -8,6 +8,7 @@ "AccountCacheReset_Message": "Tento účet si vyžaduje znova úplnú synchronizáciu, aby mohol ďalej fungovať. Počkajte, prosím, kým Wino znovu zosynchronizuje vaše správy…", "AccountContactNameYou": "Vy", "AccountCreationDialog_Completed": "hotovo", + "AccountCreationDialog_FetchingCalendarMetadata": "Načítavam údaje o kalendári.", "AccountCreationDialog_FetchingEvents": "Načítavajú sa udalosti kalendára.", "AccountCreationDialog_FetchingProfileInformation": "Načítavajú sa podrobnosti o profile.", "AccountCreationDialog_GoogleAuthHelpClipboardText_Row0": "Ak sa prehliadač nespustil automaticky a nedokončil overovanie:", @@ -17,6 +18,7 @@ "AccountCreationDialog_Initializing": "inicializácia", "AccountCreationDialog_PreparingFolders": "Práve získavame informácie o priečinkoch.", "AccountCreationDialog_SigninIn": "Informácie o účte sa ukladajú.", + "Purchased": "Zakúpené", "AccountEditDialog_Message": "Názov účtu", "AccountEditDialog_Title": "Upraviť účet", "AccountPickerDialog_Title": "Vyberte účet", @@ -26,6 +28,10 @@ "AccountDetailsPage_Description": "Zmena názvu účtu vo Wino a nastavenie požadovaného názvu odosielateľa.", "AccountDetailsPage_ColorPicker_Title": "Farba účtu", "AccountDetailsPage_ColorPicker_Description": "Priradenie farby novému účtu na zafarbenie jeho symbolu v zozname.", + "AccountDetailsPage_TabGeneral": "Všeobecné", + "AccountDetailsPage_TabMail": "Pošta", + "AccountDetailsPage_TabCalendar": "Kalendár", + "AccountDetailsPage_CalendarListDescription": "Vyberte kalendár na konfiguráciu jeho nastavení.", "AddHyperlink": "Pridať", "AppCloseBackgroundSynchronizationWarningTitle": "Synchronizácia na pozadí", "AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Aplikácia sa nebude spúšťať pri spustení systému Windows.", @@ -47,8 +53,10 @@ "BasicIMAPSetupDialog_Title": "Účet IMAP", "Busy": "Zaneprázdnený", "Buttons_AddAccount": "Pridať účet", + "Buttons_FixAccount": "Opraviť účet", "Buttons_AddNewAlias": "Pridať nový alias", "Buttons_Allow": "Zapnúť", + "Buttons_Apply": "Použiť", "Buttons_ApplyTheme": "Použiť motív", "Buttons_Browse": "Prehľadávať", "Buttons_Cancel": "Zrušiť", @@ -62,6 +70,7 @@ "Buttons_Edit": "Upraviť", "Buttons_EnableImageRendering": "Zapnúť", "Buttons_Multiselect": "Vybrať viac", + "Buttons_Manage": "Spravovať", "Buttons_No": "Nie", "Buttons_Open": "Otvoriť", "Buttons_Purchase": "Kúpiť", @@ -70,15 +79,134 @@ "Buttons_Save": "Uložiť", "Buttons_SaveConfiguration": "Uložiť konfiguráciu", "Buttons_Send": "Odoslať", + "Buttons_SendToServer": "Odoslať na server", "Buttons_Share": "Zdielať", "Buttons_SignIn": "Prihlásiť sa", "Buttons_Sync": "Synchronizovať", "Buttons_SyncAliases": "Synchronizovať aliasy", "Buttons_TryAgain": "Skúsiť znova", "Buttons_Yes": "Áno", + "Sync_SynchronizingFolder": "Synchronizujem {0} {1}%", + "Sync_DownloadedMessages": "Stiahnuté {0} správ zo {1}", + "SyncAction_Archiving": "Archivujem {0} e-mailov", + "SyncAction_ClearingFlag": "Odznačujem {0} e-mailov", + "SyncAction_CreatingDraft": "Vytváram návrh", + "SyncAction_CreatingEvent": "Vytváram udalosť", + "SyncAction_Deleting": "Odstraňujem {0} e-mailov", + "SyncAction_EmptyingFolder": "Vyprázdňujem priečinok", + "SyncAction_MarkingAsRead": "Označujem {0} e-mailov ako prečítané", + "SyncAction_MarkingAsUnread": "Označujem {0} e-mailov ako neprečítané", + "SyncAction_MarkingFolderAsRead": "Označujem priečinok ako prečítaný", + "SyncAction_Moving": "Presúvam {0} e-mailov", + "SyncAction_MovingToFocused": "Presúvam {0} e-mailov do priečinka Focused", + "SyncAction_RenamingFolder": "Premenúvam názov priečinka", + "SyncAction_SendingMail": "Odosielam e-mail", + "SyncAction_SettingFlag": "Značím {0} e-mailov ako dôležité", + "SyncAction_SynchronizingAccount": "Synchronizujem {0}", + "SyncAction_SynchronizingAccounts": "Synchronizujem {0} účet(ov)", + "SyncAction_SynchronizingCalendarData": "Synchronizujem údaje kalendára", + "SyncAction_SynchronizingCalendarEvents": "Synchronizujem udalosti kalendára", + "SyncAction_SynchronizingCalendarMetadata": "Synchronizujem metadáta kalendára", + "SyncAction_Unarchiving": "Rozbaľujem {0} e-mailov", "CalendarAllDayEventSummary": "celodenné udalosti", "CalendarDisplayOptions_Color": "Farba", "CalendarDisplayOptions_Expand": "Rozšíriť", + "CalendarEventResponse_Accept": "Prijať", + "CalendarEventResponse_AcceptedResponse": "Prijaté", + "CalendarEventResponse_Decline": "Odmietnuť", + "CalendarEventResponse_DeclinedResponse": "Odmietnuté", + "CalendarEventResponse_NotResponded": "Neodpovedané", + "CalendarEventResponse_Tentative": "Predbežné", + "CalendarEventResponse_TentativeResponse": "Predbežná odpoveď", + "CalendarEventRsvpPanel_Accept": "Prijať", + "CalendarEventRsvpPanel_AddMessage": "Pridajte správu k vašej odpovedi... (nepovinné)", + "CalendarEventRsvpPanel_Decline": "Odmietnuť", + "CalendarEventRsvpPanel_Message": "Správa", + "CalendarEventRsvpPanel_SendReplyMessage": "Odošlite odpovednú správu", + "CalendarEventRsvpPanel_Tentative": "Predbežné", + "CalendarEventRsvpPanel_Title": "Možnosti odpovede", + "CalendarAttendeeStatus_Accepted": "Prijaté", + "CalendarAttendeeStatus_Declined": "Odmietnuté", + "CalendarAttendeeStatus_NeedsAction": "Vyžaduje akciu", + "CalendarAttendeeStatus_Tentative": "Predbežné", + "CalendarEventDetails_Attachments": "Prílohy", + "CalendarEventCompose_AddAttachment": "Pridať prílohu", + "CalendarEventCompose_AllDay": "Celý deň", + "CalendarEventCompose_AttachmentsNotSupportedForCalDav": "Prílohy nie sú podporované pre kalendáre CalDAV.", + "CalendarEventCompose_EndDate": "Dátum konca", + "CalendarEventCompose_EndTime": "Čas konca", + "CalendarEventCompose_Every": "každý", + "CalendarEventCompose_ForWeekdays": "pre", + "CalendarEventCompose_FrequencyDay": "deň", + "CalendarEventCompose_FrequencyDayPlural": "dni", + "CalendarEventCompose_FrequencyMonth": "mesiac", + "CalendarEventCompose_FrequencyMonthPlural": "mesiace", + "CalendarEventCompose_FrequencyWeek": "týždeň", + "CalendarEventCompose_FrequencyWeekPlural": "týždne", + "CalendarEventCompose_FrequencyYear": "rok", + "CalendarEventCompose_FrequencyYearPlural": "roky", + "CalendarEventCompose_Location": "Miesto", + "CalendarEventCompose_LocationPlaceholder": "Pridajte miesto", + "CalendarEventCompose_NewEventButton": "Nová udalosť", + "CalendarEventCompose_DefaultCalendarHint": "V nastaveniach kalendára môžete vybrať predvolený kalendár pre nové udalosti.", + "CalendarEventCompose_DefaultCalendarSettingsLink": "Otvoriť nastavenia Kalendára", + "CalendarEventCompose_NoCalendarsMessage": "Pre vytváranie udalostí zatiaľ nie sú k dispozícii žiadne kalendáre.", + "CalendarEventCompose_NoCalendarsTitle": "Nie sú k dispozícii žiadne kalendáre", + "CalendarEventCompose_NoEndDate": "Bez koncového dátumu", + "CalendarEventCompose_Notes": "Poznámky", + "CalendarEventCompose_PickCalendarTitle": "Vyberte kalendár", + "CalendarEventCompose_Recurring": "Opakujúce sa", + "CalendarEventCompose_RecurringSummary": "Vyskytuje sa každých {0} {1}{2} {3} platných {4}{5}", + "CalendarEventCompose_RecurringSummarySmart": "Vyskytuje sa {0}{1} {2} platné {3}{4}", + "CalendarEventCompose_RepeatEvery": "Opakovať každých", + "CalendarEventCompose_SelectCalendar": "Vybrať kalendár", + "CalendarEventCompose_SingleOccurrenceSummary": "Vyskytuje sa {0} {1}", + "CalendarEventCompose_StartDate": "Dátum začiatku", + "CalendarEventCompose_StartTime": "Čas začiatku", + "CalendarEventCompose_TimeRangeSummary": "od {0} do {1}", + "CalendarEventCompose_Title": "Názov udalosti", + "CalendarEventCompose_TitlePlaceholder": "Pridajte názov", + "CalendarEventCompose_Until": "až do", + "CalendarEventCompose_UntilSummary": " až {0}", + "CalendarEventCompose_ValidationInvalidAllDayRange": "Dátum konca pre celodennú udalosť musí byť po dátume začiatku.", + "CalendarEventCompose_ValidationInvalidAttendee": "Jeden alebo viac účastníkov má neplatnú e-mailovú adresu.", + "CalendarEventCompose_ValidationInvalidRecurrenceEnd": "Dátum ukončenia opakovania musí byť rovný alebo neskorší ako dátum začiatku udalosti.", + "CalendarEventCompose_ValidationInvalidTimeRange": "Čas konca musí byť neskorší ako čas začiatku.", + "CalendarEventCompose_ValidationMissingAttachment": "Jeden alebo viac príloh už nie je k dispozícii: {0}", + "CalendarEventCompose_ValidationMissingCalendar": "Vyberte kalendár pred vytvorením udalosti.", + "CalendarEventCompose_ValidationMissingTitle": "Zadajte názov udalosti pred vytvorením udalosti.", + "CalendarEventCompose_ValidationTitle": "Overenie udalosti zlyhalo", + "CalendarEventCompose_WeekdaySummary": " v {0}", + "CalendarEventCompose_Weekday_Friday": "Piatok", + "CalendarEventCompose_Weekday_Monday": "Pondelok", + "CalendarEventCompose_Weekday_Saturday": "Sobota", + "CalendarEventCompose_Weekday_Sunday": "Nedeľa", + "CalendarEventCompose_Weekday_Thursday": "Štvrtok", + "CalendarEventCompose_Weekday_Tuesday": "Utorok", + "CalendarEventCompose_Weekday_Wednesday": "Streda", + "CalendarEventDetails_Details": "Podrobnosti", + "CalendarEventDetails_EditSeries": "Upraviť sériu", + "CalendarEventDetails_Editing": "Úpravy", + "CalendarEventDetails_InviteSomeone": "Pozvať niekoho", + "CalendarEventDetails_JoinOnline": "Pripojiť sa online", + "CalendarEventDetails_Organizer": "Organizátor", + "CalendarEventDetails_People": "Ľudia", + "CalendarEventDetails_ReadOnlyEvent": "Udalosť iba na čítanie", + "CalendarEventDetails_Reminder": "Pripomienka", + "CalendarReminder_StartedHoursAgo": "Začalo pred {0} hodinami", + "CalendarReminder_StartedMinutesAgo": "Začalo pred {0} minútami", + "CalendarReminder_StartedNow": "Začalo práve teraz", + "CalendarReminder_StartingNow": "Začíname teraz", + "CalendarReminder_StartsInHours": "Začína sa za {0} hodín", + "CalendarReminder_StartsInMinutes": "Začína sa za {0} minút", + "CalendarReminder_SnoozeAction": "Odložiť", + "CalendarReminder_SnoozeMinutesOption": "{0} minút", + "CalendarEventDetails_ShowAs": "Zobraziť ako", + "CalendarShowAs_Free": "Voľný", + "CalendarShowAs_Tentative": "Predpokladaný", + "CalendarShowAs_Busy": "Zaneprázdnený", + "CalendarShowAs_OutOfOffice": "Mimo kancelárie", + "CalendarShowAs_WorkingElsewhere": "Pracuje inde", "CalendarItem_DetailsPopup_JoinOnline": "Pripojiť online", "CalendarItem_DetailsPopup_ViewEventButton": "Zobraziť udalosť", "CalendarItem_DetailsPopup_ViewSeriesButton": "Zobraziť sériu", @@ -88,6 +216,9 @@ "ClipboardTextCopied_Message": "{0} skopírované do schránky.", "ClipboardTextCopied_Title": "Skopírované", "ClipboardTextCopyFailed_Message": "{0} sa nepodarilo skopírovať do schránky.", + "ContactInfoBar_ErrorTitle": "Nepodarilo sa načítať kontaktné informácie", + "ContactInfoBar_SuccessTitle": "Kontaktné informácie načítané", + "ContactInfoBar_WarningTitle": "Kontaktné informácie môžu byť neúplné", "ComingSoon": "Už čoskoro…", "ComposerAttachmentsDragDropAttach_Message": "Priložiť", "ComposerAttachmentsDropZone_Message": "Sem presuňte súbory", @@ -129,6 +260,10 @@ "DialogMessage_CreateLinkedAccountTitle": "Názov prepojeného účtu", "DialogMessage_DeleteAccountConfirmationMessage": "Odstrániť {0}?", "DialogMessage_DeleteAccountConfirmationTitle": "Všetky údaje spojené s týmto účtom budú z disku natrvalo odstránené.", + "DialogMessage_DeleteEmailTemplateConfirmationMessage": "Odstrániť šablónu \"{0}\"?", + "DialogMessage_DeleteEmailTemplateConfirmationTitle": "Odstrániť e-mailovú šablónu", + "DialogMessage_DeleteRecurringSeriesMessage": "Týmto sa odstránia všetky udalosti zo série. Chcete pokračovať?", + "DialogMessage_DeleteRecurringSeriesTitle": "Odstrániť opakovanú sériu", "DialogMessage_DiscardDraftConfirmationMessage": "Tento koncept sa zahodí. Chcete pokračovať?", "DialogMessage_DiscardDraftConfirmationTitle": "Zrušiť koncept", "DialogMessage_EmptySubjectConfirmation": "Chýbajúci predmet", @@ -172,11 +307,18 @@ "ElementTheme_Light": "Svetlý režim", "Emoji": "Emoji", "Error_FailedToSetupSystemFolders_Title": "Nepodarilo sa nastaviť systémové priečinky", + "Exception_AccountNeedsAttention_Title": "Účet si vyžaduje pozornosť", + "Exception_AccountNeedsAttention_Message": "'{0}' vyžaduje vašu pozornosť, aby ste mohli pokračovať v práci.", + "Exception_WebView2RuntimeMissing_Message": "Wino Mail nenašiel runtime Microsoft Edge WebView2. Nainštalujte alebo opravte runtime, aby sa obsah správne zobrazoval.", + "Exception_WebView2RuntimeMissing_Title": "Vyžaduje sa runtime WebView2", "Exception_AuthenticationCanceled": "Overenie bolo zrušené", "Exception_CustomThemeExists": "Tento motív už existuje.", "Exception_CustomThemeMissingName": "Musíte zadať názov.", "Exception_CustomThemeMissingWallpaper": "Musíte zadať vlastný obrázok pozadia.", "Exception_FailedToSynchronizeAliases": "Nepodarilo sa synchronizovať aliasy", + "Exception_FailedToSynchronizeCalendarData": "Nepodarilo sa synchronizovať údaje kalendára", + "Exception_FailedToSynchronizeCalendarEvents": "Nepodarilo sa synchronizovať udalosti kalendára", + "Exception_FailedToSynchronizeCalendarMetadata": "Nepodarilo sa synchronizovať podrobnosti kalendára", "Exception_FailedToSynchronizeFolders": "Nepodarilo sa synchronizovať priečinky", "Exception_FailedToSynchronizeProfileInformation": "Nepodarilo sa synchronizovať informácie o profile", "Exception_GoogleAuthCallbackNull": "Callback URL je pri aktivácii null.", @@ -229,6 +371,32 @@ "HoverActionOption_MoveJunk": "Presunúť do Nevyžiadaná pošta", "HoverActionOption_ToggleFlag": "Označiť/Zrušiť označenie", "HoverActionOption_ToggleRead": "Prečítané/Neprečítané", + "KeyboardShortcuts_FailedToReset": "Nepodarilo sa resetovať klávesové skratky.", + "KeyboardShortcuts_FailedToUpdate": "Nepodarilo sa aktualizovať klávesové skratky.", + "KeyboardShortcuts_MailoperationAction": "Akcia", + "KeyboardShortcuts_Action": "Akcia", + "KeyboardShortcuts_FailedToLoad": "Nepodarilo sa načítať klávesové skratky.", + "KeyboardShortcuts_EnterKeyForShortcut": "Prosím zadajte klávesu pre skratku.", + "KeyboardShortcuts_SelectOperationForShortcut": "Prosím zvoľte akciu, ktorú má skratka vykonať.", + "KeyboardShortcuts_EnterKey": "Prosím zadajte klávesu pre skratku.", + "KeyboardShortcuts_SelectOperation": "Prosím vyberte akciu pre skratku.", + "KeyboardShortcuts_ShortcutInUse": "Táto skratka je už použitá inou skratkou.", + "KeyboardShortcuts_FailedToSave": "Nepodarilo sa uložiť skratku.", + "KeyboardShortcuts_FailedToDelete": "Nepodarilo sa odstrániť skratku.", + "KeyboardShortcuts_PageDescription": "Nastavte klávesové skratky pre rýchle operácie s poštou. Stlačte klávesy, keď je zamerané pole pre zadanie kláves, aby sa skratky zachytili.", + "KeyboardShortcuts_Add": "Pridať skratku", + "KeyboardShortcuts_EditTitle": "Upraviť klávesovú skratku", + "KeyboardShortcuts_ResetToDefaults": "Obnoviť predvolený stav", + "KeyboardShortcuts_PressKeysHere": "Stlačte sem klávesy...", + "KeyboardShortcuts_KeyCombination": "Klávesová kombinácia", + "KeyboardShortcuts_FocusArea": "Zamerajte vyššie uvedené pole a stlačte požadovanú kombináciu kláves", + "KeyboardShortcuts_Modifiers": "Modifikačné klávesy", + "KeyboardShortcuts_Mode": "Mód aplikácie", + "KeyboardShortcuts_ModeMail": "E-mail", + "KeyboardShortcuts_ModeCalendar": "Kalendár", + "KeyboardShortcuts_ActionToggleReadUnread": "Prepnúť prečítané/neprečítané", + "KeyboardShortcuts_ActionToggleFlag": "Prepnúť vlajku", + "KeyboardShortcuts_ActionToggleArchive": "Prepnúť archivovať/odarchivovať", "ImageRenderingDisabled": "Zobrazovanie obrázkov je pre túto správu vypnuté.", "ImapAdvancedSetupDialog_AuthenticationMethod": "Metóda overovania", "ImapAdvancedSetupDialog_ConnectionSecurity": "Zabezpečenie pripojenia", @@ -295,12 +463,58 @@ "IMAPSetupDialog_Username": "Používateľské meno", "IMAPSetupDialog_UsernamePlaceholder": "jan.novak, jan.novak@mai.sk, mail.sk/jan.novak", "IMAPSetupDialog_UseSameConfig": "Na odosielanie správy použiť rovnaké používateľské meno a heslo", + "ImapCalDavSettingsPage_TitleCreate": "Nastavenie IMAP a Kalendára", + "ImapCalDavSettingsPage_TitleEdit": "Upraviť nastavenia IMAP a Kalendára", + "ImapCalDavSettingsPage_Subtitle": "Nastavte IMAP/SMTP a voliteľnú synchronizáciu kalendára pre tento účet.", + "ImapCalDavSettingsPage_BasicSectionTitle": "Základné nastavenie", + "ImapCalDavSettingsPage_BasicSectionDescription": "Zadajte svoju identitu a poverenia. Wino sa môže pokúsiť automaticky zistiť nastavenia servera.", + "ImapCalDavSettingsPage_BasicTab": "Základné", + "ImapCalDavSettingsPage_EnableCalendarSupport": "Povoliť podporu kalendára", + "ImapCalDavSettingsPage_AutoDiscoverButton": "Automatické zistenie nastavenia pošty", + "ImapCalDavSettingsPage_AutoDiscoverySuccessMessage": "Nastavenia pošty zistené a aplikované.", + "ImapCalDavSettingsPage_AdvancedSectionTitle": "Pokročilé nastavenia", + "ImapCalDavSettingsPage_AdvancedSectionDescription": "Zadajte nastavenia servera manuálne, ak automatické zistenie nie je k dispozícii alebo je nesprávne.", + "ImapCalDavSettingsPage_AdvancedTab": "Pokročilé", + "ImapCalDavSettingsPage_CalendarSectionTitle": "Nastavenie kalendára", + "ImapCalDavSettingsPage_CalendarSectionDescription": "Vyberte, ako by mali údaje kalendára fungovať pre tento účet IMAP.", + "ImapCalDavSettingsPage_CalendarModeHeader": "Režim kalendára", + "ImapCalDavSettingsPage_ConnectionSecurityHeader": "Zabezpečenie spojenia", + "ImapCalDavSettingsPage_AuthenticationMethodHeader": "Metóda overenia", + "ImapCalDavSettingsPage_CalendarModeDisabled": "Vypnutý", + "ImapCalDavSettingsPage_CalendarModeCalDav": "CalDAV synchronizácia", + "ImapCalDavSettingsPage_CalendarModeLocalOnly": "Iba miestny kalendár", + "ImapCalDavSettingsPage_CalendarModeDisabledDescription": "Kalendár je pre tento účet zakázaný.", + "ImapCalDavSettingsPage_CalendarModeCalDavDescription": "Položky kalendára sú synchronizované so serverom CalDAV.", + "ImapCalDavSettingsPage_CalendarModeLocalOnlyDescription": "Položky kalendára sa ukladajú iba na tomto počítači a nie sú synchronizované cez sieť.", + "ImapCalDavSettingsPage_LocalCalendarLearnMore": "Ako funguje miestny kalendár", + "ImapCalDavSettingsPage_LocalCalendarDialogTitle": "Iba miestny kalendár", + "ImapCalDavSettingsPage_LocalCalendarDialogMessage": "Miestny kalendár uchováva všetky udalosti iba na vašom počítači. Žiadna synchronizácia s iCloud, Yahoo ani s iným poskytovateľom.", + "ImapCalDavSettingsPage_CalDavServiceUrl": "URL služby CalDAV", + "ImapCalDavSettingsPage_CalDavUsername": "Prihlasovacie meno CalDAV", + "ImapCalDavSettingsPage_CalDavPassword": "CalDAV heslo", + "ImapCalDavSettingsPage_CalDavNotRequiredMessage": "Test CalDAV sa vyžaduje iba vtedy, keď je režim kalendára nastavený na synchronizáciu CalDAV.", + "ImapCalDavSettingsPage_CalDavUrlRequired": "URL služby CalDAV je povinný.", + "ImapCalDavSettingsPage_CalDavUrlInvalid": "URL služby CalDAV musí byť absolútny URL.", + "ImapCalDavSettingsPage_CalDavUsernameRequired": "CalDAV prihlasovacie meno je povinné.", + "ImapCalDavSettingsPage_CalDavPasswordRequired": "CalDAV heslo je povinné.", + "ImapCalDavSettingsPage_TestImapButton": "Otestovať pripojenie k IMAP", + "ImapCalDavSettingsPage_TestCalDavButton": "Otestovať pripojenie k CalDAV", + "ImapCalDavSettingsPage_ImapTestSuccessMessage": "Test pripojenia IMAP bol úspešný.", + "ImapCalDavSettingsPage_CalDavTestSuccessMessage": "Test pripojenia CalDAV bol úspešný.", + "ImapCalDavSettingsPage_SaveSuccessMessage": "Nastavenia účtu boli overené a uložené.", + "ImapCalDavSettingsPage_ICloudHint": "Použite heslo špecifické pre aplikáciu vygenerované v nastaveniach vášho účtu Apple.", + "ImapCalDavSettingsPage_YahooHint": "Použite heslo pre aplikáciu zo zabezpečenia svojho účtu Yahoo.", "Info_AccountCreatedMessage": "Vytvorený účet {0}", "Info_AccountCreatedTitle": "Vytvorenie účtu", "Info_AccountCreationFailedTitle": "Vytvorenie účtu zlyhalo", "Info_AccountDeletedMessage": "Účet {0} bol úspešne odstránený.", "Info_AccountDeletedTitle": "Účet bol odstránený", "Info_AccountIssueFixFailedTitle": "Oprava zlyhala", + "Info_AccountIssueFixImapMessage": "Otvorte stránku nastavenia IMAP a kalendára a zadajte znovu prihlasovacie údaje servera.", + "Info_AccountAttentionRequiredMessage": "Tento účet vyžaduje vašu pozornosť.", + "Info_AccountAttentionRequiredClickableMessage": "Kliknite pre opravu tohto účtu a jeho opätovnú synchronizáciu.", + "Info_AccountAttentionRequiredAction": "Opraviť", + "Info_AccountAttentionRequiredActionHint": "Kliknite na Opraviť, aby sa vyriešil problém s týmto účtom.", "Info_AccountIssueFixSuccessMessage": "Boli opravené všetky problémy s účtom.", "Info_AccountIssueFixSuccessTitle": "Úspešné", "Info_AttachmentOpenFailedMessage": "Túto prílohu sa nepodarilo otvoriť.", @@ -370,6 +584,7 @@ "InfoBarMessage_SynchronizationDisabledFolder": "Synchronizácia tohto priečinka je zakázaná.", "InfoBarTitle_SynchronizationDisabledFolder": "Nesynchronizovaný priečinok", "Justify": "Do bloku", + "MenuUpdateAvailable": "Dostupná aktualizácia", "Left": "Doľava", "Link": "Prepojiť", "LinkedAccountsCreatePolicyMessage": "na vytvorenie prepojenia musíte mať aspoň 2 účty\nprepojenie bude po uložení odstránené", @@ -403,6 +618,7 @@ "MailOperation_Unarchive": "Obnoviť z archívu", "MailOperation_ViewMessageSource": "Zobraziť zdroj správy", "MailOperation_Zoom": "Priblížiť", + "MailsDragging": "Presúvam {0} položku(y)", "MailsSelected": "Označené položky: {0}", "MarkFlagUnflag": "Označiť ako označené/neoznačené", "MarkReadUnread": "Označiť ako prečítané/neprečítané", @@ -434,6 +650,8 @@ "Notifications_MultipleNotificationsTitle": "Nová pošta", "Notifications_WinoUpdatedMessage": "Pozrite si novú verziu {0}", "Notifications_WinoUpdatedTitle": "Wino Mail bol aktualizovaný.", + "Notifications_StoreUpdateAvailableTitle": "Dostupná aktualizácia", + "Notifications_StoreUpdateAvailableMessage": "Novšia verzia aplikácie Wino Mail je pripravená na inštaláciu cez Microsoft Store.", "OnlineSearchFailed_Message": "Nepodarilo sa vyhľadať\n{0}\n\nZoznam správ offline.", "OnlineSearchTry_Line1": "Nenašli ste, čo ste hľadali?", "OnlineSearchTry_Line2": "Vyskúšajte online vyhľadávanie.", @@ -446,7 +664,6 @@ "PaneLengthOption_Small": "Malé", "Photos": "Fotografie", "PreparingFoldersMessage": "Príprava priečinkov", - "ProtocolLogAvailable_Message": "Logy protokolu sú pripravené na diagnostiku.", "ProviderDetail_Gmail_Description": "Účet Google", "ProviderDetail_iCloud_Description": "Účet Apple iCloud", "ProviderDetail_iCloud_Title": "iCloud", @@ -465,9 +682,14 @@ "SearchBarPlaceholder": "Hľadať", "SearchingIn": "Vyhľadávanie v", "SearchPivotName": "Výsledky", + "Settings_KeyboardShortcuts_Title": "Klávesové skratky", + "Settings_KeyboardShortcuts_Description": "Spravujte klávesové skratky pre rýchle akcie na e-mailoch.", "SettingConfigureSpecialFolders_Button": "Konfigurovať", "SettingsEditAccountDetails_IMAPConfiguration_Title": "Konfigurácia IMAP/SMTP", "SettingsEditAccountDetails_IMAPConfiguration_Description": "Zmena nastavenia prichádzajúceho/odchádzajúceho servera.", + "SettingsEditAccountDetails_ImapCalDavSettings_Title": "Nastavenia IMAP a kalendára", + "SettingsEditAccountDetails_ImapCalDavSettings_Description": "Otvorte vyhradenú stránku nastavenia IMAP, SMTP a CalDAV pre tento účet.", + "SettingsEditAccountDetails_ImapCalDavSettings_Action": "Otvoriť nastavenia", "SettingsAbout_Description": "Ďalšie informácie o Wino.", "SettingsAbout_Title": "O aplikácii", "SettingsAboutGithub_Description": "Prejsť na systém sledovania chýb repozitára na Githube.", @@ -490,6 +712,10 @@ "SettingsAppPreferences_SearchMode_Local": "Lokálne", "SettingsAppPreferences_SearchMode_Online": "Online", "SettingsAppPreferences_SearchMode_Title": "Predvolený režim vyhľadávania", + "SettingsAppPreferences_ApplicationMode_Title": "Predvolený režim aplikácie", + "SettingsAppPreferences_ApplicationMode_Description": "Vyberte režim, v ktorom sa Wino otvorí, ak nie je explicitne určený žiadny typ aktivácie.", + "SettingsAppPreferences_ApplicationMode_Mail": "Pošta", + "SettingsAppPreferences_ApplicationMode_Calendar": "Kalendár", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description": "Aplikácia Wino Mail bude naďalej bežať na pozadí. Budete upozornení na príchod novej pošty.", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title": "​​Spustiť na pozadí", "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description": "Aplikácia Wino Mail zostane spustená na paneli úloh. Spustí sa kliknutím na ikonu. Budete upozornení na príchod novej pošty.", @@ -506,12 +732,30 @@ "SettingsAppPreferences_StartupBehavior_FatalError": "Pri zmene režimu spustenia pre Wino Mail sa vyskytla závažná chyba.", "SettingsAppPreferences_StartupBehavior_Title": "Spustiť minimalizované pri spustení Windowsu", "SettingsAppPreferences_Title": "Predvoľby aplikácie", + "SettingsAppPreferences_HideWinoAccountButton_Title": "Skryť tlačidlo účtu Wino v titulnom riadku.", + "SettingsAppPreferences_HideWinoAccountButton_Description": "Skryť tlačidlo profilu v titulnom riadku, ktoré otvára vysúvací panel účtu Wino.", + "SettingsAppPreferences_StoreUpdateNotifications_Title": "Oznámenia o aktualizáciách obchodu", + "SettingsAppPreferences_StoreUpdateNotifications_Description": "Zobraziť upozornenia a akcie na päte stránky, keď je k dispozícii aktualizácia z obchodu Microsoft Store.", + "SettingsAppPreferences_AiActions_Title": "AI akcie", + "SettingsAppPreferences_AiActions_Description": "Vyberte predvolené jazyky AI a miesto, kam sa majú ukladať súhrny.", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Title": "Predvolený jazyk pre preklad", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Description": "Vyberte predvolený cieľový jazyk používaný akciami prekladu AI.", + "SettingsAppPreferences_AiSummarizeLanguage_Title": "Jazyk zhrnutia", + "SettingsAppPreferences_AiSummarizeLanguage_Description": "Zvoľte preferovaný jazyk pre budúce výstupy AI zhrnutia.", + "SettingsAppPreferences_AiSummarySavePath_Title": "Predvolená cesta na uloženie súhrnov", + "SettingsAppPreferences_AiSummarySavePath_Description": "Vyberte priečinok, ktorý by Wino mal používať ako predvolený pri ukladaní AI súhrnov.", + "SettingsAppPreferences_AiSummarySavePath_Placeholder": "Použiť predvolenú polohu ukladania v systéme.", + "SettingsAppPreferences_AiSummarySavePath_InvalidHint": "Tento priečinok neexistuje. Predvolená poloha uloženia bude použitá pre súhrny.", "SettingsAutoSelectNextItem_Description": "Po odstránení alebo presunutí pošty vybrať ďalšiu položku.", "SettingsAutoSelectNextItem_Title": "Automaticky vybrať ďalšiu položku", "SettingsAvailableThemes_Description": "Vyberte motív z vlastnej kolekcie Wino podľa svojho vkusu alebo použite vlastné motívy.", "SettingsAvailableThemes_Title": "Dostupné motívy", "SettingsCalendarSettings_Description": "Zmena prvého dňa v týždni, výška bunky hodiny a ďalšie…", "SettingsCalendarSettings_Title": "Nastavenia kalendára", + "CalendarSettings_DefaultSnoozeDuration_Header": "Predvolená dĺžka odloženia pripomienky", + "CalendarSettings_DefaultSnoozeDuration_Description": "Nastavte predvolenú dĺžku odloženia pre upozornenia pripomienok kalendára.", + "CalendarSettings_TimedDayHeaderFormat_Header": "Formát hlavičky dňa v časovom zobrazení", + "CalendarSettings_TimedDayHeaderFormat_Description": "Vyberte, ako sa horné štítky dní zobrazujú v zobrazeniach deň, týždeň a pracovný týždeň. Použite tokeny formátu dátumu ako ddd, dd, MMM alebo dddd.", "SettingsComposer_Title": "Editor", "SettingsComposerFont_Title": "Predvolené písmo editora", "SettingsComposerFontFamily_Description": "Zmena predvolenej rodiny písma a veľkosť písma pri písaní správ.", @@ -531,6 +775,9 @@ "SettingsDiscord_Title": "Kanál Discord", "SettingsEditLinkedInbox_Description": "Pridať/odstrániť účty, premenovať alebo zrušiť prepojenie medzi účtami.", "SettingsEditLinkedInbox_Title": "Upraviť prepojenie priečinka doručenej pošty", + "SettingsWindowBackdrop_Title": "Pozadie okna", + "SettingsWindowBackdrop_Description": "Vyberte efekt pozadia pre okná Wino.", + "SettingsWindowBackdrop_Disabled": "Výber pozadia okna je zakázaný, keď sa zvolí iný motív aplikácie ako Predvolený.", "SettingsElementTheme_Description": "Vyberte Windows motív pre Wino", "SettingsElementTheme_Title": "Motív prvku", "SettingsElementThemeSelectionDisabled": "Výber motívu prvku je zakázaný, ak je vybraný iný motív aplikácie ako predvolený.", @@ -581,6 +828,8 @@ "SettingsManageAliases_Title": "Aliasy", "SettingsEditAccountDetails_Title": "Úprava podrobností o účte", "SettingsEditAccountDetails_Description": "Zmeňte názov účtu, meno odosielateľa a priraďte novú farbu, ak chcete.", + "EditAccountDetailsPage_SaveSuccess_Title": "Zmeny uložené.", + "EditAccountDetailsPage_SaveSuccess_Message": "Údaje o účte boli úspešne aktualizované.", "SettingsManageLink_Description": "Presunutím položiek môžete pridať nové prepojenie alebo odstrániť existujúce prepojenie.", "SettingsManageLink_Title": "Správa prepojení", "SettingsMarkAsRead_Description": "Zmena toho, čo sa má stať s vybranou položkou.", @@ -596,7 +845,41 @@ "SettingsNotifications_Title": "Oznámenia", "SettingsNotificationsAndTaskbar_Description": "Zmena zobrazenia oznámení a odznaku na paneli úloh pre tento účet.", "SettingsNotificationsAndTaskbar_Title": "Oznámenia a panel úloh", + "SettingsHome_Title": "Domov", + "SettingsHome_SearchTitle": "Nájsť nastavenie", + "SettingsHome_SearchDescription": "Hľadajte podľa funkcie, témy alebo kľúčového slova a rýchlo sa dostanete na správnu stránku nastavení.", + "SettingsHome_SearchPlaceholder": "Hľadať nastavenia", + "SettingsHome_SearchExamples": "Skúste: téma, úložisko, jazyk, podpis", + "SettingsHome_QuickLinks_Title": "Rýchle odkazy", + "SettingsHome_QuickLinks_Description": "Prejdite na nastavenia, ku ktorým majú používatelia najčastejší prístup.", + "SettingsHome_StorageCard_Description": "Zistite, koľko lokálneho MIME obsahu Wino uchováva na tomto zariadení a podľa potreby ho vymažte.", + "SettingsHome_StorageEmptySummary": "Žiadny cache MIME obsahu zatiaľ nebol zistený.", + "SettingsHome_StorageLoading": "Prebieha kontrola lokálneho MIME obsahu...", + "SettingsHome_Tips_Title": "Tipy a triky", + "SettingsHome_Tips_Description": "Niekoľko malých zmien môže spraviť Wino oveľa osobnejším.", + "SettingsHome_Tip_Theme": "Chcete tmavý režim alebo zmeny farieb? Otvorte Personalizáciu.", + "SettingsHome_Tip_Background": "Použite nastavenia aplikácie na riadenie spúšťania a synchronizácie na pozadí.", + "SettingsHome_Tip_Shortcuts": "Klávesové skratky vám pomáhajú rýchlejšie prechádzať po pošte.", + "SettingsHome_Resources_Title": "Užitočné odkazy", + "SettingsHome_Resources_Description": "Otvorte zdroje projektu, informácie o podpore a kanály vydania.", "SettingsOptions_Title": "Nastavenia", + "SettingsOptions_GeneralSection": "Všeobecné", + "SettingsOptions_MailSection": "Pošta", + "SettingsOptions_CalendarSection": "Kalendár", + "SettingsOptions_MoreComingSoon": "Ďalšie možnosti čoskoro", + "SettingsOptions_HeroDescription": "Prispôsobte si skúsenosť s Wino Mail.", + "SettingsOptions_AccountsSummary": "{0} účtov nakonfigurovaných", + "SettingsSearch_ManageAccounts_Keywords": "účet;účty;poštová schránka;poštové schránky;alias;aliasy;profil;adresa;adresy", + "SettingsSearch_AppPreferences_Keywords": "štartovanie;pozadie;spustenie;synchronizácia;upozornenia;upozornenia;vyhľadávanie;panel;predvolené", + "SettingsSearch_LanguageTime_Keywords": "jazyk;čas;hodiny;lokalita;región;formát;24 hodín;24h", + "SettingsSearch_Personalization_Keywords": "téma;tmavý;svetlý;vzhľad;akcent;farba;farba;režim;rozloženie;hustota", + "SettingsSearch_About_Keywords": "o;verzia;webstránka;súkromie;github;prispieť;obchod;podpora", + "SettingsSearch_KeyboardShortcuts_Keywords": "skratka;skratky;klávesová skratka;klávesové skratky;klávesy", + "SettingsSearch_MessageList_Keywords": "správa;správy;zoznam;vlákno;vlákna;avatar;náhľad;odosielateľ", + "SettingsSearch_ReadComposePane_Keywords": "čitateľ;skladanie;editor;písmo;písma;externý obsah;zobrazenie;čítanie", + "SettingsSearch_SignatureAndEncryption_Keywords": "podpis;podpisy;šifrovanie;certifikát;certifikáty;smime;smime;bezpečnosť", + "SettingsSearch_Storage_Keywords": "úložisko;cache;kešovanie;mime;disk;miesto;čistenie;vyčistiť;lokálne dáta", + "SettingsSearch_CalendarSettings_Keywords": "kalendár;týždeň;hodiny;harmonogram;udalosti", "SettingsPaneLengthReset_Description": "Ak máte problém so zoznamom správ, obnovte ho na predvolenú veľkosť.", "SettingsPaneLengthReset_Title": "Obnoviť veľkosť zoznamu správ", "SettingsPaypal_Description": "Prejavte oveľa viac lásky ❤️ Všetky dary sú vítané.", @@ -610,6 +893,8 @@ "SettingsPrefer24HourClock_Title": "Zobraziť 24-hodinový formát času", "SettingsPrivacyPolicy_Description": "Prečítajte si Ochranu osobných údajov.", "SettingsPrivacyPolicy_Title": "Ochrany osobných údajov", + "SettingsWebsite_Description": "Otvorte webovú stránku Wino Mail.", + "SettingsWebsite_Title": "Webstránka", "SettingsReadComposePane_Description": "Písmo, externý obsah.", "SettingsReadComposePane_Title": "Prehliadač a editor", "SettingsReader_Title": "Prehliadač", @@ -625,6 +910,19 @@ "SettingsShowPreviewText_Title": "Zobraziť náhľad textu", "SettingsShowSenderPictures_Description": "Skryť/zobraziť miniatúry obrázkov odosielateľa.", "SettingsShowSenderPictures_Title": "Zobraziť avatary odosielateľa", + "SettingsEmailTemplates_Title": "Šablóny e-mailov", + "SettingsEmailTemplates_Description": "Spravujte šablóny e-mailov.", + "SettingsEmailTemplates_CreatePageTitle": "Nová šablóna", + "SettingsEmailTemplates_EditPageTitle": "Upraviť šablónu", + "SettingsEmailTemplates_NewTemplateTitle": "Nová šablóna", + "SettingsEmailTemplates_NewTemplateDescription": "Vytvorte novú šablónu e-mailu.", + "SettingsEmailTemplates_NameTitle": "Názov", + "SettingsEmailTemplates_NamePlaceholder": "Názov šablóny", + "SettingsEmailTemplates_DescriptionTitle": "Popis", + "SettingsEmailTemplates_DescriptionPlaceholder": "Voliteľný popis", + "SettingsEmailTemplates_ContentTitle": "Obsah šablóny", + "SettingsEmailTemplates_ContentDescription": "Upravte HTML obsah pre túto šablónu.", + "SettingsEmailTemplates_NameRequired": "Názov šablóny je povinný.", "SettingsEnableGravatarAvatars_Title": "Gravatar", "SettingsEnableGravatarAvatars_Description": "Use gravatar (if available) as sender picture", "SettingsEnableFavicons_Title": "Domain icons (Favicons)", @@ -645,6 +943,33 @@ "SettingsStartupItem_Title": "Účet po spustení", "SettingsStore_Description": "Ukážte trochu lásky ❤️", "SettingsStore_Title": "Hodnotenie v Microsoft Store", + "SettingsStorage_Title": "Úložisko", + "SettingsStorage_Description": "Skenovať a spravovať MIME cache uloženú vo vašom lokálnom dátovom priečinku.", + "SettingsStorage_ScanFolder": "Skenovať lokálny dátový priečinok", + "SettingsStorage_NoLocalMimeDataFound": "Neboli nájdené žiadne lokálne MIME dáta.", + "SettingsStorage_NoAccountsFound": "Neboli nájdené žiadne účty.", + "SettingsStorage_TotalUsage": "Celkové lokálne používanie MIME: {0}", + "SettingsStorage_AccountUsageDescription": "Použité {0} v lokálnom MIME cache.", + "SettingsStorage_DeleteAll_Title": "Odstrániť všetok MIME obsah", + "SettingsStorage_DeleteAll_Description": "Odstrániť celú zložku MIME cache pre tento účet.", + "SettingsStorage_DeleteAll_Button": "Odstrániť všetko", + "SettingsStorage_DeleteAll_Confirm_Title": "Odstrániť všetok MIME obsah", + "SettingsStorage_DeleteAll_Confirm_Message": "Odstrániť všetky lokálne MIME dáta pre {0}?", + "SettingsStorage_DeleteAll_Success": "Všetok MIME obsah bol odstránený.", + "SettingsStorage_DeleteOld_Title": "Odstrániť starý MIME obsah", + "SettingsStorage_DeleteOld_Description": "Odstrániť MIME súbory na základe dátumu vytvorenia správy v lokálnej databáze.", + "SettingsStorage_DeleteOld_1Month": "> 1 mesiac", + "SettingsStorage_DeleteOld_3Months": "> 3 mesiacov", + "SettingsStorage_DeleteOld_6Months": "> 6 mesiacov", + "SettingsStorage_DeleteOld_1Year": "> 1 rok", + "SettingsStorage_DeleteOld_Confirm_Title": "Odstrániť starý MIME obsah", + "SettingsStorage_DeleteOld_Confirm_Message": "Odstrániť lokálne MIME dáta staršie ako {0} pre {1}?", + "SettingsStorage_DeleteOld_Success": "Odstránené {0} MIME priečinok(ov) starších ako {1}.", + "SettingsStorage_1Month": "1 mesiac", + "SettingsStorage_3Months": "3 mesiace", + "SettingsStorage_6Months": "6 mesiacov", + "SettingsStorage_1Year": "1 rok", + "SettingsStorage_Months": "{0} mesiacov", "SettingsTaskbarBadge_Description": "Zahrnúť počet neprečítaných správ do ikony na paneli úloh.", "SettingsTaskbarBadge_Title": "Odznak na paneli úloh", "SettingsThreads_Description": "Usporiadanie správ do konverzácií.", @@ -683,6 +1008,9 @@ "SystemFolderConfigDialogValidation_InboxSelected": "Priečinok Doručená pošta nemôžete priradiť k žiadnemu inému systémovému priečinku.", "SystemFolderConfigSetupSuccess_Message": "Systémové priečinky sú úspešne nakonfigurované.", "SystemFolderConfigSetupSuccess_Title": "Nastavenie systémových priečinkov", + "SystemTrayMenu_ShowWino": "Otvoriť Wino Mail", + "SystemTrayMenu_ShowWinoCalendar": "Otvoriť Wino Kalendár", + "SystemTrayMenu_ExitWino": "Ukončiť", "TestingImapConnectionMessage": "Testovanie pripojenia k serveru…", "TitleBarServerDisconnectedButton_Description": "Wino je odpojený od siete. Kliknutím na tlačidlo obnoviť pripojenie obnovte pripojenie.", "TitleBarServerDisconnectedButton_Title": "žiadne pripojenie", @@ -699,8 +1027,422 @@ "WinoUpgradeMessage": "Navýšiť na neobmedzený počet", "WinoUpgradeRemainingAccountsMessage": "Využitie bezplatných účtov: {0}/{1}.", "Yesterday": "Včera", + "Smime_ImportCertificates_Success": "Certifikáty úspešne importované.", + "Smime_ImportCertificates_Error": "Chyba pri importe certifikátov: {0}", + "Smime_RemoveCertificates_Confirm": "Naozaj chcete odstrániť certifikáty {0}?", + "Smime_RemoveCertificates_Success": "Certifikáty odstránené.", + "Smime_ExportCertificates_Success": "Certifikáty exportované.", + "Smime_ExportCertificates_Error": "Chyba pri exporte certifikátov.", + "Smime_CertificateDetails": "Predmet: {0}\nVydavateľ: {1}\nPlatné od: {2}\nPlatné do: {3}\nOtlačok: {4}", + "Smime_CertificatePassword_Title": "Heslo certifikátu je vyžadované", + "Smime_CertificatePassword_Placeholder": "Heslo certifikátu pre {0} (nepovinné)", + "Smime_Confirm_Title": "Potvrdiť", + "Buttons_OK": "OK", + "Buttons_Refresh": "Obnoviť", + "SettingsSignatureAndEncryption_Title": "Podpis a šifrovanie", + "SettingsSignatureAndEncryption_Description": "Spravovať certifikáty S/MIME pre podpisovanie a šifrovanie e-mailov.", + "SettingsSignatureAndEncryption_MyCertificatesHeader": "Moje certifikáty", + "SettingsSignatureAndEncryption_MyCertificatesDescription": "Osobné certifikáty pre podpisovanie a šifrovanie", + "SettingsSignatureAndEncryption_RecipientCertificatesHeader": "Certifikáty príjemcov", + "SettingsSignatureAndEncryption_RecipientCertificatesDescription": "Certifikáty príjemcov na dešifrovanie", + "SettingsSignatureAndEncryption_NameColumn": "Názov", + "SettingsSignatureAndEncryption_ExpiresColumn": "Dátum expirácie", + "SettingsSignatureAndEncryption_ThumbprintColumn": "Otlačok", + "Buttons_Remove": "Odstrániť", + "Buttons_Export": "Exportovať", + "Buttons_Import": "Importovať", + "SettingsSignatureAndEncryption_SigningCertificate": "Podpisovací certifikát S/MIME", + "SettingsSignatureAndEncryption_EncryptionCertificate": "Šifrovací certifikát S/MIME", + "SettingsSignatureAndEncryption_SigningCertificatePlaceholder": "Žiadny", + "SmimeSignaturesInMessage": "Podpisy v tejto správe:", + "SmimeSignatureEntry": "• {0} {1} ({2}, platný do {3} - {4})", + "SmimeSigningCertificateInfoTitle": "Informácie o podpisovacom certifikáte S/MIME", + "SmimeCertificateInfoTitle": "Informácie o certifikáte S/MIME", + "SmimeNoCertificateFileFound": "Nebol nájdený súbor certifikátu", + "SmimeSaveCertificate": "Uložiť certifikát...", + "SmimeCertificate": "Certifikát S/MIME", + "SmimeCertificateSavedTo": "Certifikát uložený do {0}", + "SmimeSignedTooltip": "Táto správa je podpísaná certifikátom S/MIME. Kliknite pre viac podrobností", + "SmimeEncryptedTooltip": "Táto správa je šifrovaná certifikátom S/MIME.", + "SmimeCertificateFileInfo": "Súbor: {0}\nTyp: {1}\nVeľkosť: {2:N0} bajtov", + "Composer_LightTheme": "Svetlá téma", + "Composer_DarkTheme": "Tmavá téma", + "Composer_Outdent": "Znížiť odsadenie", + "Composer_Indent": "Odsadiť", + "Composer_BulletList": "Zoznam odrážok", + "Composer_OrderedList": "Číslovaný zoznam", + "Composer_Stroke": "Čiara", + "Composer_Bold": "Tučné", + "Composer_Italic": "Kurzíva", + "Composer_Underline": "Podčiarknuté", + "Composer_CcBcc": "Kópia (Cc) a skrytá kópia (Bcc)", + "Composer_EnableSmimeSignature": "Povoliť/Zakázať podpis S/MIME", + "Composer_EnableSmimeEncryption": "Povoliť/Zakázať šifrovanie S/MIME", + "Composer_LocalDraftSyncInfo": "Tento koncept je iba lokálny. Wino sa nepodarilo odoslať na váš poštový server. Kliknite pre opätovné odoslanie na server.", + "Composer_CertificateExpires": "Vyprší dňa: ", + "Composer_SmimeSignature": "Podpis S/MIME", + "Composer_SmimeEncryption": "Šifrovanie S/MIME", + "Composer_EmailTemplatesPlaceholder": "Šablóny e-mailov", + "Composer_AiSummarize": "Zhrnúť pomocou AI", + "Composer_AiSummarizeDescription": "Získajte kľúčové body, akčné úlohy a rozhodnutia z tohto e-mailu.", + "Composer_AiTranslate": "Preložiť pomocou AI", + "Composer_AiActions": "AI akcie", + "Composer_AiRewrite": "Prepísať pomocou AI", + "AiActions_CheckingStatus": "Kontrolujem prístup k AI...", + "AiActions_SignedOutTitle": "Odomknúť Wino AI balík", + "AiActions_SignedOutDescription": "Prekladať, prepisovať a zhrňovať e-maily pomocou AI po prihlásení do svojho účtu Wino a aktivovaní doplnku AI Pack.", + "AiActions_NoPackTitle": "AI balík vyžadovaný", + "AiActions_NoPackDescription": "Ste prihlásený, ale AI balík ešte nie je aktívny. Kúpte si ho, aby ste mohli používať nástroje Wino AI pre preklad, prepísanie a zhrnutie e-mailov.", + "AiActions_UsageSummary": "{0} z {1} kreditov použitých tento mesiac.", + "Composer_AiRewritePolite": "Urob to zdvorilé", + "Composer_AiRewritePoliteDescription": "Zjemní formuláciu pri zachovaní rovnakého zámeru.", + "Composer_AiRewriteAngry": "Urob to agresívne", + "Composer_AiRewriteAngryDescription": "Používa ostrejší a konfrontačný tón.", + "Composer_AiRewriteHappy": "Urob to veselým", + "Composer_AiRewriteHappyDescription": "Pridá viac pozitívny a nadšenejší tón.", + "Composer_AiRewriteFormal": "Urob to formálne.", + "Composer_AiRewriteFormalDescription": "Znie správu profesionálnejšie a štruktúrovanejšie.", + "Composer_AiRewriteFriendly": "Urob to priateľskejším.", + "Composer_AiRewriteFriendlyDescription": "Zjemní správu a pridá priateľskejší tón.", + "Composer_AiRewriteShorter": "Urob to stručnejšie.", + "Composer_AiRewriteShorterDescription": "Zostrí text a odstráni zbytočné detaily.", + "Composer_AiRewriteClearer": "Urob to jasnejším.", + "Composer_AiRewriteClearerDescription": "Zlepší čitateľnosť a uľahčí pochopenie správy.", + "Composer_AiRewriteCustom": "Vlastné", + "Composer_AiRewriteCustomDescription": "Popíšte, aký prepis chcete.", + "Composer_AiRewriteCustomPlaceholder": "Popíšte, ako chcete správu prepísať.", + "Composer_AiRewriteMode": "Prepis tónu", + "Composer_AiRewriteApply": "Aplikovať prepis", + "Composer_AiTranslateDialogTitle": "Preložiť pomocou AI", + "Composer_AiTranslateDialogDescription": "Zadajte cieľový jazyk alebo kultúrny kód, napr. en-US, tr-TR, de-DE alebo fr-FR.", + "Composer_AiTranslateApply": "Preložiť", + "Composer_AiTranslateLanguage": "Cieľový jazyk", + "Composer_AiTranslateCustomPlaceholder": "Zadajte kultúrny kód", + "Composer_AiTranslateLanguageEnglish": "Angličtina (en-US)", + "Composer_AiTranslateLanguageTurkish": "Turečtina (tr-TR)", + "Composer_AiTranslateLanguageGerman": "Nemecký (de-DE)", + "Composer_AiTranslateLanguageFrench": "Francúzština (fr-FR)", + "Composer_AiTranslateLanguageSpanish": "Španielčina (es-ES)", + "Composer_AiTranslateLanguageItalian": "Taliančina (it-IT)", + "Composer_AiTranslateLanguagePortugueseBrazil": "portugalčina (Brazília) (pt-BR)", + "Composer_AiTranslateLanguageDutch": "holandčina (nl-NL)", + "Composer_AiTranslateLanguagePolish": "poľština (pl-PL)", + "Composer_AiTranslateLanguageRussian": "ruština (ru-RU)", + "Composer_AiTranslateLanguageJapanese": "japončina (ja-JP)", + "Composer_AiTranslateLanguageKorean": "kórejčina (ko-KR)", + "Composer_AiTranslateLanguageChineseSimplified": "čínština, zjednodušená (zh-CN)", + "Composer_AiTranslateLanguageArabic": "arabčina (ar-SA)", + "Composer_AiTranslateLanguageHindi": "hindčina (hi-IN)", + "Composer_AiTranslateLanguageOther": "Iné...", + "Composer_AiBusyTitle": "AI už pracuje", + "Composer_AiBusyMessage": "Prosím, počkajte, kým sa dokončí aktuálna AI akcia.", + "Composer_AiSignInRequired": "Prihláste sa do svojho konta Wino, aby ste mohli používať funkcie AI.", + "Composer_AiMissingHtml": "Zatiaľ neexistuje žiadny obsah správy na odoslanie do Wino AI.", + "Composer_AiQuotaUnavailable": "Výsledok AI bol použitý.", + "Composer_AiAppliedMessage": "Výsledok AI bol aplikovaný do editora. Ak chcete vrátiť zmenu, použite Zrušiť.", + "Composer_AiSummarizeSuccessTitle": "AI súhrn aplikovaný", + "Composer_AiTranslateSuccessTitle": "AI preklad aplikovaný", + "Composer_AiRewriteSuccessTitle": "AI prepis aplikovaný", + "Composer_AiErrorTitle": "AI akcia zlyhala", + "Reader_AiAppliedMessage": "Výsledok AI je pre túto správu teraz zobrazený. Znovu otvorením správy zobrazíte pôvodný obsah.", "SettingsAppPreferences_EmailSyncInterval_Title": "Email sync interval", - "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail." + "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail.", + "ContactsPage_Title": "Kontakty", + "ContactsPage_AddContact": "Pridať kontakt", + "ContactsPage_EditContact": "Upraviť kontakt", + "ContactsPage_DeleteContact": "Odstrániť kontakt", + "ContactsPage_SearchPlaceholder": "Hľadať kontakty...", + "ContactsPage_NoContacts": "Žiadne kontakty na zobrazenie", + "ContactsPage_ContactsCount": "{0} kontaktov", + "ContactsPage_SelectedContactsCount": "{0} vybratých", + "ContactsPage_DeleteSelectedContacts": "Odstrániť vybrané", + "ContactEditDialog_Title": "Upraviť kontakt", + "ContactEditDialog_PhotoSection": "Fotka", + "ContactEditDialog_ChoosePhoto": "Vybrať fotku", + "ContactEditDialog_RemovePhoto": "Odstrániť fotku", + "ContactEditDialog_NameHeader": "Meno", + "ContactEditDialog_NamePlaceholder": "Meno kontaktu", + "ContactEditDialog_EmailHeader": "Emailová adresa", + "ContactEditDialog_EmailPlaceholder": "contact@example.com", + "ContactEditDialog_InfoSection": "Informácie o kontakte", + "ContactEditDialog_RootContactInfo": "Toto je koreňový kontakt spojený s vašimi kontami a nie je možné ho zmazať.", + "ContactEditDialog_OverriddenContactInfo": "Tento kontakt bol ručne upravený a počas synchronizácie sa nebude aktualizovať.", + "ContactsPage_Subtitle": "Spravujte svoje e-mailové kontakty a ich informácie", + "ContactStatus_Account": "Účet", + "ContactStatus_Modified": "Upravený", + "ContactAction_Edit": "Upraviť kontakt", + "ContactAction_ChangePhoto": "Zmeniť foto", + "ContactAction_Delete": "Odstrániť kontakt", + "ContactAction_Add": "Pridať kontakt", + "ContactSelection_Selected": "označené", + "ContactSelection_SelectAll": "Vybrať všetko", + "ContactSelection_Clear": "Vyčistiť výber", + "ContactsPage_EmptyState": "Žiadne kontakty na zobrazenie", + "ContactsPage_AddFirstContact": "Pridajte svoj prvý kontakt", + "ContactsPage_ContactsCountSuffix": "kontakty", + "ContactsPane_NewContact": "Nový kontakt", + "ContactsPane_DescriptionTitle": "Spravovať svoje kontakty", + "ContactsPane_DescriptionBody": "Vytvárajte kontakty, premenovávajte ich, aktualizujte profilové fotky a majte uložené údaje usporiadané na jednom mieste.", + "ContactEditDialog_AddTitle": "Pridať kontakt", + "ContactInfoBar_ContactAdded": "Kontakt bol úspešne pridaný.", + "ContactInfoBar_ContactUpdated": "Kontakt bol úspešne aktualizovaný.", + "ContactInfoBar_ContactsDeleted": "Kontakty boli úspešne odstránené.", + "ContactInfoBar_ContactPhotoUpdated": "Fotka kontaktu bola úspešne aktualizovaná.", + "ContactInfoBar_FailedToLoadContacts": "Nepodarilo sa načítať kontakty: {0}", + "ContactInfoBar_FailedToAddContact": "Nepodarilo sa pridať kontakt: {0}", + "ContactInfoBar_FailedToUpdateContact": "Nepodarilo sa aktualizovať kontakt: {0}", + "ContactInfoBar_FailedToDeleteContacts": "Nepodarilo sa odstrániť kontakty: {0}", + "ContactInfoBar_FailedToUpdatePhoto": "Nepodarilo sa aktualizovať fotku: {0}", + "ContactInfoBar_CannotDeleteRoot": "Koreňové kontakty sa nedajú odstrániť.", + "ContactConfirmDialog_DeleteTitle": "Odstrániť kontakt", + "ContactConfirmDialog_DeleteMessage": "Ste si istí, že chcete odstrániť kontakt '{0}'?", + "ContactConfirmDialog_DeleteMultipleMessage": "Ste si istí, že chcete odstrániť {0} kontaktov?", + "ContactConfirmDialog_DeleteButton": "Odstrániť", + "CalendarAccountSettings_Title": "Nastavenia kalendára účtu", + "CalendarAccountSettings_Description": "Spravujte nastavenia kalendára pre {0}", + "CalendarAccountSettings_AccountColor": "Farba účtu", + "CalendarAccountSettings_AccountColorDescription": "Zmeňte zobrazovaciu farbu pre tento kalendárny účet", + "CalendarAccountSettings_SyncEnabled": "Povoliť synchronizáciu", + "CalendarAccountSettings_SyncEnabledDescription": "Povoliť alebo zakázať synchronizáciu kalendára pre tento účet", + "CalendarAccountSettings_DefaultShowAs": "Predvolený stav dostupnosti", + "CalendarAccountSettings_DefaultShowAsDescription": "Predvolený stav dostupnosti pre nové udalosti vytvorené s týmto účtom", + "CalendarAccountSettings_PrimaryCalendar": "Hlavný kalendár", + "CalendarAccountSettings_PrimaryCalendarDescription": "Označte tento kalendár ako hlavný kalendár pre účet", + "CalendarSettings_NewEventBehavior_Header": "Správanie tlačidla Nová udalosť", + "CalendarSettings_NewEventBehavior_Description": "Vyberte, či tlačidlo Nová udalosť by malo zakaždým žiadať kalendár, alebo by malo vždy otvárať špecifický kalendár.", + "CalendarSettings_NewEventBehavior_AskEachTime": "Spýtať sa vždy.", + "CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar": "Vždy použiť konkrétny kalendár.", + "CalendarSettings_Rendering_Title": "Zobrazenie", + "CalendarSettings_Rendering_Description": "Nastavte rozloženie kalendára a spôsob zobrazenia.", + "CalendarSettings_Notifications_Title": "Upozornenia", + "CalendarSettings_Notifications_Description": "Zvoľte predvolené pripomínanie a správanie pri odkladaní.", + "CalendarSettings_Preferences_Title": "Predvoľby", + "CalendarSettings_Preferences_Description": "Nastavte, ako má tlačidlo Nová udalosť fungovať.", + "WhatIsNew_GetStartedButton": "Začať", + "WhatIsNew_ContinueAnywayButton": "Pokračovať aj tak.", + "WhatIsNew_PreparingForNewVersionButton": "Príprava na novú verziu...", + "WhatIsNew_MigrationPreparing_Title": "Pripravujeme vaše údaje", + "WhatIsNew_MigrationPreparing_Description": "Wino aplikuje migračné aktualizácie. Počkajte prosím, kým pripravíme údaje vášho účtu pre túto verziu.", + "WhatIsNew_MigrationFailedMessage": "Aplikácia migračných aktualizácií zlyhala s chybovým kódom {0}. Môžete ďalej používať aplikáciu. Ak však narazíte na vážne problémy, nainštalujte aplikáciu znova.", + "WhatIsNew_MigrationNotification_Title": "Wino Mail je aktualizovaný", + "WhatIsNew_MigrationNotification_Message": "Otvorte aplikáciu, aby ste dokončili aktualizáciu a zobrazili novinky.", + "WelcomeWindow_Title": "Vitajte v Wino Mail", + "WelcomeWindow_Subtitle": "Nativné Windows prostredie pre Poštu a Kalendár.", + "WelcomeWindow_WhatsNewTitle": "Najnovšie zmeny", + "WelcomeWindow_FeaturesTitle": "Funkcie", + "WelcomeWindow_WhatsNewTab": "Čo je nové", + "WelcomeWindow_FeaturesTab": "Funkcie", + "WelcomeWindow_GetStartedButton": "Začať pridaním účtu", + "WelcomeWindow_GetStartedDescription": "Pridajte svoj účet Outlook, Gmail alebo IMAP a začnite používať Wino Mail.", + "WelcomeWindow_ImportFromWinoAccount": "Importovať zo svojho Wino účtu", + "WelcomeWindow_ImportInProgress": "Importujem synchronizované preferencie a účty...", + "WelcomeWindow_ImportNoAccountsFound": "Nenašli sa žiadne synchronizované účty vo vašom Wino účte. Ak boli k dispozícii preferencie, boli obnovené. Použite možnosť Začať na manuálne pridanie účtu.", + "WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} synchronizovaných účtov je už k dispozícii na tomto zariadení. Ak potrebujete, použite možnosť Začať na ručné pridanie ďalšieho účtu.", + "WelcomeWindow_SetupTitle": "Nastavte si svoj účet", + "WelcomeWindow_SetupSubtitle": "Vyberte poskytovateľa e-mailu a začnite.", + "WelcomeWindow_AddAccountButton": "Pridať účet", + "WelcomeWindow_SkipForNow": "Preskočiť na teraz — nastavím to neskôr.", + "WelcomeWindow_AppDescription": "Rýchly, sústredený inbox — prepracovaný pre Windows 11.", + "WelcomeWizard_Step1Title": "Vitajte", + "SystemTrayMenu_Open": "Otvoriť", + "WinoAccount_Titlebar_SyncBenefitTitle": "Nastavenia synchronizácie", + "WinoAccount_Titlebar_SyncBenefitDescription": "Udržujte svoje nastavenia Wino synchronizované naprieč zariadeniami.", + "WinoAccount_Titlebar_AddonsBenefitTitle": "Odomknúť doplnky", + "WinoAccount_Titlebar_AddonsBenefitDescription": "Získajte prístup k prémiovým funkciám, ako napríklad Wino AI Pack.", + "WinoAccount_Management_Description": "Spravujte svoj účet Wino, prístup k AI Pack a synchronizované preferencie a údaje o účte.", + "WinoAccount_Management_SignedOutTitle": "Prihláste sa do Wino Mail", + "WinoAccount_Management_SignedOutDescription": "Prihláste sa alebo vytvorte účet na synchronizáciu e-mailov, prístup k AI funkciám a správu nastavení naprieč zariadeniami.", + "WinoAccount_Management_ProfileSectionHeader": "Profil", + "WinoAccount_Management_AddOnsSectionHeader": "Doplnky Wino", + "WinoAccount_Management_DataSectionHeader": "Údaje", + "WinoAccount_Management_AccountActionsSectionHeader": "Akcie účtu", + "WinoAccount_Management_AccountCardTitle": "Účet", + "WinoAccount_Management_AccountCardDescription": "E-mailová adresa vášho účtu Wino a aktuálny stav účtu.", + "WinoAccount_Management_AiPackCardTitle": "AI balík", + "WinoAccount_Management_AiPackCardDescription": "Zistite, či je Wino AI balík aktívny a koľko využitia zostáva.", + "WinoAccount_Management_AiPackActive": "AI balík je aktívny", + "WinoAccount_Management_AiPackInactive": "AI balík nie je aktívny", + "WinoAccount_Management_AiPackUsage": "{0} z {1} použití bolo spotrebovaných. Zostáva {2}.", + "WinoAccount_Management_AiPackBillingPeriod": "Fakturačné obdobie: {0:d} - {1:d}", + "WinoAccount_Management_AiPackUnknownUsage": "Podrobnosti o využívaní AI ešte nie sú k dispozícii.", + "WinoAccount_Management_AiPackBuyDescription": "Kúpte Wino AI Pack, aby ste mohli prekladať, prepisovať alebo zhrňovať e-maily pomocou AI.", + "WinoAccount_Management_AiPackPromoTitle": "Odomknúť AI balík", + "WinoAccount_Management_AiPackPromoDescription": "Vylepšite svoje pracovné postupy e-mailov s nástrojmi poháňanými AI. Prekladajte správy do viac ako 50 jazykov, prepisujte pre jasnosť a tón a získajte okamžité zhrnutia dlhých konverzácií.", + "WinoAccount_Management_AiPackPromoPrice": "$4.99 / mesiac", + "WinoAccount_Management_AiPackPromoRequests": "1 000 kreditov", + "WinoAccount_Management_AiPackGetButton": "Získať AI balík", + "WinoAddOn_AI_PACK_Name": "Wino AI Balík", + "WinoAddOn_AI_PACK_Description": "Nástroje poháňané AI na preklad, prepisovanie a zhrnutie akcií v Wino Mail.", + "WinoAddOn_AI_PACK_Keywords": "AI, preklad, prepisovanie, zhrnutie, produktivita", + "WinoAddOn_UNLIMITED_ACCOUNTS_Name": "Neobmedzené účty", + "WinoAddOn_UNLIMITED_ACCOUNTS_Description": "Odstráňte obmedzenie účtov a pridajte toľko e-mailových účtov, koľko potrebujete.", + "WinoAddOn_UNLIMITED_ACCOUNTS_Keywords": "účty, neobmedzené, prémiové, doplnok", + "WinoAccount_Management_PurchaseRequiresSignIn": "Prihláste sa cez svoj účet Wino na dokončenie nákupu.", + "WinoAccount_Management_PurchaseStartFailed": "Wino nemohlo dokončiť tento nákup v Microsoft Store.", + "WinoAccount_Management_StoreSyncFailed": "Nákup bol dokončený, no Wino nemôže obnoviť výhody účtu. Skúste to prosím o chvíľu znova.", + "WinoAccount_Management_AiPackSubscriptionActive": "Predplatné je aktívne", + "WinoAccount_Management_AiPackRenews": "Obnovuje sa {0:d}", + "WinoAccount_Management_AiPackRequestsUsed": "Využité kredity tento mesiac", + "WinoAccount_Management_AiPackResets": "Resetov {0:d}", + "WinoAccount_Management_AiPackUsageLoadFailed": "Nepodarilo sa načítať stav používania AI.", + "WinoAccount_Management_AiPackFeatureTranslate": "Preklad", + "WinoAccount_Management_AiPackFeatureRewrite": "Prepisovanie", + "WinoAccount_Management_AiPackFeatureSummarize": "Zhrnutie", + "WinoAccount_Management_AddOnLoadFailed": "Pri načítaní tohto doplnku došlo k problémom.", + "WinoAccount_Management_SyncPreferencesTitle": "Synchronizovať predvoľby a účty", + "WinoAccount_Management_SyncPreferencesDescription": "Importujte alebo exportujte svoje predvoľby Wino a podrobnosti poštovej schránky naprieč zariadeniami. Heslá, tokeny a ďalšie citlivé informácie sa nikdy nesynchronizujú.", + "WinoAccount_Management_SignOutTitle": "Odhlásiť sa", + "WinoAccount_Management_SignOutDescription": "Odhláste sa zo svojho účtu na tomto zariadení", + "WinoAccount_Management_StatusLabel": "Stav: {0}", + "WinoAccount_Management_NoRemoteSettings": "Pre tento účet ešte nie sú uložené žiadne synchronizované údaje.", + "WinoAccount_Management_ExportSucceeded": "Vybrané údaje Wino boli úspešne exportované.", + "WinoAccount_Management_ExportPreferencesSucceeded": "Vaše predvoľby boli exportované do vášho Wino účtu.", + "WinoAccount_Management_ExportAccountsSucceeded": "Exportované {0} podrobnosti účtu do vášho Wino účtu.", + "WinoAccount_Management_ImportSucceeded": "Synchronizované údaje z vášho Wino účtu boli importované.", + "WinoAccount_Management_ImportPreferencesSucceeded": "Aplikované {0} synchronizované predvoľby.", + "WinoAccount_Management_ImportAccountsSucceeded": "Importované {0} účty.", + "WinoAccount_Management_ImportDuplicateAccountsSkipped": "Vynechané {0} účtov, ktoré už na tomto zariadení existujú.", + "WinoAccount_Management_ImportPartial": "Použité bolo {0} synchronizovaných predvolieb. {1} predvolieb sa nepodarilo obnoviť.", + "WinoAccount_Management_ImportReloginReminder": "Heslá, tokeny a ďalšie citlivé údaje neboli importované. Na každom účte na tomto zariadení sa pred použitím znova prihláste.", + "WinoAccount_Management_SerializeFailed": "Wino nemohol serializovať vaše aktuálne predvoľby.", + "WinoAccount_Management_EmptyExport": "Nie sú žiadne hodnoty predvolieb na export.", + "WinoAccount_Management_ImportEmpty": "Synchronizované dáta neobsahujú nič nové na obnovenie.", + "WinoAccount_Management_ExportDialog_Title": "Export do vášho Wino účtu", + "WinoAccount_Management_ExportDialog_Description": "Vyberte, čo chcete synchronizovať so svojím Wino účtom.", + "WinoAccount_Management_ExportDialog_IncludePreferences": "Predvoľby", + "WinoAccount_Management_ExportDialog_IncludeAccounts": "Účty", + "WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Heslá, tokeny a ďalšie citlivé údaje nie sú synchronizované.", + "WinoAccount_Management_ExportDialog_AccountsRelogin": "Importované účty na inom PC budú vyžadovať opätovné prihlásenie pred ich použitím.", + "WinoAccount_Management_ExportDialog_InProgress": "Exportujem vybrané údaje Wino.", + "WinoAccount_Management_LoadFailed": "Wino nemohol načítať najnovšie informácie o Wino účte.", + "WinoAccount_Management_ActionFailed": "Žiadosť o Wino účet nemohla byť dokončená.", + "WinoAccount_SettingsSection_Title": "Wino účet", + "WinoAccount_SettingsSection_Description": "Vytvorte alebo sa prihláste do Wino účtu pomocou lokálnej autentifikačnej služby.", + "WinoAccount_RegisterButton_Title": "Registrovať účet", + "WinoAccount_RegisterButton_Description": "Vytvorte účet Wino s e-mailom a heslom.", + "WinoAccount_RegisterButton_Action": "Otvoriť registráciu", + "WinoAccount_LoginButton_Title": "Prihlásiť sa", + "WinoAccount_LoginButton_Description": "Prihláste sa do existujúceho Wino účtu pomocou e-mailu a hesla.", + "WinoAccount_LoginButton_Action": "Otvoriť prihlásenie", + "WinoAccount_SignOutButton_Title": "Odhlásiť sa", + "WinoAccount_SignOutButton_Description": "Odstrániť lokálne uloženú reláciu účtu Wino.", + "WinoAccount_SignOutButton_Action": "Odhlásiť sa", + "WinoAccount_RegisterDialog_Title": "Vytvoriť Wino účet", + "WinoAccount_RegisterDialog_Description": "Vytvorte Wino účet, aby bol váš Wino zážitok v synchronizácii a aby ste získali prístup k doplnkom založeným na účte.", + "WinoAccount_RegisterDialog_HeroTitle": "Vytvorte si svoj Wino účet", + "WinoAccount_RegisterDialog_BenefitsTitle": "Prečo si ho vytvoriť?", + "WinoAccount_RegisterDialog_BenefitSyncTitle": "Import a export nastavení naprieč zariadeniami", + "WinoAccount_RegisterDialog_BenefitSyncDescription": "Presúvajte svoje Wino predvoľby medzi zariadeniami bez nutnosti znovu zostavovať nastavenia od začiatku.", + "WinoAccount_RegisterDialog_BenefitAiTitle": "Získajte prístup k exkluzívnym doplnkom ako Wino AI Pack (platené)", + "WinoAccount_RegisterDialog_BenefitAiDescription": "Použite jeden účet na odomknutie prémiových funkcií Wino, keď budú dostupné.", + "WinoAccount_RegisterDialog_DifferenceTitle": "Wino účet je samostatný od vašich e-mailových účtov", + "WinoAccount_RegisterDialog_DifferenceDescription": "Vaše účty Outlook, Gmail, IMAP alebo iné e-mailové účty zostávajú presne také, aké sú. Wino účet spravuje iba funkcie špecifické pre Wino a doplnky založené na účte.", + "WinoAccount_RegisterDialog_PrimaryButton": "Registrovať", + "WinoAccount_RegisterDialog_PrivacyTitle": "Ochrana súkromia a spracovanie API", + "WinoAccount_RegisterDialog_PrivacyDescription": "Voliteľné doplnky, ako napríklad Wino AI Pack, môžu pri použití týchto funkcií posielať vybrané HTML obsah e-mailov do služby Wino API.", + "WinoAccount_RegisterDialog_PrivacyLinkText": "Prečítajte si zásady ochrany osobných údajov", + "WinoAccount_RegisterDialog_PrivacyCheckbox": "Súhlasím so zásadami ochrany osobných údajov.", + "WinoAccount_LoginDialog_Title": "Prihlásiť sa do Wino účtu", + "WinoAccount_LoginDialog_Description": "Prihláste sa do svojho Wino účtu, aby ste synchronizovali svoje nastavenia a mali prístup k funkciám založeným na účte.", + "WinoAccount_LoginDialog_HeroTitle": "Vitajte späť", + "WinoAccount_LoginDialog_BenefitsTitle": "Čo vám prihlásenie prináša", + "WinoAccount_LoginDialog_BenefitsDescription": "Použite svoj Wino účet na pokračovanie synchronizácie nastavení naprieč zariadeniami a na prístup k plateným doplnkom, ako je Wino AI Pack.", + "WinoAccount_LoginDialog_DifferenceTitle": "Toto nie je prihlásenie do vášho e-mailového účtu", + "WinoAccount_LoginDialog_DifferenceDescription": "Prihlásenie sa sem nepridáva ani nenahrádza vaše účty Outlook, Gmail alebo IMAP vo Wino. Prihlási vás iba do služieb špecifických pre Wino.", + "WinoAccount_LoginDialog_ForgotPasswordLink": "Zabudli ste heslo?", + "WinoAccount_EmailLabel": "E-mail", + "WinoAccount_EmailPlaceholder": "name@example.com", + "WinoAccount_PasswordLabel": "Heslo", + "WinoAccount_ConfirmPasswordLabel": "Potvrdiť heslo", + "WinoAccount_ForgotPasswordDialog_Title": "Obnoviť heslo", + "WinoAccount_ForgotPasswordDialog_PrimaryButton": "Odoslať e-mail na resetovanie hesla", + "WinoAccount_ForgotPasswordDialog_BackToSignIn": "Späť na prihlásenie", + "WinoAccount_ForgotPasswordDialog_Description": "Zadajte e-mailovú adresu Vášho Wino účtu a my Vám pošleme odkaz na obnovenie hesla, ak je adresa registrovaná.", + "WinoAccount_Validation_EmailRequired": "E-mail je povinný.", + "WinoAccount_Validation_PasswordRequired": "Heslo je povinné.", + "WinoAccount_Validation_PasswordMismatch": "Heslá sa nezhodujú.", + "WinoAccount_Validation_PrivacyConsentRequired": "Pred vytvorením Wino účtu musíte súhlasiť so zásadami ochrany osobných údajov.", + "WinoAccount_Error_InvalidCredentials": "E-mailová adresa alebo heslo nie sú správne.", + "WinoAccount_Error_AccountLocked": "Tento účet je dočasne uzamknutý.", + "WinoAccount_Error_AccountBanned": "Tento účet bol zablokovaný.", + "WinoAccount_Error_AccountSuspended": "Tento účet bol pozastavený.", + "WinoAccount_Error_EmailNotConfirmed": "Prosím potvrďte svoju e-mailovú adresu pred prihlásením.", + "WinoAccount_Error_EmailConfirmationRequired": "Prosím potvrďte svoju e-mailovú adresu pred prihlásením.", + "WinoAccount_Error_EmailConfirmationResendNotAvailable": "Nový overovací e-mail ešte nie je k dispozícii.", + "WinoAccount_Error_EmailConfirmationResendInvalid": "Požiadavka na potvrdenie už nie je platná. Skúste sa prosím znova prihlásiť.", + "WinoAccount_Error_EmailNotRegistered": "Táto e-mailová adresa nie je registrovaná.", + "WinoAccount_Error_RefreshTokenInvalid": "Váš session už nie je platná. Prosím, prihláste sa znovu.", + "WinoAccount_Error_EmailAlreadyRegistered": "Táto e-mailová adresa je už zaregistrovaná.", + "WinoAccount_Error_ExternalLoginEmailRequired": "E-mailová adresa je potrebná na dokončenie externého prihlásenia.", + "WinoAccount_Error_ExternalLoginInvalid": "Žiadosť o externé prihlásenie je neplatná.", + "WinoAccount_Error_ExternalAuthStateInvalid": "Stav externého prihlásenia je neplatný alebo vypršal.", + "WinoAccount_Error_ExternalAuthCodeInvalid": "Externý sign-in kód je neplatný alebo vypršal.", + "WinoAccount_Error_AiPackRequired": "Pre túto akciu je vyžadované aktívne predplatné Wino AI Pack.", + "WinoAccount_Error_AiQuotaExceeded": "Váš limit využívania AI Pack bol vyčerpaný pre aktuálne fakturačné obdobie.", + "WinoAccount_Error_AiHtmlEmpty": "Nie je žiadny obsah e-mailu na spracovanie.", + "WinoAccount_Error_AiHtmlTooLarge": "Tento e-mail je príliš veľký na spracovanie pomocou Wino AI.", + "WinoAccount_Error_AiUnsupportedLanguage": "Tento jazyk nie je podporovaný. Skúste platný kultúrny kód, napríklad en-US alebo tr-TR.", + "WinoAccount_Error_Forbidden": "Nemáte oprávnenie na vykonanie tejto akcie.", + "WinoAccount_Error_ValidationFailed": "Žiadosť je neplatná. Skontrolujte zadané hodnoty.", + "WinoAccount_RegisterSuccessMessage": "Registrácia Wino účtu bola dokončená pre {0}.", + "WinoAccount_LoginSuccessMessage": "Prihlásený do Wino účtu ako {0}.", + "WinoAccount_EmailConfirmationSentDialog_Title": "Potvrďte svoju e-mailovú adresu", + "WinoAccount_EmailConfirmationSentDialog_Message": "Poslali sme potvrdenie e-mailu na {0}. Prosím potvrďte ho a skúste sa znovu prihlásiť.", + "WinoAccount_EmailConfirmationPendingDialog_Title": "Potvrdenie e-mailu je vyžadované", + "WinoAccount_EmailConfirmationPendingDialog_Message": "Stále čakáme na potvrdenie {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ResendButton": "Odoslať znovu potvrdzovací e-mail.", + "WinoAccount_EmailConfirmationPendingDialog_Countdown": "Potvrdzovací e-mail môžete znovu odoslať za {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ReadyToResend": "Teraz môžete odoslať potvrdzovací e-mail znova.", + "WinoAccount_EmailConfirmationResentDialog_Title": "Potvrdzovací e-mail bol odoslaný znovu.", + "WinoAccount_EmailConfirmationResentDialog_Message": "Poslali sme ďalší potvrdzovací e-mail na {0}. Prosím potvrďte ho a skúste sa prihlásiť znovu.", + "WinoAccount_ForgotPasswordDialog_SuccessTitle": "E-mail na resetovanie hesla bol odoslaný.", + "WinoAccount_ForgotPasswordDialog_SuccessMessage": "Odoslali sme e-mail na resetovanie hesla na {0}. Otvorte tú správu a vyberte nové heslo.", + "WinoAccount_ChangePassword_Title": "Zmeniť heslo", + "WinoAccount_ChangePassword_Description": "Odošlite e-mail na resetovanie hesla na tento Wino účet.", + "WinoAccount_ChangePassword_Action": "Odoslať e-mail na resetovanie hesla", + "WinoAccount_ChangePassword_ConfirmationMessage": "Chcete, aby Wino poslalo e-mail na obnovenie hesla na {0}?", + "WinoAccount_SignOut_SuccessMessage": "Boli ste odhlásení z účtu Wino {0}.", + "WinoAccount_SignOut_NoAccountMessage": "Nie je k dispozícii žiadny aktívny účet Wino na odhlásenie.", + "WinoAccount_Titlebar_SignedOutTitle": "Účet Wino", + "WinoAccount_Titlebar_SignedOutDescription": "Prihláste sa alebo si vytvorte účet Wino a spravujte svoju reláciu Wino.", + "WinoAccount_Titlebar_SignedInStatus": "Stav: {0}", + "WelcomeWizard_Step2Title": "Pridať účet", + "WelcomeWizard_Step3Title": "Dokončiť nastavenie", + "ProviderSelection_Title": "Vyberte si svojho poskytovateľa e-mailu", + "ProviderSelection_Subtitle": "Vyberte nižšie uvedeného poskytovateľa a pridajte svoj e-mailový účet do Wino Mail.", + "ProviderSelection_AccountNameHeader": "Názov účtu", + "ProviderSelection_AccountNamePlaceholder": "napr. Osobný, Práca", + "ProviderSelection_DisplayNameHeader": "Zobrazené meno", + "ProviderSelection_DisplayNamePlaceholder": "napr. Ján Novak", + "ProviderSelection_EmailHeader": "E-mailová adresa", + "ProviderSelection_EmailPlaceholder": "napr. johndoe@example.com", + "ProviderSelection_AppPasswordHeader": "Heslo pre aplikáciu", + "ProviderSelection_AppPasswordHelp": "Ako získať heslo pre aplikáciu?", + "ProviderSelection_CalendarModeHeader": "Integrácia kalendára", + "ProviderSelection_CalendarMode_DisabledTitle": "Vypnuté", + "ProviderSelection_CalendarMode_DisabledDescription": "Žiadna integrácia kalendára", + "ProviderSelection_CalendarMode_CalDavTitle": "Synchronizácia CalDAV", + "ProviderSelection_CalendarMode_CalDavDescription_Apple": "Vaše udalosti v kalendári sa medzi vašimi zariadeniami synchronizujú so servermi Apple.", + "ProviderSelection_CalendarMode_CalDavDescription_Yahoo": "Vaše udalosti v kalendári sa medzi vašimi zariadeniami synchronizujú so servermi Yahoo.", + "ProviderSelection_CalendarMode_LocalTitle": "Lokálny kalendár", + "ProviderSelection_CalendarMode_LocalDescription": "Vaše udalosti sú uložené iba vo vašom počítači. Žiadne pripojenie k serveru.", + "ProviderSelection_ClearColor": "Vymazať farbu", + "ProviderSelection_ContinueButton": "Pokračovať", + "ProviderSelection_SpecialImap_Subtitle": "Zadajte prihlasovacie údaje k účtu pre pripojenie.", + "AccountSetup_Title": "Nastavovanie účtu", + "AccountSetup_Step_Authenticating": "Overovanie s {0}", + "AccountSetup_Step_TestingMailAuth": "Testujem autentifikáciu pošty", + "AccountSetup_Step_SyncingFolders": "Synchronizujem metadáty priečinkov", + "AccountSetup_Step_FetchingProfile": "Načítavam informácie z profilu", + "AccountSetup_Step_DiscoveringCalDav": "Objavujem nastavenia CalDAV", + "AccountSetup_Step_TestingCalendarAuth": "Testujem autentifikáciu kalendára", + "AccountSetup_Step_SavingAccount": "Ukladám informácie o účte", + "AccountSetup_Step_FetchingCalendarMetadata": "Načítavam metadáty kalendára", + "AccountSetup_Step_SyncingAliases": "Synchronizujem aliasy", + "AccountSetup_Step_Finalizing": "Dokončujem nastavenie", + "AccountSetup_FailureMessage": "Nastavenie zlyhalo. Vráťte sa späť a opravte nastavenia, alebo to skúste neskôr.", + "AccountSetup_SuccessMessage": "Váš účet bol úspešne nastavený!", + "AccountSetup_GoBackButton": "Vrátiť sa späť", + "AccountSetup_TryAgainButton": "Skúsiť znova", + "ImapCalDavSettings_AutoDiscoveryFailed": "Automatické vyhľadávanie zlyhalo. Zadajte nastavenia ručne na karte Rozšírené." } - - diff --git a/Wino.Core.Domain/Translations/tr_TR/resources.json b/Wino.Core.Domain/Translations/tr_TR/resources.json index bda12315..e3cfd510 100644 --- a/Wino.Core.Domain/Translations/tr_TR/resources.json +++ b/Wino.Core.Domain/Translations/tr_TR/resources.json @@ -8,6 +8,7 @@ "AccountCacheReset_Message": "Bu hesabın kullanılabilmesi için tekrar senkronize edilmesi gerekiyor. Lütfen Wino mesajlarınızı senkronize ederken bekleyiniz...", "AccountContactNameYou": "Siz", "AccountCreationDialog_Completed": "hepsi tamam", + "AccountCreationDialog_FetchingCalendarMetadata": "Takvim ayrıntıları getiriliyor.", "AccountCreationDialog_FetchingEvents": "Etkinlikler indiriliyor.", "AccountCreationDialog_FetchingProfileInformation": "Profil detayları yükleniyor.", "AccountCreationDialog_GoogleAuthHelpClipboardText_Row0": "Eğer tarayıcınız otomatik olarak açılmadıysa:", @@ -17,6 +18,7 @@ "AccountCreationDialog_Initializing": "başlatılıyor", "AccountCreationDialog_PreparingFolders": "Klasör bilgileri yükleniyor.", "AccountCreationDialog_SigninIn": "Hesap bilgileri kaydediliyor.", + "Purchased": "Satın Alındı", "AccountEditDialog_Message": "Hesap Adı", "AccountEditDialog_Title": "Hesabı Düzenle", "AccountPickerDialog_Title": "Bir hesap seçiniz", @@ -26,6 +28,10 @@ "AccountDetailsPage_Description": "Hesabınızın adını ve gönderilen kişi ismini değiştirin.", "AccountDetailsPage_ColorPicker_Title": "Hesap rengi", "AccountDetailsPage_ColorPicker_Description": "Hesabınıza yeni bir renk atayarak diğerlerinden ayrı görünmesini sağlayabilirsiniz.", + "AccountDetailsPage_TabGeneral": "Genel", + "AccountDetailsPage_TabMail": "Posta", + "AccountDetailsPage_TabCalendar": "Takvim", + "AccountDetailsPage_CalendarListDescription": "Ayarlarını yapılandırmak için bir takvim seçin.", "AddHyperlink": "Ekle", "AppCloseBackgroundSynchronizationWarningTitle": "Arkaplan Senkronizasyonu", "AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Uygulama Windows açılırken çalıştırılacak şekilde ayarlanmadı.", @@ -47,8 +53,10 @@ "BasicIMAPSetupDialog_Title": "IMAP Hesabı", "Busy": "Meşgul", "Buttons_AddAccount": "Hesap Ekle", + "Buttons_FixAccount": "Hesabı Düzelt", "Buttons_AddNewAlias": "Yenii Rumuz Ekle", "Buttons_Allow": "İzin ver", + "Buttons_Apply": "Uygula", "Buttons_ApplyTheme": "Temayı Uygula", "Buttons_Browse": "Gözat", "Buttons_Cancel": "İptal Et", @@ -62,6 +70,7 @@ "Buttons_Edit": "Düzenle", "Buttons_EnableImageRendering": "Etkinleştir", "Buttons_Multiselect": "Çoklu Seç", + "Buttons_Manage": "Yönet", "Buttons_No": "Hayır", "Buttons_Open": "Aç", "Buttons_Purchase": "Satın Al", @@ -70,15 +79,134 @@ "Buttons_Save": "Kaydet", "Buttons_SaveConfiguration": "Ayarları Kaydet", "Buttons_Send": "Gönder", + "Buttons_SendToServer": "Sunucuya gönder", "Buttons_Share": "Paylaş", "Buttons_SignIn": "Oturum Aç", "Buttons_Sync": "Senkronize et", "Buttons_SyncAliases": "Rumuzları Eşitle", "Buttons_TryAgain": "Tekrar Dene", "Buttons_Yes": "Evet", + "Sync_SynchronizingFolder": "Senkronize ediliyor: {0} {1}%", + "Sync_DownloadedMessages": "{1} adresinden indirilen {0} mesaj", + "SyncAction_Archiving": " {0} posta arşivleniyor.", + "SyncAction_ClearingFlag": "{0} posta için işaret kaldırılıyor.", + "SyncAction_CreatingDraft": "Taslak oluşturuluyor", + "SyncAction_CreatingEvent": "Etkinlik oluşturuluyor", + "SyncAction_Deleting": "{0} posta siliniyor.", + "SyncAction_EmptyingFolder": "Klasör boşaltılıyor", + "SyncAction_MarkingAsRead": "{0} posta okunmuş olarak işaretleniyor.", + "SyncAction_MarkingAsUnread": "{0} posta okunmamış olarak işaretleniyor.", + "SyncAction_MarkingFolderAsRead": "Klasör okunmuş olarak işaretleniyor", + "SyncAction_Moving": "{0} posta taşınıyor", + "SyncAction_MovingToFocused": "{0} posta Odaklı klasöre taşınıyor", + "SyncAction_RenamingFolder": "Klasör yeniden adlandırılıyor", + "SyncAction_SendingMail": "Posta gönderiliyor", + "SyncAction_SettingFlag": "{0} posta için işaret belirleniyor.", + "SyncAction_SynchronizingAccount": "{0} hesabı senkronize ediliyor", + "SyncAction_SynchronizingAccounts": "{0} hesaplar senkronize ediliyor", + "SyncAction_SynchronizingCalendarData": "Takvim verileri senkronize ediliyor", + "SyncAction_SynchronizingCalendarEvents": "Takvim etkinlikleri senkronize ediliyor", + "SyncAction_SynchronizingCalendarMetadata": "Takvim meta verileri senkronize ediliyor", + "SyncAction_Unarchiving": "{0} posta arşivinden çıkarılıyor.", "CalendarAllDayEventSummary": "tüm gün etkinlikleri", "CalendarDisplayOptions_Color": "Renk", "CalendarDisplayOptions_Expand": "Genişlet", + "CalendarEventResponse_Accept": "Kabul", + "CalendarEventResponse_AcceptedResponse": "Kabul edildi", + "CalendarEventResponse_Decline": "Reddet", + "CalendarEventResponse_DeclinedResponse": "Reddedildi", + "CalendarEventResponse_NotResponded": "Yanıtlanmadı", + "CalendarEventResponse_Tentative": "Geçici", + "CalendarEventResponse_TentativeResponse": "Geçici", + "CalendarEventRsvpPanel_Accept": "Kabul", + "CalendarEventRsvpPanel_AddMessage": "Yanıtınıza bir mesaj ekleyin... (isteğe bağlı)", + "CalendarEventRsvpPanel_Decline": "Reddet", + "CalendarEventRsvpPanel_Message": "Mesaj", + "CalendarEventRsvpPanel_SendReplyMessage": "Yanıt mesajını gönder", + "CalendarEventRsvpPanel_Tentative": "Geçici", + "CalendarEventRsvpPanel_Title": "Yanıt Seçenekleri", + "CalendarAttendeeStatus_Accepted": "Kabul edildi", + "CalendarAttendeeStatus_Declined": "Reddedildi", + "CalendarAttendeeStatus_NeedsAction": "İşlem Gerekiyor", + "CalendarAttendeeStatus_Tentative": "Geçici", + "CalendarEventDetails_Attachments": "Ekler", + "CalendarEventCompose_AddAttachment": "Ek Ekle", + "CalendarEventCompose_AllDay": "Tüm Gün", + "CalendarEventCompose_AttachmentsNotSupportedForCalDav": "CalDAV takvimleri için ekler desteklenmiyor.", + "CalendarEventCompose_EndDate": "Bitiş tarihi", + "CalendarEventCompose_EndTime": "Bitiş saati", + "CalendarEventCompose_Every": "her", + "CalendarEventCompose_ForWeekdays": "için", + "CalendarEventCompose_FrequencyDay": "gün", + "CalendarEventCompose_FrequencyDayPlural": "günler", + "CalendarEventCompose_FrequencyMonth": "ay", + "CalendarEventCompose_FrequencyMonthPlural": "aylar", + "CalendarEventCompose_FrequencyWeek": "hafta", + "CalendarEventCompose_FrequencyWeekPlural": "haftalar", + "CalendarEventCompose_FrequencyYear": "yıl", + "CalendarEventCompose_FrequencyYearPlural": "yıllar", + "CalendarEventCompose_Location": "Konum", + "CalendarEventCompose_LocationPlaceholder": "Bir konum ekleyin", + "CalendarEventCompose_NewEventButton": "Yeni Etkinlik", + "CalendarEventCompose_DefaultCalendarHint": "Takvim ayarlarında yeni etkinlikler için varsayılan bir takvim seçebilirsiniz.", + "CalendarEventCompose_DefaultCalendarSettingsLink": "Takvim ayarlarını aç", + "CalendarEventCompose_NoCalendarsMessage": "Etkinlik oluşturmak için henüz kullanılabilir takvim yok.", + "CalendarEventCompose_NoCalendarsTitle": "Kullanılabilir takvim yok", + "CalendarEventCompose_NoEndDate": "Bitiş tarihi yok", + "CalendarEventCompose_Notes": "Notlar", + "CalendarEventCompose_PickCalendarTitle": "Bir takvim seçin", + "CalendarEventCompose_Recurring": "Yinelenen", + "CalendarEventCompose_RecurringSummary": "Her {0} {1}{2} {3} geçerli {4}{5}", + "CalendarEventCompose_RecurringSummarySmart": "Oluşur {0}{1} {2} geçerli {3}{4}", + "CalendarEventCompose_RepeatEvery": "Her seferinde tekrarla", + "CalendarEventCompose_SelectCalendar": "Takvim seçin", + "CalendarEventCompose_SingleOccurrenceSummary": " {0} {1} üzerinde gerçekleşir", + "CalendarEventCompose_StartDate": "Başlangıç tarihi", + "CalendarEventCompose_StartTime": "Başlangıç saati", + "CalendarEventCompose_TimeRangeSummary": "{0} ile {1} arasında", + "CalendarEventCompose_Title": "Etkinlik başlığı", + "CalendarEventCompose_TitlePlaceholder": "Bir başlık ekleyin", + "CalendarEventCompose_Until": "kadar", + "CalendarEventCompose_UntilSummary": " kadar {0}", + "CalendarEventCompose_ValidationInvalidAllDayRange": "Tüm gün için bitiş tarihi başlangıç tarihinden sonra olmalıdır.", + "CalendarEventCompose_ValidationInvalidAttendee": "Bir veya daha fazla katılımcının geçersiz bir e-posta adresi var.", + "CalendarEventCompose_ValidationInvalidRecurrenceEnd": "Yinelenme son tarihi, etkinlik başlangıç tarihinden sonra veya ona eşit olmalıdır.", + "CalendarEventCompose_ValidationInvalidTimeRange": "Bitiş saati, başlangıç saatinden daha geç olmalıdır.", + "CalendarEventCompose_ValidationMissingAttachment": "Bir veya daha fazla ek artık mevcut değil: {0}", + "CalendarEventCompose_ValidationMissingCalendar": "Etkinliği oluşturmadan önce bir takvim seçin.", + "CalendarEventCompose_ValidationMissingTitle": "Etkinlik başlığını girin.", + "CalendarEventCompose_ValidationTitle": "Etkinlik doğrulaması başarısız oldu", + "CalendarEventCompose_WeekdaySummary": " {0} üzerinde", + "CalendarEventCompose_Weekday_Friday": "C", + "CalendarEventCompose_Weekday_Monday": "P", + "CalendarEventCompose_Weekday_Saturday": "Cumartesi", + "CalendarEventCompose_Weekday_Sunday": "Pazar", + "CalendarEventCompose_Weekday_Thursday": "Perşembe", + "CalendarEventCompose_Weekday_Tuesday": "Salı", + "CalendarEventCompose_Weekday_Wednesday": "Çarşamba", + "CalendarEventDetails_Details": "Detaylar", + "CalendarEventDetails_EditSeries": "Seriyi Düzenle", + "CalendarEventDetails_Editing": "Düzenleniyor", + "CalendarEventDetails_InviteSomeone": "Birini davet et", + "CalendarEventDetails_JoinOnline": "Çevrimiçi Katıl", + "CalendarEventDetails_Organizer": "Organizatör", + "CalendarEventDetails_People": "Kişiler", + "CalendarEventDetails_ReadOnlyEvent": "Salt okunur etkinlik", + "CalendarEventDetails_Reminder": "Hatırlatma", + "CalendarReminder_StartedHoursAgo": "{0} saat önce başladı", + "CalendarReminder_StartedMinutesAgo": "{0} dakika önce başladı", + "CalendarReminder_StartedNow": "Şimdi başladı", + "CalendarReminder_StartingNow": "Şu anda başlıyor", + "CalendarReminder_StartsInHours": "{0} saat sonra başlıyor", + "CalendarReminder_StartsInMinutes": "{0} dakika sonra başlıyor", + "CalendarReminder_SnoozeAction": "Ertele", + "CalendarReminder_SnoozeMinutesOption": "{0} dakika", + "CalendarEventDetails_ShowAs": "Durum olarak göster", + "CalendarShowAs_Free": "Boş", + "CalendarShowAs_Tentative": "Geçici", + "CalendarShowAs_Busy": "Meşgul", + "CalendarShowAs_OutOfOffice": "Ofis Dışında", + "CalendarShowAs_WorkingElsewhere": "Başka bir yerde çalışıyor", "CalendarItem_DetailsPopup_JoinOnline": "Çevrimiçi katıl", "CalendarItem_DetailsPopup_ViewEventButton": "Etkinliği gör", "CalendarItem_DetailsPopup_ViewSeriesButton": "Etkinlik serisini gör", @@ -88,6 +216,9 @@ "ClipboardTextCopied_Message": "\"{0}\" panoya kopyalandı.", "ClipboardTextCopied_Title": "Kopyalandı", "ClipboardTextCopyFailed_Message": "{0} kopyalanamadı.", + "ContactInfoBar_ErrorTitle": "Kişi bilgileri yüklenemedi", + "ContactInfoBar_SuccessTitle": "Kişi bilgileri yüklendi", + "ContactInfoBar_WarningTitle": "Kişi bilgileri eksik olabilir", "ComingSoon": "Yakında...", "ComposerAttachmentsDragDropAttach_Message": "Ekle", "ComposerAttachmentsDropZone_Message": "Dosyaları buraya bırakabilirisiniz", @@ -129,6 +260,10 @@ "DialogMessage_CreateLinkedAccountTitle": "Hesap Bağlantı Adı", "DialogMessage_DeleteAccountConfirmationMessage": "{0} silinsin mi?", "DialogMessage_DeleteAccountConfirmationTitle": "Bu hesapla ilgili her şey bilgisayarınızdan silinecektir.", + "DialogMessage_DeleteEmailTemplateConfirmationMessage": "Şablonu \"{0}\" silmek istiyor musunuz?", + "DialogMessage_DeleteEmailTemplateConfirmationTitle": "E-posta Şablonunu Sil", + "DialogMessage_DeleteRecurringSeriesMessage": "Bu işlem serideki tüm etkinlikleri silecek. Devam etmek istiyor musunuz?", + "DialogMessage_DeleteRecurringSeriesTitle": "Tekrarlayan Seriyi Sil", "DialogMessage_DiscardDraftConfirmationMessage": "Taslak yoksayılacaktır. Devam etmek istiyor musunuz?", "DialogMessage_DiscardDraftConfirmationTitle": "Taslağı Sil", "DialogMessage_EmptySubjectConfirmation": "Konu Eksik", @@ -172,11 +307,18 @@ "ElementTheme_Light": "Aydınlık mod", "Emoji": "Emoji", "Error_FailedToSetupSystemFolders_Title": "Sistem klasörleri ayarlanamadı", + "Exception_AccountNeedsAttention_Title": "Hesap Dikkat Gerektiriyor", + "Exception_AccountNeedsAttention_Message": "'{0}' devam etmek için dikkatinize ihtiyaç duyuyor.", + "Exception_WebView2RuntimeMissing_Message": "Wino Mail, Microsoft Edge WebView2 Runtime'ını bulamadı. İçeriği doğru şekilde görüntülemek için lütfen runtime'ı kurun veya onarın.", + "Exception_WebView2RuntimeMissing_Title": "WebView2 Çalışma Zamanı Gerekli", "Exception_AuthenticationCanceled": "Kimlik doğrulama iptal edildi", "Exception_CustomThemeExists": "Bu tema zaten mevcut.", "Exception_CustomThemeMissingName": "Bir isim giirmelisiniz.", "Exception_CustomThemeMissingWallpaper": "Bir arkaplan resmi tanımlamalısınız.", "Exception_FailedToSynchronizeAliases": "Rumuzlar eşitlenemedi", + "Exception_FailedToSynchronizeCalendarData": "Takvim verileri senkronize edilemedi", + "Exception_FailedToSynchronizeCalendarEvents": "Takvim etkinlikleri senkronize edilemedi", + "Exception_FailedToSynchronizeCalendarMetadata": "Takvim bilgileri senkronize edilemedi", "Exception_FailedToSynchronizeFolders": "Klasörler eşitlenemedi", "Exception_FailedToSynchronizeProfileInformation": "Profil bilgisi eşitlenemedi", "Exception_GoogleAuthCallbackNull": "Yeniden yönlendirme adresi aktifleştirme sırasında null.", @@ -229,6 +371,32 @@ "HoverActionOption_MoveJunk": "Spam'e taşı", "HoverActionOption_ToggleFlag": "Bayrak ekle / kaldır", "HoverActionOption_ToggleRead": "Okundu / okunmadı", + "KeyboardShortcuts_FailedToReset": "Klavye kısayolları sıfırlanamadı.", + "KeyboardShortcuts_FailedToUpdate": "Klavye kısayolları güncellenemedi", + "KeyboardShortcuts_MailoperationAction": "İşlem", + "KeyboardShortcuts_Action": "İşlem", + "KeyboardShortcuts_FailedToLoad": "Klavye kısayolları yüklenemedi.", + "KeyboardShortcuts_EnterKeyForShortcut": "Kısayol için bir tuşa basınız.", + "KeyboardShortcuts_SelectOperationForShortcut": "Kısayol için bir işlem seçiniz.", + "KeyboardShortcuts_EnterKey": "Kısayol için bir tuşa basınız.", + "KeyboardShortcuts_SelectOperation": "Kısayol için bir işlem seçiniz.", + "KeyboardShortcuts_ShortcutInUse": "Bu kısayol zaten başka bir kısayol tarafından kullanılıyor.", + "KeyboardShortcuts_FailedToSave": "Kısayol kaydedilemedi.", + "KeyboardShortcuts_FailedToDelete": "Kısayol silinemedi.", + "KeyboardShortcuts_PageDescription": "Hızlı e-posta işlemleri için klavye kısayollarını ayarlayın. Kısayol tuşlarını yakalamak için anahtar girdi alanına odaklanırken tuşlara basın.", + "KeyboardShortcuts_Add": "Kısayol Ekle", + "KeyboardShortcuts_EditTitle": "Klavye Kısayolunu Düzenle", + "KeyboardShortcuts_ResetToDefaults": "Varsayılanlara Sıfırla", + "KeyboardShortcuts_PressKeysHere": "Buraya tuşlara basın...", + "KeyboardShortcuts_KeyCombination": "Tuş Kombinasyonu", + "KeyboardShortcuts_FocusArea": "Yukarıdaki alanı odaklayın ve istenen tuş kombinasyonunu basın.", + "KeyboardShortcuts_Modifiers": "Mod Tuşları", + "KeyboardShortcuts_Mode": "Mod", + "KeyboardShortcuts_ModeMail": "Posta", + "KeyboardShortcuts_ModeCalendar": "Takvim", + "KeyboardShortcuts_ActionToggleReadUnread": "Okundu/Okunmadı durumunu değiştir", + "KeyboardShortcuts_ActionToggleFlag": "Bayrağı değiştir", + "KeyboardShortcuts_ActionToggleArchive": "Arşiv durumunu değiştir", "ImageRenderingDisabled": "Bu mesaj için resimler devre dışı bırakıldı.", "ImapAdvancedSetupDialog_AuthenticationMethod": "Oturum Açma yöntemi", "ImapAdvancedSetupDialog_ConnectionSecurity": "Bağlantı güvenliği", @@ -295,12 +463,58 @@ "IMAPSetupDialog_Username": "Kullanıcı Adı", "IMAPSetupDialog_UsernamePlaceholder": "johndoe, johndoe@fabrikam.com, domain/johndoe", "IMAPSetupDialog_UseSameConfig": "E-Posta göndermek için aynı ayarları kullan", + "ImapCalDavSettingsPage_TitleCreate": "IMAP ve Takvim Kurulumu", + "ImapCalDavSettingsPage_TitleEdit": "IMAP ve Takvim Ayarlarını Düzenle", + "ImapCalDavSettingsPage_Subtitle": "Bu hesap için IMAP/SMTP ve isteğe bağlı takvim senkronizasyonunu yapılandırın.", + "ImapCalDavSettingsPage_BasicSectionTitle": "Temel kurulum", + "ImapCalDavSettingsPage_BasicSectionDescription": "Kullanıcı kimliğinizi ve kimlik bilgilerinizi girin. Wino sunucu ayarlarını otomatik olarak algılamaya çalışabilir.", + "ImapCalDavSettingsPage_BasicTab": "Temel", + "ImapCalDavSettingsPage_EnableCalendarSupport": "Takvim desteğini etkinleştir", + "ImapCalDavSettingsPage_AutoDiscoverButton": "Posta ayarlarını otomatik keşfet", + "ImapCalDavSettingsPage_AutoDiscoverySuccessMessage": "Posta ayarları bulundu ve uygulandı.", + "ImapCalDavSettingsPage_AdvancedSectionTitle": "Gelişmiş yapılandırma", + "ImapCalDavSettingsPage_AdvancedSectionDescription": "Otomatik keşif kullanılamıyorsa veya hatalıysa sunucu ayarlarını manuel olarak girin.", + "ImapCalDavSettingsPage_AdvancedTab": "Gelişmiş", + "ImapCalDavSettingsPage_CalendarSectionTitle": "Takvim kurulumu", + "ImapCalDavSettingsPage_CalendarSectionDescription": "Bu IMAP hesabı için takvim verilerinin nasıl çalışacağını seçin.", + "ImapCalDavSettingsPage_CalendarModeHeader": "Takvim modu", + "ImapCalDavSettingsPage_ConnectionSecurityHeader": "Bağlantı güvenliği", + "ImapCalDavSettingsPage_AuthenticationMethodHeader": "Doğrulama yöntemi", + "ImapCalDavSettingsPage_CalendarModeDisabled": "Devre dışı", + "ImapCalDavSettingsPage_CalendarModeCalDav": "CalDAV senkronizasyonu", + "ImapCalDavSettingsPage_CalendarModeLocalOnly": "Yalnızca Yerel Takvim", + "ImapCalDavSettingsPage_CalendarModeDisabledDescription": "Bu hesap için takvim devre dışı.", + "ImapCalDavSettingsPage_CalendarModeCalDavDescription": "Takvim öğeleri CalDAV sunucunuzla senkronize edilir.", + "ImapCalDavSettingsPage_CalendarModeLocalOnlyDescription": "Takvim öğeleri yalnızca bu bilgisayarda saklanır ve ağa senkronize edilmez.", + "ImapCalDavSettingsPage_LocalCalendarLearnMore": "Yerel takvim nasıl çalışır", + "ImapCalDavSettingsPage_LocalCalendarDialogTitle": "Yalnızca Yerel Takvim", + "ImapCalDavSettingsPage_LocalCalendarDialogMessage": "Yerel takvim tüm etkinlikleri yalnızca bilgisayarınızda tutar. Hiçbir şey iCloud, Yahoo veya herhangi bir sağlayıcıya senkronize edilmez.", + "ImapCalDavSettingsPage_CalDavServiceUrl": "CalDAV hizmeti URL'si", + "ImapCalDavSettingsPage_CalDavUsername": "CalDAV kullanıcı adı", + "ImapCalDavSettingsPage_CalDavPassword": "CalDAV şifresi", + "ImapCalDavSettingsPage_CalDavNotRequiredMessage": "CalDAV testi yalnızca takvim modu CalDAV senkronizasyonu olarak ayarlandığında gerekir.", + "ImapCalDavSettingsPage_CalDavUrlRequired": "CalDAV hizmeti URL'si gereklidir.", + "ImapCalDavSettingsPage_CalDavUrlInvalid": "CalDAV hizmeti URL'si mutlak URL olmalıdır.", + "ImapCalDavSettingsPage_CalDavUsernameRequired": "CalDAV kullanıcı adı gerekli.", + "ImapCalDavSettingsPage_CalDavPasswordRequired": "CalDAV parolası gerekli.", + "ImapCalDavSettingsPage_TestImapButton": "IMAP bağlantısını test et.", + "ImapCalDavSettingsPage_TestCalDavButton": "CalDAV bağlantısını test et.", + "ImapCalDavSettingsPage_ImapTestSuccessMessage": "IMAP bağlantı testi başarıyla gerçekleştirildi.", + "ImapCalDavSettingsPage_CalDavTestSuccessMessage": "CalDAV bağlantı testi başarıyla gerçekleştirildi.", + "ImapCalDavSettingsPage_SaveSuccessMessage": "Hesap ayarları doğrulandı ve kaydedildi.", + "ImapCalDavSettingsPage_ICloudHint": "Apple hesabı ayarlarından oluşturulan uygulama özel şifresini kullanın.", + "ImapCalDavSettingsPage_YahooHint": "Yahoo hesap güvenlik ayarlarından bir uygulama şifresi kullanın.", "Info_AccountCreatedMessage": "{0} oluşturuldu", "Info_AccountCreatedTitle": "Hesap Oluşturma", "Info_AccountCreationFailedTitle": "Hesap oluşturulamadı", "Info_AccountDeletedMessage": "{0} başarıyla silindi.", "Info_AccountDeletedTitle": "Hesap Silindi", "Info_AccountIssueFixFailedTitle": "Başarısız oldu", + "Info_AccountIssueFixImapMessage": "IMAP ve takvim ayarları sayfasını açın ve sunucu kimlik bilgilerinizi yeniden girin.", + "Info_AccountAttentionRequiredMessage": "Bu hesap için dikkatiniz gerekli.", + "Info_AccountAttentionRequiredClickableMessage": "Bu hesabı düzeltmek ve yeniden senkronize etmek için tıklayın.", + "Info_AccountAttentionRequiredAction": "Düzelt", + "Info_AccountAttentionRequiredActionHint": "Bu hesap sorununu çözmek için Düzelt'e tıklayın.", "Info_AccountIssueFixSuccessMessage": "Bütün hesap sorunları giderildi.", "Info_AccountIssueFixSuccessTitle": "Başarılı", "Info_AttachmentOpenFailedMessage": "Bu eklenti açılamıyor.", @@ -370,6 +584,7 @@ "InfoBarMessage_SynchronizationDisabledFolder": "Bu klasör için senkronizasyon kapatıldı.", "InfoBarTitle_SynchronizationDisabledFolder": "Pasif Klasör", "Justify": "Yasla", + "MenuUpdateAvailable": "Güncelleme mevcut.", "Left": "Sol", "Link": "Bağlantı", "LinkedAccountsCreatePolicyMessage": "hesap bağlantısı oluşturabilmeniz için en az 2 adet hesap gerekli\nkaydet'e basıldığında hesap bağlantısı silinecektir", @@ -403,6 +618,7 @@ "MailOperation_Unarchive": "Arşivden çıkar", "MailOperation_ViewMessageSource": "Posta kaynağını göster", "MailOperation_Zoom": "Yakınlaştır", + "MailsDragging": "{0} öğe sürükleniyor", "MailsSelected": "{0} seçili", "MarkFlagUnflag": "Bayrak ekle/kaldır", "MarkReadUnread": "Okundu/okunmadı olarak işaretle", @@ -434,6 +650,8 @@ "Notifications_MultipleNotificationsTitle": "Yeni E-Posta", "Notifications_WinoUpdatedMessage": "{0} sürümünü deneyin", "Notifications_WinoUpdatedTitle": "Wino Mail güncellendi.", + "Notifications_StoreUpdateAvailableTitle": "Güncelleme mevcut", + "Notifications_StoreUpdateAvailableMessage": "Microsoft Store'dan kuruluma hazır daha yeni bir Wino Mail sürümü mevcut.", "OnlineSearchFailed_Message": "Çevrimiçi arama yapılamadı\n{0}\n\nÇevrimdışı postalar görüntüleniyor.", "OnlineSearchTry_Line1": "Aradığınızı bulamadınız mı?", "OnlineSearchTry_Line2": "Çevrimiçi aramayı deneyin.", @@ -446,7 +664,6 @@ "PaneLengthOption_Small": "Küçük", "Photos": "Fotoğraflar", "PreparingFoldersMessage": "Klasörler hazırlanıyor", - "ProtocolLogAvailable_Message": "Protocol logları hata ayıklama için mevcut.", "ProviderDetail_Gmail_Description": "Google Hesabı", "ProviderDetail_iCloud_Description": "Apple iCloud Hesabı", "ProviderDetail_iCloud_Title": "iCloud", @@ -465,9 +682,14 @@ "SearchBarPlaceholder": "Ara", "SearchingIn": "Aranıyor", "SearchPivotName": "Sonuçlar", + "Settings_KeyboardShortcuts_Title": "Klavye Kısayolları", + "Settings_KeyboardShortcuts_Description": "Postalar için hızlı işlemler amacıyla klavye kısayollarını yönetin.", "SettingConfigureSpecialFolders_Button": "Ayarla", "SettingsEditAccountDetails_IMAPConfiguration_Title": "IMAP/SMTP Ayarları", "SettingsEditAccountDetails_IMAPConfiguration_Description": "Gelen ve giden sunucu ayarlarınızı değiştirebilirsiniz.", + "SettingsEditAccountDetails_ImapCalDavSettings_Title": "IMAP ve takvim ayarları", + "SettingsEditAccountDetails_ImapCalDavSettings_Description": "Bu hesap için özel IMAP, SMTP ve CalDAV ayarları sayfasını açın.", + "SettingsEditAccountDetails_ImapCalDavSettings_Action": "Ayarları aç", "SettingsAbout_Description": "Wino hakkında daha fazla bilgi edinin.", "SettingsAbout_Title": "Hakkında", "SettingsAboutGithub_Description": "GitHub'daki hatalar bölümüne gidin.", @@ -490,6 +712,10 @@ "SettingsAppPreferences_SearchMode_Local": "Çevrimdışı", "SettingsAppPreferences_SearchMode_Online": "Çevrimiçi", "SettingsAppPreferences_SearchMode_Title": "Varsayılan arama modu", + "SettingsAppPreferences_ApplicationMode_Title": "Varsayılan uygulama modu", + "SettingsAppPreferences_ApplicationMode_Description": "Etkinleştirme türü açıkça belirlenmediğinde Wino'nun açılacağı varsayılan modu seçin.", + "SettingsAppPreferences_ApplicationMode_Mail": "Posta", + "SettingsAppPreferences_ApplicationMode_Calendar": "Takvim", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description": "Wino Mail arkaplanda çalışmaya devam edecektir. Yeni e-postalar için bildirim almaya devam edeceksiniz.", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title": "Arkaplanda çalışmaya devam et", "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description": "Uygulama sistem saatinin yanında çalışmaya arkaplanda devam edecektir. Yeni e-postalar geldiğinide bildirim almaya devam edeceksiniz.", @@ -506,12 +732,30 @@ "SettingsAppPreferences_StartupBehavior_FatalError": "Açılış modu ayarlanırken bilinmeyen bir hata oluştu.", "SettingsAppPreferences_StartupBehavior_Title": "Windows açıldığında saatin yanında küçültülmüş başlat", "SettingsAppPreferences_Title": "Uygulama Seçenekleri", + "SettingsAppPreferences_HideWinoAccountButton_Title": "Başlık çubuğunda Wino hesap düğmesini gizle", + "SettingsAppPreferences_HideWinoAccountButton_Description": "Wino hesap açılır menüsünü açan başlık çubuğu profil düğmesini gizleyin.", + "SettingsAppPreferences_StoreUpdateNotifications_Title": "Mağaza güncelleme bildirimleri", + "SettingsAppPreferences_StoreUpdateNotifications_Description": "Bir Microsoft Store güncellemesi mevcut olduğunda bildirimleri ve altbilgi işlemlerini gösterin.", + "SettingsAppPreferences_AiActions_Title": "Yapay Zeka işlemleri", + "SettingsAppPreferences_AiActions_Description": "Yapay zeka işlemleri için varsayılan dilleri ve özetlerin nerede kaydedileceğini seçin.", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Title": "Varsayılan çeviri dili", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Description": "Yapay zeka çeviri işlemlerinde kullanılacak varsayılan hedef dili seçin.", + "SettingsAppPreferences_AiSummarizeLanguage_Title": "Özetleme dili", + "SettingsAppPreferences_AiSummarizeLanguage_Description": "Gelecek yapay zeka özet çıktısı için tercih edilen özetleme dilini seçin.", + "SettingsAppPreferences_AiSummarySavePath_Title": "Varsayılan özet kaydetme yolu", + "SettingsAppPreferences_AiSummarySavePath_Description": "Yapay zeka özetlerini kaydederken Wino'nun varsayılan olarak kullanacağı klasörü seçin.", + "SettingsAppPreferences_AiSummarySavePath_Placeholder": "Sistem varsayılan kaydetme konumunu kullanın", + "SettingsAppPreferences_AiSummarySavePath_InvalidHint": "Bu klasör mevcut değil. Özetler için varsayılan kaydetme konumu kullanılacaktır.", "SettingsAutoSelectNextItem_Description": "Bir mesajı sildikten sonra sonrakini otomatik olarak seç.", "SettingsAutoSelectNextItem_Title": "Sonraki mesajı otomatik seç", "SettingsAvailableThemes_Description": "Tercihlerinize göre Wino için varolan bir tema seçini ya da yeni tema oluşturun.", "SettingsAvailableThemes_Title": "Mevcut Temalar", "SettingsCalendarSettings_Description": "Haftanın başlanıç günü, hücre yüksekliği gibi ayarlar...", "SettingsCalendarSettings_Title": "Takvim Ayarları", + "CalendarSettings_DefaultSnoozeDuration_Header": "Varsayılan erteleme süresi", + "CalendarSettings_DefaultSnoozeDuration_Description": "Takvim hatırlatıcı bildirimleri için varsayılan erteleme süresi ayarlayın.", + "CalendarSettings_TimedDayHeaderFormat_Header": "Zamanlı görünüm gün başlığı biçimi", + "CalendarSettings_TimedDayHeaderFormat_Description": "Gün, hafta ve iş haftası görünümlerinde üst gün etiketlerinin nasıl gösterileceğini seçin. ddd, dd, MMM veya dddd gibi tarih biçimleme tokenlerini kullanın.", "SettingsComposer_Title": "Editör", "SettingsComposerFont_Title": "Varsayılan Editör Yazı Tipi", "SettingsComposerFontFamily_Description": "E-posta oluştururken kullanılan yazı tipini değiştirin.", @@ -531,6 +775,9 @@ "SettingsDiscord_Title": "Discord Kanalı", "SettingsEditLinkedInbox_Description": "Hesap ekle / kaldır, yeniden adlandır ya da varolan bağlantıyı kaldır.", "SettingsEditLinkedInbox_Title": "Bağlı Hesabı Düzenle", + "SettingsWindowBackdrop_Title": "Pencere Arka Planı", + "SettingsWindowBackdrop_Description": "Wino pencereleri için bir perde etkisi seçin.", + "SettingsWindowBackdrop_Disabled": "Uygulama teması Varsayılan dışında bir tema seçildiğinde pencere arka planı seçimi devre dışı bırakılır.", "SettingsElementTheme_Description": "Wino için bir Windows teması seçin", "SettingsElementTheme_Title": "Element Teması", "SettingsElementThemeSelectionDisabled": "Uygulama teması 'Varsayılan' dışında ayarlandığında element temaları aktif değildir.", @@ -581,6 +828,8 @@ "SettingsManageAliases_Title": "Rumuzlar", "SettingsEditAccountDetails_Title": "Hesap Detaylarını Düzenle", "SettingsEditAccountDetails_Description": "Hesabınızın adını ve gönderilen kişi adını değiştirerek yeni bir renk atayabilirsiniz.", + "EditAccountDetailsPage_SaveSuccess_Title": "Değişiklikler Kaydedildi", + "EditAccountDetailsPage_SaveSuccess_Message": "Hesap bilgileriniz başarıyla güncellendi.", "SettingsManageLink_Description": "Hesapları sürükleyeren bağlantı oluşturun ya da kaldırın.", "SettingsManageLink_Title": "Bağlantıyı yönet", "SettingsMarkAsRead_Description": "Seçilen e-postaya ne olacağını seçin.", @@ -596,7 +845,41 @@ "SettingsNotifications_Title": "Bildirimler", "SettingsNotificationsAndTaskbar_Description": "Bu hesap için bildirim ve sistem çubuğu ayarlarını değiştirin.", "SettingsNotificationsAndTaskbar_Title": "Bildirimler ve Sistem Çubuğu", + "SettingsHome_Title": "Ana Sayfa", + "SettingsHome_SearchTitle": "Bir ayar bulun", + "SettingsHome_SearchDescription": "Özellik, konu veya anahtar kelimeye göre arayın ve doğru ayarlar sayfasına doğrudan gidin.", + "SettingsHome_SearchPlaceholder": "Ayarları ara", + "SettingsHome_SearchExamples": "Deneyin: tema, depolama, dil, imza", + "SettingsHome_QuickLinks_Title": "Hızlı bağlantılar", + "SettingsHome_QuickLinks_Description": "Kullanıcıların en çok başvurduğu ayarlara hızlıca ulaşın.", + "SettingsHome_StorageCard_Description": "Bu cihazda Wino'nun yerel MIME içeriğini ne kadar tuttuğunu görün ve gerektiğinde temizleyin.", + "SettingsHome_StorageEmptySummary": "Henüz önbelleğe alınmış MIME içeriği tespit edilmedi.", + "SettingsHome_StorageLoading": "Yerel MIME kullanımı kontrol ediliyor...", + "SettingsHome_Tips_Title": "İpuçları ve püf noktaları", + "SettingsHome_Tips_Description": "Birkaç küçük değişiklik, Wino'yu çok daha kişisel hissettirir.", + "SettingsHome_Tip_Theme": "Koyu mod veya vurgu renklerini değiştirmek mı istiyorsunuz? Kişiselleştirme'yi açın.", + "SettingsHome_Tip_Background": "Başlangıç davranışını ve arka plan senkronizasyonunu yönetmek için Uygulama Tercihlerini kullanın.", + "SettingsHome_Tip_Shortcuts": "Klavye kısayolları, postaları daha hızlı gezmenize yardımcı olur.", + "SettingsHome_Resources_Title": "Yararlı bağlantılar", + "SettingsHome_Resources_Description": "Proje kaynaklarını, destek bilgilerini ve sürüm kanallarını açın.", "SettingsOptions_Title": "Ayarlar", + "SettingsOptions_GeneralSection": "Genel", + "SettingsOptions_MailSection": "Posta", + "SettingsOptions_CalendarSection": "Takvim", + "SettingsOptions_MoreComingSoon": "Daha fazla seçenek yakında", + "SettingsOptions_HeroDescription": "Wino Mail deneyiminizi özelleştirin.", + "SettingsOptions_AccountsSummary": "{0} hesap(lar) yapılandırıldı.", + "SettingsSearch_ManageAccounts_Keywords": "hesap;hesaplar;posta kutusu;posta kutuları;takma ad;takma adlar;profil;adres;adresler", + "SettingsSearch_AppPreferences_Keywords": "başlatma;arka plan;başlatma;senkronizasyon;bildirimin;bildirimler;arama;bildirim alanı;varsayılanlar", + "SettingsSearch_LanguageTime_Keywords": "dil;zaman;saat;yerel ayar;bölge;biçim;24 saat;24 saat", + "SettingsSearch_Personalization_Keywords": "tema;koyu;açık;görünüm;vurgu;renk;renk;mod;düzen;yogunluk", + "SettingsSearch_About_Keywords": "hakkında;sürüm;web sitesi;gizlilik;GitHub;bağış;mağaza;destek", + "SettingsSearch_KeyboardShortcuts_Keywords": "kısayol;kısayollar;kısayol;kısayollar;klavye;tuşlar", + "SettingsSearch_MessageList_Keywords": "mesaj;mesajlar;liste;İleti zincirleme;konular;profil resmi;önizleme;gönderen", + "SettingsSearch_ReadComposePane_Keywords": "okuyucu;oluşturucu;yazı tipi;yazı tipleri;harici içerik;görüntüleme;okuma", + "SettingsSearch_SignatureAndEncryption_Keywords": "imza;imzalar;şifreleme;sertifika;sertifikalar;S/MIME;S/MIME;güvenlik", + "SettingsSearch_Storage_Keywords": "depolama;önbellek;önbellekleme;MIME;disk;alan;temizle;temizle;yerel veriler", + "SettingsSearch_CalendarSettings_Keywords": "takvim;hafta;saatler;plan;etkinlik;etkinlikler", "SettingsPaneLengthReset_Description": "Eğer problem yaşıyorsanız mesaj listesi geniişliğini sıfırlayın.", "SettingsPaneLengthReset_Title": "Mesaj Listesi Genişliğini Sıfırla", "SettingsPaypal_Description": "Daha fazla destek ol ❤️. Bağışlarınız için teşekkürler.", @@ -610,6 +893,8 @@ "SettingsPrefer24HourClock_Title": "24 saatlik format", "SettingsPrivacyPolicy_Description": "Giiizliilik sözleşmesine gözat.", "SettingsPrivacyPolicy_Title": "Gizlilik Politikası", + "SettingsWebsite_Description": "Wino Mail web sitesini açın.", + "SettingsWebsite_Title": "Web Sitesi", "SettingsReadComposePane_Description": "Yazı tipi, dış bağlantılar.", "SettingsReadComposePane_Title": "Okuyucu ve Editör", "SettingsReader_Title": "Okuyucu", @@ -625,6 +910,19 @@ "SettingsShowPreviewText_Title": "Önizleme", "SettingsShowSenderPictures_Description": "Gönderen resmini göster/gösterme.", "SettingsShowSenderPictures_Title": "Gönderen Resimleri", + "SettingsEmailTemplates_Title": "E-posta Şablonları", + "SettingsEmailTemplates_Description": "E-posta şablonlarını yönetin.", + "SettingsEmailTemplates_CreatePageTitle": "Yeni şablon", + "SettingsEmailTemplates_EditPageTitle": "Şablonu düzenle", + "SettingsEmailTemplates_NewTemplateTitle": "Yeni şablon", + "SettingsEmailTemplates_NewTemplateDescription": "Yeni bir e-posta şablonu oluşturun", + "SettingsEmailTemplates_NameTitle": "İsim", + "SettingsEmailTemplates_NamePlaceholder": "Şablon adı", + "SettingsEmailTemplates_DescriptionTitle": "Açıklama", + "SettingsEmailTemplates_DescriptionPlaceholder": "İsteğe bağlı açıklama", + "SettingsEmailTemplates_ContentTitle": "Şablon içeriği", + "SettingsEmailTemplates_ContentDescription": "Bu şablon için HTML içeriğini düzenleyin.", + "SettingsEmailTemplates_NameRequired": "Şablon adı gereklidir.", "SettingsEnableGravatarAvatars_Title": "Gravatar", "SettingsEnableGravatarAvatars_Description": "Use gravatar (if available) as sender picture", "SettingsEnableFavicons_Title": "Domain icons (Favicons)", @@ -645,6 +943,33 @@ "SettingsStartupItem_Title": "Başlangıç Hesabı", "SettingsStore_Description": "Biraz sevgi gösterin ❤️", "SettingsStore_Title": "Mağaza'da Puanla", + "SettingsStorage_Title": "Depolama", + "SettingsStorage_Description": "Yerel veri klasörünüzde depolanan MIME önbelleğini tara ve yönetin.", + "SettingsStorage_ScanFolder": "Yerel veri klasörünü tara", + "SettingsStorage_NoLocalMimeDataFound": "Yerel MIME verisi bulunamadı.", + "SettingsStorage_NoAccountsFound": "Kayıtlı hesap bulunamadı.", + "SettingsStorage_TotalUsage": "Toplam yerel MIME kullanımı: {0}", + "SettingsStorage_AccountUsageDescription": "{0} yerel MIME önbelleğinde kullanılıyor.", + "SettingsStorage_DeleteAll_Title": "Tüm MIME içeriğini sil", + "SettingsStorage_DeleteAll_Description": "Bu hesabın tüm MIME önbellek klasörünü sil.", + "SettingsStorage_DeleteAll_Button": "Tümü Sil", + "SettingsStorage_DeleteAll_Confirm_Title": "Tüm MIME içeriğini sil", + "SettingsStorage_DeleteAll_Confirm_Message": "{0} için tüm yerel MIME verisini silmek istiyor musunuz?", + "SettingsStorage_DeleteAll_Success": "Tüm MIME içeriği silindi.", + "SettingsStorage_DeleteOld_Title": "Eski MIME içeriğini sil", + "SettingsStorage_DeleteOld_Description": "Yerel veritabanındaki e-posta oluşturma tarihine göre MIME dosyalarını sil.", + "SettingsStorage_DeleteOld_1Month": "> 1 ay", + "SettingsStorage_DeleteOld_3Months": "> 3 ay", + "SettingsStorage_DeleteOld_6Months": "> 6 ay", + "SettingsStorage_DeleteOld_1Year": "> 1 yıl", + "SettingsStorage_DeleteOld_Confirm_Title": "Eski MIME içeriğini sil", + "SettingsStorage_DeleteOld_Confirm_Message": "{1} için {0} süresinden daha eski olan yerel MIME verisini silmek istiyor musunuz?", + "SettingsStorage_DeleteOld_Success": "{1} süresinden daha eski olan {0} MIME klasörü silindi.", + "SettingsStorage_1Month": "1 ay", + "SettingsStorage_3Months": "3 ay", + "SettingsStorage_6Months": "6 ay", + "SettingsStorage_1Year": "1 yıl", + "SettingsStorage_Months": "{0} ay", "SettingsTaskbarBadge_Description": "Okunmamış posta sayısını sistem çubuğu balonuna ekle.", "SettingsTaskbarBadge_Title": "Sistem Çubuğu Balonu", "SettingsThreads_Description": "Bağlı mesajları tek bir liste halinde kategorize eder.", @@ -683,6 +1008,9 @@ "SystemFolderConfigDialogValidation_InboxSelected": "Gelen Kutusu'nu başka bir sistem klasörü olarak seçemezsiniz.", "SystemFolderConfigSetupSuccess_Message": "Sistem klasörleri başarıyla ayarlandı.", "SystemFolderConfigSetupSuccess_Title": "Sistem Klasörleri Ayarlaması", + "SystemTrayMenu_ShowWino": "Wino Postalarını Aç", + "SystemTrayMenu_ShowWinoCalendar": "Wino Takvimini Aç", + "SystemTrayMenu_ExitWino": "Çıkış", "TestingImapConnectionMessage": "Sunucu bağlantısı kontrol ediliyor...", "TitleBarServerDisconnectedButton_Description": "Wino'nun sunucu bağlantısı yok. Tekrar bağlan'a basarak bağlantıyı onarınız.", "TitleBarServerDisconnectedButton_Title": "bağlantı yok", @@ -699,8 +1027,422 @@ "WinoUpgradeMessage": "Sınırsız Hesaba Yükselt", "WinoUpgradeRemainingAccountsMessage": "{1} hesabın {0} tanesi kullanıldı.", "Yesterday": "Dün", + "Smime_ImportCertificates_Success": "Sertifikalar başarıyla içe aktarıldı.", + "Smime_ImportCertificates_Error": "Sertifikalar içe aktarılırken hata: {0}", + "Smime_RemoveCertificates_Confirm": "{0} sertifikalarını gerçekten kaldırmak istiyor musunuz?", + "Smime_RemoveCertificates_Success": "Sertifikalar kaldırıldı.", + "Smime_ExportCertificates_Success": "Sertifikalar dışa aktarıldı.", + "Smime_ExportCertificates_Error": "Sertifikalar dışa aktarılırken hata.", + "Smime_CertificateDetails": "Konu: {0}\nVerici: {1}\nGeçerlilik Başlangıcı: {2}\nGeçerlilik Bitişi: {3}\nParmak izi: {4}", + "Smime_CertificatePassword_Title": "Sertifika parolası gerekli", + "Smime_CertificatePassword_Placeholder": "{0} için sertifika parolası (isteğe bağlı)", + "Smime_Confirm_Title": "Onayla", + "Buttons_OK": "OK", + "Buttons_Refresh": "Yenile", + "SettingsSignatureAndEncryption_Title": "İmza ve Şifreleme", + "SettingsSignatureAndEncryption_Description": "E-postaları imzalamak ve şifrelemek için S/MIME sertifikalarını yönetin.", + "SettingsSignatureAndEncryption_MyCertificatesHeader": "Benim sertifikalarım", + "SettingsSignatureAndEncryption_MyCertificatesDescription": "İmzalama ve şifreleme için kişisel sertifikalar", + "SettingsSignatureAndEncryption_RecipientCertificatesHeader": "Alıcı sertifikaları", + "SettingsSignatureAndEncryption_RecipientCertificatesDescription": "Açma için alıcı sertifikaları", + "SettingsSignatureAndEncryption_NameColumn": "İsim", + "SettingsSignatureAndEncryption_ExpiresColumn": "Sona erer", + "SettingsSignatureAndEncryption_ThumbprintColumn": "Parmak izi", + "Buttons_Remove": "Kaldır", + "Buttons_Export": "Dışa aktar", + "Buttons_Import": "İçe aktar", + "SettingsSignatureAndEncryption_SigningCertificate": "S/Mime İmzalama Sertifikası", + "SettingsSignatureAndEncryption_EncryptionCertificate": "S/MIME Şifreleme", + "SettingsSignatureAndEncryption_SigningCertificatePlaceholder": "Yok", + "SmimeSignaturesInMessage": "Bu mesajdaki imzalar:", + "SmimeSignatureEntry": "• {0} {1} ({2}, geçerli {3} - {4})", + "SmimeSigningCertificateInfoTitle": "S/MIME İmzalama Sertifikası Bilgisi", + "SmimeCertificateInfoTitle": "S/MIME Sertifikası Bilgisi", + "SmimeNoCertificateFileFound": "Hiç sertifika dosyası bulunamadı.", + "SmimeSaveCertificate": "Sertifikayı kaydet...", + "SmimeCertificate": "S/MIME Sertifikası", + "SmimeCertificateSavedTo": "Sertifika {0}'e kaydedildi", + "SmimeSignedTooltip": "Bu mesaj S/MIME sertifikası ile imzalanmıştır. Ayrıntılar için tıklayın.", + "SmimeEncryptedTooltip": "Bu mesaj S/MIME sertifikası ile şifrelenmiştir.", + "SmimeCertificateFileInfo": "Dosya: {0}", + "Composer_LightTheme": "Açık Tema", + "Composer_DarkTheme": "Koyu Tema", + "Composer_Outdent": "Girintiyi azalt", + "Composer_Indent": "Girinti ekle", + "Composer_BulletList": "Noktalı Liste", + "Composer_OrderedList": "Numaralı Liste", + "Composer_Stroke": "Çizgi", + "Composer_Bold": "Kalın", + "Composer_Italic": "İtalik", + "Composer_Underline": "Altı Çizili", + "Composer_CcBcc": "Cc ve Bcc", + "Composer_EnableSmimeSignature": "S/MIME imzasını etkinleştir/kapat", + "Composer_EnableSmimeEncryption": "S/MIME şifrelemesini etkinleştir/kapat", + "Composer_LocalDraftSyncInfo": "Bu taslak yalnızca yerel. Wino bunu posta sunucunuza göndermeyi başaramadı. Sunucuya göndermeyi yeniden denemek için tıklayın.", + "Composer_CertificateExpires": "Sona eriyor: ", + "Composer_SmimeSignature": "S/MIME İmza", + "Composer_SmimeEncryption": "S/MIME Şifreleme", + "Composer_EmailTemplatesPlaceholder": "E-posta şablonları", + "Composer_AiSummarize": "Yapay zeka ile özetle", + "Composer_AiSummarizeDescription": "Bu e-postadan ana noktaları, eylem adımlarını ve kararları çıkarın.", + "Composer_AiTranslate": "Yapay zeka ile çevir", + "Composer_AiActions": "Yapay Zeka Eylemleri", + "Composer_AiRewrite": "Yapay zeka ile yeniden yaz", + "AiActions_CheckingStatus": "Yapay zeka erişimi kontrol ediliyor...", + "AiActions_SignedOutTitle": "Wino AI Paketi Kilidi Aç", + "AiActions_SignedOutDescription": "Wino Hesabınıza giriş yapıp AI Paket eklentisini etkinleştirdikten sonra e-postaları AI ile çevirin, yeniden yazın ve özetleyin.", + "AiActions_NoPackTitle": "AI Paketi gerekli", + "AiActions_NoPackDescription": "Giriş yaptınız, ancak AI Paketi henüz etkin değil. Wino'nun AI çeviri, yeniden yazma ve özetleme araçlarını kullanmak için satın alın.", + "AiActions_UsageSummary": "Bu ay kullanılan kredi: {0} / {1} kredi.", + "Composer_AiRewritePolite": "Daha nazik hale getir", + "Composer_AiRewritePoliteDescription": "Aynı amacı koruyarak ifadeyi yumuşatır.", + "Composer_AiRewriteAngry": "Kızgın hale getir", + "Composer_AiRewriteAngryDescription": "Daha keskin ve daha çatışmacı bir üslup kullanır.", + "Composer_AiRewriteHappy": "Mutlu hale getir", + "Composer_AiRewriteHappyDescription": "Daha neşeli ve hevesli bir ton ekler.", + "Composer_AiRewriteFormal": "Resmi hale getir", + "Composer_AiRewriteFormalDescription": "Mesajı daha profesyonel ve yapılandırılmış bir şekilde ifade eder.", + "Composer_AiRewriteFriendly": "Dostane hale getir", + "Composer_AiRewriteFriendlyDescription": "Mesajı daha samimi bir tonla daha yakın hale getirir.", + "Composer_AiRewriteShorter": "Kısa hale getir", + "Composer_AiRewriteShorterDescription": "Metni kısaltır ve gereksiz ayrıntıları çıkarır.", + "Composer_AiRewriteClearer": "Daha net hale getir", + "Composer_AiRewriteClearerDescription": "Okunabilirliği artırır ve mesajı daha kolay takip edilebilir hale getirir.", + "Composer_AiRewriteCustom": "Özel", + "Composer_AiRewriteCustomDescription": "Kendi yeniden yazım amacınızı tanımlayın.", + "Composer_AiRewriteCustomPlaceholder": "Mesajın nasıl yeniden yazılmasını istediğinizi tanımlayın.", + "Composer_AiRewriteMode": "Ton yeniden yaz", + "Composer_AiRewriteApply": "Yeniden yazımı uygula", + "Composer_AiTranslateDialogTitle": "Yapay Zeka ile Çevir", + "Composer_AiTranslateDialogDescription": "Hedef dili veya kültür kodunu girin, örneğin en-US, tr-TR, de-DE veya fr-FR.", + "Composer_AiTranslateApply": "Çevir", + "Composer_AiTranslateLanguage": "Hedef dil", + "Composer_AiTranslateCustomPlaceholder": "Kültür kodunu girin", + "Composer_AiTranslateLanguageEnglish": "İngilizce (en-US)", + "Composer_AiTranslateLanguageTurkish": "Türkçe (tr-TR)", + "Composer_AiTranslateLanguageGerman": "Almanca (de-DE)", + "Composer_AiTranslateLanguageFrench": "Fransızca (fr-FR)", + "Composer_AiTranslateLanguageSpanish": "İspanyolca (es-ES)", + "Composer_AiTranslateLanguageItalian": "İtalyanca (it-IT)", + "Composer_AiTranslateLanguagePortugueseBrazil": "Portekizce (Brezilya) (pt-BR)", + "Composer_AiTranslateLanguageDutch": "Felemenkçe (nl-NL)", + "Composer_AiTranslateLanguagePolish": "Lehçe (pl-PL)", + "Composer_AiTranslateLanguageRussian": "Rusça (ru-RU)", + "Composer_AiTranslateLanguageJapanese": "Japonca (ja-JP)", + "Composer_AiTranslateLanguageKorean": "Korece (ko-KR)", + "Composer_AiTranslateLanguageChineseSimplified": "Çince, Basitleştirilmiş (zh-CN)", + "Composer_AiTranslateLanguageArabic": "Arapça (ar-SA)", + "Composer_AiTranslateLanguageHindi": "Hintçe (hi-IN)", + "Composer_AiTranslateLanguageOther": "Diğer...", + "Composer_AiBusyTitle": "Yapay Zeka zaten çalışıyor", + "Composer_AiBusyMessage": "Mevcut Yapay Zeka işlemi tamamlanana kadar lütfen bekleyin.", + "Composer_AiSignInRequired": "AI özelliklerini kullanmak için Wino hesabınıza giriş yapın.", + "Composer_AiMissingHtml": "Henüz Wino AI'ye gönderilecek bir mesaj içeriği yok.", + "Composer_AiQuotaUnavailable": "Yapay zeka sonucu uygulandı.", + "Composer_AiAppliedMessage": "Yapay zeka sonucu kompozitöre uygulandı. Geri almak istersen Geri Al'ı kullan.", + "Composer_AiSummarizeSuccessTitle": "AI özeti uygulandı", + "Composer_AiTranslateSuccessTitle": "AI çevirisi uygulandı", + "Composer_AiRewriteSuccessTitle": "AI yeniden yazımı uygulandı", + "Composer_AiErrorTitle": "Yapay zeka işlemi başarısız oldu", + "Reader_AiAppliedMessage": "Bu mesaj için yapay zeka sonucu artık gösteriliyor. Orijinal içeriği görmek için mesajı yeniden açın.", "SettingsAppPreferences_EmailSyncInterval_Title": "Email sync interval", - "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail." + "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail.", + "ContactsPage_Title": "Kişiler", + "ContactsPage_AddContact": "Kişi Ekle", + "ContactsPage_EditContact": "Kişiyi Düzenle", + "ContactsPage_DeleteContact": "Kişi Sil", + "ContactsPage_SearchPlaceholder": "Kişileri ara...", + "ContactsPage_NoContacts": "Kişi bulunamadı", + "ContactsPage_ContactsCount": "{0} kişiler", + "ContactsPage_SelectedContactsCount": "{0} seçili", + "ContactsPage_DeleteSelectedContacts": "Seçili Kişileri Sil", + "ContactEditDialog_Title": "Kişiyi Düzenle", + "ContactEditDialog_PhotoSection": "Fotoğraf", + "ContactEditDialog_ChoosePhoto": "Fotoğraf Seç", + "ContactEditDialog_RemovePhoto": "Fotoğrafı Kaldır", + "ContactEditDialog_NameHeader": "İsim", + "ContactEditDialog_NamePlaceholder": "Kişi adı", + "ContactEditDialog_EmailHeader": "E-posta Adresi", + "ContactEditDialog_EmailPlaceholder": "contact@example.com", + "ContactEditDialog_InfoSection": "İletişim Bilgisi", + "ContactEditDialog_RootContactInfo": "Bu kök kişi hesaplarınızla ilişkilidir ve silinemez.", + "ContactEditDialog_OverriddenContactInfo": "Bu iletişim manuel olarak değiştirilmiştir ve senkronizasyon sırasında güncellenmeyecektir.", + "ContactsPage_Subtitle": "E-posta kişilerinizi ve bilgilerini yönetin", + "ContactStatus_Account": "Hesap", + "ContactStatus_Modified": "Değiştirildi", + "ContactAction_Edit": "Kişiyi Düzenle", + "ContactAction_ChangePhoto": "Fotoğrafı Değiştir", + "ContactAction_Delete": "Kişi Sil", + "ContactAction_Add": "Kişi Ekle", + "ContactSelection_Selected": "seçili", + "ContactSelection_SelectAll": "Tümünü Seç", + "ContactSelection_Clear": "Seçimi Temizle", + "ContactsPage_EmptyState": "Gösterilecek iletişim yok", + "ContactsPage_AddFirstContact": "İlk kişiyi ekleyin", + "ContactsPage_ContactsCountSuffix": "kişiler", + "ContactsPane_NewContact": "Yeni Kişi", + "ContactsPane_DescriptionTitle": "Kişilerinizi yönetin", + "ContactsPane_DescriptionBody": "Kişiler oluşturun, bunları yeniden adlandırın, profil resimlerini güncelleyin ve kaydedilmiş bilgileri tek bir yerde düzenli tutun.", + "ContactEditDialog_AddTitle": "Kişi Ekle", + "ContactInfoBar_ContactAdded": "Kişi başarıyla eklendi.", + "ContactInfoBar_ContactUpdated": "Kişi başarıyla güncellendi.", + "ContactInfoBar_ContactsDeleted": "Kişiler başarıyla silindi.", + "ContactInfoBar_ContactPhotoUpdated": "Kişi fotoğrafı başarıyla güncellendi.", + "ContactInfoBar_FailedToLoadContacts": "Kişiler yüklenemedi: {0}", + "ContactInfoBar_FailedToAddContact": "Kişi eklenemedi: {0}", + "ContactInfoBar_FailedToUpdateContact": "Kişi güncellenemedi: {0}", + "ContactInfoBar_FailedToDeleteContacts": "Kişiler silinemedi: {0}", + "ContactInfoBar_FailedToUpdatePhoto": "Fotoğraf güncellenemedi: {0}", + "ContactInfoBar_CannotDeleteRoot": "Kök kişiler silinemez.", + "ContactConfirmDialog_DeleteTitle": "Kişiyi Sil", + "ContactConfirmDialog_DeleteMessage": "Kişi '{0}' silmek istediğinize emin misiniz?", + "ContactConfirmDialog_DeleteMultipleMessage": "{0} adet kişi silmek istediğinizden emin misiniz?", + "ContactConfirmDialog_DeleteButton": "Sil", + "CalendarAccountSettings_Title": "Takvim Hesabı Ayarları", + "CalendarAccountSettings_Description": "{0} için takvim ayarlarını yönetin.", + "CalendarAccountSettings_AccountColor": "Hesap Rengi", + "CalendarAccountSettings_AccountColorDescription": "Bu takvim hesabı için görüntülenen rengi değiştirin.", + "CalendarAccountSettings_SyncEnabled": "Senkronizasyonu Etkinleştir", + "CalendarAccountSettings_SyncEnabledDescription": "Bu hesap için takvim senkronizasyonunu etkinleştirin veya devre dışı bırakın.", + "CalendarAccountSettings_DefaultShowAs": "Varsayılan Gösterim Durumu", + "CalendarAccountSettings_DefaultShowAsDescription": "Bu hesap ile oluşturulan yeni etkinlikler için varsayılan kullanılabilirlik durumunu belirtin.", + "CalendarAccountSettings_PrimaryCalendar": "Birincil Takvim", + "CalendarAccountSettings_PrimaryCalendarDescription": "Bu takvimi hesabın birincil takvimi olarak işaretleyin.", + "CalendarSettings_NewEventBehavior_Header": "Yeni Etkinlik düğmesi davranışı", + "CalendarSettings_NewEventBehavior_Description": "Yeni Etkinlik düğmesinin her seferinde bir takvim sorup sormayacağını mı yoksa her zaman belirli bir takvimi mi açacağını seçin.", + "CalendarSettings_NewEventBehavior_AskEachTime": "Her seferinde sor.", + "CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar": "Her zaman belirli bir takvimi kullan.", + "CalendarSettings_Rendering_Title": "Görüntüleme", + "CalendarSettings_Rendering_Description": "Takvim yerleşimini ve görüntüleme davranışını yapılandırın.", + "CalendarSettings_Notifications_Title": "Bildirimler", + "CalendarSettings_Notifications_Description": "Varsayılan hatırlatma ve erteleme davranışını seçin.", + "CalendarSettings_Preferences_Title": "Tercihler", + "CalendarSettings_Preferences_Description": "Yeni Etkinlik düğmesinin nasıl davrandığını ayarlayın.", + "WhatIsNew_GetStartedButton": "Başla", + "WhatIsNew_ContinueAnywayButton": "Her ne olursa olsun devam et", + "WhatIsNew_PreparingForNewVersionButton": "Yeni sürüm için hazırlanıyor...", + "WhatIsNew_MigrationPreparing_Title": "Verileriniz hazırlanıyor", + "WhatIsNew_MigrationPreparing_Description": "Wino güncelleme geçişlerini uyguluyor. Bu sürüm için hesap verilerinizi hazırlarken lütfen bekleyin.", + "WhatIsNew_MigrationFailedMessage": "Geçişler hata kodu {0} ile uygulanamadı. Uygulamayı kullanmaya devam edebilirsiniz. Ancak ciddi problemlerle karşılaşırsanız lütfen uygulamayı yeniden yükleyin.", + "WhatIsNew_MigrationNotification_Title": "Wino Mail Güncellendi", + "WhatIsNew_MigrationNotification_Message": "Güncellemeyi tamamlamak ve yeni özellikleri görmek için uygulamayı açın.", + "WelcomeWindow_Title": "Wino Mail'e Hoş Geldiniz", + "WelcomeWindow_Subtitle": "Posta ve Takvim için yerel Windows deneyimi.", + "WelcomeWindow_WhatsNewTitle": "Son Değişiklikler", + "WelcomeWindow_FeaturesTitle": "Özellikler", + "WelcomeWindow_WhatsNewTab": "Neler Yeni", + "WelcomeWindow_FeaturesTab": "Özellikler", + "WelcomeWindow_GetStartedButton": "Bir hesap ekleyerek başlayın", + "WelcomeWindow_GetStartedDescription": "Wino Mail'e başlamak için Outlook, Gmail veya IMAP hesabınızı ekleyin.", + "WelcomeWindow_ImportFromWinoAccount": "Wino Hesabınızdan İçe Aktar", + "WelcomeWindow_ImportInProgress": "Senkronize edilmiş tercihleriniz ve hesaplarınız içe aktarılıyor...", + "WelcomeWindow_ImportNoAccountsFound": "Wino Hesabınızda senkronize edilen hesap bulunamadı. Tercihler mevcutsa geri yüklendi. Bir hesabı manuel olarak eklemek için Başla'ya tıklayın.", + "WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} senkronize edilmiş hesap bu cihazda zaten mevcut. Gerekirse başka bir hesap eklemek için Başla'ya tıklayın.", + "WelcomeWindow_SetupTitle": "Hesabınızı Kurun", + "WelcomeWindow_SetupSubtitle": "Başlamak için e-posta sağlayıcınızı seçin", + "WelcomeWindow_AddAccountButton": "Hesap Ekle", + "WelcomeWindow_SkipForNow": "Şimdilik atla — Daha sonra kuracağım.", + "WelcomeWindow_AppDescription": "Hızlı, odaklı bir gelen kutusu — Windows 11 için yeniden tasarlandı", + "WelcomeWizard_Step1Title": "Hoşgeldiniz", + "SystemTrayMenu_Open": "Aç", + "WinoAccount_Titlebar_SyncBenefitTitle": "Senkronizasyon Ayarları", + "WinoAccount_Titlebar_SyncBenefitDescription": "Wino tercihlerinizi cihazlar arasında senkronize edin.", + "WinoAccount_Titlebar_AddonsBenefitTitle": "Eklentileri Aç", + "WinoAccount_Titlebar_AddonsBenefitDescription": "Wino AI Paketi gibi premium özelliklere erişin.", + "WinoAccount_Management_Description": "Wino Hesabınızı, AI Pack erişimini ve senkronize tercihler ile hesap ayrıntılarını yönetin.", + "WinoAccount_Management_SignedOutTitle": "Wino Mail'e giriş yapın", + "WinoAccount_Management_SignedOutDescription": "Postanızı senkronize etmek ve Yapay Zeka özelliklerine erişmek ve ayarlarınızı cihazlar arasında yönetmek için oturum açın veya bir hesap oluşturun.", + "WinoAccount_Management_ProfileSectionHeader": "Profil", + "WinoAccount_Management_AddOnsSectionHeader": "Wino Eklentileri", + "WinoAccount_Management_DataSectionHeader": "Veri", + "WinoAccount_Management_AccountActionsSectionHeader": "Hesap işlemleri", + "WinoAccount_Management_AccountCardTitle": "Hesap", + "WinoAccount_Management_AccountCardDescription": "Wino Hesabınızın e-posta adresi ve mevcut hesap durumu.", + "WinoAccount_Management_AiPackCardTitle": "AI Paketi", + "WinoAccount_Management_AiPackCardDescription": "Wino AI Paketi'nin etkin olup olmadığını ve kullanılabilir kullanım miktarını görün.", + "WinoAccount_Management_AiPackActive": "AI Paketi etkin", + "WinoAccount_Management_AiPackInactive": "AI Paketi etkin değil", + "WinoAccount_Management_AiPackUsage": "{0} kullanımdan {1} kullanım tüketildi. Kalan {2}.", + "WinoAccount_Management_AiPackBillingPeriod": "Faturalama dönemi: {0:d} - {1:d}", + "WinoAccount_Management_AiPackUnknownUsage": "Kullanım ayrıntıları şu anda mevcut değil.", + "WinoAccount_Management_AiPackBuyDescription": "E-postaları AI ile çevirmek, yeniden yazmak veya özetlemek için Wino AI Paketi'ni satın alın.", + "WinoAccount_Management_AiPackPromoTitle": "AI Paketi Kilidini Kaldır", + "WinoAccount_Management_AiPackPromoDescription": "Mesajları 50'den fazla dile çevirin, netlik ve ton için yeniden yazın ve uzun e-posta zincirlerinin anlık özetlerini alın.", + "WinoAccount_Management_AiPackPromoPrice": "$4.99 / mo", + "WinoAccount_Management_AiPackPromoRequests": "1.000 kredi", + "WinoAccount_Management_AiPackGetButton": "AI Paketi Al", + "WinoAddOn_AI_PACK_Name": "Wino AI Paketi", + "WinoAddOn_AI_PACK_Description": "Wino Mail için çevirmek, yeniden yazmak ve özetlemek işlemlerinde AI destekli araçlar.", + "WinoAddOn_AI_PACK_Keywords": "AI, çeviri, yeniden yazma, özetleme, verimlilik", + "WinoAddOn_UNLIMITED_ACCOUNTS_Name": "Sınırsız Hesaplar", + "WinoAddOn_UNLIMITED_ACCOUNTS_Description": "İhtiyacınız olan kadar e-posta hesabı ekleyin ve hesap sınırını kaldırın.", + "WinoAddOn_UNLIMITED_ACCOUNTS_Keywords": "hesaplar, sınırsız, premium, eklenti", + "WinoAccount_Management_PurchaseRequiresSignIn": "Bu satınımı tamamlamak için Wino Hesabınızla oturum açın.", + "WinoAccount_Management_PurchaseStartFailed": "Wino bu Microsoft Store satın alımını tamamlayamadı.", + "WinoAccount_Management_StoreSyncFailed": "Satın alımınız tamamlandı, ancak Wino hesap avantajlarınızı henüz yenileyemedi. Lütfen bir süre sonra tekrar deneyin.", + "WinoAccount_Management_AiPackSubscriptionActive": "Aboneliğiniz aktif", + "WinoAccount_Management_AiPackRenews": "Yenileniyor {0:d}", + "WinoAccount_Management_AiPackRequestsUsed": "Bu ay kullanılan krediler", + "WinoAccount_Management_AiPackResets": "Sıfırlamalar {0:d}", + "WinoAccount_Management_AiPackUsageLoadFailed": "AI kullanım bakiyenizin yüklenmesinde sorun yaşadık.", + "WinoAccount_Management_AiPackFeatureTranslate": "Çevir", + "WinoAccount_Management_AiPackFeatureRewrite": "Yeniden Yaz", + "WinoAccount_Management_AiPackFeatureSummarize": "Özetle", + "WinoAccount_Management_AddOnLoadFailed": "Bu eklentinin yüklenmesi sırasında sorun yaşadık.", + "WinoAccount_Management_SyncPreferencesTitle": "Tercihleri ve Hesapları Senkronize Et", + "WinoAccount_Management_SyncPreferencesDescription": "Wino tercihlerinizi ve posta kutusu ayrıntılarınızı cihazlar arasında içe aktarın veya dışa aktarın. Şifreler, belirteçler ve diğer hassas bilgiler asla senkronize edilmez.", + "WinoAccount_Management_SignOutTitle": "Çıkış Yap", + "WinoAccount_Management_SignOutDescription": "Bu cihazdaki hesabınızdan çıkış yapın", + "WinoAccount_Management_StatusLabel": "Durum: {0}", + "WinoAccount_Management_NoRemoteSettings": "Bu hesap için henüz senkronize edilmiş veri bulunmuyor.", + "WinoAccount_Management_ExportSucceeded": "Seçili Wino verileri başarıyla dışa aktarıldı.", + "WinoAccount_Management_ExportPreferencesSucceeded": "Tercihleriniz Wino Hesabınıza aktarıldı.", + "WinoAccount_Management_ExportAccountsSucceeded": "{0} hesap ayrıntısı Wino Hesabınıza aktarıldı.", + "WinoAccount_Management_ImportSucceeded": "Wino Hesabınızdan senkronize edilen veriler içe aktarıldı.", + "WinoAccount_Management_ImportPreferencesSucceeded": "{0} senkronize tercih uygulandı.", + "WinoAccount_Management_ImportAccountsSucceeded": "{0} hesap içe aktarıldı.", + "WinoAccount_Management_ImportDuplicateAccountsSkipped": "Bu cihazda zaten mevcut olan {0} adet hesap atlandı.", + "WinoAccount_Management_ImportPartial": "{0} senkronize edilmiş ayar uygulandı. {1} ayar geri yüklenemedi.", + "WinoAccount_Management_ImportReloginReminder": "Şifreler, erişim belirteçleri ve diğer hassas bilgiler içe aktarılmadı. Bu cihazdaki her hesap için bunları kullanmadan önce yeniden oturum açın.", + "WinoAccount_Management_SerializeFailed": "Wino mevcut ayarlarınızı serileştiremedi.", + "WinoAccount_Management_EmptyExport": "Dışa aktarılacak hiçbir ayar değeri yok.", + "WinoAccount_Management_ImportEmpty": "Geri yüklemek için senkronize edilmiş veri yükünde yeni bir şey yok.", + "WinoAccount_Management_ExportDialog_Title": "Wino Hesabınıza Dışa Aktar", + "WinoAccount_Management_ExportDialog_Description": "Wino Hesabınıza hangi öğeleri senkronize etmek istediğinizi seçin.", + "WinoAccount_Management_ExportDialog_IncludePreferences": "Ayarlar", + "WinoAccount_Management_ExportDialog_IncludeAccounts": "Hesaplar", + "WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Şifreler, erişim belirteçleri ve diğer hassas bilgiler senkronize edilmez.", + "WinoAccount_Management_ExportDialog_AccountsRelogin": "Başka bir PC'de içe aktarılan hesaplar, kullanılabilir hale gelmeden önce yine oturum açmanızı gerektirecek.", + "WinoAccount_Management_ExportDialog_InProgress": "Seçili Wino verileri dışa aktarılıyor...", + "WinoAccount_Management_LoadFailed": "Wino en son Wino Hesap bilgilerini yükleyemedi.", + "WinoAccount_Management_ActionFailed": "Wino Hesap isteği tamamlanamadı.", + "WinoAccount_SettingsSection_Title": "Wino Hesabı", + "WinoAccount_SettingsSection_Description": "Yerel oturum açma hizmetinizi kullanarak bir Wino Hesabı oluşturun veya oturum açın.", + "WinoAccount_RegisterButton_Title": "Hesap Oluştur", + "WinoAccount_RegisterButton_Description": "E-posta ve şifre ile Wino Hesabı oluşturun.", + "WinoAccount_RegisterButton_Action": "Kayıt sayfasını aç", + "WinoAccount_LoginButton_Title": "Giriş Yap", + "WinoAccount_LoginButton_Description": "E-posta ve şifre ile mevcut Wino Hesabınıza giriş yapın.", + "WinoAccount_LoginButton_Action": "Giriş sayfasını aç", + "WinoAccount_SignOutButton_Title": "Çıkış Yap", + "WinoAccount_SignOutButton_Description": "Yerel olarak saklanan Wino Hesap oturumunu kaldır.", + "WinoAccount_SignOutButton_Action": "Çıkış Yap", + "WinoAccount_RegisterDialog_Title": "Wino Hesabı Oluştur", + "WinoAccount_RegisterDialog_Description": "Wino deneyiminizi senkronize tutmak ve hesap tabanlı ek özellikleri kullanabilmek için bir Wino Hesabı oluşturun.", + "WinoAccount_RegisterDialog_HeroTitle": "Wino Hesabınızı Oluşturun", + "WinoAccount_RegisterDialog_BenefitsTitle": "Neden bir hesap oluşturalım?", + "WinoAccount_RegisterDialog_BenefitSyncTitle": "Ayarları cihazlar arasında içe aktarın ve dışa aktarın.", + "WinoAccount_RegisterDialog_BenefitSyncDescription": "Kurulumunuzu baştan kurmadan Wino ayarlarını cihazlar arasında taşıyın.", + "WinoAccount_RegisterDialog_BenefitAiTitle": "Wino AI Pack (ücretli) gibi özel eklentilere erişim", + "WinoAccount_RegisterDialog_BenefitAiDescription": "Bir hesapla mevcut olduğunda premium Wino özelliklerini kullanıma açın.", + "WinoAccount_RegisterDialog_DifferenceTitle": "Wino Hesabı, e-posta hesaplarınızdan bağımsızdır.", + "WinoAccount_RegisterDialog_DifferenceDescription": "Outlook, Gmail, IMAP veya diğer e-posta hesaplarınız olduğu gibi kalır. Wino Hesabı yalnızca Wino'ya özgü özellikleri ve hesap tabanlı ek özellikleri yönetir.", + "WinoAccount_RegisterDialog_PrimaryButton": "Kayıt Ol", + "WinoAccount_RegisterDialog_PrivacyTitle": "Gizlilik ve API İşleme", + "WinoAccount_RegisterDialog_PrivacyDescription": "Wino AI Pack gibi isteğe bağlı eklentiler, bu özellikleri kullandığınızda yalnızca seçili e-posta HTML içeriğini Wino API hizmetine gönderebilir.", + "WinoAccount_RegisterDialog_PrivacyLinkText": "Gizlilik Politikası'nı Oku", + "WinoAccount_RegisterDialog_PrivacyCheckbox": "Gizlilik politikasını kabul ediyorum.", + "WinoAccount_LoginDialog_Title": "Wino Hesabına Giriş Yap", + "WinoAccount_LoginDialog_Description": "Wino Hesabınıza giriş yaparak Wino kurulumunuzu senkronize edin ve hesap tabanlı özelliklere erişin.", + "WinoAccount_LoginDialog_HeroTitle": "Tekrar Hoşgeldiniz", + "WinoAccount_LoginDialog_BenefitsTitle": "Giriş Yapmanın Sunduğu Avantajlar", + "WinoAccount_LoginDialog_BenefitsDescription": "Wino Hesabınızı kullanarak cihazlar arasında ayarları senkronize etmeye devam edin ve Wino AI Pack gibi ücretli eklentilere erişin.", + "WinoAccount_LoginDialog_DifferenceTitle": "Bu, e-posta posta kutunuz için oturum açma değildir.", + "WinoAccount_LoginDialog_DifferenceDescription": "Burada oturum açmak, Wino'daki Outlook, Gmail veya IMAP hesaplarınızı eklemez veya değiştirmez. Sadece Wino'ya özgü hizmetlere giriş yapmanızı sağlar.", + "WinoAccount_LoginDialog_ForgotPasswordLink": "Şifrenizi mi unuttunuz?", + "WinoAccount_EmailLabel": "E-posta", + "WinoAccount_EmailPlaceholder": "name@example.com", + "WinoAccount_PasswordLabel": "Şifre", + "WinoAccount_ConfirmPasswordLabel": "Şifreyi Doğrula", + "WinoAccount_ForgotPasswordDialog_Title": "Şifrenizi Sıfırlayın", + "WinoAccount_ForgotPasswordDialog_PrimaryButton": "Şifre sıfırlama e-postası gönder", + "WinoAccount_ForgotPasswordDialog_BackToSignIn": "Girişe geri dön", + "WinoAccount_ForgotPasswordDialog_Description": "Wino Hesap e-posta adresinizi girin; adres kayıtlıysa size bir şifre sıfırlama bağlantısı göndereceğiz.", + "WinoAccount_Validation_EmailRequired": "E-posta gerekli.", + "WinoAccount_Validation_PasswordRequired": "Şifre gerekli.", + "WinoAccount_Validation_PasswordMismatch": "Şifreler eşleşmiyor.", + "WinoAccount_Validation_PrivacyConsentRequired": "Wino Hesabı oluşturmadan önce gizlilik politikasını kabul etmelisiniz.", + "WinoAccount_Error_InvalidCredentials": "E-posta adresi veya şifre yanlış.", + "WinoAccount_Error_AccountLocked": "Bu hesap geçici olarak kilitlidir.", + "WinoAccount_Error_AccountBanned": "Bu hesap yasaklandı.", + "WinoAccount_Error_AccountSuspended": "Bu hesap askıya alındı.", + "WinoAccount_Error_EmailNotConfirmed": "Giriş yapmadan önce e-posta adresinizi doğrulayın.", + "WinoAccount_Error_EmailConfirmationRequired": "Giriş yapmadan önce e-posta adresinizi doğrulayın.", + "WinoAccount_Error_EmailConfirmationResendNotAvailable": "Yeni bir onay e-postası henüz mevcut değil.", + "WinoAccount_Error_EmailConfirmationResendInvalid": "Bu onay isteği artık geçerli değildir. Lütfen tekrar giriş yapmayı deneyin.", + "WinoAccount_Error_EmailNotRegistered": "Bu e-posta adresi kayıtlı değildir.", + "WinoAccount_Error_RefreshTokenInvalid": "Oturumunuz artık geçerli değildir. Lütfen tekrar oturum açın.", + "WinoAccount_Error_EmailAlreadyRegistered": "Bu e-posta adresi zaten kayıtlı.", + "WinoAccount_Error_ExternalLoginEmailRequired": "Harici oturum açmayı tamamlamak için bir e-posta adresine ihtiyaç vardır.", + "WinoAccount_Error_ExternalLoginInvalid": "Harici oturum açma isteği geçersiz.", + "WinoAccount_Error_ExternalAuthStateInvalid": "Harici oturum açma durumu geçersiz veya süresi dolmuş.", + "WinoAccount_Error_ExternalAuthCodeInvalid": "Harici oturum açma kodu geçersiz veya süresi dolmuş.", + "WinoAccount_Error_AiPackRequired": "Bu işlem için aktif bir Wino AI Pack aboneliği gereklidir.", + "WinoAccount_Error_AiQuotaExceeded": "Geçerli faturalandırma döneminde AI Pack kullanımınızın sınırı aşıldı.", + "WinoAccount_Error_AiHtmlEmpty": "İşlenecek e-posta içeriği yok.", + "WinoAccount_Error_AiHtmlTooLarge": "Bu e-posta, Wino AI ile işlemek için çok büyük.", + "WinoAccount_Error_AiUnsupportedLanguage": "Bu dil desteklenmiyor. Geçerli bir kültür kodu olarak en-US veya tr-TR deneyin.", + "WinoAccount_Error_Forbidden": "Bu işlemi gerçekleştirme izniniz yok.", + "WinoAccount_Error_ValidationFailed": "İstek geçersiz. Lütfen girilen değerleri inceleyin.", + "WinoAccount_RegisterSuccessMessage": "{0} için Wino Hesabı kaydı tamamlandı.", + "WinoAccount_LoginSuccessMessage": "{0} olarak Wino Hesabınıza giriş yapıldı.", + "WinoAccount_EmailConfirmationSentDialog_Title": "E-posta adresinizi doğrulayın.", + "WinoAccount_EmailConfirmationSentDialog_Message": "{0} adresine bir e-posta onayı gönderdik. Lütfen onaylayın ve tekrar giriş yapmayı deneyin.", + "WinoAccount_EmailConfirmationPendingDialog_Title": "E-posta onayı gerekiyor.", + "WinoAccount_EmailConfirmationPendingDialog_Message": "{0} adresini doğrulamanızı hâlâ bekliyoruz.", + "WinoAccount_EmailConfirmationPendingDialog_ResendButton": "Onay e-postasını yeniden gönder", + "WinoAccount_EmailConfirmationPendingDialog_Countdown": "Onay e-postasını {0} içinde yeniden gönderebilirsiniz.", + "WinoAccount_EmailConfirmationPendingDialog_ReadyToResend": "Şimdi onay e-postasını yeniden gönderebilirsiniz.", + "WinoAccount_EmailConfirmationResentDialog_Title": "Onay e-postası yeniden gönderildi.", + "WinoAccount_EmailConfirmationResentDialog_Message": "Yine {0} adresine bir onay e-postası gönderdik. Lütfen onaylayın ve tekrar giriş yapmayı deneyin.", + "WinoAccount_ForgotPasswordDialog_SuccessTitle": "Şifre sıfırlama e-postası gönderildi.", + "WinoAccount_ForgotPasswordDialog_SuccessMessage": "{0} adresine bir şifre sıfırlama e-postası gönderdik. Bu mesajı açın ve yeni bir şifre belirleyin.", + "WinoAccount_ChangePassword_Title": "Şifreyi Değiştir", + "WinoAccount_ChangePassword_Description": "Bu Wino Hesabına bir şifre sıfırlama e-postası gönderin.", + "WinoAccount_ChangePassword_Action": "Şifre sıfırlama e-posta gönder", + "WinoAccount_ChangePassword_ConfirmationMessage": "Wino'nun {0} adresine şifre sıfırlama e-postası göndermesini istiyor musunuz?", + "WinoAccount_SignOut_SuccessMessage": "Wino Hesabı {0}'ndan çıkış yapıldı.", + "WinoAccount_SignOut_NoAccountMessage": "Çıkış yapılacak aktif bir Wino Hesabı bulunmuyor.", + "WinoAccount_Titlebar_SignedOutTitle": "Wino Hesabı", + "WinoAccount_Titlebar_SignedOutDescription": "Wino oturumunuzu yönetmek için Wino Hesabınıza giriş yapın veya bir Wino Hesabı oluşturun.", + "WinoAccount_Titlebar_SignedInStatus": "Durum: {0}", + "WelcomeWizard_Step2Title": "Hesap Ekle", + "WelcomeWizard_Step3Title": "Kurulumu Tamamla", + "ProviderSelection_Title": "E-posta sağlayıcınızı seçin", + "ProviderSelection_Subtitle": "Aşağıdaki sağlayıcıyı seçerek e-posta hesabınızı Wino Mail'e ekleyin.", + "ProviderSelection_AccountNameHeader": "Hesap Adı", + "ProviderSelection_AccountNamePlaceholder": "örn. Kişisel, İş", + "ProviderSelection_DisplayNameHeader": "Görünen Ad", + "ProviderSelection_DisplayNamePlaceholder": "örn. John Doe", + "ProviderSelection_EmailHeader": "E-posta Adresi", + "ProviderSelection_EmailPlaceholder": "örn. johndoe@example.com", + "ProviderSelection_AppPasswordHeader": "Uygulama İçin Özel Şifre", + "ProviderSelection_AppPasswordHelp": "Uygulama için özel bir şifre nasıl alırım?", + "ProviderSelection_CalendarModeHeader": "Takvim Entegrasyonu", + "ProviderSelection_CalendarMode_DisabledTitle": "Devre Dışı", + "ProviderSelection_CalendarMode_DisabledDescription": "Takvim entegrasyonu yok", + "ProviderSelection_CalendarMode_CalDavTitle": "CalDAV Senkronizasyonu", + "ProviderSelection_CalendarMode_CalDavDescription_Apple": "Takvim etkinlikleriniz cihazlarınız arasında Apple sunucularına senkronize edilir.", + "ProviderSelection_CalendarMode_CalDavDescription_Yahoo": "Takvim etkinlikleriniz cihazlarınız arasında Yahoo sunucularına senkronize edilir.", + "ProviderSelection_CalendarMode_LocalTitle": "Yerel takvim", + "ProviderSelection_CalendarMode_LocalDescription": "Etkinlikleriniz yalnızca bilgisayarınızda saklanır. Sunucu bağlantısı yok.", + "ProviderSelection_ClearColor": "Rengi temizle", + "ProviderSelection_ContinueButton": "Devam Et", + "ProviderSelection_SpecialImap_Subtitle": "Bağlanmak için hesabınızın kimlik bilgilerini girin.", + "AccountSetup_Title": "Hesabınız Kuruluyor", + "AccountSetup_Step_Authenticating": "{0} ile kimlik doğrulama yapılıyor", + "AccountSetup_Step_TestingMailAuth": "E-posta kimlik doğrulaması test ediliyor.", + "AccountSetup_Step_SyncingFolders": "Klasör meta verilerini senkronize ediliyor.", + "AccountSetup_Step_FetchingProfile": "Profil bilgileri alınıyor.", + "AccountSetup_Step_DiscoveringCalDav": "CalDAV ayarları keşfediliyor.", + "AccountSetup_Step_TestingCalendarAuth": "Takvim kimlik doğrulaması test ediliyor.", + "AccountSetup_Step_SavingAccount": "Hesap bilgileri kaydediliyor.", + "AccountSetup_Step_FetchingCalendarMetadata": "Takvim meta verileri alınıyor.", + "AccountSetup_Step_SyncingAliases": "Takma adlar senkronize ediliyor.", + "AccountSetup_Step_Finalizing": "Kurulumu tamamlanıyor.", + "AccountSetup_FailureMessage": "Kurulum başarısız oldu. Ayarlarınızı düzeltmek için geri gidin veya daha sonra tekrar deneyin.", + "AccountSetup_SuccessMessage": "Hesabınız başarıyla kuruldu!", + "AccountSetup_GoBackButton": "Geri Dön", + "AccountSetup_TryAgainButton": "Tekrar Dene", + "ImapCalDavSettings_AutoDiscoveryFailed": "Otomatik keşif başarısız oldu. Gelişmiş sekmede ayarları manuel olarak girin." } - - diff --git a/Wino.Core.Domain/Translations/uk_UA/resources.json b/Wino.Core.Domain/Translations/uk_UA/resources.json index 6bc82f76..2a81aafd 100644 --- a/Wino.Core.Domain/Translations/uk_UA/resources.json +++ b/Wino.Core.Domain/Translations/uk_UA/resources.json @@ -8,6 +8,7 @@ "AccountCacheReset_Message": "Для цього облікового запису необхідно повністю пересинхронізувати ваші повідомлення. Будь ласка, дочекайтеся, поки Wino синхронізує ваші повідомлення...", "AccountContactNameYou": "Ви", "AccountCreationDialog_Completed": "все готово", + "AccountCreationDialog_FetchingCalendarMetadata": "Отримання відомостей календаря.", "AccountCreationDialog_FetchingEvents": "Події календаря завантажуються.", "AccountCreationDialog_FetchingProfileInformation": "Деталі профілю завантажуються.", "AccountCreationDialog_GoogleAuthHelpClipboardText_Row0": "Якщо Ваш браузер не запустився автоматично для закінчення автентифікації:", @@ -17,6 +18,7 @@ "AccountCreationDialog_Initializing": "ініціалізація", "AccountCreationDialog_PreparingFolders": "Ми зараз отримуємо інформацію про теки.", "AccountCreationDialog_SigninIn": "Дані облікового запису зберігаються.", + "Purchased": "Куплено", "AccountEditDialog_Message": "Назва облікового запису", "AccountEditDialog_Title": "Редагувати обліковий запис", "AccountPickerDialog_Title": "Оберіть обліковий запис", @@ -26,6 +28,10 @@ "AccountDetailsPage_Description": "Змініть ім'я облікового запису у Wino і встановіть бажане ім'я відправника.", "AccountDetailsPage_ColorPicker_Title": "Колір облікового запису", "AccountDetailsPage_ColorPicker_Description": "Встановіть новий колір облікового запису для його символу в списку.", + "AccountDetailsPage_TabGeneral": "Загальне", + "AccountDetailsPage_TabMail": "Пошта", + "AccountDetailsPage_TabCalendar": "Календар", + "AccountDetailsPage_CalendarListDescription": "Виберіть календар, щоб налаштувати його параметри.", "AddHyperlink": "Додати", "AppCloseBackgroundSynchronizationWarningTitle": "Фонова синхронізація", "AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Програму не налаштовано на автозапуск під час запуску Windows.", @@ -47,8 +53,10 @@ "BasicIMAPSetupDialog_Title": "Обліковий запис IMAP", "Busy": "Зайнятий", "Buttons_AddAccount": "Додати обліковий запис", + "Buttons_FixAccount": "Виправити обліковий запис", "Buttons_AddNewAlias": "Додати новий псевдонім", "Buttons_Allow": "Дозволити", + "Buttons_Apply": "Застосувати", "Buttons_ApplyTheme": "Застосувати тему", "Buttons_Browse": "Огляд", "Buttons_Cancel": "Скасувати", @@ -62,6 +70,7 @@ "Buttons_Edit": "Редагувати", "Buttons_EnableImageRendering": "Увімкнути", "Buttons_Multiselect": "Обрати кілька", + "Buttons_Manage": "Керувати", "Buttons_No": "Ні", "Buttons_Open": "Відкрити", "Buttons_Purchase": "Купити", @@ -70,15 +79,134 @@ "Buttons_Save": "Зберегти", "Buttons_SaveConfiguration": "Зберегти конфігурацію", "Buttons_Send": "Надіслати", + "Buttons_SendToServer": "Надіслати на сервер", "Buttons_Share": "Поділитись", "Buttons_SignIn": "Увійти", "Buttons_Sync": "Синхронізувати", "Buttons_SyncAliases": "Синхронізувати псевдоніми", "Buttons_TryAgain": "Спробувати ще раз", "Buttons_Yes": "Так", + "Sync_SynchronizingFolder": "Синхронізується {0} {1}%", + "Sync_DownloadedMessages": "Завантажено {0} повідомлень з {1}", + "SyncAction_Archiving": "Архівування {0} лист(ів)", + "SyncAction_ClearingFlag": "Зняття позначки з {0} лист(ів)", + "SyncAction_CreatingDraft": "Створення чернетки", + "SyncAction_CreatingEvent": "Створення події", + "SyncAction_Deleting": "Видалення {0} лист(ів)", + "SyncAction_EmptyingFolder": "Очищення папки", + "SyncAction_MarkingAsRead": "Позначення {0} лист(ів) як прочитаних", + "SyncAction_MarkingAsUnread": "Позначення {0} лист(ів) як непрочитаних", + "SyncAction_MarkingFolderAsRead": "Позначення папки як прочитаної", + "SyncAction_Moving": "Переміщення {0} лист(ів)", + "SyncAction_MovingToFocused": "Переміщення {0} лист(ів) до Focused", + "SyncAction_RenamingFolder": "Перейменування папки", + "SyncAction_SendingMail": "Надсилання листа", + "SyncAction_SettingFlag": "Позначення {0} лист(ів) прапорцем", + "SyncAction_SynchronizingAccount": "Синхронізується {0}", + "SyncAction_SynchronizingAccounts": "Синхронізується {0} обліковий запис(и)", + "SyncAction_SynchronizingCalendarData": "Синхронізуються дані календаря", + "SyncAction_SynchronizingCalendarEvents": "Синхронізуються події календаря", + "SyncAction_SynchronizingCalendarMetadata": "Синхронізуються метадані календаря", + "SyncAction_Unarchiving": "Розархівування {0} лист(ів)", "CalendarAllDayEventSummary": "події на увесь день", "CalendarDisplayOptions_Color": "Колір", "CalendarDisplayOptions_Expand": "Розгорнути", + "CalendarEventResponse_Accept": "Прийняти", + "CalendarEventResponse_AcceptedResponse": "Прийнято", + "CalendarEventResponse_Decline": "Відхилити", + "CalendarEventResponse_DeclinedResponse": "Відхилено", + "CalendarEventResponse_NotResponded": "Без відповіді", + "CalendarEventResponse_Tentative": "Попередньо", + "CalendarEventResponse_TentativeResponse": "Попередньо", + "CalendarEventRsvpPanel_Accept": "Прийняти", + "CalendarEventRsvpPanel_AddMessage": "Додати повідомлення до вашої відповіді... (необов'язково)", + "CalendarEventRsvpPanel_Decline": "Відхилити", + "CalendarEventRsvpPanel_Message": "Повідомлення", + "CalendarEventRsvpPanel_SendReplyMessage": "Надіслати відповідь", + "CalendarEventRsvpPanel_Tentative": "Попередньо", + "CalendarEventRsvpPanel_Title": "Опції відповіді", + "CalendarAttendeeStatus_Accepted": "Прийнято", + "CalendarAttendeeStatus_Declined": "Відхилено", + "CalendarAttendeeStatus_NeedsAction": "Потрібна дія", + "CalendarAttendeeStatus_Tentative": "Попередньо", + "CalendarEventDetails_Attachments": "Вкладення", + "CalendarEventCompose_AddAttachment": "Додати вкладення", + "CalendarEventCompose_AllDay": "Увесь день", + "CalendarEventCompose_AttachmentsNotSupportedForCalDav": "Вкладення не підтримуються для календарів CalDAV.", + "CalendarEventCompose_EndDate": "Дата завершення", + "CalendarEventCompose_EndTime": "Час завершення", + "CalendarEventCompose_Every": "кожен", + "CalendarEventCompose_ForWeekdays": "для", + "CalendarEventCompose_FrequencyDay": "день", + "CalendarEventCompose_FrequencyDayPlural": "днів", + "CalendarEventCompose_FrequencyMonth": "місяць", + "CalendarEventCompose_FrequencyMonthPlural": "місяців", + "CalendarEventCompose_FrequencyWeek": "тиждень", + "CalendarEventCompose_FrequencyWeekPlural": "тижнів", + "CalendarEventCompose_FrequencyYear": "рік", + "CalendarEventCompose_FrequencyYearPlural": "років", + "CalendarEventCompose_Location": "Місцезнаходження", + "CalendarEventCompose_LocationPlaceholder": "Додати місцезнаходження", + "CalendarEventCompose_NewEventButton": "Нова подія", + "CalendarEventCompose_DefaultCalendarHint": "Ви можете вибрати календар за замовчуванням для нових подій у налаштуваннях календаря.", + "CalendarEventCompose_DefaultCalendarSettingsLink": "Відкрити налаштування календаря", + "CalendarEventCompose_NoCalendarsMessage": "Наразі немає доступних календарів для створення події.", + "CalendarEventCompose_NoCalendarsTitle": "Немає доступних календарів", + "CalendarEventCompose_NoEndDate": "Немає дати завершення", + "CalendarEventCompose_Notes": "Примітки", + "CalendarEventCompose_PickCalendarTitle": "Виберіть календар", + "CalendarEventCompose_Recurring": "Повторюваний", + "CalendarEventCompose_RecurringSummary": "Відбувається кожні {0} {1}{2} {3} дійсні {4}{5}", + "CalendarEventCompose_RecurringSummarySmart": "Відбувається {0}{1} {2} дійсні {3}{4}", + "CalendarEventCompose_RepeatEvery": "Повторювати кожні", + "CalendarEventCompose_SelectCalendar": "Вибрати календар", + "CalendarEventCompose_SingleOccurrenceSummary": "Відбувається {0} {1}", + "CalendarEventCompose_StartDate": "Дата початку", + "CalendarEventCompose_StartTime": "Час початку", + "CalendarEventCompose_TimeRangeSummary": "від {0} до {1}", + "CalendarEventCompose_Title": "Назва події", + "CalendarEventCompose_TitlePlaceholder": "Додати назву", + "CalendarEventCompose_Until": "до", + "CalendarEventCompose_UntilSummary": " до {0}", + "CalendarEventCompose_ValidationInvalidAllDayRange": "Дата завершення події на увесь день має бути після дати початку.", + "CalendarEventCompose_ValidationInvalidAttendee": "Одна або кілька учасників мають некоректну адресу електронної пошти.", + "CalendarEventCompose_ValidationInvalidRecurrenceEnd": "Дата закінчення повторюваності повинна бути рівною або більшою за дату початку події.", + "CalendarEventCompose_ValidationInvalidTimeRange": "Кінцевий час повинен бути пізніше за час початку.", + "CalendarEventCompose_ValidationMissingAttachment": "Одне або кілька вкладень більше не доступні: {0}", + "CalendarEventCompose_ValidationMissingCalendar": "Виберіть календар перед створенням події.", + "CalendarEventCompose_ValidationMissingTitle": "Введіть назву події перед створенням події.", + "CalendarEventCompose_ValidationTitle": "Перевірка події не вдалася.", + "CalendarEventCompose_WeekdaySummary": " на {0}", + "CalendarEventCompose_Weekday_Friday": "Пт", + "CalendarEventCompose_Weekday_Monday": "Пн", + "CalendarEventCompose_Weekday_Saturday": "Сб", + "CalendarEventCompose_Weekday_Sunday": "Нд", + "CalendarEventCompose_Weekday_Thursday": "Чт", + "CalendarEventCompose_Weekday_Tuesday": "Вт", + "CalendarEventCompose_Weekday_Wednesday": "Ср", + "CalendarEventDetails_Details": "Деталі", + "CalendarEventDetails_EditSeries": "Редагувати серію", + "CalendarEventDetails_Editing": "Редагування", + "CalendarEventDetails_InviteSomeone": "Запросити когось", + "CalendarEventDetails_JoinOnline": "Приєднатися онлайн", + "CalendarEventDetails_Organizer": "Організатор", + "CalendarEventDetails_People": "Учасники", + "CalendarEventDetails_ReadOnlyEvent": "Подія лише для читання", + "CalendarEventDetails_Reminder": "Нагадування", + "CalendarReminder_StartedHoursAgo": "Розпочато {0} годин тому", + "CalendarReminder_StartedMinutesAgo": "Розпочато {0} хвилин тому", + "CalendarReminder_StartedNow": "Розпочато лише зараз", + "CalendarReminder_StartingNow": "Починається зараз", + "CalendarReminder_StartsInHours": "Починається через {0} годин", + "CalendarReminder_StartsInMinutes": "Починається через {0} хвилин", + "CalendarReminder_SnoozeAction": "Відкласти", + "CalendarReminder_SnoozeMinutesOption": "{0} хвилин", + "CalendarEventDetails_ShowAs": "Показати як", + "CalendarShowAs_Free": "Вільно", + "CalendarShowAs_Tentative": "Попередньо", + "CalendarShowAs_Busy": "Зайнятий", + "CalendarShowAs_OutOfOffice": "За межами офісу", + "CalendarShowAs_WorkingElsewhere": "Працює в іншому місці", "CalendarItem_DetailsPopup_JoinOnline": "Приєднатись онлайн", "CalendarItem_DetailsPopup_ViewEventButton": "Переглянути подію", "CalendarItem_DetailsPopup_ViewSeriesButton": "Переглянути послідовність", @@ -88,6 +216,9 @@ "ClipboardTextCopied_Message": "{0} скопійовано до буфера обміну.", "ClipboardTextCopied_Title": "Скопійовано", "ClipboardTextCopyFailed_Message": "Не вдалося скопіювати {0} в буфер обміну.", + "ContactInfoBar_ErrorTitle": "Не вдалося завантажити контактну інформацію", + "ContactInfoBar_SuccessTitle": "Контактна інформація завантажена", + "ContactInfoBar_WarningTitle": "Контактна інформація може бути неповною", "ComingSoon": "Незабаром...", "ComposerAttachmentsDragDropAttach_Message": "Прикріпити", "ComposerAttachmentsDropZone_Message": "Перетягніть файли сюди", @@ -129,6 +260,10 @@ "DialogMessage_CreateLinkedAccountTitle": "Назва зв'язки облікових записів", "DialogMessage_DeleteAccountConfirmationMessage": "Видалити {0}?", "DialogMessage_DeleteAccountConfirmationTitle": "Усі дані, пов'язані з цим обліковим записом, будуть видалені з диска назавжди.", + "DialogMessage_DeleteEmailTemplateConfirmationMessage": "Видалити шаблон \"{0}\"?", + "DialogMessage_DeleteEmailTemplateConfirmationTitle": "Видалити шаблон електронної пошти", + "DialogMessage_DeleteRecurringSeriesMessage": "Це видалить усі події з серії. Продовжити?", + "DialogMessage_DeleteRecurringSeriesTitle": "Видалити повторювану серію", "DialogMessage_DiscardDraftConfirmationMessage": "Цю чернетку буде відкинуто. Продовжити?", "DialogMessage_DiscardDraftConfirmationTitle": "Відкинути чернетку", "DialogMessage_EmptySubjectConfirmation": "Теми не вказано", @@ -172,11 +307,18 @@ "ElementTheme_Light": "Світлий режим", "Emoji": "Емодзі", "Error_FailedToSetupSystemFolders_Title": "Не вдалося налаштувати системні теки", + "Exception_AccountNeedsAttention_Title": "Обліковий запис потребує уваги", + "Exception_AccountNeedsAttention_Message": "'{0}' потребує вашої уваги для продовження роботи.", + "Exception_WebView2RuntimeMissing_Message": "Wino Mail не вдалося знайти середовище виконання WebView2 від Microsoft Edge. Встановіть або відновіть середовище виконання, щоб правильно відображати вміст повідомлення.", + "Exception_WebView2RuntimeMissing_Title": "Потрібне середовище виконання WebView2", "Exception_AuthenticationCanceled": "Автентифікацію скасовано", "Exception_CustomThemeExists": "Ця тема вже існує.", "Exception_CustomThemeMissingName": "Необхідно вказати ім'я.", "Exception_CustomThemeMissingWallpaper": "Необхідно надати власне фонове зображення.", "Exception_FailedToSynchronizeAliases": "Не вдалося синхронізувати псевдоніми", + "Exception_FailedToSynchronizeCalendarData": "Не вдалося синхронізувати дані календаря", + "Exception_FailedToSynchronizeCalendarEvents": "Не вдалося синхронізувати події календаря", + "Exception_FailedToSynchronizeCalendarMetadata": "Не вдалося синхронізувати деталі календаря", "Exception_FailedToSynchronizeFolders": "Не вдалося синхронізувати теки", "Exception_FailedToSynchronizeProfileInformation": "Не вдалося синхронізувати дані профілю", "Exception_GoogleAuthCallbackNull": "Callback uri дорівнює null при активації.", @@ -229,6 +371,32 @@ "HoverActionOption_MoveJunk": "Перемістити до Небажаних", "HoverActionOption_ToggleFlag": "Відмітити / Прибрати помітку", "HoverActionOption_ToggleRead": "Прочитане / Непрочитане", + "KeyboardShortcuts_FailedToReset": "Не вдалося скинути ярлики клавіатури.", + "KeyboardShortcuts_FailedToUpdate": "Не вдалося оновити ярлики клавіатури", + "KeyboardShortcuts_MailoperationAction": "Дія", + "KeyboardShortcuts_Action": "Дія", + "KeyboardShortcuts_FailedToLoad": "Не вдалося завантажити ярлики клавіатури.", + "KeyboardShortcuts_EnterKeyForShortcut": "Будь ласка, введіть клавішу для ярлика.", + "KeyboardShortcuts_SelectOperationForShortcut": "Будь ласка, оберіть дію для ярлика.", + "KeyboardShortcuts_EnterKey": "Будь ласка, введіть клавішу для ярлика.", + "KeyboardShortcuts_SelectOperation": "Будь ласка, оберіть дію для ярлика.", + "KeyboardShortcuts_ShortcutInUse": "Цей ярлик уже використовується іншим ярликом.", + "KeyboardShortcuts_FailedToSave": "Не вдалося зберегти ярлик.", + "KeyboardShortcuts_FailedToDelete": "Не вдалося видалити ярлик.", + "KeyboardShortcuts_PageDescription": "Налаштуйте ярлики клавіатури для швидких операцій з поштою. Натискайте клавіші, коли фокус знаходиться у полі вводу клавіші, щоб зафіксувати ярлики.", + "KeyboardShortcuts_Add": "Додати ярлик", + "KeyboardShortcuts_EditTitle": "Редагувати ярлик клавіатури", + "KeyboardShortcuts_ResetToDefaults": "Скинути за замовчуванням", + "KeyboardShortcuts_PressKeysHere": "Натискайте клавіші тут...", + "KeyboardShortcuts_KeyCombination": "Комбінація клавіш", + "KeyboardShortcuts_FocusArea": "Перемістіть фокус у поле вище та натисніть потрібну комбінацію клавіш", + "KeyboardShortcuts_Modifiers": "Модифікатори клавіш", + "KeyboardShortcuts_Mode": "Режим застосунку", + "KeyboardShortcuts_ModeMail": "Пошта", + "KeyboardShortcuts_ModeCalendar": "Календар", + "KeyboardShortcuts_ActionToggleReadUnread": "Переключити читане/не читане", + "KeyboardShortcuts_ActionToggleFlag": "Переключити прапорець", + "KeyboardShortcuts_ActionToggleArchive": "Переключити архів/розархівувати", "ImageRenderingDisabled": "Показ зображень вимкнено для цього повідомлення.", "ImapAdvancedSetupDialog_AuthenticationMethod": "Метод автентифікації", "ImapAdvancedSetupDialog_ConnectionSecurity": "Безпека підключення", @@ -295,12 +463,58 @@ "IMAPSetupDialog_Username": "Ім'я користувача", "IMAPSetupDialog_UsernamePlaceholder": "marjanatkachenko, marjanatkachenko@fabrikam.com, domain/marjanatkachenko", "IMAPSetupDialog_UseSameConfig": "Використовувати ті самі ім'я користувача та пароль для надсилання пошти", + "ImapCalDavSettingsPage_TitleCreate": "Налаштування IMAP та календаря", + "ImapCalDavSettingsPage_TitleEdit": "Редагувати налаштування IMAP та календаря", + "ImapCalDavSettingsPage_Subtitle": "Налаштувати IMAP/SMTP та необов'язкову синхронізацію календаря для цього облікового запису.", + "ImapCalDavSettingsPage_BasicSectionTitle": "Базова конфігурація", + "ImapCalDavSettingsPage_BasicSectionDescription": "Введіть ваші дані для входу та облікові дані. Wino може спробувати автоматично визначити параметри сервера.", + "ImapCalDavSettingsPage_BasicTab": "Базове", + "ImapCalDavSettingsPage_EnableCalendarSupport": "Увімкнути підтримку календаря", + "ImapCalDavSettingsPage_AutoDiscoverButton": "Автоматичне визначення налаштувань пошти", + "ImapCalDavSettingsPage_AutoDiscoverySuccessMessage": "Налаштування пошти знайдено та застосовано.", + "ImapCalDavSettingsPage_AdvancedSectionTitle": "Розширена конфігурація", + "ImapCalDavSettingsPage_AdvancedSectionDescription": "Введіть параметри сервера вручну, якщо автоматичне визначення недоступне або неправильне.", + "ImapCalDavSettingsPage_AdvancedTab": "Розширено", + "ImapCalDavSettingsPage_CalendarSectionTitle": "Налаштування календаря", + "ImapCalDavSettingsPage_CalendarSectionDescription": "Виберіть, як дані календаря повинні працювати з цим обліковим записом IMAP.", + "ImapCalDavSettingsPage_CalendarModeHeader": "Режим календаря", + "ImapCalDavSettingsPage_ConnectionSecurityHeader": "Захист з'єднання", + "ImapCalDavSettingsPage_AuthenticationMethodHeader": "Метод автентифікації", + "ImapCalDavSettingsPage_CalendarModeDisabled": "Вимкнено", + "ImapCalDavSettingsPage_CalendarModeCalDav": "Синхронізація CalDAV", + "ImapCalDavSettingsPage_CalendarModeLocalOnly": "Локальний календар лише", + "ImapCalDavSettingsPage_CalendarModeDisabledDescription": "Календар вимкнено для цього облікового запису.", + "ImapCalDavSettingsPage_CalendarModeCalDavDescription": "Елементи календаря синхронізуються з вашим сервером CalDAV.", + "ImapCalDavSettingsPage_CalendarModeLocalOnlyDescription": "Елементи календаря зберігаються тільки на цьому комп'ютері і не синхронізуються з мережею.", + "ImapCalDavSettingsPage_LocalCalendarLearnMore": "Як працює локальний календар", + "ImapCalDavSettingsPage_LocalCalendarDialogTitle": "Локальний календар лише", + "ImapCalDavSettingsPage_LocalCalendarDialogMessage": "Локальний календар зберігає всі події лише на вашому комп'ютері. Нічого не синхронізується з iCloud, Yahoo або будь-яким іншим постачальником.", + "ImapCalDavSettingsPage_CalDavServiceUrl": "URL сервісу CalDAV", + "ImapCalDavSettingsPage_CalDavUsername": "Ім'я користувача CalDAV", + "ImapCalDavSettingsPage_CalDavPassword": "Пароль CalDAV", + "ImapCalDavSettingsPage_CalDavNotRequiredMessage": "Тест CalDAV потрібен лише тоді, коли режим календаря встановлено на синхронізацію CalDAV.", + "ImapCalDavSettingsPage_CalDavUrlRequired": "Потрібен URL сервісу CalDAV.", + "ImapCalDavSettingsPage_CalDavUrlInvalid": "URL сервісу CalDAV має бути абсолютним URL.", + "ImapCalDavSettingsPage_CalDavUsernameRequired": "Потрібне ім'я користувача CalDAV.", + "ImapCalDavSettingsPage_CalDavPasswordRequired": "Потрібен пароль CalDAV.", + "ImapCalDavSettingsPage_TestImapButton": "Перевірити з'єднання IMAP.", + "ImapCalDavSettingsPage_TestCalDavButton": "Перевірити з'єднання CalDAV.", + "ImapCalDavSettingsPage_ImapTestSuccessMessage": "Тест з'єднання IMAP успішно виконано.", + "ImapCalDavSettingsPage_CalDavTestSuccessMessage": "Тест з'єднання CalDAV успішно виконано.", + "ImapCalDavSettingsPage_SaveSuccessMessage": "Налаштування облікового запису перевірено та збережено.", + "ImapCalDavSettingsPage_ICloudHint": "Використовуйте пароль для програми, згенерований у налаштуваннях вашого облікового запису Apple.", + "ImapCalDavSettingsPage_YahooHint": "Використовуйте пароль для програми з налаштувань безпеки вашого облікового запису Yahoo.", "Info_AccountCreatedMessage": "{0} створено", "Info_AccountCreatedTitle": "Створення облікового запису", "Info_AccountCreationFailedTitle": "Не вдалося створити обліковий запис", "Info_AccountDeletedMessage": "{0} успішно видалено.", "Info_AccountDeletedTitle": "Обліковий запис видалено", "Info_AccountIssueFixFailedTitle": "Не вдалося", + "Info_AccountIssueFixImapMessage": "Відкрийте сторінку з налаштуваннями IMAP та календаря, щоб знову ввести дані сервера.", + "Info_AccountAttentionRequiredMessage": "Цьому обліковому запису потрібна ваша увага.", + "Info_AccountAttentionRequiredClickableMessage": "Клацніть, щоб виправити цей обліковий запис і повторно синхронізувати його.", + "Info_AccountAttentionRequiredAction": "Виправити", + "Info_AccountAttentionRequiredActionHint": "Натисніть Виправити, щоб вирішити проблему з цим обліковим записом.", "Info_AccountIssueFixSuccessMessage": "Виправлено всі проблеми з обліковим записом.", "Info_AccountIssueFixSuccessTitle": "Виконано", "Info_AttachmentOpenFailedMessage": "Не вдається відкрити вкладення.", @@ -370,6 +584,7 @@ "InfoBarMessage_SynchronizationDisabledFolder": "Ця тека відключена для синхронізації.", "InfoBarTitle_SynchronizationDisabledFolder": "Відключена тека", "Justify": "Вирівняти", + "MenuUpdateAvailable": "Доступне оновлення", "Left": "Ліворуч", "Link": "Посилання", "LinkedAccountsCreatePolicyMessage": "потрібно мати хоча б 2 облікові записи, щоб створити зв'язку\nзв'язку буде видалено при збереженні", @@ -403,6 +618,7 @@ "MailOperation_Unarchive": "Розархівувати", "MailOperation_ViewMessageSource": "Переглянути джерело повідомлення", "MailOperation_Zoom": "Масштабувати", + "MailsDragging": "Перетягується {0} елемент(ів)", "MailsSelected": "Елементів обрано: {0}", "MarkFlagUnflag": "Позначити відміченим/невідміченим", "MarkReadUnread": "Позначити прочитаним/непрочитаним", @@ -434,6 +650,8 @@ "Notifications_MultipleNotificationsTitle": "Свіжа пошта", "Notifications_WinoUpdatedMessage": "Погляньте на нову версію {0}", "Notifications_WinoUpdatedTitle": "Wino Mail було оновлено.", + "Notifications_StoreUpdateAvailableTitle": "Доступне оновлення", + "Notifications_StoreUpdateAvailableMessage": "Нова версія Wino Mail готова до встановлення через Microsoft Store.", "OnlineSearchFailed_Message": "Не вдалося виконати пошук\n{0}\n\nПоказ офлайн-повідомлень.", "OnlineSearchTry_Line1": "Не знайшли, що шукали?", "OnlineSearchTry_Line2": "Спробуйте онлайн-пошук.", @@ -446,7 +664,6 @@ "PaneLengthOption_Small": "Мала", "Photos": "Фото", "PreparingFoldersMessage": "Підготовка тек", - "ProtocolLogAvailable_Message": "Лоґи протоколів доступні для діагностики.", "ProviderDetail_Gmail_Description": "Обліковий запис Google", "ProviderDetail_iCloud_Description": "Обліковий запис Apple iCloud", "ProviderDetail_iCloud_Title": "iCloud", @@ -465,9 +682,14 @@ "SearchBarPlaceholder": "Пошук", "SearchingIn": "Пошук в", "SearchPivotName": "Результати", + "Settings_KeyboardShortcuts_Title": "Гарячі клавіші", + "Settings_KeyboardShortcuts_Description": "Керуйте гарячими клавішами для швидких дій з листами.", "SettingConfigureSpecialFolders_Button": "Налаштувати", "SettingsEditAccountDetails_IMAPConfiguration_Title": "Налаштування IMAP/SMTP", "SettingsEditAccountDetails_IMAPConfiguration_Description": "Змінити налаштування сервера вхідної/вихідної пошти.", + "SettingsEditAccountDetails_ImapCalDavSettings_Title": "Налаштування IMAP та календаря", + "SettingsEditAccountDetails_ImapCalDavSettings_Description": "Відкрийте спеціальну сторінку налаштувань IMAP, SMTP та CalDAV для цього облікового запису.", + "SettingsEditAccountDetails_ImapCalDavSettings_Action": "Відкрити налаштування", "SettingsAbout_Description": "Дізнатися більше про Wino.", "SettingsAbout_Title": "Про програму", "SettingsAboutGithub_Description": "Перейти до трекера задач репозиторія GitHub.", @@ -490,6 +712,10 @@ "SettingsAppPreferences_SearchMode_Local": "Локальний", "SettingsAppPreferences_SearchMode_Online": "Онлайн", "SettingsAppPreferences_SearchMode_Title": "Типовий режим пошуку", + "SettingsAppPreferences_ApplicationMode_Title": "Режим застосунку за замовчуванням", + "SettingsAppPreferences_ApplicationMode_Description": "Виберіть режим запуску Wino за замовчуванням, якщо явний тип активації не задано.", + "SettingsAppPreferences_ApplicationMode_Mail": "Пошта", + "SettingsAppPreferences_ApplicationMode_Calendar": "Календар", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description": "Wino Mail продовжуватиме працювати у фоновому режимі. Ви отримуватимете сповіщення про нові листи.", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title": "Працювати у фоні", "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description": "Wino Mail продовжуватиме працювати у системному лотку. Ви зможете відкрити його, натиснувши на іконку. Ви отримуватимете сповіщення про нові листи.", @@ -506,12 +732,30 @@ "SettingsAppPreferences_StartupBehavior_FatalError": "Критична помилка виникла під час зміни режиму автозапуску для Wino Mail.", "SettingsAppPreferences_StartupBehavior_Title": "Запускати в згорнутому стані при запуску Windows", "SettingsAppPreferences_Title": "Параметри програми", + "SettingsAppPreferences_HideWinoAccountButton_Title": "Приховати кнопку облікового запису Wino на панелі заголовка", + "SettingsAppPreferences_HideWinoAccountButton_Description": "Приховати кнопку профіля на панелі заголовка, яка відкриває випадаюче меню облікового запису Wino.", + "SettingsAppPreferences_StoreUpdateNotifications_Title": "Сповіщення про оновлення магазину", + "SettingsAppPreferences_StoreUpdateNotifications_Description": "Показувати сповіщення та дії внизу, коли доступне оновлення Microsoft Store.", + "SettingsAppPreferences_AiActions_Title": "Дії ШІ", + "SettingsAppPreferences_AiActions_Description": "Виберіть мови ШІ за замовчуванням та де зберігати підсумки.", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Title": "Мова перекладу за замовчуванням", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Description": "Виберіть мову перекладу за замовчуванням, яку використають дії перекладу ШІ.", + "SettingsAppPreferences_AiSummarizeLanguage_Title": "Мова підсумовування", + "SettingsAppPreferences_AiSummarizeLanguage_Description": "Виберіть бажану мову підсумування для майбутнього виводу резюме ШІ.", + "SettingsAppPreferences_AiSummarySavePath_Title": "Шлях збереження підсумків за замовчуванням", + "SettingsAppPreferences_AiSummarySavePath_Description": "Виберіть папку, яку Wino використовуватиме за замовчуванням для збереження підсумків ШІ.", + "SettingsAppPreferences_AiSummarySavePath_Placeholder": "Використовуйте системне місце збереження за замовчуванням.", + "SettingsAppPreferences_AiSummarySavePath_InvalidHint": "Ця папка не існує. За замовчуванням буде використано місце збереження для підсумків.", "SettingsAutoSelectNextItem_Description": "Обирати наступний елемент після видалення або переміщення листа.", "SettingsAutoSelectNextItem_Title": "Автовибір наступного елемента", "SettingsAvailableThemes_Description": "Оберіть тему з колекції Wino на свій смак або застосуйте власні теми.", "SettingsAvailableThemes_Title": "Доступні теми", "SettingsCalendarSettings_Description": "Змінити перший день тижня, висоту клітинок годин і багато іншого...", "SettingsCalendarSettings_Title": "Параметри календаря", + "CalendarSettings_DefaultSnoozeDuration_Header": "Тривалість відкладення за замовчуванням", + "CalendarSettings_DefaultSnoozeDuration_Description": "Встановіть тривалість відкладення за замовчуванням для сповіщень про нагадування в календарі.", + "CalendarSettings_TimedDayHeaderFormat_Header": "Формат заголовка дня у вигляді з часовою прив'язкою", + "CalendarSettings_TimedDayHeaderFormat_Description": "Виберіть, як верхні позначки дня відображаються в режимах день, тиждень та робочий тиждень. Використовуйте токени формату дати, такі як ddd, dd, MMM або dddd.", "SettingsComposer_Title": "Редактор", "SettingsComposerFont_Title": "Типовий шрифт редактора", "SettingsComposerFontFamily_Description": "Змінити типову сім'ю шрифтів і розмір шрифту для написання листів.", @@ -531,6 +775,9 @@ "SettingsDiscord_Title": "Канал у Discord", "SettingsEditLinkedInbox_Description": "Додати / видалити облікові записи, перейменувати або розірвати зв'язку між обліковими записами.", "SettingsEditLinkedInbox_Title": "Редагувати зв'язану скриньку", + "SettingsWindowBackdrop_Title": "Фон вікна", + "SettingsWindowBackdrop_Description": "Виберіть ефект фону для вікон Wino.", + "SettingsWindowBackdrop_Disabled": "Вибір фону вікна недоступний, якщо вибрана тема програми відмінна від за замовчуванням.", "SettingsElementTheme_Description": "Оберіть тему Windows для Wino", "SettingsElementTheme_Title": "Колірний режим", "SettingsElementThemeSelectionDisabled": "Вибір колірного режиму вимкнено, коли обрана нетипова тема програми.", @@ -581,6 +828,8 @@ "SettingsManageAliases_Title": "Псевдоніми", "SettingsEditAccountDetails_Title": "Редагувати деталі облікового запису", "SettingsEditAccountDetails_Description": "Змініть ім'я облікового запису, ім'я відправника та призначте новий колір, якщо хочете.", + "EditAccountDetailsPage_SaveSuccess_Title": "Зміни збережено.", + "EditAccountDetailsPage_SaveSuccess_Message": "Дані облікового запису успішно оновлено.", "SettingsManageLink_Description": "Перемістіть елементи, щоб додати нову зв'язку або видалити наявну зв'язку.", "SettingsManageLink_Title": "Керування зв'язкою", "SettingsMarkAsRead_Description": "Змініть те, що має статися з обраним листом.", @@ -596,7 +845,41 @@ "SettingsNotifications_Title": "Сповіщення", "SettingsNotificationsAndTaskbar_Description": "Змінити, чи повинні показуватися сповіщення і значок на панелі завдань для цього облікового запису.", "SettingsNotificationsAndTaskbar_Title": "Сповіщення та панель завдань", + "SettingsHome_Title": "Головна", + "SettingsHome_SearchTitle": "Знайдіть параметр", + "SettingsHome_SearchDescription": "Пошук за ознакою, темою або ключовим словом для негайного переходу до потрібної сторінки налаштувань.", + "SettingsHome_SearchPlaceholder": "Пошук у налаштуваннях", + "SettingsHome_SearchExamples": "Спробуйте: тема, зберігання, мова, підпис", + "SettingsHome_QuickLinks_Title": "Швидкі посилання", + "SettingsHome_QuickLinks_Description": "Перейдіть до налаштувань, до яких люди звертаються найчастіше.", + "SettingsHome_StorageCard_Description": "Перегляньте, скільки локального MIME-змісту зберігає Wino на цьому пристрої, та очистіть його за потреби.", + "SettingsHome_StorageEmptySummary": "Ще не виявлено кешованого MIME-змісту.", + "SettingsHome_StorageLoading": "Перевіряємо використання локального MIME...", + "SettingsHome_Tips_Title": "Поради та хитрощі", + "SettingsHome_Tips_Description": "Кілька невеликих змін можуть зробити Wino більш персоналізованим.", + "SettingsHome_Tip_Theme": "Хочете темний режим або зміни акцентів? Відкрийте Персоналізацію.", + "SettingsHome_Tip_Background": "Використовуйте параметри застосунку для керування запуском та фоновою синхронізацією.", + "SettingsHome_Tip_Shortcuts": "Гарячі клавіші допомагають швидше переміщатися між листами.", + "SettingsHome_Resources_Title": "Корисні посилання", + "SettingsHome_Resources_Description": "Відкрийте ресурси проекту, інформацію підтримки та канали випуску.", "SettingsOptions_Title": "Налаштування", + "SettingsOptions_GeneralSection": "Загальні", + "SettingsOptions_MailSection": "Пошта", + "SettingsOptions_CalendarSection": "Календар", + "SettingsOptions_MoreComingSoon": "Інші параметри з'являться незабаром.", + "SettingsOptions_HeroDescription": "Налаштуйте свій досвід використання Wino Mail.", + "SettingsOptions_AccountsSummary": "{0} обліковий запис(и) налаштовано", + "SettingsSearch_ManageAccounts_Keywords": "обліковий запис;облікові записи;поштова скринька;поштові скриньки;псевдонім;псевдоніми;профіль;адреса;адреси", + "SettingsSearch_AppPreferences_Keywords": "запуск;фоном;запуск;синхронізація;сповіщення;сповіщення;пошук;панель трея;за замовчуванням", + "SettingsSearch_LanguageTime_Keywords": "мова;час;годинник;локаль;регіон;формат;24-годинний;24г", + "SettingsSearch_Personalization_Keywords": "тема;темний;світлий;зовнішній вигляд;акцент;колір;колір;режим;розкладка;щільність", + "SettingsSearch_About_Keywords": "про;версія;веб-сайт;конфіденційність;GitHub;пожертвування;магазин;підтримка", + "SettingsSearch_KeyboardShortcuts_Keywords": "гаряча клавіша;гарячі клавіші;гаряча клавіша;гарячі клавіші;клавіатура;клавіші", + "SettingsSearch_MessageList_Keywords": "повідомлення;повідомлення;список;потоки;потоки;аватар;попередній перегляд;відправник", + "SettingsSearch_ReadComposePane_Keywords": "читач;скласти;композитор;шрифт;шрифти;зовнішній вміст;відображення;читання", + "SettingsSearch_SignatureAndEncryption_Keywords": "підпис;підписи;шифрування;сертифікат;сертифікати;S/MIME;SMIME;безпека", + "SettingsSearch_Storage_Keywords": "збереження;кеш;кешування;MIME;диск;місце;очистка;очистити;локальні дані", + "SettingsSearch_CalendarSettings_Keywords": "календар;тиждень;години;розклад;подія;події", "SettingsPaneLengthReset_Description": "Скинути розмір списку листів до початкового, якщо з ним є проблеми.", "SettingsPaneLengthReset_Title": "Скинути розмір списку листів", "SettingsPaypal_Description": "Покажіть ще більше любові ❤️ Ми вдячні за всі пожертви.", @@ -610,6 +893,8 @@ "SettingsPrefer24HourClock_Title": "Показувати час у 24-годинному форматі", "SettingsPrivacyPolicy_Description": "Переглянути політику конфіденційності.", "SettingsPrivacyPolicy_Title": "Політика конфіденційності", + "SettingsWebsite_Description": "Відкрити веб-сайт Wino Mail.", + "SettingsWebsite_Title": "Веб-сайт", "SettingsReadComposePane_Description": "Шрифти, зовнішній вміст.", "SettingsReadComposePane_Title": "Переглядач і Редактор", "SettingsReader_Title": "Переглядач", @@ -625,6 +910,19 @@ "SettingsShowPreviewText_Title": "Попередній перегляд тексту", "SettingsShowSenderPictures_Description": "Приховувати/показувати аватарки відправників.", "SettingsShowSenderPictures_Title": "Аватарки відправників", + "SettingsEmailTemplates_Title": "Шаблони електронної пошти", + "SettingsEmailTemplates_Description": "Керуйте шаблонами електронної пошти", + "SettingsEmailTemplates_CreatePageTitle": "Новий шаблон", + "SettingsEmailTemplates_EditPageTitle": "Редагувати шаблон", + "SettingsEmailTemplates_NewTemplateTitle": "Новий шаблон", + "SettingsEmailTemplates_NewTemplateDescription": "Створити новий шаблон електронної пошти", + "SettingsEmailTemplates_NameTitle": "Назва", + "SettingsEmailTemplates_NamePlaceholder": "Назва шаблону", + "SettingsEmailTemplates_DescriptionTitle": "Опис", + "SettingsEmailTemplates_DescriptionPlaceholder": "Необов'язковий опис", + "SettingsEmailTemplates_ContentTitle": "Зміст шаблону", + "SettingsEmailTemplates_ContentDescription": "Редагуйте HTML-зміст цього шаблона.", + "SettingsEmailTemplates_NameRequired": "Потрібна назва шаблону.", "SettingsEnableGravatarAvatars_Title": "Gravatar", "SettingsEnableGravatarAvatars_Description": "Use gravatar (if available) as sender picture", "SettingsEnableFavicons_Title": "Domain icons (Favicons)", @@ -645,6 +943,33 @@ "SettingsStartupItem_Title": "Обліковий запис для запуску", "SettingsStore_Description": "Покажіть свою любов ❤️", "SettingsStore_Title": "Оцініть у Store", + "SettingsStorage_Title": "Зберігання", + "SettingsStorage_Description": "Сканування та керування кешем MIME, що зберігається у вашій локальній папці даних.", + "SettingsStorage_ScanFolder": "Сканувати локальну папку даних", + "SettingsStorage_NoLocalMimeDataFound": "Локальні MIME-дані не знайдено.", + "SettingsStorage_NoAccountsFound": "Не знайдено облікових записів.", + "SettingsStorage_TotalUsage": "Загальне використання локальних MIME-даних: {0}", + "SettingsStorage_AccountUsageDescription": "Використано {0} у локальному кеші MIME.", + "SettingsStorage_DeleteAll_Title": "Видалити весь MIME-вміст", + "SettingsStorage_DeleteAll_Description": "Видалити повністю кеш MIME цього облікового запису.", + "SettingsStorage_DeleteAll_Button": "Видалити все", + "SettingsStorage_DeleteAll_Confirm_Title": "Видалити весь MIME-вміст", + "SettingsStorage_DeleteAll_Confirm_Message": "Видалити всі локальні MIME-дані для {0}?", + "SettingsStorage_DeleteAll_Success": "Усі MIME-дані були видалені.", + "SettingsStorage_DeleteOld_Title": "Видалити старі MIME-дані", + "SettingsStorage_DeleteOld_Description": "Видалити MIME-файли за датою створення листа в локальній базі даних.", + "SettingsStorage_DeleteOld_1Month": "> 1 місяць", + "SettingsStorage_DeleteOld_3Months": "> 3 місяці", + "SettingsStorage_DeleteOld_6Months": "> 6 місяців", + "SettingsStorage_DeleteOld_1Year": "> 1 рік", + "SettingsStorage_DeleteOld_Confirm_Title": "Видалити старий MIME-вміст", + "SettingsStorage_DeleteOld_Confirm_Message": "Видалити локальні MIME-дані старіші за {0} для {1}?", + "SettingsStorage_DeleteOld_Success": "Видалено {0} MIME-папку(-и), старіші за {1}.", + "SettingsStorage_1Month": "1 місяць", + "SettingsStorage_3Months": "3 місяці", + "SettingsStorage_6Months": "6 місяців", + "SettingsStorage_1Year": "1 рік", + "SettingsStorage_Months": "{0} місяців", "SettingsTaskbarBadge_Description": "Показувати кількість непрочитаних у значку на панелі завдань.", "SettingsTaskbarBadge_Title": "Значок на панелі завдань", "SettingsThreads_Description": "Організуйте повідомлення в ланцюжки розмов.", @@ -683,6 +1008,9 @@ "SystemFolderConfigDialogValidation_InboxSelected": "Ви не можете призначати теку Вхідні до будь-якої іншої системної теки.", "SystemFolderConfigSetupSuccess_Message": "Системні теки успішно налаштовані.", "SystemFolderConfigSetupSuccess_Title": "Налаштування системних тек", + "SystemTrayMenu_ShowWino": "Відкрити Wino Mail", + "SystemTrayMenu_ShowWinoCalendar": "Відкрити Wino Calendar", + "SystemTrayMenu_ExitWino": "Вийти", "TestingImapConnectionMessage": "Перевірка підключення до сервера...", "TitleBarServerDisconnectedButton_Description": "Wino відключено від мережі. Натисніть кнопку \"Перепідключитися\", щоб відновити підключення.", "TitleBarServerDisconnectedButton_Title": "немає з'єднання", @@ -699,8 +1027,422 @@ "WinoUpgradeMessage": "Оновити до необмеженої кількості облікових записів", "WinoUpgradeRemainingAccountsMessage": "{0} з {1} безкоштовних облікових записів використано.", "Yesterday": "Вчора", + "Smime_ImportCertificates_Success": "Сертифікати успішно імпортовано.", + "Smime_ImportCertificates_Error": "Помилка імпорту сертифікатів: {0}", + "Smime_RemoveCertificates_Confirm": "Дійсно хочете видалити сертифікати {0}?", + "Smime_RemoveCertificates_Success": "Сертифікати видалено.", + "Smime_ExportCertificates_Success": "Сертифікати експортовано.", + "Smime_ExportCertificates_Error": "Помилка експорту сертифікатів.", + "Smime_CertificateDetails": "Суб'єкт: {0}\nВидавець: {1}\nДійсний з: {2}\nДійсний до: {3}\nВідбиток: {4}", + "Smime_CertificatePassword_Title": "Потрібен пароль сертифіката", + "Smime_CertificatePassword_Placeholder": "Пароль сертифіката для {0} (необов'язковий)", + "Smime_Confirm_Title": "Підтвердити", + "Buttons_OK": "ОК", + "Buttons_Refresh": "Оновити", + "SettingsSignatureAndEncryption_Title": "Підпис та шифрування", + "SettingsSignatureAndEncryption_Description": "Керування сертифікатами S/MIME для підписання та шифрування електронних листів.", + "SettingsSignatureAndEncryption_MyCertificatesHeader": "Мої сертифікати", + "SettingsSignatureAndEncryption_MyCertificatesDescription": "Особисті сертифікати для підписання та шифрування", + "SettingsSignatureAndEncryption_RecipientCertificatesHeader": "Сертифікати одержувачів", + "SettingsSignatureAndEncryption_RecipientCertificatesDescription": "Сертифікати одержувачів для розшифрування", + "SettingsSignatureAndEncryption_NameColumn": "Ім'я", + "SettingsSignatureAndEncryption_ExpiresColumn": "Закінчується", + "SettingsSignatureAndEncryption_ThumbprintColumn": "Відбиток", + "Buttons_Remove": "Видалити", + "Buttons_Export": "Експорт", + "Buttons_Import": "Імпорт", + "SettingsSignatureAndEncryption_SigningCertificate": "S/MIME сертифікат підпису", + "SettingsSignatureAndEncryption_EncryptionCertificate": "S/MIME сертифікат шифрування", + "SettingsSignatureAndEncryption_SigningCertificatePlaceholder": "Немає", + "SmimeSignaturesInMessage": "Підписи в цьому листі:", + "SmimeSignatureEntry": "• {0} {1} ({2}, дійсний до {3} - {4})", + "SmimeSigningCertificateInfoTitle": "Інформація про S/MIME-сертифікат підпису", + "SmimeCertificateInfoTitle": "Інформація про S/MIME-сертифікат", + "SmimeNoCertificateFileFound": "Файл сертифіката не знайдено", + "SmimeSaveCertificate": "Зберегти сертифікат...", + "SmimeCertificate": "S/MIME-сертифікат", + "SmimeCertificateSavedTo": "Сертифікат збережено до {0}", + "SmimeSignedTooltip": "Цьому повідомленню підписано за допомогою S/MIME-сертифіката. Натисніть, щоб переглянути деталі.", + "SmimeEncryptedTooltip": "Цьому повідомленню використано S/MIME-сертифікат для шифрування.", + "SmimeCertificateFileInfo": "Файл: {0}", + "Composer_LightTheme": "Світла тема", + "Composer_DarkTheme": "Темна тема", + "Composer_Outdent": "Зменшити відступ", + "Composer_Indent": "Збільшити відступ", + "Composer_BulletList": "Маркерований список", + "Composer_OrderedList": "Нумерований список", + "Composer_Stroke": "Обводка", + "Composer_Bold": "Жирний", + "Composer_Italic": "Курсив", + "Composer_Underline": "Підкреслення", + "Composer_CcBcc": "Копія (Cc) та прихована копія (Bcc)", + "Composer_EnableSmimeSignature": "Увімкнути/вимкнути підпис S/MIME", + "Composer_EnableSmimeEncryption": "Увімкнути/вимкнути шифрування S/MIME", + "Composer_LocalDraftSyncInfo": "Ця чернетка зберігається локально. Wino не вдалося надіслати її на ваш поштовий сервер. Натисніть, щоб повторити відправлення на сервер.", + "Composer_CertificateExpires": "Закінчується: ", + "Composer_SmimeSignature": "S/MIME підпис", + "Composer_SmimeEncryption": "S/MIME шифрування", + "Composer_EmailTemplatesPlaceholder": "Шаблони електронної пошти", + "Composer_AiSummarize": "Підсумуйте за допомогою ШІ", + "Composer_AiSummarizeDescription": "Виділити основні пункти, завдання та рішення з цього електронного листа.", + "Composer_AiTranslate": "Перекласти за допомогою ШІ", + "Composer_AiActions": "Дії ШІ", + "Composer_AiRewrite": "Переписати за допомогою ШІ", + "AiActions_CheckingStatus": "Перевіряю доступ до ШІ...", + "AiActions_SignedOutTitle": "Розблокувати пакет Wino AI", + "AiActions_SignedOutDescription": "Перекладайте, переписуйте та підсумовуйте електронні листи за допомогою ШІ після входу в обліковий запис Wino та активації надбудови AI Pack.", + "AiActions_NoPackTitle": "Потрібен пакет AI", + "AiActions_NoPackDescription": "Ви увійшли, але пакет AI ще не активний. Придбайте його, щоб користуватися інструментами перекладу, переписування та підсумовування від Wino за допомогою ШІ.", + "AiActions_UsageSummary": "{0} з {1} кредитів використано цього місяця.", + "Composer_AiRewritePolite": "Зробити ввічливішим", + "Composer_AiRewritePoliteDescription": "Пом'якшує формулювання, зберігаючи той же зміст.", + "Composer_AiRewriteAngry": "Зробити більш різким", + "Composer_AiRewriteAngryDescription": "Використовує більш різкий та конфронтаційний тон.", + "Composer_AiRewriteHappy": "Зробити його щасливим", + "Composer_AiRewriteHappyDescription": "Додає більш піднесений та завзятий тон.", + "Composer_AiRewriteFormal": "Зробити його більш офіційним", + "Composer_AiRewriteFormalDescription": "Зміцнює повідомлення більш професійним та структурованим.", + "Composer_AiRewriteFriendly": "Зробити більш дружнім", + "Composer_AiRewriteFriendlyDescription": "Надає повідомленню більш дружелюбного тону.", + "Composer_AiRewriteShorter": "Зробити коротшим", + "Composer_AiRewriteShorterDescription": "Стискає текст і прибирає зайві деталі.", + "Composer_AiRewriteClearer": "Зробити його яснішим", + "Composer_AiRewriteClearerDescription": "Покращує читабельність та робить повідомлення легшим для сприйняття.", + "Composer_AiRewriteCustom": "Користувацьке", + "Composer_AiRewriteCustomDescription": "Опишіть свої наміри щодо переформулювання.", + "Composer_AiRewriteCustomPlaceholder": "Опишіть, як ви хочете переформулювати повідомлення", + "Composer_AiRewriteMode": "Змінити тон", + "Composer_AiRewriteApply": "Застосувати переформулювання.", + "Composer_AiTranslateDialogTitle": "Перекласти за допомогою ШІ", + "Composer_AiTranslateDialogDescription": "Введіть цільову мову або код культури, наприклад en-US, tr-TR, de-DE або fr-FR.", + "Composer_AiTranslateApply": "Перекласти", + "Composer_AiTranslateLanguage": "Цільова мова", + "Composer_AiTranslateCustomPlaceholder": "Введіть код культури", + "Composer_AiTranslateLanguageEnglish": "Англійська (en-US)", + "Composer_AiTranslateLanguageTurkish": "Турецька (tr-TR)", + "Composer_AiTranslateLanguageGerman": "Німецька (de-DE)", + "Composer_AiTranslateLanguageFrench": "Французька (fr-FR)", + "Composer_AiTranslateLanguageSpanish": "Іспанська (es-ES)", + "Composer_AiTranslateLanguageItalian": "Італійська (it-IT)", + "Composer_AiTranslateLanguagePortugueseBrazil": "Португальська (Бразилія) (pt-BR)", + "Composer_AiTranslateLanguageDutch": "Нідерландська (nl-NL)", + "Composer_AiTranslateLanguagePolish": "Польська (pl-PL)", + "Composer_AiTranslateLanguageRussian": "Російська (ru-RU)", + "Composer_AiTranslateLanguageJapanese": "Японська (ja-JP)", + "Composer_AiTranslateLanguageKorean": "Корейська (ko-KR)", + "Composer_AiTranslateLanguageChineseSimplified": "Китайська, спрощена (zh-CN)", + "Composer_AiTranslateLanguageArabic": "Арабська (ar-SA)", + "Composer_AiTranslateLanguageHindi": "Хінді (hi-IN)", + "Composer_AiTranslateLanguageOther": "Інше...", + "Composer_AiBusyTitle": "ШІ вже працює", + "Composer_AiBusyMessage": "Будь ласка, зачекайте, доки поточна дія ШІ завершиться.", + "Composer_AiSignInRequired": "Увійдіть у свій обліковий запис Wino, щоб використати функції ШІ.", + "Composer_AiMissingHtml": "Поки що немає вмісту повідомлення для відправлення до Wino AI.", + "Composer_AiQuotaUnavailable": "Результат ШІ застосовано.", + "Composer_AiAppliedMessage": "Результат ШІ застосовано до редактора. Використайте Відмінити, щоб повернути зміни.", + "Composer_AiSummarizeSuccessTitle": "Підсумок ШІ застосовано.", + "Composer_AiTranslateSuccessTitle": "Переклад ШІ застосовано.", + "Composer_AiRewriteSuccessTitle": "Переформулювання ШІ застосовано.", + "Composer_AiErrorTitle": "Помилка дії ШІ.", + "Reader_AiAppliedMessage": "Результат ШІ тепер відображається для цього повідомлення. Відкрийте повідомлення знову, щоб переглянути оригінальний вміст.", "SettingsAppPreferences_EmailSyncInterval_Title": "Email sync interval", - "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail." + "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail.", + "ContactsPage_Title": "Контакти", + "ContactsPage_AddContact": "Додати контакт", + "ContactsPage_EditContact": "Редагувати контакт", + "ContactsPage_DeleteContact": "Видалити контакт", + "ContactsPage_SearchPlaceholder": "Пошук контактів...", + "ContactsPage_NoContacts": "Контакти не знайдено", + "ContactsPage_ContactsCount": "{0} контакт(ів)", + "ContactsPage_SelectedContactsCount": "{0} обраний(і)", + "ContactsPage_DeleteSelectedContacts": "Видалити обрані", + "ContactEditDialog_Title": "Редагувати контакт", + "ContactEditDialog_PhotoSection": "Фото", + "ContactEditDialog_ChoosePhoto": "Вибрати фото", + "ContactEditDialog_RemovePhoto": "Видалити фото", + "ContactEditDialog_NameHeader": "Ім'я", + "ContactEditDialog_NamePlaceholder": "Ім'я контакту", + "ContactEditDialog_EmailHeader": "Електронна адреса", + "ContactEditDialog_EmailPlaceholder": "contact@example.com", + "ContactEditDialog_InfoSection": "Інформація контакту", + "ContactEditDialog_RootContactInfo": "Це кореневий контакт, пов'язаний з вашими обліковими записами, і його не можна видалити.", + "ContactEditDialog_OverriddenContactInfo": "Цей контакт було вручну змінено і під час синхронізації він не оновлюватиметься.", + "ContactsPage_Subtitle": "Керуйте контактами електронної пошти та їхньою інформацією.", + "ContactStatus_Account": "Обліковий запис", + "ContactStatus_Modified": "Змінено", + "ContactAction_Edit": "Редагувати контакт", + "ContactAction_ChangePhoto": "Змінити фото", + "ContactAction_Delete": "Видалити контакт", + "ContactAction_Add": "Додати контакт", + "ContactSelection_Selected": "обраний", + "ContactSelection_SelectAll": "Вибрати все", + "ContactSelection_Clear": "Очистити виділення", + "ContactsPage_EmptyState": "Немає контактів для відображення", + "ContactsPage_AddFirstContact": "Додайте ваш перший контакт", + "ContactsPage_ContactsCountSuffix": "контакти", + "ContactsPane_NewContact": "Новий контакт", + "ContactsPane_DescriptionTitle": "Керуйте своїми контактами", + "ContactsPane_DescriptionBody": "Створюйте контакти, перейменовуйте їх, оновлюйте зображення профілю та зберігайте збережені дані в одному місці.", + "ContactEditDialog_AddTitle": "Додати контакт", + "ContactInfoBar_ContactAdded": "Контакт успішно додано.", + "ContactInfoBar_ContactUpdated": "Контакт успішно оновлено.", + "ContactInfoBar_ContactsDeleted": "Контакти успішно видалено.", + "ContactInfoBar_ContactPhotoUpdated": "Фото контакту успішно оновлено.", + "ContactInfoBar_FailedToLoadContacts": "Не вдалося завантажити контакти: {0}", + "ContactInfoBar_FailedToAddContact": "Не вдалося додати контакт: {0}", + "ContactInfoBar_FailedToUpdateContact": "Не вдалося оновити контакт: {0}", + "ContactInfoBar_FailedToDeleteContacts": "Не вдалося видалити контакти: {0}", + "ContactInfoBar_FailedToUpdatePhoto": "Не вдалося оновити фото: {0}", + "ContactInfoBar_CannotDeleteRoot": "Не можна видалити корневі контакти.", + "ContactConfirmDialog_DeleteTitle": "Видалити контакт", + "ContactConfirmDialog_DeleteMessage": "Ви впевнені, що хочете видалити контакт '{0}'?", + "ContactConfirmDialog_DeleteMultipleMessage": "Ви впевнені, що хочете видалити {0} контакт(ів)?", + "ContactConfirmDialog_DeleteButton": "Видалити", + "CalendarAccountSettings_Title": "Налаштування облікового запису календаря", + "CalendarAccountSettings_Description": "Керуйте налаштуваннями календаря для {0}", + "CalendarAccountSettings_AccountColor": "Колір облікового запису", + "CalendarAccountSettings_AccountColorDescription": "Змінити відображуваний колір для цього календарного облікового запису", + "CalendarAccountSettings_SyncEnabled": "Увімкнути синхронізацію", + "CalendarAccountSettings_SyncEnabledDescription": "Увімкнути або вимкнути синхронізацію календаря для цього облікового запису", + "CalendarAccountSettings_DefaultShowAs": "Статус відображення за замовчуванням", + "CalendarAccountSettings_DefaultShowAsDescription": "Статус доступності за замовчуванням для нових подій, створених за допомогою цього облікового запису", + "CalendarAccountSettings_PrimaryCalendar": "Основний календар", + "CalendarAccountSettings_PrimaryCalendarDescription": "Позначити цей календар як основний календар для облікового запису", + "CalendarSettings_NewEventBehavior_Header": "Поведінка кнопки створення події", + "CalendarSettings_NewEventBehavior_Description": "Виберіть, чи кнопка створення події має запитувати календар щоразу, або завжди відкривати певний календар.", + "CalendarSettings_NewEventBehavior_AskEachTime": "Запитувати кожного разу", + "CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar": "Завжди використовувати певний календар.", + "CalendarSettings_Rendering_Title": "Відображення", + "CalendarSettings_Rendering_Description": "Налаштуйте макет календаря та поведінку відображення.", + "CalendarSettings_Notifications_Title": "Сповіщення", + "CalendarSettings_Notifications_Description": "Виберіть поведінку за замовчуванням для нагадувань та відкладання.", + "CalendarSettings_Preferences_Title": "Налаштування", + "CalendarSettings_Preferences_Description": "Встановіть, як має працювати кнопка створення події.", + "WhatIsNew_GetStartedButton": "Почати", + "WhatIsNew_ContinueAnywayButton": "Продовжити все одно", + "WhatIsNew_PreparingForNewVersionButton": "Підготовка до нової версії...", + "WhatIsNew_MigrationPreparing_Title": "Підготовка ваших даних", + "WhatIsNew_MigrationPreparing_Description": "Wino застосовує міграції оновлення. Будь ласка, зачекайте, поки ми підготуємо дані вашого облікового запису для цього випуску.", + "WhatIsNew_MigrationFailedMessage": "Застосування міграцій завершилося помилкою {0}. Ви можете продовжувати використовувати застосунок. Однак, якщо виникнуть серйозні проблеми, повторно встановіть застосунок.", + "WhatIsNew_MigrationNotification_Title": "Wino Mail оновлено", + "WhatIsNew_MigrationNotification_Message": "Відкрийте застосунок, щоб завершити оновлення та переглянути зміни.", + "WelcomeWindow_Title": "Ласкаво просимо до Wino Mail", + "WelcomeWindow_Subtitle": "Нативний досвід Windows для пошти та календаря.", + "WelcomeWindow_WhatsNewTitle": "Останні зміни", + "WelcomeWindow_FeaturesTitle": "Функції", + "WelcomeWindow_WhatsNewTab": "Що нового", + "WelcomeWindow_FeaturesTab": "Функції", + "WelcomeWindow_GetStartedButton": "Почати, додавши обліковий запис", + "WelcomeWindow_GetStartedDescription": "Додайте ваш Outlook, Gmail або IMAP обліковий запис, щоб почати з Wino Mail.", + "WelcomeWindow_ImportFromWinoAccount": "Імпортувати з вашого облікового запису Wino", + "WelcomeWindow_ImportInProgress": "Імпорт ваших синхронізованих налаштувань та облікових записів...", + "WelcomeWindow_ImportNoAccountsFound": "У вашому обліковому записі Wino не знайдено синхронізованих облікових записів. Якщо налаштування були доступні, вони відновлені. Використайте 'Почати', щоб додати обліковий запис вручну.", + "WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} синхронізованих облікових записів вже доступні на цьому пристрої. Використайте 'Почати', щоб додати ще один обліковий запис вручну, якщо потрібно.", + "WelcomeWindow_SetupTitle": "Налаштуйте ваш обліковий запис", + "WelcomeWindow_SetupSubtitle": "Виберіть ваш постачальник пошти, щоб почати.", + "WelcomeWindow_AddAccountButton": "Додати обліковий запис", + "WelcomeWindow_SkipForNow": "Пропустити зараз — налаштувати пізніше", + "WelcomeWindow_AppDescription": "Швидкий, сфокусований вхідний ящик — перероблений для Windows 11", + "WelcomeWizard_Step1Title": "Ласкаво просимо", + "SystemTrayMenu_Open": "Відкрити", + "WinoAccount_Titlebar_SyncBenefitTitle": "Синхронізація налаштувань", + "WinoAccount_Titlebar_SyncBenefitDescription": "Налаштування Wino синхронізуються між пристроями.", + "WinoAccount_Titlebar_AddonsBenefitTitle": "Розблокувати надбудови", + "WinoAccount_Titlebar_AddonsBenefitDescription": "Доступ до преміум-функцій, таких як Wino AI Pack.", + "WinoAccount_Management_Description": "Управляйте обліковим записом Wino, доступом до AI Pack та синхронізованими налаштуваннями й даними облікового запису.", + "WinoAccount_Management_SignedOutTitle": "Увійдіть до Wino Mail", + "WinoAccount_Management_SignedOutDescription": "Увійдіть або створіть обліковий запис, щоб синхронізувати пошту, отримати доступ до функцій з штучним інтелектом та керувати налаштуваннями на всіх пристроях.", + "WinoAccount_Management_ProfileSectionHeader": "Профіль", + "WinoAccount_Management_AddOnsSectionHeader": "Надбудови Wino", + "WinoAccount_Management_DataSectionHeader": "Дані", + "WinoAccount_Management_AccountActionsSectionHeader": "Дії з обліковим записом", + "WinoAccount_Management_AccountCardTitle": "Обліковий запис", + "WinoAccount_Management_AccountCardDescription": "Електронна адреса вашого облікового запису Wino та поточний стан облікового запису.", + "WinoAccount_Management_AiPackCardTitle": "AI Pack", + "WinoAccount_Management_AiPackCardDescription": "Перевірте, чи активний Wino AI Pack та скільки залишилося використань.", + "WinoAccount_Management_AiPackActive": "AI Pack активний", + "WinoAccount_Management_AiPackInactive": "AI Pack неактивний", + "WinoAccount_Management_AiPackUsage": "{0} з {1} використано. Залишилось {2}.", + "WinoAccount_Management_AiPackBillingPeriod": "Період оплати: {0:d} - {1:d}", + "WinoAccount_Management_AiPackUnknownUsage": "Деталі використання ще відсутні.", + "WinoAccount_Management_AiPackBuyDescription": "Купіть Wino AI Pack, щоб перекладати, переформулювати або підсумовувати електронні листи за допомогою AI.", + "WinoAccount_Management_AiPackPromoTitle": "Розблокувати AI Pack", + "WinoAccount_Management_AiPackPromoDescription": "Підвищуйте ефективність обробки електронної пошти за допомогою інструментів на основі штучного інтелекту. Перекладайте повідомлення на понад 50 мов, переформулюйте для ясності та тону, а також миттєві підсумки довгих ланцюжків листування.", + "WinoAccount_Management_AiPackPromoPrice": "$4.99 / міс.", + "WinoAccount_Management_AiPackPromoRequests": "1 000 кредитів", + "WinoAccount_Management_AiPackGetButton": "Отримати AI Pack", + "WinoAddOn_AI_PACK_Name": "Wino AI Pack", + "WinoAddOn_AI_PACK_Description": "Інструменти на базі штучного інтелекту для перекладу, переформулювання та стислих підсумків дій у Wino Mail.", + "WinoAddOn_AI_PACK_Keywords": "AI, переклад, переформулювати, підсумовувати, продуктивність", + "WinoAddOn_UNLIMITED_ACCOUNTS_Name": "Необмежені облікові записи", + "WinoAddOn_UNLIMITED_ACCOUNTS_Description": "Видалити обмеження на облікові записи та додати стільки поштових облікових записів, скільки потрібно.", + "WinoAddOn_UNLIMITED_ACCOUNTS_Keywords": "облікові записи, необмежені, преміум, надбудова", + "WinoAccount_Management_PurchaseRequiresSignIn": "Увійдіть у свій обліковий запис Wino, щоб завершити покупку.", + "WinoAccount_Management_PurchaseStartFailed": "Wino не зміг завершити це придбання в Microsoft Store.", + "WinoAccount_Management_StoreSyncFailed": "Покупка завершена, але Wino не зміг оновити переваги облікового запису. Спробуйте ще раз через мить.", + "WinoAccount_Management_AiPackSubscriptionActive": "Ваша підписка активна", + "WinoAccount_Management_AiPackRenews": "Поновлюється {0:d}", + "WinoAccount_Management_AiPackRequestsUsed": "Кредити використано цього місяця", + "WinoAccount_Management_AiPackResets": "Скидання {0:d}", + "WinoAccount_Management_AiPackUsageLoadFailed": "Під час завантаження балансу використання AI сталася помилка.", + "WinoAccount_Management_AiPackFeatureTranslate": "Переклад", + "WinoAccount_Management_AiPackFeatureRewrite": "Переформулювати", + "WinoAccount_Management_AiPackFeatureSummarize": "Підсумувати", + "WinoAccount_Management_AddOnLoadFailed": "Не вдалося завантажити це доповнення.", + "WinoAccount_Management_SyncPreferencesTitle": "Синхронізація налаштувань та облікових записів", + "WinoAccount_Management_SyncPreferencesDescription": "Імпортуйте або експортуйте ваші налаштування Wino та дані поштової скриньки між пристроями. Паролі, токени та іншу чутливу інформацію ніколи не синхронізуються.", + "WinoAccount_Management_SignOutTitle": "Вийти", + "WinoAccount_Management_SignOutDescription": "Вийдіть з облікового запису на цьому пристрої", + "WinoAccount_Management_StatusLabel": "Статус: {0}", + "WinoAccount_Management_NoRemoteSettings": "Для цього облікового запису ще немає синхронізованих даних.", + "WinoAccount_Management_ExportSucceeded": "Ваші обрані дані Wino успішно експортувалися.", + "WinoAccount_Management_ExportPreferencesSucceeded": "Ваші налаштування були експортовані до вашого облікового запису Wino.", + "WinoAccount_Management_ExportAccountsSucceeded": "Експортовано {0} відомостей облікових записів до вашого облікового запису Wino.", + "WinoAccount_Management_ImportSucceeded": "Імпортовано синхронізовані дані з вашого облікового запису Wino.", + "WinoAccount_Management_ImportPreferencesSucceeded": "Застосовано {0} синхронізованих налаштувань.", + "WinoAccount_Management_ImportAccountsSucceeded": "Імпортовано {0} облікових записів.", + "WinoAccount_Management_ImportDuplicateAccountsSkipped": "Пропущено {0} облікових записів, які вже існують на цьому пристрої.", + "WinoAccount_Management_ImportPartial": "Застосовано {0} синхронізованих налаштувань. {1} налаштувань не вдалося відновити.", + "WinoAccount_Management_ImportReloginReminder": "Паролі, токени та іншу чутливу інформацію не імпортовано. Увійдіть знову в кожен обліковий запис на цьому пристрої перед використанням.", + "WinoAccount_Management_SerializeFailed": "Wino не зміг серіалізувати ваші поточні налаштування.", + "WinoAccount_Management_EmptyExport": "Немає значень налаштувань для експорту.", + "WinoAccount_Management_ImportEmpty": "Синхронізований пакет даних не містить нічого нового для відновлення.", + "WinoAccount_Management_ExportDialog_Title": "Експорт до вашого облікового запису Wino.", + "WinoAccount_Management_ExportDialog_Description": "Виберіть те, що ви хочете синхронізувати з вашим обліковим записом Wino.", + "WinoAccount_Management_ExportDialog_IncludePreferences": "Налаштування", + "WinoAccount_Management_ExportDialog_IncludeAccounts": "Облікові записи", + "WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Паролі, токени та інша чутлива інформація не синхронізуються.", + "WinoAccount_Management_ExportDialog_AccountsRelogin": "Імпортовані облікові записи на іншому ПК все ще потребуватимуть повторного входу перед їх використанням.", + "WinoAccount_Management_ExportDialog_InProgress": "Експорт ваших вибраних даних Wino...", + "WinoAccount_Management_LoadFailed": "Не вдалося завантажити останню інформацію про обліковий запис Wino.", + "WinoAccount_Management_ActionFailed": "Запит до облікового запису Wino не вдалося виконати.", + "WinoAccount_SettingsSection_Title": "Обліковий запис Wino", + "WinoAccount_SettingsSection_Description": "Створіть або увійдіть до облікового запису Wino за допомогою локального сервісу автентифікації.", + "WinoAccount_RegisterButton_Title": "Зареєструвати обліковий запис", + "WinoAccount_RegisterButton_Description": "Створіть обліковий запис Wino за допомогою електронної пошти та пароля.", + "WinoAccount_RegisterButton_Action": "Відкрити реєстрацію", + "WinoAccount_LoginButton_Title": "Увійти", + "WinoAccount_LoginButton_Description": "Увійдіть до існуючого облікового запису Wino за допомогою електронної пошти та пароля.", + "WinoAccount_LoginButton_Action": "Відкрити вхід", + "WinoAccount_SignOutButton_Title": "Вийти", + "WinoAccount_SignOutButton_Description": "Видалити локальну сесію облікового запису Wino.", + "WinoAccount_SignOutButton_Action": "Вийти", + "WinoAccount_RegisterDialog_Title": "Створити обліковий запис Wino", + "WinoAccount_RegisterDialog_Description": "Створіть обліковий запис Wino, щоб синхронізувати свій досвід використання Wino та розблокувати розширення, що залежать від облікового запису.", + "WinoAccount_RegisterDialog_HeroTitle": "Створіть свій обліковий запис Wino", + "WinoAccount_RegisterDialog_BenefitsTitle": "Навіщо створювати обліковий запис?", + "WinoAccount_RegisterDialog_BenefitSyncTitle": "Імпорт та експорт налаштувань між пристроями", + "WinoAccount_RegisterDialog_BenefitSyncDescription": "Переміщайте свої налаштування Wino між пристроями без повторного налаштування.", + "WinoAccount_RegisterDialog_BenefitAiTitle": "Доступ до ексклюзивних розширень, таких як Wino AI Pack (платно)", + "WinoAccount_RegisterDialog_BenefitAiDescription": "Використовуйте один обліковий запис, щоб розблоковувати преміум-функції Wino по мірі їх появи.", + "WinoAccount_RegisterDialog_DifferenceTitle": "Wino Account відокремлений від ваших поштових облікових записів", + "WinoAccount_RegisterDialog_DifferenceDescription": "Ваші облікові записи Outlook, Gmail, IMAP або інші електронні поштові облікові записи залишаються такими, як є. Обліковий запис Wino керує лише функціями, пов’язаними з Wino, та розширеннями на основі облікового запису.", + "WinoAccount_RegisterDialog_PrimaryButton": "Зареєструвати", + "WinoAccount_RegisterDialog_PrivacyTitle": "Політика приватності та обробка API", + "WinoAccount_RegisterDialog_PrivacyDescription": "Необов'язкові розширення, такі як Wino AI Pack, можуть надсилати вибраний HTML-вміст електронної пошти до сервісу Wino API тільки за умови використання цих функцій.", + "WinoAccount_RegisterDialog_PrivacyLinkText": "Прочитати політику конфіденційності", + "WinoAccount_RegisterDialog_PrivacyCheckbox": "Я згоден з політикою конфіденційності.", + "WinoAccount_LoginDialog_Title": "Увійти до облікового запису Wino", + "WinoAccount_LoginDialog_Description": "Увійдіть до свого облікового запису Wino, щоб синхронізувати налаштування Wino та отримати доступ до функцій на основі облікового запису.", + "WinoAccount_LoginDialog_HeroTitle": "Ласкаво просимо знову", + "WinoAccount_LoginDialog_BenefitsTitle": "Що дає вхід у обліковий запис", + "WinoAccount_LoginDialog_BenefitsDescription": "Використовуйте свій обліковий запис Wino, щоб продовжувати синхронізувати налаштування між пристроями та отримати доступ до платних доповнень, таких як Wino AI Pack.", + "WinoAccount_LoginDialog_DifferenceTitle": "Це не вхід до вашої поштової скриньки", + "WinoAccount_LoginDialog_DifferenceDescription": "Увійдення тут не додає і не замінює ваші облікові записи Outlook, Gmail, IMAP або інші облікові записи в Wino. Воно лише входить у сервіси, пов’язані з Wino.", + "WinoAccount_LoginDialog_ForgotPasswordLink": "Забули пароль?", + "WinoAccount_EmailLabel": "Електронна пошта", + "WinoAccount_EmailPlaceholder": "name@example.com", + "WinoAccount_PasswordLabel": "Пароль", + "WinoAccount_ConfirmPasswordLabel": "Підтвердити пароль", + "WinoAccount_ForgotPasswordDialog_Title": "Скинути пароль", + "WinoAccount_ForgotPasswordDialog_PrimaryButton": "Надіслати лист із скиданням пароля", + "WinoAccount_ForgotPasswordDialog_BackToSignIn": "Повернутися до входу", + "WinoAccount_ForgotPasswordDialog_Description": "Введіть адресу електронної пошти вашого облікового запису Wino, і ми надішлемо посилання для скидання пароля, якщо така адреса зареєстрована.", + "WinoAccount_Validation_EmailRequired": "Електронна адреса обов'язкова.", + "WinoAccount_Validation_PasswordRequired": "Пароль обов'язковий.", + "WinoAccount_Validation_PasswordMismatch": "Паролі не збігаються.", + "WinoAccount_Validation_PrivacyConsentRequired": "Потрібно прийняти політику конфіденційності перед створенням облікового запису Wino.", + "WinoAccount_Error_InvalidCredentials": "Неправильна електронна адреса або пароль.", + "WinoAccount_Error_AccountLocked": "Цей обліковий запис тимчасово заблокований.", + "WinoAccount_Error_AccountBanned": "Цей обліковий запис заблоковано.", + "WinoAccount_Error_AccountSuspended": "Цей обліковий запис призупинено.", + "WinoAccount_Error_EmailNotConfirmed": "Будь ласка, підтвердіть свою електронну адресу перед входом.", + "WinoAccount_Error_EmailConfirmationRequired": "Будь ласка, підтвердіть свою електронну адресу перед входом.", + "WinoAccount_Error_EmailConfirmationResendNotAvailable": "Нове електронне повідомлення з підтвердженням ще недоступне.", + "WinoAccount_Error_EmailConfirmationResendInvalid": "Цей запит на підтвердження більше не дійсний. Будь ласка, спробуйте увійти знову.", + "WinoAccount_Error_EmailNotRegistered": "Ця електронна адреса не зареєстрована.", + "WinoAccount_Error_RefreshTokenInvalid": "Сесія більше не дійсна. Будь ласка, увійдіть знову.", + "WinoAccount_Error_EmailAlreadyRegistered": "Ця електронна адреса вже зареєстрована.", + "WinoAccount_Error_ExternalLoginEmailRequired": "Адреса електронної пошти потрібна для завершення зовнішнього входу.", + "WinoAccount_Error_ExternalLoginInvalid": "Запит зовнішнього входу недійсний.", + "WinoAccount_Error_ExternalAuthStateInvalid": "Стан зовнішнього входу недійсний або закінчився.", + "WinoAccount_Error_ExternalAuthCodeInvalid": "Код зовнішнього входу недійсний або закінчився.", + "WinoAccount_Error_AiPackRequired": "Потрібна активна підписка на Wino AI Pack для виконання цієї дії.", + "WinoAccount_Error_AiQuotaExceeded": "Ліміт використання пакета Wino AI досягнуто за поточний платіжний період.", + "WinoAccount_Error_AiHtmlEmpty": "Немає вмісту електронної пошти для обробки.", + "WinoAccount_Error_AiHtmlTooLarge": "Ця електронна пошта занадто велика для обробки за допомогою Wino AI.", + "WinoAccount_Error_AiUnsupportedLanguage": "Ця мова не підтримується. Спробуйте дійсний код культури, наприклад en-US або tr-TR.", + "WinoAccount_Error_Forbidden": "У вас немає дозволу на виконання цієї дії.", + "WinoAccount_Error_ValidationFailed": "Запит недійсний. Будь ласка, перегляньте введені значення.", + "WinoAccount_RegisterSuccessMessage": "Реєстрація облікового запису Wino завершена для {0}.", + "WinoAccount_LoginSuccessMessage": "Увійдено до облікового запису Wino як {0}.", + "WinoAccount_EmailConfirmationSentDialog_Title": "Підтвердіть вашу електронну адресу", + "WinoAccount_EmailConfirmationSentDialog_Message": "Ми надіслали підтвердження електронної пошти на {0}. Підтвердіть його та повторіть спробу входу.", + "WinoAccount_EmailConfirmationPendingDialog_Title": "Потрібне підтвердження електронної пошти", + "WinoAccount_EmailConfirmationPendingDialog_Message": "Ми все ще чекаємо, поки ви підтвердите {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ResendButton": "Надіслати повторно лист з підтвердженням", + "WinoAccount_EmailConfirmationPendingDialog_Countdown": "Ви зможете надіслати повторно підтвердження електронної адреси через {0}.", + "WinoAccount_EmailConfirmationPendingDialog_ReadyToResend": "Тепер ви можете надіслати повторно підтвердження", + "WinoAccount_EmailConfirmationResentDialog_Title": "Письмо з підтвердженням повторно надіслано", + "WinoAccount_EmailConfirmationResentDialog_Message": "Ми надіслали ще одне підтвердження на {0}. Будь ласка, підтвердіть його та спробуйте увійти знову.", + "WinoAccount_ForgotPasswordDialog_SuccessTitle": "Лист із скиданням пароля надіслано", + "WinoAccount_ForgotPasswordDialog_SuccessMessage": "Ми надіслали листа зі скиданням пароля на {0}. Відкрийте це повідомлення, щоб обрати новий пароль.", + "WinoAccount_ChangePassword_Title": "Зміна пароля", + "WinoAccount_ChangePassword_Description": "Надіслати листа зі скиданням пароля на цей обліковий запис Wino.", + "WinoAccount_ChangePassword_Action": "Надіслати лист зі скиданням пароля", + "WinoAccount_ChangePassword_ConfirmationMessage": "Чи хочете ви, щоб Wino надіслало електронного листа для скидання пароля на {0}?", + "WinoAccount_SignOut_SuccessMessage": "Ви вийшли з облікового запису Wino {0}.", + "WinoAccount_SignOut_NoAccountMessage": "Немає активного облікового запису Wino, з якого можна вийти.", + "WinoAccount_Titlebar_SignedOutTitle": "Обліковий запис Wino", + "WinoAccount_Titlebar_SignedOutDescription": "Увійдіть або створіть обліковий запис Wino, щоб керувати сеансом Wino.", + "WinoAccount_Titlebar_SignedInStatus": "Статус: {0}", + "WelcomeWizard_Step2Title": "Додати обліковий запис", + "WelcomeWizard_Step3Title": "Завершити налаштування", + "ProviderSelection_Title": "Виберіть постачальника електронної пошти", + "ProviderSelection_Subtitle": "Виберіть постачальника нижче, щоб додати свій обліковий ящик електронної пошти до Wino Mail.", + "ProviderSelection_AccountNameHeader": "Назва облікового запису", + "ProviderSelection_AccountNamePlaceholder": "наприклад, Особистий, Робочий", + "ProviderSelection_DisplayNameHeader": "Відображуване ім'я", + "ProviderSelection_DisplayNamePlaceholder": "наприклад, Іван Іванов", + "ProviderSelection_EmailHeader": "Електронна адреса", + "ProviderSelection_EmailPlaceholder": "наприклад, johndoe@example.com", + "ProviderSelection_AppPasswordHeader": "Пароль для застосунку", + "ProviderSelection_AppPasswordHelp": "Як отримати пароль для застосунку?", + "ProviderSelection_CalendarModeHeader": "Інтеграція календаря", + "ProviderSelection_CalendarMode_DisabledTitle": "Вимкнено", + "ProviderSelection_CalendarMode_DisabledDescription": "Немає інтеграції календаря", + "ProviderSelection_CalendarMode_CalDavTitle": "Синхронізація CalDAV", + "ProviderSelection_CalendarMode_CalDavDescription_Apple": "Ваші події календаря синхронізуються між пристроями з серверами Apple.", + "ProviderSelection_CalendarMode_CalDavDescription_Yahoo": "Ваші події календаря синхронізуються між вашими пристроями через сервери Yahoo.", + "ProviderSelection_CalendarMode_LocalTitle": "Локальний календар", + "ProviderSelection_CalendarMode_LocalDescription": "Ваші події зберігаються лише на вашому комп'ютері. Без з'єднання з сервером.", + "ProviderSelection_ClearColor": "Очистити колір", + "ProviderSelection_ContinueButton": "Продовжити", + "ProviderSelection_SpecialImap_Subtitle": "Введіть дані облікового запису, щоб підключитися.", + "AccountSetup_Title": "Налаштування вашого облікового запису", + "AccountSetup_Step_Authenticating": "Аутентифікація з {0}", + "AccountSetup_Step_TestingMailAuth": "Перевірка автентифікації пошти", + "AccountSetup_Step_SyncingFolders": "Синхронізація метаданих папок", + "AccountSetup_Step_FetchingProfile": "Отримання інформації профілю", + "AccountSetup_Step_DiscoveringCalDav": "Виявлення налаштувань CalDAV", + "AccountSetup_Step_TestingCalendarAuth": "Перевірка автентифікації календаря", + "AccountSetup_Step_SavingAccount": "Збереження інформації облікового запису", + "AccountSetup_Step_FetchingCalendarMetadata": "Отримання метаданих календаря", + "AccountSetup_Step_SyncingAliases": "Синхронізація псевдонімів", + "AccountSetup_Step_Finalizing": "Завершення налаштування", + "AccountSetup_FailureMessage": "Налаштування не вдалося. Поверніться назад, щоб виправити налаштування, або спробуйте ще пізніше.", + "AccountSetup_SuccessMessage": "Ваш обліковий запис успішно налаштовано!", + "AccountSetup_GoBackButton": "Назад", + "AccountSetup_TryAgainButton": "Спробуйте ще раз", + "ImapCalDavSettings_AutoDiscoveryFailed": "Автоматичне виявлення не вдалося. Будь ласка, введіть налаштування вручну на вкладці Розширені." } - - diff --git a/Wino.Core.Domain/Translations/zh_CN/resources.json b/Wino.Core.Domain/Translations/zh_CN/resources.json index 83dc9164..56ff14fc 100644 --- a/Wino.Core.Domain/Translations/zh_CN/resources.json +++ b/Wino.Core.Domain/Translations/zh_CN/resources.json @@ -4,28 +4,34 @@ "AccountAlias_Column_Verified": "已验证", "AccountAlias_Disclaimer_FirstLine": "Wino 只能从 Gmail 账户导入别名。", "AccountAlias_Disclaimer_SecondLine": "如果想要为 Outlook 和 IMAP 账户使用别名,请自行添加。", - "AccountCacheReset_Title": "重置账户缓存", - "AccountCacheReset_Message": "此帐户需要完全重新同步才能继续工作。请等待 Wino 重新同步你的消息…", - "AccountContactNameYou": "你", + "AccountCacheReset_Title": "账户缓存重置", + "AccountCacheReset_Message": "此帐户需要完全重新同步才能继续工作。请等待 Wino 重新同步您的消息……", + "AccountContactNameYou": "您", "AccountCreationDialog_Completed": "已完成", + "AccountCreationDialog_FetchingCalendarMetadata": "正在获取日历详情。", "AccountCreationDialog_FetchingEvents": "正在获取日历事件。", - "AccountCreationDialog_FetchingProfileInformation": "正在获取账户资料详情。", - "AccountCreationDialog_GoogleAuthHelpClipboardText_Row0": "如果你的浏览器没有自动启动以完成身份验证:", + "AccountCreationDialog_FetchingProfileInformation": "正在获取用户资料详情。", + "AccountCreationDialog_GoogleAuthHelpClipboardText_Row0": "如果浏览器没有自动打开完成身份验证:", "AccountCreationDialog_GoogleAuthHelpClipboardText_Row1": "1) 点击下面的按钮复制身份验证地址", "AccountCreationDialog_GoogleAuthHelpClipboardText_Row2": "2) 启动浏览器 (Edge、Chrome、Firefox 等)", "AccountCreationDialog_GoogleAuthHelpClipboardText_Row3": "3) 粘贴复制的地址并访问以手动完成认证。", "AccountCreationDialog_Initializing": "正在初始化", "AccountCreationDialog_PreparingFolders": "我们正在获取文件夹信息。", "AccountCreationDialog_SigninIn": "正在保存账户信息。", + "Purchased": "已购买", "AccountEditDialog_Message": "账户名称", "AccountEditDialog_Title": "编辑账户", "AccountPickerDialog_Title": "选择账户", "AccountSettingsDialog_AccountName": "发件人名称", - "AccountSettingsDialog_AccountNamePlaceholder": "例如:John Doe", + "AccountSettingsDialog_AccountNamePlaceholder": "例如: John Doe", "AccountDetailsPage_Title": "账户信息", "AccountDetailsPage_Description": "更改 Wino 中的账户名称与所需的发件人名称。", "AccountDetailsPage_ColorPicker_Title": "账户颜色", "AccountDetailsPage_ColorPicker_Description": "指定账户在列表中显示的颜色。", + "AccountDetailsPage_TabGeneral": "常规", + "AccountDetailsPage_TabMail": "邮件", + "AccountDetailsPage_TabCalendar": "日历", + "AccountDetailsPage_CalendarListDescription": "选择日历以配置其设置", "AddHyperlink": "添加", "AppCloseBackgroundSynchronizationWarningTitle": "后台同步", "AppCloseStartupLaunchDisabledWarningMessageFirstLine": "应用尚未设置为随 Windows 自启。", @@ -33,10 +39,10 @@ "AppCloseStartupLaunchDisabledWarningMessageThirdLine": "是否前往应用设置页面启用它?", "AppCloseTerminateBehaviorWarningMessageFirstLine": "正在停止 Wino Mail,应用关闭行为已被设置为「终止进程」。", "AppCloseTerminateBehaviorWarningMessageSecondLine": "将停止所有后台同步和通知。", - "AppCloseTerminateBehaviorWarningMessageThirdLine": "是否前往应用设置,设置 Wino Mail 为最小化或后台运行?", - "AutoDiscoveryProgressMessage": "正在搜索邮件设置……", + "AppCloseTerminateBehaviorWarningMessageThirdLine": "是否前往应用设置中将 Wino Mail 设置为最小化或后台运行?", + "AutoDiscoveryProgressMessage": "正在搜索邮件设置…", "BasicIMAPSetupDialog_AdvancedConfiguration": "高级设置", - "BasicIMAPSetupDialog_CredentialLocalMessage": "你的凭据将仅在本地存储在你的计算机上。", + "BasicIMAPSetupDialog_CredentialLocalMessage": "您的凭据将仅存储在您的计算机上。", "BasicIMAPSetupDialog_Description": "部分账户需要额外的登录步骤", "BasicIMAPSetupDialog_DisplayName": "显示名称", "BasicIMAPSetupDialog_DisplayNamePlaceholder": "例如 John Doe", @@ -47,8 +53,10 @@ "BasicIMAPSetupDialog_Title": "IMAP 账户", "Busy": "正忙", "Buttons_AddAccount": "添加账户", + "Buttons_FixAccount": "修复账户", "Buttons_AddNewAlias": "添加新别名", "Buttons_Allow": "允许", + "Buttons_Apply": "应用", "Buttons_ApplyTheme": "应用主题", "Buttons_Browse": "浏览", "Buttons_Cancel": "取消", @@ -62,6 +70,7 @@ "Buttons_Edit": "编辑", "Buttons_EnableImageRendering": "启用", "Buttons_Multiselect": "批量选择", + "Buttons_Manage": "管理", "Buttons_No": "否", "Buttons_Open": "打开", "Buttons_Purchase": "购买", @@ -70,48 +79,170 @@ "Buttons_Save": "保存", "Buttons_SaveConfiguration": "保存设置", "Buttons_Send": "发送", + "Buttons_SendToServer": "发送到服务器", "Buttons_Share": "分享", "Buttons_SignIn": "登录", "Buttons_Sync": "同步", "Buttons_SyncAliases": "同步别名", "Buttons_TryAgain": "重试", "Buttons_Yes": "是", + "Sync_SynchronizingFolder": "正在同步 {0} {1}%", + "Sync_DownloadedMessages": "已从 {1} 下载 {0} 条消息", + "SyncAction_Archiving": "正在归档 {0} 封邮件", + "SyncAction_ClearingFlag": "取消标记 {0} 封邮件", + "SyncAction_CreatingDraft": "正在创建草稿", + "SyncAction_CreatingEvent": "正在创建日历事件", + "SyncAction_Deleting": "正在删除 {0} 封邮件", + "SyncAction_EmptyingFolder": "正在清空文件夹", + "SyncAction_MarkingAsRead": "将 {0} 封邮件标记为已读", + "SyncAction_MarkingAsUnread": "将 {0} 封邮件标记为未读", + "SyncAction_MarkingFolderAsRead": "将文件夹标记为已读", + "SyncAction_Moving": "正在移动 {0} 封邮件", + "SyncAction_MovingToFocused": "将 {0} 封邮件移动到专注收件箱", + "SyncAction_RenamingFolder": "正在重命名文件夹", + "SyncAction_SendingMail": "正在发送邮件", + "SyncAction_SettingFlag": "为 {0} 封邮件设置旗标", + "SyncAction_SynchronizingAccount": "正在同步 {0}", + "SyncAction_SynchronizingAccounts": "正在同步 {0} 个账户", + "SyncAction_SynchronizingCalendarData": "正在同步日历数据", + "SyncAction_SynchronizingCalendarEvents": "正在同步日历事件", + "SyncAction_SynchronizingCalendarMetadata": "正在同步日历元数据", + "SyncAction_Unarchiving": "正在解除归档 {0} 封邮件", "CalendarAllDayEventSummary": "全天事件", "CalendarDisplayOptions_Color": "颜色", "CalendarDisplayOptions_Expand": "展开", + "CalendarEventResponse_Accept": "接受", + "CalendarEventResponse_AcceptedResponse": "已接受", + "CalendarEventResponse_Decline": "拒绝", + "CalendarEventResponse_DeclinedResponse": "已拒绝", + "CalendarEventResponse_NotResponded": "未回应", + "CalendarEventResponse_Tentative": "暂定", + "CalendarEventResponse_TentativeResponse": "暂定", + "CalendarEventRsvpPanel_Accept": "接受", + "CalendarEventRsvpPanel_AddMessage": "在回复中添加消息...(可选)", + "CalendarEventRsvpPanel_Decline": "拒绝", + "CalendarEventRsvpPanel_Message": "消息", + "CalendarEventRsvpPanel_SendReplyMessage": "发送回复消息", + "CalendarEventRsvpPanel_Tentative": "暂定", + "CalendarEventRsvpPanel_Title": "回复选项", + "CalendarAttendeeStatus_Accepted": "已接受", + "CalendarAttendeeStatus_Declined": "已拒绝", + "CalendarAttendeeStatus_NeedsAction": "需要操作", + "CalendarAttendeeStatus_Tentative": "暂定", + "CalendarEventDetails_Attachments": "附件", + "CalendarEventCompose_AddAttachment": "添加附件", + "CalendarEventCompose_AllDay": "整天", + "CalendarEventCompose_AttachmentsNotSupportedForCalDav": "CalDAV 日历不支持附件。", + "CalendarEventCompose_EndDate": "结束日期", + "CalendarEventCompose_EndTime": "结束时间", + "CalendarEventCompose_Every": "每", + "CalendarEventCompose_ForWeekdays": "持续", + "CalendarEventCompose_FrequencyDay": "天", + "CalendarEventCompose_FrequencyDayPlural": "天", + "CalendarEventCompose_FrequencyMonth": "月", + "CalendarEventCompose_FrequencyMonthPlural": "月", + "CalendarEventCompose_FrequencyWeek": "周", + "CalendarEventCompose_FrequencyWeekPlural": "周", + "CalendarEventCompose_FrequencyYear": "年", + "CalendarEventCompose_FrequencyYearPlural": "年", + "CalendarEventCompose_Location": "地点", + "CalendarEventCompose_LocationPlaceholder": "添加地点", + "CalendarEventCompose_NewEventButton": "新建事件", + "CalendarEventCompose_DefaultCalendarHint": "您可以在日历设置中为新事件选择默认日历。", + "CalendarEventCompose_DefaultCalendarSettingsLink": "打开日历设置", + "CalendarEventCompose_NoCalendarsMessage": "当前没有可用于创建事件的日历。", + "CalendarEventCompose_NoCalendarsTitle": "没有可用日历", + "CalendarEventCompose_NoEndDate": "无结束日期", + "CalendarEventCompose_Notes": "备注", + "CalendarEventCompose_PickCalendarTitle": "选择日历", + "CalendarEventCompose_Recurring": "重复", + "CalendarEventCompose_RecurringSummary": "每 {0} {1}{2} {3} 生效 {4}{5}", + "CalendarEventCompose_RecurringSummarySmart": "发生 {0}{1} {2} 生效 {3}{4}", + "CalendarEventCompose_RepeatEvery": "每隔", + "CalendarEventCompose_SelectCalendar": "选择日历", + "CalendarEventCompose_SingleOccurrenceSummary": "在 {0} {1} 发生", + "CalendarEventCompose_StartDate": "开始日期", + "CalendarEventCompose_StartTime": "开始时间", + "CalendarEventCompose_TimeRangeSummary": "从 {0} 到 {1}", + "CalendarEventCompose_Title": "事件标题", + "CalendarEventCompose_TitlePlaceholder": "添加标题", + "CalendarEventCompose_Until": "until", + "CalendarEventCompose_UntilSummary": " until {0}", + "CalendarEventCompose_ValidationInvalidAllDayRange": "全天结束日期必须晚于开始日期。", + "CalendarEventCompose_ValidationInvalidAttendee": "一个或多个与会者的电子邮件地址无效。", + "CalendarEventCompose_ValidationInvalidRecurrenceEnd": "重复结束日期必须晚于或等于事件开始日期。", + "CalendarEventCompose_ValidationInvalidTimeRange": "结束时间必须晚于开始时间。", + "CalendarEventCompose_ValidationMissingAttachment": "一个或多个附件不再可用:{0}", + "CalendarEventCompose_ValidationMissingCalendar": "在创建事件之前请选择日历。", + "CalendarEventCompose_ValidationMissingTitle": "在创建事件之前请输入事件标题。", + "CalendarEventCompose_ValidationTitle": "事件验证失败", + "CalendarEventCompose_WeekdaySummary": " 在 {0}", + "CalendarEventCompose_Weekday_Friday": "五", + "CalendarEventCompose_Weekday_Monday": "一", + "CalendarEventCompose_Weekday_Saturday": "六", + "CalendarEventCompose_Weekday_Sunday": "日", + "CalendarEventCompose_Weekday_Thursday": "四", + "CalendarEventCompose_Weekday_Tuesday": "二", + "CalendarEventCompose_Weekday_Wednesday": "三", + "CalendarEventDetails_Details": "详情", + "CalendarEventDetails_EditSeries": "编辑系列", + "CalendarEventDetails_Editing": "正在编辑", + "CalendarEventDetails_InviteSomeone": "邀请某人", + "CalendarEventDetails_JoinOnline": "在线参加", + "CalendarEventDetails_Organizer": "组织者", + "CalendarEventDetails_People": "参与者", + "CalendarEventDetails_ReadOnlyEvent": "只读事件", + "CalendarEventDetails_Reminder": "提醒", + "CalendarReminder_StartedHoursAgo": "已开始 {0} 小时前", + "CalendarReminder_StartedMinutesAgo": "已开始 {0} 分钟前", + "CalendarReminder_StartedNow": "刚刚开始", + "CalendarReminder_StartingNow": "现在开始", + "CalendarReminder_StartsInHours": "将在 {0} 小时后开始", + "CalendarReminder_StartsInMinutes": "将在 {0} 分钟后开始", + "CalendarReminder_SnoozeAction": "稍后提醒", + "CalendarReminder_SnoozeMinutesOption": "{0} 分钟", + "CalendarEventDetails_ShowAs": "显示为", + "CalendarShowAs_Free": "空闲", + "CalendarShowAs_Tentative": "暂定", + "CalendarShowAs_Busy": "忙碌", + "CalendarShowAs_OutOfOffice": "外出", + "CalendarShowAs_WorkingElsewhere": "在其他地方工作", "CalendarItem_DetailsPopup_JoinOnline": "在线加入", "CalendarItem_DetailsPopup_ViewEventButton": "查看事件", "CalendarItem_DetailsPopup_ViewSeriesButton": "查看系列", "CalendarItemAllDay": "全天", "CategoriesFolderNameOverride": "分类", "Center": "中心", - "ClipboardTextCopied_Message": "{0} 已复制到剪贴板。", + "ClipboardTextCopied_Message": "{0} 已复制到剪贴板", "ClipboardTextCopied_Title": "已复制", "ClipboardTextCopyFailed_Message": "无法将 {0} 复制到剪贴板。", - "ComingSoon": "即将到来…", + "ContactInfoBar_ErrorTitle": "无法加载联系信息", + "ContactInfoBar_SuccessTitle": "联系信息已加载", + "ContactInfoBar_WarningTitle": "联系信息可能不完整", + "ComingSoon": "即将到来...", "ComposerAttachmentsDragDropAttach_Message": "附件", "ComposerAttachmentsDropZone_Message": "将文件拖放至此", - "ComposerFrom": "从: ", + "ComposerFrom": "来自: ", "ComposerImagesDropZone_Message": "将图片拖放至此", "ComposerSubject": "主题: ", - "ComposerTo": "到: ", + "ComposerTo": "至: ", "ComposerToPlaceholder": "点击输入地址", "CreateAccountAliasDialog_AliasAddress": "地址", "CreateAccountAliasDialog_AliasAddressPlaceholder": "示例:support@mydomain.com", - "CreateAccountAliasDialog_Description": "请确保你的出站邮件服务器允许从该别名发送邮件。", + "CreateAccountAliasDialog_Description": "请确保出站邮件服务器允许使用该别名发送邮件。", "CreateAccountAliasDialog_ReplyToAddress": "回复地址", "CreateAccountAliasDialog_ReplyToAddressPlaceholder": "admin@mydomain.com", "CreateAccountAliasDialog_Title": "创建账户别名", - "CustomThemeBuilder_AccentColorDescription": "设置自定义主题颜色。如果未选择颜色将使用 Windows 默认颜色。", - "CustomThemeBuilder_AccentColorTitle": "主题颜色", + "CustomThemeBuilder_AccentColorDescription": "设置自定义颜色。若未选择颜色,将使用 Windows 默认颜色。", + "CustomThemeBuilder_AccentColorTitle": "主题色", "CustomThemeBuilder_PickColor": "选择", "CustomThemeBuilder_ThemeNameDescription": "自定义主题的名称。", "CustomThemeBuilder_ThemeNameTitle": "主题名称", "CustomThemeBuilder_Title": "自定义主题生成器", - "CustomThemeBuilder_WallpaperDescription": "为 Wino 设置自定义壁纸", + "CustomThemeBuilder_WallpaperDescription": "为Wino设置自定义壁纸", "CustomThemeBuilder_WallpaperTitle": "设置自定义壁纸", "Dialog_DontAskAgain": "不再询问", - "DialogMessage_AccountLimitMessage": "你已达到免费用户可创建账户数量上限(3个)。\n是否购买支持作者以“解除可创建账户数量上限”?", + "DialogMessage_AccountLimitMessage": "您已达到免费用户可创建账户数量上限(3个)。\n是否购买支持作者以“解除可创建账户数量上限”?", "DialogMessage_AccountLimitTitle": "已达到账户限制", "DialogMessage_AliasCreatedMessage": "成功创建新别名", "DialogMessage_AliasCreatedTitle": "创建新别名", @@ -119,7 +250,7 @@ "DialogMessage_AliasExistsTitle": "别名已存在", "DialogMessage_AliasNotSelectedMessage": "在发送消息前必须选择一个别名。", "DialogMessage_AliasNotSelectedTitle": "别名未找到", - "DialogMessage_CantDeleteRootAliasMessage": "根别名无法被删除。该名称和你的账户设置关联的主要标识。", + "DialogMessage_CantDeleteRootAliasMessage": "根别名无法被删除。该名称是与账户设置关联的主要标识。", "DialogMessage_CantDeleteRootAliasTitle": "无法删除别名", "DialogMessage_CleanupFolderMessage": "您想永久删除此文件夹中的所有邮件吗?", "DialogMessage_CleanupFolderTitle": "清空文件夹", @@ -128,13 +259,17 @@ "DialogMessage_CreateLinkedAccountMessage": "给这个新链接命名。账户将在此名称下合并。", "DialogMessage_CreateLinkedAccountTitle": "账户链接名称", "DialogMessage_DeleteAccountConfirmationMessage": "删除 {0}?", - "DialogMessage_DeleteAccountConfirmationTitle": "所有与此账户相关的数据将从磁盘上永久删除。", + "DialogMessage_DeleteAccountConfirmationTitle": "与此账户相关的所有数据将从磁盘上永久删除。", + "DialogMessage_DeleteEmailTemplateConfirmationMessage": "删除模板 \"{0}\"?", + "DialogMessage_DeleteEmailTemplateConfirmationTitle": "删除邮件模板", + "DialogMessage_DeleteRecurringSeriesMessage": "这将删除系列中的所有事件。是否继续?", + "DialogMessage_DeleteRecurringSeriesTitle": "删除重复系列", "DialogMessage_DiscardDraftConfirmationMessage": "草稿将被删除。你想要继续吗?", - "DialogMessage_DiscardDraftConfirmationTitle": "丢弃草稿", - "DialogMessage_EmptySubjectConfirmation": "缺少主题", - "DialogMessage_EmptySubjectConfirmationMessage": "邮件没有主题。你想要继续吗?", - "DialogMessage_EnableStartupLaunchDeniedMessage": "可以在「设置」->「应用偏好」中启用自启动。", - "DialogMessage_EnableStartupLaunchMessage": "让 Wino Mail 在 Windows 开机时自动最小化启动以不错过任何通知。\n\n想要启用自启动吗?", + "DialogMessage_DiscardDraftConfirmationTitle": "舍弃草稿", + "DialogMessage_EmptySubjectConfirmation": "缺少标题", + "DialogMessage_EmptySubjectConfirmationMessage": "邮件没有主题。您想要继续吗?", + "DialogMessage_EnableStartupLaunchDeniedMessage": "可以在「设置」-「应用设置」中启用自启动。", + "DialogMessage_EnableStartupLaunchMessage": "让 Wino Mail 随 Windows 自启,不错过任何通知。\n\n想要启用自启动吗?", "DialogMessage_EnableStartupLaunchTitle": "启用自启动", "DialogMessage_HardDeleteConfirmationMessage": "永久删除", "DialogMessage_HardDeleteConfirmationTitle": "邮件将被永久删除。是否继续?", @@ -150,12 +285,12 @@ "DialogMessage_RenameFolderTitle": "重命名文件夹", "DialogMessage_RenameLinkedAccountsMessage": "输入链接账户的新名称", "DialogMessage_RenameLinkedAccountsTitle": "重命名已链接账户", - "DialogMessage_UnlinkAccountsConfirmationMessage": "此操作不会删除你的账户,只会断开共享文件夹的连接。你是否想要继续?", + "DialogMessage_UnlinkAccountsConfirmationMessage": "此操作不会删除您的账户,只会断开共享文件夹的连接。是否继续?", "DialogMessage_UnlinkAccountsConfirmationTitle": "取消链接账户", "DialogMessage_UnsubscribeConfirmationGoToWebsiteConfirmButton": "前往网站", - "DialogMessage_UnsubscribeConfirmationGoToWebsiteMessage": "要停止从 {0} 获取消息,请前往他们的网站取消订阅。", - "DialogMessage_UnsubscribeConfirmationMailtoMessage": "你想要停止从 {0} 获取消息吗? Wino 将通过向 {1} 发送一封电子邮件以取消订阅。", - "DialogMessage_UnsubscribeConfirmationOneClickMessage": "你想要停止从 {0} 处获取信息吗?", + "DialogMessage_UnsubscribeConfirmationGoToWebsiteMessage": "要停止从 {0} 处获取消息,请前往他们的网站退订。", + "DialogMessage_UnsubscribeConfirmationMailtoMessage": "您想要停止从 {0} 处获取消息吗? Wino将通过向 {1} 发送一封电子邮件以取消订阅。", + "DialogMessage_UnsubscribeConfirmationOneClickMessage": "您想要停止从 {0} 处获取信息吗?", "DialogMessage_UnsubscribeConfirmationTitle": "取消订阅", "DiscordChannelDisclaimerMessage": "Wino 没有自己的 Discord 服务器,但在 Developer Sanctuary 服务器上设有专门的 wino-mail 频道。如果要获取有关 Wino 的更新,请加入 Developer Sanctuary 服务器,并关注 Community Projects下的 wino-mail 频道。\n\n因为 Discord 不支持频道邀请,所以你需要通过网页加入频道。", "DiscordChannelDisclaimerTitle": "重要的 Discord 信息", @@ -170,34 +305,41 @@ "ElementTheme_Dark": "深色模式", "ElementTheme_Default": "使用系统设置", "ElementTheme_Light": "浅色模式", - "Emoji": "表情符号", + "Emoji": "表情", "Error_FailedToSetupSystemFolders_Title": "无法设置系统文件夹", - "Exception_AuthenticationCanceled": "授权已取消", + "Exception_AccountNeedsAttention_Title": "账户需要关注", + "Exception_AccountNeedsAttention_Message": "'{0}' 需要您的关注以继续工作。", + "Exception_WebView2RuntimeMissing_Message": "Wino Mail 无法找到 Microsoft Edge WebView2 运行时。请安装或修复运行时以正确呈现消息内容。", + "Exception_WebView2RuntimeMissing_Title": "需要 WebView2 运行时", + "Exception_AuthenticationCanceled": "身份验证已取消", "Exception_CustomThemeExists": "此主题已经存在。", - "Exception_CustomThemeMissingName": "你必须提供名称。", - "Exception_CustomThemeMissingWallpaper": "你必须提供自定义背景图像。", + "Exception_CustomThemeMissingName": "您必须提供名称。", + "Exception_CustomThemeMissingWallpaper": "您必须提供自定义背景图像。", "Exception_FailedToSynchronizeAliases": "别名同步失败", + "Exception_FailedToSynchronizeCalendarData": "同步日历数据失败", + "Exception_FailedToSynchronizeCalendarEvents": "同步日历事件失败", + "Exception_FailedToSynchronizeCalendarMetadata": "日历详情同步失败", "Exception_FailedToSynchronizeFolders": "同步文件夹失败", - "Exception_FailedToSynchronizeProfileInformation": "同步个人资料信息失败", - "Exception_GoogleAuthCallbackNull": "回调 uri 在激活时为空。", + "Exception_FailedToSynchronizeProfileInformation": "个人资料信息同步失败。", + "Exception_GoogleAuthCallbackNull": "Callback uri 在激活时无效。", "Exception_GoogleAuthCorruptedCode": "授权响应不正确。", - "Exception_GoogleAuthError": "OAuth 授权错误:{0}", + "Exception_GoogleAuthError": "OAuth 授权错误: {0}", "Exception_GoogleAuthInvalidResponse": "收到无效状态请求 ({0})", "Exception_GoogleAuthorizationCodeExchangeFailed": "授权代码交换失败。", - "Exception_ImapAutoDiscoveryFailed": "找不到邮箱设置。", + "Exception_ImapAutoDiscoveryFailed": "找不到邮箱配置。", "Exception_ImapClientPoolFailed": "IMAP 客户端池失败。", "Exception_InboxNotAvailable": "无法设置账户文件夹。", - "Exception_InvalidSystemFolderConfiguration": "系统文件夹配置无效。请检查配置然后重试。", - "Exception_InvalidMultiAccountMoveTarget": "你不能移动多个来源于不同被链接的账户的项目。", - "Exception_MailProcessing": "该邮件仍在处理中。请等待几秒后再试。", - "Exception_MissingAlias": "该账户没有主别名。草稿创建失败。", + "Exception_InvalidSystemFolderConfiguration": "系统文件夹配置无效。请检查配置,然后重试。", + "Exception_InvalidMultiAccountMoveTarget": "这些项目来源于不同账户,无法同时删除。", + "Exception_MailProcessing": "该邮件仍在处理中。请稍等几秒后再试。", + "Exception_MissingAlias": "该账户没有主要别名。草稿创建失败。", "Exception_NullAssignedAccount": "分配的账户为空", "Exception_NullAssignedFolder": "分配的文件夹为空", - "Exception_SynchronizerFailureHTTP": "处理响应失败,HTTP 代码:{0}", + "Exception_SynchronizerFailureHTTP": "处理响应失败,HTTP 错误代码: {0}", "Exception_TokenGenerationFailed": "令牌生成失败", "Exception_TokenInfoRetrivalFailed": "获取令牌信息失败。", "Exception_UnknowErrorDuringAuthentication": "身份验证时发生未知错误", - "Exception_UnsupportedAction": "请求处理器不支持活动 {0}", + "Exception_UnsupportedAction": "请求处理器不支持 {0} 操作。", "Exception_UnsupportedProvider": "不支持该提供商。", "Exception_UnsupportedSynchronizerOperation": "{0} 不支持此操作", "Exception_UserCancelSystemFolderSetupDialog": "用户取消了配置系统文件夹的对话框。", @@ -215,24 +357,50 @@ "FolderOperation_MarkAllAsRead": "全部标记为已读", "FolderOperation_Move": "移动", "FolderOperation_None": "无", - "FolderOperation_Pin": "固定", + "FolderOperation_Pin": "置顶", "FolderOperation_Rename": "重命名", - "FolderOperation_Unpin": "取消固定", + "FolderOperation_Unpin": "取消置顶", "GeneralTitle_Error": "错误", "GeneralTitle_Info": "信息", "GeneralTitle_Warning": "警告", - "GmailServiceDisabled_Title": "Gmail 错误", - "GmailServiceDisabled_Message": "你的 Google Workspace 账户似乎已禁用 Gmail 服务。请联系你的管理员以为你的账户启用 Gmail 服务。", + "GmailServiceDisabled_Title": "Gmail 出错", + "GmailServiceDisabled_Message": "你的 Google Workspace 账户似乎无法使用 Gmail 服务。如需启用,请联系该账户的管理员。", "GmailArchiveFolderNameOverride": "归档", "HoverActionOption_Archive": "归档", "HoverActionOption_Delete": "删除", "HoverActionOption_MoveJunk": "移至垃圾箱", "HoverActionOption_ToggleFlag": "标记/取消标记", "HoverActionOption_ToggleRead": "已读/未读", - "ImageRenderingDisabled": "已禁用渲染此消息的图像。", - "ImapAdvancedSetupDialog_AuthenticationMethod": "身份授权方法", + "KeyboardShortcuts_FailedToReset": "重置快捷键失败。", + "KeyboardShortcuts_FailedToUpdate": "更新快捷键失败", + "KeyboardShortcuts_MailoperationAction": "操作", + "KeyboardShortcuts_Action": "操作", + "KeyboardShortcuts_FailedToLoad": "加载快捷键失败。", + "KeyboardShortcuts_EnterKeyForShortcut": "请输入快捷键。", + "KeyboardShortcuts_SelectOperationForShortcut": "请选择要对该快捷键执行的操作。", + "KeyboardShortcuts_EnterKey": "请输入快捷键。", + "KeyboardShortcuts_SelectOperation": "请选择快捷键的操作。", + "KeyboardShortcuts_ShortcutInUse": "此快捷键已被其他快捷键使用。", + "KeyboardShortcuts_FailedToSave": "保存快捷键失败。", + "KeyboardShortcuts_FailedToDelete": "删除快捷键失败。", + "KeyboardShortcuts_PageDescription": "设置快捷键以便快速进行邮件操作。将焦点放在按键输入字段上时按下所需的按键组合。", + "KeyboardShortcuts_Add": "添加快捷键", + "KeyboardShortcuts_EditTitle": "编辑快捷键", + "KeyboardShortcuts_ResetToDefaults": "重置为默认值", + "KeyboardShortcuts_PressKeysHere": "在此处按下按键...", + "KeyboardShortcuts_KeyCombination": "按键组合", + "KeyboardShortcuts_FocusArea": "将焦点放在上面的字段并按下所需的按键组合", + "KeyboardShortcuts_Modifiers": "修饰键", + "KeyboardShortcuts_Mode": "应用模式", + "KeyboardShortcuts_ModeMail": "邮件", + "KeyboardShortcuts_ModeCalendar": "日历", + "KeyboardShortcuts_ActionToggleReadUnread": "切换已读/未读", + "KeyboardShortcuts_ActionToggleFlag": "切换标记", + "KeyboardShortcuts_ActionToggleArchive": "切换存档/取消存档", + "ImageRenderingDisabled": "此邮件的图像渲染已禁用。", + "ImapAdvancedSetupDialog_AuthenticationMethod": "身份验证方法", "ImapAdvancedSetupDialog_ConnectionSecurity": "连接安全性", - "IMAPAdvancedSetupDialog_ValidationAuthMethodRequired": "必须填写授权方式", + "IMAPAdvancedSetupDialog_ValidationAuthMethodRequired": "必须填写验证方式", "IMAPAdvancedSetupDialog_ValidationConnectionSecurityRequired": "必须填写连接安全类型", "IMAPAdvancedSetupDialog_ValidationDisplayNameRequired": "必须填写显示名称", "IMAPAdvancedSetupDialog_ValidationEmailInvalid": "请输入有效的邮箱地址", @@ -262,21 +430,21 @@ "IMAPSetupDialog_AccountType": "账户名称", "IMAPSetupDialog_ValidationSuccess_Title": "成功", "IMAPSetupDialog_ValidationSuccess_Message": "验证成功", - "IMAPSetupDialog_SaveImapSuccess_Title": "成功", - "IMAPSetupDialog_SaveImapSuccess_Message": "IMAP 设置保存成功。", + "IMAPSetupDialog_SaveImapSuccess_Title": "Success", + "IMAPSetupDialog_SaveImapSuccess_Message": "IMAP settings saved successfuly.", "IMAPSetupDialog_ValidationFailed_Title": "IMAP 服务器验证失败。", "IMAPSetupDialog_CertificateAllowanceRequired_Row0": "该服务器正在请求 SSL 握手以继续。请确认下方的证书详情。", - "IMAPSetupDialog_CertificateAllowanceRequired_Row1": "允许握手以继续设置你的账户。", + "IMAPSetupDialog_CertificateAllowanceRequired_Row1": "允许握手并继续设置账户。", "IMAPSetupDialog_CertificateDenied": "用户未授权使用该证书握手。", "IMAPSetupDialog_CertificateIssuer": "颁发者", "IMAPSetupDialog_CertificateSubject": "主题", - "IMAPSetupDialog_CertificateValidFrom": "有效起始于", - "IMAPSetupDialog_CertificateValidTo": "有效截止于", + "IMAPSetupDialog_CertificateValidFrom": "有效日期起始于", + "IMAPSetupDialog_CertificateValidTo": "有效日期截止于", "IMAPSetupDialog_CertificateView": "查看证书", - "IMAPSetupDialog_ConnectionFailedMessage": "IMAP 连接失败。", + "IMAPSetupDialog_ConnectionFailedMessage": "IMAP 连接失败", "IMAPSetupDialog_ConnectionFailedTitle": "连接失败", "IMAPSetupDialog_DisplayName": "显示名称", - "IMAPSetupDialog_DisplayNamePlaceholder": "例如:John Doe", + "IMAPSetupDialog_DisplayNamePlaceholder": "例如 John Doe", "IMAPSetupDialog_IncomingMailServer": "收件邮件服务器", "IMAPSetupDialog_IncomingMailServerPort": "端口", "IMAPSetupDialog_IMAPSettings": "IMAP 服务器设置", @@ -286,21 +454,67 @@ "IMAPSetupDialog_OutgoingMailServer": "发送 (SMTP) 邮件服务器", "IMAPSetupDialog_OutgoingMailServerPassword": "发送服务器密码", "IMAPSetupDialog_OutgoingMailServerPort": "端口", - "IMAPSetupDialog_OutgoingMailServerRequireAuthentication": "发送服务器需要授权", + "IMAPSetupDialog_OutgoingMailServerRequireAuthentication": "发送服务器需要身份验证", "IMAPSetupDialog_OutgoingMailServerUsername": "发送服务器用户名", "IMAPSetupDialog_Password": "密码", "IMAPSetupDialog_RequireSSLForIncomingMail": "接收邮件需要 SSL", "IMAPSetupDialog_RequireSSLForOutgoingMail": "接收邮件需要 SSL", - "IMAPSetupDialog_Title": "高级 IMAP 选项", + "IMAPSetupDialog_Title": "IMAP 高级选项", "IMAPSetupDialog_Username": "用户名", "IMAPSetupDialog_UsernamePlaceholder": "示例:johndoe, johndoe@fabrikam.com, domain/johndoe", - "IMAPSetupDialog_UseSameConfig": "使用同一用户名和密码发送电子邮件", + "IMAPSetupDialog_UseSameConfig": "发送电子邮件时使用同一用户名和密码", + "ImapCalDavSettingsPage_TitleCreate": "IMAP 与日历设置", + "ImapCalDavSettingsPage_TitleEdit": "编辑 IMAP 与日历设置", + "ImapCalDavSettingsPage_Subtitle": "为此账户配置 IMAP/SMTP 及可选的日历同步。", + "ImapCalDavSettingsPage_BasicSectionTitle": "基础设置", + "ImapCalDavSettingsPage_BasicSectionDescription": "输入您的身份信息和凭据。Wino 可以尝试自动检测服务器设置。", + "ImapCalDavSettingsPage_BasicTab": "基础", + "ImapCalDavSettingsPage_EnableCalendarSupport": "启用日历支持", + "ImapCalDavSettingsPage_AutoDiscoverButton": "自动发现邮件设置", + "ImapCalDavSettingsPage_AutoDiscoverySuccessMessage": "邮件设置已发现并应用。", + "ImapCalDavSettingsPage_AdvancedSectionTitle": "高级配置", + "ImapCalDavSettingsPage_AdvancedSectionDescription": "如果自动发现不可用或不正确,请手动输入服务器设置。", + "ImapCalDavSettingsPage_AdvancedTab": "高级", + "ImapCalDavSettingsPage_CalendarSectionTitle": "日历设置", + "ImapCalDavSettingsPage_CalendarSectionDescription": "选择日历数据在此 IMAP 账户中的工作方式。", + "ImapCalDavSettingsPage_CalendarModeHeader": "日历模式", + "ImapCalDavSettingsPage_ConnectionSecurityHeader": "连接安全", + "ImapCalDavSettingsPage_AuthenticationMethodHeader": "身份验证方法", + "ImapCalDavSettingsPage_CalendarModeDisabled": "已禁用", + "ImapCalDavSettingsPage_CalendarModeCalDav": "CalDAV 同步", + "ImapCalDavSettingsPage_CalendarModeLocalOnly": "仅本地日历", + "ImapCalDavSettingsPage_CalendarModeDisabledDescription": "此账户的日历已被禁用。", + "ImapCalDavSettingsPage_CalendarModeCalDavDescription": "日历条目将与您的 CalDAV 服务器同步。", + "ImapCalDavSettingsPage_CalendarModeLocalOnlyDescription": "日历条目仅存储在此计算机上,不会同步到网络。", + "ImapCalDavSettingsPage_LocalCalendarLearnMore": "本地日历如何工作", + "ImapCalDavSettingsPage_LocalCalendarDialogTitle": "仅本地日历", + "ImapCalDavSettingsPage_LocalCalendarDialogMessage": "本地日历将所有事件仅保存在您的计算机上。不会同步到 iCloud、Yahoo 或任何其他提供商。", + "ImapCalDavSettingsPage_CalDavServiceUrl": "CalDAV 服务 URL", + "ImapCalDavSettingsPage_CalDavUsername": "CalDAV 用户名", + "ImapCalDavSettingsPage_CalDavPassword": "CalDAV 密码", + "ImapCalDavSettingsPage_CalDavNotRequiredMessage": "仅在日历模式设置为 CalDAV 同步时才需要 CalDAV 测试。", + "ImapCalDavSettingsPage_CalDavUrlRequired": "需要 CalDAV 服务 URL。", + "ImapCalDavSettingsPage_CalDavUrlInvalid": "CalDAV 服务 URL 必须是一个绝对 URL。", + "ImapCalDavSettingsPage_CalDavUsernameRequired": "CalDAV 用户名是必填项。", + "ImapCalDavSettingsPage_CalDavPasswordRequired": "CalDAV 密码是必填项。", + "ImapCalDavSettingsPage_TestImapButton": "测试 IMAP 连接", + "ImapCalDavSettingsPage_TestCalDavButton": "测试 CalDAV 连接", + "ImapCalDavSettingsPage_ImapTestSuccessMessage": "IMAP 连接测试成功。", + "ImapCalDavSettingsPage_CalDavTestSuccessMessage": "CalDAV 连接测试成功。", + "ImapCalDavSettingsPage_SaveSuccessMessage": "账户设置已验证并保存。", + "ImapCalDavSettingsPage_ICloudHint": "使用在 Apple ID 设置中生成的应用专用密码。", + "ImapCalDavSettingsPage_YahooHint": "使用 Yahoo 账户安全设置中的应用密码。", "Info_AccountCreatedMessage": "{0} 已创建", "Info_AccountCreatedTitle": "创建账户", "Info_AccountCreationFailedTitle": "账户创建失败", "Info_AccountDeletedMessage": "{0} 已成功删除。", "Info_AccountDeletedTitle": "账户已删除", "Info_AccountIssueFixFailedTitle": "失败", + "Info_AccountIssueFixImapMessage": "打开 IMAP 和日历设置页面,重新输入您的服务器凭据。", + "Info_AccountAttentionRequiredMessage": "此账户需要您的关注。", + "Info_AccountAttentionRequiredClickableMessage": "点击以修复此账户并重新同步。", + "Info_AccountAttentionRequiredAction": "修复", + "Info_AccountAttentionRequiredActionHint": "点击修复以解决此账户问题。", "Info_AccountIssueFixSuccessMessage": "已修复所有账户问题。", "Info_AccountIssueFixSuccessTitle": "成功", "Info_AttachmentOpenFailedMessage": "无法打开此附件。", @@ -309,11 +523,11 @@ "Info_AttachmentSaveFailedTitle": "失败", "Info_AttachmentSaveSuccessMessage": "附件已保存。", "Info_AttachmentSaveSuccessTitle": "附件已保存", - "Info_BackgroundExecutionDeniedMessage": "应用的后台运行被拒绝。这可能会影响后台同步和实时通知。", + "Info_BackgroundExecutionDeniedMessage": "应用的后台运行被禁止。这可能会影响后台同步和实时通知。", "Info_BackgroundExecutionDeniedTitle": "后台执行被拒绝", "Info_BackgroundExecutionUnknownErrorMessage": "注册后台同步器时发生未知异常。", "Info_BackgroundExecutionUnknownErrorTitle": "后台执行失败", - "Info_CantDeletePrimaryAliasMessage": "主要别名无法被删除。请在删除前修改别名", + "Info_CantDeletePrimaryAliasMessage": "主要别名无法被删除。请在删除前修改别名。", "Info_ComposerMissingMIMEMessage": "无法找到 MIME 文件。同步可能有帮助。", "Info_ComposerMissingMIMETitle": "失败", "Info_ContactExistsMessage": "此联系人已经在收件人列表中。", @@ -325,7 +539,7 @@ "Info_FileLaunchFailedTitle": "无法启动文件", "Info_InvalidAddressMessage": "{0} 不是有效的电子邮件地址。", "Info_InvalidAddressTitle": "无效地址", - "Info_InvalidMoveTargetMessage": "你不能将选中的邮件移动到此文件夹。", + "Info_InvalidMoveTargetMessage": "您不能将选中的邮件移动到此文件夹。", "Info_InvalidMoveTargetTitle": "无效的移动目标", "Info_LogsNotFoundMessage": "没有可共享的日志。", "Info_LogsNotFoundTitle": "未找到日志", @@ -338,21 +552,21 @@ "Info_MessageCorruptedTitle": "错误", "Info_MissingFolderMessage": "此账户不存在 {0}。", "Info_MissingFolderTitle": "缺少文件夹", - "Info_PDFSaveFailedTitle": "保存 PDF 文件失败", + "Info_PDFSaveFailedTitle": "保存PDF文件失败", "Info_PDFSaveSuccessMessage": "PDF 文件已保存到 {0}", "Info_PDFSaveSuccessTitle": "成功", "Info_PurchaseExistsMessage": "看起来这个产品已经被购买了。", "Info_PurchaseExistsTitle": "产品已购买", - "Info_PurchaseThankYouMessage": "感谢你", + "Info_PurchaseThankYouMessage": "感谢您", "Info_PurchaseThankYouTitle": "购买成功", "Info_RequestCreationFailedTitle": "请求创建失败", - "Info_ReviewNetworkErrorMessage": "你的评论有一个网络问题。", + "Info_ReviewNetworkErrorMessage": "您的评论有一个网络问题。", "Info_ReviewNetworkErrorTitle": "网络问题", - "Info_ReviewNewMessage": "感谢所有反馈。谢谢你的评论!", + "Info_ReviewNewMessage": "感谢所有反馈。谢谢您的评论!", "Info_ReviewSuccessTitle": "谢谢", - "Info_ReviewUnknownErrorMessage": "你的评论有一个未知的问题。({0})", + "Info_ReviewUnknownErrorMessage": "您的评论有一个未知的问题。({0})", "Info_ReviewUnknownErrorTitle": "未知错误", - "Info_ReviewUpdatedMessage": "感谢你的最新评论。", + "Info_ReviewUpdatedMessage": "感谢您的最新评论。", "Info_SignatureDisabledMessage": "已禁用此账户的签名", "Info_SignatureDisabledTitle": "成功", "Info_SignatureSavedMessage": "已保存新签名", @@ -363,16 +577,17 @@ "Info_UnsubscribeErrorMessage": "取消订阅失败", "Info_UnsubscribeLinkInvalidMessage": "此退订链接无效。取消订阅列表失败。", "Info_UnsubscribeLinkInvalidTitle": "无效的取消订阅 Uri", - "Info_UnsubscribeSuccessMessage": "成功取消了来自 {0} 的订阅。", + "Info_UnsubscribeSuccessMessage": "成功取消了来自 {0} 处的订阅!", "Info_UnsupportedFunctionalityDescription": "此功能尚未实现。", "Info_UnsupportedFunctionalityTitle": "不支持", "InfoBarAction_Enable": "启用", "InfoBarMessage_SynchronizationDisabledFolder": "此文件夹已禁用同步。", "InfoBarTitle_SynchronizationDisabledFolder": "已禁用文件夹", "Justify": "对齐", + "MenuUpdateAvailable": "有更新可用", "Left": "左侧", "Link": "链接", - "LinkedAccountsCreatePolicyMessage": "你必须拥有至少 2 个账户才能创建链接\n链接将在保存时删除", + "LinkedAccountsCreatePolicyMessage": "您必须拥有至少 2 个账户才能创建链接\n链接将在保存时删除", "LinkedAccountsTitle": "已绑定的账户", "MailItemNoSubject": "无主题", "MailOperation_AlwaysMoveFocused": "总是移动到重点。", @@ -386,10 +601,10 @@ "MailOperation_Forward": "转发", "MailOperation_Ignore": "忽略", "MailOperation_LightEditor": "浅色", - "MailOperation_MarkAsJunk": "标记为垃圾", + "MailOperation_MarkAsJunk": "标记为垃圾邮件", "MailOperation_MarkAsRead": "标记为已读", "MailOperation_MarkAsUnread": "标记为未读", - "MailOperation_MarkNotJunk": "标记为非垃圾", + "MailOperation_MarkNotJunk": "标记为非垃圾邮件", "MailOperation_Move": "移动", "MailOperation_MoveFocused": "移动到重点", "MailOperation_MoveJunk": "移至垃圾箱", @@ -403,6 +618,7 @@ "MailOperation_Unarchive": "取消归档", "MailOperation_ViewMessageSource": "查看消息来源", "MailOperation_Zoom": "缩放", + "MailsDragging": "正在拖动 {0} 项", "MailsSelected": "已选择 {0} 项", "MarkFlagUnflag": "标记为已标记/未标记", "MarkReadUnread": "标记为已读/未读", @@ -430,12 +646,14 @@ "NoMailSelected": "未选择任何邮件", "NoMessageCrieteria": "没有符合搜索条件的邮件", "NoMessageEmptyFolder": "此文件夹为空", - "Notifications_MultipleNotificationsMessage": "你有 {0} 条新消息。", + "Notifications_MultipleNotificationsMessage": "您有 {0} 条新消息。", "Notifications_MultipleNotificationsTitle": "新邮件", "Notifications_WinoUpdatedMessage": "检查新版本 {0}", - "Notifications_WinoUpdatedTitle": "Wino Mail 已更新。", + "Notifications_WinoUpdatedTitle": "Wino 邮件已更新。", + "Notifications_StoreUpdateAvailableTitle": "有可用更新", + "Notifications_StoreUpdateAvailableMessage": "较新版本的 Wino Mail 已准备好从 Microsoft Store 安装。", "OnlineSearchFailed_Message": "无法执行搜索\n{0}\n\n列出的是离线邮件。", - "OnlineSearchTry_Line1": "找不到你想要的?", + "OnlineSearchTry_Line1": "还是找不到您想要的?", "OnlineSearchTry_Line2": "请尝试在线搜索。", "Other": "其他", "PaneLengthOption_Default": "默认", @@ -446,8 +664,7 @@ "PaneLengthOption_Small": "小", "Photos": "图片", "PreparingFoldersMessage": "正在准备文件夹", - "ProtocolLogAvailable_Message": "协议诊断日志已可用。", - "ProviderDetail_Gmail_Description": "Google 账户", + "ProviderDetail_Gmail_Description": "Google 账号", "ProviderDetail_iCloud_Description": "Apple iCloud 帐户", "ProviderDetail_iCloud_Title": "iCloud", "ProviderDetail_IMAP_Description": "自定义 IMAP/SMTP 服务器", @@ -465,31 +682,40 @@ "SearchBarPlaceholder": "搜索", "SearchingIn": "搜索于", "SearchPivotName": "结果", + "Settings_KeyboardShortcuts_Title": "快捷键", + "Settings_KeyboardShortcuts_Description": "管理邮件的快捷键以实现快速操作。", "SettingConfigureSpecialFolders_Button": "配置", "SettingsEditAccountDetails_IMAPConfiguration_Title": "IMAP/SMTP 配置", "SettingsEditAccountDetails_IMAPConfiguration_Description": "更改您的收件 / 发件服务器设置。", + "SettingsEditAccountDetails_ImapCalDavSettings_Title": "IMAP 与日历设置", + "SettingsEditAccountDetails_ImapCalDavSettings_Description": "打开此账户的专用 IMAP、SMTP 和 CalDAV 设置页面。", + "SettingsEditAccountDetails_ImapCalDavSettings_Action": "打开设置", "SettingsAbout_Description": "了解更多关于 Wino 的信息。", "SettingsAbout_Title": "关于", - "SettingsAboutGithub_Description": "转到 GitHub 仓库以跟踪议题。", + "SettingsAboutGithub_Description": "转到 GitHub 仓库以跟踪 issues。", "SettingsAboutGithub_Title": "GitHub", "SettingsAboutVersion": "版本 ", "SettingsAboutWinoDescription": "Windows 系列设备的轻量邮件客户端。", "SettingsAccentColor_Description": "更改应用的主题颜色", "SettingsAccentColor_Title": "主题颜色", - "SettingsAccentColor_UseWindowsAccentColor": "使用 Windows 主题颜色", - "SettingsAccountManagementAppendMessage_Description": "在草稿发送后在发送文件夹中创建邮件的副本。 如果你在发送文件夹中没有看到你的邮件,请启用此功能。", + "SettingsAccentColor_UseWindowsAccentColor": "使用 Windows 主题色", + "SettingsAccountManagementAppendMessage_Description": "在草稿发送后在发送文件夹中创建邮件的副本。 如果您在发送文件夹中没有看到您的邮件,请启用此功能。", "SettingsAccountManagementAppendMessage_Title": "添加邮件到已发送文件夹", "SettingsAccountName_Description": "更改账户名称。", "SettingsAccountName_Title": "账户名称", "SettingsApplicationTheme_Description": "根据你的喜好,用不同的自定义应用主题来个性化 Wino。", "SettingsApplicationTheme_Title": "应用主题", - "SettingsAppPreferences_CloseBehavior_Description": "当你关闭应用时应该怎么做?", + "SettingsAppPreferences_CloseBehavior_Description": "当关闭应用时 Wino Mail 应该怎么做?", "SettingsAppPreferences_CloseBehavior_Title": "应用关闭后行为", - "SettingsAppPreferences_Description": "Wino Mail 的常规/首选项设置。", - "SettingsAppPreferences_SearchMode_Description": "设置在搜索时是先检查本地邮件还是请求远程服务器。本地搜索通常更快,而且当本地搜索找不到邮件时,你也可以再进行在线搜索。", + "SettingsAppPreferences_Description": "Wino Mail 的常规 / 首选项设置", + "SettingsAppPreferences_SearchMode_Description": "设置在搜索时是先检查本地邮件还是请求远程服务器。本地搜索通常更快,而且当本地搜索找不到邮件时,您也可以再进行在线搜索。", "SettingsAppPreferences_SearchMode_Local": "本地", "SettingsAppPreferences_SearchMode_Online": "在线", "SettingsAppPreferences_SearchMode_Title": "默认搜索模式", + "SettingsAppPreferences_ApplicationMode_Title": "默认应用模式", + "SettingsAppPreferences_ApplicationMode_Description": "当未显式设置激活类型时,选择 Wino 打开的默认模式。", + "SettingsAppPreferences_ApplicationMode_Mail": "邮件", + "SettingsAppPreferences_ApplicationMode_Calendar": "日历", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description": "Wino Mail 将在后台运行。当收到新邮件时将推送通知。", "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title": "后台运行", "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description": "Wino Mail 将在系统托盘上继续运行。点击托盘图标时将启动应用。当收到新邮件时将推送通知。", @@ -498,31 +724,49 @@ "SettingsAppPreferences_ServerBackgroundingMode_Terminate_Title": "终止进程", "SettingsAppPreferences_StartupBehavior_Description": "允许 Wino Mail 随 Windows 最小化自启动。总是允许接收应用通知。", "SettingsAppPreferences_StartupBehavior_Disable": "禁用", - "SettingsAppPreferences_StartupBehavior_Disabled": "Wino Mail 将不会随 Windows 启动时自启。可能会导致重启系统后错过邮件通知。", + "SettingsAppPreferences_StartupBehavior_Disabled": "Wino Mail 将不会随 Windows 自动。可能会导致重启系统后错过邮件通知。", "SettingsAppPreferences_StartupBehavior_DisabledByPolicy": "管理员或组策略禁止了应用随 Windows 自启动,因此 Wino Mail 无法随 Windows 自启动。", - "SettingsAppPreferences_StartupBehavior_DisabledByUser": "请前往「应用」->「启动」以允许 Wino Mail 随 Windows 自启动。", + "SettingsAppPreferences_StartupBehavior_DisabledByUser": "请前往「任务管理器 」-「启动应用」标签以允许 Wino Mail 随 Windows 自启动。", "SettingsAppPreferences_StartupBehavior_Enable": "启用", - "SettingsAppPreferences_StartupBehavior_Enabled": "Wino Mail 成功设置为随 Windows 自启动。", + "SettingsAppPreferences_StartupBehavior_Enabled": "Wino 成功设置为随 Windows 自启动。", "SettingsAppPreferences_StartupBehavior_FatalError": "更改 Wino Mail 启动模式发生严重错误。", "SettingsAppPreferences_StartupBehavior_Title": "随 Windows 启动并最小化应用", "SettingsAppPreferences_Title": "应用设置", + "SettingsAppPreferences_HideWinoAccountButton_Title": "在标题栏隐藏 Wino 账户按钮", + "SettingsAppPreferences_HideWinoAccountButton_Description": "隐藏在标题栏中用于打开 Wino 账户弹出菜单的个人资料按钮。", + "SettingsAppPreferences_StoreUpdateNotifications_Title": "商店更新通知", + "SettingsAppPreferences_StoreUpdateNotifications_Description": "在 Microsoft Store 有更新时显示通知和页脚操作。", + "SettingsAppPreferences_AiActions_Title": "AI 操作", + "SettingsAppPreferences_AiActions_Description": "选择默认 AI 语言及摘要应保存的位置。", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Title": "默认翻译语言", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Description": "选择 AI 翻译操作使用的默认目标语言。", + "SettingsAppPreferences_AiSummarizeLanguage_Title": "摘要语言", + "SettingsAppPreferences_AiSummarizeLanguage_Description": "选择未来 AI 摘要输出的首选摘要语言。", + "SettingsAppPreferences_AiSummarySavePath_Title": "默认摘要保存路径", + "SettingsAppPreferences_AiSummarySavePath_Description": "选择 Wino 保存 AI 摘要时应使用的默认文件夹。", + "SettingsAppPreferences_AiSummarySavePath_Placeholder": "使用系统默认的保存位置", + "SettingsAppPreferences_AiSummarySavePath_InvalidHint": "此文件夹不存在。将使用默认摘要保存位置。", "SettingsAutoSelectNextItem_Description": "选择删除或移动邮件后的下一个项目。", "SettingsAutoSelectNextItem_Title": "自动选择下一个项目", - "SettingsAvailableThemes_Description": "从 Wino 的自带收藏中选择一个符合你口味的主题,或应用你自己的主题。", - "SettingsAvailableThemes_Title": "可用主题", + "SettingsAvailableThemes_Description": "从 Wino 的自带收藏中选择一个符合您口味的主题,或应用您自己的主题。", + "SettingsAvailableThemes_Title": "可用的主题", "SettingsCalendarSettings_Description": "更改一周的第一天、小时单元格高度,等等…", "SettingsCalendarSettings_Title": "日历设置", + "CalendarSettings_DefaultSnoozeDuration_Header": "默认延迟提醒时长", + "CalendarSettings_DefaultSnoozeDuration_Description": "为日历提醒通知设置默认延迟时长。", + "CalendarSettings_TimedDayHeaderFormat_Header": "带时间的日视图头部格式", + "CalendarSettings_TimedDayHeaderFormat_Description": "选择在日、周和工作周视图中顶部日期标签的呈现方式。使用日期格式标记,如 ddd、dd、MMM 或 dddd。", "SettingsComposer_Title": "编辑器", - "SettingsComposerFont_Title": "默认编辑字体", - "SettingsComposerFontFamily_Description": "更改编辑邮件时的默认字体和字体大小。", - "SettingsConfigureSpecialFolders_Description": "设置具有特殊功能的文件夹。如收件箱、草稿箱、归档等文件夹对于 Wino 正常运行是必要的。", + "SettingsComposerFont_Title": "默认撰写字体", + "SettingsComposerFontFamily_Description": "更改撰写邮件时的默认字体和字体大小。", + "SettingsConfigureSpecialFolders_Description": "设置具有特殊功能的文件夹。如收件箱、草稿箱、存档等文件夹,对于Wino正常运行是至关重要的。", "SettingsConfigureSpecialFolders_Title": "配置系统文件夹", - "SettingsCustomTheme_Description": "用自定义壁纸和主题颜色创建你自己定义的主题。", + "SettingsCustomTheme_Description": "用自定义壁纸和主题色创建您自己的自定义主题。", "SettingsCustomTheme_Title": "自定义主题", "SettingsDeleteAccount_Description": "删除与此账户关联的所有电子邮件和凭据。", "SettingsDeleteAccount_Title": "删除此账户", - "SettingsDeleteProtection_Description": "每次你尝试使用 Shift + Del 永久删除邮件时,Wino 应该向你确认吗?", - "SettingsDeleteProtection_Title": "永久删除保护", + "SettingsDeleteProtection_Description": "Should Wino ask you for confirmation every time you try to permanently delete a mail using Shift + Del keys?", + "SettingsDeleteProtection_Title": "永久性删除保护", "SettingsDiagnostics_Description": "开发者选项", "SettingsDiagnostics_DiagnosticId_Description": "如需联系开发人员请求帮助,请提供此 ID。", "SettingsDiagnostics_DiagnosticId_Title": "诊断 ID", @@ -531,22 +775,25 @@ "SettingsDiscord_Title": "Discord 频道", "SettingsEditLinkedInbox_Description": "添加/删除账户,重命名或断开账户之间的链接。", "SettingsEditLinkedInbox_Title": "编辑已链接收件箱", + "SettingsWindowBackdrop_Title": "窗口背景", + "SettingsWindowBackdrop_Description": "为 Wino 窗口选择背景效果。", + "SettingsWindowBackdrop_Disabled": "当应用主题不是默认时,窗口背景选择被禁用。", "SettingsElementTheme_Description": "为 Wino 选择一个 Windows 主题", "SettingsElementTheme_Title": "元素主题", "SettingsElementThemeSelectionDisabled": "当选择的应用主题不是默认主题时,元素主题选择将被禁用。", "SettingsEnableHoverActions_Title": "启用悬停动作", "SettingsEnableIMAPLogs_Description": "启用此选项可提供在设置 IMAP 服务器过程中遇到的 IMAP 连接问题的详细信息。", "SettingsEnableIMAPLogs_Title": "启用 IMAP 协议日志", - "SettingsEnableLogs_Description": "我可能需要日志来分析你在 GitHub 中打开的问题。 所有日志都不会公开你的凭据或敏感信息。", + "SettingsEnableLogs_Description": "我可能需要日志来分析您在 GitHub 中打开的问题。 所有日志都不会公开您的凭据或敏感信息。", "SettingsEnableLogs_Title": "启用日志", "SettingsEnableSignature": "启用签名", "SettingsExpandOnStartup_Description": "设置 Wino 是否应该在启动时展开此账户的文件夹。", "SettingsExpandOnStartup_Title": "启动时展开菜单", "SettingsExternalContent_Description": "在渲染邮件时管理外部内容设置。", "SettingsExternalContent_Title": "外部内容", - "SettingsFocusedInbox_Description": "设置是否应将收件箱分为重点和其他两部分 。", + "SettingsFocusedInbox_Description": "设置是否应将收件箱分为两部分 - 重点和其他。", "SettingsFocusedInbox_Title": "重点收件箱", - "SettingsFolderMenuStyle_Description": "控制账户文件夹是否应嵌套在账户菜单中。 如果你喜欢 Windows 邮件中的旧菜单,关闭此选项", + "SettingsFolderMenuStyle_Description": "控制账户文件夹是否应嵌套在账户菜单中。 如果您喜欢Windows邮件中的旧菜单,关闭此选项", "SettingsFolderMenuStyle_Title": "创建内部文件夹", "SettingsFolderOptions_Description": "更改个人文件夹设置,如启用/禁用同步或显示/隐藏未读标志。", "SettingsFolderOptions_Title": "文件夹设置", @@ -554,7 +801,7 @@ "SettingsFolderSync_Title": "文件夹同步", "SettingsFontFamily_Title": "字体", "SettingsFontPreview_Title": "预览", - "SettingsFontSize_Title": "文字大小", + "SettingsFontSize_Title": "字体大小", "SettingsHoverActionCenter": "中心动作", "SettingsHoverActionLeft": "左侧动作", "SettingsHoverActionRight": "右侧动作", @@ -562,14 +809,14 @@ "SettingsHoverActions_Title": "悬停动作", "SettingsLanguage_Description": "更改 Wino 的显示语言。", "SettingsLanguage_Title": "显示语言", - "SettingsLanguageTime_Description": "Wino 显示语言,首选时间格式。", + "SettingsLanguageTime_Description": "Wino 显示语言,首选时间格式", "SettingsLanguageTime_Title": "语言和时间", - "SettingsLinkAccounts_Description": "合并多个账户为一个。在同一个收件箱内查看邮件。", + "SettingsLinkAccounts_Description": "将多个账户合并为一个。在同一个收件箱内查看所有邮件。", "SettingsLinkAccounts_Title": "创建链接账户", "SettingsLinkedAccountsSave_Description": "修改当前与新账户的链接。", "SettingsLinkedAccountsSave_Title": "保存更改", "SettingsLoadImages_Title": "自动加载图片", - "SettingsLoadPlaintextLinks_Title": "转换纯文本链接为可点击链接", + "SettingsLoadPlaintextLinks_Title": "将纯文本链接转换为可点击链接", "SettingsLoadStyles_Title": "自动加载样式", "SettingsMailListActionBar_Description": "隐藏/显示消息列表顶部操作栏。", "SettingsMailListActionBar_Title": "显示邮件列表操作", @@ -577,10 +824,12 @@ "SettingsMailSpacing_Title": "邮件间距", "SettingsManageAccountSettings_Description": "每个账户的通知、签名、同步等设置。", "SettingsManageAccountSettings_Title": "管理账户设置", - "SettingsManageAliases_Description": "查看分配给该账户的电子邮箱地址,更新或删除别名。", + "SettingsManageAliases_Description": "查看分配给该账户的电子邮箱地址,可更新或删除别名。", "SettingsManageAliases_Title": "别名", "SettingsEditAccountDetails_Title": "编辑账户信息", - "SettingsEditAccountDetails_Description": "你可以按喜好调整账户名称、发件人名称和账户颜色。", + "SettingsEditAccountDetails_Description": "您可以按喜好调整账户名称、发件人名称和账户颜色。", + "EditAccountDetailsPage_SaveSuccess_Title": "已保存更改", + "EditAccountDetailsPage_SaveSuccess_Message": "您的账户详细信息已成功更新。", "SettingsManageLink_Description": "移动项目以添加新链接或删除现有链接。", "SettingsManageLink_Title": "管理账户链接", "SettingsMarkAsRead_Description": "更改应对选定项目进行何种操作。", @@ -590,46 +839,95 @@ "SettingsMarkAsRead_Title": "标记为已读", "SettingsMarkAsRead_WhenSelected": "选中时", "SettingsMessageList_Description": "更改邮件列表中的消息应如何组织。", - "SettingsMessageList_Title": "消息列表", - "SettingsNoAccountSetupMessage": "你尚未设置任何账户。", + "SettingsMessageList_Title": "邮件列表", + "SettingsNoAccountSetupMessage": "您尚未设置任何账户。", "SettingsNotifications_Description": "开启或关闭此账户的通知。", "SettingsNotifications_Title": "通知", "SettingsNotificationsAndTaskbar_Description": "更改是否应显示此帐户的通知和任务栏徽标。", "SettingsNotificationsAndTaskbar_Title": "通知与任务栏", + "SettingsHome_Title": "首页", + "SettingsHome_SearchTitle": "查找设置", + "SettingsHome_SearchDescription": "按功能、主题或关键词进行搜索,直接跳转到正确的设置页面。", + "SettingsHome_SearchPlaceholder": "搜索设置", + "SettingsHome_SearchExamples": "示例:主题、存储、语言、签名", + "SettingsHome_QuickLinks_Title": "快速链接", + "SettingsHome_QuickLinks_Description": "直达用户最常用的设置。", + "SettingsHome_StorageCard_Description": "查看 Wino 在此设备上保留的本地 MIME 内容量,并在需要时清理。", + "SettingsHome_StorageEmptySummary": "尚未检测到缓存的 MIME 内容。", + "SettingsHome_StorageLoading": "正在检查本地 MIME 使用情况...", + "SettingsHome_Tips_Title": "提示与技巧", + "SettingsHome_Tips_Description": "一些小改动就能让 Wino 更具个性。", + "SettingsHome_Tip_Theme": "想要暗黑模式或强调色变更?打开个性化设置。", + "SettingsHome_Tip_Background": "使用应用偏好设置来控制启动行为和后台同步。", + "SettingsHome_Tip_Shortcuts": "键盘快捷键可帮助你更快地浏览邮件。", + "SettingsHome_Resources_Title": "有用的链接", + "SettingsHome_Resources_Description": "打开项目资源、支持信息和发布通道。", "SettingsOptions_Title": "设置", - "SettingsPaneLengthReset_Description": "如果你遇到问题,可以将邮件列表重置至原始大小。", + "SettingsOptions_GeneralSection": "常规", + "SettingsOptions_MailSection": "邮件", + "SettingsOptions_CalendarSection": "日历", + "SettingsOptions_MoreComingSoon": "更多选项敬请期待", + "SettingsOptions_HeroDescription": "定制您的 Wino Mail 体验", + "SettingsOptions_AccountsSummary": "{0} 个账户已配置", + "SettingsSearch_ManageAccounts_Keywords": "账户;账户;邮箱;邮箱;别名;别名;个人资料;地址;地址", + "SettingsSearch_AppPreferences_Keywords": "启动;后台;启动;同步;通知;通知;搜索;托盘;默认值", + "SettingsSearch_LanguageTime_Keywords": "语言;时间;时钟;区域设置;区域;格式;24 小时制;24h", + "SettingsSearch_Personalization_Keywords": "主题;深色;浅色;外观;强调色;颜色;颜色;模式;布局;密度", + "SettingsSearch_About_Keywords": "关于;版本;网站;隐私;GitHub;捐赠;商店;支持", + "SettingsSearch_KeyboardShortcuts_Keywords": "快捷键;快捷键;热键;热键;键盘;按键", + "SettingsSearch_MessageList_Keywords": "消息;消息;列表;线程;线程;头像;预览;发件人", + "SettingsSearch_ReadComposePane_Keywords": "阅读器;撰写;撰写器;字体;字体;外部内容;显示;阅读", + "SettingsSearch_SignatureAndEncryption_Keywords": "签名;签名;加密;证书;证书;S/MIME;S/MIME;安全", + "SettingsSearch_Storage_Keywords": "存储;缓存;缓存;MIME;磁盘;空间;清理;清理;本地数据", + "SettingsSearch_CalendarSettings_Keywords": "日历;周;时;日程;事件;事件", + "SettingsPaneLengthReset_Description": "如果遇到问题,可以将邮件列表重置至原始大小。", "SettingsPaneLengthReset_Title": "重置邮件列表大小", "SettingsPaypal_Description": "展示更多的爱吧 ❤️ 我们感激所有的捐赠。", "SettingsPaypal_Title": "通过 PayPal 捐赠", - "SettingsPersonalization_Description": "按照你喜欢的方式改变 Wino 的外观。", + "SettingsPersonalization_Description": "按照您喜欢的方式更改 Wino 的外观。", "SettingsPersonalization_Title": "个性化", "SettingsPersonalizationMailDisplayCompactMode": "紧凑模式", "SettingsPersonalizationMailDisplayMediumMode": "中等模式", "SettingsPersonalizationMailDisplaySpaciousMode": "宽敞模式", "SettingsPrefer24HourClock_Description": "邮件接收时间将显示为 24 小时制,而不是 12 小时制(上午/下午)", - "SettingsPrefer24HourClock_Title": "使用 24 小时制时钟格式", + "SettingsPrefer24HourClock_Title": "使用 24 小时制时间格式", "SettingsPrivacyPolicy_Description": "查看隐私政策。", "SettingsPrivacyPolicy_Title": "隐私政策", + "SettingsWebsite_Description": "打开 Wino Mail 网站。", + "SettingsWebsite_Title": "网站", "SettingsReadComposePane_Description": "字体与外部内容。", "SettingsReadComposePane_Title": "阅读器和编辑器", "SettingsReader_Title": "阅读器", "SettingsReaderFont_Title": "默认阅读字体", - "SettingsReaderFontFamily_Description": "更改阅读邮件的默认字体和文字大小。", + "SettingsReaderFontFamily_Description": "更改阅读邮件的默认字体和字体大小。", "SettingsRenameMergeAccount_Description": "更改链接账户的显示名称。", "SettingsRenameMergeAccount_Title": "重命名", - "SettingsReorderAccounts_Description": "改变账户在列表中的顺序。", - "SettingsReorderAccounts_Title": "重新排列账账户", - "SettingsSemanticZoom_Description": "这将允许你点击消息列表中的标题并转到指定日期", + "SettingsReorderAccounts_Description": "改变账号在账号列表中的顺序。", + "SettingsReorderAccounts_Title": "重新排列账号", + "SettingsSemanticZoom_Description": "这将允许您点击消息列表中的标题并转到指定日期", "SettingsSemanticZoom_Title": "为日期标题启用语义缩放", "SettingsShowPreviewText_Description": "隐藏/显示预览文本。", "SettingsShowPreviewText_Title": "显示预览文本", "SettingsShowSenderPictures_Description": "隐藏/显示缩略图发件人图片。", "SettingsShowSenderPictures_Title": "显示发件人头像", - "SettingsEnableGravatarAvatars_Title": "Gravator 头像", - "SettingsEnableGravatarAvatars_Description": "使用 Gravatar (如果可用) 作为发件人图片", - "SettingsEnableFavicons_Title": "域图标 (Favicons)", - "SettingsEnableFavicons_Description": "使用域图标 (Favicons) 作为发件人图片", - "SettingsMailList_ClearAvatarsCache_Button": "清除缓存的头像", + "SettingsEmailTemplates_Title": "邮件模板", + "SettingsEmailTemplates_Description": "管理电子邮件模板", + "SettingsEmailTemplates_CreatePageTitle": "新模板", + "SettingsEmailTemplates_EditPageTitle": "编辑模板", + "SettingsEmailTemplates_NewTemplateTitle": "新模板", + "SettingsEmailTemplates_NewTemplateDescription": "创建一个新的电子邮件模板", + "SettingsEmailTemplates_NameTitle": "名称", + "SettingsEmailTemplates_NamePlaceholder": "模板名称", + "SettingsEmailTemplates_DescriptionTitle": "描述", + "SettingsEmailTemplates_DescriptionPlaceholder": "可选描述", + "SettingsEmailTemplates_ContentTitle": "模板内容", + "SettingsEmailTemplates_ContentDescription": "编辑此模板的 HTML 内容。", + "SettingsEmailTemplates_NameRequired": "模板名称是必填项。", + "SettingsEnableGravatarAvatars_Title": "Gravatar", + "SettingsEnableGravatarAvatars_Description": "Use gravatar (if available) as sender picture", + "SettingsEnableFavicons_Title": "Domain icons (Favicons)", + "SettingsEnableFavicons_Description": "Use domain favicons (if available) as sender picture", + "SettingsMailList_ClearAvatarsCache_Button": "Clear cached avatars", "SettingsSignature_AddCustomSignature_Button": "添加签名", "SettingsSignature_AddCustomSignature_Title": "添加自定义签名", "SettingsSignature_DeleteSignature_Title": "删除签名", @@ -645,44 +943,74 @@ "SettingsStartupItem_Title": "启动项", "SettingsStore_Description": "展示更多的爱❤️", "SettingsStore_Title": "在商店中评分", + "SettingsStorage_Title": "存储", + "SettingsStorage_Description": "在本地数据文件夹中扫描并管理 MIME 缓存。", + "SettingsStorage_ScanFolder": "扫描本地数据文件夹", + "SettingsStorage_NoLocalMimeDataFound": "未找到本地 MIME 数据。", + "SettingsStorage_NoAccountsFound": "未找到账户。", + "SettingsStorage_TotalUsage": "本地 MIME 总使用量:{0}", + "SettingsStorage_AccountUsageDescription": "{0} 已用于本地 MIME 缓存", + "SettingsStorage_DeleteAll_Title": "删除所有 MIME 内容", + "SettingsStorage_DeleteAll_Description": "删除此账户的整个 MIME 缓存文件夹。", + "SettingsStorage_DeleteAll_Button": "删除所有", + "SettingsStorage_DeleteAll_Confirm_Title": "删除所有 MIME 内容", + "SettingsStorage_DeleteAll_Confirm_Message": "是否删除 {0} 的本地 MIME 数据?", + "SettingsStorage_DeleteAll_Success": "所有 MIME 内容已被删除。", + "SettingsStorage_DeleteOld_Title": "删除旧的 MIME 内容", + "SettingsStorage_DeleteOld_Description": "根据本地数据库中的邮件创建日期删除 MIME 文件。", + "SettingsStorage_DeleteOld_1Month": "> 1 个月", + "SettingsStorage_DeleteOld_3Months": "> 3 个月", + "SettingsStorage_DeleteOld_6Months": "> 6 个月", + "SettingsStorage_DeleteOld_1Year": "> 1 年", + "SettingsStorage_DeleteOld_Confirm_Title": "删除旧的 MIME 内容", + "SettingsStorage_DeleteOld_Confirm_Message": "是否删除 {1} 的本地 MIME 数据,且早于 {0}?", + "SettingsStorage_DeleteOld_Success": "已删除 {0} 个早于 {1} 的 MIME 文件夹。", + "SettingsStorage_1Month": "1 个月", + "SettingsStorage_3Months": "3 个月", + "SettingsStorage_6Months": "6 个月", + "SettingsStorage_1Year": "1 年", + "SettingsStorage_Months": "{0} 个月", "SettingsTaskbarBadge_Description": "在任务栏图标中包含未读邮件数量。", "SettingsTaskbarBadge_Title": "任务栏徽标", - "SettingsThreads_Description": "将邮件组织成对话线。", - "SettingsThreads_Title": "线性对话", - "SettingsUnlinkAccounts_Description": "删除账户之间的链接。这不会删除你的账户。", + "SettingsThreads_Description": "将邮件组织成对话主题。", + "SettingsThreads_Title": "邮件会话", + "SettingsUnlinkAccounts_Description": "删除账户之间的链接。这不会删除您的账户。", "SettingsUnlinkAccounts_Title": "取消链接账户", - "SettingsMailRendering_ActionLabels_Title": "活动标签", - "SettingsMailRendering_ActionLabels_Description": "显示活动标签。", + "SettingsMailRendering_ActionLabels_Title": "Action labels", + "SettingsMailRendering_ActionLabels_Description": "Show action labels.", "SignatureDeleteDialog_Message": "确定要删除签名「{0}」吗?", "SignatureDeleteDialog_Title": "删除签名", - "SignatureEditorDialog_SignatureName_Placeholder": "命名签名", + "SignatureEditorDialog_SignatureName_Placeholder": "为签名命名", "SignatureEditorDialog_SignatureName_TitleEdit": "当前签名:{0}", "SignatureEditorDialog_SignatureName_TitleNew": "签名名称", "SignatureEditorDialog_Title": "签名编辑器", "SortingOption_Date": "按日期", "SortingOption_Name": "按名称", - "StoreRatingDialog_MessageFirstLine": "所有反馈都值得赞赏,它们将来会使 Wino 变得更好。你想要在 Microsoft Store 给 Wino 打分吗?", - "StoreRatingDialog_MessageSecondLine": "你想在 Microsoft Store 中给 Wino Mail 打分吗?", + "StoreRatingDialog_MessageFirstLine": "所有反馈都值得赞赏,它们将来会使 Wino 变得更好。您想要在 Microsoft Store 给 Wino 打分吗?", + "StoreRatingDialog_MessageSecondLine": "您想在 Microsoft Store 中给 Wino Mail 打分吗?", "StoreRatingDialog_Title": "喜欢 Wino 吗?", "SynchronizationFolderReport_Failed": "同步失败", "SynchronizationFolderReport_Success": "已是最新", - "SystemFolderConfigDialog_ArchiveFolderDescription": "已归档的邮件将被移至此处。", + "SystemFolderConfigDialog_ArchiveFolderDescription": "已存档的邮件将被移至此处。", "SystemFolderConfigDialog_ArchiveFolderHeader": "归档文件夹", "SystemFolderConfigDialog_DeletedFolderDescription": "已删除的邮件将被移至此处。", "SystemFolderConfigDialog_DeletedFolderHeader": "已删除文件夹", "SystemFolderConfigDialog_DraftFolderDescription": "将在这里撰写新邮件/回复。", "SystemFolderConfigDialog_DraftFolderHeader": "草稿文件夹", "SystemFolderConfigDialog_JunkFolderDescription": "所有垃圾邮件都在这里。", - "SystemFolderConfigDialog_JunkFolderHeader": "垃圾文件夹", + "SystemFolderConfigDialog_JunkFolderHeader": "垃圾邮件文件夹", "SystemFolderConfigDialog_MessageFirstLine": "此 IMAP 服务器不支持 SPECIAL-USE 扩展,因此 Wino 无法正确设置系统文件夹。", "SystemFolderConfigDialog_MessageSecondLine": "请为特定功能选择相应的文件夹。", "SystemFolderConfigDialog_SentFolderDescription": "发送的邮件将被存储在该文件夹中。", "SystemFolderConfigDialog_SentFolderHeader": "已发送文件夹", "SystemFolderConfigDialog_Title": "配置系统文件夹", "SystemFolderConfigDialogValidation_DuplicateSystemFolders": "某些系统文件夹在配置中不止一次被使用。", - "SystemFolderConfigDialogValidation_InboxSelected": "你不能将收件箱设置为任何其他系统文件夹。", + "SystemFolderConfigDialogValidation_InboxSelected": "您不能将收件箱设置为任何其他系统文件夹。", "SystemFolderConfigSetupSuccess_Message": "系统文件夹配置成功。", "SystemFolderConfigSetupSuccess_Title": "系统文件夹设置", + "SystemTrayMenu_ShowWino": "打开 Wino 邮件", + "SystemTrayMenu_ShowWinoCalendar": "打开 Wino 日历", + "SystemTrayMenu_ExitWino": "退出", "TestingImapConnectionMessage": "正在测试服务器连接...", "TitleBarServerDisconnectedButton_Description": "Wino 已断开网络连接。点击「重新连接」重试。", "TitleBarServerDisconnectedButton_Title": "无网络连接", @@ -691,16 +1019,430 @@ "Today": "今天", "UnknownAddress": "未知地址", "UnknownDateHeader": "未知日期", - "UnknownGroupAddress": "未知邮件组地址", + "UnknownGroupAddress": "未知邮件群组地址", "UnknownSender": "未知发件人", "Unsubscribe": "取消订阅", "ViewContactDetails": "查看详情", - "WinoUpgradeDescription": "Wino 免费提供 3 个账户。如果你需要同时使用 3 个以上的账户,请升级", - "WinoUpgradeMessage": "升级为无限制账户数", + "WinoUpgradeDescription": "Wino 免费使用 3 个邮件账户。如果您需要同时使用 3 个以上的账户,请升级。", + "WinoUpgradeMessage": "升级为无限账户数", "WinoUpgradeRemainingAccountsMessage": "已使用 {0} 个免费账户,共 {1} 个。", "Yesterday": "昨天", - "SettingsAppPreferences_EmailSyncInterval_Title": "电子邮件同步间隔", - "SettingsAppPreferences_EmailSyncInterval_Description": "自动电子邮件同步间隔 (分钟)。此设置仅在重启 Wino Mail 后才会应用。" + "Smime_ImportCertificates_Success": "证书导入成功。", + "Smime_ImportCertificates_Error": "导入证书时出错:{0}", + "Smime_RemoveCertificates_Confirm": "您确定要删除证书 {0} 吗?", + "Smime_RemoveCertificates_Success": "证书已移除。", + "Smime_ExportCertificates_Success": "证书已导出。", + "Smime_ExportCertificates_Error": "导出证书时出错。", + "Smime_CertificateDetails": "主题:{0}\\n颁发者:{1}\\n有效期起始:{2}\\n有效期结束:{3}\\n指纹:{4}", + "Smime_CertificatePassword_Title": "需要证书密码", + "Smime_CertificatePassword_Placeholder": "{0} 的证书密码(可选)", + "Smime_Confirm_Title": "确认", + "Buttons_OK": "确定", + "Buttons_Refresh": "刷新", + "SettingsSignatureAndEncryption_Title": "签名和加密", + "SettingsSignatureAndEncryption_Description": "管理用于签名和加密电子邮件的 S/MIME 证书。", + "SettingsSignatureAndEncryption_MyCertificatesHeader": "我的证书", + "SettingsSignatureAndEncryption_MyCertificatesDescription": "用于签名和加密的个人证书", + "SettingsSignatureAndEncryption_RecipientCertificatesHeader": "收件人证书", + "SettingsSignatureAndEncryption_RecipientCertificatesDescription": "用于解密的收件人证书", + "SettingsSignatureAndEncryption_NameColumn": "名称", + "SettingsSignatureAndEncryption_ExpiresColumn": "到期日", + "SettingsSignatureAndEncryption_ThumbprintColumn": "指纹", + "Buttons_Remove": "移除", + "Buttons_Export": "导出", + "Buttons_Import": "导入", + "SettingsSignatureAndEncryption_SigningCertificate": "S/MIME 签名证书", + "SettingsSignatureAndEncryption_EncryptionCertificate": "S/MIME 加密", + "SettingsSignatureAndEncryption_SigningCertificatePlaceholder": "无", + "SmimeSignaturesInMessage": "此消息中的签名:", + "SmimeSignatureEntry": "• {0} {1}({2},有效期至 {3} - {4})", + "SmimeSigningCertificateInfoTitle": "S/MIME 签名证书信息", + "SmimeCertificateInfoTitle": "S/MIME 证书信息", + "SmimeNoCertificateFileFound": "未找到证书文件", + "SmimeSaveCertificate": "保存证书…", + "SmimeCertificate": "S/MIME 证书", + "SmimeCertificateSavedTo": "证书已保存至 {0}", + "SmimeSignedTooltip": "此消息已使用 S/MIME 证书签名。点击查看详细信息", + "SmimeEncryptedTooltip": "此消息使用 S/MIME 证书进行加密。", + "SmimeCertificateFileInfo": "文件:{0}", + "Composer_LightTheme": "浅色主题", + "Composer_DarkTheme": "深色主题", + "Composer_Outdent": "减少缩进", + "Composer_Indent": "缩进", + "Composer_BulletList": "项目符号列表", + "Composer_OrderedList": "有序列表", + "Composer_Stroke": "描边", + "Composer_Bold": "粗体", + "Composer_Italic": "斜体", + "Composer_Underline": "下划线", + "Composer_CcBcc": "抄送与密送", + "Composer_EnableSmimeSignature": "启用/禁用 S/MIME 签名", + "Composer_EnableSmimeEncryption": "启用/禁用 S/MIME 加密", + "Composer_LocalDraftSyncInfo": "此草稿仅限本地。Wino 无法将其发送到您的邮件服务器。点击可重试发送到服务器。", + "Composer_CertificateExpires": "到期日:", + "Composer_SmimeSignature": "S/MIME 签名", + "Composer_SmimeEncryption": "S/MIME 加密", + "Composer_EmailTemplatesPlaceholder": "电子邮件模板", + "Composer_AiSummarize": "使用 AI 总结", + "Composer_AiSummarizeDescription": "提取此电子邮件中的要点、行动项和决议。", + "Composer_AiTranslate": "使用 AI 翻译", + "Composer_AiActions": "AI 操作", + "Composer_AiRewrite": "使用 AI 重写", + "AiActions_CheckingStatus": "正在检查 AI 访问权限…", + "AiActions_SignedOutTitle": "解锁 Wino AI 套件", + "AiActions_SignedOutDescription": "在登录您的 Wino 帐户并激活 AI 套件插件后,使用 AI 翻译、重写和摘要电子邮件。", + "AiActions_NoPackTitle": "需要 AI 套件", + "AiActions_NoPackDescription": "您已登录,但 AI 套件尚未激活。购买后即可使用 Wino 的 AI 翻译、重写和摘要工具。", + "AiActions_UsageSummary": "{0} / {1} 积分本月已使用。", + "Composer_AiRewritePolite": "使其更有礼貌", + "Composer_AiRewritePoliteDescription": "在保持相同意图的前提下软化措辞。", + "Composer_AiRewriteAngry": "让措辞更愤怒", + "Composer_AiRewriteAngryDescription": "使用更尖锐、更具对抗性的语气。", + "Composer_AiRewriteHappy": "让它更开心", + "Composer_AiRewriteHappyDescription": "添加更积极热情的语气。", + "Composer_AiRewriteFormal": "让它更正式", + "Composer_AiRewriteFormalDescription": "使信息听起来更专业、结构化。", + "Composer_AiRewriteFriendly": "让它更友好", + "Composer_AiRewriteFriendlyDescription": "用更友好、易于接近的语气让信息更亲切。", + "Composer_AiRewriteShorter": "让它更简短", + "Composer_AiRewriteShorterDescription": "缩短文本并去除不必要的细节。", + "Composer_AiRewriteClearer": "让它更清晰", + "Composer_AiRewriteClearerDescription": "提升可读性,使信息更易于理解。", + "Composer_AiRewriteCustom": "自定义", + "Composer_AiRewriteCustomDescription": "描述你希望的改写意图。", + "Composer_AiRewriteCustomPlaceholder": "描述你希望如何改写信息", + "Composer_AiRewriteMode": "改写语气", + "Composer_AiRewriteApply": "应用改写", + "Composer_AiTranslateDialogTitle": "使用AI翻译", + "Composer_AiTranslateDialogDescription": "输入目标语言或地区代码,如 en-US、tr-TR、de-DE 或 fr-FR。", + "Composer_AiTranslateApply": "翻译", + "Composer_AiTranslateLanguage": "目标语言", + "Composer_AiTranslateCustomPlaceholder": "输入区域代码", + "Composer_AiTranslateLanguageEnglish": "英语(en-US)", + "Composer_AiTranslateLanguageTurkish": "土耳其语(tr-TR)", + "Composer_AiTranslateLanguageGerman": "德语(de-DE)", + "Composer_AiTranslateLanguageFrench": "法语(fr-FR)", + "Composer_AiTranslateLanguageSpanish": "西班牙语(es-ES)", + "Composer_AiTranslateLanguageItalian": "意大利语(it-IT)", + "Composer_AiTranslateLanguagePortugueseBrazil": "葡萄牙语(巴西)(pt-BR)", + "Composer_AiTranslateLanguageDutch": "荷兰语(nl-NL)", + "Composer_AiTranslateLanguagePolish": "波兰语(pl-PL)", + "Composer_AiTranslateLanguageRussian": "俄语(ru-RU)", + "Composer_AiTranslateLanguageJapanese": "日语(ja-JP)", + "Composer_AiTranslateLanguageKorean": "韩语(ko-KR)", + "Composer_AiTranslateLanguageChineseSimplified": "简体中文(zh-CN)", + "Composer_AiTranslateLanguageArabic": "阿拉伯语(ar-SA)", + "Composer_AiTranslateLanguageHindi": "印地语(hi-IN)", + "Composer_AiTranslateLanguageOther": "其他…", + "Composer_AiBusyTitle": "AI 已在工作中", + "Composer_AiBusyMessage": "请等待当前 AI 操作完成。", + "Composer_AiSignInRequired": "请登录 Wino 账号以使用 AI 功能。", + "Composer_AiMissingHtml": "目前还没有要发送给 Wino AI 的消息内容。", + "Composer_AiQuotaUnavailable": "AI 结果已应用。", + "Composer_AiAppliedMessage": "AI 结果已应用到编辑器。若要还原,请使用撤销。", + "Composer_AiSummarizeSuccessTitle": "AI 摘要已应用", + "Composer_AiTranslateSuccessTitle": "AI 翻译已应用", + "Composer_AiRewriteSuccessTitle": "AI 改写已应用", + "Composer_AiErrorTitle": "AI 操作失败", + "Reader_AiAppliedMessage": "AI 结果现已显示在此消息中。重新打开消息以查看原始内容。", + "SettingsAppPreferences_EmailSyncInterval_Title": "Email sync interval", + "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail.", + "ContactsPage_Title": "联系人", + "ContactsPage_AddContact": "添加联系人", + "ContactsPage_EditContact": "编辑联系人", + "ContactsPage_DeleteContact": "删除联系人", + "ContactsPage_SearchPlaceholder": "搜索联系人...", + "ContactsPage_NoContacts": "未找到联系人", + "ContactsPage_ContactsCount": "{0} 联系人", + "ContactsPage_SelectedContactsCount": "已选择 {0}", + "ContactsPage_DeleteSelectedContacts": "删除所选", + "ContactEditDialog_Title": "编辑联系人", + "ContactEditDialog_PhotoSection": "照片", + "ContactEditDialog_ChoosePhoto": "选择照片", + "ContactEditDialog_RemovePhoto": "删除照片", + "ContactEditDialog_NameHeader": "姓名", + "ContactEditDialog_NamePlaceholder": "联系人姓名", + "ContactEditDialog_EmailHeader": "电子邮件地址", + "ContactEditDialog_EmailPlaceholder": "contact@example.com", + "ContactEditDialog_InfoSection": "联系信息", + "ContactEditDialog_RootContactInfo": "这是与您的账户相关联的根联系人,无法删除。", + "ContactEditDialog_OverriddenContactInfo": "此联系人已被手动修改,在同步过程中不会更新。", + "ContactsPage_Subtitle": "管理您的电子邮件联系人及其信息", + "ContactStatus_Account": "账户", + "ContactStatus_Modified": "已修改", + "ContactAction_Edit": "编辑联系人", + "ContactAction_ChangePhoto": "更改照片", + "ContactAction_Delete": "删除联系人", + "ContactAction_Add": "添加联系人", + "ContactSelection_Selected": "已选中", + "ContactSelection_SelectAll": "全选", + "ContactSelection_Clear": "清除选中", + "ContactsPage_EmptyState": "无联系人可显示", + "ContactsPage_AddFirstContact": "添加您的第一个联系人", + "ContactsPage_ContactsCountSuffix": "联系人", + "ContactsPane_NewContact": "新联系人", + "ContactsPane_DescriptionTitle": "管理您的联系人", + "ContactsPane_DescriptionBody": "创建联系人、重命名、更新个人资料图片,并在一个地方整理已保存的详细信息。", + "ContactEditDialog_AddTitle": "添加联系人", + "ContactInfoBar_ContactAdded": "联系人已成功添加。", + "ContactInfoBar_ContactUpdated": "联系人已成功更新。", + "ContactInfoBar_ContactsDeleted": "联系人已成功删除。", + "ContactInfoBar_ContactPhotoUpdated": "联系人照片已成功更新。", + "ContactInfoBar_FailedToLoadContacts": "加载联系人失败:{0}", + "ContactInfoBar_FailedToAddContact": "添加联系人失败:{0}", + "ContactInfoBar_FailedToUpdateContact": "更新联系人失败:{0}", + "ContactInfoBar_FailedToDeleteContacts": "删除联系人失败:{0}", + "ContactInfoBar_FailedToUpdatePhoto": "更新照片失败:{0}", + "ContactInfoBar_CannotDeleteRoot": "无法删除根联系人。", + "ContactConfirmDialog_DeleteTitle": "删除联系人", + "ContactConfirmDialog_DeleteMessage": "您确定要删除联系人 '{0}' 吗?", + "ContactConfirmDialog_DeleteMultipleMessage": "您确定要删除 {0} 个联系人吗?", + "ContactConfirmDialog_DeleteButton": "删除", + "CalendarAccountSettings_Title": "日历账户设置", + "CalendarAccountSettings_Description": "为 {0} 管理日历设置", + "CalendarAccountSettings_AccountColor": "账户颜色", + "CalendarAccountSettings_AccountColorDescription": "更改此日历帐户的显示颜色", + "CalendarAccountSettings_SyncEnabled": "启用同步", + "CalendarAccountSettings_SyncEnabledDescription": "为此账户启用或禁用日历同步", + "CalendarAccountSettings_DefaultShowAs": "默认显示状态", + "CalendarAccountSettings_DefaultShowAsDescription": "使用此账户创建的新事件的默认可用性状态", + "CalendarAccountSettings_PrimaryCalendar": "主日历", + "CalendarAccountSettings_PrimaryCalendarDescription": "将此日历标记为该账户的主日历", + "CalendarSettings_NewEventBehavior_Header": "新建事件按钮行为", + "CalendarSettings_NewEventBehavior_Description": "选择新建事件按钮在每次创建时是否要询问日历,还是始终打开特定日历。", + "CalendarSettings_NewEventBehavior_AskEachTime": "每次询问。", + "CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar": "始终使用特定日历。", + "CalendarSettings_Rendering_Title": "渲染", + "CalendarSettings_Rendering_Description": "配置日历的布局和显示行为。", + "CalendarSettings_Notifications_Title": "通知", + "CalendarSettings_Notifications_Description": "选择默认的提醒与打盹行为。", + "CalendarSettings_Preferences_Title": "偏好设置", + "CalendarSettings_Preferences_Description": "设置新建事件按钮的行为。", + "WhatIsNew_GetStartedButton": "开始使用", + "WhatIsNew_ContinueAnywayButton": "仍然继续", + "WhatIsNew_PreparingForNewVersionButton": "正在为新版本做准备...", + "WhatIsNew_MigrationPreparing_Title": "正在准备您的数据", + "WhatIsNew_MigrationPreparing_Description": "Wino 正在应用更新迁移。请稍等,我们正在为此版本准备您的账户数据。", + "WhatIsNew_MigrationFailedMessage": "应用迁移失败,错误代码 {0}。您仍然可以继续使用应用程序。然而,如遇严重问题,请重新安装应用程序。", + "WhatIsNew_MigrationNotification_Title": "Wino Mail 已更新", + "WhatIsNew_MigrationNotification_Message": "打开应用以完成更新并查看新功能。", + "WelcomeWindow_Title": "欢迎使用 Wino Mail", + "WelcomeWindow_Subtitle": "面向邮件和日历的原生 Windows 体验。", + "WelcomeWindow_WhatsNewTitle": "最新变更", + "WelcomeWindow_FeaturesTitle": "功能", + "WelcomeWindow_WhatsNewTab": "更新内容", + "WelcomeWindow_FeaturesTab": "功能", + "WelcomeWindow_GetStartedButton": "开始添加账户", + "WelcomeWindow_GetStartedDescription": "添加您的 Outlook、Gmail 或 IMAP 账户来开始使用 Wino Mail。", + "WelcomeWindow_ImportFromWinoAccount": "从您的 Wino 账户导入", + "WelcomeWindow_ImportInProgress": "正在导入同步的偏好设置和账户...", + "WelcomeWindow_ImportNoAccountsFound": "在 Wino 账户中未找到同步的账户。如果存在偏好设置,它们已被还原。使用“开始使用”手动添加账户。", + "WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} 同步账户已在此设备上可用。如有需要,请使用“开始使用”手动再添加一个账户。", + "WelcomeWindow_SetupTitle": "设置您的账户", + "WelcomeWindow_SetupSubtitle": "选择您的电子邮件提供商以开始。", + "WelcomeWindow_AddAccountButton": "添加账户", + "WelcomeWindow_SkipForNow": "暂时跳过 —— 稍后设置", + "WelcomeWindow_AppDescription": "快速、专注的收件箱,专为 Windows 11 重新设计", + "WelcomeWizard_Step1Title": "欢迎", + "SystemTrayMenu_Open": "打开", + "WinoAccount_Titlebar_SyncBenefitTitle": "同步设置", + "WinoAccount_Titlebar_SyncBenefitDescription": "在设备之间同步您的 Wino 偏好设置。", + "WinoAccount_Titlebar_AddonsBenefitTitle": "解锁附加组件", + "WinoAccount_Titlebar_AddonsBenefitDescription": "访问高级功能,如 Wino AI Pack。", + "WinoAccount_Management_Description": "管理您的 Wino 账户、AI Pack 访问权限,以及同步的偏好设置和账户详细信息。", + "WinoAccount_Management_SignedOutTitle": "登录 Wino Mail", + "WinoAccount_Management_SignedOutDescription": "登录或创建账户以同步您的邮件、使用 AI 功能,并在设备之间管理设置。", + "WinoAccount_Management_ProfileSectionHeader": "个人资料", + "WinoAccount_Management_AddOnsSectionHeader": "Wino 附加组件", + "WinoAccount_Management_DataSectionHeader": "数据", + "WinoAccount_Management_AccountActionsSectionHeader": "账户操作", + "WinoAccount_Management_AccountCardTitle": "账户", + "WinoAccount_Management_AccountCardDescription": "您的 Wino 账户邮箱地址和当前账户状态。", + "WinoAccount_Management_AiPackCardTitle": "AI Pack", + "WinoAccount_Management_AiPackCardDescription": "查看 Wino AI Pack 是否处于激活状态以及剩余多少用量。", + "WinoAccount_Management_AiPackActive": "AI Pack 已激活", + "WinoAccount_Management_AiPackInactive": "AI Pack 未激活", + "WinoAccount_Management_AiPackUsage": "{0} / {1} 次已使用。剩余 {2}。", + "WinoAccount_Management_AiPackBillingPeriod": "计费期:{0:d} - {1:d}", + "WinoAccount_Management_AiPackUnknownUsage": "用量详情尚不可用。", + "WinoAccount_Management_AiPackBuyDescription": "购买 Wino AI Pack,可使用 AI 进行邮件翻译、改写或摘要。", + "WinoAccount_Management_AiPackPromoTitle": "解锁 AI Pack", + "WinoAccount_Management_AiPackPromoDescription": "使用 AI 驱动的工具,为您的邮件工作流提速。将邮件翻译成 50 多种语言,改写以提高清晰度和语气,并获得长邮件对话的即时摘要。", + "WinoAccount_Management_AiPackPromoPrice": "$4.99 / 月", + "WinoAccount_Management_AiPackPromoRequests": "1,000 积分", + "WinoAccount_Management_AiPackGetButton": "获取 AI Pack", + "WinoAddOn_AI_PACK_Name": "Wino AI Pack", + "WinoAddOn_AI_PACK_Description": "用于在 Wino Mail 中进行翻译、改写和摘要操作的 AI 驱动工具。", + "WinoAddOn_AI_PACK_Keywords": "AI、翻译、改写、摘要、生产力", + "WinoAddOn_UNLIMITED_ACCOUNTS_Name": "无限账户", + "WinoAddOn_UNLIMITED_ACCOUNTS_Description": "移除账户数量限制,您可以添加任意数量的邮箱账户。", + "WinoAddOn_UNLIMITED_ACCOUNTS_Keywords": "账户、无限、高级、插件", + "WinoAccount_Management_PurchaseRequiresSignIn": "使用您的 Wino 账户登录以完成此购买。", + "WinoAccount_Management_PurchaseStartFailed": "Wino 无法完成此 Microsoft Store 购买。", + "WinoAccount_Management_StoreSyncFailed": "购买已完成,但 Wino 尚无法刷新您的账户权限。请稍后再尝试。", + "WinoAccount_Management_AiPackSubscriptionActive": "您的订阅处于激活状态", + "WinoAccount_Management_AiPackRenews": "将于 {0:d} 续订", + "WinoAccount_Management_AiPackRequestsUsed": "本月已用积分", + "WinoAccount_Management_AiPackResets": "重置 {0:d}", + "WinoAccount_Management_AiPackUsageLoadFailed": "加载 AI 使用余额时出现问题。", + "WinoAccount_Management_AiPackFeatureTranslate": "翻译", + "WinoAccount_Management_AiPackFeatureRewrite": "改写", + "WinoAccount_Management_AiPackFeatureSummarize": "摘要", + "WinoAccount_Management_AddOnLoadFailed": "加载此附加组件时出现问题。", + "WinoAccount_Management_SyncPreferencesTitle": "同步偏好设置和账户", + "WinoAccount_Management_SyncPreferencesDescription": "跨设备导入或导出您的 Wino 偏好设置和邮箱详细信息。密码、令牌及其他敏感信息从不同步。", + "WinoAccount_Management_SignOutTitle": "登出", + "WinoAccount_Management_SignOutDescription": "在此设备上退出您的账户", + "WinoAccount_Management_StatusLabel": "状态:{0}", + "WinoAccount_Management_NoRemoteSettings": "此账户尚未存储任何同步数据。", + "WinoAccount_Management_ExportSucceeded": "您选择的 Wino 数据已成功导出。", + "WinoAccount_Management_ExportPreferencesSucceeded": "您的偏好设置已导出到您的 Wino 账户。", + "WinoAccount_Management_ExportAccountsSucceeded": "已将 {0} 条账户信息导出到您的 Wino 账户。", + "WinoAccount_Management_ImportSucceeded": "已从您的 Wino 账户导入同步数据。", + "WinoAccount_Management_ImportPreferencesSucceeded": "应用了 {0} 条同步偏好设置。", + "WinoAccount_Management_ImportAccountsSucceeded": "已导入 {0} 个账户。", + "WinoAccount_Management_ImportDuplicateAccountsSkipped": "已跳过 {0} 个在此设备上已存在的账户。", + "WinoAccount_Management_ImportPartial": "已应用 {0} 条同步偏好设置。 {1} 条偏好设置无法还原。", + "WinoAccount_Management_ImportReloginReminder": "未导入密码、令牌及其他敏感信息。在使用前,请在此设备上为每个账户重新登录。", + "WinoAccount_Management_SerializeFailed": "Wino 无法序列化您当前的偏好设置。", + "WinoAccount_Management_EmptyExport": "没有可导出的偏好设置值。", + "WinoAccount_Management_ImportEmpty": "同步的数据负载不包含任何可还原的新内容。", + "WinoAccount_Management_ExportDialog_Title": "导出到您的 Wino 账户", + "WinoAccount_Management_ExportDialog_Description": "选择要同步到您的 Wino 账户的内容。", + "WinoAccount_Management_ExportDialog_IncludePreferences": "偏好设置", + "WinoAccount_Management_ExportDialog_IncludeAccounts": "账户", + "WinoAccount_Management_ExportDialog_AccountsDisclaimer": "密码、令牌和其他敏感信息不会同步。", + "WinoAccount_Management_ExportDialog_AccountsRelogin": "在另一台电脑上导入的账户仍需要您再次登录后才能使用。", + "WinoAccount_Management_ExportDialog_InProgress": "正在导出您选择的 Wino 数据...", + "WinoAccount_Management_LoadFailed": "无法加载最新的 Wino 账户信息。", + "WinoAccount_Management_ActionFailed": "Wino 账户请求无法完成。", + "WinoAccount_SettingsSection_Title": "Wino 账户", + "WinoAccount_SettingsSection_Description": "使用本地身份验证服务创建或登录 Wino 账户。", + "WinoAccount_RegisterButton_Title": "注册账户", + "WinoAccount_RegisterButton_Description": "使用电子邮件和密码创建 Wino 账户。", + "WinoAccount_RegisterButton_Action": "打开注册", + "WinoAccount_LoginButton_Title": "登录", + "WinoAccount_LoginButton_Description": "使用电子邮件和密码登录到现有的 Wino 账户。", + "WinoAccount_LoginButton_Action": "打开登录", + "WinoAccount_SignOutButton_Title": "登出", + "WinoAccount_SignOutButton_Description": "移除本地存储的 Wino 账户会话。", + "WinoAccount_SignOutButton_Action": "登出", + "WinoAccount_RegisterDialog_Title": "创建 Wino 账户", + "WinoAccount_RegisterDialog_Description": "创建 Wino 账户以保持 Wino 体验同步并解锁基于账户的插件。", + "WinoAccount_RegisterDialog_HeroTitle": "创建您的 Wino 账户", + "WinoAccount_RegisterDialog_BenefitsTitle": "为什么要创建一个?", + "WinoAccount_RegisterDialog_BenefitSyncTitle": "在设备之间导入和导出设置", + "WinoAccount_RegisterDialog_BenefitSyncDescription": "在设备之间移动您的 Wino 偏好设置,而无需从头重新配置。", + "WinoAccount_RegisterDialog_BenefitAiTitle": "获取独家插件,如 Wino AI Pack(付费)", + "WinoAccount_RegisterDialog_BenefitAiDescription": "使用一个账户在功能可用时解锁高级 Wino 功能。", + "WinoAccount_RegisterDialog_DifferenceTitle": "Wino 账户与您的邮箱账户分开", + "WinoAccount_RegisterDialog_DifferenceDescription": "您的 Outlook、Gmail、IMAP 或其他邮件账户将保持原样。Wino 账户仅管理 Wino 特定功能和基于账户的附加组件。", + "WinoAccount_RegisterDialog_PrimaryButton": "注册", + "WinoAccount_RegisterDialog_PrivacyTitle": "隐私与 API 处理", + "WinoAccount_RegisterDialog_PrivacyDescription": "可选附加组件(如 Wino AI Pack)可能仅在您使用这些功能时,将所选的电子邮件 HTML 内容发送至 Wino API 服务。", + "WinoAccount_RegisterDialog_PrivacyLinkText": "阅读隐私政策", + "WinoAccount_RegisterDialog_PrivacyCheckbox": "我同意隐私政策。", + "WinoAccount_LoginDialog_Title": "登录 Wino 账户", + "WinoAccount_LoginDialog_Description": "登录 Wino 账户以同步您的 Wino 设置并访问基于账户的功能。", + "WinoAccount_LoginDialog_HeroTitle": "欢迎回来", + "WinoAccount_LoginDialog_BenefitsTitle": "登录带来的好处", + "WinoAccount_LoginDialog_BenefitsDescription": "使用您的 Wino 账户在设备之间继续同步设置,并访问如 Wino AI Pack 这样的付费附加组件。", + "WinoAccount_LoginDialog_DifferenceTitle": "这不是您邮箱账户的登录入口", + "WinoAccount_LoginDialog_DifferenceDescription": "在此处登录不会向 Wino 添加或替换您的 Outlook、Gmail、IMAP 等账户。它只用于登录 Wino 专有服务。", + "WinoAccount_LoginDialog_ForgotPasswordLink": "忘记密码?", + "WinoAccount_EmailLabel": "邮箱", + "WinoAccount_EmailPlaceholder": "name@example.com", + "WinoAccount_PasswordLabel": "密码", + "WinoAccount_ConfirmPasswordLabel": "确认密码", + "WinoAccount_ForgotPasswordDialog_Title": "重置密码", + "WinoAccount_ForgotPasswordDialog_PrimaryButton": "发送重置邮件", + "WinoAccount_ForgotPasswordDialog_BackToSignIn": "返回登录", + "WinoAccount_ForgotPasswordDialog_Description": "输入您的 Wino 账户邮箱地址,如果已注册,我们将发送密码重置链接。", + "WinoAccount_Validation_EmailRequired": "必须填写邮箱。", + "WinoAccount_Validation_PasswordRequired": "必须填写密码。", + "WinoAccount_Validation_PasswordMismatch": "两次输入的密码不一致。", + "WinoAccount_Validation_PrivacyConsentRequired": "在创建 Wino 账户之前,您必须同意隐私政策。", + "WinoAccount_Error_InvalidCredentials": "邮箱地址或密码不正确。", + "WinoAccount_Error_AccountLocked": "此账户暂时被锁定。", + "WinoAccount_Error_AccountBanned": "此账户已被禁用。", + "WinoAccount_Error_AccountSuspended": "此账户已被暂停。", + "WinoAccount_Error_EmailNotConfirmed": "在登录前请确认您的邮箱地址。", + "WinoAccount_Error_EmailConfirmationRequired": "在登录前请确认您的邮箱地址。", + "WinoAccount_Error_EmailConfirmationResendNotAvailable": "尚无法重新发送确认邮件。", + "WinoAccount_Error_EmailConfirmationResendInvalid": "此确认请求已失效。请尝试再次登录。", + "WinoAccount_Error_EmailNotRegistered": "此邮箱地址未注册。", + "WinoAccount_Error_RefreshTokenInvalid": "您的会话已无效。请重新登录。", + "WinoAccount_Error_EmailAlreadyRegistered": "此邮箱地址已被注册。", + "WinoAccount_Error_ExternalLoginEmailRequired": "完成外部登录需要提供一个邮箱地址。", + "WinoAccount_Error_ExternalLoginInvalid": "外部登录请求无效。", + "WinoAccount_Error_ExternalAuthStateInvalid": "外部登录状态无效或已过期。", + "WinoAccount_Error_ExternalAuthCodeInvalid": "外部登录代码无效或已过期。", + "WinoAccount_Error_AiPackRequired": "执行此操作需要有效的 Wino AI Pack 订阅。", + "WinoAccount_Error_AiQuotaExceeded": "本计费周期内,您的 AI Pack 使用额度已用尽。", + "WinoAccount_Error_AiHtmlEmpty": "没有要处理的邮件内容。", + "WinoAccount_Error_AiHtmlTooLarge": "此邮件过大,无法通过 Wino AI 处理。", + "WinoAccount_Error_AiUnsupportedLanguage": "该语言不受支持。请尝试使用有效的区域代码,例如 en-US 或 tr-TR。", + "WinoAccount_Error_Forbidden": "您没有执行此操作的权限。", + "WinoAccount_Error_ValidationFailed": "请求无效。请检查输入的值。", + "WinoAccount_RegisterSuccessMessage": "为 {0} 完成 Wino 账户注册。", + "WinoAccount_LoginSuccessMessage": "已以 {0} 登录 Wino 账户。", + "WinoAccount_EmailConfirmationSentDialog_Title": "请确认您的邮箱地址", + "WinoAccount_EmailConfirmationSentDialog_Message": "我们已向 {0} 发送了邮箱确认。请确认后再尝试登录。", + "WinoAccount_EmailConfirmationPendingDialog_Title": "需要邮箱确认", + "WinoAccount_EmailConfirmationPendingDialog_Message": "我们仍在等待您确认 {0}。", + "WinoAccount_EmailConfirmationPendingDialog_ResendButton": "重新发送确认邮件", + "WinoAccount_EmailConfirmationPendingDialog_Countdown": "您可以在 {0} 之后重新发送确认邮件。", + "WinoAccount_EmailConfirmationPendingDialog_ReadyToResend": "现在可以重新发送确认邮件。", + "WinoAccount_EmailConfirmationResentDialog_Title": "确认邮件已重新发送", + "WinoAccount_EmailConfirmationResentDialog_Message": "我们已向 {0} 发送了另一封确认邮件。请确认后再次尝试登录。", + "WinoAccount_ForgotPasswordDialog_SuccessTitle": "密码重置邮件已发送", + "WinoAccount_ForgotPasswordDialog_SuccessMessage": "我们已向 {0} 发送了密码重置邮件。打开该邮件以设置新密码。", + "WinoAccount_ChangePassword_Title": "修改密码", + "WinoAccount_ChangePassword_Description": "向此 Wino 账户发送密码重置邮件。", + "WinoAccount_ChangePassword_Action": "发送重置邮件", + "WinoAccount_ChangePassword_ConfirmationMessage": "您是否希望 Wino 向 {0} 发送密码重置邮件?", + "WinoAccount_SignOut_SuccessMessage": "已从 Wino 账户 {0} 登出。", + "WinoAccount_SignOut_NoAccountMessage": "当前没有活动的 Wino 账户可供登出。", + "WinoAccount_Titlebar_SignedOutTitle": "Wino 账户", + "WinoAccount_Titlebar_SignedOutDescription": "请登录或创建 Wino 账户以管理您的 Wino 会话。", + "WinoAccount_Titlebar_SignedInStatus": "状态:{0}", + "WelcomeWizard_Step2Title": "添加账户", + "WelcomeWizard_Step3Title": "完成设置", + "ProviderSelection_Title": "选择您的电子邮件提供商", + "ProviderSelection_Subtitle": "在下方选择一个提供商,以将您的电子邮件账户添加到 Wino Mail。", + "ProviderSelection_AccountNameHeader": "账户名", + "ProviderSelection_AccountNamePlaceholder": "例如 个人、工作", + "ProviderSelection_DisplayNameHeader": "显示名称", + "ProviderSelection_DisplayNamePlaceholder": "例如 约翰·多伊", + "ProviderSelection_EmailHeader": "电子邮件地址", + "ProviderSelection_EmailPlaceholder": "例如 johndoe@example.com", + "ProviderSelection_AppPasswordHeader": "应用专用密码", + "ProviderSelection_AppPasswordHelp": "如何获取应用专用密码?", + "ProviderSelection_CalendarModeHeader": "日历集成", + "ProviderSelection_CalendarMode_DisabledTitle": "已禁用", + "ProviderSelection_CalendarMode_DisabledDescription": "没有日历集成", + "ProviderSelection_CalendarMode_CalDavTitle": "CalDAV 同步", + "ProviderSelection_CalendarMode_CalDavDescription_Apple": "您的日历事件会在设备之间同步到 Apple 的服务器。", + "ProviderSelection_CalendarMode_CalDavDescription_Yahoo": "您的日历事件会在设备之间同步到 Yahoo 的服务器。", + "ProviderSelection_CalendarMode_LocalTitle": "本地日历", + "ProviderSelection_CalendarMode_LocalDescription": "您的事件仅存储在本地计算机上。没有服务器连接。", + "ProviderSelection_ClearColor": "清除颜色", + "ProviderSelection_ContinueButton": "继续", + "ProviderSelection_SpecialImap_Subtitle": "请输入您的账户凭据以连接。", + "AccountSetup_Title": "正在设置您的账户", + "AccountSetup_Step_Authenticating": "正在使用 {0} 进行身份验证", + "AccountSetup_Step_TestingMailAuth": "正在测试邮箱身份验证", + "AccountSetup_Step_SyncingFolders": "正在同步文件夹元数据", + "AccountSetup_Step_FetchingProfile": "正在获取个人资料信息", + "AccountSetup_Step_DiscoveringCalDav": "正在发现 CalDAV 设置", + "AccountSetup_Step_TestingCalendarAuth": "正在测试日历身份验证", + "AccountSetup_Step_SavingAccount": "正在保存账户信息", + "AccountSetup_Step_FetchingCalendarMetadata": "正在获取日历元数据", + "AccountSetup_Step_SyncingAliases": "正在同步别名", + "AccountSetup_Step_Finalizing": "正在完成设置", + "AccountSetup_FailureMessage": "设置失败。返回上一步以修正设置,或稍后再试。", + "AccountSetup_SuccessMessage": "您的账户已成功设置!", + "AccountSetup_GoBackButton": "返回", + "AccountSetup_TryAgainButton": "再试一次", + "ImapCalDavSettings_AutoDiscoveryFailed": "自动发现失败。请在高级选项卡中手动输入设置。" } - - diff --git a/Wino.Core.Domain/Translator.cs b/Wino.Core.Domain/Translator.cs index 1fc2fb4c..9ada00c6 100644 --- a/Wino.Core.Domain/Translator.cs +++ b/Wino.Core.Domain/Translator.cs @@ -7,4 +7,7 @@ namespace Wino.Core.Domain; /// All translations generated automatically by the source generator. /// [TranslatorGen] -public partial class Translator; +public partial class Translator +{ + public static string GetTranslatedString(string key) => Resources.GetTranslatedString(key); +} diff --git a/Wino.Core.Domain/Validation/CalendarEventComposeResultValidator.cs b/Wino.Core.Domain/Validation/CalendarEventComposeResultValidator.cs new file mode 100644 index 00000000..5e722f6f --- /dev/null +++ b/Wino.Core.Domain/Validation/CalendarEventComposeResultValidator.cs @@ -0,0 +1,73 @@ +using System; +using System.IO; +using System.Linq; +using System.Net.Mail; +using Wino.Core.Domain.Exceptions; +using Wino.Core.Domain.Models.Calendar; + +namespace Wino.Core.Domain.Validation; + +public sealed class CalendarEventComposeResultValidator +{ + public void Validate(CalendarEventComposeResult result) + { + ArgumentNullException.ThrowIfNull(result); + + if (result.CalendarId == Guid.Empty) + throw new CalendarEventComposeValidationException(Translator.CalendarEventCompose_ValidationMissingCalendar); + + if (result.AccountId == Guid.Empty) + throw new CalendarEventComposeValidationException(Translator.CalendarEventCompose_ValidationMissingCalendar); + + if (string.IsNullOrWhiteSpace(result.Title)) + throw new CalendarEventComposeValidationException(Translator.CalendarEventCompose_ValidationMissingTitle); + + if (result.EndDate <= result.StartDate) + { + var message = result.IsAllDay + ? Translator.CalendarEventCompose_ValidationInvalidAllDayRange + : Translator.CalendarEventCompose_ValidationInvalidTimeRange; + + throw new CalendarEventComposeValidationException(message); + } + + var missingAttachments = result.Attachments + .Where(attachment => string.IsNullOrWhiteSpace(attachment.FilePath) || !File.Exists(attachment.FilePath)) + .Select(attachment => attachment.FileName) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (missingAttachments.Count > 0) + { + throw new CalendarEventComposeValidationException( + string.Format(Translator.CalendarEventCompose_ValidationMissingAttachment, string.Join(", ", missingAttachments))); + } + + var invalidAttendee = result.Attendees + .FirstOrDefault(attendee => string.IsNullOrWhiteSpace(attendee.Email) || !IsValidEmailAddress(attendee.Email.Trim())); + + if (invalidAttendee != null) + throw new CalendarEventComposeValidationException(Translator.CalendarEventCompose_ValidationInvalidAttendee); + + var duplicateAttendeeGroups = result.Attendees + .Where(attendee => !string.IsNullOrWhiteSpace(attendee.Email)) + .GroupBy(attendee => attendee.Email.Trim(), StringComparer.OrdinalIgnoreCase) + .FirstOrDefault(group => group.Count() > 1); + + if (duplicateAttendeeGroups != null) + throw new CalendarEventComposeValidationException(Translator.CalendarEventCompose_ValidationInvalidAttendee); + } + + private static bool IsValidEmailAddress(string address) + { + try + { + var parsedAddress = new MailAddress(address); + return parsedAddress.Address.Equals(address, StringComparison.OrdinalIgnoreCase); + } + catch + { + return false; + } + } +} diff --git a/Wino.Core.Domain/Wino.Core.Domain.csproj b/Wino.Core.Domain/Wino.Core.Domain.csproj index f34bd83b..c17ec515 100644 --- a/Wino.Core.Domain/Wino.Core.Domain.csproj +++ b/Wino.Core.Domain/Wino.Core.Domain.csproj @@ -1,11 +1,13 @@  - net9.0 + net10.0 win-x86;win-x64;win-arm64 true x86;x64;arm64 true - true + true + true + true @@ -57,8 +59,8 @@ - + @@ -70,4 +72,4 @@ - \ No newline at end of file + diff --git a/Wino.Core.Tests/CalendarPageViewModelTests.cs b/Wino.Core.Tests/CalendarPageViewModelTests.cs new file mode 100644 index 00000000..95239be5 --- /dev/null +++ b/Wino.Core.Tests/CalendarPageViewModelTests.cs @@ -0,0 +1,300 @@ +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Globalization; +using CommunityToolkit.Mvvm.Collections; +using FluentAssertions; +using Itenso.TimePeriod; +using Moq; +using Wino.Calendar.ViewModels; +using Wino.Calendar.ViewModels.Data; +using Wino.Calendar.ViewModels.Interfaces; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Calendar; +using Xunit; + +namespace Wino.Core.Tests; + +public class CalendarPageViewModelTests +{ + [Fact] + public async Task ApplyDisplayRequestAsync_UpdatesVisibleRangeAndThreePeriodLoadWindow() + { + var settings = CreateSettings(firstDayOfWeek: DayOfWeek.Monday); + var today = new DateOnly(2026, 3, 20); + var preferencesService = CreatePreferencesService(settings); + var calendarService = new Mock(); + ITimePeriod? requestedPeriod = null; + + calendarService + .Setup(service => service.GetCalendarEventsAsync(It.IsAny(), It.IsAny())) + .Callback((_, period) => requestedPeriod = period) + .ReturnsAsync([]); + + var viewModel = CreateViewModel(calendarService.Object, preferencesService.Object, today); + var request = new CalendarDisplayRequest(CalendarDisplayType.Week, new DateOnly(2026, 3, 18)); + + await viewModel.ApplyDisplayRequestAsync(request); + + viewModel.CurrentVisibleRange.StartDate.Should().Be(new DateOnly(2026, 3, 16)); + viewModel.CurrentVisibleRange.EndDate.Should().Be(new DateOnly(2026, 3, 22)); + viewModel.LoadedDateWindow.StartDate.Should().Be(new DateTime(2026, 3, 9)); + viewModel.LoadedDateWindow.EndDate.Should().Be(new DateTime(2026, 3, 30)); + viewModel.VisibleDateRangeText.Should().Be("March 16 - March 22"); + + requestedPeriod.Should().NotBeNull(); + requestedPeriod!.Start.Should().Be(new DateTime(2026, 3, 9)); + requestedPeriod.End.Should().Be(new DateTime(2026, 3, 30)); + calendarService.Verify(service => service.GetCalendarEventsAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task ApplyDisplayRequestAsync_DoesNotReloadWhenResolvedRangeIsUnchanged() + { + var settings = CreateSettings(); + var preferencesService = CreatePreferencesService(settings); + var calendarService = new Mock(); + + calendarService + .Setup(service => service.GetCalendarEventsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync([]); + + var viewModel = CreateViewModel(calendarService.Object, preferencesService.Object, new DateOnly(2026, 3, 20)); + var request = new CalendarDisplayRequest(CalendarDisplayType.Day, new DateOnly(2026, 3, 20)); + + await viewModel.ApplyDisplayRequestAsync(request); + await viewModel.ApplyDisplayRequestAsync(request); + + calendarService.Verify(service => service.GetCalendarEventsAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task ReloadCurrentVisibleRangeAsync_RecomputesWhenCalendarSettingsChange() + { + var currentSettings = CreateSettings(firstDayOfWeek: DayOfWeek.Monday); + var preferencesService = CreatePreferencesService(() => currentSettings); + var calendarService = new Mock(); + + calendarService + .Setup(service => service.GetCalendarEventsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync([]); + + var viewModel = CreateViewModel(calendarService.Object, preferencesService.Object, new DateOnly(2026, 3, 20)); + var request = new CalendarDisplayRequest(CalendarDisplayType.Week, new DateOnly(2026, 3, 18)); + + await viewModel.ApplyDisplayRequestAsync(request); + viewModel.CurrentVisibleRange.StartDate.Should().Be(new DateOnly(2026, 3, 16)); + + currentSettings = CreateSettings(firstDayOfWeek: DayOfWeek.Sunday); + await viewModel.ReloadCurrentVisibleRangeAsync(); + + viewModel.CurrentVisibleRange.StartDate.Should().Be(new DateOnly(2026, 3, 15)); + calendarService.Verify(service => service.GetCalendarEventsAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); + } + + [Fact] + public async Task ApplyDisplayRequestAsync_LoadsOnlyActiveCalendars() + { + var settings = CreateSettings(); + var preferencesService = CreatePreferencesService(settings); + var calendarService = new Mock(); + + var account = new MailAccount + { + Id = Guid.NewGuid(), + Name = "Primary", + SenderName = "Primary", + Address = "primary@example.com", + ProviderType = MailProviderType.Outlook + }; + + var visibleCalendar = CreateCalendar(account, "Visible calendar"); + var hiddenCalendar = CreateCalendar(account, "Hidden calendar"); + var visibleCalendarViewModel = new AccountCalendarViewModel(account, visibleCalendar); + var hiddenCalendarViewModel = new AccountCalendarViewModel(account, hiddenCalendar); + hiddenCalendarViewModel.IsChecked = false; + + calendarService + .Setup(service => service.GetCalendarEventsAsync(It.Is(calendar => calendar.Id == visibleCalendar.Id), It.IsAny())) + .ReturnsAsync([ + new CalendarItem + { + Id = Guid.NewGuid(), + CalendarId = visibleCalendar.Id, + StartDate = new DateTime(2026, 3, 20, 9, 0, 0), + DurationInSeconds = TimeSpan.FromMinutes(30).TotalSeconds, + Title = "Visible event" + } + ]); + + calendarService + .Setup(service => service.GetCalendarEventsAsync(It.Is(calendar => calendar.Id == hiddenCalendar.Id), It.IsAny())) + .ReturnsAsync([ + new CalendarItem + { + Id = Guid.NewGuid(), + CalendarId = hiddenCalendar.Id, + StartDate = new DateTime(2026, 3, 20, 10, 0, 0), + DurationInSeconds = TimeSpan.FromMinutes(30).TotalSeconds, + Title = "Hidden event" + } + ]); + + var accountCalendarStateService = new FakeAccountCalendarStateService( + [visibleCalendarViewModel, hiddenCalendarViewModel], + [visibleCalendarViewModel]); + + var viewModel = CreateViewModel(calendarService.Object, preferencesService.Object, new DateOnly(2026, 3, 20), accountCalendarStateService); + + await viewModel.ApplyDisplayRequestAsync(new CalendarDisplayRequest(CalendarDisplayType.Day, new DateOnly(2026, 3, 20))); + + viewModel.CalendarItems.Should().ContainSingle(item => item.CalendarItem.CalendarId == visibleCalendar.Id); + calendarService.Verify(service => service.GetCalendarEventsAsync(It.Is(calendar => calendar.Id == visibleCalendar.Id), It.IsAny()), Times.Once); + calendarService.Verify(service => service.GetCalendarEventsAsync(It.Is(calendar => calendar.Id == hiddenCalendar.Id), It.IsAny()), Times.Never); + } + + private static CalendarPageViewModel CreateViewModel( + ICalendarService calendarService, + IPreferencesService preferencesService, + DateOnly today) + { + var account = new MailAccount + { + Id = Guid.NewGuid(), + Name = "Primary", + SenderName = "Primary", + Address = "primary@example.com", + ProviderType = MailProviderType.Outlook + }; + + var calendar = CreateCalendar(account, "Calendar"); + var accountCalendarViewModel = new AccountCalendarViewModel(account, calendar); + var accountCalendarStateService = new FakeAccountCalendarStateService([accountCalendarViewModel]); + + return CreateViewModel(calendarService, preferencesService, today, accountCalendarStateService); + } + + private static CalendarPageViewModel CreateViewModel( + ICalendarService calendarService, + IPreferencesService preferencesService, + DateOnly today, + IAccountCalendarStateService accountCalendarStateService) + { + var statePersistenceService = new Mock(); + statePersistenceService.SetupAllProperties(); + statePersistenceService.Object.ApplicationMode = WinoApplicationMode.Calendar; + statePersistenceService.Object.CalendarDisplayType = CalendarDisplayType.Week; + + return new CalendarPageViewModel( + statePersistenceService.Object, + calendarService, + Mock.Of(), + Mock.Of(), + Mock.Of(), + accountCalendarStateService, + preferencesService, + Mock.Of(), + Mock.Of(), + new TestDateContextProvider("en-US", today), + new CalendarRangeTextFormatter()); + } + + private static AccountCalendar CreateCalendar(MailAccount account, string name) + => new() + { + Id = Guid.NewGuid(), + AccountId = account.Id, + Name = name, + RemoteCalendarId = "calendar", + SynchronizationDeltaToken = string.Empty, + TextColorHex = "#000000", + BackgroundColorHex = "#ffffff", + TimeZone = TimeZoneInfo.Utc.Id, + IsExtended = true, + IsPrimary = true, + IsSynchronizationEnabled = true + }; + + private static Mock CreatePreferencesService(CalendarSettings settings) + => CreatePreferencesService(() => settings); + + private static Mock CreatePreferencesService(Func settingsFactory) + { + var preferencesService = new Mock(); + preferencesService.Setup(service => service.GetCurrentCalendarSettings()).Returns(settingsFactory); + return preferencesService; + } + + private static CalendarSettings CreateSettings( + DayOfWeek firstDayOfWeek = DayOfWeek.Monday, + DayOfWeek workWeekStart = DayOfWeek.Monday, + DayOfWeek workWeekEnd = DayOfWeek.Friday, + string cultureName = "en-US") + { + return new CalendarSettings( + firstDayOfWeek, + [DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday], + true, + workWeekStart, + workWeekEnd, + TimeSpan.FromHours(9), + TimeSpan.FromHours(18), + 64, + DayHeaderDisplayType.TwentyFourHour, + CultureInfo.GetCultureInfo(cultureName)); + } + + private sealed class FakeAccountCalendarStateService : IAccountCalendarStateService + { + private readonly List _calendars; + private readonly List _activeCalendars; + private readonly ObservableCollection _groupedCalendars = []; + + public FakeAccountCalendarStateService(IEnumerable calendars, IEnumerable? activeCalendars = null) + { + _calendars = calendars.ToList(); + _activeCalendars = (activeCalendars ?? _calendars.Where(calendar => calendar.IsChecked)).ToList(); + GroupedAccountCalendars = new ReadOnlyObservableCollection(_groupedCalendars); + } + + public IDispatcher Dispatcher { get; set; } = null!; + public ReadOnlyObservableCollection GroupedAccountCalendars { get; } + + public event EventHandler? CollectiveAccountGroupSelectionStateChanged + { + add { } + remove { } + } + + public event EventHandler? AccountCalendarSelectionStateChanged + { + add { } + remove { } + } + + public event PropertyChangedEventHandler? PropertyChanged + { + add { } + remove { } + } + + public IEnumerable ActiveCalendars => _activeCalendars; + public IEnumerable AllCalendars => _calendars; + public ReadOnlyObservableGroupedCollection GroupedCalendars { get; set; } = null!; + + public void AddGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar) => _groupedCalendars.Add(groupedAccountCalendar); + public void RemoveGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar) => _groupedCalendars.Remove(groupedAccountCalendar); + public void ClearGroupedAccountCalendars() => _groupedCalendars.Clear(); + public void AddAccountCalendar(AccountCalendarViewModel accountCalendar) => _calendars.Add(accountCalendar); + public void RemoveAccountCalendar(AccountCalendarViewModel accountCalendar) => _calendars.Remove(accountCalendar); + } + + private sealed class TestDateContextProvider(string cultureName, DateOnly today) : IDateContextProvider + { + public CultureInfo Culture => CultureInfo.GetCultureInfo(cultureName); + public TimeZoneInfo TimeZone => TimeZoneInfo.Utc; + public DateOnly GetToday() => today; + } +} diff --git a/Wino.Core.Tests/CalendarRangeResolverTests.cs b/Wino.Core.Tests/CalendarRangeResolverTests.cs new file mode 100644 index 00000000..20b8e3fe --- /dev/null +++ b/Wino.Core.Tests/CalendarRangeResolverTests.cs @@ -0,0 +1,190 @@ +using System.Globalization; +using FluentAssertions; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models.Calendar; +using Xunit; + +namespace Wino.Core.Tests; + +public class CalendarRangeResolverTests +{ + [Fact] + public void Resolve_Day_ReturnsAnchorDateOnly() + { + var settings = CreateSettings(); + var today = new DateOnly(2026, 3, 20); + + var range = CalendarRangeResolver.Resolve(new CalendarDisplayRequest(CalendarDisplayType.Day, today), settings, today); + + range.StartDate.Should().Be(today); + range.EndDate.Should().Be(today); + range.DayCount.Should().Be(1); + range.ContainsToday.Should().BeTrue(); + range.Dates.Should().ContainSingle().Which.Should().Be(today); + } + + [Fact] + public void Resolve_Week_HonorsConfiguredFirstDayOfWeek() + { + var settings = CreateSettings(firstDayOfWeek: DayOfWeek.Sunday); + var anchor = new DateOnly(2026, 3, 18); + + var range = CalendarRangeResolver.Resolve(new CalendarDisplayRequest(CalendarDisplayType.Week, anchor), settings, today: anchor); + + range.StartDate.Should().Be(new DateOnly(2026, 3, 15)); + range.EndDate.Should().Be(new DateOnly(2026, 3, 21)); + range.DayCount.Should().Be(7); + } + + [Fact] + public void Resolve_WorkWeek_UsesConfiguredBounds() + { + var settings = CreateSettings( + firstDayOfWeek: DayOfWeek.Sunday, + workWeekStart: DayOfWeek.Monday, + workWeekEnd: DayOfWeek.Thursday); + var anchor = new DateOnly(2026, 3, 18); + + var range = CalendarRangeResolver.Resolve(new CalendarDisplayRequest(CalendarDisplayType.WorkWeek, anchor), settings, today: anchor); + + range.StartDate.Should().Be(new DateOnly(2026, 3, 16)); + range.EndDate.Should().Be(new DateOnly(2026, 3, 19)); + range.DayCount.Should().Be(4); + } + + [Fact] + public void Resolve_Month_CoversEntireAnchorMonth() + { + var settings = CreateSettings(); + var anchor = new DateOnly(2026, 2, 14); + + var range = CalendarRangeResolver.Resolve(new CalendarDisplayRequest(CalendarDisplayType.Month, anchor), settings, today: anchor); + + range.StartDate.Should().Be(new DateOnly(2026, 2, 1)); + range.EndDate.Should().Be(new DateOnly(2026, 2, 28)); + range.DayCount.Should().Be(28); + range.SpansSingleMonth.Should().BeTrue(); + } + + [Theory] + [InlineData(CalendarDisplayType.Day, 2026, 3, 18, 2026, 3, 19, 2026, 3, 17)] + [InlineData(CalendarDisplayType.Week, 2026, 3, 18, 2026, 3, 25, 2026, 3, 11)] + [InlineData(CalendarDisplayType.WorkWeek, 2026, 3, 18, 2026, 3, 25, 2026, 3, 11)] + [InlineData(CalendarDisplayType.Month, 2026, 3, 18, 2026, 4, 18, 2026, 2, 18)] + public void Navigate_MovesExactlyOnePeriod( + CalendarDisplayType displayType, + int year, + int month, + int day, + int nextYear, + int nextMonth, + int nextDay, + int previousYear, + int previousMonth, + int previousDay) + { + var settings = CreateSettings(); + var today = new DateOnly(2026, 3, 20); + var current = CalendarRangeResolver.Resolve( + new CalendarDisplayRequest(displayType, new DateOnly(year, month, day)), + settings, + today); + + var next = CalendarRangeResolver.Navigate(current, 1, settings, today); + var previous = CalendarRangeResolver.Navigate(current, -1, settings, today); + + next.AnchorDate.Should().Be(new DateOnly(nextYear, nextMonth, nextDay)); + previous.AnchorDate.Should().Be(new DateOnly(previousYear, previousMonth, previousDay)); + } + + [Fact] + public void ChangeDisplayType_FromMonth_UsesTodayWhenTodayIsInsideCurrentMonth() + { + var settings = CreateSettings(); + var today = new DateOnly(2026, 3, 20); + var monthRange = CalendarRangeResolver.Resolve( + new CalendarDisplayRequest(CalendarDisplayType.Month, new DateOnly(2026, 3, 5)), + settings, + today); + + var dayRange = CalendarRangeResolver.ChangeDisplayType(monthRange, CalendarDisplayType.Day, settings, today); + + dayRange.AnchorDate.Should().Be(today); + dayRange.StartDate.Should().Be(today); + dayRange.EndDate.Should().Be(today); + } + + [Fact] + public void Formatter_Day_UsesMonthDayPattern() + { + var formatter = new CalendarRangeTextFormatter(); + var range = new VisibleDateRange( + CalendarDisplayType.Day, + new DateOnly(2026, 3, 20), + new DateOnly(2026, 3, 20), + new DateOnly(2026, 3, 20), + new DateOnly(2026, 3, 20), + 1, + true, + true, + [new DateOnly(2026, 3, 20)]); + + var text = formatter.Format(range, new TestDateContextProvider("en-US", today: new DateOnly(2026, 3, 20))); + + text.Should().Be("March 20"); + } + + [Fact] + public void Formatter_Range_UsesCultureMonthDayPattern() + { + var formatter = new CalendarRangeTextFormatter(); + var range = new VisibleDateRange( + CalendarDisplayType.Week, + new DateOnly(2026, 3, 20), + new DateOnly(2026, 3, 16), + new DateOnly(2026, 3, 22), + new DateOnly(2026, 3, 20), + 7, + true, + true, + [ + new DateOnly(2026, 3, 16), + new DateOnly(2026, 3, 17), + new DateOnly(2026, 3, 18), + new DateOnly(2026, 3, 19), + new DateOnly(2026, 3, 20), + new DateOnly(2026, 3, 21), + new DateOnly(2026, 3, 22) + ]); + + var text = formatter.Format(range, new TestDateContextProvider("de-DE", today: new DateOnly(2026, 3, 20))); + + text.Should().Be("16. März - 22. März"); + } + + private static CalendarSettings CreateSettings( + DayOfWeek firstDayOfWeek = DayOfWeek.Monday, + DayOfWeek workWeekStart = DayOfWeek.Monday, + DayOfWeek workWeekEnd = DayOfWeek.Friday, + string cultureName = "en-US") + { + return new CalendarSettings( + firstDayOfWeek, + [DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday], + true, + workWeekStart, + workWeekEnd, + TimeSpan.FromHours(9), + TimeSpan.FromHours(18), + 64, + DayHeaderDisplayType.TwentyFourHour, + CultureInfo.GetCultureInfo(cultureName)); + } + + private sealed class TestDateContextProvider(string cultureName, DateOnly today) : IDateContextProvider + { + public CultureInfo Culture => CultureInfo.GetCultureInfo(cultureName); + public TimeZoneInfo TimeZone => TimeZoneInfo.Utc; + public DateOnly GetToday() => today; + } +} diff --git a/Wino.Core.Tests/CalendarRangeTextFormatterTests.cs b/Wino.Core.Tests/CalendarRangeTextFormatterTests.cs new file mode 100644 index 00000000..8c302e64 --- /dev/null +++ b/Wino.Core.Tests/CalendarRangeTextFormatterTests.cs @@ -0,0 +1,103 @@ +using System.Linq; +using FluentAssertions; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models.Calendar; +using Xunit; + +namespace Wino.Core.Tests; + +public class CalendarRangeTextFormatterTests +{ + private static readonly CalendarRangeTextFormatter Formatter = new(); + private static readonly TestDateContextProvider DateContextProvider = new("en-US", new DateOnly(2026, 3, 24)); + + [Fact] + public void Format_ReturnsMonthDay_ForSingleDate() + { + var range = CreateRange( + CalendarDisplayType.Day, + anchorDate: new DateOnly(2026, 3, 6), + startDate: new DateOnly(2026, 3, 6), + endDate: new DateOnly(2026, 3, 6)); + + Formatter.Format(range, DateContextProvider).Should().Be("March 6"); + } + + [Fact] + public void Format_ReturnsFullRange_ForDatesInSameMonth() + { + var range = CreateRange( + CalendarDisplayType.Week, + anchorDate: new DateOnly(2026, 3, 6), + startDate: new DateOnly(2026, 3, 3), + endDate: new DateOnly(2026, 3, 10)); + + Formatter.Format(range, DateContextProvider).Should().Be("March 3 - 10"); + } + + [Fact] + public void Format_ReturnsFullRange_ForDatesInDifferentMonths() + { + var range = CreateRange( + CalendarDisplayType.Week, + anchorDate: new DateOnly(2026, 4, 2), + startDate: new DateOnly(2026, 3, 30), + endDate: new DateOnly(2026, 4, 7)); + + Formatter.Format(range, DateContextProvider).Should().Be("March 30 - April 7"); + } + + [Fact] + public void Format_ReturnsAnchorMonth_WhenVisibleRangeSpansMonthGrid() + { + var range = CreateRange( + CalendarDisplayType.Month, + anchorDate: new DateOnly(2026, 3, 12), + startDate: new DateOnly(2026, 2, 23), + endDate: new DateOnly(2026, 4, 5)); + + Formatter.Format(range, DateContextProvider).Should().Be("March 2026"); + } + + [Fact] + public void Format_ReturnsAnchorMonth_WhenVisibleRangeHasExactlyTwentyEightDays() + { + var range = CreateRange( + CalendarDisplayType.Month, + anchorDate: new DateOnly(2026, 2, 14), + startDate: new DateOnly(2026, 2, 1), + endDate: new DateOnly(2026, 2, 28)); + + Formatter.Format(range, DateContextProvider).Should().Be("February 2026"); + } + + private static VisibleDateRange CreateRange( + CalendarDisplayType displayType, + DateOnly anchorDate, + DateOnly startDate, + DateOnly endDate) + { + var dayCount = endDate.DayNumber - startDate.DayNumber + 1; + var dates = Enumerable.Range(0, dayCount) + .Select(offset => startDate.AddDays(offset)) + .ToArray(); + + return new VisibleDateRange( + displayType, + anchorDate, + startDate, + endDate, + anchorDate, + dayCount, + ContainsToday: false, + SpansSingleMonth: startDate.Month == endDate.Month && startDate.Year == endDate.Year, + Dates: dates); + } + + private sealed class TestDateContextProvider(string cultureName, DateOnly today) : IDateContextProvider + { + public System.Globalization.CultureInfo Culture => System.Globalization.CultureInfo.GetCultureInfo(cultureName); + public TimeZoneInfo TimeZone => TimeZoneInfo.Utc; + public DateOnly GetToday() => today; + } +} diff --git a/Wino.Core.Tests/CalendarReminderSnoozeOptionsTests.cs b/Wino.Core.Tests/CalendarReminderSnoozeOptionsTests.cs new file mode 100644 index 00000000..561e3024 --- /dev/null +++ b/Wino.Core.Tests/CalendarReminderSnoozeOptionsTests.cs @@ -0,0 +1,48 @@ +using FluentAssertions; +using Wino.Core.Domain; +using Xunit; + +namespace Wino.Core.Tests; + +public class CalendarReminderSnoozeOptionsTests +{ + [Fact] + public void GetAllowedSnoozeMinutes_WhenDefaultIs15AndReminderIs15_Excludes30() + { + var options = CalendarReminderSnoozeOptions.GetAllowedSnoozeMinutes( + reminderDurationInSeconds: 15 * 60, + defaultReminderDurationInSeconds: 15 * 60); + + options.Should().Equal(5, 10, 15); + } + + [Fact] + public void GetAllowedSnoozeMinutes_WhenReminderIs5AndDefaultIs15_DoesNotPassEventStart() + { + var options = CalendarReminderSnoozeOptions.GetAllowedSnoozeMinutes( + reminderDurationInSeconds: 5 * 60, + defaultReminderDurationInSeconds: 15 * 60); + + options.Should().Equal(5); + } + + [Fact] + public void GetAllowedSnoozeMinutes_WhenDefaultReminderIsNone_UsesReminderDurationOnly() + { + var options = CalendarReminderSnoozeOptions.GetAllowedSnoozeMinutes( + reminderDurationInSeconds: 30 * 60, + defaultReminderDurationInSeconds: 0); + + options.Should().Equal(5, 10, 15, 30); + } + + [Fact] + public void GetAllowedSnoozeMinutes_WhenReminderIsUnderFiveMinutes_ReturnsNoOptions() + { + var options = CalendarReminderSnoozeOptions.GetAllowedSnoozeMinutes( + reminderDurationInSeconds: 60, + defaultReminderDurationInSeconds: 15 * 60); + + options.Should().BeEmpty(); + } +} diff --git a/Wino.Core.Tests/GlobalUsings.cs b/Wino.Core.Tests/GlobalUsings.cs new file mode 100644 index 00000000..29ee885e --- /dev/null +++ b/Wino.Core.Tests/GlobalUsings.cs @@ -0,0 +1,4 @@ +global using System; +global using System.Collections.Generic; +global using System.Linq; +global using System.Threading.Tasks; diff --git a/Wino.Core.Tests/Helpers/InMemoryDatabaseService.cs b/Wino.Core.Tests/Helpers/InMemoryDatabaseService.cs new file mode 100644 index 00000000..2c027196 --- /dev/null +++ b/Wino.Core.Tests/Helpers/InMemoryDatabaseService.cs @@ -0,0 +1,69 @@ +using SQLite; +using System.IO; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Entities.Shared; +using Wino.Services; + +namespace Wino.Core.Tests.Helpers; + +/// +/// In-memory database service for testing purposes. +/// Creates a temporary SQLite database in memory that is destroyed after tests complete. +/// +public class InMemoryDatabaseService : IDatabaseService +{ + private readonly string _databasePath; + public SQLiteAsyncConnection Connection { get; private set; } + + public InMemoryDatabaseService() + { + // Use a unique temporary file per test instance for stable async access. + _databasePath = Path.Combine(Path.GetTempPath(), $"wino-tests-{Guid.NewGuid():N}.db"); + Connection = new SQLiteAsyncConnection(_databasePath); + } + + public async Task InitializeAsync() + { + await CreateTablesAsync(); + } + + private async Task CreateTablesAsync() + { + // Keep table creation sequential for in-memory SQLite to avoid connection contention. + await Connection.CreateTableAsync(); + await Connection.CreateTableAsync(); + await Connection.CreateTableAsync(); + await Connection.CreateTableAsync(); + await Connection.CreateTableAsync(); + await Connection.CreateTableAsync(); + await Connection.CreateTableAsync(); + await Connection.CreateTableAsync(); + await Connection.CreateTableAsync(); + await Connection.CreateTableAsync(); + await Connection.CreateTableAsync(); + await Connection.CreateTableAsync(); + await Connection.CreateTableAsync(); + await Connection.CreateTableAsync(); + await Connection.CreateTableAsync(); + await Connection.CreateTableAsync(); + await Connection.CreateTableAsync(); + await Connection.CreateTableAsync(); + await Connection.CreateTableAsync(); + await Connection.CreateTableAsync(); + } + + public async ValueTask DisposeAsync() + { + if (Connection != null) + { + await Connection.CloseAsync(); + Connection = null!; + } + + if (File.Exists(_databasePath)) + { + File.Delete(_databasePath); + } + } +} diff --git a/Wino.Core.Tests/Models/HtmlPreviewVisitorTests.cs b/Wino.Core.Tests/Models/HtmlPreviewVisitorTests.cs new file mode 100644 index 00000000..7fe2b080 --- /dev/null +++ b/Wino.Core.Tests/Models/HtmlPreviewVisitorTests.cs @@ -0,0 +1,76 @@ +using Xunit; +using FluentAssertions; +using MimeKit; +using Wino.Core.Domain.Models.MailItem; + +namespace Wino.Core.Tests.Models; + +public class HtmlPreviewVisitorTests +{ + [Fact] + public void HtmlPreviewVisitor_Should_Remove_Blocked_Tags_And_Event_Attributes() + { + // Arrange + var html = """ + + +

hello

+ + + + + + + """; + + var message = new MimeMessage(); + message.Body = new TextPart("html") { Text = html }; + + var visitor = new HtmlPreviewVisitor(Path.GetTempPath()); + + // Act + message.Accept(visitor); + var output = visitor.HtmlBody; + + // Assert + output.Should().NotContain(" + + safe + bad + + + + + """; + + var message = new MimeMessage(); + message.Body = new TextPart("html") { Text = html }; + + var visitor = new HtmlPreviewVisitor(Path.GetTempPath()); + + // Act + message.Accept(visitor); + var output = visitor.HtmlBody; + + // Assert + output.Should().Contain("id=\"safe-link\" href=\"https://contoso.com/path\"", "http/https links should be preserved"); + output.Should().Contain("id=\"js-link\"", "the element should remain"); + output.Should().NotContain("href=\"javascript:", "javascript URLs must be removed"); + output.Should().Contain("id=\"allowed\" src=\"data:image/png;base64", "safe image data URLs should be preserved"); + output.Should().NotContain("id=\"svg-script\" src=\"data:text/html", "non-image data URLs should be removed"); + } +} diff --git a/Wino.Core.Tests/Services/AccountServiceTests.cs b/Wino.Core.Tests/Services/AccountServiceTests.cs new file mode 100644 index 00000000..69c69dfd --- /dev/null +++ b/Wino.Core.Tests/Services/AccountServiceTests.cs @@ -0,0 +1,162 @@ +using System; +using System.Linq; +using FluentAssertions; +using Moq; +using Wino.Core.Domain; +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Misc; +using Wino.Core.Tests.Helpers; +using Wino.Services; +using Xunit; + +namespace Wino.Core.Tests.Services; + +public class AccountServiceTests : IAsyncLifetime +{ + private InMemoryDatabaseService _databaseService = null!; + private AccountService _accountService = null!; + + public async Task InitializeAsync() + { + _databaseService = new InMemoryDatabaseService(); + await _databaseService.InitializeAsync(); + _accountService = CreateService(_databaseService); + } + + public async Task DisposeAsync() + { + await _databaseService.DisposeAsync(); + } + + [Fact] + public async Task CreateAccountAsync_ImapLocalOnly_CreatesSinglePrimaryDefaultCalendar() + { + var accountId = Guid.NewGuid(); + var account = CreateImapAccount(accountId); + var server = new CustomServerInformation + { + Id = Guid.NewGuid(), + AccountId = accountId, + CalendarSupportMode = ImapCalendarSupportMode.LocalOnly + }; + + await _accountService.CreateAccountAsync(account, server); + + var calendars = await _databaseService.Connection.Table() + .Where(a => a.AccountId == accountId) + .ToListAsync(); + + calendars.Should().HaveCount(1); + calendars[0].IsPrimary.Should().BeTrue(); + calendars[0].Name.Should().Be(Translator.AccountDetailsPage_TabCalendar); + ColorHelpers.GetFlatColorPalette().Should().Contain(calendars[0].BackgroundColorHex); + } + + [Fact] + public async Task CreateAccountAsync_ImapCalDav_DoesNotCreateDefaultLocalCalendar() + { + var accountId = Guid.NewGuid(); + var account = CreateImapAccount(accountId); + var server = new CustomServerInformation + { + Id = Guid.NewGuid(), + AccountId = accountId, + CalendarSupportMode = ImapCalendarSupportMode.CalDav + }; + + await _accountService.CreateAccountAsync(account, server); + + var calendars = await _databaseService.Connection.Table() + .Where(a => a.AccountId == accountId) + .ToListAsync(); + + calendars.Should().BeEmpty(); + } + + [Fact] + public async Task CreateAccountAsync_ImapLocalOnly_AssignsDistinctCalendarColorsAcrossAccounts() + { + var firstAccountId = Guid.NewGuid(); + var secondAccountId = Guid.NewGuid(); + + await _accountService.CreateAccountAsync( + CreateImapAccount(firstAccountId), + new CustomServerInformation + { + Id = Guid.NewGuid(), + AccountId = firstAccountId, + CalendarSupportMode = ImapCalendarSupportMode.LocalOnly + }); + + await _accountService.CreateAccountAsync( + CreateImapAccount(secondAccountId), + new CustomServerInformation + { + Id = Guid.NewGuid(), + AccountId = secondAccountId, + CalendarSupportMode = ImapCalendarSupportMode.LocalOnly + }); + + var calendars = await _databaseService.Connection.Table() + .OrderBy(a => a.AccountId) + .ToListAsync(); + + calendars.Should().HaveCount(2); + calendars.Select(a => a.BackgroundColorHex).Should().OnlyHaveUniqueItems(); + calendars.Should().OnlyContain(a => ColorHelpers.GetFlatColorPalette().Contains(a.BackgroundColorHex)); + } + + [Fact] + public void FlatCalendarPalette_ProvidesAtLeastFiftyDistinctColors() + { + ColorHelpers.GetFlatColorPalette() + .Distinct(StringComparer.OrdinalIgnoreCase) + .Count() + .Should() + .BeGreaterThanOrEqualTo(50); + } + + private static MailAccount CreateImapAccount(Guid accountId) + { + return new MailAccount + { + Id = accountId, + Name = "IMAP Test Account", + Address = "imap@test.local", + SenderName = "IMAP Test", + ProviderType = MailProviderType.IMAP4 + }; + } + + private static AccountService CreateService(InMemoryDatabaseService databaseService) + { + var signatureService = new Mock(); + signatureService + .Setup(a => a.CreateDefaultSignatureAsync(It.IsAny())) + .ReturnsAsync((Guid accountId) => new AccountSignature + { + Id = Guid.NewGuid(), + MailAccountId = accountId, + Name = "Default", + HtmlBody = string.Empty + }); + + var authenticationProvider = new Mock(); + var mimeFileService = new Mock(); + var contactPictureFileService = new Mock(); + + var preferencesService = new Mock(); + preferencesService.SetupProperty(a => a.StartupEntityId); + + return new AccountService( + databaseService, + signatureService.Object, + authenticationProvider.Object, + mimeFileService.Object, + preferencesService.Object, + contactPictureFileService.Object); + } +} diff --git a/Wino.Core.Tests/Services/AutoDiscoveryServiceTests.cs b/Wino.Core.Tests/Services/AutoDiscoveryServiceTests.cs new file mode 100644 index 00000000..fd1dd8c1 --- /dev/null +++ b/Wino.Core.Tests/Services/AutoDiscoveryServiceTests.cs @@ -0,0 +1,221 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text; +using FluentAssertions; +using Wino.Core.Domain.Models.AutoDiscovery; +using Wino.Core.Services; +using Xunit; + +namespace Wino.Core.Tests.Services; + +public class AutoDiscoveryServiceTests +{ + [Fact] + public async Task GetAutoDiscoverySettings_UsesThunderbirdAutoconfig_WhenAvailable() + { + var handler = new StubHttpMessageHandler(request => + { + var uri = request.RequestUri!.ToString(); + + if (uri.StartsWith("https://autoconfig.example.com/mail/config-v1.1.xml", StringComparison.OrdinalIgnoreCase)) + { + return CreateXmlResponse(""" + + + + imap.example.com + 993 + SSL + %EMAILLOCALPART% + + + smtp.example.com + 587 + STARTTLS + %EMAILADDRESS% + + + + """, request); + } + + return CreateStatusResponse(HttpStatusCode.NotFound, request); + }); + + using var client = new HttpClient(handler); + var sut = new AutoDiscoveryService(client); + + var settings = await sut.GetAutoDiscoverySettings(new AutoDiscoveryMinimalSettings + { + Email = "user@example.com", + DisplayName = "User", + Password = "secret" + }); + + settings.Should().NotBeNull(); + settings!.Domain.Should().Be("example.com"); + settings.GetImapSettings()!.Address.Should().Be("imap.example.com"); + settings.GetImapSettings()!.Username.Should().Be("user"); + settings.GetSmptpSettings()!.Address.Should().Be("smtp.example.com"); + settings.GetSmptpSettings()!.Username.Should().Be("user@example.com"); + handler.RequestedUris.Should().NotContain(uri => uri.Contains("emailsettings.firetrust.com", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task GetAutoDiscoverySettings_FallsBackToFiretrust_WhenThunderbirdMethodsFail() + { + var handler = new StubHttpMessageHandler(request => + { + var uri = request.RequestUri!.ToString(); + + if (uri.StartsWith("https://emailsettings.firetrust.com/settings?q=", StringComparison.OrdinalIgnoreCase)) + { + return CreateJsonResponse(""" + { + "domain": "example.com", + "settings": [ + { + "protocol": "IMAP", + "address": "imap.firetrust.example.com", + "port": 993, + "secure": "SSL", + "username": "user@example.com" + }, + { + "protocol": "SMTP", + "address": "smtp.firetrust.example.com", + "port": 587, + "secure": "STARTTLS", + "username": "user@example.com" + } + ] + } + """, request); + } + + if (uri.StartsWith("https://dns.google/resolve", StringComparison.OrdinalIgnoreCase)) + { + return CreateJsonResponse("{\"Status\":0}", request); + } + + return CreateStatusResponse(HttpStatusCode.NotFound, request); + }); + + using var client = new HttpClient(handler); + var sut = new AutoDiscoveryService(client); + + var settings = await sut.GetAutoDiscoverySettings(new AutoDiscoveryMinimalSettings + { + Email = "user@example.com" + }); + + settings.Should().NotBeNull(); + settings!.GetImapSettings()!.Address.Should().Be("imap.firetrust.example.com"); + settings.GetSmptpSettings()!.Address.Should().Be("smtp.firetrust.example.com"); + handler.RequestedUris.Should().Contain(uri => uri.Contains("emailsettings.firetrust.com", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task DiscoverCalDavServiceUriAsync_ReturnsKnownYahooEndpoint() + { + var sut = new AutoDiscoveryService(new HttpClient(new StubHttpMessageHandler(_ => throw new InvalidOperationException("No network call expected")))); + + var uri = await sut.DiscoverCalDavServiceUriAsync("user@yahoo.com"); + + uri.Should().Be(new Uri("https://caldav.calendar.yahoo.com/")); + } + + [Fact] + public async Task DiscoverCalDavServiceUriAsync_ResolvesWellKnownRedirect() + { + var handler = new StubHttpMessageHandler(request => + { + var uri = request.RequestUri!.ToString(); + + if (uri.Equals("https://calendar.example.com/.well-known/caldav", StringComparison.OrdinalIgnoreCase)) + { + var response = CreateStatusResponse(HttpStatusCode.Found, request); + response.Headers.Location = new Uri("https://dav.example.net/caldav/"); + return response; + } + + return CreateStatusResponse(HttpStatusCode.NotFound, request); + }); + + using var client = new HttpClient(handler); + var sut = new AutoDiscoveryService(client); + + var uri = await sut.DiscoverCalDavServiceUriAsync("user@calendar.example.com"); + + uri.Should().Be(new Uri("https://dav.example.net/caldav/")); + } + + private static HttpResponseMessage CreateXmlResponse(string xml, HttpRequestMessage request) + => new(HttpStatusCode.OK) + { + RequestMessage = request, + Content = new StringContent(xml, Encoding.UTF8, "application/xml") + }; + + private static HttpResponseMessage CreateJsonResponse(string json, HttpRequestMessage request) + => new(HttpStatusCode.OK) + { + RequestMessage = request, + Content = new StringContent(json, Encoding.UTF8, "application/json") + }; + + private static HttpResponseMessage CreateStatusResponse(HttpStatusCode statusCode, HttpRequestMessage request) + => new(statusCode) + { + RequestMessage = request + }; + + private sealed class StubHttpMessageHandler(Func responseFactory) : HttpMessageHandler + { + private readonly Func _responseFactory = responseFactory; + + public List RequestedUris { get; } = []; + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + RequestedUris.Add(request.RequestUri?.ToString() ?? string.Empty); + var response = _responseFactory(request); + + // Ensure probing logic sees a request URI even if response factory forgot it. + response.RequestMessage ??= request; + + if (response.Headers.Date == null) + { + response.Headers.Date = DateTimeOffset.UtcNow; + } + + if (!response.Headers.Contains("DAV") && + response.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden) + { + response.Headers.Add("DAV", "1, calendar-access"); + } + + var hasAllowHeader = response.Headers.TryGetValues("Allow", out var allowValues) && + allowValues.Any(); + + if (!hasAllowHeader && + response.StatusCode == HttpStatusCode.OK && + request.Method == HttpMethod.Options) + { + response.Headers.Add("Allow", "PROPFIND"); + } + + if (response.Content == null) + { + response.Content = new StringContent(string.Empty, Encoding.UTF8, "text/plain"); + } + + if (response.Content.Headers.ContentType == null) + { + response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/plain"); + } + + return Task.FromResult(response); + } + } +} diff --git a/Wino.Core.Tests/Services/CalendarEventComposeResultValidatorTests.cs b/Wino.Core.Tests/Services/CalendarEventComposeResultValidatorTests.cs new file mode 100644 index 00000000..31a7ad85 --- /dev/null +++ b/Wino.Core.Tests/Services/CalendarEventComposeResultValidatorTests.cs @@ -0,0 +1,166 @@ +using FluentAssertions; +using System.IO; +using Wino.Core.Domain; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Exceptions; +using Wino.Core.Domain.Models.Calendar; +using Wino.Core.Domain.Validation; +using Xunit; + +namespace Wino.Core.Tests.Services; + +public sealed class CalendarEventComposeResultValidatorTests +{ + private readonly CalendarEventComposeResultValidator _validator = new(); + + [Fact] + public void Validate_WhenResultIsValid_DoesNotThrow() + { + var tempFilePath = Path.GetTempFileName(); + + try + { + var result = CreateValidResult(); + result.Attachments.Add(new CalendarEventComposeAttachmentDraft + { + Id = Guid.NewGuid(), + FileName = Path.GetFileName(tempFilePath), + FilePath = tempFilePath, + FileExtension = ".tmp", + Size = 12 + }); + + Action act = () => _validator.Validate(result); + + act.Should().NotThrow(); + } + finally + { + File.Delete(tempFilePath); + } + } + + [Fact] + public void Validate_WhenEndDateIsBeforeStartDate_ThrowsValidationException() + { + var result = CreateValidResult(); + result.EndDate = result.StartDate.AddMinutes(-30); + + Action act = () => _validator.Validate(result); + + act.Should() + .Throw() + .WithMessage(Translator.CalendarEventCompose_ValidationInvalidTimeRange); + } + + [Fact] + public void Validate_WhenAllDayEndDateMatchesStartDate_ThrowsValidationException() + { + var result = CreateValidResult(); + result.IsAllDay = true; + result.EndDate = result.StartDate; + + Action act = () => _validator.Validate(result); + + act.Should() + .Throw() + .WithMessage(Translator.CalendarEventCompose_ValidationInvalidAllDayRange); + } + + [Fact] + public void Validate_WhenAttachmentDoesNotExist_ThrowsValidationException() + { + var result = CreateValidResult(); + result.Attachments.Add(new CalendarEventComposeAttachmentDraft + { + Id = Guid.NewGuid(), + FileName = "missing.txt", + FilePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.txt"), + FileExtension = ".txt", + Size = 42 + }); + + Action act = () => _validator.Validate(result); + + act.Should() + .Throw() + .WithMessage(string.Format(Translator.CalendarEventCompose_ValidationMissingAttachment, "missing.txt")); + } + + [Fact] + public void Validate_WhenAttendeeEmailIsInvalid_ThrowsValidationException() + { + var result = CreateValidResult(); + result.Attendees.Add(new CalendarEventAttendee + { + Id = Guid.NewGuid(), + CalendarItemId = Guid.Empty, + Email = "not-an-email" + }); + + Action act = () => _validator.Validate(result); + + act.Should() + .Throw() + .WithMessage(Translator.CalendarEventCompose_ValidationInvalidAttendee); + } + + [Fact] + public void Validate_WhenAttendeeEmailIsDuplicated_ThrowsValidationException() + { + var result = CreateValidResult(); + result.Attendees.Add(new CalendarEventAttendee + { + Id = Guid.NewGuid(), + CalendarItemId = Guid.Empty, + Email = "person@example.com" + }); + result.Attendees.Add(new CalendarEventAttendee + { + Id = Guid.NewGuid(), + CalendarItemId = Guid.Empty, + Email = "PERSON@example.com" + }); + + Action act = () => _validator.Validate(result); + + act.Should() + .Throw() + .WithMessage(Translator.CalendarEventCompose_ValidationInvalidAttendee); + } + + [Fact] + public void Validate_WhenCalendarIdIsMissing_ThrowsValidationException() + { + var result = CreateValidResult(); + result.CalendarId = Guid.Empty; + + Action act = () => _validator.Validate(result); + + act.Should() + .Throw() + .WithMessage(Translator.CalendarEventCompose_ValidationMissingCalendar); + } + + private static CalendarEventComposeResult CreateValidResult() + { + return new CalendarEventComposeResult + { + CalendarId = Guid.NewGuid(), + AccountId = Guid.NewGuid(), + Title = "Design review", + StartDate = new DateTime(2026, 3, 7, 13, 30, 0), + EndDate = new DateTime(2026, 3, 7, 14, 0, 0), + TimeZoneId = TimeZoneInfo.Local.Id, + SelectedReminders = + [ + new Reminder + { + Id = Guid.NewGuid(), + DurationInSeconds = 900 + } + ] + }; + } +} diff --git a/Wino.Core.Tests/Services/CalendarReminderServiceTests.cs b/Wino.Core.Tests/Services/CalendarReminderServiceTests.cs new file mode 100644 index 00000000..23e9b62b --- /dev/null +++ b/Wino.Core.Tests/Services/CalendarReminderServiceTests.cs @@ -0,0 +1,256 @@ +using FluentAssertions; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; +using Wino.Core.Tests.Helpers; +using Wino.Services; +using Xunit; + +namespace Wino.Core.Tests.Services; + +public class CalendarReminderServiceTests : IAsyncLifetime +{ + private InMemoryDatabaseService _databaseService = null!; + private CalendarService _calendarService = null!; + private AccountCalendar _testCalendar = null!; + + public async Task InitializeAsync() + { + _databaseService = new InMemoryDatabaseService(); + await _databaseService.InitializeAsync(); + _calendarService = new CalendarService(_databaseService); + + var account = new MailAccount + { + Id = Guid.NewGuid(), + Name = "Reminder Test", + Address = "reminder@test.local", + SenderName = "Reminder Test", + IsCalendarAccessGranted = true + }; + + await _databaseService.Connection.InsertAsync(account, typeof(MailAccount)); + + _testCalendar = new AccountCalendar + { + Id = Guid.NewGuid(), + AccountId = account.Id, + Name = "Test Calendar", + TimeZone = "UTC", + IsPrimary = true, + BackgroundColorHex = "#0A84FF", + TextColorHex = "#FFFFFF" + }; + + await _calendarService.InsertAccountCalendarAsync(_testCalendar); + } + + public async Task DisposeAsync() + { + await _databaseService.DisposeAsync(); + } + + [Fact] + public async Task CheckAndNotifyAsync_WhenReminderFallsWithinWindow_ReturnsDueReminder() + { + var nowLocal = new DateTime(2026, 1, 1, 10, 0, 0); + var lastCheckLocal = nowLocal.AddSeconds(-30); + + var calendarItem = await CreateCalendarItemWithReminderAsync( + startDate: nowLocal.AddMinutes(5), + reminderDurationInSeconds: 5 * 60, + reminderType: CalendarItemReminderType.Popup); + + HashSet sentReminderKeys = []; + + var due = await _calendarService.CheckAndNotifyAsync(lastCheckLocal, nowLocal, sentReminderKeys); + + due.Should().HaveCount(1); + due[0].CalendarItem.Id.Should().Be(calendarItem.Id); + due[0].ReminderDurationInSeconds.Should().Be(5 * 60); + due[0].ReminderKey.Should().StartWith($"{calendarItem.Id:N}:{5 * 60}:"); + sentReminderKeys.Should().ContainSingle(k => k.StartsWith($"{calendarItem.Id:N}:{5 * 60}:")); + } + + [Fact] + public async Task CheckAndNotifyAsync_WhenReminderIsOutsideWindow_ReturnsEmpty() + { + var nowLocal = new DateTime(2026, 1, 1, 10, 0, 0); + var lastCheckLocal = nowLocal.AddSeconds(-30); + + await CreateCalendarItemWithReminderAsync( + startDate: nowLocal.AddMinutes(20), + reminderDurationInSeconds: 5 * 60, + reminderType: CalendarItemReminderType.Popup); + + HashSet sentReminderKeys = []; + + var due = await _calendarService.CheckAndNotifyAsync(lastCheckLocal, nowLocal, sentReminderKeys); + + due.Should().BeEmpty(); + } + + [Fact] + public async Task CheckAndNotifyAsync_WhenReminderAlreadySent_DoesNotReturnDuplicate() + { + var nowLocal = new DateTime(2026, 1, 1, 10, 0, 0); + var lastCheckLocal = nowLocal.AddSeconds(-30); + + var calendarItem = await CreateCalendarItemWithReminderAsync( + startDate: nowLocal.AddMinutes(5), + reminderDurationInSeconds: 5 * 60, + reminderType: CalendarItemReminderType.Popup); + + HashSet sentReminderKeys = []; + + var firstRun = await _calendarService.CheckAndNotifyAsync(lastCheckLocal, nowLocal, sentReminderKeys); + var secondRun = await _calendarService.CheckAndNotifyAsync(lastCheckLocal, nowLocal, sentReminderKeys); + + firstRun.Should().HaveCount(1); + secondRun.Should().BeEmpty(); + sentReminderKeys.Should().ContainSingle(k => k.StartsWith($"{calendarItem.Id:N}:{5 * 60}:")); + } + + [Fact] + public async Task CheckAndNotifyAsync_WhenCalendarAccessNotGranted_ReturnsEmpty() + { + var restrictedAccount = new MailAccount + { + Id = Guid.NewGuid(), + Name = "No Calendar Access", + Address = "restricted@test.local", + SenderName = "Restricted", + IsCalendarAccessGranted = false + }; + await _databaseService.Connection.InsertAsync(restrictedAccount, typeof(MailAccount)); + + var restrictedCalendar = new AccountCalendar + { + Id = Guid.NewGuid(), + AccountId = restrictedAccount.Id, + Name = "Restricted Calendar", + TimeZone = "UTC", + IsPrimary = true, + BackgroundColorHex = "#111111", + TextColorHex = "#FFFFFF" + }; + await _calendarService.InsertAccountCalendarAsync(restrictedCalendar); + + var nowLocal = new DateTime(2026, 1, 1, 10, 0, 0); + var lastCheckLocal = nowLocal.AddSeconds(-30); + + await CreateCalendarItemWithReminderAsync( + startDate: nowLocal.AddMinutes(5), + reminderDurationInSeconds: 5 * 60, + reminderType: CalendarItemReminderType.Popup, + calendarId: restrictedCalendar.Id); + + HashSet sentReminderKeys = []; + + var due = await _calendarService.CheckAndNotifyAsync(lastCheckLocal, nowLocal, sentReminderKeys); + + due.Should().BeEmpty(); + } + + [Fact] + public async Task CheckAndNotifyAsync_WhenReminderTypeIsEmail_ReturnsEmpty() + { + var nowLocal = new DateTime(2026, 1, 1, 10, 0, 0); + var lastCheckLocal = nowLocal.AddSeconds(-30); + + await CreateCalendarItemWithReminderAsync( + startDate: nowLocal.AddMinutes(5), + reminderDurationInSeconds: 5 * 60, + reminderType: CalendarItemReminderType.Email); + + HashSet sentReminderKeys = []; + + var due = await _calendarService.CheckAndNotifyAsync(lastCheckLocal, nowLocal, sentReminderKeys); + + due.Should().BeEmpty(); + } + + [Fact] + public async Task CheckAndNotifyAsync_WhenItemIsRecurringParent_ReturnsEmpty() + { + var nowLocal = new DateTime(2026, 1, 1, 10, 0, 0); + var lastCheckLocal = nowLocal.AddSeconds(-30); + + await CreateCalendarItemWithReminderAsync( + startDate: nowLocal.AddMinutes(5), + reminderDurationInSeconds: 5 * 60, + reminderType: CalendarItemReminderType.Popup, + recurrence: "RRULE:FREQ=DAILY;COUNT=5"); + + HashSet sentReminderKeys = []; + + var due = await _calendarService.CheckAndNotifyAsync(lastCheckLocal, nowLocal, sentReminderKeys); + + due.Should().BeEmpty(); + } + + + [Fact] + public async Task CheckAndNotifyAsync_WhenItemIsSnoozed_TriggersAtSnoozedTime() + { + var nowLocal = new DateTime(2026, 1, 1, 10, 0, 0); + var lastCheckLocal = nowLocal.AddSeconds(-30); + + var calendarItem = await CreateCalendarItemWithReminderAsync( + startDate: nowLocal.AddMinutes(5), + reminderDurationInSeconds: 5 * 60, + reminderType: CalendarItemReminderType.Popup); + + await _calendarService.SnoozeCalendarItemAsync(calendarItem.Id, nowLocal.AddMinutes(10)); + + HashSet sentReminderKeys = []; + + var dueAtOriginalTrigger = await _calendarService.CheckAndNotifyAsync(lastCheckLocal, nowLocal, sentReminderKeys); + dueAtOriginalTrigger.Should().BeEmpty(); + + var snoozeTriggerWindowStart = nowLocal.AddMinutes(10).AddSeconds(-30); + var snoozeTriggerWindowEnd = nowLocal.AddMinutes(10); + + var dueAtSnoozeTime = await _calendarService.CheckAndNotifyAsync(snoozeTriggerWindowStart, snoozeTriggerWindowEnd, sentReminderKeys); + + dueAtSnoozeTime.Should().HaveCount(1); + dueAtSnoozeTime[0].CalendarItem.Id.Should().Be(calendarItem.Id); + dueAtSnoozeTime[0].ReminderKey.Should().StartWith($"{calendarItem.Id:N}:{5 * 60}:"); + } + + private async Task CreateCalendarItemWithReminderAsync( + DateTime startDate, + long reminderDurationInSeconds, + CalendarItemReminderType reminderType, + Guid? calendarId = null, + string? recurrence = null) + { + var item = new CalendarItem + { + Id = Guid.NewGuid(), + Title = "Reminder Test Event", + StartDate = startDate, + StartTimeZone = string.Empty, + EndTimeZone = string.Empty, + DurationInSeconds = 60 * 30, + CalendarId = calendarId ?? _testCalendar.Id, + IsHidden = false, + Recurrence = recurrence ?? string.Empty + }; + + await _calendarService.CreateNewCalendarItemAsync(item, null); + + await _calendarService.SaveRemindersAsync(item.Id, + [ + new Reminder + { + Id = Guid.NewGuid(), + CalendarItemId = item.Id, + DurationInSeconds = reminderDurationInSeconds, + ReminderType = reminderType + } + ]); + + return item; + } +} diff --git a/Wino.Core.Tests/Services/CalendarServiceTests.cs b/Wino.Core.Tests/Services/CalendarServiceTests.cs new file mode 100644 index 00000000..91df77c0 --- /dev/null +++ b/Wino.Core.Tests/Services/CalendarServiceTests.cs @@ -0,0 +1,277 @@ +using FluentAssertions; +using Itenso.TimePeriod; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Tests.Helpers; +using Wino.Services; +using Xunit; + +namespace Wino.Core.Tests.Services; + +/// +/// Tests for CalendarService, focusing on the GetCalendarEventsAsync method. +/// Note: Recurring event occurrences are now synced from the server as individual instances, +/// not calculated locally from recurrence patterns. +/// +public class CalendarServiceTests : IAsyncLifetime +{ + private InMemoryDatabaseService _databaseService = null!; + private CalendarService _calendarService = null!; + private AccountCalendar _testCalendar = null!; + + public async Task InitializeAsync() + { + _databaseService = new InMemoryDatabaseService(); + await _databaseService.InitializeAsync(); + _calendarService = new CalendarService(_databaseService); + + // Create a test calendar + _testCalendar = new AccountCalendar + { + Id = Guid.NewGuid(), + AccountId = Guid.NewGuid(), + Name = "Test Calendar", + TimeZone = "UTC", + IsPrimary = true, + BackgroundColorHex = "#FF5733", + TextColorHex = "#FFFFFF" + }; + + await _calendarService.InsertAccountCalendarAsync(_testCalendar); + } + + public async Task DisposeAsync() + { + await _databaseService.DisposeAsync(); + } + + [Fact] + public async Task GetCalendarEventsAsync_WithNoEvents_ReturnsEmptyList() + { + // Arrange + var period = new TimeRange(DateTime.UtcNow.Date, DateTime.UtcNow.Date.AddDays(7)); + + // Act + var result = await _calendarService.GetCalendarEventsAsync(_testCalendar, period); + + // Assert + result.Should().BeEmpty(); + } + + [Fact] + public async Task GetCalendarEventsAsync_WithSingleNonRecurringEvent_ReturnsEvent() + { + // Arrange + var startDate = new DateTime(2025, 1, 15, 10, 0, 0, DateTimeKind.Utc); + var calendarItem = new CalendarItem + { + Id = Guid.NewGuid(), + Title = "Team Meeting", + Description = "Weekly sync", + StartDate = startDate, + DurationInSeconds = 3600, // 1 hour + CalendarId = _testCalendar.Id, + IsHidden = false + }; + + await _calendarService.CreateNewCalendarItemAsync(calendarItem, null); + + var period = new TimeRange( + new DateTime(2025, 1, 15, 0, 0, 0, DateTimeKind.Utc), + new DateTime(2025, 1, 16, 0, 0, 0, DateTimeKind.Utc)); + + // Act + var result = await _calendarService.GetCalendarEventsAsync(_testCalendar, period); + + // Assert + result.Should().HaveCount(1); + result[0].Title.Should().Be("Team Meeting"); + result[0].StartDate.Should().Be(startDate); + } + + [Fact] + public async Task GetCalendarEventsAsync_WithNonRecurringEvent_OutsidePeriod_ReturnsEmpty() + { + // Arrange + var startDate = new DateTime(2025, 1, 15, 10, 0, 0, DateTimeKind.Utc); + var calendarItem = new CalendarItem + { + Id = Guid.NewGuid(), + Title = "Team Meeting", + StartDate = startDate, + DurationInSeconds = 3600, + CalendarId = _testCalendar.Id, + IsHidden = false + }; + + await _calendarService.CreateNewCalendarItemAsync(calendarItem, null); + + // Query for a different week + var period = new TimeRange( + new DateTime(2025, 1, 22, 0, 0, 0, DateTimeKind.Utc), + new DateTime(2025, 1, 29, 0, 0, 0, DateTimeKind.Utc)); + + // Act + var result = await _calendarService.GetCalendarEventsAsync(_testCalendar, period); + + // Assert + result.Should().BeEmpty(); + } + + [Fact] + public async Task GetCalendarEventsAsync_WithHiddenEvent_ExcludesFromResults() + { + // Arrange + var startDate = new DateTime(2025, 1, 15, 10, 0, 0, DateTimeKind.Utc); + var hiddenEvent = new CalendarItem + { + Id = Guid.NewGuid(), + Title = "Hidden Event", + StartDate = startDate, + DurationInSeconds = 3600, + CalendarId = _testCalendar.Id, + IsHidden = true + }; + + await _calendarService.CreateNewCalendarItemAsync(hiddenEvent, null); + + var period = new TimeRange( + new DateTime(2025, 1, 15, 0, 0, 0, DateTimeKind.Utc), + new DateTime(2025, 1, 16, 0, 0, 0, DateTimeKind.Utc)); + + // Act + var result = await _calendarService.GetCalendarEventsAsync(_testCalendar, period); + + // Assert + result.Should().BeEmpty("because hidden events should be excluded"); + } + + [Fact] + public async Task GetCalendarEventsAsync_WithAllDayEvent_ReturnsEvent() + { + // Arrange + var startDate = new DateTime(2025, 1, 15, 0, 0, 0, DateTimeKind.Utc); // Midnight + var allDayEvent = new CalendarItem + { + Id = Guid.NewGuid(), + Title = "Company Holiday", + StartDate = startDate, + DurationInSeconds = 86400, // 24 hours + CalendarId = _testCalendar.Id, + IsHidden = false + }; + + await _calendarService.CreateNewCalendarItemAsync(allDayEvent, null); + + var period = new TimeRange( + new DateTime(2025, 1, 15, 0, 0, 0, DateTimeKind.Utc), + new DateTime(2025, 1, 16, 0, 0, 0, DateTimeKind.Utc)); + + // Act + var result = await _calendarService.GetCalendarEventsAsync(_testCalendar, period); + + // Assert + result.Should().HaveCount(1); + result[0].Title.Should().Be("Company Holiday"); + result[0].IsAllDayEvent.Should().BeTrue(); + } + + [Fact] + public async Task GetCalendarEventsAsync_WithMultipleCalendars_ReturnsOnlyRequestedCalendarEvents() + { + // Arrange - Create another calendar + var secondCalendar = new AccountCalendar + { + Id = Guid.NewGuid(), + AccountId = _testCalendar.AccountId, + Name = "Second Calendar", + TimeZone = "UTC", + IsPrimary = false, + BackgroundColorHex = "#00FF00", + TextColorHex = "#000000" + }; + + await _calendarService.InsertAccountCalendarAsync(secondCalendar); + + // Add events to both calendars + var startDate = new DateTime(2025, 1, 15, 10, 0, 0, DateTimeKind.Utc); + + var event1 = new CalendarItem + { + Id = Guid.NewGuid(), + Title = "Calendar 1 Event", + StartDate = startDate, + DurationInSeconds = 3600, + CalendarId = _testCalendar.Id, + IsHidden = false + }; + + var event2 = new CalendarItem + { + Id = Guid.NewGuid(), + Title = "Calendar 2 Event", + StartDate = startDate, + DurationInSeconds = 3600, + CalendarId = secondCalendar.Id, + IsHidden = false + }; + + await _calendarService.CreateNewCalendarItemAsync(event1, null); + await _calendarService.CreateNewCalendarItemAsync(event2, null); + + var period = new TimeRange( + new DateTime(2025, 1, 15, 0, 0, 0, DateTimeKind.Utc), + new DateTime(2025, 1, 16, 0, 0, 0, DateTimeKind.Utc)); + + // Act - Query only the first calendar + var result = await _calendarService.GetCalendarEventsAsync(_testCalendar, period); + + // Assert + result.Should().HaveCount(1); + result[0].Title.Should().Be("Calendar 1 Event"); + result[0].CalendarId.Should().Be(_testCalendar.Id); + } + + [Fact] + public async Task GetCalendarEventsAsync_WithRecurringChildEvent_ReturnsChildAsRecurringChild() + { + // Arrange - Create a parent and child event + var parentId = Guid.NewGuid(); + var parentEvent = new CalendarItem + { + Id = parentId, + Title = "Parent Recurring Event", + StartDate = new DateTime(2025, 1, 15, 10, 0, 0, DateTimeKind.Utc), + DurationInSeconds = 3600, + CalendarId = _testCalendar.Id, + IsHidden = false, + Recurrence = "RRULE:FREQ=DAILY" + }; + + var childEvent = new CalendarItem + { + Id = Guid.NewGuid(), + Title = "Occurrence Instance", + StartDate = new DateTime(2025, 1, 16, 10, 0, 0, DateTimeKind.Utc), + DurationInSeconds = 3600, + CalendarId = _testCalendar.Id, + RecurringCalendarItemId = parentId, + IsHidden = false + }; + + await _calendarService.CreateNewCalendarItemAsync(parentEvent, null); + await _calendarService.CreateNewCalendarItemAsync(childEvent, null); + + var period = new TimeRange( + new DateTime(2025, 1, 16, 0, 0, 0, DateTimeKind.Utc), + new DateTime(2025, 1, 17, 0, 0, 0, DateTimeKind.Utc)); + + // Act + var result = await _calendarService.GetCalendarEventsAsync(_testCalendar, period); + + // Assert + result.Should().HaveCount(1); + result[0].Title.Should().Be("Occurrence Instance"); + result[0].IsRecurringChild.Should().BeTrue(); + result[0].RecurringCalendarItemId.Should().Be(parentId); + } +} diff --git a/Wino.Core.Tests/Services/ContactServiceTests.cs b/Wino.Core.Tests/Services/ContactServiceTests.cs new file mode 100644 index 00000000..4e3e10ef --- /dev/null +++ b/Wino.Core.Tests/Services/ContactServiceTests.cs @@ -0,0 +1,115 @@ +using FluentAssertions; +using MimeKit; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Tests.Helpers; +using Wino.Services; +using Xunit; + +namespace Wino.Core.Tests.Services; + +public class ContactServiceTests : IAsyncLifetime +{ + private InMemoryDatabaseService _databaseService = null!; + private ContactService _contactService = null!; + + public async Task InitializeAsync() + { + _databaseService = new InMemoryDatabaseService(); + await _databaseService.InitializeAsync(); + _contactService = new ContactService(_databaseService); + } + + public async Task DisposeAsync() + { + await _databaseService.DisposeAsync(); + } + + [Fact] + public async Task SaveAddressInformationAsync_WithNotificationReplyAddress_DoesNotPersistContact() + { + await _contactService.SaveAddressInformationAsync( + [ + new AccountContact + { + Address = "reply+ABCD1234@reply.github.com", + Name = "[owner/repository] Issue #42" + } + ]); + + var contact = await _databaseService.Connection + .Table() + .Where(c => c.Address == "reply+ABCD1234@reply.github.com") + .FirstOrDefaultAsync(); + + contact.Should().BeNull(); + } + + [Fact] + public async Task SaveAddressInformationAsync_WithHumanContact_PersistsContact() + { + await _contactService.SaveAddressInformationAsync( + [ + new AccountContact + { + Address = "alice@example.com", + Name = "Alice Example" + } + ]); + + var contact = await _databaseService.Connection + .Table() + .Where(c => c.Address == "alice@example.com") + .FirstOrDefaultAsync(); + + contact.Should().NotBeNull(); + contact!.Name.Should().Be("Alice Example"); + } + + [Fact] + public async Task SaveAddressInformationAsync_WithExistingNoisyContact_RemovesAutoCapturedEntry() + { + await _databaseService.Connection.InsertAsync( + new AccountContact + { + Address = "notifications@github.com", + Name = "GitHub Notifications" + }, + typeof(AccountContact)); + + await _contactService.SaveAddressInformationAsync( + [ + new AccountContact + { + Address = "notifications@github.com", + Name = "[owner/repository] Issue #99" + } + ]); + + var contact = await _databaseService.Connection + .Table() + .Where(c => c.Address == "notifications@github.com") + .FirstOrDefaultAsync(); + + contact.Should().BeNull(); + } + + [Fact] + public async Task SaveAddressInformationAsync_WithNoisyMimeGroup_SkipsGroupAndNoisyMembers() + { + var message = new MimeMessage(); + message.To.Add(new GroupAddress("[owner/repository] Issue #123", new InternetAddressList + { + new MailboxAddress("Alice Example", "alice@example.com"), + new MailboxAddress("[owner/repository] Issue #123", "notifications@github.com") + })); + + await _contactService.SaveAddressInformationAsync(message); + + var contacts = await _databaseService.Connection.Table().ToListAsync(); + var groups = await _databaseService.Connection.Table().ToListAsync(); + + contacts.Select(c => c.Address).Should().Contain("alice@example.com"); + contacts.Select(c => c.Address).Should().NotContain("notifications@github.com"); + groups.Should().BeEmpty(); + } +} diff --git a/Wino.Core.Tests/Services/CreateCalendarEventRequestTests.cs b/Wino.Core.Tests/Services/CreateCalendarEventRequestTests.cs new file mode 100644 index 00000000..514adbd0 --- /dev/null +++ b/Wino.Core.Tests/Services/CreateCalendarEventRequestTests.cs @@ -0,0 +1,127 @@ +using CommunityToolkit.Mvvm.Messaging; +using FluentAssertions; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models.Calendar; +using Wino.Core.Helpers; +using Wino.Core.Requests.Calendar; +using Wino.Messaging.Client.Calendar; +using Xunit; + +namespace Wino.Core.Tests.Services; + +public sealed class CreateCalendarEventRequestTests +{ + [Fact] + public void ApplyUiChanges_ForNonRecurringEvent_SendsOptimisticAddAndRevertDelete() + { + var composeResult = CreateComposeResult(); + var assignedCalendar = CreateAssignedCalendar(); + var request = new CreateCalendarEventRequest(composeResult, assignedCalendar); + var recipient = new CalendarRequestRecipient(); + + WeakReferenceMessenger.Default.RegisterAll(recipient); + + try + { + request.LocalCalendarItemId.Should().NotBeNull(); + + request.ApplyUIChanges(); + request.RevertUIChanges(); + + recipient.Added.Should().ContainSingle(); + recipient.Deleted.Should().ContainSingle(); + recipient.Added[0].Id.Should().Be(request.LocalCalendarItemId!.Value); + recipient.Deleted[0].Id.Should().Be(request.LocalCalendarItemId!.Value); + } + finally + { + WeakReferenceMessenger.Default.UnregisterAll(recipient); + } + } + + [Fact] + public void ApplyUiChanges_ForRecurringEvent_DoesNotSendOptimisticMessages() + { + var composeResult = CreateComposeResult(); + composeResult.Recurrence = "RRULE:FREQ=DAILY;INTERVAL=1"; + var request = new CreateCalendarEventRequest(composeResult, CreateAssignedCalendar()); + var recipient = new CalendarRequestRecipient(); + + WeakReferenceMessenger.Default.RegisterAll(recipient); + + try + { + request.LocalCalendarItemId.Should().BeNull(); + request.Item.Should().BeNull(); + + request.ApplyUIChanges(); + request.RevertUIChanges(); + + recipient.Added.Should().BeEmpty(); + recipient.Deleted.Should().BeEmpty(); + request.PreparedItem.Should().NotBeNull(); + } + finally + { + WeakReferenceMessenger.Default.UnregisterAll(recipient); + } + } + + [Fact] + public void SynchronizationActionHelper_ForCreateRequest_ReturnsCalendarCreateAction() + { + var request = new CreateCalendarEventRequest(CreateComposeResult(), CreateAssignedCalendar()); + + var actionItems = SynchronizationActionHelper.CreateActionItems([request], Guid.NewGuid(), "Test"); + + actionItems.Should().ContainSingle(); + actionItems[0].Description.Should().Be(Wino.Core.Domain.Translator.SyncAction_CreatingEvent); + } + + private static CalendarEventComposeResult CreateComposeResult() + { + return new CalendarEventComposeResult + { + CalendarId = Guid.NewGuid(), + AccountId = Guid.NewGuid(), + Title = "Planning", + Location = "Room 4", + HtmlNotes = "

Notes

", + StartDate = new DateTime(2026, 3, 7, 10, 0, 0), + EndDate = new DateTime(2026, 3, 7, 11, 0, 0), + TimeZoneId = TimeZoneInfo.Local.Id, + ShowAs = CalendarItemShowAs.Busy + }; + } + + private static AccountCalendar CreateAssignedCalendar() + { + return new AccountCalendar + { + Id = Guid.NewGuid(), + AccountId = Guid.NewGuid(), + Name = "Primary", + DefaultShowAs = CalendarItemShowAs.Busy, + MailAccount = new MailAccount + { + Id = Guid.NewGuid(), + Address = "user@example.com", + SenderName = "User" + } + }; + } + + internal sealed class CalendarRequestRecipient : + IRecipient, + IRecipient + { + public List Added { get; } = []; + public List Deleted { get; } = []; + + public void Receive(CalendarItemAdded message) => Added.Add(message.CalendarItem); + + public void Receive(CalendarItemDeleted message) => Deleted.Add(message.CalendarItem); + } +} diff --git a/Wino.Core.Tests/Services/MailFetchingTests.cs b/Wino.Core.Tests/Services/MailFetchingTests.cs new file mode 100644 index 00000000..4b3f9f68 --- /dev/null +++ b/Wino.Core.Tests/Services/MailFetchingTests.cs @@ -0,0 +1,383 @@ +using System.Diagnostics; +using FluentAssertions; +using Moq; +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.MailItem; +using Wino.Core.Tests.Helpers; +using Wino.Services; +using Xunit; +using Xunit.Abstractions; + +namespace Wino.Core.Tests.Services; + +/// +/// Integration tests for MailService.FetchMailsAsync that verify the correctness of +/// thread expansion and contact resolution, and track performance for large inboxes. +/// +/// All tests run against a real in-memory SQLite file via the full service stack +/// (MailService → FolderService / AccountService / ContactService) so that the +/// batch-query path introduced in the performance optimisation is exercised end-to-end. +/// +public class MailFetchingTests : IAsyncLifetime +{ + // ── Infrastructure ───────────────────────────────────────────────────────── + + private readonly ITestOutputHelper _output; + private InMemoryDatabaseService _databaseService = null!; + private MailService _mailService = null!; + private MailAccount _testAccount = null!; + private MailItemFolder _inboxFolder = null!; + + public MailFetchingTests(ITestOutputHelper output) + { + _output = output; + } + + public async Task InitializeAsync() + { + _databaseService = new InMemoryDatabaseService(); + await _databaseService.InitializeAsync(); + + _testAccount = new MailAccount + { + Id = Guid.NewGuid(), + Name = "Test Account", + Address = "me@test.local", + SenderName = "Test User", + ProviderType = MailProviderType.IMAP4 + }; + + _inboxFolder = new MailItemFolder + { + Id = Guid.NewGuid(), + MailAccountId = _testAccount.Id, + FolderName = "Inbox", + SpecialFolderType = SpecialFolderType.Inbox, + IsSystemFolder = true, + IsSynchronizationEnabled = true + }; + + await _databaseService.Connection.InsertAsync(_testAccount, typeof(MailAccount)); + await _databaseService.Connection.InsertAsync(_inboxFolder, typeof(MailItemFolder)); + + _mailService = BuildMailService(_databaseService); + } + + public async Task DisposeAsync() => await _databaseService.DisposeAsync(); + + // ── Correctness: threading ON ────────────────────────────────────────────── + + /// + /// Verifies that thread siblings which fall outside the initial SQL page are + /// fetched by the expansion step, so every thread is always fully represented. + /// + /// Setup: 2 threads of 3 mails each (6 mails total), page size = 4. + /// The main query retrieves Thread A (3 mails, newest) and Thread B mail 1 (position 4). + /// Thread expansion must then fetch Thread B mails 2-3 that were beyond the page. + /// + [Fact] + public async Task FetchMailsAsync_WithThreadingEnabled_ExpandsSiblingsOutsidePage() + { + const int PageSize = 4; + var threadA = Guid.NewGuid().ToString(); + var threadB = Guid.NewGuid().ToString(); + var baseDate = DateTime.UtcNow; + + var mails = new List + { + // Thread A – all 3 land within the first page (positions 1–3) + BuildMail(_inboxFolder.Id, baseDate.AddSeconds(-1), threadId: threadA), + BuildMail(_inboxFolder.Id, baseDate.AddSeconds(-2), threadId: threadA), + BuildMail(_inboxFolder.Id, baseDate.AddSeconds(-3), threadId: threadA), + // Thread B – only position 4 lands in the page; 5 and 6 must be expanded + BuildMail(_inboxFolder.Id, baseDate.AddSeconds(-4), threadId: threadB), + BuildMail(_inboxFolder.Id, baseDate.AddSeconds(-5), threadId: threadB), + BuildMail(_inboxFolder.Id, baseDate.AddSeconds(-6), threadId: threadB) + }; + await _databaseService.Connection.InsertAllAsync(mails, typeof(MailCopy)); + + var options = BuildOptions([_inboxFolder], createThreads: true, take: PageSize); + + // Act + var result = await _mailService.FetchMailsAsync(options); + + // Assert – all 6 mails returned even though the page only held 4 + result.Should().HaveCount(6, + "the 2 Thread B siblings outside the initial page must be fetched by expansion"); + result.Should().OnlyContain(m => m.AssignedAccount != null && m.AssignedFolder != null, + "every returned mail must have its account and folder resolved"); + result.Count(m => m.ThreadId == threadA).Should().Be(3, "Thread A must be complete"); + result.Count(m => m.ThreadId == threadB).Should().Be(3, "Thread B must be complete"); + } + + // ── Correctness: threading OFF ───────────────────────────────────────────── + + /// + /// Verifies that when threading is disabled the result exactly matches the raw + /// SQL page — no sibling expansion occurs. + /// + [Fact] + public async Task FetchMailsAsync_WithThreadingDisabled_NeverExpandsSiblings() + { + const int PageSize = 4; + var threadId = Guid.NewGuid().ToString(); + var baseDate = DateTime.UtcNow; + + // 6 mails all sharing a ThreadId; with threading OFF only the first 4 come back + var mails = Enumerable.Range(0, 6) + .Select(i => BuildMail(_inboxFolder.Id, baseDate.AddSeconds(-i), threadId: threadId)) + .ToList(); + + await _databaseService.Connection.InsertAllAsync(mails, typeof(MailCopy)); + + var options = BuildOptions([_inboxFolder], createThreads: false, take: PageSize); + + // Act + var result = await _mailService.FetchMailsAsync(options); + + // Assert – exactly the page size; no expansion happened + result.Should().HaveCount(PageSize, + "with threading disabled the result must match the raw page size"); + result.Should().OnlyContain(m => m.AssignedAccount != null && m.AssignedFolder != null); + } + + // ── Correctness: contact resolution ─────────────────────────────────────── + + /// + /// Verifies that sender contacts are resolved from three distinct paths: + /// the contact store (known sender), the unknown-sender fallback, and the + /// account-metadata shortcut used for self-sent mails. + /// + [Fact] + public async Task FetchMailsAsync_SenderContact_ResolvesFromAllThreeSources() + { + const string KnownAddress = "known@example.com"; + const string UnknownAddress = "unknown@example.com"; + + await _databaseService.Connection.InsertAsync( + new AccountContact { Address = KnownAddress, Name = "Known Sender" }, + typeof(AccountContact)); + + var mails = new List + { + BuildMail(_inboxFolder.Id, DateTime.UtcNow, fromAddress: KnownAddress), + BuildMail(_inboxFolder.Id, DateTime.UtcNow.AddSeconds(-1), fromAddress: UnknownAddress), + BuildMail(_inboxFolder.Id, DateTime.UtcNow.AddSeconds(-2), fromAddress: _testAccount.Address) + }; + await _databaseService.Connection.InsertAllAsync(mails, typeof(MailCopy)); + + var options = BuildOptions([_inboxFolder], createThreads: false, take: 10); + + // Act + var result = await _mailService.FetchMailsAsync(options); + + result.Should().HaveCount(3); + + // Known contact – resolved from AccountContact table + var knownResult = result.Single(m => m.FromAddress == KnownAddress); + knownResult.SenderContact!.Name.Should().Be("Known Sender"); + + // Unknown address – falls back to an ad-hoc contact built from From headers + var unknownResult = result.Single(m => m.FromAddress == UnknownAddress); + unknownResult.SenderContact!.Address.Should().Be(UnknownAddress); + + // Self-sent mail – contact built from account metadata, not the contact store + var selfResult = result.Single(m => m.FromAddress == _testAccount.Address); + selfResult.SenderContact!.Name.Should().Be(_testAccount.SenderName, + "self-sent mail must use account metadata for the sender contact"); + } + + // ── Performance: 1 000 mails / ~70 threads ───────────────────────────────── + + /// + /// Creates 1 000 mails: 70 threads of 7 mails each (490 mails) plus 510 standalone. + /// The mails are ordered newest-first in thread blocks so the default first-page + /// fetch (100 mails) naturally spans several complete threads and the tail of one + /// partial thread, letting us observe thread expansion. + /// + /// Two scenarios are measured and written to test output: + /// 1. First-page fetch (100 mails) plus automatic thread expansion. + /// 2. Full load of all 1 000 mails with threading enabled. + /// + /// A generous 5-second budget is asserted to catch catastrophic regressions + /// without being brittle on slow CI hardware. + /// + [Fact] + public async Task FetchMailsAsync_1000Mails_70Threads_CompletesWithinBudget() + { + // ── Arrange ──────────────────────────────────────────────────────────── + const int ThreadCount = 70; + const int MailsPerThread = 7; + const int TotalMails = 1_000; + const int StandaloneMails = TotalMails - (ThreadCount * MailsPerThread); // 510 + + // 40 rotating sender addresses; the first 20 have entries in the contact store. + var senders = Enumerable.Range(0, 40) + .Select(i => $"sender{i:D2}@example.com") + .ToList(); + + var knownContacts = senders.Take(20) + .Select((addr, i) => new AccountContact { Address = addr, Name = $"Sender {i}" }) + .ToList(); + await _databaseService.Connection.InsertAllAsync(knownContacts, typeof(AccountContact)); + + // Threads occupy the newest date slots (positions 0–489) so the default 100-mail + // page always intersects several threads, triggering sibling expansion. + var mails = new List(TotalMails); + var baseDate = DateTime.UtcNow; + int slot = 0; + + for (int t = 0; t < ThreadCount; t++) + { + var threadId = Guid.NewGuid().ToString(); + for (int m = 0; m < MailsPerThread; m++) + { + mails.Add(BuildMail( + _inboxFolder.Id, + baseDate.AddSeconds(-slot), + threadId: threadId, + fromAddress: senders[slot % senders.Count])); + slot++; + } + } + + for (int i = 0; i < StandaloneMails; i++) + { + mails.Add(BuildMail( + _inboxFolder.Id, + baseDate.AddSeconds(-slot), + fromAddress: senders[slot % senders.Count])); + slot++; + } + + await _databaseService.Connection.InsertAllAsync(mails, typeof(MailCopy)); + + _output.WriteLine($"Inserted {TotalMails} mails — " + + $"{ThreadCount} threads × {MailsPerThread} mails + {StandaloneMails} standalone"); + _output.WriteLine(string.Empty); + + // ── Scenario 1: first page (default 100) + thread expansion ─────────── + // The 100 newest mails span threads 0–13 completely (14 × 7 = 98 mails) plus + // the first 2 mails of thread 14. Expansion must fetch thread 14's 5 siblings. + var optionsPage = BuildOptions([_inboxFolder], createThreads: true); + var sw = Stopwatch.StartNew(); + var pageResult = await _mailService.FetchMailsAsync(optionsPage); + sw.Stop(); + long pageMs = sw.ElapsedMilliseconds; + + _output.WriteLine("[Scenario 1 – first page + thread expansion]"); + _output.WriteLine($" Mails returned : {pageResult.Count} (expected > 100)"); + _output.WriteLine($" Elapsed : {pageMs} ms"); + _output.WriteLine(string.Empty); + + pageResult.Should().OnlyContain(m => m.AssignedAccount != null && m.AssignedFolder != null); + + // Thread expansion must have added thread 14's 5 siblings beyond the 100-mail page. + pageResult.Count.Should().BeGreaterThan(100, + "thread expansion must pull in siblings that were beyond the initial 100-mail page"); + + // ── Scenario 2: full load of all 1 000 mails with threading ─────────── + var optionsAll = BuildOptions([_inboxFolder], createThreads: true, take: TotalMails); + sw.Restart(); + var allResult = await _mailService.FetchMailsAsync(optionsAll); + sw.Stop(); + long allMs = sw.ElapsedMilliseconds; + + _output.WriteLine($"[Scenario 2 – full load ({TotalMails} mails, threading enabled)]"); + _output.WriteLine($" Mails returned : {allResult.Count} (expected {TotalMails})"); + _output.WriteLine($" Elapsed : {allMs} ms"); + + allResult.Should().HaveCount(TotalMails, + "every mail must be returned when Take equals the total count"); + allResult.Should().OnlyContain(m => m.AssignedAccount != null && m.AssignedFolder != null); + + // All 70 threads must be intact in the full result. + var threadGroups = allResult + .Where(m => !string.IsNullOrEmpty(m.ThreadId)) + .GroupBy(m => m.ThreadId!) + .ToList(); + + threadGroups.Should().HaveCount(ThreadCount, + "all 70 threads must be represented in the full load"); + threadGroups.Should().OnlyContain(g => g.Count() == MailsPerThread, + "every thread must contain exactly the expected number of mails"); + + allMs.Should().BeLessThan(5_000, + $"fetching {TotalMails} threaded mails via batched SQLite queries should complete well under 5 s"); + } + + // ── Helpers ──────────────────────────────────────────────────────────────── + + private static MailCopy BuildMail( + Guid folderId, + DateTime creationDate, + string? threadId = null, + string fromAddress = "external@example.com") + { + return new MailCopy + { + UniqueId = Guid.NewGuid(), + Id = Guid.NewGuid().ToString(), + FileId = Guid.NewGuid(), + FolderId = folderId, + Subject = $"Subject {Guid.NewGuid():N}", + PreviewText = "Preview text", + FromAddress = fromAddress, + FromName = fromAddress.Split('@')[0], + CreationDate = creationDate, + ThreadId = threadId, + IsRead = false + }; + } + + private static MailListInitializationOptions BuildOptions( + IEnumerable folders, + bool createThreads = true, + int take = 0) + { + return new MailListInitializationOptions( + Folders: folders, + FilterType: FilterOptionType.All, + SortingOptionType: SortingOptionType.ReceiveDate, + CreateThreads: createThreads, + IsFocusedOnly: null, + SearchQuery: null, + Take: take); + } + + /// + /// Builds a MailService wired to real FolderService, AccountService, and ContactService + /// all backed by the shared in-memory database, so the full SQL batch path is exercised. + /// + private static MailService BuildMailService(InMemoryDatabaseService db) + { + var signatureService = new Mock(); + var authProvider = new Mock(); + var mimeFileService = new Mock(); + var preferencesService = new Mock(); + var contactPictureFileService = new Mock(); + + var accountService = new AccountService( + db, + signatureService.Object, + authProvider.Object, + mimeFileService.Object, + preferencesService.Object, + contactPictureFileService.Object); + + var folderService = new FolderService(db, accountService); + var contactService = new ContactService(db); + + return new MailService( + db, + folderService, + contactService, + accountService, + signatureService.Object, + mimeFileService.Object, + preferencesService.Object); + } +} diff --git a/Wino.Core.Tests/Services/MailRequestStateTests.cs b/Wino.Core.Tests/Services/MailRequestStateTests.cs new file mode 100644 index 00000000..b06c4487 --- /dev/null +++ b/Wino.Core.Tests/Services/MailRequestStateTests.cs @@ -0,0 +1,85 @@ +using CommunityToolkit.Mvvm.Messaging; +using FluentAssertions; +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Enums; +using Wino.Core.Requests.Mail; +using Wino.Messaging.UI; +using Xunit; + +namespace Wino.Core.Tests.Services; + +public sealed class MailRequestStateTests +{ + [Fact] + public void MarkReadRequest_RevertUiChanges_RestoresOriginalReadState() + { + var mailCopy = CreateMailCopy(isRead: false, isFlagged: false); + var request = new MarkReadRequest(mailCopy, IsRead: true); + var recipient = new MailRequestRecipient(); + + WeakReferenceMessenger.Default.RegisterAll(recipient); + + try + { + request.IsNoOp.Should().BeFalse(); + + request.ApplyUIChanges(); + request.RevertUIChanges(); + + mailCopy.IsRead.Should().BeFalse(); + recipient.Updated.Should().HaveCount(2); + recipient.Updated[0].Source.Should().Be(MailUpdateSource.ClientUpdated); + recipient.Updated[1].Source.Should().Be(MailUpdateSource.ClientReverted); + recipient.Updated[1].UpdatedMail.IsRead.Should().BeFalse(); + } + finally + { + WeakReferenceMessenger.Default.UnregisterAll(recipient); + } + } + + [Fact] + public void ChangeFlagRequest_RevertUiChanges_RestoresOriginalFlagState() + { + var mailCopy = CreateMailCopy(isRead: true, isFlagged: false); + var request = new ChangeFlagRequest(mailCopy, IsFlagged: true); + var recipient = new MailRequestRecipient(); + + WeakReferenceMessenger.Default.RegisterAll(recipient); + + try + { + request.IsNoOp.Should().BeFalse(); + + request.ApplyUIChanges(); + request.RevertUIChanges(); + + mailCopy.IsFlagged.Should().BeFalse(); + recipient.Updated.Should().HaveCount(2); + recipient.Updated[0].Source.Should().Be(MailUpdateSource.ClientUpdated); + recipient.Updated[1].Source.Should().Be(MailUpdateSource.ClientReverted); + recipient.Updated[1].UpdatedMail.IsFlagged.Should().BeFalse(); + } + finally + { + WeakReferenceMessenger.Default.UnregisterAll(recipient); + } + } + + private static MailCopy CreateMailCopy(bool isRead, bool isFlagged) => + new() + { + UniqueId = Guid.NewGuid(), + Id = Guid.NewGuid().ToString(), + FolderId = Guid.NewGuid(), + IsRead = isRead, + IsFlagged = isFlagged + }; + + internal sealed class MailRequestRecipient : IRecipient + { + public List Updated { get; } = []; + + public void Receive(MailUpdatedMessage message) => Updated.Add(message); + } +} diff --git a/Wino.Core.Tests/Services/WinoAccountDataSyncServiceTests.cs b/Wino.Core.Tests/Services/WinoAccountDataSyncServiceTests.cs new file mode 100644 index 00000000..770171e3 --- /dev/null +++ b/Wino.Core.Tests/Services/WinoAccountDataSyncServiceTests.cs @@ -0,0 +1,268 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Accounts; +using Wino.Core.Tests.Helpers; +using Wino.Mail.Api.Contracts.Users; +using Wino.Services; +using Xunit; + +namespace Wino.Core.Tests.Services; + +public sealed class WinoAccountDataSyncServiceTests : IAsyncLifetime +{ + private InMemoryDatabaseService _databaseService = null!; + private Mock _profileService = null!; + private Mock _preferencesService = null!; + private AccountService _accountService = null!; + private WinoAccountDataSyncService _service = null!; + + public async Task InitializeAsync() + { + _databaseService = new InMemoryDatabaseService(); + await _databaseService.InitializeAsync(); + + _profileService = new Mock(MockBehavior.Strict); + _preferencesService = new Mock(); + _preferencesService.SetupProperty(a => a.StartupEntityId); + + _accountService = CreateAccountService(_databaseService, _preferencesService.Object); + _service = new WinoAccountDataSyncService(_profileService.Object, _preferencesService.Object, _accountService); + } + + public async Task DisposeAsync() + { + await _databaseService.DisposeAsync(); + } + + [Fact] + public async Task ExportAsync_ImapMailbox_MapsSanitizedPayload() + { + var accountId = Guid.NewGuid(); + + await _accountService.CreateAccountAsync( + new MailAccount + { + Id = accountId, + Name = "Custom IMAP", + SenderName = "Custom IMAP Sender", + Address = "imap@example.com", + ProviderType = MailProviderType.IMAP4, + SpecialImapProvider = SpecialImapProvider.iCloud, + AccountColorHex = "#123456", + IsCalendarAccessGranted = true, + SynchronizationDeltaIdentifier = "delta-token", + CalendarSynchronizationDeltaIdentifier = "calendar-delta", + Base64ProfilePictureData = "profile" + }, + new CustomServerInformation + { + Id = Guid.NewGuid(), + AccountId = accountId, + Address = "imap@example.com", + IncomingServer = "imap.example.com", + IncomingServerPort = "993", + IncomingServerUsername = "imap-user", + IncomingServerPassword = "secret-incoming", + IncomingServerSocketOption = ImapConnectionSecurity.Auto, + IncomingAuthenticationMethod = ImapAuthenticationMethod.NormalPassword, + OutgoingServer = "smtp.example.com", + OutgoingServerPort = "465", + OutgoingServerUsername = "smtp-user", + OutgoingServerPassword = "secret-outgoing", + OutgoingServerSocketOption = ImapConnectionSecurity.Auto, + OutgoingAuthenticationMethod = ImapAuthenticationMethod.NormalPassword, + CalendarSupportMode = ImapCalendarSupportMode.CalDav, + CalDavServiceUrl = "https://dav.example.com", + CalDavUsername = "dav-user", + CalDavPassword = "secret-caldav", + ProxyServer = "proxy.example.com", + ProxyServerPort = "8080", + MaxConcurrentClients = 7 + }); + + ReplaceUserMailboxesRequestDto? capturedRequest = null; + _profileService + .Setup(a => a.ReplaceMailboxesAsync(It.IsAny(), It.IsAny())) + .Callback((request, _) => capturedRequest = request) + .Returns(Task.CompletedTask); + + var result = await _service.ExportAsync(new WinoAccountSyncSelection(IncludePreferences: false, IncludeAccounts: true)); + + result.ExportedMailboxCount.Should().Be(1); + capturedRequest.Should().NotBeNull(); + capturedRequest!.Mailboxes.Should().ContainSingle(); + + var exportedMailbox = capturedRequest.Mailboxes[0]; + exportedMailbox.Address.Should().Be("imap@example.com"); + exportedMailbox.ProviderType.Should().Be((int)MailProviderType.IMAP4); + exportedMailbox.SpecialImapProvider.Should().Be((int)SpecialImapProvider.iCloud); + exportedMailbox.AccountName.Should().Be("Custom IMAP"); + exportedMailbox.SenderName.Should().Be("Custom IMAP Sender"); + exportedMailbox.AccountColorHex.Should().Be("#123456"); + exportedMailbox.IsCalendarAccessGranted.Should().BeTrue(); + exportedMailbox.IncomingServer.Should().Be("imap.example.com"); + exportedMailbox.IncomingServerUsername.Should().Be("imap-user"); + exportedMailbox.OutgoingServer.Should().Be("smtp.example.com"); + exportedMailbox.OutgoingServerUsername.Should().Be("smtp-user"); + exportedMailbox.CalDavServiceUrl.Should().Be("https://dav.example.com"); + exportedMailbox.CalDavUsername.Should().Be("dav-user"); + exportedMailbox.ProxyServer.Should().Be("proxy.example.com"); + exportedMailbox.ProxyServerPort.Should().Be("8080"); + exportedMailbox.MaxConcurrentClients.Should().Be(7); + + _profileService.Verify(a => a.SaveSettingsAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task ExportAsync_GmailMailbox_DoesNotIncludeCustomServerSettings() + { + await _accountService.CreateAccountAsync( + new MailAccount + { + Id = Guid.NewGuid(), + Name = "Gmail", + SenderName = "Gmail Sender", + Address = "gmail@example.com", + ProviderType = MailProviderType.Gmail + }, + null!); + + ReplaceUserMailboxesRequestDto? capturedRequest = null; + _profileService + .Setup(a => a.ReplaceMailboxesAsync(It.IsAny(), It.IsAny())) + .Callback((request, _) => capturedRequest = request) + .Returns(Task.CompletedTask); + + await _service.ExportAsync(new WinoAccountSyncSelection(IncludePreferences: false, IncludeAccounts: true)); + + var exportedMailbox = capturedRequest!.Mailboxes.Single(); + exportedMailbox.IncomingServer.Should().BeNull(); + exportedMailbox.OutgoingServer.Should().BeNull(); + exportedMailbox.CalDavServiceUrl.Should().BeNull(); + exportedMailbox.MaxConcurrentClients.Should().BeNull(); + } + + [Fact] + public async Task ImportAsync_SkipsDuplicateMailbox_ByAddressAndProviderCaseInsensitive() + { + await _accountService.CreateAccountAsync( + new MailAccount + { + Id = Guid.NewGuid(), + Name = "Existing Gmail", + SenderName = "Existing Gmail", + Address = "User@Example.com", + ProviderType = MailProviderType.Gmail + }, + null!); + + _profileService + .Setup(a => a.GetMailboxesAsync(It.IsAny())) + .ReturnsAsync(new UserMailboxSyncListDto( + [ + new UserMailboxSyncItemDto + { + Address = "user@example.com", + ProviderType = (int)MailProviderType.Gmail, + AccountName = "Duplicate Gmail" + }, + new UserMailboxSyncItemDto + { + Address = "second@example.com", + ProviderType = (int)MailProviderType.Outlook, + AccountName = "New Outlook" + } + ])); + + var result = await _service.ImportAsync(new WinoAccountSyncSelection(IncludePreferences: false, IncludeAccounts: true)); + + result.ImportedMailboxCount.Should().Be(1); + result.SkippedDuplicateMailboxCount.Should().Be(1); + + var accounts = await _accountService.GetAccountsAsync(); + accounts.Should().HaveCount(2); + accounts.Should().Contain(a => a.Address == "second@example.com" && a.ProviderType == MailProviderType.Outlook); + } + + [Fact] + public async Task ImportAsync_ImapMailbox_CreatesRootAliasAndInvalidCredentialsAttentionWithoutPasswords() + { + _profileService + .Setup(a => a.GetMailboxesAsync(It.IsAny())) + .ReturnsAsync(new UserMailboxSyncListDto( + [ + new UserMailboxSyncItemDto + { + Address = "imap@example.com", + ProviderType = (int)MailProviderType.IMAP4, + SpecialImapProvider = (int)SpecialImapProvider.Yahoo, + AccountName = "Imported IMAP", + SenderName = "Imported Sender", + CalendarSupportMode = (int)ImapCalendarSupportMode.CalDav, + IncomingServer = "imap.example.com", + IncomingServerPort = "993", + IncomingServerUsername = "imap-user", + IncomingServerSocketOption = (int)ImapConnectionSecurity.Auto, + IncomingAuthenticationMethod = (int)ImapAuthenticationMethod.NormalPassword, + OutgoingServer = "smtp.example.com", + OutgoingServerPort = "465", + OutgoingServerUsername = "smtp-user", + OutgoingServerSocketOption = (int)ImapConnectionSecurity.Auto, + OutgoingAuthenticationMethod = (int)ImapAuthenticationMethod.NormalPassword, + CalDavServiceUrl = "https://dav.example.com", + CalDavUsername = "dav-user", + MaxConcurrentClients = 9 + } + ])); + + var result = await _service.ImportAsync(new WinoAccountSyncSelection(IncludePreferences: false, IncludeAccounts: true)); + + result.ImportedMailboxCount.Should().Be(1); + + var importedAccount = (await _accountService.GetAccountsAsync()).Single(); + importedAccount.AttentionReason.Should().Be(AccountAttentionReason.InvalidCredentials); + importedAccount.SynchronizationDeltaIdentifier.Should().BeEmpty(); + importedAccount.CalendarSynchronizationDeltaIdentifier.Should().BeEmpty(); + + var importedAliases = await _accountService.GetAccountAliasesAsync(importedAccount.Id); + importedAliases.Should().ContainSingle(a => a.IsRootAlias && a.IsPrimary && a.AliasAddress == "imap@example.com"); + + var serverInformation = await _accountService.GetAccountCustomServerInformationAsync(importedAccount.Id); + serverInformation.Should().NotBeNull(); + serverInformation.IncomingServerPassword.Should().BeEmpty(); + serverInformation.OutgoingServerPassword.Should().BeEmpty(); + serverInformation.CalDavPassword.Should().BeEmpty(); + serverInformation.MaxConcurrentClients.Should().Be(9); + serverInformation.CalDavServiceUrl.Should().Be("https://dav.example.com"); + } + + private static AccountService CreateAccountService(InMemoryDatabaseService databaseService, IPreferencesService preferencesService) + { + var signatureService = new Mock(); + signatureService + .Setup(a => a.CreateDefaultSignatureAsync(It.IsAny())) + .ReturnsAsync((Guid accountId) => new AccountSignature + { + Id = Guid.NewGuid(), + MailAccountId = accountId, + Name = "Default", + HtmlBody = string.Empty + }); + + return new AccountService( + databaseService, + signatureService.Object, + Mock.Of(), + Mock.Of(), + preferencesService, + Mock.Of()); + } +} diff --git a/Wino.Core.Tests/Services/WinoAccountProfileServiceTests.cs b/Wino.Core.Tests/Services/WinoAccountProfileServiceTests.cs new file mode 100644 index 00000000..d4531cfe --- /dev/null +++ b/Wino.Core.Tests/Services/WinoAccountProfileServiceTests.cs @@ -0,0 +1,264 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Accounts; +using Wino.Mail.Api.Contracts.Ai; +using Wino.Mail.Api.Contracts.Auth; +using Wino.Mail.Api.Contracts.Common; +using Wino.Services; +using Wino.Core.Tests.Helpers; +using Xunit; + +namespace Wino.Core.Tests.Services; + +public class WinoAccountProfileServiceTests : IAsyncLifetime +{ + private readonly Mock _apiClient = new(); + private readonly Mock _storeManagementService = new(); + private InMemoryDatabaseService _databaseService = null!; + private WinoAccountProfileService _service = null!; + + public async Task InitializeAsync() + { + _databaseService = new InMemoryDatabaseService(); + await _databaseService.InitializeAsync(); + _service = new WinoAccountProfileService(_databaseService, _apiClient.Object, _storeManagementService.Object); + } + + public async Task DisposeAsync() + { + await _databaseService.DisposeAsync(); + } + + [Fact] + public async Task LoginAsync_ShouldPersistSingleActiveAccount() + { + var authResult = CreateAuthResult("first@example.com"); + + _apiClient + .Setup(x => x.LoginAsync("first@example.com", "pw", default)) + .ReturnsAsync(WinoAccountApiResult.Success(authResult)); + + var result = await _service.LoginAsync("first@example.com", "pw"); + + result.IsSuccess.Should().BeTrue(); + result.Account.Should().NotBeNull(); + + var persisted = await _databaseService.Connection.Table().ToListAsync(); + persisted.Should().ContainSingle(); + persisted[0].Email.Should().Be("first@example.com"); + persisted[0].AccessToken.Should().Be(authResult.AccessToken); + persisted[0].RefreshToken.Should().Be(authResult.RefreshToken); + } + + [Fact] + public async Task LoginAsync_ShouldReplaceExistingActiveAccount() + { + _apiClient + .Setup(x => x.LoginAsync("first@example.com", "pw", default)) + .ReturnsAsync(WinoAccountApiResult.Success(CreateAuthResult("first@example.com"))); + + _apiClient + .Setup(x => x.LoginAsync("second@example.com", "pw", default)) + .ReturnsAsync(WinoAccountApiResult.Success(CreateAuthResult("second@example.com"))); + + await _service.LoginAsync("first@example.com", "pw"); + await _service.LoginAsync("second@example.com", "pw"); + + var persisted = await _databaseService.Connection.Table().ToListAsync(); + persisted.Should().ContainSingle(); + persisted[0].Email.Should().Be("second@example.com"); + } + + [Fact] + public async Task SignOutAsync_ShouldDeletePersistedAccount() + { + var authResult = CreateAuthResult("signout@example.com"); + + _apiClient + .Setup(x => x.LoginAsync("signout@example.com", "pw", default)) + .ReturnsAsync(WinoAccountApiResult.Success(authResult)); + + _apiClient + .Setup(x => x.LogoutAsync(authResult.RefreshToken, default)) + .ReturnsAsync(ApiEnvelope.Success(default)); + + await _service.LoginAsync("signout@example.com", "pw"); + await _service.SignOutAsync(); + + var persisted = await _databaseService.Connection.Table().ToListAsync(); + persisted.Should().BeEmpty(); + } + + [Fact] + public async Task LoginAsync_ShouldPreserveEnvelopeErrorMessage() + { + _apiClient + .Setup(x => x.LoginAsync("first@example.com", "pw", default)) + .ReturnsAsync(WinoAccountApiResult.Failure(ApiErrorCodes.InvalidCredentials, "Password does not match this account.")); + + var result = await _service.LoginAsync("first@example.com", "pw"); + + result.IsSuccess.Should().BeFalse(); + result.ErrorCode.Should().Be(ApiErrorCodes.InvalidCredentials); + result.ErrorMessage.Should().Be("Password does not match this account."); + } + + [Fact] + public async Task LoginAsync_ShouldPreserveErrorDetails() + { + var details = JsonSerializer.SerializeToElement(new EmailConfirmationRequiredDetailsDto( + "/api/v1/auth/confirm-email/resend", + "ticket", + DateTimeOffset.UtcNow.AddMinutes(-2), + DateTimeOffset.UtcNow.AddMinutes(8))); + + _apiClient + .Setup(x => x.LoginAsync("first@example.com", "pw", default)) + .ReturnsAsync(WinoAccountApiResult.Failure(ApiErrorCodes.EmailNotConfirmed, null, details)); + + var result = await _service.LoginAsync("first@example.com", "pw"); + + result.IsSuccess.Should().BeFalse(); + result.ErrorDetails.Should().NotBeNull(); + JsonSerializer.Deserialize(result.ErrorDetails!.Value.GetRawText())! + .ResendConfirmationTicket.Should().Be("ticket"); + } + + [Fact] + public async Task RegisterAsync_ShouldNotPersistAccountUntilEmailIsConfirmed() + { + var authResult = CreateAuthResult("register@example.com"); + + _apiClient + .Setup(x => x.RegisterAsync("register@example.com", "pw", default)) + .ReturnsAsync(WinoAccountApiResult.Success(authResult)); + + var result = await _service.RegisterAsync("register@example.com", "pw"); + + result.IsSuccess.Should().BeTrue(); + result.Account.Should().NotBeNull(); + + var persisted = await _databaseService.Connection.Table().ToListAsync(); + persisted.Should().BeEmpty(); + } + + [Fact] + public async Task ForgotPasswordAsync_ShouldForwardApiResponse() + { + _apiClient + .Setup(x => x.ForgotPasswordAsync("reset@example.com", default)) + .ReturnsAsync(ApiEnvelope.Success(default)); + + var result = await _service.ForgotPasswordAsync("reset@example.com"); + + result.IsSuccess.Should().BeTrue(); + } + + [Fact] + public async Task RefreshProfileAsync_ShouldPersistLatestProfileData() + { + var authResult = CreateAuthResult("first@example.com"); + + _apiClient + .Setup(x => x.LoginAsync("first@example.com", "pw", default)) + .ReturnsAsync(WinoAccountApiResult.Success(authResult)); + + _apiClient + .Setup(x => x.GetCurrentUserAsync(default)) + .ReturnsAsync(ApiEnvelope.Success(new AuthUserDto( + authResult.User.UserId, + "updated@example.com", + "Premium", + authResult.User.HasPassword, + authResult.User.HasGoogleLogin, + authResult.User.HasFacebookLogin))); + + await _service.LoginAsync("first@example.com", "pw"); + + var result = await _service.RefreshProfileAsync(); + + result.IsSuccess.Should().BeTrue(); + result.Account.Should().NotBeNull(); + result.Account!.Email.Should().Be("updated@example.com"); + result.Account.AccountStatus.Should().Be("Premium"); + + var persisted = await _databaseService.Connection.Table().FirstOrDefaultAsync(); + persisted.Should().NotBeNull(); + persisted!.Email.Should().Be("updated@example.com"); + persisted.AccountStatus.Should().Be("Premium"); + persisted.AccessToken.Should().Be(authResult.AccessToken); + persisted.RefreshToken.Should().Be(authResult.RefreshToken); + } + + [Fact] + public async Task ProcessBillingCallbackAsync_ShouldConfirmPurchasedAddOn() + { + var authResult = CreateAuthResult("first@example.com"); + var callbackUri = new Uri("wino://billing/success?productCode=UNLIMITED_ACCOUNTS"); + + _apiClient + .Setup(x => x.LoginAsync("first@example.com", "pw", default)) + .ReturnsAsync(WinoAccountApiResult.Success(authResult)); + + _apiClient + .Setup(x => x.GetCurrentUserAsync(default)) + .ReturnsAsync(ApiEnvelope.Success(authResult.User)); + + _storeManagementService + .Setup(x => x.HasProductAsync(WinoAddOnProductType.UNLIMITED_ACCOUNTS)) + .ReturnsAsync(true); + + await _service.LoginAsync("first@example.com", "pw"); + + var processed = await _service.ProcessBillingCallbackAsync(callbackUri); + + processed.Should().BeTrue(); + _apiClient.Verify(x => x.GetCurrentUserAsync(default), Times.AtLeastOnce); + } + + [Fact] + public async Task SummarizeAsync_ShouldReturnQuotaBackedResponse() + { + var authResult = CreateAuthResult("first@example.com"); + + _apiClient + .Setup(x => x.LoginAsync("first@example.com", "pw", default)) + .ReturnsAsync(WinoAccountApiResult.Success(authResult)); + + _apiClient + .Setup(x => x.SummarizeAsync("

Hello

", "en", default)) + .ReturnsAsync(ApiEnvelope.Success( + new AiTextResultDto("

Summary

"), + new QuotaInfoDto( + "Active", + DateTimeOffset.UtcNow.AddDays(-1), + DateTimeOffset.UtcNow.AddDays(29), + 1000, + 4, + 996, + new AiPackProductInfoDto("AI_PACK", 1000, 4.99m, "USD", "month")))); + + await _service.LoginAsync("first@example.com", "pw"); + + var response = await _service.SummarizeAsync("

Hello

", "en"); + + response.IsSuccess.Should().BeTrue(); + response.Result?.Html.Should().Be("

Summary

"); + } + + private static AuthResultDto CreateAuthResult(string email) + { + return new AuthResultDto( + new AuthUserDto(Guid.NewGuid(), email, "Active", true, false, false), + "access-token", + DateTimeOffset.UtcNow.AddMinutes(30), + "refresh-token", + DateTimeOffset.UtcNow.AddDays(30)); + } +} diff --git a/Wino.Core.Tests/Synchronizers/CalDavEventTimeMappingTests.cs b/Wino.Core.Tests/Synchronizers/CalDavEventTimeMappingTests.cs new file mode 100644 index 00000000..de07df39 --- /dev/null +++ b/Wino.Core.Tests/Synchronizers/CalDavEventTimeMappingTests.cs @@ -0,0 +1,111 @@ +using System.Reflection; +using FluentAssertions; +using Moq; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Calendar; +using Wino.Core.Integration.Processors; +using Wino.Services; +using Xunit; + +namespace Wino.Core.Tests.Synchronizers; + +public class CalDavEventTimeMappingTests +{ + [Fact] + public void ParseCalendarData_UtcEvent_AssignsUtcTimeZone() + { + const string ics = """ + BEGIN:VCALENDAR + VERSION:2.0 + PRODID:-//Wino Mail//Tests//EN + CALSCALE:GREGORIAN + BEGIN:VEVENT + UID:utc-event + DTSTAMP:20260201T000000Z + DTSTART:20260219T010000Z + DTEND:20260219T020000Z + SUMMARY:UTC Event + END:VEVENT + END:VCALENDAR + """; + + var events = ParseEvents(ics); + + events.Should().ContainSingle(); + events[0].StartTimeZone.Should().Be(TimeZoneInfo.Utc.Id); + events[0].EndTimeZone.Should().Be(TimeZoneInfo.Utc.Id); + } + + [Fact] + public async Task ManageCalendarEventAsync_PersistsWallClockTimeForSourceTimeZone() + { + var calendar = new AccountCalendar + { + Id = Guid.NewGuid(), + Name = "Calendar" + }; + + var remoteEvent = new CalDavCalendarEvent + { + RemoteEventId = "event-1", + Title = "Wall Clock Event", + Start = new DateTimeOffset(2026, 2, 19, 1, 0, 0, TimeSpan.FromHours(1)), + End = new DateTimeOffset(2026, 2, 19, 2, 0, 0, TimeSpan.FromHours(1)), + StartTimeZone = "Europe/Berlin", + EndTimeZone = "Europe/Berlin" + }; + + CalendarItem? capturedItem = null; + var calendarService = new Mock(); + calendarService + .Setup(x => x.GetCalendarItemAsync(calendar.Id, remoteEvent.RemoteEventId)) + .ReturnsAsync((CalendarItem?)null); + calendarService + .Setup(x => x.CreateNewCalendarItemAsync(It.IsAny(), It.IsAny>())) + .Callback>((item, _) => capturedItem = item) + .Returns(Task.CompletedTask); + calendarService + .Setup(x => x.SaveRemindersAsync(It.IsAny(), It.IsAny>())) + .Returns(Task.CompletedTask); + + var sut = new ImapChangeProcessor( + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + calendarService.Object, + Mock.Of(), + Mock.Of()); + + await sut.ManageCalendarEventAsync(remoteEvent, calendar, organizerAccount: null); + + capturedItem.Should().NotBeNull(); + var savedItem = capturedItem!; + savedItem.StartDate.Should().Be(new DateTime(2026, 2, 19, 1, 0, 0)); + savedItem.DurationInSeconds.Should().Be(3600); + savedItem.StartTimeZone.Should().Be("Europe/Berlin"); + savedItem.EndTimeZone.Should().Be("Europe/Berlin"); + } + + private static List ParseEvents(string icsContent) + { + var parseMethod = typeof(CalDavClient).GetMethod( + "ParseCalendarData", + BindingFlags.NonPublic | BindingFlags.Static); + + parseMethod.Should().NotBeNull(); + + var result = parseMethod!.Invoke( + null, + [ + icsContent, + "https://calendar.example.com/event.ics", + "\"etag\"", + new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero), + new DateTimeOffset(2026, 12, 31, 23, 59, 59, TimeSpan.Zero) + ]); + + return result.Should().BeOfType>().Subject; + } +} diff --git a/Wino.Core.Tests/Synchronizers/CalDavServiceLiveTests.cs b/Wino.Core.Tests/Synchronizers/CalDavServiceLiveTests.cs new file mode 100644 index 00000000..4d869f93 --- /dev/null +++ b/Wino.Core.Tests/Synchronizers/CalDavServiceLiveTests.cs @@ -0,0 +1,238 @@ +using System.Net.Http.Headers; +using System.Security; +using System.Text; +using System.Xml.Linq; +using FluentAssertions; +using Wino.Core.Domain.Models.Calendar; +using Wino.Services; +using Xunit; + +namespace Wino.Core.Tests.Synchronizers; + +public sealed class CalDavServiceLiveTests +{ + private const string ManualSkipMessage = "Manual live CalDAV test. Fill ServiceUri/Username/Password placeholders and remove Skip to run."; + + // Replace placeholders with your own credentials when running these live tests. + private const string ServiceUri = "https://caldav.icloud.com/"; + private const string Username = "REPLACE_WITH_USERNAME"; + private const string Password = "REPLACE_WITH_PASSWORD"; + + private static readonly DateTimeOffset SyncWindowStartUtc = new(2026, 01, 01, 0, 0, 0, TimeSpan.Zero); + private static readonly DateTimeOffset SyncWindowEndUtc = new(2026, 12, 31, 23, 59, 59, TimeSpan.Zero); + + [Fact(Skip = ManualSkipMessage)] + [Trait("Category", "Live")] + public async Task InitialSync_ReturnsCalendarEvents() + { + var client = new CalDavClient(); + var settings = BuildConnectionSettings(); + + var calendars = await client.DiscoverCalendarsAsync(settings); + calendars.Should().NotBeEmpty(); + + var calendar = calendars.First(); + var events = await client.GetCalendarEventsAsync(settings, calendar, SyncWindowStartUtc, SyncWindowEndUtc); + + events.Should().NotBeNull(); + } + + [Fact(Skip = ManualSkipMessage)] + [Trait("Category", "Live")] + public async Task AddThenRemoveEvent_ChangesServerState() + { + var client = new CalDavClient(); + var settings = BuildConnectionSettings(); + var calendar = await GetTargetCalendarAsync(client, settings); + + var eventId = $"wino-live-add-delete-{Guid.NewGuid():N}"; + var resourceUri = BuildEventResourceUri(calendar, eventId); + + await PutEventAsync(settings, resourceUri, BuildIcs(eventId, "Wino Live Add/Delete", new DateTimeOffset(2026, 04, 01, 10, 0, 0, TimeSpan.Zero), new DateTimeOffset(2026, 04, 01, 11, 0, 0, TimeSpan.Zero))); + + var afterAdd = await client.GetCalendarEventsAsync(settings, calendar, SyncWindowStartUtc, SyncWindowEndUtc); + afterAdd.Should().Contain(e => e.Uid == eventId); + + await DeleteEventAsync(settings, resourceUri); + + var afterDelete = await client.GetCalendarEventsAsync(settings, calendar, SyncWindowStartUtc, SyncWindowEndUtc); + afterDelete.Should().NotContain(e => e.Uid == eventId); + } + + [Fact(Skip = ManualSkipMessage)] + [Trait("Category", "Live")] + public async Task UpdateExistingEvent_ChangesStartAndEndDates() + { + var client = new CalDavClient(); + var settings = BuildConnectionSettings(); + var calendar = await GetTargetCalendarAsync(client, settings); + + var eventId = $"wino-live-update-{Guid.NewGuid():N}"; + var resourceUri = BuildEventResourceUri(calendar, eventId); + + var initialStart = new DateTimeOffset(2026, 05, 01, 8, 0, 0, TimeSpan.Zero); + var initialEnd = new DateTimeOffset(2026, 05, 01, 9, 0, 0, TimeSpan.Zero); + + await PutEventAsync(settings, resourceUri, BuildIcs(eventId, "Wino Live Update", initialStart, initialEnd)); + + var updatedStart = new DateTimeOffset(2026, 05, 02, 14, 30, 0, TimeSpan.Zero); + var updatedEnd = new DateTimeOffset(2026, 05, 02, 16, 0, 0, TimeSpan.Zero); + + await PutEventAsync(settings, resourceUri, BuildIcs(eventId, "Wino Live Update", updatedStart, updatedEnd)); + + var events = await client.GetCalendarEventsAsync(settings, calendar, SyncWindowStartUtc, SyncWindowEndUtc); + var updatedEvent = events.First(e => e.Uid == eventId); + + updatedEvent.Start.Should().Be(updatedStart); + updatedEvent.End.Should().Be(updatedEnd); + + await DeleteEventAsync(settings, resourceUri); + } + + [Fact(Skip = ManualSkipMessage)] + [Trait("Category", "Live")] + public async Task DeltaSync_AfterAdd_ReturnsChangedResource() + { + var client = new CalDavClient(); + var settings = BuildConnectionSettings(); + var calendar = await GetTargetCalendarAsync(client, settings); + + var initialSyncToken = await GetCalendarSyncTokenAsync(settings, new Uri(calendar.RemoteCalendarId)); + initialSyncToken.Should().NotBeNullOrWhiteSpace(); + + var eventId = $"wino-live-delta-{Guid.NewGuid():N}"; + var resourceUri = BuildEventResourceUri(calendar, eventId); + + await PutEventAsync(settings, resourceUri, BuildIcs(eventId, "Wino Live Delta", new DateTimeOffset(2026, 06, 01, 12, 0, 0, TimeSpan.Zero), new DateTimeOffset(2026, 06, 01, 13, 0, 0, TimeSpan.Zero))); + + var deltaResponse = await ReportSyncCollectionAsync(settings, new Uri(calendar.RemoteCalendarId), initialSyncToken); + var changedHrefs = ExtractChangedHrefs(deltaResponse); + + changedHrefs.Should().Contain(h => h.Contains($"{eventId}.ics", StringComparison.OrdinalIgnoreCase)); + + await DeleteEventAsync(settings, resourceUri); + } + + private static CalDavConnectionSettings BuildConnectionSettings() + => new() + { + ServiceUri = new Uri(ServiceUri), + Username = Username, + Password = Password + }; + + private static async Task GetTargetCalendarAsync(CalDavClient client, CalDavConnectionSettings settings) + { + var calendars = await client.DiscoverCalendarsAsync(settings); + calendars.Should().NotBeEmpty(); + return calendars.First(); + } + + private static Uri BuildEventResourceUri(CalDavCalendar calendar, string eventId) + => new($"{calendar.RemoteCalendarId.TrimEnd('/')}/{eventId}.ics"); + + private static string BuildIcs(string uid, string summary, DateTimeOffset startUtc, DateTimeOffset endUtc) + { + return $""" + BEGIN:VCALENDAR + VERSION:2.0 + PRODID:-//Wino Mail//CalDAV Live Tests//EN + CALSCALE:GREGORIAN + BEGIN:VEVENT + UID:{uid} + DTSTAMP:{DateTimeOffset.UtcNow:yyyyMMdd'T'HHmmss'Z'} + DTSTART:{startUtc:yyyyMMdd'T'HHmmss'Z'} + DTEND:{endUtc:yyyyMMdd'T'HHmmss'Z'} + SUMMARY:{summary} + END:VEVENT + END:VCALENDAR + """; + } + + private static async Task PutEventAsync(CalDavConnectionSettings settings, Uri eventUri, string icsContent) + { + using var client = CreateAuthenticatedHttpClient(settings); + using var request = new HttpRequestMessage(HttpMethod.Put, eventUri) + { + Content = new StringContent(icsContent, Encoding.UTF8, "text/calendar") + }; + + using var response = await client.SendAsync(request); + response.EnsureSuccessStatusCode(); + } + + private static async Task DeleteEventAsync(CalDavConnectionSettings settings, Uri eventUri) + { + using var client = CreateAuthenticatedHttpClient(settings); + using var response = await client.DeleteAsync(eventUri); + response.EnsureSuccessStatusCode(); + } + + private static async Task GetCalendarSyncTokenAsync(CalDavConnectionSettings settings, Uri calendarUri) + { + const string body = """ + + + + + + """; + + using var client = CreateAuthenticatedHttpClient(settings); + using var request = new HttpRequestMessage(new HttpMethod("PROPFIND"), calendarUri) + { + Content = new StringContent(body, Encoding.UTF8, "application/xml") + }; + + request.Headers.Add("Depth", "0"); + using var response = await client.SendAsync(request); + response.EnsureSuccessStatusCode(); + + var xml = await response.Content.ReadAsStringAsync(); + var doc = XDocument.Parse(xml); + + return doc.Descendants().FirstOrDefault(x => x.Name.LocalName == "sync-token")?.Value ?? string.Empty; + } + + private static async Task ReportSyncCollectionAsync(CalDavConnectionSettings settings, Uri calendarUri, string syncToken) + { + var body = $""" + + {SecurityElement.Escape(syncToken)} + 1 + + + + + """; + + using var client = CreateAuthenticatedHttpClient(settings); + using var request = new HttpRequestMessage(new HttpMethod("REPORT"), calendarUri) + { + Content = new StringContent(body, Encoding.UTF8, "application/xml") + }; + + request.Headers.Add("Depth", "1"); + using var response = await client.SendAsync(request); + response.EnsureSuccessStatusCode(); + + var xml = await response.Content.ReadAsStringAsync(); + return XDocument.Parse(xml); + } + + private static IReadOnlyList ExtractChangedHrefs(XDocument deltaXml) + => deltaXml + .Descendants() + .Where(x => x.Name.LocalName == "href") + .Select(x => x.Value) + .Where(v => !string.IsNullOrWhiteSpace(v)) + .ToList(); + + private static HttpClient CreateAuthenticatedHttpClient(CalDavConnectionSettings settings) + { + var client = new HttpClient(); + var basicAuth = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{settings.Username}:{settings.Password}")); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", basicAuth); + return client; + } +} diff --git a/Wino.Core.Tests/Synchronizers/CalendarItemTimeZoneDisplayTests.cs b/Wino.Core.Tests/Synchronizers/CalendarItemTimeZoneDisplayTests.cs new file mode 100644 index 00000000..f76e6115 --- /dev/null +++ b/Wino.Core.Tests/Synchronizers/CalendarItemTimeZoneDisplayTests.cs @@ -0,0 +1,61 @@ +using FluentAssertions; +using Wino.Calendar.ViewModels.Data; +using Wino.Core.Extensions; +using Wino.Core.Domain.Entities.Calendar; +using Google.Apis.Calendar.v3.Data; +using Xunit; + +namespace Wino.Core.Tests.Synchronizers; + +public sealed class CalendarItemTimeZoneDisplayTests +{ + [Fact] + public void AllDayEvents_KeepTheirOriginalCalendarDates_ForDisplay() + { + var calendarItem = new CalendarItem + { + Id = Guid.NewGuid(), + Title = "National Sovereignty and Children's Day", + StartDate = new DateTime(2026, 4, 23, 0, 0, 0), + DurationInSeconds = TimeSpan.FromDays(1).TotalSeconds, + StartTimeZone = "Turkey Standard Time", + EndTimeZone = "Turkey Standard Time" + }; + + calendarItem.IsAllDayEvent.Should().BeTrue(); + calendarItem.LocalStartDate.Should().Be(new DateTime(2026, 4, 23, 0, 0, 0)); + calendarItem.LocalEndDate.Should().Be(new DateTime(2026, 4, 24, 0, 0, 0)); + } + + [Fact] + public void EditingAllDayEventDate_DoesNotApplyTimezoneConversion() + { + var calendarItem = new CalendarItem + { + Id = Guid.NewGuid(), + Title = "Holiday", + StartDate = new DateTime(2026, 4, 23, 0, 0, 0), + DurationInSeconds = TimeSpan.FromDays(1).TotalSeconds, + StartTimeZone = "Turkey Standard Time", + EndTimeZone = "Turkey Standard Time" + }; + + var viewModel = new CalendarItemViewModel(calendarItem); + + viewModel.StartDate = new DateTime(2026, 4, 24, 0, 0, 0); + + calendarItem.StartDate.Should().Be(new DateTime(2026, 4, 24, 0, 0, 0)); + } + + [Fact] + public void GmailDateOnlyEvents_KeepFloatingCalendarDates() + { + var start = new EventDateTime { Date = "2026-04-23" }; + var end = new EventDateTime { Date = "2026-04-24" }; + + GoogleIntegratorExtensions.GetEventLocalDateTime(start).Should().Be(new DateTime(2026, 4, 23, 0, 0, 0)); + GoogleIntegratorExtensions.GetEventLocalDateTime(end).Should().Be(new DateTime(2026, 4, 24, 0, 0, 0)); + + GoogleIntegratorExtensions.GetEventDateTimeOffset(start)!.Value.UtcDateTime.Should().Be(new DateTime(2026, 4, 23, 0, 0, 0, DateTimeKind.Utc)); + } +} diff --git a/Wino.Core.Tests/Synchronizers/GmailSynchronizerRequestSuccessTests.cs b/Wino.Core.Tests/Synchronizers/GmailSynchronizerRequestSuccessTests.cs new file mode 100644 index 00000000..0e82a9b0 --- /dev/null +++ b/Wino.Core.Tests/Synchronizers/GmailSynchronizerRequestSuccessTests.cs @@ -0,0 +1,187 @@ +using System.Net; +using System.Net.Http; +using System.Reflection; +using FluentAssertions; +using Google.Apis.Requests; +using Moq; +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Synchronization; +using Wino.Core.Integration.Processors; +using Wino.Core.Requests.Bundles; +using Wino.Core.Requests.Mail; +using Wino.Core.Synchronizers.Mail; +using Xunit; + +namespace Wino.Core.Tests.Synchronizers; + +public sealed class GmailSynchronizerRequestSuccessTests +{ + [Fact] + public async Task ProcessSingleNativeRequestResponseAsync_BatchMarkReadRequest_PersistsLocalReadStateForEachMail() + { + var changeProcessor = new Mock(MockBehavior.Strict); + changeProcessor + .Setup(x => x.ChangeMailReadStatusAsync("mail-1", true)) + .Returns(Task.CompletedTask); + changeProcessor + .Setup(x => x.ChangeMailReadStatusAsync("mail-2", true)) + .Returns(Task.CompletedTask); + + var synchronizer = CreateSynchronizer(changeProcessor.Object); + var request = new BatchMarkReadRequest( + [ + new MarkReadRequest(CreateMailCopy("mail-1"), IsRead: true), + new MarkReadRequest(CreateMailCopy("mail-2"), IsRead: true) + ]); + var bundle = new HttpRequestBundle(Mock.Of(), request); + using var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(string.Empty) + }; + + await InvokeProcessSingleNativeRequestResponseAsync(synchronizer, bundle, response); + + changeProcessor.Verify(x => x.ChangeMailReadStatusAsync("mail-1", true), Times.Once); + changeProcessor.Verify(x => x.ChangeMailReadStatusAsync("mail-2", true), Times.Once); + } + + [Fact] + public async Task ProcessSingleNativeRequestResponseAsync_BatchChangeFlagRequest_PersistsLocalFlagStateForEachMail() + { + var changeProcessor = new Mock(MockBehavior.Strict); + changeProcessor + .Setup(x => x.ChangeFlagStatusAsync("mail-1", true)) + .Returns(Task.CompletedTask); + changeProcessor + .Setup(x => x.ChangeFlagStatusAsync("mail-2", true)) + .Returns(Task.CompletedTask); + + var synchronizer = CreateSynchronizer(changeProcessor.Object); + var request = new BatchChangeFlagRequest( + [ + new ChangeFlagRequest(CreateMailCopy("mail-1"), IsFlagged: true), + new ChangeFlagRequest(CreateMailCopy("mail-2"), IsFlagged: true) + ]); + var bundle = new HttpRequestBundle(Mock.Of(), request); + using var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(string.Empty) + }; + + await InvokeProcessSingleNativeRequestResponseAsync(synchronizer, bundle, response); + + changeProcessor.Verify(x => x.ChangeFlagStatusAsync("mail-1", true), Times.Once); + changeProcessor.Verify(x => x.ChangeFlagStatusAsync("mail-2", true), Times.Once); + } + + [Fact] + public async Task ProcessSingleNativeRequestResponseAsync_HandledRequestError_DoesNotPersistLocalReadState() + { + var changeProcessor = new Mock(MockBehavior.Strict); + var errorFactory = new Mock(MockBehavior.Strict); + errorFactory + .Setup(x => x.HandleErrorAsync(It.IsAny())) + .ReturnsAsync(true); + + var synchronizer = CreateSynchronizer(changeProcessor.Object, errorFactory.Object); + var request = new BatchMarkReadRequest( + [ + new MarkReadRequest(CreateMailCopy("mail-1"), IsRead: true) + ]); + var bundle = new HttpRequestBundle(Mock.Of(), request); + using var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(string.Empty) + }; + var error = new RequestError + { + Code = 429, + Message = "rate limit" + }; + + await InvokeProcessSingleNativeRequestResponseAsync(synchronizer, bundle, response, error); + + changeProcessor.Verify(x => x.ChangeMailReadStatusAsync(It.IsAny(), It.IsAny()), Times.Never); + errorFactory.Verify(x => x.HandleErrorAsync(It.IsAny()), Times.Once); + } + + [Fact] + public async Task ProcessSingleNativeRequestResponseAsync_HandledRequestError_RevertsOptimisticReadState() + { + var changeProcessor = new Mock(MockBehavior.Strict); + var errorFactory = new Mock(MockBehavior.Strict); + errorFactory + .Setup(x => x.HandleErrorAsync(It.IsAny())) + .ReturnsAsync(true); + + var mail = CreateMailCopy("mail-1"); + var request = new BatchMarkReadRequest( + [ + new MarkReadRequest(mail, IsRead: true) + ]); + request.ApplyUIChanges(); + + var synchronizer = CreateSynchronizer(changeProcessor.Object, errorFactory.Object); + var bundle = new HttpRequestBundle(Mock.Of(), request); + using var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(string.Empty) + }; + var error = new RequestError + { + Code = 429, + Message = "rate limit" + }; + + await InvokeProcessSingleNativeRequestResponseAsync(synchronizer, bundle, response, error); + + mail.IsRead.Should().BeFalse(); + changeProcessor.Verify(x => x.ChangeMailReadStatusAsync(It.IsAny(), It.IsAny()), Times.Never); + errorFactory.Verify(x => x.HandleErrorAsync(It.IsAny()), Times.Once); + } + + private static GmailSynchronizer CreateSynchronizer( + IGmailChangeProcessor changeProcessor, + IGmailSynchronizerErrorHandlerFactory? errorFactory = null) + { + var account = new MailAccount + { + Id = Guid.NewGuid(), + Name = "Gmail", + Address = "user@example.com" + }; + + var authenticator = new Mock(MockBehavior.Loose); + + return new GmailSynchronizer(account, authenticator.Object, changeProcessor, errorFactory ?? Mock.Of()); + } + + private static MailCopy CreateMailCopy(string id) => + new() + { + UniqueId = Guid.NewGuid(), + Id = id, + FolderId = Guid.NewGuid(), + IsRead = false, + IsFlagged = false + }; + + private static async Task InvokeProcessSingleNativeRequestResponseAsync( + GmailSynchronizer synchronizer, + HttpRequestBundle bundle, + HttpResponseMessage response, + RequestError? error = null) + { + var method = typeof(GmailSynchronizer).GetMethod( + "ProcessSingleNativeRequestResponseAsync", + BindingFlags.Instance | BindingFlags.NonPublic); + + method.Should().NotBeNull(); + + var task = method!.Invoke(synchronizer, [bundle, error, response, CancellationToken.None]) as Task; + task.Should().NotBeNull(); + await task!; + } +} diff --git a/Wino.Core.Tests/Synchronizers/ICloudCalDavLiveTests.cs b/Wino.Core.Tests/Synchronizers/ICloudCalDavLiveTests.cs new file mode 100644 index 00000000..4205baaf --- /dev/null +++ b/Wino.Core.Tests/Synchronizers/ICloudCalDavLiveTests.cs @@ -0,0 +1,98 @@ +using FluentAssertions; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Models.Calendar; +using Wino.Services; +using Xunit; +using Xunit.Abstractions; + +namespace Wino.Core.Tests.Synchronizers; + +public sealed class ICloudCalDavLiveTests +{ + private const string ManualSkipMessage = "Manual live iCloud CalDAV test. Fill credentials/constants in this file and remove Skip to run."; + + // Inline credentials/configuration (manual test by design). + // For iCloud, ServiceUri is typically https://caldav.icloud.com/ + private const string ServiceUri = "https://caldav.icloud.com/"; + private static readonly CustomServerInformation ServerInformation = new() + { + IncomingServerUsername = "", + IncomingServerPassword = "", + Address = "" + }; + + // Fixed UTC range for deterministic fetch checks. + private static readonly DateTimeOffset PeriodStartUtc = new(2026, 01, 01, 0, 0, 0, TimeSpan.Zero); + private static readonly DateTimeOffset PeriodEndUtc = new(2026, 12, 31, 23, 59, 59, TimeSpan.Zero); + + private readonly ITestOutputHelper _output; + + public ICloudCalDavLiveTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact(Skip = ManualSkipMessage)] + [Trait("Category", "Live")] + public async Task FetchesEventsForFixedUtcRange_AllCalendars() + { + var client = new CalDavClient(); + var settings = GetConnectionSettings(); + + var calendars = await client.DiscoverCalendarsAsync(settings); + calendars.Should().NotBeNull(); + calendars.Should().NotBeEmpty(); + + foreach (var calendar in calendars) + { + var events = await client.GetCalendarEventsAsync(settings, calendar, PeriodStartUtc, PeriodEndUtc); + _output.WriteLine($"Calendar: {calendar.Name} ({calendar.RemoteCalendarId}) => {events.Count} events"); + } + } + + [Fact(Skip = ManualSkipMessage)] + [Trait("Category", "Live")] + public async Task ParsesRequiredIcsFields_ForFetchedEvents() + { + var client = new CalDavClient(); + var settings = GetConnectionSettings(); + + var calendars = await client.DiscoverCalendarsAsync(settings); + calendars.Should().NotBeNull(); + calendars.Should().NotBeEmpty(); + + foreach (var calendar in calendars) + { + var events = await client.GetCalendarEventsAsync(settings, calendar, PeriodStartUtc, PeriodEndUtc); + + foreach (var item in events) + { + item.Uid.Should().NotBeNullOrWhiteSpace(); + item.Start.Should().NotBe(default(DateTimeOffset)); + item.Title.Should().NotBeNull(); + } + } + } + + [Fact] + public void BuildsConnectionSettings_FromCustomServerInformation() + { + var settings = GetConnectionSettings(); + + settings.ServiceUri.Should().Be(new Uri(ServiceUri)); + settings.Username.Should().Be(string.IsNullOrWhiteSpace(ServerInformation.IncomingServerUsername) + ? ServerInformation.Address + : ServerInformation.IncomingServerUsername); + settings.Password.Should().Be(ServerInformation.IncomingServerPassword); + } + + private static CalDavConnectionSettings GetConnectionSettings() + => new() + { + ServiceUri = new Uri(ServiceUri), + Username = string.IsNullOrWhiteSpace(ServerInformation.IncomingServerUsername) + ? ServerInformation.Address + : ServerInformation.IncomingServerUsername, + Password = ServerInformation.IncomingServerPassword + }; +} diff --git a/Wino.Core.Tests/Synchronizers/ImapClientPoolTests.cs b/Wino.Core.Tests/Synchronizers/ImapClientPoolTests.cs new file mode 100644 index 00000000..3d241fbb --- /dev/null +++ b/Wino.Core.Tests/Synchronizers/ImapClientPoolTests.cs @@ -0,0 +1,33 @@ +using FluentAssertions; +using Wino.Core.Integration; +using Xunit; + +namespace Wino.Core.Tests.Synchronizers; + +public class ImapClientPoolTests +{ + [Fact] + public void CalculateMaxConnections_ShouldUseDefault_WhenConfiguredValueIsNonPositive() + { + ImapClientPool.CalculateMaxConnections(0).Should().Be(5); + ImapClientPool.CalculateMaxConnections(-4).Should().Be(5); + } + + [Fact] + public void CalculateMaxConnections_ShouldClampToTen_WhenConfiguredValueIsTooHigh() + { + ImapClientPool.CalculateMaxConnections(40).Should().Be(10); + } + + [Fact] + public void CalculateTargetMinimumConnections_ShouldRespectConservativeMode() + { + ImapClientPool.CalculateTargetMinimumConnections(maxConnections: 5, useConservativeConnections: true).Should().Be(1); + } + + [Fact] + public void CalculateTargetMinimumConnections_ShouldBeTwo_WhenNotConservativeAndCapacityAllows() + { + ImapClientPool.CalculateTargetMinimumConnections(maxConnections: 5, useConservativeConnections: false).Should().Be(2); + } +} diff --git a/Wino.Core.Tests/Synchronizers/ImapSynchronizerCalDavConfigurationTests.cs b/Wino.Core.Tests/Synchronizers/ImapSynchronizerCalDavConfigurationTests.cs new file mode 100644 index 00000000..334edefe --- /dev/null +++ b/Wino.Core.Tests/Synchronizers/ImapSynchronizerCalDavConfigurationTests.cs @@ -0,0 +1,173 @@ +using System.Reflection; +using FluentAssertions; +using Moq; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Integration.Processors; +using Wino.Core.Synchronizers.ImapSync; +using Wino.Core.Synchronizers.Mail; +using Xunit; + +namespace Wino.Core.Tests.Synchronizers; + +public class ImapSynchronizerCalDavConfigurationTests +{ + [Fact] + public async Task ResolveCalDavServiceUriAsync_UsesExplicitConfigurationBeforeAutoDiscovery() + { + var tempDirectory = CreateTempDirectory(); + var autoDiscovery = new Mock(MockBehavior.Strict); + + var serverInformation = CreateServerInformation(); + serverInformation.CalDavServiceUrl = "https://caldav.explicit.example.com/"; + + var synchronizer = CreateSynchronizer(tempDirectory, serverInformation, autoDiscovery.Object); + + try + { + var resolvedUri = await InvokePrivateAsync(synchronizer, "ResolveCalDavServiceUriAsync", CancellationToken.None); + + resolvedUri.Should().Be(new Uri("https://caldav.explicit.example.com/")); + autoDiscovery.Verify(a => a.DiscoverCalDavServiceUriAsync(It.IsAny(), It.IsAny()), Times.Never); + } + finally + { + await synchronizer.KillSynchronizerAsync(); + DeleteDirectory(tempDirectory); + } + } + + [Fact] + public async Task ResolveCalDavPassword_PrefersExplicitCalDavPassword() + { + var tempDirectory = CreateTempDirectory(); + + var serverInformation = CreateServerInformation(); + serverInformation.IncomingServerPassword = "incoming-password"; + serverInformation.OutgoingServerPassword = "outgoing-password"; + serverInformation.CalDavPassword = "caldav-password"; + + var synchronizer = CreateSynchronizer(tempDirectory, serverInformation); + + try + { + var password = InvokePrivate(synchronizer, "ResolveCalDavPassword"); + + password.Should().Be("caldav-password"); + } + finally + { + await synchronizer.KillSynchronizerAsync(); + DeleteDirectory(tempDirectory); + } + } + + [Fact] + public async Task ResolveCalDavUsername_PrefersExplicitCalDavUsername() + { + var tempDirectory = CreateTempDirectory(); + + var serverInformation = CreateServerInformation(); + serverInformation.Address = "fallback@example.com"; + serverInformation.CalDavUsername = "calendar-user@example.com"; + + var synchronizer = CreateSynchronizer(tempDirectory, serverInformation); + + try + { + var username = InvokePrivate(synchronizer, "ResolveCalDavUsername"); + + username.Should().Be("calendar-user@example.com"); + } + finally + { + await synchronizer.KillSynchronizerAsync(); + DeleteDirectory(tempDirectory); + } + } + + private static ImapSynchronizer CreateSynchronizer(string appDataFolder, + CustomServerInformation serverInformation, + IAutoDiscoveryService? autoDiscoveryService = null) + { + var account = new MailAccount + { + Id = Guid.NewGuid(), + Name = "IMAP Test", + Address = "test@example.com", + ProviderType = MailProviderType.IMAP4, + IsCalendarAccessGranted = true, + ServerInformation = serverInformation + }; + + var applicationConfiguration = new Mock(); + applicationConfiguration.SetupProperty(x => x.ApplicationDataFolderPath, appDataFolder); + applicationConfiguration.SetupProperty(x => x.PublisherSharedFolderPath, appDataFolder); + applicationConfiguration.SetupProperty(x => x.ApplicationTempFolderPath, appDataFolder); + applicationConfiguration.SetupGet(x => x.SentryDNS).Returns(string.Empty); + + var unifiedSynchronizer = new UnifiedImapSynchronizer( + Mock.Of(), + Mock.Of(), + Mock.Of()); + + return new ImapSynchronizer( + account, + Mock.Of(), + applicationConfiguration.Object, + unifiedSynchronizer, + Mock.Of(), + Mock.Of(), + autoDiscoveryService ?? Mock.Of(), + Mock.Of()); + } + + private static CustomServerInformation CreateServerInformation() + => new() + { + Id = Guid.NewGuid(), + IncomingServer = "imap.example.com", + IncomingServerPort = "993", + IncomingServerUsername = "user@example.com", + IncomingServerPassword = "password", + OutgoingServer = "smtp.example.com", + OutgoingServerPort = "587", + OutgoingServerUsername = "user@example.com", + OutgoingServerPassword = "password", + MaxConcurrentClients = 5, + CalendarSupportMode = ImapCalendarSupportMode.CalDav + }; + + private static string CreateTempDirectory() + { + var path = Path.Combine(Path.GetTempPath(), "wino-imap-caldav-tests", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(path); + return path; + } + + private static void DeleteDirectory(string path) + { + if (Directory.Exists(path)) + { + Directory.Delete(path, recursive: true); + } + } + + private static T InvokePrivate(object instance, string methodName) + { + var method = instance.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance) + ?? throw new InvalidOperationException($"Method '{methodName}' not found."); + + return (T)method.Invoke(instance, null)!; + } + + private static async Task InvokePrivateAsync(object instance, string methodName, params object[] parameters) + { + var method = instance.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance) + ?? throw new InvalidOperationException($"Method '{methodName}' not found."); + + var task = (Task)method.Invoke(instance, parameters)!; + return await task.ConfigureAwait(false); + } +} diff --git a/Wino.Core.Tests/Synchronizers/ImapSynchronizerIdleTests.cs b/Wino.Core.Tests/Synchronizers/ImapSynchronizerIdleTests.cs new file mode 100644 index 00000000..d14afe37 --- /dev/null +++ b/Wino.Core.Tests/Synchronizers/ImapSynchronizerIdleTests.cs @@ -0,0 +1,82 @@ +using FluentAssertions; +using Moq; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Integration.Processors; +using Wino.Core.Synchronizers.ImapSync; +using Wino.Core.Synchronizers.Mail; +using Xunit; + +namespace Wino.Core.Tests.Synchronizers; + +public class ImapSynchronizerIdleTests +{ + [Fact] + public async Task ShouldTriggerIdleSynchronization_ShouldDebounceBurstSignals() + { + var tempDirectory = Path.Combine(Path.GetTempPath(), "wino-imap-idle-tests", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tempDirectory); + + var synchronizer = CreateSynchronizer(tempDirectory); + + try + { + var baseTime = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + synchronizer.ShouldTriggerIdleSynchronization(baseTime).Should().BeTrue(); + synchronizer.ShouldTriggerIdleSynchronization(baseTime.AddSeconds(5)).Should().BeFalse(); + synchronizer.ShouldTriggerIdleSynchronization(baseTime.AddSeconds(16)).Should().BeTrue(); + } + finally + { + await synchronizer.KillSynchronizerAsync(); + + if (Directory.Exists(tempDirectory)) + { + Directory.Delete(tempDirectory, recursive: true); + } + } + } + + private static ImapSynchronizer CreateSynchronizer(string appDataFolder) + { + var account = new MailAccount + { + Id = Guid.NewGuid(), + Name = "IMAP Test", + Address = "test@example.com", + ProviderType = MailProviderType.IMAP4, + ServerInformation = new CustomServerInformation + { + Id = Guid.NewGuid(), + IncomingServer = "imap.example.com", + IncomingServerPort = "993", + IncomingServerUsername = "user", + IncomingServerPassword = "password", + MaxConcurrentClients = 5 + } + }; + + var applicationConfiguration = new Mock(); + applicationConfiguration.SetupProperty(x => x.ApplicationDataFolderPath, appDataFolder); + applicationConfiguration.SetupProperty(x => x.PublisherSharedFolderPath, appDataFolder); + applicationConfiguration.SetupProperty(x => x.ApplicationTempFolderPath, appDataFolder); + applicationConfiguration.SetupGet(x => x.SentryDNS).Returns(string.Empty); + + var unifiedSynchronizer = new UnifiedImapSynchronizer( + Mock.Of(), + Mock.Of(), + Mock.Of()); + + return new ImapSynchronizer( + account, + Mock.Of(), + applicationConfiguration.Object, + unifiedSynchronizer, + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of()); + } +} diff --git a/Wino.Core.Tests/Synchronizers/ImapSynchronizerLiveTests.cs b/Wino.Core.Tests/Synchronizers/ImapSynchronizerLiveTests.cs new file mode 100644 index 00000000..0f508b58 --- /dev/null +++ b/Wino.Core.Tests/Synchronizers/ImapSynchronizerLiveTests.cs @@ -0,0 +1,290 @@ +using FluentAssertions; +using MailKit; +using MailKit.Net.Imap; +using MailKit.Search; +using Moq; +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.MailItem; +using Wino.Core.Domain.Models.Synchronization; +using Wino.Core.Integration.Processors; +using Wino.Core.Requests.Mail; +using Wino.Core.Synchronizers.ImapSync; +using Wino.Core.Synchronizers.Mail; +using Wino.Services.Extensions; +using Xunit; +using IMailService = Wino.Core.Domain.Interfaces.IMailService; + +namespace Wino.Core.Tests.Synchronizers; + +public sealed class ImapSynchronizerLiveTests +{ + private const string ManualSkipMessage = "Manual live IMAP test. Fill Server/Port/Username/Password placeholders and remove Skip to run."; + + // Replace placeholders with your own credentials when running these live tests. + private const string Server = "imap.example.com"; + private const int Port = 993; + private const string Username = "REPLACE_WITH_USERNAME"; + private const string Password = "REPLACE_WITH_PASSWORD"; + + [Fact(Skip = ManualSkipMessage)] + [Trait("Category", "Live")] + public async Task InitialSynchronization_DownloadsInboxMetadata() + { + using var context = await CreateContextAsync(); + + var result = await context.Synchronizer.SynchronizeMailsAsync(CreateCustomFolderSyncOptions(context.Account.Id, context.InboxFolder.Id)); + + result.CompletedState.Should().Be(SynchronizationCompletedState.Success); + result.FolderResults.Should().ContainSingle(); + result.FolderResults[0].Success.Should().BeTrue(); + result.FolderResults[0].DownloadedCount.Should().BeGreaterThan(0); + } + + [Fact(Skip = ManualSkipMessage)] + [Trait("Category", "Live")] + public async Task DeltaSynchronization_SecondRunDownloadsNoAdditionalMessages() + { + using var context = await CreateContextAsync(); + + var initialResult = await context.Synchronizer.SynchronizeMailsAsync(CreateCustomFolderSyncOptions(context.Account.Id, context.InboxFolder.Id)); + initialResult.CompletedState.Should().Be(SynchronizationCompletedState.Success); + + var deltaResult = await context.Synchronizer.SynchronizeMailsAsync(CreateCustomFolderSyncOptions(context.Account.Id, context.InboxFolder.Id)); + + deltaResult.CompletedState.Should().Be(SynchronizationCompletedState.Success); + deltaResult.FolderResults.Should().ContainSingle(); + deltaResult.FolderResults[0].DownloadedCount.Should().Be(0); + } + + [Fact(Skip = ManualSkipMessage)] + [Trait("Category", "Live")] + public async Task MarkFirstInboxMailUnreadThenRead_ValidatesReadFlagChanges() + { + using var context = await CreateContextAsync(); + + var firstUid = await GetFirstInboxMessageUidAsync(context.Account); + var mailCopy = CreateInboxMailCopy(context.InboxFolder, firstUid); + + await ExecuteMarkReadAsync(context.Synchronizer, mailCopy, isRead: false); + (await GetIsSeenAsync(context.Account, firstUid)).Should().BeFalse(); + + await ExecuteMarkReadAsync(context.Synchronizer, mailCopy, isRead: true); + (await GetIsSeenAsync(context.Account, firstUid)).Should().BeTrue(); + } + + [Fact(Skip = ManualSkipMessage)] + [Trait("Category", "Live")] + public async Task MarkFirstInboxMailReadThenUnread_ValidatesUnreadFlagChanges() + { + using var context = await CreateContextAsync(); + + var firstUid = await GetFirstInboxMessageUidAsync(context.Account); + var mailCopy = CreateInboxMailCopy(context.InboxFolder, firstUid); + + await ExecuteMarkReadAsync(context.Synchronizer, mailCopy, isRead: true); + (await GetIsSeenAsync(context.Account, firstUid)).Should().BeTrue(); + + await ExecuteMarkReadAsync(context.Synchronizer, mailCopy, isRead: false); + (await GetIsSeenAsync(context.Account, firstUid)).Should().BeFalse(); + } + + private static MailSynchronizationOptions CreateCustomFolderSyncOptions(Guid accountId, Guid folderId) + => new() + { + AccountId = accountId, + Type = MailSynchronizationType.CustomFolders, + SynchronizationFolderIds = [folderId] + }; + + private static async Task ExecuteMarkReadAsync(ImapSynchronizer synchronizer, MailCopy mailCopy, bool isRead) + { + var requests = synchronizer.MarkRead(new BatchMarkReadRequest([new MarkReadRequest(mailCopy, isRead)])); + await synchronizer.ExecuteNativeRequestsAsync(requests); + } + + private static MailCopy CreateInboxMailCopy(MailItemFolder folder, UniqueId uid) + => new() + { + Id = MailkitClientExtensions.CreateUid(folder.Id, uid.Id), + AssignedFolder = folder, + IsRead = false, + Subject = "Live test placeholder" + }; + + private static async Task GetFirstInboxMessageUidAsync(MailAccount account) + { + using var client = await CreateConnectedClientAsync(account); + var inbox = client.Inbox; + + await inbox.OpenAsync(FolderAccess.ReadWrite); + + var allUids = await inbox.SearchAsync(SearchQuery.All); + allUids.Should().NotBeEmpty("Inbox must contain at least one message for mark-read live tests."); + + var firstUid = allUids.First(); + + await inbox.CloseAsync(); + await client.DisconnectAsync(true); + + return firstUid; + } + + private static async Task GetIsSeenAsync(MailAccount account, UniqueId uid) + { + using var client = await CreateConnectedClientAsync(account); + var inbox = client.Inbox; + + await inbox.OpenAsync(FolderAccess.ReadOnly); + var summary = await inbox.FetchAsync([uid], MessageSummaryItems.Flags); + + await inbox.CloseAsync(); + await client.DisconnectAsync(true); + + summary.Should().ContainSingle(); + + var flags = summary[0].Flags; + flags.Should().NotBeNull(); + + return flags!.Value.HasFlag(MessageFlags.Seen); + } + + private static async Task CreateConnectedClientAsync(MailAccount account) + { + var client = new ImapClient(); + + await client.ConnectAsync(account.ServerInformation.IncomingServer, int.Parse(account.ServerInformation.IncomingServerPort), MailKit.Security.SecureSocketOptions.Auto); + await client.AuthenticateAsync(account.ServerInformation.IncomingServerUsername, account.ServerInformation.IncomingServerPassword); + + return client; + } + + private static async Task CreateContextAsync() + { + var tempDirectory = Path.Combine(Path.GetTempPath(), "wino-imap-live-tests", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tempDirectory); + + var account = new MailAccount + { + Id = Guid.NewGuid(), + Name = "IMAP Live Test", + Address = Username, + ProviderType = MailProviderType.IMAP4, + ServerInformation = new CustomServerInformation + { + Id = Guid.NewGuid(), + IncomingServer = Server, + IncomingServerPort = Port.ToString(), + IncomingServerUsername = Username, + IncomingServerPassword = Password, + IncomingServerSocketOption = ImapConnectionSecurity.Auto, + MaxConcurrentClients = 2 + } + }; + + var inboxFolder = new MailItemFolder + { + Id = Guid.NewGuid(), + MailAccountId = account.Id, + FolderName = "Inbox", + RemoteFolderId = "INBOX", + SpecialFolderType = SpecialFolderType.Inbox, + IsSynchronizationEnabled = true, + ShowUnreadCount = true, + UidValidity = 0, + HighestModeSeq = 0, + HighestKnownUid = 0 + }; + + var storedMails = new Dictionary(); + + var folderService = new Mock(); + folderService.Setup(x => x.GetKnownUidsForFolderAsync(inboxFolder.Id)) + .ReturnsAsync(() => storedMails.Values.Select(m => MailkitClientExtensions.ResolveUid(m.Id)).ToList()); + folderService.Setup(x => x.UpdateFolderAsync(It.IsAny())).Returns(Task.CompletedTask); + folderService.Setup(x => x.UpdateFolderHighestModeSeqAsync(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); + folderService.Setup(x => x.DeleteFolderAsync(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); + + var mailService = new Mock(); + mailService.Setup(x => x.GetMailsByFolderIdAsync(inboxFolder.Id)).ReturnsAsync(() => storedMails.Values.ToList()); + mailService.Setup(x => x.GetExistingMailsAsync(inboxFolder.Id, It.IsAny>())) + .ReturnsAsync((Guid _, IEnumerable ids) => + ids.Select(uid => MailkitClientExtensions.CreateUid(inboxFolder.Id, uid.Id)) + .Where(storedMails.ContainsKey) + .Select(id => storedMails[id]) + .ToList()); + mailService.Setup(x => x.CreateMailAsync(account.Id, It.IsAny())) + .ReturnsAsync((Guid _, NewMailItemPackage package) => + { + storedMails[package.Copy.Id] = package.Copy; + return true; + }); + mailService.Setup(x => x.ChangeReadStatusAsync(It.IsAny(), It.IsAny())) + .Returns((string mailCopyId, bool isRead) => + { + if (storedMails.TryGetValue(mailCopyId, out var copy)) + { + copy.IsRead = isRead; + } + + return Task.CompletedTask; + }); + mailService.Setup(x => x.ChangeFlagStatusAsync(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); + mailService.Setup(x => x.DeleteMailAsync(It.IsAny(), It.IsAny())) + .Returns((Guid _, string mailCopyId) => + { + storedMails.Remove(mailCopyId); + return Task.CompletedTask; + }); + + var changeProcessor = new Mock(); + changeProcessor.Setup(x => x.GetSynchronizationFoldersAsync(It.IsAny())) + .ReturnsAsync([inboxFolder]); + changeProcessor.Setup(x => x.GetRecentMailIdsForFolderAsync(inboxFolder.Id, It.IsAny())) + .ReturnsAsync((Guid _, int count) => storedMails.Keys.Take(count).ToList()); + changeProcessor.Setup(x => x.GetDownloadedUnreadMailsAsync(account.Id, It.IsAny>())) + .ReturnsAsync(new List()); + + var appConfiguration = new Mock(); + appConfiguration.SetupProperty(x => x.ApplicationDataFolderPath, tempDirectory); + appConfiguration.SetupProperty(x => x.PublisherSharedFolderPath, tempDirectory); + appConfiguration.SetupProperty(x => x.ApplicationTempFolderPath, tempDirectory); + appConfiguration.SetupGet(x => x.SentryDNS).Returns(string.Empty); + + var unifiedSynchronizer = new UnifiedImapSynchronizer( + folderService.Object, + mailService.Object, + Mock.Of()); + + var synchronizer = new ImapSynchronizer( + account, + changeProcessor.Object, + appConfiguration.Object, + unifiedSynchronizer, + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of()); + + return await Task.FromResult(new LiveTestContext(account, inboxFolder, synchronizer, tempDirectory)); + } + + private sealed class LiveTestContext(MailAccount account, MailItemFolder inboxFolder, ImapSynchronizer synchronizer, string tempDirectory) : IDisposable + { + public MailAccount Account { get; } = account; + public MailItemFolder InboxFolder { get; } = inboxFolder; + public ImapSynchronizer Synchronizer { get; } = synchronizer; + + public void Dispose() + { + Synchronizer.KillSynchronizerAsync().GetAwaiter().GetResult(); + + if (Directory.Exists(tempDirectory)) + { + Directory.Delete(tempDirectory, recursive: true); + } + } + } +} diff --git a/Wino.Core.Tests/Synchronizers/OutlookSynchronizerRequestSuccessTests.cs b/Wino.Core.Tests/Synchronizers/OutlookSynchronizerRequestSuccessTests.cs new file mode 100644 index 00000000..c723e79d --- /dev/null +++ b/Wino.Core.Tests/Synchronizers/OutlookSynchronizerRequestSuccessTests.cs @@ -0,0 +1,102 @@ +using System.Net; +using System.Net.Http; +using System.Reflection; +using FluentAssertions; +using Microsoft.Kiota.Abstractions; +using Moq; +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Integration.Processors; +using Wino.Core.Requests.Bundles; +using Wino.Core.Requests.Mail; +using Wino.Core.Synchronizers.Mail; +using Xunit; + +namespace Wino.Core.Tests.Synchronizers; + +public sealed class OutlookSynchronizerRequestSuccessTests +{ + [Fact] + public async Task HandleSuccessfulResponseAsync_MarkReadRequest_PersistsLocalReadStateEvenWithoutResponseBody() + { + var changeProcessor = new Mock(MockBehavior.Strict); + changeProcessor + .Setup(x => x.ChangeMailReadStatusAsync("mail-id", true)) + .Returns(Task.CompletedTask); + + var synchronizer = CreateSynchronizer(changeProcessor.Object); + var request = new MarkReadRequest(CreateMailCopy(), IsRead: true); + var bundle = new HttpRequestBundle(new RequestInformation(), request, request); + using var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(string.Empty) + }; + + await InvokeHandleSuccessfulResponseAsync(synchronizer, bundle, response); + + changeProcessor.Verify(x => x.ChangeMailReadStatusAsync("mail-id", true), Times.Once); + } + + [Fact] + public async Task HandleSuccessfulResponseAsync_ChangeFlagRequest_PersistsLocalFlagStateEvenWithoutResponseBody() + { + var changeProcessor = new Mock(MockBehavior.Strict); + changeProcessor + .Setup(x => x.ChangeFlagStatusAsync("mail-id", true)) + .Returns(Task.CompletedTask); + + var synchronizer = CreateSynchronizer(changeProcessor.Object); + var request = new ChangeFlagRequest(CreateMailCopy(), IsFlagged: true); + var bundle = new HttpRequestBundle(new RequestInformation(), request, request); + using var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(string.Empty) + }; + + await InvokeHandleSuccessfulResponseAsync(synchronizer, bundle, response); + + changeProcessor.Verify(x => x.ChangeFlagStatusAsync("mail-id", true), Times.Once); + } + + private static OutlookSynchronizer CreateSynchronizer(IOutlookChangeProcessor changeProcessor) + { + var account = new MailAccount + { + Id = Guid.NewGuid(), + Name = "Outlook", + Address = "user@example.com" + }; + + var authenticator = new Mock(MockBehavior.Loose); + var errorFactory = new Mock(MockBehavior.Loose); + + return new OutlookSynchronizer(account, authenticator.Object, changeProcessor, errorFactory.Object); + } + + private static MailCopy CreateMailCopy() => + new() + { + UniqueId = Guid.NewGuid(), + Id = "mail-id", + FolderId = Guid.NewGuid(), + IsRead = false, + IsFlagged = false + }; + + private static async Task InvokeHandleSuccessfulResponseAsync( + OutlookSynchronizer synchronizer, + HttpRequestBundle bundle, + HttpResponseMessage response) + { + var method = typeof(OutlookSynchronizer).GetMethod( + "HandleSuccessfulResponseAsync", + BindingFlags.Instance | BindingFlags.NonPublic); + + method.Should().NotBeNull(); + + var task = method!.Invoke(synchronizer, [bundle, response]) as Task; + task.Should().NotBeNull(); + await task!; + } +} diff --git a/Wino.Core.Tests/Synchronizers/UnifiedImapSynchronizerTests.cs b/Wino.Core.Tests/Synchronizers/UnifiedImapSynchronizerTests.cs new file mode 100644 index 00000000..87c00b9d --- /dev/null +++ b/Wino.Core.Tests/Synchronizers/UnifiedImapSynchronizerTests.cs @@ -0,0 +1,226 @@ +using FluentAssertions; +using MailKit; +using MailKit.Net.Imap; +using Moq; +using System.Reflection; +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.MailItem; +using Wino.Core.Synchronizers.ImapSync; +using Xunit; +using IMailService = Wino.Core.Domain.Interfaces.IMailService; + +namespace Wino.Core.Tests.Synchronizers; + +public class UnifiedImapSynchronizerTests +{ + private static UnifiedImapSynchronizer CreateSut() + { + return new UnifiedImapSynchronizer( + Mock.Of(), + Mock.Of(), + Mock.Of()); + } + + [Fact] + public void DetermineSyncStrategy_ShouldPrioritizeQResync_WhenEnabledAndSupported() + { + var sut = CreateSut(); + + var strategy = sut.DetermineSyncStrategy( + ImapCapabilities.QuickResync | ImapCapabilities.CondStore, + isQResyncEnabled: true, + serverHost: "imap.example.com"); + + strategy.Should().Be(ImapSyncStrategy.QResync); + } + + [Fact] + public void DetermineSyncStrategy_ShouldFallbackToCondstore_WhenQResyncNotEnabled() + { + var sut = CreateSut(); + + var strategy = sut.DetermineSyncStrategy( + ImapCapabilities.QuickResync | ImapCapabilities.CondStore, + isQResyncEnabled: false, + serverHost: "imap.example.com"); + + strategy.Should().Be(ImapSyncStrategy.Condstore); + } + + [Fact] + public void DetermineSyncStrategy_ShouldUseUidFallback_WhenNoAdvancedCapability() + { + var sut = CreateSut(); + + var strategy = sut.DetermineSyncStrategy( + ImapCapabilities.None, + isQResyncEnabled: false, + serverHost: "imap.example.com"); + + strategy.Should().Be(ImapSyncStrategy.UidBased); + } + + [Fact] + public void DetermineSyncStrategy_ShouldRespectQuirkOverride_ForStrictProviders() + { + var sut = CreateSut(); + + var strategy = sut.DetermineSyncStrategy( + ImapCapabilities.QuickResync | ImapCapabilities.CondStore, + isQResyncEnabled: true, + serverHost: "imap.qq.com"); + + strategy.Should().Be(ImapSyncStrategy.Condstore); + } + + [Fact] + public void DetermineSyncStrategy_ShouldFallbackToUid_WhenCondstoreIsUnavailable() + { + var sut = CreateSut(); + + var strategy = sut.DetermineSyncStrategy( + ImapCapabilities.QuickResync, + isQResyncEnabled: false, + serverHost: "imap.example.com"); + + strategy.Should().Be(ImapSyncStrategy.UidBased); + } + + [Fact] + public void DetermineSyncStrategy_ShouldFallbackToUid_WhenQuirkDisablesQresyncAndNoCondstore() + { + var sut = CreateSut(); + + var strategy = sut.DetermineSyncStrategy( + ImapCapabilities.QuickResync, + isQResyncEnabled: true, + serverHost: "imap.163.com"); + + strategy.Should().Be(ImapSyncStrategy.UidBased); + } + + [Fact] + public void CalculateHighestKnownUid_ShouldUseMaxOfCurrentObservedAndUidNext() + { + var result = UnifiedImapSynchronizer.CalculateHighestKnownUid( + currentHighestKnownUid: 100, + uidNext: new MailKit.UniqueId(151), + observedUids: new uint[] { 120, 140, 130 }); + + result.Should().Be(150); + } + + [Fact] + public void CalculateHighestKnownUid_ShouldNotRegress_WhenObservedUidsAreLower() + { + var result = UnifiedImapSynchronizer.CalculateHighestKnownUid( + currentHighestKnownUid: 500, + uidNext: null, + observedUids: new uint[] { 110, 120, 130 }); + + result.Should().Be(500); + } + + [Fact] + public void CalculateHighestKnownUid_ShouldUseUidNextMinusOne_WhenNoObservedUids() + { + var result = UnifiedImapSynchronizer.CalculateHighestKnownUid( + currentHighestKnownUid: 0, + uidNext: new MailKit.UniqueId(901), + observedUids: null); + + result.Should().Be(900); + } + + [Fact] + public void ShouldRunUidReconcile_ShouldReturnTrue_WhenNeverReconciled() + { + var shouldRun = UnifiedImapSynchronizer.ShouldRunUidReconcile( + lastUidReconcileUtc: null, + utcNow: DateTime.UtcNow, + reconcileInterval: TimeSpan.FromHours(12)); + + shouldRun.Should().BeTrue(); + } + + [Fact] + public void ShouldRunUidReconcile_ShouldReturnFalse_WhenWithinInterval() + { + var now = DateTime.UtcNow; + + var shouldRun = UnifiedImapSynchronizer.ShouldRunUidReconcile( + lastUidReconcileUtc: now.AddHours(-1), + utcNow: now, + reconcileInterval: TimeSpan.FromHours(12)); + + shouldRun.Should().BeFalse(); + } + + [Fact] + public void ShouldRunUidReconcile_ShouldReturnTrue_WhenIntervalElapsed() + { + var now = DateTime.UtcNow; + + var shouldRun = UnifiedImapSynchronizer.ShouldRunUidReconcile( + lastUidReconcileUtc: now.AddHours(-13), + utcNow: now, + reconcileInterval: TimeSpan.FromHours(12)); + + shouldRun.Should().BeTrue(); + } + + [Fact] + public async Task ProcessSummariesAsync_ShouldUseMetadataOnlyPackage() + { + var localFolder = new MailItemFolder + { + Id = Guid.NewGuid(), + MailAccountId = Guid.NewGuid(), + FolderName = "Inbox", + RemoteFolderId = "INBOX" + }; + + var summaryMock = new Mock(); + summaryMock.SetupGet(x => x.UniqueId).Returns(new UniqueId(42)); + summaryMock.SetupGet(x => x.Flags).Returns(MessageFlags.None); + + var mailServiceMock = new Mock(); + mailServiceMock + .Setup(x => x.GetExistingMailsAsync(localFolder.Id, It.IsAny>())) + .ReturnsAsync(new List()); + mailServiceMock + .Setup(x => x.CreateMailAsync(localFolder.MailAccountId, It.IsAny())) + .ReturnsAsync(true); + + var sut = new UnifiedImapSynchronizer( + Mock.Of(), + mailServiceMock.Object, + Mock.Of()); + + ImapMessageCreationPackage? capturedPackage = null; + + var imapSynchronizerMock = new Mock(); + imapSynchronizerMock + .Setup(x => x.CreateNewMailPackagesAsync(It.IsAny(), localFolder, It.IsAny())) + .Callback((package, _, _) => capturedPackage = package) + .ReturnsAsync(new List + { + new(new MailCopy { Id = "mail-id" }, null, localFolder.RemoteFolderId, Array.Empty()) + }); + + var processMethod = typeof(UnifiedImapSynchronizer).GetMethod("ProcessSummariesAsync", BindingFlags.Instance | BindingFlags.NonPublic); + processMethod.Should().NotBeNull(); + + var task = (Task>)processMethod!.Invoke( + sut, + [imapSynchronizerMock.Object, localFolder, new List { summaryMock.Object }, CancellationToken.None])!; + + var result = await task; + + result.Should().ContainSingle().Which.Should().Be("mail-id"); + capturedPackage.Should().NotBeNull(); + capturedPackage!.MimeMessage.Should().BeNull(); + } +} diff --git a/Wino.Core.Tests/Wino.Core.Tests.csproj b/Wino.Core.Tests/Wino.Core.Tests.csproj new file mode 100644 index 00000000..ce0461f6 --- /dev/null +++ b/Wino.Core.Tests/Wino.Core.Tests.csproj @@ -0,0 +1,34 @@ + + + net10.0 + enable + enable + false + true + false + false + false + false + false + x86;x64;arm64 + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + diff --git a/Wino.Core.UWP/AppThemes/TestTheme.xaml b/Wino.Core.UWP/AppThemes/TestTheme.xaml deleted file mode 100644 index 903b065b..00000000 --- a/Wino.Core.UWP/AppThemes/TestTheme.xaml +++ /dev/null @@ -1,22 +0,0 @@ - - - TestTheme.xaml - - - - - ms-appx:///BackgroundImages/bg6.jpg - #A3FFFFFF - #A3FFFFFF - #fdcb6e - - - - ms-appx:///BackgroundImages/bg6.jpg - - #A3000000 - #A3000000 - #A3262626 - - - diff --git a/Wino.Core.UWP/Assets/WinoIcons.ttf b/Wino.Core.UWP/Assets/WinoIcons.ttf deleted file mode 100644 index ef8acbf8..00000000 Binary files a/Wino.Core.UWP/Assets/WinoIcons.ttf and /dev/null differ diff --git a/Wino.Core.UWP/BasePage.cs b/Wino.Core.UWP/BasePage.cs deleted file mode 100644 index c39e454c..00000000 --- a/Wino.Core.UWP/BasePage.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using CommunityToolkit.Mvvm.Messaging; -using Microsoft.Extensions.DependencyInjection; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; -using Windows.UI.Xaml.Navigation; -using Wino.Core.ViewModels; -using Wino.Messaging.Client.Shell; - -namespace Wino.Core.UWP; - -public partial class BasePage : Page, IRecipient -{ - public UIElement ShellContent - { - get { return (UIElement)GetValue(ShellContentProperty); } - set { SetValue(ShellContentProperty, value); } - } - - public static readonly DependencyProperty ShellContentProperty = DependencyProperty.Register(nameof(ShellContent), typeof(UIElement), typeof(BasePage), new PropertyMetadata(null)); - - public void Receive(LanguageChanged message) - { - OnLanguageChanged(); - } - - public virtual void OnLanguageChanged() { } -} - -public abstract class BasePage : BasePage where T : CoreBaseViewModel -{ - public T ViewModel { get; } = WinoApplication.Current.Services.GetService(); - - protected BasePage() - { - ViewModel.Dispatcher = new UWPDispatcher(Dispatcher); - - Loaded += PageLoaded; - Unloaded += PageUnloaded; - } - - private void PageUnloaded(object sender, RoutedEventArgs e) - { - Loaded -= PageLoaded; - Unloaded -= PageUnloaded; - } - - private void PageLoaded(object sender, RoutedEventArgs e) => ViewModel.OnPageLoaded(); - - ~BasePage() - { - Debug.WriteLine($"Disposed {GetType().Name}"); - } - - [RequiresDynamicCode("AOT")] - [RequiresUnreferencedCode("AOT")] - protected override void OnNavigatedTo(NavigationEventArgs e) - { - base.OnNavigatedTo(e); - - var mode = GetNavigationMode(e.NavigationMode); - var parameter = e.Parameter; - - WeakReferenceMessenger.Default.UnregisterAll(this); - WeakReferenceMessenger.Default.RegisterAll(this); - - ViewModel.OnNavigatedTo(mode, parameter); - } - - protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) - { - base.OnNavigatingFrom(e); - - var mode = GetNavigationMode(e.NavigationMode); - var parameter = e.Parameter; - - WeakReferenceMessenger.Default.UnregisterAll(this); - - ViewModel.OnNavigatedFrom(mode, parameter); - - GC.Collect(); - } - - private Domain.Models.Navigation.NavigationMode GetNavigationMode(NavigationMode mode) - { - return (Domain.Models.Navigation.NavigationMode)mode; - } -} diff --git a/Wino.Core.UWP/Controls/WinoAppTitleBar.xaml b/Wino.Core.UWP/Controls/WinoAppTitleBar.xaml deleted file mode 100644 index 4a3f5eab..00000000 --- a/Wino.Core.UWP/Controls/WinoAppTitleBar.xaml +++ /dev/null @@ -1,193 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Wino.Core.UWP/Controls/WinoAppTitleBar.xaml.cs b/Wino.Core.UWP/Controls/WinoAppTitleBar.xaml.cs deleted file mode 100644 index 734650a6..00000000 --- a/Wino.Core.UWP/Controls/WinoAppTitleBar.xaml.cs +++ /dev/null @@ -1,255 +0,0 @@ -using System.Windows.Input; -using Windows.Foundation; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; -using Wino.Core.Domain.Enums; - -namespace Wino.Core.UWP.Controls; - -public sealed partial class WinoAppTitleBar : UserControl -{ - public event TypedEventHandler BackButtonClicked; - - public static readonly DependencyProperty IsRenderingPaneVisibleProperty = DependencyProperty.Register(nameof(IsRenderingPaneVisible), typeof(bool), typeof(WinoAppTitleBar), new PropertyMetadata(false, OnDrawingPropertyChanged)); - public static readonly DependencyProperty IsReaderNarrowedProperty = DependencyProperty.Register(nameof(IsReaderNarrowed), typeof(bool), typeof(WinoAppTitleBar), new PropertyMetadata(false, OnIsReaderNarrowedChanged)); - public static readonly DependencyProperty IsBackButtonVisibleProperty = DependencyProperty.Register(nameof(IsBackButtonVisible), typeof(bool), typeof(WinoAppTitleBar), new PropertyMetadata(false, OnDrawingPropertyChanged)); - public static readonly DependencyProperty OpenPaneLengthProperty = DependencyProperty.Register(nameof(OpenPaneLength), typeof(double), typeof(WinoAppTitleBar), new PropertyMetadata(0d, OnDrawingPropertyChanged)); - public static readonly DependencyProperty IsNavigationPaneOpenProperty = DependencyProperty.Register(nameof(IsNavigationPaneOpen), typeof(bool), typeof(WinoAppTitleBar), new PropertyMetadata(false, OnDrawingPropertyChanged)); - public static readonly DependencyProperty NavigationViewDisplayModeProperty = DependencyProperty.Register(nameof(NavigationViewDisplayMode), typeof(Microsoft.UI.Xaml.Controls.NavigationViewDisplayMode), typeof(WinoAppTitleBar), new PropertyMetadata(Microsoft.UI.Xaml.Controls.NavigationViewDisplayMode.Compact, OnDrawingPropertyChanged)); - public static readonly DependencyProperty ShellFrameContentProperty = DependencyProperty.Register(nameof(ShellFrameContent), typeof(UIElement), typeof(WinoAppTitleBar), new PropertyMetadata(null, OnDrawingPropertyChanged)); - public static readonly DependencyProperty SystemReservedProperty = DependencyProperty.Register(nameof(SystemReserved), typeof(double), typeof(WinoAppTitleBar), new PropertyMetadata(0, OnDrawingPropertyChanged)); - public static readonly DependencyProperty CoreWindowTextProperty = DependencyProperty.Register(nameof(CoreWindowText), typeof(string), typeof(WinoAppTitleBar), new PropertyMetadata(string.Empty, OnDrawingPropertyChanged)); - public static readonly DependencyProperty ReadingPaneLengthProperty = DependencyProperty.Register(nameof(ReadingPaneLength), typeof(double), typeof(WinoAppTitleBar), new PropertyMetadata(420d, OnDrawingPropertyChanged)); - public static readonly DependencyProperty ConnectionStatusProperty = DependencyProperty.Register(nameof(ConnectionStatus), typeof(WinoServerConnectionStatus), typeof(WinoAppTitleBar), new PropertyMetadata(WinoServerConnectionStatus.None, new PropertyChangedCallback(OnConnectionStatusChanged))); - public static readonly DependencyProperty ReconnectCommandProperty = DependencyProperty.Register(nameof(ReconnectCommand), typeof(ICommand), typeof(WinoAppTitleBar), new PropertyMetadata(null)); - public static readonly DependencyProperty ShrinkShellContentOnExpansionProperty = DependencyProperty.Register(nameof(ShrinkShellContentOnExpansion), typeof(bool), typeof(WinoAppTitleBar), new PropertyMetadata(true)); - public static readonly DependencyProperty IsDragAreaProperty = DependencyProperty.Register(nameof(IsDragArea), typeof(bool), typeof(WinoAppTitleBar), new PropertyMetadata(false, new PropertyChangedCallback(OnIsDragAreaChanged))); - public static readonly DependencyProperty IsShellFrameContentVisibleProperty = DependencyProperty.Register(nameof(IsShellFrameContentVisible), typeof(bool), typeof(WinoAppTitleBar), new PropertyMetadata(true)); - public static readonly DependencyProperty IsMenuButtonVisibleProperty = DependencyProperty.Register(nameof(IsMenuButtonVisible), typeof(bool), typeof(WinoAppTitleBar), new PropertyMetadata(true)); - - public bool IsShellFrameContentVisible - { - get { return (bool)GetValue(IsShellFrameContentVisibleProperty); } - set { SetValue(IsShellFrameContentVisibleProperty, value); } - } - - public ICommand ReconnectCommand - { - get { return (ICommand)GetValue(ReconnectCommandProperty); } - set { SetValue(ReconnectCommandProperty, value); } - } - - public WinoServerConnectionStatus ConnectionStatus - { - get { return (WinoServerConnectionStatus)GetValue(ConnectionStatusProperty); } - set { SetValue(ConnectionStatusProperty, value); } - } - - public string CoreWindowText - { - get { return (string)GetValue(CoreWindowTextProperty); } - set { SetValue(CoreWindowTextProperty, value); } - } - - public bool IsDragArea - { - get { return (bool)GetValue(IsDragAreaProperty); } - set { SetValue(IsDragAreaProperty, value); } - } - - - public double SystemReserved - { - get { return (double)GetValue(SystemReservedProperty); } - set { SetValue(SystemReservedProperty, value); } - } - - public UIElement ShellFrameContent - { - get { return (UIElement)GetValue(ShellFrameContentProperty); } - set { SetValue(ShellFrameContentProperty, value); } - } - - public Microsoft.UI.Xaml.Controls.NavigationViewDisplayMode NavigationViewDisplayMode - { - get { return (Microsoft.UI.Xaml.Controls.NavigationViewDisplayMode)GetValue(NavigationViewDisplayModeProperty); } - set { SetValue(NavigationViewDisplayModeProperty, value); } - } - - public bool ShrinkShellContentOnExpansion - { - get { return (bool)GetValue(ShrinkShellContentOnExpansionProperty); } - set { SetValue(ShrinkShellContentOnExpansionProperty, value); } - } - - public bool IsNavigationPaneOpen - { - get { return (bool)GetValue(IsNavigationPaneOpenProperty); } - set { SetValue(IsNavigationPaneOpenProperty, value); } - } - - public double OpenPaneLength - { - get { return (double)GetValue(OpenPaneLengthProperty); } - set { SetValue(OpenPaneLengthProperty, value); } - } - - - - public bool IsMenuButtonVisible - { - get { return (bool)GetValue(IsMenuButtonVisibleProperty); } - set { SetValue(IsMenuButtonVisibleProperty, value); } - } - - - public bool IsBackButtonVisible - { - get { return (bool)GetValue(IsBackButtonVisibleProperty); } - set { SetValue(IsBackButtonVisibleProperty, value); } - } - - public bool IsReaderNarrowed - { - get { return (bool)GetValue(IsReaderNarrowedProperty); } - set { SetValue(IsReaderNarrowedProperty, value); } - } - - public bool IsRenderingPaneVisible - { - get { return (bool)GetValue(IsRenderingPaneVisibleProperty); } - set { SetValue(IsRenderingPaneVisibleProperty, value); } - } - - public double ReadingPaneLength - { - get { return (double)GetValue(ReadingPaneLengthProperty); } - set { SetValue(ReadingPaneLengthProperty, value); } - } - - private static void OnIsReaderNarrowedChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) - { - if (obj is WinoAppTitleBar bar) - { - bar.DrawTitleBar(); - } - } - - private static void OnDrawingPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) - { - if (obj is WinoAppTitleBar bar) - { - bar.DrawTitleBar(); - } - } - - private static void OnConnectionStatusChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) - { - if (obj is WinoAppTitleBar bar) - { - bar.UpdateConnectionStatus(); - } - } - - private static void OnIsDragAreaChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) - { - if (obj is WinoAppTitleBar bar) - { - bar.SetDragArea(); - } - } - - private void SetDragArea() - { - if (IsDragArea) - { - Window.Current.SetTitleBar(dragbar); - } - } - - private void UpdateConnectionStatus() - { - - } - - private void DrawTitleBar() - { - UpdateLayout(); - - CoreWindowTitleTextBlock.Visibility = Visibility.Collapsed; - ShellContentContainer.Width = double.NaN; - ShellContentContainer.Margin = new Thickness(0, 0, 0, 0); - ShellContentContainer.HorizontalAlignment = HorizontalAlignment.Stretch; - - EmptySpaceWidth.Width = new GridLength(1, GridUnitType.Star); - - // Menu is not visible. - if (NavigationViewDisplayMode == Microsoft.UI.Xaml.Controls.NavigationViewDisplayMode.Minimal) - { - - } - else if (NavigationViewDisplayMode == Microsoft.UI.Xaml.Controls.NavigationViewDisplayMode.Compact) - { - // Icons are visible. - - if (!IsReaderNarrowed && ShrinkShellContentOnExpansion) - { - ShellContentContainer.HorizontalAlignment = HorizontalAlignment.Left; - ShellContentContainer.Width = ReadingPaneLength; - } - } - else if (NavigationViewDisplayMode == Microsoft.UI.Xaml.Controls.NavigationViewDisplayMode.Expanded) - { - if (IsNavigationPaneOpen) - { - CoreWindowTitleTextBlock.Visibility = Visibility.Visible; - - // LMargin = OpenPaneLength - LeftMenuStackPanel - ShellContentContainer.Margin = new Thickness(OpenPaneLength - LeftMenuStackPanel.ActualSize.X, 0, 0, 0); - - if (!IsReaderNarrowed && ShrinkShellContentOnExpansion) - { - ShellContentContainer.HorizontalAlignment = HorizontalAlignment.Left; - ShellContentContainer.Width = ReadingPaneLength; - } - } - else - { - if (ShrinkShellContentOnExpansion) - { - EmptySpaceWidth.Width = new GridLength(ReadingPaneLength, GridUnitType.Pixel); - } - else - { - EmptySpaceWidth.Width = new GridLength(ReadingPaneLength, GridUnitType.Star); - } - } - } - } - - public WinoAppTitleBar() - { - InitializeComponent(); - } - - private void BackClicked(object sender, RoutedEventArgs e) - { - BackButtonClicked?.Invoke(this, e); - } - - private void PaneClicked(object sender, RoutedEventArgs e) - { - IsNavigationPaneOpen = !IsNavigationPaneOpen; - } - - private void TitlebarSizeChanged(object sender, SizeChangedEventArgs e) => DrawTitleBar(); - - private void ReconnectClicked(object sender, RoutedEventArgs e) - { - // Close the popup for reconnect button. - ReconnectFlyout.Hide(); - - // Execute the reconnect command. - ReconnectCommand?.Execute(null); - } -} diff --git a/Wino.Core.UWP/Dialogs/AccountPickerDialog.xaml b/Wino.Core.UWP/Dialogs/AccountPickerDialog.xaml deleted file mode 100644 index e7bcd9ce..00000000 --- a/Wino.Core.UWP/Dialogs/AccountPickerDialog.xaml +++ /dev/null @@ -1,19 +0,0 @@ - - - - diff --git a/Wino.Core.UWP/Dispatcher.cs b/Wino.Core.UWP/Dispatcher.cs deleted file mode 100644 index 34b5b576..00000000 --- a/Wino.Core.UWP/Dispatcher.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Threading.Tasks; -using Windows.UI.Core; -using Wino.Core.Domain.Interfaces; - -namespace Wino.Core.UWP; - -public class UWPDispatcher : IDispatcher -{ - private readonly CoreDispatcher _coreDispatcher; - - public UWPDispatcher(CoreDispatcher coreDispatcher) - { - _coreDispatcher = coreDispatcher; - } - - public Task ExecuteOnUIThread(Action action) - => _coreDispatcher.RunAsync(CoreDispatcherPriority.Normal, () => action()).AsTask(); -} diff --git a/Wino.Core.UWP/Extensions/CompositionExtensions.Size.cs b/Wino.Core.UWP/Extensions/CompositionExtensions.Size.cs deleted file mode 100644 index 93bcb248..00000000 --- a/Wino.Core.UWP/Extensions/CompositionExtensions.Size.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System; -using System.Numerics; -using System.Threading.Tasks; -using Windows.UI.Composition; -using Windows.UI.Xaml; - -namespace Wino.Extensions; - -public static partial class CompositionExtensions -{ - public static void StartSizeAnimation(this UIElement element, Vector2? from = null, Vector2? to = null, - double duration = 800, int delay = 0, CompositionEasingFunction easing = null, Action completed = null, - AnimationIterationBehavior iterationBehavior = AnimationIterationBehavior.Count) - { - CompositionScopedBatch batch = null; - - var visual = element.Visual(); - var compositor = visual.Compositor; - - if (completed != null) - { - batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation); - batch.Completed += (s, e) => completed(); - } - - if (to == null) - { - to = Vector2.One; - } - - visual.StartAnimation("Size", - compositor.CreateVector2KeyFrameAnimation(from, to.Value, duration, delay, easing, iterationBehavior)); - - batch?.End(); - } - - public static void StartSizeAnimation(this Visual visual, Vector2? from = null, Vector2? to = null, - double duration = 800, int delay = 0, CompositionEasingFunction easing = null, Action completed = null, - AnimationIterationBehavior iterationBehavior = AnimationIterationBehavior.Count) - { - CompositionScopedBatch batch = null; - var compositor = visual.Compositor; - - if (completed != null) - { - batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation); - batch.Completed += (s, e) => completed(); - } - - if (to == null) - { - to = Vector2.One; - } - - visual.StartAnimation("Size", - compositor.CreateVector2KeyFrameAnimation(from, to.Value, duration, delay, easing, iterationBehavior)); - - batch?.End(); - } - - public static Task StartSizeAnimationAsync(this UIElement element, Vector2? from = null, Vector2? to = null, - double duration = 800, int delay = 0, CompositionEasingFunction easing = null, - AnimationIterationBehavior iterationBehavior = AnimationIterationBehavior.Count) - { - CompositionScopedBatch batch; - - var visual = element.Visual(); - var compositor = visual.Compositor; - - var taskSource = new TaskCompletionSource(); - - void Completed(object o, CompositionBatchCompletedEventArgs e) - { - batch.Completed -= Completed; - taskSource.SetResult(true); - } - - batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation); - batch.Completed += Completed; - - if (to == null) - { - to = Vector2.One; - } - - visual.StartAnimation("Size", - compositor.CreateVector2KeyFrameAnimation(from, to.Value, duration, delay, easing, iterationBehavior)); - - batch.End(); - - return taskSource.Task; - } - - public static Task StartSizeAnimationAsync(this Visual visual, Vector2? from = null, Vector2? to = null, - double duration = 800, int delay = 0, CompositionEasingFunction easing = null, - AnimationIterationBehavior iterationBehavior = AnimationIterationBehavior.Count) - { - CompositionScopedBatch batch; - - var compositor = visual.Compositor; - - var taskSource = new TaskCompletionSource(); - - void Completed(object o, CompositionBatchCompletedEventArgs e) - { - batch.Completed -= Completed; - taskSource.SetResult(true); - } - - batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation); - batch.Completed += Completed; - - if (to == null) - { - to = Vector2.One; - } - - visual.StartAnimation("Size", - compositor.CreateVector2KeyFrameAnimation(from, to.Value, duration, delay, easing, iterationBehavior)); - - batch.End(); - - return taskSource.Task; - } - -} diff --git a/Wino.Core.UWP/Selectors/AppThemePreviewTemplateSelector.cs b/Wino.Core.UWP/Selectors/AppThemePreviewTemplateSelector.cs deleted file mode 100644 index 6ed85638..00000000 --- a/Wino.Core.UWP/Selectors/AppThemePreviewTemplateSelector.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; -using Wino.Core.UWP.Models.Personalization; - -namespace Wino.Core.UWP.Selectors; - -public partial class AppThemePreviewTemplateSelector : DataTemplateSelector -{ - public DataTemplate SystemThemeTemplate { get; set; } - public DataTemplate PreDefinedThemeTemplate { get; set; } - public DataTemplate CustomAppTemplate { get; set; } - - protected override DataTemplate SelectTemplateCore(object item) - { - if (item is SystemAppTheme) - return SystemThemeTemplate; - else if (item is PreDefinedAppTheme) - return PreDefinedThemeTemplate; - else if (item is CustomAppTheme) - return CustomAppTemplate; - - return base.SelectTemplateCore(item); - } -} diff --git a/Wino.Core.UWP/Selectors/NavigationMenuTemplateSelector.cs b/Wino.Core.UWP/Selectors/NavigationMenuTemplateSelector.cs deleted file mode 100644 index 516f82ca..00000000 --- a/Wino.Core.UWP/Selectors/NavigationMenuTemplateSelector.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; -using Wino.Core.Domain.MenuItems; - -namespace Wino.Core.UWP.Selectors; - -public partial class NavigationMenuTemplateSelector : DataTemplateSelector -{ - public DataTemplate MenuItemTemplate { get; set; } - public DataTemplate AccountManagementTemplate { get; set; } - public DataTemplate ClickableAccountMenuTemplate { get; set; } - public DataTemplate MergedAccountTemplate { get; set; } - public DataTemplate MergedAccountFolderTemplate { get; set; } - public DataTemplate MergedAccountMoreExpansionItemTemplate { get; set; } - public DataTemplate FolderMenuTemplate { get; set; } - public DataTemplate SettingsItemTemplate { get; set; } - public DataTemplate MoreItemsFolderTemplate { get; set; } - public DataTemplate RatingItemTemplate { get; set; } - public DataTemplate CreateNewFolderTemplate { get; set; } - public DataTemplate SeperatorTemplate { get; set; } - public DataTemplate NewMailTemplate { get; set; } - public DataTemplate CategoryItemsTemplate { get; set; } - public DataTemplate FixAuthenticationIssueTemplate { get; set; } - public DataTemplate FixMissingFolderConfigTemplate { get; set; } - - protected override DataTemplate SelectTemplateCore(object item) - { - if (item is NewMailMenuItem) - return NewMailTemplate; - else if (item is SettingsItem) - return SettingsItemTemplate; - else if (item is SeperatorItem) - return SeperatorTemplate; - else if (item is AccountMenuItem accountMenuItem) - // Merged inbox account menu items must be nested. - return ClickableAccountMenuTemplate; - else if (item is ManageAccountsMenuItem) - return AccountManagementTemplate; - else if (item is RateMenuItem) - return RatingItemTemplate; - else if (item is MergedAccountMenuItem) - return MergedAccountTemplate; - else if (item is MergedAccountMoreFolderMenuItem) - return MergedAccountMoreExpansionItemTemplate; - else if (item is MergedAccountFolderMenuItem) - return MergedAccountFolderTemplate; - else if (item is FolderMenuItem) - return FolderMenuTemplate; - else if (item is FixAccountIssuesMenuItem fixAccountIssuesMenuItem) - return fixAccountIssuesMenuItem.Account.AttentionReason == Domain.Enums.AccountAttentionReason.MissingSystemFolderConfiguration - ? FixMissingFolderConfigTemplate : FixAuthenticationIssueTemplate; - else - { - var type = item.GetType(); - return null; - } - } -} diff --git a/Wino.Core.UWP/Services/BackgroundTaskService.cs b/Wino.Core.UWP/Services/BackgroundTaskService.cs deleted file mode 100644 index 829f1b8f..00000000 --- a/Wino.Core.UWP/Services/BackgroundTaskService.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Serilog; -using Windows.ApplicationModel.Background; -using Wino.Core.Domain.Interfaces; - -namespace Wino.Core.UWP.Services; - -public class BackgroundTaskService : IBackgroundTaskService -{ - private const string IsBackgroundTasksUnregisteredKey = nameof(IsBackgroundTasksUnregisteredKey); - public const string ToastNotificationActivationHandlerTaskName = "ToastNotificationActivationHandlerTask"; - - private readonly IConfigurationService _configurationService; - - public BackgroundTaskService(IConfigurationService configurationService) - { - _configurationService = configurationService; - } - - public void UnregisterAllBackgroundTask() - { - if (_configurationService.Get(IsBackgroundTasksUnregisteredKey, false)) - { - foreach (var task in BackgroundTaskRegistration.AllTasks) - { - task.Value.Unregister(true); - } - - Log.Information("Unregistered all background tasks."); - _configurationService.Set(IsBackgroundTasksUnregisteredKey, true); - } - } - - public Task RegisterBackgroundTasksAsync() - { - return RegisterToastNotificationHandlerBackgroundTaskAsync(); - } - - public async Task RegisterToastNotificationHandlerBackgroundTaskAsync() - { - // If background task is already registered, do nothing. - if (BackgroundTaskRegistration.AllTasks.Any(i => i.Value.Name.Equals(ToastNotificationActivationHandlerTaskName))) - return; - - // Otherwise request access - BackgroundAccessStatus status = await BackgroundExecutionManager.RequestAccessAsync(); - - // Create the background task - BackgroundTaskBuilder builder = new BackgroundTaskBuilder() - { - Name = ToastNotificationActivationHandlerTaskName - }; - - // Assign the toast action trigger - builder.SetTrigger(new ToastNotificationActionTrigger()); - - // And register the task - BackgroundTaskRegistration registration = builder.Register(); - } -} diff --git a/Wino.Core.UWP/Services/KeyPressService.cs b/Wino.Core.UWP/Services/KeyPressService.cs deleted file mode 100644 index 664682ee..00000000 --- a/Wino.Core.UWP/Services/KeyPressService.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Windows.System; -using Windows.UI.Core; -using Windows.UI.Xaml; -using Wino.Core.Domain.Interfaces; - -namespace Wino.Core.UWP.Services; - -public class KeyPressService : IKeyPressService -{ - public bool IsCtrlKeyPressed() - => Window.Current?.CoreWindow?.GetKeyState(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down) ?? false; - - public bool IsShiftKeyPressed() - => Window.Current?.CoreWindow?.GetKeyState(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down) ?? false; -} diff --git a/Wino.Core.UWP/Services/NotificationBuilder.cs b/Wino.Core.UWP/Services/NotificationBuilder.cs deleted file mode 100644 index 722979c3..00000000 --- a/Wino.Core.UWP/Services/NotificationBuilder.cs +++ /dev/null @@ -1,257 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.Messaging; -using CommunityToolkit.WinUI.Notifications; -using Serilog; -using Windows.Data.Xml.Dom; -using Windows.UI.Notifications; -using Wino.Core.Domain; -using Wino.Core.Domain.Entities.Mail; -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.MailItem; -using Wino.Messaging.UI; - -namespace Wino.Core.UWP.Services; - -// TODO: Refactor this thing. It's garbage. - -public class NotificationBuilder : INotificationBuilder -{ - private readonly IUnderlyingThemeService _underlyingThemeService; - private readonly IAccountService _accountService; - private readonly IFolderService _folderService; - private readonly IMailService _mailService; - private readonly IThumbnailService _thumbnailService; - - public NotificationBuilder(IUnderlyingThemeService underlyingThemeService, - IAccountService accountService, - IFolderService folderService, - IMailService mailService, - IThumbnailService thumbnailService) - { - _underlyingThemeService = underlyingThemeService; - _accountService = accountService; - _folderService = folderService; - _mailService = mailService; - _thumbnailService = thumbnailService; - - WeakReferenceMessenger.Default.Register(this, (r, msg) => - { - RemoveNotification(msg.UniqueId); - }); - } - - public async Task CreateNotificationsAsync(Guid inboxFolderId, IEnumerable downloadedMailItems) - { - var mailCount = downloadedMailItems.Count(); - - try - { - // If there are more than 3 mails, just display 1 general toast. - if (mailCount > 3) - { - var builder = new ToastContentBuilder(); - builder.SetToastScenario(ToastScenario.Default); - - builder.AddText(Translator.Notifications_MultipleNotificationsTitle); - builder.AddText(string.Format(Translator.Notifications_MultipleNotificationsMessage, mailCount)); - - builder.AddButton(GetDismissButton()); - builder.AddAudio(new ToastAudio() - { - Src = new Uri("ms-winsoundevent:Notification.Mail") - }); - - builder.Show(); - } - else - { - var validItems = new List(); - - // Fetch mails again to fill up assigned folder data and latest statuses. - // They've been marked as read by executing synchronizer tasks until inital sync finishes. - - foreach (var item in downloadedMailItems) - { - var mailItem = await _mailService.GetSingleMailItemAsync(item.UniqueId); - - if (mailItem != null && mailItem.AssignedFolder != null) - { - validItems.Add(mailItem); - } - } - - foreach (var mailItem in validItems) - { - if (mailItem.IsRead) - { - // Remove the notification for a specific mail if it exists - ToastNotificationManager.History.Remove(mailItem.UniqueId.ToString()); - continue; - } - - var builder = new ToastContentBuilder(); - builder.SetToastScenario(ToastScenario.Default); - - var avatarThumbnail = await _thumbnailService.GetThumbnailAsync(mailItem.FromAddress, awaitLoad: true); - if (!string.IsNullOrEmpty(avatarThumbnail)) - { - var tempFile = await Windows.Storage.ApplicationData.Current.TemporaryFolder.CreateFileAsync($"{Guid.NewGuid()}.png", Windows.Storage.CreationCollisionOption.ReplaceExisting); - await using (var stream = await tempFile.OpenStreamForWriteAsync()) - { - var bytes = Convert.FromBase64String(avatarThumbnail); - await stream.WriteAsync(bytes); - } - builder.AddAppLogoOverride(new Uri($"ms-appdata:///temp/{tempFile.Name}"), hintCrop: ToastGenericAppLogoCrop.Default); - } - - // Override system notification timetamp with received date of the mail. - // It may create confusion for some users, but still it's the truth... - builder.AddCustomTimeStamp(mailItem.CreationDate.ToLocalTime()); - - builder.AddText(mailItem.FromName); - builder.AddText(mailItem.Subject); - builder.AddText(mailItem.PreviewText); - - builder.AddArgument(Constants.ToastMailUniqueIdKey, mailItem.UniqueId.ToString()); - builder.AddArgument(Constants.ToastActionKey, MailOperation.Navigate); - - builder.AddButton(GetMarkAsReadButton(mailItem.UniqueId)); - builder.AddButton(GetDeleteButton(mailItem.UniqueId)); - builder.AddButton(GetArchiveButton(mailItem.UniqueId)); - builder.AddAudio(new ToastAudio() - { - Src = new Uri("ms-winsoundevent:Notification.Mail") - }); - - // Use UniqueId as tag to allow removal - builder.Show(toast => toast.Tag = mailItem.UniqueId.ToString()); - } - - await UpdateTaskbarIconBadgeAsync(); - } - } - catch (Exception ex) - { - Log.Error(ex, "Failed to create notifications."); - } - } - - private ToastButton GetDismissButton() - => new ToastButton() - .SetDismissActivation() - .SetImageUri(new Uri("ms-appx:///Assets/NotificationIcons/dismiss.png")); - - private static ToastButton GetArchiveButton(Guid mailUniqueId) - => new ToastButton() - .SetContent(Translator.MailOperation_Archive) - .SetImageUri(new Uri("ms-appx:///Assets/NotificationIcons/archive.png")) - .AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString()) - .AddArgument(Constants.ToastActionKey, MailOperation.Archive) - .SetBackgroundActivation(); - - private ToastButton GetDeleteButton(Guid mailUniqueId) - => new ToastButton() - .SetContent(Translator.MailOperation_Delete) - .SetImageUri(new Uri("ms-appx:///Assets/NotificationIcons/delete.png")) - .AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString()) - .AddArgument(Constants.ToastActionKey, MailOperation.SoftDelete) - .SetBackgroundActivation(); - - private static ToastButton GetMarkAsReadButton(Guid mailUniqueId) - => new ToastButton() - .SetContent(Translator.MailOperation_MarkAsRead) - .SetImageUri(new System.Uri("ms-appx:///Assets/NotificationIcons/markread.png")) - .AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString()) - .AddArgument(Constants.ToastActionKey, MailOperation.MarkAsRead) - .SetBackgroundActivation(); - - public async Task UpdateTaskbarIconBadgeAsync() - { - int totalUnreadCount = 0; - - try - { - var badgeUpdater = BadgeUpdateManager.CreateBadgeUpdaterForApplication(); - var accounts = await _accountService.GetAccountsAsync(); - - foreach (var account in accounts) - { - if (!account.Preferences.IsTaskbarBadgeEnabled) continue; - - var accountInbox = await _folderService.GetSpecialFolderByAccountIdAsync(account.Id, SpecialFolderType.Inbox); - - if (accountInbox == null) - continue; - - var inboxUnreadCount = await _folderService.GetFolderNotificationBadgeAsync(accountInbox.Id); - - totalUnreadCount += inboxUnreadCount; - } - - if (totalUnreadCount > 0) - { - // Get the blank badge XML payload for a badge number - XmlDocument badgeXml = BadgeUpdateManager.GetTemplateContent(BadgeTemplateType.BadgeNumber); - - // Set the value of the badge in the XML to our number - XmlElement badgeElement = badgeXml.SelectSingleNode("/badge") as XmlElement; - badgeElement.SetAttribute("value", totalUnreadCount.ToString()); - - // Create the badge notification - BadgeNotification badge = new BadgeNotification(badgeXml); - - // And update the badge - badgeUpdater.Update(badge); - } - else - badgeUpdater.Clear(); - } - catch (Exception ex) - { - Log.Error(ex, "Error while updating taskbar badge."); - } - } - - public async Task CreateTestNotificationAsync(string title, string message) - { - // with args test. - await CreateNotificationsAsync(Guid.Parse("28c3c39b-7147-4de3-b209-949bd19eede6"), new List() - { - new MailCopy() - { - Subject = "test subject", - PreviewText = "preview html", - CreationDate = DateTime.UtcNow, - FromAddress = "bkaankose@outlook.com", - Id = "AAkALgAAAAAAHYQDEapmEc2byACqAC-EWg0AnMdP0zg8wkS_Ib2Eeh80LAAGq91I3QAA", - } - }); - - //var builder = new ToastContentBuilder(); - //builder.SetToastScenario(ToastScenario.Default); - - //builder.AddText(title); - //builder.AddText(message); - - //builder.Show(); - - //await Task.CompletedTask; - } - - public void RemoveNotification(Guid mailUniqueId) - { - try - { - ToastNotificationManager.History.Remove(mailUniqueId.ToString()); - } - catch (Exception ex) - { - Log.Error(ex, $"Failed to remove notification for mail {mailUniqueId}"); - } - } -} diff --git a/Wino.Core.UWP/Services/StoreManagementService.cs b/Wino.Core.UWP/Services/StoreManagementService.cs deleted file mode 100644 index 91fb9504..00000000 --- a/Wino.Core.UWP/Services/StoreManagementService.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Windows.Services.Store; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Store; - -namespace Wino.Core.UWP.Services; - -public class StoreManagementService : IStoreManagementService -{ - private StoreContext CurrentContext { get; } - - private readonly Dictionary productIds = new Dictionary() - { - { StoreProductType.UnlimitedAccounts, "UnlimitedAccounts" } - }; - - private readonly Dictionary skuIds = new Dictionary() - { - { StoreProductType.UnlimitedAccounts, "9P02MXZ42GSM" } - }; - - public StoreManagementService() - { - CurrentContext = StoreContext.GetDefault(); - } - - public async Task HasProductAsync(StoreProductType productType) - { - var productKey = productIds[productType]; - var appLicense = await CurrentContext.GetAppLicenseAsync(); - - if (appLicense == null) - return false; - - // Access the valid licenses for durable add-ons for this app. - foreach (KeyValuePair item in appLicense.AddOnLicenses) - { - StoreLicense addOnLicense = item.Value; - - if (addOnLicense.InAppOfferToken == productKey) - { - return addOnLicense.IsActive; - } - } - - return false; - } - - public async Task PurchaseAsync(StoreProductType productType) - { - if (await HasProductAsync(productType)) - return Domain.Enums.StorePurchaseResult.AlreadyPurchased; - else - { - var productKey = skuIds[productType]; - - var result = await CurrentContext.RequestPurchaseAsync(productKey); - - switch (result.Status) - { - case StorePurchaseStatus.Succeeded: - return Domain.Enums.StorePurchaseResult.Succeeded; - case StorePurchaseStatus.AlreadyPurchased: - return Domain.Enums.StorePurchaseResult.AlreadyPurchased; - default: - return Domain.Enums.StorePurchaseResult.NotPurchased; - } - } - } -} diff --git a/Wino.Core.UWP/Services/WinoServerConnectionManager.cs b/Wino.Core.UWP/Services/WinoServerConnectionManager.cs deleted file mode 100644 index 49309940..00000000 --- a/Wino.Core.UWP/Services/WinoServerConnectionManager.cs +++ /dev/null @@ -1,369 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.Messaging; -using Nito.AsyncEx; -using Serilog; -using Windows.ApplicationModel; -using Windows.ApplicationModel.AppService; -using Windows.Foundation.Collections; -using Windows.Foundation.Metadata; -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Requests; -using Wino.Core.Domain.Models.Server; -using Wino.Core.Integration.Json; -using Wino.Messaging; -using Wino.Messaging.Client.Connection; -using Wino.Messaging.Enums; -using Wino.Messaging.Server; -using Wino.Messaging.UI; - -namespace Wino.Core.UWP.Services; - -public class WinoServerConnectionManager : - IWinoServerConnectionManager, - IRecipient -{ - private const int ServerConnectionTimeoutMs = 10000; - - public event EventHandler StatusChanged; - - public TaskCompletionSource ConnectingHandle { get; private set; } - - private ILogger Logger => Logger.ForContext(); - - private WinoServerConnectionStatus status; - - public WinoServerConnectionStatus Status - { - get { return status; } - private set - { - Log.Information("Server connection status changed to {Status}.", value); - status = value; - StatusChanged?.Invoke(this, value); - } - } - - private AppServiceConnection _connection; - public AppServiceConnection Connection - { - get { return _connection; } - set - { - if (_connection != null) - { - _connection.RequestReceived -= ServerMessageReceived; - _connection.ServiceClosed -= ServerDisconnected; - } - - _connection = value; - - if (value == null) - { - Status = WinoServerConnectionStatus.Disconnected; - } - else - { - value.RequestReceived += ServerMessageReceived; - value.ServiceClosed += ServerDisconnected; - - Status = WinoServerConnectionStatus.Connected; - } - } - } - - private readonly JsonSerializerOptions _jsonSerializerOptions = new() - { - TypeInfoResolver = new ServerRequestTypeInfoResolver() - }; - - public WinoServerConnectionManager() - { - WeakReferenceMessenger.Default.Register(this); - } - - public async Task ConnectAsync() - { - if (Status == WinoServerConnectionStatus.Connected) - { - Log.Information("Server is already connected."); - return true; - } - - if (Status == WinoServerConnectionStatus.Connecting) - { - // A connection is already being established at the moment. - // No need to run another connection establishment process. - // Await the connecting handler if possible. - - if (ConnectingHandle != null) - { - return await ConnectingHandle.Task; - } - } - - if (ApiInformation.IsApiContractPresent("Windows.ApplicationModel.FullTrustAppContract", 1, 0)) - { - try - { - ConnectingHandle = new TaskCompletionSource(); - - Status = WinoServerConnectionStatus.Connecting; - - var connectionCancellationToken = new CancellationTokenSource(TimeSpan.FromMilliseconds(ServerConnectionTimeoutMs)); - - await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync("WinoServer"); - - // Connection establishment handler is in App.xaml.cs OnBackgroundActivated. - // Once the connection is established, the handler will set the Connection property - // and WinoServerConnectionEstablished will be fired by the messenger. - - await ConnectingHandle.Task.WaitAsync(connectionCancellationToken.Token); - - Log.Information("Server connection established successfully."); - } - catch (OperationCanceledException canceledException) - { - Log.Error(canceledException, $"Server process did not start in {ServerConnectionTimeoutMs} ms. Operation is canceled."); - - ConnectingHandle?.TrySetException(canceledException); - - Status = WinoServerConnectionStatus.Failed; - return false; - } - catch (Exception ex) - { - Log.Error(ex, "Failed to connect to the server."); - - ConnectingHandle?.TrySetException(ex); - - Status = WinoServerConnectionStatus.Failed; - return false; - } - - return true; - } - else - { - Log.Information("FullTrustAppContract is not present in the system. Server connection is not possible."); - } - - return false; - } - - public async Task InitializeAsync() - { - var isConnectionSuccessfull = await ConnectAsync(); - - if (isConnectionSuccessfull) - { - Log.Information("ServerConnectionManager initialized successfully."); - } - else - { - Log.Error("ServerConnectionManager initialization failed."); - } - } - - private void ServerMessageReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args) - { - if (args.Request.Message.TryGetValue(MessageConstants.MessageTypeKey, out object messageTypeObject) && messageTypeObject is int messageTypeInt) - { - var messageType = (MessageType)messageTypeInt; - - if (args.Request.Message.TryGetValue(MessageConstants.MessageDataKey, out object messageDataObject) && messageDataObject is string messageJson) - { - switch (messageType) - { - case MessageType.UIMessage: - if (!args.Request.Message.TryGetValue(MessageConstants.MessageDataTypeKey, out object dataTypeObject) || dataTypeObject is not string dataTypeName) - throw new ArgumentException("Message data type is missing."); - - HandleUIMessage(messageJson, dataTypeName); - break; - default: - break; - } - } - } - } - - /// - /// Unpacks IServerMessage objects and delegate it to Messenger for UI to process. - /// - /// Message data in json format. - private void HandleUIMessage(string messageJson, string typeName) - { - switch (typeName) - { - case nameof(MailAddedMessage): - WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson, CommunicationMessagesContext.Default.MailAddedMessage)); - break; - case nameof(MailDownloadedMessage): - WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson, CommunicationMessagesContext.Default.MailDownloadedMessage)); - break; - case nameof(MailRemovedMessage): - WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson, CommunicationMessagesContext.Default.MailRemovedMessage)); - break; - case nameof(MailUpdatedMessage): - WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson, CommunicationMessagesContext.Default.MailUpdatedMessage)); - break; - case nameof(AccountCreatedMessage): - WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson, CommunicationMessagesContext.Default.AccountCreatedMessage)); - break; - case nameof(AccountRemovedMessage): - WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson, CommunicationMessagesContext.Default.AccountRemovedMessage)); - break; - case nameof(AccountUpdatedMessage): - WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson, CommunicationMessagesContext.Default.AccountUpdatedMessage)); - break; - case nameof(DraftCreated): - WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson, CommunicationMessagesContext.Default.DraftCreated)); - break; - case nameof(DraftFailed): - WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson, CommunicationMessagesContext.Default.DraftFailed)); - break; - case nameof(DraftMapped): - WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson, CommunicationMessagesContext.Default.DraftMapped)); - break; - case nameof(FolderRenamed): - WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson, CommunicationMessagesContext.Default.FolderRenamed)); - break; - case nameof(FolderSynchronizationEnabled): - WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson, CommunicationMessagesContext.Default.FolderSynchronizationEnabled)); - break; - case nameof(MergedInboxRenamed): - WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson, CommunicationMessagesContext.Default.MergedInboxRenamed)); - break; - case nameof(AccountSynchronizationCompleted): - WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson, CommunicationMessagesContext.Default.AccountSynchronizationCompleted)); - break; - case nameof(RefreshUnreadCountsMessage): - WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson, CommunicationMessagesContext.Default.RefreshUnreadCountsMessage)); - break; - case nameof(AccountSynchronizerStateChanged): - WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson, CommunicationMessagesContext.Default.AccountSynchronizerStateChanged)); - break; - case nameof(AccountSynchronizationProgressUpdatedMessage): - WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson, CommunicationMessagesContext.Default.AccountSynchronizationProgressUpdatedMessage)); - break; - case nameof(AccountFolderConfigurationUpdated): - WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson, CommunicationMessagesContext.Default.AccountFolderConfigurationUpdated)); - break; - case nameof(CopyAuthURLRequested): - WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson, CommunicationMessagesContext.Default.CopyAuthURLRequested)); - break; - case nameof(NewMailSynchronizationRequested): - WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson, CommunicationMessagesContext.Default.NewMailSynchronizationRequested)); - break; - case nameof(AccountCacheResetMessage): - WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson, CommunicationMessagesContext.Default.AccountCacheResetMessage)); - break; - default: - throw new Exception("Invalid data type name passed to client."); - } - } - - private void ServerDisconnected(AppServiceConnection sender, AppServiceClosedEventArgs args) - { - Log.Information("Server disconnected."); - } - - public async Task QueueRequestAsync(IRequestBase request, Guid accountId) - { - var queuePackage = new ServerRequestPackage(accountId, request); - - var queueResponse = await GetResponseInternalAsync(queuePackage, new Dictionary() - { - { MessageConstants.MessageDataRequestAccountIdKey, accountId } - }); - - queueResponse.ThrowIfFailed(); - } - - public Task> GetResponseAsync(TRequestType message, CancellationToken cancellationToken = default) where TRequestType : IClientMessage - => GetResponseInternalAsync(message, cancellationToken: cancellationToken); - - [RequiresDynamicCode("Calls System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions)")] - [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions)")] - private async Task> GetResponseInternalAsync(TRequestType message, - Dictionary parameters = null, - CancellationToken cancellationToken = default) - { - if (Status != WinoServerConnectionStatus.Connected) - await ConnectAsync(); - - if (Connection == null) return WinoServerResponse.CreateErrorResponse("Server connection is not established."); - - string serializedMessage = string.Empty; - - try - { - serializedMessage = JsonSerializer.Serialize(message, _jsonSerializerOptions); - } - catch (Exception serializationException) - { - Logger.Error(serializationException, $"Failed to serialize client message for sending."); - return WinoServerResponse.CreateErrorResponse($"Failed to serialize message.\n{serializationException.Message}"); - } - - AppServiceResponse response = null; - - try - { - var valueSet = new ValueSet - { - { MessageConstants.MessageTypeKey, (int)MessageType.ServerMessage }, - { MessageConstants.MessageDataKey, serializedMessage }, - { MessageConstants.MessageDataTypeKey, message.GetType().Name } - }; - - // Add additional parameters into ValueSet - if (parameters != null) - { - foreach (var item in parameters) - { - valueSet.Add(item.Key, item.Value); - } - } - - response = await Connection.SendMessageAsync(valueSet).AsTask(cancellationToken); - } - catch (OperationCanceledException) - { - return WinoServerResponse.CreateErrorResponse($"Request is canceled by client."); - } - catch (Exception serverSendException) - { - Logger.Error(serverSendException, $"Failed to send message to server."); - return WinoServerResponse.CreateErrorResponse($"Failed to send message to server.\n{serverSendException.Message}"); - } - - // It should be always Success. - if (response.Status != AppServiceResponseStatus.Success) - return WinoServerResponse.CreateErrorResponse($"Wino Server responded with '{response.Status}' status to message delivery."); - - // All responses must contain a message data. - if (!(response.Message.TryGetValue(MessageConstants.MessageDataKey, out object messageDataObject) && messageDataObject is string messageJson)) - return WinoServerResponse.CreateErrorResponse("Server response did not contain message data."); - - // Try deserialize the message data. - try - { - return JsonSerializer.Deserialize>(messageJson); - } - catch (Exception jsonDeserializationError) - { - Logger.Error(jsonDeserializationError, $"Failed to deserialize server response message data."); - return WinoServerResponse.CreateErrorResponse($"Failed to deserialize Wino server response message data.\n{jsonDeserializationError.Message}"); - } - } - - public void Receive(WinoServerConnectionEstablished message) - => ConnectingHandle?.TrySetResult(true); -} diff --git a/Wino.Core.UWP/Styles/CustomMessageDialogStyles.xaml.cs b/Wino.Core.UWP/Styles/CustomMessageDialogStyles.xaml.cs deleted file mode 100644 index 68d108bd..00000000 --- a/Wino.Core.UWP/Styles/CustomMessageDialogStyles.xaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Windows.UI.Xaml; - -namespace Wino.Core.UWP.Styles; - -partial class CustomMessageDialogStyles : ResourceDictionary -{ - public CustomMessageDialogStyles() - { - InitializeComponent(); - } -} diff --git a/Wino.Core.UWP/Views/Abstract/SettingsPageAbstract.cs b/Wino.Core.UWP/Views/Abstract/SettingsPageAbstract.cs deleted file mode 100644 index 3b776525..00000000 --- a/Wino.Core.UWP/Views/Abstract/SettingsPageAbstract.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Wino.Core.UWP; -using Wino.Core.ViewModels; - -namespace Wino.Views.Abstract; - -public abstract class SettingsPageAbstract : BasePage { } diff --git a/Wino.Core.UWP/Views/ManageAccountsPage.xaml b/Wino.Core.UWP/Views/ManageAccountsPage.xaml deleted file mode 100644 index 8b414196..00000000 --- a/Wino.Core.UWP/Views/ManageAccountsPage.xaml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Wino.Core.UWP/Views/ManageAccountsPage.xaml.cs b/Wino.Core.UWP/Views/ManageAccountsPage.xaml.cs deleted file mode 100644 index a3da8717..00000000 --- a/Wino.Core.UWP/Views/ManageAccountsPage.xaml.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System; -using System.Collections.ObjectModel; -using System.Linq; -using CommunityToolkit.Mvvm.Messaging; -using MoreLinq; -using Windows.UI.Xaml.Media.Animation; -using Windows.UI.Xaml.Navigation; -using Wino.Core.Domain; -using Wino.Core.Domain.Enums; -using Wino.Core.UWP.Views.Abstract; -using Wino.Mail.ViewModels.Data; -using Wino.Messaging.Client.Navigation; -using Wino.Messaging.UI; - -namespace Wino.Views; - -public sealed partial class ManageAccountsPage : ManageAccountsPageAbstract, - IRecipient, - IRecipient, - IRecipient, - IRecipient -{ - public ObservableCollection PageHistory { get; set; } = new ObservableCollection(); - - - public ManageAccountsPage() - { - InitializeComponent(); - } - - protected override void OnNavigatedTo(NavigationEventArgs e) - { - base.OnNavigatedTo(e); - - var initialRequest = new BreadcrumbNavigationRequested(Translator.MenuManageAccounts, WinoPage.AccountManagementPage); - PageHistory.Add(new BreadcrumbNavigationItemViewModel(initialRequest, true)); - - var accountManagementPageType = ViewModel.NavigationService.GetPageType(WinoPage.AccountManagementPage); - - AccountPagesFrame.Navigate(accountManagementPageType, null, new SuppressNavigationTransitionInfo()); - } - - - void IRecipient.Receive(BreadcrumbNavigationRequested message) - { - var pageType = ViewModel.NavigationService.GetPageType(message.PageType); - - if (pageType == null) return; - - AccountPagesFrame.Navigate(pageType, message.Parameter, new SlideNavigationTransitionInfo() { Effect = SlideNavigationTransitionEffect.FromRight }); - - PageHistory.ForEach(a => a.IsActive = false); - - PageHistory.Add(new BreadcrumbNavigationItemViewModel(message, true)); - } - - private void GoBackFrame() - { - if (AccountPagesFrame.CanGoBack) - { - PageHistory.RemoveAt(PageHistory.Count - 1); - - AccountPagesFrame.GoBack(new SlideNavigationTransitionInfo() { Effect = SlideNavigationTransitionEffect.FromRight }); - } - } - - private void BreadItemClicked(Microsoft.UI.Xaml.Controls.BreadcrumbBar sender, Microsoft.UI.Xaml.Controls.BreadcrumbBarItemClickedEventArgs args) - { - var clickedPageHistory = PageHistory[args.Index]; - - while (PageHistory.FirstOrDefault(a => a.IsActive) != clickedPageHistory) - { - AccountPagesFrame.GoBack(new SlideNavigationTransitionInfo() { Effect = SlideNavigationTransitionEffect.FromRight }); - PageHistory.RemoveAt(PageHistory.Count - 1); - PageHistory[PageHistory.Count - 1].IsActive = true; - } - } - - public void Receive(BackBreadcrumNavigationRequested message) - { - GoBackFrame(); - } - - public async void Receive(AccountUpdatedMessage message) - { - var activePage = PageHistory.FirstOrDefault(a => a.Request.PageType == WinoPage.AccountDetailsPage); - - if (activePage == null) return; - - await Dispatcher.TryRunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => - { - activePage.Title = message.Account.Name; - }); - } - - public void Receive(MergedInboxRenamed message) - { - // TODO: Find better way to retrieve page history from the stack for the merged account. - var activePage = PageHistory.LastOrDefault(); - - if (activePage == null) return; - - activePage.Title = message.NewName; - } -} diff --git a/Wino.Core.UWP/Views/SettingOptionsPage.xaml b/Wino.Core.UWP/Views/SettingOptionsPage.xaml deleted file mode 100644 index c6e4acc9..00000000 --- a/Wino.Core.UWP/Views/SettingOptionsPage.xaml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/Wino.Core.UWP/Views/SettingOptionsPage.xaml.cs b/Wino.Core.UWP/Views/SettingOptionsPage.xaml.cs deleted file mode 100644 index d5e35bd6..00000000 --- a/Wino.Core.UWP/Views/SettingOptionsPage.xaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Wino.Views.Abstract; - -namespace Wino.Views.Settings; - -public sealed partial class SettingOptionsPage : SettingOptionsPageAbstract -{ - public SettingOptionsPage() - { - InitializeComponent(); - } -} diff --git a/Wino.Core.UWP/Views/SettingsPage.xaml.cs b/Wino.Core.UWP/Views/SettingsPage.xaml.cs deleted file mode 100644 index 84e8c4db..00000000 --- a/Wino.Core.UWP/Views/SettingsPage.xaml.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System.Collections.ObjectModel; -using System.Linq; -using CommunityToolkit.Mvvm.Messaging; -using MoreLinq; -using Windows.UI.Xaml.Media.Animation; -using Windows.UI.Xaml.Navigation; -using Wino.Core.Domain; -using Wino.Core.Domain.Enums; -using Wino.Mail.ViewModels.Data; -using Wino.Messaging.Client.Navigation; -using Wino.Views.Abstract; -using Wino.Views.Settings; - -namespace Wino.Views; - -public sealed partial class SettingsPage : SettingsPageAbstract, IRecipient -{ - public ObservableCollection PageHistory { get; set; } = []; - - public SettingsPage() - { - InitializeComponent(); - } - - protected override void OnNavigatedTo(NavigationEventArgs e) - { - base.OnNavigatedTo(e); - - SettingsFrame.Navigate(typeof(SettingOptionsPage), null, new SuppressNavigationTransitionInfo()); - - var initialRequest = new BreadcrumbNavigationRequested(Translator.MenuSettings, WinoPage.SettingOptionsPage); - PageHistory.Add(new BreadcrumbNavigationItemViewModel(initialRequest, true)); - - if (e.Parameter is WinoPage parameterPage) - { - switch (parameterPage) - { - case WinoPage.AppPreferencesPage: - WeakReferenceMessenger.Default.Send(new BreadcrumbNavigationRequested(Translator.SettingsAppPreferences_Title, WinoPage.AppPreferencesPage)); - break; - case WinoPage.PersonalizationPage: - WeakReferenceMessenger.Default.Send(new BreadcrumbNavigationRequested(Translator.SettingsPersonalization_Title, WinoPage.PersonalizationPage)); - break; - } - } - } - - public override void OnLanguageChanged() - { - base.OnLanguageChanged(); - - // Update Settings header in breadcrumb. - - var settingsHeader = PageHistory.FirstOrDefault(); - - if (settingsHeader == null) return; - - settingsHeader.Title = Translator.MenuSettings; - } - - void IRecipient.Receive(BreadcrumbNavigationRequested message) - { - var pageType = ViewModel.NavigationService.GetPageType(message.PageType); - - if (pageType == null) return; - - SettingsFrame.Navigate(pageType, message.Parameter, new SlideNavigationTransitionInfo() { Effect = SlideNavigationTransitionEffect.FromRight }); - - PageHistory.ForEach(a => a.IsActive = false); - - PageHistory.Add(new BreadcrumbNavigationItemViewModel(message, true)); - } - - private void BreadItemClicked(Microsoft.UI.Xaml.Controls.BreadcrumbBar sender, Microsoft.UI.Xaml.Controls.BreadcrumbBarItemClickedEventArgs args) - { - var clickedPageHistory = PageHistory[args.Index]; - var activeIndex = PageHistory.IndexOf(PageHistory.FirstOrDefault(a => a.IsActive)); - - while (PageHistory.FirstOrDefault(a => a.IsActive) != clickedPageHistory) - { - SettingsFrame.GoBack(new SlideNavigationTransitionInfo() { Effect = SlideNavigationTransitionEffect.FromRight }); - PageHistory.RemoveAt(PageHistory.Count - 1); - PageHistory[PageHistory.Count - 1].IsActive = true; - } - } -} diff --git a/Wino.Core.UWP/Wino.Core.UWP.csproj b/Wino.Core.UWP/Wino.Core.UWP.csproj deleted file mode 100644 index e6b2d5af..00000000 --- a/Wino.Core.UWP/Wino.Core.UWP.csproj +++ /dev/null @@ -1,110 +0,0 @@ - - - net9.0-windows10.0.26100.0 - 10.0.17763.0 - x86;x64;arm64 - win-x86;win-x64;win-arm64 - disable - true - en-US - true - true - 10.0.18362.0 - True - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Wino.Core.UWP/WinoApplication.cs b/Wino.Core.UWP/WinoApplication.cs deleted file mode 100644 index 6cdaa9dc..00000000 --- a/Wino.Core.UWP/WinoApplication.cs +++ /dev/null @@ -1,267 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.Messaging; -using Microsoft.Extensions.DependencyInjection; -using Nito.AsyncEx; -using Serilog; -using Windows.ApplicationModel; -using Windows.ApplicationModel.Activation; -using Windows.ApplicationModel.AppService; -using Windows.ApplicationModel.Core; -using Windows.Foundation.Metadata; -using Windows.Globalization; -using Windows.Storage; -using Windows.UI; -using Windows.UI.Core.Preview; -using Windows.UI.ViewManagement; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; -using Wino.Activation; -using Wino.Core.Domain; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Translations; -using Wino.Messaging.Client.Shell; -using Wino.Services; - -namespace Wino.Core.UWP; - -public abstract class WinoApplication : Application, IRecipient -{ - public new static WinoApplication Current => (WinoApplication)Application.Current; - public const string WinoLaunchLogPrefix = "[Wino Launch] "; - - public IServiceProvider Services { get; } - protected IWinoLogger LogInitializer { get; } - protected IApplicationConfiguration AppConfiguration { get; } - protected IWinoServerConnectionManager AppServiceConnectionManager { get; } - public IThemeService ThemeService { get; } - public IUnderlyingThemeService UnderlyingThemeService { get; } - public IThumbnailService ThumbnailService { get; } - protected IDatabaseService DatabaseService { get; } - protected ITranslationService TranslationService { get; } - - protected WinoApplication() - { - ConfigurePrelaunch(); - - Services = ConfigureServices(); - - AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; - TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; - UnhandledException += OnAppUnhandledException; - - Resuming += OnResuming; - Suspending += OnSuspending; - - LogInitializer = Services.GetService(); - AppConfiguration = Services.GetService(); - - AppServiceConnectionManager = Services.GetService>(); - ThemeService = Services.GetService(); - DatabaseService = Services.GetService(); - TranslationService = Services.GetService(); - UnderlyingThemeService = Services.GetService(); - ThumbnailService = Services.GetService(); - - // Make sure the paths are setup on app start. - AppConfiguration.ApplicationDataFolderPath = ApplicationData.Current.LocalFolder.Path; - AppConfiguration.PublisherSharedFolderPath = ApplicationData.Current.GetPublisherCacheFolder(ApplicationConfiguration.SharedFolderName).Path; - AppConfiguration.ApplicationTempFolderPath = ApplicationData.Current.TemporaryFolder.Path; - - ConfigureLogging(); - } - - private void CurrentDomain_UnhandledException(object sender, System.UnhandledExceptionEventArgs e) - => Log.Fatal(e.ExceptionObject as Exception, "AppDomain Unhandled Exception"); - - private void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) - => Log.Error(e.Exception, "Unobserved Task Exception"); - - private void OnAppUnhandledException(object sender, Windows.UI.Xaml.UnhandledExceptionEventArgs e) - { - Log.Fatal(e.Exception, "Unhandled Exception"); - e.Handled = true; - } - - protected abstract void OnApplicationCloseRequested(object sender, SystemNavigationCloseRequestedPreviewEventArgs e); - protected abstract IEnumerable GetActivationHandlers(); - protected abstract ActivationHandler GetDefaultActivationHandler(); - protected override void OnWindowCreated(WindowCreatedEventArgs args) - { - base.OnWindowCreated(args); - - ConfigureTitleBar(); - - LogActivation($"OnWindowCreated -> IsWindowNull: {args.Window == null}"); - - TryRegisterAppCloseChange(); - } - - public IEnumerable GetActivationServices() - { - yield return DatabaseService; - yield return TranslationService; - yield return ThemeService; - } - - public Task InitializeServicesAsync() => GetActivationServices().Select(a => a.InitializeAsync()).WhenAll(); - - public bool IsInteractiveLaunchArgs(object args) => args is IActivatedEventArgs; - - public void LogActivation(string log) => Log.Information($"{WinoLaunchLogPrefix}{log}"); - - private void ConfigureTitleBar() - { - var coreTitleBar = CoreApplication.GetCurrentView().TitleBar; - var applicationViewTitleBar = ApplicationView.GetForCurrentView().TitleBar; - - // Extend shell content into core window to meet design requirements. - coreTitleBar.ExtendViewIntoTitleBar = true; - - // Change system buttons and background colors to meet design requirements. - applicationViewTitleBar.ButtonBackgroundColor = Colors.Transparent; - applicationViewTitleBar.BackgroundColor = Colors.Transparent; - applicationViewTitleBar.ButtonInactiveBackgroundColor = Colors.Transparent; - applicationViewTitleBar.ButtonForegroundColor = Colors.White; - } - - public async Task ActivateWinoAsync(object args) - { - await InitializeServicesAsync(); - - if (IsInteractiveLaunchArgs(args)) - { - if (Window.Current.Content == null) - { - var mainFrame = new Frame(); - - Window.Current.Content = mainFrame; - - await ThemeService.InitializeAsync(); - } - } - - await HandleActivationAsync(args); - - if (IsInteractiveLaunchArgs(args)) - { - Window.Current.Activate(); - - LogActivation("Window activated"); - } - } - - public async Task HandleActivationAsync(object activationArgs) - { - if (GetActivationHandlers() != null) - { - var activationHandler = GetActivationHandlers().FirstOrDefault(h => h.CanHandle(activationArgs)) ?? null; - - if (activationHandler != null) - { - await activationHandler.HandleAsync(activationArgs); - } - } - - if (IsInteractiveLaunchArgs(activationArgs)) - { - var defaultHandler = GetDefaultActivationHandler(); - - if (defaultHandler.CanHandle(activationArgs)) - { - await defaultHandler.HandleAsync(activationArgs); - } - } - } - - protected override async void OnLaunched(LaunchActivatedEventArgs args) - { - LogActivation($"OnLaunched -> {args.GetType().Name}, Kind -> {args.Kind}, PreviousExecutionState -> {args.PreviousExecutionState}, IsPrelaunch -> {args.PrelaunchActivated}"); - - if (!args.PrelaunchActivated) - { - await ActivateWinoAsync(args); - } - } - - protected override async void OnFileActivated(FileActivatedEventArgs args) - { - base.OnFileActivated(args); - - LogActivation($"OnFileActivated -> ItemCount: {args.Files.Count}, Kind: {args.Kind}, PreviousExecutionState: {args.PreviousExecutionState}"); - - await ActivateWinoAsync(args); - } - - protected override async void OnActivated(IActivatedEventArgs args) - { - base.OnActivated(args); - - Log.Information($"OnActivated -> {args.GetType().Name}, Kind -> {args.Kind}, Prev Execution State -> {args.PreviousExecutionState}"); - - await ActivateWinoAsync(args); - } - - private void TryRegisterAppCloseChange() - { - try - { - var systemNavigationManagerPreview = SystemNavigationManagerPreview.GetForCurrentView(); - - systemNavigationManagerPreview.CloseRequested -= OnApplicationCloseRequested; - systemNavigationManagerPreview.CloseRequested += OnApplicationCloseRequested; - } - catch { } - } - - private void ConfigurePrelaunch() - { - if (ApiInformation.IsMethodPresent("Windows.ApplicationModel.Core.CoreApplication", "EnablePrelaunch")) - CoreApplication.EnablePrelaunch(true); - } - - public virtual async void OnResuming(object sender, object e) - { - // App Service connection was lost on suspension. - // We must restore it. - // Server might be running already, but re-launching it will trigger a new connection attempt. - - try - { - await AppServiceConnectionManager.ConnectAsync(); - } - catch (OperationCanceledException) - { - // Ignore - } - catch (Exception ex) - { - Log.Error(ex, "Failed to connect to server after resuming the app."); - } - } - public virtual void OnSuspending(object sender, SuspendingEventArgs e) { } - - public abstract IServiceProvider ConfigureServices(); - - public void ConfigureLogging() - { - string logFilePath = Path.Combine(ApplicationData.Current.LocalFolder.Path, Constants.ClientLogFile); - LogInitializer.SetupLogger(logFilePath); - } - - public virtual void OnLanguageChanged(AppLanguageModel languageModel) - { - var newCulture = new CultureInfo(languageModel.Code); - - ApplicationLanguages.PrimaryLanguageOverride = languageModel.Code; - - CultureInfo.DefaultThreadCurrentCulture = newCulture; - CultureInfo.DefaultThreadCurrentUICulture = newCulture; - } - - public void Receive(LanguageChanged message) => OnLanguageChanged(TranslationService.CurrentLanguageModel); -} diff --git a/Wino.Core.ViewModels/AboutPageViewModel.cs b/Wino.Core.ViewModels/AboutPageViewModel.cs index 26579f61..b2755d8c 100644 --- a/Wino.Core.ViewModels/AboutPageViewModel.cs +++ b/Wino.Core.ViewModels/AboutPageViewModel.cs @@ -6,6 +6,7 @@ using Serilog; using Wino.Core.Domain; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Navigation; namespace Wino.Core.ViewModels; @@ -20,10 +21,11 @@ public partial class AboutPageViewModel : CoreBaseViewModel private readonly IWinoLogger _logInitializer; public string VersionName => _nativeAppService.GetFullAppVersion(); - public string DiscordChannelUrl => "https://discord.gg/windows-apps-hub-714581497222398064"; - public string GitHubUrl => "https://github.com/bkaankose/Wino-Mail/"; - public string PrivacyPolicyUrl => "https://www.winomail.app/support/privacy"; - public string PaypalUrl => "https://paypal.me/bkaankose?country.x=PL&locale.x=en_US"; + public string WebsiteUrl => AppUrls.Website; + public string DiscordChannelUrl => AppUrls.Discord; + public string GitHubUrl => AppUrls.GitHub; + public string PrivacyPolicyUrl => AppUrls.PrivacyPolicy; + public string PaypalUrl => AppUrls.Paypal; public IPreferencesService PreferencesService { get; } @@ -47,19 +49,17 @@ public partial class AboutPageViewModel : CoreBaseViewModel PreferencesService = preferencesService; } - [RequiresDynamicCode("AOT")] - [RequiresUnreferencedCode("AOT")] - protected override void OnActivated() + public override void OnNavigatedTo(NavigationMode mode, object parameters) { - base.OnActivated(); + base.OnNavigatedTo(mode, parameters); PreferencesService.PreferenceChanged -= PreferencesChanged; PreferencesService.PreferenceChanged += PreferencesChanged; } - protected override void OnDeactivated() + public override void OnNavigatedFrom(NavigationMode mode, object parameters) { - base.OnDeactivated(); + base.OnNavigatedFrom(mode, parameters); PreferencesService.PreferenceChanged -= PreferencesChanged; } diff --git a/Wino.Core.ViewModels/AccountManagementPageViewModelBase.cs b/Wino.Core.ViewModels/AccountManagementPageViewModelBase.cs index cb282865..5b407df5 100644 --- a/Wino.Core.ViewModels/AccountManagementPageViewModelBase.cs +++ b/Wino.Core.ViewModels/AccountManagementPageViewModelBase.cs @@ -9,7 +9,6 @@ using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Navigation; -using Wino.Core.Domain.Models.Store; using Wino.Mail.ViewModels.Data; using Wino.Messaging.Client.Navigation; @@ -28,41 +27,41 @@ public abstract partial class AccountManagementPageViewModelBase : CoreBaseViewM [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsPurchasePanelVisible))] - private bool hasUnlimitedAccountProduct; + public partial bool HasUnlimitedAccountProduct { get; set; } [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsAccountCreationAlmostOnLimit))] [NotifyPropertyChangedFor(nameof(IsPurchasePanelVisible))] - private bool isAccountCreationBlocked; + public partial bool IsAccountCreationBlocked { get; set; } [ObservableProperty] - private IAccountProviderDetailViewModel _startupAccount; + public partial IAccountProviderDetailViewModel StartupAccount { get; set; } public int FREE_ACCOUNT_COUNT { get; } = 3; protected IDialogServiceBase DialogService { get; } - protected IWinoServerConnectionManager WinoServerConnectionManager { get; } protected INavigationService NavigationService { get; } protected IAccountService AccountService { get; } protected IProviderService ProviderService { get; } protected IStoreManagementService StoreManagementService { get; } + protected IWinoAccountProfileService WinoAccountProfileService { get; } protected IAuthenticationProvider AuthenticationProvider { get; } protected IPreferencesService PreferencesService { get; } public AccountManagementPageViewModelBase(IDialogServiceBase dialogService, - IWinoServerConnectionManager winoServerConnectionManager, INavigationService navigationService, IAccountService accountService, IProviderService providerService, IStoreManagementService storeManagementService, + IWinoAccountProfileService winoAccountProfileService, IAuthenticationProvider authenticationProvider, IPreferencesService preferencesService) { DialogService = dialogService; - WinoServerConnectionManager = winoServerConnectionManager; NavigationService = navigationService; AccountService = accountService; ProviderService = providerService; StoreManagementService = storeManagementService; + WinoAccountProfileService = winoAccountProfileService; AuthenticationProvider = authenticationProvider; PreferencesService = preferencesService; } @@ -78,7 +77,7 @@ public abstract partial class AccountManagementPageViewModelBase : CoreBaseViewM [RelayCommand] public async Task PurchaseUnlimitedAccountAsync() { - var purchaseResult = await StoreManagementService.PurchaseAsync(StoreProductType.UnlimitedAccounts); + var purchaseResult = await StoreManagementService.PurchaseAsync(WinoAddOnProductType.UNLIMITED_ACCOUNTS); if (purchaseResult == StorePurchaseResult.Succeeded) DialogService.InfoBarMessage(Translator.Info_PurchaseThankYouTitle, Translator.Info_PurchaseThankYouMessage, InfoBarMessageType.Success); @@ -95,14 +94,12 @@ public abstract partial class AccountManagementPageViewModelBase : CoreBaseViewM public async Task ManageStorePurchasesAsync() { - await ExecuteUIThread(async () => - { - HasUnlimitedAccountProduct = await StoreManagementService.HasProductAsync(StoreProductType.UnlimitedAccounts); + var hasUnlimitedAccountProduct = await StoreManagementService.HasProductAsync(WinoAddOnProductType.UNLIMITED_ACCOUNTS).ConfigureAwait(false); - if (!HasUnlimitedAccountProduct) - IsAccountCreationBlocked = Accounts.Count >= FREE_ACCOUNT_COUNT; - else - IsAccountCreationBlocked = false; + await ExecuteUIThread(() => + { + HasUnlimitedAccountProduct = hasUnlimitedAccountProduct; + IsAccountCreationBlocked = !hasUnlimitedAccountProduct && Accounts.Count >= FREE_ACCOUNT_COUNT; }); } diff --git a/Wino.Core.ViewModels/CalendarBaseViewModel.cs b/Wino.Core.ViewModels/CalendarBaseViewModel.cs index 69b42bd1..a413b6ad 100644 --- a/Wino.Core.ViewModels/CalendarBaseViewModel.cs +++ b/Wino.Core.ViewModels/CalendarBaseViewModel.cs @@ -1,12 +1,45 @@ -using CommunityToolkit.Mvvm.Messaging; +using System; +using CommunityToolkit.Mvvm.Messaging; using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Enums; using Wino.Messaging.Client.Calendar; namespace Wino.Core.ViewModels; -public class CalendarBaseViewModel : CoreBaseViewModel, IRecipient +public class CalendarBaseViewModel : CoreBaseViewModel, + IRecipient, + IRecipient, + IRecipient { - public void Receive(CalendarItemAdded message) => OnCalendarItemAdded(message.CalendarItem); + public void Receive(CalendarItemAdded message) => DispatchToUIThread(() => OnCalendarItemAdded(message.CalendarItem)); + public void Receive(CalendarItemUpdated message) => DispatchToUIThread(() => OnCalendarItemUpdated(message.CalendarItem, message.Source)); + public void Receive(CalendarItemDeleted message) => DispatchToUIThread(() => OnCalendarItemDeleted(message.CalendarItem)); protected virtual void OnCalendarItemAdded(CalendarItem calendarItem) { } + protected virtual void OnCalendarItemUpdated(CalendarItem calendarItem, CalendarItemUpdateSource source) { } + protected virtual void OnCalendarItemDeleted(CalendarItem calendarItem) { } + + private void DispatchToUIThread(Action action) + { + _ = ExecuteUIThread(action); + } + + protected override void RegisterRecipients() + { + base.RegisterRecipients(); + + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + } + + protected override void UnregisterRecipients() + { + base.UnregisterRecipients(); + + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + } } + diff --git a/Wino.Core.ViewModels/CoreBaseViewModel.cs b/Wino.Core.ViewModels/CoreBaseViewModel.cs index 92b58856..17abbf45 100644 --- a/Wino.Core.ViewModels/CoreBaseViewModel.cs +++ b/Wino.Core.ViewModels/CoreBaseViewModel.cs @@ -2,18 +2,13 @@ using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Messaging; -using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models; using Wino.Core.Domain.Models.Navigation; -using Wino.Messaging.UI; namespace Wino.Core.ViewModels; -public class CoreBaseViewModel : ObservableRecipient, - INavigationAware, - IRecipient, - IRecipient, - IRecipient +public class CoreBaseViewModel : ObservableRecipient, INavigationAware { private IDispatcher _dispatcher; public IDispatcher Dispatcher @@ -33,22 +28,44 @@ public class CoreBaseViewModel : ObservableRecipient, } } - public virtual void OnNavigatedTo(NavigationMode mode, object parameters) { IsActive = true; } + public virtual void OnNavigatedTo(NavigationMode mode, object parameters) + { + UnregisterRecipients(); + RegisterRecipients(); + } - public virtual void OnNavigatedFrom(NavigationMode mode, object parameters) { IsActive = false; } + public virtual void OnNavigatedFrom(NavigationMode mode, object parameters) + { + UnregisterRecipients(); + } public virtual void OnPageLoaded() { } - public async Task ExecuteUIThread(Action action) => await Dispatcher?.ExecuteOnUIThread(action); + public virtual Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args) => Task.CompletedTask; + + public Task ExecuteUIThread(Action action) + { + if (action == null) return Task.CompletedTask; + + if (Dispatcher == null) + { + action(); + return Task.CompletedTask; + } + + return Dispatcher.ExecuteOnUIThread(action); + } public void ReportUIChange(TMessage message) where TMessage : class, IUIMessage => Messenger.Send(message); protected virtual void OnDispatcherAssigned() { } - protected virtual void OnAccountCreated(MailAccount createdAccount) { } - protected virtual void OnAccountRemoved(MailAccount removedAccount) { } - protected virtual void OnAccountUpdated(MailAccount updatedAccount) { } + /// + /// Register message recipients for this view model. Override to register specific message types. + /// + protected virtual void RegisterRecipients() { } - void IRecipient.Receive(AccountCreatedMessage message) => OnAccountCreated(message.Account); - void IRecipient.Receive(AccountRemovedMessage message) => OnAccountRemoved(message.Account); - void IRecipient.Receive(AccountUpdatedMessage message) => OnAccountUpdated(message.Account); + /// + /// Unregister message recipients for this view model. Override to unregister specific message types. + /// + protected virtual void UnregisterRecipients() { } } diff --git a/Wino.Core.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs b/Wino.Core.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs index 5d808e66..4d39385b 100644 --- a/Wino.Core.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs +++ b/Wino.Core.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs @@ -11,12 +11,18 @@ public partial class BreadcrumbNavigationItemViewModel : ObservableObject [ObservableProperty] private bool isActive; + public int StepNumber { get; set; } + + public int BackStackDepth { get; set; } + public BreadcrumbNavigationRequested Request { get; set; } - public BreadcrumbNavigationItemViewModel(BreadcrumbNavigationRequested request, bool isActive) + public BreadcrumbNavigationItemViewModel(BreadcrumbNavigationRequested request, bool isActive, int stepNumber = 0, int backStackDepth = 0) { Request = request; Title = request.PageTitle; IsActive = isActive; + StepNumber = stepNumber; + BackStackDepth = backStackDepth; } } diff --git a/Wino.Core.ViewModels/Data/KeyboardShortcutActionViewModel.cs b/Wino.Core.ViewModels/Data/KeyboardShortcutActionViewModel.cs new file mode 100644 index 00000000..85a2c624 --- /dev/null +++ b/Wino.Core.ViewModels/Data/KeyboardShortcutActionViewModel.cs @@ -0,0 +1,33 @@ +using Wino.Core.Domain; +using Wino.Core.Domain.Enums; + +namespace Wino.Core.ViewModels.Data; + +public class KeyboardShortcutActionViewModel +{ + public WinoApplicationMode Mode { get; } + public KeyboardShortcutAction Action { get; } + + public string DisplayName => Action switch + { + KeyboardShortcutAction.NewMail => Translator.MenuNewMail, + KeyboardShortcutAction.ToggleReadUnread => Translator.KeyboardShortcuts_ActionToggleReadUnread, + KeyboardShortcutAction.ToggleFlag => Translator.KeyboardShortcuts_ActionToggleFlag, + KeyboardShortcutAction.ToggleArchive => Translator.KeyboardShortcuts_ActionToggleArchive, + KeyboardShortcutAction.Delete => Translator.Buttons_Delete, + KeyboardShortcutAction.Move => Translator.MailOperation_Move, + KeyboardShortcutAction.Reply => Translator.MailOperation_Reply, + KeyboardShortcutAction.ReplyAll => Translator.MailOperation_ReplyAll, + KeyboardShortcutAction.Send => Translator.Buttons_Send, + KeyboardShortcutAction.NewEvent => Translator.CalendarEventCompose_NewEventButton, + _ => Action.ToString() + }; + + public KeyboardShortcutActionViewModel(WinoApplicationMode mode, KeyboardShortcutAction action) + { + Mode = mode; + Action = action; + } + + public override string ToString() => DisplayName; +} diff --git a/Wino.Core.ViewModels/Data/KeyboardShortcutViewModel.cs b/Wino.Core.ViewModels/Data/KeyboardShortcutViewModel.cs new file mode 100644 index 00000000..975ac663 --- /dev/null +++ b/Wino.Core.ViewModels/Data/KeyboardShortcutViewModel.cs @@ -0,0 +1,96 @@ +using System; +using CommunityToolkit.Mvvm.ComponentModel; +using Wino.Core.Domain; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; + +namespace Wino.Core.ViewModels.Data; + +/// +/// ViewModel wrapper for KeyboardShortcut entity. +/// +public partial class KeyboardShortcutViewModel : ObservableObject +{ + [ObservableProperty] + public partial bool IsEnabled { get; set; } + + public Guid Id { get; } + public WinoApplicationMode Mode { get; } + public string Key { get; } + public ModifierKeys ModifierKeys { get; } + public KeyboardShortcutAction Action { get; } + public DateTime CreatedAt { get; } + + public string DisplayName + { + get + { + var modifierText = string.Empty; + if (ModifierKeys.HasFlag(ModifierKeys.Control)) + modifierText += "Ctrl+"; + if (ModifierKeys.HasFlag(ModifierKeys.Alt)) + modifierText += "Alt+"; + if (ModifierKeys.HasFlag(ModifierKeys.Shift)) + modifierText += "Shift+"; + if (ModifierKeys.HasFlag(ModifierKeys.Windows)) + modifierText += "Win+"; + + return modifierText + Key; + } + } + + public string ModeDisplayName => Mode switch + { + WinoApplicationMode.Mail => Translator.KeyboardShortcuts_ModeMail, + WinoApplicationMode.Calendar => Translator.KeyboardShortcuts_ModeCalendar, + WinoApplicationMode.Contacts => Translator.ContactsPage_Title, + WinoApplicationMode.Settings => Translator.MenuSettings, + _ => Mode.ToString() + }; + + public string ActionDisplayName + { + get + { + return Action switch + { + KeyboardShortcutAction.NewMail => Translator.MenuNewMail, + KeyboardShortcutAction.ToggleReadUnread => Translator.KeyboardShortcuts_ActionToggleReadUnread, + KeyboardShortcutAction.ToggleFlag => Translator.KeyboardShortcuts_ActionToggleFlag, + KeyboardShortcutAction.ToggleArchive => Translator.KeyboardShortcuts_ActionToggleArchive, + KeyboardShortcutAction.Delete => Translator.Buttons_Delete, + KeyboardShortcutAction.Move => Translator.MailOperation_Move, + KeyboardShortcutAction.Reply => Translator.MailOperation_Reply, + KeyboardShortcutAction.ReplyAll => Translator.MailOperation_ReplyAll, + KeyboardShortcutAction.Send => Translator.Buttons_Send, + KeyboardShortcutAction.NewEvent => Translator.CalendarEventCompose_NewEventButton, + _ => Action.ToString() + }; + } + } + + public KeyboardShortcutViewModel(KeyboardShortcut shortcut) + { + Id = shortcut.Id; + Mode = shortcut.Mode; + Key = shortcut.Key; + ModifierKeys = shortcut.ModifierKeys; + Action = shortcut.Action; + CreatedAt = shortcut.CreatedAt; + IsEnabled = shortcut.IsEnabled; + } + + public KeyboardShortcut ToEntity() + { + return new KeyboardShortcut + { + Id = Id, + Mode = Mode, + Key = Key, + ModifierKeys = ModifierKeys, + Action = Action, + CreatedAt = CreatedAt, + IsEnabled = IsEnabled + }; + } +} diff --git a/Wino.Core.ViewModels/Data/MailOperationViewModel.cs b/Wino.Core.ViewModels/Data/MailOperationViewModel.cs new file mode 100644 index 00000000..bc16f0e7 --- /dev/null +++ b/Wino.Core.ViewModels/Data/MailOperationViewModel.cs @@ -0,0 +1,42 @@ +using Wino.Core.Domain; +using Wino.Core.Domain.Enums; + +namespace Wino.Core.ViewModels.Data; + +/// +/// ViewModel for displaying mail operations in dropdowns/lists. +/// +public class MailOperationViewModel +{ + public MailOperation Operation { get; } + + public string DisplayName + { + get + { + return Operation switch + { + MailOperation.Archive => Translator.MailOperation_Archive, + MailOperation.UnArchive => Translator.MailOperation_Unarchive, + MailOperation.SoftDelete => Translator.MailOperation_Delete, + MailOperation.Move => Translator.MailOperation_Move, + MailOperation.MoveToJunk => Translator.MailOperation_MoveJunk, + MailOperation.SetFlag => Translator.MailOperation_SetFlag, + MailOperation.ClearFlag => Translator.MailOperation_ClearFlag, + MailOperation.MarkAsRead => Translator.MailOperation_MarkAsRead, + MailOperation.MarkAsUnread => Translator.MailOperation_MarkAsUnread, + MailOperation.Reply => Translator.MailOperation_Reply, + MailOperation.ReplyAll => Translator.MailOperation_ReplyAll, + MailOperation.Forward => Translator.MailOperation_Forward, + _ => Operation.ToString() + }; + } + } + + public MailOperationViewModel(MailOperation operation) + { + Operation = operation; + } + + public override string ToString() => DisplayName; +} diff --git a/Wino.Core.ViewModels/Data/WinoAddOnItemViewModel.cs b/Wino.Core.ViewModels/Data/WinoAddOnItemViewModel.cs new file mode 100644 index 00000000..692ba90a --- /dev/null +++ b/Wino.Core.ViewModels/Data/WinoAddOnItemViewModel.cs @@ -0,0 +1,85 @@ +#nullable enable +using CommunityToolkit.Mvvm.ComponentModel; +using System.Windows.Input; +using Wino.Core.Domain.Enums; + +namespace Wino.Core.ViewModels.Data; + +public partial class WinoAddOnItemViewModel : ObservableObject +{ + public WinoAddOnProductType ProductType { get; } + + public string NameKey => $"WinoAddOn_{ProductType}_Name"; + public string DescriptionKey => $"WinoAddOn_{ProductType}_Description"; + public string KeywordsKey => $"WinoAddOn_{ProductType}_Keywords"; + + public string IconGlyph => ProductType switch + { + WinoAddOnProductType.AI_PACK => "\uE945", + WinoAddOnProductType.UNLIMITED_ACCOUNTS => "\uE716", + _ => "\uE10F" + }; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ShowPurchaseState))] + [NotifyPropertyChangedFor(nameof(ShowUsageSummary))] + public partial bool IsPurchased { get; set; } + + public ICommand? PurchaseCommand { get; set; } + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ShowLoadingState))] + [NotifyPropertyChangedFor(nameof(ShowPurchaseState))] + [NotifyPropertyChangedFor(nameof(ShowUsageSummary))] + [NotifyPropertyChangedFor(nameof(ShowErrorState))] + public partial bool IsLoading { get; set; } + + [ObservableProperty] + public partial bool IsPurchaseInProgress { get; set; } + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ShowErrorState))] + [NotifyPropertyChangedFor(nameof(ShowUsageSummary))] + public partial bool HasUsageData { get; set; } + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ShowErrorState))] + [NotifyPropertyChangedFor(nameof(ShowUsageSummary))] + public partial string ErrorText { get; set; } = string.Empty; + + [ObservableProperty] + public partial int UsageCount { get; set; } + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ShowUsageSummary))] + public partial int UsageLimit { get; set; } = 1; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ShowUsageSummary))] + public partial double UsagePercentage { get; set; } + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ShowUsageSummary))] + public partial string RenewalText { get; set; } = string.Empty; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ShowUsageSummary))] + public partial string UsageResetText { get; set; } = string.Empty; + + public bool ShowLoadingState => IsLoading; + + public bool ShowPurchaseState => !IsLoading && string.IsNullOrWhiteSpace(ErrorText); + + public bool ShowUsageSummary => ProductType == WinoAddOnProductType.AI_PACK && + IsPurchased && + !IsLoading && + string.IsNullOrWhiteSpace(ErrorText) && + HasUsageData; + + public bool ShowErrorState => !IsLoading && !string.IsNullOrWhiteSpace(ErrorText); + + public WinoAddOnItemViewModel(WinoAddOnProductType productType) + { + ProductType = productType; + } +} diff --git a/Wino.Core.ViewModels/KeyboardShortcutsPageViewModel.cs b/Wino.Core.ViewModels/KeyboardShortcutsPageViewModel.cs new file mode 100644 index 00000000..a4c27da2 --- /dev/null +++ b/Wino.Core.ViewModels/KeyboardShortcutsPageViewModel.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Serilog; +using Wino.Core.Domain; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Navigation; +using Wino.Core.ViewModels.Data; + +namespace Wino.Core.ViewModels; + +/// +/// ViewModel for managing keyboard shortcuts settings. +/// +public partial class KeyboardShortcutsPageViewModel : CoreBaseViewModel +{ + private readonly IKeyboardShortcutService _keyboardShortcutService; + private readonly IMailDialogService _dialogService; + + [ObservableProperty] + public partial ObservableCollection Shortcuts { get; set; } = new(); + + public KeyboardShortcutsPageViewModel(IKeyboardShortcutService keyboardShortcutService, + IMailDialogService dialogService) + { + _keyboardShortcutService = keyboardShortcutService; + _dialogService = dialogService; + } + + public override async void OnNavigatedTo(NavigationMode mode, object parameters) + { + base.OnNavigatedTo(mode, parameters); + await LoadShortcutsAsync(); + } + + [RelayCommand] + private async Task LoadShortcutsAsync() + { + try + { + var keyboardShortcuts = await _keyboardShortcutService.GetKeyboardShortcutsAsync(); + + Shortcuts.Clear(); + foreach (var shortcut in keyboardShortcuts) + { + Shortcuts.Add(new KeyboardShortcutViewModel(shortcut)); + } + } + catch (Exception ex) + { + Log.Error("Failed to load keyboard shortcuts.", ex); + + await _dialogService.ShowMessageAsync( + Translator.KeyboardShortcuts_FailedToLoad, + Translator.GeneralTitle_Error, + WinoCustomMessageDialogIcon.Error); + } + } + + [RelayCommand] + private async Task StartAddingShortcutAsync() + { + var result = await _dialogService.ShowKeyboardShortcutDialogAsync(); + if (result.IsSuccess) + { + try + { + // Check if key combination is already in use + var isInUse = await _keyboardShortcutService.IsKeyCombinationInUseAsync(result.Mode, result.Key, result.ModifierKeys, null); + if (isInUse) + { + await _dialogService.ShowMessageAsync(Translator.KeyboardShortcuts_ShortcutInUse, Translator.GeneralTitle_Error, WinoCustomMessageDialogIcon.Error); + return; + } + + // Create new shortcut + var shortcut = new KeyboardShortcut + { + Mode = result.Mode, + Key = result.Key, + ModifierKeys = result.ModifierKeys, + Action = result.Action, + IsEnabled = true + }; + + await _keyboardShortcutService.SaveKeyboardShortcutAsync(shortcut); + await LoadShortcutsAsync(); + } + catch (Exception ex) + { + Log.Error("Failed to save new keyboard shortcut.", ex); + await _dialogService.ShowMessageAsync( + Translator.KeyboardShortcuts_FailedToSave, + Translator.GeneralTitle_Error, + WinoCustomMessageDialogIcon.Error); + } + } + } + + [RelayCommand] + private async Task StartEditingShortcutAsync(KeyboardShortcutViewModel shortcut) + { + if (shortcut == null) return; + + var dialogService = _dialogService as IMailDialogService; + if (dialogService == null) return; + + var existingShortcut = shortcut.ToEntity(); + var result = await dialogService.ShowKeyboardShortcutDialogAsync(existingShortcut); + + if (result.IsSuccess) + { + try + { + // Check if key combination is already in use (excluding current shortcut) + var isInUse = await _keyboardShortcutService.IsKeyCombinationInUseAsync(result.Mode, result.Key, result.ModifierKeys, shortcut.Id); + if (isInUse) + { + await _dialogService.ShowMessageAsync(Translator.KeyboardShortcuts_ShortcutInUse, Translator.GeneralTitle_Error, WinoCustomMessageDialogIcon.Error); + return; + } + + // Update existing shortcut + var updatedShortcut = shortcut.ToEntity(); + updatedShortcut.Mode = result.Mode; + updatedShortcut.Key = result.Key; + updatedShortcut.ModifierKeys = result.ModifierKeys; + updatedShortcut.Action = result.Action; + + await _keyboardShortcutService.SaveKeyboardShortcutAsync(updatedShortcut); + await LoadShortcutsAsync(); + } + catch (Exception ex) + { + Log.Error("Failed to update keyboard shortcut.", ex); + + await _dialogService.ShowMessageAsync( + Translator.KeyboardShortcuts_FailedToUpdate, + Translator.GeneralTitle_Error, + WinoCustomMessageDialogIcon.Error); + } + } + } + + + + [RelayCommand] + private async Task DeleteShortcutAsync(KeyboardShortcutViewModel shortcut) + { + if (shortcut == null) return; + + try + { + await _keyboardShortcutService.DeleteKeyboardShortcutAsync(shortcut.Id); + await LoadShortcutsAsync(); + } + catch (Exception ex) + { + Log.Error("Failed to delete keyboard shortcut.", ex); + await _dialogService.ShowMessageAsync( + Translator.KeyboardShortcuts_FailedToDelete, + Translator.GeneralTitle_Error, + WinoCustomMessageDialogIcon.Error); + } + } + + [RelayCommand] + private async Task ResetToDefaultsAsync() + { + try + { + await _keyboardShortcutService.ResetToDefaultShortcutsAsync(); + await LoadShortcutsAsync(); + } + catch (Exception ex) + { + Log.Error("Failed to reset keyboard shortcuts to defaults.", ex); + await _dialogService.ShowMessageAsync( + Translator.KeyboardShortcuts_FailedToReset, + Translator.GeneralTitle_Error, + WinoCustomMessageDialogIcon.Error); + } + } + +} diff --git a/Wino.Core.ViewModels/NewAccountManagementPageViewModel.cs b/Wino.Core.ViewModels/NewAccountManagementPageViewModel.cs index 4c289889..74c40093 100644 --- a/Wino.Core.ViewModels/NewAccountManagementPageViewModel.cs +++ b/Wino.Core.ViewModels/NewAccountManagementPageViewModel.cs @@ -4,10 +4,12 @@ namespace Wino.Core.ViewModels; public class ManageAccountsPagePageViewModel : CoreBaseViewModel { - public ManageAccountsPagePageViewModel(INavigationService navigationService) + public ManageAccountsPagePageViewModel(INavigationService navigationService, IStatePersistanceService statePersistenceService) { NavigationService = navigationService; + StatePersistenceService = statePersistenceService; } public INavigationService NavigationService { get; } + public IStatePersistanceService StatePersistenceService { get; } } diff --git a/Wino.Core.ViewModels/PersonalizationPageViewModel.cs b/Wino.Core.ViewModels/PersonalizationPageViewModel.cs index 7fc857de..c247a9b8 100644 --- a/Wino.Core.ViewModels/PersonalizationPageViewModel.cs +++ b/Wino.Core.ViewModels/PersonalizationPageViewModel.cs @@ -1,14 +1,17 @@ using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; +using System; +using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Wino.Core.Domain; using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Personalization; using Wino.Core.ViewModels.Data; @@ -20,7 +23,7 @@ public partial class PersonalizationPageViewModel : CoreBaseViewModel public IPreferencesService PreferencesService { get; } private readonly IDialogServiceBase _dialogService; - private readonly IThemeService _themeService; + private readonly INewThemeService _newThemeService; private bool isPropChangeDisabled = false; @@ -28,10 +31,13 @@ public partial class PersonalizationPageViewModel : CoreBaseViewModel public MailCopy DemoPreviewMailCopy { get; } = new MailCopy() { FromName = "Sender Name", + FromAddress = "sender@wino.mail", Subject = "Mail Subject", PreviewText = "Thank you for using Wino Mail. We hope you enjoy the experience.", }; + public IMailItemDisplayInformation DemoPreviewMailItemInformation { get; } + #region Personalization public bool IsSelectedWindowsAccentColor => SelectedAppColor == Colors.LastOrDefault(); @@ -119,6 +125,13 @@ public partial class PersonalizationPageViewModel : CoreBaseViewModel } } + // Backdrop selection properties + [ObservableProperty] + public partial List AvailableBackdropTypes { get; set; } + + [ObservableProperty] + public partial BackdropTypeWrapper SelectedBackdropType { get; set; } + #endregion [RelayCommand] @@ -131,15 +144,21 @@ public partial class PersonalizationPageViewModel : CoreBaseViewModel public AsyncRelayCommand CreateCustomThemeCommand { get; set; } public PersonalizationPageViewModel(IDialogServiceBase dialogService, IStatePersistanceService statePersistanceService, - IThemeService themeService, + INewThemeService newThemeService, IPreferencesService preferencesService) { _dialogService = dialogService; - _themeService = themeService; + _newThemeService = newThemeService; StatePersistenceService = statePersistanceService; PreferencesService = preferencesService; + DemoPreviewMailItemInformation = new DemoMailItemDisplayInformation( + DemoPreviewMailCopy.FromName, + DemoPreviewMailCopy.FromAddress, + DemoPreviewMailCopy.Subject, + DemoPreviewMailCopy.PreviewText); + CreateCustomThemeCommand = new AsyncRelayCommand(CreateCustomThemeAsync); } @@ -175,7 +194,7 @@ public partial class PersonalizationPageViewModel : CoreBaseViewModel // Add system accent color as last item. - Colors.Add(new AppColorViewModel(_themeService.GetSystemAccentColorHex(), true)); + Colors.Add(new AppColorViewModel(_newThemeService.GetSystemAccentColorHex(), true)); } /// @@ -183,10 +202,10 @@ public partial class PersonalizationPageViewModel : CoreBaseViewModel /// private void SetInitialValues() { - SelectedElementTheme = ElementThemes.Find(a => a.NativeTheme == _themeService.RootTheme); + SelectedElementTheme = ElementThemes.Find(a => a.NativeTheme == _newThemeService.RootTheme); SelectedInfoDisplayMode = PreferencesService.MailItemDisplayMode; - var currentAccentColor = _themeService.AccentColor; + var currentAccentColor = _newThemeService.AccentColor; bool isWindowsColor = string.IsNullOrEmpty(currentAccentColor); @@ -198,14 +217,18 @@ public partial class PersonalizationPageViewModel : CoreBaseViewModel else SelectedAppColor = Colors.FirstOrDefault(a => a.Hex == currentAccentColor); - SelectedAppTheme = AppThemes.Find(a => a.Id == _themeService.CurrentApplicationThemeId); + // Find selected theme, handling backward compatibility where theme ID might not exist + var currentThemeId = _newThemeService.CurrentApplicationThemeId; + SelectedAppTheme = currentThemeId.HasValue ? AppThemes.Find(a => a.Id == currentThemeId.Value) : null; + + // Set the current backdrop from service - backdrop should be independent of theme selection + var currentBackdropType = _newThemeService.CurrentBackdropType; + SelectedBackdropType = AvailableBackdropTypes?.FirstOrDefault(x => x.BackdropType == currentBackdropType); } - [RequiresDynamicCode("AOT")] - [RequiresUnreferencedCode("AOT")] - protected override async void OnActivated() + public override async void OnNavigatedTo(NavigationMode mode, object parameters) { - base.OnActivated(); + base.OnNavigatedTo(mode, parameters); await InitializeSettingsAsync(); } @@ -214,21 +237,24 @@ public partial class PersonalizationPageViewModel : CoreBaseViewModel { Deactivate(); - AppThemes = await _themeService.GetAvailableThemesAsync(); + AppThemes = await _newThemeService.GetAvailableThemesAsync(); OnPropertyChanged(nameof(AppThemes)); + // Initialize backdrop types + AvailableBackdropTypes = _newThemeService.GetAvailableBackdropTypes(); + InitializeColors(); SetInitialValues(); PropertyChanged -= PersonalizationSettingsUpdated; PropertyChanged += PersonalizationSettingsUpdated; - _themeService.AccentColorChanged -= AccentColorChanged; - _themeService.ElementThemeChanged -= ElementThemeChanged; + _newThemeService.AccentColorChanged -= AccentColorChanged; + _newThemeService.ElementThemeChanged -= ElementThemeChanged; - _themeService.AccentColorChanged += AccentColorChanged; - _themeService.ElementThemeChanged += ElementThemeChanged; + _newThemeService.AccentColorChanged += AccentColorChanged; + _newThemeService.ElementThemeChanged += ElementThemeChanged; } private void AccentColorChanged(object sender, string e) @@ -260,8 +286,8 @@ public partial class PersonalizationPageViewModel : CoreBaseViewModel { PropertyChanged -= PersonalizationSettingsUpdated; - _themeService.AccentColorChanged -= AccentColorChanged; - _themeService.ElementThemeChanged -= ElementThemeChanged; + _newThemeService.AccentColorChanged -= AccentColorChanged; + _newThemeService.ElementThemeChanged -= ElementThemeChanged; if (AppThemes != null) { @@ -277,18 +303,53 @@ public partial class PersonalizationPageViewModel : CoreBaseViewModel if (e.PropertyName == nameof(SelectedElementTheme) && SelectedElementTheme != null) { - _themeService.RootTheme = SelectedElementTheme.NativeTheme; + _newThemeService.RootTheme = SelectedElementTheme.NativeTheme; } else if (e.PropertyName == nameof(SelectedAppTheme)) { - _themeService.CurrentApplicationThemeId = SelectedAppTheme.Id; + // Set the theme ID, can be null if no theme is selected + _newThemeService.CurrentApplicationThemeId = SelectedAppTheme?.Id; + + // Theme selection should not affect backdrop - they are independent settings + } + else if (e.PropertyName == nameof(SelectedBackdropType) && SelectedBackdropType != null) + { + _newThemeService.CurrentBackdropType = SelectedBackdropType.BackdropType; } else { if (e.PropertyName == nameof(SelectedInfoDisplayMode)) PreferencesService.MailItemDisplayMode = SelectedInfoDisplayMode; else if (e.PropertyName == nameof(SelectedAppColor)) - _themeService.AccentColor = SelectedAppColor.Hex; + _newThemeService.AccentColor = SelectedAppColor.Hex; + } + } + + private sealed class DemoMailItemDisplayInformation( + string fromName, + string fromAddress, + string subject, + string previewText) : IMailItemDisplayInformation + { + public string Subject { get; } = subject; + public string FromName { get; } = fromName; + public string FromAddress { get; } = fromAddress; + public string PreviewText { get; } = previewText; + public bool IsRead { get; } = false; + public bool IsDraft { get; } = false; + public bool HasAttachments { get; } = false; + public bool IsCalendarEvent { get; } = false; + public bool IsFlagged { get; } = false; + public DateTime CreationDate { get; } = DateTime.Now; + public Guid? ContactPictureFileId { get; } = null; + public bool ThumbnailUpdatedEvent { get; } = false; + public bool IsBusy { get; } = false; + public bool IsThreadExpanded { get; } = false; + public AccountContact SenderContact { get; } = null; + event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged + { + add { } + remove { } } } } diff --git a/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs b/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs index 09411a1e..087fb08e 100644 --- a/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs +++ b/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs @@ -1,62 +1,397 @@ -using System; +using System; using System.Collections.Generic; +using System.Collections.ObjectModel; +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.Models.Navigation; +using Wino.Core.Domain.Models.Personalization; using Wino.Core.Domain.Models.Settings; +using Wino.Core.Domain.Models.Translations; +using Wino.Core.Extensions; +using Wino.Core.ViewModels.Data; +using Wino.Mail.ViewModels.Data; using Wino.Messaging.Client.Navigation; namespace Wino.Core.ViewModels; public partial class SettingOptionsPageViewModel : CoreBaseViewModel { - private readonly ISettingsBuilderService _settingsBuilderService; + private readonly INativeAppService _nativeAppService; + private readonly IAccountService _accountService; + private readonly IMimeStorageService _mimeStorageService; + private readonly IStoreRatingService _storeRatingService; + private readonly ITranslationService _translationService; + private readonly INewThemeService _newThemeService; + private readonly IPreferencesService _preferencesService; + private readonly IProviderService _providerService; + private bool _isInitializingSettings; + private bool _isAppearanceSelectionPaused; - [RelayCommand] - private void GoAccountSettings() => Messenger.Send(); + public string GitHubUrl => AppUrls.GitHub; + public string PaypalUrl => AppUrls.Paypal; + public string WebsiteUrl => AppUrls.Website; + public string PrivacyPolicyUrl => AppUrls.PrivacyPolicy; + + public ObservableCollection SearchSuggestions { get; } = []; + public ObservableCollection Accounts { get; } = []; + public ObservableCollection Colors { get; } = []; + + public List ElementThemes { get; } = + [ + new(ApplicationElementTheme.Light, Translator.ElementTheme_Light), + new(ApplicationElementTheme.Dark, Translator.ElementTheme_Dark), + new(ApplicationElementTheme.Default, Translator.ElementTheme_Default), + ]; + + public bool HasAccounts => Accounts.Count > 0; + + [ObservableProperty] + public partial string VersionText { get; set; } = string.Empty; + + [ObservableProperty] + public partial string AccountSummaryText { get; set; } = string.Empty; + + [ObservableProperty] + public partial int AccountCount { get; set; } + + [ObservableProperty] + public partial string StorageSummaryText { get; set; } = string.Empty; + + [ObservableProperty] + public partial string SearchQuery { get; set; } = string.Empty; + + [ObservableProperty] + public partial List AvailableLanguages { get; set; } = []; + + [ObservableProperty] + public partial AppLanguageModel SelectedLanguage { get; set; } + + [ObservableProperty] + public partial ElementThemeContainer SelectedElementTheme { get; set; } + + [ObservableProperty] + public partial AppColorViewModel SelectedAppColor { get; set; } + + [ObservableProperty] + public partial bool UseAccentColor { get; set; } + + public SettingOptionsPageViewModel(INativeAppService nativeAppService, + IAccountService accountService, + IMimeStorageService mimeStorageService, + IStoreRatingService storeRatingService, + ITranslationService translationService, + INewThemeService newThemeService, + IPreferencesService preferencesService, + IProviderService providerService) + { + _nativeAppService = nativeAppService; + _accountService = accountService; + _mimeStorageService = mimeStorageService; + _storeRatingService = storeRatingService; + _translationService = translationService; + _newThemeService = newThemeService; + _preferencesService = preferencesService; + _providerService = providerService; + } + + public override void OnNavigatedTo(NavigationMode mode, object parameters) + { + base.OnNavigatedTo(mode, parameters); + + VersionText = string.Format("{0}{1}", Translator.SettingsAboutVersion, _nativeAppService.GetFullAppVersion()); + SearchQuery = string.Empty; + SearchSuggestions.Clear(); + StorageSummaryText = Translator.SettingsHome_StorageLoading; + InitializeQuickSettings(); + + _ = LoadDashboardAsync(); + } + + public void UpdateSearchSuggestions(string query) + { + SearchQuery = query; + + SearchSuggestions.Clear(); + + foreach (var result in SettingsNavigationInfoProvider.Search(query, AccountSummaryText).Take(6)) + { + SearchSuggestions.Add(result); + } + } + + public SettingsNavigationItemInfo GetBestSearchSuggestion(string query) + => SettingsNavigationInfoProvider.Search(query, AccountSummaryText).FirstOrDefault(); + + public void NavigateToSetting(SettingsNavigationItemInfo item) + { + if (item?.PageType is WinoPage pageType) + { + NavigateSubDetail(pageType); + } + } + + public void NavigateToAccount(IAccountProviderDetailViewModel account) + { + if (account == null) + { + return; + } + + Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsManageAccountSettings_Title, WinoPage.ManageAccountsPage)); + + switch (account) + { + case AccountProviderDetailViewModel accountDetails: + Messenger.Send(new BreadcrumbNavigationRequested(accountDetails.Account.Name, WinoPage.AccountDetailsPage, accountDetails.Account.Id)); + break; + case MergedAccountProviderDetailViewModel mergedAccount: + Messenger.Send(new BreadcrumbNavigationRequested(mergedAccount.MergedInbox.Name, WinoPage.MergedAccountDetailsPage, mergedAccount)); + break; + } + } + + public void NavigateToAddAccount() + { + Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsManageAccountSettings_Title, WinoPage.ManageAccountsPage)); + Messenger.Send(new BreadcrumbNavigationRequested(Translator.WelcomeWizard_Step2Title, WinoPage.ProviderSelectionPage)); + } + + public void NavigateToManageAccounts() + { + Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsManageAccountSettings_Title, WinoPage.ManageAccountsPage)); + } + + private async Task LoadDashboardAsync() + { + var accounts = (await _accountService.GetAccountsAsync().ConfigureAwait(false) ?? []).ToList(); + var count = accounts.Count; + Dictionary storageSizeMap = count == 0 + ? [] + : await _mimeStorageService.GetAccountsMimeStorageSizesAsync(accounts.Select(account => account.Id)).ConfigureAwait(false); + var totalStorageBytes = storageSizeMap.Values.Sum(); + var groupedAccountItems = CreateAccountItems(accounts); + + await ExecuteUIThread(() => + { + AccountCount = count; + AccountSummaryText = string.Format(Translator.SettingsOptions_AccountsSummary, count); + Accounts.Clear(); + + foreach (var account in groupedAccountItems) + { + Accounts.Add(account); + } + + OnPropertyChanged(nameof(HasAccounts)); + StorageSummaryText = totalStorageBytes == 0 + ? Translator.SettingsHome_StorageEmptySummary + : string.Format(Translator.SettingsStorage_TotalUsage, totalStorageBytes.GetBytesReadable()); + + if (!string.IsNullOrWhiteSpace(SearchQuery)) + { + UpdateSearchSuggestions(SearchQuery); + } + }); + } + + private void InitializeQuickSettings() + { + _isInitializingSettings = true; + InitializeColors(); + InitializeLanguageOptions(); + InitializeAppearanceOptions(); + _isInitializingSettings = false; + } + + private void InitializeLanguageOptions() + { + AvailableLanguages = _translationService.GetAvailableLanguages(); + SelectedLanguage = AvailableLanguages.FirstOrDefault(language => language.Language == _preferencesService.CurrentLanguage) + ?? AvailableLanguages.FirstOrDefault(); + } + + private void InitializeColors() + { + Colors.Clear(); + + foreach (var color in _newThemeService.GetAvailableAccountColors().Distinct(StringComparer.OrdinalIgnoreCase)) + { + Colors.Add(new AppColorViewModel(color)); + } + + var systemAccentColor = _newThemeService.GetSystemAccentColorHex(); + + if (Colors.All(color => !string.Equals(color.Hex, systemAccentColor, StringComparison.OrdinalIgnoreCase))) + { + Colors.Add(new AppColorViewModel(systemAccentColor, true)); + } + else + { + var matchingAccentColor = Colors.First(color => string.Equals(color.Hex, systemAccentColor, StringComparison.OrdinalIgnoreCase)); + Colors.Remove(matchingAccentColor); + Colors.Add(new AppColorViewModel(systemAccentColor, true)); + } + } + + private void InitializeAppearanceOptions() + { + _isAppearanceSelectionPaused = true; + + var currentAccentColor = _newThemeService.AccentColor; + + if (!string.IsNullOrWhiteSpace(currentAccentColor) && + Colors.All(color => !string.Equals(color.Hex, currentAccentColor, StringComparison.OrdinalIgnoreCase))) + { + Colors.Insert(0, new AppColorViewModel(currentAccentColor)); + } + + SelectedElementTheme = ElementThemes.FirstOrDefault(theme => theme.NativeTheme == _newThemeService.RootTheme) + ?? ElementThemes.LastOrDefault(); + + if (string.IsNullOrWhiteSpace(currentAccentColor)) + { + SelectedAppColor = Colors.LastOrDefault(color => color.IsAccentColor) ?? Colors.LastOrDefault(); + UseAccentColor = true; + } + else + { + SelectedAppColor = Colors.FirstOrDefault(color => string.Equals(color.Hex, currentAccentColor, StringComparison.OrdinalIgnoreCase)) + ?? Colors.FirstOrDefault(); + UseAccentColor = SelectedAppColor?.IsAccentColor == true; + } + + _isAppearanceSelectionPaused = false; + } + + private List CreateAccountItems(List accounts) + { + var groupedAccounts = accounts + .OrderBy(account => account.MergedInboxId == null ? 1 : 0) + .ThenBy(account => account.Order) + .ThenBy(account => account.Name) + .GroupBy(account => account.MergedInboxId); + var accountItems = new List(); + + foreach (var accountGroup in groupedAccounts) + { + if (accountGroup.Key == null) + { + accountItems.AddRange(accountGroup.Select(CreateAccountProviderDetails)); + continue; + } + + var mergedInbox = accountGroup.First().MergedInbox; + var holdingAccounts = accountGroup + .Select(CreateAccountProviderDetails) + .ToList(); + var mergedAccount = new MergedAccountProviderDetailViewModel(mergedInbox, holdingAccounts) + { + ProviderDetail = holdingAccounts.FirstOrDefault()?.ProviderDetail + }; + + accountItems.Add(mergedAccount); + } + + return accountItems; + } + + private AccountProviderDetailViewModel CreateAccountProviderDetails(MailAccount account) + { + var provider = _providerService.GetProviderDetail(account.ProviderType); + return new AccountProviderDetailViewModel(provider, account); + } + + partial void OnSelectedLanguageChanged(AppLanguageModel value) + { + if (_isInitializingSettings || value == null) + { + return; + } + + _ = ApplyLanguageAsync(value); + } + + partial void OnSelectedElementThemeChanged(ElementThemeContainer value) + { + if (_isInitializingSettings || value == null) + { + return; + } + + _newThemeService.RootTheme = value.NativeTheme; + } + + partial void OnSelectedAppColorChanged(AppColorViewModel value) + { + if (_isInitializingSettings || _isAppearanceSelectionPaused || value == null) + { + return; + } + + _isAppearanceSelectionPaused = true; + UseAccentColor = value.IsAccentColor; + _isAppearanceSelectionPaused = false; + + _newThemeService.AccentColor = value.Hex; + } + + partial void OnUseAccentColorChanged(bool value) + { + if (_isInitializingSettings || _isAppearanceSelectionPaused || Colors.Count == 0) + { + return; + } + + var accentColor = Colors.LastOrDefault(color => color.IsAccentColor); + var fallbackColor = Colors.FirstOrDefault(color => !color.IsAccentColor) ?? Colors.FirstOrDefault(); + var targetColor = value ? accentColor : SelectedAppColor?.IsAccentColor == true ? fallbackColor : SelectedAppColor; + + if (targetColor == null || ReferenceEquals(targetColor, SelectedAppColor)) + { + return; + } + + _isAppearanceSelectionPaused = true; + SelectedAppColor = targetColor; + _isAppearanceSelectionPaused = false; + + _newThemeService.AccentColor = targetColor.Hex; + } + + private async Task ApplyLanguageAsync(AppLanguageModel language) + { + await _translationService.InitializeLanguageAsync(language.Language); + } [RelayCommand] public void NavigateSubDetail(object type) { if (type is WinoPage pageType) { - if (pageType == WinoPage.AccountManagementPage) - { - GoAccountSettings(); - return; - } - - string pageTitle = pageType switch - { - WinoPage.PersonalizationPage => Translator.SettingsPersonalization_Title, - WinoPage.AboutPage => Translator.SettingsAbout_Title, - WinoPage.MessageListPage => Translator.SettingsMessageList_Title, - WinoPage.ReadComposePanePage => Translator.SettingsReadComposePane_Title, - WinoPage.LanguageTimePage => Translator.SettingsLanguageTime_Title, - WinoPage.AppPreferencesPage => Translator.SettingsAppPreferences_Title, - WinoPage.CalendarSettingsPage => Translator.SettingsCalendarSettings_Title, - _ => throw new NotImplementedException() - }; - - Messenger.Send(new BreadcrumbNavigationRequested(pageTitle, pageType)); + var pageInfo = SettingsNavigationInfoProvider.GetInfo(pageType, AccountSummaryText); + Messenger.Send(new BreadcrumbNavigationRequested(pageInfo.Title, pageType)); } } - [ObservableProperty] - private List _settingOptions = new(); - - public SettingOptionsPageViewModel(ISettingsBuilderService settingsBuilderService) + [RelayCommand] + private async Task NavigateExternalAsync(object target) { - _settingsBuilderService = settingsBuilderService; + if (target is not string stringTarget || string.IsNullOrWhiteSpace(stringTarget)) + return; - ReloadSettings(); - } + if (stringTarget == "Store") + { + await _storeRatingService.LaunchStorePageForReviewAsync(); + return; + } - private void ReloadSettings() - { - SettingOptions = _settingsBuilderService.GetSettingItems(); + await _nativeAppService.LaunchUriAsync(new Uri(stringTarget)); } } diff --git a/Wino.Core.ViewModels/SettingsPageViewModel.cs b/Wino.Core.ViewModels/SettingsPageViewModel.cs index 792eebe0..5df81444 100644 --- a/Wino.Core.ViewModels/SettingsPageViewModel.cs +++ b/Wino.Core.ViewModels/SettingsPageViewModel.cs @@ -1,13 +1,54 @@ -using Wino.Core.Domain.Interfaces; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using Wino.Core.Domain; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Settings; namespace Wino.Core.ViewModels; -public class SettingsPageViewModel : CoreBaseViewModel +public partial class SettingsPageViewModel : CoreBaseViewModel { - public SettingsPageViewModel(INavigationService navigationService) + private readonly IAccountService _accountService; + + public SettingsPageViewModel( + INavigationService navigationService, + IStatePersistanceService statePersistenceService, + IAccountService accountService) { NavigationService = navigationService; + StatePersistenceService = statePersistenceService; + _accountService = accountService; } public INavigationService NavigationService { get; } + public IStatePersistanceService StatePersistenceService { get; } + + [ObservableProperty] + public partial string CurrentDescription { get; set; } = string.Empty; + + [ObservableProperty] + public partial string ManageAccountsDescription { get; set; } = string.Empty; + + public async Task UpdateActivePageAsync(WinoPage pageType) + { + await EnsureAccountSummaryAsync(); + + var info = SettingsNavigationInfoProvider.GetInfo(pageType, ManageAccountsDescription); + await ExecuteUIThread(() => CurrentDescription = info.Description); + } + + private async Task EnsureAccountSummaryAsync() + { + if (!string.IsNullOrWhiteSpace(ManageAccountsDescription)) + return; + + var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false); + var count = accounts?.Count ?? 0; + + await ExecuteUIThread(() => + { + ManageAccountsDescription = string.Format(Translator.SettingsOptions_AccountsSummary, count); + }); + } } diff --git a/Wino.Core.ViewModels/WelcomeHostPageViewModel.cs b/Wino.Core.ViewModels/WelcomeHostPageViewModel.cs new file mode 100644 index 00000000..5ee5dd40 --- /dev/null +++ b/Wino.Core.ViewModels/WelcomeHostPageViewModel.cs @@ -0,0 +1,13 @@ +using Wino.Core.Domain.Interfaces; + +namespace Wino.Core.ViewModels; + +public class WelcomeHostPageViewModel : CoreBaseViewModel +{ + public WelcomeHostPageViewModel(INavigationService navigationService) + { + NavigationService = navigationService; + } + + public INavigationService NavigationService { get; } +} diff --git a/Wino.Core.ViewModels/Wino.Core.ViewModels.csproj b/Wino.Core.ViewModels/Wino.Core.ViewModels.csproj index f0cce77b..a6d57b15 100644 --- a/Wino.Core.ViewModels/Wino.Core.ViewModels.csproj +++ b/Wino.Core.ViewModels/Wino.Core.ViewModels.csproj @@ -1,10 +1,13 @@  - net9.0 + net10.0 x86;x64;arm64 win-x86;win-x64;win-arm64 true true + true + true + true diff --git a/Wino.Core.ViewModels/WinoAccountManagementPageViewModel.cs b/Wino.Core.ViewModels/WinoAccountManagementPageViewModel.cs new file mode 100644 index 00000000..80afdd15 --- /dev/null +++ b/Wino.Core.ViewModels/WinoAccountManagementPageViewModel.cs @@ -0,0 +1,616 @@ +#nullable enable +using System; +using System.Collections.ObjectModel; +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.Models.Accounts; +using Wino.Core.Domain.Models.Navigation; +using Wino.Core.ViewModels.Data; +using Wino.Mail.Api.Contracts.Common; +using Wino.Messaging.UI; + +namespace Wino.Core.ViewModels; + +public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel, + IRecipient, + IRecipient, + IRecipient +{ + private readonly IWinoAccountProfileService _profileService; + private readonly IWinoAccountDataSyncService _syncService; + private readonly IMailDialogService _dialogService; + private readonly IStoreManagementService _storeManagementService; + private readonly WinoAddOnItemViewModel _aiPackAddOn; + private readonly WinoAddOnItemViewModel _unlimitedAccountsAddOn; + + public ObservableCollection AddOns { get; } = []; + + [ObservableProperty] + public partial bool IsBusy { get; set; } + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsSignedOut))] + public partial bool IsSignedIn { get; set; } + + [ObservableProperty] + public partial string AccountEmail { get; set; } = string.Empty; + + [ObservableProperty] + public partial string AccountStatusText { get; set; } = string.Empty; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(PurchaseAddOnCommand))] + public partial bool IsCheckoutInProgress { get; set; } + + public bool IsSignedOut => !IsSignedIn; + + public WinoAccountManagementPageViewModel(IWinoAccountProfileService profileService, + IWinoAccountDataSyncService syncService, + IMailDialogService dialogService, + IStoreManagementService storeManagementService) + { + _profileService = profileService; + _syncService = syncService; + _dialogService = dialogService; + _storeManagementService = storeManagementService; + + _aiPackAddOn = CreateAddOnItem(WinoAddOnProductType.AI_PACK); + _unlimitedAccountsAddOn = CreateAddOnItem(WinoAddOnProductType.UNLIMITED_ACCOUNTS); + AddOns.Add(_aiPackAddOn); + AddOns.Add(_unlimitedAccountsAddOn); + } + + public override void OnNavigatedTo(NavigationMode mode, object parameters) + { + base.OnNavigatedTo(mode, parameters); + _ = InitializeAsync(); + } + + [RelayCommand] + private async Task RegisterAsync() + { + var account = await _dialogService.ShowWinoAccountRegistrationDialogAsync(); + if (account == null) + { + return; + } + + _dialogService.InfoBarMessage(Translator.GeneralTitle_Info, + string.Format(Translator.WinoAccount_RegisterSuccessMessage, account.Email), + InfoBarMessageType.Success); + } + + [RelayCommand] + private async Task SignInAsync() + { + var account = await _dialogService.ShowWinoAccountLoginDialogAsync(); + if (account == null) + { + return; + } + + _dialogService.InfoBarMessage(Translator.GeneralTitle_Info, + string.Format(Translator.WinoAccount_LoginSuccessMessage, account.Email), + InfoBarMessageType.Success); + } + + [RelayCommand] + private async Task SignOutAsync() + { + var account = await _profileService.GetActiveAccountAsync().ConfigureAwait(false); + if (account == null) + { + _dialogService.InfoBarMessage(Translator.GeneralTitle_Warning, + Translator.WinoAccount_SignOut_NoAccountMessage, + InfoBarMessageType.Warning); + return; + } + + await _profileService.SignOutAsync().ConfigureAwait(false); + + _dialogService.InfoBarMessage(Translator.GeneralTitle_Info, + string.Format(Translator.WinoAccount_SignOut_SuccessMessage, account.Email), + InfoBarMessageType.Success); + } + + [RelayCommand] + private async Task ChangePasswordAsync() + { + var account = await _profileService.GetActiveAccountAsync(); + if (account == null) + { + _dialogService.InfoBarMessage(Translator.GeneralTitle_Warning, + Translator.WinoAccount_SignOut_NoAccountMessage, + InfoBarMessageType.Warning); + return; + } + + var shouldContinue = await _dialogService.ShowConfirmationDialogAsync( + string.Format(Translator.WinoAccount_ChangePassword_ConfirmationMessage, account.Email), + Translator.WinoAccount_ChangePassword_Title, + Translator.WinoAccount_ChangePassword_Action); + + if (!shouldContinue) + { + return; + } + + var response = await _profileService.ForgotPasswordAsync(account.Email); + if (!response.IsSuccess) + { + _dialogService.InfoBarMessage(Translator.GeneralTitle_Error, + TranslateForgotPasswordError(response.ErrorCode), + InfoBarMessageType.Error); + return; + } + + _dialogService.InfoBarMessage(Translator.GeneralTitle_Info, + string.Format(Translator.WinoAccount_ForgotPasswordDialog_SuccessMessage, account.Email), + InfoBarMessageType.Success); + } + + private static string TranslateForgotPasswordError(string? errorCode) + => errorCode switch + { + ApiErrorCodes.EmailNotRegistered => Translator.WinoAccount_Error_EmailNotRegistered, + ApiErrorCodes.ValidationFailed => Translator.WinoAccount_Error_ValidationFailed, + _ when string.IsNullOrWhiteSpace(errorCode) => Translator.GeneralTitle_Error, + _ => errorCode! + }; + + [RelayCommand(CanExecute = nameof(CanPurchaseAddOn))] + private async Task PurchaseAddOnAsync(WinoAddOnItemViewModel? addOn) + { + if (addOn == null) + { + return; + } + + await ExecuteUIThread(() => + { + IsCheckoutInProgress = true; + addOn.IsPurchaseInProgress = true; + }); + + try + { + var purchaseResult = await _storeManagementService.PurchaseAsync(addOn.ProductType); + + if (purchaseResult == StorePurchaseResult.NotPurchased) + { + _dialogService.InfoBarMessage(Translator.GeneralTitle_Error, + Translator.WinoAccount_Management_PurchaseStartFailed, + InfoBarMessageType.Error); + return; + } + + var syncResult = await _profileService.SyncStoreEntitlementsAsync().ConfigureAwait(false); + if (!syncResult.IsSuccess && !string.Equals(syncResult.ErrorCode, "MissingAccessToken", StringComparison.Ordinal)) + { + _dialogService.InfoBarMessage(Translator.GeneralTitle_Error, + TranslateStoreSyncError(syncResult.ErrorCode), + InfoBarMessageType.Error); + return; + } + + await HandleAddOnPurchasedAsync().ConfigureAwait(false); + } + catch (OperationCanceledException) + { + } + catch (Exception) + { + _dialogService.InfoBarMessage(Translator.GeneralTitle_Error, + Translator.WinoAccount_Management_PurchaseStartFailed, + InfoBarMessageType.Error); + } + finally + { + await ExecuteUIThread(() => + { + IsCheckoutInProgress = false; + addOn.IsPurchaseInProgress = false; + }); + } + } + + private bool CanPurchaseAddOn(WinoAddOnItemViewModel? addOn) + => addOn != null && !addOn.IsPurchased && !addOn.IsLoading && !IsCheckoutInProgress; + + [RelayCommand] + private async Task ExportSettingsAsync() + { + try + { + var result = await _dialogService.ShowWinoAccountExportDialogAsync().ConfigureAwait(false); + if (result == null) + { + return; + } + + _dialogService.InfoBarMessage( + Translator.GeneralTitle_Info, + BuildExportSuccessMessage(result), + InfoBarMessageType.Success); + } + catch (Exception ex) + { + _dialogService.InfoBarMessage( + Translator.GeneralTitle_Error, + ex.Message, + InfoBarMessageType.Error); + } + } + + [RelayCommand] + private async Task ImportSettingsAsync() + { + await ExecuteUIThread(() => IsBusy = true); + + try + { + var result = await _syncService.ImportAsync(new WinoAccountSyncSelection()); + + if (!result.HasAnyRemoteData) + { + _dialogService.InfoBarMessage( + Translator.GeneralTitle_Info, + Translator.WinoAccount_Management_NoRemoteSettings, + InfoBarMessageType.Information); + return; + } + + var messageType = result.FailedPreferenceCount > 0 + ? InfoBarMessageType.Warning + : InfoBarMessageType.Success; + + _dialogService.InfoBarMessage( + result.FailedPreferenceCount > 0 ? Translator.GeneralTitle_Warning : Translator.GeneralTitle_Info, + BuildImportMessage(result), + messageType); + } + catch (Exception ex) + { + _dialogService.InfoBarMessage( + Translator.GeneralTitle_Error, + ex.Message, + InfoBarMessageType.Error); + } + finally + { + await ExecuteUIThread(() => IsBusy = false); + } + } + + protected override void RegisterRecipients() + { + base.RegisterRecipients(); + + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + } + + protected override void UnregisterRecipients() + { + base.UnregisterRecipients(); + + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + } + + public void Receive(WinoAccountProfileUpdatedMessage message) + => _ = LoadAsync(); + + public void Receive(WinoAccountProfileDeletedMessage message) + => _ = LoadAsync(); + + public void Receive(WinoAccountAddOnPurchasedMessage message) + => _ = HandleAddOnPurchasedAsync(); + + private async Task InitializeAsync() + { + await LoadAsync().ConfigureAwait(false); + } + + private async Task LoadAsync() + { + WinoAccount? cachedAccount = null; + + try + { + cachedAccount = await _profileService.GetActiveAccountAsync().ConfigureAwait(false); + + if (cachedAccount != null) + { + await ApplyAccountStateAsync(cachedAccount).ConfigureAwait(false); + } + + await ExecuteUIThread(() => IsBusy = true); + await ResetAddOnStatesAsync().ConfigureAwait(false); + var loadAiPackTask = LoadAiPackAddOnAsync(); + var loadUnlimitedAccountsTask = LoadUnlimitedAccountsAddOnAsync(); + + var resolvedAccount = cachedAccount; + + if (cachedAccount == null || IsAccessTokenExpired(cachedAccount)) + { + try + { + var account = await _profileService.GetAuthenticatedAccountAsync().ConfigureAwait(false); + if (account != null) + { + resolvedAccount = account; + + var refreshedProfileResult = await _profileService.RefreshProfileAsync().ConfigureAwait(false); + if (refreshedProfileResult.IsSuccess && refreshedProfileResult.Account != null) + { + resolvedAccount = refreshedProfileResult.Account; + } + } + } + catch (Exception) + { + resolvedAccount ??= cachedAccount; + } + } + + await ApplyAccountStateAsync(resolvedAccount).ConfigureAwait(false); + await Task.WhenAll(loadAiPackTask, loadUnlimitedAccountsTask).ConfigureAwait(false); + } + catch (Exception) + { + if (cachedAccount == null) + { + _dialogService.InfoBarMessage(Translator.GeneralTitle_Error, + Translator.WinoAccount_Management_LoadFailed, + InfoBarMessageType.Error); + await ResetStateAsync().ConfigureAwait(false); + } + } + finally + { + await ExecuteUIThread(() => IsBusy = false); + } + } + + private async Task ApplyAccountStateAsync(Wino.Core.Domain.Entities.Shared.WinoAccount? account) + { + await ExecuteUIThread(() => + { + IsSignedIn = account != null; + AccountEmail = account?.Email ?? string.Empty; + AccountStatusText = account == null + ? string.Empty + : string.Format(Translator.WinoAccount_Management_StatusLabel, account.AccountStatus); + }); + } + + private async Task HandleAddOnPurchasedAsync() + { + await LoadAsync().ConfigureAwait(false); + + _dialogService.InfoBarMessage(Translator.Info_PurchaseThankYouTitle, + Translator.Info_PurchaseThankYouMessage, + InfoBarMessageType.Success); + } + + private async Task ResetStateAsync() + { + await ExecuteUIThread(() => + { + IsSignedIn = false; + AccountEmail = string.Empty; + AccountStatusText = string.Empty; + IsCheckoutInProgress = false; + PurchaseAddOnCommand.NotifyCanExecuteChanged(); + }); + + await ResetAddOnStatesAsync().ConfigureAwait(false); + } + + private WinoAddOnItemViewModel CreateAddOnItem(WinoAddOnProductType productType) + { + return new WinoAddOnItemViewModel(productType) + { + PurchaseCommand = PurchaseAddOnCommand, + UsageLimit = 1 + }; + } + + private async Task ResetAddOnStatesAsync() + { + await ExecuteUIThread(() => + { + ResetAddOnItem(_aiPackAddOn); + ResetAddOnItem(_unlimitedAccountsAddOn); + PurchaseAddOnCommand.NotifyCanExecuteChanged(); + }); + } + + private static void ResetAddOnItem(WinoAddOnItemViewModel addOn) + { + addOn.IsLoading = true; + addOn.IsPurchased = false; + addOn.IsPurchaseInProgress = false; + addOn.HasUsageData = false; + addOn.ErrorText = string.Empty; + addOn.UsageCount = 0; + addOn.UsageLimit = 1; + addOn.UsagePercentage = 0; + addOn.RenewalText = string.Empty; + addOn.UsageResetText = string.Empty; + } + + private static string TranslateStoreSyncError(string? errorCode) + => errorCode switch + { + _ => Translator.WinoAccount_Management_StoreSyncFailed + }; + + private static string BuildExportSuccessMessage(WinoAccountSyncExportResult result) + { + var parts = new Collection(); + + if (result.IncludedPreferences) + { + parts.Add(Translator.WinoAccount_Management_ExportPreferencesSucceeded); + } + + if (result.IncludedAccounts) + { + parts.Add(string.Format(Translator.WinoAccount_Management_ExportAccountsSucceeded, result.ExportedMailboxCount)); + } + + if (parts.Count == 0) + { + parts.Add(Translator.WinoAccount_Management_ExportSucceeded); + } + + return string.Join(" ", parts); + } + + private static string BuildImportMessage(WinoAccountSyncImportResult result) + { + var parts = new Collection(); + + if (result.HadRemotePreferences) + { + parts.Add(result.FailedPreferenceCount > 0 + ? string.Format(Translator.WinoAccount_Management_ImportPartial, result.AppliedPreferenceCount, result.FailedPreferenceCount) + : string.Format(Translator.WinoAccount_Management_ImportPreferencesSucceeded, result.AppliedPreferenceCount)); + } + + if (result.ImportedMailboxCount > 0) + { + parts.Add(string.Format(Translator.WinoAccount_Management_ImportAccountsSucceeded, result.ImportedMailboxCount)); + } + + if (result.SkippedDuplicateMailboxCount > 0) + { + parts.Add(string.Format(Translator.WinoAccount_Management_ImportDuplicateAccountsSkipped, result.SkippedDuplicateMailboxCount)); + } + + if (parts.Count == 0) + { + parts.Add(Translator.WinoAccount_Management_ImportEmpty); + } + + if (result.ImportedMailboxCount > 0) + { + parts.Add(Translator.WinoAccount_Management_ImportReloginReminder); + } + + return string.Join(" ", parts); + } + + private static bool IsAccessTokenExpired(WinoAccount account) + => string.IsNullOrWhiteSpace(account.AccessToken) || account.AccessTokenExpiresAtUtc <= DateTime.UtcNow; + + private async Task LoadUnlimitedAccountsAddOnAsync() + { + try + { + var hasUnlimitedAccounts = await _storeManagementService.HasProductAsync(WinoAddOnProductType.UNLIMITED_ACCOUNTS).ConfigureAwait(false); + await ExecuteUIThread(() => + { + _unlimitedAccountsAddOn.IsPurchased = hasUnlimitedAccounts; + _unlimitedAccountsAddOn.ErrorText = string.Empty; + }); + } + catch (Exception) + { + await ExecuteUIThread(() => + { + _unlimitedAccountsAddOn.ErrorText = Translator.WinoAccount_Management_AddOnLoadFailed; + }); + } + finally + { + await ExecuteUIThread(() => + { + _unlimitedAccountsAddOn.IsLoading = false; + PurchaseAddOnCommand.NotifyCanExecuteChanged(); + }); + } + } + + private async Task LoadAiPackAddOnAsync() + { + try + { + var hasAiPack = await _storeManagementService.HasProductAsync(WinoAddOnProductType.AI_PACK).ConfigureAwait(false); + + await ExecuteUIThread(() => + { + _aiPackAddOn.IsPurchased = hasAiPack; + _aiPackAddOn.ErrorText = string.Empty; + }); + + if (!hasAiPack) + { + return; + } + + var aiStatusResponse = await _profileService.GetAiStatusAsync().ConfigureAwait(false); + if (!aiStatusResponse.IsSuccess || aiStatusResponse.Result == null) + { + await ExecuteUIThread(() => + { + _aiPackAddOn.HasUsageData = false; + _aiPackAddOn.ErrorText = Translator.WinoAccount_Management_AiPackUsageLoadFailed; + }); + return; + } + + var aiStatus = aiStatusResponse.Result; + if (aiStatus.MonthlyLimit is not int usageLimit || usageLimit <= 0 || aiStatus.Used is not int usageCount) + { + await ExecuteUIThread(() => + { + _aiPackAddOn.HasUsageData = false; + _aiPackAddOn.ErrorText = Translator.WinoAccount_Management_AiPackUsageLoadFailed; + }); + return; + } + + await ExecuteUIThread(() => + { + _aiPackAddOn.HasUsageData = true; + _aiPackAddOn.ErrorText = string.Empty; + _aiPackAddOn.UsageCount = usageCount; + _aiPackAddOn.UsageLimit = usageLimit; + _aiPackAddOn.UsagePercentage = usageLimit > 0 ? (double)usageCount / usageLimit * 100 : 0; + _aiPackAddOn.RenewalText = aiStatus.CurrentPeriodEndUtc is DateTimeOffset renewalDateUtc + ? string.Format(Translator.WinoAccount_Management_AiPackRenews, renewalDateUtc.LocalDateTime) + : string.Empty; + _aiPackAddOn.UsageResetText = aiStatus.CurrentPeriodEndUtc is DateTimeOffset resetDateUtc + ? string.Format(Translator.WinoAccount_Management_AiPackResets, resetDateUtc.LocalDateTime) + : string.Empty; + }); + } + catch (Exception) + { + await ExecuteUIThread(() => + { + _aiPackAddOn.HasUsageData = false; + _aiPackAddOn.ErrorText = Translator.WinoAccount_Management_AddOnLoadFailed; + }); + } + finally + { + await ExecuteUIThread(() => + { + _aiPackAddOn.IsLoading = false; + PurchaseAddOnCommand.NotifyCanExecuteChanged(); + }); + } + } +} diff --git a/Wino.Core/CoreContainerSetup.cs b/Wino.Core/CoreContainerSetup.cs index 1853c4c9..6f07640f 100644 --- a/Wino.Core/CoreContainerSetup.cs +++ b/Wino.Core/CoreContainerSetup.cs @@ -4,6 +4,9 @@ using Wino.Authentication; using Wino.Core.Domain.Interfaces; using Wino.Core.Integration.Processors; using Wino.Core.Services; +using Wino.Core.Synchronizers.Errors; +using Wino.Core.Synchronizers.Errors.Gmail; +using Wino.Core.Synchronizers.Errors.Imap; using Wino.Core.Synchronizers.Errors.Outlook; using Wino.Core.Synchronizers.ImapSync; @@ -17,6 +20,8 @@ public static class CoreContainerSetup services.AddSingleton(loggerLevelSwitcher); services.AddSingleton(); + services.AddSingleton(provider => SynchronizationManager.Instance); + services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -31,15 +36,36 @@ public static class CoreContainerSetup services.AddTransient(); services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); + services.AddTransient(); - // Register error factory handlers + // Register Outlook error handlers services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + // Register Gmail error handlers + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + // Register shared error handlers + services.AddTransient(); + + // Register IMAP error handlers + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + // Register Outlook auth handlers + services.AddTransient(); + + // Register error handler factories services.AddTransient(); services.AddTransient(); + services.AddTransient(); + + // Register retry executor + services.AddTransient(); } } diff --git a/Wino.Core/Domain/Interfaces/ISynchronizerErrorHandler.cs b/Wino.Core/Domain/Interfaces/ISynchronizerErrorHandler.cs index 6e5dcad2..f3a8e995 100644 --- a/Wino.Core/Domain/Interfaces/ISynchronizerErrorHandler.cs +++ b/Wino.Core/Domain/Interfaces/ISynchronizerErrorHandler.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using Wino.Core.Domain.Models.Errors; +using Wino.Core.Domain.Models.Synchronization; namespace Wino.Core.Domain.Interfaces; @@ -23,10 +23,6 @@ public interface ISynchronizerErrorHandler Task HandleAsync(SynchronizerErrorContext error); } -public interface ISynchronizerErrorHandlerFactory -{ - Task HandleErrorAsync(SynchronizerErrorContext error); -} - public interface IOutlookSynchronizerErrorHandlerFactory : ISynchronizerErrorHandlerFactory; public interface IGmailSynchronizerErrorHandlerFactory : ISynchronizerErrorHandlerFactory; +public interface IImapSynchronizerErrorHandlerFactory : ISynchronizerErrorHandlerFactory; diff --git a/Wino.Core/Domain/Models/Errors/SynchronizerErrorContext.cs b/Wino.Core/Domain/Models/Errors/SynchronizerErrorContext.cs deleted file mode 100644 index 4f868ea3..00000000 --- a/Wino.Core/Domain/Models/Errors/SynchronizerErrorContext.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using Wino.Core.Domain.Entities.Shared; -using Wino.Core.Domain.Interfaces; - -namespace Wino.Core.Domain.Models.Errors; - -/// -/// Contains context information about a synchronizer error -/// -public class SynchronizerErrorContext -{ - /// - /// Account associated with the error - /// - public MailAccount Account { get; set; } - - /// - /// Gets or sets the error code - /// - public int? ErrorCode { get; set; } - - /// - /// Gets or sets the error message - /// - public string ErrorMessage { get; set; } - - /// - /// Gets or sets the request bundle associated with the error - /// - public IRequestBundle RequestBundle { get; set; } - - /// - /// Gets or sets additional data associated with the error - /// - public Dictionary AdditionalData { get; set; } = new Dictionary(); - - /// - /// Gets or sets the exception associated with the error - /// - public Exception Exception { get; set; } -} diff --git a/Wino.Core/Extensions/CalendarInvitationExtensions.cs b/Wino.Core/Extensions/CalendarInvitationExtensions.cs new file mode 100644 index 00000000..36219272 --- /dev/null +++ b/Wino.Core/Extensions/CalendarInvitationExtensions.cs @@ -0,0 +1,121 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using MimeKit; + +namespace Wino.Core.Extensions; + +public static class CalendarInvitationExtensions +{ + public static string ExtractInvitationUid(this MimeMessage message) + { + if (message == null) + { + return null; + } + + var icsContent = GetCalendarContent(message); + if (string.IsNullOrWhiteSpace(icsContent)) + { + return null; + } + + var unfolded = UnfoldIcs(icsContent); + var veventSection = ExtractFirstVEventSection(unfolded); + if (string.IsNullOrWhiteSpace(veventSection)) + { + return null; + } + + return TryReadIcsProperty(veventSection, "UID", out var uid) + ? uid + : null; + } + + private static string GetCalendarContent(MimeMessage message) + { + var textPart = message.BodyParts + .OfType() + .FirstOrDefault(p => p.ContentType?.MimeType?.Equals("text/calendar", StringComparison.OrdinalIgnoreCase) == true); + + if (textPart != null) + { + return textPart.Text; + } + + var mimePart = message.BodyParts + .OfType() + .FirstOrDefault(p => p.ContentType?.MimeType?.Equals("text/calendar", StringComparison.OrdinalIgnoreCase) == true); + + if (mimePart == null) + { + return null; + } + + using var stream = new MemoryStream(); + mimePart.Content.DecodeTo(stream); + var bytes = stream.ToArray(); + if (bytes.Length == 0) + { + return null; + } + + var charset = mimePart.ContentType?.Charset; + var encoding = string.IsNullOrWhiteSpace(charset) ? Encoding.UTF8 : Encoding.GetEncoding(charset); + return encoding.GetString(bytes); + } + + private static string UnfoldIcs(string content) + => content + .Replace("\r\n ", string.Empty, StringComparison.Ordinal) + .Replace("\r\n\t", string.Empty, StringComparison.Ordinal) + .Replace("\n ", string.Empty, StringComparison.Ordinal) + .Replace("\n\t", string.Empty, StringComparison.Ordinal); + + private static string ExtractFirstVEventSection(string ics) + { + const string beginVevent = "BEGIN:VEVENT"; + const string endVevent = "END:VEVENT"; + + var beginIndex = ics.IndexOf(beginVevent, StringComparison.OrdinalIgnoreCase); + if (beginIndex < 0) + { + return string.Empty; + } + + var endIndex = ics.IndexOf(endVevent, beginIndex, StringComparison.OrdinalIgnoreCase); + if (endIndex < 0) + { + return ics[beginIndex..]; + } + + return ics.Substring(beginIndex, endIndex - beginIndex + endVevent.Length); + } + + private static bool TryReadIcsProperty(string icsSection, string propertyName, out string value) + { + value = string.Empty; + var lines = icsSection.Split(["\r\n", "\n"], StringSplitOptions.RemoveEmptyEntries); + + foreach (var rawLine in lines) + { + var line = rawLine.Trim(); + if (!line.StartsWith(propertyName, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var colonIndex = line.IndexOf(':'); + if (colonIndex <= 0 || colonIndex >= line.Length - 1) + { + continue; + } + + value = line[(colonIndex + 1)..].Trim(); + return !string.IsNullOrWhiteSpace(value); + } + + return false; + } +} diff --git a/Wino.Core/Extensions/GoogleIntegratorExtensions.cs b/Wino.Core/Extensions/GoogleIntegratorExtensions.cs index 8e19322a..292fb187 100644 --- a/Wino.Core/Extensions/GoogleIntegratorExtensions.cs +++ b/Wino.Core/Extensions/GoogleIntegratorExtensions.cs @@ -1,17 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Web; using Google.Apis.Calendar.v3.Data; using Google.Apis.Gmail.v1.Data; -using MimeKit; using Wino.Core.Domain; using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Enums; using Wino.Core.Misc; using Wino.Services; -using Wino.Services.Extensions; namespace Wino.Core.Extensions; @@ -121,41 +118,6 @@ public static class GoogleIntegratorExtensions return GetNormalizedLabelName(lastPart); } - /// - /// Returns MailCopy out of native Gmail message and converted MimeMessage of that native messaage. - /// - /// Gmail Message - /// MimeMessage representation of that native message. - /// MailCopy object that is ready to be inserted to database. - public static MailCopy AsMailCopy(this Message gmailMessage, MimeMessage mimeMessage) - { - bool isUnread = gmailMessage.GetIsUnread(); - bool isFocused = gmailMessage.GetIsFocused(); - bool isFlagged = gmailMessage.GetIsFlagged(); - bool isDraft = gmailMessage.GetIsDraft(); - - return new MailCopy() - { - CreationDate = mimeMessage.Date.UtcDateTime, - Subject = HttpUtility.HtmlDecode(mimeMessage.Subject), - FromName = MailkitClientExtensions.GetActualSenderName(mimeMessage), - FromAddress = MailkitClientExtensions.GetActualSenderAddress(mimeMessage), - PreviewText = HttpUtility.HtmlDecode(gmailMessage.Snippet), - ThreadId = gmailMessage.ThreadId, - Importance = (MailImportance)mimeMessage.Importance, - Id = gmailMessage.Id, - IsDraft = isDraft, - HasAttachments = mimeMessage.Attachments.Any(), - IsRead = !isUnread, - IsFlagged = isFlagged, - IsFocused = isFocused, - InReplyTo = mimeMessage.InReplyTo, - MessageId = mimeMessage.MessageId, - References = mimeMessage.References.GetReferences(), - FileId = Guid.NewGuid() - }; - } - public static List GetRemoteAliases(this ListSendAsResponse response) { return response?.SendAs?.Select(a => new RemoteAccountAlias() @@ -169,7 +131,7 @@ public static class GoogleIntegratorExtensions }).ToList(); } - public static AccountCalendar AsCalendar(this CalendarListEntry calendarListEntry, Guid accountId) + public static AccountCalendar AsCalendar(this CalendarListEntry calendarListEntry, Guid accountId, string fallbackBackgroundColor = null) { var calendar = new AccountCalendar() { @@ -179,13 +141,14 @@ public static class GoogleIntegratorExtensions Id = Guid.NewGuid(), TimeZone = calendarListEntry.TimeZone, IsPrimary = calendarListEntry.Primary.GetValueOrDefault(), + IsSynchronizationEnabled = true, }; // Bg color must present. Generate one if doesnt exists. // Text color is optional. It'll be overriden by UI for readibility. - calendar.BackgroundColorHex = string.IsNullOrEmpty(calendarListEntry.BackgroundColor) ? ColorHelpers.GenerateFlatColorHex() : calendarListEntry.BackgroundColor; - calendar.TextColorHex = string.IsNullOrEmpty(calendarListEntry.ForegroundColor) ? "#000000" : calendarListEntry.ForegroundColor; + calendar.BackgroundColorHex = fallbackBackgroundColor ?? ColorHelpers.GenerateFlatColorHex(); + calendar.TextColorHex = ColorHelpers.GetReadableTextColorHex(calendar.BackgroundColorHex); return calendar; } @@ -215,6 +178,40 @@ public static class GoogleIntegratorExtensions return null; } + public static DateTime? GetEventLocalDateTime(EventDateTime calendarEvent) + { + if (calendarEvent == null) + { + return null; + } + + if (calendarEvent.DateTimeDateTimeOffset != null) + { + return DateTime.SpecifyKind(calendarEvent.DateTimeDateTimeOffset.Value.DateTime, DateTimeKind.Unspecified); + } + + if (calendarEvent.Date != null) + { + if (DateTime.TryParse(calendarEvent.Date, out DateTime eventDateTime)) + { + return DateTime.SpecifyKind(eventDateTime, DateTimeKind.Unspecified); + } + + throw new Exception("Invalid date format in Google Calendar event date."); + } + + return null; + } + + /// + /// Extracts the timezone string from EventDateTime. + /// Returns null for all-day events or if timezone is not specified. + /// + public static string GetEventTimeZone(EventDateTime eventDateTime) + { + return eventDateTime?.TimeZone; + } + /// /// RRULE, EXRULE, RDATE and EXDATE lines for a recurring event, as specified in RFC5545. /// diff --git a/Wino.Core/Extensions/OutlookIntegratorExtensions.cs b/Wino.Core/Extensions/OutlookIntegratorExtensions.cs index 87051308..4bfdf542 100644 --- a/Wino.Core/Extensions/OutlookIntegratorExtensions.cs +++ b/Wino.Core/Extensions/OutlookIntegratorExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using Microsoft.Graph.Models; @@ -8,6 +9,7 @@ using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Extensions; using Wino.Core.Misc; namespace Wino.Core.Extensions; @@ -60,16 +62,76 @@ public static class OutlookIntegratorExtensions FromName = outlookMessage.From?.EmailAddress?.Name, FromAddress = outlookMessage.From?.EmailAddress?.Address, Subject = outlookMessage.Subject, - FileId = Guid.NewGuid() + FileId = Guid.NewGuid(), + ItemType = MailItemType.Mail // ItemType will be set by caller if calendar access is granted }; + // Extract In-Reply-To and References from InternetMessageHeaders for threading. + if (outlookMessage.InternetMessageHeaders != null) + { + var inReplyToHeader = outlookMessage.InternetMessageHeaders + .FirstOrDefault(h => string.Equals(h.Name, "In-Reply-To", StringComparison.OrdinalIgnoreCase)); + if (inReplyToHeader != null) + mailCopy.InReplyTo = MailHeaderExtensions.StripAngleBrackets(inReplyToHeader.Value); + + var referencesHeader = outlookMessage.InternetMessageHeaders + .FirstOrDefault(h => string.Equals(h.Name, "References", StringComparison.OrdinalIgnoreCase)); + if (referencesHeader != null) + mailCopy.References = MailHeaderExtensions.NormalizeReferences(referencesHeader.Value); + } + if (mailCopy.IsDraft) mailCopy.DraftId = mailCopy.ThreadId; return mailCopy; } - public static Message AsOutlookMessage(this MimeMessage mime, bool includeInternetHeaders) + public static MailItemType GetMailItemType(this Message message) + { + // Check if the message is an EventMessage (calendar-related) + if (message is EventMessage eventMessage) + { + // Try to get MeetingMessageType from the property + if (eventMessage.MeetingMessageType.HasValue) + { + return eventMessage.MeetingMessageType.Value switch + { + MeetingMessageType.MeetingRequest => MailItemType.CalendarInvitation, + MeetingMessageType.MeetingCancelled => MailItemType.CalendarCancellation, + MeetingMessageType.MeetingAccepted or + MeetingMessageType.MeetingTenativelyAccepted or + MeetingMessageType.MeetingDeclined => MailItemType.CalendarResponse, + _ => MailItemType.Mail + }; + } + + // Fallback: Check @odata.type in AdditionalData to determine specific type + if (message.AdditionalData?.TryGetValue("@odata.type", out var odataType) == true) + { + var odataTypeString = odataType?.ToString(); + if (odataTypeString != null) + { + // eventMessageRequest -> CalendarInvitation + if (odataTypeString.Contains("eventMessageRequest", StringComparison.OrdinalIgnoreCase)) + return MailItemType.CalendarInvitation; + + // eventMessageResponse -> CalendarResponse + if (odataTypeString.Contains("eventMessageResponse", StringComparison.OrdinalIgnoreCase)) + return MailItemType.CalendarResponse; + + // Generic eventMessage without specific type - assume invitation + if (odataTypeString.Contains("eventMessage", StringComparison.OrdinalIgnoreCase)) + return MailItemType.CalendarInvitation; + } + } + + return MailItemType.CalendarInvitation; + } + + return MailItemType.Mail; + } + + public static Message AsOutlookMessage(this MimeMessage mime, bool includeInternetHeaders, string conversationId = null) { var fromAddress = GetRecipients(mime.From).ElementAt(0); var toAddresses = GetRecipients(mime.To).ToList(); @@ -77,35 +139,42 @@ public static class OutlookIntegratorExtensions var bccAddresses = GetRecipients(mime.Bcc).ToList(); var replyToAddresses = GetRecipients(mime.ReplyTo).ToList(); + // Prefer HTML body, fall back to plain text. + var (bodyContent, bodyType) = mime.HtmlBody != null + ? (mime.HtmlBody, BodyType.Html) + : (mime.TextBody ?? string.Empty, BodyType.Text); + var message = new Message() { Subject = mime.Subject, Importance = GetImportance(mime.Importance), - Body = new ItemBody() { ContentType = BodyType.Html, Content = mime.HtmlBody }, + Body = new ItemBody() { ContentType = bodyType, Content = bodyContent }, IsDraft = false, - IsRead = true, // Sent messages are always read. + IsRead = true, ToRecipients = toAddresses, CcRecipients = ccAddresses, BccRecipients = bccAddresses, From = fromAddress, - InternetMessageId = GetProperId(mime.MessageId), + InternetMessageId = mime.MessageId, ReplyTo = replyToAddresses, - Attachments = [] }; - // Headers are only included when creating the draft. - // When sending, they are not included. Graph will throw an error. + if (!string.IsNullOrEmpty(conversationId)) + { + message.ConversationId = conversationId; + } + // Headers are only included when creating the draft. + // Graph API throws an error if headers are included in send/patch operations. if (includeInternetHeaders) { message.InternetMessageHeaders = GetHeaderList(mime); } - return message; } - public static AccountCalendar AsCalendar(this Calendar outlookCalendar, MailAccount assignedAccount) + public static AccountCalendar AsCalendar(this Calendar outlookCalendar, MailAccount assignedAccount, string fallbackBackgroundColor = null) { var calendar = new AccountCalendar() { @@ -114,6 +183,7 @@ public static class OutlookIntegratorExtensions RemoteCalendarId = outlookCalendar.Id, IsPrimary = outlookCalendar.IsDefaultCalendar.GetValueOrDefault(), Name = outlookCalendar.Name, + IsSynchronizationEnabled = true, IsExtended = true, }; @@ -121,8 +191,8 @@ public static class OutlookIntegratorExtensions // Bg must be present. Generate flat one if doesn't exists. // Text doesnt exists for Outlook. - calendar.BackgroundColorHex = string.IsNullOrEmpty(outlookCalendar.HexColor) ? ColorHelpers.GenerateFlatColorHex() : outlookCalendar.HexColor; - calendar.TextColorHex = "#000000"; + calendar.BackgroundColorHex = fallbackBackgroundColor ?? ColorHelpers.GenerateFlatColorHex(); + calendar.TextColorHex = ColorHelpers.GetReadableTextColorHex(calendar.BackgroundColorHex); return calendar; } @@ -209,7 +279,9 @@ public static class OutlookIntegratorExtensions { if (recurrence.Range.Type == RecurrenceRangeType.EndDate && recurrence.Range.EndDate != null) { - ruleBuilder.Append($"UNTIL={recurrence.Range.EndDate.Value:yyyyMMddTHHmmssZ};"); + // RFC 5545 requires YYYYMMDD or YYYYMMDDTHHMMSSinvalid format (no dashes or colons) + var untilDate = recurrence.Range.EndDate.Value.DateTime.ToString("yyyyMMdd'T'HHmmss'Z'", System.Globalization.CultureInfo.InvariantCulture); + ruleBuilder.Append($"UNTIL={untilDate};"); } else if (recurrence.Range.Type == RecurrenceRangeType.Numbered && recurrence.Range.NumberOfOccurrences.HasValue) { @@ -223,23 +295,37 @@ public static class OutlookIntegratorExtensions public static DateTimeOffset GetDateTimeOffsetFromDateTimeTimeZone(DateTimeTimeZone dateTimeTimeZone) { - if (dateTimeTimeZone == null || string.IsNullOrEmpty(dateTimeTimeZone.DateTime) || string.IsNullOrEmpty(dateTimeTimeZone.TimeZone)) + if (dateTimeTimeZone == null || string.IsNullOrEmpty(dateTimeTimeZone.DateTime)) { - throw new ArgumentException("DateTimeTimeZone is null or empty."); + throw new ArgumentException("DateTimeTimeZone or DateTime is null or empty."); } try { // Parse the DateTime string - if (DateTime.TryParse(dateTimeTimeZone.DateTime, out DateTime parsedDateTime)) + if (!DateTime.TryParse(dateTimeTimeZone.DateTime, out DateTime parsedDateTime)) + { + throw new ArgumentException("DateTime string is not in a valid format."); + } + + // If no timezone is provided, assume UTC + if (string.IsNullOrEmpty(dateTimeTimeZone.TimeZone)) + { + return new DateTimeOffset(parsedDateTime, TimeSpan.Zero); + } + + try { // Get TimeZoneInfo to get the offset TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(dateTimeTimeZone.TimeZone); TimeSpan offset = timeZoneInfo.GetUtcOffset(parsedDateTime); return new DateTimeOffset(parsedDateTime, offset); } - else - throw new ArgumentException("DateTime string is not in a valid format."); + catch (TimeZoneNotFoundException) + { + // If timezone is not found, assume UTC as fallback + return new DateTimeOffset(parsedDateTime, TimeSpan.Zero); + } } catch (Exception) { @@ -247,6 +333,21 @@ public static class OutlookIntegratorExtensions } } + public static DateTime GetLocalDateTimeFromDateTimeTimeZone(DateTimeTimeZone dateTimeTimeZone) + { + if (dateTimeTimeZone == null || string.IsNullOrEmpty(dateTimeTimeZone.DateTime)) + { + throw new ArgumentException("DateTimeTimeZone or DateTime is null or empty."); + } + + if (!DateTime.TryParse(dateTimeTimeZone.DateTime, out DateTime parsedDateTime)) + { + throw new ArgumentException("DateTime string is not in a valid format."); + } + + return DateTime.SpecifyKind(parsedDateTime, DateTimeKind.Unspecified); + } + private static AttendeeStatus GetAttendeeStatus(ResponseType? responseType) { return responseType switch @@ -261,9 +362,12 @@ public static class OutlookIntegratorExtensions }; } - public static CalendarEventAttendee CreateAttendee(this Attendee attendee, Guid calendarItemId) + public static CalendarEventAttendee CreateAttendee(this Attendee attendee, Guid calendarItemId, string organizerEmail = null) { - bool isOrganizer = attendee?.Status?.Response == ResponseType.Organizer; + // Check if this attendee is the organizer by comparing email addresses + bool isOrganizer = !string.IsNullOrEmpty(organizerEmail) && + !string.IsNullOrEmpty(attendee?.EmailAddress?.Address) && + string.Equals(attendee.EmailAddress.Address, organizerEmail, StringComparison.OrdinalIgnoreCase); var eventAttendee = new CalendarEventAttendee() { @@ -281,6 +385,40 @@ public static class OutlookIntegratorExtensions #region Mime to Outlook Message Helpers + /// + /// Extracts all attachments (inline and regular) from a MimeMessage + /// and returns them as Graph SDK FileAttachment objects. + /// + public static List ExtractAttachments(this MimeMessage mime) + { + var attachments = new List(); + + foreach (var part in mime.BodyParts) + { + bool isInline = part.ContentDisposition?.Disposition == "inline"; + + if (!part.IsAttachment && !isInline) + continue; + + if (part is not MimePart mimePart || mimePart.Content == null) + continue; + + using var memory = new MemoryStream(); + mimePart.Content.DecodeTo(memory); + + attachments.Add(new FileAttachment() + { + Name = part.ContentDisposition?.FileName ?? part.ContentType.Name, + ContentBytes = memory.ToArray(), + ContentType = part.ContentType.MimeType, + ContentId = part.ContentId, + IsInline = isInline + }); + } + + return attachments; + } + private static IEnumerable GetRecipients(this InternetAddressList internetAddresses) { foreach (var address in internetAddresses) @@ -313,47 +451,47 @@ public static class OutlookIntegratorExtensions private static List GetHeaderList(this MimeMessage mime) { // Graph API only allows max of 5 headers. - // Here we'll try to ignore some headers that are not neccessary. - // Outlook API will generate them automatically. - - // Some headers also require to start with X- or x-. + // Graph only allows setting custom internet headers (typically X-*). + // Reply/threading headers like In-Reply-To and References are managed by + // createReply/createReplyAll flows and must not be sent here. + const int headerLimit = 5; string[] headersToIgnore = ["Date", "To", "Cc", "Bcc", "MIME-Version", "From", "Subject", "Message-Id"]; - string[] headersToModify = ["In-Reply-To", "Reply-To", "References", "Thread-Topic"]; var headers = new List(); - int includedHeaderCount = 0; + void AddHeader(string name, string value) + { + if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(value)) return; + if (headers.Count >= headerLimit) return; + if (headers.Any(h => string.Equals(h.Name, name, StringComparison.OrdinalIgnoreCase))) return; + // No header value should exceed 995 characters. + var headerValue = value.Length >= 995 ? value.Substring(0, 995) : value; + headers.Add(new InternetMessageHeader() { Name = name, Value = headerValue }); + } + + // PRIORITY: Always include WinoLocalDraftHeader first if it exists. + var winoDraftHeader = mime.Headers.FirstOrDefault(h => h.Field == Domain.Constants.WinoLocalDraftHeader); + if (winoDraftHeader != null) + AddHeader(winoDraftHeader.Field, winoDraftHeader.Value); + + // Fill remaining slots with custom headers only (avoid Graph restrictions). foreach (var header in mime.Headers) { - if (!headersToIgnore.Contains(header.Field)) - { - var headerName = headersToModify.Contains(header.Field) ? $"X-{header.Field}" : header.Field; + if (headers.Count >= headerLimit) break; + if (header.Field == Domain.Constants.WinoLocalDraftHeader) continue; + if (headersToIgnore.Contains(header.Field)) continue; - // No header value should exceed 995 characters. - var headerValue = header.Value.Length >= 995 ? header.Value.Substring(0, 995) : header.Value; + // Only include custom headers beyond the core threading ones. + if (!header.Field.StartsWith("X-", StringComparison.OrdinalIgnoreCase)) continue; - headers.Add(new InternetMessageHeader() { Name = headerName, Value = headerValue }); - includedHeaderCount++; - } - - if (includedHeaderCount >= 5) break; + AddHeader(header.Field, header.Value); } return headers; } - private static string GetProperId(string id) - { - // Outlook requires some identifiers to start with "X-" or "x-". - if (string.IsNullOrEmpty(id)) return string.Empty; - - if (!id.StartsWith("x-") || !id.StartsWith("X-")) - return $"X-{id}"; - - return id; - } #endregion diff --git a/Wino.Core/Helpers/CalendarEventComposeMapper.cs b/Wino.Core/Helpers/CalendarEventComposeMapper.cs new file mode 100644 index 00000000..2aa2211e --- /dev/null +++ b/Wino.Core/Helpers/CalendarEventComposeMapper.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models.Calendar; + +namespace Wino.Core.Helpers; + +public sealed record PreparedCalendarEventCreateModel( + CalendarItem CalendarItem, + List Attendees, + List Reminders); + +public static class CalendarEventComposeMapper +{ + public static PreparedCalendarEventCreateModel Prepare(CalendarEventComposeResult composeResult, AccountCalendar assignedCalendar, Guid? calendarItemId = null) + { + ArgumentNullException.ThrowIfNull(composeResult); + ArgumentNullException.ThrowIfNull(assignedCalendar); + + var itemId = calendarItemId ?? Guid.NewGuid(); + var effectiveTimeZoneId = string.IsNullOrWhiteSpace(composeResult.TimeZoneId) + ? TimeZoneInfo.Local.Id + : composeResult.TimeZoneId; + var utcNow = DateTimeOffset.UtcNow; + + var calendarItem = new CalendarItem + { + Id = itemId, + CalendarId = assignedCalendar.Id, + AssignedCalendar = assignedCalendar, + Title = composeResult.Title?.Trim() ?? string.Empty, + Description = composeResult.HtmlNotes ?? string.Empty, + Location = composeResult.Location?.Trim() ?? string.Empty, + StartDate = composeResult.StartDate, + DurationInSeconds = Math.Max(0, (composeResult.EndDate - composeResult.StartDate).TotalSeconds), + StartTimeZone = effectiveTimeZoneId, + EndTimeZone = effectiveTimeZoneId, + CreatedAt = utcNow, + UpdatedAt = utcNow, + Recurrence = composeResult.Recurrence ?? string.Empty, + OrganizerDisplayName = assignedCalendar.MailAccount?.SenderName ?? string.Empty, + OrganizerEmail = assignedCalendar.MailAccount?.Address ?? string.Empty, + Status = CalendarItemStatus.Accepted, + Visibility = CalendarItemVisibility.Public, + ShowAs = composeResult.ShowAs, + IsHidden = false, + IsLocked = false + }; + + var attendees = composeResult.Attendees? + .Where(attendee => attendee != null) + .Select(attendee => new CalendarEventAttendee + { + Id = attendee.Id == Guid.Empty ? Guid.NewGuid() : attendee.Id, + CalendarItemId = itemId, + Name = attendee.Name ?? string.Empty, + Email = attendee.Email ?? string.Empty, + Comment = attendee.Comment, + AttendenceStatus = attendee.AttendenceStatus, + IsOrganizer = attendee.IsOrganizer, + IsOptionalAttendee = attendee.IsOptionalAttendee, + ResolvedContact = attendee.ResolvedContact + }) + .ToList() ?? []; + + var reminders = composeResult.SelectedReminders? + .Where(reminder => reminder != null) + .Select(reminder => new Reminder + { + Id = reminder.Id == Guid.Empty ? Guid.NewGuid() : reminder.Id, + CalendarItemId = itemId, + DurationInSeconds = reminder.DurationInSeconds, + ReminderType = reminder.ReminderType + }) + .ToList() ?? []; + + return new PreparedCalendarEventCreateModel(calendarItem, attendees, reminders); + } +} diff --git a/Wino.Core/Helpers/CalendarRecurrenceMapper.cs b/Wino.Core/Helpers/CalendarRecurrenceMapper.cs new file mode 100644 index 00000000..aac775d0 --- /dev/null +++ b/Wino.Core/Helpers/CalendarRecurrenceMapper.cs @@ -0,0 +1,195 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Microsoft.Graph.Models; +using Microsoft.Kiota.Abstractions; +using Wino.Core.Domain; +using Wino.Core.Domain.Entities.Calendar; + +namespace Wino.Core.Helpers; + +public static class CalendarRecurrenceMapper +{ + public static PatternedRecurrence CreateOutlookRecurrence(CalendarItem calendarItem) + { + if (calendarItem == null || string.IsNullOrWhiteSpace(calendarItem.Recurrence)) + return null; + + var ruleLine = calendarItem.Recurrence + .Split(Domain.Constants.CalendarEventRecurrenceRuleSeperator, StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Trim()) + .FirstOrDefault(line => line.StartsWith("RRULE:", StringComparison.OrdinalIgnoreCase)); + + if (string.IsNullOrWhiteSpace(ruleLine)) + return null; + + var components = ruleLine["RRULE:".Length..] + .Split(';', StringSplitOptions.RemoveEmptyEntries) + .Select(part => part.Split('=', 2, StringSplitOptions.TrimEntries)) + .Where(parts => parts.Length == 2) + .ToDictionary(parts => parts[0].ToUpperInvariant(), parts => parts[1], StringComparer.OrdinalIgnoreCase); + + if (!components.TryGetValue("FREQ", out var frequency)) + return null; + + var pattern = new RecurrencePattern + { + Interval = ParseInt(components, "INTERVAL", 1), + FirstDayOfWeek = DayOfWeekObject.Monday + }; + + var byDays = ParseByDays(components); + var startDate = calendarItem.StartDate; + + switch (frequency.ToUpperInvariant()) + { + case "DAILY": + pattern.Type = RecurrencePatternType.Daily; + break; + case "WEEKLY": + pattern.Type = RecurrencePatternType.Weekly; + pattern.DaysOfWeek = byDays.Any() + ? byDays.Select(day => (DayOfWeekObject?)day).ToList() + : [(DayOfWeekObject?)MapDay(startDate.DayOfWeek)]; + break; + case "MONTHLY": + if (byDays.Any()) + { + pattern.Type = RecurrencePatternType.RelativeMonthly; + pattern.DaysOfWeek = byDays.Select(day => (DayOfWeekObject?)day).ToList(); + pattern.Index = MapWeekIndex(startDate); + } + else + { + pattern.Type = RecurrencePatternType.AbsoluteMonthly; + pattern.DayOfMonth = ParseInt(components, "BYMONTHDAY", startDate.Day); + } + break; + case "YEARLY": + pattern.Month = ParseInt(components, "BYMONTH", startDate.Month); + + if (byDays.Any()) + { + pattern.Type = RecurrencePatternType.RelativeYearly; + pattern.DaysOfWeek = byDays.Select(day => (DayOfWeekObject?)day).ToList(); + pattern.Index = MapWeekIndex(startDate); + } + else + { + pattern.Type = RecurrencePatternType.AbsoluteYearly; + pattern.DayOfMonth = ParseInt(components, "BYMONTHDAY", startDate.Day); + } + break; + default: + return null; + } + + var recurrenceRange = CreateRange(components, calendarItem); + return new PatternedRecurrence + { + Pattern = pattern, + Range = recurrenceRange + }; + } + + private static RecurrenceRange CreateRange(IReadOnlyDictionary components, CalendarItem calendarItem) + { + var startDate = CreateDate(calendarItem.StartDate); + + if (components.TryGetValue("UNTIL", out var untilValue) && + TryParseUntil(untilValue, out var untilDate)) + { + return new RecurrenceRange + { + Type = RecurrenceRangeType.EndDate, + StartDate = startDate, + EndDate = CreateDate(untilDate), + RecurrenceTimeZone = calendarItem.StartTimeZone + }; + } + + return new RecurrenceRange + { + Type = RecurrenceRangeType.NoEnd, + StartDate = startDate, + RecurrenceTimeZone = calendarItem.StartTimeZone + }; + } + + private static bool TryParseUntil(string untilValue, out DateTime untilDate) + { + untilDate = default; + + if (string.IsNullOrWhiteSpace(untilValue)) + return false; + + return DateTime.TryParseExact( + untilValue, + ["yyyyMMdd", "yyyyMMdd'T'HHmmss", "yyyyMMdd'T'HHmmss'Z'"], + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, + out untilDate) + || DateTime.TryParse(untilValue, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out untilDate); + } + + private static List ParseByDays(IReadOnlyDictionary components) + { + if (!components.TryGetValue("BYDAY", out var byDayValue) || string.IsNullOrWhiteSpace(byDayValue)) + return []; + + return byDayValue + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(MapDay) + .ToList(); + } + + private static int ParseInt(IReadOnlyDictionary components, string key, int fallback) + => components.TryGetValue(key, out var value) && int.TryParse(value, out var parsedValue) ? parsedValue : fallback; + + private static DayOfWeekObject MapDay(string dayToken) + { + return dayToken.ToUpperInvariant() switch + { + "MO" => DayOfWeekObject.Monday, + "TU" => DayOfWeekObject.Tuesday, + "WE" => DayOfWeekObject.Wednesday, + "TH" => DayOfWeekObject.Thursday, + "FR" => DayOfWeekObject.Friday, + "SA" => DayOfWeekObject.Saturday, + "SU" => DayOfWeekObject.Sunday, + _ => throw new ArgumentOutOfRangeException(nameof(dayToken), dayToken, null) + }; + } + + private static DayOfWeekObject MapDay(DayOfWeek dayOfWeek) + { + return dayOfWeek switch + { + DayOfWeek.Monday => DayOfWeekObject.Monday, + DayOfWeek.Tuesday => DayOfWeekObject.Tuesday, + DayOfWeek.Wednesday => DayOfWeekObject.Wednesday, + DayOfWeek.Thursday => DayOfWeekObject.Thursday, + DayOfWeek.Friday => DayOfWeekObject.Friday, + DayOfWeek.Saturday => DayOfWeekObject.Saturday, + DayOfWeek.Sunday => DayOfWeekObject.Sunday, + _ => DayOfWeekObject.Monday + }; + } + + private static WeekIndex MapWeekIndex(DateTime date) + { + var occurrence = ((date.Day - 1) / 7) + 1; + + return occurrence switch + { + 1 => WeekIndex.First, + 2 => WeekIndex.Second, + 3 => WeekIndex.Third, + 4 => WeekIndex.Fourth, + _ => WeekIndex.Last + }; + } + + private static Date CreateDate(DateTime dateTime) => new(dateTime.Year, dateTime.Month, dateTime.Day); +} diff --git a/Wino.Core/Helpers/SynchronizationActionHelper.cs b/Wino.Core/Helpers/SynchronizationActionHelper.cs new file mode 100644 index 00000000..1646a507 --- /dev/null +++ b/Wino.Core/Helpers/SynchronizationActionHelper.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Wino.Core.Domain; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Synchronization; +using Wino.Core.Requests.Calendar; +using Wino.Core.Requests.Folder; +using Wino.Core.Requests.Mail; + +namespace Wino.Core.Helpers; + +/// +/// Converts queued synchronization requests into user-facing action descriptions. +/// +public static class SynchronizationActionHelper +{ + public static List CreateActionItems( + IEnumerable requests, Guid accountId, string accountName) + { + var items = new List(); + + // Group mail action requests by operation + var mailRequests = requests.OfType(); + var mailGroups = mailRequests.GroupBy(r => GetMailActionKey(r)); + + foreach (var group in mailGroups) + { + var description = GetMailActionDescription(group.Key, group.ToList()); + + if (description != null) + { + items.Add(new SynchronizationActionItem + { + AccountId = accountId, + AccountName = accountName, + Description = description + }); + } + } + + // Handle folder action requests individually + var folderRequests = requests.OfType(); + foreach (var folderRequest in folderRequests) + { + var description = GetFolderActionDescription(folderRequest); + + if (description != null) + { + items.Add(new SynchronizationActionItem + { + AccountId = accountId, + AccountName = accountName, + Description = description + }); + } + } + + var calendarRequests = requests.OfType(); + foreach (var calendarRequest in calendarRequests) + { + var description = GetCalendarActionDescription(calendarRequest); + + if (description != null) + { + items.Add(new SynchronizationActionItem + { + AccountId = accountId, + AccountName = accountName, + Description = description + }); + } + } + + return items; + } + + /// + /// Returns a key that differentiates MarkRead vs MarkUnread, Flag vs Unflag, Archive vs Unarchive. + /// + private static string GetMailActionKey(IMailActionRequest request) + { + return request switch + { + MarkReadRequest r => r.IsRead ? "MarkRead" : "MarkUnread", + ChangeFlagRequest r => r.IsFlagged ? "SetFlag" : "ClearFlag", + ArchiveRequest r => r.IsArchiving ? "Archive" : "Unarchive", + _ => request.Operation.ToString() + }; + } + + private static string GetMailActionDescription(string actionKey, List requests) + { + int count = requests.Count; + + return actionKey switch + { + "MarkRead" => string.Format(Translator.SyncAction_MarkingAsRead, count), + "MarkUnread" => string.Format(Translator.SyncAction_MarkingAsUnread, count), + "Delete" => string.Format(Translator.SyncAction_Deleting, count), + "Move" => string.Format(Translator.SyncAction_Moving, count), + "Archive" => string.Format(Translator.SyncAction_Archiving, count), + "Unarchive" => string.Format(Translator.SyncAction_Unarchiving, count), + "SetFlag" => string.Format(Translator.SyncAction_SettingFlag, count), + "ClearFlag" => string.Format(Translator.SyncAction_ClearingFlag, count), + "CreateDraft" => Translator.SyncAction_CreatingDraft, + "Send" => Translator.SyncAction_SendingMail, + "MoveToFocused" => string.Format(Translator.SyncAction_MovingToFocused, count), + "AlwaysMoveTo" => string.Format(Translator.SyncAction_Moving, count), + _ => null + }; + } + + private static string GetFolderActionDescription(IFolderActionRequest request) + { + return request switch + { + RenameFolderRequest => Translator.SyncAction_RenamingFolder, + EmptyFolderRequest => Translator.SyncAction_EmptyingFolder, + MarkFolderAsReadRequest => Translator.SyncAction_MarkingFolderAsRead, + DeleteFolderRequest => Translator.FolderOperation_Delete, + CreateSubFolderRequest => Translator.FolderOperation_CreateSubFolder, + _ => null + }; + } + + private static string GetCalendarActionDescription(ICalendarActionRequest request) + { + return request switch + { + CreateCalendarEventRequest => Translator.SyncAction_CreatingEvent, + _ => null + }; + } +} diff --git a/Wino.Core/Http/GraphRateLimitHandler.cs b/Wino.Core/Http/GraphRateLimitHandler.cs new file mode 100644 index 00000000..7503b701 --- /dev/null +++ b/Wino.Core/Http/GraphRateLimitHandler.cs @@ -0,0 +1,147 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Serilog; + +namespace Wino.Core.Http; + +/// +/// DelegatingHandler that automatically handles Microsoft Graph API 429 rate limiting responses. +/// Integrates directly with the Graph SDK HTTP pipeline to provide transparent retry functionality. +/// +/// Features: +/// - Intercepts 429 (Too Many Requests) HTTP responses before they become ServiceExceptions +/// - Respects Retry-After header from responses (both seconds and HTTP date formats) +/// - Maximum 3 retry attempts to prevent infinite loops +/// - Caps retry delays to 5 minutes maximum +/// - Uses 60-second default delay if no Retry-After header is provided +/// - Comprehensive logging for debugging and monitoring +/// - Thread-safe and cancellation token aware +/// - Integrates seamlessly with existing Graph SDK error handling +/// +/// Usage: +/// Add to GraphServiceClient handlers in OutlookSynchronizer constructor: +/// +/// var handlers = GraphClientFactory.CreateDefaultHandlers(); +/// handlers.Add(new MicrosoftImmutableIdHandler()); +/// handlers.Add(new GraphRateLimitHandler()); +/// var httpClient = GraphClientFactory.Create(handlers); +/// +public class GraphRateLimitHandler : DelegatingHandler +{ + private static readonly ILogger _logger = Log.ForContext(); + private const int MaxRetryAttempts = 3; + private const int MaxDelaySeconds = 300; // 5 minutes cap + private const int DefaultDelaySeconds = 60; // Default delay when no Retry-After header + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var attempt = 0; + + while (attempt <= MaxRetryAttempts) + { + HttpResponseMessage response; + + try + { + response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.Error(ex, "Error sending request to {Uri} on attempt {Attempt}", request.RequestUri, attempt + 1); + throw; + } + + // Check if we got a 429 Too Many Requests response + if (response.StatusCode == HttpStatusCode.TooManyRequests) + { + if (attempt == MaxRetryAttempts) + { + _logger.Warning("Max retry attempts ({MaxAttempts}) reached for rate limited request to {Uri}", + MaxRetryAttempts, request.RequestUri); + return response; // Return the 429 response after max attempts + } + + // Get the Retry-After header value + var retryAfterSeconds = GetRetryAfterSeconds(response); + + if (retryAfterSeconds > 0) + { + // Cap the delay to a reasonable maximum + var cappedDelay = Math.Min(retryAfterSeconds, MaxDelaySeconds); + + _logger.Information("Rate limited (429) - waiting {RetrySeconds} seconds before retry attempt {Attempt}/{MaxAttempts} for {Uri}", + cappedDelay, attempt + 1, MaxRetryAttempts, request.RequestUri); + + await Task.Delay(TimeSpan.FromSeconds(cappedDelay), cancellationToken).ConfigureAwait(false); + } + else + { + _logger.Warning("Rate limited (429) but no valid Retry-After header found for {Uri} - using default {DefaultDelay} second delay", + request.RequestUri, DefaultDelaySeconds); + + // Use a default delay if no Retry-After header is provided + await Task.Delay(TimeSpan.FromSeconds(DefaultDelaySeconds), cancellationToken).ConfigureAwait(false); + } + + attempt++; + response.Dispose(); // Dispose the 429 response before retry + continue; + } + + // Success or other error - return the response + return response; + } + + // This should never be reached, but just in case + throw new InvalidOperationException("Rate limiting retry logic error"); + } + + /// + /// Extracts the retry delay from the Retry-After header. + /// Supports both seconds (integer) and HTTP date formats. + /// + /// The HTTP response containing Retry-After header + /// Number of seconds to wait, or 0 if header is missing or invalid + private int GetRetryAfterSeconds(HttpResponseMessage response) + { + try + { + // Check if Retry-After header exists + if (response.Headers.RetryAfter == null) + { + _logger.Debug("No Retry-After header found in response"); + return 0; + } + + // Handle retry-after-seconds (integer) + if (response.Headers.RetryAfter.Delta.HasValue) + { + var seconds = (int)response.Headers.RetryAfter.Delta.Value.TotalSeconds; + _logger.Debug("Found Retry-After delta: {Seconds} seconds", seconds); + return seconds; + } + + // Handle retry-after-date (HTTP date) + if (response.Headers.RetryAfter.Date.HasValue) + { + var retryAfterTime = response.Headers.RetryAfter.Date.Value; + var delaySeconds = (int)(retryAfterTime - DateTimeOffset.UtcNow).TotalSeconds; + _logger.Debug("Found Retry-After date: {Date}, calculated delay: {Seconds} seconds", retryAfterTime, delaySeconds); + + // Ensure we don't have a negative delay + return Math.Max(0, delaySeconds); + } + + _logger.Debug("Retry-After header present but no valid value found"); + return 0; + } + catch (Exception ex) + { + _logger.Warning(ex, "Error parsing Retry-After header"); + return 0; + } + } +} \ No newline at end of file diff --git a/Wino.Core/Integration/ImapClientPool.cs b/Wino.Core/Integration/ImapClientPool.cs index 683c33aa..1c0c997d 100644 --- a/Wino.Core/Integration/ImapClientPool.cs +++ b/Wino.Core/Integration/ImapClientPool.cs @@ -1,18 +1,18 @@ -using System; +using System; using System.Collections.Concurrent; -using System.IO; +using System.Linq; using System.Net; using System.Net.Security; +using System.Reflection; using System.Security.Cryptography.X509Certificates; -using System.Text; using System.Threading; +using System.Threading.Channels; using System.Threading.Tasks; -using System.Timers; +using MailKit; using MailKit.Net.Imap; using MailKit.Net.Proxy; using MailKit.Security; using MimeKit.Cryptography; -using MoreLinq; using Serilog; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; @@ -20,401 +20,749 @@ using Wino.Core.Domain.Exceptions; using Wino.Core.Domain.Models.Connectivity; namespace Wino.Core.Integration; + /// -/// Provides a pooling mechanism for ImapClient. -/// Makes sure that we don't have too many connections to the server. -/// Rents a connected & authenticated client from the pool all the time. +/// Connection state for tracking individual client health. +/// +public enum ImapClientState +{ + Available, + InUse, + Idle, + Reconnecting, + Failed, + Disposed +} + +/// +/// Provides an enhanced pooling mechanism for ImapClient with Channel-based async rental. +/// Maintains minimum active connections and a dedicated IDLE client. /// -/// Connection/Authentication info to be used to configure ImapClient. public class ImapClientPool : IDisposable { - // Hardcoded implementation details for ID extension if the server supports. - // Some providers like Chinese 126 require Id to be sent before authentication. - // We don't expose any customer data here. Therefore it's safe for now. - // Later on maybe we can make it configurable and leave it to the user with passing - // real implementation details. - private readonly ImapImplementation _implementation = new() - { - Version = "1.8.0", - OS = "Windows", - Vendor = "Wino", - SupportUrl = "https://www.winomail.app", - Name = "Wino Mail User", - }; + private const int DefaultAcquireTimeoutMs = 45_000; + private const int KeepAliveIntervalMs = 4 * 60 * 1000; + private const int MaintenanceIntervalMs = 60 * 1000; + + private readonly ILogger _logger = Log.ForContext(); + private readonly CustomServerInformation _customServerInformation; + private readonly ConcurrentDictionary _clientStates = new(); + private readonly Channel _availableClients; + private readonly CancellationTokenSource _maintenanceCts = new(); + private readonly SemaphoreSlim _initializeSemaphore = new(1, 1); + private readonly object _idleClientLock = new(); + private readonly object _initialWarmupLock = new(); + private readonly ImapServerQuirkProfile _quirks; + private readonly ImapImplementation _implementation; + private readonly int _maxConnections; + private readonly int _targetMinimumConnections; + + private DateTime _lastKeepAliveSentUtc = DateTime.MinValue; + private WinoImapClient _dedicatedIdleClient; + private bool _disposedValue; + private bool _initialized; + private Task _maintenanceTask; + private Task _initialWarmupTask = Task.CompletedTask; public bool ThrowOnSSLHandshakeCallback { get; set; } public ImapClientPoolOptions ImapClientPoolOptions { get; } - private bool _disposedValue; - private readonly int MinimumPoolSize = 5; - private readonly ConcurrentStack _clients = new(); - private readonly SemaphoreSlim _semaphore; - private readonly CustomServerInformation _customServerInformation; - private readonly Stream _protocolLogStream; - private readonly ILogger _logger = Log.ForContext(); - private readonly System.Timers.Timer _keepAliveTimer; - private readonly System.Timers.Timer _connectionMonitorTimer; - - private const int KeepAliveInterval = 4 * 60 * 1000; // 4 minutes - private const int ConnectionMonitorInterval = 30 * 1000; // 30 seconds + /// + /// Gets the current health status of the connection pool. + /// + public ConnectionPoolHealth Health => GetHealthInternal(); public ImapClientPool(ImapClientPoolOptions imapClientPoolOptions) { _customServerInformation = imapClientPoolOptions.ServerInformation; - _protocolLogStream = imapClientPoolOptions.ProtocolLog; - - // Set the maximum pool size to 5 or the custom value if it's greater. - _semaphore = new(Math.Max(MinimumPoolSize, _customServerInformation.MaxConcurrentClients)); - - CryptographyContext.Register(typeof(WindowsSecureMimeContext)); ImapClientPoolOptions = imapClientPoolOptions; - _keepAliveTimer = new System.Timers.Timer(KeepAliveInterval); - _connectionMonitorTimer = new System.Timers.Timer(ConnectionMonitorInterval); + _quirks = ImapServerQuirks.Resolve(_customServerInformation.IncomingServer); - _keepAliveTimer.Elapsed += KeepAliveTimerElapsed; - _connectionMonitorTimer.Elapsed += ConnectionMonitorTimerElapsed; + // Keep connection counts conservative by default and always cap by provider limits. + _maxConnections = CalculateMaxConnections(_customServerInformation.MaxConcurrentClients); + _targetMinimumConnections = CalculateTargetMinimumConnections(_maxConnections, _quirks.UseConservativeConnections); + + _implementation = CreateImplementation(); + + CryptographyContext.Register(typeof(WindowsSecureMimeContext)); + + _availableClients = Channel.CreateUnbounded(new UnboundedChannelOptions + { + SingleReader = false, + SingleWriter = false, + AllowSynchronousContinuations = false + }); } - public async Task PreWarmPoolAsync() + /// + /// Initializes the pool by creating minimum connections and starting maintenance. + /// + public async Task InitializeAsync(CancellationToken cancellationToken = default) { + if (_initialized) return; + + await _initializeSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + try { - for (int i = 0; i < MinimumPoolSize; i++) + if (_initialized) return; + + _logger.Information("Initializing IMAP client pool with {MinimumConnections} minimum active connections (max: {MaxConnections})", _targetMinimumConnections, _maxConnections); + + // Fast-path startup: create one client eagerly so first RentAsync() is not blocked by full warm-up. + var initialClient = await CreateAndConnectClientAsync(cancellationToken).ConfigureAwait(false); + if (initialClient == null) { - var client = CreateNewClient(); - await EnsureCapabilitiesAsync(client, true); - _clients.Push(client); + throw CreatePoolException("Failed to create initial IMAP connection for the pool."); } - // Start monitoring timers after pool is warmed - _keepAliveTimer.Start(); - _connectionMonitorTimer.Start(); + _clientStates[initialClient] = ImapClientState.Available; + await _availableClients.Writer.WriteAsync(initialClient, cancellationToken).ConfigureAwait(false); + + _maintenanceTask = Task.Run(() => MaintenanceLoopAsync(_maintenanceCts.Token), _maintenanceCts.Token); + _initialized = true; + + ScheduleInitialWarmup(); + _logger.Information("IMAP client pool initialized. Health: {Health}", Health.Summary); } catch (Exception ex) { - _logger.Error(ex, "Failed to pre-warm client pool"); + _logger.Error(ex, "Failed to initialize IMAP client pool"); + throw CreatePoolException("IMAP client pool initialization failed.", ex); } - } - - private async void KeepAliveTimerElapsed(object sender, ElapsedEventArgs e) - { - foreach (var client in _clients) + finally { - try - { - if (client.IsConnected && !((WinoImapClient)client).IsBusy()) - { - await SendNoOpAsync(client); - } - } - catch (Exception ex) - { - _logger.Warning(ex, "Failed to send NOOP to client"); - } - } - } - - private async void ConnectionMonitorTimerElapsed(object sender, ElapsedEventArgs e) - { - foreach (var client in _clients) - { - try - { - if (!client.IsConnected && !((WinoImapClient)client).IsBusy()) - { - await EnsureCapabilitiesAsync(client, false); - } - } - catch (Exception ex) - { - _logger.Warning(ex, "Failed to reconnect client"); - } - } - } - - private async Task SendNoOpAsync(IImapClient client) - { - try - { - await client.NoOpAsync(); - } - catch (Exception ex) - { - _logger.Warning(ex, "NOOP command failed"); + _initializeSemaphore.Release(); } } /// - /// Ensures all supported capabilities are enabled in this connection. - /// Reconnects and reauthenticates if necessary. - /// - /// Whether the client has been newly created. - private async Task EnsureCapabilitiesAsync(IImapClient client, bool isCreatedNew) + /// Pre-warms the pool (legacy compatibility method). + /// + public async Task PreWarmPoolAsync() + { + await InitializeAsync(CancellationToken.None).ConfigureAwait(false); + + Task warmupTask; + lock (_initialWarmupLock) + { + warmupTask = _initialWarmupTask; + } + + if (warmupTask != null) + { + await warmupTask.ConfigureAwait(false); + } + } + + /// + /// Rents a client from the pool with the default timeout. + /// + public Task RentAsync(CancellationToken cancellationToken = default) + => RentAsync(TimeSpan.FromMilliseconds(DefaultAcquireTimeoutMs), cancellationToken); + + /// + /// Rents a client from the pool with explicit timeout and cancellation. + /// + public async Task RentAsync(TimeSpan timeout, CancellationToken cancellationToken = default) + { + if (!_initialized) + await InitializeAsync(cancellationToken).ConfigureAwait(false); + + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + linkedCts.CancelAfter(timeout); + var token = linkedCts.Token; + + int createFailures = 0; + + try + { + while (!token.IsCancellationRequested) + { + if (_availableClients.Reader.TryRead(out var pooledClient)) + { + if (pooledClient != null && _clientStates.TryGetValue(pooledClient, out var state) && state == ImapClientState.Available) + { + try + { + await EnsureClientReadyAsync(pooledClient, token).ConfigureAwait(false); + _clientStates[pooledClient] = ImapClientState.InUse; + return pooledClient; + } + catch (Exception ex) + { + _logger.Warning(ex, "Pooled IMAP client was not ready. Marking as failed."); + MarkClientAsFailed(pooledClient); + } + } + } + + if (CanCreateAdditionalConnection()) + { + var newClient = await CreateAndConnectClientAsync(token).ConfigureAwait(false); + if (newClient != null) + { + _clientStates[newClient] = ImapClientState.InUse; + return newClient; + } + + createFailures++; + } + + await Task.Delay(150, token).ConfigureAwait(false); + } + } + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + throw CreatePoolException($"Timed out while acquiring an IMAP client after {timeout.TotalSeconds:F1} seconds. Failures: {createFailures}."); + } + + throw cancellationToken.IsCancellationRequested + ? new OperationCanceledException(cancellationToken) + : CreatePoolException($"Failed to acquire IMAP client within {timeout.TotalSeconds:F1} seconds. Failures: {createFailures}."); + } + + /// + /// Gets a client from the pool (legacy compatibility method). + /// + public Task GetClientAsync() + => GetClientAsync(CancellationToken.None, null); + + /// + /// Gets a client from the pool with explicit cancellation and timeout control. + /// + public async Task GetClientAsync(CancellationToken cancellationToken, TimeSpan? timeout = null) + => await RentAsync(timeout ?? TimeSpan.FromMilliseconds(DefaultAcquireTimeoutMs), cancellationToken).ConfigureAwait(false); + + /// + /// Returns a client to the pool. + /// + public void Return(WinoImapClient client, bool isFaulted = false) + { + if (client == null || _disposedValue) + { + if (client != null) + DisposeClient(client); + return; + } + + if (isFaulted || !client.IsConnected) + { + MarkClientAsFailed(client); + return; + } + + _clientStates[client] = ImapClientState.Available; + _availableClients.Writer.TryWrite(client); + } + + /// + /// Releases a client (legacy compatibility method). + /// + public void Release(IImapClient item, bool destroyClient = false) + { + if (item is WinoImapClient winoClient) + { + Return(winoClient, destroyClient); + } + else if (item != null) + { + DisposeClient(item); + } + } + + /// + /// Gets the dedicated IDLE client. Creates one if not available. + /// + public async Task GetIdleClientAsync(CancellationToken cancellationToken = default) + { + lock (_idleClientLock) + { + if (_dedicatedIdleClient != null && _dedicatedIdleClient.IsConnected) + { + return _dedicatedIdleClient; + } + } + + if (!CanCreateAdditionalConnection()) + { + _logger.Warning("Unable to allocate a dedicated IDLE client because pool is at max capacity ({MaxConnections}).", _maxConnections); + return null; + } + + var idleClient = await CreateAndConnectClientAsync(cancellationToken).ConfigureAwait(false); + if (idleClient == null) + return null; + + lock (_idleClientLock) + { + if (_dedicatedIdleClient != null) + { + MarkClientAsFailed(_dedicatedIdleClient); + } + + _dedicatedIdleClient = idleClient; + _clientStates[idleClient] = ImapClientState.Idle; + } + + return idleClient; + } + + /// + /// Releases the IDLE client for reconnection. + /// + public void ReleaseIdleClient(bool isFaulted = false) + { + lock (_idleClientLock) + { + if (_dedicatedIdleClient == null) + return; + + if (isFaulted || !_dedicatedIdleClient.IsConnected) + { + MarkClientAsFailed(_dedicatedIdleClient); + _dedicatedIdleClient = null; + return; + } + + _clientStates[_dedicatedIdleClient] = ImapClientState.Idle; + } + } + + private ConnectionPoolHealth GetHealthInternal() + { + var health = new ConnectionPoolHealth + { + LastHealthCheck = DateTime.UtcNow, + IdleConnectionActive = _dedicatedIdleClient?.IsConnected ?? false + }; + + foreach (var kvp in _clientStates) + { + health.TotalConnections++; + switch (kvp.Value) + { + case ImapClientState.Available: + health.AvailableConnections++; + break; + case ImapClientState.InUse: + health.InUseConnections++; + break; + case ImapClientState.Failed: + health.FailedConnections++; + break; + case ImapClientState.Reconnecting: + health.ReconnectingConnections++; + break; + } + } + + return health; + } + + private async Task MaintenanceLoopAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + try + { + await Task.Delay(MaintenanceIntervalMs, cancellationToken).ConfigureAwait(false); + + var keepAliveElapsedMs = (DateTime.UtcNow - _lastKeepAliveSentUtc).TotalMilliseconds; + if (keepAliveElapsedMs >= KeepAliveIntervalMs) + { + await SendNoOpToAvailableClientsAsync(cancellationToken).ConfigureAwait(false); + _lastKeepAliveSentUtc = DateTime.UtcNow; + } + + await EnsureMinimumConnectionsAsync(cancellationToken).ConfigureAwait(false); + await CleanupFailedConnectionsAsync().ConfigureAwait(false); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + break; + } + catch (Exception ex) + { + _logger.Warning(ex, "Error in pool maintenance loop"); + } + } + } + + private async Task SendNoOpToAvailableClientsAsync(CancellationToken cancellationToken) + { + foreach (var kvp in _clientStates) + { + if (kvp.Value != ImapClientState.Available) + continue; + + if (!kvp.Key.IsConnected || kvp.Key.IsBusy()) + continue; + + try + { + await kvp.Key.NoOpAsync(cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.Debug(ex, "NOOP failed for pooled client. Marking as failed."); + MarkClientAsFailed(kvp.Key); + } + } + } + + private async Task EnsureMinimumConnectionsAsync(CancellationToken cancellationToken) + { + var availableConnections = _clientStates.Count(kvp => kvp.Value == ImapClientState.Available); + var neededConnections = _targetMinimumConnections - availableConnections; + + if (neededConnections <= 0) + return; + + for (int i = 0; i < neededConnections; i++) + { + if (!CanCreateAdditionalConnection()) + break; + + try + { + var client = await CreateAndConnectClientAsync(cancellationToken).ConfigureAwait(false); + if (client == null) + continue; + + _clientStates[client] = ImapClientState.Available; + await _availableClients.Writer.WriteAsync(client, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.Warning(ex, "Failed to create minimum pool connection during maintenance."); + break; + } + } + } + + private void ScheduleInitialWarmup() + { + lock (_initialWarmupLock) + { + if (_initialWarmupTask != null && !_initialWarmupTask.IsCompleted) + return; + + _initialWarmupTask = Task.Run(() => EnsureWarmBaselineAsync(_maintenanceCts.Token), _maintenanceCts.Token); + } + } + + private async Task EnsureWarmBaselineAsync(CancellationToken cancellationToken) { try { - bool isReconnected = await EnsureConnectedAsync(client); - bool mustDoPostAuthIdentification = false; + await EnsureMinimumConnectionsAsync(cancellationToken).ConfigureAwait(false); - if ((isCreatedNew || isReconnected) && client.IsConnected) + lock (_idleClientLock) { - if (client.Capabilities.HasFlag(ImapCapabilities.Compress)) - await client.CompressAsync(); + if (_dedicatedIdleClient != null && _dedicatedIdleClient.IsConnected) + return; + } - // Identify if the server supports ID extension. - // Some servers require it pre-authentication, some post-authentication. - // We'll observe the response here and do it after authentication if needed. + if (!CanCreateAdditionalConnection()) + return; - if (client.Capabilities.HasFlag(ImapCapabilities.Id)) + var idleCandidate = await CreateAndConnectClientAsync(cancellationToken).ConfigureAwait(false); + if (idleCandidate == null) + return; + + bool assignedAsIdle = false; + lock (_idleClientLock) + { + if (_dedicatedIdleClient == null || !_dedicatedIdleClient.IsConnected) { - try - { - await client.IdentifyAsync(_implementation); - } - catch (ImapCommandException commandException) when (commandException.Response == ImapCommandResponse.No || commandException.Response == ImapCommandResponse.Bad) - { - mustDoPostAuthIdentification = true; - } - catch (Exception) - { - throw; - } + _dedicatedIdleClient = idleCandidate; + _clientStates[idleCandidate] = ImapClientState.Idle; + assignedAsIdle = true; } } - await EnsureAuthenticatedAsync(client); - - if ((isCreatedNew || isReconnected) && client.IsAuthenticated) + if (!assignedAsIdle) { - if (mustDoPostAuthIdentification) await client.IdentifyAsync(_implementation); - - // Activate post-auth capabilities. - if (client.Capabilities.HasFlag(ImapCapabilities.QuickResync)) - { - await client.EnableQuickResyncAsync().ConfigureAwait(false); - if (client is WinoImapClient winoImapClient) winoImapClient.IsQResyncEnabled = true; - } + _clientStates[idleCandidate] = ImapClientState.Available; + _availableClients.Writer.TryWrite(idleCandidate); } } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // Pool is shutting down. + } catch (Exception ex) { - if (ex.InnerException is ImapTestSSLCertificateException imapTestSSLCertificateException) - throw imapTestSSLCertificateException; - - throw new ImapClientPoolException(ex, GetProtocolLogContent()); - } - finally - { - // Release it even if it fails. - _semaphore.Release(); + _logger.Warning(ex, "Initial IMAP pool warm-up failed. Pool will continue with maintenance recovery."); } } - public string GetProtocolLogContent() + private Task CleanupFailedConnectionsAsync() { - if (_protocolLogStream == null) return default; - - // Set the position to the beginning of the stream in case it is not already at the start - if (_protocolLogStream.CanSeek) - _protocolLogStream.Seek(0, SeekOrigin.Begin); - - using var reader = new StreamReader(_protocolLogStream, Encoding.UTF8, true, 1024, leaveOpen: true); - return reader.ReadToEnd(); - } - - public async Task GetClientAsync() - { - await _semaphore.WaitAsync(); - - if (_clients.TryPop(out IImapClient item)) + foreach (var kvp in _clientStates) { - await EnsureCapabilitiesAsync(item, false); + if (kvp.Value != ImapClientState.Failed && kvp.Value != ImapClientState.Disposed) + continue; - return item; + DisposeClient(kvp.Key); + _clientStates.TryRemove(kvp.Key, out _); } + return Task.CompletedTask; + } + + private async Task CreateAndConnectClientAsync(CancellationToken cancellationToken) + { var client = CreateNewClient(); - await EnsureCapabilitiesAsync(client, true); - - return client; + try + { + await EnsureClientReadyAsync(client, cancellationToken).ConfigureAwait(false); + return client; + } + catch (Exception ex) + { + _logger.Warning(ex, "Failed to create and connect IMAP client."); + DisposeClient(client); + return null; + } } - public void Release(IImapClient item, bool destroyClient = false) + private async Task EnsureClientReadyAsync(WinoImapClient client, CancellationToken cancellationToken) { - if (item != null) + if (!client.IsConnected) { - if (destroyClient) + client.ServerCertificateValidationCallback = MyServerCertificateValidationCallback; + + await client.ConnectAsync( + _customServerInformation.IncomingServer, + int.Parse(_customServerInformation.IncomingServerPort), + GetSocketOptions(_customServerInformation.IncomingServerSocketOption), + cancellationToken).ConfigureAwait(false); + + if (client.Capabilities.HasFlag(ImapCapabilities.Compress)) { - if (item.IsConnected) + try { - lock (item.SyncRoot) - { - item.Disconnect(quit: true); - } + await client.CompressAsync(cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.Debug(ex, "Failed to enable IMAP compression. Continuing without compression."); } - item.Dispose(); - } - else if (!_disposedValue) - { - _clients.Push(item); } - _semaphore.Release(); - } - } - - private IImapClient CreateNewClient() - { - WinoImapClient client = null; - - client = new WinoImapClient(); - - HttpProxyClient proxyClient = null; - - // Add proxy client if exists. - if (!string.IsNullOrEmpty(_customServerInformation.ProxyServer)) - { - proxyClient = new HttpProxyClient(_customServerInformation.ProxyServer, int.Parse(_customServerInformation.ProxyServerPort)); - client.ProxyClient = proxyClient; + await TryIdentifyAsync(client, cancellationToken).ConfigureAwait(false); } - _logger.Debug("Creating new ImapClient. Current clients: {Count}", _clients.Count); - - return client; - } - - private SecureSocketOptions GetSocketOptions(ImapConnectionSecurity connectionSecurity) - => connectionSecurity switch + if (!client.IsAuthenticated) { - ImapConnectionSecurity.Auto => SecureSocketOptions.Auto, - ImapConnectionSecurity.None => SecureSocketOptions.None, - ImapConnectionSecurity.StartTls => SecureSocketOptions.StartTlsWhenAvailable, - ImapConnectionSecurity.SslTls => SecureSocketOptions.SslOnConnect, - _ => SecureSocketOptions.None - }; + var cred = new NetworkCredential( + _customServerInformation.IncomingServerUsername, + _customServerInformation.IncomingServerPassword); - /// True if the connection is newly established. - public async Task EnsureConnectedAsync(IImapClient client) - { - if (client.IsConnected) return false; + var authMethod = _customServerInformation.IncomingAuthenticationMethod; - client.ServerCertificateValidationCallback = MyServerCertificateValidationCallback; - - await client.ConnectAsync(_customServerInformation.IncomingServer, - int.Parse(_customServerInformation.IncomingServerPort), - GetSocketOptions(_customServerInformation.IncomingServerSocketOption)); - - // Print out useful information for testing. - if (client.IsConnected && ImapClientPoolOptions.IsTestPool) - { - // Print supported authentication methods for the client. - var supportedAuthMethods = client.AuthenticationMechanisms; - - if (supportedAuthMethods == null || supportedAuthMethods.Count == 0) + if (authMethod != ImapAuthenticationMethod.Auto) { - WriteToProtocolLog("There are no supported authentication mechanisms..."); + client.AuthenticationMechanisms.Clear(); + var saslMechanism = GetSASLAuthenticationMethodName(authMethod); + client.AuthenticationMechanisms.Add(saslMechanism); + await client.AuthenticateAsync(SaslMechanism.Create(saslMechanism, cred), cancellationToken).ConfigureAwait(false); } else { - WriteToProtocolLog($"Supported authentication mechanisms: {string.Join(", ", supportedAuthMethods)}"); + await client.AuthenticateAsync(cred, cancellationToken).ConfigureAwait(false); + } + + await TryIdentifyAsync(client, cancellationToken).ConfigureAwait(false); + + client.IsQResyncEnabled = false; + if (!_quirks.DisableQResync && client.Capabilities.HasFlag(ImapCapabilities.QuickResync)) + { + try + { + await client.EnableQuickResyncAsync(cancellationToken).ConfigureAwait(false); + client.IsQResyncEnabled = true; + } + catch (Exception ex) + { + _logger.Debug(ex, "Failed to enable QRESYNC for {Server}. Falling back to non-QRESYNC synchronization.", _customServerInformation.IncomingServer); + } } } - - return true; } - public async Task EnsureAuthenticatedAsync(IImapClient client) + private async Task TryIdentifyAsync(WinoImapClient client, CancellationToken cancellationToken) { - if (client.IsAuthenticated) return; + if (!client.Capabilities.HasFlag(ImapCapabilities.Id)) + return; - var cred = new NetworkCredential(_customServerInformation.IncomingServerUsername, _customServerInformation.IncomingServerPassword); - var prefferedAuthenticationMethod = _customServerInformation.IncomingAuthenticationMethod; - - if (prefferedAuthenticationMethod != ImapAuthenticationMethod.Auto) - { - // Anything beside Auto must be explicitly set for the client. - client.AuthenticationMechanisms.Clear(); - var saslMechanism = GetSASLAuthenticationMethodName(prefferedAuthenticationMethod); - client.AuthenticationMechanisms.Add(saslMechanism); - await client.AuthenticateAsync(SaslMechanism.Create(saslMechanism, cred)); - } - else - { - await client.AuthenticateAsync(cred); - } - } - - private string GetSASLAuthenticationMethodName(ImapAuthenticationMethod method) - => method switch - { - ImapAuthenticationMethod.NormalPassword => "PLAIN", - ImapAuthenticationMethod.EncryptedPassword => "LOGIN", - ImapAuthenticationMethod.Ntlm => "NTLM", - ImapAuthenticationMethod.CramMd5 => "CRAM-MD5", - ImapAuthenticationMethod.DigestMd5 => "DIGEST-MD5", - _ => "PLAIN" - }; - - private void WriteToProtocolLog(string message) - { - if (_protocolLogStream == null) return; try { - var messageBytes = Encoding.UTF8.GetBytes($"W: {message}\n"); - _protocolLogStream.Write(messageBytes, 0, messageBytes.Length); + await client.IdentifyAsync(_implementation, cancellationToken).ConfigureAwait(false); } - catch (ObjectDisposedException) + catch (ImapCommandException) { - Log.Warning($"Protocol log stream is disposed. Cannot write to it."); + // Some servers refuse ID even if advertised. Ignore and continue. } - catch (Exception) + catch (Exception ex) { - throw; + _logger.Debug(ex, "Failed to send IMAP ID payload. Continuing without Identify()."); } } - bool MyServerCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + private WinoImapClient CreateNewClient() + { + var client = new WinoImapClient(); + + if (!string.IsNullOrEmpty(_customServerInformation.ProxyServer)) + { + client.ProxyClient = new HttpProxyClient( + _customServerInformation.ProxyServer, + int.Parse(_customServerInformation.ProxyServerPort)); + } + + _logger.Debug("Created new IMAP client. Current tracked pool size: {Count}", _clientStates.Count); + return client; + } + + private void DisposeClient(IImapClient client) + { + if (client == null) + return; + + try + { + if (client.IsConnected) + { + lock (client.SyncRoot) + { + client.Disconnect(quit: true); + } + } + + client.Dispose(); + } + catch (Exception ex) + { + _logger.Debug(ex, "Error disposing IMAP client."); + } + } + + private void MarkClientAsFailed(WinoImapClient client) + { + if (client == null) + return; + + _clientStates[client] = ImapClientState.Failed; + } + + private bool CanCreateAdditionalConnection() + { + var activeCount = _clientStates.Count(kvp => kvp.Value != ImapClientState.Failed && kvp.Value != ImapClientState.Disposed); + return activeCount < _maxConnections; + } + + private ImapClientPoolException CreatePoolException(string message, Exception innerException = null) + { + return innerException == null + ? new ImapClientPoolException(message, _customServerInformation) + : new ImapClientPoolException(innerException); + } + + private static ImapImplementation CreateImplementation() + { + var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown"; + + return new ImapImplementation + { + Name = "Wino Mail", + Version = version, + Vendor = "Wino", + OS = Environment.OSVersion.VersionString, + SupportUrl = "https://www.winomail.app" + }; + } + + public static int CalculateMaxConnections(int configuredMaxConcurrentClients) + => Math.Clamp(configuredMaxConcurrentClients <= 0 ? 5 : configuredMaxConcurrentClients, 1, 10); + + public static int CalculateTargetMinimumConnections(int maxConnections, bool useConservativeConnections) + => useConservativeConnections ? 1 : Math.Min(2, Math.Max(1, maxConnections)); + + private SecureSocketOptions GetSocketOptions(ImapConnectionSecurity connectionSecurity) => connectionSecurity switch + { + ImapConnectionSecurity.Auto => SecureSocketOptions.Auto, + ImapConnectionSecurity.None => SecureSocketOptions.None, + ImapConnectionSecurity.StartTls => SecureSocketOptions.StartTlsWhenAvailable, + ImapConnectionSecurity.SslTls => SecureSocketOptions.SslOnConnect, + _ => SecureSocketOptions.None + }; + + private string GetSASLAuthenticationMethodName(ImapAuthenticationMethod method) => method switch + { + ImapAuthenticationMethod.NormalPassword => "PLAIN", + ImapAuthenticationMethod.EncryptedPassword => "LOGIN", + ImapAuthenticationMethod.Ntlm => "NTLM", + ImapAuthenticationMethod.CramMd5 => "CRAM-MD5", + ImapAuthenticationMethod.DigestMd5 => "DIGEST-MD5", + _ => "PLAIN" + }; + + private bool MyServerCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { - // If there are no errors, then everything went smoothly. if (sslPolicyErrors == SslPolicyErrors.None) return true; - // Imap connectivity test will throw to alert the user here. if (ThrowOnSSLHandshakeCallback) { - throw new ImapTestSSLCertificateException(certificate.Issuer, certificate.GetExpirationDateString(), certificate.GetEffectiveDateString()); + throw new ImapTestSSLCertificateException( + certificate.Issuer, + certificate.GetExpirationDateString(), + certificate.GetEffectiveDateString()); } return true; } + // Legacy compatibility methods + public Task EnsureConnectedAsync(IImapClient client) => + Task.FromResult(client.IsConnected); + + public Task EnsureAuthenticatedAsync(IImapClient client) => + Task.CompletedTask; + protected virtual void Dispose(bool disposing) { - if (!_disposedValue) + if (_disposedValue) + return; + + if (disposing) { - if (disposing) + _maintenanceCts.Cancel(); + _maintenanceTask?.Wait(TimeSpan.FromSeconds(5)); + _maintenanceCts.Dispose(); + _initializeSemaphore.Dispose(); + + _availableClients.Writer.Complete(); + + foreach (var kvp in _clientStates) { - _keepAliveTimer.Stop(); - _connectionMonitorTimer.Stop(); - - _keepAliveTimer.Dispose(); - _connectionMonitorTimer.Dispose(); - - _clients.ForEach(client => - { - lock (client.SyncRoot) - { - client.Disconnect(true); - } - client.Dispose(); - }); - - _clients.Clear(); + DisposeClient(kvp.Key); + } + + _clientStates.Clear(); + + lock (_idleClientLock) + { + _dedicatedIdleClient = null; } - _disposedValue = true; } + + _disposedValue = true; } public void Dispose() diff --git a/Wino.Core/Integration/ImapServerQuirks.cs b/Wino.Core/Integration/ImapServerQuirks.cs new file mode 100644 index 00000000..559d14f8 --- /dev/null +++ b/Wino.Core/Integration/ImapServerQuirks.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; + +namespace Wino.Core.Integration; + +internal sealed class ImapServerQuirkProfile +{ + public static readonly ImapServerQuirkProfile Default = new(); + + public bool DisableQResync { get; init; } + public bool DisableCondstore { get; init; } + public bool UseConservativeConnections { get; init; } +} + +internal static class ImapServerQuirks +{ + private static readonly Dictionary Quirks = new(StringComparer.OrdinalIgnoreCase) + { + // Some strict providers are more stable with conservative behavior. + ["qq.com"] = new ImapServerQuirkProfile { DisableQResync = true, UseConservativeConnections = true }, + ["163.com"] = new ImapServerQuirkProfile { DisableQResync = true, UseConservativeConnections = true }, + ["126.com"] = new ImapServerQuirkProfile { DisableQResync = true, UseConservativeConnections = true }, + ["yeah.net"] = new ImapServerQuirkProfile { DisableQResync = true, UseConservativeConnections = true } + }; + + public static ImapServerQuirkProfile Resolve(string host) + { + if (string.IsNullOrWhiteSpace(host)) + return ImapServerQuirkProfile.Default; + + foreach (var (key, profile) in Quirks) + { + if (host.Contains(key, StringComparison.OrdinalIgnoreCase)) + return profile; + } + + return ImapServerQuirkProfile.Default; + } +} diff --git a/Wino.Core/Integration/Json/ServerRequestTypeInfoResolver.cs b/Wino.Core/Integration/Json/ServerRequestTypeInfoResolver.cs deleted file mode 100644 index 918b1e60..00000000 --- a/Wino.Core/Integration/Json/ServerRequestTypeInfoResolver.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Text.Json.Serialization.Metadata; -using Wino.Core.Domain.Entities.Mail; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Folders; -using Wino.Core.Domain.Models.MailItem; -using Wino.Core.Requests.Folder; -using Wino.Core.Requests.Mail; - -namespace Wino.Core.Integration.Json; - -public class ServerRequestTypeInfoResolver : DefaultJsonTypeInfoResolver -{ - public ServerRequestTypeInfoResolver() - { - Modifiers.Add(new System.Action(t => - { - if (t.Type == typeof(IRequestBase)) - { - t.PolymorphismOptions = new() - { - DerivedTypes = - { - new JsonDerivedType(typeof(AlwaysMoveToRequest), nameof(AlwaysMoveToRequest)), - new JsonDerivedType(typeof(ArchiveRequest), nameof(ArchiveRequest)), - new JsonDerivedType(typeof(ChangeFlagRequest), nameof(ChangeFlagRequest)), - new JsonDerivedType(typeof(CreateDraftRequest), nameof(CreateDraftRequest)), - new JsonDerivedType(typeof(DeleteRequest), nameof(DeleteRequest)), - new JsonDerivedType(typeof(EmptyFolderRequest), nameof(EmptyFolderRequest)), - new JsonDerivedType(typeof(MarkFolderAsReadRequest), nameof(MarkFolderAsReadRequest)), - new JsonDerivedType(typeof(MarkReadRequest), nameof(MarkReadRequest)), - new JsonDerivedType(typeof(MoveRequest), nameof(MoveRequest)), - new JsonDerivedType(typeof(MoveToFocusedRequest), nameof(MoveToFocusedRequest)), - new JsonDerivedType(typeof(RenameFolderRequest), nameof(RenameFolderRequest)), - new JsonDerivedType(typeof(SendDraftRequest), nameof(SendDraftRequest)), - } - }; - } - else if (t.Type == typeof(IMailItem)) - { - t.PolymorphismOptions = new JsonPolymorphismOptions() - { - DerivedTypes = - { - new JsonDerivedType(typeof(MailCopy), nameof(MailCopy)), - } - }; - } - else if (t.Type == typeof(IMailItemFolder)) - { - t.PolymorphismOptions = new JsonPolymorphismOptions() - { - DerivedTypes = - { - new JsonDerivedType(typeof(MailItemFolder), nameof(MailItemFolder)), - } - }; - } - })); - } -} diff --git a/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs b/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs index a7a98ea3..1db37931 100644 --- a/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs +++ b/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs @@ -9,6 +9,7 @@ using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.MailItem; +using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Synchronization; using Wino.Services; @@ -44,6 +45,8 @@ public interface IDefaultChangeProcessor Task> GetAccountCalendarsAsync(Guid accountId); Task DeleteCalendarItemAsync(Guid calendarItemId); + Task DeleteCalendarItemAsync(string calendarRemoteEventId, Guid calendarId); + Task GetCalendarItemAsync(Guid calendarId, string remoteEventId); Task DeleteAccountCalendarAsync(AccountCalendar accountCalendar); Task InsertAccountCalendarAsync(AccountCalendar accountCalendar); @@ -53,6 +56,8 @@ public interface IDefaultChangeProcessor Task> GetMailCopiesAsync(IEnumerable mailCopyIds); Task CreateMailRawAsync(MailAccount account, MailItemFolder mailItemFolder, NewMailItemPackage package); Task DeleteUserMailCacheAsync(Guid accountId); + Task UpsertMailInvitationCalendarMappingAsync(MailInvitationCalendarMapping mapping); + Task GetMailInvitationCalendarMappingAsync(Guid accountId, string mailCopyId); /// /// Checks whether the mail exists in the folder. @@ -106,6 +111,19 @@ public interface IImapChangeProcessor : IDefaultChangeProcessor /// /// Folder id to retrieve uIds for. Task> GetKnownUidsForFolderAsync(Guid folderId); + + /// + /// Gets the most recent mail IDs for a folder (for notification purposes). + /// + /// Folder ID. + /// Number of recent mails to return. + Task> GetRecentMailIdsForFolderAsync(Guid folderId, int count); + + Task ManageCalendarEventAsync(CalDavCalendarEvent calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount); + Task SaveCalendarItemIcsAsync(Guid accountId, Guid calendarId, Guid calendarItemId, string remoteEventId, string remoteResourceHref, string eTag, string icsContent); + Task GetCalendarItemIcsETagAsync(Guid accountId, Guid calendarId, Guid calendarItemId); + Task DeleteCalendarItemIcsAsync(Guid accountId, Guid calendarItemId); + Task DeleteCalendarIcsForCalendarAsync(Guid accountId, Guid calendarId); } public class DefaultChangeProcessor(IDatabaseService databaseService, @@ -185,9 +203,15 @@ public class DefaultChangeProcessor(IDatabaseService databaseService, public Task> GetAccountCalendarsAsync(Guid accountId) => CalendarService.GetAccountCalendarsAsync(accountId); - public Task DeleteCalendarItemAsync(Guid calendarItemId) + public virtual Task DeleteCalendarItemAsync(Guid calendarItemId) => CalendarService.DeleteCalendarItemAsync(calendarItemId); + public virtual Task DeleteCalendarItemAsync(string calendarRemoteEventId, Guid calendarId) + => CalendarService.DeleteCalendarItemAsync(calendarRemoteEventId, calendarId); + + public Task GetCalendarItemAsync(Guid calendarId, string remoteEventId) + => CalendarService.GetCalendarItemAsync(calendarId, remoteEventId); + public Task DeleteAccountCalendarAsync(AccountCalendar accountCalendar) => CalendarService.DeleteAccountCalendarAsync(accountCalendar); @@ -206,6 +230,43 @@ public class DefaultChangeProcessor(IDatabaseService databaseService, await AccountService.DeleteAccountMailCacheAsync(accountId, AccountCacheResetReason.ExpiredCache).ConfigureAwait(false); } + public async Task UpsertMailInvitationCalendarMappingAsync(MailInvitationCalendarMapping mapping) + { + if (mapping == null || mapping.AccountId == Guid.Empty || string.IsNullOrWhiteSpace(mapping.MailCopyId)) + return; + + var existing = await Connection.Table() + .FirstOrDefaultAsync(x => x.AccountId == mapping.AccountId && x.MailCopyId == mapping.MailCopyId) + .ConfigureAwait(false); + + if (existing == null) + { + if (mapping.Id == Guid.Empty) + mapping.Id = Guid.NewGuid(); + + mapping.UpdatedAtUtc = DateTime.UtcNow; + await Connection.InsertAsync(mapping, typeof(MailInvitationCalendarMapping)).ConfigureAwait(false); + return; + } + + existing.InvitationUid = mapping.InvitationUid; + existing.CalendarId = mapping.CalendarId; + existing.CalendarItemId = mapping.CalendarItemId; + existing.CalendarRemoteEventId = mapping.CalendarRemoteEventId; + existing.UpdatedAtUtc = DateTime.UtcNow; + + await Connection.UpdateAsync(existing, typeof(MailInvitationCalendarMapping)).ConfigureAwait(false); + } + + public Task GetMailInvitationCalendarMappingAsync(Guid accountId, string mailCopyId) + { + if (accountId == Guid.Empty || string.IsNullOrWhiteSpace(mailCopyId)) + return Task.FromResult(null); + + return Connection.Table() + .FirstOrDefaultAsync(x => x.AccountId == accountId && x.MailCopyId == mailCopyId); + } + public Task IsMailExistsInFolderAsync(string messageId, Guid folderId) => MailService.IsMailExistsAsync(messageId, folderId); } diff --git a/Wino.Core/Integration/Processors/GmailChangeProcessor.cs b/Wino.Core/Integration/Processors/GmailChangeProcessor.cs index c478308d..c1da8367 100644 --- a/Wino.Core/Integration/Processors/GmailChangeProcessor.cs +++ b/Wino.Core/Integration/Processors/GmailChangeProcessor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Threading.Tasks; using Google.Apis.Calendar.v3.Data; using Serilog; @@ -65,12 +66,14 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso // We don't have this event yet. Create a new one. var eventStartDateTimeOffset = GoogleIntegratorExtensions.GetEventDateTimeOffset(calendarEvent.Start); var eventEndDateTimeOffset = GoogleIntegratorExtensions.GetEventDateTimeOffset(calendarEvent.End); + var eventStartLocalDateTime = GoogleIntegratorExtensions.GetEventLocalDateTime(calendarEvent.Start); + var eventEndLocalDateTime = GoogleIntegratorExtensions.GetEventLocalDateTime(calendarEvent.End); double totalDurationInSeconds = 0; - if (eventStartDateTimeOffset != null && eventEndDateTimeOffset != null) + if (eventStartLocalDateTime != null && eventEndLocalDateTime != null) { - totalDurationInSeconds = (eventEndDateTimeOffset.Value - eventStartDateTimeOffset.Value).TotalSeconds; + totalDurationInSeconds = (eventEndLocalDateTime.Value - eventStartLocalDateTime.Value).TotalSeconds; } CalendarItem calendarItem = null; @@ -96,18 +99,21 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso CreatedAt = DateTimeOffset.UtcNow, Description = calendarEvent.Description ?? parentRecurringEvent.Description, Id = Guid.NewGuid(), - StartDate = eventStartDateTimeOffset.Value.DateTime, - StartDateOffset = eventStartDateTimeOffset.Value.Offset, - EndDateOffset = eventEndDateTimeOffset?.Offset ?? parentRecurringEvent.EndDateOffset, + StartDate = eventStartLocalDateTime.Value, DurationInSeconds = totalDurationInSeconds, Location = string.IsNullOrEmpty(calendarEvent.Location) ? parentRecurringEvent.Location : calendarEvent.Location, + // Store timezone information + StartTimeZone = GoogleIntegratorExtensions.GetEventTimeZone(calendarEvent.Start) ?? parentRecurringEvent.StartTimeZone, + EndTimeZone = GoogleIntegratorExtensions.GetEventTimeZone(calendarEvent.End) ?? parentRecurringEvent.EndTimeZone, + // Leave it empty if it's not populated. Recurrence = GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent) == null ? string.Empty : GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent), Status = GetStatus(calendarEvent.Status), Title = string.IsNullOrEmpty(calendarEvent.Summary) ? parentRecurringEvent.Title : calendarEvent.Summary, UpdatedAt = DateTimeOffset.UtcNow, Visibility = string.IsNullOrEmpty(calendarEvent.Visibility) ? parentRecurringEvent.Visibility : GetVisibility(calendarEvent.Visibility), + ShowAs = string.IsNullOrEmpty(calendarEvent.Transparency) ? parentRecurringEvent.ShowAs : GetShowAs(calendarEvent.Transparency), HtmlLink = string.IsNullOrEmpty(calendarEvent.HtmlLink) ? parentRecurringEvent.HtmlLink : calendarEvent.HtmlLink, RemoteEventId = calendarEvent.Id, IsLocked = calendarEvent.Locked.GetValueOrDefault(), @@ -132,16 +138,20 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso CreatedAt = DateTimeOffset.UtcNow, Description = calendarEvent.Description, Id = Guid.NewGuid(), - StartDate = eventStartDateTimeOffset.Value.DateTime, - StartDateOffset = eventStartDateTimeOffset.Value.Offset, - EndDateOffset = eventEndDateTimeOffset.Value.Offset, + StartDate = eventStartLocalDateTime.Value, DurationInSeconds = totalDurationInSeconds, Location = calendarEvent.Location, + + // Store timezone information from Google Calendar event + StartTimeZone = GoogleIntegratorExtensions.GetEventTimeZone(calendarEvent.Start), + EndTimeZone = GoogleIntegratorExtensions.GetEventTimeZone(calendarEvent.End), + Recurrence = GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent), Status = GetStatus(calendarEvent.Status), Title = calendarEvent.Summary, UpdatedAt = DateTimeOffset.UtcNow, Visibility = GetVisibility(calendarEvent.Visibility), + ShowAs = GetShowAs(calendarEvent.Transparency), HtmlLink = calendarEvent.HtmlLink, RemoteEventId = calendarEvent.Id, IsLocked = calendarEvent.Locked.GetValueOrDefault(), @@ -153,6 +163,9 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso // Hide canceled events. calendarItem.IsHidden = calendarItem.Status == CalendarItemStatus.Cancelled; + // Set assigned calendar for navigation properties to work. + calendarItem.AssignedCalendar = assignedCalendar; + // Manage the recurring event id. if (parentRecurringEvent != null) { @@ -218,7 +231,64 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso } } + // Prepare reminders list from Gmail event + List reminders = null; + if (calendarEvent.Reminders?.Overrides != null && calendarEvent.Reminders.Overrides.Count > 0) + { + reminders = new List(); + foreach (var reminderOverride in calendarEvent.Reminders.Overrides) + { + if (reminderOverride.Minutes.HasValue) + { + var durationInSeconds = reminderOverride.Minutes.Value * 60; // Convert minutes to seconds + var reminderType = reminderOverride.Method switch + { + "email" => CalendarItemReminderType.Email, + _ => CalendarItemReminderType.Popup + }; + + reminders.Add(new Reminder + { + Id = Guid.NewGuid(), + CalendarItemId = calendarItem.Id, + DurationInSeconds = durationInSeconds, + ReminderType = reminderType + }); + } + } + } + + // Prepare attachments metadata from Gmail event + List attachments = null; + if (calendarEvent.Attachments != null && calendarEvent.Attachments.Count > 0) + { + attachments = calendarEvent.Attachments + .Where(a => a != null && !string.IsNullOrEmpty(a.Title)) + .Select(a => new CalendarAttachment + { + Id = Guid.NewGuid(), + CalendarItemId = calendarItem.Id, + RemoteAttachmentId = a.FileId ?? a.FileUrl, // Gmail uses FileId or FileUrl + FileName = a.Title, + Size = 0, // Gmail API doesn't provide size in Event.Attachment + ContentType = a.MimeType ?? "application/octet-stream", + IsDownloaded = false, + LocalFilePath = null, + LastModified = DateTimeOffset.UtcNow + }) + .ToList(); + } + await CalendarService.CreateNewCalendarItemAsync(calendarItem, attendees); + + // Save reminders separately + await CalendarService.SaveRemindersAsync(calendarItem.Id, reminders).ConfigureAwait(false); + + // Save attachments metadata separately + if (attachments != null && attachments.Count > 0) + { + await CalendarService.InsertOrReplaceAttachmentsAsync(attachments).ConfigureAwait(false); + } } else { @@ -250,10 +320,67 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso // Update the event properties. } + + // Prepare reminders list from Gmail event for update + List reminders = null; + if (calendarEvent.Reminders?.Overrides != null && calendarEvent.Reminders.Overrides.Count > 0) + { + reminders = new List(); + foreach (var reminderOverride in calendarEvent.Reminders.Overrides) + { + if (reminderOverride.Minutes.HasValue) + { + var durationInSeconds = reminderOverride.Minutes.Value * 60; // Convert minutes to seconds + var reminderType = reminderOverride.Method switch + { + "email" => CalendarItemReminderType.Email, + _ => CalendarItemReminderType.Popup + }; + + reminders.Add(new Reminder + { + Id = Guid.NewGuid(), + CalendarItemId = existingCalendarItem.Id, + DurationInSeconds = durationInSeconds, + ReminderType = reminderType + }); + } + } + } + + // Save reminders + await CalendarService.SaveRemindersAsync(existingCalendarItem.Id, reminders).ConfigureAwait(false); + + // Prepare attachments metadata from Gmail event for update + List attachments = null; + if (calendarEvent.Attachments != null && calendarEvent.Attachments.Count > 0) + { + attachments = calendarEvent.Attachments + .Where(a => a != null && !string.IsNullOrEmpty(a.Title)) + .Select(a => new CalendarAttachment + { + Id = Guid.NewGuid(), + CalendarItemId = existingCalendarItem.Id, + RemoteAttachmentId = a.FileId ?? a.FileUrl, + FileName = a.Title, + Size = 0, + ContentType = a.MimeType ?? "application/octet-stream", + IsDownloaded = false, + LocalFilePath = null, + LastModified = DateTimeOffset.UtcNow + }) + .ToList(); + } + + // Save attachments metadata + if (attachments != null && attachments.Count > 0) + { + await CalendarService.InsertOrReplaceAttachmentsAsync(attachments).ConfigureAwait(false); + } } // Upsert the event. - await Connection.InsertOrReplaceAsync(existingCalendarItem); + await Connection.InsertOrReplaceAsync(existingCalendarItem, typeof(CalendarItem)); } private string GetOrganizerName(Event calendarEvent, MailAccount account) @@ -284,10 +411,10 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso { return status switch { - "confirmed" => CalendarItemStatus.Confirmed, + "confirmed" => CalendarItemStatus.Accepted, "tentative" => CalendarItemStatus.Tentative, "cancelled" => CalendarItemStatus.Cancelled, - _ => CalendarItemStatus.Confirmed + _ => CalendarItemStatus.Accepted }; } @@ -309,6 +436,20 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso }; } + private CalendarItemShowAs GetShowAs(string transparency) + { + /// Google Calendar uses "transparent" for free time (event doesn't block time) + /// and "opaque" for busy time (event blocks time on the calendar). + /// If not specified, defaults to opaque (busy). + + return transparency switch + { + "transparent" => CalendarItemShowAs.Free, + "opaque" => CalendarItemShowAs.Busy, + _ => CalendarItemShowAs.Busy + }; + } + public Task HasAccountAnyDraftAsync(Guid accountId) => MailService.HasAccountAnyDraftAsync(accountId); diff --git a/Wino.Core/Integration/Processors/ImapChangeProcessor.cs b/Wino.Core/Integration/Processors/ImapChangeProcessor.cs index f23470bf..02a27f85 100644 --- a/Wino.Core/Integration/Processors/ImapChangeProcessor.cs +++ b/Wino.Core/Integration/Processors/ImapChangeProcessor.cs @@ -1,21 +1,203 @@ -using System; +using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Calendar; using Wino.Services; namespace Wino.Core.Integration.Processors; public class ImapChangeProcessor : DefaultChangeProcessor, IImapChangeProcessor { + private readonly ICalendarIcsFileService _calendarIcsFileService; + public ImapChangeProcessor(IDatabaseService databaseService, IFolderService folderService, IMailService mailService, IAccountService accountService, ICalendarService calendarService, - IMimeFileService mimeFileService) : base(databaseService, folderService, mailService, calendarService, accountService, mimeFileService) + IMimeFileService mimeFileService, + ICalendarIcsFileService calendarIcsFileService) : base(databaseService, folderService, mailService, calendarService, accountService, mimeFileService) { + _calendarIcsFileService = calendarIcsFileService; } public Task> GetKnownUidsForFolderAsync(Guid folderId) => FolderService.GetKnownUidsForFolderAsync(folderId); + + public Task> GetRecentMailIdsForFolderAsync(Guid folderId, int count) + => MailService.GetRecentMailIdsForFolderAsync(folderId, count); + + public async Task ManageCalendarEventAsync(CalDavCalendarEvent calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount) + { + if (calendarEvent == null || assignedCalendar == null) + return; + + var existingItem = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, calendarEvent.RemoteEventId).ConfigureAwait(false); + var isNewItem = existingItem == null; + var savingItemId = existingItem?.Id ?? Guid.NewGuid(); + var savingItem = existingItem ?? new CalendarItem { Id = savingItemId }; + + var startTimeZone = NormalizeTimeZoneId(calendarEvent.StartTimeZone, calendarEvent.Start); + var endTimeZone = NormalizeTimeZoneId(calendarEvent.EndTimeZone, calendarEvent.End); + if (string.IsNullOrWhiteSpace(endTimeZone)) + endTimeZone = startTimeZone; + + var start = ConvertToEventWallClock(calendarEvent.Start, startTimeZone); + var end = ConvertToEventWallClock(calendarEvent.End, endTimeZone); + + var durationInSeconds = (calendarEvent.End - calendarEvent.Start).TotalSeconds; + if (durationInSeconds <= 0) + { + if (end <= start) + end = start.AddHours(1); + + durationInSeconds = (end - start).TotalSeconds; + } + + savingItem.RemoteEventId = calendarEvent.RemoteEventId; + savingItem.CalendarId = assignedCalendar.Id; + savingItem.StartDate = start; + savingItem.DurationInSeconds = durationInSeconds; + savingItem.StartTimeZone = startTimeZone; + savingItem.EndTimeZone = endTimeZone; + savingItem.Title = calendarEvent.Title; + savingItem.Description = calendarEvent.Description; + savingItem.Location = calendarEvent.Location; + savingItem.Recurrence = calendarEvent.Recurrence; + savingItem.Status = calendarEvent.Status; + savingItem.Visibility = calendarEvent.Visibility; + savingItem.ShowAs = calendarEvent.ShowAs; + savingItem.IsHidden = calendarEvent.IsHidden; + savingItem.HtmlLink = string.Empty; + savingItem.IsLocked = false; + savingItem.OrganizerDisplayName = !string.IsNullOrWhiteSpace(calendarEvent.OrganizerDisplayName) + ? calendarEvent.OrganizerDisplayName + : organizerAccount?.SenderName ?? string.Empty; + savingItem.OrganizerEmail = !string.IsNullOrWhiteSpace(calendarEvent.OrganizerEmail) + ? calendarEvent.OrganizerEmail + : organizerAccount?.Address ?? string.Empty; + savingItem.AssignedCalendar = assignedCalendar; + + if (savingItem.CreatedAt == default) + savingItem.CreatedAt = DateTimeOffset.UtcNow; + + savingItem.UpdatedAt = DateTimeOffset.UtcNow; + + if (!string.IsNullOrWhiteSpace(calendarEvent.SeriesMasterRemoteEventId)) + { + var parentEvent = await CalendarService + .GetCalendarItemAsync(assignedCalendar.Id, calendarEvent.SeriesMasterRemoteEventId) + .ConfigureAwait(false); + + if (parentEvent != null) + { + savingItem.RecurringCalendarItemId = parentEvent.Id; + } + } + else + { + savingItem.RecurringCalendarItemId = null; + } + + var attendees = calendarEvent.Attendees? + .Where(a => !string.IsNullOrWhiteSpace(a.Email)) + .Select(a => new CalendarEventAttendee + { + Id = Guid.NewGuid(), + CalendarItemId = savingItemId, + Name = a.Name, + Email = a.Email, + AttendenceStatus = a.AttendenceStatus, + IsOrganizer = a.IsOrganizer, + IsOptionalAttendee = a.IsOptionalAttendee + }) + .ToList(); + + var reminders = calendarEvent.Reminders? + .Where(r => r.DurationInSeconds > 0) + .Select(r => new Reminder + { + Id = Guid.NewGuid(), + CalendarItemId = savingItemId, + DurationInSeconds = r.DurationInSeconds, + ReminderType = r.ReminderType + }) + .ToList(); + + if (isNewItem) + { + await CalendarService.CreateNewCalendarItemAsync(savingItem, attendees).ConfigureAwait(false); + } + else + { + await CalendarService.UpdateCalendarItemAsync(savingItem, attendees).ConfigureAwait(false); + } + + await CalendarService.SaveRemindersAsync(savingItemId, reminders).ConfigureAwait(false); + } + + public Task SaveCalendarItemIcsAsync(Guid accountId, Guid calendarId, Guid calendarItemId, string remoteEventId, string remoteResourceHref, string eTag, string icsContent) + => _calendarIcsFileService.SaveCalendarItemIcsAsync(accountId, calendarId, calendarItemId, remoteEventId, remoteResourceHref, eTag, icsContent); + + public Task GetCalendarItemIcsETagAsync(Guid accountId, Guid calendarId, Guid calendarItemId) + => _calendarIcsFileService.GetCalendarItemIcsETagAsync(accountId, calendarId, calendarItemId); + + public Task DeleteCalendarItemIcsAsync(Guid accountId, Guid calendarItemId) + => _calendarIcsFileService.DeleteCalendarItemIcsAsync(accountId, calendarItemId); + + public Task DeleteCalendarIcsForCalendarAsync(Guid accountId, Guid calendarId) + => _calendarIcsFileService.DeleteCalendarIcsForCalendarAsync(accountId, calendarId); + + public override async Task DeleteCalendarItemAsync(Guid calendarItemId) + { + var item = await CalendarService.GetCalendarItemAsync(calendarItemId).ConfigureAwait(false); + if (item == null) + return; + + await _calendarIcsFileService.DeleteCalendarItemIcsAsync(item.AssignedCalendar?.AccountId ?? Guid.Empty, calendarItemId).ConfigureAwait(false); + await base.DeleteCalendarItemAsync(calendarItemId).ConfigureAwait(false); + } + + public override async Task DeleteCalendarItemAsync(string calendarRemoteEventId, Guid calendarId) + { + var item = await CalendarService.GetCalendarItemAsync(calendarId, calendarRemoteEventId).ConfigureAwait(false); + if (item == null) + return; + + await DeleteCalendarItemAsync(item.Id).ConfigureAwait(false); + } + + private static string NormalizeTimeZoneId(string timeZoneId, DateTimeOffset value) + { + if (!string.IsNullOrWhiteSpace(timeZoneId)) + return timeZoneId; + + if (value != default && value.Offset == TimeSpan.Zero) + return TimeZoneInfo.Utc.Id; + + return string.Empty; + } + + private static DateTime ConvertToEventWallClock(DateTimeOffset value, string eventTimeZoneId) + { + if (value == default) + return default; + + if (string.IsNullOrWhiteSpace(eventTimeZoneId)) + return DateTime.SpecifyKind(value.DateTime, DateTimeKind.Unspecified); + + try + { + var eventTimeZone = TimeZoneInfo.FindSystemTimeZoneById(eventTimeZoneId); + var inEventTimeZone = TimeZoneInfo.ConvertTime(value, eventTimeZone); + return DateTime.SpecifyKind(inEventTimeZone.DateTime, DateTimeKind.Unspecified); + } + catch + { + return DateTime.SpecifyKind(value.DateTime, DateTimeKind.Unspecified); + } + } } diff --git a/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs b/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs index 3b72c33e..c2e543c6 100644 --- a/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs +++ b/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.Graph.Models; @@ -7,8 +8,10 @@ using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Extensions; using Wino.Core.Extensions; using Wino.Services; +using Reminder = Wino.Core.Domain.Entities.Calendar.Reminder; namespace Wino.Core.Integration.Processors; @@ -40,15 +43,13 @@ public class OutlookChangeProcessor(IDatabaseService databaseService, public async Task ManageCalendarEventAsync(Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount) { - // We parse the occurrences based on the parent event. - // There is literally no point to store them because - // type=Exception events are the exceptional childs of recurrency parent event. - - if (calendarEvent.Type == EventType.Occurrence) return; + // All event types are now handled: SingleInstance, SeriesMaster, Occurrence, and Exception. + // Occurrences from CalendarView are individual instances that are saved separately. var savingItem = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, calendarEvent.Id); Guid savingItemId = Guid.Empty; + bool isNewItem = savingItem == null; if (savingItem != null) savingItemId = savingItem.Id; @@ -58,25 +59,33 @@ public class OutlookChangeProcessor(IDatabaseService databaseService, savingItem = new CalendarItem() { Id = savingItemId }; } - DateTimeOffset eventStartDateTimeOffset = OutlookIntegratorExtensions.GetDateTimeOffsetFromDateTimeTimeZone(calendarEvent.Start); - DateTimeOffset eventEndDateTimeOffset = OutlookIntegratorExtensions.GetDateTimeOffsetFromDateTimeTimeZone(calendarEvent.End); + var eventStartLocalDateTime = OutlookIntegratorExtensions.GetLocalDateTimeFromDateTimeTimeZone(calendarEvent.Start); + var eventEndLocalDateTime = OutlookIntegratorExtensions.GetLocalDateTimeFromDateTimeTimeZone(calendarEvent.End); - var durationInSeconds = (eventEndDateTimeOffset - eventStartDateTimeOffset).TotalSeconds; + var durationInSeconds = (eventEndLocalDateTime - eventStartLocalDateTime).TotalSeconds; - savingItem.RemoteEventId = calendarEvent.Id; - savingItem.StartDate = eventStartDateTimeOffset.DateTime; - savingItem.StartDateOffset = eventStartDateTimeOffset.Offset; - savingItem.EndDateOffset = eventEndDateTimeOffset.Offset; + // Store the wall-clock values exactly as Outlook returned them for the event timezone. + // Timed events are converted for display later, while all-day events stay as floating dates. + savingItem.RemoteEventId = calendarEvent.Id.WithClientTrackingId(calendarEvent.TransactionId.GetClientTrackingId()); + savingItem.StartDate = eventStartLocalDateTime; savingItem.DurationInSeconds = durationInSeconds; + // Store the timezone information from the event + // This preserves the original timezone from Outlook, allowing proper reconstruction later + // If no timezone is provided, null will indicate UTC + savingItem.StartTimeZone = calendarEvent.Start?.TimeZone; + savingItem.EndTimeZone = calendarEvent.End?.TimeZone; + savingItem.Title = calendarEvent.Subject; savingItem.Description = calendarEvent.Body?.Content; savingItem.Location = calendarEvent.Location?.DisplayName; - if (calendarEvent.Type == EventType.Exception && !string.IsNullOrEmpty(calendarEvent.SeriesMasterId)) + // Handle recurring event relationships for both Exception and Occurrence types + if ((calendarEvent.Type == EventType.Exception || calendarEvent.Type == EventType.Occurrence) + && !string.IsNullOrEmpty(calendarEvent.SeriesMasterId)) { - // This is a recurring event exception. - // We need to find the parent event and set it as recurring event id. + // This is a recurring event instance (either an exception or a regular occurrence). + // Link it to the parent series master. var parentEvent = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, calendarEvent.SeriesMasterId); @@ -86,12 +95,14 @@ public class OutlookChangeProcessor(IDatabaseService databaseService, } else { - Log.Warning($"Parent recurring event is missing for event. Skipping creation of {calendarEvent.Id}"); - return; + // Parent not found yet - this can happen if occurrences sync before the series master. + // We still save the event but without the parent link for now. + Log.Warning($"Parent recurring event (SeriesMasterId: {calendarEvent.SeriesMasterId}) not found for event {calendarEvent.Id}. Event will be saved without parent link."); } } // Convert the recurrence pattern to string for parent recurring events. + // Note: We store this for reference but don't use it to calculate occurrences. if (calendarEvent.Type == EventType.SeriesMaster && calendarEvent.Recurrence != null) { savingItem.Recurrence = OutlookIntegratorExtensions.ToRfc5545RecurrenceString(calendarEvent.Recurrence); @@ -103,6 +114,52 @@ public class OutlookChangeProcessor(IDatabaseService databaseService, savingItem.OrganizerDisplayName = calendarEvent.Organizer?.EmailAddress?.Name; savingItem.IsHidden = false; + // Set timestamps + if (calendarEvent.CreatedDateTime.HasValue) + savingItem.CreatedAt = calendarEvent.CreatedDateTime.Value; + + if (calendarEvent.LastModifiedDateTime.HasValue) + savingItem.UpdatedAt = calendarEvent.LastModifiedDateTime.Value; + + // Set visibility + if (calendarEvent.Sensitivity != null) + { + savingItem.Visibility = calendarEvent.Sensitivity.Value switch + { + Sensitivity.Normal => CalendarItemVisibility.Public, + Sensitivity.Personal => CalendarItemVisibility.Private, + Sensitivity.Private => CalendarItemVisibility.Private, + Sensitivity.Confidential => CalendarItemVisibility.Confidential, + _ => CalendarItemVisibility.Public + }; + } + else + { + savingItem.Visibility = CalendarItemVisibility.Public; + } + + // Set ShowAs status + if (calendarEvent.ShowAs != null) + { + savingItem.ShowAs = calendarEvent.ShowAs.Value switch + { + Microsoft.Graph.Models.FreeBusyStatus.Free => CalendarItemShowAs.Free, + Microsoft.Graph.Models.FreeBusyStatus.Tentative => CalendarItemShowAs.Tentative, + Microsoft.Graph.Models.FreeBusyStatus.Busy => CalendarItemShowAs.Busy, + Microsoft.Graph.Models.FreeBusyStatus.Oof => CalendarItemShowAs.OutOfOffice, + Microsoft.Graph.Models.FreeBusyStatus.WorkingElsewhere => CalendarItemShowAs.WorkingElsewhere, + _ => CalendarItemShowAs.Busy + }; + } + else + { + savingItem.ShowAs = CalendarItemShowAs.Busy; + } + + // Set IsLocked based on whether the user is the organizer + // Read-only events are those where the current user is not the organizer + savingItem.IsLocked = calendarEvent.IsOrganizer.HasValue && !calendarEvent.IsOrganizer.Value; + if (calendarEvent.ResponseStatus?.Response != null) { switch (calendarEvent.ResponseStatus.Response.Value) @@ -116,7 +173,7 @@ public class OutlookChangeProcessor(IDatabaseService databaseService, break; case ResponseType.Accepted: case ResponseType.Organizer: - savingItem.Status = CalendarItemStatus.Confirmed; + savingItem.Status = CalendarItemStatus.Accepted; break; case ResponseType.Declined: savingItem.Status = CalendarItemStatus.Cancelled; @@ -128,18 +185,80 @@ public class OutlookChangeProcessor(IDatabaseService databaseService, } else { - savingItem.Status = CalendarItemStatus.Confirmed; + savingItem.Status = CalendarItemStatus.Accepted; } - // Upsert the event. - await Connection.InsertOrReplaceAsync(savingItem); - - // Manage attendees. + // Prepare attendees list + List attendees = null; if (calendarEvent.Attendees != null) { - // Clear all attendees for this event. - var attendees = calendarEvent.Attendees.Select(a => a.CreateAttendee(savingItemId)).ToList(); - await CalendarService.ManageEventAttendeesAsync(savingItemId, attendees).ConfigureAwait(false); + // Pass the organizer's email address to properly identify the organizer in the attendees list + string organizerEmail = calendarEvent.Organizer?.EmailAddress?.Address; + attendees = calendarEvent.Attendees.Select(a => a.CreateAttendee(savingItemId, organizerEmail)).ToList(); + } + + // Prepare reminders list from Outlook event + List reminders = null; + if (calendarEvent.IsReminderOn.GetValueOrDefault() && calendarEvent.ReminderMinutesBeforeStart.HasValue) + { + var reminderMinutes = calendarEvent.ReminderMinutesBeforeStart.Value; + var reminderDurationInSeconds = reminderMinutes * 60; // Convert minutes to seconds + + reminders = new List + { + new Reminder + { + Id = Guid.NewGuid(), + CalendarItemId = savingItemId, + DurationInSeconds = reminderDurationInSeconds, + ReminderType = CalendarItemReminderType.Popup + } + }; + } + + // Prepare attachments metadata from Outlook event + List attachments = null; + if (calendarEvent.HasAttachments.GetValueOrDefault() && calendarEvent.Attachments != null) + { + attachments = calendarEvent.Attachments + .Where(a => a != null && !string.IsNullOrEmpty(a.Name)) + .Select(a => new CalendarAttachment + { + Id = Guid.NewGuid(), + CalendarItemId = savingItemId, + RemoteAttachmentId = a.Id, + FileName = a.Name, + Size = a.Size ?? 0, + ContentType = a.ContentType ?? "application/octet-stream", + IsDownloaded = false, + LocalFilePath = null, + LastModified = calendarEvent.LastModifiedDateTime ?? DateTimeOffset.UtcNow + }) + .ToList(); + } + + // Set assigned calendar for navigation properties to work. + savingItem.AssignedCalendar = assignedCalendar; + + // Use CalendarService to create or update the event + if (isNewItem) + { + // New item - use CreateNewCalendarItemAsync + await CalendarService.CreateNewCalendarItemAsync(savingItem, attendees).ConfigureAwait(false); + } + else + { + // Existing item - use UpdateCalendarItemAsync + await CalendarService.UpdateCalendarItemAsync(savingItem, attendees).ConfigureAwait(false); + } + + // Save reminders separately + await CalendarService.SaveRemindersAsync(savingItemId, reminders).ConfigureAwait(false); + + // Save attachments metadata separately + if (attachments != null && attachments.Count > 0) + { + await CalendarService.InsertOrReplaceAttachmentsAsync(attachments).ConfigureAwait(false); } } } diff --git a/Wino.Core/Integration/WinoImapClient.cs b/Wino.Core/Integration/WinoImapClient.cs index f4712268..8062d49e 100644 --- a/Wino.Core/Integration/WinoImapClient.cs +++ b/Wino.Core/Integration/WinoImapClient.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using MailKit; using MailKit.Net.Imap; @@ -9,7 +9,7 @@ namespace Wino.Core.Integration; /// /// Extended class for ImapClient that is used in Wino. /// -internal class WinoImapClient : ImapClient +public class WinoImapClient : ImapClient { private int _busyCount; @@ -24,11 +24,6 @@ internal class WinoImapClient : ImapClient HookEvents(); } - public WinoImapClient(IProtocolLogger protocolLogger) : base(protocolLogger) - { - HookEvents(); - } - private void HookEvents() { Disconnected += ClientDisconnected; diff --git a/Wino.Core/Misc/ColorHelpers.cs b/Wino.Core/Misc/ColorHelpers.cs index 5fdf9f83..8b012431 100644 --- a/Wino.Core/Misc/ColorHelpers.cs +++ b/Wino.Core/Misc/ColorHelpers.cs @@ -1,49 +1,100 @@ -using System; +using System; +using System.Collections.Generic; using System.Drawing; +using System.Globalization; +using System.Linq; +using Wino.Core.Domain.Misc; namespace Wino.Core.Misc; public static class ColorHelpers { - public static string GenerateFlatColorHex() + public static IReadOnlyList GetFlatColorPalette() => CalendarColorPalette.GetColors(); + + public static string GenerateFlatColorHex() => GetDistinctFlatColorHex(Array.Empty()); + + public static string GetDistinctFlatColorHex(IEnumerable usedColors, string preferredColor = null) { - Random random = new(); - int hue = random.Next(0, 360); // Full hue range - int saturation = 70 + random.Next(30); // High saturation (70-100%) - int lightness = 50 + random.Next(20); // Bright colors (50-70%) + var palette = CalendarColorPalette.GetColors(); + var normalizedUsedColors = usedColors? + .Select(NormalizeHexColor) + .Where(color => !string.IsNullOrWhiteSpace(color)) + .ToHashSet(StringComparer.OrdinalIgnoreCase) ?? new HashSet(StringComparer.OrdinalIgnoreCase); - var color = FromHsl(hue, saturation, lightness); + if (TryNormalizeHexColor(preferredColor, out var normalizedPreferred) && + palette.Contains(normalizedPreferred, StringComparer.OrdinalIgnoreCase) && + !normalizedUsedColors.Contains(normalizedPreferred)) + { + return normalizedPreferred; + } - return ToHexString(color); + var distinctColor = CalendarColorPalette.GetDistinctColor(usedColors); + if (palette.Contains(distinctColor)) + { + return distinctColor; + } + + var candidate = AdjustColor(palette[0], 1); + + return candidate; + } + + public static string GetReadableTextColorHex(string backgroundColor) + { + if (!TryNormalizeHexColor(backgroundColor, out var normalizedColor)) + { + return "#FFFFFF"; + } + + var color = ColorTranslator.FromHtml(normalizedColor); + var luminance = ((0.299 * color.R) + (0.587 * color.G) + (0.114 * color.B)) / 255d; + return luminance > 0.6 ? "#111111" : "#FFFFFF"; } public static string ToHexString(this Color c) => $"#{c.R:X2}{c.G:X2}{c.B:X2}"; public static string ToRgbString(this Color c) => $"RGB({c.R}, {c.G}, {c.B})"; - - private static Color FromHsl(int h, int s, int l) + private static string AdjustColor(string hexColor, int cycle) { - double hue = h / 360.0; - double saturation = s / 100.0; - double lightness = l / 100.0; + var color = ColorTranslator.FromHtml(hexColor); + var factor = Math.Max(0.55, 1.0 - (cycle * 0.08)); - // Conversion from HSL to RGB - var chroma = (1 - Math.Abs(2 * lightness - 1)) * saturation; - var x = chroma * (1 - Math.Abs((hue * 6) % 2 - 1)); - var m = lightness - chroma / 2; + var adjusted = Color.FromArgb( + (int)Math.Clamp(color.R * factor, 0, 255), + (int)Math.Clamp(color.G * factor, 0, 255), + (int)Math.Clamp(color.B * factor, 0, 255)); - double r = 0, g = 0, b = 0; - - if (hue < 1.0 / 6.0) { r = chroma; g = x; b = 0; } - else if (hue < 2.0 / 6.0) { r = x; g = chroma; b = 0; } - else if (hue < 3.0 / 6.0) { r = 0; g = chroma; b = x; } - else if (hue < 4.0 / 6.0) { r = 0; g = x; b = chroma; } - else if (hue < 5.0 / 6.0) { r = x; g = 0; b = chroma; } - else { r = chroma; g = 0; b = x; } - - return Color.FromArgb( - (int)((r + m) * 255), - (int)((g + m) * 255), - (int)((b + m) * 255)); + return adjusted.ToHexString(); } + + private static bool TryNormalizeHexColor(string value, out string normalized) + { + normalized = string.Empty; + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + var color = value.Trim(); + if (color.StartsWith('#')) + { + color = color[1..]; + } + + if (color.Length != 6) + { + return false; + } + + if (!int.TryParse(color, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _)) + { + return false; + } + + normalized = $"#{color.ToUpperInvariant()}"; + return true; + } + + private static string NormalizeHexColor(string value) + => TryNormalizeHexColor(value, out var normalized) ? normalized : string.Empty; } diff --git a/Wino.Core/Misc/OutlookFileAttachment.cs b/Wino.Core/Misc/OutlookFileAttachment.cs deleted file mode 100644 index 0839657c..00000000 --- a/Wino.Core/Misc/OutlookFileAttachment.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Wino.Core.Misc; - -public class OutlookFileAttachment -{ - [JsonPropertyName("@odata.type")] - public string OdataType { get; } = "#microsoft.graph.fileAttachment"; - - [JsonPropertyName("name")] - public string FileName { get; set; } - - [JsonPropertyName("contentBytes")] - public string Base64EncodedContentBytes { get; set; } - - [JsonPropertyName("contentType")] - public string ContentType { get; set; } - - [JsonPropertyName("contentId")] - public string ContentId { get; set; } - - [JsonPropertyName("isInline")] - public bool IsInline { get; set; } -} diff --git a/Wino.Core/Properties/AssemblyInfo.cs b/Wino.Core/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..8e625798 --- /dev/null +++ b/Wino.Core/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Wino.Core.Tests")] diff --git a/Wino.Core/Requests/Bundles/RequestBundle.cs b/Wino.Core/Requests/Bundles/RequestBundle.cs index 75ab6321..8a61727e 100644 --- a/Wino.Core/Requests/Bundles/RequestBundle.cs +++ b/Wino.Core/Requests/Bundles/RequestBundle.cs @@ -1,7 +1,7 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.Net.Http; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; using System.Threading; using System.Threading.Tasks; using Wino.Core.Domain.Interfaces; @@ -21,12 +21,10 @@ public record HttpRequestBundle(TRequest NativeRequest, IUIChangeReque /// Batch request that is generated by base synchronizer. public record HttpRequestBundle(TRequest NativeRequest, IRequestBase Request) : HttpRequestBundle(NativeRequest, Request) { - [RequiresDynamicCode("AOT")] - [RequiresUnreferencedCode("AOT")] - public async Task DeserializeBundleAsync(HttpResponseMessage httpResponse, CancellationToken cancellationToken = default) + public async Task DeserializeBundleAsync(HttpResponseMessage httpResponse, JsonTypeInfo typeInfo, CancellationToken cancellationToken = default) { var content = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false); - return JsonSerializer.Deserialize(content) ?? throw new InvalidOperationException("Invalid Http Response Deserialization"); + return JsonSerializer.Deserialize(content, typeInfo) ?? throw new InvalidOperationException("Invalid Http Response Deserialization"); } } diff --git a/Wino.Core/Requests/Bundles/TaskRequestBundle.cs b/Wino.Core/Requests/Bundles/TaskRequestBundle.cs index 7e968cfb..2db25b41 100644 --- a/Wino.Core/Requests/Bundles/TaskRequestBundle.cs +++ b/Wino.Core/Requests/Bundles/TaskRequestBundle.cs @@ -9,18 +9,20 @@ public class ImapRequest { public Func IntegratorTask { get; } public IRequestBase Request { get; } + public bool RequiresConnectedClient { get; } - public ImapRequest(Func integratorTask, IRequestBase request) + public ImapRequest(Func integratorTask, IRequestBase request, bool requiresConnectedClient = true) { IntegratorTask = integratorTask; Request = request; + RequiresConnectedClient = requiresConnectedClient; } } public class ImapRequest : ImapRequest where TRequestBaseType : IRequestBase { - public ImapRequest(Func integratorTask, TRequestBaseType request) - : base((client, request) => integratorTask(client, (TRequestBaseType)request), request) + public ImapRequest(Func integratorTask, TRequestBaseType request, bool requiresConnectedClient = true) + : base((client, request) => integratorTask(client, (TRequestBaseType)request), request, requiresConnectedClient) { } } diff --git a/Wino.Core/Requests/Calendar/AcceptEventRequest.cs b/Wino.Core/Requests/Calendar/AcceptEventRequest.cs new file mode 100644 index 00000000..8d6505e0 --- /dev/null +++ b/Wino.Core/Requests/Calendar/AcceptEventRequest.cs @@ -0,0 +1,39 @@ +using CommunityToolkit.Mvvm.Messaging; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models.Requests; +using Wino.Messaging.Client.Calendar; + +namespace Wino.Core.Requests.Calendar; + +/// +/// Request to accept a calendar event invitation on the server. +/// The calendar item status should be updated locally before queuing this request. +/// +public record AcceptEventRequest(CalendarItem Item, string ResponseMessage = null) : CalendarRequestBase(Item) +{ + private readonly CalendarItemStatus _previousStatus = Item.Status; + + public override CalendarSynchronizerOperation Operation => CalendarSynchronizerOperation.AcceptEvent; + + /// + /// After successful acceptance, we need to resync to get updated status. + /// + public override int ResynchronizationDelay => 2000; + + public override void ApplyUIChanges() + { + // Update the item status locally + Item.Status = CalendarItemStatus.Accepted; + + // Notify UI that the event status was updated + WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item, CalendarItemUpdateSource.ClientUpdated)); + } + + public override void RevertUIChanges() + { + // If acceptance fails, revert to the previous status + Item.Status = _previousStatus; + WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item, CalendarItemUpdateSource.ClientReverted)); + } +} diff --git a/Wino.Core/Requests/Calendar/CreateCalendarEventRequest.cs b/Wino.Core/Requests/Calendar/CreateCalendarEventRequest.cs new file mode 100644 index 00000000..1b1c0832 --- /dev/null +++ b/Wino.Core/Requests/Calendar/CreateCalendarEventRequest.cs @@ -0,0 +1,63 @@ +using System; +using CommunityToolkit.Mvvm.Messaging; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models.Calendar; +using Wino.Core.Domain.Models.Requests; +using Wino.Core.Helpers; +using Wino.Messaging.Client.Calendar; + +namespace Wino.Core.Requests.Calendar; + +/// +/// Request to create a new calendar event on the server. +/// Non-recurring events create an optimistic in-memory item for immediate UI feedback. +/// Recurring events skip optimistic rendering and rely on provider synchronization to materialize instances. +/// +public record CreateCalendarEventRequest : CalendarRequestBase +{ + public CalendarEventComposeResult ComposeResult { get; } + public AccountCalendar AssignedCalendar { get; } + public PreparedCalendarEventCreateModel PreparedEvent { get; } + public CalendarItem PreparedItem => PreparedEvent.CalendarItem; + public bool IsRecurring => !string.IsNullOrWhiteSpace(ComposeResult?.Recurrence); + + public CreateCalendarEventRequest(CalendarEventComposeResult composeResult, AccountCalendar assignedCalendar) + : this(composeResult, assignedCalendar, CalendarEventComposeMapper.Prepare(composeResult, assignedCalendar)) + { + } + + private CreateCalendarEventRequest( + CalendarEventComposeResult composeResult, + AccountCalendar assignedCalendar, + PreparedCalendarEventCreateModel preparedEvent) + : base(ShouldCreateOptimisticItem(composeResult) ? preparedEvent.CalendarItem : null) + { + ComposeResult = composeResult ?? throw new ArgumentNullException(nameof(composeResult)); + AssignedCalendar = assignedCalendar ?? throw new ArgumentNullException(nameof(assignedCalendar)); + PreparedEvent = preparedEvent ?? throw new ArgumentNullException(nameof(preparedEvent)); + } + + public override CalendarSynchronizerOperation Operation => CalendarSynchronizerOperation.CreateEvent; + + public override int ResynchronizationDelay => 5000; + + public override void ApplyUIChanges() + { + if (Item == null) + return; + + WeakReferenceMessenger.Default.Send(new CalendarItemAdded(Item)); + } + + public override void RevertUIChanges() + { + if (Item == null) + return; + + WeakReferenceMessenger.Default.Send(new CalendarItemDeleted(Item)); + } + + private static bool ShouldCreateOptimisticItem(CalendarEventComposeResult composeResult) + => string.IsNullOrWhiteSpace(composeResult?.Recurrence); +} diff --git a/Wino.Core/Requests/Calendar/DeclineEventRequest.cs b/Wino.Core/Requests/Calendar/DeclineEventRequest.cs new file mode 100644 index 00000000..31f89575 --- /dev/null +++ b/Wino.Core/Requests/Calendar/DeclineEventRequest.cs @@ -0,0 +1,39 @@ +using CommunityToolkit.Mvvm.Messaging; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models.Requests; +using Wino.Messaging.Client.Calendar; + +namespace Wino.Core.Requests.Calendar; + +/// +/// Request to decline a calendar event invitation on the server. +/// The calendar item status should be updated locally before queuing this request. +/// +public record DeclineEventRequest(CalendarItem Item, string ResponseMessage = null) : CalendarRequestBase(Item) +{ + private readonly CalendarItemStatus _previousStatus = Item.Status; + + public override CalendarSynchronizerOperation Operation => CalendarSynchronizerOperation.DeclineEvent; + + /// + /// After successful decline, we need to resync to get updated status. + /// + public override int ResynchronizationDelay => 2000; + + public override void ApplyUIChanges() + { + // Update the item status locally + Item.Status = CalendarItemStatus.Cancelled; + + // Notify UI that the event status was updated + WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item, CalendarItemUpdateSource.ClientUpdated)); + } + + public override void RevertUIChanges() + { + // If decline fails, revert to the previous status + Item.Status = _previousStatus; + WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item, CalendarItemUpdateSource.ClientReverted)); + } +} diff --git a/Wino.Core/Requests/Calendar/DeleteCalendarEventRequest.cs b/Wino.Core/Requests/Calendar/DeleteCalendarEventRequest.cs new file mode 100644 index 00000000..a9ae4681 --- /dev/null +++ b/Wino.Core/Requests/Calendar/DeleteCalendarEventRequest.cs @@ -0,0 +1,32 @@ +using CommunityToolkit.Mvvm.Messaging; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models.Requests; +using Wino.Messaging.Client.Calendar; + +namespace Wino.Core.Requests.Calendar; + +/// +/// Request to delete a calendar event on the server. +/// +public record DeleteCalendarEventRequest(CalendarItem Item) : CalendarRequestBase(Item) +{ + public override CalendarSynchronizerOperation Operation => CalendarSynchronizerOperation.DeleteEvent; + + /// + /// After successful deletion, resync to confirm the event was removed. + /// + public override int ResynchronizationDelay => 2000; + + public override void ApplyUIChanges() + { + // Notify UI that the event was deleted + WeakReferenceMessenger.Default.Send(new CalendarItemDeleted(Item)); + } + + public override void RevertUIChanges() + { + // If deletion fails, we should notify the UI to add it back + WeakReferenceMessenger.Default.Send(new CalendarItemAdded(Item)); + } +} diff --git a/Wino.Core/Requests/Calendar/OutlookDeclineEventRequest.cs b/Wino.Core/Requests/Calendar/OutlookDeclineEventRequest.cs new file mode 100644 index 00000000..c7e87147 --- /dev/null +++ b/Wino.Core/Requests/Calendar/OutlookDeclineEventRequest.cs @@ -0,0 +1,38 @@ +using CommunityToolkit.Mvvm.Messaging; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models.Requests; +using Wino.Messaging.Client.Calendar; + +namespace Wino.Core.Requests.Calendar; + +/// +/// Outlook-specific request to decline a calendar event invitation. +/// In Outlook, declined events are removed from the calendar by the API after synchronization, +/// so this request sends a delete notification to remove the event from the UI. +/// +public record OutlookDeclineEventRequest(CalendarItem Item, string ResponseMessage = null) : CalendarRequestBase(Item) +{ + private readonly CalendarItemStatus _previousStatus = Item.Status; + + public override CalendarSynchronizerOperation Operation => CalendarSynchronizerOperation.DeclineEvent; + + /// + /// After successful decline, we need to resync to confirm the event is removed. + /// + public override int ResynchronizationDelay => 2000; + + public override void ApplyUIChanges() + { + // In Outlook, declined events are deleted from the calendar after sync + // Send deleted message to remove from UI immediately + WeakReferenceMessenger.Default.Send(new CalendarItemDeleted(Item)); + } + + public override void RevertUIChanges() + { + // If decline fails, restore the previous status and re-add the event + Item.Status = _previousStatus; + WeakReferenceMessenger.Default.Send(new CalendarItemAdded(Item)); + } +} diff --git a/Wino.Core/Requests/Calendar/TentativeEventRequest.cs b/Wino.Core/Requests/Calendar/TentativeEventRequest.cs new file mode 100644 index 00000000..d53c2eb7 --- /dev/null +++ b/Wino.Core/Requests/Calendar/TentativeEventRequest.cs @@ -0,0 +1,39 @@ +using CommunityToolkit.Mvvm.Messaging; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models.Requests; +using Wino.Messaging.Client.Calendar; + +namespace Wino.Core.Requests.Calendar; + +/// +/// Request to tentatively accept a calendar event invitation on the server. +/// The calendar item status should be updated locally before queuing this request. +/// +public record TentativeEventRequest(CalendarItem Item, string ResponseMessage = null) : CalendarRequestBase(Item) +{ + private readonly CalendarItemStatus _previousStatus = Item.Status; + + public override CalendarSynchronizerOperation Operation => CalendarSynchronizerOperation.TentativeEvent; + + /// + /// After successful tentative acceptance, we need to resync to get updated status. + /// + public override int ResynchronizationDelay => 2000; + + public override void ApplyUIChanges() + { + // Update the item status locally + Item.Status = CalendarItemStatus.Tentative; + + // Notify UI that the event status was updated + WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item, CalendarItemUpdateSource.ClientUpdated)); + } + + public override void RevertUIChanges() + { + // If tentative acceptance fails, revert to the previous status + Item.Status = _previousStatus; + WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item, CalendarItemUpdateSource.ClientReverted)); + } +} diff --git a/Wino.Core/Requests/Calendar/UpdateCalendarEventRequest.cs b/Wino.Core/Requests/Calendar/UpdateCalendarEventRequest.cs new file mode 100644 index 00000000..8326a37a --- /dev/null +++ b/Wino.Core/Requests/Calendar/UpdateCalendarEventRequest.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using CommunityToolkit.Mvvm.Messaging; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models.Requests; +using Wino.Messaging.Client.Calendar; + +namespace Wino.Core.Requests.Calendar; + +/// +/// Request to update an existing calendar event on the server. +/// The calendar item should be already updated in the local database before queuing this request. +/// +public record UpdateCalendarEventRequest(CalendarItem Item, List Attendees) : CalendarRequestBase(Item) +{ + /// + /// Original attendees before the update, used for reverting changes if the update fails. + /// + public List OriginalAttendees { get; init; } + + /// + /// Original calendar item state before the update, used for reverting changes if the update fails. + /// + public CalendarItem OriginalItem { get; init; } + + public override CalendarSynchronizerOperation Operation => CalendarSynchronizerOperation.UpdateEvent; + + /// + /// After successful update, we need to resync to ensure changes are properly reflected. + /// + public override int ResynchronizationDelay => 2000; + + public override void ApplyUIChanges() + { + // Notify UI that the event was updated locally + WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item, CalendarItemUpdateSource.ClientUpdated)); + } + + public override void RevertUIChanges() + { + // If update fails, restore the original state + if (OriginalItem != null && OriginalAttendees != null) + { + // Send the original item back to restore UI state + WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(OriginalItem, CalendarItemUpdateSource.ClientReverted)); + } + else + { + // Fallback: just notify with current item to trigger refresh + WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item, CalendarItemUpdateSource.ClientReverted)); + } + } +} diff --git a/Wino.Core/Requests/Folder/CreateSubFolderRequest.cs b/Wino.Core/Requests/Folder/CreateSubFolderRequest.cs new file mode 100644 index 00000000..a655cf49 --- /dev/null +++ b/Wino.Core/Requests/Folder/CreateSubFolderRequest.cs @@ -0,0 +1,11 @@ +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models.Requests; + +namespace Wino.Core.Requests.Folder; + +public record CreateSubFolderRequest(MailItemFolder Folder, string NewFolderName) : FolderRequestBase(Folder, FolderSynchronizerOperation.CreateSubFolder) +{ + public override void ApplyUIChanges() { } + public override void RevertUIChanges() { } +} diff --git a/Wino.Core/Requests/Folder/DeleteFolderRequest.cs b/Wino.Core/Requests/Folder/DeleteFolderRequest.cs new file mode 100644 index 00000000..5615e6cf --- /dev/null +++ b/Wino.Core/Requests/Folder/DeleteFolderRequest.cs @@ -0,0 +1,17 @@ +using CommunityToolkit.Mvvm.Messaging; +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models.Requests; +using Wino.Messaging.UI; + +namespace Wino.Core.Requests.Folder; + +public record DeleteFolderRequest(MailItemFolder Folder) : FolderRequestBase(Folder, FolderSynchronizerOperation.DeleteFolder) +{ + public override void ApplyUIChanges() + { + WeakReferenceMessenger.Default.Send(new FolderDeleted(Folder)); + } + + public override void RevertUIChanges() { } +} diff --git a/Wino.Core/Requests/Folder/MarkFolderAsReadRequest.cs b/Wino.Core/Requests/Folder/MarkFolderAsReadRequest.cs index 9cf9a777..242249e2 100644 --- a/Wino.Core/Requests/Folder/MarkFolderAsReadRequest.cs +++ b/Wino.Core/Requests/Folder/MarkFolderAsReadRequest.cs @@ -17,10 +17,14 @@ public record MarkFolderAsReadRequest(MailItemFolder Folder, List Mail foreach (var item in MailsToMarkRead) { + // Skip if already read + if (item.IsRead) continue; + item.IsRead = true; } - WeakReferenceMessenger.Default.Send(new BulkMailUpdatedMessage(MailsToMarkRead)); + WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(item, MailUpdateSource.ClientUpdated, MailCopyChangeFlags.IsRead)); + } } public override void RevertUIChanges() @@ -29,10 +33,14 @@ public record MarkFolderAsReadRequest(MailItemFolder Folder, List Mail foreach (var item in MailsToMarkRead) { + // Skip if already unread (wasn't changed by ApplyUIChanges) + if (!item.IsRead) continue; + item.IsRead = false; } - WeakReferenceMessenger.Default.Send(new BulkMailUpdatedMessage(MailsToMarkRead)); + WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(item, MailUpdateSource.ClientReverted, MailCopyChangeFlags.IsRead)); + } } public List SynchronizationFolderIds => [Folder.Id]; diff --git a/Wino.Core/Requests/Mail/ChangeFlagRequest.cs b/Wino.Core/Requests/Mail/ChangeFlagRequest.cs index 8410cd8e..231435f4 100644 --- a/Wino.Core/Requests/Mail/ChangeFlagRequest.cs +++ b/Wino.Core/Requests/Mail/ChangeFlagRequest.cs @@ -12,24 +12,38 @@ namespace Wino.Core.Requests.Mail; public record ChangeFlagRequest(MailCopy Item, bool IsFlagged) : MailRequestBase(Item), ICustomFolderSynchronizationRequest { + private readonly bool _originalIsFlagged = Item.IsFlagged; + public List SynchronizationFolderIds => [Item.FolderId]; public bool ExcludeMustHaveFolders => true; public override MailSynchronizerOperation Operation => MailSynchronizerOperation.ChangeFlag; + /// + /// Gets whether this request represents an actual state change. + /// If the mail is already in the desired flagged state, no change is needed. + /// + public bool IsNoOp { get; } = Item.IsFlagged == IsFlagged; + public override void ApplyUIChanges() { + // Skip UI update if the mail is already in the desired state + if (IsNoOp) return; + Item.IsFlagged = IsFlagged; - WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item)); + WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.ClientUpdated, MailCopyChangeFlags.IsFlagged)); } public override void RevertUIChanges() { - Item.IsFlagged = !IsFlagged; + // Skip UI revert if this was a no-op request + if (IsNoOp) return; - WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item)); + Item.IsFlagged = _originalIsFlagged; + + WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.ClientReverted, MailCopyChangeFlags.IsFlagged)); } } diff --git a/Wino.Core/Requests/Mail/CreateDraftRequest.cs b/Wino.Core/Requests/Mail/CreateDraftRequest.cs index 569f9f25..7477673f 100644 --- a/Wino.Core/Requests/Mail/CreateDraftRequest.cs +++ b/Wino.Core/Requests/Mail/CreateDraftRequest.cs @@ -1,11 +1,9 @@ using System; using System.Collections.Generic; -using CommunityToolkit.Mvvm.Messaging; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Requests; -using Wino.Messaging.UI; namespace Wino.Core.Requests.Mail; @@ -24,6 +22,7 @@ public record CreateDraftRequest(DraftPreparationRequest DraftPreperationRequest public override void RevertUIChanges() { - WeakReferenceMessenger.Default.Send(new MailRemovedMessage(Item)); + // Keep local draft intact when create-draft synchronization fails. + // This allows users to retry sending the local draft to the server. } } diff --git a/Wino.Core/Requests/Mail/MarkReadRequest.cs b/Wino.Core/Requests/Mail/MarkReadRequest.cs index fded7f8b..28c4fd3b 100644 --- a/Wino.Core/Requests/Mail/MarkReadRequest.cs +++ b/Wino.Core/Requests/Mail/MarkReadRequest.cs @@ -11,24 +11,38 @@ namespace Wino.Core.Requests.Mail; public record MarkReadRequest(MailCopy Item, bool IsRead) : MailRequestBase(Item), ICustomFolderSynchronizationRequest { + private readonly bool _originalIsRead = Item.IsRead; + public List SynchronizationFolderIds => [Item.FolderId]; public override MailSynchronizerOperation Operation => MailSynchronizerOperation.MarkRead; public bool ExcludeMustHaveFolders => true; + /// + /// Gets whether this request represents an actual state change. + /// If the mail is already in the desired read state, no change is needed. + /// + public bool IsNoOp { get; } = Item.IsRead == IsRead; + public override void ApplyUIChanges() { + // Skip UI update if the mail is already in the desired state + if (IsNoOp) return; + Item.IsRead = IsRead; - WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item)); + WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.ClientUpdated, MailCopyChangeFlags.IsRead)); } public override void RevertUIChanges() { - Item.IsRead = !IsRead; + // Skip UI revert if this was a no-op request + if (IsNoOp) return; - WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item)); + Item.IsRead = _originalIsRead; + + WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.ClientReverted, MailCopyChangeFlags.IsRead)); } } diff --git a/Wino.Core/Services/AutoDiscoveryService.cs b/Wino.Core/Services/AutoDiscoveryService.cs index 05cf06d0..10d21137 100644 --- a/Wino.Core/Services/AutoDiscoveryService.cs +++ b/Wino.Core/Services/AutoDiscoveryService.cs @@ -1,7 +1,12 @@ -using System; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; using System.Net.Http; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; +using System.Xml.Linq; using Serilog; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models; @@ -10,47 +15,682 @@ using Wino.Core.Domain.Models.AutoDiscovery; namespace Wino.Core.Services; /// -/// We have 2 methods to do auto discovery. -/// 1. Use https://emailsettings.firetrust.com/settings?q={address} API -/// 2. TODO: Thunderbird auto discovery file. +/// Mail and CalDAV endpoint discovery with Thunderbird-style methods and fallbacks. /// public class AutoDiscoveryService : IAutoDiscoveryService { - private const string FiretrustURL = " https://emailsettings.firetrust.com/settings?q="; + private const string ThunderbirdIspdbUrl = "https://autoconfig.thunderbird.net/v1.1/"; + private const string FiretrustUrl = "https://emailsettings.firetrust.com/settings?q="; + private const string GoogleDnsResolveUrl = "https://dns.google/resolve"; - // TODO: Try Thunderbird Auto Discovery as second approach. + private static readonly ILogger Logger = Log.ForContext(); + private static readonly StringComparer IgnoreCase = StringComparer.OrdinalIgnoreCase; + private static readonly HttpMethod OptionsMethod = new("OPTIONS"); - public Task GetAutoDiscoverySettings(AutoDiscoveryMinimalSettings autoDiscoveryMinimalSettings) - => GetSettingsFromFiretrustAsync(autoDiscoveryMinimalSettings.Email); + private readonly HttpClient _httpClient; + private readonly Dictionary _calDavUriCache = new(IgnoreCase); + private readonly object _calDavCacheLock = new(); - private static async Task GetSettingsFromFiretrustAsync(string mailAddress) + public AutoDiscoveryService(HttpClient httpClient = null) { - using var client = new HttpClient(); - var response = await client.GetAsync($"{FiretrustURL}{mailAddress}"); - - if (response.IsSuccessStatusCode) - return await DeserializeFiretrustResponse(response); - else + _httpClient = httpClient ?? new HttpClient { - Log.Warning($"Firetrust AutoDiscovery failed. ({response.StatusCode})"); - - return null; - } + Timeout = TimeSpan.FromSeconds(15) + }; } - private static async Task DeserializeFiretrustResponse(HttpResponseMessage response) + public async Task GetAutoDiscoverySettings(AutoDiscoveryMinimalSettings autoDiscoveryMinimalSettings) { - try - { - var content = await response.Content.ReadAsStringAsync(); + if (autoDiscoveryMinimalSettings == null || string.IsNullOrWhiteSpace(autoDiscoveryMinimalSettings.Email)) + return null; - return JsonSerializer.Deserialize(content, DomainModelsJsonContext.Default.AutoDiscoverySettings); - } - catch (Exception ex) + if (!TryGetEmailParts(autoDiscoveryMinimalSettings.Email, out var localPart, out var domain)) + return null; + + var cancellationToken = CancellationToken.None; + + var settings = await TryGetThunderbirdSettingsAsync(domain, autoDiscoveryMinimalSettings.Email, localPart, cancellationToken).ConfigureAwait(false) + ?? await TryGetIspdbSettingsAsync(domain, autoDiscoveryMinimalSettings.Email, localPart, cancellationToken).ConfigureAwait(false) + ?? await TryGetMxBasedSettingsAsync(domain, autoDiscoveryMinimalSettings.Email, localPart, cancellationToken).ConfigureAwait(false) + ?? await TryGetSrvBasedSettingsAsync(domain, autoDiscoveryMinimalSettings.Email, cancellationToken).ConfigureAwait(false) + ?? await TryGetGuessedHostSettingsAsync(domain, autoDiscoveryMinimalSettings.Email, cancellationToken).ConfigureAwait(false) + ?? await GetSettingsFromFiretrustAsync(autoDiscoveryMinimalSettings.Email, cancellationToken).ConfigureAwait(false); + + if (settings != null && string.IsNullOrWhiteSpace(settings.Domain)) { - Log.Error(ex, "Failed to deserialize Firetrust response."); + settings.Domain = domain; + } + + return settings; + } + + public async Task DiscoverCalDavServiceUriAsync(string mailAddress, CancellationToken cancellationToken = default) + { + if (!TryGetEmailParts(mailAddress, out _, out var domain)) + return null; + + lock (_calDavCacheLock) + { + if (_calDavUriCache.TryGetValue(domain, out var cachedUri)) + return cachedUri; + } + + var knownProviderUri = TryGetKnownProviderCalDavUri(domain); + if (knownProviderUri != null) + { + CacheCalDavUri(domain, knownProviderUri); + return knownProviderUri; + } + + foreach (var candidate in GetCalDavCandidates(domain)) + { + var resolved = await TryResolveCalDavEndpointAsync(candidate, cancellationToken).ConfigureAwait(false); + if (resolved == null) + continue; + + CacheCalDavUri(domain, resolved); + return resolved; } return null; } + + private async Task TryGetThunderbirdSettingsAsync( + string lookupDomain, + string email, + string localPart, + CancellationToken cancellationToken) + { + foreach (var endpoint in BuildThunderbirdEndpoints(lookupDomain, email)) + { + var settings = await TryGetSettingsFromXmlEndpointAsync(endpoint, email, localPart, lookupDomain, cancellationToken).ConfigureAwait(false); + if (settings != null) + return settings; + } + + return null; + } + + private async Task TryGetIspdbSettingsAsync( + string lookupDomain, + string email, + string localPart, + CancellationToken cancellationToken) + { + var endpoint = $"{ThunderbirdIspdbUrl}{lookupDomain}?emailaddress={Uri.EscapeDataString(email)}"; + return await TryGetSettingsFromXmlEndpointAsync(endpoint, email, localPart, lookupDomain, cancellationToken).ConfigureAwait(false); + } + + private async Task TryGetMxBasedSettingsAsync( + string domain, + string email, + string localPart, + CancellationToken cancellationToken) + { + var mxDomains = await GetMxSearchDomainsAsync(domain, cancellationToken).ConfigureAwait(false); + + foreach (var mxDomain in mxDomains) + { + if (IgnoreCase.Equals(mxDomain, domain)) + continue; + + var settings = await TryGetThunderbirdSettingsAsync(mxDomain, email, localPart, cancellationToken).ConfigureAwait(false) + ?? await TryGetIspdbSettingsAsync(mxDomain, email, localPart, cancellationToken).ConfigureAwait(false); + + if (settings != null) + return settings; + } + + return null; + } + + private async Task TryGetSrvBasedSettingsAsync( + string domain, + string email, + CancellationToken cancellationToken) + { + var incoming = await TryResolveSrvRecordAsync($"_imaps._tcp.{domain}", "IMAP", "SSL", cancellationToken).ConfigureAwait(false) + ?? await TryResolveSrvRecordAsync($"_imap._tcp.{domain}", "IMAP", "STARTTLS", cancellationToken).ConfigureAwait(false); + + var outgoing = await TryResolveSrvRecordAsync($"_submissions._tcp.{domain}", "SMTP", "SSL", cancellationToken).ConfigureAwait(false) + ?? await TryResolveSrvRecordAsync($"_submission._tcp.{domain}", "SMTP", "STARTTLS", cancellationToken).ConfigureAwait(false) + ?? await TryResolveSrvRecordAsync($"_smtp._tcp.{domain}", "SMTP", "STARTTLS", cancellationToken).ConfigureAwait(false); + + if (incoming == null || outgoing == null) + return null; + + incoming.Username = email; + outgoing.Username = email; + + return new AutoDiscoverySettings + { + Domain = domain, + Settings = [incoming, outgoing] + }; + } + + private async Task TryGetGuessedHostSettingsAsync( + string domain, + string email, + CancellationToken cancellationToken) + { + var imapHost = await GetFirstResolvableHostAsync( + [$"imap.{domain}", $"mail.{domain}", domain], + cancellationToken).ConfigureAwait(false); + + var smtpHost = await GetFirstResolvableHostAsync( + [$"smtp.{domain}", $"mail.{domain}", domain], + cancellationToken).ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(imapHost) || string.IsNullOrWhiteSpace(smtpHost)) + return null; + + return new AutoDiscoverySettings + { + Domain = domain, + Settings = + [ + new AutoDiscoveryProviderSetting + { + Protocol = "IMAP", + Address = imapHost, + Port = 993, + Secure = "SSL", + Username = email + }, + new AutoDiscoveryProviderSetting + { + Protocol = "SMTP", + Address = smtpHost, + Port = 587, + Secure = "STARTTLS", + Username = email + } + ] + }; + } + + private async Task TryGetSettingsFromXmlEndpointAsync( + string endpoint, + string email, + string localPart, + string domain, + CancellationToken cancellationToken) + { + try + { + using var response = await _httpClient.GetAsync(endpoint, cancellationToken).ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + return null; + + var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + return ParseThunderbirdSettings(content, email, localPart, domain); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + Logger.Debug(ex, "Failed to read autodiscovery XML endpoint {Endpoint}", endpoint); + return null; + } + } + + private static AutoDiscoverySettings ParseThunderbirdSettings(string xmlContent, string email, string localPart, string domain) + { + if (string.IsNullOrWhiteSpace(xmlContent)) + return null; + + try + { + var document = XDocument.Parse(xmlContent); + + var incomingServers = document + .Descendants() + .Where(e => e.Name.LocalName == "incomingServer") + .Where(e => string.Equals((string)e.Attribute("type"), "imap", StringComparison.OrdinalIgnoreCase)) + .Select(e => ParseThunderbirdServer(e, "IMAP", email, localPart, domain)) + .Where(e => e != null) + .ToList(); + + var outgoingServers = document + .Descendants() + .Where(e => e.Name.LocalName == "outgoingServer") + .Where(e => string.Equals((string)e.Attribute("type"), "smtp", StringComparison.OrdinalIgnoreCase)) + .Select(e => ParseThunderbirdServer(e, "SMTP", email, localPart, domain)) + .Where(e => e != null) + .ToList(); + + var bestIncoming = SelectBestServerSetting(incomingServers); + var bestOutgoing = SelectBestServerSetting(outgoingServers); + + if (bestIncoming == null || bestOutgoing == null) + return null; + + return new AutoDiscoverySettings + { + Domain = domain, + Settings = [bestIncoming, bestOutgoing] + }; + } + catch (Exception ex) + { + Logger.Debug(ex, "Failed to parse Thunderbird autodiscovery XML."); + return null; + } + } + + private static AutoDiscoveryProviderSetting ParseThunderbirdServer( + XElement serverElement, + string protocol, + string email, + string localPart, + string domain) + { + var address = ResolveTemplate(GetElementValue(serverElement, "hostname"), email, localPart, domain); + var username = ResolveTemplate(GetElementValue(serverElement, "username"), email, localPart, domain); + var socketType = ResolveTemplate(GetElementValue(serverElement, "socketType"), email, localPart, domain); + + if (string.IsNullOrWhiteSpace(address)) + return null; + + if (!int.TryParse(GetElementValue(serverElement, "port"), out var port)) + return null; + + return new AutoDiscoveryProviderSetting + { + Protocol = protocol, + Address = address.Trim(), + Port = port, + Secure = socketType?.Trim() ?? string.Empty, + Username = string.IsNullOrWhiteSpace(username) ? email : username.Trim() + }; + } + + private static AutoDiscoveryProviderSetting SelectBestServerSetting(IReadOnlyCollection settings) + { + if (settings == null || settings.Count == 0) + return null; + + return settings + .OrderByDescending(GetSecurityScore) + .ThenBy(s => s.Port) + .FirstOrDefault(); + } + + private static int GetSecurityScore(AutoDiscoveryProviderSetting setting) + { + if (setting == null) + return 0; + + var secureValue = setting.Secure ?? string.Empty; + + if (secureValue.Contains("SSL", StringComparison.OrdinalIgnoreCase) || + secureValue.Contains("TLS", StringComparison.OrdinalIgnoreCase)) + { + return 3; + } + + if (secureValue.Contains("STARTTLS", StringComparison.OrdinalIgnoreCase)) + return 2; + + return 1; + } + + private static string GetElementValue(XElement element, string localName) + => element.Elements().FirstOrDefault(e => e.Name.LocalName == localName)?.Value; + + private static string ResolveTemplate(string value, string email, string localPart, string domain) + { + if (string.IsNullOrWhiteSpace(value)) + return value; + + return value + .Replace("%EMAILADDRESS%", email, StringComparison.OrdinalIgnoreCase) + .Replace("%EMAILLOCALPART%", localPart, StringComparison.OrdinalIgnoreCase) + .Replace("%EMAILDOMAIN%", domain, StringComparison.OrdinalIgnoreCase); + } + + private static IEnumerable BuildThunderbirdEndpoints(string domain, string email) + { + var escapedEmail = Uri.EscapeDataString(email); + yield return $"https://autoconfig.{domain}/mail/config-v1.1.xml?emailaddress={escapedEmail}"; + yield return $"https://{domain}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={escapedEmail}"; + } + + private async Task GetSettingsFromFiretrustAsync(string mailAddress, CancellationToken cancellationToken) + { + try + { + using var response = await _httpClient.GetAsync($"{FiretrustUrl}{Uri.EscapeDataString(mailAddress)}", cancellationToken).ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + { + Logger.Warning("Firetrust autodiscovery failed with status {StatusCode}", response.StatusCode); + return null; + } + + var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + return JsonSerializer.Deserialize(content, DomainModelsJsonContext.Default.AutoDiscoverySettings); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + Logger.Error(ex, "Failed to deserialize Firetrust autodiscovery response."); + return null; + } + } + + private async Task TryResolveSrvRecordAsync( + string queryName, + string protocol, + string secureHint, + CancellationToken cancellationToken) + { + var records = await QueryDnsAsync(queryName, "SRV", cancellationToken).ConfigureAwait(false); + var srvRecord = records + .Select(ParseSrvRecord) + .Where(r => r != null) + .OrderBy(r => r.Priority) + .ThenBy(r => r.Weight) + .FirstOrDefault(); + + if (srvRecord == null) + return null; + + return new AutoDiscoveryProviderSetting + { + Protocol = protocol, + Address = srvRecord.Target, + Port = srvRecord.Port, + Secure = secureHint + }; + } + + private async Task> GetMxSearchDomainsAsync(string domain, CancellationToken cancellationToken) + { + var results = new List { domain }; + var records = await QueryDnsAsync(domain, "MX", cancellationToken).ConfigureAwait(false); + + var hosts = records + .Select(ParseMxRecord) + .Where(r => r != null) + .OrderBy(r => r.Preference) + .Select(r => r.Target) + .Distinct(IgnoreCase) + .ToList(); + + foreach (var host in hosts) + { + foreach (var candidateDomain in BuildDomainCandidatesFromHost(host)) + { + if (!results.Contains(candidateDomain, IgnoreCase)) + { + results.Add(candidateDomain); + } + } + } + + return results; + } + + private async Task GetFirstResolvableHostAsync(IEnumerable hostCandidates, CancellationToken cancellationToken) + { + foreach (var host in hostCandidates.Where(h => !string.IsNullOrWhiteSpace(h)).Distinct(IgnoreCase)) + { + if (await HasAnyDnsAddressRecordAsync(host, cancellationToken).ConfigureAwait(false)) + return host; + } + + return null; + } + + private async Task HasAnyDnsAddressRecordAsync(string host, CancellationToken cancellationToken) + { + var aRecords = await QueryDnsAsync(host, "A", cancellationToken).ConfigureAwait(false); + if (aRecords.Count > 0) + return true; + + var aaaaRecords = await QueryDnsAsync(host, "AAAA", cancellationToken).ConfigureAwait(false); + return aaaaRecords.Count > 0; + } + + private async Task> QueryDnsAsync(string queryName, string queryType, CancellationToken cancellationToken) + { + try + { + var url = $"{GoogleDnsResolveUrl}?name={Uri.EscapeDataString(queryName)}&type={Uri.EscapeDataString(queryType)}"; + using var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + return Array.Empty(); + + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false); + + if (!document.RootElement.TryGetProperty("Answer", out var answerArray) || + answerArray.ValueKind != JsonValueKind.Array) + { + return Array.Empty(); + } + + var values = new List(); + + foreach (var answer in answerArray.EnumerateArray()) + { + if (answer.TryGetProperty("data", out var dataNode) && dataNode.ValueKind == JsonValueKind.String) + { + var data = dataNode.GetString(); + if (!string.IsNullOrWhiteSpace(data)) + values.Add(data); + } + } + + return values; + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + Logger.Debug(ex, "DNS-over-HTTPS query failed for {QueryName} ({Type})", queryName, queryType); + return Array.Empty(); + } + } + + private async Task TryResolveCalDavEndpointAsync(Uri candidate, CancellationToken cancellationToken) + { + var getResult = await ProbeCalDavEndpointAsync(candidate, HttpMethod.Get, cancellationToken).ConfigureAwait(false); + if (getResult != null) + return getResult; + + return await ProbeCalDavEndpointAsync(candidate, OptionsMethod, cancellationToken).ConfigureAwait(false); + } + + private async Task ProbeCalDavEndpointAsync(Uri uri, HttpMethod method, CancellationToken cancellationToken) + { + try + { + using var request = new HttpRequestMessage(method, uri); + using var response = await _httpClient + .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken) + .ConfigureAwait(false); + + if (TryResolveRedirectTarget(uri, response, out var redirectTarget)) + return redirectTarget; + + if (!IsPossibleCalDavEndpoint(response)) + return null; + + return response.RequestMessage?.RequestUri ?? uri; + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + Logger.Debug(ex, "CalDAV probe failed for {Uri} with method {Method}", uri, method); + return null; + } + } + + private static bool IsPossibleCalDavEndpoint(HttpResponseMessage response) + { + if (response == null) + return false; + + if (response.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden or HttpStatusCode.MultiStatus) + return true; + + var hasDavHeader = response.Headers.Contains("DAV"); + var hasDavMethod = response.Headers.TryGetValues("Allow", out var allowValues) + && allowValues.Any(value => + value.Contains("PROPFIND", StringComparison.OrdinalIgnoreCase) || + value.Contains("REPORT", StringComparison.OrdinalIgnoreCase)); + + if (response.StatusCode == HttpStatusCode.MethodNotAllowed) + return hasDavHeader || hasDavMethod; + + return response.IsSuccessStatusCode && (hasDavHeader || hasDavMethod); + } + + private static bool TryResolveRedirectTarget(Uri baseUri, HttpResponseMessage response, out Uri resolvedUri) + { + resolvedUri = null; + + if (response == null || !IsRedirectStatusCode(response.StatusCode)) + return false; + + if (response.Headers.Location == null) + return false; + + resolvedUri = response.Headers.Location.IsAbsoluteUri + ? response.Headers.Location + : new Uri(baseUri, response.Headers.Location); + + return true; + } + + private static bool IsRedirectStatusCode(HttpStatusCode statusCode) + => statusCode == HttpStatusCode.MovedPermanently + || statusCode == HttpStatusCode.Found + || statusCode == HttpStatusCode.RedirectMethod + || statusCode == HttpStatusCode.TemporaryRedirect + || (int)statusCode == 308; + + private static Uri TryGetKnownProviderCalDavUri(string domain) + { + if (domain.EndsWith("icloud.com", StringComparison.OrdinalIgnoreCase) || + domain.EndsWith("me.com", StringComparison.OrdinalIgnoreCase) || + domain.EndsWith("mac.com", StringComparison.OrdinalIgnoreCase)) + { + return new Uri("https://caldav.icloud.com/"); + } + + if (domain.Contains("yahoo.", StringComparison.OrdinalIgnoreCase) || + domain.EndsWith("aol.com", StringComparison.OrdinalIgnoreCase)) + { + return new Uri("https://caldav.calendar.yahoo.com/"); + } + + return null; + } + + private static IEnumerable GetCalDavCandidates(string domain) + { + foreach (var candidateDomain in BuildDomainCandidatesFromHost(domain)) + { + yield return new Uri($"https://{candidateDomain}/.well-known/caldav"); + yield return new Uri($"https://caldav.{candidateDomain}/"); + } + } + + private static IEnumerable BuildDomainCandidatesFromHost(string hostOrDomain) + { + if (string.IsNullOrWhiteSpace(hostOrDomain)) + yield break; + + var normalized = hostOrDomain.Trim().TrimEnd('.'); + if (string.IsNullOrWhiteSpace(normalized)) + yield break; + + yield return normalized; + + var segments = normalized.Split('.', StringSplitOptions.RemoveEmptyEntries); + if (segments.Length > 2) + { + yield return string.Join('.', segments.Skip(1)); + } + } + + private static bool TryGetEmailParts(string email, out string localPart, out string domain) + { + localPart = null; + domain = null; + + if (string.IsNullOrWhiteSpace(email)) + return false; + + var separatorIndex = email.IndexOf('@'); + if (separatorIndex <= 0 || separatorIndex >= email.Length - 1) + return false; + + localPart = email[..separatorIndex]; + domain = email[(separatorIndex + 1)..]; + return !string.IsNullOrWhiteSpace(localPart) && !string.IsNullOrWhiteSpace(domain); + } + + private void CacheCalDavUri(string domain, Uri calDavUri) + { + lock (_calDavCacheLock) + { + _calDavUriCache[domain] = calDavUri; + } + } + + private static SrvRecord ParseSrvRecord(string rawValue) + { + if (string.IsNullOrWhiteSpace(rawValue)) + return null; + + var parts = rawValue.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length < 4) + return null; + + if (!ushort.TryParse(parts[0], out var priority) || + !ushort.TryParse(parts[1], out var weight) || + !int.TryParse(parts[2], out var port)) + { + return null; + } + + var target = parts[3].Trim().TrimEnd('.'); + if (string.IsNullOrWhiteSpace(target)) + return null; + + return new SrvRecord(priority, weight, port, target); + } + + private static MxRecord ParseMxRecord(string rawValue) + { + if (string.IsNullOrWhiteSpace(rawValue)) + return null; + + var parts = rawValue.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length < 2 || !ushort.TryParse(parts[0], out var preference)) + return null; + + var target = parts[1].Trim().TrimEnd('.'); + if (string.IsNullOrWhiteSpace(target)) + return null; + + return new MxRecord(preference, target); + } + + private sealed record SrvRecord(ushort Priority, ushort Weight, int Port, string Target); + private sealed record MxRecord(ushort Preference, string Target); } diff --git a/Wino.Core/Services/GmailSynchronizerErrorHandlingFactory.cs b/Wino.Core/Services/GmailSynchronizerErrorHandlingFactory.cs index b2153c32..0f64aaf1 100644 --- a/Wino.Core/Services/GmailSynchronizerErrorHandlingFactory.cs +++ b/Wino.Core/Services/GmailSynchronizerErrorHandlingFactory.cs @@ -1,11 +1,27 @@ -using System.Threading.Tasks; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Errors; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Synchronizers.Errors; +using Wino.Core.Synchronizers.Errors.Gmail; namespace Wino.Core.Services; + +/// +/// Factory for handling Gmail synchronizer errors. +/// Registers and routes errors to appropriate handlers. +/// public class GmailSynchronizerErrorHandlingFactory : SynchronizerErrorHandlingFactory, IGmailSynchronizerErrorHandlerFactory { - public bool CanHandle(SynchronizerErrorContext error) => CanHandle(error); - - public Task HandleAsync(SynchronizerErrorContext error) => HandleErrorAsync(error); + public GmailSynchronizerErrorHandlingFactory( + GmailAuthenticationFailedHandler authenticationFailedHandler, + GmailQuotaExceededHandler quotaExceededHandler, + GmailRateLimitHandler rateLimitHandler, + GmailHistoryExpiredHandler historyExpiredHandler, + EntityNotFoundHandler entityNotFoundHandler) + { + // Order matters - more specific handlers should be registered first + RegisterHandler(authenticationFailedHandler); + RegisterHandler(quotaExceededHandler); + RegisterHandler(historyExpiredHandler); + RegisterHandler(entityNotFoundHandler); + RegisterHandler(rateLimitHandler); // Most generic rate limit handler last + } } diff --git a/Wino.Core/Services/ImapSynchronizerErrorHandlingFactory.cs b/Wino.Core/Services/ImapSynchronizerErrorHandlingFactory.cs new file mode 100644 index 00000000..2a319896 --- /dev/null +++ b/Wino.Core/Services/ImapSynchronizerErrorHandlingFactory.cs @@ -0,0 +1,27 @@ +using Wino.Core.Domain.Interfaces; +using Wino.Core.Synchronizers.Errors; +using Wino.Core.Synchronizers.Errors.Imap; + +namespace Wino.Core.Services; + +/// +/// Factory for handling IMAP synchronizer errors. +/// Registers and routes errors to appropriate handlers. +/// +public class ImapSynchronizerErrorHandlingFactory : SynchronizerErrorHandlingFactory, IImapSynchronizerErrorHandlerFactory +{ + public ImapSynchronizerErrorHandlingFactory( + ImapConnectionLostHandler connectionLostHandler, + ImapAuthenticationFailedHandler authFailedHandler, + EntityNotFoundHandler entityNotFoundHandler, + ImapFolderNotFoundHandler folderNotFoundHandler, + ImapProtocolErrorHandler protocolErrorHandler) + { + // Order matters - more specific handlers should be registered first + RegisterHandler(authFailedHandler); + RegisterHandler(entityNotFoundHandler); + RegisterHandler(folderNotFoundHandler); + RegisterHandler(connectionLostHandler); + RegisterHandler(protocolErrorHandler); // Most generic, registered last + } +} diff --git a/Wino.Core/Services/ImapTestService.cs b/Wino.Core/Services/ImapTestService.cs index 437f72ce..9f8e7065 100644 --- a/Wino.Core/Services/ImapTestService.cs +++ b/Wino.Core/Services/ImapTestService.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using System.Threading.Tasks; using MailKit.Net.Smtp; using Wino.Core.Domain.Entities.Shared; @@ -11,69 +9,32 @@ namespace Wino.Core.Services; public class ImapTestService : IImapTestService { - public const string ProtocolLogFileName = "ImapProtocolLog.log"; - - private readonly IPreferencesService _preferencesService; - private readonly IApplicationConfiguration _appInitializerService; - - private Stream _protocolLogStream; - - public ImapTestService(IPreferencesService preferencesService, IApplicationConfiguration appInitializerService) + public ImapTestService() { - _preferencesService = preferencesService; - _appInitializerService = appInitializerService; - } - - private void EnsureProtocolLogFileExists() - { - // Create new file for protocol logger. - var localAppFolderPath = _appInitializerService.ApplicationDataFolderPath; - - var logFile = Path.Combine(localAppFolderPath, ProtocolLogFileName); - - if (File.Exists(logFile)) - File.Delete(logFile); - - _protocolLogStream = File.Create(logFile); } public async Task TestImapConnectionAsync(CustomServerInformation serverInformation, bool allowSSLHandShake) { - try + var poolOptions = ImapClientPoolOptions.CreateTestPool(serverInformation); + + using (var clientPool = new ImapClientPool(poolOptions) { - EnsureProtocolLogFileExists(); - - var poolOptions = ImapClientPoolOptions.CreateTestPool(serverInformation, _protocolLogStream); - - var clientPool = new ImapClientPool(poolOptions) - { - ThrowOnSSLHandshakeCallback = !allowSSLHandShake - }; - - using (clientPool) - { - // This call will make sure that everything is authenticated + connected successfully. - var client = await clientPool.GetClientAsync(); - - clientPool.Release(client); - } - - // Test SMTP connectivity. - using var smtpClient = new SmtpClient(); - - if (!smtpClient.IsConnected) - await smtpClient.ConnectAsync(serverInformation.OutgoingServer, int.Parse(serverInformation.OutgoingServerPort), MailKit.Security.SecureSocketOptions.Auto); - - if (!smtpClient.IsAuthenticated) - await smtpClient.AuthenticateAsync(serverInformation.OutgoingServerUsername, serverInformation.OutgoingServerPassword); - } - catch (Exception) + ThrowOnSSLHandshakeCallback = !allowSSLHandShake + }) { - throw; - } - finally - { - _protocolLogStream?.Dispose(); + // This call will make sure that everything is authenticated + connected successfully. + var client = await clientPool.GetClientAsync(); + + clientPool.Release(client); } + + // Test SMTP connectivity. + using var smtpClient = new SmtpClient(); + + if (!smtpClient.IsConnected) + await smtpClient.ConnectAsync(serverInformation.OutgoingServer, int.Parse(serverInformation.OutgoingServerPort), MailKit.Security.SecureSocketOptions.Auto); + + if (!smtpClient.IsAuthenticated) + await smtpClient.AuthenticateAsync(serverInformation.OutgoingServerUsername, serverInformation.OutgoingServerPassword); } } diff --git a/Wino.Core/Services/OutlookSynchronizerErrorHandlingFactory.cs b/Wino.Core/Services/OutlookSynchronizerErrorHandlingFactory.cs index 4947060f..91f411d3 100644 --- a/Wino.Core/Services/OutlookSynchronizerErrorHandlingFactory.cs +++ b/Wino.Core/Services/OutlookSynchronizerErrorHandlingFactory.cs @@ -1,18 +1,21 @@ -using System.Threading.Tasks; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Errors; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Synchronizers.Errors; using Wino.Core.Synchronizers.Errors.Outlook; namespace Wino.Core.Services; public class OutlookSynchronizerErrorHandlingFactory : SynchronizerErrorHandlingFactory, IOutlookSynchronizerErrorHandlerFactory { - public OutlookSynchronizerErrorHandlingFactory(ObjectCannotBeDeletedHandler objectCannotBeDeleted) + public OutlookSynchronizerErrorHandlingFactory(OutlookAuthenticationFailedHandler authenticationFailedHandler, + ObjectCannotBeDeletedHandler objectCannotBeDeleted, + EntityNotFoundHandler entityNotFoundHandler, + DeltaTokenExpiredHandler deltaTokenExpiredHandler, + OutlookRateLimitHandler outlookRateLimitHandler) { + RegisterHandler(authenticationFailedHandler); + RegisterHandler(outlookRateLimitHandler); RegisterHandler(objectCannotBeDeleted); + RegisterHandler(entityNotFoundHandler); + RegisterHandler(deltaTokenExpiredHandler); } - - public bool CanHandle(SynchronizerErrorContext error) => CanHandle(error); - - public Task HandleAsync(SynchronizerErrorContext error) => HandleErrorAsync(error); } diff --git a/Wino.Core/Services/RetryExecutor.cs b/Wino.Core/Services/RetryExecutor.cs new file mode 100644 index 00000000..f59d5ffb --- /dev/null +++ b/Wino.Core/Services/RetryExecutor.cs @@ -0,0 +1,140 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Serilog; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Retry; +using Wino.Core.Domain.Models.Synchronization; + +namespace Wino.Core.Services; + +/// +/// Executes operations with automatic retry and error handling support. +/// Implements exponential backoff with jitter. +/// +public class RetryExecutor : IRetryExecutor +{ + private readonly ILogger _logger = Log.ForContext(); + + /// + public async Task ExecuteWithRetryAsync( + Func> operation, + RetryPolicy policy, + Func errorContextFactory, + ISynchronizerErrorHandlerFactory errorHandler = null, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(operation); + ArgumentNullException.ThrowIfNull(policy); + ArgumentNullException.ThrowIfNull(errorContextFactory); + + int attempt = 0; + Exception lastException = null; + + while (attempt <= policy.MaxRetries) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + return await operation(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + throw; // Don't retry on cancellation + } + catch (Exception ex) + { + lastException = ex; + attempt++; + + var errorContext = errorContextFactory(ex); + errorContext.RetryCount = attempt; + errorContext.MaxRetries = policy.MaxRetries; + + // Let the error handler process the error first + if (errorHandler != null) + { + try + { + var handled = await errorHandler.HandleErrorAsync(errorContext).ConfigureAwait(false); + if (handled) + { + _logger.Debug("Error handled by error handler, severity: {Severity}", errorContext.Severity); + } + } + catch (Exception handlerEx) + { + _logger.Warning(handlerEx, "Error handler threw an exception"); + } + } + + // Check if we should retry based on error severity + if (errorContext.Severity == SynchronizerErrorSeverity.Fatal || + errorContext.Severity == SynchronizerErrorSeverity.AuthRequired) + { + _logger.Warning(ex, "Non-retryable error (severity: {Severity}), failing immediately", errorContext.Severity); + throw; + } + + if (errorContext.Severity == SynchronizerErrorSeverity.Recoverable) + { + _logger.Debug(ex, "Recoverable error, not retrying but allowing continuation"); + throw; + } + + // Transient error - check if we have retries left + if (attempt > policy.MaxRetries) + { + _logger.Warning(ex, "All {MaxRetries} retries exhausted", policy.MaxRetries); + throw; + } + + // Calculate delay and wait + var delay = errorContext.RetryDelay ?? policy.GetDelay(attempt); + _logger.Debug("Retry attempt {Attempt}/{MaxRetries} after {Delay}ms delay for error: {ErrorMessage}", + attempt, policy.MaxRetries, delay.TotalMilliseconds, ex.Message); + + await Task.Delay(delay, cancellationToken).ConfigureAwait(false); + } + } + + // Should not reach here, but just in case + throw lastException ?? new InvalidOperationException("Retry loop completed without result"); + } + + /// + public async Task ExecuteWithRetryAsync( + Func operation, + RetryPolicy policy, + Func errorContextFactory, + ISynchronizerErrorHandlerFactory errorHandler = null, + CancellationToken cancellationToken = default) + { + await ExecuteWithRetryAsync( + async ct => + { + await operation(ct).ConfigureAwait(false); + return true; // Dummy return value + }, + policy, + errorContextFactory, + errorHandler, + cancellationToken).ConfigureAwait(false); + } + + /// + public Task ExecuteWithRetryAsync( + Func> operation, + Func errorContextFactory, + CancellationToken cancellationToken = default) + { + return ExecuteWithRetryAsync( + operation, + RetryPolicy.Default, + errorContextFactory, + null, + cancellationToken); + } +} diff --git a/Wino.Core/Services/SynchronizationManager.cs b/Wino.Core/Services/SynchronizationManager.cs new file mode 100644 index 00000000..798431f5 --- /dev/null +++ b/Wino.Core/Services/SynchronizationManager.cs @@ -0,0 +1,829 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.Messaging; +using Serilog; +using Wino.Core.Domain; +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Exceptions; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Authentication; +using Wino.Core.Domain.Models.Connectivity; +using Wino.Core.Domain.Models.Synchronization; +using Wino.Messaging.UI; + +namespace Wino.Core.Services; + +/// +/// Singleton manager that handles synchronizer instances and operations for all accounts. +/// Replaces the old WinoServerConnectionManager functionality. +/// +public class SynchronizationManager : ISynchronizationManager +{ + private static readonly Lazy _instance = new(() => new SynchronizationManager()); + public static SynchronizationManager Instance => _instance.Value; + + private readonly ConcurrentDictionary _synchronizerCache = new(); + private readonly ConcurrentDictionary _accountSynchronizationCancellationSources = new(); + private readonly ConcurrentDictionary _calendarSynchronizationLocks = new(); + private readonly SemaphoreSlim _initializationSemaphore = new(1, 1); + private readonly ILogger _logger = Log.ForContext(); + + private SynchronizerFactory _concreteSynchronizerFactory; + private IImapTestService _imapTestService; + private IAccountService _accountService; + private IAuthenticationProvider _authenticationProvider; + private INotificationBuilder _notificationBuilder; + + private bool _isInitialized = false; + + private SynchronizationManager() { } + + /// + /// Initializes the SynchronizationManager with required dependencies. + /// This must be called before using any other methods. + /// Note: Synchronizers are created lazily to avoid requiring window handles during app initialization. + /// + /// Factory for creating synchronizers + /// Service for testing IMAP connectivity + /// Service for account operations + /// Provider for OAuth authentication + public async Task InitializeAsync(ISynchronizerFactory synchronizerFactory, + IImapTestService imapTestService, + IAccountService accountService, + INotificationBuilder notificationBuilder, + IAuthenticationProvider authenticationProvider) + { + await _initializationSemaphore.WaitAsync(); + + try + { + if (_isInitialized) return; + + _concreteSynchronizerFactory = synchronizerFactory as SynchronizerFactory ?? throw new ArgumentException("SynchronizerFactory must be the concrete implementation"); + _imapTestService = imapTestService ?? throw new ArgumentNullException(nameof(imapTestService)); + _accountService = accountService ?? throw new ArgumentNullException(nameof(accountService)); + _authenticationProvider = authenticationProvider ?? throw new ArgumentNullException(nameof(authenticationProvider)); + _notificationBuilder = notificationBuilder ?? throw new ArgumentNullException(nameof(notificationBuilder)); + + // DO NOT create synchronizers here to avoid requiring window handles during initialization. + // Synchronizers will be created lazily when first accessed via GetOrCreateSynchronizerAsync. + + _isInitialized = true; + _logger.Information("SynchronizationManager dependencies initialized. Synchronizers will be created lazily."); + } + finally + { + _initializationSemaphore.Release(); + } + } + + /// + /// Tests IMAP server connectivity for the given server information. + /// + /// Server information to test + /// Whether to allow SSL handshake + /// Test results indicating success or failure with details + public async Task TestImapConnectivityAsync(CustomServerInformation serverInformation, bool allowSSLHandshake) + { + EnsureInitialized(); + + try + { + _logger.Information("Testing IMAP connectivity for {Server}:{Port}", + serverInformation.IncomingServer, + serverInformation.IncomingServerPort); + + await _imapTestService.TestImapConnectionAsync(serverInformation, allowSSLHandshake); + + _logger.Information("IMAP connectivity test successful"); + return ImapConnectivityTestResults.Success(); + } + catch (ImapTestSSLCertificateException sslTestException) + { + _logger.Warning("IMAP connectivity test requires SSL certificate confirmation"); + return ImapConnectivityTestResults.CertificateUIRequired( + sslTestException.Issuer, + sslTestException.ExpirationDateString, + sslTestException.ValidFromDateString); + } + catch (ImapClientPoolException clientPoolException) + { + _logger.Error(clientPoolException, "IMAP connectivity test failed"); + return ImapConnectivityTestResults.Failure(clientPoolException); + } + catch (Exception exception) + { + _logger.Error(exception, "IMAP connectivity test failed"); + return ImapConnectivityTestResults.Failure(exception); + } + } + + /// + /// Starts a new mail synchronization for the given account. + /// + /// Mail synchronization options + /// Cancellation token + /// Synchronization result + public async Task SynchronizeMailAsync(MailSynchronizationOptions options, + CancellationToken cancellationToken = default) + { + EnsureInitialized(); + + if (await IsSynchronizationBlockedByAttentionAsync(options.AccountId).ConfigureAwait(false)) + { + _logger.Information("Skipping mail synchronization for account {AccountId} because it requires credential attention.", options.AccountId); + return MailSynchronizationResult.Canceled; + } + + var synchronizer = await GetOrCreateSynchronizerAsync(options.AccountId); + if (synchronizer == null) + { + _logger.Error("Could not find or create synchronizer for account {AccountId}", options.AccountId); + + return MailSynchronizationResult.Failed(new Exception("Can't create/get synchronizer.")); + } + + _logger.Information("Starting mail synchronization for account {AccountId} with type {SyncType}", + options.AccountId, options.Type); + + var accountCancellationSource = _accountSynchronizationCancellationSources.GetOrAdd(options.AccountId, _ => new CancellationTokenSource()); + using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, + accountCancellationSource.Token); + + try + { + var result = await synchronizer.SynchronizeMailsAsync(options, linkedCancellationTokenSource.Token); + + _logger.Information("Mail synchronization completed for account {AccountId} with state {State}", + options.AccountId, result.CompletedState); + + // Create notifications. + if (result.DownloadedMessages?.Any() ?? false) + await _notificationBuilder.CreateNotificationsAsync(result.DownloadedMessages); + + await _notificationBuilder.UpdateTaskbarIconBadgeAsync(); + + return result; + } + catch (OperationCanceledException) + { + _logger.Information("Mail synchronization canceled for account {AccountId}", options.AccountId); + return MailSynchronizationResult.Canceled; + } + catch (AuthenticationAttentionException authEx) + { + _logger.Warning("Account {AccountId} requires attention due to authentication issues", options.AccountId); + await SetInvalidCredentialAttentionAsync(authEx.Account).ConfigureAwait(false); + + // Create app notification for authentication attention + _notificationBuilder.CreateAttentionRequiredNotification(authEx.Account); + + return MailSynchronizationResult.Failed(authEx); + } + catch (Exception ex) + { + _logger.Error(ex, "Mail synchronization failed for account {AccountId}", options.AccountId); + return MailSynchronizationResult.Failed(ex); + } + } + + /// + /// Checks if there is an ongoing synchronization for the given account. + /// + /// Account ID to check + /// True if synchronization is ongoing, false otherwise + public bool IsAccountSynchronizing(Guid accountId) + { + EnsureInitialized(); + + if (_synchronizerCache.TryGetValue(accountId, out var synchronizer)) + { + return synchronizer.State == AccountSynchronizerState.Synchronizing || + synchronizer.State == AccountSynchronizerState.ExecutingRequests; + } + + return false; + } + + /// + /// Queues a request to the corresponding account's synchronizer with optional synchronization triggering. + /// Automatically determines whether to trigger mail or calendar synchronization based on the request type. + /// + /// Request to queue + /// Account ID to queue the request for + /// Whether to automatically trigger synchronization after queuing the request + public async Task QueueRequestAsync(IRequestBase request, Guid accountId, bool triggerSynchronization) + { + EnsureInitialized(); + + var synchronizer = await GetOrCreateSynchronizerAsync(accountId); + if (synchronizer == null) + { + _logger.Error("Could not find or create synchronizer for account {AccountId} to queue request", accountId); + return; + } + + _logger.Debug("Queuing request {RequestType} for account {AccountId}", + request.GetType().Name, accountId); + + synchronizer.QueueRequest(request); + + if (triggerSynchronization) + { + // Determine if this is a calendar or mail operation + bool isCalendarOperation = request is ICalendarActionRequest; + + if (isCalendarOperation) + { + // Trigger calendar synchronization + _logger.Debug("Triggering calendar synchronization to execute queued request for account {AccountId}", accountId); + + var calendarSyncOptions = new CalendarSynchronizationOptions() + { + AccountId = accountId + }; + + // Trigger synchronization asynchronously without waiting for completion + _ = Task.Run(async () => + { + try + { + await SynchronizeCalendarAsync(calendarSyncOptions); + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to execute calendar synchronization after queuing request for account {AccountId}", accountId); + } + }); + } + else + { + // Trigger mail synchronization (includes mail and folder operations) + _logger.Debug("Triggering mail synchronization to execute queued request for account {AccountId}", accountId); + + var mailSyncOptions = new MailSynchronizationOptions() + { + AccountId = accountId, + Type = MailSynchronizationType.ExecuteRequests + }; + + // Trigger synchronization asynchronously without waiting for completion + // This matches the pattern used in WinoRequestDelegator + _ = Task.Run(async () => + { + try + { + await SynchronizeMailAsync(mailSyncOptions); + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to execute mail synchronization after queuing request for account {AccountId}", accountId); + } + }); + } + } + } + + /// + /// Handles folder synchronization for the given account. + /// + /// Account ID to synchronize folders for + /// Cancellation token + /// Synchronization result + public async Task SynchronizeFoldersAsync(Guid accountId, + CancellationToken cancellationToken = default) + { + EnsureInitialized(); + + var options = new MailSynchronizationOptions + { + AccountId = accountId, + Type = MailSynchronizationType.FoldersOnly + }; + + return await SynchronizeMailAsync(options, cancellationToken); + } + + /// + /// Handles alias synchronization for the given account. + /// + /// Account ID to synchronize aliases for + /// Cancellation token + /// Synchronization result + public async Task SynchronizeAliasesAsync(Guid accountId, + CancellationToken cancellationToken = default) + { + EnsureInitialized(); + + var options = new MailSynchronizationOptions + { + AccountId = accountId, + Type = MailSynchronizationType.Alias + }; + + return await SynchronizeMailAsync(options, cancellationToken); + } + + /// + /// Handles profile synchronization for the given account. + /// + /// Account ID to synchronize profile for + /// Cancellation token + /// Synchronization result + public async Task SynchronizeProfileAsync(Guid accountId, + CancellationToken cancellationToken = default) + { + EnsureInitialized(); + + var options = new MailSynchronizationOptions + { + AccountId = accountId, + Type = MailSynchronizationType.UpdateProfile + }; + + return await SynchronizeMailAsync(options, cancellationToken); + } + + /// + /// Handles calendar synchronization for the given account. + /// + /// Calendar synchronization options + /// Cancellation token + /// Synchronization result + public async Task SynchronizeCalendarAsync(CalendarSynchronizationOptions options, + CancellationToken cancellationToken = default) + => options.Type == CalendarSynchronizationType.Strict + ? await SynchronizeCalendarStrictAsync(options, cancellationToken).ConfigureAwait(false) + : await RunCalendarSynchronizationWithLockAsync( + options.AccountId, + cancellationToken, + () => SynchronizeCalendarCoreAsync(options, cancellationToken, reportState: true)).ConfigureAwait(false); + + private async Task SynchronizeCalendarStrictAsync( + CalendarSynchronizationOptions options, + CancellationToken cancellationToken) + { + var metadataOptions = new CalendarSynchronizationOptions + { + AccountId = options.AccountId, + Type = CalendarSynchronizationType.CalendarMetadata, + SynchronizationCalendarIds = options.SynchronizationCalendarIds + }; + + var eventOptions = new CalendarSynchronizationOptions + { + AccountId = options.AccountId, + Type = CalendarSynchronizationType.CalendarEvents, + SynchronizationCalendarIds = options.SynchronizationCalendarIds + }; + + return await RunCalendarSynchronizationWithLockAsync(options.AccountId, cancellationToken, async () => + { + try + { + PublishCalendarSynchronizationState( + options.AccountId, + CalendarSynchronizationType.Strict, + isSynchronizationInProgress: true, + Translator.SyncAction_SynchronizingCalendarMetadata); + + var metadataResult = await SynchronizeCalendarCoreAsync(metadataOptions, cancellationToken, reportState: false).ConfigureAwait(false); + if (metadataResult.CompletedState is SynchronizationCompletedState.Failed or SynchronizationCompletedState.Canceled) + { + return metadataResult; + } + + PublishCalendarSynchronizationState( + options.AccountId, + CalendarSynchronizationType.Strict, + isSynchronizationInProgress: true, + Translator.SyncAction_SynchronizingCalendarEvents); + + return await SynchronizeCalendarCoreAsync(eventOptions, cancellationToken, reportState: false).ConfigureAwait(false); + } + finally + { + PublishCalendarSynchronizationState(options.AccountId, CalendarSynchronizationType.Strict, isSynchronizationInProgress: false); + } + }).ConfigureAwait(false); + } + + private async Task SynchronizeCalendarCoreAsync( + CalendarSynchronizationOptions options, + CancellationToken cancellationToken, + bool reportState) + { + EnsureInitialized(); + + if (await IsSynchronizationBlockedByAttentionAsync(options.AccountId).ConfigureAwait(false)) + { + _logger.Information("Skipping calendar synchronization for account {AccountId} because it requires credential attention.", options.AccountId); + return CalendarSynchronizationResult.Canceled; + } + + var synchronizer = await GetOrCreateSynchronizerAsync(options.AccountId); + if (synchronizer == null) + { + _logger.Error("Could not find or create synchronizer for account {AccountId}", options.AccountId); + return CalendarSynchronizationResult.Failed; + } + + _logger.Information("Starting calendar synchronization for account {AccountId} with type {SyncType}", + options.AccountId, options.Type); + + if (reportState) + { + PublishCalendarSynchronizationState( + options.AccountId, + options.Type, + isSynchronizationInProgress: true, + GetCalendarSynchronizationStatus(options.Type)); + } + + var accountCancellationSource = _accountSynchronizationCancellationSources.GetOrAdd(options.AccountId, _ => new CancellationTokenSource()); + using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, + accountCancellationSource.Token); + + try + { + var result = await synchronizer.SynchronizeCalendarEventsAsync(options, linkedCancellationTokenSource.Token); + + _logger.Information("Calendar synchronization completed for account {AccountId} with state {State}", + options.AccountId, result.CompletedState); + + // TODO: Create notifications for new calendar events when INotificationBuilder supports it + // if (result.DownloadedEvents?.Any() ?? false) + // await _notificationBuilder.CreateCalendarNotificationsAsync(result.DownloadedEvents); + + return result; + } + catch (OperationCanceledException) + { + _logger.Information("Calendar synchronization canceled for account {AccountId}", options.AccountId); + return CalendarSynchronizationResult.Canceled; + } + catch (AuthenticationAttentionException authEx) + { + _logger.Warning("Account {AccountId} requires attention due to authentication issues", options.AccountId); + await SetInvalidCredentialAttentionAsync(authEx.Account).ConfigureAwait(false); + + // Create app notification for authentication attention + _notificationBuilder.CreateAttentionRequiredNotification(authEx.Account); + + return CalendarSynchronizationResult.Failed; + } + catch (Exception ex) + { + _logger.Error(ex, "Calendar synchronization failed for account {AccountId}", options.AccountId); + return CalendarSynchronizationResult.Failed; + } + finally + { + if (reportState) + { + PublishCalendarSynchronizationState(options.AccountId, options.Type, isSynchronizationInProgress: false); + } + } + } + + /// + /// Downloads a MIME message for the given mail item. + /// + /// Mail item to download + /// Account ID that owns the mail item + /// Cancellation token + /// Downloaded MIME content path + public async Task DownloadMimeMessageAsync(MailCopy mailItem, Guid accountId, + CancellationToken cancellationToken = default) + { + EnsureInitialized(); + + var synchronizer = await GetOrCreateSynchronizerAsync(accountId); + if (synchronizer == null) + { + _logger.Error("Could not find or create synchronizer for account {AccountId} to download MIME", accountId); + return null; + } + + _logger.Debug("Downloading MIME message for mail item {MailItemId}", mailItem.Id); + + try + { + await synchronizer.DownloadMissingMimeMessageAsync(mailItem, null, cancellationToken); + return mailItem.Id.ToString(); // Return some identifier, actual implementation might be different + } + catch (SynchronizerEntityNotFoundException) + { + _logger.Warning("MIME message for mail item {MailItemId} no longer exists on server. Removed locally.", mailItem.Id); + return null; + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to download MIME message for mail item {MailItemId}", mailItem.Id); + return null; + } + } + + /// + /// Downloads a calendar attachment using the appropriate synchronizer. + /// + public async Task DownloadCalendarAttachmentAsync( + Wino.Core.Domain.Entities.Calendar.CalendarItem calendarItem, + Wino.Core.Domain.Entities.Calendar.CalendarAttachment attachment, + string localFilePath, + CancellationToken cancellationToken = default) + { + EnsureInitialized(); + + if (calendarItem == null) + throw new ArgumentNullException(nameof(calendarItem)); + + if (attachment == null) + throw new ArgumentNullException(nameof(attachment)); + + var accountId = calendarItem.AssignedCalendar?.AccountId ?? Guid.Empty; + if (accountId == Guid.Empty) + throw new InvalidOperationException("Calendar item does not have an assigned account."); + + var synchronizer = await GetOrCreateSynchronizerAsync(accountId); + + if (synchronizer == null) + { + _logger.Error("Could not find or create synchronizer for account {AccountId} to download calendar attachment", accountId); + throw new InvalidOperationException("No synchronizer available for downloading calendar attachment."); + } + + _logger.Debug("Downloading calendar attachment {AttachmentId} for calendar item {CalendarItemId}", + attachment.Id, calendarItem.Id); + + try + { + await synchronizer.DownloadCalendarAttachmentAsync( + calendarItem, + attachment, + localFilePath, + cancellationToken); + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to download calendar attachment {AttachmentId}", attachment.Id); + throw; + } + } + + /// + /// Creates a new synchronizer for a newly added account. + /// + /// Account to create synchronizer for + /// Created synchronizer + public IWinoSynchronizerBase CreateSynchronizerForAccount(MailAccount account) + { + EnsureInitialized(); + + try + { + var synchronizer = _concreteSynchronizerFactory.CreateNewSynchronizer(account); + _synchronizerCache.TryAdd(account.Id, synchronizer); + + _logger.Information("Created new synchronizer for account {AccountName} ({AccountId})", + account.Name, account.Id); + + return synchronizer; + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to create synchronizer for account {AccountName} ({AccountId})", + account.Name, account.Id); + return null; + } + } + + /// + /// Cancels all in-flight synchronizations for the given account. + /// + /// Account ID to cancel synchronizations for + public Task CancelSynchronizationsAsync(Guid accountId) + { + EnsureInitialized(); + + if (_accountSynchronizationCancellationSources.TryRemove(accountId, out var cancellationSource)) + { + try + { + if (!cancellationSource.IsCancellationRequested) + { + cancellationSource.Cancel(); + } + } + catch (ObjectDisposedException) + { + // no-op + } + finally + { + cancellationSource.Dispose(); + } + + _logger.Information("Canceled ongoing synchronizations for account {AccountId}", accountId); + } + + return Task.CompletedTask; + } + + /// + /// Destroys the synchronizer for the given account. + /// + /// Account ID to destroy synchronizer for + public async Task DestroySynchronizerAsync(Guid accountId) + { + EnsureInitialized(); + await CancelSynchronizationsAsync(accountId); + + if (_synchronizerCache.TryRemove(accountId, out var synchronizer)) + { + try + { + await synchronizer.KillSynchronizerAsync(); + _logger.Information("Destroyed synchronizer for account {AccountId}", accountId); + } + catch (OperationCanceledException) + { + _logger.Information("Synchronizer destruction canceled for account {AccountId}", accountId); + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to destroy synchronizer for account {AccountId}", accountId); + } + } + } + + /// + /// Gets all cached synchronizers. + /// + /// Collection of all cached synchronizers + public IEnumerable GetAllSynchronizers() + { + EnsureInitialized(); + return _synchronizerCache.Values.ToList(); + } + + /// + /// Gets a synchronizer for the given account ID. + /// + /// Account ID + /// Synchronizer if found, null otherwise + public async Task GetSynchronizerAsync(Guid accountId) + { + EnsureInitialized(); + return await GetOrCreateSynchronizerAsync(accountId); + } + + private async Task GetOrCreateSynchronizerAsync(Guid accountId) + { + if (_synchronizerCache.TryGetValue(accountId, out var existingSynchronizer)) + { + return existingSynchronizer; + } + + // Try to create a new synchronizer if not found + var account = await _accountService.GetAccountAsync(accountId); + if (account != null) + { + return CreateSynchronizerForAccount(account); + } + + return null; + } + + /// + /// Handles OAuth authentication for the specified provider. + /// + /// The mail provider type to authenticate + /// Optional account to authenticate (null for initial authentication) + /// Whether to propose copying auth URL for Gmail + /// Token information containing access token and username + public async Task HandleAuthorizationAsync(MailProviderType providerType, + MailAccount account = null, + bool proposeCopyAuthorizationURL = false) + { + EnsureInitialized(); + + try + { + var authenticator = _authenticationProvider.GetAuthenticator(providerType); + + // Some users are having issues with Gmail authentication. + // Their browsers may never launch to complete authentication. + // Offer to copy auth url for them to complete it manually. + // Redirection will occur to the app and the token will be saved. + if (proposeCopyAuthorizationURL && authenticator is IGmailAuthenticator gmailAuthenticator) + { + gmailAuthenticator.ProposeCopyAuthURL = true; + } + + TokenInformationEx tokenInfo; + + if (account != null) + { + // Get token for existing account (may trigger interactive auth if token is expired) + tokenInfo = await authenticator.GetTokenInformationAsync(account); + _logger.Information("Retrieved token for existing account {AccountAddress}", account.Address); + } + else + { + // Initial authentication request - there is no account to get token for + // This will always trigger interactive authentication + tokenInfo = await authenticator.GenerateTokenInformationAsync(null); + _logger.Information("Generated new token for {ProviderType} authentication", providerType); + } + + return tokenInfo; + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to handle authorization for {ProviderType}", providerType); + throw; + } + } + + private void EnsureInitialized() + { + if (!_isInitialized) + { + throw new InvalidOperationException("SynchronizationManager must be initialized before use. Call InitializeAsync first."); + } + } + + private async Task SetInvalidCredentialAttentionAsync(MailAccount account) + { + if (account == null || _accountService == null) + return; + + var persistedAccount = await _accountService.GetAccountAsync(account.Id).ConfigureAwait(false); + + if (persistedAccount == null) + return; + + if (persistedAccount.AttentionReason == AccountAttentionReason.InvalidCredentials) + return; + + persistedAccount.AttentionReason = AccountAttentionReason.InvalidCredentials; + await _accountService.UpdateAccountAsync(persistedAccount).ConfigureAwait(false); + } + + private async Task IsSynchronizationBlockedByAttentionAsync(Guid accountId) + { + if (_accountService == null) + return false; + + var account = await _accountService.GetAccountAsync(accountId).ConfigureAwait(false); + return account?.AttentionReason == AccountAttentionReason.InvalidCredentials; + } + + private void PublishCalendarSynchronizationState( + Guid accountId, + CalendarSynchronizationType synchronizationType, + bool isSynchronizationInProgress, + string synchronizationStatus = "") + { + WeakReferenceMessenger.Default.Send(new AccountCalendarSynchronizationStateChanged( + accountId, + synchronizationType, + isSynchronizationInProgress, + synchronizationStatus)); + } + + private static string GetCalendarSynchronizationStatus(CalendarSynchronizationType synchronizationType) + => synchronizationType switch + { + CalendarSynchronizationType.CalendarMetadata => Translator.SyncAction_SynchronizingCalendarMetadata, + CalendarSynchronizationType.Strict => Translator.SyncAction_SynchronizingCalendarData, + _ => Translator.SyncAction_SynchronizingCalendarEvents + }; + + private async Task RunCalendarSynchronizationWithLockAsync( + Guid accountId, + CancellationToken cancellationToken, + Func> synchronizationFactory) + { + var calendarSemaphore = _calendarSynchronizationLocks.GetOrAdd(accountId, _ => new SemaphoreSlim(1, 1)); + await calendarSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + return await synchronizationFactory().ConfigureAwait(false); + } + finally + { + calendarSemaphore.Release(); + } + } +} diff --git a/Wino.Core/Services/SynchronizationManagerInitializer.cs b/Wino.Core/Services/SynchronizationManagerInitializer.cs new file mode 100644 index 00000000..71b22e8e --- /dev/null +++ b/Wino.Core/Services/SynchronizationManagerInitializer.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Wino.Core.Domain.Interfaces; + +namespace Wino.Core.Services; + +/// +/// Service responsible for initializing the SynchronizationManager during app startup. +/// +public class SynchronizationManagerInitializer : IInitializeAsync +{ + private readonly IServiceProvider _serviceProvider; + + public SynchronizationManagerInitializer(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public async Task InitializeAsync() + { + var synchronizerFactory = _serviceProvider.GetRequiredService(); + var imapTestService = _serviceProvider.GetRequiredService(); + var accountService = _serviceProvider.GetRequiredService(); + var authenticationProvider = _serviceProvider.GetRequiredService(); + var notificationBuilder = _serviceProvider.GetRequiredService(); + + // Cast to concrete type to access CreateNewSynchronizer method + var concreteSynchronizerFactory = synchronizerFactory as SynchronizerFactory; + + await SynchronizationManager.Instance.InitializeAsync(concreteSynchronizerFactory, imapTestService, accountService, notificationBuilder, authenticationProvider); + } +} diff --git a/Wino.Core/Services/SynchronizerErrorHandlingFactory.cs b/Wino.Core/Services/SynchronizerErrorHandlingFactory.cs index 7c9336c0..446688db 100644 --- a/Wino.Core/Services/SynchronizerErrorHandlingFactory.cs +++ b/Wino.Core/Services/SynchronizerErrorHandlingFactory.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Serilog; using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Errors; +using Wino.Core.Domain.Models.Synchronization; namespace Wino.Core.Services; diff --git a/Wino.Core/Services/SynchronizerFactory.cs b/Wino.Core/Services/SynchronizerFactory.cs index 1cab069c..7aa25880 100644 --- a/Wino.Core/Services/SynchronizerFactory.cs +++ b/Wino.Core/Services/SynchronizerFactory.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Interfaces; using Wino.Core.Integration.Processors; +using Wino.Core.Synchronizers.ImapSync; using Wino.Core.Synchronizers.Mail; namespace Wino.Core.Services; @@ -13,39 +14,48 @@ public class SynchronizerFactory : ISynchronizerFactory private bool isInitialized = false; private readonly IAccountService _accountService; - private readonly IImapSynchronizationStrategyProvider _imapSynchronizationStrategyProvider; private readonly IApplicationConfiguration _applicationConfiguration; private readonly IOutlookSynchronizerErrorHandlerFactory _outlookSynchronizerErrorHandlerFactory; private readonly IGmailSynchronizerErrorHandlerFactory _gmailSynchronizerErrorHandlerFactory; + private readonly IImapSynchronizerErrorHandlerFactory _imapSynchronizerErrorHandlerFactory; private readonly IOutlookChangeProcessor _outlookChangeProcessor; private readonly IGmailChangeProcessor _gmailChangeProcessor; private readonly IImapChangeProcessor _imapChangeProcessor; - private readonly IOutlookAuthenticator _outlookAuthenticator; - private readonly IGmailAuthenticator _gmailAuthenticator; + private readonly IAuthenticationProvider _authenticationProvider; + private readonly UnifiedImapSynchronizer _unifiedImapSynchronizer; + private readonly ICalDavClient _calDavClient; + private readonly IAutoDiscoveryService _autoDiscoveryService; + private readonly ICalendarService _calendarService; private readonly List synchronizerCache = new(); public SynchronizerFactory(IOutlookChangeProcessor outlookChangeProcessor, IGmailChangeProcessor gmailChangeProcessor, IImapChangeProcessor imapChangeProcessor, - IOutlookAuthenticator outlookAuthenticator, - IGmailAuthenticator gmailAuthenticator, + IAuthenticationProvider authenticationProvider, IAccountService accountService, - IImapSynchronizationStrategyProvider imapSynchronizationStrategyProvider, IApplicationConfiguration applicationConfiguration, IOutlookSynchronizerErrorHandlerFactory outlookSynchronizerErrorHandlerFactory, - IGmailSynchronizerErrorHandlerFactory gmailSynchronizerErrorHandlerFactory) + IGmailSynchronizerErrorHandlerFactory gmailSynchronizerErrorHandlerFactory, + IImapSynchronizerErrorHandlerFactory imapSynchronizerErrorHandlerFactory, + UnifiedImapSynchronizer unifiedImapSynchronizer, + ICalDavClient calDavClient, + IAutoDiscoveryService autoDiscoveryService, + ICalendarService calendarService) { _outlookChangeProcessor = outlookChangeProcessor; _gmailChangeProcessor = gmailChangeProcessor; _imapChangeProcessor = imapChangeProcessor; - _outlookAuthenticator = outlookAuthenticator; - _gmailAuthenticator = gmailAuthenticator; + _authenticationProvider = authenticationProvider; _accountService = accountService; - _imapSynchronizationStrategyProvider = imapSynchronizationStrategyProvider; _applicationConfiguration = applicationConfiguration; _outlookSynchronizerErrorHandlerFactory = outlookSynchronizerErrorHandlerFactory; _gmailSynchronizerErrorHandlerFactory = gmailSynchronizerErrorHandlerFactory; + _imapSynchronizerErrorHandlerFactory = imapSynchronizerErrorHandlerFactory; + _unifiedImapSynchronizer = unifiedImapSynchronizer; + _calDavClient = calDavClient; + _autoDiscoveryService = autoDiscoveryService; + _calendarService = calendarService; } public async Task GetAccountSynchronizerAsync(Guid accountId) @@ -75,11 +85,13 @@ public class SynchronizerFactory : ISynchronizerFactory switch (providerType) { case Domain.Enums.MailProviderType.Outlook: - return new OutlookSynchronizer(mailAccount, _outlookAuthenticator, _outlookChangeProcessor, _outlookSynchronizerErrorHandlerFactory); + var outlookAuthenticator = _authenticationProvider.GetAuthenticator(Domain.Enums.MailProviderType.Outlook) as IOutlookAuthenticator; + return new OutlookSynchronizer(mailAccount, outlookAuthenticator, _outlookChangeProcessor, _outlookSynchronizerErrorHandlerFactory); case Domain.Enums.MailProviderType.Gmail: - return new GmailSynchronizer(mailAccount, _gmailAuthenticator, _gmailChangeProcessor, _gmailSynchronizerErrorHandlerFactory); + var gmailAuthenticator = _authenticationProvider.GetAuthenticator(Domain.Enums.MailProviderType.Gmail) as IGmailAuthenticator; + return new GmailSynchronizer(mailAccount, gmailAuthenticator, _gmailChangeProcessor, _gmailSynchronizerErrorHandlerFactory); case Domain.Enums.MailProviderType.IMAP4: - return new ImapSynchronizer(mailAccount, _imapChangeProcessor, _imapSynchronizationStrategyProvider, _applicationConfiguration); + return new ImapSynchronizer(mailAccount, _imapChangeProcessor, _applicationConfiguration, _unifiedImapSynchronizer, _imapSynchronizerErrorHandlerFactory, _calDavClient, _autoDiscoveryService, _calendarService); default: break; } diff --git a/Wino.Core/Services/ThreadingStrategyProvider.cs b/Wino.Core/Services/ThreadingStrategyProvider.cs deleted file mode 100644 index 37af968e..00000000 --- a/Wino.Core/Services/ThreadingStrategyProvider.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Interfaces; -using Wino.Services.Threading; - -namespace Wino.Core.Services; - -public class ThreadingStrategyProvider : IThreadingStrategyProvider -{ - private readonly OutlookThreadingStrategy _outlookThreadingStrategy; - private readonly GmailThreadingStrategy _gmailThreadingStrategy; - private readonly ImapThreadingStrategy _imapThreadStrategy; - - public ThreadingStrategyProvider(OutlookThreadingStrategy outlookThreadingStrategy, - GmailThreadingStrategy gmailThreadingStrategy, - ImapThreadingStrategy imapThreadStrategy) - { - _outlookThreadingStrategy = outlookThreadingStrategy; - _gmailThreadingStrategy = gmailThreadingStrategy; - _imapThreadStrategy = imapThreadStrategy; - } - - public IThreadingStrategy GetStrategy(MailProviderType mailProviderType) - { - return mailProviderType switch - { - MailProviderType.Outlook => _outlookThreadingStrategy, - MailProviderType.Gmail => _gmailThreadingStrategy, - _ => _imapThreadStrategy, - }; - } -} diff --git a/Wino.Core/Services/WinoRequestDelegator.cs b/Wino.Core/Services/WinoRequestDelegator.cs index 38003c62..7c04ef37 100644 --- a/Wino.Core/Services/WinoRequestDelegator.cs +++ b/Wino.Core/Services/WinoRequestDelegator.cs @@ -5,33 +5,41 @@ using System.Threading.Tasks; using CommunityToolkit.Mvvm.Messaging; using Serilog; using Wino.Core.Domain; +using Wino.Core.Domain.Entities.Calendar; 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.Folders; using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Synchronization; +using Wino.Core.Helpers; +using Wino.Core.Requests.Calendar; using Wino.Core.Requests.Mail; using Wino.Messaging.Server; +using Wino.Messaging.UI; namespace Wino.Core.Services; public class WinoRequestDelegator : IWinoRequestDelegator { private readonly IWinoRequestProcessor _winoRequestProcessor; - private readonly IWinoServerConnectionManager _winoServerConnectionManager; private readonly IFolderService _folderService; private readonly IMailDialogService _dialogService; + private readonly IAccountService _accountService; + private readonly ICalendarService _calendarService; public WinoRequestDelegator(IWinoRequestProcessor winoRequestProcessor, - IWinoServerConnectionManager winoServerConnectionManager, IFolderService folderService, - IMailDialogService dialogService) + IMailDialogService dialogService, + IAccountService accountService, + ICalendarService calendarService) { _winoRequestProcessor = winoRequestProcessor; - _winoServerConnectionManager = winoServerConnectionManager; _folderService = folderService; _dialogService = dialogService; + _accountService = accountService; + _calendarService = calendarService; } public async Task ExecuteAsync(MailOperationPreperationRequest request) @@ -82,14 +90,20 @@ public class WinoRequestDelegator : IWinoRequestDelegator var accountIds = requests.GroupBy(a => a.Item.AssignedAccount.Id); // Queue requests for each account and start synchronization. - foreach (var accountId in accountIds) + foreach (var accountGroup in accountIds) { - foreach (var accountRequest in accountId) + foreach (var accountRequest in accountGroup) { - await QueueRequestAsync(accountRequest, accountId.Key); + await QueueRequestAsync(accountRequest, accountGroup.Key); } - await QueueSynchronizationAsync(accountId.Key); + var account = accountGroup.First().Item.AssignedAccount; + var actionItems = SynchronizationActionHelper.CreateActionItems(accountGroup, accountGroup.Key, account.Name); + + if (actionItems.Count > 0) + WeakReferenceMessenger.Default.Send(new SynchronizationActionsAdded(accountGroup.Key, account.Name, actionItems)); + + await QueueSynchronizationAsync(accountGroup.Key); } } @@ -117,55 +131,147 @@ public class WinoRequestDelegator : IWinoRequestDelegator if (request == null) return; await QueueRequestAsync(request, accountId); + await SendSyncActionsAddedAsync([request], accountId); await QueueSynchronizationAsync(accountId); + + if (folderRequest.Action is FolderOperation.Delete or FolderOperation.CreateSubFolder) + { + await QueueFoldersOnlySynchronizationAsync(accountId); + } } public async Task ExecuteAsync(DraftPreparationRequest draftPreperationRequest) { var request = new CreateDraftRequest(draftPreperationRequest); + var accountId = draftPreperationRequest.Account.Id; - await QueueRequestAsync(request, draftPreperationRequest.Account.Id); - await QueueSynchronizationAsync(draftPreperationRequest.Account.Id); + await QueueRequestAsync(request, accountId); + await SendSyncActionsAddedAsync([request], accountId, draftPreperationRequest.Account.Name); + await QueueSynchronizationAsync(accountId); } public async Task ExecuteAsync(SendDraftPreparationRequest sendDraftPreperationRequest) { var request = new SendDraftRequest(sendDraftPreperationRequest); + var account = sendDraftPreperationRequest.MailItem.AssignedAccount; - await QueueRequestAsync(request, sendDraftPreperationRequest.MailItem.AssignedAccount.Id); - await QueueSynchronizationAsync(sendDraftPreperationRequest.MailItem.AssignedAccount.Id); + await QueueRequestAsync(request, account.Id); + await SendSyncActionsAddedAsync([request], account.Id, account.Name); + await QueueSynchronizationAsync(account.Id); + } + + public async Task ExecuteAsync(CalendarOperationPreparationRequest calendarPreparationRequest) + { + if (calendarPreparationRequest == null) + return; + + IRequestBase request = calendarPreparationRequest.Operation switch + { + CalendarSynchronizerOperation.CreateEvent => await CreateCalendarEventRequestAsync(calendarPreparationRequest).ConfigureAwait(false), + CalendarSynchronizerOperation.DeleteEvent => new DeleteCalendarEventRequest(calendarPreparationRequest.CalendarItem), + CalendarSynchronizerOperation.AcceptEvent => new AcceptEventRequest(calendarPreparationRequest.CalendarItem, calendarPreparationRequest.ResponseMessage), + CalendarSynchronizerOperation.DeclineEvent => CreateDeclineRequest(calendarPreparationRequest.CalendarItem, calendarPreparationRequest.ResponseMessage), + CalendarSynchronizerOperation.TentativeEvent => new TentativeEventRequest(calendarPreparationRequest.CalendarItem, calendarPreparationRequest.ResponseMessage), + CalendarSynchronizerOperation.UpdateEvent => new UpdateCalendarEventRequest(calendarPreparationRequest.CalendarItem, calendarPreparationRequest.Attendees) + { + OriginalItem = calendarPreparationRequest.OriginalItem, + OriginalAttendees = calendarPreparationRequest.OriginalAttendees + }, + _ => throw new NotImplementedException($"Calendar operation {calendarPreparationRequest.Operation} is not implemented yet.") + }; + + if (request == null) + return; + + var accountId = calendarPreparationRequest.Operation == CalendarSynchronizerOperation.CreateEvent + ? calendarPreparationRequest.ComposeResult.AccountId + : calendarPreparationRequest.CalendarItem.AssignedCalendar.AccountId; + var accountName = calendarPreparationRequest.Operation == CalendarSynchronizerOperation.CreateEvent + ? null + : calendarPreparationRequest.CalendarItem.AssignedCalendar.MailAccount?.Name; + + await QueueRequestAsync(request, accountId); + await SendSyncActionsAddedAsync([request], accountId, accountName); + await QueueCalendarSynchronizationAsync(accountId); + } + + private async Task CreateCalendarEventRequestAsync(CalendarOperationPreparationRequest calendarPreparationRequest) + { + var composeResult = calendarPreparationRequest.ComposeResult + ?? throw new InvalidOperationException("Create event requests require a compose result."); + var assignedCalendar = await _calendarService.GetAccountCalendarAsync(composeResult.CalendarId).ConfigureAwait(false); + + if (assignedCalendar == null) + throw new InvalidOperationException($"Calendar {composeResult.CalendarId} could not be resolved."); + + return new CreateCalendarEventRequest(composeResult, assignedCalendar); + } + + private IRequestBase CreateDeclineRequest(CalendarItem calendarItem, string responseMessage) + { + // For Outlook accounts, declined events are deleted by the server after synchronization. + // Use OutlookDeclineEventRequest to handle UI removal. + if (calendarItem.AssignedCalendar?.MailAccount?.ProviderType == MailProviderType.Outlook) + { + return new OutlookDeclineEventRequest(calendarItem, responseMessage); + } + + return new DeclineEventRequest(calendarItem, responseMessage); } private async Task QueueRequestAsync(IRequestBase request, Guid accountId) { - try - { - await EnsureServerConnectedAsync(); - await _winoServerConnectionManager.QueueRequestAsync(request, accountId); - } - catch (WinoServerException serverException) - { - _dialogService.InfoBarMessage("Wino Server Exception", serverException.Message, InfoBarMessageType.Error); - } + // Don't trigger synchronization for individual requests - we'll trigger it once for all requests + await SynchronizationManager.Instance.QueueRequestAsync(request, accountId, triggerSynchronization: false); } - private async Task QueueSynchronizationAsync(Guid accountId) + private Task QueueSynchronizationAsync(Guid accountId) { - await EnsureServerConnectedAsync(); - var options = new MailSynchronizationOptions() { AccountId = accountId, Type = MailSynchronizationType.ExecuteRequests }; - WeakReferenceMessenger.Default.Send(new NewMailSynchronizationRequested(options, SynchronizationSource.Client)); + WeakReferenceMessenger.Default.Send(new NewMailSynchronizationRequested(options)); + return Task.CompletedTask; } - private async Task EnsureServerConnectedAsync() + private Task QueueFoldersOnlySynchronizationAsync(Guid accountId) { - if (_winoServerConnectionManager.Status == WinoServerConnectionStatus.Connected) return; + var options = new MailSynchronizationOptions() + { + AccountId = accountId, + Type = MailSynchronizationType.FoldersOnly + }; - await _winoServerConnectionManager.ConnectAsync(); + WeakReferenceMessenger.Default.Send(new NewMailSynchronizationRequested(options)); + return Task.CompletedTask; + } + + private async Task SendSyncActionsAddedAsync(IEnumerable requests, Guid accountId, string accountName = null) + { + if (accountName == null) + { + var account = await _accountService.GetAccountAsync(accountId); + accountName = account?.Name ?? string.Empty; + } + + var actionItems = SynchronizationActionHelper.CreateActionItems(requests, accountId, accountName); + + if (actionItems.Count > 0) + WeakReferenceMessenger.Default.Send(new SynchronizationActionsAdded(accountId, accountName, actionItems)); + } + + private Task QueueCalendarSynchronizationAsync(Guid accountId) + { + var options = new CalendarSynchronizationOptions() + { + AccountId = accountId, + Type = CalendarSynchronizationType.ExecuteRequests + }; + + WeakReferenceMessenger.Default.Send(new NewCalendarSynchronizationRequested(options)); + return Task.CompletedTask; } } diff --git a/Wino.Core/Services/WinoRequestProcessor.cs b/Wino.Core/Services/WinoRequestProcessor.cs index 5eead88c..87af29a0 100644 --- a/Wino.Core/Services/WinoRequestProcessor.cs +++ b/Wino.Core/Services/WinoRequestProcessor.cs @@ -31,10 +31,10 @@ public class WinoRequestProcessor : IWinoRequestProcessor /// private readonly List _toggleRequestRules = [ - new ToggleRequestRule(MailOperation.MarkAsRead, MailOperation.MarkAsUnread, new System.Func((item) => item.IsRead)), - new ToggleRequestRule(MailOperation.MarkAsUnread, MailOperation.MarkAsRead, new System.Func((item) => !item.IsRead)), - new ToggleRequestRule(MailOperation.SetFlag, MailOperation.ClearFlag, new System.Func((item) => item.IsFlagged)), - new ToggleRequestRule(MailOperation.ClearFlag, MailOperation.SetFlag, new System.Func((item) => !item.IsFlagged)), + new ToggleRequestRule(MailOperation.MarkAsRead, MailOperation.MarkAsUnread, new System.Func((item) => item.IsRead)), + new ToggleRequestRule(MailOperation.MarkAsUnread, MailOperation.MarkAsRead, new System.Func((item) => !item.IsRead)), + new ToggleRequestRule(MailOperation.SetFlag, MailOperation.ClearFlag, new System.Func((item) => item.IsFlagged)), + new ToggleRequestRule(MailOperation.ClearFlag, MailOperation.SetFlag, new System.Func((item) => !item.IsFlagged)), ]; public WinoRequestProcessor(IFolderService folderService, @@ -94,7 +94,7 @@ public class WinoRequestProcessor : IWinoRequestProcessor var requests = new List(); // TODO: Fix: Collection was modified; enumeration operation may not execute - foreach (var item in preperationRequest.MailItems) + foreach (var item in preperationRequest.MailItems.ToList()) { var singleRequest = await GetSingleRequestAsync(item, action, moveTargetStructure, preperationRequest.ToggleExecution); @@ -255,15 +255,29 @@ public class WinoRequestProcessor : IWinoRequestProcessor change = new MarkFolderAsReadRequest(folder, unreadItems); break; - //case FolderOperation.Delete: - // var isConfirmed = await _dialogService.ShowConfirmationDialogAsync($"'{folderStructure.FolderName}' is going to be deleted. Do you want to continue?", "Are you sure?", "Yes delete."); + case FolderOperation.Delete: + var deleteQuestion = string.Format(Translator.DialogMessage_DeleteAccountConfirmationMessage, folder.FolderName); + var shouldDelete = await _dialogService.ShowConfirmationDialogAsync(deleteQuestion, Translator.FolderOperation_Delete, Translator.FolderOperation_Delete); - // if (isConfirmed) - // change = new DeleteFolderRequest(accountId, folderStructure.RemoteFolderId, folderStructure.FolderId); + if (shouldDelete) + { + change = new DeleteFolderRequest(folder); + } - // break; - //default: - // throw new NotImplementedException(); + break; + case FolderOperation.CreateSubFolder: + var subFolderName = await _dialogService.ShowTextInputDialogAsync( + string.Empty, + Translator.FolderOperation_CreateSubFolder, + Translator.DialogMessage_RenameFolderMessage, + Translator.FolderOperation_CreateSubFolder); + + if (!string.IsNullOrWhiteSpace(subFolderName)) + { + change = new CreateSubFolderRequest(folder, subFolderName.Trim()); + } + + break; } return change; diff --git a/Wino.Core/Synchronizers/BaseSynchronizer.cs b/Wino.Core/Synchronizers/BaseSynchronizer.cs index 267ef01a..1e0bf8ec 100644 --- a/Wino.Core/Synchronizers/BaseSynchronizer.cs +++ b/Wino.Core/Synchronizers/BaseSynchronizer.cs @@ -1,8 +1,11 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Net.Http; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Messaging; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; @@ -13,12 +16,16 @@ using Wino.Messaging.UI; namespace Wino.Core.Synchronizers; -public abstract class BaseSynchronizer : IBaseSynchronizer +public abstract partial class BaseSynchronizer : ObservableObject, IBaseSynchronizer { protected SemaphoreSlim synchronizationSemaphore = new(1); protected CancellationToken activeSynchronizationCancellationToken; protected List changeRequestQueue = []; + private readonly ConcurrentDictionary _pendingMailOperationIds = new(); + private readonly ConcurrentDictionary _pendingCalendarOperationIds = new(); + protected readonly IMessenger Messenger; + public MailAccount Account { get; } private AccountSynchronizerState state; @@ -29,20 +36,142 @@ public abstract class BaseSynchronizer : IBaseSynchronizer { state = value; - WeakReferenceMessenger.Default.Send(new AccountSynchronizerStateChanged(Account.Id, value)); + // Send state changed message with current progress information + Messenger.Send(new AccountSynchronizerStateChanged( + Account.Id, + value, + TotalItemsToSync, + RemainingItemsToSync, + SynchronizationStatus)); } } - protected BaseSynchronizer(MailAccount account) + /// + /// Current synchronization status message. + /// + [ObservableProperty] + public partial string SynchronizationStatus { get; set; } = string.Empty; + + /// + /// Total items to download/sync in current operation. + /// 0 means no active download or indeterminate progress. + /// + [ObservableProperty] + public partial int TotalItemsToSync { get; set; } + + /// + /// Remaining items to download/sync in current operation. + /// + [ObservableProperty] + public partial int RemainingItemsToSync { get; set; } + + /// + /// Calculated progress percentage (0-100) based on TotalItemsToSync and RemainingItemsToSync. + /// Returns -1 for indeterminate progress (when both are 0). + /// + public double SynchronizationProgress + { + get + { + if (TotalItemsToSync == 0 || RemainingItemsToSync == 0) + return -1; // Indeterminate + + return ((double)(TotalItemsToSync - RemainingItemsToSync) / TotalItemsToSync) * 100; + } + } + + protected BaseSynchronizer(MailAccount account, IMessenger messenger) { Account = account; + Messenger = messenger ?? WeakReferenceMessenger.Default; + } + + /// + /// Resets synchronization progress to default state. + /// + protected void ResetSyncProgress() + { + TotalItemsToSync = 0; + RemainingItemsToSync = 0; + SynchronizationStatus = string.Empty; + OnPropertyChanged(nameof(SynchronizationProgress)); + } + + /// + /// Updates synchronization progress with current item counts. + /// + /// Total items to sync + /// Remaining items to sync + /// Optional status message + protected void UpdateSyncProgress(int total, int remaining, string status = "") + { + TotalItemsToSync = total; + RemainingItemsToSync = remaining; + SynchronizationStatus = status; + OnPropertyChanged(nameof(SynchronizationProgress)); + + // Send progress update message + Messenger.Send(new AccountSynchronizerStateChanged( + Account.Id, + State, + TotalItemsToSync, + RemainingItemsToSync, + SynchronizationStatus)); } /// /// Queues a single request to be executed in the next synchronization. /// /// Request to execute. - public void QueueRequest(IRequestBase request) => changeRequestQueue.Add(request); + public void QueueRequest(IRequestBase request) + { + changeRequestQueue.Add(request); + TrackQueuedRequest(request); + } + + public bool HasPendingOperation(Guid mailUniqueId) => _pendingMailOperationIds.ContainsKey(mailUniqueId); + + public IReadOnlyCollection GetPendingOperationUniqueIds() => _pendingMailOperationIds.Keys.ToArray(); + + public bool HasPendingCalendarOperation(Guid calendarItemId) => _pendingCalendarOperationIds.ContainsKey(calendarItemId); + + protected void TrackQueuedRequest(IRequestBase request) + { + if (request is IMailActionRequest mailActionRequest) + { + _pendingMailOperationIds.TryAdd(mailActionRequest.Item.UniqueId, 0); + } + + if (request is ICalendarActionRequest calendarActionRequest) + { + if (calendarActionRequest.LocalCalendarItemId.HasValue) + { + _pendingCalendarOperationIds.TryAdd(calendarActionRequest.LocalCalendarItemId.Value, 0); + } + } + } + + protected void UntrackProcessedRequest(IRequestBase request) + { + if (request is IMailActionRequest mailActionRequest) + { + _pendingMailOperationIds.TryRemove(mailActionRequest.Item.UniqueId, out _); + } + + if (request is ICalendarActionRequest calendarActionRequest) + { + if (calendarActionRequest.LocalCalendarItemId.HasValue) + { + _pendingCalendarOperationIds.TryRemove(calendarActionRequest.LocalCalendarItemId.Value, out _); + } + } + } + + protected void UntrackProcessedRequests(IEnumerable requests) + { + foreach (var request in requests) + UntrackProcessedRequest(request); + } /// /// Runs existing queued requests in the queue. diff --git a/Wino.Core/Synchronizers/Errors/EntityNotFoundHandler.cs b/Wino.Core/Synchronizers/Errors/EntityNotFoundHandler.cs new file mode 100644 index 00000000..afa154b8 --- /dev/null +++ b/Wino.Core/Synchronizers/Errors/EntityNotFoundHandler.cs @@ -0,0 +1,89 @@ +using System; +using System.Threading.Tasks; +using Serilog; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Synchronization; + +namespace Wino.Core.Synchronizers.Errors; + +/// +/// Generic handler for 404 (Not Found) errors across all synchronizers. +/// When a resource is already gone on the server, this handler applies +/// the intended change locally instead of throwing. +/// Works for all mail actions, folder actions, and batch operations. +/// +public class EntityNotFoundHandler : ISynchronizerErrorHandler +{ + private readonly ILogger _logger = Log.ForContext(); + private readonly IMailService _mailService; + private readonly IFolderService _folderService; + + public EntityNotFoundHandler(IMailService mailService, IFolderService folderService) + { + _mailService = mailService; + _folderService = folderService; + } + + public bool CanHandle(SynchronizerErrorContext error) + { + if (error.ErrorCode != 404) return false; + if (error.RequestBundle == null) return false; + return true; + } + + public async Task HandleAsync(SynchronizerErrorContext error) + { + error.Severity = SynchronizerErrorSeverity.Recoverable; + error.Category = SynchronizerErrorCategory.ResourceNotFound; + + var uiRequest = error.RequestBundle.UIChangeRequest; + + // --- Folder actions --- + if (uiRequest is IFolderActionRequest folderAction) + { + _logger.Warning("Entity not found (404) for folder operation {Op} on {RemoteFolderId}. Deleting locally.", + folderAction.Operation, folderAction.Folder.RemoteFolderId); + + try + { + await _folderService.DeleteFolderAsync( + folderAction.Folder.MailAccountId, + folderAction.Folder.RemoteFolderId).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to delete folder locally after 404."); + } + + return true; + } + + // --- Individual mail actions --- + if (uiRequest is IMailActionRequest mailAction && error.Account != null) + { + _logger.Warning("Entity not found (404) for mail operation {Op} on {MailId}. Deleting locally.", + mailAction.Operation, mailAction.Item.Id); + + // Revert optimistic UI change (e.g. mark-read/flag toggle) before deleting + error.RequestBundle.UIChangeRequest?.RevertUIChanges(); + + try + { + await _mailService.DeleteMailAsync( + error.Account.Id, mailAction.Item.Id).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to delete mail locally after 404."); + } + + return true; + } + + // --- Batch requests (can't identify specific item) --- + // Mark as recoverable. Next sync will clean up stale items. + _logger.Warning("Entity not found (404) for batch operation. Marking as recoverable."); + return true; + } +} diff --git a/Wino.Core/Synchronizers/Errors/Gmail/GmailAuthenticationFailedHandler.cs b/Wino.Core/Synchronizers/Errors/Gmail/GmailAuthenticationFailedHandler.cs new file mode 100644 index 00000000..c82ec459 --- /dev/null +++ b/Wino.Core/Synchronizers/Errors/Gmail/GmailAuthenticationFailedHandler.cs @@ -0,0 +1,68 @@ +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Google; +using Serilog; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Synchronization; + +namespace Wino.Core.Synchronizers.Errors.Gmail; + +public class GmailAuthenticationFailedHandler : ISynchronizerErrorHandler +{ + private readonly ILogger _logger = Log.ForContext(); + private readonly IAccountService _accountService; + + public GmailAuthenticationFailedHandler(IAccountService accountService) + { + _accountService = accountService; + } + + public bool CanHandle(SynchronizerErrorContext error) + { + if (error.Exception is not GoogleApiException googleEx) + return false; + + var reason = googleEx.Error?.Errors?.FirstOrDefault()?.Reason?.ToLowerInvariant() ?? string.Empty; + var message = googleEx.Message?.ToLowerInvariant() ?? string.Empty; + + return googleEx.HttpStatusCode == HttpStatusCode.Unauthorized || + (googleEx.HttpStatusCode == HttpStatusCode.Forbidden && + (reason.Contains("auth") || + reason.Contains("credential") || + message.Contains("invalid credentials") || + message.Contains("insufficient authentication") || + message.Contains("login required"))); + } + + public async Task HandleAsync(SynchronizerErrorContext error) + { + _logger.Warning(error.Exception, + "Gmail authentication failed for account {AccountName} ({AccountId}). User intervention is required.", + error.Account?.Name, error.Account?.Id); + + if (error.Account != null) + { + await PersistInvalidCredentialAttentionAsync(error.Account).ConfigureAwait(false); + } + + error.Severity = SynchronizerErrorSeverity.AuthRequired; + error.Category = SynchronizerErrorCategory.Authentication; + error.RetryDelay = null; + + return true; + } + + private async Task PersistInvalidCredentialAttentionAsync(MailAccount account) + { + var persistedAccount = await _accountService.GetAccountAsync(account.Id).ConfigureAwait(false); + + if (persistedAccount == null || persistedAccount.AttentionReason == AccountAttentionReason.InvalidCredentials) + return; + + persistedAccount.AttentionReason = AccountAttentionReason.InvalidCredentials; + await _accountService.UpdateAccountAsync(persistedAccount).ConfigureAwait(false); + } +} diff --git a/Wino.Core/Synchronizers/Errors/Gmail/GmailHistoryExpiredHandler.cs b/Wino.Core/Synchronizers/Errors/Gmail/GmailHistoryExpiredHandler.cs new file mode 100644 index 00000000..70435125 --- /dev/null +++ b/Wino.Core/Synchronizers/Errors/Gmail/GmailHistoryExpiredHandler.cs @@ -0,0 +1,78 @@ +using System; +using System.Threading.Tasks; +using Google; +using Serilog; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Synchronization; +using Wino.Core.Integration.Processors; + +namespace Wino.Core.Synchronizers.Errors.Gmail; + +/// +/// Handles Gmail history ID expiration errors. +/// When history is no longer available, resets the account's history ID to force a full resync. +/// +public class GmailHistoryExpiredHandler : ISynchronizerErrorHandler +{ + private readonly ILogger _logger = Log.ForContext(); + private readonly IGmailChangeProcessor _gmailChangeProcessor; + + public GmailHistoryExpiredHandler(IGmailChangeProcessor gmailChangeProcessor) + { + _gmailChangeProcessor = gmailChangeProcessor; + } + + public bool CanHandle(SynchronizerErrorContext error) + { + // Gmail returns 404 when history ID is no longer valid + if (error.ErrorCode == 404) + { + var message = error.ErrorMessage?.ToLowerInvariant() ?? string.Empty; + return message.Contains("history") || message.Contains("notfound"); + } + + if (error.Exception is GoogleApiException googleEx) + { + if (googleEx.HttpStatusCode == System.Net.HttpStatusCode.NotFound) + { + var errorMessage = googleEx.Message?.ToLowerInvariant() ?? string.Empty; + return errorMessage.Contains("history") || + errorMessage.Contains("not found") || + errorMessage.Contains("starthistoryid"); + } + } + + return false; + } + + public async Task HandleAsync(SynchronizerErrorContext error) + { + _logger.Warning(error.Exception, + "Gmail history ID expired for account {AccountName} ({AccountId}). Resetting to force full sync.", + error.Account?.Name, error.Account?.Id); + + error.Severity = SynchronizerErrorSeverity.Recoverable; + error.Category = SynchronizerErrorCategory.ResourceNotFound; + + // Reset the account's synchronization identifier (history ID) + if (error.Account != null) + { + try + { + await _gmailChangeProcessor.UpdateAccountDeltaSynchronizationIdentifierAsync( + error.Account.Id, string.Empty).ConfigureAwait(false); + + _logger.Information("Successfully reset Gmail history ID for account {AccountName}. Next sync will be full sync.", + error.Account.Name); + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to reset Gmail history ID for account {AccountName}", + error.Account.Name); + } + } + + return true; + } +} diff --git a/Wino.Core/Synchronizers/Errors/Gmail/GmailQuotaExceededHandler.cs b/Wino.Core/Synchronizers/Errors/Gmail/GmailQuotaExceededHandler.cs new file mode 100644 index 00000000..77363d4a --- /dev/null +++ b/Wino.Core/Synchronizers/Errors/Gmail/GmailQuotaExceededHandler.cs @@ -0,0 +1,57 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Google; +using Serilog; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Synchronization; + +namespace Wino.Core.Synchronizers.Errors.Gmail; + +/// +/// Handles Gmail API quota exceeded errors (HTTP 403 with quota error). +/// This is a more severe rate limit that indicates daily quota exhaustion. +/// +public class GmailQuotaExceededHandler : ISynchronizerErrorHandler +{ + private readonly ILogger _logger = Log.ForContext(); + + public bool CanHandle(SynchronizerErrorContext error) + { + if (error.Exception is GoogleApiException googleEx) + { + // Quota exceeded usually returns 403 + if (googleEx.HttpStatusCode == System.Net.HttpStatusCode.Forbidden) + { + var errorMessage = googleEx.Message?.ToLowerInvariant() ?? string.Empty; + var errorReason = googleEx.Error?.Errors?.FirstOrDefault()?.Reason?.ToLowerInvariant() ?? string.Empty; + + return errorMessage.Contains("quota") || + errorMessage.Contains("limit exceeded") || + errorReason.Contains("quota") || + errorReason.Contains("ratelimitexceeded") || + errorReason.Contains("userlimitexceeded"); + } + } + + return false; + } + + public Task HandleAsync(SynchronizerErrorContext error) + { + _logger.Warning(error.Exception, + "Gmail API quota exceeded for account {AccountName} ({AccountId}). Sync will be paused.", + error.Account?.Name, error.Account?.Id); + + // Quota exceeded is more severe - treat as fatal to prevent repeated failures + // The user will be notified and sync will resume after quota resets + error.Severity = SynchronizerErrorSeverity.Fatal; + error.Category = SynchronizerErrorCategory.RateLimit; + + // Suggest a very long delay - quotas typically reset daily + error.RetryDelay = TimeSpan.FromHours(1); + + return Task.FromResult(true); + } +} diff --git a/Wino.Core/Synchronizers/Errors/Gmail/GmailRateLimitHandler.cs b/Wino.Core/Synchronizers/Errors/Gmail/GmailRateLimitHandler.cs new file mode 100644 index 00000000..30251513 --- /dev/null +++ b/Wino.Core/Synchronizers/Errors/Gmail/GmailRateLimitHandler.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading.Tasks; +using Google; +using Serilog; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Synchronization; + +namespace Wino.Core.Synchronizers.Errors.Gmail; + +/// +/// Handles Gmail API rate limiting errors (HTTP 429 Too Many Requests). +/// Marks the error as transient with appropriate backoff delay. +/// +public class GmailRateLimitHandler : ISynchronizerErrorHandler +{ + private readonly ILogger _logger = Log.ForContext(); + + public bool CanHandle(SynchronizerErrorContext error) + { + if (error.ErrorCode == 429) + return true; + + if (error.Exception is GoogleApiException googleEx) + { + return googleEx.HttpStatusCode == System.Net.HttpStatusCode.TooManyRequests || + (googleEx.Error?.Code == 429); + } + + return false; + } + + public Task HandleAsync(SynchronizerErrorContext error) + { + _logger.Warning(error.Exception, + "Gmail API rate limit hit for account {AccountName} ({AccountId}). Operation: {Operation}. Will retry with backoff.", + error.Account?.Name, error.Account?.Id, error.OperationType ?? "N/A"); + + error.Severity = SynchronizerErrorSeverity.Transient; + error.Category = SynchronizerErrorCategory.RateLimit; + + // Gmail rate limits are usually per-user, suggest a longer delay + error.RetryDelay = TimeSpan.FromSeconds(10); + + return Task.FromResult(true); + } +} diff --git a/Wino.Core/Synchronizers/Errors/Imap/ImapAuthenticationFailedHandler.cs b/Wino.Core/Synchronizers/Errors/Imap/ImapAuthenticationFailedHandler.cs new file mode 100644 index 00000000..144217b8 --- /dev/null +++ b/Wino.Core/Synchronizers/Errors/Imap/ImapAuthenticationFailedHandler.cs @@ -0,0 +1,66 @@ +using System.Threading.Tasks; +using MailKit.Security; +using Serilog; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Synchronization; + +namespace Wino.Core.Synchronizers.Errors.Imap; + +/// +/// Handles IMAP authentication failures (AuthenticationException, SaslException). +/// Marks the error as requiring re-authentication. +/// +public class ImapAuthenticationFailedHandler : ISynchronizerErrorHandler +{ + private readonly ILogger _logger = Log.ForContext(); + private readonly IAccountService _accountService; + + public ImapAuthenticationFailedHandler(IAccountService accountService) + { + _accountService = accountService; + } + + public bool CanHandle(SynchronizerErrorContext error) + { + return error.Exception is AuthenticationException || + error.Exception is SaslException || + (error.ErrorMessage?.Contains("authentication", System.StringComparison.OrdinalIgnoreCase) ?? false); + } + + public async Task HandleAsync(SynchronizerErrorContext error) + { + _logger.Warning(error.Exception, + "IMAP authentication failed for account {AccountName} ({AccountId}). User needs to re-authenticate.", + error.Account?.Name, error.Account?.Id); + + if (error.Account != null) + { + await PersistInvalidCredentialAttentionAsync(error.Account).ConfigureAwait(false); + } + + // Mark as requiring authentication - this will stop sync and notify user + error.Severity = SynchronizerErrorSeverity.AuthRequired; + error.Category = SynchronizerErrorCategory.Authentication; + + // No point in retrying auth failures - credentials need to be updated + error.RetryDelay = null; + + return true; + } + + private async Task PersistInvalidCredentialAttentionAsync(MailAccount account) + { + var persistedAccount = await _accountService.GetAccountAsync(account.Id).ConfigureAwait(false); + + if (persistedAccount == null) + return; + + if (persistedAccount.AttentionReason == AccountAttentionReason.InvalidCredentials) + return; + + persistedAccount.AttentionReason = AccountAttentionReason.InvalidCredentials; + await _accountService.UpdateAccountAsync(persistedAccount).ConfigureAwait(false); + } +} diff --git a/Wino.Core/Synchronizers/Errors/Imap/ImapConnectionLostHandler.cs b/Wino.Core/Synchronizers/Errors/Imap/ImapConnectionLostHandler.cs new file mode 100644 index 00000000..186dca26 --- /dev/null +++ b/Wino.Core/Synchronizers/Errors/Imap/ImapConnectionLostHandler.cs @@ -0,0 +1,45 @@ +using System; +using System.IO; +using System.Net.Sockets; +using System.Threading.Tasks; +using MailKit; +using Serilog; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Synchronization; + +namespace Wino.Core.Synchronizers.Errors.Imap; + +/// +/// Handles IMAP connection loss errors (IOException, SocketException, ServiceNotConnectedException). +/// Marks the error as transient for retry with backoff. +/// +public class ImapConnectionLostHandler : ISynchronizerErrorHandler +{ + private readonly ILogger _logger = Log.ForContext(); + + public bool CanHandle(SynchronizerErrorContext error) + { + return error.Exception is IOException || + error.Exception is SocketException || + error.Exception is ServiceNotConnectedException || + error.Exception?.InnerException is IOException || + error.Exception?.InnerException is SocketException; + } + + public Task HandleAsync(SynchronizerErrorContext error) + { + _logger.Warning(error.Exception, + "IMAP connection lost for account {AccountName} ({AccountId}). Folder: {FolderName}. Operation: {Operation}. Will retry.", + error.Account?.Name, error.Account?.Id, error.FolderName ?? "N/A", error.OperationType ?? "N/A"); + + // Mark as transient - the RetryExecutor will handle the retry logic + error.Severity = SynchronizerErrorSeverity.Transient; + error.Category = SynchronizerErrorCategory.Network; + + // Suggest a reasonable retry delay for connection issues + error.RetryDelay = TimeSpan.FromSeconds(2); + + return Task.FromResult(true); + } +} diff --git a/Wino.Core/Synchronizers/Errors/Imap/ImapFolderNotFoundHandler.cs b/Wino.Core/Synchronizers/Errors/Imap/ImapFolderNotFoundHandler.cs new file mode 100644 index 00000000..d23302c3 --- /dev/null +++ b/Wino.Core/Synchronizers/Errors/Imap/ImapFolderNotFoundHandler.cs @@ -0,0 +1,67 @@ +using System.Threading.Tasks; +using MailKit; +using Serilog; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Synchronization; +using Wino.Core.Integration.Processors; + +namespace Wino.Core.Synchronizers.Errors.Imap; + +/// +/// Handles IMAP folder not found errors (FolderNotFoundException). +/// Deletes the folder locally and allows sync to continue with other folders. +/// +public class ImapFolderNotFoundHandler : ISynchronizerErrorHandler +{ + private readonly ILogger _logger = Log.ForContext(); + private readonly IImapChangeProcessor _imapChangeProcessor; + + public ImapFolderNotFoundHandler(IImapChangeProcessor imapChangeProcessor) + { + _imapChangeProcessor = imapChangeProcessor; + } + + public bool CanHandle(SynchronizerErrorContext error) + { + return error.Exception is FolderNotFoundException || + error.ErrorCode == 404 || + (error.ErrorMessage?.Contains("folder not found", System.StringComparison.OrdinalIgnoreCase) ?? false) || + (error.ErrorMessage?.Contains("mailbox not found", System.StringComparison.OrdinalIgnoreCase) ?? false); + } + + public async Task HandleAsync(SynchronizerErrorContext error) + { + _logger.Warning(error.Exception, + "IMAP folder not found for account {AccountName} ({AccountId}). Folder: {FolderName} ({FolderId}). Removing locally.", + error.Account?.Name, error.Account?.Id, error.FolderName, error.FolderId); + + // Mark as recoverable - sync can continue with other folders + error.Severity = SynchronizerErrorSeverity.Recoverable; + error.Category = SynchronizerErrorCategory.ResourceNotFound; + + // Try to delete the folder locally if we have the folder ID + if (error.FolderId.HasValue && error.Account != null) + { + try + { + // Get the folder's remote ID from the exception if available + var remoteId = error.Exception is FolderNotFoundException fnf ? fnf.FolderName : null; + + if (!string.IsNullOrEmpty(remoteId)) + { + await _imapChangeProcessor.DeleteFolderAsync(error.Account.Id, remoteId).ConfigureAwait(false); + _logger.Information("Successfully deleted local folder {FolderName} after server deletion.", + error.FolderName); + } + } + catch (System.Exception ex) + { + _logger.Warning(ex, "Failed to delete local folder {FolderName} ({FolderId})", + error.FolderName, error.FolderId); + } + } + + return true; + } +} diff --git a/Wino.Core/Synchronizers/Errors/Imap/ImapProtocolErrorHandler.cs b/Wino.Core/Synchronizers/Errors/Imap/ImapProtocolErrorHandler.cs new file mode 100644 index 00000000..6d4eb540 --- /dev/null +++ b/Wino.Core/Synchronizers/Errors/Imap/ImapProtocolErrorHandler.cs @@ -0,0 +1,100 @@ +using System; +using System.Threading.Tasks; +using MailKit.Net.Imap; +using Serilog; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Synchronization; + +namespace Wino.Core.Synchronizers.Errors.Imap; + +/// +/// Handles generic IMAP protocol errors (ImapProtocolException, ImapCommandException). +/// This is the catch-all handler for IMAP errors not handled by more specific handlers. +/// +public class ImapProtocolErrorHandler : ISynchronizerErrorHandler +{ + private readonly ILogger _logger = Log.ForContext(); + + public bool CanHandle(SynchronizerErrorContext error) + { + // This is a catch-all for IMAP-related exceptions + return error.Exception is ImapProtocolException || + error.Exception is ImapCommandException; + } + + public Task HandleAsync(SynchronizerErrorContext error) + { + var severity = ClassifyProtocolError(error); + var category = SynchronizerErrorCategory.ProtocolError; + + _logger.Warning(error.Exception, + "IMAP protocol error for account {AccountName} ({AccountId}). Folder: {FolderName}. Operation: {Operation}. Severity: {Severity}", + error.Account?.Name, error.Account?.Id, error.FolderName ?? "N/A", error.OperationType ?? "N/A", severity); + + error.Severity = severity; + error.Category = category; + + // For transient protocol errors, suggest a retry delay + if (severity == SynchronizerErrorSeverity.Transient) + { + error.RetryDelay = TimeSpan.FromSeconds(5); + } + + return Task.FromResult(true); + } + + /// + /// Classifies the protocol error to determine if it's transient, recoverable, or fatal. + /// + private static SynchronizerErrorSeverity ClassifyProtocolError(SynchronizerErrorContext error) + { + var message = error.ErrorMessage?.ToLowerInvariant() ?? string.Empty; + var exMessage = error.Exception?.Message?.ToLowerInvariant() ?? string.Empty; + + // Check for rate limiting / throttling + if (message.Contains("too many") || message.Contains("rate limit") || + message.Contains("throttl") || exMessage.Contains("too many")) + { + return SynchronizerErrorSeverity.Transient; + } + + // Check for temporary server issues + if (message.Contains("try again") || message.Contains("temporary") || + message.Contains("busy") || exMessage.Contains("try again")) + { + return SynchronizerErrorSeverity.Transient; + } + + // Check for command-specific errors that are usually transient + if (error.Exception is ImapCommandException cmdEx) + { + // NO response usually means the operation failed but can be retried + if (cmdEx.Response == ImapCommandResponse.No) + { + // Unless it's a permanent failure indication + if (message.Contains("permanent") || message.Contains("invalid")) + { + return SynchronizerErrorSeverity.Recoverable; + } + return SynchronizerErrorSeverity.Transient; + } + + // BAD response usually indicates a protocol violation - don't retry + if (cmdEx.Response == ImapCommandResponse.Bad) + { + return SynchronizerErrorSeverity.Recoverable; + } + } + + // Protocol exceptions that indicate connection issues + if (error.Exception is ImapProtocolException) + { + // Most protocol exceptions are connection-related and transient + return SynchronizerErrorSeverity.Transient; + } + + // Default to recoverable for unknown protocol errors + return SynchronizerErrorSeverity.Recoverable; + } +} diff --git a/Wino.Core/Synchronizers/Errors/Outlook/DeltaTokenExpiredHandler.cs b/Wino.Core/Synchronizers/Errors/Outlook/DeltaTokenExpiredHandler.cs new file mode 100644 index 00000000..28a963b3 --- /dev/null +++ b/Wino.Core/Synchronizers/Errors/Outlook/DeltaTokenExpiredHandler.cs @@ -0,0 +1,68 @@ +using System.Threading.Tasks; +using Microsoft.Graph.Models.ODataErrors; +using Microsoft.Kiota.Abstractions; +using Serilog; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Synchronization; +using Wino.Core.Integration.Processors; + +namespace Wino.Core.Synchronizers.Errors.Outlook; + +/// +/// Handles 410 Gone errors for Outlook synchronization, which indicates that delta tokens have expired. +/// When this occurs, all local mail cache should be deleted and initial synchronization should be reset. +/// +public class DeltaTokenExpiredHandler : ISynchronizerErrorHandler +{ + private readonly ILogger _logger = Log.ForContext(); + private readonly IOutlookChangeProcessor _outlookChangeProcessor; + + public DeltaTokenExpiredHandler(IOutlookChangeProcessor outlookChangeProcessor) + { + _outlookChangeProcessor = outlookChangeProcessor; + } + + public bool CanHandle(SynchronizerErrorContext error) + { + // Handle 410 Gone responses which indicate delta token expiration + return error.ErrorCode == 410 || + (error.Exception is ODataError oDataError && oDataError.ResponseStatusCode == 410) || + (error.Exception is ApiException apiException && apiException.ResponseStatusCode == 410); + } + + public async Task HandleAsync(SynchronizerErrorContext error) + { + _logger.Warning("Delta token has expired for account {AccountName} ({AccountId}). Deleting all local mail cache and resetting synchronization.", + error.Account.Name, error.Account.Id); + + try + { + // Delete all local mail cache for the account + await _outlookChangeProcessor.DeleteUserMailCacheAsync(error.Account.Id).ConfigureAwait(false); + + // Reset the account's delta synchronization identifier + await _outlookChangeProcessor.UpdateAccountDeltaSynchronizationIdentifierAsync(error.Account.Id, string.Empty).ConfigureAwait(false); + + // Get all folders for the account and reset their delta tokens + var folders = await _outlookChangeProcessor.GetLocalFoldersAsync(error.Account.Id).ConfigureAwait(false); + + foreach (var folder in folders) + { + // Reset folder delta token to force full re-sync (last 30 days) + await _outlookChangeProcessor.UpdateFolderDeltaSynchronizationIdentifierAsync(folder.Id, string.Empty).ConfigureAwait(false); + } + + _logger.Information("Successfully reset synchronization state for account {AccountName} ({AccountId}). Next sync will download last 30 days.", + error.Account.Name, error.Account.Id); + + return true; + } + catch (System.Exception ex) + { + _logger.Error(ex, "Failed to handle delta token expiration for account {AccountName} ({AccountId})", + error.Account.Name, error.Account.Id); + + return false; + } + } +} \ No newline at end of file diff --git a/Wino.Core/Synchronizers/Errors/Outlook/ObjectCannotBeDeletedHandler.cs b/Wino.Core/Synchronizers/Errors/Outlook/ObjectCannotBeDeletedHandler.cs index 2d78ad64..f31ba474 100644 --- a/Wino.Core/Synchronizers/Errors/Outlook/ObjectCannotBeDeletedHandler.cs +++ b/Wino.Core/Synchronizers/Errors/Outlook/ObjectCannotBeDeletedHandler.cs @@ -1,8 +1,8 @@ using System.Threading.Tasks; using Microsoft.Kiota.Abstractions; using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Errors; using Wino.Core.Domain.Models.Requests; +using Wino.Core.Domain.Models.Synchronization; using Wino.Core.Requests.Bundles; namespace Wino.Core.Synchronizers.Errors.Outlook; @@ -18,7 +18,7 @@ public class ObjectCannotBeDeletedHandler : ISynchronizerErrorHandler public bool CanHandle(SynchronizerErrorContext error) { - return error.ErrorMessage.Contains("ErrorCannotDeleteObject") && error.RequestBundle is HttpRequestBundle; + return error.ErrorMessage.Contains("Object cannot be deleted.") && error.RequestBundle is HttpRequestBundle; } public async Task HandleAsync(SynchronizerErrorContext error) diff --git a/Wino.Core/Synchronizers/Errors/Outlook/OutlookAuthenticationFailedHandler.cs b/Wino.Core/Synchronizers/Errors/Outlook/OutlookAuthenticationFailedHandler.cs new file mode 100644 index 00000000..9183504c --- /dev/null +++ b/Wino.Core/Synchronizers/Errors/Outlook/OutlookAuthenticationFailedHandler.cs @@ -0,0 +1,83 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Graph.Models.ODataErrors; +using Microsoft.Kiota.Abstractions; +using Serilog; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Synchronization; + +namespace Wino.Core.Synchronizers.Errors.Outlook; + +public class OutlookAuthenticationFailedHandler : ISynchronizerErrorHandler +{ + private readonly ILogger _logger = Log.ForContext(); + private readonly IAccountService _accountService; + + public OutlookAuthenticationFailedHandler(IAccountService accountService) + { + _accountService = accountService; + } + + public bool CanHandle(SynchronizerErrorContext error) + { + if (error.Exception is ApiException apiException) + { + if (apiException.ResponseStatusCode == 401) + return true; + + if (apiException.ResponseStatusCode == 403) + { + var message = apiException.Message?.ToLowerInvariant() ?? string.Empty; + return message.Contains("access denied") || message.Contains("authentication"); + } + } + + if (error.Exception is ODataError oDataError) + { + if (oDataError.ResponseStatusCode == 401) + return true; + + var code = oDataError.Error?.Code?.ToLowerInvariant() ?? string.Empty; + var message = oDataError.Error?.Message?.ToLowerInvariant() ?? string.Empty; + + return code.Contains("invalidauthenticationtoken") || + code.Contains("invalidgrant") || + code.Contains("token") || + message.Contains("access token") || + message.Contains("authentication"); + } + + return false; + } + + public async Task HandleAsync(SynchronizerErrorContext error) + { + _logger.Warning(error.Exception, + "Outlook authentication failed for account {AccountName} ({AccountId}). User intervention is required.", + error.Account?.Name, error.Account?.Id); + + if (error.Account != null) + { + await PersistInvalidCredentialAttentionAsync(error.Account).ConfigureAwait(false); + } + + error.Severity = SynchronizerErrorSeverity.AuthRequired; + error.Category = SynchronizerErrorCategory.Authentication; + error.RetryDelay = null; + + return true; + } + + private async Task PersistInvalidCredentialAttentionAsync(MailAccount account) + { + var persistedAccount = await _accountService.GetAccountAsync(account.Id).ConfigureAwait(false); + + if (persistedAccount == null || persistedAccount.AttentionReason == AccountAttentionReason.InvalidCredentials) + return; + + persistedAccount.AttentionReason = AccountAttentionReason.InvalidCredentials; + await _accountService.UpdateAccountAsync(persistedAccount).ConfigureAwait(false); + } +} diff --git a/Wino.Core/Synchronizers/Errors/Outlook/OutlookRateLimitHandler.cs b/Wino.Core/Synchronizers/Errors/Outlook/OutlookRateLimitHandler.cs new file mode 100644 index 00000000..82456e66 --- /dev/null +++ b/Wino.Core/Synchronizers/Errors/Outlook/OutlookRateLimitHandler.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Graph.Models.ODataErrors; +using Microsoft.Kiota.Abstractions; +using Serilog; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Synchronization; + +namespace Wino.Core.Synchronizers.Errors.Outlook; + +/// +/// Handles Microsoft Graph throttling responses for Outlook synchronization. +/// +public class OutlookRateLimitHandler : ISynchronizerErrorHandler +{ + private readonly ILogger _logger = Log.ForContext(); + + public bool CanHandle(SynchronizerErrorContext error) + { + return error.ErrorCode == 429 || + (error.Exception is ODataError oDataError && oDataError.ResponseStatusCode == 429) || + (error.Exception is ApiException apiException && apiException.ResponseStatusCode == 429); + } + + public Task HandleAsync(SynchronizerErrorContext error) + { + _logger.Warning(error.Exception, + "Microsoft Graph rate limit hit for account {AccountName} ({AccountId}). Operation: {Operation}.", + error.Account?.Name, error.Account?.Id, error.OperationType ?? "N/A"); + + error.Severity = SynchronizerErrorSeverity.Transient; + error.Category = SynchronizerErrorCategory.RateLimit; + error.RetryDelay = TimeSpan.FromSeconds(10); + + return Task.FromResult(true); + } +} diff --git a/Wino.Core/Synchronizers/GmailSynchronizer.cs b/Wino.Core/Synchronizers/GmailSynchronizer.cs index e1fb9b49..5445e3aa 100644 --- a/Wino.Core/Synchronizers/GmailSynchronizer.cs +++ b/Wino.Core/Synchronizers/GmailSynchronizer.cs @@ -1,18 +1,23 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net.Http; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using System.Web; using CommunityToolkit.Mvvm.Messaging; using Google; using Google.Apis.Calendar.v3.Data; +using Google.Apis.Drive.v3; using Google.Apis.Gmail.v1; using Google.Apis.Gmail.v1.Data; using Google.Apis.Http; using Google.Apis.PeopleService.v1; using Google.Apis.Requests; using Google.Apis.Services; +using Google.Apis.Upload; using MailKit; using Microsoft.IdentityModel.Tokens; using MimeKit; @@ -23,29 +28,63 @@ using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Exceptions; +using Wino.Core.Domain.Extensions; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Accounts; -using Wino.Core.Domain.Models.Errors; using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Synchronization; using Wino.Core.Extensions; using Wino.Core.Http; using Wino.Core.Integration.Processors; +using Wino.Core.Misc; using Wino.Core.Requests.Bundles; +using Wino.Core.Requests.Calendar; using Wino.Core.Requests.Folder; using Wino.Core.Requests.Mail; using Wino.Messaging.UI; using Wino.Services; using CalendarService = Google.Apis.Calendar.v3.CalendarService; +using DriveFile = Google.Apis.Drive.v3.Data.File; +using DriveService = Google.Apis.Drive.v3.DriveService; namespace Wino.Core.Synchronizers.Mail; +[JsonSerializable(typeof(Message))] +[JsonSerializable(typeof(Label))] +[JsonSerializable(typeof(Draft))] +[JsonSerializable(typeof(Event))] +public partial class GmailSynchronizerJsonContext : JsonSerializerContext; + +/// +/// Gmail synchronizer implementation using Gmail History API for efficient incremental sync. +/// +/// SYNCHRONIZATION STRATEGY: +/// - Initial sync: Downloads up to 1500 messages PER FOLDER with metadata only. +/// Uses a global HashSet to track downloaded message IDs, avoiding duplicate downloads +/// when messages have multiple labels. Each folder gets its full quota of messages. +/// - Incremental sync: Uses ONLY History API to get changes since last sync. +/// No per-folder downloads during incremental sync - this is the proper Gmail sync approach. +/// - Messages are downloaded with metadata only during initial sync (no MIME content) +/// - New messages during incremental sync are downloaded with full MIME content +/// - MIME files for initial sync messages are downloaded on-demand when user reads a message +/// +/// Key implementation details: +/// - PerformInitialSyncAsync: Downloads messages per-folder with global deduplication +/// - SynchronizeDeltaAsync: Processes incremental changes using History API with pagination +/// - Handles 404/410 errors (history expired) by triggering full resync +/// - CreateMinimalMailCopyAsync: Extracts MailCopy fields from Gmail Metadata format +/// - DownloadMissingMimeMessageAsync: Downloads raw MIME only when explicitly requested +/// public class GmailSynchronizer : WinoSynchronizer, IHttpClientFactory { public override uint BatchModificationSize => 1000; - /// This now represents actual per-folder download count for initial sync + /// + /// Maximum messages to fetch per folder during initial sync (1500). + /// All messages are downloaded with METADATA ONLY - no raw MIME content. + /// Uses Gmail API's Metadata format which includes headers, labels, and snippet but NOT full message body. + /// public override uint InitialMessageDownloadCountPerFolder => 1500; // It's actually 100. But Gmail SDK has internal bug for Out of Memory exception. @@ -55,6 +94,7 @@ public class GmailSynchronizer : WinoSynchronizer(); + var folderResults = new List(); - // Gmail must always synchronize folders before because it doesn't have a per-folder sync. - bool shouldSynchronizeFolders = true; - - if (shouldSynchronizeFolders) + try { + _isFolderStructureChanged = false; + + // Make sure that virtual archive folder exists before all. + if (!archiveFolderId.HasValue) + await InitializeArchiveFolderAsync().ConfigureAwait(false); + + // Gmail must always synchronize folders before because it doesn't have a per-folder sync. _logger.Information("Synchronizing folders for {Name}", Account.Name); + UpdateSyncProgress(0, 0, "Synchronizing folders..."); try { @@ -142,214 +188,318 @@ public class GmailSynchronizer : WinoSynchronizer(); - - var deltaChanges = new List(); // For tracking delta changes. - var listChanges = new List(); // For tracking initial sync changes. - - /* Processing flow order is important to preserve the validity of history. - * 1 - Process added mails. Because we need to create the mail first before assigning it to labels. - * 2 - Process label assignments. - * 3 - Process removed mails. - * This affects reporting progres if done individually for each history change. - * Therefore we need to process all changes in one go after the fetch. - */ - - if (isInitialSync) - { - // Get all folders that need synchronization - var folders = await _gmailChangeProcessor.GetLocalFoldersAsync(Account.Id).ConfigureAwait(false); - var syncFolders = folders.Where(f => - f.IsSynchronizationEnabled && - f.SpecialFolderType != SpecialFolderType.Category && - f.SpecialFolderType != SpecialFolderType.Archive).ToList(); - - // Download messages for each folder separately - foreach (var folder in syncFolders) + if (isInitialSync) { - var messageRequest = _gmailService.Users.Messages.List("me"); - messageRequest.MaxResults = InitialMessageDownloadCountPerFolder; - messageRequest.LabelIds = new[] { folder.RemoteFolderId }; - // messageRequest.OrderBy = "internalDate desc"; // Get latest messages first - messageRequest.IncludeSpamTrash = true; + // INITIAL SYNC: Download all messages globally (not per-folder) to avoid duplicates. + // Gmail messages can have multiple labels, so per-folder download would fetch same message multiple times. + downloadedMessageIds = await PerformInitialSyncAsync(cancellationToken).ConfigureAwait(false); - string nextPageToken = null; - uint downloadedCount = 0; - - do + // Set the history ID to the latest value after initial sync + UpdateSyncProgress(0, 0, "Finalizing synchronization..."); + var profile = await _gmailService.Users.GetProfile("me").ExecuteAsync(cancellationToken); + if (profile.HistoryId.HasValue) { - if (!string.IsNullOrEmpty(nextPageToken)) - { - messageRequest.PageToken = nextPageToken; - } + await UpdateAccountSyncIdentifierAsync(profile.HistoryId.Value).ConfigureAwait(false); + _logger.Information("Initial sync completed. Set history ID to {HistoryId}", profile.HistoryId.Value); + } - var result = await messageRequest.ExecuteAsync(cancellationToken); - nextPageToken = result.NextPageToken; - - if (result.Messages != null) - { - downloadedCount += (uint)result.Messages.Count; - listChanges.Add(result); - } - - // Stop if we've downloaded enough messages for this folder - if (downloadedCount >= InitialMessageDownloadCountPerFolder) - { - break; - } - - } while (!string.IsNullOrEmpty(nextPageToken)); - - _logger.Information("Downloaded {Count} messages for folder {Folder}", downloadedCount, folder.FolderName); - } - } - else - { - var startHistoryId = ulong.Parse(Account.SynchronizationDeltaIdentifier); - var nextPageToken = ulong.Parse(Account.SynchronizationDeltaIdentifier).ToString(); - - var historyRequest = _gmailService.Users.History.List("me"); - historyRequest.StartHistoryId = startHistoryId; - - try - { - while (!string.IsNullOrEmpty(nextPageToken)) + // Create successful folder results for all folders + var allFolders = await _gmailChangeProcessor.GetSynchronizationFoldersAsync(options).ConfigureAwait(false); + foreach (var folder in allFolders.Where(f => f.RemoteFolderId != ServiceConstants.ARCHIVE_LABEL_ID)) { - // If this is the first delta check, start from the last history id. - // Otherwise start from the next page token. We set them both to the same value for start. - // For each different page we set the page token to the next page token. - - bool isFirstDeltaCheck = nextPageToken == startHistoryId.ToString(); - - if (!isFirstDeltaCheck) - historyRequest.PageToken = nextPageToken; - - var historyResponse = await historyRequest.ExecuteAsync(cancellationToken); - - nextPageToken = historyResponse.NextPageToken; - - if (historyResponse.History == null) - continue; - - deltaChanges.Add(historyResponse); + folderResults.Add(FolderSyncResult.Successful(folder.Id, folder.FolderName, 0)); } } - catch (GoogleApiException ex) when (ex.HttpStatusCode == System.Net.HttpStatusCode.NotFound) + else { - // History ID is too old or expired, need to do a full sync. - // Theoratically we need to delete the local cache and start from scratch. + // INCREMENTAL SYNC: Use ONLY History API - no per-folder downloads. + // This is the proper Gmail sync strategy as recommended by Google. + UpdateSyncProgress(0, 0, "Synchronizing changes..."); + var deltaResult = await SynchronizeDeltaAsync(options, cancellationToken).ConfigureAwait(false); + downloadedMessageIds.AddRange(deltaResult.DownloadedMessageIds); - _logger.Warning("History ID {StartHistoryId} is expired for {Name}. Will remove user's mail cache and do full sync.", startHistoryId, Account.Name); + // If history sync was reset due to expired history ID, we need to do initial sync + if (deltaResult.RequiresFullResync) + { + _logger.Warning("History ID expired. Performing full resync for {Name}", Account.Name); + downloadedMessageIds = await PerformInitialSyncAsync(cancellationToken).ConfigureAwait(false); - await _gmailChangeProcessor.DeleteUserMailCacheAsync(Account.Id).ConfigureAwait(false); + // Update history ID after full resync + var profile = await _gmailService.Users.GetProfile("me").ExecuteAsync(cancellationToken); + if (profile.HistoryId.HasValue) + { + await UpdateAccountSyncIdentifierAsync(profile.HistoryId.Value).ConfigureAwait(false); + _logger.Information("Full resync completed. Set history ID to {HistoryId}", profile.HistoryId.Value); + } + } - Account.SynchronizationDeltaIdentifier = string.Empty; + UpdateSyncProgress(0, 0, "Changes synchronized"); - await _gmailChangeProcessor.UpdateAccountAsync(Account).ConfigureAwait(false); + // Create folder results for incremental sync + var allFolders = await _gmailChangeProcessor.GetSynchronizationFoldersAsync(options).ConfigureAwait(false); + foreach (var folder in allFolders.Where(f => f.RemoteFolderId != ServiceConstants.ARCHIVE_LABEL_ID)) + { + folderResults.Add(FolderSyncResult.Successful(folder.Id, folder.FolderName, 0)); + } + } - goto retry; + // Map Gmail Draft resource IDs for all drafts. + // Gmail's Messages API doesn't expose Draft IDs, so we query the Drafts API separately. + // This ensures DraftId is correctly set for both Wino-created and externally-created drafts. + await MapDraftIdsAsync(cancellationToken).ConfigureAwait(false); + + // Keep virtual Archive folder assignments in sync with Gmail "in:archive" query. + try + { + await MapArchivedMailsAsync(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.Warning(ex, "Failed to map Gmail archive folder for {Name}", Account.Name); } } - - // Add initial message ids from initial sync. - missingMessageIds.AddRange(listChanges.Where(a => a.Messages != null).SelectMany(a => a.Messages).Select(a => a.Id)); - - // Add missing message ids from delta changes. - foreach (var historyResponse in deltaChanges) + catch (OperationCanceledException) { - var addedMessageIds = historyResponse.History - .Where(a => a.MessagesAdded != null) - .SelectMany(a => a.MessagesAdded) - .Where(a => a.Message != null) - .Select(a => a.Message.Id); - - missingMessageIds.AddRange(addedMessageIds); - } - - // Start downloading missing messages. - foreach (var messageId in missingMessageIds) - { - await DownloadSingleMessageAsync(messageId, cancellationToken).ConfigureAwait(false); - } - - // Map archive assignments if there are any changes reported. - if (listChanges.Any() || deltaChanges.Any()) - { - await MapArchivedMailsAsync(cancellationToken).ConfigureAwait(false); - } - - // Map remote drafts to local drafts. - await MapDraftIdsAsync(cancellationToken).ConfigureAwait(false); - - // Start processing delta changes. - foreach (var historyResponse in deltaChanges) - { - await ProcessHistoryChangesAsync(historyResponse).ConfigureAwait(false); - } - - // Take the max history id from delta changes and update the account sync modifier. - - if (deltaChanges.Any()) - { - var maxHistoryId = deltaChanges.Where(a => a.HistoryId != null).Max(a => a.HistoryId); - - await UpdateAccountSyncIdentifierAsync(maxHistoryId); - - if (maxHistoryId != null) - { - // TODO: This is not good. Centralize the identifier fetch and prevent direct access here. - // Account.SynchronizationDeltaIdentifier = await _gmailChangeProcessor.UpdateAccountDeltaSynchronizationIdentifierAsync(Account.Id, maxHistoryId.ToString()).ConfigureAwait(false); - - _logger.Debug("Final sync identifier {SynchronizationDeltaIdentifier}", Account.SynchronizationDeltaIdentifier); - } - } - - // Get all unred new downloaded items and return in the result. - // This is primarily used in notifications. - - var unreadNewItems = await _gmailChangeProcessor.GetDownloadedUnreadMailsAsync(Account.Id, missingMessageIds).ConfigureAwait(false); - - return MailSynchronizationResult.Completed(unreadNewItems); - } - - private async Task DownloadSingleMessageAsync(string messageId, CancellationToken cancellationToken = default) - { - // Google .NET SDK has memory issues with batch downloading messages which will not be fixed since the library is in maintenance mode. - // https://github.com/googleapis/google-api-dotnet-client/issues/2603 - // This method will be used to download messages one by one to prevent memory spikes. - - try - { - var singleRequest = CreateSingleMessageGet(messageId); - var downloadedMessage = await singleRequest.ExecuteAsync(cancellationToken).ConfigureAwait(false); - - await HandleSingleItemDownloadedCallbackAsync(downloadedMessage, null, messageId, cancellationToken).ConfigureAwait(false); - await UpdateAccountSyncIdentifierAsync(downloadedMessage.HistoryId).ConfigureAwait(false); + _logger.Information("Synchronization was canceled for {Name}", Account.Name); + return MailSynchronizationResult.Canceled; } catch (Exception ex) { - _logger.Error(ex, "Error while downloading message {MessageId} for {Name}", messageId, Account.Name); + _logger.Error(ex, "Synchronization failed for {Name}", Account.Name); + return MailSynchronizationResult.Failed(ex); + } + + // Get all unread new downloaded items for notifications + var unreadNewItems = await _gmailChangeProcessor.GetDownloadedUnreadMailsAsync(Account.Id, downloadedMessageIds).ConfigureAwait(false); + + return MailSynchronizationResult.CompletedWithFolderResults(unreadNewItems, folderResults); + } + + /// + /// Result of delta synchronization using History API. + /// + private record DeltaSyncResult(List DownloadedMessageIds, bool RequiresFullResync); + + /// + /// Performs initial synchronization by downloading messages per-folder. + /// Each folder gets up to 1500 messages, but we track already downloaded message IDs globally + /// to avoid downloading the same message multiple times (Gmail messages can have multiple labels). + /// + private async Task> PerformInitialSyncAsync(CancellationToken cancellationToken) + { + // Track all downloaded message IDs globally to avoid duplicate downloads + var downloadedMessageIds = new HashSet(); + + _logger.Information("Performing initial sync for {Name} - downloading messages per folder", Account.Name); + + try + { + // Get all folders to sync (exclude virtual ARCHIVE folder) + var folders = await _gmailChangeProcessor.GetLocalFoldersAsync(Account.Id).ConfigureAwait(false); + var syncableFolders = folders + .Where(f => f.IsSynchronizationEnabled && f.RemoteFolderId != ServiceConstants.ARCHIVE_LABEL_ID) + .OrderByDescending(f => f.SpecialFolderType == SpecialFolderType.Draft || f.RemoteFolderId == ServiceConstants.DRAFT_LABEL_ID) + .ToList(); + + var totalFolders = syncableFolders.Count; + var totalMessagesDownloaded = 0; + + for (int i = 0; i < totalFolders; i++) + { + var folder = syncableFolders[i]; + cancellationToken.ThrowIfCancellationRequested(); + + UpdateSyncProgress(totalFolders, totalFolders - (i + 1), $"Syncing {folder.FolderName}..."); + + _logger.Debug("Downloading messages for folder {FolderName} (label: {LabelId})", folder.FolderName, folder.RemoteFolderId); + + var folderDownloaded = 0; + string pageToken = null; + var remainingToDownload = (int)InitialMessageDownloadCountPerFolder; + + do + { + cancellationToken.ThrowIfCancellationRequested(); + + var request = _gmailService.Users.Messages.List("me"); + request.LabelIds = new Google.Apis.Util.Repeatable(new[] { folder.RemoteFolderId }); + request.MaxResults = Math.Min(remainingToDownload, 500); // API max is 500 + request.PageToken = pageToken; + + var response = await request.ExecuteAsync(cancellationToken); + + if (response.Messages != null && response.Messages.Count > 0) + { + // Filter out already downloaded messages to avoid duplicates + var newMessageIds = response.Messages + .Select(m => m.Id) + .Where(id => !downloadedMessageIds.Contains(id)) + .ToList(); + + if (newMessageIds.Count > 0) + { + // Draft folder needs MIME during initial sync so compose can open immediately. + bool shouldDownloadRawMime = folder.SpecialFolderType == SpecialFolderType.Draft || folder.RemoteFolderId == ServiceConstants.DRAFT_LABEL_ID; + await DownloadMessagesInBatchAsync(newMessageIds, downloadRawMime: shouldDownloadRawMime, cancellationToken).ConfigureAwait(false); + + foreach (var id in newMessageIds) + { + downloadedMessageIds.Add(id); + } + + folderDownloaded += newMessageIds.Count; + totalMessagesDownloaded += newMessageIds.Count; + } + + // Count all messages (including duplicates) toward the folder limit + remainingToDownload -= response.Messages.Count; + + _logger.Debug("Folder {FolderName}: Downloaded {New} new messages ({Total} total in folder)", + folder.FolderName, newMessageIds.Count, folderDownloaded); + } + + pageToken = response.NextPageToken; + + // Stop if we've processed enough messages for this folder or no more pages + if (remainingToDownload <= 0 || string.IsNullOrEmpty(pageToken)) + break; + + } while (!string.IsNullOrEmpty(pageToken)); + + _logger.Information("Folder {FolderName}: Downloaded {Count} messages", folder.FolderName, folderDownloaded); + UpdateSyncProgress(0, 0, $"Downloaded {totalMessagesDownloaded} messages"); + } + + _logger.Information("Initial sync completed. Downloaded {Count} unique messages for {Name}", downloadedMessageIds.Count, Account.Name); + } + catch (GoogleApiException ex) when (ex.HttpStatusCode == System.Net.HttpStatusCode.TooManyRequests) + { + _logger.Warning("Rate limit exceeded during initial sync. Retrying after delay."); + await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken); + throw; + } + catch (Exception ex) + { + _logger.Error(ex, "Error during initial sync for {Name}", Account.Name); + throw; + } + + return downloadedMessageIds.ToList(); + } + + /// + /// Performs incremental synchronization using Gmail History API. + /// This is the recommended approach for Gmail sync after initial sync is complete. + /// Returns a result indicating downloaded messages and whether a full resync is needed. + /// + private async Task SynchronizeDeltaAsync(MailSynchronizationOptions options, CancellationToken cancellationToken = default) + { + var downloadedMessageIds = new List(); + + try + { + string pageToken = null; + + do + { + cancellationToken.ThrowIfCancellationRequested(); + + var historyRequest = _gmailService.Users.History.List("me"); + historyRequest.StartHistoryId = ulong.Parse(Account.SynchronizationDeltaIdentifier!); + + if (!string.IsNullOrEmpty(pageToken)) + historyRequest.PageToken = pageToken; + + var historyResponse = await historyRequest.ExecuteAsync(cancellationToken); + + if (historyResponse.History != null) + { + var addedMessageIds = new List(); + + // Collect all added messages first + foreach (var historyRecord in historyResponse.History) + { + if (historyRecord.MessagesAdded != null) + { + addedMessageIds.AddRange(historyRecord.MessagesAdded.Select(ma => ma.Message.Id)); + } + } + + // Process added messages in batches if any + // During delta sync, download with Raw format to get MIME content for new messages + if (addedMessageIds.Count != 0) + { + // Deduplicate message IDs + var uniqueAddedIds = addedMessageIds.Distinct().ToList(); + await DownloadMessagesInBatchAsync(uniqueAddedIds, downloadRawMime: true, cancellationToken).ConfigureAwait(false); + downloadedMessageIds.AddRange(uniqueAddedIds); + } + + // Process other history changes (label changes, deletions) + await ProcessHistoryChangesAsync(historyResponse).ConfigureAwait(false); + } + + // CRITICAL: Update the history ID to the latest one after processing all changes + // History IDs are always incremental, so the response contains the latest history ID + if (historyResponse.HistoryId.HasValue) + { + await UpdateAccountSyncIdentifierAsync(historyResponse.HistoryId.Value).ConfigureAwait(false); + _logger.Debug("Updated history ID to {HistoryId} after delta sync", historyResponse.HistoryId.Value); + } + + pageToken = historyResponse.NextPageToken; + + } while (!string.IsNullOrEmpty(pageToken)); + + _logger.Information("Delta sync completed. Downloaded {Count} new messages for {Name}", downloadedMessageIds.Count, Account.Name); + + return new DeltaSyncResult(downloadedMessageIds, RequiresFullResync: false); + } + catch (GoogleApiException ex) when (ex.HttpStatusCode == System.Net.HttpStatusCode.NotFound || + (int)ex.HttpStatusCode == 410) // Gone - history expired + { + // History ID is no longer valid (expired or not found) + // This happens when: + // 1. The history ID is too old (Gmail keeps history for ~30 days) + // 2. The account was reset or history was cleared + // Reset the sync identifier and signal that a full resync is needed + _logger.Warning("History ID {HistoryId} expired or not found for {Name}. Full resync required. Error: {Error}", + Account.SynchronizationDeltaIdentifier, Account.Name, ex.Message); + + // Clear the sync identifier to trigger initial sync + Account.SynchronizationDeltaIdentifier = await _gmailChangeProcessor + .UpdateAccountDeltaSynchronizationIdentifierAsync(Account.Id, null) + .ConfigureAwait(false); + + return new DeltaSyncResult(downloadedMessageIds, RequiresFullResync: true); + } + catch (GoogleApiException ex) when (ex.HttpStatusCode == System.Net.HttpStatusCode.TooManyRequests) + { + _logger.Warning("Rate limit exceeded during delta sync for {Name}. Retrying after delay.", Account.Name); + await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken); + throw; } } @@ -361,18 +511,26 @@ public class GmailSynchronizer : WinoSynchronizer c.IsSynchronizationEnabled) + .ToList(); // TODO: Better logging and exception handling. foreach (var calendar in localCalendars) { var request = _calendarService.Events.List(calendar.RemoteCalendarId); - request.SingleEvents = false; + // Fetch individual event instances (including recurring event occurrences) + // rather than recurring event masters. This ensures we get all occurrences + // as separate events that can be stored and displayed directly. + request.SingleEvents = true; request.ShowDeleted = true; if (!string.IsNullOrEmpty(calendar.SynchronizationDeltaToken)) @@ -444,6 +602,8 @@ public class GmailSynchronizer : WinoSynchronizer(StringComparer.OrdinalIgnoreCase); List insertedCalendars = new(); List updatedCalendars = new(); @@ -473,15 +633,26 @@ public class GmailSynchronizer : WinoSynchronizer User might've already have another special folder for Archive. // We must remove that type assignment. @@ -544,6 +718,11 @@ public class GmailSynchronizer : WinoSynchronizer a.SpecialFolderType == SpecialFolderType.Archive && a.Id != archiveFolderId.Value).ToList(); + if (otherArchiveFolders.Any()) + { + _isFolderStructureChanged = true; + } + foreach (var otherArchiveFolder in otherArchiveFolders) { otherArchiveFolder.SpecialFolderType = SpecialFolderType.Other; @@ -644,18 +823,45 @@ public class GmailSynchronizer : WinoSynchronizer remoteCalendars) + { + if (remoteCalendars == null || remoteCalendars.Count == 0) + return string.Empty; + + var explicitPrimary = remoteCalendars.FirstOrDefault(c => c.Primary.GetValueOrDefault()); + if (explicitPrimary != null) + return explicitPrimary.Id; + + var byPrimaryKeyword = remoteCalendars.FirstOrDefault(c => string.Equals(c.Id, "primary", StringComparison.OrdinalIgnoreCase)); + if (byPrimaryKeyword != null) + return byPrimaryKeyword.Id; + + var byAccountAddress = remoteCalendars.FirstOrDefault(c => string.Equals(c.Id, Account.Address, StringComparison.OrdinalIgnoreCase)); + if (byAccountAddress != null) + return byAccountAddress.Id; + + return remoteCalendars.First().Id; } private bool ShouldUpdateFolder(Label remoteFolder, MailItemFolder existingLocalFolder) @@ -671,13 +877,34 @@ public class GmailSynchronizer : WinoSynchronizer - /// Returns a single get request to retrieve the raw message with the given id + /// Returns a single get request to retrieve the message with the given id. + /// Always uses Metadata format to download only headers and labels - NOT raw MIME content. + /// MIME content is only downloaded when explicitly needed via DownloadMissingMimeMessageAsync. /// /// Message to download. - /// Get request for raw mail. + /// Get request for message with Metadata format. private UsersResource.MessagesResource.GetRequest CreateSingleMessageGet(string messageId) { var singleRequest = _gmailService.Users.Messages.Get("me", messageId); + + // Always use Metadata format for synchronization - this populates Payload.Headers + // but does NOT download the raw MIME content, saving significant bandwidth and time + singleRequest.Format = UsersResource.MessagesResource.GetRequest.FormatEnum.Metadata; + + return singleRequest; + } + + /// + /// Returns a single get request to retrieve the message with Raw format (includes MIME). + /// Used during delta sync to download full message content. + /// + /// Message to download. + /// Get request for message with Raw format. + private UsersResource.MessagesResource.GetRequest CreateSingleMessageGetRaw(string messageId) + { + var singleRequest = _gmailService.Users.Messages.Get("me", messageId); + + // Use Raw format to get full MIME content singleRequest.Format = UsersResource.MessagesResource.GetRequest.FormatEnum.Raw; return singleRequest; @@ -690,7 +917,7 @@ public class GmailSynchronizer : WinoSynchronizerList of history changes. private async Task ProcessHistoryChangesAsync(ListHistoryResponse listHistoryResponse) { - _logger.Debug("Processing delta change {HistoryId} for {Name}", Account.Name, listHistoryResponse.HistoryId.GetValueOrDefault()); + _logger.Debug("Processing delta change {HistoryId} for {Name}", listHistoryResponse.HistoryId.GetValueOrDefault(), Account.Name); foreach (var history in listHistoryResponse.History) { @@ -758,6 +985,13 @@ public class GmailSynchronizer : WinoSynchronizer> OnlineSearchAsync(string queryText, List folders, CancellationToken cancellationToken = default) { - var request = _gmailService.Users.Messages.List("me"); - request.Q = queryText; - request.MaxResults = 500; // Max 500 is returned. + if (string.IsNullOrWhiteSpace(queryText)) + return []; - string pageToken = null; + static bool IsArchiveFolder(IMailItemFolder folder) + => folder?.SpecialFolderType == SpecialFolderType.Archive || folder?.RemoteFolderId == ServiceConstants.ARCHIVE_LABEL_ID; - List messagesToDownload = []; + var messageIds = new HashSet(StringComparer.Ordinal); - do + async Task CollectMessageIdsAsync(UsersResource.MessagesResource.ListRequest request) { - if (queryText.StartsWith("label:") || queryText.StartsWith("in:")) + string pageToken = null; + + do { - // Ignore the folders if the query starts with these keywords. - // User is trying to list everything. - } - else if (folders?.Count > 0) - { - request.LabelIds = folders.Select(a => a.RemoteFolderId).ToList(); - } + if (!string.IsNullOrEmpty(pageToken)) + { + request.PageToken = pageToken; + } - if (!string.IsNullOrEmpty(pageToken)) - { - request.PageToken = pageToken; - } + var response = await request.ExecuteAsync(cancellationToken).ConfigureAwait(false); + if (response.Messages == null || response.Messages.Count == 0) break; - var response = await request.ExecuteAsync(cancellationToken); - if (response.Messages == null) break; + foreach (var message in response.Messages) + { + if (!string.IsNullOrEmpty(message.Id)) + { + messageIds.Add(message.Id); + } + } - // Handle skipping manually - messagesToDownload.AddRange(response.Messages); - - pageToken = response.NextPageToken; - } while (!string.IsNullOrEmpty(pageToken)); - - // Do not download messages that exists, but return them for listing. - - var messageIds = messagesToDownload.Select(a => a.Id); - - var downloadRequireMessageIds = messageIds.Except(await _gmailChangeProcessor.AreMailsExistsAsync(messageIds)); - - // Download missing messages. - foreach (var messageId in downloadRequireMessageIds) - { - await DownloadSingleMessageAsync(messageId, cancellationToken).ConfigureAwait(false); + pageToken = response.NextPageToken; + } while (!string.IsNullOrEmpty(pageToken)); } - // Get results from database and return. + bool hasScopedQuery = queryText.StartsWith("label:", StringComparison.OrdinalIgnoreCase) || + queryText.StartsWith("in:", StringComparison.OrdinalIgnoreCase); - return await _gmailChangeProcessor.GetMailCopiesAsync(messageIds); + if (hasScopedQuery || folders?.Count == 0) + { + var request = _gmailService.Users.Messages.List("me"); + request.Q = queryText; + request.MaxResults = 500; + + await CollectMessageIdsAsync(request).ConfigureAwait(false); + } + else + { + foreach (var folder in folders) + { + cancellationToken.ThrowIfCancellationRequested(); + + var request = _gmailService.Users.Messages.List("me"); + request.MaxResults = 500; + + if (IsArchiveFolder(folder)) + { + // Gmail archive is virtual. Query via search operator instead of label id. + request.Q = $"in:archive {queryText}".Trim(); + } + else + { + request.Q = queryText; + request.LabelIds = new List { folder.RemoteFolderId }; + } + + await CollectMessageIdsAsync(request).ConfigureAwait(false); + } + } + + if (messageIds.Count == 0) + return []; + + var messageIdList = messageIds.ToList(); + + // Do not download messages that already exist locally. + var existingMessageIds = await _gmailChangeProcessor.AreMailsExistsAsync(messageIdList).ConfigureAwait(false); + var messagesToDownload = messageIdList.Except(existingMessageIds, StringComparer.Ordinal); + + // Download missing messages in batch with metadata only. + await DownloadMessagesInBatchAsync(messagesToDownload, cancellationToken).ConfigureAwait(false); + + // Get results from database and return. + return await _gmailChangeProcessor.GetMailCopiesAsync(messageIdList).ConfigureAwait(false); } - public override async Task DownloadMissingMimeMessageAsync(IMailItem mailItem, - ITransferProgress transferProgress = null, - CancellationToken cancellationToken = default) + /// + /// Downloads multiple messages in batches with metadata only (no MIME) and creates mail packages. + /// Uses Gmail batch API to download up to MaximumAllowedBatchRequestSize messages per request. + /// Used for initial sync where MIME is not needed. + /// + /// List of Gmail message IDs to download + /// Cancellation token + private async Task DownloadMessagesInBatchAsync(IEnumerable messageIds, CancellationToken cancellationToken = default) { - var request = _gmailService.Users.Messages.Get("me", mailItem.Id); - request.Format = UsersResource.MessagesResource.GetRequest.FormatEnum.Raw; + await DownloadMessagesInBatchAsync(messageIds, downloadRawMime: false, cancellationToken).ConfigureAwait(false); + } - var gmailMessage = await request.ExecuteAsync(cancellationToken).ConfigureAwait(false); - var mimeMessage = gmailMessage.GetGmailMimeMessage(); + /// + /// Downloads multiple messages in batches with optional MIME content and creates mail packages. + /// Uses Gmail batch API to download up to MaximumAllowedBatchRequestSize messages per request. + /// + /// List of Gmail message IDs to download + /// True to download Raw format with MIME, false for Metadata only + /// Cancellation token + private async Task DownloadMessagesInBatchAsync(IEnumerable messageIds, bool downloadRawMime, CancellationToken cancellationToken = default) + { + var messageIdList = messageIds.ToList(); + if (messageIdList.Count == 0) return; - if (mimeMessage == null) + // Split into batches based on MaximumAllowedBatchRequestSize + var batches = messageIdList.Batch((int)MaximumAllowedBatchRequestSize); + + foreach (var batch in batches) { - _logger.Warning("Tried to download Gmail Raw Mime with {Id} id and server responded without a data.", mailItem.Id); + var batchRequest = new BatchRequest(_gmailService); + var downloadedMessages = new List(); + var batchTasks = new List(); + + foreach (var messageId in batch) + { + var request = downloadRawMime ? CreateSingleMessageGetRaw(messageId) : CreateSingleMessageGet(messageId); + + batchRequest.Queue(request, (message, error, index, httpMessage) => + { + var task = Task.Run(async () => + { + if (error != null) + { + _logger.Warning("Failed to download message {MessageId}: {Error}", messageId, error.Message); + return; + } + + if (message != null) + { + lock (downloadedMessages) + { + downloadedMessages.Add(message); + } + } + }); + + batchTasks.Add(task); + }); + } + + // Execute the batch request + await batchRequest.ExecuteAsync(cancellationToken).ConfigureAwait(false); + await Task.WhenAll(batchTasks).ConfigureAwait(false); + + // Process all downloaded messages + foreach (var gmailMessage in downloadedMessages) + { + try + { + // Create mail packages from metadata/raw. + // If Gmail response is Raw format, CreateNewMailPackagesAsync will parse MIME and + // include it in package(s) so it can be saved to disk. + var packages = await CreateNewMailPackagesAsync(gmailMessage, null, cancellationToken).ConfigureAwait(false); + + if (packages != null) + { + foreach (var package in packages) + { + await _gmailChangeProcessor.CreateMailAsync(Account.Id, package).ConfigureAwait(false); + } + } + + // Update sync identifier if available + if (gmailMessage.HistoryId.HasValue) + { + await UpdateAccountSyncIdentifierAsync(gmailMessage.HistoryId.Value).ConfigureAwait(false); + } + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to process downloaded message {MessageId}", gmailMessage.Id); + } + } + } + } + + /// + /// Downloads a single message by ID with metadata only (no MIME) and creates mail packages. + /// + /// Gmail message ID to download + /// Cancellation token + private async Task DownloadSingleMessageMetadataAsync(string messageId, CancellationToken cancellationToken = default) + { + var request = CreateSingleMessageGet(messageId); + var gmailMessage = await request.ExecuteAsync(cancellationToken).ConfigureAwait(false); + + if (gmailMessage == null) + { + _logger.Warning("Failed to download message metadata for {MessageId}", messageId); return; } - await _gmailChangeProcessor.SaveMimeFileAsync(mailItem.FileId, mimeMessage, Account.Id).ConfigureAwait(false); + // Create mail packages from metadata + var packages = await CreateNewMailPackagesAsync(gmailMessage, null, cancellationToken).ConfigureAwait(false); + + if (packages != null) + { + foreach (var package in packages) + { + await _gmailChangeProcessor.CreateMailAsync(Account.Id, package).ConfigureAwait(false); + } + } + + // Update sync identifier if available + if (gmailMessage.HistoryId.HasValue) + { + await UpdateAccountSyncIdentifierAsync(gmailMessage.HistoryId.Value).ConfigureAwait(false); + } + } + + public override async Task DownloadMissingMimeMessageAsync(MailCopy mailItem, + ITransferProgress transferProgress = null, + CancellationToken cancellationToken = default) + { + try + { + var request = _gmailService.Users.Messages.Get("me", mailItem.Id); + request.Format = UsersResource.MessagesResource.GetRequest.FormatEnum.Raw; + + var gmailMessage = await request.ExecuteAsync(cancellationToken).ConfigureAwait(false); + var mimeMessage = gmailMessage.GetGmailMimeMessage(); + + if (mimeMessage == null) + { + _logger.Warning("Tried to download Gmail Raw Mime with {Id} id and server responded without a data.", mailItem.Id); + return; + } + + await _gmailChangeProcessor.SaveMimeFileAsync(mailItem.FileId, mimeMessage, Account.Id).ConfigureAwait(false); + } + catch (GoogleApiException ex) when (ex.HttpStatusCode == System.Net.HttpStatusCode.NotFound) + { + _logger.Warning("Gmail message {MailId} not found (404) during MIME download. Deleting locally.", mailItem.Id); + await _gmailChangeProcessor.DeleteMailAsync(Account.Id, mailItem.Id).ConfigureAwait(false); + throw new SynchronizerEntityNotFoundException(ex.Message); + } + } + + public override async Task DownloadCalendarAttachmentAsync( + Wino.Core.Domain.Entities.Calendar.CalendarItem calendarItem, + Wino.Core.Domain.Entities.Calendar.CalendarAttachment attachment, + string localFilePath, + CancellationToken cancellationToken = default) + { + try + { + // Gmail calendar attachments are stored in Google Drive + // RemoteAttachmentId contains either FileId or FileUrl + // For simplicity, we'll try to download from the FileId/FileUrl + + if (string.IsNullOrEmpty(attachment.RemoteAttachmentId)) + { + _logger.Error("RemoteAttachmentId is empty for attachment {AttachmentId}", attachment.Id); + throw new InvalidOperationException("RemoteAttachmentId is required to download Gmail calendar attachment."); + } + + // Gmail calendar attachments are links to Google Drive files + // The attachment.RemoteAttachmentId is either a FileId or FileUrl + // Since we can't directly download from Calendar API, this would require Drive API access + // For now, throw NotSupportedException as Gmail attachments require additional Drive API setup + + _logger.Warning("Gmail calendar attachment download requires Google Drive API access. FileId/URL: {RemoteId}", attachment.RemoteAttachmentId); + throw new NotSupportedException("Gmail calendar attachments are stored in Google Drive and require additional API configuration to download."); + } + catch (Exception ex) + { + _logger.Error(ex, "Error downloading Gmail calendar attachment {AttachmentId}", attachment.Id); + throw; + } } public override List> RenameFolder(RenameFolderRequest request) @@ -1075,6 +1525,38 @@ public class GmailSynchronizer : WinoSynchronizer> MarkFolderAsRead(MarkFolderAsReadRequest request) => MarkRead(new BatchMarkReadRequest(request.MailsToMarkRead.Select(a => new MarkReadRequest(a, true)))); + public override List> DeleteFolder(DeleteFolderRequest request) + { + var networkCall = _gmailService.Users.Labels.Delete("me", request.Folder.RemoteFolderId); + return [new HttpRequestBundle(networkCall, request, request)]; + } + + public override List> CreateSubFolder(CreateSubFolderRequest request) + { + var parentLabelName = request.Folder.FolderName; + + try + { + var parentLabel = _gmailService.Users.Labels.Get("me", request.Folder.RemoteFolderId).Execute(); + if (!string.IsNullOrWhiteSpace(parentLabel?.Name)) + { + parentLabelName = parentLabel.Name; + } + } + catch (Exception ex) + { + _logger.Warning(ex, "Failed to resolve full parent label name for {FolderId}. Falling back to local folder name.", request.Folder.RemoteFolderId); + } + + var label = new Label() + { + Name = $"{parentLabelName}/{request.NewFolderName}" + }; + + var networkCall = _gmailService.Users.Labels.Create(label, "me"); + return [new HttpRequestBundle(networkCall, request, request)]; + } + #endregion #region Request Execution @@ -1082,6 +1564,14 @@ public class GmailSynchronizer : WinoSynchronizer> batchedRequests, CancellationToken cancellationToken = default) { + // First apply all UI changes immediately before any batching. + // This ensures UI reflects changes right away, regardless of batch processing. + foreach (var bundle in batchedRequests) + { + bundle.UIChangeRequest?.ApplyUIChanges(); + } + + // Now batch and execute the network requests. var batchedBundles = batchedRequests.Batch((int)MaximumAllowedBatchRequestSize); var bundleCount = batchedBundles.Count(); @@ -1098,7 +1588,7 @@ public class GmailSynchronizer : WinoSynchronizer(requestBundle.NativeRequest, (content, error, index, message) => bundleTasks.Add(ProcessSingleNativeRequestResponseAsync(requestBundle, error, message, cancellationToken))); @@ -1117,12 +1607,12 @@ public class GmailSynchronizer : WinoSynchronizer { - { "Account", Account }, { "Error", error } } }; @@ -1130,6 +1620,16 @@ public class GmailSynchronizer : WinoSynchronizer - /// Handles after each single message download. - /// This involves adding the Gmail message into Wino database. - /// - /// - /// - /// - /// - private async Task HandleSingleItemDownloadedCallbackAsync(Message message, - RequestError error, - string downloadingMessageId, - CancellationToken cancellationToken = default) - { - try - { - await ProcessGmailRequestErrorAsync(error, null); - } - catch (OutOfMemoryException) - { - _logger.Warning("Gmail SDK got OutOfMemoryException due to bug in the SDK"); - } - catch (SynchronizerEntityNotFoundException) - { - _logger.Warning("Resource not found for {DownloadingMessageId}", downloadingMessageId); - } - catch (SynchronizerException synchronizerException) - { - _logger.Error("Gmail SDK returned error for {DownloadingMessageId}\n{SynchronizerException}", downloadingMessageId, synchronizerException); - } - - if (message == null) - { - _logger.Warning("Skipped GMail message download for {DownloadingMessageId}", downloadingMessageId); - - return null; - } - - // Gmail has LabelId property for each message. - // Therefore we can pass null as the assigned folder safely. - var mailPackage = await CreateNewMailPackagesAsync(message, null, cancellationToken); - - // If CreateNewMailPackagesAsync returns null it means local draft mapping is done. - // We don't need to insert anything else. - if (mailPackage == null) return message; - - foreach (var package in mailPackage) - { - await _gmailChangeProcessor.CreateMailAsync(Account.Id, package).ConfigureAwait(false); - } - - return message; - } + private static bool ShouldRevertOptimisticMailStateChange(IUIChangeRequest request) + => request is BatchMarkReadRequest + || request is MarkReadRequest + || request is BatchChangeFlagRequest + || request is ChangeFlagRequest; private bool ShouldUpdateSyncIdentifier(ulong? historyId) { @@ -1233,30 +1686,51 @@ public class GmailSynchronizer : WinoSynchronizer messageBundle) { - var gmailMessage = await messageBundle.DeserializeBundleAsync(httpResponseMessage, cancellationToken).ConfigureAwait(false); + var gmailMessage = await messageBundle.DeserializeBundleAsync(httpResponseMessage, GmailSynchronizerJsonContext.Default.Message, cancellationToken).ConfigureAwait(false); if (gmailMessage == null) return; - await HandleSingleItemDownloadedCallbackAsync(gmailMessage, error, string.Empty, cancellationToken); + // Create mail packages from the downloaded message + var packages = await CreateNewMailPackagesAsync(gmailMessage, null, cancellationToken).ConfigureAwait(false); + + if (packages != null) + { + foreach (var package in packages) + { + await _gmailChangeProcessor.CreateMailAsync(Account.Id, package).ConfigureAwait(false); + } + } + await UpdateAccountSyncIdentifierAsync(gmailMessage.HistoryId).ConfigureAwait(false); } else if (bundle is HttpRequestBundle folderBundle) { - var gmailLabel = await folderBundle.DeserializeBundleAsync(httpResponseMessage, cancellationToken).ConfigureAwait(false); - - if (gmailLabel == null) return; - // TODO: Handle new Gmail Label added or updated. } + else if (bundle is HttpRequestBundle eventBundle && eventBundle.Request is CreateCalendarEventRequest createCalendarEventRequest) + { + var createdEvent = await eventBundle.DeserializeBundleAsync(httpResponseMessage, GmailSynchronizerJsonContext.Default.Event, cancellationToken).ConfigureAwait(false); + + if (createdEvent == null || string.IsNullOrWhiteSpace(createdEvent.Id)) + return; + + await UploadCalendarEventAttachmentsAsync(createCalendarEventRequest, createdEvent, cancellationToken).ConfigureAwait(false); + } else if (bundle is HttpRequestBundle draftBundle && draftBundle.Request is CreateDraftRequest createDraftRequest) { // New draft mail is created. - var messageDraft = await draftBundle.DeserializeBundleAsync(httpResponseMessage, cancellationToken).ConfigureAwait(false); + var messageDraft = await draftBundle.DeserializeBundleAsync(httpResponseMessage, GmailSynchronizerJsonContext.Default.Draft, cancellationToken).ConfigureAwait(false); if (messageDraft == null) return; @@ -1281,6 +1755,34 @@ public class GmailSynchronizer : WinoSynchronizer bundle) + { + switch (bundle.UIChangeRequest) + { + case BatchMarkReadRequest batchMarkReadRequest: + foreach (var request in batchMarkReadRequest) + { + await _gmailChangeProcessor.ChangeMailReadStatusAsync(request.Item.Id, request.IsRead).ConfigureAwait(false); + } + break; + + case MarkReadRequest markReadRequest: + await _gmailChangeProcessor.ChangeMailReadStatusAsync(markReadRequest.Item.Id, markReadRequest.IsRead).ConfigureAwait(false); + break; + + case BatchChangeFlagRequest batchChangeFlagRequest: + foreach (var request in batchChangeFlagRequest) + { + await _gmailChangeProcessor.ChangeFlagStatusAsync(request.Item.Id, request.IsFlagged).ConfigureAwait(false); + } + break; + + case ChangeFlagRequest changeFlagRequest: + await _gmailChangeProcessor.ChangeFlagStatusAsync(changeFlagRequest.Item.Id, changeFlagRequest.IsFlagged).ConfigureAwait(false); + break; + } + } + /// /// Gmail Archive is a special folder that is not visible in the Gmail web interface. /// We need to handle it separately. @@ -1288,41 +1790,62 @@ public class GmailSynchronizer : WinoSynchronizerCancellation token. private async Task MapArchivedMailsAsync(CancellationToken cancellationToken) { + if (!archiveFolderId.HasValue) return; + var request = _gmailService.Users.Messages.List("me"); request.Q = "in:archive"; - request.MaxResults = InitialMessageDownloadCountPerFolder; + request.MaxResults = 500; string pageToken = null; - var archivedMessageIds = new List(); + var archivedMessageIds = new HashSet(StringComparer.Ordinal); do { if (!string.IsNullOrEmpty(pageToken)) request.PageToken = pageToken; - var response = await request.ExecuteAsync(cancellationToken); + var response = await request.ExecuteAsync(cancellationToken).ConfigureAwait(false); if (response.Messages == null) break; foreach (var message in response.Messages) { - if (archivedMessageIds.Contains(message.Id)) continue; - - archivedMessageIds.Add(message.Id); + if (!string.IsNullOrEmpty(message.Id)) + { + archivedMessageIds.Add(message.Id); + } } pageToken = response.NextPageToken; } while (!string.IsNullOrEmpty(pageToken)); - var result = await _gmailChangeProcessor.GetGmailArchiveComparisonResultAsync(archiveFolderId.Value, archivedMessageIds).ConfigureAwait(false); + var result = await _gmailChangeProcessor.GetGmailArchiveComparisonResultAsync(archiveFolderId.Value, archivedMessageIds.ToList()).ConfigureAwait(false); - foreach (var archiveAddedItem in result.Added) + var addedArchiveIds = result.Added.Distinct(StringComparer.Ordinal).ToList(); + var removedArchiveIds = result.Removed.Distinct(StringComparer.Ordinal).ToList(); + + if (addedArchiveIds.Count > 0) { - await HandleArchiveAssignmentAsync(archiveAddedItem); + // Archive sync can surface messages that were never downloaded before. + // Download metadata first so assignment creation can succeed. + var existingBeforeDownload = await _gmailChangeProcessor.AreMailsExistsAsync(addedArchiveIds).ConfigureAwait(false); + var missingArchiveIds = addedArchiveIds.Except(existingBeforeDownload, StringComparer.Ordinal).ToList(); + + if (missingArchiveIds.Count > 0) + { + await DownloadMessagesInBatchAsync(missingArchiveIds, cancellationToken).ConfigureAwait(false); + } + + var existingAfterDownload = await _gmailChangeProcessor.AreMailsExistsAsync(addedArchiveIds).ConfigureAwait(false); + + foreach (var archiveAddedItem in existingAfterDownload) + { + await HandleArchiveAssignmentAsync(archiveAddedItem).ConfigureAwait(false); + } } - foreach (var unAarchivedRemovedItem in result.Removed) + foreach (var unAarchivedRemovedItem in removedArchiveIds) { - await HandleUnarchiveAssignmentAsync(unAarchivedRemovedItem); + await HandleUnarchiveAssignmentAsync(unAarchivedRemovedItem).ConfigureAwait(false); } } @@ -1355,11 +1878,270 @@ public class GmailSynchronizer : WinoSynchronizer CreateMinimalMailCopyAsync(Message gmailMessage, MailItemFolder assignedFolder, CancellationToken cancellationToken = default) + { + bool isUnread = gmailMessage.GetIsUnread(); + bool isFocused = gmailMessage.GetIsFocused(); + bool isFlagged = gmailMessage.GetIsFlagged(); + bool isDraft = gmailMessage.GetIsDraft(); + + // Try to get the most accurate date from Gmail's InternalDate first, then fallback to Date header + DateTime creationDate = DateTime.UtcNow; + + if (gmailMessage.InternalDate.HasValue) + { + // Gmail's InternalDate is in milliseconds since Unix epoch + creationDate = DateTimeOffset.FromUnixTimeMilliseconds(gmailMessage.InternalDate.Value).UtcDateTime; + } + else + { + // Fallback to parsing the Date header + var dateHeaderValue = gmailMessage.Payload?.Headers?.FirstOrDefault(h => h.Name.Equals("Date", StringComparison.OrdinalIgnoreCase))?.Value; + if (!string.IsNullOrEmpty(dateHeaderValue) && DateTime.TryParse(dateHeaderValue, out var parsedDate)) + { + creationDate = parsedDate.ToUniversalTime(); + } + } + + // Extract From header and parse name/address + var fromHeaderValue = gmailMessage.Payload?.Headers?.FirstOrDefault(h => h.Name.Equals("From", StringComparison.OrdinalIgnoreCase))?.Value ?? ""; + var (fromName, fromAddress) = ExtractNameAndEmailFromHeader(fromHeaderValue); + + // Detect calendar invitation by checking Content-Type header (only if calendar access granted) + var itemType = Account.IsCalendarAccessGranted ? GetMailItemTypeFromHeaders(gmailMessage.Payload?.Headers) : MailItemType.Mail; + + var copy = new MailCopy() + { + CreationDate = creationDate, + Subject = HttpUtility.HtmlDecode(gmailMessage.Payload?.Headers?.FirstOrDefault(h => h.Name.Equals("Subject", StringComparison.OrdinalIgnoreCase))?.Value ?? ""), + FromName = HttpUtility.HtmlDecode(fromName), + FromAddress = fromAddress, + PreviewText = HttpUtility.HtmlDecode(gmailMessage.Snippet ?? "").Trim(), + ThreadId = gmailMessage.ThreadId, + Importance = MailImportance.Normal, // Default importance without MIME parsing + Id = gmailMessage.Id, + IsDraft = isDraft, + HasAttachments = gmailMessage.Payload?.Parts?.Any(p => !string.IsNullOrEmpty(p.Filename)) ?? false, + IsRead = !isUnread, + IsFlagged = isFlagged, + IsFocused = isFocused, + InReplyTo = MailHeaderExtensions.StripAngleBrackets(gmailMessage.Payload?.Headers?.FirstOrDefault(h => h.Name.Equals("In-Reply-To", StringComparison.OrdinalIgnoreCase))?.Value), + MessageId = MailHeaderExtensions.StripAngleBrackets(gmailMessage.Payload?.Headers?.FirstOrDefault(h => h.Name.Equals("Message-Id", StringComparison.OrdinalIgnoreCase))?.Value), + References = MailHeaderExtensions.NormalizeReferences(gmailMessage.Payload?.Headers?.FirstOrDefault(h => h.Name.Equals("References", StringComparison.OrdinalIgnoreCase))?.Value), + FileId = Guid.NewGuid(), + ItemType = itemType + }; + + // Note: DraftId is NOT set here. Gmail's Draft resource ID is separate from ThreadId + // and can only be obtained from the Drafts API (not Messages API). + // DraftId is populated by: + // - MapLocalDraftAsync (for Wino-created drafts, from CreateDraft response) + // - MapDraftIdsAsync (for all drafts, from Drafts.List API) + + return Task.FromResult(copy); + } + + /// + /// Enriches a MailCopy with fields extracted from a parsed MimeMessage. + /// This is needed when messages are downloaded with Raw format (delta sync), + /// because the Gmail API does not populate Payload.Headers in Raw format. + /// Fields already populated (non-null/non-empty) are NOT overwritten. + /// + private static void EnrichMailCopyFromMime(MailCopy copy, MimeMessage mime) + { + if (copy == null || mime == null) return; + + if (string.IsNullOrEmpty(copy.Subject)) + copy.Subject = mime.Subject ?? string.Empty; + + if (string.IsNullOrEmpty(copy.FromName)) + { + var from = mime.From.Mailboxes.FirstOrDefault(); + if (from != null) + copy.FromName = from.Name ?? string.Empty; + } + + if (string.IsNullOrEmpty(copy.FromAddress)) + { + var from = mime.From.Mailboxes.FirstOrDefault(); + if (from != null) + copy.FromAddress = from.Address ?? string.Empty; + } + + if (string.IsNullOrEmpty(copy.MessageId)) + copy.MessageId = mime.MessageId; + + if (string.IsNullOrEmpty(copy.InReplyTo)) + copy.InReplyTo = mime.InReplyTo; + + if (string.IsNullOrEmpty(copy.References) && mime.References?.Count > 0) + copy.References = string.Join(";", mime.References); + + if (!copy.HasAttachments && mime.Attachments.Any()) + copy.HasAttachments = true; + + if (copy.Importance == MailImportance.Normal) + { + copy.Importance = mime.Importance switch + { + MessageImportance.High => MailImportance.High, + MessageImportance.Low => MailImportance.Low, + _ => MailImportance.Normal + }; + } + } + + /// + /// Determines MailItemType based on Gmail message headers. + /// Gmail doesn't have EventMessage type like Outlook, but calendar invitations can be detected + /// by checking Content-Type header for text/calendar or multipart/alternative with text/calendar part. + /// + private static MailItemType GetMailItemTypeFromHeaders(IList headers) + { + if (headers == null) return MailItemType.Mail; + + // Check Content-Type header for text/calendar + var contentTypeHeader = headers.FirstOrDefault(h => h.Name.Equals("Content-Type", StringComparison.OrdinalIgnoreCase))?.Value; + + if (!string.IsNullOrEmpty(contentTypeHeader)) + { + // Check if it's a calendar message (text/calendar or multipart with calendar) + if (contentTypeHeader.Contains("text/calendar", StringComparison.OrdinalIgnoreCase)) + { + // Check the METHOD parameter to determine invitation type + var methodMatch = System.Text.RegularExpressions.Regex.Match(contentTypeHeader, @"method=([^;\s]+)", System.Text.RegularExpressions.RegexOptions.IgnoreCase); + + if (methodMatch.Success) + { + var method = methodMatch.Groups[1].Value.Trim('"').ToUpperInvariant(); + + return method switch + { + "REQUEST" => MailItemType.CalendarInvitation, + "CANCEL" => MailItemType.CalendarCancellation, + "REPLY" => MailItemType.CalendarResponse, + _ => MailItemType.Mail + }; + } + + // If no method specified, assume it's an invitation + return MailItemType.CalendarInvitation; + } + } + + return MailItemType.Mail; + } + + /// + /// Extracts name and email address from a header value like "Name " or "email@domain.com" + /// + private static (string name, string email) ExtractNameAndEmailFromHeader(string headerValue) + { + if (string.IsNullOrEmpty(headerValue)) + return ("", ""); + + // Try to match "Name " format + var match = System.Text.RegularExpressions.Regex.Match(headerValue, @"^(.+?)\s*<(.+?)>$"); + if (match.Success) + { + var name = match.Groups[1].Value.Trim().Trim('"'); + var email = match.Groups[2].Value.Trim(); + return (name, email); + } + + // If no angle brackets, assume the whole value is the email with no name + var emailOnly = headerValue.Trim(); + return ("", emailOnly); + } + + private static IReadOnlyList ExtractContactsFromGmailMessage(Message message, MimeMessage mimeMessage) + { + var contacts = new Dictionary(StringComparer.OrdinalIgnoreCase); + + AddFromHeaders(message?.Payload?.Headers); + + if (mimeMessage != null) + { + AddFromInternetAddressList(mimeMessage.From); + AddFromInternetAddressList(mimeMessage.To); + AddFromInternetAddressList(mimeMessage.Cc); + AddFromInternetAddressList(mimeMessage.Bcc); + AddFromInternetAddressList(mimeMessage.ReplyTo); + + if (mimeMessage.Sender is MailboxAddress senderMailbox) + { + AddContact(senderMailbox.Address, senderMailbox.Name); + } + } + + return contacts.Values.ToList(); + + void AddFromHeaders(IList headers) + { + if (headers == null || headers.Count == 0) return; + + AddFromHeader("From"); + AddFromHeader("Sender"); + AddFromHeader("To"); + AddFromHeader("Cc"); + AddFromHeader("Bcc"); + AddFromHeader("Reply-To"); + + void AddFromHeader(string headerName) + { + var headerValue = headers + .FirstOrDefault(h => h.Name.Equals(headerName, StringComparison.OrdinalIgnoreCase)) + ?.Value; + + if (string.IsNullOrWhiteSpace(headerValue)) return; + + try + { + var addresses = InternetAddressList.Parse(headerValue); + foreach (var mailbox in addresses.Mailboxes) + { + AddContact(mailbox.Address, mailbox.Name); + } + } + catch + { + var (name, email) = ExtractNameAndEmailFromHeader(headerValue); + AddContact(email, name); + } + } + } + + void AddFromInternetAddressList(InternetAddressList addresses) + { + if (addresses == null) return; + + foreach (var mailbox in addresses.Mailboxes) + { + AddContact(mailbox.Address, mailbox.Name); + } + } + + void AddContact(string address, string name) + { + var trimmedAddress = address?.Trim(); + if (string.IsNullOrWhiteSpace(trimmedAddress)) return; + + var displayName = string.IsNullOrWhiteSpace(name) ? trimmedAddress : name.Trim(); + + contacts[trimmedAddress] = new AccountContact + { + Address = trimmedAddress, + Name = displayName + }; + } + } + /// /// Creates new mail packages for the given message. /// AssignedFolder is null since the LabelId is parsed out of the Message. + /// If Gmail Message includes Raw payload, MIME is parsed and attached to packages. /// - /// Gmail message to create package for. + /// Gmail message to create package for (must have Metadata format). /// Null, not used. /// Cancellation token /// New mail package that change processor can use to insert new mail into database. @@ -1368,40 +2150,508 @@ public class GmailSynchronizer : WinoSynchronizer(); + MimeMessage mimeMessage = null; - MimeMessage mimeMessage = message.GetGmailMimeMessage(); - var mailCopy = message.AsMailCopy(mimeMessage); - - // Check whether this message is mapped to any local draft. - // Previously we were using Draft resource response as mapping drafts. - // This seem to be a worse approach. Now both Outlook and Gmail use X-Wino-Draft-Id header to map drafts. - // This is a better approach since we don't need to fetch the draft resource to get the draft id. - - if (mailCopy.IsDraft - && mimeMessage.Headers.Contains(Domain.Constants.WinoLocalDraftHeader) - && Guid.TryParse(mimeMessage.Headers[Domain.Constants.WinoLocalDraftHeader], out Guid localDraftCopyUniqueId)) + // Raw format is used in delta sync and does not populate Payload.Headers. + // Parse MIME from Raw so we can resolve draft mapping header and persist mime content. + if (!string.IsNullOrEmpty(message?.Raw)) { - // This message belongs to existing local draft copy. - // We don't need to create a new mail copy for this message, just update the existing one. - - bool isMappingSuccesfull = await _gmailChangeProcessor.MapLocalDraftAsync(Account.Id, localDraftCopyUniqueId, mailCopy.Id, mailCopy.DraftId, mailCopy.ThreadId); - - if (isMappingSuccesfull) return null; - - // Local copy doesn't exists. Continue execution to insert mail copy. + try + { + mimeMessage = message.GetGmailMimeMessage(); + } + catch (Exception ex) + { + _logger.Warning(ex, "Failed to parse MIME from raw Gmail message {MessageId}", message?.Id); + } } + // Create base MailCopy from metadata only - NO MIME download + var baseMailCopy = await CreateMinimalMailCopyAsync(message, assignedFolder, cancellationToken); + + // Initial sync metadata flow does not include MIME, but calendar invitations need MIME + // for date rendering and invitation-to-calendar mapping. + if (mimeMessage == null && baseMailCopy?.ItemType == MailItemType.CalendarInvitation && !string.IsNullOrEmpty(message?.Id)) + { + try + { + var rawRequest = _gmailService.Users.Messages.Get("me", message.Id); + rawRequest.Format = UsersResource.MessagesResource.GetRequest.FormatEnum.Raw; + + var rawMessage = await rawRequest.ExecuteAsync(cancellationToken).ConfigureAwait(false); + if (!string.IsNullOrEmpty(rawMessage?.Raw)) + { + mimeMessage = rawMessage.GetGmailMimeMessage(); + } + } + catch (Exception ex) + { + _logger.Warning(ex, "Failed to fetch raw MIME for calendar invitation {MessageId}", message.Id); + } + } + + if (mimeMessage != null) + { + // Raw responses don't include metadata headers. Backfill important fields from MIME. + EnrichMailCopyFromMime(baseMailCopy, mimeMessage); + } + + await TryMapCalendarInvitationAsync(baseMailCopy, mimeMessage, cancellationToken).ConfigureAwait(false); + + var extractedContacts = ExtractContactsFromGmailMessage(message, mimeMessage); + + // Check for local draft mapping using X-Wino-Draft-Id header. + // For Metadata format we read from Payload.Headers. + // For Raw format (Payload is null), we read from parsed MIME headers. + if (baseMailCopy.IsDraft) + { + var draftIdHeader = message.Payload?.Headers?.FirstOrDefault(h => h.Name.Equals(Domain.Constants.WinoLocalDraftHeader, StringComparison.OrdinalIgnoreCase))?.Value + ?? mimeMessage?.Headers?.FirstOrDefault(h => h.Field.Equals(Domain.Constants.WinoLocalDraftHeader, StringComparison.OrdinalIgnoreCase))?.Value; + + if (!string.IsNullOrEmpty(draftIdHeader) && Guid.TryParse(draftIdHeader, out _)) + { + if (Guid.TryParse(draftIdHeader, out Guid localDraftCopyUniqueId)) + { + // This message belongs to existing local draft copy. + // Map remote ids to local copy and skip creating duplicate rows. + bool isMappingSuccessful = await _gmailChangeProcessor.MapLocalDraftAsync( + Account.Id, + localDraftCopyUniqueId, + baseMailCopy.Id, + baseMailCopy.DraftId, + baseMailCopy.ThreadId).ConfigureAwait(false); + + if (isMappingSuccessful) + { + // Keep local draft MIME in sync with the fetched remote raw MIME if available. + if (mimeMessage != null) + { + var mappedDraftCopies = await _gmailChangeProcessor.GetMailCopiesAsync([baseMailCopy.Id]).ConfigureAwait(false); + if (mappedDraftCopies != null) + { + var savedFileIds = new HashSet(); + foreach (var mappedCopy in mappedDraftCopies) + { + if (mappedCopy.FileId == Guid.Empty || !savedFileIds.Add(mappedCopy.FileId)) + continue; + + await _gmailChangeProcessor.SaveMimeFileAsync(mappedCopy.FileId, mimeMessage, Account.Id).ConfigureAwait(false); + } + } + } + + return null; + } + } + } + } + + // For Gmail, a single mail can have multiple labels (folders). + // Each label requires a separate MailCopy entry in the database with: + // - Same Id, UniqueId, FileId (shared across all copies) + // - Different FolderId (one per label) + // ARCHIVE label is excluded here as it's virtual and handled by MapArchivedMailsAsync if (message.LabelIds is not null) { + // Generate shared identifiers that will be the same for all copies of this mail + var sharedId = baseMailCopy.Id; + var sharedFileId = baseMailCopy.FileId; + foreach (var labelId in message.LabelIds) { - packageList.Add(new NewMailItemPackage(mailCopy, mimeMessage, labelId)); + // Skip ARCHIVE label - it's virtual and handled separately + if (labelId == ServiceConstants.ARCHIVE_LABEL_ID) + continue; + + // Create a new MailCopy instance for each label to avoid shared reference issues + var mailCopyForLabel = await CreateMinimalMailCopyAsync(message, assignedFolder, cancellationToken); + + if (mimeMessage != null) + { + EnrichMailCopyFromMime(mailCopyForLabel, mimeMessage); + } + + // Ensure all copies share the same Id and FileId + mailCopyForLabel.Id = sharedId; + mailCopyForLabel.FileId = sharedFileId; + + packageList.Add(new NewMailItemPackage(mailCopyForLabel, mimeMessage, labelId, extractedContacts)); } } return packageList; } + private async Task TryMapCalendarInvitationAsync(MailCopy baseMailCopy, MimeMessage mimeMessage, CancellationToken cancellationToken) + { + if (baseMailCopy == null || baseMailCopy.ItemType != MailItemType.CalendarInvitation || mimeMessage == null) + return; + + var invitationUid = mimeMessage.ExtractInvitationUid(); + if (string.IsNullOrWhiteSpace(invitationUid)) + return; + + var calendars = await _gmailChangeProcessor.GetAccountCalendarsAsync(Account.Id).ConfigureAwait(false); + if (calendars == null || calendars.Count == 0) + return; + + foreach (var calendar in calendars) + { + try + { + var listRequest = _calendarService.Events.List(calendar.RemoteCalendarId); + listRequest.ICalUID = invitationUid; + listRequest.MaxResults = 1; + listRequest.SingleEvents = false; + + var listResponse = await listRequest.ExecuteAsync(cancellationToken).ConfigureAwait(false); + var matchedEvent = listResponse?.Items?.FirstOrDefault(); + if (matchedEvent == null || string.IsNullOrWhiteSpace(matchedEvent.Id)) + continue; + + await _gmailChangeProcessor.ManageCalendarEventAsync(matchedEvent, calendar, Account).ConfigureAwait(false); + + var localCalendarItem = await _gmailChangeProcessor.GetCalendarItemAsync(calendar.Id, matchedEvent.Id).ConfigureAwait(false); + if (localCalendarItem == null) + return; + + await _gmailChangeProcessor.UpsertMailInvitationCalendarMappingAsync(new MailInvitationCalendarMapping() + { + Id = Guid.NewGuid(), + AccountId = Account.Id, + MailCopyId = baseMailCopy.Id, + InvitationUid = invitationUid, + CalendarId = calendar.Id, + CalendarItemId = localCalendarItem.Id, + CalendarRemoteEventId = matchedEvent.Id + }).ConfigureAwait(false); + + return; + } + catch (Exception ex) + { + _logger.Warning(ex, "Failed to map Gmail calendar invitation mail {MailCopyId} for calendar {CalendarId}", baseMailCopy.Id, calendar.Id); + } + } + } + + #endregion + + #region Calendar Operations + + public override List> CreateCalendarEvent(CreateCalendarEventRequest request) + { + var calendarItem = request.PreparedItem; + var attendees = request.PreparedEvent.Attendees; + var reminders = request.PreparedEvent.Reminders; + var calendar = request.AssignedCalendar; + + var googleEvent = new Event + { + Id = calendarItem.Id.ToString("N").ToLowerInvariant(), + Summary = calendarItem.Title, + Description = calendarItem.Description, + Location = calendarItem.Location, + Status = calendarItem.Status == CalendarItemStatus.Accepted ? "confirmed" : "tentative", + Transparency = calendarItem.ShowAs == CalendarItemShowAs.Free ? "transparent" : "opaque" + }; + + if (calendarItem.IsAllDayEvent) + { + googleEvent.Start = new EventDateTime + { + Date = calendarItem.StartDate.ToString("yyyy-MM-dd"), + TimeZone = calendarItem.StartTimeZone + }; + googleEvent.End = new EventDateTime + { + Date = calendarItem.EndDate.ToString("yyyy-MM-dd"), + TimeZone = calendarItem.EndTimeZone + }; + } + else + { + googleEvent.Start = new EventDateTime + { + DateTimeDateTimeOffset = new DateTimeOffset(calendarItem.StartDate, ResolveOffset(calendarItem.StartDate, calendarItem.StartTimeZone)), + TimeZone = calendarItem.StartTimeZone + }; + googleEvent.End = new EventDateTime + { + DateTimeDateTimeOffset = new DateTimeOffset(calendarItem.EndDate, ResolveOffset(calendarItem.EndDate, calendarItem.EndTimeZone ?? calendarItem.StartTimeZone)), + TimeZone = calendarItem.EndTimeZone + }; + } + + if (attendees.Count > 0) + { + googleEvent.Attendees = attendees.Select(a => new EventAttendee + { + Email = a.Email, + DisplayName = a.Name, + Optional = a.IsOptionalAttendee + }).ToList(); + } + + if (reminders.Count > 0) + { + googleEvent.Reminders = new Event.RemindersData + { + UseDefault = false, + Overrides = reminders.Select(reminder => new EventReminder + { + Method = reminder.ReminderType == CalendarItemReminderType.Email ? "email" : "popup", + Minutes = (int)Math.Max(0, reminder.DurationInSeconds / 60) + }).ToList() + }; + } + + if (!string.IsNullOrWhiteSpace(calendarItem.Recurrence)) + { + googleEvent.Recurrence = calendarItem.Recurrence + .Split(Wino.Core.Domain.Constants.CalendarEventRecurrenceRuleSeperator, StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Trim()) + .Where(line => !string.IsNullOrWhiteSpace(line)) + .ToList(); + } + + var insertRequest = _calendarService.Events.Insert(googleEvent, calendar.RemoteCalendarId); + insertRequest.SendUpdates = attendees.Count > 0 + ? Google.Apis.Calendar.v3.EventsResource.InsertRequest.SendUpdatesEnum.All + : Google.Apis.Calendar.v3.EventsResource.InsertRequest.SendUpdatesEnum.None; + + return [new HttpRequestBundle(insertRequest, request)]; + } + + public override List> AcceptEvent(AcceptEventRequest request) + { + var calendarItem = request.Item; + var calendar = calendarItem.AssignedCalendar; + + if (calendar == null) + { + throw new InvalidOperationException("Calendar item must have an assigned calendar"); + } + + var remoteEventId = calendarItem.RemoteEventId.GetProviderRemoteEventId(); + if (string.IsNullOrEmpty(remoteEventId)) + { + throw new InvalidOperationException("Cannot accept event without remote event ID"); + } + + // For Gmail, we need to patch the event with the user's response status + // Get the current user's email from the account + var userEmail = Account.Address; + + // Create a patch event to update only the attendee response + var patchEvent = new Event(); + + // We need to get the event first to update the specific attendee + // However, for efficiency, we'll use the patch method with sendUpdates parameter + var patchRequest = _calendarService.Events.Patch(new Event + { + // The API will handle updating the current user's attendee status + Attendees = new List + { + new EventAttendee + { + Email = userEmail, + ResponseStatus = "accepted" + } + } + }, calendar.RemoteCalendarId, remoteEventId); + + // Send updates to other attendees if there's a message + patchRequest.SendUpdates = !string.IsNullOrEmpty(request.ResponseMessage) + ? Google.Apis.Calendar.v3.EventsResource.PatchRequest.SendUpdatesEnum.All + : Google.Apis.Calendar.v3.EventsResource.PatchRequest.SendUpdatesEnum.None; + + return [new HttpRequestBundle(patchRequest, request)]; + } + + public override List> DeclineEvent(DeclineEventRequest request) + { + var calendarItem = request.Item; + var calendar = calendarItem.AssignedCalendar; + + if (calendar == null) + { + throw new InvalidOperationException("Calendar item must have an assigned calendar"); + } + + var remoteEventId = calendarItem.RemoteEventId.GetProviderRemoteEventId(); + if (string.IsNullOrEmpty(remoteEventId)) + { + throw new InvalidOperationException("Cannot decline event without remote event ID"); + } + + var userEmail = Account.Address; + + var patchRequest = _calendarService.Events.Patch(new Event + { + Attendees = new List + { + new EventAttendee + { + Email = userEmail, + ResponseStatus = "declined", + Comment = request.ResponseMessage + } + } + }, calendar.RemoteCalendarId, remoteEventId); + + patchRequest.SendUpdates = !string.IsNullOrEmpty(request.ResponseMessage) + ? Google.Apis.Calendar.v3.EventsResource.PatchRequest.SendUpdatesEnum.All + : Google.Apis.Calendar.v3.EventsResource.PatchRequest.SendUpdatesEnum.None; + + return [new HttpRequestBundle(patchRequest, request)]; + } + + public override List> TentativeEvent(TentativeEventRequest request) + { + var calendarItem = request.Item; + var calendar = calendarItem.AssignedCalendar; + + if (calendar == null) + { + throw new InvalidOperationException("Calendar item must have an assigned calendar"); + } + + var remoteEventId = calendarItem.RemoteEventId.GetProviderRemoteEventId(); + if (string.IsNullOrEmpty(remoteEventId)) + { + throw new InvalidOperationException("Cannot tentatively accept event without remote event ID"); + } + + var userEmail = Account.Address; + + var patchRequest = _calendarService.Events.Patch(new Event + { + Attendees = new List + { + new EventAttendee + { + Email = userEmail, + ResponseStatus = "tentative", + Comment = request.ResponseMessage + } + } + }, calendar.RemoteCalendarId, remoteEventId); + + patchRequest.SendUpdates = !string.IsNullOrEmpty(request.ResponseMessage) + ? Google.Apis.Calendar.v3.EventsResource.PatchRequest.SendUpdatesEnum.All + : Google.Apis.Calendar.v3.EventsResource.PatchRequest.SendUpdatesEnum.None; + + return [new HttpRequestBundle(patchRequest, request)]; + } + + public override List> UpdateCalendarEvent(UpdateCalendarEventRequest request) + { + var calendarItem = request.Item; + var attendees = request.Attendees; + + // Get the calendar for this event + var calendar = calendarItem.AssignedCalendar; + if (calendar == null) + { + throw new InvalidOperationException("Calendar item must have an assigned calendar"); + } + + var remoteEventId = calendarItem.RemoteEventId.GetProviderRemoteEventId(); + if (string.IsNullOrEmpty(remoteEventId)) + { + throw new InvalidOperationException("Cannot update event without remote event ID"); + } + + // Convert CalendarItem to Google Event for update + var googleEvent = new Event + { + Summary = calendarItem.Title, + Description = calendarItem.Description, + Location = calendarItem.Location, + Status = calendarItem.Status == CalendarItemStatus.Accepted ? "confirmed" : "tentative", + Transparency = calendarItem.ShowAs == CalendarItemShowAs.Free ? "transparent" : "opaque" + }; + + // Set start and end time with proper timezone handling + // CalendarItem stores dates in the event's timezone (StartTimeZone/EndTimeZone) + // When user edits in local timezone, the dates are already converted and stored correctly + if (calendarItem.IsAllDayEvent) + { + // All-day events use Date instead of DateTime + googleEvent.Start = new EventDateTime + { + Date = calendarItem.StartDate.ToString("yyyy-MM-dd") + }; + googleEvent.End = new EventDateTime + { + Date = calendarItem.EndDate.ToString("yyyy-MM-dd") + }; + } + else + { + // Regular events with time + // StartDate and EndDate are stored in the event's timezone + // We preserve the timezone information during update + googleEvent.Start = new EventDateTime + { + DateTimeDateTimeOffset = new DateTimeOffset(calendarItem.StartDate, TimeSpan.Zero), + TimeZone = calendarItem.StartTimeZone ?? TimeZoneInfo.Local.Id + }; + googleEvent.End = new EventDateTime + { + DateTimeDateTimeOffset = new DateTimeOffset(calendarItem.EndDate, TimeSpan.Zero), + TimeZone = calendarItem.EndTimeZone ?? TimeZoneInfo.Local.Id + }; + } + + // Add attendees if any + if (attendees != null && attendees.Count > 0) + { + googleEvent.Attendees = attendees.Select(a => new EventAttendee + { + Email = a.Email, + DisplayName = a.Name, + Optional = a.IsOptionalAttendee + }).ToList(); + } + + // Update the event using Google Calendar API + var updateRequest = _calendarService.Events.Update(googleEvent, calendar.RemoteCalendarId, remoteEventId); + + // Send notifications to attendees if the event has attendees + updateRequest.SendUpdates = (attendees != null && attendees.Count > 0) + ? Google.Apis.Calendar.v3.EventsResource.UpdateRequest.SendUpdatesEnum.All + : Google.Apis.Calendar.v3.EventsResource.UpdateRequest.SendUpdatesEnum.None; + + return [new HttpRequestBundle(updateRequest, request)]; + } + + public override List> DeleteCalendarEvent(DeleteCalendarEventRequest request) + { + var calendarItem = request.Item; + + // Get the calendar for this event + var calendar = calendarItem.AssignedCalendar; + if (calendar == null) + { + throw new InvalidOperationException("Calendar item must have an assigned calendar"); + } + + var remoteEventId = calendarItem.RemoteEventId.GetProviderRemoteEventId(); + if (string.IsNullOrEmpty(remoteEventId)) + { + throw new InvalidOperationException("Cannot delete event without remote event ID"); + } + + var deleteRequest = _calendarService.Events.Delete(calendar.RemoteCalendarId, remoteEventId); + + // Send cancellation notifications to attendees + deleteRequest.SendUpdates = Google.Apis.Calendar.v3.EventsResource.DeleteRequest.SendUpdatesEnum.All; + + return [new HttpRequestBundle(deleteRequest, request)]; + } + #endregion public override async Task KillSynchronizerAsync() @@ -1411,6 +2661,96 @@ public class GmailSynchronizer : WinoSynchronizer 25) + throw new InvalidOperationException("Google Calendar supports at most 25 attachments per event."); + + var eventAttachments = createdEvent.Attachments? + .Where(attachment => attachment != null && !string.IsNullOrWhiteSpace(attachment.FileUrl)) + .ToList() ?? []; + + foreach (var attachment in composeAttachments.Where(a => !string.IsNullOrWhiteSpace(a.FilePath) && File.Exists(a.FilePath))) + { + cancellationToken.ThrowIfCancellationRequested(); + eventAttachments.Add(await UploadAttachmentToDriveAsync(attachment, cancellationToken).ConfigureAwait(false)); + } + + if (eventAttachments.Count == 0) + return; + + var patchRequest = _calendarService.Events.Patch(new Event + { + Attachments = eventAttachments + }, request.AssignedCalendar.RemoteCalendarId, createdEvent.Id); + + patchRequest.SupportsAttachments = true; + patchRequest.SendUpdates = Google.Apis.Calendar.v3.EventsResource.PatchRequest.SendUpdatesEnum.None; + + await patchRequest.ExecuteAsync(cancellationToken).ConfigureAwait(false); + } + + private async Task UploadAttachmentToDriveAsync( + Wino.Core.Domain.Models.Calendar.CalendarEventComposeAttachmentDraft attachment, + CancellationToken cancellationToken) + { + var fileName = string.IsNullOrWhiteSpace(attachment.FileName) + ? Path.GetFileName(attachment.FilePath) + : attachment.FileName; + var contentType = MimeTypes.GetMimeType(fileName); + + await using var fileStream = File.OpenRead(attachment.FilePath); + + var uploadRequest = _driveService.Files.Create(new DriveFile + { + Name = fileName, + MimeType = contentType + }, fileStream, contentType); + uploadRequest.Fields = "id,name,mimeType,webViewLink"; + + var uploadProgress = await uploadRequest.UploadAsync(cancellationToken).ConfigureAwait(false); + + if (uploadProgress.Status != UploadStatus.Completed) + { + throw new InvalidOperationException( + $"Failed to upload '{fileName}' to Google Drive. Upload status: {uploadProgress.Status}."); + } + + var uploadedFile = uploadRequest.ResponseBody; + if (uploadedFile == null || string.IsNullOrWhiteSpace(uploadedFile.Id) || string.IsNullOrWhiteSpace(uploadedFile.WebViewLink)) + { + throw new InvalidOperationException($"Google Drive did not return a valid attachment link for '{fileName}'."); + } + + return new EventAttachment + { + FileId = uploadedFile.Id, + FileUrl = uploadedFile.WebViewLink, + MimeType = uploadedFile.MimeType ?? contentType, + Title = uploadedFile.Name ?? fileName + }; + } + + private static TimeSpan ResolveOffset(DateTime dateTime, string timeZoneId) + { + if (string.IsNullOrWhiteSpace(timeZoneId)) + return TimeSpan.Zero; + + try + { + return TimeZoneInfo.FindSystemTimeZoneById(timeZoneId).GetUtcOffset(dateTime); + } + catch + { + return TimeSpan.Zero; + } + } } diff --git a/Wino.Core/Synchronizers/ImapSync/CondstoreSynchronizer.cs b/Wino.Core/Synchronizers/ImapSync/CondstoreSynchronizer.cs deleted file mode 100644 index 5bc89f4f..00000000 --- a/Wino.Core/Synchronizers/ImapSync/CondstoreSynchronizer.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MailKit; -using MailKit.Net.Imap; -using MailKit.Search; -using Wino.Core.Domain.Entities.Mail; -using Wino.Core.Domain.Exceptions; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Integration; -using IMailService = Wino.Core.Domain.Interfaces.IMailService; - -namespace Wino.Core.Synchronizers.ImapSync; - -/// -/// RFC 4551 CONDSTORE IMAP Synchronization strategy. -/// -internal class CondstoreSynchronizer : ImapSynchronizationStrategyBase -{ - public CondstoreSynchronizer(IFolderService folderService, IMailService mailService) : base(folderService, mailService) - { - } - - public async override Task> HandleSynchronizationAsync(IImapClient client, - MailItemFolder folder, - IImapSynchronizer synchronizer, - CancellationToken cancellationToken = default) - { - if (client is not WinoImapClient winoClient) - throw new ArgumentException("Client must be of type WinoImapClient.", nameof(client)); - - if (!client.Capabilities.HasFlag(ImapCapabilities.CondStore)) - throw new ImapSynchronizerStrategyException("Server does not support CONDSTORE."); - - IMailFolder remoteFolder = null; - - var downloadedMessageIds = new List(); - - Folder = folder; - - try - { - remoteFolder = await winoClient.GetFolderAsync(folder.RemoteFolderId, cancellationToken).ConfigureAwait(false); - - await remoteFolder.OpenAsync(FolderAccess.ReadOnly, cancellationToken).ConfigureAwait(false); - - var localHighestModSeq = (ulong)folder.HighestModeSeq; - - bool isInitialSynchronization = localHighestModSeq == 0; - - // There are some changes on new messages or flag changes. - // Deletions are tracked separately because some servers do not increase - // the MODSEQ value for deleted messages. - if (remoteFolder.HighestModSeq > localHighestModSeq) - { - var changedUids = await GetChangedUidsAsync(client, remoteFolder, synchronizer, cancellationToken).ConfigureAwait(false); - - // Get locally exists mails for the returned UIDs. - downloadedMessageIds = await HandleChangedUIdsAsync(synchronizer, remoteFolder, changedUids, cancellationToken).ConfigureAwait(false); - - folder.HighestModeSeq = unchecked((long)remoteFolder.HighestModSeq); - - await FolderService.UpdateFolderAsync(folder).ConfigureAwait(false); - } - - await ManageUUIdBasedDeletedMessagesAsync(folder, remoteFolder, cancellationToken).ConfigureAwait(false); - - return downloadedMessageIds; - } - catch (FolderNotFoundException) - { - await FolderService.DeleteFolderAsync(folder.MailAccountId, folder.RemoteFolderId).ConfigureAwait(false); - - return default; - } - catch (Exception) - { - throw; - } - finally - { - if (!cancellationToken.IsCancellationRequested) - { - if (remoteFolder != null) - { - if (remoteFolder.IsOpen) - { - await remoteFolder.CloseAsync(cancellationToken: cancellationToken).ConfigureAwait(false); - } - } - } - } - } - - internal override async Task> GetChangedUidsAsync(IImapClient winoClient, IMailFolder remoteFolder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default) - { - var localHighestModSeq = (ulong)Folder.HighestModeSeq; - var remoteHighestModSeq = remoteFolder.HighestModSeq; - - // Search for emails with a MODSEQ greater than the last known value. - // Use SORT extension if server supports. - - IList changedUids = null; - - if (winoClient.Capabilities.HasFlag(ImapCapabilities.Sort)) - { - // Highest mod seq must be greater than 0 for SORT. - changedUids = await remoteFolder.SortAsync(SearchQuery.ChangedSince(Math.Max(localHighestModSeq, 1)), [OrderBy.ReverseDate], cancellationToken).ConfigureAwait(false); - } - else - { - changedUids = await remoteFolder.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken).ConfigureAwait(false); - } - - changedUids = await remoteFolder.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken).ConfigureAwait(false); - - // For initial synchronizations, take the first allowed number of items. - // For consequtive synchronizations, take all the items. We don't want to miss any changes. - // Smaller uid means newer message. For initial sync, we need start taking items from the top. - - bool isInitialSynchronization = localHighestModSeq == 0; - - if (isInitialSynchronization) - { - changedUids = changedUids.OrderByDescending(a => a.Id).Take((int)synchronizer.InitialMessageDownloadCountPerFolder).ToList(); - } - - return changedUids; - } -} diff --git a/Wino.Core/Synchronizers/ImapSync/ImapSynchronizationStrategyBase.cs b/Wino.Core/Synchronizers/ImapSync/ImapSynchronizationStrategyBase.cs deleted file mode 100644 index ae40e56a..00000000 --- a/Wino.Core/Synchronizers/ImapSync/ImapSynchronizationStrategyBase.cs +++ /dev/null @@ -1,194 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MailKit; -using MailKit.Net.Imap; -using MailKit.Search; -using MoreLinq; -using Serilog; -using Wino.Core.Domain.Entities.Mail; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.MailItem; -using Wino.Services.Extensions; -using IMailService = Wino.Core.Domain.Interfaces.IMailService; - -namespace Wino.Core.Synchronizers.ImapSync; - -public abstract class ImapSynchronizationStrategyBase : IImapSynchronizerStrategy -{ - // Minimum summary items to Fetch for mail synchronization from IMAP. - protected readonly MessageSummaryItems MailSynchronizationFlags = - MessageSummaryItems.Flags | - MessageSummaryItems.UniqueId | - MessageSummaryItems.ThreadId | - MessageSummaryItems.EmailId | - MessageSummaryItems.Headers | - MessageSummaryItems.PreviewText | - MessageSummaryItems.GMailThreadId | - MessageSummaryItems.References | - MessageSummaryItems.ModSeq; - - protected IFolderService FolderService { get; } - protected IMailService MailService { get; } - protected MailItemFolder Folder { get; set; } - - protected ImapSynchronizationStrategyBase(IFolderService folderService, IMailService mailService) - { - FolderService = folderService; - MailService = mailService; - } - - public abstract Task> HandleSynchronizationAsync(IImapClient client, MailItemFolder folder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default); - internal abstract Task> GetChangedUidsAsync(IImapClient client, IMailFolder remoteFolder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default); - - protected async Task> HandleChangedUIdsAsync(IImapSynchronizer synchronizer, - IMailFolder remoteFolder, - IList changedUids, - CancellationToken cancellationToken) - { - List downloadedMessageIds = new(); - - var existingMails = await MailService.GetExistingMailsAsync(Folder.Id, changedUids).ConfigureAwait(false); - var existingMailUids = existingMails.Select(m => MailkitClientExtensions.ResolveUidStruct(m.Id)).ToArray(); - - // These are the non-existing mails. They will be downloaded + processed. - var newMessageIds = changedUids.Except(existingMailUids).ToList(); - var deletedMessageIds = existingMailUids.Except(changedUids).ToList(); - - // Fetch minimum data for the existing mails in one query. - var existingFlagData = await remoteFolder.FetchAsync(existingMailUids, MessageSummaryItems.Flags | MessageSummaryItems.UniqueId).ConfigureAwait(false); - - foreach (var update in existingFlagData) - { - if (update.UniqueId == UniqueId.Invalid) - { - Log.Warning($"Couldn't fetch UniqueId for the mail. FetchAsync failed."); - continue; - } - - if (update.Flags == null) - { - Log.Warning($"Couldn't fetch flags for the mail with UID {update.UniqueId.Id}. FetchAsync failed."); - continue; - } - - var existingMail = existingMails.FirstOrDefault(m => MailkitClientExtensions.ResolveUidStruct(m.Id).Id == update.UniqueId.Id); - - if (existingMail == null) - { - Log.Warning($"Couldn't find the mail with UID {update.UniqueId.Id} in the local database. Flag update is ignored."); - continue; - } - - await HandleMessageFlagsChangeAsync(existingMail, update.Flags.Value).ConfigureAwait(false); - } - - // Fetch the new mails in batch. - - var batchedMessageIds = newMessageIds.Batch(50).ToList(); - // Create tasks for each batch. - foreach (var group in batchedMessageIds) - { - downloadedMessageIds.AddRange(group.Select(a => MailkitClientExtensions.CreateUid(Folder.Id, a.Id))); - - await DownloadMessagesAsync(synchronizer, remoteFolder, Folder, new UniqueIdSet(group, SortOrder.Ascending), cancellationToken).ConfigureAwait(false); - } - - return downloadedMessageIds; - } - - protected async Task HandleMessageFlagsChangeAsync(UniqueId? uniqueId, MessageFlags flags) - { - if (Folder == null) return; - if (uniqueId == null) return; - - var localMailCopyId = MailkitClientExtensions.CreateUid(Folder.Id, uniqueId.Value.Id); - - var isFlagged = MailkitClientExtensions.GetIsFlagged(flags); - var isRead = MailkitClientExtensions.GetIsRead(flags); - - await MailService.ChangeReadStatusAsync(localMailCopyId, isRead).ConfigureAwait(false); - await MailService.ChangeFlagStatusAsync(localMailCopyId, isFlagged).ConfigureAwait(false); - } - - protected async Task HandleMessageFlagsChangeAsync(MailCopy mailCopy, MessageFlags flags) - { - if (mailCopy == null) return; - - var isFlagged = MailkitClientExtensions.GetIsFlagged(flags); - var isRead = MailkitClientExtensions.GetIsRead(flags); - - if (isFlagged != mailCopy.IsFlagged) - { - await MailService.ChangeFlagStatusAsync(mailCopy.Id, isFlagged).ConfigureAwait(false); - } - - if (isRead != mailCopy.IsRead) - { - await MailService.ChangeReadStatusAsync(mailCopy.Id, isRead).ConfigureAwait(false); - } - } - - protected async Task HandleMessageDeletedAsync(IList uniqueIds) - { - if (Folder == null) return; - if (uniqueIds == null || uniqueIds.Count == 0) return; - - foreach (var uniqueId in uniqueIds) - { - if (uniqueId == null) continue; - var localMailCopyId = MailkitClientExtensions.CreateUid(Folder.Id, uniqueId.Id); - - await MailService.DeleteMailAsync(Folder.MailAccountId, localMailCopyId).ConfigureAwait(false); - } - } - - protected void OnMessagesVanished(object sender, MessagesVanishedEventArgs args) - => HandleMessageDeletedAsync(args.UniqueIds).ConfigureAwait(false); - - protected void OnMessageFlagsChanged(object sender, MessageFlagsChangedEventArgs args) - => HandleMessageFlagsChangeAsync(args.UniqueId, args.Flags).ConfigureAwait(false); - - protected async Task ManageUUIdBasedDeletedMessagesAsync(MailItemFolder localFolder, IMailFolder remoteFolder, CancellationToken cancellationToken = default) - { - var allUids = (await FolderService.GetKnownUidsForFolderAsync(localFolder.Id)).Select(a => new UniqueId(a)).ToList(); - - if (allUids.Count > 0) - { - var remoteAllUids = await remoteFolder.SearchAsync(SearchQuery.All, cancellationToken); - var deletedUids = allUids.Except(remoteAllUids).ToList(); - - await HandleMessageDeletedAsync(deletedUids).ConfigureAwait(false); - } - } - - public async Task DownloadMessagesAsync(IImapSynchronizer synchronizer, - IMailFolder folder, - MailItemFolder localFolder, - UniqueIdSet uniqueIdSet, - CancellationToken cancellationToken = default) - { - var summaries = await folder.FetchAsync(uniqueIdSet, MailSynchronizationFlags, cancellationToken).ConfigureAwait(false); - - foreach (var summary in summaries) - { - var mimeMessage = await folder.GetMessageAsync(summary.UniqueId, cancellationToken).ConfigureAwait(false); - - var creationPackage = new ImapMessageCreationPackage(summary, mimeMessage); - - var mailPackages = await synchronizer.CreateNewMailPackagesAsync(creationPackage, localFolder, cancellationToken).ConfigureAwait(false); - - if (mailPackages != null) - { - foreach (var package in mailPackages) - { - // Local draft is mapped. We don't need to create a new mail copy. - if (package == null) continue; - - await MailService.CreateMailAsync(localFolder.MailAccountId, package).ConfigureAwait(false); - } - } - } - } -} diff --git a/Wino.Core/Synchronizers/ImapSync/ImapSynchronizationStrategyProvider.cs b/Wino.Core/Synchronizers/ImapSync/ImapSynchronizationStrategyProvider.cs deleted file mode 100644 index 25c7d93e..00000000 --- a/Wino.Core/Synchronizers/ImapSync/ImapSynchronizationStrategyProvider.cs +++ /dev/null @@ -1,30 +0,0 @@ -using MailKit.Net.Imap; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Integration; - -namespace Wino.Core.Synchronizers.ImapSync; - -internal class ImapSynchronizationStrategyProvider : IImapSynchronizationStrategyProvider -{ - private readonly QResyncSynchronizer _qResyncSynchronizer; - private readonly CondstoreSynchronizer _condstoreSynchronizer; - private readonly UidBasedSynchronizer _uidBasedSynchronizer; - - public ImapSynchronizationStrategyProvider(QResyncSynchronizer qResyncSynchronizer, CondstoreSynchronizer condstoreSynchronizer, UidBasedSynchronizer uidBasedSynchronizer) - { - _qResyncSynchronizer = qResyncSynchronizer; - _condstoreSynchronizer = condstoreSynchronizer; - _uidBasedSynchronizer = uidBasedSynchronizer; - } - - public IImapSynchronizerStrategy GetSynchronizationStrategy(IImapClient client) - { - if (client is not WinoImapClient winoImapClient) - throw new System.ArgumentException("Client must be of type WinoImapClient.", nameof(client)); - - if (client.Capabilities.HasFlag(ImapCapabilities.QuickResync) && winoImapClient.IsQResyncEnabled) return _qResyncSynchronizer; - if (client.Capabilities.HasFlag(ImapCapabilities.CondStore)) return _condstoreSynchronizer; - - return _uidBasedSynchronizer; - } -} diff --git a/Wino.Core/Synchronizers/ImapSync/QResyncSynchronizer.cs b/Wino.Core/Synchronizers/ImapSync/QResyncSynchronizer.cs deleted file mode 100644 index 5316336e..00000000 --- a/Wino.Core/Synchronizers/ImapSync/QResyncSynchronizer.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MailKit; -using MailKit.Net.Imap; -using MailKit.Search; -using Wino.Core.Domain.Entities.Mail; -using Wino.Core.Domain.Exceptions; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Integration; -using IMailService = Wino.Core.Domain.Interfaces.IMailService; - -namespace Wino.Core.Synchronizers.ImapSync; - -/// -/// RFC 5162 QRESYNC IMAP Synchronization strategy. -/// -internal class QResyncSynchronizer : ImapSynchronizationStrategyBase -{ - public QResyncSynchronizer(IFolderService folderService, IMailService mailService) : base(folderService, mailService) - { - } - - public override async Task> HandleSynchronizationAsync(IImapClient client, - MailItemFolder folder, - IImapSynchronizer synchronizer, - CancellationToken cancellationToken = default) - { - var downloadedMessageIds = new List(); - - if (client is not WinoImapClient winoClient) - throw new ImapSynchronizerStrategyException("Client must be of type WinoImapClient."); - - if (!client.Capabilities.HasFlag(ImapCapabilities.QuickResync)) - throw new ImapSynchronizerStrategyException("Server does not support QRESYNC."); - - if (!winoClient.IsQResyncEnabled) - throw new ImapSynchronizerStrategyException("QRESYNC is not enabled for WinoImapClient."); - - // Ready to implement QRESYNC synchronization. - - IMailFolder remoteFolder = null; - - Folder = folder; - - try - { - remoteFolder = await client.GetFolderAsync(folder.RemoteFolderId, cancellationToken).ConfigureAwait(false); - - // Check the Uid validity first. - // If they don't match, clear all the local data and perform full-resync. - - bool isCacheValid = remoteFolder.UidValidity == folder.UidValidity; - - if (!isCacheValid) - { - // TODO: Remove all local data. - } - - // Perform QRESYNC synchronization. - var localHighestModSeq = (ulong)folder.HighestModeSeq; - // HIGHESTMODSEQ must be a positive integer, 0 is illegal. - // It's harmless to set it to 1, as RFC-compliant server without mod-seq would ignore this parameter. - if (localHighestModSeq == 0) localHighestModSeq = 1; - - remoteFolder.MessagesVanished += OnMessagesVanished; - remoteFolder.MessageFlagsChanged += OnMessageFlagsChanged; - - var allUids = await FolderService.GetKnownUidsForFolderAsync(folder.Id); - var allUniqueIds = allUids.Select(a => new UniqueId(a)).ToList(); - - await remoteFolder.OpenAsync(FolderAccess.ReadOnly, folder.UidValidity, localHighestModSeq, allUniqueIds).ConfigureAwait(false); - - var changedUids = await GetChangedUidsAsync(client, remoteFolder, synchronizer, cancellationToken).ConfigureAwait(false); - - downloadedMessageIds = await HandleChangedUIdsAsync(synchronizer, remoteFolder, changedUids, cancellationToken).ConfigureAwait(false); - - // Update the local folder with the new highest mod-seq and validity. - folder.HighestModeSeq = unchecked((long)remoteFolder.HighestModSeq); - folder.UidValidity = remoteFolder.UidValidity; - - await ManageUUIdBasedDeletedMessagesAsync(folder, remoteFolder, cancellationToken).ConfigureAwait(false); - - await FolderService.UpdateFolderAsync(folder).ConfigureAwait(false); - } - catch (FolderNotFoundException) - { - await FolderService.DeleteFolderAsync(folder.MailAccountId, folder.RemoteFolderId).ConfigureAwait(false); - - return default; - } - catch (Exception) - { - throw; - } - finally - { - if (!cancellationToken.IsCancellationRequested) - { - if (remoteFolder != null) - { - remoteFolder.MessagesVanished -= OnMessagesVanished; - remoteFolder.MessageFlagsChanged -= OnMessageFlagsChanged; - - if (remoteFolder.IsOpen) - { - await remoteFolder.CloseAsync(); - } - } - } - } - - return downloadedMessageIds; - } - - internal override async Task> GetChangedUidsAsync(IImapClient client, IMailFolder remoteFolder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default) - { - var localHighestModSeq = (ulong)Folder.HighestModeSeq; - if (localHighestModSeq == 0) localHighestModSeq = 1; - return await remoteFolder.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken).ConfigureAwait(false); - } -} diff --git a/Wino.Core/Synchronizers/ImapSync/UidBasedSynchronizer.cs b/Wino.Core/Synchronizers/ImapSync/UidBasedSynchronizer.cs deleted file mode 100644 index 6f67be3a..00000000 --- a/Wino.Core/Synchronizers/ImapSync/UidBasedSynchronizer.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MailKit; -using MailKit.Net.Imap; -using MailKit.Search; -using Wino.Core.Domain.Entities.Mail; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Integration; - -namespace Wino.Core.Synchronizers.ImapSync; - -/// -/// Uid based IMAP Synchronization strategy. -/// -internal class UidBasedSynchronizer : ImapSynchronizationStrategyBase -{ - public UidBasedSynchronizer(IFolderService folderService, Domain.Interfaces.IMailService mailService) : base(folderService, mailService) - { - } - - public override async Task> HandleSynchronizationAsync(IImapClient client, MailItemFolder folder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default) - { - if (client is not WinoImapClient winoClient) - throw new ArgumentException("Client must be of type WinoImapClient.", nameof(client)); - - Folder = folder; - - var downloadedMessageIds = new List(); - IMailFolder remoteFolder = null; - - try - { - remoteFolder = await winoClient.GetFolderAsync(folder.RemoteFolderId, cancellationToken).ConfigureAwait(false); - - await remoteFolder.OpenAsync(FolderAccess.ReadOnly, cancellationToken).ConfigureAwait(false); - - // Fetch UIDs from the remote folder - var remoteUids = await remoteFolder.SearchAsync(SearchQuery.All, cancellationToken).ConfigureAwait(false); - - remoteUids = remoteUids.OrderByDescending(a => a.Id).Take((int)synchronizer.InitialMessageDownloadCountPerFolder).ToList(); - - await HandleChangedUIdsAsync(synchronizer, remoteFolder, remoteUids, cancellationToken).ConfigureAwait(false); - await ManageUUIdBasedDeletedMessagesAsync(folder, remoteFolder, cancellationToken).ConfigureAwait(false); - } - catch (FolderNotFoundException) - { - await FolderService.DeleteFolderAsync(folder.MailAccountId, folder.RemoteFolderId).ConfigureAwait(false); - - return default; - } - catch (Exception) - { - - throw; - } - finally - { - if (!cancellationToken.IsCancellationRequested) - { - if (remoteFolder != null) - { - if (remoteFolder.IsOpen) - { - await remoteFolder.CloseAsync(cancellationToken: cancellationToken).ConfigureAwait(false); - } - } - } - } - - return downloadedMessageIds; - } - - internal override Task> GetChangedUidsAsync(IImapClient client, IMailFolder remoteFolder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } -} diff --git a/Wino.Core/Synchronizers/ImapSync/UnifiedImapSynchronizer.cs b/Wino.Core/Synchronizers/ImapSync/UnifiedImapSynchronizer.cs new file mode 100644 index 00000000..8ec32bfc --- /dev/null +++ b/Wino.Core/Synchronizers/ImapSync/UnifiedImapSynchronizer.cs @@ -0,0 +1,735 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MailKit; +using MailKit.Net.Imap; +using MailKit.Search; +using MoreLinq; +using Serilog; +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.MailItem; +using Wino.Core.Domain.Models.Synchronization; +using Wino.Core.Integration; +using Wino.Services.Extensions; +using IMailService = Wino.Core.Domain.Interfaces.IMailService; + +namespace Wino.Core.Synchronizers.ImapSync; + +/// +/// Unified IMAP synchronization strategy that automatically selects the best available method: +/// 1. QRESYNC (RFC 5162) - Best: supports quick resync with vanished messages +/// 2. CONDSTORE (RFC 4551) - Good: supports mod-seq based change tracking +/// 3. UID-based delta - Fallback: tracks UIDNEXT/high-water UID without sequence-number persistence +/// +public class UnifiedImapSynchronizer +{ + private static readonly TimeSpan UidReconcileInterval = TimeSpan.FromHours(12); + + private readonly ILogger _logger = Log.ForContext(); + private readonly IFolderService _folderService; + private readonly IMailService _mailService; + private readonly IImapSynchronizerErrorHandlerFactory _errorHandlerFactory; + + // Metadata-first synchronization flags: no full MIME body download. + private readonly MessageSummaryItems _mailSynchronizationFlags = + MessageSummaryItems.Flags | + MessageSummaryItems.UniqueId | + MessageSummaryItems.InternalDate | + MessageSummaryItems.Envelope | + MessageSummaryItems.Headers | + MessageSummaryItems.PreviewText | + MessageSummaryItems.GMailThreadId | + MessageSummaryItems.References | + MessageSummaryItems.ModSeq | + MessageSummaryItems.BodyStructure; + + public UnifiedImapSynchronizer( + IFolderService folderService, + IMailService mailService, + IImapSynchronizerErrorHandlerFactory errorHandlerFactory) + { + _folderService = folderService; + _mailService = mailService; + _errorHandlerFactory = errorHandlerFactory; + } + + /// + /// Determines the best synchronization strategy based on server capabilities and known quirks. + /// + public ImapSyncStrategy DetermineSyncStrategy(IImapClient client, string serverHost) + { + var capabilities = client.Capabilities; + var isQResyncEnabled = client is WinoImapClient winoClient && winoClient.IsQResyncEnabled; + + return DetermineSyncStrategy(capabilities, isQResyncEnabled, serverHost); + } + + public ImapSyncStrategy DetermineSyncStrategy(ImapCapabilities capabilities, bool isQResyncEnabled, string serverHost = null) + { + var quirks = ImapServerQuirks.Resolve(serverHost); + + if (!quirks.DisableQResync && capabilities.HasFlag(ImapCapabilities.QuickResync) && isQResyncEnabled) + return ImapSyncStrategy.QResync; + + if (!quirks.DisableCondstore && capabilities.HasFlag(ImapCapabilities.CondStore)) + return ImapSyncStrategy.Condstore; + + return ImapSyncStrategy.UidBased; + } + + /// + /// Main synchronization entry point. Automatically selects the best strategy. + /// + public async Task SynchronizeFolderAsync( + IImapClient client, + MailItemFolder folder, + IImapSynchronizer synchronizer, + string serverHost, + CancellationToken cancellationToken = default) + { + var strategy = DetermineSyncStrategy(client, serverHost); + _logger.Debug("Using {Strategy} sync strategy for folder {FolderName}", strategy, folder.FolderName); + + var originalHighestModeSeq = folder.HighestModeSeq; + var originalUidValidity = folder.UidValidity; + var originalHighestKnownUid = folder.HighestKnownUid; + var originalLastUidReconcileUtc = folder.LastUidReconcileUtc; + + try + { + var downloadedIds = strategy switch + { + ImapSyncStrategy.QResync => await SynchronizeWithQResyncAsync(client, folder, synchronizer, cancellationToken).ConfigureAwait(false), + ImapSyncStrategy.Condstore => await SynchronizeWithCondstoreAsync(client, folder, synchronizer, cancellationToken).ConfigureAwait(false), + _ => await SynchronizeWithUidDeltaAsync(client, folder, synchronizer, cancellationToken).ConfigureAwait(false) + }; + + bool highestModeSeqChanged = folder.HighestModeSeq != originalHighestModeSeq; + bool requiresFullFolderUpdate = + folder.UidValidity != originalUidValidity + || folder.HighestKnownUid != originalHighestKnownUid + || folder.LastUidReconcileUtc != originalLastUidReconcileUtc; + + if (requiresFullFolderUpdate) + { + // Persist all sync-state fields in one write when any non-mod-seq token changed. + await _folderService.UpdateFolderAsync(folder).ConfigureAwait(false); + } + else if (highestModeSeqChanged) + { + // Avoid full-folder write when only mod-seq changed. + await _folderService.UpdateFolderHighestModeSeqAsync(folder.Id, folder.HighestModeSeq).ConfigureAwait(false); + } + + return FolderSyncResult.Successful(folder.Id, folder.FolderName, downloadedIds.Count); + } + catch (FolderNotFoundException) + { + _logger.Warning("Folder {FolderName} not found on server, deleting locally", folder.FolderName); + await _folderService.DeleteFolderAsync(folder.MailAccountId, folder.RemoteFolderId).ConfigureAwait(false); + + return FolderSyncResult.Skipped(folder.Id, folder.FolderName, "Folder not found on server"); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + var errorContext = new SynchronizerErrorContext + { + ErrorMessage = ex.Message, + Exception = ex, + FolderId = folder.Id, + FolderName = folder.FolderName, + OperationType = "ImapFolderSync" + }; + + _ = await _errorHandlerFactory.HandleErrorAsync(errorContext).ConfigureAwait(false); + + if (errorContext.CanContinueSync) + { + _logger.Warning(ex, "Folder {FolderName} sync failed with recoverable error", folder.FolderName); + return FolderSyncResult.Failed(folder.Id, folder.FolderName, errorContext); + } + + _logger.Error(ex, "Folder {FolderName} sync failed with fatal error", folder.FolderName); + throw; + } + } + + /// + /// Metadata-only message download helper used by IMAP online search. + /// + public async Task> DownloadMessagesByUidsAsync( + IImapClient client, + IMailFolder remoteFolder, + MailItemFolder localFolder, + IList uids, + IImapSynchronizer synchronizer, + CancellationToken cancellationToken = default) + { + if (uids == null || uids.Count == 0) + return []; + + if (!remoteFolder.IsOpen) + await remoteFolder.OpenAsync(FolderAccess.ReadOnly, cancellationToken).ConfigureAwait(false); + + var downloadedMessageIds = new List(); + + foreach (var batch in uids.Distinct().OrderBy(a => a.Id).Batch(50)) + { + cancellationToken.ThrowIfCancellationRequested(); + + var summaryBatch = await remoteFolder + .FetchAsync(new UniqueIdSet(batch.ToList(), SortOrder.Ascending), _mailSynchronizationFlags, cancellationToken) + .ConfigureAwait(false); + + downloadedMessageIds.AddRange(await ProcessSummariesAsync(synchronizer, localFolder, summaryBatch, cancellationToken).ConfigureAwait(false)); + } + + UpdateHighestKnownUid(localFolder, remoteFolder, uids.Select(a => a.Id)); + return downloadedMessageIds; + } + + #region Strategy Implementations + + private async Task> SynchronizeWithQResyncAsync( + IImapClient client, + MailItemFolder folder, + IImapSynchronizer synchronizer, + CancellationToken cancellationToken) + { + if (client is not WinoImapClient) + throw new InvalidOperationException("QRESYNC requires WinoImapClient."); + + var downloadedMessageIds = new List(); + IMailFolder remoteFolder = null; + + var vanishedUids = new List(); + var changedFlags = new Dictionary(); + + void OnMessagesVanished(object sender, MessagesVanishedEventArgs args) + { + lock (vanishedUids) + { + vanishedUids.AddRange(args.UniqueIds); + } + } + + void OnMessageFlagsChanged(object sender, MessageFlagsChangedEventArgs args) + { + if (args.UniqueId is not UniqueId uniqueId) + return; + + lock (changedFlags) + { + changedFlags[uniqueId.Id] = args.Flags; + } + } + + try + { + remoteFolder = await client.GetFolderAsync(folder.RemoteFolderId, cancellationToken).ConfigureAwait(false); + + // Open once to validate UIDVALIDITY and reset local state if needed. + await remoteFolder.OpenAsync(FolderAccess.ReadOnly, cancellationToken).ConfigureAwait(false); + await EnsureUidValidityStateAsync(folder, remoteFolder).ConfigureAwait(false); + await remoteFolder.CloseAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + + var knownUids = await _folderService.GetKnownUidsForFolderAsync(folder.Id).ConfigureAwait(false); + var knownUidStructs = knownUids.Select(a => new UniqueId(a)).ToList(); + var localHighestModSeq = (ulong)Math.Max(folder.HighestModeSeq, 1); + + remoteFolder.MessagesVanished += OnMessagesVanished; + remoteFolder.MessageFlagsChanged += OnMessageFlagsChanged; + + await remoteFolder + .OpenAsync(FolderAccess.ReadOnly, folder.UidValidity, localHighestModSeq, knownUidStructs, cancellationToken) + .ConfigureAwait(false); + + var changedUids = await remoteFolder + .SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken) + .ConfigureAwait(false); + + downloadedMessageIds = await DownloadMessagesByUidsAsync(client, remoteFolder, folder, changedUids, synchronizer, cancellationToken).ConfigureAwait(false); + + folder.HighestModeSeq = unchecked((long)remoteFolder.HighestModSeq); + + await ApplyFlagChangesAsync(folder, changedFlags).ConfigureAwait(false); + await ApplyDeletedUidsAsync(folder, vanishedUids).ConfigureAwait(false); + + if (ShouldRunUidReconcile(folder)) + { + await ReconcileDeletedMessagesAsync(folder, remoteFolder, cancellationToken).ConfigureAwait(false); + } + } + finally + { + if (remoteFolder != null) + { + remoteFolder.MessagesVanished -= OnMessagesVanished; + remoteFolder.MessageFlagsChanged -= OnMessageFlagsChanged; + + if (remoteFolder.IsOpen && !cancellationToken.IsCancellationRequested) + { + await remoteFolder.CloseAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + } + } + } + + return downloadedMessageIds; + } + + private async Task> SynchronizeWithCondstoreAsync( + IImapClient client, + MailItemFolder folder, + IImapSynchronizer synchronizer, + CancellationToken cancellationToken) + { + var downloadedMessageIds = new List(); + IMailFolder remoteFolder = null; + + try + { + remoteFolder = await client.GetFolderAsync(folder.RemoteFolderId, cancellationToken).ConfigureAwait(false); + await remoteFolder.OpenAsync(FolderAccess.ReadOnly, cancellationToken).ConfigureAwait(false); + + await EnsureUidValidityStateAsync(folder, remoteFolder).ConfigureAwait(false); + + var localHighestModSeq = (ulong)Math.Max(folder.HighestModeSeq, 1); + bool isInitialSync = folder.HighestModeSeq == 0; + + if (remoteFolder.HighestModSeq > localHighestModSeq || isInitialSync) + { + IList changedUids; + + if (client.Capabilities.HasFlag(ImapCapabilities.Sort)) + { + changedUids = await remoteFolder + .SortAsync(SearchQuery.ChangedSince(localHighestModSeq), [OrderBy.ReverseDate], cancellationToken) + .ConfigureAwait(false); + } + else + { + changedUids = await remoteFolder + .SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken) + .ConfigureAwait(false); + } + + if (isInitialSync) + { + changedUids = changedUids + .OrderByDescending(a => a.Id) + .Take((int)synchronizer.InitialMessageDownloadCountPerFolder) + .ToList(); + } + + downloadedMessageIds = await DownloadMessagesByUidsAsync(client, remoteFolder, folder, changedUids, synchronizer, cancellationToken).ConfigureAwait(false); + folder.HighestModeSeq = unchecked((long)remoteFolder.HighestModSeq); + } + + if (ShouldRunUidReconcile(folder)) + { + await ReconcileDeletedMessagesAsync(folder, remoteFolder, cancellationToken).ConfigureAwait(false); + } + } + finally + { + if (remoteFolder?.IsOpen == true && !cancellationToken.IsCancellationRequested) + { + await remoteFolder.CloseAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + } + } + + return downloadedMessageIds; + } + + private async Task> SynchronizeWithUidDeltaAsync( + IImapClient client, + MailItemFolder folder, + IImapSynchronizer synchronizer, + CancellationToken cancellationToken) + { + var downloadedMessageIds = new List(); + IMailFolder remoteFolder = null; + + try + { + remoteFolder = await client.GetFolderAsync(folder.RemoteFolderId, cancellationToken).ConfigureAwait(false); + await remoteFolder.OpenAsync(FolderAccess.ReadOnly, cancellationToken).ConfigureAwait(false); + + await EnsureUidValidityStateAsync(folder, remoteFolder).ConfigureAwait(false); + + if (folder.HighestKnownUid == 0) + { + var remoteUids = await remoteFolder.SearchAsync(SearchQuery.All, cancellationToken).ConfigureAwait(false); + + var initialUids = remoteUids + .OrderByDescending(a => a.Id) + .Take((int)synchronizer.InitialMessageDownloadCountPerFolder) + .ToList(); + + downloadedMessageIds = await DownloadMessagesByUidsAsync(client, remoteFolder, folder, initialUids, synchronizer, cancellationToken).ConfigureAwait(false); + UpdateHighestKnownUid(folder, remoteFolder, remoteUids.Select(a => a.Id)); + } + else + { + var minUid = new UniqueId(folder.HighestKnownUid + 1); + var deltaUids = await remoteFolder + .SearchAsync(SearchQuery.Uids(new UniqueIdRange(minUid, UniqueId.MaxValue)), cancellationToken) + .ConfigureAwait(false); + + downloadedMessageIds = await DownloadMessagesByUidsAsync(client, remoteFolder, folder, deltaUids, synchronizer, cancellationToken).ConfigureAwait(false); + UpdateHighestKnownUid(folder, remoteFolder, deltaUids.Select(a => a.Id)); + } + + await ReconcileUidBasedFlagChangesAsync(folder, remoteFolder, cancellationToken).ConfigureAwait(false); + + if (ShouldRunUidReconcile(folder)) + { + await ReconcileDeletedMessagesAsync(folder, remoteFolder, cancellationToken).ConfigureAwait(false); + } + } + finally + { + if (remoteFolder?.IsOpen == true && !cancellationToken.IsCancellationRequested) + { + await remoteFolder.CloseAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + } + } + + return downloadedMessageIds; + } + + #endregion + + #region Shared Helpers + + private async Task EnsureUidValidityStateAsync(MailItemFolder folder, IMailFolder remoteFolder) + { + if (folder.UidValidity != 0 && remoteFolder.UidValidity != folder.UidValidity) + { + _logger.Warning("UIDVALIDITY changed for folder {FolderName}. Resetting local folder state.", folder.FolderName); + + var existingMails = await _mailService.GetMailsByFolderIdAsync(folder.Id).ConfigureAwait(false); + foreach (var mail in existingMails) + { + await _mailService.DeleteMailAsync(folder.MailAccountId, mail.Id).ConfigureAwait(false); + } + + folder.HighestKnownUid = 0; + folder.HighestModeSeq = 0; + folder.LastUidReconcileUtc = null; + } + + folder.UidValidity = remoteFolder.UidValidity; + } + + private async Task> ProcessSummariesAsync( + IImapSynchronizer synchronizer, + MailItemFolder localFolder, + IList summaries, + CancellationToken cancellationToken) + { + var downloadedMessageIds = new List(); + + if (summaries == null || summaries.Count == 0) + return downloadedMessageIds; + + var uniqueIds = summaries + .Where(s => s.UniqueId != UniqueId.Invalid) + .Select(s => s.UniqueId) + .ToList(); + + if (uniqueIds.Count == 0) + return downloadedMessageIds; + + var existingMails = await _mailService.GetExistingMailsAsync(localFolder.Id, uniqueIds).ConfigureAwait(false); + var existingByUid = existingMails + .Select(m => (Uid: MailkitClientExtensions.ResolveUidStruct(m.Id), Mail: m)) + .ToDictionary(a => a.Uid.Id, a => a.Mail); + + foreach (var summary in summaries) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (summary.UniqueId == UniqueId.Invalid) + continue; + + if (existingByUid.TryGetValue(summary.UniqueId.Id, out var existingMail)) + { + if (summary.Flags != null) + { + await UpdateMailFlagsAsync(existingMail, summary.Flags.Value).ConfigureAwait(false); + } + + continue; + } + + var creationPackage = new ImapMessageCreationPackage(summary, mimeMessage: null); + var mailPackages = await synchronizer.CreateNewMailPackagesAsync(creationPackage, localFolder, cancellationToken).ConfigureAwait(false); + + if (mailPackages == null) + continue; + + foreach (var package in mailPackages) + { + if (package == null) + continue; + + var inserted = await _mailService.CreateMailAsync(localFolder.MailAccountId, package).ConfigureAwait(false); + if (inserted) + { + downloadedMessageIds.Add(package.Copy.Id); + } + } + } + + return downloadedMessageIds; + } + + private async Task UpdateMailFlagsAsync(MailCopy mailCopy, MessageFlags flags) + { + var isFlagged = MailkitClientExtensions.GetIsFlagged(flags); + var isRead = MailkitClientExtensions.GetIsRead(flags); + + if (isFlagged != mailCopy.IsFlagged) + { + await _mailService.ChangeFlagStatusAsync(mailCopy.Id, isFlagged).ConfigureAwait(false); + } + + if (isRead != mailCopy.IsRead) + { + await _mailService.ChangeReadStatusAsync(mailCopy.Id, isRead).ConfigureAwait(false); + } + } + + private async Task ApplyDeletedUidsAsync(MailItemFolder folder, IList uniqueIds) + { + if (uniqueIds == null || uniqueIds.Count == 0) + return; + + foreach (var uniqueId in uniqueIds.Distinct()) + { + var localMailCopyId = MailkitClientExtensions.CreateUid(folder.Id, uniqueId.Id); + await _mailService.DeleteMailAsync(folder.MailAccountId, localMailCopyId).ConfigureAwait(false); + } + } + + private async Task ApplyFlagChangesAsync(MailItemFolder folder, IDictionary changedFlags) + { + if (changedFlags == null || changedFlags.Count == 0) + return; + + foreach (var changed in changedFlags) + { + var localMailCopyId = MailkitClientExtensions.CreateUid(folder.Id, changed.Key); + var isFlagged = MailkitClientExtensions.GetIsFlagged(changed.Value); + var isRead = MailkitClientExtensions.GetIsRead(changed.Value); + + await _mailService.ChangeReadStatusAsync(localMailCopyId, isRead).ConfigureAwait(false); + await _mailService.ChangeFlagStatusAsync(localMailCopyId, isFlagged).ConfigureAwait(false); + } + } + + private async Task ReconcileUidBasedFlagChangesAsync(MailItemFolder localFolder, IMailFolder remoteFolder, CancellationToken cancellationToken) + { + var localMails = await _mailService.GetMailsByFolderIdAsync(localFolder.Id).ConfigureAwait(false); + + if (localMails == null || localMails.Count == 0) + return; + + var localByUid = new Dictionary(); + var localUnreadUids = new HashSet(); + var localFlaggedUids = new HashSet(); + + foreach (var localMail in localMails) + { + if (localMail == null || string.IsNullOrEmpty(localMail.Id)) + continue; + + uint uid; + try + { + uid = MailkitClientExtensions.ResolveUid(localMail.Id); + } + catch (ArgumentOutOfRangeException) + { + continue; + } + + localByUid[uid] = localMail; + + if (!localMail.IsRead) + localUnreadUids.Add(uid); + + if (localMail.IsFlagged) + localFlaggedUids.Add(uid); + } + + if (localByUid.Count == 0) + return; + + var remoteUnreadUids = (await remoteFolder.SearchAsync(SearchQuery.NotSeen, cancellationToken).ConfigureAwait(false)) + .Select(a => a.Id) + .ToHashSet(); + var remoteFlaggedUids = (await remoteFolder.SearchAsync(SearchQuery.Flagged, cancellationToken).ConfigureAwait(false)) + .Select(a => a.Id) + .ToHashSet(); + + var markReadCandidates = localUnreadUids.Except(remoteUnreadUids).ToList(); + var unflagCandidates = localFlaggedUids.Except(remoteFlaggedUids).ToList(); + + var existingMarkReadCandidates = await FilterExistingRemoteUidsAsync(remoteFolder, markReadCandidates, cancellationToken).ConfigureAwait(false); + var existingUnflagCandidates = await FilterExistingRemoteUidsAsync(remoteFolder, unflagCandidates, cancellationToken).ConfigureAwait(false); + + foreach (var uid in existingMarkReadCandidates) + { + if (!localByUid.TryGetValue(uid, out var localMail) || localMail.IsRead) + continue; + + await _mailService.ChangeReadStatusAsync(localMail.Id, true).ConfigureAwait(false); + } + + foreach (var uid in remoteUnreadUids) + { + if (!localByUid.TryGetValue(uid, out var localMail) || !localMail.IsRead) + continue; + + await _mailService.ChangeReadStatusAsync(localMail.Id, false).ConfigureAwait(false); + } + + foreach (var uid in existingUnflagCandidates) + { + if (!localByUid.TryGetValue(uid, out var localMail) || !localMail.IsFlagged) + continue; + + await _mailService.ChangeFlagStatusAsync(localMail.Id, false).ConfigureAwait(false); + } + + foreach (var uid in remoteFlaggedUids) + { + if (!localByUid.TryGetValue(uid, out var localMail) || localMail.IsFlagged) + continue; + + await _mailService.ChangeFlagStatusAsync(localMail.Id, true).ConfigureAwait(false); + } + } + + private static async Task> FilterExistingRemoteUidsAsync(IMailFolder remoteFolder, IEnumerable candidateUids, CancellationToken cancellationToken) + { + var existing = new HashSet(); + var uidList = candidateUids?.Distinct().ToList(); + + if (uidList == null || uidList.Count == 0) + return existing; + + foreach (var batch in uidList.Batch(200)) + { + cancellationToken.ThrowIfCancellationRequested(); + + var batchUids = batch.Select(a => new UniqueId(a)).ToList(); + var existingBatch = await remoteFolder + .SearchAsync(SearchQuery.Uids(new UniqueIdSet(batchUids, SortOrder.Ascending)), cancellationToken) + .ConfigureAwait(false); + + foreach (var existingUid in existingBatch) + { + existing.Add(existingUid.Id); + } + } + + return existing; + } + + private bool ShouldRunUidReconcile(MailItemFolder folder) + { + return ShouldRunUidReconcile(folder.LastUidReconcileUtc, DateTime.UtcNow, UidReconcileInterval); + } + + private async Task ReconcileDeletedMessagesAsync(MailItemFolder localFolder, IMailFolder remoteFolder, CancellationToken cancellationToken) + { + var allLocalUids = (await _folderService.GetKnownUidsForFolderAsync(localFolder.Id).ConfigureAwait(false)) + .Select(a => new UniqueId(a)) + .ToList(); + + if (allLocalUids.Count == 0) + { + localFolder.LastUidReconcileUtc = DateTime.UtcNow; + return; + } + + var remoteAllUids = await remoteFolder.SearchAsync(SearchQuery.All, cancellationToken).ConfigureAwait(false); + var deletedUids = allLocalUids.Except(remoteAllUids).ToList(); + + await ApplyDeletedUidsAsync(localFolder, deletedUids).ConfigureAwait(false); + localFolder.LastUidReconcileUtc = DateTime.UtcNow; + } + + private static void UpdateHighestKnownUid(MailItemFolder folder, IMailFolder remoteFolder, IEnumerable observedUids) + { + folder.HighestKnownUid = CalculateHighestKnownUid(folder.HighestKnownUid, remoteFolder?.UidNext, observedUids); + } + + public static bool ShouldRunUidReconcile(DateTime? lastUidReconcileUtc, DateTime utcNow, TimeSpan reconcileInterval) + { + if (!lastUidReconcileUtc.HasValue) + { + return true; + } + + return utcNow - lastUidReconcileUtc.Value >= reconcileInterval; + } + + public static uint CalculateHighestKnownUid(uint currentHighestKnownUid, UniqueId? uidNext, IEnumerable observedUids) + { + uint observedMax = 0; + + if (observedUids != null) + { + foreach (var uid in observedUids) + { + if (uid > observedMax) + { + observedMax = uid; + } + } + } + + uint uidNextBased = 0; + if (uidNext.HasValue) + { + uidNextBased = uidNext.Value.Id > 0 ? uidNext.Value.Id - 1 : 0; + } + + return Math.Max(currentHighestKnownUid, Math.Max(observedMax, uidNextBased)); + } + + #endregion +} + +/// +/// IMAP synchronization strategy enumeration. +/// +public enum ImapSyncStrategy +{ + /// + /// RFC 5162 Quick Resync - supports vanished messages and efficient delta sync. + /// + QResync, + + /// + /// RFC 4551 Conditional Store - supports mod-seq based change tracking. + /// + Condstore, + + /// + /// UID-based delta synchronization fallback. + /// + UidBased +} diff --git a/Wino.Core/Synchronizers/ImapSynchronizer.cs b/Wino.Core/Synchronizers/ImapSynchronizer.cs index 58680553..b0e925d9 100644 --- a/Wino.Core/Synchronizers/ImapSynchronizer.cs +++ b/Wino.Core/Synchronizers/ImapSynchronizer.cs @@ -1,21 +1,24 @@ -using System; +using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.Messaging; +using Itenso.TimePeriod; using MailKit; using MailKit.Net.Imap; using MailKit.Search; -using MoreLinq; +using MimeKit; using Serilog; +using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Exceptions; +using Wino.Core.Domain.Extensions; using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Connectivity; using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.MailItem; @@ -24,8 +27,11 @@ using Wino.Core.Extensions; using Wino.Core.Integration; using Wino.Core.Integration.Processors; using Wino.Core.Requests.Bundles; +using Wino.Core.Requests.Calendar; using Wino.Core.Requests.Folder; using Wino.Core.Requests.Mail; +using Wino.Core.Synchronizers.ImapSync; +using Wino.Core.Misc; using Wino.Messaging.Server; using Wino.Messaging.UI; using Wino.Services.Extensions; @@ -34,49 +40,62 @@ namespace Wino.Core.Synchronizers.Mail; public class ImapSynchronizer : WinoSynchronizer, IImapSynchronizer { - [Obsolete("N/A")] + /// + /// N/A for IMAP as it doesn't support batch modifications natively. + /// public override uint BatchModificationSize => 1000; public override uint InitialMessageDownloadCountPerFolder => 500; #region Idle Implementation - private CancellationTokenSource idleCancellationTokenSource; - private CancellationTokenSource idleDoneTokenSource; + private static readonly Random IdleReconnectJitter = new(); + private readonly object _idleDebounceLock = new(); + private CancellationTokenSource _idleLoopCancellationTokenSource; + private Task _idleLoopTask; + private int _lastIdleInboxCount = -1; + private DateTime _lastIdleSyncRequestUtc = DateTime.MinValue; + private readonly TimeSpan _idleSyncDebounceWindow = TimeSpan.FromSeconds(15); #endregion private readonly ILogger _logger = Log.ForContext(); private readonly ImapClientPool _clientPool; private readonly IImapChangeProcessor _imapChangeProcessor; - private readonly IImapSynchronizationStrategyProvider _imapSynchronizationStrategyProvider; private readonly IApplicationConfiguration _applicationConfiguration; + private readonly UnifiedImapSynchronizer _unifiedSynchronizer; + private readonly IImapSynchronizerErrorHandlerFactory _errorHandlerFactory; + private readonly ICalDavClient _calDavClient; + private readonly IAutoDiscoveryService _autoDiscoveryService; + private readonly ICalendarService _calendarService; + private readonly SemaphoreSlim _calDavDiscoveryLock = new(1, 1); + private Uri _cachedCalDavServiceUri; + private bool _isCalDavDiscoveryAttempted; + private readonly IImapCalendarOperationHandler _localCalendarOperationHandler; + private readonly IImapCalendarOperationHandler _calDavCalendarOperationHandler; + private bool _isFolderStructureChanged; public ImapSynchronizer(MailAccount account, IImapChangeProcessor imapChangeProcessor, - IImapSynchronizationStrategyProvider imapSynchronizationStrategyProvider, - IApplicationConfiguration applicationConfiguration) : base(account) + IApplicationConfiguration applicationConfiguration, + UnifiedImapSynchronizer unifiedSynchronizer, + IImapSynchronizerErrorHandlerFactory errorHandlerFactory, + ICalDavClient calDavClient, + IAutoDiscoveryService autoDiscoveryService, + ICalendarService calendarService) : base(account, WeakReferenceMessenger.Default) { - // Create client pool with account protocol log. _imapChangeProcessor = imapChangeProcessor; - _imapSynchronizationStrategyProvider = imapSynchronizationStrategyProvider; _applicationConfiguration = applicationConfiguration; + _unifiedSynchronizer = unifiedSynchronizer; + _errorHandlerFactory = errorHandlerFactory; + _calDavClient = calDavClient; + _autoDiscoveryService = autoDiscoveryService; + _calendarService = calendarService; - var protocolLogStream = CreateAccountProtocolLogFileStream(); - var poolOptions = ImapClientPoolOptions.CreateDefault(Account.ServerInformation, protocolLogStream); + var poolOptions = ImapClientPoolOptions.CreateDefault(Account.ServerInformation); _clientPool = new ImapClientPool(poolOptions); - } - - private Stream CreateAccountProtocolLogFileStream() - { - if (Account == null) throw new ArgumentNullException(nameof(Account)); - - var logFile = Path.Combine(_applicationConfiguration.ApplicationDataFolderPath, $"Protocol_{Account.Address}_{Account.Id}.log"); - - // Each session should start a new log. - if (File.Exists(logFile)) File.Delete(logFile); - - return new FileStream(logFile, FileMode.CreateNew); + _localCalendarOperationHandler = new LocalCalendarOperationHandler(Account, _imapChangeProcessor, _calendarService, _applicationConfiguration.ApplicationDataFolderPath, "local"); + _calDavCalendarOperationHandler = new CalDavCalendarOperationHandler(this, Account, _calendarService, _calDavClient); } /// @@ -112,7 +131,7 @@ public class ImapSynchronizer : WinoSynchronizer> RenameFolder(RenameFolderRequest request) @@ -250,6 +298,105 @@ public class ImapSynchronizer : WinoSynchronizer> DeleteFolder(DeleteFolderRequest request) + { + return CreateSingleTaskBundle(async (client, item) => + { + var folder = await client.GetFolderAsync(request.Folder.RemoteFolderId).ConfigureAwait(false); + await folder.DeleteAsync().ConfigureAwait(false); + }, request, request); + } + + public override List> CreateSubFolder(CreateSubFolderRequest request) + { + return CreateSingleTaskBundle(async (client, item) => + { + var parentFolder = await client.GetFolderAsync(request.Folder.RemoteFolderId).ConfigureAwait(false); + await parentFolder.CreateAsync(request.NewFolderName, true).ConfigureAwait(false); + }, request, request); + } + + public override List> CreateCalendarEvent(CreateCalendarEventRequest request) + { + var handler = ResolveCalendarOperationHandler(); + return CreateCalendarOperationTaskBundle( + request, + async value => await handler.CreateCalendarEventAsync(value).ConfigureAwait(false), + handler.RequiresConnectedClient); + } + + public override List> UpdateCalendarEvent(UpdateCalendarEventRequest request) + { + var handler = ResolveCalendarOperationHandler(); + return CreateCalendarOperationTaskBundle( + request, + async value => await handler.UpdateCalendarEventAsync(value).ConfigureAwait(false), + handler.RequiresConnectedClient); + } + + public override List> DeleteCalendarEvent(DeleteCalendarEventRequest request) + { + var handler = ResolveCalendarOperationHandler(); + return CreateCalendarOperationTaskBundle( + request, + async value => await handler.DeleteCalendarEventAsync(value).ConfigureAwait(false), + handler.RequiresConnectedClient); + } + + public override List> AcceptEvent(AcceptEventRequest request) + { + var handler = ResolveCalendarOperationHandler(); + return CreateCalendarOperationTaskBundle( + request, + async value => await handler.AcceptEventAsync(value).ConfigureAwait(false), + handler.RequiresConnectedClient); + } + + public override List> DeclineEvent(DeclineEventRequest request) + { + var handler = ResolveCalendarOperationHandler(); + return CreateCalendarOperationTaskBundle( + request, + async value => await handler.DeclineEventAsync(value).ConfigureAwait(false), + handler.RequiresConnectedClient); + } + + public override List> TentativeEvent(TentativeEventRequest request) + { + var handler = ResolveCalendarOperationHandler(); + return CreateCalendarOperationTaskBundle( + request, + async value => await handler.TentativeEventAsync(value).ConfigureAwait(false), + handler.RequiresConnectedClient); + } + + private IImapCalendarOperationHandler ResolveCalendarOperationHandler() + { + var mode = Account.ServerInformation?.CalendarSupportMode ?? ImapCalendarSupportMode.Disabled; + + return mode switch + { + ImapCalendarSupportMode.LocalOnly => _localCalendarOperationHandler, + ImapCalendarSupportMode.CalDav => _calDavCalendarOperationHandler, + _ => throw new NotSupportedException("Calendar operations are disabled for this IMAP account.") + }; + } + + private List> CreateCalendarOperationTaskBundle( + TRequest request, + Func operation, + bool requiresConnectedClient) + where TRequest : IRequestBase, IUIChangeRequest + { + return + [ + new ImapRequestBundle( + new ImapRequest((client, value) => operation(value), request, requiresConnectedClient), + request, + request) + ]; + } + #endregion public override async Task> CreateNewMailPackagesAsync(ImapMessageCreationPackage message, MailItemFolder assignedFolder, CancellationToken cancellationToken = default) @@ -264,21 +411,34 @@ public class ImapSynchronizer : WinoSynchronizer ExtractContactsFromMimeMessage(MimeMessage mimeMessage) + { + if (mimeMessage == null) return []; + + var contacts = new Dictionary(StringComparer.OrdinalIgnoreCase); + + AddFromInternetAddressList(mimeMessage.From); + AddFromInternetAddressList(mimeMessage.To); + AddFromInternetAddressList(mimeMessage.Cc); + AddFromInternetAddressList(mimeMessage.Bcc); + AddFromInternetAddressList(mimeMessage.ReplyTo); + + if (mimeMessage.Sender is MailboxAddress senderMailbox) + { + AddContact(senderMailbox.Address, senderMailbox.Name); + } + + return contacts.Values.ToList(); + + void AddFromInternetAddressList(InternetAddressList addresses) + { + if (addresses == null) return; + + foreach (var mailbox in addresses.Mailboxes) + { + AddContact(mailbox.Address, mailbox.Name); + } + } + + void AddContact(string address, string name) + { + var trimmedAddress = address?.Trim(); + if (string.IsNullOrWhiteSpace(trimmedAddress)) return; + + var displayName = string.IsNullOrWhiteSpace(name) ? trimmedAddress : name.Trim(); + + contacts[trimmedAddress] = new AccountContact + { + Address = trimmedAddress, + Name = displayName + }; + } + } + + private static IReadOnlyList ExtractContactsFromMessageSummary(IMessageSummary summary) + { + if (summary?.Envelope == null) return []; + + var contacts = new Dictionary(StringComparer.OrdinalIgnoreCase); + + AddFromInternetAddressList(summary.Envelope.From); + AddFromInternetAddressList(summary.Envelope.To); + AddFromInternetAddressList(summary.Envelope.Cc); + AddFromInternetAddressList(summary.Envelope.Bcc); + AddFromInternetAddressList(summary.Envelope.ReplyTo); + + var senderMailbox = summary.Envelope.Sender?.Mailboxes?.FirstOrDefault(); + if (senderMailbox != null) + { + AddContact(senderMailbox.Address, senderMailbox.Name); + } + + return contacts.Values.ToList(); + + void AddFromInternetAddressList(InternetAddressList addresses) + { + if (addresses == null) return; + + foreach (var mailbox in addresses.Mailboxes) + { + AddContact(mailbox.Address, mailbox.Name); + } + } + + void AddContact(string address, string name) + { + var trimmedAddress = address?.Trim(); + if (string.IsNullOrWhiteSpace(trimmedAddress)) return; + + var displayName = string.IsNullOrWhiteSpace(name) ? trimmedAddress : name.Trim(); + + contacts[trimmedAddress] = new AccountContact + { + Address = trimmedAddress, + Name = displayName + }; + } + } + protected override async Task SynchronizeMailsInternalAsync(MailSynchronizationOptions options, CancellationToken cancellationToken = default) { var downloadedMessageIds = new List(); + var folderResults = new List(); _logger.Information("Internal synchronization started for {Name}", Account.Name); _logger.Information("Options: {Options}", options); - PublishSynchronizationProgress(1); - - bool shouldDoFolderSync = options.Type == MailSynchronizationType.FullFolders || options.Type == MailSynchronizationType.FoldersOnly; - - if (shouldDoFolderSync) + try { - await SynchronizeFoldersAsync(cancellationToken).ConfigureAwait(false); - } + _isFolderStructureChanged = false; - if (options.Type != MailSynchronizationType.FoldersOnly) - { - var synchronizationFolders = await _imapChangeProcessor.GetSynchronizationFoldersAsync(options).ConfigureAwait(false); + // Set indeterminate progress initially + UpdateSyncProgress(0, 0, "Synchronizing..."); - for (int i = 0; i < synchronizationFolders.Count; i++) + bool shouldDoFolderSync = options.Type == MailSynchronizationType.FullFolders || options.Type == MailSynchronizationType.FoldersOnly; + + if (shouldDoFolderSync) { - var folder = synchronizationFolders[i]; - var progress = (int)Math.Round((double)(i + 1) / synchronizationFolders.Count * 100); + await SynchronizeFoldersAsync(cancellationToken).ConfigureAwait(false); - PublishSynchronizationProgress(progress); - - var folderDownloadedMessageIds = await SynchronizeFolderInternalAsync(folder, cancellationToken).ConfigureAwait(false); - - if (cancellationToken.IsCancellationRequested) return MailSynchronizationResult.Canceled; - - if (folderDownloadedMessageIds != null) + if (_isFolderStructureChanged) { - downloadedMessageIds.AddRange(folderDownloadedMessageIds); + WeakReferenceMessenger.Default.Send(new AccountFolderConfigurationUpdated(Account.Id)); } } - } - PublishSynchronizationProgress(100); + if (options.Type != MailSynchronizationType.FoldersOnly) + { + var synchronizationFolders = await _imapChangeProcessor.GetSynchronizationFoldersAsync(options).ConfigureAwait(false); + + var totalFolders = synchronizationFolders.Count; + const int maxParallelFolderSyncClients = 3; + var folderSyncSemaphore = new SemaphoreSlim(maxParallelFolderSyncClients, maxParallelFolderSyncClients); + using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var linkedToken = linkedCancellationTokenSource.Token; + var resultLock = new object(); + int completedFolders = 0; + + var syncTasks = synchronizationFolders.Select(async folder => + { + await folderSyncSemaphore.WaitAsync(linkedToken).ConfigureAwait(false); + + try + { + IImapClient client = null; + + try + { + client = await _clientPool.GetClientAsync(linkedToken).ConfigureAwait(false); + var folderResult = await _unifiedSynchronizer + .SynchronizeFolderAsync(client, folder, this, Account.ServerInformation?.IncomingServer, linkedToken) + .ConfigureAwait(false); + + List folderDownloadedIds = null; + if (folderResult.Success && folderResult.DownloadedCount > 0) + { + folderDownloadedIds = await GetDownloadedIdsForFolderAsync(folder, folderResult.DownloadedCount).ConfigureAwait(false); + } + + lock (resultLock) + { + folderResults.Add(folderResult); + if (folderDownloadedIds != null && folderDownloadedIds.Count > 0) + { + downloadedMessageIds.AddRange(folderDownloadedIds); + } + } + } + finally + { + if (client != null) + { + _clientPool.Release(client); + } + } + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + var errorContext = new SynchronizerErrorContext + { + Account = Account, + ErrorMessage = ex.Message, + Exception = ex, + FolderId = folder.Id, + FolderName = folder.FolderName, + OperationType = "ImapFolderSync" + }; + + _ = await _errorHandlerFactory.HandleErrorAsync(errorContext).ConfigureAwait(false); + var failedResult = FolderSyncResult.Failed(folder.Id, folder.FolderName, errorContext); + + lock (resultLock) + { + folderResults.Add(failedResult); + } + + if (!errorContext.CanContinueSync) + { + _logger.Error(ex, "Folder {FolderName} sync failed with fatal error", folder.FolderName); + linkedCancellationTokenSource.Cancel(); + throw; + } + + _logger.Warning(ex, "Folder {FolderName} sync failed, continuing with other folders", folder.FolderName); + } + finally + { + folderSyncSemaphore.Release(); + + var completed = Interlocked.Increment(ref completedFolders); + UpdateSyncProgress(totalFolders, totalFolders - completed, $"Syncing {folder.FolderName}..."); + } + }).ToList(); + + await Task.WhenAll(syncTasks).ConfigureAwait(false); + + if (cancellationToken.IsCancellationRequested) return MailSynchronizationResult.Canceled; + } + } + catch (OperationCanceledException) + { + _logger.Information("Synchronization was canceled for {Name}", Account.Name); + return MailSynchronizationResult.Canceled; + } + catch (Exception ex) + { + _logger.Error(ex, "Synchronization failed for {Name}", Account.Name); + return MailSynchronizationResult.Failed(ex); + } + finally + { + // Reset progress + ResetSyncProgress(); + } // Get all unread new downloaded items and return in the result. // This is primarily used in notifications. var unreadNewItems = await _imapChangeProcessor.GetDownloadedUnreadMailsAsync(Account.Id, downloadedMessageIds).ConfigureAwait(false); - return MailSynchronizationResult.Completed(unreadNewItems); + return MailSynchronizationResult.CompletedWithFolderResults(unreadNewItems, folderResults); + } + + /// + /// Gets the most recent downloaded message IDs for a folder. + /// Used for notification purposes after sync completes. + /// + private async Task> GetDownloadedIdsForFolderAsync(MailItemFolder folder, int count) + { + // Get the most recent mail IDs from the folder + var recentMails = await _imapChangeProcessor.GetRecentMailIdsForFolderAsync(folder.Id, count).ConfigureAwait(false); + return recentMails?.ToList() ?? new List(); } public override async Task ExecuteNativeRequestsAsync(List> batchedRequests, CancellationToken cancellationToken = default) @@ -341,7 +701,10 @@ public class ImapSynchronizer : WinoSynchronizer /// Assigns special folder type for the given local folder. /// If server doesn't support special folders, we can't determine the type. MailKit will throw for GetFolder. @@ -610,7 +1013,7 @@ public class ImapSynchronizer : WinoSynchronizer> OnlineSearchAsync(string queryText, List folders, CancellationToken cancellationToken = default) { IImapClient client = null; - IMailFolder activeFolder = null; try { @@ -642,6 +1044,9 @@ public class ImapSynchronizer : WinoSynchronizer> SynchronizeFolderInternalAsync(MailItemFolder folder, CancellationToken cancellationToken = default) - { - if (!folder.IsSynchronizationEnabled) return default; - - IImapClient availableClient = null; - - retry: - try - { - - availableClient = await _clientPool.GetClientAsync().ConfigureAwait(false); - - var strategy = _imapSynchronizationStrategyProvider.GetSynchronizationStrategy(availableClient); - return await strategy.HandleSynchronizationAsync(availableClient, folder, this, cancellationToken).ConfigureAwait(false); - } - catch (IOException) - { - _clientPool.Release(availableClient, false); - - goto retry; - } - catch (OperationCanceledException) - { - // Ignore cancellations. - } - catch (Exception ex) - { - _logger.Error(ex, "Synchronization failed for folder {FolderName}", folder.FolderName); - } - finally - { - _clientPool.Release(availableClient, false); - } - - return new List(); - } - /// /// Whether the local folder should be updated with the remote folder. /// IMAP only compares folder name for now. @@ -742,114 +1105,963 @@ public class ImapSynchronizer : WinoSynchronizer !localFolder.FolderName.Equals(remoteFolder.Name, StringComparison.OrdinalIgnoreCase); - protected override Task SynchronizeCalendarEventsInternalAsync(CalendarSynchronizationOptions options, CancellationToken cancellationToken = default) - => throw new NotImplementedException(); - - public async Task StartIdleClientAsync() + protected override async Task SynchronizeCalendarEventsInternalAsync(CalendarSynchronizationOptions options, CancellationToken cancellationToken = default) { - IImapClient idleClient = null; - IMailFolder inboxFolder = null; + if (Account.ProviderType != MailProviderType.IMAP4 || !Account.IsCalendarAccessGranted || Account.ServerInformation == null) + return CalendarSynchronizationResult.Empty; - bool? reconnect = null; + if (Account.ServerInformation.CalendarSupportMode is ImapCalendarSupportMode.Disabled or ImapCalendarSupportMode.LocalOnly) + return CalendarSynchronizationResult.Empty; + + var calDavServiceUri = await ResolveCalDavServiceUriAsync(cancellationToken).ConfigureAwait(false); + if (calDavServiceUri == null) + { + _logger.Information("Skipping calendar sync for {AccountName}: CalDAV endpoint is not configured.", Account.Name); + return CalendarSynchronizationResult.Empty; + } + + var password = ResolveCalDavPassword(); + if (string.IsNullOrWhiteSpace(password)) + { + _logger.Warning("Skipping calendar sync for {AccountName}: empty credentials.", Account.Name); + return CalendarSynchronizationResult.Empty; + } + + cancellationToken.ThrowIfCancellationRequested(); + + var calDavUsername = ResolveCalDavUsername(); + if (string.IsNullOrWhiteSpace(calDavUsername)) + { + _logger.Warning("Skipping calendar sync for {AccountName}: account email address is empty for CalDAV credentials.", Account.Name); + return CalendarSynchronizationResult.Empty; + } + + var activeConnection = new CalDavConnectionSettings + { + ServiceUri = calDavServiceUri, + Username = calDavUsername, + Password = password + }; + + IReadOnlyList remoteCalendars; try { - var client = await _clientPool.GetClientAsync().ConfigureAwait(false); + remoteCalendars = await _calDavClient + .DiscoverCalendarsAsync(activeConnection, cancellationToken) + .ConfigureAwait(false); + } + catch (UnauthorizedAccessException) + { + _logger.Warning("Skipping calendar sync for {AccountName}: CalDAV authentication failed for username {Username}.", Account.Name, calDavUsername); + return CalendarSynchronizationResult.Empty; + } - if (!client.Capabilities.HasFlag(ImapCapabilities.Idle)) + await SynchronizeCalendarMetadataAsync(remoteCalendars).ConfigureAwait(false); + + if (options?.Type == CalendarSynchronizationType.CalendarMetadata) + return CalendarSynchronizationResult.Empty; + + var localCalendars = await _imapChangeProcessor.GetAccountCalendarsAsync(Account.Id).ConfigureAwait(false); + var remoteCalendarsById = remoteCalendars.ToDictionary(c => c.RemoteCalendarId, StringComparer.OrdinalIgnoreCase); + + if (options?.Type == CalendarSynchronizationType.SingleCalendar && options.SynchronizationCalendarIds?.Count > 0) + { + localCalendars = localCalendars + .Where(c => options.SynchronizationCalendarIds.Contains(c.Id)) + .ToList(); + } + + localCalendars = localCalendars + .Where(c => c.IsSynchronizationEnabled) + .ToList(); + + var periodStartUtc = DateTimeOffset.UtcNow.AddYears(-1); + var periodEndUtc = DateTimeOffset.UtcNow.AddYears(2); + + foreach (var localCalendar in localCalendars) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (!remoteCalendarsById.TryGetValue(localCalendar.RemoteCalendarId, out var remoteCalendar)) + continue; + + var remoteToken = BuildCalendarDeltaToken(remoteCalendar); + + var isInitialSync = string.IsNullOrWhiteSpace(localCalendar.SynchronizationDeltaToken); + var tokenChanged = !string.Equals(localCalendar.SynchronizationDeltaToken, remoteToken, StringComparison.Ordinal); + var forceSync = options?.Type is CalendarSynchronizationType.ExecuteRequests or CalendarSynchronizationType.SingleCalendar; + + if (!isInitialSync && !tokenChanged && !forceSync) + continue; + + var remoteEvents = await _calDavClient.GetCalendarEventsAsync( + activeConnection, + remoteCalendar, + periodStartUtc, + periodEndUtc, + cancellationToken).ConfigureAwait(false); + var remoteEventIds = new HashSet( + remoteEvents + .Where(e => !string.IsNullOrWhiteSpace(e.RemoteEventId)) + .Select(e => e.RemoteEventId), + StringComparer.OrdinalIgnoreCase); + + foreach (var remoteEvent in remoteEvents) { - Log.Debug($"{Account.Name} does not support Idle command. Ignored."); - return; + var existingLocalItem = await _imapChangeProcessor + .GetCalendarItemAsync(localCalendar.Id, remoteEvent.RemoteEventId) + .ConfigureAwait(false); + + var shouldSkipUnchangedEvent = await ShouldSkipUnchangedCalDavEventAsync( + localCalendar, + existingLocalItem, + remoteEvent).ConfigureAwait(false); + + if (shouldSkipUnchangedEvent) + continue; + + await _imapChangeProcessor + .ManageCalendarEventAsync(remoteEvent, localCalendar, Account) + .ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(remoteEvent.IcsContent)) + continue; + + var localItem = existingLocalItem ?? await _imapChangeProcessor + .GetCalendarItemAsync(localCalendar.Id, remoteEvent.RemoteEventId) + .ConfigureAwait(false); + + if (localItem == null) + continue; + + await _imapChangeProcessor + .SaveCalendarItemIcsAsync( + Account.Id, + localCalendar.Id, + localItem.Id, + remoteEvent.RemoteEventId, + remoteEvent.RemoteResourceHref, + remoteEvent.ETag, + remoteEvent.IcsContent) + .ConfigureAwait(false); } - if (client.Inbox == null) + await ReconcileDeletedCalendarItemsAsync(localCalendar, periodStartUtc, periodEndUtc, remoteEventIds) + .ConfigureAwait(false); + + localCalendar.SynchronizationDeltaToken = remoteToken; + await _imapChangeProcessor.UpdateAccountCalendarAsync(localCalendar).ConfigureAwait(false); + } + + return CalendarSynchronizationResult.Empty; + } + + private async Task ShouldSkipUnchangedCalDavEventAsync( + AccountCalendar localCalendar, + CalendarItem existingLocalItem, + CalDavCalendarEvent remoteEvent) + { + if (localCalendar == null || existingLocalItem == null || remoteEvent == null) + return false; + + // Ensure unresolved parent-child linkage still gets corrected when required. + if (!string.IsNullOrWhiteSpace(remoteEvent.SeriesMasterRemoteEventId) && + existingLocalItem.RecurringCalendarItemId == null) + { + return false; + } + + if (string.IsNullOrWhiteSpace(remoteEvent.ETag)) + return false; + + var savedETag = await _imapChangeProcessor + .GetCalendarItemIcsETagAsync(Account.Id, localCalendar.Id, existingLocalItem.Id) + .ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(savedETag)) + return false; + + return string.Equals(savedETag.Trim(), remoteEvent.ETag.Trim(), StringComparison.Ordinal); + } + + private async Task ReconcileDeletedCalendarItemsAsync( + AccountCalendar localCalendar, + DateTimeOffset periodStartUtc, + DateTimeOffset periodEndUtc, + HashSet remoteEventIds) + { + var syncPeriod = new TimeRange(periodStartUtc.UtcDateTime, periodEndUtc.UtcDateTime); + var localEventsInWindow = await _calendarService + .GetCalendarEventsAsync(localCalendar, syncPeriod) + .ConfigureAwait(false); + + foreach (var localEvent in localEventsInWindow) + { + if (string.IsNullOrWhiteSpace(localEvent.RemoteEventId)) + continue; + + if (remoteEventIds.Contains(localEvent.RemoteEventId)) + continue; + + await _imapChangeProcessor.DeleteCalendarItemAsync(localEvent.Id).ConfigureAwait(false); + } + } + + private static string BuildCalendarDeltaToken(CalDavCalendar calendar) + { + if (calendar == null) + return string.Empty; + + var syncToken = calendar.SyncToken?.Trim() ?? string.Empty; + var ctag = calendar.CTag?.Trim() ?? string.Empty; + + if (!string.IsNullOrWhiteSpace(syncToken) && !string.IsNullOrWhiteSpace(ctag)) + return $"{syncToken}|{ctag}"; + + return !string.IsNullOrWhiteSpace(syncToken) ? syncToken : ctag; + } + + private async Task ResolveCalDavServiceUriAsync(CancellationToken cancellationToken) + { + var explicitCalDavUri = TryGetExplicitCalDavServiceUri(); + if (explicitCalDavUri != null) + { + _cachedCalDavServiceUri = explicitCalDavUri; + _isCalDavDiscoveryAttempted = true; + return _cachedCalDavServiceUri; + } + + if (_cachedCalDavServiceUri != null) + return _cachedCalDavServiceUri; + + if (_isCalDavDiscoveryAttempted) + return null; + + await _calDavDiscoveryLock.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + if (_cachedCalDavServiceUri != null) + return _cachedCalDavServiceUri; + + if (_isCalDavDiscoveryAttempted) + return null; + + _isCalDavDiscoveryAttempted = true; + + var emailCandidates = new[] { - Log.Warning($"{Account.Name} does not have an Inbox folder for idle client to track. Ignored."); - return; + Account.ServerInformation?.Address, + Account.Address + } + .Where(value => !string.IsNullOrWhiteSpace(value) && value.Contains('@')) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + foreach (var email in emailCandidates) + { + var discoveredUri = await _autoDiscoveryService + .DiscoverCalDavServiceUriAsync(email, cancellationToken) + .ConfigureAwait(false); + + if (discoveredUri == null) + continue; + + _cachedCalDavServiceUri = discoveredUri; + return _cachedCalDavServiceUri; } - // Setup idle client. - idleClient = client; + if (Account.SpecialImapProvider == SpecialImapProvider.iCloud) + { + _cachedCalDavServiceUri = new Uri("https://caldav.icloud.com/"); + return _cachedCalDavServiceUri; + } - idleDoneTokenSource ??= new CancellationTokenSource(); - idleCancellationTokenSource ??= new CancellationTokenSource(); + if (Account.SpecialImapProvider == SpecialImapProvider.Yahoo) + { + _cachedCalDavServiceUri = new Uri("https://caldav.calendar.yahoo.com/"); + return _cachedCalDavServiceUri; + } - inboxFolder = client.Inbox; - - await inboxFolder.OpenAsync(FolderAccess.ReadOnly, idleCancellationTokenSource.Token); - - inboxFolder.CountChanged += IdleNotificationTriggered; - inboxFolder.MessageFlagsChanged += IdleNotificationTriggered; - inboxFolder.MessageExpunged += IdleNotificationTriggered; - inboxFolder.MessagesVanished += IdleNotificationTriggered; - - Log.Debug("Starting an idle client for {Name}", Account.Name); - - await client.IdleAsync(idleDoneTokenSource.Token, idleCancellationTokenSource.Token); - } - catch (ImapProtocolException protocolException) - { - Log.Information(protocolException, "Idle client received protocol exception."); - reconnect = true; - } - catch (IOException ioException) - { - Log.Information(ioException, "Idle client received IO exception."); - reconnect = true; - } - catch (OperationCanceledException) - { - reconnect = !IsDisposing; - } - catch (Exception ex) - { - Log.Error(ex, "Idle client failed to start."); - reconnect = false; + return null; } finally { - if (inboxFolder != null) + _calDavDiscoveryLock.Release(); + } + } + + private string ResolveCalDavPassword() + { + if (!string.IsNullOrWhiteSpace(Account.ServerInformation?.CalDavPassword)) + return Account.ServerInformation.CalDavPassword; + + if (!string.IsNullOrWhiteSpace(Account.ServerInformation?.IncomingServerPassword)) + return Account.ServerInformation.IncomingServerPassword; + + if (!string.IsNullOrWhiteSpace(Account.ServerInformation?.OutgoingServerPassword)) + return Account.ServerInformation.OutgoingServerPassword; + + return string.Empty; + } + + private string ResolveCalDavUsername() + { + if (!string.IsNullOrWhiteSpace(Account.ServerInformation?.CalDavUsername)) + return Account.ServerInformation.CalDavUsername.Trim(); + + if (!string.IsNullOrWhiteSpace(Account.ServerInformation?.Address)) + return Account.ServerInformation.Address.Trim(); + + if (!string.IsNullOrWhiteSpace(Account.Address)) + return Account.Address.Trim(); + + return string.Empty; + } + + private Uri TryGetExplicitCalDavServiceUri() + { + var configuredUrl = Account.ServerInformation?.CalDavServiceUrl; + if (string.IsNullOrWhiteSpace(configuredUrl)) + return null; + + if (!Uri.TryCreate(configuredUrl, UriKind.Absolute, out var uri)) + { + _logger.Warning("Configured CalDAV URL is invalid for account {AccountName}: {Url}", Account.Name, configuredUrl); + return null; + } + + return uri; + } + + private async Task SynchronizeCalendarMetadataAsync(IReadOnlyList remoteCalendars) + { + var localCalendars = await _imapChangeProcessor.GetAccountCalendarsAsync(Account.Id).ConfigureAwait(false); + var remoteCalendarsById = remoteCalendars + .GroupBy(c => c.RemoteCalendarId, StringComparer.OrdinalIgnoreCase) + .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase); + var usedCalendarColors = new HashSet(StringComparer.OrdinalIgnoreCase); + + var remotePrimaryCalendarId = remoteCalendars.FirstOrDefault()?.RemoteCalendarId; + + foreach (var localCalendar in localCalendars.ToList()) + { + if (remoteCalendarsById.ContainsKey(localCalendar.RemoteCalendarId)) + continue; + + await _imapChangeProcessor + .DeleteCalendarIcsForCalendarAsync(Account.Id, localCalendar.Id) + .ConfigureAwait(false); + await _imapChangeProcessor.DeleteAccountCalendarAsync(localCalendar).ConfigureAwait(false); + localCalendars.Remove(localCalendar); + } + + foreach (var remoteCalendar in remoteCalendars) + { + var existingLocal = localCalendars.FirstOrDefault(c => + string.Equals(c.RemoteCalendarId, remoteCalendar.RemoteCalendarId, StringComparison.OrdinalIgnoreCase)); + + var isPrimary = string.Equals(remoteCalendar.RemoteCalendarId, remotePrimaryCalendarId, StringComparison.OrdinalIgnoreCase); + + if (existingLocal == null) { - inboxFolder.CountChanged -= IdleNotificationTriggered; - inboxFolder.MessageFlagsChanged -= IdleNotificationTriggered; - inboxFolder.MessageExpunged -= IdleNotificationTriggered; - inboxFolder.MessagesVanished -= IdleNotificationTriggered; + var newCalendar = new AccountCalendar + { + Id = Guid.NewGuid(), + AccountId = Account.Id, + RemoteCalendarId = remoteCalendar.RemoteCalendarId, + Name = remoteCalendar.Name, + IsPrimary = isPrimary, + IsSynchronizationEnabled = true, + IsExtended = true, + BackgroundColorHex = ColorHelpers.GetDistinctFlatColorHex(usedCalendarColors), + TimeZone = "UTC", + SynchronizationDeltaToken = string.Empty + }; + + newCalendar.TextColorHex = ColorHelpers.GetReadableTextColorHex(newCalendar.BackgroundColorHex); + usedCalendarColors.Add(newCalendar.BackgroundColorHex); + await _imapChangeProcessor.InsertAccountCalendarAsync(newCalendar).ConfigureAwait(false); + continue; } - if (idleDoneTokenSource != null) + var resolvedColor = ColorHelpers.GetDistinctFlatColorHex(usedCalendarColors, existingLocal.BackgroundColorHex); + var shouldUpdate = !string.Equals(existingLocal.Name, remoteCalendar.Name, StringComparison.Ordinal) + || existingLocal.IsPrimary != isPrimary + || !string.Equals(existingLocal.BackgroundColorHex, resolvedColor, StringComparison.OrdinalIgnoreCase); + + if (!shouldUpdate) { - idleDoneTokenSource.Dispose(); - idleDoneTokenSource = null; + usedCalendarColors.Add(resolvedColor); + continue; } - if (idleClient != null) - { - // Killing the client is not necessary. We can re-use it later. - _clientPool.Release(idleClient, destroyClient: false); + existingLocal.Name = remoteCalendar.Name; + existingLocal.IsPrimary = isPrimary; + existingLocal.BackgroundColorHex = resolvedColor; + existingLocal.TextColorHex = ColorHelpers.GetReadableTextColorHex(existingLocal.BackgroundColorHex); + usedCalendarColors.Add(existingLocal.BackgroundColorHex); + await _imapChangeProcessor.UpdateAccountCalendarAsync(existingLocal).ConfigureAwait(false); + } + } - idleClient = null; + private interface IImapCalendarOperationHandler + { + bool RequiresConnectedClient { get; } + Task CreateCalendarEventAsync(CreateCalendarEventRequest request); + Task UpdateCalendarEventAsync(UpdateCalendarEventRequest request); + Task DeleteCalendarEventAsync(DeleteCalendarEventRequest request); + Task AcceptEventAsync(AcceptEventRequest request); + Task DeclineEventAsync(DeclineEventRequest request); + Task TentativeEventAsync(TentativeEventRequest request); + } + + private class LocalCalendarOperationHandler : IImapCalendarOperationHandler + { + private readonly MailAccount _account; + private readonly IImapChangeProcessor _changeProcessor; + private readonly ICalendarService _calendarService; + private readonly string _applicationDataFolderPath; + private readonly string _resourceScheme; + + public bool RequiresConnectedClient => false; + + public LocalCalendarOperationHandler(MailAccount account, IImapChangeProcessor changeProcessor, ICalendarService calendarService, string applicationDataFolderPath, string resourceScheme) + { + _account = account; + _changeProcessor = changeProcessor; + _calendarService = calendarService; + _applicationDataFolderPath = applicationDataFolderPath; + _resourceScheme = resourceScheme; + } + + public async Task CreateCalendarEventAsync(CreateCalendarEventRequest request) + { + var item = request.PreparedItem; + var attendees = request.PreparedEvent.Attendees; + var reminders = request.PreparedEvent.Reminders; + EnsureCalendarItemDefaults(item, _account, "local"); + item.AssignedCalendar ??= await _calendarService.GetAccountCalendarAsync(item.CalendarId).ConfigureAwait(false); + + var existing = await _calendarService.GetCalendarItemAsync(item.Id).ConfigureAwait(false); + + if (existing == null) + await _calendarService.CreateNewCalendarItemAsync(item, attendees).ConfigureAwait(false); + else + await _calendarService.UpdateCalendarItemAsync(item, attendees).ConfigureAwait(false); + + await _calendarService.SaveRemindersAsync(item.Id, reminders).ConfigureAwait(false); + await SaveAttachmentsAsync(request.ComposeResult, item.Id).ConfigureAwait(false); + await PersistIcsAsync(item, attendees).ConfigureAwait(false); + } + + public async Task UpdateCalendarEventAsync(UpdateCalendarEventRequest request) + { + var item = request.Item; + EnsureCalendarItemDefaults(item, _account, "local"); + item.AssignedCalendar ??= await _calendarService.GetAccountCalendarAsync(item.CalendarId).ConfigureAwait(false); + + var attendees = request.Attendees ?? await _calendarService.GetAttendeesAsync(item.Id).ConfigureAwait(false); + + await _calendarService.UpdateCalendarItemAsync(item, attendees).ConfigureAwait(false); + await PersistIcsAsync(item, attendees).ConfigureAwait(false); + } + + public Task DeleteCalendarEventAsync(DeleteCalendarEventRequest request) + => _changeProcessor.DeleteCalendarItemAsync(request.Item.Id); + + public async Task AcceptEventAsync(AcceptEventRequest request) + { + request.Item.Status = CalendarItemStatus.Accepted; + await UpdateStatusAsync(request.Item).ConfigureAwait(false); + } + + public async Task DeclineEventAsync(DeclineEventRequest request) + { + request.Item.Status = CalendarItemStatus.Cancelled; + await UpdateStatusAsync(request.Item).ConfigureAwait(false); + } + + public async Task TentativeEventAsync(TentativeEventRequest request) + { + request.Item.Status = CalendarItemStatus.Tentative; + await UpdateStatusAsync(request.Item).ConfigureAwait(false); + } + + private async Task UpdateStatusAsync(CalendarItem item) + { + EnsureCalendarItemDefaults(item, _account, "local"); + item.AssignedCalendar ??= await _calendarService.GetAccountCalendarAsync(item.CalendarId).ConfigureAwait(false); + + var attendees = await _calendarService.GetAttendeesAsync(item.Id).ConfigureAwait(false); + await _calendarService.UpdateCalendarItemAsync(item, attendees).ConfigureAwait(false); + await PersistIcsAsync(item, attendees).ConfigureAwait(false); + } + + private Task PersistIcsAsync(CalendarItem item, List attendees) + { + var resourceHref = $"{_resourceScheme}://calendar/{item.CalendarId:N}/{item.Id:N}"; + var icsContent = BuildIcsContent(item, attendees); + + return _changeProcessor.SaveCalendarItemIcsAsync( + _account.Id, + item.CalendarId, + item.Id, + item.RemoteEventId, + resourceHref, + DateTimeOffset.UtcNow.ToString("O"), + icsContent); + } + + private async Task SaveAttachmentsAsync(CalendarEventComposeResult composeResult, Guid calendarItemId) + { + await _calendarService.DeleteAttachmentsAsync(calendarItemId).ConfigureAwait(false); + + var attachments = composeResult?.Attachments; + if (attachments == null || attachments.Count == 0) + return; + + var attachmentsRoot = Path.Combine(_applicationDataFolderPath, "CalendarAttachments", calendarItemId.ToString("N")); + Directory.CreateDirectory(attachmentsRoot); + + var storedAttachments = new List(); + + foreach (var attachment in attachments.Where(a => !string.IsNullOrWhiteSpace(a.FilePath) && File.Exists(a.FilePath))) + { + var fileName = string.IsNullOrWhiteSpace(attachment.FileName) ? Path.GetFileName(attachment.FilePath) : attachment.FileName; + var destinationPath = Path.Combine(attachmentsRoot, fileName); + File.Copy(attachment.FilePath, destinationPath, overwrite: true); + + storedAttachments.Add(new CalendarAttachment + { + Id = Guid.NewGuid(), + CalendarItemId = calendarItemId, + RemoteAttachmentId = attachment.Id.ToString("N"), + FileName = fileName, + Size = attachment.Size, + ContentType = MimeTypes.GetMimeType(fileName), + IsDownloaded = true, + LocalFilePath = destinationPath, + LastModified = DateTimeOffset.UtcNow + }); } - if (reconnect == true) + if (storedAttachments.Count > 0) { - Log.Information("Idle client is reconnecting."); - - _ = StartIdleClientAsync(); - } - else if (reconnect == false) - { - Log.Information("Finalized idle client."); + await _calendarService.InsertOrReplaceAttachmentsAsync(storedAttachments).ConfigureAwait(false); } } } + private sealed class CalDavCalendarOperationHandler : IImapCalendarOperationHandler + { + private readonly ImapSynchronizer _owner; + private readonly MailAccount _account; + private readonly ICalendarService _calendarService; + private readonly ICalDavClient _calDavClient; + + public bool RequiresConnectedClient => false; + + public CalDavCalendarOperationHandler( + ImapSynchronizer owner, + MailAccount account, + ICalendarService calendarService, + ICalDavClient calDavClient) + { + _owner = owner; + _account = account; + _calendarService = calendarService; + _calDavClient = calDavClient; + } + + public Task CreateCalendarEventAsync(CreateCalendarEventRequest request) + => UpsertCalendarEventAsync(request.PreparedItem, request.PreparedEvent.Attendees); + + public Task UpdateCalendarEventAsync(UpdateCalendarEventRequest request) + => UpsertCalendarEventAsync(request.Item, request.Attendees); + + public async Task DeleteCalendarEventAsync(DeleteCalendarEventRequest request) + { + var (connection, calendar) = await ResolveCalDavContextAsync(request.Item.CalendarId).ConfigureAwait(false); + if (string.IsNullOrWhiteSpace(request.Item?.RemoteEventId)) + { + throw new InvalidOperationException("Cannot delete CalDAV event because remote event ID is missing."); + } + + await _calDavClient + .DeleteCalendarEventAsync(connection, calendar, request.Item.RemoteEventId.GetProviderRemoteEventId()) + .ConfigureAwait(false); + } + + public Task AcceptEventAsync(AcceptEventRequest request) + { + request.Item.Status = CalendarItemStatus.Accepted; + return UpsertCalendarEventAsync(request.Item, null); + } + + public Task DeclineEventAsync(DeclineEventRequest request) + { + request.Item.Status = CalendarItemStatus.Cancelled; + return UpsertCalendarEventAsync(request.Item, null); + } + + public Task TentativeEventAsync(TentativeEventRequest request) + { + request.Item.Status = CalendarItemStatus.Tentative; + return UpsertCalendarEventAsync(request.Item, null); + } + + private async Task UpsertCalendarEventAsync(CalendarItem item, List attendees) + { + EnsureCalendarItemDefaults(item, _account, "caldav"); + + if (attendees == null) + { + attendees = await _calendarService.GetAttendeesAsync(item.Id).ConfigureAwait(false); + } + + var (connection, calendar) = await ResolveCalDavContextAsync(item.CalendarId).ConfigureAwait(false); + var icsContent = BuildIcsContent(item, attendees); + + await _calDavClient + .UpsertCalendarEventAsync(connection, calendar, item.RemoteEventId.GetProviderRemoteEventId(), icsContent) + .ConfigureAwait(false); + } + + private async Task<(CalDavConnectionSettings Connection, CalDavCalendar Calendar)> ResolveCalDavContextAsync(Guid calendarId) + { + var assignedCalendar = await _calendarService.GetAccountCalendarAsync(calendarId).ConfigureAwait(false); + if (assignedCalendar == null || string.IsNullOrWhiteSpace(assignedCalendar.RemoteCalendarId)) + { + throw new InvalidOperationException("Cannot execute CalDAV operation because the target calendar has no remote ID."); + } + + var serviceUri = await _owner.ResolveCalDavServiceUriAsync(CancellationToken.None).ConfigureAwait(false); + if (serviceUri == null) + { + throw new InvalidOperationException("Cannot execute CalDAV operation because no CalDAV service URI is configured."); + } + + var username = _owner.ResolveCalDavUsername(); + var password = _owner.ResolveCalDavPassword(); + + if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) + { + throw new InvalidOperationException("Cannot execute CalDAV operation because credentials are missing."); + } + + var connection = new CalDavConnectionSettings + { + ServiceUri = serviceUri, + Username = username, + Password = password + }; + + var remoteCalendar = new CalDavCalendar + { + RemoteCalendarId = assignedCalendar.RemoteCalendarId, + Name = assignedCalendar.Name + }; + + return (connection, remoteCalendar); + } + } + + private static void EnsureCalendarItemDefaults(CalendarItem item, MailAccount account, string idPrefix) + { + if (item == null) + throw new ArgumentNullException(nameof(item)); + + if (item.Id == Guid.Empty) + item.Id = Guid.NewGuid(); + + if (string.IsNullOrWhiteSpace(item.RemoteEventId)) + item.RemoteEventId = $"{idPrefix}-{item.Id:N}"; + + if (item.CreatedAt == default) + item.CreatedAt = DateTimeOffset.UtcNow; + + item.UpdatedAt = DateTimeOffset.UtcNow; + item.OrganizerDisplayName ??= account?.SenderName ?? string.Empty; + item.OrganizerEmail ??= account?.Address ?? string.Empty; + item.StartTimeZone ??= TimeZoneInfo.Local.Id; + item.EndTimeZone ??= item.StartTimeZone; + } + + private static string BuildIcsContent(CalendarItem item, List attendees) + { + var uid = item.RemoteEventId?.Split(new[] { "::" }, StringSplitOptions.None)[0] ?? item.Id.ToString("N"); + var dtStamp = DateTimeOffset.UtcNow.ToString("yyyyMMdd'T'HHmmss'Z'"); + + var lines = new List + { + "BEGIN:VCALENDAR", + "VERSION:2.0", + "PRODID:-//Wino Mail//Calendar//EN", + "CALSCALE:GREGORIAN", + "BEGIN:VEVENT", + $"UID:{EscapeIcs(uid)}", + $"DTSTAMP:{dtStamp}", + }; + + if (item.IsAllDayEvent) + { + lines.Add($"DTSTART;VALUE=DATE:{item.StartDate:yyyyMMdd}"); + lines.Add($"DTEND;VALUE=DATE:{item.EndDate:yyyyMMdd}"); + } + else + { + var startUtc = ConvertEventTimeToUtc(item.StartDate, item.StartTimeZone); + var endUtc = ConvertEventTimeToUtc(item.EndDate, item.EndTimeZone ?? item.StartTimeZone); + + lines.Add($"DTSTART:{startUtc:yyyyMMdd'T'HHmmss'Z'}"); + lines.Add($"DTEND:{endUtc:yyyyMMdd'T'HHmmss'Z'}"); + } + + if (!string.IsNullOrWhiteSpace(item.Title)) + lines.Add($"SUMMARY:{EscapeIcs(item.Title)}"); + + if (!string.IsNullOrWhiteSpace(item.Description)) + lines.Add($"DESCRIPTION:{EscapeIcs(item.Description)}"); + + if (!string.IsNullOrWhiteSpace(item.Location)) + lines.Add($"LOCATION:{EscapeIcs(item.Location)}"); + + lines.Add($"STATUS:{MapStatus(item.Status)}"); + lines.Add($"TRANSP:{(item.ShowAs == CalendarItemShowAs.Free ? "TRANSPARENT" : "OPAQUE")}"); + lines.Add($"CLASS:{MapVisibility(item.Visibility)}"); + + if (!string.IsNullOrWhiteSpace(item.Recurrence)) + { + var recurrenceLines = item.Recurrence + .Split(Wino.Core.Domain.Constants.CalendarEventRecurrenceRuleSeperator, StringSplitOptions.RemoveEmptyEntries) + .Select(l => l.Trim()) + .Where(l => !string.IsNullOrWhiteSpace(l)); + + lines.AddRange(recurrenceLines); + } + + if (!string.IsNullOrWhiteSpace(item.OrganizerEmail)) + { + var organizerName = string.IsNullOrWhiteSpace(item.OrganizerDisplayName) + ? item.OrganizerEmail + : item.OrganizerDisplayName; + lines.Add($"ORGANIZER;CN={EscapeIcs(organizerName)}:mailto:{EscapeIcs(item.OrganizerEmail)}"); + } + + if (attendees != null) + { + foreach (var attendee in attendees.Where(a => !string.IsNullOrWhiteSpace(a.Email))) + { + var role = attendee.IsOptionalAttendee ? "OPT-PARTICIPANT" : "REQ-PARTICIPANT"; + var partStat = attendee.AttendenceStatus switch + { + AttendeeStatus.Accepted => "ACCEPTED", + AttendeeStatus.Declined => "DECLINED", + AttendeeStatus.Tentative => "TENTATIVE", + _ => "NEEDS-ACTION" + }; + + var cn = string.IsNullOrWhiteSpace(attendee.Name) ? attendee.Email : attendee.Name; + lines.Add($"ATTENDEE;CN={EscapeIcs(cn)};ROLE={role};PARTSTAT={partStat}:mailto:{EscapeIcs(attendee.Email)}"); + } + } + + lines.Add("END:VEVENT"); + lines.Add("END:VCALENDAR"); + + return string.Join(Environment.NewLine, lines); + } + + private static DateTime ConvertEventTimeToUtc(DateTime eventDateTime, string eventTimeZoneId) + { + if (string.IsNullOrWhiteSpace(eventTimeZoneId)) + return eventDateTime.ToUniversalTime(); + + try + { + var eventTimeZone = TimeZoneInfo.FindSystemTimeZoneById(eventTimeZoneId); + var unspecifiedDateTime = DateTime.SpecifyKind(eventDateTime, DateTimeKind.Unspecified); + return TimeZoneInfo.ConvertTimeToUtc(unspecifiedDateTime, eventTimeZone); + } + catch + { + return eventDateTime.ToUniversalTime(); + } + } + + private static string EscapeIcs(string value) + { + if (string.IsNullOrEmpty(value)) + return string.Empty; + + return value + .Replace("\\", "\\\\", StringComparison.Ordinal) + .Replace(";", "\\;", StringComparison.Ordinal) + .Replace(",", "\\,", StringComparison.Ordinal) + .Replace("\r\n", "\\n", StringComparison.Ordinal) + .Replace("\n", "\\n", StringComparison.Ordinal); + } + + private static string MapStatus(CalendarItemStatus status) + { + return status switch + { + CalendarItemStatus.Cancelled => "CANCELLED", + CalendarItemStatus.Tentative => "TENTATIVE", + _ => "CONFIRMED" + }; + } + + private static string MapVisibility(CalendarItemVisibility visibility) + { + return visibility switch + { + CalendarItemVisibility.Public => "PUBLIC", + CalendarItemVisibility.Private => "PRIVATE", + CalendarItemVisibility.Confidential => "CONFIDENTIAL", + _ => "PUBLIC" + }; + } + + public Task StartIdleClientAsync() + { + if (IsDisposing) + return Task.CompletedTask; + + if (_idleLoopTask != null && !_idleLoopTask.IsCompleted) + return Task.CompletedTask; + + _idleLoopCancellationTokenSource = new CancellationTokenSource(); + _idleLoopTask = RunIdleLoopAsync(_idleLoopCancellationTokenSource.Token); + + return Task.CompletedTask; + } + + private async Task RunIdleLoopAsync(CancellationToken cancellationToken) + { + int reconnectAttempt = 0; + + while (!cancellationToken.IsCancellationRequested && !IsDisposing) + { + IImapClient idleClient = null; + IMailFolder inboxFolder = null; + bool shouldReconnect = false; + + try + { + idleClient = await _clientPool.GetIdleClientAsync(cancellationToken).ConfigureAwait(false); + + if (idleClient == null) + { + _logger.Warning("Dedicated IDLE client could not be allocated for {AccountName}.", Account.Name); + return; + } + + if (!idleClient.Capabilities.HasFlag(ImapCapabilities.Idle)) + { + _logger.Information("{AccountName} does not support IMAP IDLE. Automatic updates rely on global sync interval.", Account.Name); + return; + } + + if (idleClient.Inbox == null) + { + _logger.Warning("{AccountName} does not expose Inbox for IDLE listening.", Account.Name); + return; + } + + inboxFolder = idleClient.Inbox; + + await inboxFolder.OpenAsync(FolderAccess.ReadOnly, cancellationToken).ConfigureAwait(false); + + _lastIdleInboxCount = inboxFolder.Count; + inboxFolder.CountChanged += IdleInboxCountChanged; + + reconnectAttempt = 0; + _logger.Debug("Started dedicated IDLE loop for {AccountName}.", Account.Name); + + while (!cancellationToken.IsCancellationRequested && !IsDisposing && idleClient.IsConnected) + { + using var idleDoneTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(9)); + await idleClient.IdleAsync(idleDoneTokenSource.Token, cancellationToken).ConfigureAwait(false); + } + } + catch (ImapProtocolException protocolException) + { + _logger.Information(protocolException, "Idle client received protocol exception for {AccountName}.", Account.Name); + shouldReconnect = true; + } + catch (IOException ioException) + { + _logger.Information(ioException, "Idle client received IO exception for {AccountName}.", Account.Name); + shouldReconnect = true; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested || IsDisposing) + { + break; + } + catch (OperationCanceledException) + { + shouldReconnect = true; + } + catch (Exception ex) + { + _logger.Error(ex, "Idle client loop failed for {AccountName}.", Account.Name); + shouldReconnect = true; + } + finally + { + if (inboxFolder != null) + { + inboxFolder.CountChanged -= IdleInboxCountChanged; + + if (inboxFolder.IsOpen && !cancellationToken.IsCancellationRequested) + { + await inboxFolder.CloseAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + } + } + + _clientPool.ReleaseIdleClient(isFaulted: shouldReconnect); + } + + if (!shouldReconnect) + { + break; + } + + reconnectAttempt++; + var reconnectDelay = GetIdleReconnectDelay(reconnectAttempt); + _logger.Information("Reconnecting IDLE client for {AccountName} in {Delay}.", Account.Name, reconnectDelay); + + try + { + await Task.Delay(reconnectDelay, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + break; + } + } + } + + private static TimeSpan GetIdleReconnectDelay(int attempt) + { + var backoffSeconds = Math.Min(60, Math.Pow(2, Math.Min(attempt, 6))); + int jitterMs; + + lock (IdleReconnectJitter) + { + jitterMs = IdleReconnectJitter.Next(250, 1250); + } + + return TimeSpan.FromSeconds(backoffSeconds) + TimeSpan.FromMilliseconds(jitterMs); + } + private void RequestIdleChangeSynchronization() { - Debug.WriteLine("Detected idle change."); - - // We don't really need to act on the count change in detail. - // Our synchronization should be enough to handle the changes with on-demand sync. - // We can just trigger a sync here IMAPIdle type. + if (!ShouldTriggerIdleSynchronization(DateTime.UtcNow)) + return; var options = new MailSynchronizationOptions() { @@ -857,18 +2069,60 @@ public class ImapSynchronizer : WinoSynchronizer RequestIdleChangeSynchronization(); - - public Task StopIdleClientAsync() + internal bool ShouldTriggerIdleSynchronization(DateTime nowUtc) { - idleDoneTokenSource?.Cancel(); - idleCancellationTokenSource?.Cancel(); + lock (_idleDebounceLock) + { + if (nowUtc - _lastIdleSyncRequestUtc < _idleSyncDebounceWindow) + { + return false; + } - return Task.CompletedTask; + _lastIdleSyncRequestUtc = nowUtc; + return true; + } + } + + private void IdleInboxCountChanged(object sender, EventArgs e) + { + if (sender is not IMailFolder inboxFolder) + return; + + var currentCount = inboxFolder.Count; + var previousCount = _lastIdleInboxCount; + _lastIdleInboxCount = currentCount; + + if (currentCount > previousCount) + { + RequestIdleChangeSynchronization(); + } + } + + public async Task StopIdleClientAsync() + { + if (_idleLoopCancellationTokenSource != null) + { + _idleLoopCancellationTokenSource.Cancel(); + } + + if (_idleLoopTask != null) + { + try + { + await _idleLoopTask.ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // no-op + } + } + + _idleLoopCancellationTokenSource?.Dispose(); + _idleLoopCancellationTokenSource = null; + _idleLoopTask = null; } public override async Task KillSynchronizerAsync() @@ -882,3 +2136,5 @@ public class ImapSynchronizer : WinoSynchronizer _clientPool.PreWarmPoolAsync(); } + + diff --git a/Wino.Core/Synchronizers/OutlookSynchronizer.cs b/Wino.Core/Synchronizers/OutlookSynchronizer.cs index a2ab4bd9..445db927 100644 --- a/Wino.Core/Synchronizers/OutlookSynchronizer.cs +++ b/Wino.Core/Synchronizers/OutlookSynchronizer.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using System.Net; using System.Net.Http; using System.Text; using System.Text.Json; @@ -13,47 +11,70 @@ using System.Text.Json.Serialization; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using CommunityToolkit.Mvvm.Messaging; using Microsoft.Graph; +using Microsoft.Graph.Me.MailFolders.Item.Messages.Delta; using Microsoft.Graph.Models; using Microsoft.Graph.Models.ODataErrors; using Microsoft.Kiota.Abstractions; using Microsoft.Kiota.Abstractions.Authentication; -using Microsoft.Kiota.Abstractions.Serialization; -using Microsoft.Kiota.Http.HttpClientLibrary.Middleware; -using Microsoft.Kiota.Http.HttpClientLibrary.Middleware.Options; using MimeKit; using MoreLinq.Extensions; using Serilog; +using Wino.Core.Domain; using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Exceptions; +using Wino.Core.Domain.Extensions; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Accounts; -using Wino.Core.Domain.Models.Errors; using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Synchronization; using Wino.Core.Extensions; using Wino.Core.Http; +using Wino.Core.Helpers; using Wino.Core.Integration.Processors; using Wino.Core.Misc; using Wino.Core.Requests.Bundles; +using Wino.Core.Requests.Calendar; using Wino.Core.Requests.Folder; using Wino.Core.Requests.Mail; +using Wino.Messaging.UI; namespace Wino.Core.Synchronizers.Mail; [JsonSerializable(typeof(Microsoft.Graph.Me.Messages.Item.Move.MovePostRequestBody))] -[JsonSerializable(typeof(OutlookFileAttachment))] public partial class OutlookSynchronizerJsonContext : JsonSerializerContext; +/// +/// Outlook synchronizer implementation with delta token synchronization. +/// +/// SYNCHRONIZATION STRATEGY: +/// - Uses delta API for both initial and incremental sync +/// - Initial sync: Downloads last 30 days of emails with metadata only +/// - Incremental sync: Uses delta token to get only changes since last sync +/// - Messages are downloaded with metadata only (no MIME content during sync) +/// - MIME files are downloaded on-demand when user explicitly reads a message +/// +/// Key implementation details: +/// - SynchronizeFolderAsync: Main entry point for per-folder synchronization +/// - DownloadMailsForInitialSyncAsync: Downloads last 30 days using delta API with filter +/// - ProcessDeltaChangesAsync: Processes incremental changes using delta token +/// - DownloadMessageMetadataBatchAsync: Downloads metadata in batches using Graph batch API +/// - CreateMailCopyFromMessageAsync: Creates MailCopy from Message metadata +/// - DownloadMissingMimeMessageAsync: Downloads raw MIME only when explicitly requested +/// public class OutlookSynchronizer : WinoSynchronizer { public override uint BatchModificationSize => 20; - public override uint InitialMessageDownloadCountPerFolder => 250; + public override uint InitialMessageDownloadCountPerFolder => 1000; private const uint MaximumAllowedBatchRequestSize = 20; + private const int SimpleAttachmentUploadLimitBytes = 3 * 1024 * 1024; + private const int MaximumUploadSessionAttachmentSizeBytes = 150 * 1024 * 1024; + private const int LargeAttachmentUploadChunkSizeBytes = 320 * 1024; private const string INBOX_NAME = "inbox"; private const string SENT_NAME = "sentitems"; @@ -75,22 +96,32 @@ public class OutlookSynchronizer : WinoSynchronizer(); private readonly IOutlookChangeProcessor _outlookChangeProcessor; private readonly GraphServiceClient _graphClient; private readonly IOutlookSynchronizerErrorHandlerFactory _errorHandlingFactory; + private bool _isFolderStructureChanged; + + private readonly SemaphoreSlim _concurrentDownloadSemaphore = new(10); // Limit to 10 concurrent downloads public OutlookSynchronizer(MailAccount account, IAuthenticator authenticator, IOutlookChangeProcessor outlookChangeProcessor, - IOutlookSynchronizerErrorHandlerFactory errorHandlingFactory) : base(account) + IOutlookSynchronizerErrorHandlerFactory errorHandlingFactory) : base(account, WeakReferenceMessenger.Default) { var tokenProvider = new MicrosoftTokenProvider(Account, authenticator); @@ -98,14 +129,7 @@ public class OutlookSynchronizer : WinoSynchronizer a is RetryHandler); - if (existingRetryHandler != null) - handlers.Remove(existingRetryHandler); - - // Add custom one. - handlers.Add(GetRetryHandler()); + handlers.Add(GetGraphRateLimitHandler()); var httpClient = GraphClientFactory.Create(handlers); _graphClient = new GraphServiceClient(httpClient, new BaseBearerTokenAuthenticationProvider(tokenProvider)); @@ -118,29 +142,7 @@ public class OutlookSynchronizer : WinoSynchronizer new(); - private RetryHandler GetRetryHandler() - { - var options = new RetryHandlerOption() - { - ShouldRetry = (delay, attempt, httpResponse) => - { - var statusCode = httpResponse.StatusCode; - - return statusCode switch - { - HttpStatusCode.ServiceUnavailable => true, - HttpStatusCode.GatewayTimeout => true, - (HttpStatusCode)429 => true, - HttpStatusCode.Unauthorized => true, - _ => false - }; - }, - Delay = 3, - MaxRetry = 3 - }; - - return new RetryHandler(options); - } + private GraphRateLimitHandler GetGraphRateLimitHandler() => new(); #endregion @@ -148,13 +150,15 @@ public class OutlookSynchronizer : WinoSynchronizer SynchronizeMailsInternalAsync(MailSynchronizationOptions options, CancellationToken cancellationToken = default) { var downloadedMessageIds = new List(); + var folderResults = new List(); _logger.Information("Internal synchronization started for {Name}", Account.Name); _logger.Information("Options: {Options}", options); try { - PublishSynchronizationProgress(1); + // Set indeterminate progress initially + UpdateSyncProgress(0, 0, "Synchronizing folders..."); await SynchronizeFoldersAsync(cancellationToken).ConfigureAwait(false); @@ -164,40 +168,115 @@ public class OutlookSynchronizer : WinoSynchronizer a.FolderName)), synchronizationFolders.Count)); - for (int i = 0; i < synchronizationFolders.Count; i++) + var totalFolders = synchronizationFolders.Count; + + for (int i = 0; i < totalFolders; i++) { var folder = synchronizationFolders[i]; - var progress = (int)Math.Round((double)(i + 1) / synchronizationFolders.Count * 100); - PublishSynchronizationProgress(progress); + // Update progress based on folder completion + var progressPercentage = (int)Math.Round((double)(i + 1) / totalFolders * 100); + var statusMessage = string.Format(Translator.Sync_SynchronizingFolder, folder.FolderName, progressPercentage); + UpdateSyncProgress(totalFolders, totalFolders - (i + 1), statusMessage); - var folderDownloadedMessageIds = await SynchronizeFolderAsync(folder, cancellationToken).ConfigureAwait(false); - downloadedMessageIds.AddRange(folderDownloadedMessageIds); + try + { + var folderDownloadedMessageIds = await SynchronizeFolderAsync(folder, cancellationToken).ConfigureAwait(false); + downloadedMessageIds.AddRange(folderDownloadedMessageIds); + + folderResults.Add(FolderSyncResult.Successful(folder.Id, folder.FolderName, folderDownloadedMessageIds.Count())); + } + catch (OperationCanceledException) + { + // Cancellation should stop the entire sync + throw; + } + catch (ODataError odataError) + { + // Handle OData errors - determine if we should continue or stop + var errorContext = new SynchronizerErrorContext + { + Account = Account, + ErrorCode = (int?)odataError.ResponseStatusCode, + ErrorMessage = odataError.Error?.Message ?? odataError.Message, + Exception = odataError, + FolderId = folder.Id, + FolderName = folder.FolderName, + OperationType = "FolderSync" + }; + + var handled = await _errorHandlingFactory.HandleErrorAsync(errorContext).ConfigureAwait(false); + + if (errorContext.CanContinueSync) + { + _logger.Warning("Folder {FolderName} sync failed with recoverable error, continuing with other folders. Error: {Error}", + folder.FolderName, odataError.Error?.Message); + folderResults.Add(FolderSyncResult.Failed(folder.Id, folder.FolderName, errorContext)); + } + else + { + _logger.Error(odataError, "Folder {FolderName} sync failed with fatal error, stopping sync", folder.FolderName); + folderResults.Add(FolderSyncResult.Failed(folder.Id, folder.FolderName, errorContext)); + throw; + } + } + catch (Exception ex) + { + // For unexpected exceptions, try to classify and decide if we should continue + var errorContext = new SynchronizerErrorContext + { + Account = Account, + ErrorMessage = ex.Message, + Exception = ex, + FolderId = folder.Id, + FolderName = folder.FolderName, + OperationType = "FolderSync", + Severity = SynchronizerErrorSeverity.Recoverable, // Default to recoverable for individual folders + Category = SynchronizerErrorCategory.Unknown + }; + + _logger.Warning(ex, "Folder {FolderName} sync failed, continuing with other folders", folder.FolderName); + folderResults.Add(FolderSyncResult.Failed(folder.Id, folder.FolderName, errorContext)); + } } } } + catch (OperationCanceledException) + { + _logger.Information("Synchronization was canceled for {Name}", Account.Name); + return MailSynchronizationResult.Canceled; + } catch (Exception ex) { _logger.Error(ex, "Synchronizing folders for {Name}", Account.Name); - Debugger.Break(); - - throw; + return MailSynchronizationResult.Failed(ex); } finally { - PublishSynchronizationProgress(100); + // Reset progress at the end + ResetSyncProgress(); } - // Get all unred new downloaded items and return in the result. + // Get all unread new downloaded items and return in the result. // This is primarily used in notifications. var unreadNewItems = await _outlookChangeProcessor.GetDownloadedUnreadMailsAsync(Account.Id, downloadedMessageIds).ConfigureAwait(false); - return MailSynchronizationResult.Completed(unreadNewItems); + return MailSynchronizationResult.CompletedWithFolderResults(unreadNewItems, folderResults); } public async Task DownloadSearchResultMessageAsync(string messageId, MailItemFolder assignedFolder, CancellationToken cancellationToken = default) { + if (string.IsNullOrWhiteSpace(messageId) || assignedFolder == null) return; + + // Online search can return the same message across repeated invocations/races. + // Guard before network+MIME download and before database insert. + var existing = await _outlookChangeProcessor.AreMailsExistsAsync([messageId]).ConfigureAwait(false); + if (existing.Contains(messageId)) + { + return; + } + Log.Information("Downloading search result message {messageId} for {Name} - {FolderName}", messageId, Account.Name, assignedFolder.FolderName); // Outlook message handling was a little strange. @@ -211,6 +290,17 @@ public class OutlookSynchronizer : WinoSynchronizer - { - config.QueryParameters.Top = (int)InitialMessageDownloadCountPerFolder; - config.QueryParameters.Select = outlookMessageSelectParameters; - config.QueryParameters.Orderby = ["receivedDateTime desc"]; - }, cancellationToken).ConfigureAwait(false); + // Download mails for initial sync (last 30 days) + await DownloadMailsForInitialSyncAsync(folder, downloadedMessageIds, cancellationToken).ConfigureAwait(false); } else { - var currentDeltaToken = folder.DeltaToken; + // Initial sync is completed, process delta changes + _logger.Debug("Delta token exists for folder {FolderName}. Processing incremental changes.", folder.FolderName); - var requestInformation = _graphClient.Me.MailFolders[folder.RemoteFolderId].Messages.Delta.ToGetRequestInformation((config) => - { - config.QueryParameters.Top = (int)InitialMessageDownloadCountPerFolder; - config.QueryParameters.Select = outlookMessageSelectParameters; - config.QueryParameters.Orderby = ["receivedDateTime desc"]; - }); - - requestInformation.UrlTemplate = requestInformation.UrlTemplate.Insert(requestInformation.UrlTemplate.Length - 1, ",%24deltatoken"); - requestInformation.QueryParameters.Add("%24deltatoken", currentDeltaToken); - - try - { - messageCollectionPage = await _graphClient.RequestAdapter.SendAsync(requestInformation, Microsoft.Graph.Me.MailFolders.Item.Messages.Delta.DeltaGetResponse.CreateFromDiscriminatorValue, cancellationToken: cancellationToken); - } - catch (ApiException apiException) when (apiException.ResponseStatusCode == 410) - { - folder.DeltaToken = string.Empty; - - goto retry; - } - } - - var messageIteratorAsync = PageIterator.CreatePageIterator(_graphClient, messageCollectionPage, async (item) => - { - try - { - await _handleItemRetrievalSemaphore.WaitAsync(); - return await HandleItemRetrievedAsync(item, folder, downloadedMessageIds, cancellationToken); - } - catch (Exception ex) - { - _logger.Error(ex, "Error occurred while handling item {Id} for folder {FolderName}", item.Id, folder.FolderName); - } - finally - { - _handleItemRetrievalSemaphore.Release(); - } - - return true; - }); - - await messageIteratorAsync - .IterateAsync(cancellationToken) - .ConfigureAwait(false); - - latestDeltaLink = messageIteratorAsync.Deltalink; - - if (downloadedMessageIds.Any()) - { - _logger.Debug("Downloaded {Count} messages for folder {FolderName}", downloadedMessageIds.Count, folder.FolderName); - } - - //Store delta link for tracking new changes. - if (!string.IsNullOrEmpty(latestDeltaLink)) - { - // Parse Delta Token from Delta Link since v5 of Graph SDK works based on the token, not the link. - - var deltaToken = GetDeltaTokenFromDeltaLink(latestDeltaLink); - - await _outlookChangeProcessor.UpdateFolderDeltaSynchronizationIdentifierAsync(folder.Id, deltaToken).ConfigureAwait(false); + await ProcessDeltaChangesAsync(folder, downloadedMessageIds, cancellationToken).ConfigureAwait(false); } await _outlookChangeProcessor.UpdateFolderLastSyncDateAsync(folder.Id).ConfigureAwait(false); + if (downloadedMessageIds.Any()) + { + _logger.Information("Downloaded {Count} messages for folder {FolderName}", downloadedMessageIds.Count, folder.FolderName); + } + return downloadedMessageIds; } + /// + /// Downloads mails for initial synchronization using Delta API with 30-day filter. + /// Downloads metadata only (no MIME content) for messages received in the last 30 days. + /// + private async Task DownloadMailsForInitialSyncAsync(MailItemFolder folder, List downloadedMessageIds, CancellationToken cancellationToken) + { + _logger.Debug("Starting initial mail download for folder {FolderName} (last 6 months)", folder.FolderName); + + try + { + // Calculate date 6 months ago + var sixMonthsAgo = DateTime.UtcNow.AddMonths(-6); + var filterDate = sixMonthsAgo.ToString("yyyy-MM-ddTHH:mm:ssZ"); + + _logger.Information("Downloading messages received after {FilterDate} for folder {FolderName}", filterDate, folder.FolderName); + + // Use Delta API with receivedDateTime filter for last 6 months + var messageCollectionPage = await _graphClient.Me.MailFolders[folder.RemoteFolderId].Messages.Delta.GetAsDeltaGetResponseAsync((config) => + { + config.QueryParameters.Select = outlookMessageSelectParameters; + config.QueryParameters.Orderby = ["receivedDateTime desc"]; + config.QueryParameters.Filter = $"receivedDateTime ge {filterDate}"; + }, cancellationToken).ConfigureAwait(false); + + var totalProcessed = 0; + + // Use PageIterator to process all messages + var messageIterator = PageIterator.CreatePageIterator(_graphClient, messageCollectionPage, async (message) => + { + try + { + await _handleItemRetrievalSemaphore.WaitAsync(); + + if (!IsResourceDeleted(message.AdditionalData) && !IsNotRealMessageType(message)) + { + // Check if this is an EventMessage and fetch it separately if needed (only if calendar access granted) + if (Account.IsCalendarAccessGranted && message is EventMessage) + { + message = await FetchEventMessageAsync(message.Id, cancellationToken).ConfigureAwait(false); + if (message == null) + { + return true; // Skip this message if fetch failed + } + } + + // Check if message already exists + bool mailExists = await _outlookChangeProcessor.IsMailExistsInFolderAsync(message.Id, folder.Id).ConfigureAwait(false); + + if (!mailExists) + { + // For drafts and calendar invitations, download MIME during initial sync like delta sync. + var itemType = Account.IsCalendarAccessGranted ? message.GetMailItemType() : MailItemType.Mail; + if (folder.SpecialFolderType == SpecialFolderType.Draft || itemType == MailItemType.CalendarInvitation) + { + var draftPackages = await CreateNewMailPackagesAsync(message, folder, cancellationToken).ConfigureAwait(false); + + if (draftPackages != null) + { + foreach (var package in draftPackages) + { + bool isInserted = await _outlookChangeProcessor.CreateMailAsync(Account.Id, package).ConfigureAwait(false); + if (isInserted) + { + downloadedMessageIds.Add(package.Copy.Id); + totalProcessed++; + } + } + } + } + else + { + // Create MailCopy from metadata + var mailCopy = await CreateMailCopyFromMessageAsync(message, folder).ConfigureAwait(false); + + if (mailCopy != null) + { + // Create package without MIME + var contacts = ExtractContactsFromOutlookMessage(message); + var package = new NewMailItemPackage(mailCopy, null, folder.RemoteFolderId, contacts); + bool isInserted = await _outlookChangeProcessor.CreateMailAsync(Account.Id, package).ConfigureAwait(false); + + if (isInserted) + { + downloadedMessageIds.Add(mailCopy.Id); + totalProcessed++; + } + } + } + + // Update progress periodically + if (totalProcessed > 0 && totalProcessed % 50 == 0) + { + var statusMessage = string.Format(Translator.Sync_DownloadedMessages, totalProcessed, folder.FolderName); + UpdateSyncProgress(0, 0, statusMessage); + } + } + else + { + _logger.Debug("Mail {MailId} already exists in folder {FolderName}, skipping", message.Id, folder.FolderName); + } + } + + return true; // Continue processing + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to process message {MessageId} during initial sync for folder {FolderName}", message.Id, folder.FolderName); + return true; // Continue despite error + } + finally + { + _handleItemRetrievalSemaphore.Release(); + } + }); + + await messageIterator.IterateAsync(cancellationToken).ConfigureAwait(false); + + // Extract and store delta token for future incremental syncs + if (!string.IsNullOrEmpty(messageIterator.Deltalink)) + { + var deltaToken = GetDeltaTokenFromDeltaLink(messageIterator.Deltalink); + await _outlookChangeProcessor.UpdateFolderDeltaSynchronizationIdentifierAsync(folder.Id, deltaToken).ConfigureAwait(false); + await _outlookChangeProcessor.UpdateFolderLastSyncDateAsync(folder.Id).ConfigureAwait(false); + folder.DeltaToken = deltaToken; + _logger.Information("Stored delta token for folder {FolderName} - future syncs will be incremental", folder.FolderName); + } + else + { + _logger.Warning("No delta token received for folder {FolderName} - future syncs may re-download messages", folder.FolderName); + } + + _logger.Information("Initial sync completed for folder {FolderName}. Downloaded {Count} messages", folder.FolderName, totalProcessed); + } + catch (ApiException apiException) + { + // Handle API errors + var errorContext = new SynchronizerErrorContext + { + Account = Account, + ErrorCode = (int?)apiException.ResponseStatusCode, + ErrorMessage = $"API error during initial sync: {apiException.Message}", + Exception = apiException + }; + + var handled = await _errorHandlingFactory.HandleErrorAsync(errorContext).ConfigureAwait(false); + + if (handled) + { + if (apiException.ResponseStatusCode == 410) + { + folder.DeltaToken = string.Empty; + _logger.Information("API error handled successfully for folder {FolderName} during initial sync. Error: {ErrorCode}", folder.FolderName, apiException.ResponseStatusCode); + } + } + else + { + _logger.Error(apiException, "Unhandled API error during initial sync for folder {FolderName}. Error: {ErrorCode}", folder.FolderName, apiException.ResponseStatusCode); + } + + throw; + } + catch (Exception ex) + { + _logger.Error(ex, "Error occurred during initial mail download for folder {FolderName}", folder.FolderName); + throw; + } + } + + /// + /// Downloads metadata for a batch of messages using Graph SDK batch API (no MIME content). + /// Processes up to 20 messages per batch request as per MaximumAllowedBatchRequestSize. + /// + private async Task> DownloadMessageMetadataBatchAsync(List messageIds, MailItemFolder folder, bool retryFailedOnce, CancellationToken cancellationToken) + { + if (messageIds == null || messageIds.Count == 0) + return new List(); + + var downloadedIds = new List(); + + // Filter out messages that already exist in the database + var messagesToDownload = new List(); + foreach (var messageId in messageIds) + { + bool mailExists = await _outlookChangeProcessor.IsMailExistsInFolderAsync(messageId, folder.Id).ConfigureAwait(false); + if (!mailExists) + { + messagesToDownload.Add(messageId); + } + else + { + _logger.Debug("Mail {MailId} already exists in folder {FolderName}, skipping download", messageId, folder.FolderName); + } + } + + if (messagesToDownload.Count == 0) + { + _logger.Debug("All messages already exist in folder {FolderName}", folder.FolderName); + return downloadedIds; + } + + // Store failed message ids to retry after. + + List failedMessageIds = new(); + + // Process in batches of MaximumAllowedBatchRequestSize (20) + var batches = messagesToDownload.Batch((int)MaximumAllowedBatchRequestSize); + + foreach (var batch in batches) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + var batchContent = new BatchRequestContentCollection(_graphClient); + var requestIdToMessageIdMap = new Dictionary(); + + // Add all message requests to the batch + foreach (var messageId in batch) + { + var requestInfo = _graphClient.Me.Messages[messageId].ToGetRequestInformation((config) => + { + config.QueryParameters.Select = outlookMessageSelectParameters; + }); + + var batchRequestId = await batchContent.AddBatchRequestStepAsync(requestInfo).ConfigureAwait(false); + requestIdToMessageIdMap[batchRequestId] = messageId; + } + + // Execute the batch request + var batchResponse = await _graphClient.Batch.PostAsync(batchContent, cancellationToken).ConfigureAwait(false); + + // Process all responses + foreach (var batchRequestId in requestIdToMessageIdMap.Keys) + { + var messageId = requestIdToMessageIdMap[batchRequestId]; + + try + { + // Deserialize the Message directly from batch response + var message = await batchResponse.GetResponseByIdAsync(batchRequestId).ConfigureAwait(false); + + if (message != null) + { + // Create MailCopy from metadata only + var mailCopy = await CreateMailCopyFromMessageAsync(message, folder).ConfigureAwait(false); + + if (mailCopy != null) + { + // Create package without MIME + var contacts = ExtractContactsFromOutlookMessage(message); + var package = new NewMailItemPackage(mailCopy, null, folder.RemoteFolderId, contacts); + bool isInserted = await _outlookChangeProcessor.CreateMailAsync(Account.Id, package).ConfigureAwait(false); + + if (isInserted) + { + downloadedIds.Add(mailCopy.Id); + _logger.Debug("Downloaded metadata for message {MailId} in folder {FolderName}", messageId, folder.FolderName); + } + else + { + _logger.Warning("Failed to insert mail {MailId} for folder {FolderName}", messageId, folder.FolderName); + } + } + } + else + { + _logger.Warning("Failed to deserialize message {MailId} for folder {FolderName}", messageId, folder.FolderName); + failedMessageIds.Add(messageId); + } + } + catch (ODataError odataError) + { + // Handle OData errors from the batch response + if (odataError.ResponseStatusCode == 404) + { + _logger.Warning("Mail {MailId} not found on server (404) for folder {FolderName}", messageId, folder.FolderName); + } + else + { + failedMessageIds.Add(messageId); + _logger.Error("OData error while downloading mail {MailId} for folder {FolderName}. Error: {Error}", messageId, folder.FolderName, odataError.Error?.Message); + } + } + catch (ServiceException serviceException) + { + // Try to handle the error using the error handling factory + var errorContext = new SynchronizerErrorContext + { + Account = Account, + ErrorCode = (int?)serviceException.ResponseStatusCode, + ErrorMessage = $"Service error during batch mail download: {serviceException.Message}", + Exception = serviceException, + }; + + var handled = await _errorHandlingFactory.HandleErrorAsync(errorContext).ConfigureAwait(false); + + if (!handled) + { + failedMessageIds.Add(messageId); + _logger.Error(serviceException, "Unhandled service error while downloading mail {MailId} for folder {FolderName}. Error: {ErrorCode}", messageId, folder.FolderName, serviceException.ResponseStatusCode); + } + } + catch (Exception ex) + { + failedMessageIds.Add(messageId); + _logger.Error(ex, "Error occurred while processing message {MailId} for folder {FolderName}", messageId, folder.FolderName); + } + } + } + catch (Exception ex) + { + failedMessageIds.AddRange(batch); + + _logger.Error(ex, "Error occurred during batch download for folder {FolderName}", folder.FolderName); + } + } + + if (retryFailedOnce && failedMessageIds.Any()) + { + // For a good cause wait a little bit. + + await Task.Delay(3000); + + // Do not retry here once again. + var failedDownloadedMessagIds = await DownloadMessageMetadataBatchAsync(failedMessageIds, folder, false, cancellationToken); + + downloadedIds.Concat(failedDownloadedMessagIds); + } + + return downloadedIds; + } + + /// + /// Creates a MailCopy from an Outlook Message with metadata only (centralized method). + /// This replaces the scattered CreateMinimalMailCopyAsync and AsMailCopy calls. + /// + private async Task CreateMailCopyFromMessageAsync(Message message, MailItemFolder assignedFolder) + { + if (message == null) return null; + + var mailCopy = message.AsMailCopy(); + mailCopy.FolderId = assignedFolder.Id; + mailCopy.UniqueId = Guid.NewGuid(); + mailCopy.FileId = Guid.NewGuid(); + + // Set ItemType based on calendar access permissions + if (Account.IsCalendarAccessGranted && message is EventMessage) + { + mailCopy.ItemType = message.GetMailItemType(); + } + + // Check for draft mapping if this is a draft with WinoLocalDraftHeader + if (message.IsDraft.GetValueOrDefault() && message.InternetMessageHeaders != null) + { + var winoDraftHeader = message.InternetMessageHeaders + .FirstOrDefault(h => string.Equals(h.Name, Domain.Constants.WinoLocalDraftHeader, StringComparison.OrdinalIgnoreCase)); + + if (winoDraftHeader != null && Guid.TryParse(winoDraftHeader.Value, out Guid localDraftCopyUniqueId)) + { + // This message belongs to existing local draft copy. + // We don't need to create a new mail copy for this message, just update the existing one. + + bool isMappingSuccessful = await _outlookChangeProcessor.MapLocalDraftAsync( + Account.Id, + localDraftCopyUniqueId, + mailCopy.Id, + mailCopy.DraftId, + mailCopy.ThreadId); + + if (isMappingSuccessful) + { + _logger.Debug("Successfully mapped remote draft {RemoteId} to local draft {LocalId}", + mailCopy.Id, localDraftCopyUniqueId); + return null; // Don't create new mail copy, existing one was updated + } + + // Local copy doesn't exist. Continue execution to insert mail copy. + _logger.Debug("Local draft copy {LocalId} not found, creating new mail copy for {RemoteId}", + localDraftCopyUniqueId, mailCopy.Id); + } + } + + return mailCopy; + } + + private static IReadOnlyList ExtractContactsFromOutlookMessage(Message message) + { + if (message == null) return []; + + var contacts = new Dictionary(StringComparer.OrdinalIgnoreCase); + + AddRecipient(message.From?.EmailAddress); + AddRecipient(message.Sender?.EmailAddress); + + if (message.ToRecipients != null) + { + foreach (var recipient in message.ToRecipients) + { + AddRecipient(recipient?.EmailAddress); + } + } + + if (message.CcRecipients != null) + { + foreach (var recipient in message.CcRecipients) + { + AddRecipient(recipient?.EmailAddress); + } + } + + if (message.BccRecipients != null) + { + foreach (var recipient in message.BccRecipients) + { + AddRecipient(recipient?.EmailAddress); + } + } + + if (message.ReplyTo != null) + { + foreach (var recipient in message.ReplyTo) + { + AddRecipient(recipient?.EmailAddress); + } + } + + return contacts.Values.ToList(); + + void AddRecipient(EmailAddress emailAddress) + { + var address = emailAddress?.Address?.Trim(); + if (string.IsNullOrWhiteSpace(address)) return; + + var displayName = string.IsNullOrWhiteSpace(emailAddress.Name) ? address : emailAddress.Name.Trim(); + + contacts[address] = new AccountContact + { + Address = address, + Name = displayName + }; + } + } + private string GetDeltaTokenFromDeltaLink(string deltaLink) => Regex.Split(deltaLink, "deltatoken=")[1]; + /// + /// Determines MailItemType based on EventMessage's MeetingMessageType. + /// + private static MailItemType GetMailItemType(EventMessage eventMessage) + { + if (eventMessage.MeetingMessageType.HasValue) + { + return eventMessage.MeetingMessageType.Value switch + { + MeetingMessageType.MeetingRequest => MailItemType.CalendarInvitation, + MeetingMessageType.MeetingCancelled => MailItemType.CalendarCancellation, + MeetingMessageType.MeetingAccepted or + MeetingMessageType.MeetingTenativelyAccepted or + MeetingMessageType.MeetingDeclined => MailItemType.CalendarResponse, + _ => MailItemType.Mail + }; + } + + // Fallback to CalendarInvitation if type is unknown + return MailItemType.CalendarInvitation; + } + + protected override async Task CreateMinimalMailCopyAsync(Message message, MailItemFolder assignedFolder, CancellationToken cancellationToken = default) + { + // Use centralized method + return await CreateMailCopyFromMessageAsync(message, assignedFolder).ConfigureAwait(false); + } + + private async Task GetMessageByIdAsync(string messageId, CancellationToken cancellationToken = default) + { + try + { + var message = await _graphClient.Me.Messages[messageId].GetAsync((config) => + { + config.QueryParameters.Select = outlookMessageSelectParameters; + }, cancellationToken).ConfigureAwait(false); + + // Check if this is an EventMessage and fetch it separately if needed (only if calendar access granted) + if (Account.IsCalendarAccessGranted && message is EventMessage) + { + message = await FetchEventMessageAsync(message.Id, cancellationToken).ConfigureAwait(false); + } + + return message; + } + catch (ServiceException serviceException) + { + // Try to handle the error using the error handling factory first + var errorContext = new SynchronizerErrorContext + { + Account = Account, + ErrorCode = (int?)serviceException.ResponseStatusCode, + ErrorMessage = $"Service error during message retrieval: {serviceException.Message}", + Exception = serviceException + }; + + var handled = await _errorHandlingFactory.HandleErrorAsync(errorContext).ConfigureAwait(false); + + if (!handled) + { + // No handler could process this error, log and handle appropriately + if (serviceException.ResponseStatusCode == 404) + { + // Re-throw 404 errors to be handled by the caller for queue cleanup + throw; + } + else + { + _logger.Error(serviceException, "Unhandled service error while getting message {MessageId}. Error: {ErrorCode}", messageId, serviceException.ResponseStatusCode); + return null; + } + } + else + { + _logger.Information("Service error handled successfully during message retrieval. Message: {MessageId}, Error: {ErrorCode}", messageId, serviceException.ResponseStatusCode); + return null; // Return null since the error was handled but we couldn't get the message + } + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to get message {MessageId}", messageId); + return null; + } + } + + private async Task ProcessDeltaChangesAsync(MailItemFolder folder, List downloadedMessageIds, CancellationToken cancellationToken = default) + { + // Only process delta changes if we have a delta token (not initial sync) + if (string.IsNullOrEmpty(folder.DeltaToken)) + return; + + try + { + var currentDeltaToken = folder.DeltaToken; + + // Always use Delta endpoint with proper configuration + var requestInformation = _graphClient.Me.MailFolders[folder.RemoteFolderId].Messages.Delta.ToGetRequestInformation((config) => + { + config.QueryParameters.Select = outlookMessageSelectParameters; + config.QueryParameters.Orderby = ["receivedDateTime desc"]; // Sort by received date desc + }); + + requestInformation.UrlTemplate = requestInformation.UrlTemplate.Insert(requestInformation.UrlTemplate.Length - 1, ",%24deltatoken"); + requestInformation.QueryParameters.Add("%24deltatoken", currentDeltaToken); + + var messageCollectionPage = await _graphClient.RequestAdapter.SendAsync(requestInformation, + DeltaGetResponse.CreateFromDiscriminatorValue, + cancellationToken: cancellationToken); + + // Use PageIterator for iterating mails + var messageIterator = PageIterator + .CreatePageIterator(_graphClient, messageCollectionPage, async (item) => + { + try + { + await _handleItemRetrievalSemaphore.WaitAsync(); + return await HandleItemRetrievedAsync(item, folder, downloadedMessageIds, cancellationToken); + } + catch (Exception ex) + { + _logger.Error(ex, "Error occurred while handling delta item {Id} for folder {FolderName}", item.Id, folder.FolderName); + } + finally + { + _handleItemRetrievalSemaphore.Release(); + } + + return true; + }); + + await messageIterator.IterateAsync(cancellationToken).ConfigureAwait(false); + + // Update delta token for next sync - store delta token when there are no nextPageToken remaining + if (!string.IsNullOrEmpty(messageIterator.Deltalink)) + { + var deltaToken = GetDeltaTokenFromDeltaLink(messageIterator.Deltalink); + await _outlookChangeProcessor.UpdateFolderDeltaSynchronizationIdentifierAsync(folder.Id, deltaToken).ConfigureAwait(false); + _logger.Debug("Updated delta token for folder {FolderName} after processing delta changes", folder.FolderName); + } + } + catch (ApiException apiException) + { + // Try to handle the error using the error handling factory + var errorContext = new SynchronizerErrorContext + { + Account = Account, + ErrorCode = (int?)apiException.ResponseStatusCode, + ErrorMessage = $"API error during legacy delta sync: {apiException.Message}", + Exception = apiException + }; + + var handled = await _errorHandlingFactory.HandleErrorAsync(errorContext).ConfigureAwait(false); + + if (!handled) + { + // No handler could process this error, log and re-throw + _logger.Error(apiException, "Unhandled API error during legacy delta sync for folder {FolderName}. Error: {ErrorCode}", folder.FolderName, apiException.ResponseStatusCode); + } + } + } + private bool IsResourceDeleted(IDictionary additionalData) => additionalData != null && additionalData.ContainsKey("@removed"); + /// + /// Fetches an EventMessage with full details including MeetingMessageType from the Messages endpoint. + /// This is necessary because MeetingMessageType is not available when fetching as Message type. + /// + private async Task FetchEventMessageAsync(string messageId, CancellationToken cancellationToken) + { + try + { + var requestInfo = _graphClient.Me.Messages[messageId].ToGetRequestInformation((config) => + { + config.QueryParameters.Select = outlookMessageSelectParameters.Concat(["MeetingMessageType"]).ToArray(); + }); + + var eventMessage = await _graphClient.Me.Messages[messageId].GetAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + + var odataType = eventMessage?.AdditionalData?.ContainsKey("@odata.type") == true + ? eventMessage.AdditionalData["@odata.type"]?.ToString() + : "unknown"; + + _logger.Debug("Fetched EventMessage {MessageId} with type {ODataType}", messageId, odataType); + + return eventMessage as EventMessage; + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to fetch EventMessage {MessageId}", messageId); + return null; + } + } + private async Task HandleFolderRetrievedAsync(MailFolder folder, OutlookSpecialFolderIdInformation outlookSpecialFolderIdInformation, CancellationToken cancellationToken = default) { if (IsResourceDeleted(folder.AdditionalData)) { await _outlookChangeProcessor.DeleteFolderAsync(Account.Id, folder.Id).ConfigureAwait(false); + _isFolderStructureChanged = true; } else { @@ -365,6 +1026,7 @@ public class OutlookSynchronizer : WinoSynchronizer /// Retrieved message. /// Whether the item is non-Message type or not. private bool IsNotRealMessageType(Message item) - => item is EventMessage || item.From?.EmailAddress == null; + => item.From?.EmailAddress == null; private async Task HandleItemRetrievedAsync(Message item, MailItemFolder folder, IList downloadedMessageIds, CancellationToken cancellationToken = default) { @@ -392,6 +1055,16 @@ public class OutlookSynchronizer : WinoSynchronizer(String, CancellationToken)")] - private async Task DeserializeGraphBatchResponseAsync(BatchResponseContentCollection collection, string requestId, CancellationToken cancellationToken = default) where T : IParsable, new() - { - // This deserialization may throw generalException in case of failure. - // Bug: https://github.com/microsoftgraph/msgraph-sdk-dotnet/issues/2010 - // This is a workaround for the bug to retrieve the actual exception. - // All generic batch response deserializations must go under this method. - - try + if (_isFolderStructureChanged) { - return await collection.GetResponseByIdAsync(requestId); - } - catch (ODataError) - { - throw; - } - catch (ServiceException serviceException) - { - // Actual exception is hidden inside ServiceException. - - - ODataError errorResult = await KiotaJsonSerializer.DeserializeAsync(serviceException.RawResponseBody, cancellationToken); - - throw new SynchronizerException("Outlook Error", errorResult); + WeakReferenceMessenger.Default.Send(new AccountFolderConfigurationUpdated(Account.Id)); } } private async Task GetSpecialFolderIdsAsync(CancellationToken cancellationToken) { - var wellKnownFolderIdBatch = new BatchRequestContentCollection(_graphClient); - var folderRequests = new Dictionary - { - { INBOX_NAME, _graphClient.Me.MailFolders[INBOX_NAME].ToGetRequestInformation((t) => { t.QueryParameters.Select = ["id"]; }) }, - { SENT_NAME, _graphClient.Me.MailFolders[SENT_NAME].ToGetRequestInformation((t) => { t.QueryParameters.Select = ["id"]; }) }, - { DELETED_NAME, _graphClient.Me.MailFolders[DELETED_NAME].ToGetRequestInformation((t) => { t.QueryParameters.Select = ["id"]; }) }, - { JUNK_NAME, _graphClient.Me.MailFolders[JUNK_NAME].ToGetRequestInformation((t) => { t.QueryParameters.Select = ["id"]; }) }, - { DRAFTS_NAME, _graphClient.Me.MailFolders[DRAFTS_NAME].ToGetRequestInformation((t) => { t.QueryParameters.Select = ["id"]; }) }, - { ARCHIVE_NAME, _graphClient.Me.MailFolders[ARCHIVE_NAME].ToGetRequestInformation((t) => { t.QueryParameters.Select = ["id"]; }) } - }; + var localFolders = await _outlookChangeProcessor.GetLocalFoldersAsync(Account.Id).ConfigureAwait(false); + var cachedSpecialFolders = TryGetSpecialFolderIdsFromLocalFolders(localFolders); - var batchIds = new Dictionary(); - foreach (var request in folderRequests) + if (cachedSpecialFolders != null) { - batchIds[request.Key] = await wellKnownFolderIdBatch.AddBatchRequestStepAsync(request.Value); + _logger.Debug("Using cached Outlook special folder ids for {AccountName}", Account.Name); + return cachedSpecialFolders; } - var returnedResponse = await _graphClient.Batch.PostAsync(wellKnownFolderIdBatch, cancellationToken).ConfigureAwait(false); - - var folderIds = new Dictionary(); - foreach (var batchId in batchIds) - { - folderIds[batchId.Key] = (await DeserializeGraphBatchResponseAsync(returnedResponse, batchId.Value, cancellationToken)).Id; - } + _logger.Information("Cached Outlook special folder ids are incomplete for {AccountName}. Fetching from Microsoft Graph.", Account.Name); return new OutlookSpecialFolderIdInformation( - folderIds[INBOX_NAME], - folderIds[DELETED_NAME], - folderIds[JUNK_NAME], - folderIds[DRAFTS_NAME], - folderIds[SENT_NAME], - folderIds[ARCHIVE_NAME]); + await GetWellKnownFolderIdAsync(INBOX_NAME, cancellationToken).ConfigureAwait(false), + await GetWellKnownFolderIdAsync(DELETED_NAME, cancellationToken).ConfigureAwait(false), + await GetWellKnownFolderIdAsync(JUNK_NAME, cancellationToken).ConfigureAwait(false), + await GetWellKnownFolderIdAsync(DRAFTS_NAME, cancellationToken).ConfigureAwait(false), + await GetWellKnownFolderIdAsync(SENT_NAME, cancellationToken).ConfigureAwait(false), + await GetWellKnownFolderIdAsync(ARCHIVE_NAME, cancellationToken).ConfigureAwait(false)); } + private async Task GetWellKnownFolderIdAsync(string wellKnownFolderName, CancellationToken cancellationToken) + { + try + { + var folder = await _graphClient.Me.MailFolders[wellKnownFolderName] + .GetAsync(requestConfiguration => + { + requestConfiguration.QueryParameters.Select = ["id"]; + }, cancellationToken: cancellationToken) + .ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(folder?.Id)) + { + throw new SynchronizerException($"Outlook special folder '{wellKnownFolderName}' returned no id."); + } + + return folder.Id; + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.Warning(ex, "Failed to fetch Outlook special folder id for {FolderName}", wellKnownFolderName); + throw; + } + } + + private static OutlookSpecialFolderIdInformation TryGetSpecialFolderIdsFromLocalFolders(IEnumerable localFolders) + { + if (localFolders == null) + { + return null; + } + + var inboxId = GetSpecialFolderRemoteId(localFolders, SpecialFolderType.Inbox); + var deletedId = GetSpecialFolderRemoteId(localFolders, SpecialFolderType.Deleted); + var junkId = GetSpecialFolderRemoteId(localFolders, SpecialFolderType.Junk); + var draftId = GetSpecialFolderRemoteId(localFolders, SpecialFolderType.Draft); + var sentId = GetSpecialFolderRemoteId(localFolders, SpecialFolderType.Sent); + var archiveId = GetSpecialFolderRemoteId(localFolders, SpecialFolderType.Archive); + + if (new[] { inboxId, deletedId, junkId, draftId, sentId, archiveId }.Any(string.IsNullOrWhiteSpace)) + { + return null; + } + + return new OutlookSpecialFolderIdInformation(inboxId, deletedId, junkId, draftId, sentId, archiveId); + } + + private static string GetSpecialFolderRemoteId(IEnumerable localFolders, SpecialFolderType specialFolderType) + => localFolders.FirstOrDefault(folder => folder.SpecialFolderType == specialFolderType && !string.IsNullOrWhiteSpace(folder.RemoteFolderId))?.RemoteFolderId; + private async Task GetDeltaFoldersAsync(CancellationToken cancellationToken) { if (string.IsNullOrEmpty(Account.SynchronizationDeltaIdentifier)) @@ -551,10 +1241,44 @@ public class OutlookSynchronizer : WinoSynchronizer> SendDraft(SendDraftRequest request) { var sendDraftPreparationRequest = request.Request; - - // 1. Delete draft - // 2. Create new Message with new MIME. - // 3. Make sure that conversation id is tagged correctly for replies. - var mailCopyId = sendDraftPreparationRequest.MailItem.Id; var mimeMessage = sendDraftPreparationRequest.Mime; - // Convert mime message to Outlook message. - // Outlook synchronizer does not send MIME messages directly anymore. - // Alias support is lacking with direct MIMEs. - // Therefore we convert the MIME message to Outlook message and use proper APIs. - - var outlookMessage = mimeMessage.AsOutlookMessage(false); - - // Create attachment requests. - // TODO: We need to support large file attachments with sessioned upload at some point. - - var attachmentRequestList = CreateAttachmentUploadBundles(mimeMessage, mailCopyId, request).ToList(); - - // Update draft. + // Graph API ignores the From header in direct MIME uploads, so we must convert + // to a JSON Message object to properly support sending from aliases. + var conversationId = sendDraftPreparationRequest.MailItem.ThreadId; + var outlookMessage = mimeMessage.AsOutlookMessage(false, conversationId); var patchDraftRequest = _graphClient.Me.Messages[mailCopyId].ToPatchRequestInformation(outlookMessage); - var patchDraftRequestBundle = new HttpRequestBundle(patchDraftRequest, request); + var patchDraftBundle = new HttpRequestBundle(patchDraftRequest, request); - // Send draft. + var sendRequest = PreparePostRequestInformation(_graphClient.Me.Messages[mailCopyId].Send.ToPostRequestInformation()); + var sendBundle = new HttpRequestBundle(sendRequest, request); - var sendDraftRequest = PreparePostRequestInformation(_graphClient.Me.Messages[mailCopyId].Send.ToPostRequestInformation()); - var sendDraftRequestBundle = new HttpRequestBundle(sendDraftRequest, request); - - return [.. attachmentRequestList, patchDraftRequestBundle, sendDraftRequestBundle]; + // Attachment uploads are handled outside batching because large attachments + // require upload sessions whose URLs are generated dynamically. + return [patchDraftBundle, sendBundle]; } - private List> CreateAttachmentUploadBundles(MimeMessage mime, string mailCopyId, IRequestBase sourceRequest) + private async Task UploadDraftAttachmentsAsync(SendDraftRequest sendDraftRequest, CancellationToken cancellationToken) { - var allAttachments = new List(); + var mailCopyId = sendDraftRequest.Request.MailItem.Id; + var attachments = sendDraftRequest.Request.Mime.ExtractAttachments(); - foreach (var part in mime.BodyParts) + if (!attachments.Any()) { - var isAttachmentOrInline = part.IsAttachment ? true : part.ContentDisposition?.Disposition == "inline"; + return; + } - if (!isAttachmentOrInline) continue; + foreach (var attachment in attachments) + { + cancellationToken.ThrowIfCancellationRequested(); - using var memory = new MemoryStream(); - ((MimePart)part).Content.DecodeTo(memory); - - var base64String = Convert.ToBase64String(memory.ToArray()); - - var attachment = new OutlookFileAttachment() + var contentBytes = attachment.ContentBytes ?? []; + if (contentBytes.Length <= SimpleAttachmentUploadLimitBytes) { - Base64EncodedContentBytes = base64String, - FileName = part.ContentDisposition?.FileName ?? part.ContentType.Name, - ContentId = part.ContentId, - ContentType = part.ContentType.MimeType, - IsInline = part.ContentDisposition?.Disposition == "inline" + await _graphClient.Me.Messages[mailCopyId].Attachments.PostAsync(attachment, cancellationToken: cancellationToken).ConfigureAwait(false); + continue; + } + + if (contentBytes.Length > MaximumUploadSessionAttachmentSizeBytes) + { + var attachmentSizeMb = contentBytes.LongLength / (1024d * 1024d); + var maximumSizeMb = MaximumUploadSessionAttachmentSizeBytes / (1024d * 1024d); + + throw new InvalidOperationException( + $"Attachment '{attachment.Name}' is {attachmentSizeMb:F1} MB, which exceeds Outlook's upload limit of {maximumSizeMb:F0} MB per attachment."); + } + + var sessionBody = new Microsoft.Graph.Me.Messages.Item.Attachments.CreateUploadSession.CreateUploadSessionPostRequestBody + { + AttachmentItem = new AttachmentItem + { + AttachmentType = AttachmentType.File, + ContentType = attachment.ContentType, + Name = attachment.Name, + Size = contentBytes.LongLength + } }; - allAttachments.Add(attachment); - } + var uploadSession = await _graphClient.Me.Messages[mailCopyId].Attachments.CreateUploadSession.PostAsync(sessionBody, cancellationToken: cancellationToken).ConfigureAwait(false); - static RequestInformation PrepareUploadAttachmentRequest(RequestInformation requestInformation, OutlookFileAttachment outlookFileAttachment) + if (uploadSession?.UploadUrl == null) + { + throw new InvalidOperationException($"Failed to create upload session for attachment '{attachment.Name}'."); + } + + await UploadAttachmentInChunksAsync(uploadSession.UploadUrl, contentBytes, cancellationToken).ConfigureAwait(false); + } + } + + private static async Task UploadAttachmentInChunksAsync(string uploadUrl, byte[] content, CancellationToken cancellationToken) + { + using var client = new HttpClient(); + + var totalSize = content.Length; + var offset = 0; + + while (offset < totalSize) { - requestInformation.Headers.Clear(); + cancellationToken.ThrowIfCancellationRequested(); - string contentJson = JsonSerializer.Serialize(outlookFileAttachment, OutlookSynchronizerJsonContext.Default.OutlookFileAttachment); + var chunkLength = Math.Min(LargeAttachmentUploadChunkSizeBytes, totalSize - offset); + var end = offset + chunkLength - 1; - requestInformation.Content = new MemoryStream(Encoding.UTF8.GetBytes(contentJson)); - requestInformation.HttpMethod = Method.POST; - requestInformation.Headers.Add("Content-Type", "application/json"); + using var request = new HttpRequestMessage(HttpMethod.Put, uploadUrl) + { + Content = new ByteArrayContent(content, offset, chunkLength) + }; - return requestInformation; + request.Content.Headers.Add("Content-Range", $"bytes {offset}-{end}/{totalSize}"); + + using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false); + + // Upload session returns either 202 (continue) or 201/200 (completed). + if (!response.IsSuccessStatusCode) + { + var responseContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + throw new InvalidOperationException($"Attachment chunk upload failed with status {(int)response.StatusCode}: {responseContent}"); + } + + offset += chunkLength; } - - var retList = new List>(); - - // Prepare attachment upload requests. - - foreach (var attachment in allAttachments) - { - var emptyPostRequest = _graphClient.Me.Messages[mailCopyId].Attachments.ToPostRequestInformation(new Attachment()); - var modifiedAttachmentUploadRequest = PrepareUploadAttachmentRequest(emptyPostRequest, attachment); - - var bundle = new HttpRequestBundle(modifiedAttachmentUploadRequest, null); - - retList.Add(bundle); - } - - return retList; } public override List> Archive(BatchArchiveRequest request) @@ -871,12 +1610,77 @@ public class OutlookSynchronizer : WinoSynchronizer> RenameFolder(RenameFolderRequest request) @@ -897,10 +1701,52 @@ public class OutlookSynchronizer : WinoSynchronizer> MarkFolderAsRead(MarkFolderAsReadRequest request) => MarkRead(new BatchMarkReadRequest(request.MailsToMarkRead.Select(a => new MarkReadRequest(a, true)))); + public override List> DeleteFolder(DeleteFolderRequest request) + { + var networkCall = _graphClient.Me.MailFolders[request.Folder.RemoteFolderId].ToDeleteRequestInformation(); + return [new HttpRequestBundle(networkCall, request)]; + } + + public override List> CreateSubFolder(CreateSubFolderRequest request) + { + var requestBody = new MailFolder + { + DisplayName = request.NewFolderName + }; + + var networkCall = _graphClient.Me.MailFolders[request.Folder.RemoteFolderId].ChildFolders.ToPostRequestInformation(requestBody); + return [new HttpRequestBundle(networkCall, request)]; + } + #endregion public override async Task ExecuteNativeRequestsAsync(List> batchedRequests, CancellationToken cancellationToken = default) { + // First apply all UI changes immediately before any batching. + // This ensures UI reflects changes right away, regardless of batch processing. + foreach (var bundle in batchedRequests) + { + bundle.UIChangeRequest?.ApplyUIChanges(); + } + + // SendDraft requests may include large attachments, which require upload sessions. + // Upload these attachments before the batched patch/send sequence. + foreach (var sendDraftBundle in batchedRequests.Where(b => b.UIChangeRequest is SendDraftRequest)) + { + var sendDraftRequest = sendDraftBundle.UIChangeRequest as SendDraftRequest; + + try + { + await UploadDraftAttachmentsAsync(sendDraftRequest, cancellationToken).ConfigureAwait(false); + } + catch + { + sendDraftRequest?.RevertUIChanges(); + throw; + } + } + + // Now batch and execute the network requests. var batchedGroups = batchedRequests.Batch((int)MaximumAllowedBatchRequestSize); foreach (var batch in batchedGroups) @@ -937,7 +1783,7 @@ public class OutlookSynchronizer : WinoSynchronizer bundle, HttpResponseMessage response) + { + try + { + if (bundle?.UIChangeRequest is MarkReadRequest markReadRequest) + { + await _outlookChangeProcessor.ChangeMailReadStatusAsync(markReadRequest.Item.Id, markReadRequest.IsRead).ConfigureAwait(false); + return; + } + + if (bundle?.UIChangeRequest is ChangeFlagRequest changeFlagRequest) + { + await _outlookChangeProcessor.ChangeFlagStatusAsync(changeFlagRequest.Item.Id, changeFlagRequest.IsFlagged).ConfigureAwait(false); + return; + } + + var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + if (string.IsNullOrWhiteSpace(content)) + return; + + var json = JsonNode.Parse(content); + if (bundle?.UIChangeRequest is CreateDraftRequest createDraftRequest) + { + var createdDraftId = json?["id"]?.GetValue(); + if (string.IsNullOrWhiteSpace(createdDraftId)) + return; + + var createdConversationId = json?["conversationId"]?.GetValue(); + var localDraft = createDraftRequest.DraftPreperationRequest.CreatedLocalDraftCopy; + + await _outlookChangeProcessor.MapLocalDraftAsync( + Account.Id, + localDraft.UniqueId, + createdDraftId, + createdConversationId, + createdConversationId).ConfigureAwait(false); + return; + } + + if (bundle?.UIChangeRequest is CreateCalendarEventRequest createCalendarEventRequest) + { + var createdEventId = json?["id"]?.GetValue(); + if (string.IsNullOrWhiteSpace(createdEventId)) + return; + + await UploadCalendarEventAttachmentsAsync(createCalendarEventRequest, createdEventId, CancellationToken.None).ConfigureAwait(false); + } + } + catch (Exception ex) + { + _logger.Debug(ex, "Failed to process Outlook create response."); + } + } + + private async Task UploadCalendarEventAttachmentsAsync(CreateCalendarEventRequest request, string remoteEventId, CancellationToken cancellationToken) + { + var attachments = request.ComposeResult.Attachments ?? []; + if (attachments.Count == 0) + return; + + var remoteCalendarId = request.AssignedCalendar.RemoteCalendarId; + + foreach (var attachment in attachments.Where(a => !string.IsNullOrWhiteSpace(a.FilePath) && File.Exists(a.FilePath))) + { + cancellationToken.ThrowIfCancellationRequested(); + + var contentBytes = await File.ReadAllBytesAsync(attachment.FilePath, cancellationToken).ConfigureAwait(false); + var contentType = MimeTypes.GetMimeType(attachment.FileName ?? attachment.FilePath); + + var fileAttachment = new FileAttachment + { + Name = attachment.FileName, + ContentType = contentType, + ContentBytes = contentBytes + }; + + if (contentBytes.Length <= SimpleAttachmentUploadLimitBytes) + { + await _graphClient.Me.Calendars[remoteCalendarId].Events[remoteEventId].Attachments.PostAsync(fileAttachment, cancellationToken: cancellationToken).ConfigureAwait(false); + continue; + } + + if (contentBytes.Length > MaximumUploadSessionAttachmentSizeBytes) + { + var attachmentSizeMb = contentBytes.LongLength / (1024d * 1024d); + var maximumSizeMb = MaximumUploadSessionAttachmentSizeBytes / (1024d * 1024d); + + throw new InvalidOperationException( + $"Attachment '{attachment.FileName}' is {attachmentSizeMb:F1} MB, which exceeds Outlook's upload limit of {maximumSizeMb:F0} MB per attachment."); + } + + var sessionBody = new Microsoft.Graph.Me.Calendars.Item.Events.Item.Attachments.CreateUploadSession.CreateUploadSessionPostRequestBody + { + AttachmentItem = new AttachmentItem + { + AttachmentType = AttachmentType.File, + ContentType = contentType, + Name = attachment.FileName, + Size = contentBytes.LongLength + } + }; + + var uploadSession = await _graphClient.Me.Calendars[remoteCalendarId].Events[remoteEventId].Attachments.CreateUploadSession.PostAsync(sessionBody, cancellationToken: cancellationToken).ConfigureAwait(false); + + if (uploadSession?.UploadUrl == null) + { + throw new InvalidOperationException($"Failed to create upload session for attachment '{attachment.FileName}'."); + } + + await UploadAttachmentInChunksAsync(uploadSession.UploadUrl, contentBytes, cancellationToken).ConfigureAwait(false); + } + } + private void ThrowBatchExecutionException(List errors) { var formattedErrorString = string.Join("\n", @@ -1039,12 +2002,16 @@ public class OutlookSynchronizer : WinoSynchronizer> OnlineSearchAsync(string queryText, List folders, CancellationToken cancellationToken = default) { - List messagesReturnedByApi = []; + var messagesById = new Dictionary(StringComparer.Ordinal); // Perform search for each folder separately. if (folders?.Count > 0) { - var folderIds = folders.Select(a => a.RemoteFolderId); + var folderIds = folders + .Where(a => a != null && !string.IsNullOrWhiteSpace(a.RemoteFolderId)) + .Select(a => a.RemoteFolderId) + .Distinct(StringComparer.Ordinal) + .ToList(); var tasks = folderIds.Select(async folderId => { @@ -1054,15 +2021,19 @@ public class OutlookSynchronizer : WinoSynchronizer x.RemoteFolderId); - var messagesDictionary = messagesReturnedByApi.ToDictionary(a => a.Id); - // Contains a list of message ids that potentially can be downloaded. - List messageIdsWithKnownFolder = []; + var messageIdsWithKnownFolder = new HashSet(StringComparer.Ordinal); // Validate that all messages are in a known folder. - foreach (var message in messagesReturnedByApi) + foreach (var message in messagesById.Values) { if (!localFolders.ContainsKey(message.ParentFolderId)) { @@ -1110,13 +2083,18 @@ public class OutlookSynchronizer : WinoSynchronizer messagesToDownload = []; - foreach (var id in messagesDictionary.Keys.Except(locallyExistingMails)) + foreach (var id in messageIdsWithKnownFolder.Except(locallyExistingMails, StringComparer.Ordinal)) { - messagesToDownload.Add(messagesDictionary[id]); + if (messagesById.TryGetValue(id, out var message)) + { + messagesToDownload.Add(message); + } } foreach (var message in messagesToDownload) @@ -1136,30 +2114,92 @@ public class OutlookSynchronizer : WinoSynchronizer> CreateNewMailPackagesAsync(Message message, MailItemFolder assignedFolder, CancellationToken cancellationToken = default) { + // Download MIME message for specific scenarios (e.g., search results, draft handling) + // During normal sync, this method should not be called - use CreateMailCopyFromMessageAsync instead var mimeMessage = await DownloadMimeMessageAsync(message.Id, cancellationToken).ConfigureAwait(false); - var mailCopy = message.AsMailCopy(); + var mailCopy = await CreateMailCopyFromMessageAsync(message, assignedFolder).ConfigureAwait(false); - if (message.IsDraft.GetValueOrDefault() - && mimeMessage.Headers.Contains(Domain.Constants.WinoLocalDraftHeader) - && Guid.TryParse(mimeMessage.Headers[Domain.Constants.WinoLocalDraftHeader], out Guid localDraftCopyUniqueId)) - { - // This message belongs to existing local draft copy. - // We don't need to create a new mail copy for this message, just update the existing one. + // If draft mapping was successful, mailCopy will be null + if (mailCopy == null) return null; - bool isMappingSuccessful = await _outlookChangeProcessor.MapLocalDraftAsync(Account.Id, localDraftCopyUniqueId, mailCopy.Id, mailCopy.DraftId, mailCopy.ThreadId); - - if (isMappingSuccessful) return null; - - // Local copy doesn't exists. Continue execution to insert mail copy. - } + await TryMapCalendarInvitationAsync(mailCopy, mimeMessage, cancellationToken).ConfigureAwait(false); // Outlook messages can only be assigned to 1 folder at a time. // Therefore we don't need to create multiple copies of the same message for different folders. - var package = new NewMailItemPackage(mailCopy, mimeMessage, assignedFolder.RemoteFolderId); + var contacts = ExtractContactsFromOutlookMessage(message); + var package = new NewMailItemPackage(mailCopy, mimeMessage, assignedFolder.RemoteFolderId, contacts); return [package]; } + private async Task TryMapCalendarInvitationAsync(MailCopy mailCopy, MimeMessage mimeMessage, CancellationToken cancellationToken) + { + if (mailCopy.ItemType != MailItemType.CalendarInvitation || mimeMessage == null) + return; + + var invitationUid = mimeMessage.ExtractInvitationUid(); + if (string.IsNullOrWhiteSpace(invitationUid)) + return; + + var calendars = await _outlookChangeProcessor.GetAccountCalendarsAsync(Account.Id).ConfigureAwait(false); + if (calendars == null || calendars.Count == 0) + return; + + string escapedUid = invitationUid.Replace("'", "''", StringComparison.Ordinal); + + foreach (var calendar in calendars) + { + try + { + var eventsResponse = await _graphClient.Me.Calendars[calendar.RemoteCalendarId].Events + .GetAsync(requestConfiguration => + { + requestConfiguration.QueryParameters.Filter = $"iCalUId eq '{escapedUid}'"; + requestConfiguration.QueryParameters.Select = ["id"]; + requestConfiguration.QueryParameters.Top = 1; + }, cancellationToken: cancellationToken) + .ConfigureAwait(false); + + var matchedEvent = eventsResponse?.Value?.FirstOrDefault(); + if (matchedEvent == null || string.IsNullOrWhiteSpace(matchedEvent.Id)) + continue; + + var fullEvent = await _graphClient.Me.Calendars[calendar.RemoteCalendarId].Events[matchedEvent.Id] + .GetAsync(requestConfiguration => + { + requestConfiguration.QueryParameters.Expand = ["attachments($select=id,name,contentType,size,isInline)"]; + }, cancellationToken: cancellationToken) + .ConfigureAwait(false); + + if (fullEvent == null) + continue; + + await _outlookChangeProcessor.ManageCalendarEventAsync(fullEvent, calendar, Account).ConfigureAwait(false); + + var localCalendarItem = await _outlookChangeProcessor.GetCalendarItemAsync(calendar.Id, fullEvent.Id).ConfigureAwait(false); + if (localCalendarItem == null) + return; + + await _outlookChangeProcessor.UpsertMailInvitationCalendarMappingAsync(new MailInvitationCalendarMapping() + { + Id = Guid.NewGuid(), + AccountId = Account.Id, + MailCopyId = mailCopy.Id, + InvitationUid = invitationUid, + CalendarId = calendar.Id, + CalendarItemId = localCalendarItem.Id, + CalendarRemoteEventId = fullEvent.Id + }).ConfigureAwait(false); + + return; + } + catch (Exception ex) + { + _logger.Warning(ex, "Failed to map Outlook calendar invitation mail {MailCopyId} for calendar {CalendarId}", mailCopy.Id, calendar.Id); + } + } + } + protected override async Task SynchronizeCalendarEventsInternalAsync(CalendarSynchronizationOptions options, CancellationToken cancellationToken = default) { _logger.Information("Internal calendar synchronization started for {Name}", Account.Name); @@ -1168,7 +2208,12 @@ public class OutlookSynchronizer : WinoSynchronizer c.IsSynchronizationEnabled) + .ToList(); Microsoft.Graph.Me.Calendars.Item.CalendarView.Delta.DeltaGetResponse eventsDeltaResponse = null; @@ -1182,26 +2227,17 @@ public class OutlookSynchronizer : WinoSynchronizer { + requestConfiguration.QueryParameters.Select = ["id", "type"]; requestConfiguration.QueryParameters.StartDateTime = startDate; requestConfiguration.QueryParameters.EndDateTime = endDate; }, cancellationToken: cancellationToken); - - // No delta link. Performing initial sync. - //eventsDeltaResponse = await _graphClient.Me.CalendarView.Delta.GetAsDeltaGetResponseAsync((requestConfiguration) => - //{ - // requestConfiguration.QueryParameters.StartDateTime = startDate; - // requestConfiguration.QueryParameters.EndDateTime = endDate; - - // // TODO: Expand does not work. - // // https://github.com/microsoftgraph/msgraph-sdk-dotnet/issues/2358 - - // requestConfiguration.QueryParameters.Expand = new string[] { "calendar($select=name,id)" }; // Expand the calendar and select name and id. Customize as needed. - //}, cancellationToken: cancellationToken); } else { @@ -1209,20 +2245,7 @@ public class OutlookSynchronizer : WinoSynchronizer - { - - //requestConfiguration.QueryParameters.StartDateTime = startDate; - //requestConfiguration.QueryParameters.EndDateTime = endDate; - }); - - //var requestInformation = _graphClient.Me.Calendars[calendar.RemoteCalendarId].CalendarView.Delta.ToGetRequestInformation((config) => - //{ - // config.QueryParameters.Top = (int)InitialMessageDownloadCountPerFolder; - // config.QueryParameters.Select = outlookMessageSelectParameters; - // config.QueryParameters.Orderby = ["receivedDateTime desc"]; - //}); - + var requestInformation = _graphClient.Me.Calendars[calendar.RemoteCalendarId].CalendarView.Delta.ToGetRequestInformation(); requestInformation.UrlTemplate = requestInformation.UrlTemplate.Insert(requestInformation.UrlTemplate.Length - 1, ",%24deltatoken"); requestInformation.QueryParameters.Add("%24deltatoken", currentDeltaToken); @@ -1237,6 +2260,8 @@ public class OutlookSynchronizer : WinoSynchronizer.CreatePageIterator(_graphClient, eventsDeltaResponse, (item) => { + // Include all event types: SingleInstance, SeriesMaster, Occurrence, and Exception + // CalendarView already expands recurring events into individual occurrences events.Add(item); return true; @@ -1253,18 +2278,35 @@ public class OutlookSynchronizer : WinoSynchronizer + { + // Expand attachments but only get metadata, not the full content + requestConfiguration.QueryParameters.Expand = new[] { "attachments($select=id,name,contentType,size,isInline)" }; + }, cancellationToken: cancellationToken).ConfigureAwait(false); + await _outlookChangeProcessor.ManageCalendarEventAsync(fullEvent, calendar, Account).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.Error(ex, "Error occurred while handling item {Id} for calendar {Name}", item.Id, calendar.Name); + } + finally + { + _handleCalendarEventRetrievalSemaphore.Release(); + } } } @@ -1281,14 +2323,17 @@ public class OutlookSynchronizer : WinoSynchronizer(StringComparer.OrdinalIgnoreCase); List insertedCalendars = new(); List updatedCalendars = new(); @@ -1318,15 +2363,25 @@ public class OutlookSynchronizer : WinoSynchronizer GetPrimaryCalendarIdAsync(IList remoteCalendars, CancellationToken cancellationToken) + { + if (remoteCalendars == null || remoteCalendars.Count == 0) + return string.Empty; + + var explicitPrimary = remoteCalendars.FirstOrDefault(c => c.IsDefaultCalendar.GetValueOrDefault()); + if (explicitPrimary != null) + return explicitPrimary.Id; + + try + { + var meCalendar = await _graphClient.Me.Calendar.GetAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + if (!string.IsNullOrEmpty(meCalendar?.Id)) + return meCalendar.Id; + } + catch (Exception ex) + { + _logger.Warning(ex, "Failed to fetch default Outlook calendar for {Name}. Falling back to first available calendar.", Account.Name); + } + + return remoteCalendars.First().Id; + } + + #region Calendar Operations + + public override List> CreateCalendarEvent(CreateCalendarEventRequest request) + { + var calendarItem = request.PreparedItem; + var attendees = request.PreparedEvent.Attendees; + var reminders = request.PreparedEvent.Reminders; + var calendar = request.AssignedCalendar; + + var outlookEvent = new Microsoft.Graph.Models.Event + { + Subject = calendarItem.Title, + Body = new Microsoft.Graph.Models.ItemBody + { + ContentType = Microsoft.Graph.Models.BodyType.Html, + Content = calendarItem.Description + }, + Location = new Microsoft.Graph.Models.Location + { + DisplayName = calendarItem.Location + }, + ShowAs = calendarItem.ShowAs switch + { + CalendarItemShowAs.Free => Microsoft.Graph.Models.FreeBusyStatus.Free, + CalendarItemShowAs.Tentative => Microsoft.Graph.Models.FreeBusyStatus.Tentative, + CalendarItemShowAs.Busy => Microsoft.Graph.Models.FreeBusyStatus.Busy, + CalendarItemShowAs.OutOfOffice => Microsoft.Graph.Models.FreeBusyStatus.Oof, + CalendarItemShowAs.WorkingElsewhere => Microsoft.Graph.Models.FreeBusyStatus.WorkingElsewhere, + _ => Microsoft.Graph.Models.FreeBusyStatus.Busy + }, + TransactionId = calendarItem.Id.ToString("N") + }; + + if (calendarItem.IsAllDayEvent) + { + outlookEvent.IsAllDay = true; + outlookEvent.Start = new Microsoft.Graph.Models.DateTimeTimeZone + { + DateTime = calendarItem.StartDate.ToString("yyyy-MM-dd"), + TimeZone = calendarItem.StartTimeZone ?? TimeZoneInfo.Local.Id + }; + outlookEvent.End = new Microsoft.Graph.Models.DateTimeTimeZone + { + DateTime = calendarItem.EndDate.ToString("yyyy-MM-dd"), + TimeZone = calendarItem.EndTimeZone ?? calendarItem.StartTimeZone ?? TimeZoneInfo.Local.Id + }; + } + else + { + outlookEvent.IsAllDay = false; + outlookEvent.Start = new Microsoft.Graph.Models.DateTimeTimeZone + { + DateTime = calendarItem.StartDate.ToString("yyyy-MM-ddTHH:mm:ss"), + TimeZone = calendarItem.StartTimeZone ?? TimeZoneInfo.Local.Id + }; + outlookEvent.End = new Microsoft.Graph.Models.DateTimeTimeZone + { + DateTime = calendarItem.EndDate.ToString("yyyy-MM-ddTHH:mm:ss"), + TimeZone = calendarItem.EndTimeZone ?? TimeZoneInfo.Local.Id + }; + } + + if (attendees.Count > 0) + { + outlookEvent.Attendees = attendees.Select(a => new Microsoft.Graph.Models.Attendee + { + EmailAddress = new Microsoft.Graph.Models.EmailAddress + { + Address = a.Email, + Name = a.Name + }, + Type = a.IsOptionalAttendee ? Microsoft.Graph.Models.AttendeeType.Optional : Microsoft.Graph.Models.AttendeeType.Required + }).ToList(); + } + + if (reminders.Count > 0) + { + var reminder = reminders + .OrderBy(reminder => reminder.DurationInSeconds) + .FirstOrDefault(reminder => reminder.ReminderType == CalendarItemReminderType.Popup) + ?? reminders.OrderBy(reminder => reminder.DurationInSeconds).First(); + + outlookEvent.IsReminderOn = true; + outlookEvent.ReminderMinutesBeforeStart = (int)Math.Max(0, reminder.DurationInSeconds / 60); + } + + var recurrence = CalendarRecurrenceMapper.CreateOutlookRecurrence(calendarItem); + if (recurrence != null) + { + outlookEvent.Recurrence = recurrence; + } + + var createRequest = _graphClient.Me.Calendars[calendar.RemoteCalendarId].Events.ToPostRequestInformation(outlookEvent); + + return [new HttpRequestBundle(createRequest, request)]; + } + + public override List> AcceptEvent(AcceptEventRequest request) + { + var calendarItem = request.Item; + var calendar = calendarItem.AssignedCalendar; + + if (calendar == null) + { + throw new InvalidOperationException("Calendar item must have an assigned calendar"); + } + + var remoteEventId = calendarItem.RemoteEventId.GetProviderRemoteEventId(); + if (string.IsNullOrEmpty(remoteEventId)) + { + throw new InvalidOperationException("Cannot accept event without remote event ID"); + } + + var acceptRequestInfo = _graphClient.Me.Calendars[calendar.RemoteCalendarId].Events[remoteEventId].Accept.ToPostRequestInformation(new Microsoft.Graph.Me.Calendars.Item.Events.Item.Accept.AcceptPostRequestBody + { + Comment = request.ResponseMessage, + SendResponse = !string.IsNullOrEmpty(request.ResponseMessage) + }); + + return [new HttpRequestBundle(acceptRequestInfo, request)]; + } + + public override List> OutlookDeclineEvent(OutlookDeclineEventRequest request) + { + var responseMessage = request.ResponseMessage; + + var calendarItem = request.Item; + var calendar = calendarItem.AssignedCalendar; + + if (calendar == null) + { + throw new InvalidOperationException("Calendar item must have an assigned calendar"); + } + + var remoteEventId = calendarItem.RemoteEventId.GetProviderRemoteEventId(); + if (string.IsNullOrEmpty(remoteEventId)) + { + throw new InvalidOperationException("Cannot decline event without remote event ID"); + } + + var declineRequestInfo = _graphClient.Me.Calendars[calendar.RemoteCalendarId].Events[remoteEventId].Decline.ToPostRequestInformation(new Microsoft.Graph.Me.Calendars.Item.Events.Item.Decline.DeclinePostRequestBody + { + Comment = responseMessage, + SendResponse = !string.IsNullOrEmpty(responseMessage) + }); + + return [new HttpRequestBundle(declineRequestInfo, request)]; + } + + public override List> TentativeEvent(TentativeEventRequest request) + { + var calendarItem = request.Item; + var calendar = calendarItem.AssignedCalendar; + + if (calendar == null) + { + throw new InvalidOperationException("Calendar item must have an assigned calendar"); + } + + var remoteEventId = calendarItem.RemoteEventId.GetProviderRemoteEventId(); + if (string.IsNullOrEmpty(remoteEventId)) + { + throw new InvalidOperationException("Cannot tentatively accept event without remote event ID"); + } + + var tentativelyAcceptRequestInfo = _graphClient.Me.Calendars[calendar.RemoteCalendarId].Events[remoteEventId].TentativelyAccept.ToPostRequestInformation(new Microsoft.Graph.Me.Calendars.Item.Events.Item.TentativelyAccept.TentativelyAcceptPostRequestBody + { + Comment = request.ResponseMessage, + SendResponse = !string.IsNullOrEmpty(request.ResponseMessage) + }); + + return [new HttpRequestBundle(tentativelyAcceptRequestInfo, request)]; + } + + public override List> UpdateCalendarEvent(UpdateCalendarEventRequest request) + { + var calendarItem = request.Item; + var attendees = request.Attendees; + + // Get the calendar for this event + var calendar = calendarItem.AssignedCalendar; + if (calendar == null) + { + throw new InvalidOperationException("Calendar item must have an assigned calendar"); + } + + // Convert CalendarItem to Outlook Event for update + var outlookEvent = new Microsoft.Graph.Models.Event + { + Subject = calendarItem.Title, + Body = new Microsoft.Graph.Models.ItemBody + { + ContentType = Microsoft.Graph.Models.BodyType.Text, + Content = calendarItem.Description + }, + Location = new Microsoft.Graph.Models.Location + { + DisplayName = calendarItem.Location + }, + ShowAs = calendarItem.ShowAs switch + { + CalendarItemShowAs.Free => Microsoft.Graph.Models.FreeBusyStatus.Free, + CalendarItemShowAs.Tentative => Microsoft.Graph.Models.FreeBusyStatus.Tentative, + CalendarItemShowAs.Busy => Microsoft.Graph.Models.FreeBusyStatus.Busy, + CalendarItemShowAs.OutOfOffice => Microsoft.Graph.Models.FreeBusyStatus.Oof, + CalendarItemShowAs.WorkingElsewhere => Microsoft.Graph.Models.FreeBusyStatus.WorkingElsewhere, + _ => Microsoft.Graph.Models.FreeBusyStatus.Busy + } + }; + + // Set start and end time using DateTimeTimeZone + if (calendarItem.IsAllDayEvent) + { + // All-day events + outlookEvent.IsAllDay = true; + outlookEvent.Start = new Microsoft.Graph.Models.DateTimeTimeZone + { + DateTime = calendarItem.StartDate.ToString("yyyy-MM-dd"), + TimeZone = "UTC" + }; + outlookEvent.End = new Microsoft.Graph.Models.DateTimeTimeZone + { + DateTime = calendarItem.EndDate.ToString("yyyy-MM-dd"), + TimeZone = "UTC" + }; + } + else + { + // Regular events with time + // StartDate and EndDate are stored in the event's timezone + // We preserve the timezone information during update + outlookEvent.IsAllDay = false; + outlookEvent.Start = new Microsoft.Graph.Models.DateTimeTimeZone + { + DateTime = calendarItem.StartDate.ToString("yyyy-MM-ddTHH:mm:ss"), + TimeZone = calendarItem.StartTimeZone ?? TimeZoneInfo.Local.Id + }; + outlookEvent.End = new Microsoft.Graph.Models.DateTimeTimeZone + { + DateTime = calendarItem.EndDate.ToString("yyyy-MM-ddTHH:mm:ss"), + TimeZone = calendarItem.EndTimeZone ?? TimeZoneInfo.Local.Id + }; + } + + // Add attendees if any + if (attendees != null && attendees.Count > 0) + { + outlookEvent.Attendees = attendees.Select(a => new Microsoft.Graph.Models.Attendee + { + EmailAddress = new Microsoft.Graph.Models.EmailAddress + { + Address = a.Email, + Name = a.Name + }, + Type = a.IsOptionalAttendee ? Microsoft.Graph.Models.AttendeeType.Optional : Microsoft.Graph.Models.AttendeeType.Required + }).ToList(); + } + + // Update the event using Graph API + var updateRequest = _graphClient.Me.Events[calendarItem.RemoteEventId.GetProviderRemoteEventId()].ToPatchRequestInformation(outlookEvent); + + return [new HttpRequestBundle(updateRequest, request)]; + } + + public override List> DeleteCalendarEvent(DeleteCalendarEventRequest request) + { + var calendarItem = request.Item; + + // Get the calendar for this event + var calendar = calendarItem.AssignedCalendar; + if (calendar == null) + { + throw new InvalidOperationException("Calendar item must have an assigned calendar"); + } + + var remoteEventId = calendarItem.RemoteEventId.GetProviderRemoteEventId(); + if (string.IsNullOrEmpty(remoteEventId)) + { + throw new InvalidOperationException("Cannot delete event without remote event ID"); + } + + var deleteRequest = _graphClient.Me.Calendars[calendar.RemoteCalendarId].Events[remoteEventId].ToDeleteRequestInformation(); + + return [new HttpRequestBundle(deleteRequest, request)]; + } + + #endregion + public override async Task KillSynchronizerAsync() { await base.KillSynchronizerAsync(); diff --git a/Wino.Core/Synchronizers/WinoSynchronizer.cs b/Wino.Core/Synchronizers/WinoSynchronizer.cs index f1ba0133..9375611b 100644 --- a/Wino.Core/Synchronizers/WinoSynchronizer.cs +++ b/Wino.Core/Synchronizers/WinoSynchronizer.cs @@ -18,6 +18,7 @@ using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Synchronization; using Wino.Core.Requests.Bundles; +using Wino.Core.Requests.Calendar; using Wino.Core.Requests.Folder; using Wino.Core.Requests.Mail; using Wino.Messaging.UI; @@ -32,7 +33,7 @@ public abstract class WinoSynchronizer>(); - protected WinoSynchronizer(MailAccount account) : base(account) { } + protected WinoSynchronizer(MailAccount account, IMessenger messenger) : base(account, messenger) { } /// /// How many items per single HTTP call can be modified. @@ -41,15 +42,19 @@ public abstract class WinoSynchronizer /// How many items must be downloaded per folder when the folder is first synchronized. + /// Only metadata is downloaded during sync - MIME content is fetched on-demand when user reads mail. /// public abstract uint InitialMessageDownloadCountPerFolder { get; } /// - /// Creates a new Wino Mail Item package out of native message type with full Mime. + /// Creates a new Wino Mail Item package out of native message type with metadata only. + /// NO MIME content is downloaded during synchronization - only headers and essential metadata. + /// MIME will be downloaded on-demand when user explicitly reads the message. /// /// Native message type for the synchronizer. + /// Folder to assign the mail to. /// Cancellation token - /// Package that encapsulates downloaded Mime and additional information for adding new mail. + /// Package with MailCopy metadata. MimeMessage will be null during sync. public abstract Task> CreateNewMailPackagesAsync(TMessageType message, MailItemFolder assignedFolder, CancellationToken cancellationToken = default); /// @@ -58,6 +63,38 @@ public abstract class WinoSynchronizer protected virtual Task SynchronizeAliasesAsync() => Task.CompletedTask; + /// + /// Queues all mail ids for initial synchronization for a specific folder. + /// Only overridden by synchronizers that support the new queue-based sync. + /// + /// Folder to queue mail ids for + /// Cancellation token + /// Task + protected virtual Task QueueMailIdsForInitialSyncAsync(MailItemFolder folder, CancellationToken cancellationToken = default) => Task.CompletedTask; + + /// + /// Downloads mail items from the queue in batches. + /// Only overridden by synchronizers that support the new queue-based sync. + /// + /// Folder to download mails for + /// Number of items to download in each batch + /// Cancellation token + /// List of downloaded mail ids + protected virtual Task> DownloadMailsFromQueueAsync(MailItemFolder folder, int batchSize, CancellationToken cancellationToken = default) => Task.FromResult(new List()); + + /// + /// Creates a MailCopy object with minimal properties from the native message type. + /// This is used during synchronization to create mail entries WITHOUT downloading MIME content. + /// Only metadata (headers, labels, flags) is extracted from the native message format. + /// MIME content will be downloaded later on-demand when user reads the message. + /// Only overridden by synchronizers that support metadata-only synchronization. + /// + /// Native message type + /// Folder this message belongs to + /// Cancellation token + /// MailCopy with minimal properties populated from metadata + protected virtual Task CreateMinimalMailCopyAsync(TMessageType message, MailItemFolder assignedFolder, CancellationToken cancellationToken = default) => Task.FromResult(null); + /// /// Internally synchronizes the account's mails with the given options. /// Not exposed and overriden for each synchronizer. @@ -166,6 +203,12 @@ public abstract class WinoSynchronizerSynchronization options. /// Cancellation token. /// Synchronization result that contains summary of the sync. - public Task SynchronizeCalendarEventsAsync(CalendarSynchronizationOptions options, CancellationToken cancellationToken = default) + public async Task SynchronizeCalendarEventsAsync(CalendarSynchronizationOptions options, CancellationToken cancellationToken = default) { - // TODO: Execute requests for calendar events. - return SynchronizeCalendarEventsInternalAsync(options, cancellationToken); + bool shouldExecuteRequests = changeRequestQueue.Any(r => r is ICalendarActionRequest); + bool shouldDelayExecution = false; + int maxExecutionDelay = 0; + + if (shouldExecuteRequests) + { + State = AccountSynchronizerState.ExecutingRequests; + + List> nativeRequests = new(); + List requestCopies = new(changeRequestQueue.Where(r => r is ICalendarActionRequest)); + + var keys = requestCopies.GroupBy(a => a.GroupingKey()); + + foreach (var group in keys) + { + var key = group.Key; + + if (key is CalendarSynchronizerOperation calendarSynchronizerOperation) + { + switch (calendarSynchronizerOperation) + { + case CalendarSynchronizerOperation.CreateEvent: + nativeRequests.AddRange(group + .OfType() + .SelectMany(CreateCalendarEvent)); + break; + case CalendarSynchronizerOperation.AcceptEvent: + nativeRequests.AddRange(group + .OfType() + .SelectMany(AcceptEvent)); + break; + case CalendarSynchronizerOperation.DeclineEvent: + if (Account.ProviderType == MailProviderType.Outlook) + { + nativeRequests.AddRange(group + .OfType() + .SelectMany(OutlookDeclineEvent)); + } + else + { + nativeRequests.AddRange(group + .OfType() + .SelectMany(DeclineEvent)); + } + break; + case CalendarSynchronizerOperation.TentativeEvent: + nativeRequests.AddRange(group + .OfType() + .SelectMany(TentativeEvent)); + break; + case CalendarSynchronizerOperation.UpdateEvent: + nativeRequests.AddRange(group + .OfType() + .SelectMany(UpdateCalendarEvent)); + break; + case CalendarSynchronizerOperation.DeleteEvent: + nativeRequests.AddRange(group + .OfType() + .SelectMany(DeleteCalendarEvent)); + break; + default: + break; + } + } + } + + // Remove processed calendar requests from queue + changeRequestQueue.RemoveAll(r => r is ICalendarActionRequest); + + Console.WriteLine($"Prepared {nativeRequests.Count()} native calendar requests"); + + try + { + await ExecuteNativeRequestsAsync(nativeRequests, cancellationToken).ConfigureAwait(false); + } + finally + { + UntrackProcessedRequests(requestCopies); + } + + Messenger.Send(new SynchronizationActionsCompleted(Account.Id)); + + // Let servers to finish their job. Sometimes the servers don't respond immediately. + shouldDelayExecution = requestCopies.Any(a => a.ResynchronizationDelay > 0); + + if (shouldDelayExecution) + { + maxExecutionDelay = requestCopies.Aggregate(0, (max, next) => Math.Max(max, next.ResynchronizationDelay)); + } + } + + if (shouldDelayExecution) + { + await Task.Delay(maxExecutionDelay, cancellationToken); + } + + // Execute the actual synchronization + return await SynchronizeCalendarEventsInternalAsync(options, cancellationToken); } /// @@ -313,13 +462,6 @@ public abstract class WinoSynchronizer WeakReferenceMessenger.Default.Send(new RefreshUnreadCountsMessage(Account.Id)); - /// - /// Sends a message to the shell to update the synchronization progress. - /// - /// Percentage of the progress. - public void PublishSynchronizationProgress(double progress) - => WeakReferenceMessenger.Default.Send(new AccountSynchronizationProgressUpdatedMessage(Account.Id, progress)); - /// /// Attempts to find out the best possible synchronization options after the batch request execution. /// @@ -400,11 +542,20 @@ public abstract class WinoSynchronizer> RenameFolder(RenameFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); public virtual List> EmptyFolder(EmptyFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); public virtual List> MarkFolderAsRead(MarkFolderAsReadRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); + public virtual List> DeleteFolder(DeleteFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); + public virtual List> CreateSubFolder(CreateSubFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); #endregion #region Calendar Operations + public virtual List> CreateCalendarEvent(CreateCalendarEventRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); + public virtual List> UpdateCalendarEvent(UpdateCalendarEventRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); + public virtual List> DeleteCalendarEvent(DeleteCalendarEventRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); + public virtual List> AcceptEvent(AcceptEventRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); + public virtual List> DeclineEvent(DeclineEventRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); + public virtual List> OutlookDeclineEvent(OutlookDeclineEventRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); + public virtual List> TentativeEvent(TentativeEventRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); #endregion @@ -415,7 +566,20 @@ public abstract class WinoSynchronizerMail item that its mime file does not exist on the disk. /// Optional download progress for IMAP synchronizer. /// Cancellation token. - public virtual Task DownloadMissingMimeMessageAsync(IMailItem mailItem, ITransferProgress transferProgress = null, CancellationToken cancellationToken = default) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); + public virtual Task DownloadMissingMimeMessageAsync(MailCopy mailItem, ITransferProgress transferProgress = null, CancellationToken cancellationToken = default) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); + + /// + /// Downloads a calendar attachment from the provider. + /// + /// Calendar item the attachment belongs to. + /// Attachment metadata to download. + /// Local file path to save the attachment to. + /// Cancellation token. + public virtual Task DownloadCalendarAttachmentAsync( + Wino.Core.Domain.Entities.Calendar.CalendarItem calendarItem, + Wino.Core.Domain.Entities.Calendar.CalendarAttachment attachment, + string localFilePath, + CancellationToken cancellationToken = default) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); /// /// Performs an online search for the given query text in the given folders. diff --git a/Wino.Core/Wino.Core.csproj b/Wino.Core/Wino.Core.csproj index 4c44cde9..7734fc96 100644 --- a/Wino.Core/Wino.Core.csproj +++ b/Wino.Core/Wino.Core.csproj @@ -1,17 +1,21 @@  - net9.0 + net10.0 win-x86;win-x64;win-arm64 Wino.Core x86;x64;arm64 true true + true + true + true + @@ -28,8 +32,6 @@ - - @@ -38,4 +40,8 @@ - \ No newline at end of file + + + + + diff --git a/Wino.Mail.ViewModels.Tests/Collections/WinoMailCollectionTests.cs b/Wino.Mail.ViewModels.Tests/Collections/WinoMailCollectionTests.cs new file mode 100644 index 00000000..3c13879b --- /dev/null +++ b/Wino.Mail.ViewModels.Tests/Collections/WinoMailCollectionTests.cs @@ -0,0 +1,229 @@ +using FluentAssertions; +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Mail.ViewModels.Collections; +using Wino.Mail.ViewModels.Data; +using Xunit; + +namespace Wino.Mail.ViewModels.Tests.Collections; + +public class WinoMailCollectionTests +{ + [Fact] + public async Task AddAsync_ShouldAddSingleItemAsMailItemViewModel() + { + var sut = CreateCollection(); + var mail = CreateMailCopy(threadId: "thread-1"); + + await sut.AddAsync(mail); + + var items = FlattenItems(sut); + items.Should().ContainSingle().Which.Should().BeOfType(); + sut.ContainsMailUniqueId(mail.UniqueId).Should().BeTrue(); + } + + [Fact] + public async Task AddAsync_ShouldKeepItemsSeparate_WhenThreadIdsDiffer() + { + var sut = CreateCollection(); + var first = CreateMailCopy(threadId: "thread-1"); + var second = CreateMailCopy(threadId: "thread-2"); + + await sut.AddAsync(first); + await sut.AddAsync(second); + + var items = FlattenItems(sut); + items.Should().HaveCount(2); + items.Should().OnlyContain(item => item is MailItemViewModel); + } + + [Fact] + public async Task AddAsync_ShouldConvertSingleItemToThread_WhenSecondItemWithSameThreadIdIsAdded() + { + var sut = CreateCollection(); + var first = CreateMailCopy(threadId: "shared-thread", creationDate: DateTime.UtcNow.AddMinutes(-1)); + var second = CreateMailCopy(threadId: "shared-thread", creationDate: DateTime.UtcNow); + + await sut.AddAsync(first); + FlattenItems(sut).Should().ContainSingle().Which.Should().BeOfType(); + + await sut.AddAsync(second); + + var items = FlattenItems(sut); + var threadItem = items.Should().ContainSingle().Which.Should().BeOfType().Subject; + threadItem.EmailCount.Should().Be(2); + threadItem.GetContainingIds().Should().BeEquivalentTo([first.UniqueId, second.UniqueId]); + } + + [Fact] + public async Task RemoveAsync_ShouldConvertThreadToSingleItem_WhenThreadDropsToOneItem() + { + var sut = CreateCollection(); + var first = CreateMailCopy(threadId: "shared-thread", creationDate: DateTime.UtcNow.AddMinutes(-1)); + var second = CreateMailCopy(threadId: "shared-thread", creationDate: DateTime.UtcNow); + + await sut.AddAsync(first); + await sut.AddAsync(second); + + await sut.RemoveAsync(second); + + var items = FlattenItems(sut); + var remainingItem = items.Should().ContainSingle().Which.Should().BeOfType().Subject; + remainingItem.MailCopy.UniqueId.Should().Be(first.UniqueId); + + var container = sut.GetMailItemContainer(first.UniqueId); + container.Should().NotBeNull(); + container.ThreadViewModel.Should().BeNull(); + } + + [Fact] + public async Task RemoveAsync_ShouldRemoveSingleItem() + { + var sut = CreateCollection(); + var mail = CreateMailCopy(threadId: "thread-1"); + + await sut.AddAsync(mail); + await sut.RemoveAsync(mail); + + FlattenItems(sut).Should().BeEmpty(); + sut.ContainsMailUniqueId(mail.UniqueId).Should().BeFalse(); + } + + [Fact] + public async Task AddRangeAsync_ShouldCreateThreadsForItemsWithMatchingThreadId() + { + var sut = CreateCollection(); + + var threadAFirst = new MailItemViewModel(CreateMailCopy(threadId: "thread-a", creationDate: DateTime.UtcNow.AddMinutes(-10))); + var threadASecond = new MailItemViewModel(CreateMailCopy(threadId: "thread-a", creationDate: DateTime.UtcNow.AddMinutes(-9))); + var threadCFirst = new MailItemViewModel(CreateMailCopy(threadId: "thread-c", creationDate: DateTime.UtcNow.AddMinutes(-8))); + var threadCSecond = new MailItemViewModel(CreateMailCopy(threadId: "thread-c", creationDate: DateTime.UtcNow.AddMinutes(-7))); + var single = new MailItemViewModel(CreateMailCopy(threadId: "thread-b", creationDate: DateTime.UtcNow.AddMinutes(-6))); + + await sut.AddRangeAsync([threadAFirst, threadASecond, threadCFirst, threadCSecond, single], clearIdCache: true); + + var items = FlattenItems(sut); + items.Should().HaveCount(3); + items.Count(item => item is ThreadMailItemViewModel).Should().Be(2); + items.Count(item => item is MailItemViewModel).Should().Be(1); + + var threadItems = items.OfType().ToList(); + threadItems.Should().Contain(item => item.ThreadId == "thread-a" && item.EmailCount == 2); + threadItems.Should().Contain(item => item.ThreadId == "thread-c" && item.EmailCount == 2); + } + + [Fact] + public async Task UpdateMailCopy_ShouldMergeExistingSingles_WhenThreadIdChangesToMatch() + { + var sut = CreateCollection(); + var first = CreateMailCopy(threadId: "shared-thread", creationDate: DateTime.UtcNow.AddMinutes(-1)); + var second = CreateMailCopy(threadId: string.Empty, creationDate: DateTime.UtcNow); + + await sut.AddAsync(first); + await sut.AddAsync(second); + + var updatedSecond = CloneMailCopy(second); + updatedSecond.ThreadId = "shared-thread"; + + await sut.UpdateMailCopy(updatedSecond, MailUpdateSource.Server, MailCopyChangeFlags.ThreadId); + + var items = FlattenItems(sut); + var threadItem = items.Should().ContainSingle().Which.Should().BeOfType().Subject; + threadItem.EmailCount.Should().Be(2); + threadItem.GetContainingIds().Should().BeEquivalentTo([first.UniqueId, second.UniqueId]); + } + + [Fact] + public async Task AddAsync_ShouldThreadWithUpdatedItem_WhenThreadIdWasSetByPriorUpdate() + { + var sut = CreateCollection(); + var existing = CreateMailCopy(threadId: string.Empty, creationDate: DateTime.UtcNow.AddMinutes(-1)); + var incoming = CreateMailCopy(threadId: "shared-thread", creationDate: DateTime.UtcNow); + + await sut.AddAsync(existing); + + var updatedExisting = CloneMailCopy(existing); + updatedExisting.ThreadId = "shared-thread"; + + await sut.UpdateMailCopy(updatedExisting, MailUpdateSource.Server, MailCopyChangeFlags.ThreadId); + await sut.AddAsync(incoming); + + var items = FlattenItems(sut); + var threadItem = items.Should().ContainSingle().Which.Should().BeOfType().Subject; + threadItem.EmailCount.Should().Be(2); + threadItem.GetContainingIds().Should().BeEquivalentTo([existing.UniqueId, incoming.UniqueId]); + } + + private static WinoMailCollection CreateCollection() => new() + { + CoreDispatcher = new ImmediateDispatcher() + }; + + private static List FlattenItems(WinoMailCollection collection) + { + var items = new List(); + + foreach (var group in collection.MailItems) + { + foreach (var item in group) + { + items.Add(item); + } + } + + return items; + } + + private static MailCopy CreateMailCopy(string threadId, DateTime? creationDate = null) + => new() + { + UniqueId = Guid.NewGuid(), + ThreadId = threadId, + CreationDate = creationDate ?? DateTime.UtcNow, + FromName = "Sender", + FromAddress = "sender@wino.dev", + Subject = "Subject", + PreviewText = "Preview", + FileId = Guid.NewGuid(), + FolderId = Guid.NewGuid() + }; + + private static MailCopy CloneMailCopy(MailCopy source) + => new() + { + UniqueId = source.UniqueId, + Id = source.Id, + FolderId = source.FolderId, + ThreadId = source.ThreadId, + MessageId = source.MessageId, + References = source.References, + InReplyTo = source.InReplyTo, + FromName = source.FromName, + FromAddress = source.FromAddress, + Subject = source.Subject, + PreviewText = source.PreviewText, + CreationDate = source.CreationDate, + Importance = source.Importance, + IsRead = source.IsRead, + IsFlagged = source.IsFlagged, + IsFocused = source.IsFocused, + HasAttachments = source.HasAttachments, + ItemType = source.ItemType, + DraftId = source.DraftId, + IsDraft = source.IsDraft, + FileId = source.FileId, + SenderContact = source.SenderContact, + AssignedAccount = source.AssignedAccount, + AssignedFolder = source.AssignedFolder + }; + + private sealed class ImmediateDispatcher : IDispatcher + { + public Task ExecuteOnUIThread(Action action) + { + action(); + return Task.CompletedTask; + } + } +} diff --git a/Wino.Mail.ViewModels.Tests/Data/MailItemViewModelUpdateTests.cs b/Wino.Mail.ViewModels.Tests/Data/MailItemViewModelUpdateTests.cs new file mode 100644 index 00000000..a674ba1f --- /dev/null +++ b/Wino.Mail.ViewModels.Tests/Data/MailItemViewModelUpdateTests.cs @@ -0,0 +1,173 @@ +using FluentAssertions; +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Mail.ViewModels.Collections; +using Wino.Mail.ViewModels.Data; +using Xunit; + +namespace Wino.Mail.ViewModels.Tests.Data; + +public class MailItemViewModelUpdateTests +{ + [Fact] + public void UpdateFrom_ShouldNotifyOnlyReadState_WhenSameInstanceAndHintProvided() + { + var mailCopy = CreateMailCopy("thread-1", DateTime.UtcNow); + var sut = new MailItemViewModel(mailCopy); + var raisedProperties = new List(); + + sut.PropertyChanged += (_, e) => + { + if (!string.IsNullOrEmpty(e.PropertyName)) + { + raisedProperties.Add(e.PropertyName); + } + }; + + mailCopy.IsRead = true; + + sut.UpdateFrom(mailCopy, MailCopyChangeFlags.IsRead); + + raisedProperties.Should().Equal(nameof(MailItemViewModel.IsRead)); + } + + [Fact] + public void UpdateFrom_ShouldNotifyAddressAndDependentSenderFields_WhenFromAddressChanges() + { + var original = CreateMailCopy("thread-1", DateTime.UtcNow); + original.FromName = string.Empty; + var updated = CloneMailCopy(original); + updated.FromAddress = "updated@wino.dev"; + + var sut = new MailItemViewModel(original); + var raisedProperties = new List(); + + sut.PropertyChanged += (_, e) => + { + if (!string.IsNullOrEmpty(e.PropertyName)) + { + raisedProperties.Add(e.PropertyName); + } + }; + + sut.UpdateFrom(updated); + + raisedProperties.Should().Equal( + nameof(MailItemViewModel.FromAddress), + nameof(MailItemViewModel.FromName), + nameof(MailItemViewModel.SortingName)); + } + + [Fact] + public async Task UpdateMailCopy_ShouldNotifyThreadOnlyForReadState_WhenReadStateChanges() + { + var collection = new WinoMailCollection + { + CoreDispatcher = new ImmediateDispatcher() + }; + + var older = CreateMailCopy("thread-1", DateTime.UtcNow.AddMinutes(-5)); + var latest = CreateMailCopy("thread-1", DateTime.UtcNow); + + await collection.AddAsync(older); + await collection.AddAsync(latest); + + ThreadMailItemViewModel? threadItem = null; + foreach (var group in collection.MailItems) + { + foreach (var item in group) + { + if (item is ThreadMailItemViewModel thread) + { + threadItem = thread; + break; + } + } + + if (threadItem != null) + break; + } + + threadItem.Should().NotBeNull(); + + var raisedProperties = new List(); + threadItem!.PropertyChanged += (_, e) => + { + if (!string.IsNullOrEmpty(e.PropertyName)) + { + raisedProperties.Add(e.PropertyName); + } + }; + + latest.IsRead = true; + + await collection.UpdateMailCopy(latest, MailUpdateSource.ClientUpdated, MailCopyChangeFlags.IsRead); + + raisedProperties.Should().Equal(nameof(ThreadMailItemViewModel.IsRead)); + } + + private static MailCopy CreateMailCopy(string threadId, DateTime creationDate) + => new() + { + UniqueId = Guid.NewGuid(), + Id = Guid.NewGuid().ToString("N"), + FolderId = Guid.NewGuid(), + ThreadId = threadId, + MessageId = $"message-{Guid.NewGuid():N}", + References = string.Empty, + InReplyTo = string.Empty, + FromName = "Sender", + FromAddress = "sender@wino.dev", + Subject = "Subject", + PreviewText = "Preview", + CreationDate = creationDate, + Importance = MailImportance.Normal, + IsRead = false, + IsFlagged = false, + IsFocused = false, + HasAttachments = false, + ItemType = MailItemType.Mail, + DraftId = string.Empty, + IsDraft = false, + FileId = Guid.NewGuid() + }; + + private static MailCopy CloneMailCopy(MailCopy source) + => new() + { + UniqueId = source.UniqueId, + Id = source.Id, + FolderId = source.FolderId, + ThreadId = source.ThreadId, + MessageId = source.MessageId, + References = source.References, + InReplyTo = source.InReplyTo, + FromName = source.FromName, + FromAddress = source.FromAddress, + Subject = source.Subject, + PreviewText = source.PreviewText, + CreationDate = source.CreationDate, + Importance = source.Importance, + IsRead = source.IsRead, + IsFlagged = source.IsFlagged, + IsFocused = source.IsFocused, + HasAttachments = source.HasAttachments, + ItemType = source.ItemType, + DraftId = source.DraftId, + IsDraft = source.IsDraft, + FileId = source.FileId, + SenderContact = source.SenderContact, + AssignedAccount = source.AssignedAccount, + AssignedFolder = source.AssignedFolder + }; + + private sealed class ImmediateDispatcher : IDispatcher + { + public Task ExecuteOnUIThread(Action action) + { + action(); + return Task.CompletedTask; + } + } +} diff --git a/Wino.Mail.ViewModels.Tests/GlobalUsings.cs b/Wino.Mail.ViewModels.Tests/GlobalUsings.cs new file mode 100644 index 00000000..29ee885e --- /dev/null +++ b/Wino.Mail.ViewModels.Tests/GlobalUsings.cs @@ -0,0 +1,4 @@ +global using System; +global using System.Collections.Generic; +global using System.Linq; +global using System.Threading.Tasks; diff --git a/Wino.Mail.ViewModels.Tests/Wino.Mail.ViewModels.Tests.csproj b/Wino.Mail.ViewModels.Tests/Wino.Mail.ViewModels.Tests.csproj new file mode 100644 index 00000000..b93bd23a --- /dev/null +++ b/Wino.Mail.ViewModels.Tests/Wino.Mail.ViewModels.Tests.csproj @@ -0,0 +1,29 @@ + + + net10.0 + enable + enable + false + true + false + false + false + false + false + x86;x64;arm64 + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/Wino.Mail.ViewModels/AccountDetailsPageViewModel.cs b/Wino.Mail.ViewModels/AccountDetailsPageViewModel.cs index 494d8813..b6d2d8f2 100644 --- a/Wino.Mail.ViewModels/AccountDetailsPageViewModel.cs +++ b/Wino.Mail.ViewModels/AccountDetailsPageViewModel.cs @@ -1,18 +1,26 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; +using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; +using Wino.Core.Misc; +using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Accounts; using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.Navigation; +using Wino.Core.Services; +using Wino.Core.ViewModels.Data; +using Wino.Mail.ViewModels.Data; +using Wino.Messaging.Client.Calendar; using Wino.Messaging.Client.Navigation; -using Wino.Messaging.Server; namespace Wino.Mail.ViewModels; @@ -20,13 +28,55 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel { private readonly IMailDialogService _dialogService; private readonly IAccountService _accountService; - private readonly IWinoServerConnectionManager _serverConnectionManager; private readonly IFolderService _folderService; + private readonly ICalendarService _calendarService; + private readonly IStatePersistanceService _statePersistanceService; + private readonly INewThemeService _themeService; + private readonly IImapTestService _imapTestService; private bool isLoaded = false; - public MailAccount Account { get; set; } + [ObservableProperty] + public partial MailAccount Account { get; set; } public ObservableCollection CurrentFolders { get; set; } = []; + public ObservableCollection AccountCalendars { get; set; } = []; + public ObservableCollection AccountCalendarSettingsItems { get; } = []; + public ObservableCollection ShowAsOptions { get; } = []; + [ObservableProperty] + public partial AccountCalendar SelectedPrimaryCalendar { get; set; } + + [ObservableProperty] + public partial int SelectedTabIndex { get; set; } = 0; // Default to Mail tab + + [ObservableProperty] + public partial string AccountName { get; set; } + + [ObservableProperty] + public partial string SenderName { get; set; } + + [ObservableProperty] + public partial AppColorViewModel SelectedColor { get; set; } + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsImapServer))] + public partial CustomServerInformation ServerInformation { get; set; } + + [ObservableProperty] + public partial List AvailableColors { get; set; } + + [ObservableProperty] + public partial int SelectedIncomingServerConnectionSecurityIndex { get; set; } + + [ObservableProperty] + public partial int SelectedIncomingServerAuthenticationMethodIndex { get; set; } + + [ObservableProperty] + public partial int SelectedOutgoingServerConnectionSecurityIndex { get; set; } + + [ObservableProperty] + public partial int SelectedOutgoingServerAuthenticationMethodIndex { get; set; } + + // Mail-related properties [ObservableProperty] private bool isFocusedInboxEnabled; @@ -46,17 +96,56 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel private bool isTaskbarBadgeEnabled; public bool IsFocusedInboxSupportedForAccount => Account != null && Account.Preferences.IsFocusedInboxEnabled != null; + public bool IsImapServer => ServerInformation != null; + public string ProviderIconPath => Account?.SpecialImapProvider != SpecialImapProvider.None + ? $"ms-appx:///Assets/Providers/{Account.SpecialImapProvider}.png" + : $"ms-appx:///Assets/Providers/{Account?.ProviderType}.png"; + public string Address => Account?.Address ?? string.Empty; + + public List AvailableAuthenticationMethods { get; } = + [ + new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.Auto, Translator.ImapAuthenticationMethod_Auto), + new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.None, Translator.ImapAuthenticationMethod_None), + new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.NormalPassword, Translator.ImapAuthenticationMethod_Plain), + new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.EncryptedPassword, Translator.ImapAuthenticationMethod_EncryptedPassword), + new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.Ntlm, Translator.ImapAuthenticationMethod_Ntlm), + new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.CramMd5, Translator.ImapAuthenticationMethod_CramMD5), + new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.DigestMd5, Translator.ImapAuthenticationMethod_DigestMD5) + ]; + + public List AvailableConnectionSecurities { get; set; } = + [ + new ImapConnectionSecurityModel(Core.Domain.Enums.ImapConnectionSecurity.Auto, Translator.ImapConnectionSecurity_Auto), + new ImapConnectionSecurityModel(Core.Domain.Enums.ImapConnectionSecurity.SslTls, Translator.ImapConnectionSecurity_SslTls), + new ImapConnectionSecurityModel(Core.Domain.Enums.ImapConnectionSecurity.StartTls, Translator.ImapConnectionSecurity_StartTls), + new ImapConnectionSecurityModel(Core.Domain.Enums.ImapConnectionSecurity.None, Translator.ImapConnectionSecurity_None) + ]; public AccountDetailsPageViewModel(IMailDialogService dialogService, IAccountService accountService, - IWinoServerConnectionManager serverConnectionManager, - IFolderService folderService) + IFolderService folderService, + ICalendarService calendarService, + IStatePersistanceService statePersistanceService, + INewThemeService themeService, + IImapTestService imapTestService) { _dialogService = dialogService; _accountService = accountService; - _serverConnectionManager = serverConnectionManager; _folderService = folderService; + _calendarService = calendarService; + _statePersistanceService = statePersistanceService; + _themeService = themeService; + _imapTestService = imapTestService; + + var colorHexList = _themeService.GetAvailableAccountColors(); + AvailableColors = colorHexList.Select(a => new AppColorViewModel(a)).ToList(); + + ShowAsOptions.Add(new AccountCalendarShowAsOption(CalendarItemShowAs.Free, Translator.CalendarShowAs_Free)); + ShowAsOptions.Add(new AccountCalendarShowAsOption(CalendarItemShowAs.Tentative, Translator.CalendarShowAs_Tentative)); + ShowAsOptions.Add(new AccountCalendarShowAsOption(CalendarItemShowAs.Busy, Translator.CalendarShowAs_Busy)); + ShowAsOptions.Add(new AccountCalendarShowAsOption(CalendarItemShowAs.OutOfOffice, Translator.CalendarShowAs_OutOfOffice)); + ShowAsOptions.Add(new AccountCalendarShowAsOption(CalendarItemShowAs.WorkingElsewhere, Translator.CalendarShowAs_WorkingElsewhere)); } [RelayCommand] @@ -71,44 +160,80 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel private void EditAliases() => Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsManageAliases_Title, WinoPage.AliasManagementPage, Account.Id)); - public Task FolderSyncToggledAsync(IMailItemFolder folderStructure, bool isEnabled) - => _folderService.ChangeFolderSynchronizationStateAsync(folderStructure.Id, isEnabled); - - public Task FolderShowUnreadToggled(IMailItemFolder folderStructure, bool isEnabled) - => _folderService.ChangeFolderShowUnreadCountStateAsync(folderStructure.Id, isEnabled); + [RelayCommand] + private void EditImapCalDavSettings() + => Messenger.Send(new BreadcrumbNavigationRequested( + Translator.ImapCalDavSettingsPage_TitleEdit, + WinoPage.ImapCalDavSettingsPage, + ImapCalDavSettingsNavigationContext.CreateForEditMode(Account.Id))); [RelayCommand] - private void EditAccountDetails() - => Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsEditAccountDetails_Title, WinoPage.EditAccountDetailsPage, Account)); + private async Task SaveChangesAsync() + { + await UpdateAccountAsync(); + + _dialogService.InfoBarMessage(Translator.EditAccountDetailsPage_SaveSuccess_Title, Translator.EditAccountDetailsPage_SaveSuccess_Message, InfoBarMessageType.Success); + } [RelayCommand] - private async Task DeleteAccount() + private async Task DeleteAccountAsync() { if (Account == null) return; var confirmation = await _dialogService.ShowConfirmationDialogAsync(Translator.DialogMessage_DeleteAccountConfirmationTitle, - string.Format(Translator.DialogMessage_DeleteAccountConfirmationMessage, Account.Name), - Translator.Buttons_Delete); + string.Format(Translator.DialogMessage_DeleteAccountConfirmationMessage, Account.Name), + Translator.Buttons_Delete); if (!confirmation) return; + await SynchronizationManager.Instance.DestroySynchronizerAsync(Account.Id); + await _accountService.DeleteAccountAsync(Account); - var isSynchronizerKilledResponse = await _serverConnectionManager.GetResponseAsync(new KillAccountSynchronizerRequested(Account.Id)); + _dialogService.InfoBarMessage(Translator.Info_AccountDeletedTitle, string.Format(Translator.Info_AccountDeletedMessage, Account.Name), InfoBarMessageType.Success); - if (isSynchronizerKilledResponse.IsSuccess) + Messenger.Send(new BackBreadcrumNavigationRequested()); + } + + [RelayCommand] + private async Task ValidateImapSettingsAsync() + { + try { - await _accountService.DeleteAccountAsync(Account); - - _dialogService.InfoBarMessage(Translator.Info_AccountDeletedTitle, string.Format(Translator.Info_AccountDeletedMessage, Account.Name), InfoBarMessageType.Success); - - Messenger.Send(new BackBreadcrumNavigationRequested()); + await _imapTestService.TestImapConnectionAsync(ServerInformation, true); + _dialogService.InfoBarMessage(Translator.IMAPSetupDialog_ValidationSuccess_Title, Translator.IMAPSetupDialog_ValidationSuccess_Message, InfoBarMessageType.Success); + } + catch (Exception ex) + { + _dialogService.InfoBarMessage(Translator.IMAPSetupDialog_ValidationFailed_Title, ex.Message, InfoBarMessageType.Error); } } + [RelayCommand] + private async Task UpdateCustomServerInformationAsync() + { + if (ServerInformation != null) + { + ServerInformation.IncomingAuthenticationMethod = AvailableAuthenticationMethods[SelectedIncomingServerAuthenticationMethodIndex].ImapAuthenticationMethod; + ServerInformation.IncomingServerSocketOption = AvailableConnectionSecurities[SelectedIncomingServerConnectionSecurityIndex].ImapConnectionSecurity; + ServerInformation.OutgoingAuthenticationMethod = AvailableAuthenticationMethods[SelectedOutgoingServerAuthenticationMethodIndex].ImapAuthenticationMethod; + ServerInformation.OutgoingServerSocketOption = AvailableConnectionSecurities[SelectedOutgoingServerConnectionSecurityIndex].ImapConnectionSecurity; + Account.ServerInformation = ServerInformation; + } + + await _accountService.UpdateAccountCustomServerInformationAsync(Account.ServerInformation); + + _dialogService.InfoBarMessage(Translator.IMAPSetupDialog_SaveImapSuccess_Title, Translator.IMAPSetupDialog_SaveImapSuccess_Message, InfoBarMessageType.Success); + } + + public Task FolderSyncToggledAsync(IMailItemFolder folderStructure, bool isEnabled) + => _folderService.ChangeFolderSynchronizationStateAsync(folderStructure.Id, isEnabled); + + public Task FolderShowUnreadToggled(IMailItemFolder folderStructure, bool isEnabled) + => _folderService.ChangeFolderShowUnreadCountStateAsync(folderStructure.Id, isEnabled); public override async void OnNavigatedTo(NavigationMode mode, object parameters) { @@ -117,6 +242,9 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel if (parameters is Guid accountId) { Account = await _accountService.GetAccountAsync(accountId); + AccountName = Account.Name; + SenderName = Account.SenderName; + ServerInformation = Account.ServerInformation; IsFocusedInboxEnabled = Account.Preferences.IsFocusedInboxEnabled.GetValueOrDefault(); AreNotificationsEnabled = Account.Preferences.IsNotificationsEnabled; @@ -126,7 +254,24 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel IsAppendMessageSettinEnabled = Account.Preferences.ShouldAppendMessagesToSentFolder; IsTaskbarBadgeEnabled = Account.Preferences.IsTaskbarBadgeEnabled; - OnPropertyChanged(nameof(IsFocusedInboxSupportedForAccount)); + if (!string.IsNullOrEmpty(Account.AccountColorHex)) + { + SelectedColor = AvailableColors.FirstOrDefault(a => a.Hex == Account.AccountColorHex); + } + else + { + SelectedColor = null; + } + + if (ServerInformation != null) + { + SelectedIncomingServerAuthenticationMethodIndex = AvailableAuthenticationMethods.FindIndex(a => a.ImapAuthenticationMethod == ServerInformation.IncomingAuthenticationMethod); + SelectedIncomingServerConnectionSecurityIndex = AvailableConnectionSecurities.FindIndex(a => a.ImapConnectionSecurity == ServerInformation.IncomingServerSocketOption); + SelectedOutgoingServerAuthenticationMethodIndex = AvailableAuthenticationMethods.FindIndex(a => a.ImapAuthenticationMethod == ServerInformation.OutgoingAuthenticationMethod); + SelectedOutgoingServerConnectionSecurityIndex = AvailableConnectionSecurities.FindIndex(a => a.ImapConnectionSecurity == ServerInformation.OutgoingServerSocketOption); + } + + SelectedTabIndex = _statePersistanceService.ApplicationMode == WinoApplicationMode.Calendar ? 2 : 1; var folderStructures = (await _folderService.GetFolderStructureForAccountAsync(Account.Id, true)).Folders; @@ -135,10 +280,90 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel CurrentFolders.Add(folder); } + // Load calendar list + await LoadAccountCalendarsAsync(); + isLoaded = true; } } + private Task UpdateAccountAsync() + { + Account.Name = AccountName; + Account.SenderName = SenderName; + Account.AccountColorHex = SelectedColor?.Hex ?? string.Empty; + + return _accountService.UpdateAccountAsync(Account); + } + + private async Task LoadAccountCalendarsAsync() + { + var calendars = await _calendarService.GetAccountCalendarsAsync(Account.Id); + + await ExecuteUIThread(() => + { + AccountCalendars.Clear(); + AccountCalendarSettingsItems.Clear(); + + foreach (var calendar in calendars) + { + AccountCalendars.Add(calendar); + AccountCalendarSettingsItems.Add(new AccountCalendarSettingsItemViewModel(calendar, ShowAsOptions, AvailableColors)); + } + }); + + SelectedPrimaryCalendar = AccountCalendars.FirstOrDefault(calendar => calendar.IsPrimary) ?? AccountCalendars.FirstOrDefault(); + } + + public AccountCalendarShowAsOption GetShowAsOption(CalendarItemShowAs showAs) + => ShowAsOptions.FirstOrDefault(option => option.ShowAs == showAs) ?? ShowAsOptions.First(); + + public async Task UpdateCalendarSynchronizationAsync(AccountCalendar calendar, bool isEnabled) + { + if (calendar == null || calendar.IsSynchronizationEnabled == isEnabled) + return; + + calendar.IsSynchronizationEnabled = isEnabled; + await _calendarService.UpdateAccountCalendarAsync(calendar); + } + + public async Task UpdateCalendarDefaultShowAsAsync(AccountCalendar calendar, AccountCalendarShowAsOption option) + { + if (calendar == null || option == null || calendar.DefaultShowAs == option.ShowAs) + return; + + calendar.DefaultShowAs = option.ShowAs; + await _calendarService.UpdateAccountCalendarAsync(calendar); + } + + public async Task UpdateCalendarColorAsync(AccountCalendarSettingsItemViewModel calendarItem, AppColorViewModel color) + { + if (calendarItem?.Calendar == null || color == null || calendarItem.Calendar.BackgroundColorHex == color.Hex) + return; + + calendarItem.SetBackgroundColor(color); + await _calendarService.UpdateAccountCalendarAsync(calendarItem.Calendar); + } + + [RelayCommand] + private void ResetColor() + => SelectedColor = null; + + partial void OnSelectedColorChanged(AppColorViewModel oldValue, AppColorViewModel newValue) + { + if (Account != null) + { + _ = UpdateAccountAsync(); + } + } + + partial void OnAccountChanged(MailAccount value) + { + OnPropertyChanged(nameof(IsFocusedInboxSupportedForAccount)); + OnPropertyChanged(nameof(ProviderIconPath)); + OnPropertyChanged(nameof(Address)); + } + protected override async void OnPropertyChanged(PropertyChangedEventArgs e) { base.OnPropertyChanged(e); @@ -167,6 +392,64 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel Account.Preferences.IsTaskbarBadgeEnabled = IsTaskbarBadgeEnabled; await _accountService.UpdateAccountAsync(Account); break; + case nameof(SelectedPrimaryCalendar) when SelectedPrimaryCalendar != null: + foreach (var calendar in AccountCalendars) + { + calendar.IsPrimary = calendar.Id == SelectedPrimaryCalendar.Id; + } + + await _calendarService.SetPrimaryCalendarAsync(Account.Id, SelectedPrimaryCalendar.Id); + break; } } } + +public sealed class AccountCalendarShowAsOption +{ + public CalendarItemShowAs ShowAs { get; } + public string DisplayText { get; } + + public AccountCalendarShowAsOption(CalendarItemShowAs showAs, string displayText) + { + ShowAs = showAs; + DisplayText = displayText; + } +} + +public partial class AccountCalendarSettingsItemViewModel : ObservableObject +{ + public AccountCalendar Calendar { get; } + public ObservableCollection ShowAsOptions { get; } + public List AvailableColors { get; } + + public string Name => Calendar.Name; + public string TimeZone => Calendar.TimeZone; + public string BackgroundColorHex => Calendar.BackgroundColorHex; + + [ObservableProperty] + public partial bool IsSynchronizationEnabled { get; set; } + + [ObservableProperty] + public partial AccountCalendarShowAsOption SelectedShowAsOption { get; set; } + + [ObservableProperty] + public partial AppColorViewModel SelectedColor { get; set; } + + public AccountCalendarSettingsItemViewModel(AccountCalendar calendar, ObservableCollection showAsOptions, List availableColors) + { + Calendar = calendar; + ShowAsOptions = showAsOptions; + AvailableColors = availableColors; + IsSynchronizationEnabled = calendar.IsSynchronizationEnabled; + SelectedShowAsOption = showAsOptions.FirstOrDefault(option => option.ShowAs == calendar.DefaultShowAs) ?? showAsOptions.FirstOrDefault(); + SelectedColor = availableColors.FirstOrDefault(color => string.Equals(color.Hex, calendar.BackgroundColorHex, StringComparison.OrdinalIgnoreCase)) + ?? new AppColorViewModel(calendar.BackgroundColorHex ?? ColorHelpers.GenerateFlatColorHex()); + } + + public void SetBackgroundColor(AppColorViewModel color) + { + SelectedColor = color; + Calendar.BackgroundColorHex = color.Hex; + OnPropertyChanged(nameof(BackgroundColorHex)); + } +} diff --git a/Wino.Mail.ViewModels/AccountManagementViewModel.cs b/Wino.Mail.ViewModels/AccountManagementViewModel.cs index a225d76d..2c531298 100644 --- a/Wino.Mail.ViewModels/AccountManagementViewModel.cs +++ b/Wino.Mail.ViewModels/AccountManagementViewModel.cs @@ -12,43 +12,42 @@ using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Exceptions; using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Authentication; -using Wino.Core.Domain.Models.Connectivity; +using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Synchronization; +using Wino.Core.Services; using Wino.Core.ViewModels; using Wino.Core.ViewModels.Data; using Wino.Mail.ViewModels.Data; using Wino.Messaging.Client.Navigation; -using Wino.Messaging.Server; using Wino.Messaging.UI; namespace Wino.Mail.ViewModels; public partial class AccountManagementViewModel : AccountManagementPageViewModelBase { - private readonly ISpecialImapProviderConfigResolver _specialImapProviderConfigResolver; - private readonly IImapTestService _imapTestService; private readonly IWinoLogger _winoLogger; + private readonly ISpecialImapProviderConfigResolver _specialImapProviderConfigResolver; + private readonly ICalDavClient _calDavClient; public IMailDialogService MailDialogService { get; } public AccountManagementViewModel(IMailDialogService dialogService, - IWinoServerConnectionManager winoServerConnectionManager, INavigationService navigationService, IAccountService accountService, - ISpecialImapProviderConfigResolver specialImapProviderConfigResolver, IProviderService providerService, - IImapTestService imapTestService, IStoreManagementService storeManagementService, + IWinoAccountProfileService winoAccountProfileService, IWinoLogger winoLogger, + ISpecialImapProviderConfigResolver specialImapProviderConfigResolver, + ICalDavClient calDavClient, IAuthenticationProvider authenticationProvider, - IPreferencesService preferencesService) : base(dialogService, winoServerConnectionManager, navigationService, accountService, providerService, storeManagementService, authenticationProvider, preferencesService) + IPreferencesService preferencesService) : base(dialogService, navigationService, accountService, providerService, storeManagementService, winoAccountProfileService, authenticationProvider, preferencesService) { MailDialogService = dialogService; - _specialImapProviderConfigResolver = specialImapProviderConfigResolver; - _imapTestService = imapTestService; _winoLogger = winoLogger; + _specialImapProviderConfigResolver = specialImapProviderConfigResolver; + _calDavClient = calDavClient; } [RelayCommand] @@ -88,246 +87,114 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel return; } - MailAccount createdAccount = null; - IAccountCreationDialog creationDialog = null; + Messenger.Send(new BreadcrumbNavigationRequested(Translator.WelcomeWizard_Step2Title, WinoPage.ProviderSelectionPage)); + } - try + public Task StartAddNewAccountAsync() => AddNewAccountAsync(); + + private async Task ValidateSpecialImapConnectivityAsync(CustomServerInformation serverInformation) + { + var connectivityResult = await SynchronizationManager.Instance + .TestImapConnectivityAsync(serverInformation, allowSSLHandshake: false) + .ConfigureAwait(false); + + if (connectivityResult.IsCertificateUIRequired) { - var providers = ProviderService.GetAvailableProviders(); + var certificateMessage = + $"{Translator.IMAPSetupDialog_CertificateAllowanceRequired_Row0}\n\n" + + $"{Translator.IMAPSetupDialog_CertificateIssuer}: {connectivityResult.CertificateIssuer}\n" + + $"{Translator.IMAPSetupDialog_CertificateValidFrom}: {connectivityResult.CertificateValidFromDateString}\n" + + $"{Translator.IMAPSetupDialog_CertificateValidTo}: {connectivityResult.CertificateExpirationDateString}\n\n" + + $"{Translator.IMAPSetupDialog_CertificateAllowanceRequired_Row1}"; - // Select provider. - var accountCreationDialogResult = await MailDialogService.ShowAccountProviderSelectionDialogAsync(providers); + var allowCertificate = await ExecuteUIThreadTaskAsync( + () => MailDialogService.ShowConfirmationDialogAsync(certificateMessage, Translator.GeneralTitle_Warning, Translator.Buttons_Allow)) + .ConfigureAwait(false); - var accountCreationCancellationTokenSource = new CancellationTokenSource(); + if (!allowCertificate) + throw new InvalidOperationException(Translator.IMAPSetupDialog_CertificateDenied); - if (accountCreationDialogResult != null) + connectivityResult = await SynchronizationManager.Instance + .TestImapConnectivityAsync(serverInformation, allowSSLHandshake: true) + .ConfigureAwait(false); + } + + if (!connectivityResult.IsSuccess) + throw new InvalidOperationException(connectivityResult.FailedReason ?? Translator.IMAPSetupDialog_ConnectionFailedMessage); + + if (serverInformation.CalendarSupportMode != ImapCalendarSupportMode.CalDav) + return; + + if (string.IsNullOrWhiteSpace(serverInformation.CalDavServiceUrl)) + throw new InvalidOperationException(Translator.ImapCalDavSettingsPage_CalDavUrlRequired); + + var settings = new CalDavConnectionSettings + { + ServiceUri = new Uri(serverInformation.CalDavServiceUrl, UriKind.Absolute), + Username = serverInformation.CalDavUsername, + Password = serverInformation.CalDavPassword + }; + + await _calDavClient.DiscoverCalendarsAsync(settings).ConfigureAwait(false); + } + + private async Task ExecuteUIThreadTaskAsync(Func action) + { + if (Dispatcher == null) + { + await action().ConfigureAwait(false); + return; + } + + var completionSource = new TaskCompletionSource(); + + await ExecuteUIThread(() => + { + _ = ExecuteAndCaptureAsync(); + + async Task ExecuteAndCaptureAsync() { - creationDialog = MailDialogService.GetAccountCreationDialog(accountCreationDialogResult); - - CustomServerInformation customServerInformation = null; - - createdAccount = new MailAccount() + try { - ProviderType = accountCreationDialogResult.ProviderType, - Name = accountCreationDialogResult.AccountName, - SpecialImapProvider = accountCreationDialogResult.SpecialImapProviderDetails?.SpecialImapProvider ?? SpecialImapProvider.None, - Id = Guid.NewGuid(), - AccountColorHex = accountCreationDialogResult.AccountColorHex - }; - - await creationDialog.ShowDialogAsync(accountCreationCancellationTokenSource); - await Task.Delay(500); - - creationDialog.State = AccountCreationDialogState.SigningIn; - - string tokenInformation = string.Empty; - - // Custom server implementation requires more async waiting. - if (creationDialog is IImapAccountCreationDialog customServerDialog) - { - // Pass along the account properties and perform initial navigation on the imap frame. - customServerDialog.StartImapConnectionSetup(createdAccount); - - customServerInformation = await customServerDialog.GetCustomServerInformationAsync() - ?? throw new AccountSetupCanceledException(); - - // At this point connection is successful. - // Save the server setup information and later on we'll fetch folders. - - customServerInformation.AccountId = createdAccount.Id; - - createdAccount.Address = customServerInformation.Address; - createdAccount.ServerInformation = customServerInformation; - createdAccount.SenderName = customServerInformation.DisplayName; + await action().ConfigureAwait(false); + completionSource.TrySetResult(null); } - else + catch (Exception ex) { - // Hanle special imap providers like iCloud and Yahoo. - if (accountCreationDialogResult.SpecialImapProviderDetails != null) - { - // Special imap provider testing dialog. This is only available for iCloud and Yahoo. - customServerInformation = _specialImapProviderConfigResolver.GetServerInformation(createdAccount, accountCreationDialogResult); - customServerInformation.Id = Guid.NewGuid(); - customServerInformation.AccountId = createdAccount.Id; - - createdAccount.SenderName = accountCreationDialogResult.SpecialImapProviderDetails.SenderName; - createdAccount.Address = customServerInformation.Address; - - // Let server validate the imap/smtp connection. - var testResultResponse = await WinoServerConnectionManager.GetResponseAsync(new ImapConnectivityTestRequested(customServerInformation, true)); - - if (!testResultResponse.IsSuccess) - { - throw new Exception($"{Translator.IMAPSetupDialog_ConnectionFailedTitle}\n{testResultResponse.Message}"); - } - else if (!testResultResponse.Data.IsSuccess) - { - // Server connectivity might succeed, but result might be failed. - throw new ImapClientPoolException(testResultResponse.Data.FailedReason, customServerInformation, testResultResponse.Data.FailureProtocolLog); - } - } - else - { - // OAuth authentication is handled here. - // Server authenticates, returns the token info here. - - var tokenInformationResponse = await WinoServerConnectionManager - .GetResponseAsync(new AuthorizationRequested(accountCreationDialogResult.ProviderType, - createdAccount, - createdAccount.ProviderType == MailProviderType.Gmail), accountCreationCancellationTokenSource.Token); - - if (creationDialog.State == AccountCreationDialogState.Canceled) - throw new AccountSetupCanceledException(); - - if (!tokenInformationResponse.IsSuccess) - throw new Exception(tokenInformationResponse.Message); - - createdAccount.Address = tokenInformationResponse.Data.AccountAddress; - - tokenInformationResponse.ThrowIfFailed(); - } + completionSource.TrySetException(ex); } - - // Address is still doesn't have a value for API synchronizers. - // It'll be synchronized with profile information. - - await AccountService.CreateAccountAsync(createdAccount, customServerInformation); - - // Local account has been created. - - // Sync profile information if supported. - if (createdAccount.IsProfileInfoSyncSupported) - { - // Start profile information synchronization. - // It's only available for Outlook and Gmail synchronizers. - - var profileSyncOptions = new MailSynchronizationOptions() - { - AccountId = createdAccount.Id, - Type = MailSynchronizationType.UpdateProfile - }; - - var profileSynchronizationResponse = await WinoServerConnectionManager.GetResponseAsync(new NewMailSynchronizationRequested(profileSyncOptions, SynchronizationSource.Client)); - - var profileSynchronizationResult = profileSynchronizationResponse.Data; - - if (profileSynchronizationResult.CompletedState != SynchronizationCompletedState.Success) - throw new Exception(Translator.Exception_FailedToSynchronizeProfileInformation); - - createdAccount.SenderName = profileSynchronizationResult.ProfileInformation.SenderName; - createdAccount.Base64ProfilePictureData = profileSynchronizationResult.ProfileInformation.Base64ProfilePictureData; - - if (!string.IsNullOrEmpty(profileSynchronizationResult.ProfileInformation.AccountAddress)) - { - createdAccount.Address = profileSynchronizationResult.ProfileInformation.AccountAddress; - } - - await AccountService.UpdateProfileInformationAsync(createdAccount.Id, profileSynchronizationResult.ProfileInformation); - } - - if (creationDialog is IImapAccountCreationDialog customServerAccountCreationDialog) - customServerAccountCreationDialog.ShowPreparingFolders(); - else - creationDialog.State = AccountCreationDialogState.PreparingFolders; - - // Start synchronizing folders. - var folderSyncOptions = new MailSynchronizationOptions() - { - AccountId = createdAccount.Id, - Type = MailSynchronizationType.FoldersOnly - }; - - var folderSynchronizationResponse = await WinoServerConnectionManager.GetResponseAsync(new NewMailSynchronizationRequested(folderSyncOptions, SynchronizationSource.Client)); - - var folderSynchronizationResult = folderSynchronizationResponse.Data; - - if (folderSynchronizationResult == null || folderSynchronizationResult.CompletedState != SynchronizationCompletedState.Success) - throw new Exception($"{Translator.Exception_FailedToSynchronizeFolders}\n{folderSynchronizationResponse.Message}"); - - // Sync aliases if supported. - if (createdAccount.IsAliasSyncSupported) - { - // Try to synchronize aliases for the account. - - var aliasSyncOptions = new MailSynchronizationOptions() - { - AccountId = createdAccount.Id, - Type = MailSynchronizationType.Alias - }; - - var aliasSyncResponse = await WinoServerConnectionManager.GetResponseAsync(new NewMailSynchronizationRequested(aliasSyncOptions, SynchronizationSource.Client)); - var aliasSynchronizationResult = folderSynchronizationResponse.Data; - - if (aliasSynchronizationResult.CompletedState != SynchronizationCompletedState.Success) - throw new Exception(Translator.Exception_FailedToSynchronizeAliases); - } - else - { - // Create root primary alias for the account. - // This is only available for accounts that do not support alias synchronization. - - await AccountService.CreateRootAliasAsync(createdAccount.Id, createdAccount.Address); - } - - // Send changes to listeners. - ReportUIChange(new AccountCreatedMessage(createdAccount)); - - // Notify success. - DialogService.InfoBarMessage(Translator.Info_AccountCreatedTitle, string.Format(Translator.Info_AccountCreatedMessage, createdAccount.Address), InfoBarMessageType.Success); } - } - catch (Exception ex) when (ex.Message.Contains(nameof(GmailServiceDisabledException))) + }); + + await completionSource.Task.ConfigureAwait(false); + } + + private async Task ExecuteUIThreadTaskAsync(Func> action) + { + if (Dispatcher == null) + return await action().ConfigureAwait(false); + + var completionSource = new TaskCompletionSource(); + + await ExecuteUIThread(() => { - // For Google Workspace accounts, Gmail API might be disabled by the admin. - // Wino can't continue synchronization in this case. - // We must notify the user about this and prevent account creation. + _ = ExecuteAndCaptureAsync(); - DialogService.InfoBarMessage(Translator.GmailServiceDisabled_Title, Translator.GmailServiceDisabled_Message, InfoBarMessageType.Error); - - if (createdAccount != null) + async Task ExecuteAndCaptureAsync() { - await AccountService.DeleteAccountAsync(createdAccount); + try + { + var result = await action().ConfigureAwait(false); + completionSource.TrySetResult(result); + } + catch (Exception ex) + { + completionSource.TrySetException(ex); + } } - } - catch (AccountSetupCanceledException) - { - // Ignore - } - catch (Exception ex) when (ex.Message.Contains(nameof(AccountSetupCanceledException))) - { - // Ignore - } - catch (ImapClientPoolException testClientPoolException) when (testClientPoolException.CustomServerInformation != null) - { - var properties = testClientPoolException.CustomServerInformation.GetConnectionProperties(); + }); - properties.Add("ProtocolLog", testClientPoolException.ProtocolLog); - properties.Add("DiagnosticId", PreferencesService.DiagnosticId); - - _winoLogger.TrackEvent("IMAP Test Failed", properties); - - DialogService.InfoBarMessage(Translator.Info_AccountCreationFailedTitle, testClientPoolException.Message, InfoBarMessageType.Error); - } - catch (ImapClientPoolException clientPoolException) when (clientPoolException.InnerException != null) - { - DialogService.InfoBarMessage(Translator.Info_AccountCreationFailedTitle, clientPoolException.InnerException.Message, InfoBarMessageType.Error); - } - catch (Exception ex) - { - Log.Error(ex, "Failed to create account."); - - DialogService.InfoBarMessage(Translator.Info_AccountCreationFailedTitle, ex.Message, InfoBarMessageType.Error); - - // Delete account in case of failure. - if (createdAccount != null) - { - await AccountService.DeleteAccountAsync(createdAccount); - } - } - finally - { - creationDialog?.Complete(false); - } + return await completionSource.Task.ConfigureAwait(false); } [RelayCommand] diff --git a/Wino.Mail.ViewModels/AccountSetupProgressPageViewModel.cs b/Wino.Mail.ViewModels/AccountSetupProgressPageViewModel.cs new file mode 100644 index 00000000..85423bab --- /dev/null +++ b/Wino.Mail.ViewModels/AccountSetupProgressPageViewModel.cs @@ -0,0 +1,477 @@ +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using Serilog; +using Wino.Core.Domain; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Accounts; +using Wino.Core.Domain.Models.Calendar; +using Wino.Core.Domain.Exceptions; +using Wino.Core.Domain.Models.Navigation; +using Wino.Core.Domain.Models.Synchronization; +using Wino.Core.Services; +using Wino.Mail.ViewModels.Data; +using Wino.Messaging.Client.Navigation; +using Wino.Messaging.UI; + +namespace Wino.Mail.ViewModels; + +public partial class AccountSetupProgressPageViewModel : MailBaseViewModel +{ + private readonly IAccountService _accountService; + private readonly ISpecialImapProviderConfigResolver _specialImapProviderConfigResolver; + private readonly ICalDavClient _calDavClient; + private readonly IMailDialogService _dialogService; + + public WelcomeWizardContext WizardContext { get; } + + public ObservableCollection Steps { get; } = []; + + [ObservableProperty] + public partial bool IsSetupComplete { get; set; } + + [ObservableProperty] + public partial bool IsSetupFailed { get; set; } + + [ObservableProperty] + public partial string FailureMessage { get; set; } + + private MailAccount _createdAccount; + private bool _dbWritten; + + public AccountSetupProgressPageViewModel( + IAccountService accountService, + ISpecialImapProviderConfigResolver specialImapProviderConfigResolver, + ICalDavClient calDavClient, + IMailDialogService dialogService, + WelcomeWizardContext wizardContext) + { + _accountService = accountService; + _specialImapProviderConfigResolver = specialImapProviderConfigResolver; + _calDavClient = calDavClient; + _dialogService = dialogService; + WizardContext = wizardContext; + } + + public override async void OnNavigatedTo(NavigationMode mode, object parameters) + { + base.OnNavigatedTo(mode, parameters); + + // Only run on fresh navigation, not on back-navigation + if (mode == NavigationMode.Back) return; + + await RunSetupAsync(); + } + + private void BuildSteps() + { + Steps.Clear(); + + if (WizardContext.IsOAuthProvider) + { + Steps.Add(new AccountSetupStepModel + { + Title = string.Format(Translator.AccountSetup_Step_Authenticating, WizardContext.SelectedProvider.Name) + }); + Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingProfile }); + Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SavingAccount }); + Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders }); + Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingCalendarMetadata }); + Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingAliases }); + Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_Finalizing }); + } + else if (WizardContext.IsSpecialImapProvider) + { + Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_TestingMailAuth }); + + if (WizardContext.CalendarSupportMode == ImapCalendarSupportMode.CalDav) + { + Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_DiscoveringCalDav }); + Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_TestingCalendarAuth }); + } + + Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SavingAccount }); + Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders }); + + if (WizardContext.CalendarSupportMode != ImapCalendarSupportMode.Disabled) + { + Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingCalendarMetadata }); + } + + Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_Finalizing }); + } + else // Generic IMAP + { + Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SavingAccount }); + Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders }); + + var setupResult = WizardContext.ImapCalDavSetupResult; + if (setupResult?.IsCalendarAccessGranted == true && + setupResult.ServerInformation?.CalendarSupportMode == ImapCalendarSupportMode.CalDav) + { + Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_DiscoveringCalDav }); + Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_TestingCalendarAuth }); + } + + if (setupResult?.IsCalendarAccessGranted == true) + { + Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingCalendarMetadata }); + } + + Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_Finalizing }); + } + } + + private int _currentStepIndex; + + private void SetStepInProgress(string title) + { + for (int i = 0; i < Steps.Count; i++) + { + if (Steps[i].Title == title) + { + _currentStepIndex = i; + Steps[i].Status = AccountSetupStepStatus.InProgress; + return; + } + } + } + + private void SetCurrentStepSucceeded() + { + if (_currentStepIndex < Steps.Count) + Steps[_currentStepIndex].Status = AccountSetupStepStatus.Succeeded; + } + + private void SetCurrentStepFailed(string errorMessage) + { + if (_currentStepIndex < Steps.Count) + { + Steps[_currentStepIndex].Status = AccountSetupStepStatus.Failed; + Steps[_currentStepIndex].ErrorMessage = errorMessage; + } + } + + private async Task RunSetupAsync() + { + IsSetupComplete = false; + IsSetupFailed = false; + FailureMessage = null; + _dbWritten = false; + _createdAccount = null; + + BuildSteps(); + + try + { + CustomServerInformation customServerInformation = null; + + // Build account in memory + _createdAccount = new MailAccount + { + Id = Guid.NewGuid(), + ProviderType = WizardContext.SelectedProvider.Type, + Name = WizardContext.AccountName, + SpecialImapProvider = WizardContext.SelectedProvider.SpecialImapProvider, + AccountColorHex = WizardContext.AccountColorHex, + IsCalendarAccessGranted = true + }; + + if (WizardContext.IsOAuthProvider) + { + // Step: Authenticating + SetStepInProgress(string.Format(Translator.AccountSetup_Step_Authenticating, WizardContext.SelectedProvider.Name)); + + var authTokenInfo = await SynchronizationManager.Instance.HandleAuthorizationAsync( + WizardContext.SelectedProvider.Type, + _createdAccount, + _createdAccount.ProviderType == MailProviderType.Gmail); + + _createdAccount.Address = authTokenInfo.AccountAddress; + SetCurrentStepSucceeded(); + + // Step: Save to DB + SetStepInProgress(Translator.AccountSetup_Step_SavingAccount); + await _accountService.CreateAccountAsync(_createdAccount, null); + _dbWritten = true; + SetCurrentStepSucceeded(); + + // Step: Profile + SetStepInProgress(Translator.AccountSetup_Step_FetchingProfile); + var profileResult = await SynchronizationManager.Instance.SynchronizeProfileAsync(_createdAccount.Id); + if (profileResult.CompletedState != SynchronizationCompletedState.Success) + throw new Exception(Translator.Exception_FailedToSynchronizeProfileInformation); + + if (profileResult.ProfileInformation != null) + { + _createdAccount.SenderName = profileResult.ProfileInformation.SenderName; + _createdAccount.Base64ProfilePictureData = profileResult.ProfileInformation.Base64ProfilePictureData; + + if (!string.IsNullOrEmpty(profileResult.ProfileInformation.AccountAddress)) + _createdAccount.Address = profileResult.ProfileInformation.AccountAddress; + + await _accountService.UpdateProfileInformationAsync(_createdAccount.Id, profileResult.ProfileInformation); + } + SetCurrentStepSucceeded(); + + // Step: Folders + SetStepInProgress(Translator.AccountSetup_Step_SyncingFolders); + var folderResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(_createdAccount.Id); + if (folderResult == null || folderResult.CompletedState != SynchronizationCompletedState.Success) + throw new Exception(Translator.Exception_FailedToSynchronizeFolders); + SetCurrentStepSucceeded(); + + // Step: Calendar metadata + SetStepInProgress(Translator.AccountSetup_Step_FetchingCalendarMetadata); + if (_createdAccount.IsCalendarAccessGranted) + { + var calResult = await SynchronizationManager.Instance.SynchronizeCalendarAsync(new CalendarSynchronizationOptions + { + AccountId = _createdAccount.Id, + Type = CalendarSynchronizationType.CalendarMetadata + }); + if (calResult == null || calResult.CompletedState != SynchronizationCompletedState.Success) + throw new Exception(Translator.Exception_FailedToSynchronizeCalendarMetadata); + } + SetCurrentStepSucceeded(); + + // Step: Aliases + SetStepInProgress(Translator.AccountSetup_Step_SyncingAliases); + if (_createdAccount.IsAliasSyncSupported) + { + var aliasResult = await SynchronizationManager.Instance.SynchronizeAliasesAsync(_createdAccount.Id); + if (aliasResult.CompletedState != SynchronizationCompletedState.Success) + throw new Exception(Translator.Exception_FailedToSynchronizeAliases); + } + else + { + await _accountService.CreateRootAliasAsync(_createdAccount.Id, _createdAccount.Address); + } + SetCurrentStepSucceeded(); + } + else if (WizardContext.IsSpecialImapProvider) + { + var dialogResult = WizardContext.BuildAccountCreationDialogResult(); + + customServerInformation = _specialImapProviderConfigResolver.GetServerInformation(_createdAccount, dialogResult); + if (customServerInformation == null) throw new Exception("Failed to resolve server information."); + + customServerInformation.Id = Guid.NewGuid(); + customServerInformation.AccountId = _createdAccount.Id; + + _createdAccount.Address = WizardContext.EmailAddress; + _createdAccount.SenderName = WizardContext.DisplayName; + _createdAccount.IsCalendarAccessGranted = customServerInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled; + _createdAccount.ServerInformation = customServerInformation; + + // Step: Test IMAP + SetStepInProgress(Translator.AccountSetup_Step_TestingMailAuth); + await ValidateImapConnectivityAsync(customServerInformation); + SetCurrentStepSucceeded(); + + // Step: CalDAV discovery and testing (if applicable) + if (customServerInformation.CalendarSupportMode == ImapCalendarSupportMode.CalDav) + { + SetStepInProgress(Translator.AccountSetup_Step_DiscoveringCalDav); + SetCurrentStepSucceeded(); + + SetStepInProgress(Translator.AccountSetup_Step_TestingCalendarAuth); + await ValidateCalDavConnectivityAsync(customServerInformation); + SetCurrentStepSucceeded(); + } + + // Step: Save to DB + SetStepInProgress(Translator.AccountSetup_Step_SavingAccount); + await _accountService.CreateAccountAsync(_createdAccount, customServerInformation); + _dbWritten = true; + SetCurrentStepSucceeded(); + + // Step: Folders + SetStepInProgress(Translator.AccountSetup_Step_SyncingFolders); + var folderResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(_createdAccount.Id); + if (folderResult == null || folderResult.CompletedState != SynchronizationCompletedState.Success) + throw new Exception(Translator.Exception_FailedToSynchronizeFolders); + SetCurrentStepSucceeded(); + + // Step: Calendar metadata (if not disabled) + if (_createdAccount.IsCalendarAccessGranted) + { + SetStepInProgress(Translator.AccountSetup_Step_FetchingCalendarMetadata); + var calResult = await SynchronizationManager.Instance.SynchronizeCalendarAsync(new CalendarSynchronizationOptions + { + AccountId = _createdAccount.Id, + Type = CalendarSynchronizationType.CalendarMetadata + }); + if (calResult == null || calResult.CompletedState != SynchronizationCompletedState.Success) + throw new Exception(Translator.Exception_FailedToSynchronizeCalendarMetadata); + SetCurrentStepSucceeded(); + } + + // Aliases for IMAP + await _accountService.CreateRootAliasAsync(_createdAccount.Id, _createdAccount.Address); + } + else // Generic IMAP + { + var setupResult = WizardContext.ImapCalDavSetupResult + ?? throw new Exception("IMAP setup was not completed."); + + customServerInformation = setupResult.ServerInformation + ?? throw new Exception("Server information is missing."); + + customServerInformation.Id = Guid.NewGuid(); + customServerInformation.AccountId = _createdAccount.Id; + + _createdAccount.Address = setupResult.EmailAddress; + _createdAccount.SenderName = setupResult.DisplayName; + _createdAccount.IsCalendarAccessGranted = setupResult.IsCalendarAccessGranted; + _createdAccount.ServerInformation = customServerInformation; + + // Step: Save to DB + SetStepInProgress(Translator.AccountSetup_Step_SavingAccount); + await _accountService.CreateAccountAsync(_createdAccount, customServerInformation); + _dbWritten = true; + SetCurrentStepSucceeded(); + + // Step: Folders + SetStepInProgress(Translator.AccountSetup_Step_SyncingFolders); + var folderResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(_createdAccount.Id); + if (folderResult == null || folderResult.CompletedState != SynchronizationCompletedState.Success) + throw new Exception(Translator.Exception_FailedToSynchronizeFolders); + SetCurrentStepSucceeded(); + + // Step: CalDAV (if applicable) + if (setupResult.IsCalendarAccessGranted && + customServerInformation.CalendarSupportMode == ImapCalendarSupportMode.CalDav) + { + SetStepInProgress(Translator.AccountSetup_Step_DiscoveringCalDav); + SetCurrentStepSucceeded(); + + SetStepInProgress(Translator.AccountSetup_Step_TestingCalendarAuth); + await ValidateCalDavConnectivityAsync(customServerInformation); + SetCurrentStepSucceeded(); + } + + // Step: Calendar metadata + if (setupResult.IsCalendarAccessGranted) + { + SetStepInProgress(Translator.AccountSetup_Step_FetchingCalendarMetadata); + var calResult = await SynchronizationManager.Instance.SynchronizeCalendarAsync(new CalendarSynchronizationOptions + { + AccountId = _createdAccount.Id, + Type = CalendarSynchronizationType.CalendarMetadata + }); + if (calResult == null || calResult.CompletedState != SynchronizationCompletedState.Success) + throw new Exception(Translator.Exception_FailedToSynchronizeCalendarMetadata); + SetCurrentStepSucceeded(); + } + + // Aliases for IMAP + await _accountService.CreateRootAliasAsync(_createdAccount.Id, _createdAccount.Address); + } + + // Step: Finalizing + SetStepInProgress(Translator.AccountSetup_Step_Finalizing); + SetCurrentStepSucceeded(); + + IsSetupComplete = true; + + // Notify listeners — this triggers ShellWindow creation from App.xaml.cs + Messenger.Send(new AccountCreatedMessage(_createdAccount)); + } + catch (AccountSetupCanceledException) + { + // User canceled authentication — go back silently, no error UI + Messenger.Send(new BackBreadcrumNavigationRequested(NavigationTransitionEffect.FromLeft)); + } + catch (Exception ex) when (ex.Message.Contains(nameof(AccountSetupCanceledException))) + { + // Wrapped cancellation — same silent behavior + Messenger.Send(new BackBreadcrumNavigationRequested(NavigationTransitionEffect.FromLeft)); + } + catch (Exception ex) + { + Log.Error(ex, "Account setup failed."); + + SetCurrentStepFailed(ex.Message); + IsSetupFailed = true; + FailureMessage = Translator.AccountSetup_FailureMessage; + + // Rollback if DB write happened + if (_dbWritten && _createdAccount != null) + { + try + { + await _accountService.DeleteAccountAsync(_createdAccount); + } + catch (Exception deleteEx) + { + Log.Error(deleteEx, "Failed to rollback account creation."); + } + + _dbWritten = false; + } + } + } + + private async Task ValidateImapConnectivityAsync(CustomServerInformation serverInformation) + { + var connectivityResult = await SynchronizationManager.Instance + .TestImapConnectivityAsync(serverInformation, allowSSLHandshake: false); + + if (connectivityResult.IsCertificateUIRequired) + { + var certificateMessage = + $"{Translator.IMAPSetupDialog_CertificateAllowanceRequired_Row0}\n\n" + + $"{Translator.IMAPSetupDialog_CertificateIssuer}: {connectivityResult.CertificateIssuer}\n" + + $"{Translator.IMAPSetupDialog_CertificateValidFrom}: {connectivityResult.CertificateValidFromDateString}\n" + + $"{Translator.IMAPSetupDialog_CertificateValidTo}: {connectivityResult.CertificateExpirationDateString}\n\n" + + $"{Translator.IMAPSetupDialog_CertificateAllowanceRequired_Row1}"; + + var allowCertificate = await _dialogService.ShowConfirmationDialogAsync( + certificateMessage, + Translator.GeneralTitle_Warning, + Translator.Buttons_Allow); + + if (!allowCertificate) + throw new InvalidOperationException(Translator.IMAPSetupDialog_CertificateDenied); + + connectivityResult = await SynchronizationManager.Instance + .TestImapConnectivityAsync(serverInformation, allowSSLHandshake: true); + } + + if (!connectivityResult.IsSuccess) + throw new InvalidOperationException(connectivityResult.FailedReason ?? Translator.IMAPSetupDialog_ConnectionFailedMessage); + } + + private async Task ValidateCalDavConnectivityAsync(CustomServerInformation serverInformation) + { + if (string.IsNullOrWhiteSpace(serverInformation.CalDavServiceUrl)) + throw new InvalidOperationException(Translator.ImapCalDavSettingsPage_CalDavUrlRequired); + + var settings = new CalDavConnectionSettings + { + ServiceUri = new Uri(serverInformation.CalDavServiceUrl, UriKind.Absolute), + Username = serverInformation.CalDavUsername, + Password = serverInformation.CalDavPassword + }; + + await _calDavClient.DiscoverCalendarsAsync(settings); + } + + [RelayCommand] + private void GoBack() + { + Messenger.Send(new BackBreadcrumNavigationRequested(NavigationTransitionEffect.FromLeft)); + } + + [RelayCommand] + private async Task TryAgainAsync() + { + await RunSetupAsync(); + } +} diff --git a/Wino.Mail.ViewModels/AliasManagementPageViewModel.cs b/Wino.Mail.ViewModels/AliasManagementPageViewModel.cs index 71b1f1bc..7b1a3071 100644 --- a/Wino.Mail.ViewModels/AliasManagementPageViewModel.cs +++ b/Wino.Mail.ViewModels/AliasManagementPageViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -12,7 +13,7 @@ using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Synchronization; -using Wino.Messaging.Server; +using Wino.Core.Services; namespace Wino.Mail.ViewModels; @@ -20,7 +21,7 @@ public partial class AliasManagementPageViewModel : MailBaseViewModel { private readonly IMailDialogService _dialogService; private readonly IAccountService _accountService; - private readonly IWinoServerConnectionManager _winoServerConnectionManager; + private readonly ISmimeCertificateService _smimeCertificateService; [ObservableProperty] [NotifyPropertyChangedFor(nameof(CanSynchronizeAliases))] @@ -33,11 +34,11 @@ public partial class AliasManagementPageViewModel : MailBaseViewModel public AliasManagementPageViewModel(IMailDialogService dialogService, IAccountService accountService, - IWinoServerConnectionManager winoServerConnectionManager) + ISmimeCertificateService smimeCertificateService) { _dialogService = dialogService; _accountService = accountService; - _winoServerConnectionManager = winoServerConnectionManager; + _smimeCertificateService = smimeCertificateService; } public override async void OnNavigatedTo(NavigationMode mode, object parameters) @@ -54,7 +55,22 @@ public partial class AliasManagementPageViewModel : MailBaseViewModel private async Task LoadAliasesAsync() { - AccountAliases = await _accountService.GetAccountAliasesAsync(Account.Id); + var aliases = await _accountService.GetAccountAliasesAsync(Account.Id); + foreach (var alias in aliases) + { + alias.Certificates.Clear(); + alias.Certificates.Add(null); // First blank optioon + var certs = _smimeCertificateService.GetCertificates() + .Where(cert => cert.Subject.Contains(alias.AliasAddress, StringComparison.OrdinalIgnoreCase)) + .ToList(); + foreach (var cert in certs) + alias.Certificates.Add(cert); + + alias.SelectedSigningCertificate = !string.IsNullOrEmpty(alias.SelectedSigningCertificateThumbprint) + ? alias.Certificates.FirstOrDefault(c => c?.Thumbprint == alias.SelectedSigningCertificateThumbprint) + : null; + } + AccountAliases = aliases; } [RelayCommand] @@ -82,12 +98,12 @@ public partial class AliasManagementPageViewModel : MailBaseViewModel Type = MailSynchronizationType.Alias }; - var aliasSyncResponse = await _winoServerConnectionManager.GetResponseAsync(new NewMailSynchronizationRequested(aliasSyncOptions, SynchronizationSource.Client)); + var aliasSyncResult = await SynchronizationManager.Instance.SynchronizeAliasesAsync(Account.Id); - if (aliasSyncResponse.IsSuccess) + if (aliasSyncResult.CompletedState == SynchronizationCompletedState.Success) await LoadAliasesAsync(); else - _dialogService.InfoBarMessage(Translator.GeneralTitle_Error, aliasSyncResponse.Message, InfoBarMessageType.Error); + _dialogService.InfoBarMessage(Translator.GeneralTitle_Error, "Failed to synchronize aliases", InfoBarMessageType.Error); } [RelayCommand] @@ -151,4 +167,20 @@ public partial class AliasManagementPageViewModel : MailBaseViewModel await _accountService.DeleteAccountAliasAsync(alias.Id); await LoadAliasesAsync(); } + + public async Task SetAliasSmimeEncryption(MailAccountAlias alias, bool value) + { + alias.IsSmimeEncryptionEnabled = value; + await _accountService.UpdateAccountAliasesAsync(Account.Id, AccountAliases); + await LoadAliasesAsync(); + } + + public async Task SetSelectedSigningCertificate(MailAccountAlias alias, X509Certificate2 cert) + { + alias.SelectedSigningCertificate = cert; + alias.SelectedSigningCertificateThumbprint = cert?.Thumbprint; + + await _accountService.UpdateAccountAliasesAsync(Account.Id, AccountAliases); + await LoadAliasesAsync(); + } } diff --git a/Wino.Mail.ViewModels/AppPreferencesPageViewModel.cs b/Wino.Mail.ViewModels/AppPreferencesPageViewModel.cs index 3a7489d0..4c6ab537 100644 --- a/Wino.Mail.ViewModels/AppPreferencesPageViewModel.cs +++ b/Wino.Mail.ViewModels/AppPreferencesPageViewModel.cs @@ -1,92 +1,31 @@ -using System.Collections.Generic; -using System.ComponentModel; +using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Wino.Core.Domain; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Ai; using Wino.Core.Domain.Models.Navigation; -using Wino.Messaging.Server; +using Wino.Core.Domain.Models.Translations; namespace Wino.Mail.ViewModels; public partial class AppPreferencesPageViewModel : MailBaseViewModel { - public IPreferencesService PreferencesService { get; } - - [ObservableProperty] - private List _appTerminationBehavior; - - [ObservableProperty] - public partial List SearchModes { get; set; } - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(IsStartupBehaviorDisabled))] - [NotifyPropertyChangedFor(nameof(IsStartupBehaviorEnabled))] - private StartupBehaviorResult startupBehaviorResult; - - private int _emailSyncIntervalMinutes; - public int EmailSyncIntervalMinutes - { - get => _emailSyncIntervalMinutes; - set - { - SetProperty(ref _emailSyncIntervalMinutes, value); - - PreferencesService.EmailSyncIntervalMinutes = value; - } - } - - public bool IsStartupBehaviorDisabled => !IsStartupBehaviorEnabled; - public bool IsStartupBehaviorEnabled => StartupBehaviorResult == StartupBehaviorResult.Enabled; - - private string _selectedAppTerminationBehavior; - public string SelectedAppTerminationBehavior - { - get => _selectedAppTerminationBehavior; - set - { - SetProperty(ref _selectedAppTerminationBehavior, value); - - PreferencesService.ServerTerminationBehavior = (ServerBackgroundMode)AppTerminationBehavior.IndexOf(value); - } - } - - private string _selectedDefaultSearchMode; - public string SelectedDefaultSearchMode - { - get => _selectedDefaultSearchMode; - set - { - SetProperty(ref _selectedDefaultSearchMode, value); - - PreferencesService.DefaultSearchMode = (SearchMode)SearchModes.IndexOf(value); - } - } - - private readonly IMailDialogService _dialogService; - private readonly IWinoServerConnectionManager _winoServerConnectionManager; - private readonly IStartupBehaviorService _startupBehaviorService; - - public AppPreferencesPageViewModel(IMailDialogService dialogService, - IPreferencesService preferencesService, - IWinoServerConnectionManager winoServerConnectionManager, - IStartupBehaviorService startupBehaviorService) + public AppPreferencesPageViewModel( + IMailDialogService dialogService, + IPreferencesService preferencesService, + IStartupBehaviorService startupBehaviorService, + ITranslationService translationService, + IAiActionOptionsService aiActionOptionsService) { _dialogService = dialogService; PreferencesService = preferencesService; - _winoServerConnectionManager = winoServerConnectionManager; _startupBehaviorService = startupBehaviorService; - - // Load the app termination behavior options - - _appTerminationBehavior = - [ - Translator.SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Title, // "Minimize to tray" - Translator.SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title, // "Invisible" - Translator.SettingsAppPreferences_ServerBackgroundingMode_Terminate_Title // "Terminate" - ]; + _translationService = translationService; + _aiActionOptionsService = aiActionOptionsService; SearchModes = [ @@ -94,22 +33,137 @@ public partial class AppPreferencesPageViewModel : MailBaseViewModel Translator.SettingsAppPreferences_SearchMode_Online ]; - SelectedAppTerminationBehavior = _appTerminationBehavior[(int)PreferencesService.ServerTerminationBehavior]; + ApplicationModes = + [ + Translator.SettingsAppPreferences_ApplicationMode_Mail, + Translator.SettingsAppPreferences_ApplicationMode_Calendar, + Translator.ContactsPage_Title, + Translator.MenuSettings + ]; + SelectedDefaultSearchMode = SearchModes[(int)PreferencesService.DefaultSearchMode]; + SelectedDefaultApplicationMode = ApplicationModes[(int)PreferencesService.DefaultApplicationMode]; EmailSyncIntervalMinutes = PreferencesService.EmailSyncIntervalMinutes; + SummarySavePath = PreferencesService.AiSummarySavePath; + } + + public IPreferencesService PreferencesService { get; } + + [ObservableProperty] + public partial List SearchModes { get; set; } + + [ObservableProperty] + public partial List ApplicationModes { get; set; } + + [ObservableProperty] + public partial List AvailableLanguages { get; set; } = []; + + [ObservableProperty] + public partial AppLanguageModel SelectedLanguage { get; set; } + + [ObservableProperty] + public partial List AvailableAiLanguages { get; set; } = []; + + [ObservableProperty] + public partial AiTranslateLanguageOption SelectedDefaultTranslationLanguage { get; set; } + + [ObservableProperty] + public partial AiTranslateLanguageOption SelectedSummarizeLanguage { get; set; } + + [ObservableProperty] + public partial string SummarySavePath { get; set; } = string.Empty; + + [ObservableProperty] + public partial bool HasInvalidSummarySavePath { get; set; } + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsStartupBehaviorDisabled))] + [NotifyPropertyChangedFor(nameof(IsStartupBehaviorEnabled))] + private StartupBehaviorResult startupBehaviorResult; + + private readonly IMailDialogService _dialogService; + private readonly IStartupBehaviorService _startupBehaviorService; + private readonly ITranslationService _translationService; + private readonly IAiActionOptionsService _aiActionOptionsService; + private bool _isLanguageInitialized; + private bool _isAiPreferencesInitialized; + private int _emailSyncIntervalMinutes; + private string _selectedDefaultSearchMode; + private string _selectedDefaultApplicationMode; + + public int EmailSyncIntervalMinutes + { + get => _emailSyncIntervalMinutes; + set + { + SetProperty(ref _emailSyncIntervalMinutes, value); + PreferencesService.EmailSyncIntervalMinutes = value; + } + } + + public bool IsStartupBehaviorDisabled => !IsStartupBehaviorEnabled; + public bool IsStartupBehaviorEnabled => StartupBehaviorResult == StartupBehaviorResult.Enabled; + + public string SelectedDefaultSearchMode + { + get => _selectedDefaultSearchMode; + set + { + SetProperty(ref _selectedDefaultSearchMode, value); + PreferencesService.DefaultSearchMode = (SearchMode)SearchModes.IndexOf(value); + } + } + + public string SelectedDefaultApplicationMode + { + get => _selectedDefaultApplicationMode; + set + { + SetProperty(ref _selectedDefaultApplicationMode, value); + PreferencesService.DefaultApplicationMode = (WinoApplicationMode)ApplicationModes.IndexOf(value); + } + } + + partial void OnSelectedLanguageChanged(AppLanguageModel value) + { + if (!_isLanguageInitialized || value == null) + return; + + _ = _translationService.InitializeLanguageAsync(value.Language); + } + + partial void OnSelectedDefaultTranslationLanguageChanged(AiTranslateLanguageOption value) + { + if (!_isAiPreferencesInitialized || value == null) + return; + + PreferencesService.AiDefaultTranslationLanguageCode = value.Code; + } + + partial void OnSelectedSummarizeLanguageChanged(AiTranslateLanguageOption value) + { + if (!_isAiPreferencesInitialized || value == null) + return; + + PreferencesService.AiSummarizeLanguageCode = value.Code; + } + + partial void OnSummarySavePathChanged(string value) + { + if (!_isAiPreferencesInitialized) + return; + + PreferencesService.AiSummarySavePath = value ?? string.Empty; + RefreshSummarySavePathState(); } [RelayCommand] private async Task ToggleStartupBehaviorAsync() { if (IsStartupBehaviorEnabled) - { await DisableStartupAsync(); - } else - { await EnableStartupAsync(); - } OnPropertyChanged(nameof(IsStartupBehaviorEnabled)); } @@ -117,14 +171,12 @@ public partial class AppPreferencesPageViewModel : MailBaseViewModel private async Task EnableStartupAsync() { StartupBehaviorResult = await _startupBehaviorService.ToggleStartupBehavior(true); - NotifyCurrentStartupState(); } private async Task DisableStartupAsync() { StartupBehaviorResult = await _startupBehaviorService.ToggleStartupBehavior(false); - NotifyCurrentStartupState(); } @@ -152,25 +204,56 @@ public partial class AppPreferencesPageViewModel : MailBaseViewModel } } - protected override async void OnPropertyChanged(PropertyChangedEventArgs e) + [RelayCommand] + private async Task BrowseSummarySavePathAsync() { - base.OnPropertyChanged(e); + var pickedPath = await _dialogService.PickWindowsFolderAsync(); + if (string.IsNullOrWhiteSpace(pickedPath)) + return; - if (e.PropertyName == nameof(SelectedAppTerminationBehavior)) - { - var terminationModeChangedResult = await _winoServerConnectionManager.GetResponseAsync(new ServerTerminationModeChanged(PreferencesService.ServerTerminationBehavior)); - - if (!terminationModeChangedResult.IsSuccess) - { - _dialogService.InfoBarMessage(Translator.GeneralTitle_Error, terminationModeChangedResult.Message, InfoBarMessageType.Error); - } - } + await ExecuteUIThread(() => SummarySavePath = pickedPath); } public override async void OnNavigatedTo(NavigationMode mode, object parameters) { base.OnNavigatedTo(mode, parameters); - StartupBehaviorResult = await _startupBehaviorService.GetCurrentStartupBehaviorAsync(); + var availableLanguages = _translationService.GetAvailableLanguages(); + var availableAiLanguages = new List(_aiActionOptionsService.GetTranslateLanguageOptions()); + var startupBehaviorResult = await _startupBehaviorService.GetCurrentStartupBehaviorAsync(); + + await ExecuteUIThread(() => + { + AvailableLanguages = availableLanguages; + SelectedLanguage = AvailableLanguages.Find(language => language.Language == PreferencesService.CurrentLanguage) + ?? (AvailableLanguages.Count > 0 ? AvailableLanguages[0] : null); + _isLanguageInitialized = true; + + AvailableAiLanguages = availableAiLanguages; + SelectedDefaultTranslationLanguage = FindAiLanguageOption(PreferencesService.AiDefaultTranslationLanguageCode) + ?? FindAiLanguageOption("en-US") + ?? (AvailableAiLanguages.Count > 0 ? AvailableAiLanguages[0] : null); + SelectedSummarizeLanguage = FindAiLanguageOption(PreferencesService.AiSummarizeLanguageCode) + ?? FindAiLanguageOption("en-US") + ?? (AvailableAiLanguages.Count > 0 ? AvailableAiLanguages[0] : null); + SummarySavePath = PreferencesService.AiSummarySavePath; + RefreshSummarySavePathState(); + _isAiPreferencesInitialized = true; + + StartupBehaviorResult = startupBehaviorResult; + }); + } + + private AiTranslateLanguageOption FindAiLanguageOption(string languageCode) + { + if (string.IsNullOrWhiteSpace(languageCode)) + return null; + + return AvailableAiLanguages.Find(option => option.Code == languageCode); + } + + private void RefreshSummarySavePathState() + { + HasInvalidSummarySavePath = !string.IsNullOrWhiteSpace(SummarySavePath) && !Directory.Exists(SummarySavePath); } } diff --git a/Wino.Mail.ViewModels/Collections/GroupHeaders.cs b/Wino.Mail.ViewModels/Collections/GroupHeaders.cs new file mode 100644 index 00000000..834d9064 --- /dev/null +++ b/Wino.Mail.ViewModels/Collections/GroupHeaders.cs @@ -0,0 +1,79 @@ +using System; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Wino.Mail.ViewModels.Collections; + +/// +/// Base class for group headers in the flat collection +/// +public abstract partial class GroupHeaderBase : ObservableObject +{ + [ObservableProperty] + private int itemCount; + + [ObservableProperty] + private int unreadCount; + + protected GroupHeaderBase(string key, string displayName) + { + Key = key; + DisplayName = displayName; + } + + /// + /// The unique key for this group (used for sorting and identification) + /// + public string Key { get; } + + /// + /// The display name shown in the UI + /// + public string DisplayName { get; } +} + +/// +/// Group header for date-based grouping +/// +public partial class DateGroupHeader : GroupHeaderBase +{ + public DateGroupHeader(DateTime date) : base(date.ToString("yyyy-MM-dd"), FormatDisplayName(date)) + { + Date = date; + } + + /// + /// The date this group represents + /// + public DateTime Date { get; } + + private static string FormatDisplayName(DateTime date) + { + var today = DateTime.Today; + var yesterday = today.AddDays(-1); + + return date.Date switch + { + var d when d == today => "Today", + var d when d == yesterday => "Yesterday", + var d when d >= today.AddDays(-7) => date.ToString("dddd"), // This week + var d when d.Year == today.Year => date.ToString("MMMM dd"), // This year + _ => date.ToString("MMMM dd, yyyy") // Other years + }; + } +} + +/// +/// Group header for sender name-based grouping +/// +public partial class SenderGroupHeader : GroupHeaderBase +{ + public SenderGroupHeader(string senderName) : base(senderName, senderName) + { + SenderName = senderName; + } + + /// + /// The sender name this group represents + /// + public string SenderName { get; } +} diff --git a/Wino.Mail.ViewModels/Collections/ThreadingManager.cs b/Wino.Mail.ViewModels/Collections/ThreadingManager.cs deleted file mode 100644 index 471cf9af..00000000 --- a/Wino.Mail.ViewModels/Collections/ThreadingManager.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Wino.Core.Domain.Entities.Mail; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.MailItem; - -namespace Wino.Mail.ViewModels.Collections; - -internal class ThreadingManager -{ - private readonly IThreadingStrategyProvider _threadingStrategyProvider; - - public ThreadingManager(IThreadingStrategyProvider threadingStrategyProvider) - { - _threadingStrategyProvider = threadingStrategyProvider; - } - - public bool ShouldThread(MailCopy newItem, IMailItem existingItem) - { - if (_threadingStrategyProvider == null) return false; - - var strategy = _threadingStrategyProvider.GetStrategy(newItem.AssignedAccount.ProviderType); - return strategy?.ShouldThreadWithItem(newItem, existingItem) ?? false; - } - - public ThreadMailItem CreateNewThread(IMailItem existingItem, MailCopy newItem) - { - var thread = new ThreadMailItem(); - thread.AddThreadItem(existingItem); - thread.AddThreadItem(newItem); - return thread; - } -} diff --git a/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs b/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs index 3425bcfa..2d4f804a 100644 --- a/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs +++ b/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs @@ -1,43 +1,75 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.Collections; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Messaging; +using MoreLinq.Extensions; using Serilog; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Comparers; -using Wino.Core.Domain.Models.MailItem; using Wino.Mail.ViewModels.Data; +using Wino.Messaging.Client.Mails; namespace Wino.Mail.ViewModels.Collections; -public class WinoMailCollection +public class WinoMailCollection : ObservableRecipient, IRecipient { // We cache each mail copy id for faster access on updates. // If the item provider here for update or removal doesn't exist here // we can ignore the operation. - public HashSet MailCopyIdHashSet = []; + public ConcurrentDictionary MailCopyIdHashSet = []; - public event EventHandler MailItemRemoved; + // Cache ThreadIds to quickly find items that should be threaded together + private readonly ConcurrentDictionary> _threadIdToItemsMap = new(); - private ListItemComparer listComparer = new ListItemComparer(); + // Cache item to group mapping for faster lookups + private readonly ConcurrentDictionary> _itemToGroupMap = new(); - private readonly ObservableGroupedCollection _mailItemSource = new ObservableGroupedCollection(); + // Cache uniqueId to MailItemViewModel for faster GetMailItemContainer lookups + private readonly ConcurrentDictionary _uniqueIdToMailItemMap = new(); - public ReadOnlyObservableGroupedCollection MailItems { get; } + // Cache uniqueId to ThreadMailItemViewModel for O(1) thread membership checks + private readonly ConcurrentDictionary _uniqueIdToThreadMap = new(); + + public event EventHandler MailItemRemoved; + public event EventHandler ItemSelectionChanged; + + private ListItemComparer listComparer = new(); + + private readonly ObservableGroupedCollection _mailItemSource = new ObservableGroupedCollection(); + + public ReadOnlyObservableGroupedCollection MailItems { get; } + + private SortingOptionType _sortingType; /// /// Property that defines how the item sorting should be done in the collection. /// - public SortingOptionType SortingType { get; set; } + public SortingOptionType SortingType + { + get => _sortingType; + set + { + _sortingType = value; + // Update the comparer's sort mode when sorting type changes + listComparer.SortByName = value == SortingOptionType.Sender; + } + } /// - /// Threading strategy that will help thread items according to the account type. + /// Gets or sets the grouping type for emails. + /// Note: WinoMailCollection groups automatically on the UI, so this just affects the grouping key logic. /// - public IThreadingStrategyProvider ThreadingStrategyProvider { get; set; } + public EmailGroupingType GroupingType + { + get => SortingType == SortingOptionType.ReceiveDate ? EmailGroupingType.ByDate : EmailGroupingType.ByFromName; + set => SortingType = value == EmailGroupingType.ByDate ? SortingOptionType.ReceiveDate : SortingOptionType.Sender; + } /// /// Automatically deletes single mail items after the delete operation or thread->single transition. @@ -47,19 +79,56 @@ public class WinoMailCollection public int Count => _mailItemSource.Count; - public IDispatcher CoreDispatcher { get; set; } - - private readonly ThreadingManager _threadingManager; - - public WinoMailCollection(IThreadingStrategyProvider threadingStrategyProvider) + public bool IsAllSelected { - _threadingManager = new ThreadingManager(threadingStrategyProvider); - MailItems = new ReadOnlyObservableGroupedCollection(_mailItemSource); + get + { + return AllItemsCount == SelectedItemsCount; + } } - public void Clear() => _mailItemSource.Clear(); + public IDispatcher CoreDispatcher { get; set; } - private object GetGroupingKey(IMailItem mailItem) + public WinoMailCollection() + { + MailItems = new ReadOnlyObservableGroupedCollection(_mailItemSource); + + // Initialize sorting type to default (date-based) + SortingType = SortingOptionType.ReceiveDate; + + Messenger.Register(this); + } + + public void Cleanup() + { + Messenger.Unregister(this); + } + + public async Task ClearAsync() + { + await ExecuteUIThread(() => + { + foreach (var group in _mailItemSource) + { + foreach (var item in group) + { + if (item is ThreadMailItemViewModel threadItem) + { + threadItem.UnregisterThreadEmailPropertyChangedHandlers(); + } + } + } + + _mailItemSource.Clear(); + MailCopyIdHashSet.Clear(); + _threadIdToItemsMap.Clear(); + _itemToGroupMap.Clear(); + _uniqueIdToMailItemMap.Clear(); + _uniqueIdToThreadMap.Clear(); + }); + } + + private object GetGroupingKey(IMailListItem mailItem) { if (SortingType == SortingOptionType.ReceiveDate) return mailItem.CreationDate.ToLocalTime().Date; @@ -69,68 +138,254 @@ public class WinoMailCollection private void UpdateUniqueIdHashes(IMailHashContainer itemContainer, bool isAdd) { - foreach (var item in itemContainer.GetContainingIds()) + if (isAdd) { - if (isAdd) + if (itemContainer is MailItemViewModel mailItemVM) { - MailCopyIdHashSet.Add(item); + MailCopyIdHashSet.TryAdd(mailItemVM.MailCopy.UniqueId, true); + _uniqueIdToMailItemMap[mailItemVM.MailCopy.UniqueId] = mailItemVM; } - else + else if (itemContainer is ThreadMailItemViewModel threadVM) { - MailCopyIdHashSet.Remove(item); + foreach (var email in threadVM.ThreadEmails) + { + MailCopyIdHashSet.TryAdd(email.MailCopy.UniqueId, true); + _uniqueIdToMailItemMap[email.MailCopy.UniqueId] = email; + _uniqueIdToThreadMap[email.MailCopy.UniqueId] = threadVM; + } } } - } - - private void InsertItemInternal(object groupKey, IMailItem mailItem) - { - UpdateUniqueIdHashes(mailItem, true); - - if (mailItem is MailCopy mailCopy) - { - _mailItemSource.InsertItem(groupKey, listComparer, new MailItemViewModel(mailCopy), listComparer.GetItemComparer()); - } - else if (mailItem is ThreadMailItem threadMailItem) - { - _mailItemSource.InsertItem(groupKey, listComparer, new ThreadMailItemViewModel(threadMailItem), listComparer.GetItemComparer()); - } else { - _mailItemSource.InsertItem(groupKey, listComparer, mailItem, listComparer.GetItemComparer()); + foreach (var id in itemContainer.GetContainingIds()) + { + MailCopyIdHashSet.TryRemove(id, out _); + _uniqueIdToMailItemMap.TryRemove(id, out _); + _uniqueIdToThreadMap.TryRemove(id, out _); + } } } - private void RemoveItemInternal(ObservableGroup group, IMailItem mailItem) + private void UpdateThreadIdCache(IMailListItem item, bool isAdd) + { + var threadIds = GetThreadIdsFromItem(item); + + foreach (var threadId in threadIds) + { + if (string.IsNullOrEmpty(threadId)) continue; + + if (isAdd) + { + var list = _threadIdToItemsMap.GetOrAdd(threadId, _ => new List()); + list.Add(item); + } + else + { + if (_threadIdToItemsMap.TryGetValue(threadId, out var list)) + { + list.Remove(item); + if (list.Count == 0) + { + _threadIdToItemsMap.TryRemove(threadId, out _); + } + } + } + } + } + + private IEnumerable GetThreadIdsFromItem(IMailListItem item) + { + if (item is MailItemViewModel mailItem && !string.IsNullOrEmpty(mailItem.MailCopy.ThreadId)) + { + yield return mailItem.MailCopy.ThreadId; + } + else if (item is ThreadMailItemViewModel threadItem) + { + var uniqueThreadIds = threadItem.ThreadEmails + .Where(e => !string.IsNullOrEmpty(e.MailCopy.ThreadId)) + .Select(e => e.MailCopy.ThreadId) + .Distinct(); + + foreach (var threadId in uniqueThreadIds) + { + yield return threadId; + } + } + } + + private IMailListItem FindThreadableItem(string threadId, Guid? excludedUniqueId = null, IMailListItem excludedItem = null) + { + if (string.IsNullOrEmpty(threadId) || !_threadIdToItemsMap.TryGetValue(threadId, out var items)) + { + return null; + } + + foreach (var item in items) + { + if (ReferenceEquals(item, excludedItem)) + { + continue; + } + + if (excludedUniqueId.HasValue) + { + if (item is MailItemViewModel mailItem && mailItem.MailCopy.UniqueId == excludedUniqueId.Value) + { + continue; + } + + if (item is ThreadMailItemViewModel threadItem && threadItem.HasUniqueId(excludedUniqueId.Value)) + { + continue; + } + } + + return item; + } + + return null; + } + + /// + /// Checks if a ThreadId exists in the collection. + /// + /// The ThreadId to check for. + /// True if the ThreadId exists in the collection, false otherwise. + public bool ContainsThreadId(string threadId) + { + return !string.IsNullOrEmpty(threadId) && _threadIdToItemsMap.ContainsKey(threadId); + } + + /// + /// Checks whether a mail with the given UniqueId currently exists in this collection. + /// + public bool ContainsMailUniqueId(Guid uniqueId) => MailCopyIdHashSet.ContainsKey(uniqueId); + + /// + /// Finds a MailItemViewModel by its UniqueId, searching through all items including those inside threads. + /// + /// The UniqueId of the mail item to find. + /// The MailItemViewModel if found, otherwise null. + public MailItemViewModel Find(Guid uniqueId) + { + // Fast path: check the cache for O(1) lookup + if (_uniqueIdToMailItemMap.TryGetValue(uniqueId, out var cachedMailItem)) + { + return cachedMailItem; + } + + // Fallback: scan all groups and populate caches + foreach (var group in _mailItemSource) + { + foreach (var item in group) + { + if (item is MailItemViewModel mailItem && mailItem.MailCopy.UniqueId == uniqueId) + { + _uniqueIdToMailItemMap[uniqueId] = mailItem; + return mailItem; + } + else if (item is ThreadMailItemViewModel threadItem) + { + var foundInThread = threadItem.ThreadEmails.FirstOrDefault(e => e.MailCopy.UniqueId == uniqueId); + if (foundInThread != null) + { + _uniqueIdToMailItemMap[uniqueId] = foundInThread; + _uniqueIdToThreadMap[uniqueId] = threadItem; + return foundInThread; + } + } + } + } + + return null; + } + + private async Task InsertItemInternalAsync(object groupKey, IMailListItem mailItem) + { + UpdateUniqueIdHashes(mailItem, true); + UpdateThreadIdCache(mailItem, true); + await ExecuteUIThread(() => + { + _mailItemSource.InsertItem(groupKey, listComparer, mailItem, listComparer); + + // Update item-to-group cache + var group = _mailItemSource.FirstGroupByKeyOrDefault(groupKey); + if (group != null) + { + _itemToGroupMap[mailItem] = group; + } + }); + } + + private async Task RemoveItemInternalAsync(ObservableGroup group, IMailListItem mailItem, bool detachThreadHandlers = true) { UpdateUniqueIdHashes(mailItem, false); + UpdateThreadIdCache(mailItem, false); - MailItemRemoved?.Invoke(this, mailItem); - - group.Remove(mailItem); - - if (group.Count == 0) + if (mailItem is MailItemViewModel singleMailItem) { - _mailItemSource.RemoveGroup(group.Key); + MailItemRemoved?.Invoke(this, singleMailItem); } + else if (mailItem is ThreadMailItemViewModel threadViewModel) + { + foreach (var threadMailItem in threadViewModel.ThreadEmails) + { + MailItemRemoved?.Invoke(this, threadMailItem); + } + + if (detachThreadHandlers) + { + threadViewModel.UnregisterThreadEmailPropertyChangedHandlers(); + } + } + + await ExecuteUIThread(() => + { + group.Remove(mailItem); + + // Remove from item-to-group cache + _itemToGroupMap.TryRemove(mailItem, out _); + + if (group.Count == 0) + { + _mailItemSource.RemoveGroup(group.Key); + } + }); } - private async Task HandleThreadingAsync(ObservableGroup group, IMailItem item, MailCopy addedItem) + private async Task HandleThreadingAsync(ObservableGroup group, IMailListItem item, MailCopy addedItem) { if (item is ThreadMailItemViewModel threadViewModel) { await HandleExistingThreadAsync(group, threadViewModel, addedItem); } - else + else if (item is MailItemViewModel mailViewModel) { - await HandleNewThreadAsync(group, item, addedItem); + await HandleNewThreadAsync(group, mailViewModel, addedItem); } } - private async Task HandleExistingThreadAsync(ObservableGroup group, ThreadMailItemViewModel threadViewModel, MailCopy addedItem) + private async Task HandleExistingThreadAsync(ObservableGroup group, ThreadMailItemViewModel threadViewModel, MailCopy addedItem) { var existingGroupKey = GetGroupingKey(threadViewModel); - await ExecuteUIThread(() => { threadViewModel.AddMailItemViewModel(addedItem); }); + // Update ThreadId cache before modifying the thread + UpdateThreadIdCache(threadViewModel, false); + + var newMailItem = new MailItemViewModel(addedItem); + + await ExecuteUIThread(() => + { + threadViewModel.AddEmail(newMailItem); + }); + + // Update ThreadId cache after modifying the thread + UpdateThreadIdCache(threadViewModel, true); + + // Update caches for the new mail item (use the actual instance, not a throwaway) + MailCopyIdHashSet.TryAdd(addedItem.UniqueId, true); + _uniqueIdToMailItemMap[addedItem.UniqueId] = newMailItem; + _uniqueIdToThreadMap[addedItem.UniqueId] = threadViewModel; var newGroupKey = GetGroupingKey(threadViewModel); @@ -140,17 +395,16 @@ public class WinoMailCollection } else { - await ExecuteUIThread(() => { threadViewModel.NotifyPropertyChanges(); }); + await ExecuteUIThread(() => { threadViewModel.ThreadEmails = threadViewModel.ThreadEmails; }); } - - UpdateUniqueIdHashes(addedItem, true); } - private async Task HandleNewThreadAsync(ObservableGroup group, IMailItem item, MailCopy addedItem) + private async Task HandleNewThreadAsync(ObservableGroup group, MailItemViewModel item, MailCopy addedItem) { - if (item.Id == addedItem.Id) + if (item.MailCopy.UniqueId == addedItem.UniqueId) { - await UpdateExistingItemAsync(item, addedItem); + var existingItemContainer = GetMailItemContainer(addedItem.UniqueId); + await UpdateExistingItemAsync(existingItemContainer, addedItem); } else { @@ -158,121 +412,371 @@ public class WinoMailCollection } } - private async Task MoveThreadToNewGroupAsync(ObservableGroup currentGroup, ThreadMailItemViewModel threadViewModel, object newGroupKey) + private async Task MoveThreadToNewGroupAsync(ObservableGroup currentGroup, ThreadMailItemViewModel threadViewModel, object newGroupKey) { - var mailThreadItems = threadViewModel.GetThreadMailItem(); - - await ExecuteUIThread(() => - { - RemoveItemInternal(currentGroup, threadViewModel); - InsertItemInternal(newGroupKey, new ThreadMailItemViewModel(mailThreadItems)); - }); + await RemoveItemInternalAsync(currentGroup, threadViewModel, detachThreadHandlers: false); + await InsertItemInternalAsync(newGroupKey, threadViewModel); } - private async Task CreateNewThreadAsync(ObservableGroup group, IMailItem item, MailCopy addedItem) + private async Task CreateNewThreadAsync(ObservableGroup group, MailItemViewModel item, MailCopy addedItem) { - var threadMailItem = _threadingManager.CreateNewThread(item, addedItem); - var newGroupKey = GetGroupingKey(threadMailItem); + var threadViewModel = new ThreadMailItemViewModel(item.MailCopy.ThreadId); await ExecuteUIThread(() => { - RemoveItemInternal(group, item); - InsertItemInternal(newGroupKey, threadMailItem); + threadViewModel.AddEmail(item); + threadViewModel.AddEmail(new MailItemViewModel(addedItem)); }); + + var newGroupKey = GetGroupingKey(threadViewModel); + + await RemoveItemInternalAsync(group, item); + await InsertItemInternalAsync(newGroupKey, threadViewModel); } public async Task AddAsync(MailCopy addedItem) { - foreach (var group in _mailItemSource) + // First check if this is an update to an existing item + if (MailCopyIdHashSet.ContainsKey(addedItem.UniqueId)) { - foreach (var item in group) + // Find and update the existing item + var existingItemContainer = GetMailItemContainer(addedItem.UniqueId); + if (existingItemContainer?.ItemViewModel != null) { - if (_threadingManager.ShouldThread(addedItem, item)) + await UpdateExistingItemAsync(existingItemContainer, addedItem); + return; + } + } + + // Check if this item should be threaded with an existing item + if (!string.IsNullOrEmpty(addedItem.ThreadId)) + { + var threadableItem = FindThreadableItem(addedItem.ThreadId); + if (threadableItem != null) + { + // Find the group containing this item + var targetGroup = FindGroupContainingItem(threadableItem); + if (targetGroup != null) { - await HandleThreadingAsync(group, item, addedItem); - return; - } - else if (item.Id == addedItem.Id && item is MailItemViewModel itemViewModel) - { - await UpdateExistingItemAsync(itemViewModel, addedItem); + await HandleThreadingAsync(targetGroup, threadableItem, addedItem); return; } } } + // No threading needed, add as new item await AddNewItemAsync(addedItem); } + private ObservableGroup FindGroupContainingItem(IMailListItem item) + { + // Try cache first + if (_itemToGroupMap.TryGetValue(item, out var cachedGroup)) + { + // Cache can become stale during concurrent list refreshes/moves. + // Validate before returning so we don't mutate a detached group. + if (_mailItemSource.Contains(cachedGroup) && cachedGroup.Contains(item)) + { + return cachedGroup; + } + + _itemToGroupMap.TryRemove(item, out _); + } + + // Fallback to search if not in cache + foreach (var group in _mailItemSource) + { + if (group.Contains(item)) + { + _itemToGroupMap[item] = group; + return group; + } + } + return null; + } + private async Task AddNewItemAsync(MailCopy addedItem) { - var groupKey = GetGroupingKey(addedItem); - await ExecuteUIThread(() => { InsertItemInternal(groupKey, addedItem); }); + var newMailItem = new MailItemViewModel(addedItem); + var groupKey = GetGroupingKey(newMailItem); + await InsertItemInternalAsync(groupKey, newMailItem); } - private async Task UpdateExistingItemAsync(IMailItem existingItem, MailCopy updatedItem) + private async Task ReinsertUpdatedItemAsync(MailCopy updatedItem, bool isSelected, bool isBusy) { - if (existingItem is MailItemViewModel itemViewModel) - { - UpdateUniqueIdHashes(itemViewModel, false); - UpdateUniqueIdHashes(updatedItem, true); + await RemoveAsync(updatedItem); + await AddAsync(updatedItem); - await ExecuteUIThread(() => { itemViewModel.MailCopy = updatedItem; }); + var updatedContainer = GetMailItemContainer(updatedItem.UniqueId); + if (updatedContainer?.ItemViewModel == null) + { + return; + } + + await ExecuteUIThread(() => + { + updatedContainer.ItemViewModel.IsSelected = isSelected; + updatedContainer.ItemViewModel.IsBusy = isBusy; + }); + } + + private async Task UpdateExistingItemAsync(MailItemContainer itemContainer, + MailCopy updatedItem, + MailUpdateSource mailUpdateSource = MailUpdateSource.Server, + MailCopyChangeFlags changeHint = MailCopyChangeFlags.None) + { + if (itemContainer?.ItemViewModel == null) + { + return; + } + + var existingItem = itemContainer.ItemViewModel; + var threadOwner = itemContainer.ThreadViewModel as IMailListItem ?? existingItem; + var wasSelected = existingItem.IsSelected; + MailCopyChangeFlags appliedChanges = MailCopyChangeFlags.None; + + await ExecuteUIThread(() => + { + UpdateUniqueIdHashes(existingItem, false); + UpdateThreadIdCache(threadOwner, false); + + itemContainer.ThreadViewModel?.SuspendChildPropertyNotifications(); + + try + { + appliedChanges = existingItem.UpdateFrom(updatedItem, changeHint); + } + finally + { + itemContainer.ThreadViewModel?.ResumeChildPropertyNotifications(); + } + + existingItem.IsBusy = mailUpdateSource == MailUpdateSource.ClientUpdated; + + UpdateUniqueIdHashes(existingItem, true); + UpdateThreadIdCache(threadOwner, true); + + if (itemContainer.ThreadViewModel != null) + { + _uniqueIdToThreadMap[existingItem.MailCopy.UniqueId] = itemContainer.ThreadViewModel; + } + else + { + _uniqueIdToThreadMap.TryRemove(existingItem.MailCopy.UniqueId, out _); + } + }); + + if ((appliedChanges & MailCopyChangeFlags.ThreadId) != 0) + { + await ReinsertUpdatedItemAsync(updatedItem, wasSelected, existingItem.IsBusy); + return; + } + + if (itemContainer.ThreadViewModel != null && appliedChanges != MailCopyChangeFlags.None) + { + await ExecuteUIThread(() => + { + itemContainer.ThreadViewModel.NotifyMailItemUpdated(existingItem, appliedChanges); + }); } } - public void AddRange(IEnumerable items, bool clearIdCache) + /// + /// Adds multiple emails to the collection. + /// + public async Task AddRangeAsync(IEnumerable items, bool clearIdCache) { if (clearIdCache) { MailCopyIdHashSet.Clear(); + _threadIdToItemsMap.Clear(); + _itemToGroupMap.Clear(); + _uniqueIdToMailItemMap.Clear(); + _uniqueIdToThreadMap.Clear(); } - var groupedByName = items - .GroupBy(a => GetGroupingKey(a)) - .Select(a => new ObservableGroup(a.Key, a)); + var itemsList = items as List ?? items.ToList(); + if (itemsList.Count == 0) return; - foreach (var group in groupedByName) + var itemsToAdd = new List(itemsList.Count); + var processedItems = new HashSet(itemsList.Count); + var itemsToUpdate = new List<(MailItemViewModel existing, MailCopy updated)>(); + var threadingOperations = new List<(ObservableGroup group, IMailListItem item, MailCopy addedItem)>(); + + // Build a lookup for existing groups to avoid repeated searches + var groupLookup = new Dictionary>(_mailItemSource.Count * 10); + foreach (var group in _mailItemSource) { - // Store all mail copy ids for faster access. foreach (var item in group) { - if (item is MailItemViewModel mailCopyItem && !MailCopyIdHashSet.Contains(item.UniqueId)) + groupLookup[item] = group; + } + } + + // Build thread lookup from the batch items + var batchThreadLookup = new Dictionary>(); + foreach (var item in itemsList) + { + if (!string.IsNullOrEmpty(item.MailCopy.ThreadId)) + { + if (!batchThreadLookup.TryGetValue(item.MailCopy.ThreadId, out var list)) { - MailCopyIdHashSet.Add(item.UniqueId); + list = new List(); + batchThreadLookup[item.MailCopy.ThreadId] = list; } - else if (item is ThreadMailItemViewModel threadMailItem) + list.Add(item); + } + } + + // Process items and handle threading + foreach (var item in itemsList) + { + if (processedItems.Contains(item)) + continue; + + // Check if this is an update to an existing item + if (MailCopyIdHashSet.ContainsKey(item.MailCopy.UniqueId)) + { + var existingItemContainer = GetMailItemContainer(item.MailCopy.UniqueId); + if (existingItemContainer?.ItemViewModel != null) { - foreach (var mailItem in threadMailItem.ThreadItems) + itemsToUpdate.Add((existingItemContainer.ItemViewModel, item.MailCopy)); + processedItems.Add(item); + continue; + } + } + + // Check if this item should be threaded + if (!string.IsNullOrEmpty(item.MailCopy.ThreadId)) + { + // Look for existing item with same ThreadId + var existingThreadableItem = FindThreadableItem(item.MailCopy.ThreadId); + + if (existingThreadableItem != null) + { + // Thread with existing item + if (groupLookup.TryGetValue(existingThreadableItem, out var targetGroup)) { - if (!MailCopyIdHashSet.Contains(mailItem.UniqueId)) + threadingOperations.Add((targetGroup, existingThreadableItem, item.MailCopy)); + processedItems.Add(item); + continue; + } + } + + // Look for other items in the current batch with same ThreadId + if (batchThreadLookup.TryGetValue(item.MailCopy.ThreadId, out var threadableItems) && threadableItems.Count > 1) + { + // Create a new thread with all matching items - defer UI operations + var threadViewModel = new ThreadMailItemViewModel(item.MailCopy.ThreadId); + + // Add emails without UI thread for now + foreach (var threadItem in threadableItems) + { + threadViewModel.AddEmail(threadItem); + } + + itemsToAdd.Add(threadViewModel); + + // Mark all threaded items as processed + foreach (var threadItem in threadableItems) + { + processedItems.Add(threadItem); + } + continue; + } + } + + // No threading needed, add as single item + itemsToAdd.Add(item); + processedItems.Add(item); + } + + // Execute all threading operations in a single UI thread call + if (threadingOperations.Count > 0) + { + foreach (var (group, existingItem, addedItem) in threadingOperations) + { + await HandleThreadingAsync(group, existingItem, addedItem); + } + } + + // Execute all updates in a single UI thread call + if (itemsToUpdate.Count > 0) + { + await ExecuteUIThread(() => + { + foreach (var (existing, updated) in itemsToUpdate) + { + UpdateUniqueIdHashes(existing, false); + existing.UpdateFrom(updated); + UpdateUniqueIdHashes(existing, true); + } + }); + } + + // Group items by their grouping key and add them in a single UI thread call + if (itemsToAdd.Count > 0) + { + // Pre-compute grouping on background thread to reduce UI thread work + var groupedItems = await Task.Run(() => itemsToAdd + .GroupBy(GetGroupingKey) + .ToDictionary(g => g.Key, g => g.ToList())).ConfigureAwait(false); + + await ExecuteUIThread(() => + { + foreach (var kvp in groupedItems) + { + var groupKey = kvp.Key; + var groupItems = kvp.Value; + + // Update caches first + foreach (var item in groupItems) + { + UpdateUniqueIdHashes(item, true); + UpdateThreadIdCache(item, true); + } + + var existingGroup = _mailItemSource.FirstGroupByKeyOrDefault(groupKey); + + if (existingGroup == null) + { + var newGroup = new ObservableGroup(groupKey, groupItems); + _mailItemSource.AddGroup(groupKey, newGroup); + + // Update item-to-group cache + foreach (var item in groupItems) { - MailCopyIdHashSet.Add(mailItem.UniqueId); + _itemToGroupMap[item] = newGroup; + } + } + else + { + foreach (var item in groupItems) + { + existingGroup.Add(item); + _itemToGroupMap[item] = existingGroup; } } } - } - - var existingGroup = _mailItemSource.FirstGroupByKeyOrDefault(group.Key); - - if (existingGroup == null) - { - _mailItemSource.AddGroup(group.Key, group); - } - else - { - foreach (var item in group) - { - existingGroup.Add(item); - } - } + }); } } public MailItemContainer GetMailItemContainer(Guid uniqueMailId) { - var groupCount = _mailItemSource.Count; + // Fast path: use caches for O(1) lookup + if (_uniqueIdToMailItemMap.TryGetValue(uniqueMailId, out var cachedMailItem)) + { + if (_uniqueIdToThreadMap.TryGetValue(uniqueMailId, out var threadVM)) + { + return new MailItemContainer(cachedMailItem, threadVM); + } - for (int i = 0; i < groupCount; i++) + return new MailItemContainer(cachedMailItem); + } + + // Fallback: scan all groups and populate caches + for (int i = 0; i < _mailItemSource.Count; i++) { var group = _mailItemSource[i]; @@ -280,11 +784,20 @@ public class WinoMailCollection { var item = group[k]; - if (item is MailItemViewModel singleMailItemViewModel && singleMailItemViewModel.UniqueId == uniqueMailId) + if (item is MailItemViewModel singleMailItemViewModel && singleMailItemViewModel.MailCopy.UniqueId == uniqueMailId) + { + _uniqueIdToMailItemMap[uniqueMailId] = singleMailItemViewModel; return new MailItemContainer(singleMailItemViewModel); + } else if (item is ThreadMailItemViewModel threadMailItemViewModel && threadMailItemViewModel.HasUniqueId(uniqueMailId)) { - var singleItemViewModel = threadMailItemViewModel.GetItemById(uniqueMailId) as MailItemViewModel; + var singleItemViewModel = threadMailItemViewModel.ThreadEmails.FirstOrDefault(e => e.MailCopy.UniqueId == uniqueMailId); + + if (singleItemViewModel != null) + { + _uniqueIdToMailItemMap[uniqueMailId] = singleItemViewModel; + _uniqueIdToThreadMap[uniqueMailId] = threadMailItemViewModel; + } return new MailItemContainer(singleItemViewModel, threadMailItemViewModel); } @@ -294,62 +807,57 @@ public class WinoMailCollection return null; } - public void UpdateThumbnails(string address) + /// + /// Updates thumbnails for all mail items with the specified address. + /// + public Task UpdateThumbnailsForAddressAsync(string address) { - if (CoreDispatcher == null) return; + if (CoreDispatcher == null) return Task.CompletedTask; - CoreDispatcher.ExecuteOnUIThread(() => + return CoreDispatcher.ExecuteOnUIThread(() => { foreach (var group in _mailItemSource) { foreach (var item in group) { - if (item is MailItemViewModel mailItemViewModel && mailItemViewModel.MailCopy.FromAddress.Equals(address, StringComparison.OrdinalIgnoreCase)) + if (item is MailItemViewModel mailItemViewModel && mailItemViewModel.MailCopy.FromAddress?.Equals(address, StringComparison.OrdinalIgnoreCase) == true) { mailItemViewModel.ThumbnailUpdatedEvent = !mailItemViewModel.ThumbnailUpdatedEvent; } + else if (item is ThreadMailItemViewModel threadViewModel) + { + foreach (var threadMailItem in threadViewModel.ThreadEmails) + { + if (threadMailItem.MailCopy.FromAddress?.Equals(address, StringComparison.OrdinalIgnoreCase) == true) + { + threadMailItem.ThumbnailUpdatedEvent = !threadMailItem.ThumbnailUpdatedEvent; + } + } + } } } }); } /// - /// Fins the item container that updated mail copy belongs to and updates it. + /// Finds the item container that updated mail copy belongs to and updates it. /// /// Updated mail copy. /// - public async Task UpdateMailCopy(MailCopy updatedMailCopy) + public Task UpdateMailCopy(MailCopy updatedMailCopy, MailUpdateSource mailUpdateSource, MailCopyChangeFlags changedProperties = MailCopyChangeFlags.None) { - // This item doesn't exist in the list. - if (!MailCopyIdHashSet.Contains(updatedMailCopy.UniqueId)) + var itemContainer = GetMailItemContainer(updatedMailCopy.UniqueId); + if (itemContainer?.ItemViewModel == null) { - return; + return Task.CompletedTask; } - await ExecuteUIThread(() => - { - var itemContainer = GetMailItemContainer(updatedMailCopy.UniqueId); - - if (itemContainer == null) return; - - if (itemContainer.ItemViewModel != null) - { - UpdateUniqueIdHashes(itemContainer.ItemViewModel, false); - } - - if (itemContainer.ItemViewModel != null) - { - itemContainer.ItemViewModel.MailCopy = updatedMailCopy; - } - - UpdateUniqueIdHashes(updatedMailCopy, true); - - // Call thread notifications if possible. - itemContainer.ThreadViewModel?.NotifyPropertyChanges(); - }); + return UpdateExistingItemAsync(itemContainer, updatedMailCopy, mailUpdateSource, changedProperties); } + public MailItemViewModel GetFirst() => AllItems.ElementAtOrDefault(0); + public MailItemViewModel GetNextItem(MailCopy mailCopy) { try @@ -364,7 +872,7 @@ public class WinoMailCollection { var item = group[k]; - if (item is MailItemViewModel singleMailItemViewModel && singleMailItemViewModel.UniqueId == mailCopy.UniqueId) + if (item is MailItemViewModel singleMailItemViewModel && singleMailItemViewModel.MailCopy.UniqueId == mailCopy.UniqueId) { if (k + 1 < group.Count) { @@ -381,15 +889,15 @@ public class WinoMailCollection } else if (item is ThreadMailItemViewModel threadMailItemViewModel && threadMailItemViewModel.HasUniqueId(mailCopy.UniqueId)) { - var singleItemViewModel = threadMailItemViewModel.GetItemById(mailCopy.UniqueId) as MailItemViewModel; + var singleItemViewModel = threadMailItemViewModel.ThreadEmails.FirstOrDefault(e => e.MailCopy.UniqueId == mailCopy.UniqueId); if (singleItemViewModel == null) return null; - var singleItemIndex = threadMailItemViewModel.ThreadItems.IndexOf(singleItemViewModel); + var singleItemIndex = threadMailItemViewModel.ThreadEmails.ToList().IndexOf(singleItemViewModel); - if (singleItemIndex + 1 < threadMailItemViewModel.ThreadItems.Count) + if (singleItemIndex + 1 < threadMailItemViewModel.ThreadEmails.Count) { - return threadMailItemViewModel.ThreadItems[singleItemIndex + 1] as MailItemViewModel; + return threadMailItemViewModel.ThreadEmails[singleItemIndex + 1]; } else if (i + 1 < groupCount) { @@ -415,98 +923,215 @@ public class WinoMailCollection public async Task RemoveAsync(MailCopy removeItem) { + var itemContainer = GetMailItemContainer(removeItem.UniqueId); + // This item doesn't exist in the list. - if (!MailCopyIdHashSet.Contains(removeItem.UniqueId)) return; + if (itemContainer?.ItemViewModel == null) return; - // Check all items for whether this item should be threaded with them. - bool shouldExit = false; - - var groupCount = _mailItemSource.Count; - - for (int i = 0; i < groupCount; i++) + if (itemContainer.ThreadViewModel != null) { - if (shouldExit) break; + // Item is inside a thread - use cached lookups instead of scanning all groups. + var threadMailItemViewModel = itemContainer.ThreadViewModel; + var group = FindGroupContainingItem(threadMailItemViewModel); + if (group == null) return; - var group = _mailItemSource[i]; + var removalItem = itemContainer.ItemViewModel; - for (int k = 0; k < group.Count; k++) + // Update ThreadId cache before modifying the thread + UpdateThreadIdCache(threadMailItemViewModel, false); + + await ExecuteUIThread(() => { threadMailItemViewModel.RemoveEmail(removalItem); }); + + // Always clean up the removed item's hashes (fixes leak when thread converts to single) + UpdateUniqueIdHashes(removalItem, false); + + // Update ThreadId cache after modifying the thread + if (threadMailItemViewModel.EmailCount > 0) { - var item = group[k]; + UpdateThreadIdCache(threadMailItemViewModel, true); + } - if (item is ThreadMailItemViewModel threadMailItemViewModel && threadMailItemViewModel.HasUniqueId(removeItem.UniqueId)) + if (threadMailItemViewModel.EmailCount == 1) + { + // Convert to single item. + var singleViewModel = threadMailItemViewModel.ThreadEmails.First(); + var groupKey = GetGroupingKey(singleViewModel); + + await RemoveItemInternalAsync(group, threadMailItemViewModel); + await InsertItemInternalAsync(groupKey, singleViewModel); + + // If thread->single conversion is being done, we should ignore it for non-draft items. + // eg. Deleting a reply message from draft folder. Single non-draft item should not be re-added. + if (PruneSingleNonDraftItems && !singleViewModel.IsDraft) { - var removalItem = threadMailItemViewModel.GetItemById(removeItem.UniqueId); - - if (removalItem == null) return; - - // Threads' Id is equal to the last item they hold. - // We can't do Id check here because that'd remove the whole thread. - - /* Remove item from the thread. - * If thread had 1 item inside: - * -> Remove the thread and insert item as single item. - * If thread had 0 item inside: - * -> Remove the thread. - */ - - var oldGroupKey = GetGroupingKey(threadMailItemViewModel); - - await ExecuteUIThread(() => { threadMailItemViewModel.RemoveCopyItem(removalItem); }); - - if (threadMailItemViewModel.ThreadItems.Count == 1) + var newGroup = _mailItemSource.FirstGroupByKeyOrDefault(groupKey); + if (newGroup != null) { - // Convert to single item. + await RemoveItemInternalAsync(newGroup, singleViewModel); + } + } + } + else if (threadMailItemViewModel.EmailCount == 0) + { + await RemoveItemInternalAsync(group, threadMailItemViewModel); + } + } + else + { + // Standalone item. + IMailListItem mailItem = itemContainer.ItemViewModel; + var group = FindGroupContainingItem(mailItem); - var singleViewModel = threadMailItemViewModel.GetSingleItemViewModel(); - var groupKey = GetGroupingKey(singleViewModel); + if (group != null) + { + await RemoveItemInternalAsync(group, mailItem); + } + } - await ExecuteUIThread(() => + await NotifySelectionChangesAsync(); + } + + private IEnumerable AllItemsIncludingThreads + { + get + { + foreach (var group in _mailItemSource) + { + foreach (var item in group) + { + if (item is ThreadMailItemViewModel threadMailItemViewModel) + { + foreach (var child in threadMailItemViewModel.ThreadEmails) { - RemoveItemInternal(group, threadMailItemViewModel); - InsertItemInternal(groupKey, singleViewModel); - }); - - // If thread->single conversion is being done, we should ignore it for non-draft items. - // eg. Deleting a reply message from draft folder. Single non-draft item should not be re-added. - - if (PruneSingleNonDraftItems && !singleViewModel.IsDraft) - { - // This item should not be here anymore. - // It's basically a reply mail in Draft folder. - var newGroup = _mailItemSource.FirstGroupByKeyOrDefault(groupKey); - - if (newGroup != null) - { - await ExecuteUIThread(() => { RemoveItemInternal(newGroup, singleViewModel); }); - } + yield return child; } } - else if (threadMailItemViewModel.ThreadItems.Count == 0) - { - await ExecuteUIThread(() => { RemoveItemInternal(group, threadMailItemViewModel); }); - } - else - { - // Item inside the thread is removed. - await ExecuteUIThread(() => { threadMailItemViewModel.ThreadItems.Remove(removalItem); }); - - UpdateUniqueIdHashes(removalItem, false); - } - - shouldExit = true; - break; - } - else if (item.UniqueId == removeItem.UniqueId) - { - await ExecuteUIThread(() => { RemoveItemInternal(group, item); }); - - shouldExit = true; - - break; + yield return item; } } } } - private async Task ExecuteUIThread(Action action) => await CoreDispatcher?.ExecuteOnUIThread(action); + private IEnumerable AllItems + { + get + { + foreach (var group in _mailItemSource) + { + foreach (var item in group) + { + if (item is ThreadMailItemViewModel threadMail) + { + foreach (var singleItem in threadMail.ThreadEmails) + { + yield return singleItem; + } + } + else if (item is MailItemViewModel mailItemViewModel) + yield return mailItemViewModel; + } + } + } + } + + public IEnumerable SelectedItems => AllItems.Where(a => a.IsSelected); + public int SelectedItemsCount => AllItems.Count(a => a.IsSelected); + public int AllItemsCount => AllItems.Count(); + public bool IsAllItemsSelected => AllItems.Any() && AllItems.All(a => a.IsSelected); + public bool HasSingleItemSelected => SelectedItemsCount == 1; + + public async Task ExecuteWithoutRaiseSelectionChangedAsync(Action action, bool includeThreads) + { + try + { + // Do not listen to individual selection changes while we are doing bulk selection. + Messenger.Unregister(this); + + await ExecuteUIThread(() => + { + if (includeThreads) + { + foreach (var item in AllItemsIncludingThreads) + { + action(item); + } + } + else + { + foreach (var item in AllItems) + { + action(item); + } + } + }); + } + catch (Exception) + { + } + finally + { + Messenger.Unregister(this); + Messenger.Register(this); + Messenger.Send(new SelectedItemsChangedMessage()); + + await NotifySelectionChangesAsync(); + } + } + + public Task ToggleSelectAllAsync() + { + if (IsAllItemsSelected) + { + return UnselectAllAsync(); + } + else + { + return SelectAllAsync(); + } + } + + /// + /// Gets the index of an item in the flat Items collection. + /// Note: WinoMailCollection doesn't have a flat Items collection like GroupedEmailCollection. + /// This returns -1 as it's not applicable to the grouped structure. + /// + public int IndexOf(object item) + { + // WinoMailCollection uses grouped structure, so we need to search through groups + int currentIndex = 0; + + foreach (var group in _mailItemSource) + { + foreach (var groupItem in group) + { + if (ReferenceEquals(groupItem, item)) + { + return currentIndex; + } + currentIndex++; + } + } + + return -1; + } + + public Task SelectAllAsync() => ExecuteWithoutRaiseSelectionChangedAsync(a => a.IsSelected = true, true); + public Task UnselectAllAsync(IMailListItem exceptItem = null) => ExecuteWithoutRaiseSelectionChangedAsync(a => { if (a != exceptItem) a.IsSelected = false; }, true); + public Task CollapseAllThreadsAsync() => ExecuteWithoutRaiseSelectionChangedAsync(a => { if (a is ThreadMailItemViewModel thread) thread.IsThreadExpanded = false; }, true); + + private Task ExecuteUIThread(Action action) => CoreDispatcher?.ExecuteOnUIThread(action); + + public void Receive(SelectedItemsChangedMessage message) => _ = NotifySelectionChangesAsync(); + + private async Task NotifySelectionChangesAsync() + { + await ExecuteUIThread(() => + { + OnPropertyChanged(nameof(IsAllItemsSelected)); + OnPropertyChanged(nameof(SelectedItemsCount)); + OnPropertyChanged(nameof(HasSingleItemSelected)); + + ItemSelectionChanged?.Invoke(this, null); + }); + } } diff --git a/Wino.Mail.ViewModels/ComposePageViewModel.cs b/Wino.Mail.ViewModels/ComposePageViewModel.cs index 979fcf0a..bf6c23b6 100644 --- a/Wino.Mail.ViewModels/ComposePageViewModel.cs +++ b/Wino.Mail.ViewModels/ComposePageViewModel.cs @@ -3,39 +3,64 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; +using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; using MimeKit; +using MimeKit.Cryptography; using Wino.Core.Domain; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Exceptions; +using Wino.Core.Domain.Extensions; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.MailItem; +using Wino.Core.Domain.Models; using Wino.Core.Domain.Models.Navigation; using Wino.Core.Extensions; using Wino.Core.Services; using Wino.Mail.ViewModels.Data; +using Wino.Mail.ViewModels.Messages; using Wino.Messaging.Client.Mails; -using Wino.Messaging.Server; +using Wino.Messaging.UI; namespace Wino.Mail.ViewModels; -public partial class ComposePageViewModel : MailBaseViewModel +public partial class ComposePageViewModel : MailBaseViewModel, + IRecipient, + IRecipient, + IRecipient, + IRecipient { + private static readonly TimeSpan LocalDraftRetryGracePeriod = TimeSpan.FromSeconds(15); + public Func> GetHTMLBodyFunction; + public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args) + { + if (args.Handled || args.Mode != WinoApplicationMode.Mail) + return; + + if (args.Action == KeyboardShortcutAction.Send) + { + await SendAsync(); + args.Handled = true; + } + } + // When we send the message or discard it, we need to block the mime update // Update is triggered when we leave the page. private bool isUpdatingMimeBlocked = false; - private bool canSendMail => ComposingAccount != null && !IsLocalDraft && CurrentMimeMessage != null; + private bool canSendMail => ComposingAccount != null && !IsLocalDraft && CurrentMimeMessage != null && !IsDraftBusy; + private bool canSendLocalDraftToServer => ComposingAccount != null && IsLocalDraft && CurrentMimeMessage != null && !IsDraftBusy && !IsRetryingSendToServer; [NotifyCanExecuteChangedFor(nameof(DiscardCommand))] [NotifyCanExecuteChangedFor(nameof(SendCommand))] + [NotifyCanExecuteChangedFor(nameof(SendToServerCommand))] [ObservableProperty] private MimeMessage currentMimeMessage = null; @@ -47,47 +72,72 @@ public partial class ComposePageViewModel : MailBaseViewModel [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsLocalDraft))] + [NotifyPropertyChangedFor(nameof(ShouldShowSendToServerButton))] + [NotifyPropertyChangedFor(nameof(ShouldShowSendButton))] [NotifyCanExecuteChangedFor(nameof(DiscardCommand))] [NotifyCanExecuteChangedFor(nameof(SendCommand))] - private MailItemViewModel currentMailDraftItem; + [NotifyCanExecuteChangedFor(nameof(SendToServerCommand))] + public partial MailItemViewModel CurrentMailDraftItem { get; set; } [ObservableProperty] - private bool isImportanceSelected; + [NotifyPropertyChangedFor(nameof(ShouldShowSendToServerButton))] + [NotifyCanExecuteChangedFor(nameof(DiscardCommand))] + [NotifyCanExecuteChangedFor(nameof(SendCommand))] + [NotifyCanExecuteChangedFor(nameof(SendToServerCommand))] + public partial bool IsDraftBusy { get; set; } [ObservableProperty] - private MessageImportance selectedMessageImportance; + [NotifyCanExecuteChangedFor(nameof(SendToServerCommand))] + public partial bool IsRetryingSendToServer { get; set; } [ObservableProperty] - private bool isCCBCCVisible; + public partial bool IsImportanceSelected { get; set; } [ObservableProperty] - private string subject; + public partial MessageImportance SelectedMessageImportance { get; set; } + + [ObservableProperty] + public partial bool IsCCBCCVisible { get; set; } + + [ObservableProperty] + public partial string Subject { get; set; } [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(DiscardCommand))] [NotifyCanExecuteChangedFor(nameof(SendCommand))] - private MailAccount composingAccount; + [NotifyCanExecuteChangedFor(nameof(SendToServerCommand))] + public partial MailAccount ComposingAccount { get; set; } [ObservableProperty] - private List availableAliases; + public partial List AvailableAliases { get; set; } + [ObservableProperty] + public partial MailAccountAlias SelectedAlias { get; set; } + [ObservableProperty] + public partial bool IsDraggingOverComposerGrid { get; set; } + [ObservableProperty] + public partial bool IsDraggingOverFilesDropZone { get; set; } + [ObservableProperty] + public partial bool IsDraggingOverImagesDropZone { get; set; } + [ObservableProperty] + public partial bool IsSmimeSignatureEnabled { get; set; } + [ObservableProperty] + public partial bool IsSmimeEncryptionEnabled { get; set; } [ObservableProperty] - private MailAccountAlias selectedAlias; + public partial X509Certificate2 SelectedSigningCertificate { get; set; } - [ObservableProperty] - private bool isDraggingOverComposerGrid; + public ObservableCollection AvailableCertificates = []; - [ObservableProperty] - private bool isDraggingOverFilesDropZone; - - [ObservableProperty] - private bool isDraggingOverImagesDropZone; + public bool AreCertificatesAvailable => AvailableCertificates.Count > 0; + public ObservableCollection AvailableEmailTemplates { get; } = []; public ObservableCollection IncludedAttachments { get; set; } = []; public ObservableCollection Accounts { get; set; } = []; public ObservableCollection ToItems { get; set; } = []; public ObservableCollection CCItems { get; set; } = []; public ObservableCollection BCCItems { get; set; } = []; + public bool ShouldShowSendToServerButton => IsLocalDraft && !IsDraftBusy; + public bool ShouldShowSendButton => !IsLocalDraft; #endregion @@ -99,11 +149,12 @@ public partial class ComposePageViewModel : MailBaseViewModel private readonly IFileService _fileService; private readonly IFolderService _folderService; private readonly IAccountService _accountService; + private readonly IEmailTemplateService _emailTemplateService; private readonly IWinoRequestDelegator _worker; public readonly IFontService FontService; public readonly IPreferencesService PreferencesService; - private readonly IWinoServerConnectionManager _winoServerConnectionManager; public readonly IContactService ContactService; + public readonly ISmimeCertificateService _smimeCertificateService; public ComposePageViewModel(IMailDialogService dialogService, IMailService mailService, @@ -112,11 +163,12 @@ public partial class ComposePageViewModel : MailBaseViewModel INativeAppService nativeAppService, IFolderService folderService, IAccountService accountService, + IEmailTemplateService emailTemplateService, IWinoRequestDelegator worker, IContactService contactService, IFontService fontService, IPreferencesService preferencesService, - IWinoServerConnectionManager winoServerConnectionManager) + ISmimeCertificateService smimeCertificateService) { NativeAppService = nativeAppService; ContactService = contactService; @@ -129,8 +181,40 @@ public partial class ComposePageViewModel : MailBaseViewModel _mimeFileService = mimeFileService; _fileService = fileService; _accountService = accountService; + _emailTemplateService = emailTemplateService; _worker = worker; - _winoServerConnectionManager = winoServerConnectionManager; + _smimeCertificateService = smimeCertificateService; + + foreach (var cert in _smimeCertificateService.GetCertificates(emailAddress: SelectedAlias?.AliasAddress)) + { + if (cert != null) + { + AvailableCertificates.Add(cert); + } + } + } + + partial void OnSelectedAliasChanged(MailAccountAlias value) + { + if (value != null) + { + IsSmimeSignatureEnabled = value.SelectedSigningCertificateThumbprint != null; + IsSmimeEncryptionEnabled = value.IsSmimeEncryptionEnabled; + + AvailableCertificates.Clear(); + var certs = _smimeCertificateService.GetCertificates(emailAddress: SelectedAlias.AliasAddress); + foreach (var cert in certs) + { + AvailableCertificates.Add(cert); + } + SelectedSigningCertificate = AvailableCertificates + .Where(c => c.Thumbprint == SelectedAlias.SelectedSigningCertificateThumbprint).FirstOrDefault() ?? AvailableCertificates.FirstOrDefault(); + } + } + + partial void OnSelectedSigningCertificateChanged(X509Certificate2 value) + { + IsSmimeSignatureEnabled = value != null; } [RelayCommand] @@ -214,25 +298,116 @@ public partial class ComposePageViewModel : MailBaseViewModel isUpdatingMimeBlocked = true; - var assignedAccount = CurrentMailDraftItem.AssignedAccount; + var assignedAccount = CurrentMailDraftItem.MailCopy.AssignedAccount; var sentFolder = await _folderService.GetSpecialFolderByAccountIdAsync(assignedAccount.Id, SpecialFolderType.Sent); + + // Load alias certs + var certs = _smimeCertificateService.GetCertificates(emailAddress: SelectedAlias.AliasAddress); + + if (IsSmimeSignatureEnabled) + { + var signingCertificate = !string.IsNullOrEmpty(SelectedAlias.SelectedSigningCertificateThumbprint) + ? certs.FirstOrDefault(c => c?.Thumbprint == SelectedAlias.SelectedSigningCertificateThumbprint) + : null; + + var signer = new CmsSigner(signingCertificate) { DigestAlgorithm = DigestAlgorithm.Sha1 }; + + if (IsSmimeEncryptionEnabled) + { + var recipients = new CmsRecipientCollection(); + var cmsRecipients = CurrentMimeMessage.To.Mailboxes + .Select(mailbox => new CmsRecipient( + _smimeCertificateService.GetCertificates(emailAddress: mailbox.Address).FirstOrDefault() ?? _smimeCertificateService.GetCertificates(StoreName.AddressBook, emailAddress: mailbox.Address).FirstOrDefault() + )); + foreach (var recipient in cmsRecipients) + { + recipients.Add(recipient); + } + + CurrentMimeMessage.Body = ApplicationPkcs7Mime.SignAndEncrypt(signer, recipients, CurrentMimeMessage.Body); + } + else + { + // CurrentMimeMessage.Body = MultipartSigned.Create(signer, CurrentMimeMessage.Body); + CurrentMimeMessage.Body = ApplicationPkcs7Mime.Sign(signer, CurrentMimeMessage.Body); + } + } + else if (IsSmimeEncryptionEnabled) + { + // var encryptionCertificate = !string.IsNullOrEmpty(SelectedAlias.SelectedEncryptionCertificateThumbprint) + // ? certs.FirstOrDefault(c => c?.Thumbprint == SelectedAlias.SelectedEncryptionCertificateThumbprint) + // : null; + // Encrypt the message if encryption certificate is selected. + CurrentMimeMessage.Body = ApplicationPkcs7Mime.Encrypt(CurrentMimeMessage.To.Mailboxes, CurrentMimeMessage.Body); + } + using MemoryStream memoryStream = new(); CurrentMimeMessage.WriteTo(FormatOptions.Default, memoryStream); - byte[] buffer = memoryStream.GetBuffer(); - int count = (int)memoryStream.Length; - - var base64EncodedMessage = Convert.ToBase64String(buffer); + var base64EncodedMessage = Convert.ToBase64String(memoryStream.ToArray()); var draftSendPreparationRequest = new SendDraftPreparationRequest(CurrentMailDraftItem.MailCopy, SelectedAlias, sentFolder, - CurrentMailDraftItem.AssignedFolder, - CurrentMailDraftItem.AssignedAccount.Preferences, + CurrentMailDraftItem.MailCopy.AssignedFolder, + CurrentMailDraftItem.MailCopy.AssignedAccount.Preferences, base64EncodedMessage); + await ExecuteUIThread(() => + { + IsDraftBusy = true; + }); + await _worker.ExecuteAsync(draftSendPreparationRequest); } + [RelayCommand(CanExecute = nameof(canSendLocalDraftToServer))] + private async Task SendToServerAsync() + { + if (CurrentMailDraftItem?.MailCopy == null || ComposingAccount == null || CurrentMimeMessage == null) + return; + + try + { + await ExecuteUIThread(() => + { + IsRetryingSendToServer = true; + IsDraftBusy = true; + NotifyComposeActionStateChanged(); + }); + + await UpdateMimeChangesAsync().ConfigureAwait(false); + + var localDraftCopy = CurrentMailDraftItem.MailCopy; + var (retryReason, referenceMailCopy) = await ResolveRetryDraftContextAsync().ConfigureAwait(false); + var draftPreparationRequest = new DraftPreparationRequest( + localDraftCopy.AssignedAccount ?? ComposingAccount, + localDraftCopy, + CurrentMimeMessage.GetBase64MimeMessage(), + retryReason, + referenceMailCopy); + + await _worker.ExecuteAsync(draftPreparationRequest).ConfigureAwait(false); + } + catch (Exception ex) + { + _dialogService.InfoBarMessage(Translator.Info_RequestCreationFailedTitle, ex.Message, InfoBarMessageType.Error); + } + finally + { + await ExecuteUIThread(() => + { + IsRetryingSendToServer = false; + }); + + await UpdatePendingOperationStateAsync().ConfigureAwait(false); + + await ExecuteUIThread(() => + { + NotifyComposeActionStateChanged(); + }); + } + } + public async Task UpdateMimeChangesAsync() { if (isUpdatingMimeBlocked || CurrentMimeMessage == null || ComposingAccount == null || CurrentMailDraftItem == null) return; @@ -332,13 +507,13 @@ public partial class ComposePageViewModel : MailBaseViewModel } } - public override void OnNavigatedFrom(NavigationMode mode, object parameters) - { - base.OnNavigatedFrom(mode, parameters); + //public override void OnNavigatedFrom(NavigationMode mode, object parameters) + //{ + // base.OnNavigatedFrom(mode, parameters); - /// Do not put any code here. - /// Make sure to use Page's OnNavigatedTo instead. - } + // /// Do not put any code here. + // /// Make sure to use Page's OnNavigatedTo instead. + //} public override async void OnNavigatedTo(NavigationMode mode, object parameters) { @@ -348,17 +523,97 @@ public partial class ComposePageViewModel : MailBaseViewModel { CurrentMailDraftItem = mailItem; + await UpdatePendingOperationStateAsync(); + await LoadEmailTemplatesAsync(); await TryPrepareComposeAsync(true); } } + public async void Receive(ReaderItemRefreshRequestedEvent message) + { + if (message.MailItemViewModel == null || !message.MailItemViewModel.IsDraft) return; + + // Save current draft before switching. + await UpdateMimeChangesAsync(); + + // Reset state for the new draft. + isUpdatingMimeBlocked = false; + ComposingAccount = null; + IncludedAttachments.Clear(); + + // Set the new draft item and prepare it. + CurrentMailDraftItem = message.MailItemViewModel; + await UpdatePendingOperationStateAsync(); + await LoadEmailTemplatesAsync(); + await TryPrepareComposeAsync(true); + } + + private async Task LoadEmailTemplatesAsync() + { + var templates = await _emailTemplateService.GetEmailTemplatesAsync().ConfigureAwait(false); + + await ExecuteUIThread(() => + { + AvailableEmailTemplates.Clear(); + + foreach (var template in templates) + { + AvailableEmailTemplates.Add(template); + } + }); + } + + public async void Receive(SynchronizationActionsAdded message) + { + if (!ShouldTrackDraftSynchronizationState(message.AccountId)) + return; + + await UpdatePendingOperationStateAsync().ConfigureAwait(false); + } + + public async void Receive(SynchronizationActionsCompleted message) + { + if (!ShouldTrackDraftSynchronizationState(message.AccountId)) + return; + + await UpdatePendingOperationStateAsync().ConfigureAwait(false); + } + + public async void Receive(AccountSynchronizerStateChanged message) + { + if (message.NewState != AccountSynchronizerState.Idle || !ShouldTrackDraftSynchronizationState(message.AccountId)) + return; + + await UpdatePendingOperationStateAsync().ConfigureAwait(false); + } + + protected override void RegisterRecipients() + { + base.RegisterRecipients(); + + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + } + + protected override void UnregisterRecipients() + { + base.UnregisterRecipients(); + + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + } + private async Task InitializeComposerAccountAsync() { if (CurrentMailDraftItem == null) return false; if (ComposingAccount != null) return true; - var composingAccount = await _accountService.GetAccountAsync(CurrentMailDraftItem.AssignedAccount.Id).ConfigureAwait(false); + var composingAccount = await _accountService.GetAccountAsync(CurrentMailDraftItem.MailCopy.AssignedAccount.Id).ConfigureAwait(false); if (composingAccount == null) return false; var aliases = await _accountService.GetAccountAliasesAsync(composingAccount.Id).ConfigureAwait(false); @@ -377,7 +632,7 @@ public partial class ComposePageViewModel : MailBaseViewModel primaryAlias = aliases.Find(a => a.AliasAddress == CurrentMailDraftItem.FromAddress); } - primaryAlias ??= await _accountService.GetPrimaryAccountAliasAsync(ComposingAccount.Id).ConfigureAwait(false); + primaryAlias ??= await _accountService.GetPrimaryAccountAliasAsync(composingAccount.Id).ConfigureAwait(false); await ExecuteUIThread(() => { @@ -389,6 +644,44 @@ public partial class ComposePageViewModel : MailBaseViewModel return true; } + private async Task UpdatePendingOperationStateAsync() + { + var hasPendingOperation = false; + var keepBusyForInitialGracePeriod = false; + + if (CurrentMailDraftItem?.MailCopy == null || !CurrentMailDraftItem.MailCopy.IsDraft) + { + await ExecuteUIThread(() => + { + IsDraftBusy = false; + NotifyComposeActionStateChanged(); + }); + return; + } + + var accountId = CurrentMailDraftItem.MailCopy.AssignedAccount?.Id ?? Guid.Empty; + + if (accountId != Guid.Empty) + { + var synchronizer = await SynchronizationManager.Instance.GetSynchronizerAsync(accountId).ConfigureAwait(false); + hasPendingOperation = synchronizer?.HasPendingOperation(CurrentMailDraftItem.MailCopy.UniqueId) ?? false; + } + + // Newly created local drafts can have a short period where request queue is empty + // while folder synchronization/mapping is still in progress. + // Keep progress visible during this grace period to prevent "Send to server" flicker. + if (!hasPendingOperation && CurrentMailDraftItem.MailCopy.IsLocalDraft) + { + keepBusyForInitialGracePeriod = IsWithinLocalDraftRetryGracePeriod(CurrentMailDraftItem.MailCopy); + } + + await ExecuteUIThread(() => + { + IsDraftBusy = hasPendingOperation || keepBusyForInitialGracePeriod; + NotifyComposeActionStateChanged(); + }); + } + private async Task TryPrepareComposeAsync(bool downloadIfNeeded) { if (CurrentMailDraftItem == null) return; @@ -397,7 +690,7 @@ public partial class ComposePageViewModel : MailBaseViewModel if (!isComposerInitialized) return; - retry: + retry: // Replying existing message. MimeMessageInformation mimeMessageInformation = null; @@ -412,13 +705,12 @@ public partial class ComposePageViewModel : MailBaseViewModel { downloadIfNeeded = false; - var package = new DownloadMissingMessageRequested(CurrentMailDraftItem.AssignedAccount.Id, CurrentMailDraftItem.MailCopy); - var downloadResponse = await _winoServerConnectionManager.GetResponseAsync(package); + // Download missing MIME message using SynchronizationManager + await SynchronizationManager.Instance.DownloadMimeMessageAsync( + CurrentMailDraftItem.MailCopy, + CurrentMailDraftItem.MailCopy.AssignedAccount.Id); - if (downloadResponse.IsSuccess) - { - goto retry; - } + goto retry; } else _dialogService.InfoBarMessage(Translator.Info_ComposerMissingMIMETitle, Translator.Info_ComposerMissingMIMEMessage, InfoBarMessageType.Error); @@ -471,6 +763,8 @@ public partial class ComposePageViewModel : MailBaseViewModel { if (CurrentMimeMessage == null) return; + IncludedAttachments.Clear(); + foreach (var attachment in CurrentMimeMessage.Attachments) { if (attachment.IsAttachment && attachment is MimePart attachmentPart) @@ -530,6 +824,32 @@ public partial class ComposePageViewModel : MailBaseViewModel list.Add(new MailboxAddress(item.Name, item.Address)); } + private async Task<(DraftCreationReason reason, MailCopy referenceMailCopy)> ResolveRetryDraftContextAsync() + { + if (CurrentMimeMessage == null || CurrentMailDraftItem?.MailCopy?.AssignedAccount == null) + return (DraftCreationReason.Empty, null); + + var inReplyTo = CurrentMimeMessage.InReplyTo; + if (string.IsNullOrWhiteSpace(inReplyTo) && CurrentMimeMessage.Headers.Contains(HeaderId.InReplyTo)) + inReplyTo = CurrentMimeMessage.Headers[HeaderId.InReplyTo]; + + inReplyTo = MailHeaderExtensions.StripAngleBrackets(inReplyTo); + if (string.IsNullOrWhiteSpace(inReplyTo)) + return (DraftCreationReason.Empty, null); + + var accountId = CurrentMailDraftItem.MailCopy.AssignedAccount.Id; + var referenceMailCopy = await _mailService.GetMailCopyByMessageIdAsync(accountId, inReplyTo).ConfigureAwait(false); + if (referenceMailCopy == null) + return (DraftCreationReason.Empty, null); + + // We cannot perfectly reconstruct original intent (Reply vs ReplyAll) from persisted data. + // Infer ReplyAll when multiple recipients exist on the local MIME. + var totalRecipients = CurrentMimeMessage.To.Mailboxes.Count() + CurrentMimeMessage.Cc.Mailboxes.Count(); + var reason = totalRecipients > 1 ? DraftCreationReason.ReplyAll : DraftCreationReason.Reply; + + return (reason, referenceMailCopy); + } + public async Task GetAddressInformationAsync(string tokenText, ObservableCollection collection) { // Get model from the service. This will make sure the name is properly included if there is any record. @@ -554,21 +874,57 @@ public partial class ComposePageViewModel : MailBaseViewModel _dialogService.InfoBarMessage(Translator.Info_InvalidAddressTitle, string.Format(Translator.Info_InvalidAddressMessage, address), InfoBarMessageType.Warning); } - protected override async void OnMailUpdated(MailCopy updatedMail) + protected override async void OnMailUpdated(MailCopy updatedMail, MailUpdateSource source, MailCopyChangeFlags changedProperties) { - base.OnMailUpdated(updatedMail); + base.OnMailUpdated(updatedMail, source, changedProperties); if (CurrentMailDraftItem == null) return; - if (updatedMail.UniqueId == CurrentMailDraftItem.UniqueId) + if (updatedMail.UniqueId == CurrentMailDraftItem.MailCopy.UniqueId) { - await ExecuteUIThread(() => + await ExecuteUIThread(async () => { - CurrentMailDraftItem.MailCopy = updatedMail; - - DiscardCommand.NotifyCanExecuteChanged(); - SendCommand.NotifyCanExecuteChanged(); + CurrentMailDraftItem.UpdateFrom(updatedMail, changedProperties); + await UpdatePendingOperationStateAsync(); + NotifyComposeActionStateChanged(); }); } } + + private void NotifyComposeActionStateChanged() + { + OnPropertyChanged(nameof(IsLocalDraft)); + OnPropertyChanged(nameof(ShouldShowSendToServerButton)); + OnPropertyChanged(nameof(ShouldShowSendButton)); + + DiscardCommand.NotifyCanExecuteChanged(); + SendCommand.NotifyCanExecuteChanged(); + SendToServerCommand.NotifyCanExecuteChanged(); + } + + private bool ShouldTrackDraftSynchronizationState(Guid accountId) + { + if (accountId == Guid.Empty) + return false; + + var currentDraftAccountId = CurrentMailDraftItem?.MailCopy?.AssignedAccount?.Id + ?? ComposingAccount?.Id + ?? Guid.Empty; + + return currentDraftAccountId != Guid.Empty && currentDraftAccountId == accountId; + } + + private bool IsWithinLocalDraftRetryGracePeriod(MailCopy localDraft) + { + if (localDraft == null || localDraft.CreationDate == default) + return false; + + var elapsed = DateTime.UtcNow - localDraft.CreationDate; + + // Clock skew safety. + if (elapsed < TimeSpan.Zero) + return true; + + return elapsed < LocalDraftRetryGracePeriod; + } } diff --git a/Wino.Mail.ViewModels/ContactsPageViewModel.cs b/Wino.Mail.ViewModels/ContactsPageViewModel.cs new file mode 100644 index 00000000..f08e4ac6 --- /dev/null +++ b/Wino.Mail.ViewModels/ContactsPageViewModel.cs @@ -0,0 +1,489 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; +using System.Threading; +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.Models.Navigation; +using Wino.Mail.ViewModels.Data; +using Wino.Messaging.Client.Contacts; + +namespace Wino.Mail.ViewModels; + +public partial class ContactsPageViewModel : MailBaseViewModel, + IRecipient +{ + private const int ContactPageSize = 50; + + private readonly IContactService _contactService; + private readonly IMailDialogService _dialogService; + private readonly IContactPictureFileService _contactPictureFileService; + + private CancellationTokenSource _searchDebounceCancellationTokenSource; + private int _currentOffset = 0; + private int _currentQueryVersion = 0; + + [ObservableProperty] + public partial string SearchQuery { get; set; } = string.Empty; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(LoadMoreContactsCommand))] + [NotifyPropertyChangedFor(nameof(IsEmpty))] + public partial bool IsLoading { get; set; } = false; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(LoadMoreContactsCommand))] + public partial bool IsLoadingMore { get; set; } = false; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(LoadMoreContactsCommand))] + public partial bool HasMoreContacts { get; set; } = false; + + [ObservableProperty] + public partial bool IsSelectionMode { get; set; } = false; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(DeleteSelectedContactsCommand))] + public partial int SelectedContactsCount { get; set; } = 0; + + [ObservableProperty] + public partial int TotalContactsCount { get; set; } = 0; + + public bool IsEmpty => !IsLoading && Contacts.Count == 0; + public bool CanLoadMoreContacts => HasMoreContacts && !IsLoading && !IsLoadingMore; + public bool CanDeleteSelectedContacts => SelectedContactsCount > 0; + + public ObservableCollection Contacts { get; } = new(); + public ObservableCollection SelectedContacts { get; } = new(); + + public ContactsPageViewModel(IContactService contactService, IMailDialogService dialogService, IContactPictureFileService contactPictureFileService) + { + _contactService = contactService; + _dialogService = dialogService; + _contactPictureFileService = contactPictureFileService; + + Contacts.CollectionChanged += ContactsCollectionChanged; + } + + public override async void OnNavigatedTo(NavigationMode mode, object parameters) + { + base.OnNavigatedTo(mode, parameters); + + SelectedContacts.CollectionChanged -= SelectedContactsChanged; + SelectedContacts.CollectionChanged += SelectedContactsChanged; + + await ReloadContactsAsync(); + } + + public override void OnNavigatedFrom(NavigationMode mode, object parameters) + { + base.OnNavigatedFrom(mode, parameters); + + SelectedContacts.CollectionChanged -= SelectedContactsChanged; + + _searchDebounceCancellationTokenSource?.Cancel(); + _searchDebounceCancellationTokenSource?.Dispose(); + _searchDebounceCancellationTokenSource = null; + } + + private async void SelectedContactsChanged(object sender, NotifyCollectionChangedEventArgs e) + => await ExecuteUIThread(() => { SelectedContactsCount = SelectedContacts.Count; }); + + private async void ContactsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + => await ExecuteUIThread(() => { OnPropertyChanged(nameof(IsEmpty)); }); + + void IRecipient.Receive(NewContactRequested message) + => _ = AddContactAsync(); + + [RelayCommand] + private async Task ReloadContactsAsync() + { + var queryVersion = ++_currentQueryVersion; + _currentOffset = 0; + + await ExecuteUIThread(() => + { + HasMoreContacts = false; + Contacts.Clear(); + SelectedContacts.Clear(); + }); + + await LoadContactsPageAsync(queryVersion, reset: true); + } + + [RelayCommand(CanExecute = nameof(CanLoadMoreContacts))] + private async Task LoadMoreContactsAsync() + { + await LoadContactsPageAsync(_currentQueryVersion, reset: false); + } + + private async Task LoadContactsPageAsync(int queryVersion, bool reset) + { + if (IsLoading || IsLoadingMore) + return; + + await ExecuteUIThread(() => + { + if (reset) + IsLoading = true; + else + IsLoadingMore = true; + }); + + try + { + var searchQuery = string.IsNullOrWhiteSpace(SearchQuery) ? null : SearchQuery.Trim(); + var page = await _contactService.GetContactsPageAsync( + _currentOffset, + ContactPageSize, + searchQuery, + excludeRootContacts: true).ConfigureAwait(false); + + if (queryVersion != _currentQueryVersion) + return; + + await ExecuteUIThread(() => + { + if (reset) + { + Contacts.Clear(); + } + + foreach (var contact in page.Contacts) + { + Contacts.Add(new AccountContactViewModel(contact)); + } + + TotalContactsCount = page.TotalCount; + HasMoreContacts = page.HasMore; + _currentOffset = Contacts.Count; + }); + } + catch (Exception ex) + { + if (queryVersion != _currentQueryVersion) + return; + + _dialogService.InfoBarMessage( + Translator.ContactInfoBar_ErrorTitle, + string.Format(Translator.ContactInfoBar_FailedToLoadContacts, ex.Message), + InfoBarMessageType.Error); + } + finally + { + if (queryVersion == _currentQueryVersion) + { + await ExecuteUIThread(() => + { + if (reset) + IsLoading = false; + else + IsLoadingMore = false; + }); + } + } + } + + [RelayCommand] + private async Task AddContactAsync() + { + var result = await _dialogService.ShowEditContactDialogAsync(null); + + if (result == null) return; + + try + { + var newContact = await _contactService.CreateNewContactAsync(result.Address, result.Name); + + if (result.ContactPictureFileId.HasValue) + { + newContact.ContactPictureFileId = result.ContactPictureFileId; + await _contactService.UpdateContactAsync(newContact); + } + + await ReloadContactsAsync(); + + _dialogService.InfoBarMessage( + Translator.ContactInfoBar_SuccessTitle, + Translator.ContactInfoBar_ContactAdded, + InfoBarMessageType.Success); + } + catch (Exception ex) + { + _dialogService.InfoBarMessage( + Translator.ContactInfoBar_ErrorTitle, + string.Format(Translator.ContactInfoBar_FailedToAddContact, ex.Message), + InfoBarMessageType.Error); + } + } + + protected override void RegisterRecipients() + { + base.RegisterRecipients(); + + Messenger.Register(this); + } + + protected override void UnregisterRecipients() + { + base.UnregisterRecipients(); + + Messenger.Unregister(this); + } + + [RelayCommand] + private async Task EditContactAsync(AccountContactViewModel contactViewModel) + { + var contact = contactViewModel?.SourceContact; + if (contact == null) return; + + var result = await _dialogService.ShowEditContactDialogAsync(contact); + + if (result == null) return; + + try + { + contact.Name = result.Name; + contact.ContactPictureFileId = result.ContactPictureFileId; + contact.IsOverridden = result.IsOverridden; + + await _contactService.UpdateContactAsync(contact); + await ReloadContactsAsync(); + + _dialogService.InfoBarMessage( + Translator.ContactInfoBar_SuccessTitle, + Translator.ContactInfoBar_ContactUpdated, + InfoBarMessageType.Success); + } + catch (Exception ex) + { + _dialogService.InfoBarMessage( + Translator.ContactInfoBar_ErrorTitle, + string.Format(Translator.ContactInfoBar_FailedToUpdateContact, ex.Message), + InfoBarMessageType.Error); + } + } + + [RelayCommand] + private async Task DeleteContactAsync(AccountContactViewModel contactViewModel) + { + var contact = contactViewModel?.SourceContact; + if (contact == null || contact.IsRootContact) + { + _dialogService.InfoBarMessage( + Translator.ContactInfoBar_WarningTitle, + Translator.ContactInfoBar_CannotDeleteRoot, + InfoBarMessageType.Warning); + return; + } + + var confirmed = await _dialogService.ShowConfirmationDialogAsync( + string.Format(Translator.ContactConfirmDialog_DeleteMessage, contact.Name ?? contact.Address), + Translator.ContactConfirmDialog_DeleteTitle, + Translator.ContactConfirmDialog_DeleteButton); + + if (confirmed) + { + await DeleteContactsInternalAsync(new[] { contact }); + } + } + + [RelayCommand(CanExecute = nameof(CanDeleteSelectedContacts))] + private async Task DeleteSelectedContactsAsync() + { + if (SelectedContacts.Count == 0) return; + + var deletableContacts = SelectedContacts + .Select(c => c?.SourceContact) + .Where(c => c != null && !c.IsRootContact) + .GroupBy(c => c.Address, StringComparer.OrdinalIgnoreCase) + .Select(g => g.First()) + .ToList(); + + if (deletableContacts.Count == 0) + { + _dialogService.InfoBarMessage( + Translator.ContactInfoBar_WarningTitle, + Translator.ContactInfoBar_CannotDeleteRoot, + InfoBarMessageType.Warning); + return; + } + + var confirmed = await _dialogService.ShowConfirmationDialogAsync( + string.Format(Translator.ContactConfirmDialog_DeleteMultipleMessage, deletableContacts.Count), + Translator.ContactConfirmDialog_DeleteTitle, + Translator.ContactConfirmDialog_DeleteButton); + + if (confirmed) + { + await DeleteContactsInternalAsync(deletableContacts); + } + } + + private async Task DeleteContactsInternalAsync(IEnumerable contactsToDelete) + { + try + { + var addresses = contactsToDelete + .Select(c => c.Address) + .Where(a => !string.IsNullOrWhiteSpace(a)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (addresses.Count == 0) return; + + await _contactService.DeleteContactsAsync(addresses); + await ReloadContactsAsync(); + + _dialogService.InfoBarMessage( + Translator.ContactInfoBar_SuccessTitle, + Translator.ContactInfoBar_ContactsDeleted, + InfoBarMessageType.Success); + } + catch (Exception ex) + { + _dialogService.InfoBarMessage( + Translator.ContactInfoBar_ErrorTitle, + string.Format(Translator.ContactInfoBar_FailedToDeleteContacts, ex.Message), + InfoBarMessageType.Error); + } + } + + [RelayCommand] + private async Task ToggleSelection() + { + await ExecuteUIThread(() => + { + IsSelectionMode = !IsSelectionMode; + + if (!IsSelectionMode) + { + SelectedContacts.Clear(); + } + }); + } + + [RelayCommand] + private async Task SelectAllContacts() + { + await ExecuteUIThread(() => + { + SelectedContacts.Clear(); + + foreach (var contact in Contacts) + { + SelectedContacts.Add(contact); + } + }); + } + + [RelayCommand] + private async Task ClearSelection() + { + await ExecuteUIThread(() => { SelectedContacts.Clear(); }); + } + + [RelayCommand] + private async Task PickContactPhotoAsync(AccountContactViewModel contactViewModel) + { + var contact = contactViewModel?.SourceContact; + if (contact == null) return; + + try + { + var files = await _dialogService.PickFilesAsync(".png", ".jpg", ".jpeg"); + + if (files?.Any() == true) + { + var file = files.First(); + + if (contact.ContactPictureFileId.HasValue) + await _contactPictureFileService.DeleteContactPictureAsync(contact.ContactPictureFileId.Value); + + contact.ContactPictureFileId = await _contactPictureFileService + .SaveContactPictureAsync(file.Data) + .ConfigureAwait(false); + + await _contactService.UpdateContactAsync(contact); + await RefreshContactInUiAsync(contact); + + _dialogService.InfoBarMessage( + Translator.ContactInfoBar_SuccessTitle, + Translator.ContactInfoBar_ContactPhotoUpdated, + InfoBarMessageType.Success); + } + } + catch (Exception ex) + { + _dialogService.InfoBarMessage( + Translator.ContactInfoBar_ErrorTitle, + string.Format(Translator.ContactInfoBar_FailedToUpdatePhoto, ex.Message), + InfoBarMessageType.Error); + } + } + + private async Task RefreshContactInUiAsync(AccountContact contact) + { + if (contact == null || string.IsNullOrWhiteSpace(contact.Address)) + return; + + await ExecuteUIThread(() => + { + ReplaceContactByAddress(Contacts, contact); + ReplaceContactByAddress(SelectedContacts, contact); + }); + } + + private static void ReplaceContactByAddress(ObservableCollection source, AccountContact updatedContact) + { + var index = source + .Select((item, i) => new { item, i }) + .FirstOrDefault(x => string.Equals(x.item.Address, updatedContact.Address, StringComparison.OrdinalIgnoreCase)) + ?.i ?? -1; + + if (index < 0) return; + + source[index] = new AccountContactViewModel(CloneContact(updatedContact)); + } + + private static AccountContact CloneContact(AccountContact contact) + => new() + { + Address = contact.Address, + Name = contact.Name, + ContactPictureFileId = contact.ContactPictureFileId, + IsRootContact = contact.IsRootContact, + IsOverridden = contact.IsOverridden + }; + + partial void OnSearchQueryChanged(string value) + { + DebounceSearchAndReload(); + } + + private async void DebounceSearchAndReload() + { + _searchDebounceCancellationTokenSource?.Cancel(); + _searchDebounceCancellationTokenSource?.Dispose(); + + _searchDebounceCancellationTokenSource = new CancellationTokenSource(); + + try + { + await Task.Delay(250, _searchDebounceCancellationTokenSource.Token); + await ReloadContactsAsync(); + } + catch (OperationCanceledException) + { + // Ignore stale search input. + } + } +} diff --git a/Wino.Mail.ViewModels/CreateEmailTemplatePageViewModel.cs b/Wino.Mail.ViewModels/CreateEmailTemplatePageViewModel.cs new file mode 100644 index 00000000..7e678132 --- /dev/null +++ b/Wino.Mail.ViewModels/CreateEmailTemplatePageViewModel.cs @@ -0,0 +1,112 @@ +using System; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using Wino.Core.Domain; +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; + +namespace Wino.Mail.ViewModels; + +public partial class CreateEmailTemplatePageViewModel( + IEmailTemplateService emailTemplateService, + IMailDialogService dialogService, + INavigationService navigationService) : MailBaseViewModel +{ + private readonly IEmailTemplateService _emailTemplateService = emailTemplateService; + private readonly IMailDialogService _dialogService = dialogService; + + private EmailTemplate _editingTemplate; + + public INavigationService NavigationService { get; } = navigationService; + + [ObservableProperty] + public partial string TemplateName { get; set; } = string.Empty; + + [ObservableProperty] + public partial string TemplateDescription { get; set; } = string.Empty; + + [ObservableProperty] + public partial bool IsExistingTemplate { get; set; } + + public async Task LoadAsync(object parameter) + { + EmailTemplate template = null; + + var templateId = parameter switch + { + Guid guid when guid != Guid.Empty => guid, + string value when Guid.TryParse(value, out var parsedGuid) => parsedGuid, + EmailTemplate emailTemplate when emailTemplate.Id != Guid.Empty => emailTemplate.Id, + _ => Guid.Empty + }; + + if (templateId != Guid.Empty) + { + template = await _emailTemplateService.GetEmailTemplateAsync(templateId).ConfigureAwait(false); + } + + _editingTemplate = template; + + await ExecuteUIThread(() => + { + IsExistingTemplate = template != null; + TemplateName = template?.Name ?? string.Empty; + TemplateDescription = template?.Description ?? string.Empty; + }); + + return template?.HtmlContent ?? string.Empty; + } + + public async Task SaveAsync(string htmlContent) + { + var trimmedName = TemplateName?.Trim() ?? string.Empty; + + if (string.IsNullOrWhiteSpace(trimmedName)) + { + _dialogService.InfoBarMessage( + Translator.GeneralTitle_Error, + Translator.SettingsEmailTemplates_NameRequired, + InfoBarMessageType.Warning); + return; + } + + var template = _editingTemplate ?? new EmailTemplate + { + Id = Guid.NewGuid() + }; + + template.Name = trimmedName; + template.Description = TemplateDescription?.Trim() ?? string.Empty; + template.HtmlContent = htmlContent ?? string.Empty; + + if (_editingTemplate == null) + { + await _emailTemplateService.CreateEmailTemplateAsync(template).ConfigureAwait(false); + } + else + { + await _emailTemplateService.UpdateEmailTemplateAsync(template).ConfigureAwait(false); + } + + _editingTemplate = template; + NavigationService.GoBack(); + } + + public async Task DeleteAsync() + { + if (_editingTemplate == null) + return; + + var shouldDelete = await _dialogService.ShowConfirmationDialogAsync( + string.Format(Translator.DialogMessage_DeleteEmailTemplateConfirmationMessage, _editingTemplate.Name), + Translator.DialogMessage_DeleteEmailTemplateConfirmationTitle, + Translator.Buttons_Delete).ConfigureAwait(false); + + if (!shouldDelete) + return; + + await _emailTemplateService.DeleteEmailTemplateAsync(_editingTemplate).ConfigureAwait(false); + NavigationService.GoBack(); + } +} diff --git a/Wino.Mail.ViewModels/Data/AccountContactViewModel.cs b/Wino.Mail.ViewModels/Data/AccountContactViewModel.cs index cc9ff979..7a9ae7f2 100644 --- a/Wino.Mail.ViewModels/Data/AccountContactViewModel.cs +++ b/Wino.Mail.ViewModels/Data/AccountContactViewModel.cs @@ -1,23 +1,28 @@ -using System; +using System; using CommunityToolkit.Mvvm.ComponentModel; using Wino.Core.Domain; using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Interfaces; namespace Wino.Mail.ViewModels.Data; -public partial class AccountContactViewModel : ObservableObject +public partial class AccountContactViewModel : ObservableObject, IMailItemDisplayInformation { + public AccountContact SourceContact { get; } public string Address { get; set; } public string Name { get; set; } - public string Base64ContactPicture { get; set; } + public Guid? ContactPictureFileId { get; set; } public bool IsRootContact { get; set; } + public bool IsOverridden { get; set; } public AccountContactViewModel(AccountContact contact) { + SourceContact = contact; Address = contact.Address; Name = contact.Name; - Base64ContactPicture = contact.Base64ContactPicture; + ContactPictureFileId = contact.ContactPictureFileId; IsRootContact = contact.IsRootContact; + IsOverridden = contact.IsOverridden; } /// @@ -49,4 +54,26 @@ public partial class AccountContactViewModel : ObservableObject [ObservableProperty] public partial bool ThumbnailUpdatedEvent { get; set; } + + // IMailItemDisplayInformation implementation for avatar-only rendering. + public string Subject => string.Empty; + public string FromName => Name ?? string.Empty; + public string FromAddress => Address ?? string.Empty; + public string PreviewText => string.Empty; + public bool IsRead => true; + public bool IsDraft => false; + public bool HasAttachments => false; + public bool IsCalendarEvent => false; + public bool IsFlagged => false; + public DateTime CreationDate => default; + public bool IsBusy => false; + public bool IsThreadExpanded => false; + public AccountContact SenderContact => new() + { + Address = Address, + Name = Name, + ContactPictureFileId = ContactPictureFileId, + IsRootContact = IsRootContact, + IsOverridden = IsOverridden + }; } diff --git a/Wino.Mail.ViewModels/Data/AccountStorageItemViewModel.cs b/Wino.Mail.ViewModels/Data/AccountStorageItemViewModel.cs new file mode 100644 index 00000000..b82046a1 --- /dev/null +++ b/Wino.Mail.ViewModels/Data/AccountStorageItemViewModel.cs @@ -0,0 +1,40 @@ +using System.Windows.Input; +using CommunityToolkit.Mvvm.ComponentModel; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Extensions; + +namespace Wino.Mail.ViewModels.Data; + +public partial class AccountStorageItemViewModel(MailAccount account, long sizeBytes, ICommand deleteAllCommand, ICommand deleteOneMonthCommand, ICommand deleteThreeMonthsCommand, ICommand deleteSixMonthsCommand, ICommand deleteYearCommand) : ObservableObject +{ + public MailAccount Account { get; } = account; + + [ObservableProperty] + public partial bool IsBusy { get; set; } + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(SizeText))] + public partial long SizeBytes { get; set; } = sizeBytes; + + [ObservableProperty] + public partial string SizeDescription { get; set; } = string.Empty; + + [ObservableProperty] + public partial ICommand DeleteAllCommand { get; set; } = deleteAllCommand; + + [ObservableProperty] + public partial ICommand DeleteOneMonthCommand { get; set; } = deleteOneMonthCommand; + + [ObservableProperty] + public partial ICommand DeleteThreeMonthsCommand { get; set; } = deleteThreeMonthsCommand; + + [ObservableProperty] + public partial ICommand DeleteSixMonthsCommand { get; set; } = deleteSixMonthsCommand; + + [ObservableProperty] + public partial ICommand DeleteYearCommand { get; set; } = deleteYearCommand; + + public string AccountName => string.IsNullOrWhiteSpace(Account.Name) ? Account.Address ?? string.Empty : Account.Name; + public string AccountAddress => Account.Address ?? string.Empty; + public string SizeText => SizeBytes.GetBytesReadable(); +} diff --git a/Wino.Mail.ViewModels/Data/IMailListItem.cs b/Wino.Mail.ViewModels/Data/IMailListItem.cs new file mode 100644 index 00000000..957915ba --- /dev/null +++ b/Wino.Mail.ViewModels/Data/IMailListItem.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Wino.Core.Domain.Interfaces; + +namespace Wino.Mail.ViewModels.Data; + +/// +/// Common interface for mail items that can be displayed in a mail list. +/// Implemented by both MailItemViewModel and ThreadMailItemViewModel. +/// +public interface IMailListItem : IMailHashContainer, IMailListItemSorting, INotifyPropertyChanged +{ + /// + /// Gets the latest creation date for sorting purposes. + /// For MailItemViewModel: the mail's creation date + /// For ThreadMailItemViewModel: the latest email's creation date + /// + DateTime CreationDate { get; } + + /// + /// Gets the sender's name for grouping purposes. + /// For MailItemViewModel: the mail's from name + /// For ThreadMailItemViewModel: the latest email's from name + /// + string FromName { get; } + + /// + /// Gets whether this item is selected. + /// For MailItemViewModel: returns IsSelected + /// For ThreadMailItemViewModel: returns IsSelected + /// + bool IsSelected { get; set; } + + /// + /// Gets whether this item is currently processing a network operation. + /// + bool IsBusy { get; } + + /// + /// Gets all selected mail items within this list item. + /// For MailItemViewModel: returns itself if IsSelected is true, otherwise empty + /// For ThreadMailItemViewModel: returns all selected emails within the thread + /// + IEnumerable GetSelectedMailItems(); +} diff --git a/Wino.Mail.ViewModels/Data/ImapCalDavSettingsNavigationContext.cs b/Wino.Mail.ViewModels/Data/ImapCalDavSettingsNavigationContext.cs new file mode 100644 index 00000000..8a1bce39 --- /dev/null +++ b/Wino.Mail.ViewModels/Data/ImapCalDavSettingsNavigationContext.cs @@ -0,0 +1,63 @@ +using System; +using System.Threading.Tasks; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models.Accounts; + +namespace Wino.Mail.ViewModels.Data; + +public enum ImapCalDavSettingsPageMode +{ + Create, + Edit, + Wizard +} + +public sealed class ImapCalDavSettingsNavigationContext +{ + public ImapCalDavSettingsPageMode Mode { get; init; } + public Guid AccountId { get; init; } + public AccountCreationDialogResult AccountCreationDialogResult { get; init; } + public TaskCompletionSource CompletionSource { get; init; } + + public static ImapCalDavSettingsNavigationContext CreateForCreateMode( + AccountCreationDialogResult accountCreationDialogResult, + TaskCompletionSource completionSource) + => new() + { + Mode = ImapCalDavSettingsPageMode.Create, + AccountCreationDialogResult = accountCreationDialogResult, + CompletionSource = completionSource + }; + + public static ImapCalDavSettingsNavigationContext CreateForEditMode(Guid accountId) + => new() + { + Mode = ImapCalDavSettingsPageMode.Edit, + AccountId = accountId + }; + + public static ImapCalDavSettingsNavigationContext CreateForWizardMode( + AccountCreationDialogResult accountCreationDialogResult) + => new() + { + Mode = ImapCalDavSettingsPageMode.Wizard, + AccountCreationDialogResult = accountCreationDialogResult + }; + + public bool IsWizardMode => Mode == ImapCalDavSettingsPageMode.Wizard; +} + +public sealed class ImapCalDavSetupResult +{ + public string DisplayName { get; init; } + public string EmailAddress { get; init; } + public bool IsCalendarAccessGranted { get; init; } + public CustomServerInformation ServerInformation { get; init; } +} + +public sealed class ImapCalendarSupportModeOption(ImapCalendarSupportMode mode, string title) +{ + public ImapCalendarSupportMode Mode { get; } = mode; + public string Title { get; } = title; +} diff --git a/Wino.Mail.ViewModels/Data/MailItemContainer.cs b/Wino.Mail.ViewModels/Data/MailItemContainer.cs index 08920973..67d5f590 100644 --- a/Wino.Mail.ViewModels/Data/MailItemContainer.cs +++ b/Wino.Mail.ViewModels/Data/MailItemContainer.cs @@ -6,6 +6,25 @@ public class MailItemContainer { public MailItemViewModel ItemViewModel { get; set; } public ThreadMailItemViewModel ThreadViewModel { get; set; } + + /// + /// Indicates whether the mail item is currently visible in the UI's Items collection. + /// For threaded items, this indicates if the individual mail item is visible (thread must be expanded). + /// + public bool IsItemVisible { get; set; } + + /// + /// Indicates whether the thread expander (if applicable) is currently visible in the UI's Items collection. + /// Only relevant when ThreadViewModel is not null. + /// + public bool IsThreadVisible { get; set; } + + /// + /// Indicates whether the container can be successfully navigated to in the UI. + /// For standalone items: true if IsItemVisible is true. + /// For threaded items: true if IsThreadVisible is true (the thread expander can be navigated to). + /// + public bool CanNavigate => ThreadViewModel != null ? IsThreadVisible : IsItemVisible; public MailItemContainer(MailItemViewModel itemViewModel, ThreadMailItemViewModel threadViewModel) : this(itemViewModel) { diff --git a/Wino.Mail.ViewModels/Data/MailItemViewModel.cs b/Wino.Mail.ViewModels/Data/MailItemViewModel.cs index 66344587..7eebd613 100644 --- a/Wino.Mail.ViewModels/Data/MailItemViewModel.cs +++ b/Wino.Mail.ViewModels/Data/MailItemViewModel.cs @@ -3,34 +3,78 @@ using System.Collections.Generic; using CommunityToolkit.Mvvm.ComponentModel; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Shared; -using Wino.Core.Domain.Models.MailItem; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; namespace Wino.Mail.ViewModels.Data; /// /// Single view model for IMailItem representation. /// -public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject, IMailItem +public partial class MailItemViewModel(MailCopy mailCopy) : ObservableRecipient, IMailListItem, IMailItemDisplayInformation { [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CreationDate))] + [NotifyPropertyChangedFor(nameof(IsFlagged))] + [NotifyPropertyChangedFor(nameof(FromName))] + [NotifyPropertyChangedFor(nameof(IsFocused))] + [NotifyPropertyChangedFor(nameof(IsRead))] + [NotifyPropertyChangedFor(nameof(IsDraft))] + [NotifyPropertyChangedFor(nameof(DraftId))] + [NotifyPropertyChangedFor(nameof(Id))] + [NotifyPropertyChangedFor(nameof(Subject))] + [NotifyPropertyChangedFor(nameof(PreviewText))] + [NotifyPropertyChangedFor(nameof(FromAddress))] + [NotifyPropertyChangedFor(nameof(HasAttachments))] + [NotifyPropertyChangedFor(nameof(IsCalendarEvent))] + [NotifyPropertyChangedFor(nameof(Importance))] + [NotifyPropertyChangedFor(nameof(ThreadId))] + [NotifyPropertyChangedFor(nameof(MessageId))] + [NotifyPropertyChangedFor(nameof(References))] + [NotifyPropertyChangedFor(nameof(InReplyTo))] + [NotifyPropertyChangedFor(nameof(FileId))] + [NotifyPropertyChangedFor(nameof(FolderId))] + [NotifyPropertyChangedFor(nameof(UniqueId))] + [NotifyPropertyChangedFor(nameof(ContactPictureFileId))] + [NotifyPropertyChangedFor(nameof(SenderContact))] public partial MailCopy MailCopy { get; set; } = mailCopy; - public Guid UniqueId => ((IMailItem)MailCopy).UniqueId; - public string ThreadId => ((IMailItem)MailCopy).ThreadId; - public string MessageId => ((IMailItem)MailCopy).MessageId; - public DateTime CreationDate => ((IMailItem)MailCopy).CreationDate; - public string References => ((IMailItem)MailCopy).References; - public string InReplyTo => ((IMailItem)MailCopy).InReplyTo; + [ObservableProperty] + public partial bool IsDisplayedInThread { get; set; } + + [ObservableProperty] + [NotifyPropertyChangedRecipients] + public partial bool IsSelected { get; set; } + + /// + /// Direct callback invoked when changes. + /// Used by the ListViewItem container to update its IsCustomSelected DP + /// without subscribing to INotifyPropertyChanged (faster, AOT-safe). + /// + public Action OnSelectionChanged { get; set; } + + partial void OnIsSelectedChanged(bool value) => OnSelectionChanged?.Invoke(value); + + /// + /// Indicates if this mail item is currently being processed by a network operation. + /// Used to show loading state in the UI. + /// + [ObservableProperty] + public partial bool IsBusy { get; set; } + + public bool IsThreadExpanded => false; + + public AccountContact SenderContact => MailCopy.SenderContact; + + public DateTime CreationDate + { + get => MailCopy.CreationDate; + set => SetProperty(MailCopy.CreationDate, value, MailCopy, (u, n) => u.CreationDate = n); + } [ObservableProperty] public partial bool ThumbnailUpdatedEvent { get; set; } = false; - [ObservableProperty] - public partial bool IsCustomFocused { get; set; } - - [ObservableProperty] - public partial bool IsSelected { get; set; } - public bool IsFlagged { get => MailCopy.IsFlagged; @@ -97,13 +141,274 @@ public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject, IM set => SetProperty(MailCopy.HasAttachments, value, MailCopy, (u, n) => u.HasAttachments = n); } - public MailItemFolder AssignedFolder => ((IMailItem)MailCopy).AssignedFolder; + public bool IsCalendarEvent => MailCopy.ItemType == MailItemType.CalendarInvitation; - public MailAccount AssignedAccount => ((IMailItem)MailCopy).AssignedAccount; + public MailImportance Importance + { + get => MailCopy.Importance; + set => SetProperty(MailCopy.Importance, value, MailCopy, (u, n) => u.Importance = n); + } - public Guid FileId => ((IMailItem)MailCopy).FileId; + public string ThreadId + { + get => MailCopy.ThreadId; + set => SetProperty(MailCopy.ThreadId, value, MailCopy, (u, n) => u.ThreadId = n); + } - public AccountContact SenderContact => ((IMailItem)MailCopy).SenderContact; + public string MessageId + { + get => MailCopy.MessageId; + set => SetProperty(MailCopy.MessageId, value, MailCopy, (u, n) => u.MessageId = n); + } - public IEnumerable GetContainingIds() => new[] { UniqueId }; + public string References + { + get => MailCopy.References; + set => SetProperty(MailCopy.References, value, MailCopy, (u, n) => u.References = n); + } + + public string InReplyTo + { + get => MailCopy.InReplyTo; + set => SetProperty(MailCopy.InReplyTo, value, MailCopy, (u, n) => u.InReplyTo = n); + } + + public Guid FileId + { + get => MailCopy.FileId; + set => SetProperty(MailCopy.FileId, value, MailCopy, (u, n) => u.FileId = n); + } + + public Guid FolderId + { + get => MailCopy.FolderId; + set => SetProperty(MailCopy.FolderId, value, MailCopy, (u, n) => u.FolderId = n); + } + + public Guid UniqueId + { + get => MailCopy.UniqueId; + set => SetProperty(MailCopy.UniqueId, value, MailCopy, (u, n) => u.UniqueId = n); + } + + public Guid? ContactPictureFileId + { + get => MailCopy.SenderContact?.ContactPictureFileId; + set => SetProperty(MailCopy.SenderContact?.ContactPictureFileId, value, MailCopy, (u, n) => + { + if (u.SenderContact != null) + u.SenderContact.ContactPictureFileId = n; + }); + } + + public DateTime SortingDate => CreationDate; + + public string SortingName => FromName; + + public IEnumerable GetContainingIds() => [MailCopy.UniqueId]; + + public IEnumerable GetSelectedMailItems() + { + if (IsSelected) + { + yield return this; + } + } + + public static MailCopyChangeFlags GetChangeFlagsForProperty(string propertyName) + { + return propertyName switch + { + nameof(CreationDate) or nameof(SortingDate) => MailCopyChangeFlags.CreationDate, + nameof(IsFlagged) => MailCopyChangeFlags.IsFlagged, + nameof(FromName) or nameof(SortingName) => MailCopyChangeFlags.FromName, + nameof(IsFocused) => MailCopyChangeFlags.IsFocused, + nameof(IsRead) => MailCopyChangeFlags.IsRead, + nameof(IsDraft) => MailCopyChangeFlags.IsDraft, + nameof(DraftId) => MailCopyChangeFlags.DraftId, + nameof(Id) => MailCopyChangeFlags.Id, + nameof(Subject) => MailCopyChangeFlags.Subject, + nameof(PreviewText) => MailCopyChangeFlags.PreviewText, + nameof(FromAddress) => MailCopyChangeFlags.FromAddress, + nameof(HasAttachments) => MailCopyChangeFlags.HasAttachments, + nameof(IsCalendarEvent) => MailCopyChangeFlags.ItemType, + nameof(Importance) => MailCopyChangeFlags.Importance, + nameof(ThreadId) => MailCopyChangeFlags.ThreadId, + nameof(MessageId) => MailCopyChangeFlags.MessageId, + nameof(References) => MailCopyChangeFlags.References, + nameof(InReplyTo) => MailCopyChangeFlags.InReplyTo, + nameof(FileId) => MailCopyChangeFlags.FileId, + nameof(FolderId) => MailCopyChangeFlags.FolderId, + nameof(UniqueId) => MailCopyChangeFlags.UniqueId, + nameof(ContactPictureFileId) or nameof(SenderContact) => MailCopyChangeFlags.SenderContact, + _ => MailCopyChangeFlags.None + }; + } + + /// + /// Updates the existing while raising only the relevant UI notifications. + /// + /// Source data used to update this item. + /// + /// Optional set of known changes. This is required when is the same instance + /// and has already been mutated by Apply/Revert flows. + /// + /// The effective set of changed fields used for notifications. + public MailCopyChangeFlags UpdateFrom(MailCopy source, MailCopyChangeFlags changeHint = MailCopyChangeFlags.None) + { + if (source == null) return MailCopyChangeFlags.None; + + var changedFlags = MailCopyChangeFlags.None; + var isSameReference = ReferenceEquals(MailCopy, source); + + if (!isSameReference) + { + changedFlags |= SetIfChanged(MailCopy.Id, source.Id, value => MailCopy.Id = value, MailCopyChangeFlags.Id); + changedFlags |= SetIfChanged(MailCopy.FolderId, source.FolderId, value => MailCopy.FolderId = value, MailCopyChangeFlags.FolderId); + changedFlags |= SetIfChanged(MailCopy.ThreadId, source.ThreadId, value => MailCopy.ThreadId = value, MailCopyChangeFlags.ThreadId); + changedFlags |= SetIfChanged(MailCopy.MessageId, source.MessageId, value => MailCopy.MessageId = value, MailCopyChangeFlags.MessageId); + changedFlags |= SetIfChanged(MailCopy.References, source.References, value => MailCopy.References = value, MailCopyChangeFlags.References); + changedFlags |= SetIfChanged(MailCopy.InReplyTo, source.InReplyTo, value => MailCopy.InReplyTo = value, MailCopyChangeFlags.InReplyTo); + changedFlags |= SetIfChanged(MailCopy.IsDraft, source.IsDraft, value => MailCopy.IsDraft = value, MailCopyChangeFlags.IsDraft); + changedFlags |= SetIfChanged(MailCopy.DraftId, source.DraftId, value => MailCopy.DraftId = value, MailCopyChangeFlags.DraftId); + changedFlags |= SetIfChanged(MailCopy.CreationDate, source.CreationDate, value => MailCopy.CreationDate = value, MailCopyChangeFlags.CreationDate); + changedFlags |= SetIfChanged(MailCopy.Subject, source.Subject, value => MailCopy.Subject = value, MailCopyChangeFlags.Subject); + changedFlags |= SetIfChanged(MailCopy.PreviewText, source.PreviewText, value => MailCopy.PreviewText = value, MailCopyChangeFlags.PreviewText); + changedFlags |= SetIfChanged(MailCopy.FromName, source.FromName, value => MailCopy.FromName = value, MailCopyChangeFlags.FromName); + changedFlags |= SetIfChanged(MailCopy.FromAddress, source.FromAddress, value => MailCopy.FromAddress = value, MailCopyChangeFlags.FromAddress); + changedFlags |= SetIfChanged(MailCopy.HasAttachments, source.HasAttachments, value => MailCopy.HasAttachments = value, MailCopyChangeFlags.HasAttachments); + changedFlags |= SetIfChanged(MailCopy.Importance, source.Importance, value => MailCopy.Importance = value, MailCopyChangeFlags.Importance); + changedFlags |= SetIfChanged(MailCopy.IsRead, source.IsRead, value => MailCopy.IsRead = value, MailCopyChangeFlags.IsRead); + changedFlags |= SetIfChanged(MailCopy.IsFlagged, source.IsFlagged, value => MailCopy.IsFlagged = value, MailCopyChangeFlags.IsFlagged); + changedFlags |= SetIfChanged(MailCopy.IsFocused, source.IsFocused, value => MailCopy.IsFocused = value, MailCopyChangeFlags.IsFocused); + changedFlags |= SetIfChanged(MailCopy.FileId, source.FileId, value => MailCopy.FileId = value, MailCopyChangeFlags.FileId); + changedFlags |= SetIfChanged(MailCopy.ItemType, source.ItemType, value => MailCopy.ItemType = value, MailCopyChangeFlags.ItemType); + changedFlags |= SetIfChanged(MailCopy.SenderContact, source.SenderContact, value => MailCopy.SenderContact = value, MailCopyChangeFlags.SenderContact); + changedFlags |= SetIfChanged(MailCopy.AssignedAccount, source.AssignedAccount, value => MailCopy.AssignedAccount = value, MailCopyChangeFlags.AssignedAccount); + changedFlags |= SetIfChanged(MailCopy.AssignedFolder, source.AssignedFolder, value => MailCopy.AssignedFolder = value, MailCopyChangeFlags.AssignedFolder); + changedFlags |= SetIfChanged(MailCopy.UniqueId, source.UniqueId, value => MailCopy.UniqueId = value, MailCopyChangeFlags.UniqueId); + } + + changedFlags |= changeHint; + + if (isSameReference && changedFlags == MailCopyChangeFlags.None) + { + // Without a hint there is no reliable way to diff in-place updates on the same instance. + // Fall back to full refresh to preserve correctness. + changedFlags = MailCopyChangeFlags.All; + } + + RaisePropertyChanges(changedFlags); + + return changedFlags; + } + + private static MailCopyChangeFlags SetIfChanged(T currentValue, T newValue, Action setter, MailCopyChangeFlags flag) + { + if (EqualityComparer.Default.Equals(currentValue, newValue)) + return MailCopyChangeFlags.None; + + setter(newValue); + return flag; + } + + private void RaisePropertyChanges(MailCopyChangeFlags changedFlags) + { + if (changedFlags == MailCopyChangeFlags.None) + return; + + var changedProperties = new List(12); + + void Queue(string propertyName) + { + if (!changedProperties.Contains(propertyName)) + { + changedProperties.Add(propertyName); + } + } + + if ((changedFlags & MailCopyChangeFlags.CreationDate) != 0) + { + Queue(nameof(CreationDate)); + Queue(nameof(SortingDate)); + } + + if ((changedFlags & MailCopyChangeFlags.IsFlagged) != 0) + Queue(nameof(IsFlagged)); + + if ((changedFlags & MailCopyChangeFlags.FromName) != 0) + { + Queue(nameof(FromName)); + Queue(nameof(SortingName)); + } + + if ((changedFlags & MailCopyChangeFlags.FromAddress) != 0) + { + Queue(nameof(FromAddress)); + Queue(nameof(FromName)); + Queue(nameof(SortingName)); + } + + if ((changedFlags & MailCopyChangeFlags.IsFocused) != 0) + Queue(nameof(IsFocused)); + + if ((changedFlags & MailCopyChangeFlags.IsRead) != 0) + Queue(nameof(IsRead)); + + if ((changedFlags & MailCopyChangeFlags.IsDraft) != 0) + Queue(nameof(IsDraft)); + + if ((changedFlags & MailCopyChangeFlags.DraftId) != 0) + Queue(nameof(DraftId)); + + if ((changedFlags & MailCopyChangeFlags.Id) != 0) + Queue(nameof(Id)); + + if ((changedFlags & MailCopyChangeFlags.Subject) != 0) + Queue(nameof(Subject)); + + if ((changedFlags & MailCopyChangeFlags.PreviewText) != 0) + Queue(nameof(PreviewText)); + + if ((changedFlags & MailCopyChangeFlags.HasAttachments) != 0) + Queue(nameof(HasAttachments)); + + if ((changedFlags & MailCopyChangeFlags.ItemType) != 0) + Queue(nameof(IsCalendarEvent)); + + if ((changedFlags & MailCopyChangeFlags.Importance) != 0) + Queue(nameof(Importance)); + + if ((changedFlags & MailCopyChangeFlags.ThreadId) != 0) + Queue(nameof(ThreadId)); + + if ((changedFlags & MailCopyChangeFlags.MessageId) != 0) + Queue(nameof(MessageId)); + + if ((changedFlags & MailCopyChangeFlags.References) != 0) + Queue(nameof(References)); + + if ((changedFlags & MailCopyChangeFlags.InReplyTo) != 0) + Queue(nameof(InReplyTo)); + + if ((changedFlags & MailCopyChangeFlags.FileId) != 0) + Queue(nameof(FileId)); + + if ((changedFlags & MailCopyChangeFlags.FolderId) != 0) + Queue(nameof(FolderId)); + + if ((changedFlags & MailCopyChangeFlags.UniqueId) != 0) + Queue(nameof(UniqueId)); + + if ((changedFlags & MailCopyChangeFlags.SenderContact) != 0) + { + Queue(nameof(ContactPictureFileId)); + Queue(nameof(SenderContact)); + } + + foreach (var changedProperty in changedProperties) + { + OnPropertyChanged(changedProperty); + } + } } diff --git a/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs b/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs index bcc87aef..bc82acf5 100644 --- a/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs +++ b/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs @@ -1,126 +1,484 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics; +using System.ComponentModel; using System.Linq; using CommunityToolkit.Mvvm.ComponentModel; -using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain; using Wino.Core.Domain.Entities.Shared; -using Wino.Core.Domain.Models.MailItem; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; namespace Wino.Mail.ViewModels.Data; /// /// Thread mail item (multiple IMailItem) view model representation. /// -public partial class ThreadMailItemViewModel : ObservableObject, IMailItemThread, IComparable, IComparable +public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListItem, IMailItemDisplayInformation { - public ObservableCollection ThreadItems => (MailItem as IMailItemThread)?.ThreadItems ?? []; - public AccountContact SenderContact => ((IMailItemThread)MailItem).SenderContact; + private readonly string _threadId; + private readonly HashSet _uniqueIdSet = []; + private MailItemViewModel _cachedLatestMailViewModel; + private int _suspendChildPropertyNotificationsCount; [ObservableProperty] - private ThreadMailItem mailItem; + [NotifyPropertyChangedRecipients] + [NotifyPropertyChangedFor(nameof(IsSelectedOrExpanded))] + public partial bool IsThreadExpanded { get; set; } [ObservableProperty] - private bool isThreadExpanded; + [NotifyPropertyChangedRecipients] + [NotifyPropertyChangedFor(nameof(IsSelectedOrExpanded))] + public partial bool IsSelected { get; set; } - public ThreadMailItemViewModel(ThreadMailItem threadMailItem) + /// + /// Direct callback invoked when changes. + /// Used by the ListViewItem container to update its IsCustomSelected DP + /// without subscribing to INotifyPropertyChanged (faster, AOT-safe). + /// + public Action OnSelectionChanged { get; set; } + + partial void OnIsSelectedChanged(bool value) => OnSelectionChanged?.Invoke(value); + + [ObservableProperty] + public partial bool IsBusy { get; set; } + + public bool IsSelectedOrExpanded => IsSelected || IsThreadExpanded; + + /// + /// Gets the number of emails in this thread + /// + public int EmailCount => ThreadEmails.Count; + + /// + /// Gets the latest email's subject for display + /// + public string Subject => latestMailViewModel?.MailCopy?.Subject; + + /// + /// Gets the latest email's sender name for display + /// + public string FromName => latestMailViewModel?.MailCopy?.FromName ?? Translator.UnknownSender; + + /// + /// Gets the latest email's creation date for sorting + /// + public DateTime CreationDate => latestMailViewModel?.MailCopy?.CreationDate ?? DateTime.MinValue; + + /// + /// Gets the latest email's sender address for display + /// + public string FromAddress => latestMailViewModel?.FromAddress ?? string.Empty; + + /// + /// Gets the preview text from the latest email + /// + public string PreviewText => latestMailViewModel?.PreviewText ?? string.Empty; + + /// + /// Gets whether any email in this thread has attachments + /// + public bool HasAttachments => ThreadEmails.Any(e => e.HasAttachments); + + /// + /// Gets whether any email in this thread is a calendar invitation. + /// + public bool IsCalendarEvent => ThreadEmails.Any(e => e.IsCalendarEvent); + + /// + /// Gets whether any email in this thread is flagged + /// + public bool IsFlagged => ThreadEmails.Any(e => e.IsFlagged); + + /// + /// Gets whether the latest email is focused + /// + public bool IsFocused => latestMailViewModel?.IsFocused ?? false; + + /// + /// Gets whether all emails in this thread are read + /// + public bool IsRead => ThreadEmails.All(e => e.IsRead); + + /// + /// Gets whether any email in this thread is a draft + /// + public bool IsDraft => ThreadEmails.Any(e => e.IsDraft); + + /// + /// Gets the draft ID from the latest email if it's a draft + /// + public string DraftId => latestMailViewModel?.DraftId ?? string.Empty; + + /// + /// Gets the ID from the latest email + /// + public string Id => latestMailViewModel?.Id ?? string.Empty; + + /// + /// Gets the importance of the latest email + /// + public MailImportance Importance => latestMailViewModel?.Importance ?? MailImportance.Normal; + + /// + /// Gets the thread ID from the latest email + /// + public string ThreadId => latestMailViewModel?.ThreadId ?? _threadId; + + /// + /// Gets the message ID from the latest email + /// + public string MessageId => latestMailViewModel?.MessageId ?? string.Empty; + + /// + /// Gets the references from the latest email + /// + public string References => latestMailViewModel?.References ?? string.Empty; + + /// + /// Gets the in-reply-to from the latest email + /// + public string InReplyTo => latestMailViewModel?.InReplyTo ?? string.Empty; + + /// + /// Gets the file ID from the latest email + /// + public Guid FileId => latestMailViewModel?.FileId ?? Guid.Empty; + + /// + /// Gets the folder ID from the latest email + /// + public Guid FolderId => latestMailViewModel?.FolderId ?? Guid.Empty; + + /// + /// Gets the unique ID from the latest email + /// + public Guid UniqueId => latestMailViewModel?.UniqueId ?? Guid.Empty; + + public Guid? ContactPictureFileId => latestMailViewModel?.MailCopy?.SenderContact?.ContactPictureFileId; + + public bool ThumbnailUpdatedEvent => latestMailViewModel?.ThumbnailUpdatedEvent ?? false; + + public AccountContact SenderContact => latestMailViewModel?.MailCopy?.SenderContact; + + /// + /// Gets all emails in this thread (observable) + /// + /// + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(EmailCount))] + [NotifyPropertyChangedFor(nameof(Subject))] + [NotifyPropertyChangedFor(nameof(FromName))] + [NotifyPropertyChangedFor(nameof(CreationDate))] + [NotifyPropertyChangedFor(nameof(FromAddress))] + [NotifyPropertyChangedFor(nameof(PreviewText))] + [NotifyPropertyChangedFor(nameof(HasAttachments))] + [NotifyPropertyChangedFor(nameof(IsCalendarEvent))] + [NotifyPropertyChangedFor(nameof(IsFlagged))] + [NotifyPropertyChangedFor(nameof(IsFocused))] + [NotifyPropertyChangedFor(nameof(IsRead))] + [NotifyPropertyChangedFor(nameof(IsDraft))] + [NotifyPropertyChangedFor(nameof(DraftId))] + [NotifyPropertyChangedFor(nameof(Id))] + [NotifyPropertyChangedFor(nameof(Importance))] + [NotifyPropertyChangedFor(nameof(ThreadId))] + [NotifyPropertyChangedFor(nameof(MessageId))] + [NotifyPropertyChangedFor(nameof(References))] + [NotifyPropertyChangedFor(nameof(InReplyTo))] + [NotifyPropertyChangedFor(nameof(FileId))] + [NotifyPropertyChangedFor(nameof(FolderId))] + [NotifyPropertyChangedFor(nameof(UniqueId))] + [NotifyPropertyChangedFor(nameof(ContactPictureFileId))] + [NotifyPropertyChangedFor(nameof(SenderContact))] + public partial ObservableCollection ThreadEmails { get; set; } = []; + + private MailItemViewModel latestMailViewModel => _cachedLatestMailViewModel; + + public DateTime SortingDate => CreationDate; + + public string SortingName => FromName; + + public ThreadMailItemViewModel(string threadId) { - MailItem = new ThreadMailItem(); + _threadId = threadId; + } - // Local copies - foreach (var item in threadMailItem.ThreadItems) + internal void SuspendChildPropertyNotifications() => _suspendChildPropertyNotificationsCount++; + + internal void ResumeChildPropertyNotifications() + { + if (_suspendChildPropertyNotificationsCount > 0) { - AddMailItemViewModel(item); + _suspendChildPropertyNotificationsCount--; } } - public ThreadMailItem GetThreadMailItem() => MailItem; - - public IEnumerable GetMailCopies() - => ThreadItems.OfType().Select(a => a.MailCopy); - - public void AddMailItemViewModel(IMailItem mailItem) + private void RefreshLatestMailCache() { - if (mailItem == null) return; + _cachedLatestMailViewModel = ThreadEmails + .OrderByDescending(static item => item.MailCopy.CreationDate) + .FirstOrDefault(); + } - if (mailItem is MailCopy mailCopy) - MailItem.AddThreadItem(new MailItemViewModel(mailCopy)); - else if (mailItem is MailItemViewModel mailItemViewModel) - MailItem.AddThreadItem(mailItemViewModel); + /// + /// Adds an email to this thread + /// + public void AddEmail(MailItemViewModel email) + { + if (email.MailCopy.ThreadId != _threadId) + throw new ArgumentException($"Email ThreadId '{email.MailCopy.ThreadId}' does not match expander ThreadId '{_threadId}'"); + + // Insert email in sorted order by CreationDate (newest first, oldest last) + var insertIndex = 0; + for (int i = 0; i < ThreadEmails.Count; i++) + { + if (ThreadEmails[i].MailCopy.CreationDate < email.MailCopy.CreationDate) + { + insertIndex = i; + break; + } + insertIndex = i + 1; + } + + ThreadEmails.Insert(insertIndex, email); + email.PropertyChanged += ThreadEmailPropertyChanged; + _uniqueIdSet.Add(email.MailCopy.UniqueId); + RefreshLatestMailCache(); + OnPropertyChanged(nameof(EmailCount)); + NotifyMailItemUpdated(email, MailCopyChangeFlags.All); + } + + /// + /// Removes an email from this thread + /// + public void RemoveEmail(MailItemViewModel email) + { + if (ThreadEmails.Remove(email)) + { + email.PropertyChanged -= ThreadEmailPropertyChanged; + _uniqueIdSet.Remove(email.MailCopy.UniqueId); + RefreshLatestMailCache(); + OnPropertyChanged(nameof(EmailCount)); + NotifyMailItemUpdated(email, MailCopyChangeFlags.All); + } + } + + public void UnregisterThreadEmailPropertyChangedHandlers() + { + foreach (var email in ThreadEmails) + { + email.PropertyChanged -= ThreadEmailPropertyChanged; + } + } + + + private void ThreadEmailPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (_suspendChildPropertyNotificationsCount > 0) + return; + + if (sender is not MailItemViewModel updatedMailItem) + return; + + if (e.PropertyName == nameof(MailItemViewModel.IsSelected) || + e.PropertyName == nameof(MailItemViewModel.IsDisplayedInThread) || + e.PropertyName == nameof(MailItemViewModel.IsBusy)) + { + return; + } + + if (e.PropertyName == nameof(MailItemViewModel.ThumbnailUpdatedEvent)) + { + if (ReferenceEquals(updatedMailItem, latestMailViewModel)) + { + OnPropertyChanged(nameof(ThumbnailUpdatedEvent)); + } + + return; + } + + var changedFlags = string.IsNullOrEmpty(e.PropertyName) + ? MailCopyChangeFlags.All + : MailItemViewModel.GetChangeFlagsForProperty(e.PropertyName); + + if (changedFlags == MailCopyChangeFlags.None) + { + NotifyMailItemUpdated(updatedMailItem, MailCopyChangeFlags.All); + return; + } + + NotifyMailItemUpdated(updatedMailItem, changedFlags); + } + + /// + /// Notifies that a mail item within this thread has been updated. + /// + /// The mail item that was updated (can be null to refresh all). + /// Set of changed child fields. + public void NotifyMailItemUpdated(MailItemViewModel updatedMailItem, MailCopyChangeFlags changedFlags = MailCopyChangeFlags.All) + { + if (changedFlags == MailCopyChangeFlags.None) + return; + + var previousLatest = latestMailViewModel; + + if (changedFlags == MailCopyChangeFlags.All || + (changedFlags & MailCopyChangeFlags.CreationDate) != 0 || + previousLatest == null || + !ThreadEmails.Contains(previousLatest)) + { + RefreshLatestMailCache(); + } + + var currentLatest = latestMailViewModel; + var latestChanged = !ReferenceEquals(previousLatest, currentLatest); + + var updatesDisplayedLatest = changedFlags == MailCopyChangeFlags.All || + updatedMailItem == null || + latestChanged || + ReferenceEquals(updatedMailItem, previousLatest) || + ReferenceEquals(updatedMailItem, currentLatest); + + var changedProperties = new List(10); + + void Queue(string propertyName) + { + if (!changedProperties.Contains(propertyName)) + { + changedProperties.Add(propertyName); + } + } + + if (updatesDisplayedLatest) + { + if (changedFlags == MailCopyChangeFlags.All || latestChanged) + { + Queue(nameof(Subject)); + Queue(nameof(FromName)); + Queue(nameof(CreationDate)); + Queue(nameof(FromAddress)); + Queue(nameof(PreviewText)); + Queue(nameof(IsFocused)); + Queue(nameof(DraftId)); + Queue(nameof(Id)); + Queue(nameof(Importance)); + Queue(nameof(ThreadId)); + Queue(nameof(MessageId)); + Queue(nameof(References)); + Queue(nameof(InReplyTo)); + Queue(nameof(FileId)); + Queue(nameof(FolderId)); + Queue(nameof(UniqueId)); + Queue(nameof(ContactPictureFileId)); + Queue(nameof(SenderContact)); + Queue(nameof(ThumbnailUpdatedEvent)); + Queue(nameof(SortingDate)); + Queue(nameof(SortingName)); + } + else + { + if ((changedFlags & MailCopyChangeFlags.Subject) != 0) + Queue(nameof(Subject)); + + if ((changedFlags & MailCopyChangeFlags.FromName) != 0) + { + Queue(nameof(FromName)); + Queue(nameof(SortingName)); + } + + if ((changedFlags & MailCopyChangeFlags.CreationDate) != 0) + { + Queue(nameof(CreationDate)); + Queue(nameof(SortingDate)); + } + + if ((changedFlags & MailCopyChangeFlags.FromAddress) != 0) + Queue(nameof(FromAddress)); + + if ((changedFlags & MailCopyChangeFlags.PreviewText) != 0) + Queue(nameof(PreviewText)); + + if ((changedFlags & MailCopyChangeFlags.IsFocused) != 0) + Queue(nameof(IsFocused)); + + if ((changedFlags & MailCopyChangeFlags.DraftId) != 0) + Queue(nameof(DraftId)); + + if ((changedFlags & MailCopyChangeFlags.Id) != 0) + Queue(nameof(Id)); + + if ((changedFlags & MailCopyChangeFlags.Importance) != 0) + Queue(nameof(Importance)); + + if ((changedFlags & MailCopyChangeFlags.ThreadId) != 0) + Queue(nameof(ThreadId)); + + if ((changedFlags & MailCopyChangeFlags.MessageId) != 0) + Queue(nameof(MessageId)); + + if ((changedFlags & MailCopyChangeFlags.References) != 0) + Queue(nameof(References)); + + if ((changedFlags & MailCopyChangeFlags.InReplyTo) != 0) + Queue(nameof(InReplyTo)); + + if ((changedFlags & MailCopyChangeFlags.FileId) != 0) + Queue(nameof(FileId)); + + if ((changedFlags & MailCopyChangeFlags.FolderId) != 0) + Queue(nameof(FolderId)); + + if ((changedFlags & MailCopyChangeFlags.UniqueId) != 0) + Queue(nameof(UniqueId)); + + if ((changedFlags & MailCopyChangeFlags.SenderContact) != 0) + { + Queue(nameof(ContactPictureFileId)); + Queue(nameof(SenderContact)); + } + } + } + + if ((changedFlags & MailCopyChangeFlags.HasAttachments) != 0 || changedFlags == MailCopyChangeFlags.All) + Queue(nameof(HasAttachments)); + + if ((changedFlags & MailCopyChangeFlags.ItemType) != 0 || changedFlags == MailCopyChangeFlags.All) + Queue(nameof(IsCalendarEvent)); + + if ((changedFlags & MailCopyChangeFlags.IsFlagged) != 0 || changedFlags == MailCopyChangeFlags.All) + Queue(nameof(IsFlagged)); + + if ((changedFlags & MailCopyChangeFlags.IsRead) != 0 || changedFlags == MailCopyChangeFlags.All) + Queue(nameof(IsRead)); + + if ((changedFlags & MailCopyChangeFlags.IsDraft) != 0 || changedFlags == MailCopyChangeFlags.All) + Queue(nameof(IsDraft)); + + foreach (var changedProperty in changedProperties) + { + OnPropertyChanged(changedProperty); + } + } + + /// + /// Checks if this thread contains an email with the specified unique ID + /// + public bool HasUniqueId(Guid uniqueId) => _uniqueIdSet.Contains(uniqueId); + + public IEnumerable GetContainingIds() => ThreadEmails.Select(a => a.MailCopy.UniqueId); + + public IEnumerable GetSelectedMailItems() + { + if (IsSelected) + { + // If the thread itself is selected, return all emails in the thread + return ThreadEmails; + } else - Debugger.Break(); + { + // Otherwise, return only individually selected emails within the thread + return ThreadEmails.Where(e => e.IsSelected); + } } - - public bool HasUniqueId(Guid uniqueMailId) - => ThreadItems.Any(a => a.UniqueId == uniqueMailId); - - public IMailItem GetItemById(Guid uniqueMailId) - => ThreadItems.FirstOrDefault(a => a.UniqueId == uniqueMailId); - - public void RemoveCopyItem(IMailItem item) - { - MailCopy copyToRemove = null; - - if (item is MailItemViewModel mailItemViewModel) - copyToRemove = mailItemViewModel.MailCopy; - else if (item is MailCopy copyItem) - copyToRemove = copyItem; - - var existedItem = ThreadItems.FirstOrDefault(a => a.Id == copyToRemove.Id); - - if (existedItem == null) return; - - ThreadItems.Remove(existedItem); - - NotifyPropertyChanges(); - } - - public void NotifyPropertyChanges() - { - // TODO - // Stupid temporary fix for not updating UI. - // This view model must be reworked with ThreadMailItem together. - - var current = MailItem; - - MailItem = null; - MailItem = current; - } - - public IMailItem LatestMailItem => ((IMailItemThread)MailItem).LatestMailItem; - public IMailItem FirstMailItem => ((IMailItemThread)MailItem).FirstMailItem; - - public string Id => ((IMailItem)MailItem).Id; - public string Subject => ((IMailItem)MailItem).Subject; - public string ThreadId => ((IMailItem)MailItem).ThreadId; - public string MessageId => ((IMailItem)MailItem).MessageId; - public string References => ((IMailItem)MailItem).References; - public string PreviewText => ((IMailItem)MailItem).PreviewText; - public string FromName => ((IMailItem)MailItem).FromName; - public DateTime CreationDate => ((IMailItem)MailItem).CreationDate; - public string FromAddress => ((IMailItem)MailItem).FromAddress; - public bool HasAttachments => ((IMailItem)MailItem).HasAttachments; - public bool IsFlagged => ((IMailItem)MailItem).IsFlagged; - public bool IsFocused => ((IMailItem)MailItem).IsFocused; - public bool IsRead => ((IMailItem)MailItem).IsRead; - public bool IsDraft => ((IMailItem)MailItem).IsDraft; - public string DraftId => string.Empty; - public string InReplyTo => ((IMailItem)MailItem).InReplyTo; - - public MailItemFolder AssignedFolder => ((IMailItem)MailItem).AssignedFolder; - - public MailAccount AssignedAccount => ((IMailItem)MailItem).AssignedAccount; - - public Guid UniqueId => ((IMailItem)MailItem).UniqueId; - - public Guid FileId => ((IMailItem)MailItem).FileId; - - public int CompareTo(DateTime other) => CreationDate.CompareTo(other); - public int CompareTo(string other) => FromName.CompareTo(other); - - // Get single mail item view model out of the only item in thread items. - public MailItemViewModel GetSingleItemViewModel() => ThreadItems.First() as MailItemViewModel; - - public IEnumerable GetContainingIds() => ((IMailItemThread)MailItem).GetContainingIds(); } + diff --git a/Wino.Mail.ViewModels/Data/WelcomeWizardContext.cs b/Wino.Mail.ViewModels/Data/WelcomeWizardContext.cs new file mode 100644 index 00000000..fe38bd23 --- /dev/null +++ b/Wino.Mail.ViewModels/Data/WelcomeWizardContext.cs @@ -0,0 +1,79 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Accounts; + +namespace Wino.Mail.ViewModels.Data; + +public partial class WelcomeWizardContext : ObservableObject +{ + // Step 2 — Provider selection + [ObservableProperty] + public partial IProviderDetail SelectedProvider { get; set; } + + [ObservableProperty] + public partial string AccountName { get; set; } + + [ObservableProperty] + public partial string AccountColorHex { get; set; } + + // Special IMAP fields (iCloud/Yahoo) + [ObservableProperty] + public partial string DisplayName { get; set; } + + [ObservableProperty] + public partial string EmailAddress { get; set; } + + [ObservableProperty] + public partial string AppSpecificPassword { get; set; } + + [ObservableProperty] + public partial ImapCalendarSupportMode CalendarSupportMode { get; set; } = ImapCalendarSupportMode.Disabled; + + // Generic IMAP — populated by ImapCalDavSettingsPage + public ImapCalDavSetupResult ImapCalDavSetupResult { get; set; } + + // Computed helpers + public bool IsOAuthProvider => SelectedProvider?.Type is MailProviderType.Outlook or MailProviderType.Gmail; + + public bool IsSpecialImapProvider => + SelectedProvider?.SpecialImapProvider is SpecialImapProvider.iCloud or SpecialImapProvider.Yahoo; + + public bool IsGenericImap => + SelectedProvider?.Type == MailProviderType.IMAP4 + && SelectedProvider?.SpecialImapProvider == SpecialImapProvider.None; + + public SpecialImapProviderDetails BuildSpecialImapProviderDetails() + { + if (!IsSpecialImapProvider) return null; + + return new SpecialImapProviderDetails( + EmailAddress, + AppSpecificPassword, + DisplayName, + SelectedProvider.SpecialImapProvider, + CalendarSupportMode); + } + + public AccountCreationDialogResult BuildAccountCreationDialogResult() + { + return new AccountCreationDialogResult( + SelectedProvider.Type, + AccountName, + BuildSpecialImapProviderDetails(), + AccountColorHex); + } + + public void Reset() + { + SelectedProvider = null; + AccountName = null; + AccountColorHex = null; + DisplayName = null; + EmailAddress = null; + AppSpecificPassword = null; + CalendarSupportMode = ImapCalendarSupportMode.Disabled; + ImapCalDavSetupResult = null; + } +} diff --git a/Wino.Mail.ViewModels/EditAccountDetailsPageViewModel.cs b/Wino.Mail.ViewModels/EditAccountDetailsPageViewModel.cs deleted file mode 100644 index 9878d71e..00000000 --- a/Wino.Mail.ViewModels/EditAccountDetailsPageViewModel.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System; -using System.Collections.Generic; -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.Interfaces; -using Wino.Core.Domain.Models.Accounts; -using Wino.Core.Domain.Models.Navigation; -using Wino.Core.ViewModels.Data; -using Wino.Messaging.Client.Navigation; - -namespace Wino.Mail.ViewModels; - -public partial class EditAccountDetailsPageViewModel : MailBaseViewModel -{ - private readonly IAccountService _accountService; - private readonly IThemeService _themeService; - private readonly IImapTestService _imapTestService; - private readonly IMailDialogService _mailDialogService; - - [ObservableProperty] - public partial MailAccount Account { get; set; } - - [ObservableProperty] - public partial string AccountName { get; set; } - - [ObservableProperty] - public partial string SenderName { get; set; } - - [ObservableProperty] - public partial AppColorViewModel SelectedColor { get; set; } - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(IsImapServer))] - public partial CustomServerInformation ServerInformation { get; set; } - - [ObservableProperty] - public partial List AvailableColors { get; set; } - - - [ObservableProperty] - public partial int SelectedIncomingServerConnectionSecurityIndex { get; set; } - - [ObservableProperty] - public partial int SelectedIncomingServerAuthenticationMethodIndex { get; set; } - - [ObservableProperty] - public partial int SelectedOutgoingServerConnectionSecurityIndex { get; set; } - - [ObservableProperty] - public partial int SelectedOutgoingServerAuthenticationMethodIndex { get; set; } - - public List AvailableAuthenticationMethods { get; } = - [ - new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.Auto, Translator.ImapAuthenticationMethod_Auto), - new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.None, Translator.ImapAuthenticationMethod_None), - new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.NormalPassword, Translator.ImapAuthenticationMethod_Plain), - new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.EncryptedPassword, Translator.ImapAuthenticationMethod_EncryptedPassword), - new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.Ntlm, Translator.ImapAuthenticationMethod_Ntlm), - new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.CramMd5, Translator.ImapAuthenticationMethod_CramMD5), - new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.DigestMd5, Translator.ImapAuthenticationMethod_DigestMD5) - ]; - - public List AvailableConnectionSecurities { get; set; } = - [ - new ImapConnectionSecurityModel(Core.Domain.Enums.ImapConnectionSecurity.Auto, Translator.ImapConnectionSecurity_Auto), - new ImapConnectionSecurityModel(Core.Domain.Enums.ImapConnectionSecurity.SslTls, Translator.ImapConnectionSecurity_SslTls), - new ImapConnectionSecurityModel(Core.Domain.Enums.ImapConnectionSecurity.StartTls, Translator.ImapConnectionSecurity_StartTls), - new ImapConnectionSecurityModel(Core.Domain.Enums.ImapConnectionSecurity.None, Translator.ImapConnectionSecurity_None) - ]; - - public bool IsImapServer => ServerInformation != null; - - public EditAccountDetailsPageViewModel(IAccountService accountService, - IThemeService themeService, - IImapTestService imapTestService, - IMailDialogService mailDialogService) - { - _accountService = accountService; - _themeService = themeService; - _imapTestService = imapTestService; - _mailDialogService = mailDialogService; - - var colorHexList = _themeService.GetAvailableAccountColors(); - - AvailableColors = colorHexList.Select(a => new AppColorViewModel(a)).ToList(); - } - - [RelayCommand] - private async Task SaveChangesAsync() - { - await UpdateAccountAsync(); - - Messenger.Send(new BackBreadcrumNavigationRequested()); - } - - [RelayCommand] - private async Task ValidateImapSettingsAsync() - { - try - { - await _imapTestService.TestImapConnectionAsync(ServerInformation, true); - _mailDialogService.InfoBarMessage(Translator.IMAPSetupDialog_ValidationSuccess_Title, Translator.IMAPSetupDialog_ValidationSuccess_Message, Core.Domain.Enums.InfoBarMessageType.Success); - } - catch (Exception ex) - { - _mailDialogService.InfoBarMessage(Translator.IMAPSetupDialog_ValidationFailed_Title, ex.Message, Core.Domain.Enums.InfoBarMessageType.Error); ; - } - } - - [RelayCommand] - private async Task UpdateCustomServerInformationAsync() - { - if (ServerInformation != null) - { - ServerInformation.IncomingAuthenticationMethod = AvailableAuthenticationMethods[SelectedIncomingServerAuthenticationMethodIndex].ImapAuthenticationMethod; - ServerInformation.IncomingServerSocketOption = AvailableConnectionSecurities[SelectedIncomingServerConnectionSecurityIndex].ImapConnectionSecurity; - - ServerInformation.OutgoingAuthenticationMethod = AvailableAuthenticationMethods[SelectedOutgoingServerAuthenticationMethodIndex].ImapAuthenticationMethod; - ServerInformation.OutgoingServerSocketOption = AvailableConnectionSecurities[SelectedOutgoingServerConnectionSecurityIndex].ImapConnectionSecurity; - - Account.ServerInformation = ServerInformation; - } - - await _accountService.UpdateAccountCustomServerInformationAsync(Account.ServerInformation); - - _mailDialogService.InfoBarMessage(Translator.IMAPSetupDialog_SaveImapSuccess_Title, Translator.IMAPSetupDialog_SaveImapSuccess_Message, Core.Domain.Enums.InfoBarMessageType.Success); - } - - private Task UpdateAccountAsync() - { - Account.Name = AccountName; - Account.SenderName = SenderName; - Account.AccountColorHex = SelectedColor == null ? string.Empty : SelectedColor.Hex; - - return _accountService.UpdateAccountAsync(Account); - } - - [RelayCommand] - private void ResetColor() - => SelectedColor = null; - - partial void OnSelectedColorChanged(AppColorViewModel oldValue, AppColorViewModel newValue) - { - _ = UpdateAccountAsync(); - } - - public override void OnNavigatedTo(NavigationMode mode, object parameters) - { - base.OnNavigatedTo(mode, parameters); - - if (parameters is MailAccount account) - { - Account = account; - AccountName = account.Name; - SenderName = account.SenderName; - ServerInformation = Account.ServerInformation; - - if (!string.IsNullOrEmpty(account.AccountColorHex)) - { - SelectedColor = AvailableColors.FirstOrDefault(a => a.Hex == account.AccountColorHex); - } - - if (ServerInformation != null) - { - SelectedIncomingServerAuthenticationMethodIndex = AvailableAuthenticationMethods.FindIndex(a => a.ImapAuthenticationMethod == ServerInformation.IncomingAuthenticationMethod); - SelectedIncomingServerConnectionSecurityIndex = AvailableConnectionSecurities.FindIndex(a => a.ImapConnectionSecurity == ServerInformation.IncomingServerSocketOption); - - SelectedOutgoingServerAuthenticationMethodIndex = AvailableAuthenticationMethods.FindIndex(a => a.ImapAuthenticationMethod == ServerInformation.OutgoingAuthenticationMethod); - SelectedOutgoingServerConnectionSecurityIndex = AvailableConnectionSecurities.FindIndex(a => a.ImapConnectionSecurity == ServerInformation.OutgoingServerSocketOption); - } - } - } -} diff --git a/Wino.Mail.ViewModels/EmailTemplatesPageViewModel.cs b/Wino.Mail.ViewModels/EmailTemplatesPageViewModel.cs new file mode 100644 index 00000000..e3106e67 --- /dev/null +++ b/Wino.Mail.ViewModels/EmailTemplatesPageViewModel.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.Messaging; +using Wino.Core.Domain; +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Messaging.Client.Navigation; + +namespace Wino.Mail.ViewModels; + +public partial class EmailTemplatesPageViewModel(IEmailTemplateService emailTemplateService) : MailBaseViewModel +{ + private readonly IEmailTemplateService _emailTemplateService = emailTemplateService; + + public ObservableCollection EmailTemplates { get; } = []; + + public async Task LoadAsync() + { + var templates = await _emailTemplateService.GetEmailTemplatesAsync().ConfigureAwait(false); + + await ExecuteUIThread(() => + { + EmailTemplates.Clear(); + + foreach (var template in templates) + { + EmailTemplates.Add(template); + } + }); + } + + public void CreateTemplate() + { + Messenger.Send(new BreadcrumbNavigationRequested( + Translator.SettingsEmailTemplates_CreatePageTitle, + WinoPage.CreateEmailTemplatePage)); + } + + public void OpenTemplate(EmailTemplate template) + { + if (template == null) + return; + + var title = string.IsNullOrWhiteSpace(template.Name) + ? Translator.SettingsEmailTemplates_EditPageTitle + : template.Name; + + Messenger.Send(new BreadcrumbNavigationRequested( + title, + WinoPage.CreateEmailTemplatePage, + template.Id)); + } +} diff --git a/Wino.Mail.ViewModels/ImapCalDavSettingsPageViewModel.cs b/Wino.Mail.ViewModels/ImapCalDavSettingsPageViewModel.cs new file mode 100644 index 00000000..b6b5d9d5 --- /dev/null +++ b/Wino.Mail.ViewModels/ImapCalDavSettingsPageViewModel.cs @@ -0,0 +1,1073 @@ + +using System; +using System.Collections.Generic; +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.Models.Accounts; +using Wino.Core.Domain.Models.AutoDiscovery; +using Wino.Core.Domain.Models.Calendar; +using Wino.Core.Domain.Models.Navigation; +using Wino.Core.Domain.Models.Synchronization; +using Wino.Core.Services; +using Wino.Mail.ViewModels.Data; +using Wino.Messaging.Client.Calendar; +using Wino.Messaging.Client.Navigation; +using Wino.Messaging.Server; + +namespace Wino.Mail.ViewModels; + +public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel +{ + private readonly IAutoDiscoveryService _autoDiscoveryService; + private readonly ICalDavClient _calDavClient; + private readonly IAccountService _accountService; + private readonly IMailDialogService _mailDialogService; + private readonly ISpecialImapProviderConfigResolver _specialImapProviderConfigResolver; + private readonly WelcomeWizardContext _wizardContext; + + private ImapCalDavSettingsPageMode _pageMode; + private Guid _editingAccountId; + private SpecialImapProvider _editingSpecialImapProvider; + private TaskCompletionSource _completionSource; + private bool _isCompletionFinalized; + private bool _localOnlyInfoShown; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsCreateMode))] + [NotifyPropertyChangedFor(nameof(IsEditMode))] + private string pageTitle = string.Empty; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasProviderHint))] + private string providerHint = string.Empty; + + [ObservableProperty] + private string displayName = string.Empty; + + [ObservableProperty] + private string emailAddress = string.Empty; + + [ObservableProperty] + private string password = string.Empty; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsCalendarModeSelectionVisible))] + [NotifyPropertyChangedFor(nameof(IsCalDavSettingsVisible))] + [NotifyPropertyChangedFor(nameof(IsLocalCalendarModeSelected))] + [NotifyPropertyChangedFor(nameof(SelectedCalendarSupportDescription))] + private bool isCalendarSupportEnabled = true; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsCalDavSettingsVisible))] + [NotifyPropertyChangedFor(nameof(IsLocalCalendarModeSelected))] + [NotifyPropertyChangedFor(nameof(SelectedCalendarSupportDescription))] + [NotifyPropertyChangedFor(nameof(SelectedCalendarSupportModeIndex))] + private ImapCalendarSupportMode selectedCalendarSupportMode = ImapCalendarSupportMode.CalDav; + + [ObservableProperty] + private string incomingServer = string.Empty; + + [ObservableProperty] + private string incomingServerPort = string.Empty; + + [ObservableProperty] + private string incomingServerUsername = string.Empty; + + [ObservableProperty] + private string incomingServerPassword = string.Empty; + + [ObservableProperty] + private string outgoingServer = string.Empty; + + [ObservableProperty] + private string outgoingServerPort = string.Empty; + + [ObservableProperty] + private string outgoingServerUsername = string.Empty; + + [ObservableProperty] + private string outgoingServerPassword = string.Empty; + + [ObservableProperty] + private string proxyServer = string.Empty; + + [ObservableProperty] + private string proxyServerPort = string.Empty; + + [ObservableProperty] + private string calDavServiceUrl = string.Empty; + + [ObservableProperty] + private string calDavUsername = string.Empty; + + [ObservableProperty] + private string calDavPassword = string.Empty; + + [ObservableProperty] + private int maxConcurrentClients = 5; + + [ObservableProperty] + private bool isImapValidationSucceeded; + + [ObservableProperty] + private bool isCalDavValidationSucceeded; + + [ObservableProperty] + private int selectedIncomingServerConnectionSecurityIndex; + + [ObservableProperty] + private int selectedIncomingServerAuthenticationMethodIndex; + + [ObservableProperty] + private int selectedOutgoingServerConnectionSecurityIndex; + + [ObservableProperty] + private int selectedOutgoingServerAuthenticationMethodIndex; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsBasicSetupSelected))] + [NotifyPropertyChangedFor(nameof(IsAdvancedSetupSelected))] + private int selectedSetupTabIndex; + + public bool IsCreateMode => _pageMode == ImapCalDavSettingsPageMode.Create; + public bool IsEditMode => !IsCreateMode; + public bool HasProviderHint => !string.IsNullOrWhiteSpace(ProviderHint); + public bool IsBasicSetupSelected => SelectedSetupTabIndex == 0; + public bool IsAdvancedSetupSelected => SelectedSetupTabIndex == 1; + public bool IsCalendarModeSelectionVisible => IsCalendarSupportEnabled; + public bool IsCalDavSettingsVisible => IsCalendarSupportEnabled && SelectedCalendarSupportMode == ImapCalendarSupportMode.CalDav; + public bool IsLocalCalendarModeSelected => IsCalendarSupportEnabled && SelectedCalendarSupportMode == ImapCalendarSupportMode.LocalOnly; + public string SubtitleText => Translator.ImapCalDavSettingsPage_Subtitle; + public string BasicSectionTitleText => Translator.ImapCalDavSettingsPage_BasicSectionTitle; + public string BasicSectionDescriptionText => Translator.ImapCalDavSettingsPage_BasicSectionDescription; + public string DisplayNameHeaderText => Translator.IMAPSetupDialog_DisplayName; + public string DisplayNamePlaceholderText => Translator.IMAPSetupDialog_DisplayNamePlaceholder; + public string EmailAddressHeaderText => Translator.IMAPSetupDialog_MailAddress; + public string EmailAddressPlaceholderText => Translator.IMAPSetupDialog_MailAddressPlaceholder; + public string PasswordHeaderText => Translator.IMAPSetupDialog_Password; + public string EnableCalendarSupportText => Translator.ImapCalDavSettingsPage_EnableCalendarSupport; + public string AutoDiscoverButtonText => Translator.ImapCalDavSettingsPage_AutoDiscoverButton; + public string BasicTabText => Translator.ImapCalDavSettingsPage_BasicTab; + public string AdvancedTabText => Translator.ImapCalDavSettingsPage_AdvancedTab; + public string AdvancedSectionTitleText => Translator.ImapCalDavSettingsPage_AdvancedSectionTitle; + public string AdvancedSectionDescriptionText => Translator.ImapCalDavSettingsPage_AdvancedSectionDescription; + public string IncomingSectionTitleText => Translator.IMAPSetupDialog_IMAPSettings; + public string IncomingServerHeaderText => Translator.IMAPSetupDialog_IncomingMailServer; + public string PortHeaderText => Translator.IMAPSetupDialog_IncomingMailServerPort; + public string IncomingUsernameHeaderText => Translator.IMAPSetupDialog_Username; + public string IncomingPasswordHeaderText => Translator.IMAPSetupDialog_Password; + public string OutgoingSectionTitleText => Translator.IMAPSetupDialog_SMTPSettings; + public string OutgoingServerHeaderText => Translator.IMAPSetupDialog_OutgoingMailServer; + public string OutgoingUsernameHeaderText => Translator.IMAPSetupDialog_OutgoingMailServerUsername; + public string OutgoingPasswordHeaderText => Translator.IMAPSetupDialog_OutgoingMailServerPassword; + public string ConnectionSecurityHeaderText => Translator.ImapCalDavSettingsPage_ConnectionSecurityHeader; + public string AuthenticationMethodHeaderText => Translator.ImapCalDavSettingsPage_AuthenticationMethodHeader; + public string CalendarSectionTitleText => Translator.ImapCalDavSettingsPage_CalendarSectionTitle; + public string CalendarSectionDescriptionText => Translator.ImapCalDavSettingsPage_CalendarSectionDescription; + public string CalendarModeHeaderText => Translator.ImapCalDavSettingsPage_CalendarModeHeader; + public string LocalCalendarLearnMoreText => Translator.ImapCalDavSettingsPage_LocalCalendarLearnMore; + public string CalDavServiceUrlHeaderText => Translator.ImapCalDavSettingsPage_CalDavServiceUrl; + public string CalDavUsernameHeaderText => Translator.ImapCalDavSettingsPage_CalDavUsername; + public string CalDavPasswordHeaderText => Translator.ImapCalDavSettingsPage_CalDavPassword; + public string TestImapButtonText => Translator.ImapCalDavSettingsPage_TestImapButton; + public string TestCalDavButtonText => Translator.ImapCalDavSettingsPage_TestCalDavButton; + public string SaveButtonText => Translator.Buttons_Save; + public string CancelButtonText => Translator.Buttons_Cancel; + + public string SelectedCalendarSupportDescription => SelectedCalendarSupportMode switch + { + ImapCalendarSupportMode.CalDav => Translator.ImapCalDavSettingsPage_CalendarModeCalDavDescription, + ImapCalendarSupportMode.LocalOnly => Translator.ImapCalDavSettingsPage_CalendarModeLocalOnlyDescription, + _ => Translator.ImapCalDavSettingsPage_CalendarModeDisabledDescription + }; + + public List AvailableAuthenticationMethods { get; } = + [ + new ImapAuthenticationMethodModel(ImapAuthenticationMethod.Auto, Translator.ImapAuthenticationMethod_Auto), + new ImapAuthenticationMethodModel(ImapAuthenticationMethod.None, Translator.ImapAuthenticationMethod_None), + new ImapAuthenticationMethodModel(ImapAuthenticationMethod.NormalPassword, Translator.ImapAuthenticationMethod_Plain), + new ImapAuthenticationMethodModel(ImapAuthenticationMethod.EncryptedPassword, Translator.ImapAuthenticationMethod_EncryptedPassword), + new ImapAuthenticationMethodModel(ImapAuthenticationMethod.Ntlm, Translator.ImapAuthenticationMethod_Ntlm), + new ImapAuthenticationMethodModel(ImapAuthenticationMethod.CramMd5, Translator.ImapAuthenticationMethod_CramMD5), + new ImapAuthenticationMethodModel(ImapAuthenticationMethod.DigestMd5, Translator.ImapAuthenticationMethod_DigestMD5) + ]; + + public List AvailableConnectionSecurities { get; } = + [ + new ImapConnectionSecurityModel(ImapConnectionSecurity.Auto, Translator.ImapConnectionSecurity_Auto), + new ImapConnectionSecurityModel(ImapConnectionSecurity.SslTls, Translator.ImapConnectionSecurity_SslTls), + new ImapConnectionSecurityModel(ImapConnectionSecurity.StartTls, Translator.ImapConnectionSecurity_StartTls), + new ImapConnectionSecurityModel(ImapConnectionSecurity.None, Translator.ImapConnectionSecurity_None) + ]; + + public List AvailableConnectionSecurityDisplayNames { get; } = + [ + Translator.ImapConnectionSecurity_Auto, + Translator.ImapConnectionSecurity_SslTls, + Translator.ImapConnectionSecurity_StartTls, + Translator.ImapConnectionSecurity_None + ]; + + public List AvailableCalendarSupportModes { get; } = + [ + new ImapCalendarSupportModeOption(ImapCalendarSupportMode.CalDav, Translator.ImapCalDavSettingsPage_CalendarModeCalDav), + new ImapCalendarSupportModeOption(ImapCalendarSupportMode.LocalOnly, Translator.ImapCalDavSettingsPage_CalendarModeLocalOnly), + new ImapCalendarSupportModeOption(ImapCalendarSupportMode.Disabled, Translator.ImapCalDavSettingsPage_CalendarModeDisabled) + ]; + + public List AvailableAuthenticationMethodDisplayNames { get; } = + [ + Translator.ImapAuthenticationMethod_Auto, + Translator.ImapAuthenticationMethod_None, + Translator.ImapAuthenticationMethod_Plain, + Translator.ImapAuthenticationMethod_EncryptedPassword, + Translator.ImapAuthenticationMethod_Ntlm, + Translator.ImapAuthenticationMethod_CramMD5, + Translator.ImapAuthenticationMethod_DigestMD5 + ]; + + public List AvailableCalendarSupportModeTitles { get; } = + [ + Translator.ImapCalDavSettingsPage_CalendarModeCalDav, + Translator.ImapCalDavSettingsPage_CalendarModeLocalOnly, + Translator.ImapCalDavSettingsPage_CalendarModeDisabled + ]; + + public int SelectedCalendarSupportModeIndex + { + get + { + var index = AvailableCalendarSupportModes.FindIndex(a => a.Mode == SelectedCalendarSupportMode); + return index < 0 ? 0 : index; + } + set + { + if (value < 0 || value >= AvailableCalendarSupportModes.Count) + return; + + var selectedMode = AvailableCalendarSupportModes[value].Mode; + if (selectedMode != SelectedCalendarSupportMode) + { + SelectedCalendarSupportMode = selectedMode; + } + } + } + + public ImapCalDavSettingsPageViewModel(IAutoDiscoveryService autoDiscoveryService, + ICalDavClient calDavClient, + IAccountService accountService, + IMailDialogService mailDialogService, + ISpecialImapProviderConfigResolver specialImapProviderConfigResolver, + WelcomeWizardContext wizardContext) + { + _autoDiscoveryService = autoDiscoveryService; + _calDavClient = calDavClient; + _accountService = accountService; + _mailDialogService = mailDialogService; + _specialImapProviderConfigResolver = specialImapProviderConfigResolver; + _wizardContext = wizardContext; + } + + public override async void OnNavigatedTo(NavigationMode mode, object parameters) + { + base.OnNavigatedTo(mode, parameters); + + if (parameters is not ImapCalDavSettingsNavigationContext context) + return; + + _pageMode = context.Mode; + _editingAccountId = context.AccountId; + _completionSource = context.CompletionSource; + _isCompletionFinalized = false; + _localOnlyInfoShown = false; + SelectedSetupTabIndex = 0; + + if (_pageMode == ImapCalDavSettingsPageMode.Create || _pageMode == ImapCalDavSettingsPageMode.Wizard) + { + PageTitle = Translator.ImapCalDavSettingsPage_TitleCreate; + ApplyCreateContextDefaults(context.AccountCreationDialogResult); + } + else + { + PageTitle = Translator.ImapCalDavSettingsPage_TitleEdit; + await InitializeEditModeAsync(context.AccountId); + } + } + + public override void OnNavigatedFrom(NavigationMode mode, object parameters) + { + if (_pageMode == ImapCalDavSettingsPageMode.Create && !_isCompletionFinalized) + { + _completionSource?.TrySetResult(null); + _isCompletionFinalized = true; + } + + base.OnNavigatedFrom(mode, parameters); + } + + public bool IsWizardMode => _pageMode == ImapCalDavSettingsPageMode.Wizard; + + [RelayCommand] + private async Task AutoDiscoverSettingsAsync() + { + try + { + var minimalSettings = BuildMinimalSettingsOrThrow(); + await AutoDiscoverAndApplySettingsAsync(minimalSettings).ConfigureAwait(false); + + _mailDialogService.InfoBarMessage( + Translator.IMAPSetupDialog_ValidationSuccess_Title, + Translator.ImapCalDavSettingsPage_AutoDiscoverySuccessMessage, + InfoBarMessageType.Success); + } + catch (Exception ex) + { + _mailDialogService.InfoBarMessage( + Translator.IMAPSetupDialog_ValidationFailed_Title, + ex.Message, + InfoBarMessageType.Error); + } + } + + [RelayCommand] + private async Task TestImapConnectionAsync() + { + try + { + await EnsureImapSettingsPreparedAsync().ConfigureAwait(false); + var serverInformation = BuildServerInformation(); + + ValidateImapSettings(serverInformation); + await ValidateImapConnectivityAsync(serverInformation).ConfigureAwait(false); + + IsImapValidationSucceeded = true; + + _mailDialogService.InfoBarMessage( + Translator.IMAPSetupDialog_ValidationSuccess_Title, + Translator.ImapCalDavSettingsPage_ImapTestSuccessMessage, + InfoBarMessageType.Success); + } + catch (Exception ex) + { + IsImapValidationSucceeded = false; + + _mailDialogService.InfoBarMessage( + Translator.IMAPSetupDialog_ValidationFailed_Title, + ex.Message, + InfoBarMessageType.Error); + } + } + + [RelayCommand] + private async Task TestCalDavConnectionAsync() + { + try + { + if (!IsCalendarSupportEnabled || SelectedCalendarSupportMode != ImapCalendarSupportMode.CalDav) + throw new InvalidOperationException(Translator.ImapCalDavSettingsPage_CalDavNotRequiredMessage); + + TryApplyKnownProviderSettingsIfNeeded(requireCompleteImapSettings: false, requireCompleteCalDavSettings: true); + var serverInformation = BuildServerInformation(); + ValidateCalDavSettings(serverInformation); + await ValidateCalDavConnectivityAsync(serverInformation).ConfigureAwait(false); + + IsCalDavValidationSucceeded = true; + + _mailDialogService.InfoBarMessage( + Translator.IMAPSetupDialog_ValidationSuccess_Title, + Translator.ImapCalDavSettingsPage_CalDavTestSuccessMessage, + InfoBarMessageType.Success); + } + catch (Exception ex) + { + IsCalDavValidationSucceeded = false; + + _mailDialogService.InfoBarMessage( + Translator.IMAPSetupDialog_ValidationFailed_Title, + ex.Message, + InfoBarMessageType.Error); + } + } + [RelayCommand] + private async Task SaveAsync() + { + try + { + await EnsureImapSettingsPreparedAsync().ConfigureAwait(false); + + var serverInformation = BuildServerInformation(); + + ValidateIdentitySettings(); + ValidateImapSettings(serverInformation); + ValidateCalendarModeSpecificSettings(serverInformation); + + await ValidateImapConnectivityAsync(serverInformation).ConfigureAwait(false); + IsImapValidationSucceeded = true; + + if (serverInformation.CalendarSupportMode == ImapCalendarSupportMode.CalDav) + { + await ValidateCalDavConnectivityAsync(serverInformation).ConfigureAwait(false); + IsCalDavValidationSucceeded = true; + } + else + { + IsCalDavValidationSucceeded = false; + } + + if (_pageMode == ImapCalDavSettingsPageMode.Wizard) + { + CompleteWizardFlow(serverInformation); + return; + } + + if (_pageMode == ImapCalDavSettingsPageMode.Create) + { + CompleteCreateFlow(serverInformation); + return; + } + + await SaveEditFlowAsync(serverInformation).ConfigureAwait(false); + } + catch (Exception ex) + { + _mailDialogService.InfoBarMessage( + Translator.IMAPSetupDialog_ValidationFailed_Title, + ex.Message, + InfoBarMessageType.Error); + } + } + + [RelayCommand] + private void Cancel() + { + if (_pageMode == ImapCalDavSettingsPageMode.Create && !_isCompletionFinalized) + { + _completionSource?.TrySetResult(null); + _isCompletionFinalized = true; + } + + Messenger.Send(new BackBreadcrumNavigationRequested()); + } + + private void CompleteWizardFlow(CustomServerInformation serverInformation) + { + serverInformation.Id = Guid.NewGuid(); + serverInformation.AccountId = Guid.Empty; + + _wizardContext.ImapCalDavSetupResult = new ImapCalDavSetupResult + { + DisplayName = DisplayName.Trim(), + EmailAddress = EmailAddress.Trim(), + IsCalendarAccessGranted = serverInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled, + ServerInformation = serverInformation + }; + + Messenger.Send(new BreadcrumbNavigationRequested(Translator.WelcomeWizard_Step3Title, WinoPage.AccountSetupProgressPage)); + } + + [RelayCommand] + private Task ShowLocalCalendarExplanationAsync() + => _mailDialogService.ShowMessageAsync( + Translator.ImapCalDavSettingsPage_LocalCalendarDialogMessage, + Translator.ImapCalDavSettingsPage_LocalCalendarDialogTitle, + WinoCustomMessageDialogIcon.Information); + + partial void OnIsCalendarSupportEnabledChanged(bool value) + { + if (!value && SelectedCalendarSupportMode != ImapCalendarSupportMode.Disabled) + { + SelectedCalendarSupportMode = ImapCalendarSupportMode.Disabled; + } + else if (value && SelectedCalendarSupportMode == ImapCalendarSupportMode.Disabled) + { + SelectedCalendarSupportMode = ImapCalendarSupportMode.CalDav; + } + } + + partial void OnSelectedCalendarSupportModeChanged(ImapCalendarSupportMode value) + { + if (value == ImapCalendarSupportMode.LocalOnly && !_localOnlyInfoShown) + { + _localOnlyInfoShown = true; + _ = ShowLocalCalendarExplanationAsync(); + } + + if (value != ImapCalendarSupportMode.CalDav) + { + IsCalDavValidationSucceeded = false; + } + } + + private async Task InitializeEditModeAsync(Guid accountId) + { + var account = await _accountService.GetAccountAsync(accountId); + if (account == null) + throw new InvalidOperationException(Translator.Exception_NullAssignedAccount); + + _editingSpecialImapProvider = account.SpecialImapProvider; + DisplayName = account.SenderName ?? string.Empty; + EmailAddress = account.Address ?? string.Empty; + ApplyProviderHint(_editingSpecialImapProvider); + + ApplyServerInformation(account.ServerInformation); + + if (account.ServerInformation != null) + { + SelectedCalendarSupportMode = account.ServerInformation.CalendarSupportMode; + } + + if (SelectedCalendarSupportMode == ImapCalendarSupportMode.Disabled && account.IsCalendarAccessGranted) + { + SelectedCalendarSupportMode = ImapCalendarSupportMode.CalDav; + } + + IsCalendarSupportEnabled = SelectedCalendarSupportMode != ImapCalendarSupportMode.Disabled; + } + + private void ApplyCreateContextDefaults(AccountCreationDialogResult accountCreationDialogResult) + { + DisplayName = accountCreationDialogResult?.AccountName ?? string.Empty; + EmailAddress = accountCreationDialogResult?.SpecialImapProviderDetails?.Address ?? string.Empty; + Password = accountCreationDialogResult?.SpecialImapProviderDetails?.Password ?? string.Empty; + var normalizedEmail = !string.IsNullOrWhiteSpace(EmailAddress) && !EmailAddress.Contains('@') + ? $"{EmailAddress}@icloud.com" + : EmailAddress; + + if (!string.IsNullOrWhiteSpace(accountCreationDialogResult?.SpecialImapProviderDetails?.SenderName)) + DisplayName = accountCreationDialogResult.SpecialImapProviderDetails.SenderName; + + IsCalendarSupportEnabled = true; + SelectedCalendarSupportMode = ImapCalendarSupportMode.CalDav; + + var specialProvider = accountCreationDialogResult?.SpecialImapProviderDetails?.SpecialImapProvider ?? SpecialImapProvider.None; + _editingSpecialImapProvider = specialProvider; + ApplyProviderHint(specialProvider); + + switch (specialProvider) + { + case SpecialImapProvider.iCloud: + ApplySpecialProviderDefaults( + "imap.mail.me.com", + "993", + normalizedEmail, + "smtp.mail.me.com", + "587", + normalizedEmail, + Password, + "https://caldav.icloud.com/", + normalizedEmail, + Password); + break; + case SpecialImapProvider.Yahoo: + ApplySpecialProviderDefaults( + "imap.mail.yahoo.com", + "993", + EmailAddress, + "smtp.mail.yahoo.com", + "587", + EmailAddress, + Password, + "https://caldav.calendar.yahoo.com/", + EmailAddress, + Password); + break; + } + } + + private void ApplySpecialProviderDefaults(string incomingServer, + string incomingPort, + string incomingUsername, + string outgoingServer, + string outgoingPort, + string outgoingUsername, + string password, + string calDavServiceUrl, + string calDavUsername, + string calDavPassword) + { + IncomingServer = incomingServer; + IncomingServerPort = incomingPort; + IncomingServerUsername = incomingUsername; + IncomingServerPassword = password; + + OutgoingServer = outgoingServer; + OutgoingServerPort = outgoingPort; + OutgoingServerUsername = outgoingUsername; + OutgoingServerPassword = password; + CalDavServiceUrl = calDavServiceUrl; + CalDavUsername = calDavUsername; + CalDavPassword = calDavPassword; + + SelectedIncomingServerConnectionSecurityIndex = 0; + SelectedIncomingServerAuthenticationMethodIndex = 0; + SelectedOutgoingServerConnectionSecurityIndex = 0; + SelectedOutgoingServerAuthenticationMethodIndex = 0; + } + + private void ApplyServerInformation(CustomServerInformation serverInformation) + { + if (serverInformation == null) + return; + + IncomingServer = serverInformation.IncomingServer ?? string.Empty; + IncomingServerPort = serverInformation.IncomingServerPort ?? string.Empty; + IncomingServerUsername = serverInformation.IncomingServerUsername ?? string.Empty; + IncomingServerPassword = serverInformation.IncomingServerPassword ?? string.Empty; + + OutgoingServer = serverInformation.OutgoingServer ?? string.Empty; + OutgoingServerPort = serverInformation.OutgoingServerPort ?? string.Empty; + OutgoingServerUsername = serverInformation.OutgoingServerUsername ?? string.Empty; + OutgoingServerPassword = serverInformation.OutgoingServerPassword ?? string.Empty; + + ProxyServer = serverInformation.ProxyServer ?? string.Empty; + ProxyServerPort = serverInformation.ProxyServerPort ?? string.Empty; + MaxConcurrentClients = serverInformation.MaxConcurrentClients <= 0 ? 5 : serverInformation.MaxConcurrentClients; + + CalDavServiceUrl = serverInformation.CalDavServiceUrl ?? string.Empty; + CalDavUsername = serverInformation.CalDavUsername ?? string.Empty; + CalDavPassword = serverInformation.CalDavPassword ?? string.Empty; + + if (string.IsNullOrWhiteSpace(CalDavUsername)) + CalDavUsername = EmailAddress; + + if (string.IsNullOrWhiteSpace(CalDavPassword)) + CalDavPassword = IncomingServerPassword; + + SelectedIncomingServerAuthenticationMethodIndex = FindAuthenticationMethodIndex(serverInformation.IncomingAuthenticationMethod); + SelectedIncomingServerConnectionSecurityIndex = FindConnectionSecurityIndex(serverInformation.IncomingServerSocketOption); + SelectedOutgoingServerAuthenticationMethodIndex = FindAuthenticationMethodIndex(serverInformation.OutgoingAuthenticationMethod); + SelectedOutgoingServerConnectionSecurityIndex = FindConnectionSecurityIndex(serverInformation.OutgoingServerSocketOption); + } + + private async Task EnsureImapSettingsPreparedAsync() + { + if (HasCompleteImapSettings()) + return; + + if (TryApplyKnownProviderSettingsIfNeeded(requireCompleteImapSettings: true, requireCompleteCalDavSettings: false)) + return; + + var minimalSettings = BuildMinimalSettingsOrThrow(); + await AutoDiscoverAndApplySettingsAsync(minimalSettings).ConfigureAwait(false); + + if (!HasCompleteImapSettings()) + throw new InvalidOperationException(Translator.Exception_ImapAutoDiscoveryFailed); + } + + private async Task AutoDiscoverAndApplySettingsAsync(AutoDiscoveryMinimalSettings minimalSettings) + { + if (TryApplyKnownProviderSettings(alwaysApplyForKnownProvider: true)) + return; + + var discoverySettings = await _autoDiscoveryService.GetAutoDiscoverySettings(minimalSettings).ConfigureAwait(false); + + if (discoverySettings == null) + throw new InvalidOperationException(Translator.Exception_ImapAutoDiscoveryFailed); + + discoverySettings.UserMinimalSettings = minimalSettings; + + var serverInformation = discoverySettings.ToServerInformation(); + if (serverInformation == null) + throw new InvalidOperationException(Translator.Exception_ImapAutoDiscoveryFailed); + + ApplyServerInformation(serverInformation); + + if (IsCalendarSupportEnabled && SelectedCalendarSupportMode == ImapCalendarSupportMode.CalDav) + { + var discoveredCalDavUri = await _autoDiscoveryService.DiscoverCalDavServiceUriAsync(minimalSettings.Email).ConfigureAwait(false); + if (discoveredCalDavUri != null) + { + CalDavServiceUrl = discoveredCalDavUri.ToString(); + } + + if (string.IsNullOrWhiteSpace(CalDavUsername)) + CalDavUsername = minimalSettings.Email; + + if (string.IsNullOrWhiteSpace(CalDavPassword)) + CalDavPassword = minimalSettings.Password; + } + } + private async Task ValidateImapConnectivityAsync(CustomServerInformation serverInformation) + { + var connectivityResult = await SynchronizationManager.Instance + .TestImapConnectivityAsync(serverInformation, allowSSLHandshake: false) + .ConfigureAwait(false); + + if (connectivityResult.IsCertificateUIRequired) + { + var certificateMessage = + $"{Translator.IMAPSetupDialog_CertificateAllowanceRequired_Row0}\n\n" + + $"{Translator.IMAPSetupDialog_CertificateIssuer}: {connectivityResult.CertificateIssuer}\n" + + $"{Translator.IMAPSetupDialog_CertificateValidFrom}: {connectivityResult.CertificateValidFromDateString}\n" + + $"{Translator.IMAPSetupDialog_CertificateValidTo}: {connectivityResult.CertificateExpirationDateString}\n\n" + + $"{Translator.IMAPSetupDialog_CertificateAllowanceRequired_Row1}"; + + var allowCertificate = await _mailDialogService + .ShowConfirmationDialogAsync(certificateMessage, Translator.GeneralTitle_Warning, Translator.Buttons_Allow) + .ConfigureAwait(false); + + if (!allowCertificate) + throw new InvalidOperationException(Translator.IMAPSetupDialog_CertificateDenied); + + connectivityResult = await SynchronizationManager.Instance + .TestImapConnectivityAsync(serverInformation, allowSSLHandshake: true) + .ConfigureAwait(false); + } + + if (!connectivityResult.IsSuccess) + throw new InvalidOperationException(connectivityResult.FailedReason ?? Translator.IMAPSetupDialog_ConnectionFailedMessage); + } + + private async Task ValidateCalDavConnectivityAsync(CustomServerInformation serverInformation) + { + ValidateCalDavSettings(serverInformation); + + var uri = new Uri(serverInformation.CalDavServiceUrl, UriKind.Absolute); + var username = serverInformation.CalDavUsername; + var password = serverInformation.CalDavPassword; + + var settings = new CalDavConnectionSettings + { + ServiceUri = uri, + Username = username, + Password = password + }; + + await _calDavClient.DiscoverCalendarsAsync(settings).ConfigureAwait(false); + } + + private void CompleteCreateFlow(CustomServerInformation serverInformation) + { + if (_completionSource == null || _isCompletionFinalized) + return; + + serverInformation.Id = Guid.NewGuid(); + serverInformation.AccountId = Guid.Empty; + + _completionSource.TrySetResult(new ImapCalDavSetupResult + { + DisplayName = DisplayName.Trim(), + EmailAddress = EmailAddress.Trim(), + IsCalendarAccessGranted = serverInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled, + ServerInformation = serverInformation + }); + + _isCompletionFinalized = true; + + _mailDialogService.InfoBarMessage( + Translator.IMAPSetupDialog_ValidationSuccess_Title, + Translator.ImapCalDavSettingsPage_SaveSuccessMessage, + InfoBarMessageType.Success); + + Messenger.Send(new BackBreadcrumNavigationRequested()); + } + + private async Task SaveEditFlowAsync(CustomServerInformation serverInformation) + { + var account = await _accountService.GetAccountAsync(_editingAccountId).ConfigureAwait(false); + if (account == null) + throw new InvalidOperationException(Translator.Exception_NullAssignedAccount); + + account.SenderName = DisplayName.Trim(); + account.Address = EmailAddress.Trim(); + account.IsCalendarAccessGranted = serverInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled; + + serverInformation.Id = account.ServerInformation?.Id ?? Guid.NewGuid(); + serverInformation.AccountId = account.Id; + + account.ServerInformation = serverInformation; + account.AttentionReason = AccountAttentionReason.None; + + await _accountService.UpdateAccountCustomServerInformationAsync(serverInformation).ConfigureAwait(false); + await _accountService.UpdateAccountAsync(account).ConfigureAwait(false); + + Messenger.Send(new NewMailSynchronizationRequested(new MailSynchronizationOptions + { + AccountId = account.Id, + Type = MailSynchronizationType.FullFolders + })); + + if (account.IsCalendarAccessGranted) + { + Messenger.Send(new NewCalendarSynchronizationRequested(new CalendarSynchronizationOptions + { + AccountId = account.Id, + Type = CalendarSynchronizationType.CalendarEvents + })); + } + + _mailDialogService.InfoBarMessage( + Translator.IMAPSetupDialog_ValidationSuccess_Title, + Translator.ImapCalDavSettingsPage_SaveSuccessMessage, + InfoBarMessageType.Success); + + Messenger.Send(new BackBreadcrumNavigationRequested()); + } + + private AutoDiscoveryMinimalSettings BuildMinimalSettingsOrThrow() + { + ValidateIdentitySettings(); + + if (string.IsNullOrWhiteSpace(Password)) + throw new InvalidOperationException(Translator.IMAPAdvancedSetupDialog_ValidationPasswordRequired); + + return new AutoDiscoveryMinimalSettings + { + DisplayName = DisplayName.Trim(), + Email = EmailAddress.Trim(), + Password = Password + }; + } + + private CustomServerInformation BuildServerInformation() + { + var incomingAuth = GetAuthenticationMethodByIndex(SelectedIncomingServerAuthenticationMethodIndex); + var incomingSecurity = GetConnectionSecurityByIndex(SelectedIncomingServerConnectionSecurityIndex); + var outgoingAuth = GetAuthenticationMethodByIndex(SelectedOutgoingServerAuthenticationMethodIndex); + var outgoingSecurity = GetConnectionSecurityByIndex(SelectedOutgoingServerConnectionSecurityIndex); + + var mode = IsCalendarSupportEnabled ? SelectedCalendarSupportMode : ImapCalendarSupportMode.Disabled; + + var calDavUser = (CalDavUsername ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(calDavUser)) + calDavUser = (EmailAddress ?? string.Empty).Trim(); + + var calDavPassword = string.IsNullOrWhiteSpace(CalDavPassword) + ? IncomingServerPassword + : CalDavPassword; + + return new CustomServerInformation + { + Id = Guid.NewGuid(), + Address = (EmailAddress ?? string.Empty).Trim(), + IncomingServer = (IncomingServer ?? string.Empty).Trim(), + IncomingServerPort = (IncomingServerPort ?? string.Empty).Trim(), + IncomingServerUsername = (IncomingServerUsername ?? string.Empty).Trim(), + IncomingServerPassword = IncomingServerPassword ?? string.Empty, + IncomingServerType = CustomIncomingServerType.IMAP4, + IncomingAuthenticationMethod = incomingAuth, + IncomingServerSocketOption = incomingSecurity, + OutgoingServer = (OutgoingServer ?? string.Empty).Trim(), + OutgoingServerPort = (OutgoingServerPort ?? string.Empty).Trim(), + OutgoingServerUsername = (OutgoingServerUsername ?? string.Empty).Trim(), + OutgoingServerPassword = OutgoingServerPassword ?? string.Empty, + OutgoingAuthenticationMethod = outgoingAuth, + OutgoingServerSocketOption = outgoingSecurity, + ProxyServer = (ProxyServer ?? string.Empty).Trim(), + ProxyServerPort = (ProxyServerPort ?? string.Empty).Trim(), + MaxConcurrentClients = MaxConcurrentClients <= 0 ? 5 : MaxConcurrentClients, + CalendarSupportMode = mode, + CalDavServiceUrl = mode == ImapCalendarSupportMode.CalDav ? (CalDavServiceUrl ?? string.Empty).Trim() : string.Empty, + CalDavUsername = mode == ImapCalendarSupportMode.CalDav ? calDavUser : string.Empty, + CalDavPassword = mode == ImapCalendarSupportMode.CalDav ? calDavPassword : string.Empty + }; + } + + private void ValidateIdentitySettings() + { + if (string.IsNullOrWhiteSpace(DisplayName)) + throw new InvalidOperationException(Translator.IMAPAdvancedSetupDialog_ValidationDisplayNameRequired); + + if (string.IsNullOrWhiteSpace(EmailAddress)) + throw new InvalidOperationException(Translator.IMAPAdvancedSetupDialog_ValidationEmailRequired); + + if (!EmailValidation.EmailValidator.Validate(EmailAddress.Trim())) + throw new InvalidOperationException(Translator.IMAPAdvancedSetupDialog_ValidationEmailInvalid); + } + + private static bool IsValidPort(string portText) + => int.TryParse(portText, out var value) && value > 0 && value <= 65535; + + private void ValidateImapSettings(CustomServerInformation serverInformation) + { + ValidateIdentitySettings(); + + if (string.IsNullOrWhiteSpace(serverInformation.IncomingServer)) + throw new InvalidOperationException(Translator.IMAPAdvancedSetupDialog_ValidationIncomingServerRequired); + + if (string.IsNullOrWhiteSpace(serverInformation.IncomingServerPort)) + throw new InvalidOperationException(Translator.IMAPAdvancedSetupDialog_ValidationIncomingPortRequired); + + if (!IsValidPort(serverInformation.IncomingServerPort)) + throw new InvalidOperationException(Translator.IMAPAdvancedSetupDialog_ValidationIncomingPortInvalid); + + if (string.IsNullOrWhiteSpace(serverInformation.IncomingServerUsername)) + throw new InvalidOperationException(Translator.IMAPAdvancedSetupDialog_ValidationUsernameRequired); + + if (string.IsNullOrWhiteSpace(serverInformation.IncomingServerPassword)) + throw new InvalidOperationException(Translator.IMAPAdvancedSetupDialog_ValidationPasswordRequired); + + if (string.IsNullOrWhiteSpace(serverInformation.OutgoingServer)) + throw new InvalidOperationException(Translator.IMAPAdvancedSetupDialog_ValidationOutgoingServerRequired); + + if (string.IsNullOrWhiteSpace(serverInformation.OutgoingServerPort)) + throw new InvalidOperationException(Translator.IMAPAdvancedSetupDialog_ValidationOutgoingPortRequired); + + if (!IsValidPort(serverInformation.OutgoingServerPort)) + throw new InvalidOperationException(Translator.IMAPAdvancedSetupDialog_ValidationOutgoingPortInvalid); + + if (string.IsNullOrWhiteSpace(serverInformation.OutgoingServerUsername)) + throw new InvalidOperationException(Translator.IMAPAdvancedSetupDialog_ValidationOutgoingUsernameRequired); + + if (string.IsNullOrWhiteSpace(serverInformation.OutgoingServerPassword)) + throw new InvalidOperationException(Translator.IMAPAdvancedSetupDialog_ValidationOutgoingPasswordRequired); + } + + private void ValidateCalendarModeSpecificSettings(CustomServerInformation serverInformation) + { + if (serverInformation.CalendarSupportMode != ImapCalendarSupportMode.CalDav) + return; + + ValidateCalDavSettings(serverInformation); + } + + private void ValidateCalDavSettings(CustomServerInformation serverInformation) + { + if (string.IsNullOrWhiteSpace(serverInformation.CalDavServiceUrl)) + throw new InvalidOperationException(Translator.ImapCalDavSettingsPage_CalDavUrlRequired); + + if (!Uri.TryCreate(serverInformation.CalDavServiceUrl, UriKind.Absolute, out _)) + throw new InvalidOperationException(Translator.ImapCalDavSettingsPage_CalDavUrlInvalid); + + if (string.IsNullOrWhiteSpace(serverInformation.CalDavUsername)) + throw new InvalidOperationException(Translator.ImapCalDavSettingsPage_CalDavUsernameRequired); + + if (string.IsNullOrWhiteSpace(serverInformation.CalDavPassword)) + throw new InvalidOperationException(Translator.ImapCalDavSettingsPage_CalDavPasswordRequired); + } + + private void ApplyProviderHint(SpecialImapProvider provider) + { + ProviderHint = provider switch + { + SpecialImapProvider.iCloud => Translator.ImapCalDavSettingsPage_ICloudHint, + SpecialImapProvider.Yahoo => Translator.ImapCalDavSettingsPage_YahooHint, + _ => string.Empty + }; + } + + private bool TryApplyKnownProviderSettingsIfNeeded(bool requireCompleteImapSettings, bool requireCompleteCalDavSettings) + { + var needsImapSettings = requireCompleteImapSettings && !HasCompleteImapSettings(); + var needsCalDavSettings = requireCompleteCalDavSettings + && IsCalendarSupportEnabled + && SelectedCalendarSupportMode == ImapCalendarSupportMode.CalDav + && !HasCompleteCalDavSettings(); + + if (!needsImapSettings && !needsCalDavSettings) + return false; + + return TryApplyKnownProviderSettings(alwaysApplyForKnownProvider: false); + } + + private bool TryApplyKnownProviderSettings(bool alwaysApplyForKnownProvider) + { + if (_editingSpecialImapProvider is not (SpecialImapProvider.iCloud or SpecialImapProvider.Yahoo)) + return false; + + var effectivePassword = GetKnownProviderPasswordCandidate(); + if (string.IsNullOrWhiteSpace(EmailAddress) || string.IsNullOrWhiteSpace(effectivePassword)) + return false; + + if (!alwaysApplyForKnownProvider && HasCompleteImapSettings() && HasCompleteCalDavSettings()) + return false; + + var mode = IsCalendarSupportEnabled ? SelectedCalendarSupportMode : ImapCalendarSupportMode.Disabled; + var providerDetails = new SpecialImapProviderDetails( + EmailAddress.Trim(), + effectivePassword, + DisplayName.Trim(), + _editingSpecialImapProvider, + mode); + + var serverInformation = _specialImapProviderConfigResolver.GetServerInformation( + new MailAccount + { + Address = EmailAddress.Trim(), + SenderName = DisplayName.Trim(), + ProviderType = MailProviderType.IMAP4, + SpecialImapProvider = _editingSpecialImapProvider, + IsCalendarAccessGranted = mode != ImapCalendarSupportMode.Disabled + }, + new AccountCreationDialogResult(MailProviderType.IMAP4, DisplayName.Trim(), providerDetails, string.Empty)); + + if (serverInformation == null) + return false; + + serverInformation.ProxyServer = (ProxyServer ?? string.Empty).Trim(); + serverInformation.ProxyServerPort = (ProxyServerPort ?? string.Empty).Trim(); + serverInformation.MaxConcurrentClients = MaxConcurrentClients <= 0 ? serverInformation.MaxConcurrentClients : MaxConcurrentClients; + + ApplyServerInformation(serverInformation); + Password = effectivePassword; + return true; + } + + private string GetKnownProviderPasswordCandidate() + { + if (!string.IsNullOrWhiteSpace(Password)) + return Password; + + if (!string.IsNullOrWhiteSpace(IncomingServerPassword)) + return IncomingServerPassword; + + if (!string.IsNullOrWhiteSpace(OutgoingServerPassword)) + return OutgoingServerPassword; + + return CalDavPassword ?? string.Empty; + } + + private bool HasCompleteCalDavSettings() + => !IsCalendarSupportEnabled + || SelectedCalendarSupportMode != ImapCalendarSupportMode.CalDav + || (!string.IsNullOrWhiteSpace(CalDavServiceUrl) + && Uri.TryCreate(CalDavServiceUrl, UriKind.Absolute, out _) + && !string.IsNullOrWhiteSpace(CalDavUsername) + && !string.IsNullOrWhiteSpace(CalDavPassword)); + + private bool HasCompleteImapSettings() + => !string.IsNullOrWhiteSpace(IncomingServer) + && !string.IsNullOrWhiteSpace(IncomingServerPort) + && !string.IsNullOrWhiteSpace(IncomingServerUsername) + && !string.IsNullOrWhiteSpace(IncomingServerPassword) + && !string.IsNullOrWhiteSpace(OutgoingServer) + && !string.IsNullOrWhiteSpace(OutgoingServerPort) + && !string.IsNullOrWhiteSpace(OutgoingServerUsername) + && !string.IsNullOrWhiteSpace(OutgoingServerPassword) + && IsValidPort(IncomingServerPort) + && IsValidPort(OutgoingServerPort); + + private int FindAuthenticationMethodIndex(ImapAuthenticationMethod method) + { + var index = AvailableAuthenticationMethods.FindIndex(a => a.ImapAuthenticationMethod == method); + return index < 0 ? 0 : index; + } + + private int FindConnectionSecurityIndex(ImapConnectionSecurity security) + { + var index = AvailableConnectionSecurities.FindIndex(a => a.ImapConnectionSecurity == security); + return index < 0 ? 0 : index; + } + + private ImapAuthenticationMethod GetAuthenticationMethodByIndex(int index) + { + if (index < 0 || index >= AvailableAuthenticationMethods.Count) + return ImapAuthenticationMethod.Auto; + + return AvailableAuthenticationMethods[index].ImapAuthenticationMethod; + } + + private ImapConnectionSecurity GetConnectionSecurityByIndex(int index) + { + if (index < 0 || index >= AvailableConnectionSecurities.Count) + return ImapConnectionSecurity.Auto; + + return AvailableConnectionSecurities[index].ImapConnectionSecurity; + } +} diff --git a/Wino.Mail.ViewModels/LanguageTimePageViewModel.cs b/Wino.Mail.ViewModels/LanguageTimePageViewModel.cs deleted file mode 100644 index 51d36ab9..00000000 --- a/Wino.Mail.ViewModels/LanguageTimePageViewModel.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using CommunityToolkit.Mvvm.ComponentModel; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Navigation; -using Wino.Core.Domain.Models.Translations; - -namespace Wino.Mail.ViewModels; - -public partial class LanguageTimePageViewModel(IPreferencesService preferencesService, ITranslationService translationService) : MailBaseViewModel -{ - public IPreferencesService PreferencesService { get; } = preferencesService; - private readonly ITranslationService _translationService = translationService; - - [ObservableProperty] - private List _availableLanguages; - - [ObservableProperty] - private AppLanguageModel _selectedLanguage; - - private bool isInitialized = false; - - public override void OnNavigatedTo(NavigationMode mode, object parameters) - { - base.OnNavigatedTo(mode, parameters); - - AvailableLanguages = _translationService.GetAvailableLanguages(); - - SelectedLanguage = AvailableLanguages.FirstOrDefault(a => a.Language == PreferencesService.CurrentLanguage); - - isInitialized = true; - } - - protected override async void OnPropertyChanged(PropertyChangedEventArgs e) - { - base.OnPropertyChanged(e); - - if (!isInitialized) return; - - if (e.PropertyName == nameof(SelectedLanguage)) - { - await _translationService.InitializeLanguageAsync(SelectedLanguage.Language); - } - } -} diff --git a/Wino.Mail.ViewModels/AppShellViewModel.cs b/Wino.Mail.ViewModels/MailAppShellViewModel.cs similarity index 71% rename from Wino.Mail.ViewModels/AppShellViewModel.cs rename to Wino.Mail.ViewModels/MailAppShellViewModel.cs index 8bccc626..b4168541 100644 --- a/Wino.Mail.ViewModels/AppShellViewModel.cs +++ b/Wino.Mail.ViewModels/MailAppShellViewModel.cs @@ -1,11 +1,10 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; using MoreLinq; using MoreLinq.Extensions; @@ -17,9 +16,12 @@ using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.MenuItems; using Wino.Core.Domain.Models.Folders; +using Wino.Core.Domain.Models; using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Synchronization; +using Wino.Core.Services; +using Wino.Mail.ViewModels.Data; using Wino.Messaging.Client.Accounts; using Wino.Messaging.Client.Navigation; using Wino.Messaging.Client.Shell; @@ -28,17 +30,19 @@ using Wino.Messaging.UI; namespace Wino.Mail.ViewModels; -public partial class AppShellViewModel : MailBaseViewModel, - IRecipient, +public partial class MailAppShellViewModel : MailBaseViewModel, + IMailShellClient, IRecipient, IRecipient, IRecipient, IRecipient, IRecipient, IRecipient, - IRecipient, + IRecipient, IRecipient, - IRecipient + IRecipient, + IRecipient, + IRecipient { #region Menu Items @@ -51,7 +55,8 @@ public partial class AppShellViewModel : MailBaseViewModel, public MenuItemCollection MenuItems { get; set; } private readonly SettingsItem SettingsItem = new SettingsItem(); - private readonly ManageAccountsMenuItem ManageAccountsMenuItem = new ManageAccountsMenuItem(); + private readonly ContactsMenuItem ContactsMenuItem = new ContactsMenuItem(); + private readonly StoreUpdateMenuItem StoreUpdateMenuItem = new StoreUpdateMenuItem(); public IMenuItem CreateMailMenuItem = new NewMailMenuItem(); @@ -60,9 +65,11 @@ public partial class AppShellViewModel : MailBaseViewModel, private const string IsActivateStartupLaunchAskedKey = nameof(IsActivateStartupLaunchAskedKey); public IStatePersistanceService StatePersistenceService { get; } - public IWinoServerConnectionManager ServerConnectionManager { get; } public IPreferencesService PreferencesService { get; } public INavigationService NavigationService { get; } + public WinoApplicationMode Mode => WinoApplicationMode.Mail; + public bool HandlesNavigationSelection => true; + public IMenuItem CreatePrimaryMenuItem => CreateMailMenuItem; private readonly IFolderService _folderService; private readonly IConfigurationService _configurationService; @@ -74,20 +81,19 @@ public partial class AppShellViewModel : MailBaseViewModel, private readonly INotificationBuilder _notificationBuilder; private readonly IWinoRequestDelegator _winoRequestDelegator; private readonly IMailDialogService _dialogService; - private readonly IBackgroundTaskService _backgroundTaskService; private readonly IMimeFileService _mimeFileService; + private readonly IWebView2RuntimeValidatorService _webView2RuntimeValidatorService; + private readonly IStoreUpdateService _storeUpdateService; private readonly INativeAppService _nativeAppService; private readonly IMailService _mailService; + private bool _hasRegisteredPersistentRecipients; + private readonly SemaphoreSlim _menuRefreshSemaphore = new(1, 1); private readonly SemaphoreSlim accountInitFolderUpdateSlim = new SemaphoreSlim(1); - [ObservableProperty] - private WinoServerConnectionStatus activeConnectionStatus; - - public AppShellViewModel(IMailDialogService dialogService, + public MailAppShellViewModel(IMailDialogService dialogService, INavigationService navigationService, - IBackgroundTaskService backgroundTaskService, IMimeFileService mimeFileService, INativeAppService nativeAppService, IMailService mailService, @@ -100,21 +106,12 @@ public partial class AppShellViewModel : MailBaseViewModel, IWinoRequestDelegator winoRequestDelegator, IFolderService folderService, IStatePersistanceService statePersistanceService, - IWinoServerConnectionManager serverConnectionManager, IConfigurationService configurationService, - IStartupBehaviorService startupBehaviorService) + IStartupBehaviorService startupBehaviorService, + IWebView2RuntimeValidatorService webView2RuntimeValidatorService, + IStoreUpdateService storeUpdateService) { StatePersistenceService = statePersistanceService; - ServerConnectionManager = serverConnectionManager; - - ActiveConnectionStatus = serverConnectionManager.Status; - ServerConnectionManager.StatusChanged += async (sender, status) => - { - await ExecuteUIThread(() => - { - ActiveConnectionStatus = status; - }); - }; PreferencesService = preferencesService; _dialogService = dialogService; @@ -122,7 +119,6 @@ public partial class AppShellViewModel : MailBaseViewModel, _configurationService = configurationService; _startupBehaviorService = startupBehaviorService; - _backgroundTaskService = backgroundTaskService; _mimeFileService = mimeFileService; _nativeAppService = nativeAppService; _mailService = mailService; @@ -133,11 +129,10 @@ public partial class AppShellViewModel : MailBaseViewModel, _launchProtocolService = launchProtocolService; _notificationBuilder = notificationBuilder; _winoRequestDelegator = winoRequestDelegator; + _webView2RuntimeValidatorService = webView2RuntimeValidatorService; + _storeUpdateService = storeUpdateService; } - [RelayCommand] - private Task ReconnectServerAsync() => ServerConnectionManager.ConnectAsync(); - protected override void OnDispatcherAssigned() { base.OnDispatcherAssigned(); @@ -154,23 +149,11 @@ public partial class AppShellViewModel : MailBaseViewModel, return _contextMenuItemService.GetFolderContextMenuActions(folder); } - private async Task CreateFooterItemsAsync() + private async Task CreateFooterItemsAsync(bool showNotification = false) { await ExecuteUIThread(() => { - // TODO: Selected footer item container still remains selected after re-creation. - // To reproduce, go settings and change the language. - - foreach (var item in FooterItems) - { - item.IsExpanded = false; - item.IsSelected = false; - } - FooterItems.Clear(); - - FooterItems.Add(ManageAccountsMenuItem); - FooterItems.Add(SettingsItem); }); } @@ -234,21 +217,82 @@ public partial class AppShellViewModel : MailBaseViewModel, } } + private async void PreferencesServiceChanged(object sender, string e) + { + if (e == nameof(IPreferencesService.IsStoreUpdateNotificationsEnabled)) + { + await CreateFooterItemsAsync(); + } + } + public override async void OnNavigatedTo(NavigationMode mode, object parameters) { - base.OnNavigatedTo(mode, parameters); - await CreateFooterItemsAsync(); + if (!_hasRegisteredPersistentRecipients) + { + RegisterRecipients(); + _hasRegisteredPersistentRecipients = true; + } + + var activationContext = parameters as ShellModeActivationContext; + var shouldRunStartupFlows = (activationContext?.IsInitialActivation ?? true) && + activationContext?.SuppressStartupFlows != true; + var hasExistingAccountMenuItems = MenuItems?.OfType().Any() == true; + + PreferencesService.PreferenceChanged -= PreferencesServiceChanged; + PreferencesService.PreferenceChanged += PreferencesServiceChanged; + + await CreateFooterItemsAsync(true); + + if (!hasExistingAccountMenuItems) + { + await RecreateMenuItemsAsync(); + } - await RecreateMenuItemsAsync(); await ProcessLaunchOptionsAsync(); + await ValidateWebView2RuntimeAsync(); - if (!Debugger.IsAttached) + if (shouldRunStartupFlows && !Debugger.IsAttached) { await ForceAllAccountSynchronizationsAsync(); } - await MakeSureEnableStartupLaunchAsync(); - await ConfigureBackgroundTasksAsync(); + if (shouldRunStartupFlows) + { + await MakeSureEnableStartupLaunchAsync(); + } + } + + private async Task ValidateWebView2RuntimeAsync() + { + var isRuntimeAvailable = await _webView2RuntimeValidatorService.IsRuntimeAvailableAsync(); + + if (!isRuntimeAvailable) + { + await ExecuteUIThread(() => _notificationBuilder.CreateWebView2RuntimeMissingNotification()); + } + } + + public override void OnNavigatedFrom(NavigationMode mode, object parameters) + { + PreferencesService.PreferenceChanged -= PreferencesServiceChanged; + } + + public void PrepareForShellShutdown() + { + PreferencesService.PreferenceChanged -= PreferencesServiceChanged; + + if (_hasRegisteredPersistentRecipients) + { + UnregisterRecipients(); + _hasRegisteredPersistentRecipients = false; + } + + latestSelectedAccountMenuItem = null; + SelectedMenuItem = null; + + MenuItems?.Clear(); + MenuItems?.Add(CreateMailMenuItem); + FooterItems?.Clear(); } private async Task MakeSureEnableStartupLaunchAsync() @@ -291,22 +335,6 @@ public partial class AppShellViewModel : MailBaseViewModel, } } - private async Task ConfigureBackgroundTasksAsync() - { - try - { - // This will only unregister once. Safe to execute multiple times. - _backgroundTaskService.UnregisterAllBackgroundTask(); - - await _backgroundTaskService.RegisterBackgroundTasksAsync(); - } - catch (Exception ex) - { - Log.Error(ex, "Failed to configure background tasks."); - - _dialogService.InfoBarMessage(Translator.Info_BackgroundExecutionUnknownErrorTitle, Translator.Info_BackgroundExecutionUnknownErrorMessage, InfoBarMessageType.Error); - } - } private async Task ForceAllAccountSynchronizationsAsync() { @@ -321,7 +349,7 @@ public partial class AppShellViewModel : MailBaseViewModel, Type = MailSynchronizationType.FullFolders }; - Messenger.Send(new NewMailSynchronizationRequested(options, SynchronizationSource.Client)); + Messenger.Send(new NewMailSynchronizationRequested(options)); } } @@ -382,7 +410,7 @@ public partial class AppShellViewModel : MailBaseViewModel, { if (PreferencesService.StartupEntityId == null) { - NavigationService.Navigate(WinoPage.WelcomePage); + NavigateToWelcomeWizard(); } else { @@ -406,8 +434,8 @@ public partial class AppShellViewModel : MailBaseViewModel, } else { - // Fallback to welcome page if startup entity is not found. - NavigationService.Navigate(WinoPage.WelcomePage); + // Fallback to the welcome wizard if startup entity is not found. + NavigateToWelcomeWizard(); } } } @@ -426,7 +454,7 @@ public partial class AppShellViewModel : MailBaseViewModel, var args = new NavigateMailFolderEventArgs(baseFolderMenuItem, folderInitAwaitTask); - NavigationService.Navigate(WinoPage.MailListPage, args, NavigationReferenceFrame.ShellFrame); + NavigationService.Navigate(WinoPage.MailListPage, args, NavigationReferenceFrame.InnerShellFrame); UpdateWindowTitleForFolder(baseFolderMenuItem); }); @@ -440,6 +468,11 @@ public partial class AppShellViewModel : MailBaseViewModel, StatePersistenceService.CoreWindowTitle = $"{folder.AssignedAccountName} - {folder.FolderName}"; } + private void UpdateWindowTitle(string title) + { + StatePersistenceService.CoreWindowTitle = title; + } + private async Task NavigateSpecialFolderAsync(MailAccount account, SpecialFolderType specialFolderType, bool extendAccountMenu) { try @@ -558,21 +591,75 @@ public partial class AppShellViewModel : MailBaseViewModel, } } + public Task HandleAccountAttentionAsync(MailAccount account) + => FixAccountIssuesAsync(account); + + private void TriggerFullSynchronization(MailAccount account) + { + Messenger.Send(new NewMailSynchronizationRequested(new MailSynchronizationOptions + { + AccountId = account.Id, + Type = MailSynchronizationType.FullFolders + })); + + if (account.IsCalendarAccessGranted) + { + Messenger.Send(new NewCalendarSynchronizationRequested(new CalendarSynchronizationOptions + { + AccountId = account.Id, + Type = CalendarSynchronizationType.CalendarEvents + })); + } + } + private async Task FixAccountIssuesAsync(MailAccount account) { - // TODO: This area is very unclear. Needs to be rewritten with care. - // Fix account issues are expected to not work, but may work for some cases. - try { if (account.AttentionReason == AccountAttentionReason.InvalidCredentials) - await _accountService.FixTokenIssuesAsync(account.Id); + { + if (account.ProviderType is MailProviderType.Gmail or MailProviderType.Outlook) + { + await SynchronizationManager.Instance.HandleAuthorizationAsync( + account.ProviderType, + account, + account.ProviderType == MailProviderType.Gmail); + + await _accountService.ClearAccountAttentionAsync(account.Id); + + _dialogService.InfoBarMessage( + Translator.Info_AccountIssueFixSuccessTitle, + Translator.Info_AccountIssueFixSuccessMessage, + InfoBarMessageType.Success); + + TriggerFullSynchronization(account); + return; + } + + NavigationService.Navigate(WinoPage.SettingsPage, WinoPage.ManageAccountsPage); + Messenger.Send(new BreadcrumbNavigationRequested( + Translator.ImapCalDavSettingsPage_TitleEdit, + WinoPage.ImapCalDavSettingsPage, + ImapCalDavSettingsNavigationContext.CreateForEditMode(account.Id))); + + _dialogService.InfoBarMessage( + Translator.Info_AccountIssueFixSuccessTitle, + Translator.Info_AccountIssueFixImapMessage, + InfoBarMessageType.Information); + return; + } else if (account.AttentionReason == AccountAttentionReason.MissingSystemFolderConfiguration) + { await _dialogService.HandleSystemFolderConfigurationDialogAsync(account.Id, _folderService); + await _accountService.ClearAccountAttentionAsync(account.Id); - await _accountService.ClearAccountAttentionAsync(account.Id); + _dialogService.InfoBarMessage( + Translator.Info_AccountIssueFixSuccessTitle, + Translator.Info_AccountIssueFixSuccessMessage, + InfoBarMessageType.Success); - _dialogService.InfoBarMessage(Translator.Info_AccountIssueFixFailedTitle, Translator.Info_AccountIssueFixSuccessMessage, InfoBarMessageType.Success); + TriggerFullSynchronization(account); + } } catch (Exception ex) { @@ -619,14 +706,6 @@ public partial class AppShellViewModel : MailBaseViewModel, // Don't navigate to merged account if it's already selected. Preserve user's already selected folder. await ChangeLoadedAccountAsync(clickedMergedAccountMenuItem, true); } - else if (clickedMenuItem is SettingsItem) - { - NavigationService.Navigate(WinoPage.SettingsPage, parameter, NavigationReferenceFrame.ShellFrame, NavigationTransitionType.None); - } - else if (clickedMenuItem is ManageAccountsMenuItem) - { - NavigationService.Navigate(WinoPage.ManageAccountsPage, parameter, NavigationReferenceFrame.ShellFrame, NavigationTransitionType.None); - } else if (clickedMenuItem is IAccountMenuItem clickedAccountMenuItem) { // Changing loaded account. @@ -801,7 +880,7 @@ public partial class AppShellViewModel : MailBaseViewModel, if (isManageAccountClicked) { - SelectedMenuItem = ManageAccountsMenuItem; + NavigationService.Navigate(WinoPage.SettingsPage, WinoPage.ManageAccountsPage); } return; @@ -874,49 +953,20 @@ public partial class AppShellViewModel : MailBaseViewModel, await _winoRequestDelegator.ExecuteAsync(draftPreparationRequest); } - protected override async void OnAccountUpdated(MailAccount updatedAccount) + public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args) { - await ExecuteUIThread(() => + if (args.Handled || args.Mode != WinoApplicationMode.Mail) + return; + + if (args.Action == KeyboardShortcutAction.NewMail) { - if (MenuItems.TryGetAccountMenuItem(updatedAccount.Id, out IAccountMenuItem foundAccountMenuItem)) - { - foundAccountMenuItem.UpdateAccount(updatedAccount); - } - }); - } - - protected override void OnAccountRemoved(MailAccount removedAccount) - => Messenger.Send(new AccountsMenuRefreshRequested(false)); - - protected override async void OnAccountCreated(MailAccount createdAccount) - { - latestSelectedAccountMenuItem = null; - - await RecreateMenuItemsAsync(); - - if (!MenuItems.TryGetAccountMenuItem(createdAccount.Id, out IAccountMenuItem createdMenuItem)) return; - - await ChangeLoadedAccountAsync(createdMenuItem); - - // Each created account should start a new synchronization automatically. - var options = new MailSynchronizationOptions() - { - AccountId = createdAccount.Id, - Type = MailSynchronizationType.FullFolders, - }; - - Messenger.Send(new NewMailSynchronizationRequested(options, SynchronizationSource.Client)); - - try - { - await _nativeAppService.PinAppToTaskbarAsync(); - } - catch (Exception ex) - { - Log.Error(ex, "Failed to pin Wino to taskbar."); + await HandleCreateNewMailAsync(); + args.Handled = true; } } + + // TODO: Handle by messaging. private async Task SetAccountAttentionAsync(Guid accountId, AccountAttentionReason reason) { @@ -931,8 +981,6 @@ public partial class AppShellViewModel : MailBaseViewModel, accountMenuItem.UpdateAccount(accountModel); } - public void Receive(NavigateManageAccountsRequested message) => SelectedMenuItem = ManageAccountsMenuItem; - public async void Receive(MailtoProtocolMessageRequested message) { var accounts = await _accountService.GetAccountsAsync(); @@ -963,13 +1011,94 @@ public partial class AppShellViewModel : MailBaseViewModel, private async Task RecreateMenuItemsAsync() { - await ExecuteUIThread(() => + await _menuRefreshSemaphore.WaitAsync().ConfigureAwait(false); + try { - MenuItems.Clear(); - MenuItems.Add(CreateMailMenuItem); - }); + await ExecuteUIThread(() => + { + MenuItems.Clear(); + MenuItems.Add(CreateMailMenuItem); + }); - await LoadAccountsAsync(); + await LoadAccountsAsync(); + } + finally + { + _menuRefreshSemaphore.Release(); + } + } + + private async Task RestoreSelectedAccountAfterMenuRefreshAsync(bool automaticallyNavigateFirstItem) + { + IAccountMenuItem validSelectedMenuItem = null; + bool hasPreviousSelection = latestSelectedAccountMenuItem != null; + + if (hasPreviousSelection) + { + var selectedEntityId = latestSelectedAccountMenuItem.EntityId.GetValueOrDefault(); + + if (selectedEntityId != Guid.Empty && + MenuItems.TryGetAccountMenuItem(selectedEntityId, out IAccountMenuItem foundSelectedMenuItem)) + { + validSelectedMenuItem = foundSelectedMenuItem; + } + else + { + latestSelectedAccountMenuItem = null; + } + } + + if (validSelectedMenuItem == null) + { + validSelectedMenuItem = MenuItems.FirstOrDefault(a => a is IAccountMenuItem) as IAccountMenuItem; + hasPreviousSelection = false; + } + + if (validSelectedMenuItem != null) + { + await ChangeLoadedAccountAsync(validSelectedMenuItem, hasPreviousSelection || automaticallyNavigateFirstItem); + } + else + { + await ExecuteUIThread(() => SelectedMenuItem = null); + NavigateToWelcomeWizard(); + } + } + + private void NavigateToWelcomeWizard() + => NavigationService.Navigate( + WinoPage.WelcomeHostPage, + null, + NavigationReferenceFrame.ShellFrame, + NavigationTransitionType.None); + + private bool IsAccountCurrentlyLoaded(Guid accountId) + { + return latestSelectedAccountMenuItem?.HoldingAccounts?.Any(a => a.Id == accountId) == true; + } + + private async Task RefreshLoadedAccountFolderStructureAsync(Guid accountId) + { + if (!IsAccountCurrentlyLoaded(accountId) || latestSelectedAccountMenuItem == null) + return; + + var selectedFolderId = (SelectedMenuItem as IBaseFolderMenuItem)?.HandlingFolders + ?.FirstOrDefault(a => a.MailAccountId == accountId)?.Id; + + var folders = await _folderService.GetAccountFoldersForDisplayAsync(latestSelectedAccountMenuItem); + + await MenuItems.ReplaceFoldersAsync(folders); + await UpdateUnreadItemCountAsync(); + + if (selectedFolderId.HasValue && + MenuItems.TryGetFolderMenuItem(selectedFolderId.Value, out IBaseFolderMenuItem selectedFolderMenuItem)) + { + await NavigateFolderAsync(selectedFolderMenuItem); + } + else + { + await NavigateInboxAsync(latestSelectedAccountMenuItem); + } } public async void Receive(RefreshUnreadCountsMessage message) @@ -978,27 +1107,12 @@ public partial class AppShellViewModel : MailBaseViewModel, public async void Receive(AccountsMenuRefreshRequested message) { await RecreateMenuItemsAsync(); - - // Try to restore latest selected account. - if (latestSelectedAccountMenuItem != null) - { - await ChangeLoadedAccountAsync(latestSelectedAccountMenuItem, navigateInbox: true); - } - else if (MenuItems.FirstOrDefault(a => a is IAccountMenuItem) is IAccountMenuItem firstAccount) - { - await ChangeLoadedAccountAsync(firstAccount, message.AutomaticallyNavigateFirstItem); - } + await RestoreSelectedAccountAfterMenuRefreshAsync(message.AutomaticallyNavigateFirstItem); } public async void Receive(AccountFolderConfigurationUpdated message) { - // Reloading of folders is needed to re-create folder tree if the account is loaded. - - if (MenuItems.TryGetAccountMenuItem(message.AccountId, out IAccountMenuItem accountMenuItem) && - latestSelectedAccountMenuItem == accountMenuItem) - { - await ChangeLoadedAccountAsync(accountMenuItem, true); - } + await RefreshLoadedAccountFolderStructureAsync(message.AccountId); } public async void Receive(MergedInboxRenamed message) @@ -1015,10 +1129,9 @@ public partial class AppShellViewModel : MailBaseViewModel, public async void Receive(LanguageChanged message) { - await CreateFooterItemsAsync(); + await CreateFooterItemsAsync(true); await RecreateMenuItemsAsync(); - - await ChangeLoadedAccountAsync(latestSelectedAccountMenuItem, navigateInbox: false); + await RestoreSelectedAccountAfterMenuRefreshAsync(false); } private void ReorderAccountMenuItems(Dictionary newAccountOrder) @@ -1056,6 +1169,24 @@ public partial class AppShellViewModel : MailBaseViewModel, UpdateFolderCollection(mailItemFolder); } + protected override async void OnFolderDeleted(MailItemFolder folder) + { + base.OnFolderDeleted(folder); + + bool wasSelected = SelectedMenuItem is IBaseFolderMenuItem selectedFolder && + selectedFolder.HandlingFolders.Any(a => a.Id == folder.Id); + + await ExecuteUIThread(() => MenuItems.RemoveFolderMenuItem(folder.Id)); + + if (wasSelected && latestSelectedAccountMenuItem != null) + { + await NavigateInboxAsync(latestSelectedAccountMenuItem); + return; + } + + await RefreshLoadedAccountFolderStructureAsync(folder.MailAccountId); + } + protected override void OnFolderSynchronizationEnabled(IMailItemFolder mailItemFolder) { base.OnFolderSynchronizationEnabled(mailItemFolder); @@ -1063,17 +1194,163 @@ public partial class AppShellViewModel : MailBaseViewModel, UpdateFolderCollection(mailItemFolder); } - public async void Receive(AccountSynchronizationProgressUpdatedMessage message) + public async void Receive(AccountSynchronizerStateChanged message) { var accountMenuItem = MenuItems.GetSpecificAccountMenuItem(message.AccountId); if (accountMenuItem == null) return; - await ExecuteUIThread(() => { accountMenuItem.SynchronizationProgress = message.Progress; }); + await ExecuteUIThread(() => + { + accountMenuItem.TotalItemsToSync = message.TotalItemsToSync; + accountMenuItem.RemainingItemsToSync = message.RemainingItemsToSync; + accountMenuItem.SynchronizationStatus = message.SynchronizationStatus; + + // If this account is part of a merged inbox, update the merged inbox progress as well + if (accountMenuItem.ParentMenuItem is MergedAccountMenuItem mergedAccountMenuItem) + { + mergedAccountMenuItem.RefreshSynchronizationProgress(); + } + }); } public async void Receive(NavigateAppPreferencesRequested message) { - await MenuItemInvokedOrSelectedAsync(SettingsItem, WinoPage.AppPreferencesPage); + NavigationService.Navigate(WinoPage.SettingsPage, WinoPage.AppPreferencesPage); } + + protected override void RegisterRecipients() + { + base.RegisterRecipients(); + + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + } + + protected override void UnregisterRecipients() + { + base.UnregisterRecipients(); + + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + } + + public async void Receive(AccountRemovedMessage message) + { + var remainingAccounts = await _accountService.GetAccountsAsync().ConfigureAwait(false); + if (!remainingAccounts.Any()) + { + latestSelectedAccountMenuItem = null; + await ExecuteUIThread(() => + { + SelectedMenuItem = null; + MenuItems?.Clear(); + MenuItems?.Add(CreateMailMenuItem); + }); + return; + } + + if (latestSelectedAccountMenuItem?.HoldingAccounts?.Any(a => a.Id == message.Account.Id) == true) + { + latestSelectedAccountMenuItem = null; + await ExecuteUIThread(() => SelectedMenuItem = null); + } + + await RecreateMenuItemsAsync(); + await RestoreSelectedAccountAfterMenuRefreshAsync(false); + } + + public async Task HandleAccountCreatedAsync(MailAccount createdAccount) + { + latestSelectedAccountMenuItem = null; + + await RecreateMenuItemsAsync(); + + if (!MenuItems.TryGetAccountMenuItem(createdAccount.Id, out IAccountMenuItem createdMenuItem)) + { + Log.Warning("Created account {AccountId} could not be found in menu items after refresh.", createdAccount.Id); + return; + } + + await ChangeLoadedAccountAsync(createdMenuItem); + + // Each created account should start a new synchronization automatically. + var options = new MailSynchronizationOptions() + { + AccountId = createdAccount.Id, + Type = MailSynchronizationType.FullFolders, + }; + + Messenger.Send(new NewMailSynchronizationRequested(options)); + + if (createdAccount.IsCalendarAccessGranted) + { + var calendarOptions = new CalendarSynchronizationOptions() + { + AccountId = createdAccount.Id, + Type = CalendarSynchronizationType.CalendarEvents + }; + + Messenger.Send(new NewCalendarSynchronizationRequested(calendarOptions)); + } + + try + { + await _nativeAppService.PinAppToTaskbarAsync(); + } + catch (Exception ex) + { + Log.Error(ex, "Failed to pin Wino to taskbar."); + } + } + + public async void Receive(AccountUpdatedMessage message) + { + var updatedAccount = message.Account; + + await ExecuteUIThread(() => + { + if (MenuItems.TryGetAccountMenuItem(updatedAccount.Id, out IAccountMenuItem foundAccountMenuItem)) + { + foundAccountMenuItem.UpdateAccount(updatedAccount); + } + }); + } + + void IShellClient.Activate(ShellModeActivationContext activationContext) + => OnNavigatedTo(NavigationMode.New, activationContext); + + void IShellClient.Deactivate() + => OnNavigatedFrom(NavigationMode.New, null!); + + Task IShellClient.HandleNavigationItemInvokedAsync(IMenuItem menuItem) + => MenuItemInvokedOrSelectedAsync(menuItem); + + Task IShellClient.HandleNavigationSelectionChangedAsync(IMenuItem menuItem) + => menuItem == null ? Task.CompletedTask : MenuItemInvokedOrSelectedAsync(menuItem); } + + + + + + + diff --git a/Wino.Mail.ViewModels/MailBaseViewModel.cs b/Wino.Mail.ViewModels/MailBaseViewModel.cs index 084c498d..07be737f 100644 --- a/Wino.Mail.ViewModels/MailBaseViewModel.cs +++ b/Wino.Mail.ViewModels/MailBaseViewModel.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; -using CommunityToolkit.Mvvm.Messaging; +using CommunityToolkit.Mvvm.Messaging; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; using Wino.Core.Domain.Models.Folders; using Wino.Core.ViewModels; using Wino.Messaging.UI; @@ -12,39 +12,28 @@ public class MailBaseViewModel : CoreBaseViewModel, IRecipient, IRecipient, IRecipient, - IRecipient, IRecipient, IRecipient, IRecipient, IRecipient, IRecipient, + IRecipient, IRecipient { protected virtual void OnMailAdded(MailCopy addedMail) { } protected virtual void OnMailRemoved(MailCopy removedMail) { } - protected virtual void OnMailUpdated(MailCopy updatedMail) { } - - protected virtual void OnMailUpdated(IReadOnlyList updatedMails) - { - if (updatedMails == null) return; - - foreach (var mail in updatedMails) - { - OnMailUpdated(mail); - } - } - + protected virtual void OnMailUpdated(MailCopy updatedMail, MailUpdateSource source, MailCopyChangeFlags changedProperties) { } protected virtual void OnMailDownloaded(MailCopy downloadedMail) { } protected virtual void OnDraftCreated(MailCopy draftMail, MailAccount account) { } protected virtual void OnDraftFailed(MailCopy draftMail, MailAccount account) { } protected virtual void OnDraftMapped(string localDraftCopyId, string remoteDraftCopyId) { } protected virtual void OnFolderRenamed(IMailItemFolder mailItemFolder) { } + protected virtual void OnFolderDeleted(MailItemFolder folder) { } protected virtual void OnFolderSynchronizationEnabled(IMailItemFolder mailItemFolder) { } void IRecipient.Receive(MailAddedMessage message) => OnMailAdded(message.AddedMail); void IRecipient.Receive(MailRemovedMessage message) => OnMailRemoved(message.RemovedMail); - void IRecipient.Receive(MailUpdatedMessage message) => OnMailUpdated(message.UpdatedMail); - void IRecipient.Receive(BulkMailUpdatedMessage message) => OnMailUpdated(message.UpdatedMails); + void IRecipient.Receive(MailUpdatedMessage message) => OnMailUpdated(message.UpdatedMail, message.Source, message.ChangedProperties); void IRecipient.Receive(MailDownloadedMessage message) => OnMailDownloaded(message.DownloadedMail); void IRecipient.Receive(DraftMapped message) => OnDraftMapped(message.LocalDraftCopyId, message.RemoteDraftCopyId); @@ -52,5 +41,40 @@ public class MailBaseViewModel : CoreBaseViewModel, void IRecipient.Receive(DraftCreated message) => OnDraftCreated(message.DraftMail, message.Account); void IRecipient.Receive(FolderRenamed message) => OnFolderRenamed(message.MailItemFolder); + void IRecipient.Receive(FolderDeleted message) => OnFolderDeleted(message.MailItemFolder); void IRecipient.Receive(FolderSynchronizationEnabled message) => OnFolderSynchronizationEnabled(message.MailItemFolder); + + protected override void RegisterRecipients() + { + base.RegisterRecipients(); + + UnregisterRecipients(); + + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + } + + protected override void UnregisterRecipients() + { + base.UnregisterRecipients(); + + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + } } diff --git a/Wino.Mail.ViewModels/MailListPageViewModel.cs b/Wino.Mail.ViewModels/MailListPageViewModel.cs index d7bfc2a0..991bd517 100644 --- a/Wino.Mail.ViewModels/MailListPageViewModel.cs +++ b/Wino.Mail.ViewModels/MailListPageViewModel.cs @@ -1,15 +1,14 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Collections.Specialized; using System.Diagnostics; using System.Linq; -using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; +using CommunityToolkit.Mvvm.Messaging.Messages; using MoreLinq; using Nito.AsyncEx; using Serilog; @@ -18,12 +17,14 @@ using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models; using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Menus; +using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Reader; -using Wino.Core.Domain.Models.Server; using Wino.Core.Domain.Models.Synchronization; +using Wino.Core.Services; using Wino.Mail.ViewModels.Collections; using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Messages; @@ -36,13 +37,13 @@ namespace Wino.Mail.ViewModels; public partial class MailListPageViewModel : MailBaseViewModel, IRecipient, IRecipient, - IRecipient, - IRecipient, IRecipient, IRecipient, IRecipient, IRecipient, - IRecipient + IRecipient, + IRecipient>, + IRecipient { private bool isChangingFolder = false; @@ -57,13 +58,9 @@ public partial class MailListPageViewModel : MailBaseViewModel, private readonly HashSet gmailUnreadFolderMarkedAsReadUniqueIds = []; - private IObservable> selectionChangedObservable = null; - - public WinoMailCollection MailCollection { get; } - - public ObservableCollection SelectedItems { get; set; } = []; + public WinoMailCollection MailCollection { get; set; } = new WinoMailCollection(); public ObservableCollection PivotFolders { get; set; } = []; - public ObservableCollection ActionItems { get; set; } = []; + public ObservableCollection ActionItems { get; set; } = []; private readonly SemaphoreSlim listManipulationSemepahore = new SemaphoreSlim(1); private CancellationTokenSource listManipulationCancellationTokenSource = new CancellationTokenSource(); @@ -71,18 +68,18 @@ public partial class MailListPageViewModel : MailBaseViewModel, public INavigationService NavigationService { get; } public IStatePersistanceService StatePersistenceService { get; } public IPreferencesService PreferencesService { get; } - public IThemeService ThemeService { get; } + public INewThemeService ThemeService { get; } private readonly IAccountService _accountService; private readonly IMailDialogService _mailDialogService; private readonly IMailService _mailService; + private readonly IMimeFileService _mimeFileService; + private readonly INotificationBuilder _notificationBuilder; private readonly IFolderService _folderService; - private readonly IThreadingStrategyProvider _threadingStrategyProvider; private readonly IContextMenuItemService _contextMenuItemService; private readonly IWinoRequestDelegator _winoRequestDelegator; private readonly IKeyPressService _keyPressService; private readonly IWinoLogger _winoLogger; - private readonly IWinoServerConnectionManager _winoServerConnectionManager; private MailItemViewModel _activeMailItem; public List SortingOptions { get; } = @@ -104,6 +101,16 @@ public partial class MailListPageViewModel : MailBaseViewModel, [ObservableProperty] private bool isMultiSelectionModeEnabled; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(SelectedMessageText))] + [NotifyPropertyChangedFor(nameof(DraggingMessageText))] + public partial bool IsDragInProgress { get; set; } + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(SelectedMessageText))] + [NotifyPropertyChangedFor(nameof(DraggingMessageText))] + public partial int DraggingItemsCount { get; set; } + [ObservableProperty] public partial string SearchQuery { get; set; } @@ -116,25 +123,32 @@ public partial class MailListPageViewModel : MailBaseViewModel, [NotifyPropertyChangedFor(nameof(IsEmpty))] [NotifyPropertyChangedFor(nameof(IsFolderEmpty))] [NotifyPropertyChangedFor(nameof(IsProgressRing))] - private bool isInitializingFolder; + [NotifyCanExecuteChangedFor(nameof(LoadMoreItemsCommand))] + public partial bool IsInitializingFolder { get; set; } [ObservableProperty] - private InfoBarMessageType barSeverity; + [NotifyCanExecuteChangedFor(nameof(LoadMoreItemsCommand))] + public partial bool FinishedLoading { get; set; } = false; + + public bool CanLoadMoreItems => !IsInitializingFolder && !IsOnlineSearchEnabled && !FinishedLoading; [ObservableProperty] - private string barMessage; + public partial InfoBarMessageType BarSeverity { get; set; } [ObservableProperty] - private double mailListLength = 420; + public partial string BarMessage { get; set; } [ObservableProperty] - private double maxMailListLength = 1200; + public partial double MailListLength { get; set; } = 420; [ObservableProperty] - private string barTitle; + public partial double MaxMailListLength { get; set; } = 1200; [ObservableProperty] - private bool isBarOpen; + public partial string BarTitle { get; set; } + + [ObservableProperty] + public partial bool IsBarOpen { get; set; } /// /// Current folder that is being represented from the menu. @@ -142,80 +156,89 @@ public partial class MailListPageViewModel : MailBaseViewModel, [ObservableProperty] [NotifyPropertyChangedFor(nameof(CanSynchronize))] [NotifyPropertyChangedFor(nameof(IsFolderSynchronizationEnabled))] - private IBaseFolderMenuItem activeFolder; + public partial IBaseFolderMenuItem ActiveFolder { get; set; } [ObservableProperty] [NotifyPropertyChangedFor(nameof(CanSynchronize))] - private bool isAccountSynchronizerInSynchronization; + public partial bool IsAccountSynchronizerInSynchronization { get; set; } public MailListPageViewModel(IMailDialogService dialogService, INavigationService navigationService, IAccountService accountService, IMailDialogService mailDialogService, IMailService mailService, + IMimeFileService mimeFileService, IStatePersistanceService statePersistenceService, + INotificationBuilder notificationBuilder, IFolderService folderService, - IThreadingStrategyProvider threadingStrategyProvider, IContextMenuItemService contextMenuItemService, IWinoRequestDelegator winoRequestDelegator, IKeyPressService keyPressService, IPreferencesService preferencesService, - IThemeService themeService, - IWinoLogger winoLogger, - IWinoServerConnectionManager winoServerConnectionManager) + INewThemeService themeService, + IWinoLogger winoLogger) { - MailCollection = new WinoMailCollection(threadingStrategyProvider); - PreferencesService = preferencesService; - ThemeService = themeService; _winoLogger = winoLogger; - _winoServerConnectionManager = winoServerConnectionManager; - StatePersistenceService = statePersistenceService; - NavigationService = navigationService; _accountService = accountService; _mailDialogService = mailDialogService; _mailService = mailService; + _mimeFileService = mimeFileService; _folderService = folderService; - _threadingStrategyProvider = threadingStrategyProvider; _contextMenuItemService = contextMenuItemService; _winoRequestDelegator = winoRequestDelegator; _keyPressService = keyPressService; + PreferencesService = preferencesService; + ThemeService = themeService; + StatePersistenceService = statePersistenceService; + _notificationBuilder = notificationBuilder; + NavigationService = navigationService; + SelectedFilterOption = FilterOptions[0]; SelectedSortingOption = SortingOptions[0]; - mailListLength = statePersistenceService.MailListPaneLength; + MailListLength = statePersistenceService.MailListPaneLength; + } - selectionChangedObservable = Observable.FromEventPattern(SelectedItems, nameof(SelectedItems.CollectionChanged)); - selectionChangedObservable - .Throttle(TimeSpan.FromMilliseconds(100)) - .Subscribe(async a => - { - await ExecuteUIThread(() => { SelectedItemCollectionUpdated(a.EventArgs); }); - }); + public override void OnNavigatedTo(NavigationMode mode, object parameters) + { + base.OnNavigatedTo(mode, parameters); - MailCollection.MailItemRemoved += (c, removedItem) => + MailCollection.ItemSelectionChanged += MailItemSelectionChanged; + } + + public override async void OnNavigatedFrom(NavigationMode mode, object parameters) + { + base.OnNavigatedFrom(mode, parameters); + + MailCollection.ItemSelectionChanged -= MailItemSelectionChanged; + + await MailCollection.ClearAsync(); + MailCollection.Cleanup(); + } + + private void MailItemSelectionChanged(object sender, EventArgs e) + { + if (MailCollection.HasSingleItemSelected) { - if (removedItem is ThreadMailItemViewModel removedThreadViewModelItem) - { - foreach (var viewModel in removedThreadViewModelItem.ThreadItems.Cast()) - { - if (SelectedItems.Contains(viewModel)) - { - SelectedItems.Remove(viewModel); - } - } - } - else if (removedItem is MailItemViewModel removedMailItemViewModel && SelectedItems.Contains(removedMailItemViewModel)) - { - SelectedItems.Remove(removedMailItemViewModel); - } - }; + var selectedItem = MailCollection.SelectedItems.ElementAtOrDefault(0); + ActiveMailItemChanged(selectedItem); + } + else if (MailCollection.SelectedItemsCount == 0) + { + ActiveMailItemChanged(null); + } + + NotifyItemFoundState(); + NotifyItemSelected(); + SetupTopBarActions(); } private void SetupTopBarActions() { ActionItems.Clear(); - var actions = GetAvailableMailActions(SelectedItems); + + var actions = GetAvailableMailActions(MailCollection.SelectedItems); actions.ForEach(a => ActionItems.Add(a)); } @@ -248,7 +271,7 @@ public partial class MailListPageViewModel : MailBaseViewModel, { if (value != null && MailCollection != null) { - MailCollection.SortingType = value.Type; + MailCollection.GroupingType = value.Type == SortingOptionType.ReceiveDate ? EmailGroupingType.ByDate : EmailGroupingType.ByFromName; } } } @@ -256,18 +279,20 @@ public partial class MailListPageViewModel : MailBaseViewModel, public bool CanSynchronize => !IsAccountSynchronizerInSynchronization && IsFolderSynchronizationEnabled; public bool IsFolderSynchronizationEnabled => ActiveFolder?.IsSynchronizationEnabled ?? false; - public int SelectedItemCount => SelectedItems.Count; - public bool HasMultipleItemSelections => SelectedItemCount > 1; - public bool HasSingleItemSelection => SelectedItemCount == 1; - public bool HasSelectedItems => SelectedItems.Any(); public bool IsArchiveSpecialFolder => ActiveFolder?.SpecialFolderType == SpecialFolderType.Archive; - public string SelectedMessageText => HasSelectedItems ? string.Format(Translator.MailsSelected, SelectedItemCount) : Translator.NoMailSelected; + public string SelectedMessageText => IsDragInProgress + ? string.Format(Translator.MailsDragging, DraggingItemsCount) + : MailCollection.SelectedItemsCount > 0 + ? string.Format(Translator.MailsSelected, MailCollection.SelectedItemsCount) + : Translator.NoMailSelected; + + public string DraggingMessageText => string.Format(Translator.MailsDragging, DraggingItemsCount); /// /// Indicates current state of the mail list. Doesn't matter it's loading or no. /// - public bool IsEmpty => MailCollection.Count == 0; + public bool IsEmpty => MailCollection.AllItemsCount == 0; /// /// Progress ring only should be visible when the folder is initializing and there are no items. We don't need to show it when there are items. @@ -284,6 +309,7 @@ public partial class MailListPageViewModel : MailBaseViewModel, public partial bool IsOnlineSearchButtonVisible { get; set; } [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(LoadMoreItemsCommand))] public partial bool IsOnlineSearchEnabled { get; set; } [ObservableProperty] @@ -295,20 +321,6 @@ public partial class MailListPageViewModel : MailBaseViewModel, { if (_activeMailItem == selectedMailItemViewModel) return; - // Don't update active mail item if Ctrl key is pressed or multi selection is enabled. - // User is probably trying to select multiple items. - // This is not the same behavior in Windows Mail, - // but it's a trash behavior. - - var isCtrlKeyPressed = _keyPressService.IsCtrlKeyPressed(); - - bool isMultiSelecting = isCtrlKeyPressed || IsMultiSelectionModeEnabled; - - if (isMultiSelecting && StatePersistenceService.IsReaderNarrowed) - { - return; - } - _activeMailItem = selectedMailItemViewModel; Messenger.Send(new ActiveMailItemChangedEvent(_activeMailItem)); @@ -341,13 +353,14 @@ public partial class MailListPageViewModel : MailBaseViewModel, public void NotifyItemSelected() { OnPropertyChanged(nameof(SelectedMessageText)); - OnPropertyChanged(nameof(HasSingleItemSelection)); - OnPropertyChanged(nameof(HasSelectedItems)); - OnPropertyChanged(nameof(SelectedItemCount)); - OnPropertyChanged(nameof(HasMultipleItemSelections)); - if (SelectedFolderPivot != null) - SelectedFolderPivot.SelectedItemCount = SelectedItemCount; + SelectedFolderPivot?.SelectedItemCount = MailCollection.SelectedItemsCount; + } + + public void SetDragState(bool isDragInProgress, int draggingItemsCount = 0) + { + IsDragInProgress = isDragInProgress; + DraggingItemsCount = isDragInProgress ? Math.Max(1, draggingItemsCount) : 0; } private void NotifyItemFoundState() @@ -356,13 +369,6 @@ public partial class MailListPageViewModel : MailBaseViewModel, OnPropertyChanged(nameof(IsFolderEmpty)); } - protected override void OnDispatcherAssigned() - { - base.OnDispatcherAssigned(); - - MailCollection.CoreDispatcher = Dispatcher; - } - private async void UpdateBarMessage(InfoBarMessageType severity, string title, string message) { await ExecuteUIThread(() => @@ -375,26 +381,6 @@ public partial class MailListPageViewModel : MailBaseViewModel, }); } - private void SelectedItemCollectionUpdated(NotifyCollectionChangedEventArgs e) - { - if (SelectedItems.Count == 1) - { - ActiveMailItemChanged(SelectedItems[0]); - } - else - { - // At this point, either we don't have any item selected - // or we have multiple item selected. In either case - // there should be no active item. - - ActiveMailItemChanged(null); - } - - NotifyItemSelected(); - - SetupTopBarActions(); - } - private async Task UpdateFolderPivotsAsync() { if (ActiveFolder == null) return; @@ -453,11 +439,11 @@ public partial class MailListPageViewModel : MailBaseViewModel, public Task ExecuteHoverAction(MailOperationPreperationRequest request) => ExecuteMailOperationAsync(request); [RelayCommand] - private async Task ExecuteTopBarAction(MailOperationMenuItem menuItem) + private async Task ExecuteTopBarAction(IMenuOperation menuItem) { - if (menuItem == null || !SelectedItems.Any()) return; + if (menuItem is not MailOperationMenuItem mailOperationMenuItem || MailCollection.SelectedItemsCount == 0) return; - await HandleMailOperation(menuItem.Operation, SelectedItems); + await HandleMailOperation(mailOperationMenuItem.Operation, MailCollection.SelectedItems); } /// @@ -467,9 +453,9 @@ public partial class MailListPageViewModel : MailBaseViewModel, [RelayCommand] private async Task ExecuteMailOperation(MailOperation mailOperation) { - if (!SelectedItems.Any()) return; + if (MailCollection.SelectedItemsCount == 0) return; - await HandleMailOperation(mailOperation, SelectedItems); + await HandleMailOperation(mailOperation, MailCollection.SelectedItems); } private async Task HandleMailOperation(MailOperation mailOperation, IEnumerable mailItems) @@ -507,7 +493,7 @@ public partial class MailListPageViewModel : MailBaseViewModel, GroupedSynchronizationTrackingId = trackingSynchronizationId }; - Messenger.Send(new NewMailSynchronizationRequested(options, SynchronizationSource.Client)); + Messenger.Send(new NewMailSynchronizationRequested(options)); } } @@ -544,6 +530,8 @@ public partial class MailListPageViewModel : MailBaseViewModel, { IsOnlineSearchEnabled = false; AreSearchResultsOnline = false; + HasNoOnlineSearchResult = false; + OnPropertyChanged(nameof(HasNoOnlineSearchResult)); IsInSearchMode = !string.IsNullOrEmpty(SearchQuery); if (IsInSearchMode) @@ -565,11 +553,12 @@ public partial class MailListPageViewModel : MailBaseViewModel, } } - [RelayCommand] + [RelayCommand(CanExecute = nameof(CanLoadMoreItems))] private async Task LoadMoreItemsAsync() { - if (IsInitializingFolder || IsOnlineSearchEnabled) return; + if (IsInitializingFolder || IsOnlineSearchEnabled || FinishedLoading) return; + Debug.WriteLine("Loading more..."); await ExecuteUIThread(() => { IsInitializingFolder = true; }); var initializationOptions = new MailListInitializationOptions(ActiveFolder.HandlingFolders, @@ -582,9 +571,18 @@ public partial class MailListPageViewModel : MailBaseViewModel, var items = await _mailService.FetchMailsAsync(initializationOptions).ConfigureAwait(false); - var viewModels = PrepareMailViewModels(items); + if (items.Count == 0) + { + await ExecuteUIThread(() => { FinishedLoading = true; }); - await ExecuteUIThread(() => { MailCollection.AddRange(viewModels, clearIdCache: false); }); + return; + } + + var viewModels = await PrepareMailViewModelsAsync(items).ConfigureAwait(false); + var pendingOperationUniqueIds = await GetPendingOperationUniqueIdsForActiveFolderAccountsAsync().ConfigureAwait(false); + ApplyPendingOperationBusyStates(viewModels, pendingOperationUniqueIds); + + await MailCollection.AddRangeAsync(viewModels, false); await ExecuteUIThread(() => { IsInitializingFolder = false; }); } @@ -592,43 +590,91 @@ public partial class MailListPageViewModel : MailBaseViewModel, public Task ExecuteMailOperationAsync(MailOperationPreperationRequest package) => _winoRequestDelegator.ExecuteAsync(package); - public IEnumerable GetTargetMailItemViewModels(IMailItem clickedItem) + public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args) { - // Threat threads as a whole and include everything in the group. Except single selections outside of the thread. - IEnumerable contextMailItems = null; + if (args.Handled || args.Mode != WinoApplicationMode.Mail) + return; - if (clickedItem is ThreadMailItemViewModel clickedThreadItem) + var targetItems = GetShortcutTargetItems().ToList(); + + switch (args.Action) { - // Clicked item is a thread. - - clickedThreadItem.IsThreadExpanded = true; - contextMailItems = clickedThreadItem.ThreadItems.Cast(); - - // contextMailItems = clickedThreadItem.GetMailCopies(); + case KeyboardShortcutAction.ToggleReadUnread: + if (!targetItems.Any()) return; + await ExecuteMailOperationAsync(new MailOperationPreperationRequest(MailOperation.MarkAsRead, targetItems.Select(x => x.MailCopy), true)); + args.Handled = true; + break; + case KeyboardShortcutAction.ToggleFlag: + if (!targetItems.Any()) return; + await ExecuteMailOperationAsync(new MailOperationPreperationRequest(MailOperation.SetFlag, targetItems.Select(x => x.MailCopy), true)); + args.Handled = true; + break; + case KeyboardShortcutAction.ToggleArchive: + if (!targetItems.Any()) return; + await ExecuteMailOperationAsync(new MailOperationPreperationRequest(MailOperation.Archive, targetItems.Select(x => x.MailCopy), true)); + args.Handled = true; + break; + case KeyboardShortcutAction.Delete: + if (!targetItems.Any()) return; + await ExecuteMailOperationAsync(new MailOperationPreperationRequest(MailOperation.SoftDelete, targetItems.Select(x => x.MailCopy))); + args.Handled = true; + break; + case KeyboardShortcutAction.Move: + if (!targetItems.Any()) return; + await ExecuteMailOperationAsync(new MailOperationPreperationRequest(MailOperation.Move, targetItems.Select(x => x.MailCopy))); + args.Handled = true; + break; + case KeyboardShortcutAction.Reply: + await CreateReplyDraftAsync(DraftCreationReason.Reply); + args.Handled = true; + break; + case KeyboardShortcutAction.ReplyAll: + await CreateReplyDraftAsync(DraftCreationReason.ReplyAll); + args.Handled = true; + break; } - else if (clickedItem is MailItemViewModel clickedMailItemViewModel) - { - // If the clicked item is included in SelectedItems, then we need to thing them as whole. - // If there are selected items, but clicked item is not one of them, then it's a single context menu. - - bool includedInSelectedItems = SelectedItems.Contains(clickedItem); - - if (includedInSelectedItems) - contextMailItems = SelectedItems; - else - contextMailItems = [clickedMailItemViewModel]; - } - - return contextMailItems; } - public IEnumerable GetAvailableMailActions(IEnumerable contextMailItems) - => _contextMenuItemService.GetMailItemContextMenuActions(contextMailItems); + private IEnumerable GetShortcutTargetItems() + { + if (MailCollection.SelectedItemsCount > 0) + return MailCollection.SelectedItems.OfType(); - public void ChangeCustomFocusedState(IEnumerable mailItems, bool isFocused) - => mailItems.OfType().ForEach(a => a.IsCustomFocused = isFocused); + if (_activeMailItem != null) + return [_activeMailItem]; - private bool ShouldPreventItemAdd(IMailItem mailItem) + return []; + } + + private async Task CreateReplyDraftAsync(DraftCreationReason reason) + { + var targetMail = GetShortcutTargetItems().FirstOrDefault(); + if (targetMail?.MailCopy == null || targetMail.MailCopy.FileId == Guid.Empty) + return; + + var mimeInformation = await _mimeFileService.GetMimeMessageInformationAsync(targetMail.MailCopy.FileId, targetMail.MailCopy.AssignedAccount.Id); + if (mimeInformation?.MimeMessage == null) + return; + + var draftOptions = new DraftCreationOptions + { + Reason = reason, + ReferencedMessage = new ReferencedMessage + { + MimeMessage = mimeInformation.MimeMessage, + MailCopy = targetMail.MailCopy + } + }; + + var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(targetMail.MailCopy.AssignedAccount.Id, draftOptions).ConfigureAwait(false); + var draftPreparationRequest = new DraftPreparationRequest(targetMail.MailCopy.AssignedAccount, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason, targetMail.MailCopy); + await _winoRequestDelegator.ExecuteAsync(draftPreparationRequest); + } + + public IEnumerable GetAvailableMailActions(IEnumerable contextMailItems) + => _contextMenuItemService.GetMailItemContextMenuActions(contextMailItems.Select(a => a.MailCopy)); + + private bool ShouldPreventItemAdd(MailCopy mailItem) { bool condition = mailItem.IsRead && SelectedFilterOption.Type == FilterOptionType.Unread @@ -638,6 +684,80 @@ public partial class MailListPageViewModel : MailBaseViewModel, return condition; } + private static bool IsDraftOrSentFolder(MailCopy mailItem) + => mailItem?.AssignedFolder?.SpecialFolderType is SpecialFolderType.Draft or SpecialFolderType.Sent; + + private bool IsActiveDraftFolder() + => ActiveFolder?.SpecialFolderType == SpecialFolderType.Draft; + + private bool BelongsToActiveFolder(MailCopy mailItem) + => mailItem?.AssignedFolder != null && ActiveFolder?.HandlingFolders?.Any(a => a.Id == mailItem.AssignedFolder.Id) == true; + + private bool ShouldIncludeByThread(MailCopy mailItem) + => PreferencesService.IsThreadingEnabled + && !string.IsNullOrEmpty(mailItem?.ThreadId) + && ThreadIdExistsInCollection(mailItem); + + private bool ShouldIncludeAddedMailInCurrentList(MailCopy addedMail) + { + if (addedMail == null || ActiveFolder == null || addedMail.AssignedFolder == null) + return false; + + // 1) If threading is enabled and we already have the same conversation in view, include it. + if (ShouldIncludeByThread(addedMail)) + return true; + + // 2) Include items that belong to the active folder. + if (BelongsToActiveFolder(addedMail)) + return true; + + // 3) Draft-specific visibility: include drafts while viewing Drafts. + if (addedMail.IsDraft && IsActiveDraftFolder()) + return true; + + return false; + } + + private bool IsMailMatchingLocalSearch(MailCopy mailItem) + { + if (!IsInSearchMode) return true; + if (string.IsNullOrWhiteSpace(SearchQuery)) return true; + + var query = SearchQuery.Trim(); + + return (!string.IsNullOrEmpty(mailItem.Subject) && mailItem.Subject.Contains(query, StringComparison.OrdinalIgnoreCase)) + || (!string.IsNullOrEmpty(mailItem.PreviewText) && mailItem.PreviewText.Contains(query, StringComparison.OrdinalIgnoreCase)) + || (!string.IsNullOrEmpty(mailItem.FromName) && mailItem.FromName.Contains(query, StringComparison.OrdinalIgnoreCase)) + || (!string.IsNullOrEmpty(mailItem.FromAddress) && mailItem.FromAddress.Contains(query, StringComparison.OrdinalIgnoreCase)); + } + + private bool ShouldRemoveUpdatedMailFromCurrentList(MailCopy updatedMail) + { + // Update flow already checks if this item is currently listed. + // Keep the item in the list and update in-place. + _ = updatedMail; + return false; + } + + [RelayCommand] + public void RemoveFirst() + { + var fi = MailCollection.GetFirst(); + if (fi == null) return; + + Messenger.Send(new MailRemovedMessage(fi.MailCopy)); + } + + /// + /// Checks if a ThreadId exists in the current mail collection. + /// + /// The mail item to check ThreadId for. + /// True if the ThreadId exists in the collection, false otherwise. + private bool ThreadIdExistsInCollection(MailCopy mailItem) + { + return MailCollection.ContainsThreadId(mailItem.ThreadId); + } + protected override async void OnMailAdded(MailCopy addedMail) { base.OnMailAdded(addedMail); @@ -651,21 +771,82 @@ public partial class MailListPageViewModel : MailBaseViewModel, // At least one of the accounts we are listing must match with the account of the added mail. if (!ActiveFolder.HandlingFolders.Any(a => a.MailAccountId == addedMail.AssignedAccount.Id)) return; - // Messages coming to sent or draft folder must be inserted regardless of the filter. - bool shouldPreventIgnoringFilter = addedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Draft || - addedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Sent; + // Fix for draft duplication: When a draft is created for reply/forward, it's first added as local draft. + // Then the server sync fetches it back. We should skip adding remote drafts if a local draft already exists + // with the same ThreadId. The mapping system (DraftMapped) will handle updating the existing local draft. + if (addedMail.IsDraft && !addedMail.IsLocalDraft && !string.IsNullOrEmpty(addedMail.ThreadId)) + { + // Check if collection already has a local draft with the same ThreadId in the same folder + bool hasLocalDraftInSameThread = false; - // Item does not belong to this folder and doesn't have special type to be inserted. - if (!shouldPreventIgnoringFilter && !ActiveFolder.HandlingFolders.Any(a => a.Id == addedMail.AssignedFolder.Id)) return; + foreach (var group in MailCollection.MailItems) + { + foreach (var item in group) + { + if (item is MailItemViewModel mailItem) + { + if (mailItem.IsDraft && + mailItem.MailCopy.IsLocalDraft && + mailItem.MailCopy.ThreadId == addedMail.ThreadId && + mailItem.MailCopy.FolderId == addedMail.FolderId) + { + hasLocalDraftInSameThread = true; + break; + } + } + else if (item is ThreadMailItemViewModel threadItem) + { + foreach (var threadEmail in threadItem.ThreadEmails) + { + if (threadEmail.IsDraft && + threadEmail.MailCopy.IsLocalDraft && + threadEmail.MailCopy.ThreadId == addedMail.ThreadId && + threadEmail.MailCopy.FolderId == addedMail.FolderId) + { + hasLocalDraftInSameThread = true; + break; + } + } + if (hasLocalDraftInSameThread) break; + } + } + if (hasLocalDraftInSameThread) break; + } - // Item should be prevented from being added to the list due to filter. - if (!shouldPreventIgnoringFilter && ShouldPreventItemAdd(addedMail)) return; + if (hasLocalDraftInSameThread) + { + // Local draft exists in the same thread - skip adding remote duplicate + // The mapping system will update the local draft with remote IDs when DraftMapped message is received + return; + } + } + + if (!ShouldIncludeAddedMailInCurrentList(addedMail)) return; + if (ShouldPreventItemAdd(addedMail)) return; + + if (SelectedFolderPivot?.IsFocused is bool isFocused && addedMail.IsFocused != isFocused) + { + return; + } + + if (IsInSearchMode) + { + // Online search results are loaded from a dedicated query snapshot. + // Ignore live additions while that snapshot is active. + if (IsOnlineSearchEnabled || AreSearchResultsOnline) return; + + if (!IsMailMatchingLocalSearch(addedMail)) return; + } await listManipulationSemepahore.WaitAsync(); + // AddAsync already handles UI threading internally, no need to wrap it await MailCollection.AddAsync(addedMail); - await ExecuteUIThread(() => { NotifyItemFoundState(); }); + await ExecuteUIThread(() => + { + NotifyItemFoundState(); + }); } catch { } finally @@ -674,15 +855,32 @@ public partial class MailListPageViewModel : MailBaseViewModel, } } - protected override async void OnMailUpdated(MailCopy updatedMail) + protected override async void OnMailUpdated(MailCopy updatedMail, MailUpdateSource source, MailCopyChangeFlags changedProperties) { - base.OnMailUpdated(updatedMail); + base.OnMailUpdated(updatedMail, source, changedProperties); - Debug.WriteLine($"Updating {updatedMail.Id}-> {updatedMail.UniqueId}"); + try + { + await listManipulationSemepahore.WaitAsync(); - await MailCollection.UpdateMailCopy(updatedMail); + bool isItemListed = MailCollection.ContainsMailUniqueId(updatedMail.UniqueId); + if (!isItemListed) return; - await ExecuteUIThread(() => { SetupTopBarActions(); }); + if (ShouldRemoveUpdatedMailFromCurrentList(updatedMail)) + { + await MailCollection.RemoveAsync(updatedMail); + await ExecuteUIThread(() => { NotifyItemFoundState(); }); + return; + } + + await MailCollection.UpdateMailCopy(updatedMail, source, changedProperties); + } + finally + { + listManipulationSemepahore.Release(); + } + + // await ExecuteUIThread(() => { SetupTopBarActions(); }); } protected override async void OnMailUpdated(IReadOnlyList updatedMails) @@ -720,55 +918,74 @@ public partial class MailListPageViewModel : MailBaseViewModel, { base.OnMailRemoved(removedMail); - if (removedMail.AssignedAccount == null || removedMail.AssignedFolder == null) return; + if (removedMail.AssignedAccount == null) return; - // We should delete the items only if: - // 1. They are deleted from the active folder. - // 2. Deleted from draft or sent folder. - // 3. Removal is not caused by Gmail Unread folder action. - // Delete/sent are special folders that can list their items in other folders. - - bool removedFromActiveFolder = ActiveFolder.HandlingFolders.Any(a => a.Id == removedMail.AssignedFolder.Id); - bool removedFromDraftOrSent = removedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Draft || - removedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Sent; - - bool isDeletedByGmailUnreadFolderAction = ActiveFolder.SpecialFolderType == SpecialFolderType.Unread && - gmailUnreadFolderMarkedAsReadUniqueIds.Contains(removedMail.UniqueId); - - if ((removedFromActiveFolder || removedFromDraftOrSent) && !isDeletedByGmailUnreadFolderAction) + try { - bool isDeletedMailSelected = SelectedItems.Any(a => a.MailCopy.UniqueId == removedMail.UniqueId); + await listManipulationSemepahore.WaitAsync(); - // Automatically select the next item in the list if the setting is enabled. - MailItemViewModel nextItem = null; + // Remove only if this specific mail copy currently exists in this list. + // Using AssignedFolder-based checks is unreliable for move flows because the + // same MailCopy instance can be updated before this message is handled. + bool removedItemExistsInCurrentList = MailCollection.ContainsMailUniqueId(removedMail.UniqueId); - if (isDeletedMailSelected && PreferencesService.AutoSelectNextItem) + bool isDeletedByGmailUnreadFolderAction = ActiveFolder?.SpecialFolderType == SpecialFolderType.Unread && + gmailUnreadFolderMarkedAsReadUniqueIds.Contains(removedMail.UniqueId); + + if (removedItemExistsInCurrentList && !isDeletedByGmailUnreadFolderAction) { - await ExecuteUIThread(() => + bool isDeletedMailSelected = MailCollection.SelectedItems.Any(a => a.MailCopy.UniqueId == removedMail.UniqueId); + + // Automatically select the next item in the list if the setting is enabled. + MailItemViewModel nextItem = null; + + if (isDeletedMailSelected && PreferencesService.AutoSelectNextItem) { - nextItem = MailCollection.GetNextItem(removedMail); - }); + await ExecuteUIThread(() => + { + nextItem = MailCollection.GetNextItem(removedMail); + }); + } + + // RemoveAsync already handles UI threading internally + await MailCollection.RemoveAsync(removedMail); + + if (nextItem != null) + WeakReferenceMessenger.Default.Send(new SelectMailItemContainerEvent(nextItem.UniqueId, ScrollToItem: true)); + else if (isDeletedMailSelected) + { + // There are no next item to select, but we removed the last item which was selected. + // Clearing selected item will dispose rendering page. + + // UnselectAllAsync already handles UI threading internally + await MailCollection.UnselectAllAsync(); + } + + await ExecuteUIThread(() => { NotifyItemFoundState(); }); } - - // Remove the deleted item from the list. - await MailCollection.RemoveAsync(removedMail); - - if (nextItem != null) - WeakReferenceMessenger.Default.Send(new SelectMailItemContainerEvent(nextItem, ScrollToItem: true)); - else if (isDeletedMailSelected) + else if (isDeletedByGmailUnreadFolderAction) { - // There are no next item to select, but we removed the last item which was selected. - // Clearing selected item will dispose rendering page. - - SelectedItems.Clear(); + // Remove the entry from the set so we can listen to actual deletes next time. + gmailUnreadFolderMarkedAsReadUniqueIds.Remove(removedMail.UniqueId); } - - await ExecuteUIThread(() => { NotifyItemFoundState(); }); } - else if (isDeletedByGmailUnreadFolderAction) + finally { - // Remove the entry from the set so we can listen to actual deletes next time. - gmailUnreadFolderMarkedAsReadUniqueIds.Remove(removedMail.UniqueId); + listManipulationSemepahore.Release(); + } + } + + protected override async void OnFolderDeleted(MailItemFolder folder) + { + base.OnFolderDeleted(folder); + + if (ActiveFolder == null) return; + + bool isActiveFolder = ActiveFolder.HandlingFolders.Any(a => a.Id == folder.Id); + + if (isActiveFolder) + { + await MailCollection.ClearAsync(); } } @@ -782,7 +999,7 @@ public partial class MailListPageViewModel : MailBaseViewModel, // Otherwise the draft mail item will be duplicated on the next add execution. await listManipulationSemepahore.WaitAsync(); - // Create the item. Draft folder navigation is already done at this point. + // AddAsync already handles UI threading internally await MailCollection.AddAsync(draftMail); await ExecuteUIThread(() => @@ -799,14 +1016,71 @@ public partial class MailListPageViewModel : MailBaseViewModel, } } - private IEnumerable PrepareMailViewModels(IEnumerable mailItems) + protected override void OnDraftMapped(string localDraftCopyId, string remoteDraftCopyId) { - foreach (var item in mailItems) + base.OnDraftMapped(localDraftCopyId, remoteDraftCopyId); + + // When a draft is mapped from local to remote, the database has been updated + // but the UI collection still references the MailCopy object with old IDs. + // The MailCollection.AddAsync method checks UniqueId (which doesn't change during mapping) + // so if mapping worked correctly, no duplicate should appear. + // This method is here for future enhancements if additional UI updates are needed. + } + + private async Task> PrepareMailViewModelsAsync(IEnumerable mailItems, CancellationToken cancellationToken = default) + { + // Run ViewModel creation on background thread to avoid blocking UI + return await Task.Run(() => { - if (item is MailCopy singleMailItem) - yield return new MailItemViewModel(singleMailItem); - else if (item is ThreadMailItem threadMailItem) - yield return new ThreadMailItemViewModel(threadMailItem); + var viewModels = new List(); + foreach (var mailItem in mailItems) + { + cancellationToken.ThrowIfCancellationRequested(); + viewModels.Add(new MailItemViewModel(mailItem)); + } + return viewModels; + }, cancellationToken).ConfigureAwait(false); + } + + private async Task> GetPendingOperationUniqueIdsForActiveFolderAccountsAsync(CancellationToken cancellationToken = default) + { + var pendingOperationUniqueIds = new HashSet(); + + var accountIds = ActiveFolder?.HandlingFolders? + .Select(folder => folder.MailAccountId) + .Where(accountId => accountId != Guid.Empty) + .Distinct() + .ToList(); + + if (accountIds == null || accountIds.Count == 0) + return pendingOperationUniqueIds; + + foreach (var accountId in accountIds) + { + cancellationToken.ThrowIfCancellationRequested(); + + var synchronizer = await SynchronizationManager.Instance.GetSynchronizerAsync(accountId).ConfigureAwait(false); + + if (synchronizer == null) + continue; + + foreach (var uniqueId in synchronizer.GetPendingOperationUniqueIds()) + { + pendingOperationUniqueIds.Add(uniqueId); + } + } + + return pendingOperationUniqueIds; + } + + private static void ApplyPendingOperationBusyStates(IEnumerable viewModels, HashSet pendingOperationUniqueIds) + { + if (viewModels == null || pendingOperationUniqueIds == null || pendingOperationUniqueIds.Count == 0) + return; + + foreach (var viewModel in viewModels) + { + viewModel.IsBusy = pendingOperationUniqueIds.Contains(viewModel.MailCopy.UniqueId); } } @@ -819,6 +1093,36 @@ public partial class MailListPageViewModel : MailBaseViewModel, await InitializeFolderAsync(); } + private async Task> PerformSynchronizerOnlineSearchAsync(string queryText, + IEnumerable handlingFolders, + CancellationToken cancellationToken) + { + if (handlingFolders == null) return []; + + var foldersByAccount = handlingFolders + .GroupBy(a => a.MailAccountId) + .ToList(); + + if (foldersByAccount.Count == 0) return []; + + var searchTasks = foldersByAccount.Select(async groupedFolders => + { + var synchronizer = await SynchronizationManager.Instance.GetSynchronizerAsync(groupedFolders.Key).ConfigureAwait(false); + if (synchronizer == null) return new List(); + + var accountResults = await synchronizer.OnlineSearchAsync(queryText, groupedFolders.ToList(), cancellationToken).ConfigureAwait(false); + return accountResults ?? new List(); + }); + + var allResults = await Task.WhenAll(searchTasks).ConfigureAwait(false); + + return allResults + .SelectMany(a => a) + .GroupBy(a => a.UniqueId) + .Select(a => a.First()) + .ToList(); + } + private async Task InitializeFolderAsync() { if (SelectedFilterOption == null || SelectedFolderPivot == null || SelectedSortingOption == null) @@ -826,10 +1130,7 @@ public partial class MailListPageViewModel : MailBaseViewModel, try { - MailCollection.Clear(); - MailCollection.MailCopyIdHashSet.Clear(); - - SelectedItems.Clear(); + await MailCollection.ClearAsync(); if (ActiveFolder == null) return; @@ -850,17 +1151,9 @@ public partial class MailListPageViewModel : MailBaseViewModel, await listManipulationSemepahore.WaitAsync(cancellationToken); - // Setup MailCollection configuration. - - // Don't pass any threading strategy if disabled in settings. - MailCollection.ThreadingStrategyProvider = PreferencesService.IsThreadingEnabled ? _threadingStrategyProvider : null; - - // TODO: This should go inside - MailCollection.PruneSingleNonDraftItems = ActiveFolder.SpecialFolderType == SpecialFolderType.Draft; - // Here items are sorted and filtered. - List items = null; + List items = null; List onlineSearchItems = null; bool isDoingSearch = !string.IsNullOrEmpty(SearchQuery); @@ -873,27 +1166,10 @@ public partial class MailListPageViewModel : MailBaseViewModel, // Perform online search. if (isDoingOnlineSearch) { - WinoServerResponse onlineSearchResult = null; - string onlineSearchFailedMessage = null; - try { - var accountIds = ActiveFolder.HandlingFolders.Select(a => a.MailAccountId).ToList(); - var folders = ActiveFolder.HandlingFolders.ToList(); - var searchRequest = new OnlineSearchRequested(accountIds, SearchQuery, folders); - - onlineSearchResult = await _winoServerConnectionManager.GetResponseAsync(searchRequest, cancellationToken); - - if (onlineSearchResult.IsSuccess) - { - await ExecuteUIThread(() => { AreSearchResultsOnline = true; }); - - onlineSearchItems = onlineSearchResult.Data.SearchResult; - } - else - { - onlineSearchFailedMessage = onlineSearchResult.Message; - } + onlineSearchItems = await PerformSynchronizerOnlineSearchAsync(SearchQuery, ActiveFolder.HandlingFolders, cancellationToken).ConfigureAwait(false); + await ExecuteUIThread(() => { AreSearchResultsOnline = true; }); } catch (OperationCanceledException) { @@ -902,21 +1178,18 @@ public partial class MailListPageViewModel : MailBaseViewModel, catch (Exception ex) { Log.Warning(ex, "Failed to perform online search."); - onlineSearchFailedMessage = ex.Message; - } - if (onlineSearchResult != null && !onlineSearchResult.IsSuccess) - { - // Query or server error. - var serverErrorMessage = string.Format(Translator.OnlineSearchFailed_Message, onlineSearchResult.Message); - _mailDialogService.InfoBarMessage(Translator.GeneralTitle_Error, serverErrorMessage, InfoBarMessageType.Warning); + isDoingOnlineSearch = false; + onlineSearchItems = null; - } - else if (!string.IsNullOrEmpty(onlineSearchFailedMessage)) - { - // Fatal error. - var serverErrorMessage = string.Format(Translator.OnlineSearchFailed_Message, onlineSearchFailedMessage); - _mailDialogService.InfoBarMessage(Translator.GeneralTitle_Error, serverErrorMessage, InfoBarMessageType.Warning); + await ExecuteUIThread(() => + { + IsOnlineSearchEnabled = false; + AreSearchResultsOnline = false; + + var serverErrorMessage = string.Format(Translator.OnlineSearchFailed_Message, ex.Message); + _mailDialogService.InfoBarMessage(Translator.GeneralTitle_Error, serverErrorMessage, InfoBarMessageType.Warning); + }); } } } @@ -926,7 +1199,7 @@ public partial class MailListPageViewModel : MailBaseViewModel, SelectedSortingOption.Type, PreferencesService.IsThreadingEnabled, SelectedFolderPivot.IsFocused, - SearchQuery, + isDoingOnlineSearch ? string.Empty : SearchQuery, MailCollection.MailCopyIdHashSet, onlineSearchItems); @@ -938,11 +1211,16 @@ public partial class MailListPageViewModel : MailBaseViewModel, // We don't need to insert them one by one. // Just create VMs and do bulk insert. - var viewModels = PrepareMailViewModels(items); + var viewModels = await PrepareMailViewModelsAsync(items, cancellationToken).ConfigureAwait(false); + var pendingOperationUniqueIds = await GetPendingOperationUniqueIdsForActiveFolderAccountsAsync(cancellationToken).ConfigureAwait(false); + ApplyPendingOperationBusyStates(viewModels, pendingOperationUniqueIds); + + await MailCollection.AddRangeAsync(viewModels, clearIdCache: true); await ExecuteUIThread(() => { - MailCollection.AddRange(viewModels, true); + HasNoOnlineSearchResult = isDoingOnlineSearch && items.Count == 0; + OnPropertyChanged(nameof(HasNoOnlineSearchResult)); if (isDoingSearch && !isDoingOnlineSearch) { @@ -974,22 +1252,15 @@ public partial class MailListPageViewModel : MailBaseViewModel, OnPropertyChanged(nameof(CanSynchronize)); NotifyItemFoundState(); + + // Clear the loading message after completion + IsBarOpen = false; }); } } #region Receivers - void IRecipient.Receive(MailItemSelectedEvent message) - { - if (!SelectedItems.Contains(message.SelectedMailItem)) SelectedItems.Add(message.SelectedMailItem); - } - - void IRecipient.Receive(MailItemSelectionRemovedEvent message) - { - if (SelectedItems.Contains(message.RemovedMailItem)) SelectedItems.Remove(message.RemovedMailItem); - } - async void IRecipient.Receive(ActiveMailFolderChangedEvent message) { NotifyItemSelected(); @@ -1008,6 +1279,8 @@ public partial class MailListPageViewModel : MailBaseViewModel, IsInSearchMode = false; IsOnlineSearchButtonVisible = false; AreSearchResultsOnline = false; + HasNoOnlineSearchResult = false; + OnPropertyChanged(nameof(HasNoOnlineSearchResult)); // Prepare Focused - Other or folder name tabs. await UpdateFolderPivotsAsync(); @@ -1041,6 +1314,7 @@ public partial class MailListPageViewModel : MailBaseViewModel, SearchQuery = string.Empty; IsInSearchMode = false; IsOnlineSearchEnabled = false; + HasNoOnlineSearchResult = false; } } @@ -1070,6 +1344,9 @@ public partial class MailListPageViewModel : MailBaseViewModel, case SynchronizationCompletedState.Success: UpdateBarMessage(InfoBarMessageType.Success, ActiveFolder.FolderName, Translator.SynchronizationFolderReport_Success); break; + case SynchronizationCompletedState.PartiallyCompleted: + UpdateBarMessage(InfoBarMessageType.Warning, ActiveFolder.FolderName, Translator.SynchronizationFolderReport_Failed); + break; case SynchronizationCompletedState.Failed: UpdateBarMessage(InfoBarMessageType.Error, ActiveFolder.FolderName, Translator.SynchronizationFolderReport_Failed); break; @@ -1080,30 +1357,9 @@ public partial class MailListPageViewModel : MailBaseViewModel, void IRecipient.Receive(MailItemNavigationRequested message) { - Debug.WriteLine($"Mail item navigation requested"); - // Find mail item and add to selected items. + // TODO: Remove this. - MailItemViewModel navigatingMailItem = null; - ThreadMailItemViewModel threadMailItemViewModel = null; - - for (int i = 0; i < 3; i++) - { - var mailContainer = MailCollection.GetMailItemContainer(message.UniqueMailId); - - if (mailContainer != null) - { - navigatingMailItem = mailContainer.ItemViewModel; - threadMailItemViewModel = mailContainer.ThreadViewModel; - - break; - } - } - - if (threadMailItemViewModel != null) - threadMailItemViewModel.IsThreadExpanded = true; - - if (navigatingMailItem != null) - WeakReferenceMessenger.Default.Send(new SelectMailItemContainerEvent(navigatingMailItem, message.ScrollToItem)); + WeakReferenceMessenger.Default.Send(new SelectMailItemContainerEvent(message.UniqueMailId, message.ScrollToItem)); } #endregion @@ -1142,9 +1398,7 @@ public partial class MailListPageViewModel : MailBaseViewModel, foreach (var accountId in accountIds) { - var serverResponse = await _winoServerConnectionManager.GetResponseAsync(new SynchronizationExistenceCheckRequest(accountId)); - - if (serverResponse.IsSuccess && serverResponse.Data == true) + if (SynchronizationManager.Instance.IsAccountSynchronizing(accountId)) { isAnyAccountSynchronizing = true; break; @@ -1155,7 +1409,7 @@ public partial class MailListPageViewModel : MailBaseViewModel, await ExecuteUIThread(() => { IsAccountSynchronizerInSynchronization = isAnyAccountSynchronizing; }); } - public void Receive(AccountCacheResetMessage message) + public async void Receive(AccountCacheResetMessage message) { if (message.Reason == AccountCacheResetReason.ExpiredCache && ActiveFolder.HandlingFolders.Any(a => a.MailAccountId == message.AccountId)) @@ -1164,14 +1418,97 @@ public partial class MailListPageViewModel : MailBaseViewModel, if (handlingFolder == null) return; - _ = ExecuteUIThread(() => - { - MailCollection.Clear(); + // ClearAsync already handles UI threading internally + await MailCollection.ClearAsync(); + await ExecuteUIThread(() => + { _mailDialogService.InfoBarMessage(Translator.AccountCacheReset_Title, Translator.AccountCacheReset_Message, InfoBarMessageType.Warning); }); } } - public void Receive(ThumbnailAdded message) => MailCollection.UpdateThumbnails(message.Email); + protected override void OnDispatcherAssigned() + { + base.OnDispatcherAssigned(); + + MailCollection.CoreDispatcher = Dispatcher; + } + + public void Receive(ThumbnailAdded message) + { + _ = MailCollection.UpdateThumbnailsForAddressAsync(message.Email); + } + + protected override void RegisterRecipients() + { + base.RegisterRecipients(); + + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register(this); + Messenger.Register>(this); + } + + protected override void UnregisterRecipients() + { + base.UnregisterRecipients(); + + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister>(this); + } + + public void Receive(PropertyChangedMessage message) + { + // Handle IsSelected property changes from MailItemViewModel + if (message.PropertyName == nameof(MailItemViewModel.IsSelected) && message.Sender is MailItemViewModel mailItemViewModel) + { + Messenger.Send(new SelectedItemsChangedMessage()); + } + else if (message.Sender is ThreadMailItemViewModel threadMailItemViewModel) + { + if (message.PropertyName == nameof(ThreadMailItemViewModel.IsSelected)) + { + // Thread selected. + } + else if (message.PropertyName == nameof(ThreadMailItemViewModel.IsThreadExpanded)) + { + // Thread expanded. + } + } + } + + public async void Receive(SwipeActionRequested message) + { + if (message.MailItem == null) return; + + // Get mail copies based on the mail item type + IEnumerable mailCopies; + + if (message.MailItem is MailItemViewModel singleItem) + { + mailCopies = new[] { singleItem.MailCopy }; + } + else if (message.MailItem is ThreadMailItemViewModel threadItem) + { + mailCopies = threadItem.ThreadEmails.Select(e => e.MailCopy); + } + else + { + return; // Unknown mail item type + } + + var package = new MailOperationPreperationRequest(message.Operation, mailCopies); + await ExecuteMailOperationAsync(package); + } } diff --git a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs index 5fccd83a..e89d12c0 100644 --- a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs +++ b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs @@ -11,27 +11,30 @@ using CommunityToolkit.Mvvm.Messaging; using MailKit; using MimeKit; +using MimeKit.Cryptography; using Serilog; using Wino.Core.Domain; using Wino.Core.Domain.Entities.Mail; 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.MailItem; using Wino.Core.Domain.Models.Menus; using Wino.Core.Domain.Models.Navigation; +using Wino.Core.Domain.Models.Printing; using Wino.Core.Domain.Models.Reader; +using Wino.Core.Services; using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Messages; using Wino.Messaging.Client.Mails; -using Wino.Messaging.Server; using Wino.Messaging.UI; using IMailService = Wino.Core.Domain.Interfaces.IMailService; namespace Wino.Mail.ViewModels; public partial class MailRenderingPageViewModel : MailBaseViewModel, - IRecipient, + IRecipient, IRecipient, ITransferProgress // For listening IMAP message download progress. { @@ -40,13 +43,13 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, private readonly IMimeFileService _mimeFileService; private readonly Core.Domain.Interfaces.IMailService _mailService; + private readonly IFolderService _folderService; private readonly IFileService _fileService; private readonly IWinoRequestDelegator _requestDelegator; private readonly IContactService _contactService; private readonly IClipboardService _clipboardService; private readonly IUnsubscriptionService _unsubscriptionService; private readonly IApplicationConfiguration _applicationConfiguration; - private readonly IWinoServerConnectionManager _winoServerConnectionManager; private bool forceImageLoading = false; private MailItemViewModel initializedMailItemViewModel = null; @@ -56,11 +59,17 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, // Used in 'Save as' and 'Print' functionality. public Func> SaveHTMLasPDFFunc { get; set; } + public Func> DirectPrintFuncAsync { get; set; } + #region Properties public bool ShouldDisplayDownloadProgress => IsIndetermineProgress || (CurrentDownloadPercentage > 0 && CurrentDownloadPercentage <= 100); public bool CanUnsubscribe => CurrentRenderModel?.UnsubscribeInfo?.CanUnsubscribe ?? false; - public bool IsJunkMail => initializedMailItemViewModel?.AssignedFolder != null && initializedMailItemViewModel.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk; + public bool IsSmimeSigned => (CurrentRenderModel?.Signatures?.Count ?? 0) > 0; + public bool IsSmimeEncrypted => CurrentRenderModel?.IsSmimeEncrypted ?? false; + public bool IsJunkMail => initializedMailItemViewModel?.MailCopy.AssignedFolder != null && initializedMailItemViewModel.MailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk; + public bool SmimeSignaturesValid => CurrentRenderModel?.Signatures?.Any(x => x.Value) ?? false; + public bool SmimeSignaturesInvalid => !SmimeSignaturesValid; public bool IsImageRenderingDisabled { @@ -100,6 +109,10 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, [ObservableProperty] [NotifyPropertyChangedFor(nameof(CanUnsubscribe))] + [NotifyPropertyChangedFor(nameof(IsSmimeSigned))] + [NotifyPropertyChangedFor(nameof(IsSmimeEncrypted))] + [NotifyPropertyChangedFor(nameof(SmimeSignaturesValid))] + [NotifyPropertyChangedFor(nameof(SmimeSignaturesInvalid))] public partial MailRenderModel CurrentRenderModel { get; set; } [ObservableProperty] @@ -112,7 +125,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, public partial string FromName { get; set; } [ObservableProperty] - public partial string ContactPicture { get; set; } + public partial IMailItemDisplayInformation CurrentMailItemDisplayInformation { get; set; } [ObservableProperty] public partial DateTime CreationDate { get; set; } @@ -120,7 +133,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, public ObservableCollection CcItems { get; set; } = []; public ObservableCollection BccItems { get; set; } = []; public ObservableCollection Attachments { get; set; } = []; - public ObservableCollection MenuItems { get; set; } = []; + public ObservableCollection MenuItems { get; set; } = []; #endregion @@ -128,22 +141,24 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, public IStatePersistanceService StatePersistenceService { get; } public IPreferencesService PreferencesService { get; } public IPrintService PrintService { get; } + public Guid? CurrentMailAccountId => initializedMailItemViewModel?.MailCopy.AssignedAccount?.Id; + public Guid? CurrentMailFileId => initializedMailItemViewModel?.MailCopy.FileId; public MailRenderingPageViewModel(IMailDialogService dialogService, - INativeAppService nativeAppService, - IUnderlyingThemeService underlyingThemeService, - IMimeFileService mimeFileService, - IMailService mailService, - IFileService fileService, - IWinoRequestDelegator requestDelegator, - IStatePersistanceService statePersistenceService, - IContactService contactService, - IClipboardService clipboardService, - IUnsubscriptionService unsubscriptionService, - IPreferencesService preferencesService, - IPrintService printService, - IApplicationConfiguration applicationConfiguration, - IWinoServerConnectionManager winoServerConnectionManager) + INativeAppService nativeAppService, + IUnderlyingThemeService underlyingThemeService, + IMimeFileService mimeFileService, + IMailService mailService, + IFolderService folderService, + IFileService fileService, + IWinoRequestDelegator requestDelegator, + IStatePersistanceService statePersistenceService, + IContactService contactService, + IClipboardService clipboardService, + IUnsubscriptionService unsubscriptionService, + IPreferencesService preferencesService, + IPrintService printService, + IApplicationConfiguration applicationConfiguration) { _dialogService = dialogService; NativeAppService = nativeAppService; @@ -152,12 +167,12 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, PreferencesService = preferencesService; PrintService = printService; _applicationConfiguration = applicationConfiguration; - _winoServerConnectionManager = winoServerConnectionManager; _clipboardService = clipboardService; _unsubscriptionService = unsubscriptionService; _underlyingThemeService = underlyingThemeService; _mimeFileService = mimeFileService; _mailService = mailService; + _folderService = folderService; _fileService = fileService; _requestDelegator = requestDelegator; } @@ -240,63 +255,98 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, } [RelayCommand] - private async Task OperationClicked(MailOperationMenuItem menuItem) + private async Task OperationClicked(IMenuOperation menuItem) { - if (menuItem == null) return; + if (menuItem is not MailOperationMenuItem mailOperationMenuItem) return; - await HandleMailOperationAsync(menuItem.Operation); + await HandleMailOperationAsync(mailOperationMenuItem.Operation); } private async Task HandleMailOperationAsync(MailOperation operation) { - // Toggle theme - if (operation == MailOperation.DarkEditor || operation == MailOperation.LightEditor) - IsDarkWebviewRenderer = !IsDarkWebviewRenderer; - else if (operation == MailOperation.SaveAs) + try { - await SaveAsAsync(); - } - else if (operation == MailOperation.Print) - { - await PrintAsync(); - } - else if (operation == MailOperation.ViewMessageSource) - { - await _dialogService.ShowMessageSourceDialogAsync(initializedMimeMessageInformation.MimeMessage.ToString()); - } - else if (operation == MailOperation.Reply || operation == MailOperation.ReplyAll || operation == MailOperation.Forward) - { - if (initializedMailItemViewModel == null) return; - - // Create new draft. - var draftOptions = new DraftCreationOptions() + if (operation == MailOperation.SaveAs) { - Reason = operation switch + await SaveAsAsync(); + } + else if (operation == MailOperation.Print) + { + var settings = await _dialogService.ShowPrintDialogAsync(); + + if (settings == null) return; + + var printingResult = await DirectPrintFuncAsync.Invoke(settings); + + // TODO: More detailed printing result handling. + if (printingResult == PrintingResult.Submitted) { - MailOperation.Reply => DraftCreationReason.Reply, - MailOperation.ReplyAll => DraftCreationReason.ReplyAll, - MailOperation.Forward => DraftCreationReason.Forward, - _ => DraftCreationReason.Empty - }, - ReferencedMessage = new ReferencedMessage() - { - MimeMessage = initializedMimeMessageInformation.MimeMessage, - MailCopy = initializedMailItemViewModel.MailCopy + _dialogService.InfoBarMessage(Translator.DialogMessage_PrintingSuccessTitle, Translator.DialogMessage_PrintingSuccessMessage, InfoBarMessageType.Success); + } + else if (printingResult == PrintingResult.Failed) + { + _dialogService.InfoBarMessage(Translator.DialogMessage_PrintingFailedTitle, Translator.DialogMessage_PrintingFailedMessage, InfoBarMessageType.Error); } - }; - var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(initializedMailItemViewModel.AssignedAccount.Id, draftOptions).ConfigureAwait(false); + } + else if (operation == MailOperation.ViewMessageSource) + { + await _dialogService.ShowMessageSourceDialogAsync(initializedMimeMessageInformation.MimeMessage.ToString()); + } + else if (operation == MailOperation.Reply || operation == MailOperation.ReplyAll || operation == MailOperation.Forward) + { + if (initializedMailItemViewModel == null) return; - var draftPreparationRequest = new DraftPreparationRequest(initializedMailItemViewModel.AssignedAccount, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason, initializedMailItemViewModel.MailCopy); + // Create new draft. + var draftOptions = new DraftCreationOptions() + { + Reason = operation switch + { + MailOperation.Reply => DraftCreationReason.Reply, + MailOperation.ReplyAll => DraftCreationReason.ReplyAll, + MailOperation.Forward => DraftCreationReason.Forward, + _ => DraftCreationReason.Empty + }, + ReferencedMessage = new ReferencedMessage() + { + MimeMessage = initializedMimeMessageInformation.MimeMessage, + MailCopy = initializedMailItemViewModel.MailCopy + } + }; - await _requestDelegator.ExecuteAsync(draftPreparationRequest); + var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(initializedMailItemViewModel.MailCopy.AssignedAccount.Id, draftOptions).ConfigureAwait(false); + var draftPreparationRequest = new DraftPreparationRequest(initializedMailItemViewModel.MailCopy.AssignedAccount, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason, initializedMailItemViewModel.MailCopy); + + await _requestDelegator.ExecuteAsync(draftPreparationRequest); + + } + else if (initializedMailItemViewModel != null) + { + // All other operations require a mail item. + var prepRequest = new MailOperationPreperationRequest(operation, initializedMailItemViewModel.MailCopy); + await _requestDelegator.ExecuteAsync(prepRequest); + } } - else if (initializedMailItemViewModel != null) + catch (UnavailableSpecialFolderException unavailableSpecialFolderException) { - // All other operations require a mail item. - var prepRequest = new MailOperationPreperationRequest(operation, initializedMailItemViewModel.MailCopy); - await _requestDelegator.ExecuteAsync(prepRequest); + _dialogService.InfoBarMessage(Translator.Info_MissingFolderTitle, + string.Format(Translator.Info_MissingFolderMessage, unavailableSpecialFolderException.SpecialFolderType), + InfoBarMessageType.Warning, + Translator.SettingConfigureSpecialFolders_Button, + () => + { + _dialogService.HandleSystemFolderConfigurationDialogAsync(unavailableSpecialFolderException.AccountId, _folderService); + }); + } + catch (NotImplementedException) + { + _dialogService.ShowNotSupportedMessage(); + } + catch (Exception ex) + { + Log.Error(ex, "Mail operation execution failed. Operation: {Operation}", operation); + _dialogService.InfoBarMessage(Translator.Info_RequestCreationFailedTitle, ex.Message, InfoBarMessageType.Error); } } @@ -310,6 +360,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, initializedMailItemViewModel = null; initializedMimeMessageInformation = null; + CurrentMailItemDisplayInformation = null; // Dispose existing content first. Messenger.Send(new CancelRenderingContentRequested()); @@ -355,8 +406,10 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, // To show the progress on the UI. CurrentDownloadPercentage = 1; - var package = new DownloadMissingMessageRequested(mailItemViewModel.AssignedAccount.Id, mailItemViewModel.MailCopy); - await _winoServerConnectionManager.GetResponseAsync(package); + // Download missing MIME message using SynchronizationManager + await SynchronizationManager.Instance.DownloadMimeMessageAsync( + mailItemViewModel.MailCopy, + mailItemViewModel.MailCopy.AssignedAccount.Id); } catch (OperationCanceledException) { @@ -375,7 +428,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, private async Task RenderAsync(MailItemViewModel mailItemViewModel, CancellationToken cancellationToken = default) { ResetProgress(); - var isMimeExists = await _mimeFileService.IsMimeExistAsync(mailItemViewModel.AssignedAccount.Id, mailItemViewModel.MailCopy.FileId); + var isMimeExists = await _mimeFileService.IsMimeExistAsync(mailItemViewModel.MailCopy.AssignedAccount.Id, mailItemViewModel.MailCopy.FileId); if (!isMimeExists) { @@ -384,7 +437,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, // Find the MIME for this item and render it. var mimeMessageInformation = await _mimeFileService.GetMimeMessageInformationAsync(mailItemViewModel.MailCopy.FileId, - mailItemViewModel.AssignedAccount.Id, + mailItemViewModel.MailCopy.AssignedAccount.Id, cancellationToken).ConfigureAwait(false); if (mimeMessageInformation == null) @@ -394,6 +447,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, } initializedMailItemViewModel = mailItemViewModel; + await ExecuteUIThread(() => { CurrentMailItemDisplayInformation = mailItemViewModel; }); await RenderAsync(mimeMessageInformation); } @@ -435,13 +489,14 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, // TODO: FromName and FromAddress is probably not correct here for mail lists. FromAddress = message.From.Mailboxes.FirstOrDefault()?.Address ?? Translator.UnknownAddress; FromName = message.From.Mailboxes.FirstOrDefault()?.Name ?? Translator.UnknownSender; - CreationDate = message.Date.DateTime; - ContactPicture = initializedMailItemViewModel?.SenderContact?.Base64ContactPicture; + + // Use the received date from MailCopy if available, otherwise fall back to the sent date from MIME message + CreationDate = initializedMailItemViewModel?.MailCopy.CreationDate ?? message.Date.DateTime; // Automatically disable images for Junk folder to prevent pixel tracking. // This can only work for selected mail item rendering, not for EML file rendering. if (initializedMailItemViewModel != null && - initializedMailItemViewModel.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk) + initializedMailItemViewModel.MailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk) { renderingOptions.LoadImages = false; } @@ -480,7 +535,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, var contactViewModel = new AccountContactViewModel(foundContact); // Make sure that user account first in the list. - if (string.Equals(contactViewModel.Address, initializedMailItemViewModel?.AssignedAccount?.Address, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(contactViewModel.Address, initializedMailItemViewModel?.MailCopy.AssignedAccount?.Address, StringComparison.OrdinalIgnoreCase)) { contactViewModel.IsMe = true; accounts.Insert(0, contactViewModel); @@ -506,11 +561,15 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, { base.OnNavigatedFrom(mode, parameters); - renderCancellationTokenSource.Cancel(); + renderCancellationTokenSource?.Cancel(); + renderCancellationTokenSource?.Dispose(); + renderCancellationTokenSource = null; + CurrentDownloadPercentage = 0d; initializedMailItemViewModel = null; initializedMimeMessageInformation = null; + CurrentMailItemDisplayInformation = null; forceImageLoading = false; @@ -527,12 +586,6 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, { MenuItems.Clear(); - // Add light/dark editor theme switch. - if (IsDarkWebviewRenderer) - MenuItems.Add(MailOperationMenuItem.Create(MailOperation.LightEditor)); - else - MenuItems.Add(MailOperationMenuItem.Create(MailOperation.DarkEditor)); - // Save As PDF MenuItems.Add(MailOperationMenuItem.Create(MailOperation.SaveAs, true, true)); @@ -563,7 +616,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, } // Archive - Unarchive - if (initializedMailItemViewModel.AssignedFolder.SpecialFolderType == SpecialFolderType.Archive) + if (initializedMailItemViewModel.MailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Archive) MenuItems.Add(MailOperationMenuItem.Create(MailOperation.UnArchive)); else MenuItems.Add(MailOperationMenuItem.Create(MailOperation.Archive)); @@ -586,15 +639,15 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, MenuItems.Add(MailOperationMenuItem.Create(MailOperation.MarkAsRead, true, false)); } - protected override async void OnMailUpdated(MailCopy updatedMail) + protected override async void OnMailUpdated(MailCopy updatedMail, MailUpdateSource source, MailCopyChangeFlags changedProperties) { - base.OnMailUpdated(updatedMail); + base.OnMailUpdated(updatedMail, source, changedProperties); if (initializedMailItemViewModel == null) return; // Check if the updated mail is the same mail item we are rendering. // This is done with UniqueId to include FolderId into calculations. - if (initializedMailItemViewModel.UniqueId != updatedMail.UniqueId) return; + if (initializedMailItemViewModel.MailCopy.UniqueId != updatedMail.UniqueId) return; await ExecuteUIThread(() => { InitializeCommandBarItems(); }); } @@ -690,40 +743,6 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, } } - private async Task PrintAsync() - { - // Printing: - // 1. Let WebView2 save the current HTML as PDF to temporary location. - // 2. Saving as PDF will divide pages correctly for Win2D CanvasBitmap. - // 3. Use Win2D CanvasBitmap as IPrintDocumentSource and WinRT APIs to print the PDF. - - try - { - var printFilePath = Path.Combine(_applicationConfiguration.ApplicationTempFolderPath, "print.pdf"); - - if (File.Exists(printFilePath)) File.Delete(printFilePath); - - await SaveHTMLasPDFFunc(printFilePath); - - var result = await PrintService.PrintPdfFileAsync(printFilePath, Subject); - - if (result == PrintingResult.Submitted) - { - _dialogService.InfoBarMessage(Translator.DialogMessage_PrintingSuccessTitle, Translator.DialogMessage_PrintingSuccessMessage, InfoBarMessageType.Success); - } - else if (result != PrintingResult.Canceled) - { - var message = string.Format(Translator.DialogMessage_PrintingFailedMessage, result); - _dialogService.InfoBarMessage(Translator.DialogMessage_PrintingFailedTitle, message, InfoBarMessageType.Warning); - } - } - catch (Exception ex) - { - Log.Error(ex, "Failed to print mail."); - _dialogService.InfoBarMessage(string.Empty, ex.Message, InfoBarMessageType.Error); - } - } - private async Task SaveAsAsync() { try @@ -739,8 +758,8 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, if (isSaved) { _dialogService.InfoBarMessage(Translator.Info_PDFSaveSuccessTitle, - string.Format(Translator.Info_PDFSaveSuccessMessage, pdfFilePath), - InfoBarMessageType.Success); + string.Format(Translator.Info_PDFSaveSuccessMessage, pdfFilePath), + InfoBarMessageType.Success); } } catch (Exception ex) @@ -782,8 +801,10 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, // For upload. void ITransferProgress.Report(long bytesTransferred) { } - public async void Receive(NewMailItemRenderingRequestedEvent message) + public async void Receive(ReaderItemRefreshRequestedEvent message) { + if (message.MailItemViewModel == null || message.MailItemViewModel.IsDraft) return; + try { await RenderAsync(message.MailItemViewModel, renderCancellationTokenSource.Token); @@ -822,4 +843,86 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, } }); } + + [RelayCommand] + private async Task ShowSmimeSigningCertificateInfoAsync() + { + if (IsSmimeSigned) + { + MimePart signaturePart; + if (initializedMimeMessageInformation?.MimeMessage?.Body is MultipartSigned signed && signed[1] is MimePart signaturePart1) + { + signaturePart = signaturePart1; + } + else if (initializedMimeMessageInformation?.MimeMessage?.Body is ApplicationPkcs7Mime pkcs7) + { + signaturePart = null; + } + else + { + //_dialogService.InfoBarMessage(Translator.Info_SmimeSignatureNotFoundTitle, Translator.Info_SmimeSignatureNotFoundMessage, InfoBarMessageType.Error); + return; + } + + string info = $"{Translator.SmimeSignaturesInMessage}:\n"; + foreach (var (signature, valid) in CurrentRenderModel.Signatures) + { + info += string.Format(Translator.SmimeSignatureEntry, valid ? "✅" : "❌", signature.SignerCertificate.Name, signature.SignerCertificate.Fingerprint, signature.SignerCertificate.CreationDate, signature.SignerCertificate.ExpirationDate); + } + await ShowSmimeCertificateInfoAsync(signaturePart, info, Translator.SmimeSigningCertificateInfoTitle); + } + + } + + private async Task ShowSmimeCertificateInfoAsync(MimePart certificateAttachment, string additionalInfo = "", string title = null) + { + { + if (certificateAttachment == null) + { + await _dialogService.ShowConfirmationDialogAsync( + $"{additionalInfo}\n{Translator.SmimeNoCertificateFileFound}", title ?? Translator.SmimeCertificateInfoTitle, Translator.Buttons_OK); + return; + } + var fileName = certificateAttachment.FileName ?? "smime.p7s"; + var contentType = certificateAttachment.ContentType?.MimeType ?? "application/pkcs7-signature"; + var size = certificateAttachment.Content?.Stream?.Length ?? 0; + var info = string.Format(Translator.SmimeCertificateFileInfo, fileName, contentType, size); + + var result = await _dialogService.ShowConfirmationDialogAsync( + $"{additionalInfo}\n{info}", title ?? Translator.SmimeCertificateInfoTitle, + Translator.SmimeSaveCertificate); + if (result) + { + var pickedPath = await _dialogService.PickFilePathAsync(fileName); + if (!string.IsNullOrEmpty(pickedPath)) + { + var pickedDirectory = Path.GetDirectoryName(pickedPath); + var pickedFileName = Path.GetFileName(pickedPath); + await using (var stream = await _fileService.GetFileStreamAsync(pickedDirectory, pickedFileName)) + { + await certificateAttachment.Content!.DecodeToAsync(stream); + } + + _dialogService.InfoBarMessage(Translator.SmimeCertificate, string.Format(Translator.SmimeCertificateSavedTo, pickedPath), + InfoBarMessageType.Success); + } + } + } + } + + protected override void RegisterRecipients() + { + base.RegisterRecipients(); + + Messenger.Register(this); + Messenger.Register(this); + } + + protected override void UnregisterRecipients() + { + base.UnregisterRecipients(); + + Messenger.Unregister(this); + Messenger.Unregister(this); + } } diff --git a/Wino.Mail.ViewModels/MailViewModelsContainerSetup.cs b/Wino.Mail.ViewModels/MailViewModelsContainerSetup.cs deleted file mode 100644 index d1526993..00000000 --- a/Wino.Mail.ViewModels/MailViewModelsContainerSetup.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Wino.Core; - -namespace Wino.Mail.ViewModels; - -public static class MailViewModelsContainerSetup -{ - public static void RegisterViewModelService(this IServiceCollection services) - { - // View models use core services. - services.RegisterCoreServices(); - } -} diff --git a/Wino.Mail.ViewModels/MessageListPageViewModel.cs b/Wino.Mail.ViewModels/MessageListPageViewModel.cs index b1bf22bb..a08cf320 100644 --- a/Wino.Mail.ViewModels/MessageListPageViewModel.cs +++ b/Wino.Mail.ViewModels/MessageListPageViewModel.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Threading.Tasks; using CommunityToolkit.Mvvm.Input; using Wino.Core.Domain; +using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; @@ -12,22 +14,8 @@ public partial class MessageListPageViewModel : MailBaseViewModel { public IPreferencesService PreferencesService { get; } private readonly IThumbnailService _thumbnailService; - - private int selectedMarkAsOptionIndex; - public int SelectedMarkAsOptionIndex - { - get => selectedMarkAsOptionIndex; - set - { - if (SetProperty(ref selectedMarkAsOptionIndex, value)) - { - if (value >= 0) - { - PreferencesService.MarkAsPreference = (MailMarkAsOption)Enum.GetValues().GetValue(value); - } - } - } - } + private readonly IStatePersistanceService _statePersistenceService; + private readonly IDialogServiceBase _dialogService; private readonly List availableHoverActions = [ @@ -38,6 +26,13 @@ public partial class MessageListPageViewModel : MailBaseViewModel MailOperation.MoveToJunk ]; + private readonly List availableMailSpacingOptions = + [ + MailListDisplayMode.Compact, + MailListDisplayMode.Medium, + MailListDisplayMode.Spacious + ]; + public List AvailableHoverActionsTranslations { get; set; } = [ Translator.HoverActionOption_Archive, @@ -47,6 +42,37 @@ public partial class MessageListPageViewModel : MailBaseViewModel Translator.HoverActionOption_MoveJunk ]; + public IMailItemDisplayInformation DemoPreviewMailItemInformation { get; } = new DemoMailItemDisplayInformation(); + + public MailListDisplayMode SelectedMailSpacingMode => availableMailSpacingOptions[selectedMailSpacingIndex]; + + private int selectedMarkAsOptionIndex; + public int SelectedMarkAsOptionIndex + { + get => selectedMarkAsOptionIndex; + set + { + if (SetProperty(ref selectedMarkAsOptionIndex, value) && value >= 0) + { + PreferencesService.MarkAsPreference = (MailMarkAsOption)Enum.GetValues().GetValue(value); + } + } + } + + private int selectedMailSpacingIndex; + public int SelectedMailSpacingIndex + { + get => selectedMailSpacingIndex; + set + { + if (SetProperty(ref selectedMailSpacingIndex, value) && value >= 0 && value < availableMailSpacingOptions.Count) + { + PreferencesService.MailItemDisplayMode = availableMailSpacingOptions[value]; + OnPropertyChanged(nameof(SelectedMailSpacingMode)); + } + } + } + #region Properties private int leftHoverActionIndex; public int LeftHoverActionIndex @@ -88,13 +114,19 @@ public partial class MessageListPageViewModel : MailBaseViewModel } #endregion - public MessageListPageViewModel(IPreferencesService preferencesService, IThumbnailService thumbnailService) + public MessageListPageViewModel(IPreferencesService preferencesService, + IThumbnailService thumbnailService, + IStatePersistanceService statePersistenceService, + IDialogServiceBase dialogService) { PreferencesService = preferencesService; _thumbnailService = thumbnailService; + _statePersistenceService = statePersistenceService; + _dialogService = dialogService; leftHoverActionIndex = availableHoverActions.IndexOf(PreferencesService.LeftHoverAction); centerHoverActionIndex = availableHoverActions.IndexOf(PreferencesService.CenterHoverAction); rightHoverActionIndex = availableHoverActions.IndexOf(PreferencesService.RightHoverAction); + selectedMailSpacingIndex = availableMailSpacingOptions.IndexOf(PreferencesService.MailItemDisplayMode); SelectedMarkAsOptionIndex = Array.IndexOf(Enum.GetValues(), PreferencesService.MarkAsPreference); } @@ -103,4 +135,39 @@ public partial class MessageListPageViewModel : MailBaseViewModel { await _thumbnailService.ClearCache(); } + + [RelayCommand] + private void ResetMailListPaneLength() + { + _statePersistenceService.MailListPaneLength = 420; + _dialogService.InfoBarMessage(Translator.GeneralTitle_Info, Translator.Info_MailListSizeResetSuccessMessage, InfoBarMessageType.Success); + } + + private sealed class DemoMailItemDisplayInformation : IMailItemDisplayInformation + { + public event PropertyChangedEventHandler PropertyChanged + { + add { } + remove { } + } + + public string Subject => "Quarterly planning notes"; + public string FromName => "Ava Brooks"; + public string FromAddress => "ava@contoso.com"; + public string PreviewText => "Agenda draft, attendee updates, and a few follow-up items for this week."; + public bool IsRead => false; + public bool IsDraft => false; + public bool HasAttachments => true; + public bool IsCalendarEvent => false; + public bool IsFlagged => true; + public DateTime CreationDate => DateTime.Now.AddMinutes(-12); + public Guid? ContactPictureFileId => null; + public bool ThumbnailUpdatedEvent => false; + public bool IsThreadExpanded => false; + public AccountContact SenderContact => new() + { + Address = "ava@contoso.com", + Name = "Ava Brooks" + }; + } } diff --git a/Wino.Mail.ViewModels/Messages/MailItemSelectedEvent.cs b/Wino.Mail.ViewModels/Messages/MailItemSelectedEvent.cs deleted file mode 100644 index b30b6a36..00000000 --- a/Wino.Mail.ViewModels/Messages/MailItemSelectedEvent.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Wino.Mail.ViewModels.Data; - -namespace Wino.Mail.ViewModels.Messages; - -/// -/// Wino has complex selected item detection mechanism with nested ListViews that -/// supports multi selection with threads. Each list view will raise this for mail list page -/// to react. -/// -public class MailItemSelectedEvent -{ - public MailItemSelectedEvent(MailItemViewModel selectedMailItem) - { - SelectedMailItem = selectedMailItem; - } - - public MailItemViewModel SelectedMailItem { get; set; } -} diff --git a/Wino.Mail.ViewModels/Messages/MailItemSelectionRemovedEvent.cs b/Wino.Mail.ViewModels/Messages/MailItemSelectionRemovedEvent.cs deleted file mode 100644 index d098ba0f..00000000 --- a/Wino.Mail.ViewModels/Messages/MailItemSelectionRemovedEvent.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Wino.Mail.ViewModels.Data; - -namespace Wino.Mail.ViewModels.Messages; - -/// -/// Selected item removed event. -/// -public class MailItemSelectionRemovedEvent -{ - public MailItemSelectionRemovedEvent(MailItemViewModel removedMailItem) - { - RemovedMailItem = removedMailItem; - } - - public MailItemViewModel RemovedMailItem { get; set; } -} diff --git a/Wino.Mail.ViewModels/Messages/NewMailItemRenderingRequestedEvent.cs b/Wino.Mail.ViewModels/Messages/NewMailItemRenderingRequestedEvent.cs deleted file mode 100644 index 095b1670..00000000 --- a/Wino.Mail.ViewModels/Messages/NewMailItemRenderingRequestedEvent.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Wino.Mail.ViewModels.Data; - -namespace Wino.Mail.ViewModels.Messages; - -/// -/// When the rendering page is active, but new item is requested to be rendered. -/// To not trigger navigation again and re-use existing Chromium. -/// -/// -public record NewMailItemRenderingRequestedEvent(MailItemViewModel MailItemViewModel); diff --git a/Wino.Mail.ViewModels/Messages/ReaderItemRefreshRequestedEvent.cs b/Wino.Mail.ViewModels/Messages/ReaderItemRefreshRequestedEvent.cs new file mode 100644 index 00000000..eab15937 --- /dev/null +++ b/Wino.Mail.ViewModels/Messages/ReaderItemRefreshRequestedEvent.cs @@ -0,0 +1,10 @@ +using Wino.Mail.ViewModels.Data; + +namespace Wino.Mail.ViewModels.Messages; + +/// +/// Requests refreshing the currently active reader page (mail rendering or compose) +/// with a different selected mail item without re-navigation. +/// +/// The selected mail item to refresh with. +public record ReaderItemRefreshRequestedEvent(MailItemViewModel MailItemViewModel); diff --git a/Wino.Mail.ViewModels/Messages/SelectMailItemContainerEvent.cs b/Wino.Mail.ViewModels/Messages/SelectMailItemContainerEvent.cs index 814d75a6..ec6797ec 100644 --- a/Wino.Mail.ViewModels/Messages/SelectMailItemContainerEvent.cs +++ b/Wino.Mail.ViewModels/Messages/SelectMailItemContainerEvent.cs @@ -1,8 +1,8 @@ -using Wino.Mail.ViewModels.Data; +using System; namespace Wino.Mail.ViewModels.Messages; /// /// When listing view model manipulated the selected mail container in the UI. /// -public record SelectMailItemContainerEvent(MailItemViewModel SelectedMailViewModel, bool ScrollToItem = false); +public record SelectMailItemContainerEvent(Guid MailUniqueId, bool ScrollToItem = false); diff --git a/Wino.Mail.ViewModels/Messages/SwipeActionRequested.cs b/Wino.Mail.ViewModels/Messages/SwipeActionRequested.cs new file mode 100644 index 00000000..236cc440 --- /dev/null +++ b/Wino.Mail.ViewModels/Messages/SwipeActionRequested.cs @@ -0,0 +1,9 @@ +using Wino.Core.Domain.Enums; +using Wino.Mail.ViewModels.Data; + +namespace Wino.Mail.ViewModels.Messages; + +/// +/// When a swipe action is performed on a mail item container. +/// +public record SwipeActionRequested(MailOperation Operation, IMailListItem MailItem); \ No newline at end of file diff --git a/Wino.Mail.ViewModels/ProviderSelectionPageViewModel.cs b/Wino.Mail.ViewModels/ProviderSelectionPageViewModel.cs new file mode 100644 index 00000000..c5480a68 --- /dev/null +++ b/Wino.Mail.ViewModels/ProviderSelectionPageViewModel.cs @@ -0,0 +1,122 @@ +using System.Collections.Generic; +using System.Linq; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using Wino.Core.Domain; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Navigation; +using Wino.Core.ViewModels.Data; +using Wino.Mail.ViewModels.Data; +using Wino.Messaging.Client.Navigation; + +namespace Wino.Mail.ViewModels; + +public partial class ProviderSelectionPageViewModel : MailBaseViewModel +{ + private readonly IProviderService _providerService; + private readonly INewThemeService _themeService; + + public WelcomeWizardContext WizardContext { get; } + + public List Providers { get; private set; } = []; + public List AvailableColors { get; private set; } = []; + + [ObservableProperty] + public partial IProviderDetail SelectedProvider { get; set; } + + [ObservableProperty] + public partial AppColorViewModel SelectedColor { get; set; } + + [ObservableProperty] + public partial string AccountName { get; set; } + + [ObservableProperty] + public partial bool CanProceed { get; set; } + + public bool IsColorSelected => SelectedColor != null; + + public ProviderSelectionPageViewModel( + IProviderService providerService, + INewThemeService themeService, + WelcomeWizardContext wizardContext) + { + _providerService = providerService; + _themeService = themeService; + WizardContext = wizardContext; + } + + public override void OnNavigatedTo(NavigationMode mode, object parameters) + { + base.OnNavigatedTo(mode, parameters); + + Providers = _providerService.GetAvailableProviders(); + AvailableColors = _themeService.GetAvailableAccountColors() + .Select(hex => new AppColorViewModel(hex)) + .ToList(); + + // Restore from wizard context if navigating back + if (WizardContext.SelectedProvider != null) + { + SelectedProvider = Providers.FirstOrDefault(p => + p.Type == WizardContext.SelectedProvider.Type && + p.SpecialImapProvider == WizardContext.SelectedProvider.SpecialImapProvider); + AccountName = WizardContext.AccountName; + + if (WizardContext.AccountColorHex != null) + SelectedColor = AvailableColors.FirstOrDefault(c => c.Hex == WizardContext.AccountColorHex); + } + + Validate(); + } + + partial void OnSelectedProviderChanged(IProviderDetail value) => Validate(); + partial void OnAccountNameChanged(string value) => Validate(); + partial void OnSelectedColorChanged(AppColorViewModel value) => OnPropertyChanged(nameof(IsColorSelected)); + + [RelayCommand] + private void ClearColor() => SelectedColor = null; + + private void Validate() + { + CanProceed = SelectedProvider != null && !string.IsNullOrWhiteSpace(AccountName); + } + + [RelayCommand] + private void Proceed() + { + if (!CanProceed) return; + + // Persist to wizard context + WizardContext.SelectedProvider = SelectedProvider; + WizardContext.AccountName = AccountName?.Trim(); + WizardContext.AccountColorHex = SelectedColor?.Hex ?? string.Empty; + + if (WizardContext.IsGenericImap) + { + // Navigate to ImapCalDavSettingsPage in wizard mode + var context = ImapCalDavSettingsNavigationContext.CreateForWizardMode( + WizardContext.BuildAccountCreationDialogResult()); + + Messenger.Send(new BreadcrumbNavigationRequested( + Translator.ImapCalDavSettingsPage_TitleCreate, + WinoPage.ImapCalDavSettingsPage, + context)); + } + else if (SelectedProvider.SpecialImapProvider is SpecialImapProvider.iCloud or SpecialImapProvider.Yahoo) + { + // Navigate to credentials page for special IMAP providers + Messenger.Send(new BreadcrumbNavigationRequested( + SelectedProvider.Name, + WinoPage.SpecialImapCredentialsPage)); + } + else + { + // OAuth — go directly to progress page + Messenger.Send(new BreadcrumbNavigationRequested( + Translator.WelcomeWizard_Step3Title, + WinoPage.AccountSetupProgressPage)); + } + } +} diff --git a/Wino.Mail.ViewModels/SignatureAndEncryptionPageViewModel.cs b/Wino.Mail.ViewModels/SignatureAndEncryptionPageViewModel.cs new file mode 100644 index 00000000..3eba7180 --- /dev/null +++ b/Wino.Mail.ViewModels/SignatureAndEncryptionPageViewModel.cs @@ -0,0 +1,238 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.Input; +using Wino.Core.Domain; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Common; + +namespace Wino.Mail.ViewModels; + +public partial class SignatureAndEncryptionPageViewModel : MailBaseViewModel +{ + private readonly ISmimeCertificateService _smimeCertificateService; + private readonly IDialogServiceBase _dialogService; + private readonly IFileService _fileService; + + public ObservableCollection PersonalCertificates { get; } = []; + public ObservableCollection RecipientCertificates { get; } = []; + public List SelectedPersonalCertificates { get; } = []; + public List SelectedRecipientCertificates { get; } = []; + + public bool PersonalCertificatesEmpty => PersonalCertificates.Count == 0; + + public SignatureAndEncryptionPageViewModel( + IDialogServiceBase dialogService, + ISmimeCertificateService smimeCertificateService, + IFileService fileService + ) + { + _dialogService = dialogService; + _fileService = fileService; + _smimeCertificateService = smimeCertificateService; + + PersonalCertificates.CollectionChanged += (s, e) => { OnPropertyChanged(nameof(PersonalCertificatesEmpty)); }; + LoadAllCertificates(); + } + + private void LoadAllCertificates() + { + PersonalCertificates.Clear(); + var personalCerts = _smimeCertificateService.GetCertificates(); + foreach (var cert in personalCerts) + { + PersonalCertificates.Add(cert); + } + + // Recipient certificates + RecipientCertificates.Clear(); + var recipientCerts = _smimeCertificateService.GetCertificates(storeName: StoreName.AddressBook); + foreach (var cert in recipientCerts) + { + RecipientCertificates.Add(cert); + } + } + + [RelayCommand] + public async Task ImportPersonalCertificatesAsync() + { + await ImportCertificates(StoreName.My); + } + + [RelayCommand] + public async Task ImportRecipientCertificatesAsync() + { + await ImportCertificates(StoreName.AddressBook); + } + + private async Task ImportCertificates(StoreName storeName) + { + var files = await PickCertificateFilesAsync(); + var failedImports = new List(); + var successCount = 0; + foreach (var file in files) + { + string password = null; + if (file.FileExtension.Equals(".pfx") || file.FileExtension.Equals(".p12")) + { + password = await PromptForPasswordAsync(file.FileName); + } + + try + { + _smimeCertificateService.ImportCertificate(file.FileExtension, file.Data, password, + storeName: storeName); + successCount++; + } + catch (Exception ex) + { + failedImports.Add($"{file.FileName}: {ex.Message}"); + } + } + LoadAllCertificates(); + if (successCount > 0) + { + _dialogService.InfoBarMessage( + string.Format(Translator.Smime_ImportCertificates_Success), + Translator.GeneralTitle_Info, + InfoBarMessageType.Success); + } + if (failedImports.Count > 0) + { + await _dialogService.ShowMessageAsync( + $"{Translator.Smime_ImportCertificates_Error}\n\n{string.Join("\n", failedImports)}", + Translator.GeneralTitle_Warning, + Core.Domain.Enums.WinoCustomMessageDialogIcon.Warning); + } + } + + [RelayCommand] + public async Task RemovePersonalCertificatesAsync() + { + await RemoveCertificatesAsync(SelectedPersonalCertificates, StoreName.My); + } + + [RelayCommand] + public async Task RemoveRecipientCertificatesAsync() + { + await RemoveCertificatesAsync(SelectedRecipientCertificates, StoreName.AddressBook); + } + + private async Task RemoveCertificatesAsync(List certificates, StoreName storeName) + { + if (certificates.Any()) + { + var confirm = await ConfirmAsync(string.Format(Translator.Smime_RemoveCertificates_Confirm, + string.Join(", ", certificates.Select(cert => cert.Subject)))); + if (confirm) + { + foreach (var cert in certificates) + { + _smimeCertificateService.RemoveCertificate(cert.Thumbprint, storeName: storeName); + } + + LoadAllCertificates(); + _dialogService.InfoBarMessage( + Translator.Smime_RemoveCertificates_Success, + Translator.GeneralTitle_Info, + InfoBarMessageType.Success + ); + } + } + } + + [RelayCommand] + public async Task ExportPersonalCertificatesAsync() + { + await ExportCertificatesAsync(SelectedPersonalCertificates); + } + + [RelayCommand] + public async Task ExportRecipientCertificatesAsync() + { + await ExportCertificatesAsync(SelectedRecipientCertificates); + } + + // Export logic for .cer or .pem + private async Task ExportCertificatesAsync(IEnumerable cert) + { + var failedExports = new List(); + var successCount = 0; + foreach (var certificate in cert) + { + var fileName = $"{certificate.Subject.Replace("CN=", "")}.cer"; + var path = await _dialogService.PickFilePathAsync(fileName); + if (path != null) + { + var folderPath = System.IO.Path.GetDirectoryName(path); + await using var stream = await _fileService.GetFileStreamAsync(folderPath, fileName); + if (stream != null) + { + try + { + var certificateData = certificate.Export(X509ContentType.Cert); + await stream.WriteAsync(certificateData, 0, certificateData.Length); + await stream.FlushAsync(); + successCount++; + } + catch (Exception ex) + { + failedExports.Add($"{certificate.Subject}: {ex.Message}"); + } + } + else + { + failedExports.Add($"{certificate.Subject}: File stream error"); + } + } + } + if (successCount > 0) + { + _dialogService.InfoBarMessage( + Translator.Smime_ExportCertificates_Success, + Translator.GeneralTitle_Info, + InfoBarMessageType.Success + ); + } + if (failedExports.Count > 0) + { + await _dialogService.ShowMessageAsync( + $"{Translator.Smime_ExportCertificates_Error}\n\n{string.Join("\n", failedExports)}", + Translator.GeneralTitle_Warning, + Core.Domain.Enums.WinoCustomMessageDialogIcon.Warning); + } + } + + private async Task ShowCertificateDetailsAsync(X509Certificate2 cert) + { + var details = string.Format(Translator.Smime_CertificateDetails, cert.Subject, cert.Issuer, cert.NotBefore, + cert.NotAfter, cert.Thumbprint); + await _dialogService.ShowMessageAsync(details, Translator.GeneralTitle_Info, + Core.Domain.Enums.WinoCustomMessageDialogIcon.Information); + } + + // Confirmation dialog + private async Task ConfirmAsync(string message) + { + return await _dialogService.ShowConfirmationDialogAsync(message, Translator.Smime_Confirm_Title, + Translator.Buttons_Yes); + } + + // File picker for importing certificates + private async Task> PickCertificateFilesAsync() + { + return await _dialogService.PickFilesAsync(".pfx", ".p12", ".cer", ".crt"); + } + + // Ask for password for .pfx/.p12 + private async Task PromptForPasswordAsync(string fileName) + { + return await _dialogService.ShowTextInputDialogAsync("", + Translator.Smime_CertificatePassword_Title, + string.Format(Translator.Smime_CertificatePassword_Placeholder, fileName), Translator.Buttons_OK); + } +} diff --git a/Wino.Mail.ViewModels/SpecialImapCredentialsPageViewModel.cs b/Wino.Mail.ViewModels/SpecialImapCredentialsPageViewModel.cs new file mode 100644 index 00000000..9ba42445 --- /dev/null +++ b/Wino.Mail.ViewModels/SpecialImapCredentialsPageViewModel.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using Wino.Core.Domain; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Navigation; +using Wino.Mail.ViewModels.Data; +using Wino.Messaging.Client.Navigation; + +namespace Wino.Mail.ViewModels; + +public partial class SpecialImapCredentialsPageViewModel : MailBaseViewModel +{ + private static readonly Dictionary AppPasswordHelpLinks = new() + { + { SpecialImapProvider.iCloud, "https://support.apple.com/en-us/102654" }, + { SpecialImapProvider.Yahoo, "http://help.yahoo.com/kb/SLN15241.html" }, + }; + + private readonly INativeAppService _nativeAppService; + + public WelcomeWizardContext WizardContext { get; } + + [ObservableProperty] + public partial string DisplayName { get; set; } + + [ObservableProperty] + public partial string EmailAddress { get; set; } + + [ObservableProperty] + public partial string AppSpecificPassword { get; set; } + + [ObservableProperty] + public partial int SelectedCalendarModeIndex { get; set; } + + [ObservableProperty] + public partial bool CanProceed { get; set; } + + public string AppPasswordHelpUrl + { + get + { + if (WizardContext.SelectedProvider == null) return null; + AppPasswordHelpLinks.TryGetValue(WizardContext.SelectedProvider.SpecialImapProvider, out var url); + return url; + } + } + + public string CalendarModeCalDavDescription + => WizardContext.SelectedProvider?.SpecialImapProvider == SpecialImapProvider.iCloud + ? Translator.ProviderSelection_CalendarMode_CalDavDescription_Apple + : Translator.ProviderSelection_CalendarMode_CalDavDescription_Yahoo; + + public SpecialImapCredentialsPageViewModel( + INativeAppService nativeAppService, + WelcomeWizardContext wizardContext) + { + _nativeAppService = nativeAppService; + WizardContext = wizardContext; + } + + public override void OnNavigatedTo(NavigationMode mode, object parameters) + { + base.OnNavigatedTo(mode, parameters); + + // Restore from context when navigating back + DisplayName = WizardContext.DisplayName; + EmailAddress = WizardContext.EmailAddress; + AppSpecificPassword = WizardContext.AppSpecificPassword; + + SelectedCalendarModeIndex = WizardContext.CalendarSupportMode switch + { + ImapCalendarSupportMode.CalDav => 1, + ImapCalendarSupportMode.LocalOnly => 2, + _ => 0 + }; + + OnPropertyChanged(nameof(AppPasswordHelpUrl)); + OnPropertyChanged(nameof(CalendarModeCalDavDescription)); + + Validate(); + } + + partial void OnDisplayNameChanged(string value) => Validate(); + partial void OnEmailAddressChanged(string value) => Validate(); + partial void OnAppSpecificPasswordChanged(string value) => Validate(); + + private void Validate() + { + CanProceed = !string.IsNullOrWhiteSpace(DisplayName) + && !string.IsNullOrWhiteSpace(EmailAddress) + && EmailValidation.EmailValidator.Validate(EmailAddress ?? string.Empty) + && !string.IsNullOrWhiteSpace(AppSpecificPassword); + } + + [RelayCommand] + private void Proceed() + { + if (!CanProceed) return; + + WizardContext.DisplayName = DisplayName?.Trim(); + WizardContext.EmailAddress = EmailAddress?.Trim(); + WizardContext.AppSpecificPassword = AppSpecificPassword?.Trim(); + WizardContext.CalendarSupportMode = SelectedCalendarModeIndex switch + { + 1 => ImapCalendarSupportMode.CalDav, + 2 => ImapCalendarSupportMode.LocalOnly, + _ => ImapCalendarSupportMode.Disabled + }; + + Messenger.Send(new BreadcrumbNavigationRequested( + Translator.WelcomeWizard_Step3Title, + WinoPage.AccountSetupProgressPage)); + } + + [RelayCommand] + private async Task OpenAppPasswordHelp() + { + var url = AppPasswordHelpUrl; + if (url != null) + await _nativeAppService.LaunchUriAsync(new Uri(url)); + } +} diff --git a/Wino.Mail.ViewModels/StoragePageViewModel.cs b/Wino.Mail.ViewModels/StoragePageViewModel.cs new file mode 100644 index 00000000..58226a7c --- /dev/null +++ b/Wino.Mail.ViewModels/StoragePageViewModel.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Serilog; +using Wino.Core.Domain; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Navigation; +using Wino.Core.Extensions; +using Wino.Mail.ViewModels.Data; + +namespace Wino.Mail.ViewModels; + +public partial class StoragePageViewModel( + IAccountService accountService, + IMimeStorageService mimeStorageService, + IMailDialogService dialogService) : MailBaseViewModel +{ + private readonly ILogger _logger = Log.ForContext(); + private readonly IAccountService _accountService = accountService; + private readonly IMimeStorageService _mimeStorageService = mimeStorageService; + private readonly IMailDialogService _dialogService = dialogService; + + public ObservableCollection AccountStorageItems { get; } = []; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsBusy))] + public partial bool IsLoading { get; set; } + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsBusy))] + public partial bool IsCleaning { get; set; } + + [ObservableProperty] + public partial string MimeRootPath { get; set; } = string.Empty; + + [ObservableProperty] + public partial string SummaryText { get; set; } = ""; + + public bool IsBusy => IsLoading || IsCleaning; + + public override async void OnNavigatedTo(NavigationMode mode, object parameters) + { + base.OnNavigatedTo(mode, parameters); + await ExecuteUIThread(() => { SummaryText = Translator.SettingsStorage_NoLocalMimeDataFound; }); + + await RefreshStorageAsync(); + } + + partial void OnIsLoadingChanged(bool value) + { + UpdateAccountBusyState(); + } + + partial void OnIsCleaningChanged(bool value) + { + UpdateAccountBusyState(); + } + + private void UpdateAccountBusyState() + { + Dispatcher.ExecuteOnUIThread(() => + { + foreach (var item in AccountStorageItems) + { + item.IsBusy = IsBusy; + } + }); + } + + [RelayCommand] + private async Task RefreshStorageAsync() + { + if (IsBusy) return; + + await ExecuteUIThread(() => { IsLoading = true; }); + + try + { + var mimeRootPath = await _mimeStorageService.GetMimeRootPathAsync().ConfigureAwait(false); + var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false); + var sizeMap = await _mimeStorageService.GetAccountsMimeStorageSizesAsync(accounts.Select(a => a.Id)).ConfigureAwait(false); + + var storageItems = accounts + .Select(account => + { + sizeMap.TryGetValue(account.Id, out var accountSize); + var viewModel = new AccountStorageItemViewModel(account, accountSize, DeleteAllCommand, DeleteOlderThanOneMonthCommand, DeleteOlderThanThreeMonthsCommand, DeleteOlderThanSixMonthsCommand, DeleteOlderThanOneYearCommand); + viewModel.SizeDescription = string.Format(Translator.SettingsStorage_AccountUsageDescription, viewModel.SizeText); + return viewModel; + }) + .OrderByDescending(a => a.SizeBytes) + .ToList(); + + await ExecuteUIThread(() => + { + MimeRootPath = mimeRootPath; + AccountStorageItems.Clear(); + + foreach (var item in storageItems) + { + AccountStorageItems.Add(item); + } + + var total = storageItems.Sum(a => a.SizeBytes); + SummaryText = storageItems.Count == 0 + ? Translator.SettingsStorage_NoAccountsFound + : string.Format(Translator.SettingsStorage_TotalUsage, total.GetBytesReadable()); + }); + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to refresh storage data."); + await ExecuteUIThread(() => + { + _dialogService.InfoBarMessage(Translator.GeneralTitle_Error, ex.Message, Core.Domain.Enums.InfoBarMessageType.Error); + }); + } + finally + { + await ExecuteUIThread(() => { IsLoading = false; }); + } + } + + [RelayCommand] + private async Task DeleteAllAsync(AccountStorageItemViewModel accountItem) + { + if (accountItem == null || IsBusy) return; + + bool approved = await _dialogService.ShowConfirmationDialogAsync( + string.Format(Translator.SettingsStorage_DeleteAll_Confirm_Message, accountItem.AccountName), + Translator.SettingsStorage_DeleteAll_Confirm_Title, + Translator.Buttons_Delete); + + if (!approved) return; + + await ExecuteUIThread(() => { IsCleaning = true; }); + + try + { + await _mimeStorageService.DeleteAccountMimeStorageAsync(accountItem.Account.Id).ConfigureAwait(false); + await ExecuteUIThread(() => + { + _dialogService.InfoBarMessage(Translator.GeneralTitle_Info, Translator.SettingsStorage_DeleteAll_Success, Core.Domain.Enums.InfoBarMessageType.Success); + }); + await RefreshStorageAsync(); + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to delete all MIME content for account {AccountId}", accountItem.Account.Id); + await ExecuteUIThread(() => + { + _dialogService.InfoBarMessage(Translator.GeneralTitle_Error, ex.Message, Core.Domain.Enums.InfoBarMessageType.Error); + }); + } + finally + { + await ExecuteUIThread(() => { IsCleaning = false; }); + } + } + + [RelayCommand] + private Task DeleteOlderThanOneMonthAsync(AccountStorageItemViewModel accountItem) + => DeleteOlderThanAsync(accountItem, 1); + + [RelayCommand] + private Task DeleteOlderThanThreeMonthsAsync(AccountStorageItemViewModel accountItem) + => DeleteOlderThanAsync(accountItem, 3); + + [RelayCommand] + private Task DeleteOlderThanSixMonthsAsync(AccountStorageItemViewModel accountItem) + => DeleteOlderThanAsync(accountItem, 6); + + [RelayCommand] + private Task DeleteOlderThanOneYearAsync(AccountStorageItemViewModel accountItem) + => DeleteOlderThanAsync(accountItem, 12); + + private async Task DeleteOlderThanAsync(AccountStorageItemViewModel accountItem, int months) + { + if (accountItem == null || IsBusy) return; + + string rangeText = GetRangeText(months); + + bool approved = await _dialogService.ShowConfirmationDialogAsync( + string.Format(Translator.SettingsStorage_DeleteOld_Confirm_Message, rangeText, accountItem.AccountName), + Translator.SettingsStorage_DeleteOld_Confirm_Title, + Translator.Buttons_Delete); + + if (!approved) return; + + await ExecuteUIThread(() => { IsCleaning = true; }); + + try + { + var cutoffDateUtc = DateTime.UtcNow.AddMonths(-months); + var deletedDirectoryCount = await _mimeStorageService + .DeleteAccountMimeStorageOlderThanAsync(accountItem.Account.Id, cutoffDateUtc) + .ConfigureAwait(false); + + await ExecuteUIThread(() => + { + _dialogService.InfoBarMessage( + Translator.GeneralTitle_Info, + string.Format(Translator.SettingsStorage_DeleteOld_Success, deletedDirectoryCount, rangeText), + Core.Domain.Enums.InfoBarMessageType.Success); + }); + + await RefreshStorageAsync(); + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to delete MIME content by cutoff for account {AccountId}", accountItem.Account.Id); + await ExecuteUIThread(() => + { + _dialogService.InfoBarMessage(Translator.GeneralTitle_Error, ex.Message, Core.Domain.Enums.InfoBarMessageType.Error); + }); + } + finally + { + await ExecuteUIThread(() => { IsCleaning = false; }); + } + } + + private static string GetRangeText(int months) + { + return months switch + { + 1 => Translator.SettingsStorage_1Month, + 3 => Translator.SettingsStorage_3Months, + 6 => Translator.SettingsStorage_6Months, + 12 => Translator.SettingsStorage_1Year, + _ => string.Format(Translator.SettingsStorage_Months, months) + }; + } +} diff --git a/Wino.Mail.ViewModels/WelcomePageV2ViewModel.cs b/Wino.Mail.ViewModels/WelcomePageV2ViewModel.cs new file mode 100644 index 00000000..9626af2a --- /dev/null +++ b/Wino.Mail.ViewModels/WelcomePageV2ViewModel.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using Wino.Core.Domain; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Accounts; +using Wino.Core.Domain.Models.Navigation; +using Wino.Core.Domain.Models.Updates; +using Wino.Messaging.Client.Navigation; +using Wino.Messaging.UI; + +namespace Wino.Mail.ViewModels; + +public partial class WelcomePageV2ViewModel : MailBaseViewModel +{ + private readonly IUpdateManager _updateManager; + private readonly IMailDialogService _dialogService; + private readonly IWinoAccountDataSyncService _syncService; + + [ObservableProperty] + public partial List UpdateSections { get; set; } = []; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(GetStartedCommand))] + [NotifyCanExecuteChangedFor(nameof(ImportFromWinoAccountCommand))] + public partial bool IsImportInProgress { get; set; } + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasImportStatus))] + public partial string ImportStatusMessage { get; set; } = string.Empty; + + public bool HasImportStatus => !string.IsNullOrWhiteSpace(ImportStatusMessage); + + public WelcomePageV2ViewModel(IUpdateManager updateManager, + IMailDialogService dialogService, + IWinoAccountDataSyncService syncService) + { + _updateManager = updateManager; + _dialogService = dialogService; + _syncService = syncService; + } + + public override async void OnNavigatedTo(NavigationMode mode, object parameters) + { + base.OnNavigatedTo(mode, parameters); + + try + { + var updateNotes = await _updateManager.GetLatestUpdateNotesAsync(); + UpdateSections = updateNotes.Sections; + } + catch (Exception) + { + UpdateSections = []; + } + } + + [RelayCommand(CanExecute = nameof(CanOpenWelcomeActions))] + private void GetStarted() + { + Messenger.Send(new BreadcrumbNavigationRequested( + Translator.WelcomeWizard_Step2Title, + WinoPage.ProviderSelectionPage)); + } + + [RelayCommand(CanExecute = nameof(CanOpenWelcomeActions))] + private async Task ImportFromWinoAccountAsync() + { + await ExecuteUIThread(() => ImportStatusMessage = string.Empty); + + try + { + var account = await _dialogService.ShowWinoAccountLoginDialogAsync().ConfigureAwait(false); + if (account == null) + { + return; + } + + await ExecuteUIThread(() => IsImportInProgress = true); + + var result = await _syncService.ImportAsync(new WinoAccountSyncSelection()).ConfigureAwait(false); + if (result.ImportedMailboxCount > 0) + { + ReportUIChange(new WelcomeImportCompletedMessage(result.ImportedMailboxCount)); + return; + } + + await ExecuteUIThread(() => ImportStatusMessage = BuildInlineImportMessage(result)); + } + catch (Exception ex) + { + await _dialogService.ShowMessageAsync(ex.Message, Translator.GeneralTitle_Error, WinoCustomMessageDialogIcon.Error); + } + finally + { + await ExecuteUIThread(() => IsImportInProgress = false); + } + } + + private bool CanOpenWelcomeActions() => !IsImportInProgress; + + private static string BuildInlineImportMessage(WinoAccountSyncImportResult result) + { + var preferencesMessage = result.FailedPreferenceCount > 0 + ? string.Format(Translator.WinoAccount_Management_ImportPartial, result.AppliedPreferenceCount, result.FailedPreferenceCount) + : result.HadRemotePreferences + ? string.Format(Translator.WinoAccount_Management_ImportPreferencesSucceeded, result.AppliedPreferenceCount) + : string.Empty; + + if (result.RemoteMailboxCount == 0) + { + return string.IsNullOrWhiteSpace(preferencesMessage) + ? Translator.WelcomeWindow_ImportNoAccountsFound + : $"{preferencesMessage} {Translator.WelcomeWindow_ImportNoAccountsFound}"; + } + + if (result.SkippedDuplicateMailboxCount > 0 && result.ImportedMailboxCount == 0) + { + var duplicateMessage = string.Format(Translator.WelcomeWindow_ImportDuplicateAccountsSkipped, result.SkippedDuplicateMailboxCount); + return string.IsNullOrWhiteSpace(preferencesMessage) + ? duplicateMessage + : $"{preferencesMessage} {duplicateMessage}"; + } + + return string.IsNullOrWhiteSpace(preferencesMessage) + ? Translator.WinoAccount_Management_ImportEmpty + : preferencesMessage; + } +} diff --git a/Wino.Mail.ViewModels/WelcomePageViewModel.cs b/Wino.Mail.ViewModels/WelcomePageViewModel.cs deleted file mode 100644 index abe77009..00000000 --- a/Wino.Mail.ViewModels/WelcomePageViewModel.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using CommunityToolkit.Mvvm.ComponentModel; -using Wino.Core.Domain; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Navigation; - -namespace Wino.Mail.ViewModels; - -public partial class WelcomePageViewModel : MailBaseViewModel -{ - public const string VersionFile = "1102.md"; - private readonly IMailDialogService _dialogService; - private readonly IFileService _fileService; - - [ObservableProperty] - private string currentVersionNotes; - - public WelcomePageViewModel(IMailDialogService dialogService, IFileService fileService) - { - _dialogService = dialogService; - _fileService = fileService; - } - - public override async void OnNavigatedTo(NavigationMode mode, object parameters) - { - base.OnNavigatedTo(mode, parameters); - - try - { - CurrentVersionNotes = await _fileService.GetFileContentByApplicationUriAsync($"ms-appx:///Assets/ReleaseNotes/{VersionFile}"); - } - catch (Exception) - { - _dialogService.InfoBarMessage(Translator.GeneralTitle_Error, "Can't find the patch notes.", Core.Domain.Enums.InfoBarMessageType.Information); - } - } -} diff --git a/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj b/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj index 7cbcd411..b84d5cb7 100644 --- a/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj +++ b/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj @@ -1,15 +1,19 @@  - net9.0 + net10.0 x86;x64;arm64 win-x86;win-x64;win-arm64 true true + true + true + true + @@ -19,4 +23,7 @@ + + + \ No newline at end of file diff --git a/Wino.Core.UWP/Activation/ActivationHandler.cs b/Wino.Mail.WinUI/Activation/ActivationHandler.cs similarity index 81% rename from Wino.Core.UWP/Activation/ActivationHandler.cs rename to Wino.Mail.WinUI/Activation/ActivationHandler.cs index bbe692f1..845d8bae 100644 --- a/Wino.Core.UWP/Activation/ActivationHandler.cs +++ b/Wino.Mail.WinUI/Activation/ActivationHandler.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; namespace Wino.Activation; @@ -18,13 +18,16 @@ public abstract class ActivationHandler : ActivationHandler public override async Task HandleAsync(object args) { - await HandleInternalAsync(args as T); + if (args is T typedArgs) + { + await HandleInternalAsync(typedArgs); + } } public override bool CanHandle(object args) { // CanHandle checks the args is of type you have configured - return args is T && CanHandleInternal(args as T); + return args is T typedArgs && CanHandleInternal(typedArgs); } // You can override this method to add extra validation on activation args diff --git a/Wino.Mail.WinUI/Activation/AppModeActivationResolver.cs b/Wino.Mail.WinUI/Activation/AppModeActivationResolver.cs new file mode 100644 index 00000000..965da52a --- /dev/null +++ b/Wino.Mail.WinUI/Activation/AppModeActivationResolver.cs @@ -0,0 +1,92 @@ +using System; +using Wino.Core.Domain.Enums; + +namespace Wino.Mail.WinUI.Activation; + +internal static class AppModeActivationResolver +{ + public static WinoApplicationMode Resolve(string? launchArguments, string? tileId, string? appId, WinoApplicationMode defaultMode = WinoApplicationMode.Mail) + { + if (TryResolveFromText(launchArguments, defaultMode, out var mode)) + return mode; + + if (TryResolveFromText(tileId, defaultMode, out mode)) + return mode; + + if (TryResolveFromText(appId, defaultMode, out mode)) + return mode; + + return defaultMode; + } + + private static bool TryResolveFromText(string? value, WinoApplicationMode defaultMode, out WinoApplicationMode mode) + { + mode = defaultMode; + + if (string.IsNullOrWhiteSpace(value)) + return false; + + if (Contains(value, "--mode=toggle-default") || + Contains(value, "mode=toggle-default")) + { + mode = GetOpposite(defaultMode); + return true; + } + + if (Contains(value, "wino-calendar") || + Contains(value, "--mode=calendar") || + Contains(value, "mode=calendar") || + Contains(value, "calendarapp") || + EqualsToken(value, "calendar")) + { + mode = WinoApplicationMode.Calendar; + return true; + } + + if (Contains(value, "wino-contacts") || + Contains(value, "--mode=contacts") || + Contains(value, "mode=contacts") || + Contains(value, "contactsapp") || + EqualsToken(value, "contacts")) + { + mode = WinoApplicationMode.Contacts; + return true; + } + + if (Contains(value, "wino-settings") || + Contains(value, "--mode=settings") || + Contains(value, "mode=settings") || + Contains(value, "settingsapp") || + EqualsToken(value, "settings")) + { + mode = WinoApplicationMode.Settings; + return true; + } + + if (Contains(value, "wino-mail") || + Contains(value, "--mode=mail") || + Contains(value, "mode=mail") || + Contains(value, "mailapp") || + EqualsToken(value, "mail")) + { + mode = WinoApplicationMode.Mail; + return true; + } + + return false; + } + + private static bool Contains(string source, string token) + => source.Contains(token, StringComparison.OrdinalIgnoreCase); + + private static bool EqualsToken(string source, string token) + => string.Equals(source.Trim(), token, StringComparison.OrdinalIgnoreCase); + + private static WinoApplicationMode GetOpposite(WinoApplicationMode defaultMode) + => defaultMode switch + { + WinoApplicationMode.Mail => WinoApplicationMode.Calendar, + WinoApplicationMode.Calendar => WinoApplicationMode.Mail, + _ => WinoApplicationMode.Mail + }; +} diff --git a/Wino.Mail/Activation/ProtocolActivationHandler.cs b/Wino.Mail.WinUI/Activation/ProtocolActivationHandler.cs similarity index 99% rename from Wino.Mail/Activation/ProtocolActivationHandler.cs rename to Wino.Mail.WinUI/Activation/ProtocolActivationHandler.cs index 9692ea20..938c61d5 100644 --- a/Wino.Mail/Activation/ProtocolActivationHandler.cs +++ b/Wino.Mail.WinUI/Activation/ProtocolActivationHandler.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; using CommunityToolkit.Mvvm.Messaging; using Windows.ApplicationModel.Activation; diff --git a/Wino.Mail.WinUI/App.xaml b/Wino.Mail.WinUI/App.xaml new file mode 100644 index 00000000..21d04fe6 --- /dev/null +++ b/Wino.Mail.WinUI/App.xaml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + 24,24,24,24 + + + + + + + + + + + + + + 4 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wino.Mail.WinUI/App.xaml.cs b/Wino.Mail.WinUI/App.xaml.cs new file mode 100644 index 00000000..06289cb2 --- /dev/null +++ b/Wino.Mail.WinUI/App.xaml.cs @@ -0,0 +1,1424 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.Messaging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Toolkit.Uwp.Notifications; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Media.Animation; +using Microsoft.Windows.AppLifecycle; +using Microsoft.Windows.AppNotifications; +using MimeKit.Cryptography; +using Windows.ApplicationModel.Activation; +using Wino.Calendar.ViewModels; +using Wino.Calendar.ViewModels.Interfaces; +using Wino.Core; +using Wino.Core.Domain; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Calendar; +using Wino.Core.Domain.Models.MailItem; +using Wino.Core.Domain.Models.Navigation; +using Wino.Core.Domain.Models.Synchronization; +using Wino.Core.ViewModels; +using Wino.Mail.Services; +using Wino.Mail.ViewModels; +using Wino.Mail.ViewModels.Data; +using Wino.Mail.WinUI.Activation; +using Wino.Mail.WinUI.Interfaces; +using Wino.Mail.WinUI.Models; +using Wino.Mail.WinUI.Services; +using Wino.Mail.WinUI.ViewModels; +using Wino.Messaging.Client.Accounts; +using Wino.Messaging.Client.Navigation; +using Wino.Messaging.Server; +using Wino.Messaging.UI; +using Wino.Services; +using Wino.Views; +using WinUIEx; +namespace Wino.Mail.WinUI; + +public partial class App : WinoApplication, + IRecipient, + IRecipient, + IRecipient, + IRecipient, + IRecipient, + IRecipient +{ + private const int InboxSyncsPerFullSync = 20; + private const string ToggleDefaultModeLaunchArgument = "--mode=toggle-default"; + private const string WinoProtocolScheme = "wino"; + private const string BillingProtocolHost = "billing"; + private const string BillingSuccessPath = "/success"; + private ISynchronizationManager? _synchronizationManager; + private IPreferencesService? _preferencesService; + private IAccountService? _accountService; + private bool _windowManagerConfigured; + private bool _hasConfiguredAccounts; + private bool _isExiting; + private CancellationTokenSource? _autoSynchronizationLoopCts; + private readonly SemaphoreSlim _autoSynchronizationSemaphore = new(1, 1); + private readonly ConcurrentDictionary _inboxSyncCounters = []; + private NativeTrayIcon? _trayIcon; + + internal bool IsExiting => _isExiting; + + public App() + { + InitializeComponent(); + + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + CryptographyContext.Register(typeof(WindowsSecureMimeContext)); + + RegisterRecipients(); + } + + private void EnsureWindowManagerConfigured() + { + if (_windowManagerConfigured) + return; + + var windowManager = Services.GetRequiredService(); + windowManager.ActiveWindowChanged -= OnActiveWindowChanged; + windowManager.ActiveWindowChanged += OnActiveWindowChanged; + windowManager.WindowRemoved -= OnManagedWindowRemoved; + windowManager.WindowRemoved += OnManagedWindowRemoved; + + var nativeAppService = Services.GetRequiredService(); + nativeAppService.GetCoreWindowHwnd = () => + { + var window = windowManager.ActiveWindow + ?? windowManager.GetWindow(WinoWindowKind.Shell) + ?? windowManager.GetWindow(WinoWindowKind.Welcome) + ?? MainWindow; + + return window == null + ? IntPtr.Zero + : WinRT.Interop.WindowNative.GetWindowHandle(window); + }; + + _windowManagerConfigured = true; + } + + private void OnActiveWindowChanged(object? sender, WindowEx? window) + { + if (window == null) + return; + + MainWindow = window; + InitializeNavigationDispatcher(); + } + + private void OnManagedWindowRemoved(object? sender, WindowEx window) + { + var windowManager = Services.GetRequiredService(); + MainWindow = windowManager.ActiveWindow + ?? windowManager.GetWindow(WinoWindowKind.Shell) + ?? windowManager.GetWindow(WinoWindowKind.Welcome); + + InitializeNavigationDispatcher(); + } + + private void EnsureTrayIconCreated() + { + if (_trayIcon != null) + return; + + var iconPath = Path.Combine(AppContext.BaseDirectory, "Assets", "Wino_Icon.ico"); + var dispatcherQueue = DispatcherQueue.GetForCurrentThread() + ?? throw new InvalidOperationException("Tray icon must be created on a thread with a DispatcherQueue."); + + _trayIcon = new NativeTrayIcon( + dispatcherQueue, + iconPath, + "Wino Mail", + BuildTrayMenu, + ActivatePreferredWindowAsync); + + _trayIcon.Create(); + } + + private IReadOnlyList BuildTrayMenu() + { + List items = + [ + new(Translator.SystemTrayMenu_Open, ActivatePreferredWindowAsync, IsDefault: true), + new(Translator.SystemTrayMenu_ShowWino, OpenMailFromTrayAsync) + ]; + + items.Add(new NativeTrayIcon.NativeTrayMenuItem( + Translator.SystemTrayMenu_ShowWinoCalendar, + OpenCalendarFromTrayAsync)); + items.Add(new NativeTrayIcon.NativeTrayMenuItem( + Translator.SystemTrayMenu_ExitWino, + ExitApplicationAsync)); + + return items; + } + + private Task ActivatePreferredWindowAsync() + { + if (!_hasConfiguredAccounts) + return ActivateWelcomeWindowAsync(); + + return ActivateShellWindowAsync(_preferencesService?.DefaultApplicationMode); + } + + private Task OpenMailFromTrayAsync() + => _hasConfiguredAccounts + ? ActivateShellWindowAsync(WinoApplicationMode.Mail) + : ActivateWelcomeWindowAsync(); + + private Task OpenCalendarFromTrayAsync() + => _hasConfiguredAccounts + ? ActivateShellWindowAsync(WinoApplicationMode.Calendar) + : ActivateWelcomeWindowAsync(); + + private async Task ActivateWelcomeWindowAsync() + { + var windowManager = Services.GetRequiredService(); + var welcomeWindow = windowManager.GetWindow(WinoWindowKind.Welcome) as WelcomeWindow; + + if (welcomeWindow == null) + { + CreateWelcomeWindow(); + welcomeWindow = MainWindow as WelcomeWindow; + } + + if (welcomeWindow == null) + return; + + CloseShellWindowIfPresent(); + await ActivateWindowAsync(welcomeWindow); + } + + private async Task ActivateShellWindowAsync(WinoApplicationMode? mode, IWinoShellWindow? existingShellWindow = null) + { + var windowManager = Services.GetRequiredService(); + var shellWindow = existingShellWindow; + + if (shellWindow == null) + { + shellWindow = windowManager.GetWindow(WinoWindowKind.Shell) as IWinoShellWindow; + + if (shellWindow == null) + { + CreateWindow(null); + shellWindow = MainWindow as IWinoShellWindow; + } + } + + if (shellWindow == null) + return; + + if (mode.HasValue) + shellWindow.HandleAppActivation(GetModeLaunchArgument(mode.Value)); + + CloseWelcomeWindowIfPresent(); + await ActivateWindowAsync((WindowEx)shellWindow); + } + + private void CloseWelcomeWindowIfPresent() + { + var windowManager = Services.GetRequiredService(); + if (windowManager.GetWindow(WinoWindowKind.Welcome) is not WelcomeWindow welcomeWindow) + return; + + welcomeWindow.PrepareForClose(); + welcomeWindow.AllowClose(); + welcomeWindow.Close(); + } + + private void CloseShellWindowIfPresent() + { + var windowManager = Services.GetRequiredService(); + if (windowManager.GetWindow(WinoWindowKind.Shell) is not ShellWindow shellWindow) + return; + + windowManager.HideWindow(shellWindow); + if (ReferenceEquals(MainWindow, shellWindow)) + { + MainWindow = null; + InitializeNavigationDispatcher(); + } + + shellWindow.PrepareForClose(); + shellWindow.Close(); + } + + private async Task ActivateWindowAsync(WindowEx window) + { + var windowManager = Services.GetRequiredService(); + MainWindow = window; + windowManager.ActivateWindow(window); + await NewThemeService.ApplyThemeToActiveWindowAsync(); + } + + private Task ExitApplicationAsync() + { + ExitApplication(); + return Task.CompletedTask; + } + + private void ExitApplication() + { + if (_isExiting) + return; + + _isExiting = true; + _trayIcon?.Dispose(); + _trayIcon = null; + + Services.GetRequiredService().CloseAllWindows(); + Application.Current.Exit(); + } + + public bool IsNotificationActivation(out AppNotificationActivatedEventArgs args) + { + var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs(); + + if (activationArgs.Kind == ExtendedActivationKind.AppNotification) + { + args = ((AppNotificationActivatedEventArgs)activationArgs.Data); + return true; + } + + args = null!; + return false; + } + + #region Dependency Injection + + + private void RegisterUWPServices(IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } + + private void RegisterViewModels(IServiceCollection services) + { + services.AddSingleton(typeof(MailAppShellViewModel)); + services.AddSingleton(typeof(CalendarAppShellViewModel)); + services.AddSingleton(typeof(ContactsShellClient)); + services.AddSingleton(typeof(SettingsShellClient)); + services.AddSingleton(typeof(WinoAppShellViewModel)); + services.AddSingleton(serviceProvider => serviceProvider.GetRequiredService()); + services.AddSingleton(serviceProvider => serviceProvider.GetRequiredService()); + services.AddSingleton(serviceProvider => serviceProvider.GetRequiredService()); + services.AddSingleton(serviceProvider => serviceProvider.GetRequiredService()); + services.AddSingleton(serviceProvider => serviceProvider.GetRequiredService()); + services.AddSingleton(serviceProvider => serviceProvider.GetRequiredService()); + + services.AddTransient(typeof(MailListPageViewModel)); + services.AddTransient(typeof(MailRenderingPageViewModel)); + services.AddTransient(typeof(AccountManagementViewModel)); + services.AddTransient(typeof(WelcomePageV2ViewModel)); + services.AddTransient(typeof(ProviderSelectionPageViewModel)); + services.AddTransient(typeof(AccountSetupProgressPageViewModel)); + services.AddTransient(typeof(SpecialImapCredentialsPageViewModel)); + services.AddSingleton(typeof(WelcomeWizardContext)); + + services.AddTransient(typeof(ComposePageViewModel)); + services.AddTransient(typeof(IdlePageViewModel)); + + services.AddTransient(typeof(ImapCalDavSettingsPageViewModel)); + services.AddTransient(typeof(AccountDetailsPageViewModel)); + services.AddTransient(typeof(SignatureManagementPageViewModel)); + services.AddTransient(typeof(MessageListPageViewModel)); + services.AddTransient(typeof(ReadComposePanePageViewModel)); + services.AddTransient(typeof(MergedAccountDetailsPageViewModel)); + services.AddTransient(typeof(AppPreferencesPageViewModel)); + services.AddTransient(typeof(StoragePageViewModel)); + services.AddTransient(typeof(WinoAccountManagementPageViewModel)); + services.AddTransient(typeof(AliasManagementPageViewModel)); + services.AddTransient(typeof(ContactsPageViewModel)); + services.AddTransient(typeof(SignatureAndEncryptionPageViewModel)); + services.AddTransient(typeof(EmailTemplatesPageViewModel)); + services.AddTransient(typeof(CreateEmailTemplatePageViewModel)); + services.AddSingleton(typeof(CalendarPageViewModel)); + services.AddTransient(typeof(CalendarRenderingSettingsPageViewModel)); + services.AddTransient(typeof(CalendarNotificationSettingsPageViewModel)); + services.AddTransient(typeof(CalendarPreferenceSettingsPageViewModel)); + services.AddTransient(typeof(CalendarAccountSettingsPageViewModel)); + services.AddTransient(typeof(EventDetailsPageViewModel)); + services.AddTransient(typeof(CalendarEventComposePageViewModel)); + } + + #endregion + + public override IServiceProvider ConfigureServices() + { + var services = new ServiceCollection(); + + services.RegisterCoreServices(); + services.RegisterSharedServices(); + services.RegisterCoreUWPServices(); + services.RegisterCoreViewModels(); + + RegisterUWPServices(services); + RegisterViewModels(services); + + return services.BuildServiceProvider(); + } + + private bool IsStartupTaskLaunch() => AppInstance.GetCurrent().GetActivatedEventArgs()?.Kind == ExtendedActivationKind.StartupTask; + public bool IsAppRunning() => MainWindow != null; + + protected override async void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) + { + base.OnLaunched(args); + + // Always register notification callbacks. + TryRegisterAppNotifications(); + + await Services.GetRequiredService() + .RunIfNeededAsync(); + + // Initialize required services regardless of launch activation type. + // All activation scenarios require these services to be ready. + // Note: Theme service is initialized separately after window creation. + await InitializeServicesAsync(); + + _synchronizationManager = Services.GetRequiredService(); + _preferencesService = Services.GetRequiredService(); + _accountService = Services.GetRequiredService(); + + EnsureWindowManagerConfigured(); + EnsureTrayIconCreated(); + + var hasAnyAccount = (await _accountService.GetAccountsAsync()).Any(); + _hasConfiguredAccounts = hasAnyAccount; + if (!IsStartupTaskLaunch() && !hasAnyAccount) + { + CreateWelcomeWindow(); + await NewThemeService.InitializeAsync(); + MainWindow?.Activate(); + LogActivation("Welcome window created and activated."); + return; + } + + _preferencesService.PreferenceChanged -= PreferencesServiceChanged; + _preferencesService.PreferenceChanged += PreferencesServiceChanged; + + RestartAutoSynchronizationLoop(); + + // Check if launched from toast notification. + if (IsNotificationActivation(out AppNotificationActivatedEventArgs toastArgs)) + { + await HandleToastActivationAsync(toastArgs); + return; + } + + // Check if launched by startup task. + bool isStartupTaskLaunch = IsStartupTaskLaunch(); + + if (isStartupTaskLaunch && !hasAnyAccount) + { + CreateWelcomeWindow(); + } + else + { + CreateWindow(args); + } + + // Initialize theme service after window creation. + // Theme service requires the window to exist to properly load and apply themes. + await NewThemeService.InitializeAsync(); + + if (hasAnyAccount) + { + // Wino account loading and activation. + await LoadInitialWinoAccountAsync(); + await HandlePostActivationAsync(AppInstance.GetCurrent().GetActivatedEventArgs()); + } + + LogActivation("Theme service initialized."); + + // If startup task launch, keep window hidden (system tray only). + // Otherwise, activate the window normally. + if (isStartupTaskLaunch) + { + LogActivation("Launched by startup task. Window created but hidden (system tray only)."); + } + else + { + // Normal launch - show and activate the window. + // The What's New dialog is shown from MailAppShellViewModel.OnNavigatedTo once XamlRoot is ready. + MainWindow?.Activate(); + LogActivation("Window created and activated."); + } + } + + private void AppNotificationInvoked(AppNotificationManager sender, AppNotificationActivatedEventArgs args) + { + // AppNotification callbacks are not guaranteed to run on the UI thread. + // Marshal toast handling to the window dispatcher before touching window APIs. + if (MainWindow?.DispatcherQueue?.TryEnqueue(() => _ = HandleToastActivationAsync(args)) == true) + return; + + _ = HandleToastActivationAsync(args); + } + + private void TryRegisterAppNotifications() + { + var notificationManager = AppNotificationManager.Default; + + notificationManager.NotificationInvoked -= AppNotificationInvoked; + notificationManager.NotificationInvoked += AppNotificationInvoked; + + try + { + notificationManager.Register(); + } + catch (Exception ex) + { + LogActivation($"App notification registration failed: {ex.GetType().Name} - {ex.Message}"); + } + } + + /// + /// Handles toast notification activation scenarios. + /// + private async Task HandleToastActivationAsync(AppNotificationActivatedEventArgs toastArgs) + { + var toastArguments = ToastArguments.Parse(toastArgs.Argument); + + if (toastArguments.TryGetValue(Constants.ToastStoreUpdateActionKey, out string storeUpdateAction) && + storeUpdateAction == Constants.ToastStoreUpdateActionInstall) + { + await HandleStoreUpdateToastAsync(); + return; + } + + // Check calendar reminder toast activation first. + if (toastArguments.TryGetValue(Constants.ToastCalendarActionKey, out string calendarAction) && + toastArguments.TryGetValue(Constants.ToastCalendarItemIdKey, out string calendarItemIdString) && + Guid.TryParse(calendarItemIdString, out Guid calendarItemId)) + { + if (calendarAction == Constants.ToastCalendarNavigateAction) + { + await HandleCalendarToastNavigationAsync(calendarItemId); + return; + } + + if (calendarAction == Constants.ToastCalendarSnoozeAction) + { + await HandleCalendarToastSnoozeAsync(toastArgs, calendarItemId); + return; + } + } + + // Check if this is a navigation toast (user clicked the notification). + if (toastArguments.TryGetValue(Constants.ToastActionKey, out MailOperation action) && + Guid.TryParse(toastArguments[Constants.ToastMailUniqueIdKey], out Guid mailItemUniqueId)) + { + if (action == MailOperation.Navigate) + { + // User clicked notification - create window if needed and navigate. + await HandleToastNavigationAsync(mailItemUniqueId); + } + else + { + // User clicked action button (Mark as Read, Delete, etc.) + // Execute action without window and exit. + + await HandleToastActionAsync(action, mailItemUniqueId); + } + } + } + + private async Task HandleStoreUpdateToastAsync() + { + if (!IsAppRunning()) + { + await CreateAndActivateWindow(null!); + } + else + { + EnsureMainWindowVisibleAndForeground(); + } + + var storeUpdateService = Services.GetRequiredService(); + await storeUpdateService.StartUpdateAsync(); + } + + private async Task HandleCalendarToastNavigationAsync(Guid calendarItemId) + { + var calendarService = Services.GetRequiredService(); + var navigationService = Services.GetRequiredService(); + + var calendarItem = await calendarService.GetCalendarItemAsync(calendarItemId); + if (calendarItem == null) + return; + + var target = new CalendarItemTarget(calendarItem, CalendarEventTargetType.Single); + + if (!IsAppRunning()) + { + await CreateAndActivateWindow(null!); + } + else + { + EnsureMainWindowVisibleAndForeground(); + } + + navigationService.ChangeApplicationMode(Core.Domain.Enums.WinoApplicationMode.Calendar); + navigationService.Navigate(WinoPage.EventDetailsPage, target); + } + + private async Task HandleCalendarToastSnoozeAsync(AppNotificationActivatedEventArgs toastArgs, Guid calendarItemId) + { + if (!TryGetSnoozeDurationMinutes(toastArgs, out var snoozeDurationMinutes)) + return; + + var calendarService = Services.GetRequiredService(); + var snoozedUntilLocal = DateTime.Now.AddMinutes(snoozeDurationMinutes); + + await calendarService.SnoozeCalendarItemAsync(calendarItemId, snoozedUntilLocal).ConfigureAwait(false); + } + + private static bool TryGetSnoozeDurationMinutes(AppNotificationActivatedEventArgs toastArgs, out int snoozeDurationMinutes) + { + snoozeDurationMinutes = 0; + + if (toastArgs.UserInput == null || + !toastArgs.UserInput.TryGetValue(Constants.ToastCalendarSnoozeDurationInputId, out var selectedValue) || + selectedValue == null) + { + return false; + } + + var selectedText = selectedValue.ToString(); + + return int.TryParse(selectedText, out snoozeDurationMinutes) && snoozeDurationMinutes > 0; + } + + /// + /// Handles toast notification click for navigation. + /// Creates window if not running, sets up navigation parameter. + /// + private async Task HandleToastNavigationAsync(Guid mailItemUniqueId) + { + var mailService = Services.GetRequiredService(); + var navigationService = Services.GetRequiredService(); + + var account = await mailService.GetMailAccountByUniqueIdAsync(mailItemUniqueId); + if (account == null) return; + + var mailItem = await mailService.GetSingleMailItemAsync(mailItemUniqueId); + if (mailItem == null) return; + + var message = new AccountMenuItemExtended(mailItem.AssignedFolder.Id, mailItem); + + // Store navigation parameter in LaunchProtocolService so AppShell can pick it up. + var launchProtocolService = Services.GetRequiredService(); + launchProtocolService.LaunchParameter = message; + + // Create window if not already created. + if (!IsAppRunning()) + { + // Pass null for args since we're handling toast navigation + await CreateAndActivateWindow(null!); + navigationService.ChangeApplicationMode(Core.Domain.Enums.WinoApplicationMode.Mail); + } + else + { + // App is already running - send message and bring window to front. + navigationService.ChangeApplicationMode(Core.Domain.Enums.WinoApplicationMode.Mail); + WeakReferenceMessenger.Default.Send(message); + EnsureMainWindowVisibleAndForeground(); + } + } + + /// + /// Handles toast action button clicks (Mark as Read, Delete, etc.). + /// Executes the action without showing UI and exits the app. + /// + private async Task HandleToastActionAsync(MailOperation action, Guid mailItemUniqueId) + { + LogActivation($"Handling toast action: {action} for mail {mailItemUniqueId}"); + + var mailService = Services.GetRequiredService(); + var mailItem = await mailService.GetSingleMailItemAsync(mailItemUniqueId); + + if (mailItem == null) + { + LogActivation("Mail item not found. Exiting."); + ExitApplication(); + return; + } + + var package = new MailOperationPreperationRequest(action, mailItem); + + // Check if app is already running (has a window). + if (IsAppRunning()) + { + // App is running - use the simple delegator pattern. + // The synchronization will happen in the background. + LogActivation("App is running. Queueing request via delegator."); + + var delegator = Services.GetRequiredService(); + await delegator.ExecuteAsync(package); + + // Don't exit - app continues running. + LogActivation($"Toast action {action} queued successfully."); + } + else + { + // App is not running - we need to wait for sync before exiting. + LogActivation("App is not running. Executing synchronization and waiting for completion."); + + if (_synchronizationManager == null) + { + LogActivation("Synchronization manager is not initialized. Exiting."); + ExitApplication(); + return; + } + + var processor = Services.GetRequiredService(); + var notificationBuilder = Services.GetRequiredService(); + + // Prepare the requests for the action. + var requests = await processor.PrepareRequestsAsync(package); + + if (requests != null && requests.Any()) + { + // Group requests by account ID (usually just one account). + var accountIds = requests.GroupBy(a => a.Item.AssignedAccount.Id); + + foreach (var accountGroup in accountIds) + { + var accountId = accountGroup.Key; + + // Queue all requests for this account. + foreach (var request in accountGroup) + { + await _synchronizationManager.QueueRequestAsync(request, accountId, triggerSynchronization: false); + } + + // Create synchronization options to execute the queued requests. + var syncOptions = new MailSynchronizationOptions() + { + AccountId = accountId, + Type = MailSynchronizationType.ExecuteRequests + }; + + LogActivation($"Executing synchronization for account {accountId}..."); + + // Wait for synchronization to complete before exiting. + var syncResult = await _synchronizationManager.SynchronizeMailAsync(syncOptions); + + LogActivation($"Toast action {action} completed. Sync result: {syncResult.CompletedState}"); + } + + await notificationBuilder.UpdateTaskbarIconBadgeAsync(); + } + + LogActivation("Toast action handling complete. Exiting app."); + + // Exit the app after synchronization is complete. + ExitApplication(); + } + } + + /// + /// Creates the main window and activates it. + /// + private async Task CreateAndActivateWindow(Microsoft.UI.Xaml.LaunchActivatedEventArgs? args) + { + CreateWindow(args); + + // Initialize theme service after window is created. + await NewThemeService.InitializeAsync(); + + if (MainWindow != null) + Services.GetRequiredService().ActivateWindow(MainWindow); + + LogActivation("Window created and activated."); + } + + public Task OpenManageAccountsFromWelcomeAsync() + { + Services.GetRequiredService() + .Navigate(WinoPage.SettingsPage, WinoPage.ManageAccountsPage, NavigationReferenceFrame.ShellFrame, NavigationTransitionType.DrillIn); + return Task.CompletedTask; + } + + /// + /// Creates the main window without activating it. + /// Used for both normal launch and startup task launch (tray only). + /// + private void CreateWindow(Microsoft.UI.Xaml.LaunchActivatedEventArgs? args, + string? forcedLaunchArguments = null, + ShellModeActivationContext? activationContextOverride = null) + { + LogActivation("Creating main window."); + + var windowManager = Services.GetRequiredService(); + MainWindow = windowManager.CreateWindow(WinoWindowKind.Shell, () => new ShellWindow()); + InitializeNavigationDispatcher(); + + if (MainWindow is not IWinoShellWindow shellWindow) + throw new ArgumentException("MainWindow must implement IWinoShellWindow"); + + windowManager.SetPrimaryNavigationFrame(WinoWindowKind.Shell, shellWindow.GetMainFrame()); + + var navigationService = Services.GetRequiredService(); + var defaultMode = _preferencesService?.DefaultApplicationMode ?? WinoApplicationMode.Mail; + var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs(); + + if (activationContextOverride != null) + { + var targetMode = !string.IsNullOrWhiteSpace(forcedLaunchArguments) + ? AppModeActivationResolver.Resolve(forcedLaunchArguments, null, null, defaultMode) + : TryResolveActivationMode(activationArgs, defaultMode, out var resolvedActivationMode) + ? resolvedActivationMode + : AppModeActivationResolver.Resolve(args?.Arguments, GetCurrentLaunchTileId(), Environment.CommandLine, defaultMode); + + navigationService.ChangeApplicationMode(targetMode, activationContextOverride); + return; + } + + if (!string.IsNullOrWhiteSpace(forcedLaunchArguments)) + { + shellWindow.HandleAppActivation(forcedLaunchArguments); + return; + } + + if (activationArgs.Kind == ExtendedActivationKind.Launch && + activationArgs.Data is ILaunchActivatedEventArgs launchArgs) + { + var launchArguments = launchArgs.Arguments; + + if (Program.TryConsumeCurrentProcessAlternateModeOverride()) + { + launchArguments = AppendLaunchArgument(launchArguments, ToggleDefaultModeLaunchArgument); + } + + shellWindow.HandleAppActivation(launchArguments, launchArgs.TileId, Environment.CommandLine); + return; + } + + if (TryResolveActivationMode(activationArgs, defaultMode, out var activationMode)) + { + shellWindow.HandleAppActivation(GetModeLaunchArgument(activationMode)); + return; + } + + shellWindow.HandleAppActivation(args?.Arguments, GetCurrentLaunchTileId(), Environment.CommandLine); + } + + private void CreateWelcomeWindow() + { + LogActivation("Creating welcome window."); + + var windowManager = Services.GetRequiredService(); + MainWindow = windowManager.CreateWindow(WinoWindowKind.Welcome, () => new WelcomeWindow()); + if (MainWindow is WelcomeWindow welcomeWindow) + { + var rootFrame = welcomeWindow.GetRootFrame(); + windowManager.SetPrimaryNavigationFrame(WinoWindowKind.Welcome, rootFrame); + + if (rootFrame.Content is WelcomeHostPage welcomeHostPage) + { + welcomeHostPage.ResetWizard(); + } + else + { + rootFrame.BackStack.Clear(); + rootFrame.ForwardStack.Clear(); + rootFrame.Navigate(typeof(WelcomeHostPage), null, new SuppressNavigationTransitionInfo()); + } + } + + InitializeNavigationDispatcher(); + } + + private void InitializeNavigationDispatcher() + { + if (MainWindow == null) + return; + + if (Services.GetService() is WinUIDispatcher dispatcher) + { + dispatcher.Initialize(MainWindow.DispatcherQueue); + } + } + + private void EnsureMainWindowVisibleAndForeground() + { + var windowManager = Services.GetRequiredService(); + var currentWindow = windowManager.ActiveWindow + ?? windowManager.GetWindow(WinoWindowKind.Shell) + ?? windowManager.GetWindow(WinoWindowKind.Welcome) + ?? MainWindow; + + if (currentWindow == null) + return; + + MainWindow = currentWindow; + windowManager.ActivateWindow(currentWindow); + } + + private void RegisterRecipients() + { + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + } + + public async void Receive(NewMailSynchronizationRequested message) + { + if (_synchronizationManager == null) return; + + MailSynchronizationResult syncResult; + + try + { + syncResult = await _synchronizationManager.SynchronizeMailAsync(message.Options); + } + catch (Exception ex) + { + // Defensive fallback to guarantee completion message emission. + syncResult = MailSynchronizationResult.Failed(ex); + } + + WeakReferenceMessenger.Default.Send(new AccountSynchronizationCompleted( + message.Options.AccountId, + syncResult.CompletedState, + message.Options.GroupedSynchronizationTrackingId)); + + if (syncResult.CompletedState is SynchronizationCompletedState.Success or SynchronizationCompletedState.PartiallyCompleted) + { + await ClearInvalidCredentialAttentionIfNeededAsync(message.Options.AccountId).ConfigureAwait(false); + } + + if (syncResult.CompletedState == SynchronizationCompletedState.Failed || + syncResult.CompletedState == SynchronizationCompletedState.PartiallyCompleted) + { + var dialogService = Services.GetRequiredService(); + var errorMessage = GetSynchronizationFailureMessage(message.Options.Type, syncResult.Exception?.Message); + var severity = syncResult.CompletedState == SynchronizationCompletedState.PartiallyCompleted + ? InfoBarMessageType.Warning + : InfoBarMessageType.Error; + + dialogService.InfoBarMessage(Translator.Info_SyncFailedTitle, errorMessage, severity); + } + } + + public async void Receive(NewCalendarSynchronizationRequested message) + { + if (_synchronizationManager == null) return; + + var calendarSyncResult = await _synchronizationManager.SynchronizeCalendarAsync(message.Options); + + if (calendarSyncResult.CompletedState == SynchronizationCompletedState.Failed) + { + var dialogService = Services.GetRequiredService(); + dialogService.InfoBarMessage( + Translator.Info_SyncFailedTitle, + message.Options.Type switch + { + CalendarSynchronizationType.CalendarMetadata => Translator.Exception_FailedToSynchronizeCalendarMetadata, + CalendarSynchronizationType.Strict => Translator.Exception_FailedToSynchronizeCalendarData, + _ => Translator.Exception_FailedToSynchronizeCalendarEvents + }, + InfoBarMessageType.Error); + } + } + + public void Receive(AccountCreatedMessage message) + { + _hasConfiguredAccounts = true; + + var windowManager = Services.GetRequiredService(); + + // Only transition when the account was created from the WelcomeWindow. + if (windowManager.GetWindow(WinoWindowKind.Welcome) == null) + return; + + MainWindow?.DispatcherQueue?.TryEnqueue(async () => + { + // Create and activate ShellWindow — ActiveWindowChanged fires and rebinds the dispatcher. + CreateWindow(null, GetModeLaunchArgument(WinoApplicationMode.Mail)); + CloseWelcomeWindowIfPresent(); + if (MainWindow != null) + await ActivateWindowAsync(MainWindow); + + if (message.Account.IsCalendarAccessGranted) + { + WeakReferenceMessenger.Default.Send(new NewCalendarSynchronizationRequested(new CalendarSynchronizationOptions + { + AccountId = message.Account.Id, + Type = CalendarSynchronizationType.CalendarEvents + })); + } + + RestartAutoSynchronizationLoop(); + }); + } + + public void Receive(WelcomeImportCompletedMessage message) + { + _hasConfiguredAccounts = message.ImportedMailboxCount > 0; + + var windowManager = Services.GetRequiredService(); + if (windowManager.GetWindow(WinoWindowKind.Welcome) == null) + return; + + MainWindow?.DispatcherQueue?.TryEnqueue(async () => + { + if (_preferencesService != null) + { + _preferencesService.PreferenceChanged -= PreferencesServiceChanged; + _preferencesService.PreferenceChanged += PreferencesServiceChanged; + } + + CreateWindow( + null, + GetModeLaunchArgument(WinoApplicationMode.Mail), + new ShellModeActivationContext + { + SuppressStartupFlows = true + }); + + await LoadInitialWinoAccountAsync(); + CloseWelcomeWindowIfPresent(); + + if (MainWindow != null) + { + await ActivateWindowAsync(MainWindow); + } + + RestartAutoSynchronizationLoop(); + + Services.GetRequiredService().InfoBarMessage( + Translator.GeneralTitle_Info, + Translator.WinoAccount_Management_ImportReloginReminder, + InfoBarMessageType.Information); + }); + } + + public void Receive(AccountRemovedMessage message) + { + var windowManager = Services.GetRequiredService(); + + // Only handle when ShellWindow is active (not during wizard rollback) + if (windowManager.GetWindow(WinoWindowKind.Shell) == null) + return; + + MainWindow?.DispatcherQueue?.TryEnqueue(async () => + { + var accounts = await _accountService!.GetAccountsAsync(); + _hasConfiguredAccounts = accounts.Any(); + if (_hasConfiguredAccounts) return; + + // All accounts removed — go back to welcome wizard from step 1 + Services.GetRequiredService().Reset(); + StopAutoSynchronizationLoop(); + CloseShellWindowIfPresent(); + CreateWelcomeWindow(); + if (MainWindow != null) + await ActivateWindowAsync(MainWindow); + }); + } + + public void Receive(GetStartedFromWelcomeRequested message) + { + var windowManager = Services.GetRequiredService(); + + if (windowManager.GetWindow(WinoWindowKind.Welcome) == null) + return; + + MainWindow?.DispatcherQueue?.TryEnqueue(async () => + { + CreateWindow(null); + windowManager.HideWindow(WinoWindowKind.Welcome); + await NewThemeService.ApplyThemeToActiveWindowAsync(); + MainWindow?.Activate(); + }); + } + + private static string GetSynchronizationFailureMessage(MailSynchronizationType synchronizationType, string? exceptionMessage) + { + if (!string.IsNullOrWhiteSpace(exceptionMessage)) + { + return exceptionMessage; + } + + return synchronizationType switch + { + MailSynchronizationType.Alias => Translator.Exception_FailedToSynchronizeAliases, + MailSynchronizationType.UpdateProfile => Translator.Exception_FailedToSynchronizeProfileInformation, + _ => Translator.Exception_FailedToSynchronizeFolders + }; + } + + private void PreferencesServiceChanged(object? sender, string propertyName) + { + if (propertyName != nameof(IPreferencesService.EmailSyncIntervalMinutes)) + return; + + RestartAutoSynchronizationLoop(); + } + + private void RestartAutoSynchronizationLoop() + { + if (_preferencesService == null) + return; + + StopAutoSynchronizationLoop(); + + int intervalMinutes = Math.Max(1, _preferencesService.EmailSyncIntervalMinutes); + _autoSynchronizationLoopCts = new CancellationTokenSource(); + + _ = RunAutoSynchronizationLoopAsync(TimeSpan.FromMinutes(intervalMinutes), _autoSynchronizationLoopCts.Token); + LogActivation($"Automatic sync loop started. Interval: {intervalMinutes} minute(s)."); + } + + private void StopAutoSynchronizationLoop() + { + if (_autoSynchronizationLoopCts == null) + return; + + _autoSynchronizationLoopCts.Cancel(); + _autoSynchronizationLoopCts.Dispose(); + _autoSynchronizationLoopCts = null; + } + + private async Task LoadInitialWinoAccountAsync() + { + var winoAccountProfileService = Services.GetRequiredService(); + var winoAccount = await winoAccountProfileService.GetActiveAccountAsync().ConfigureAwait(false); + + if (winoAccount != null) + { + WeakReferenceMessenger.Default.Send(new WinoAccountProfileUpdatedMessage(winoAccount)); + } + } + + private async Task RunAutoSynchronizationLoopAsync(TimeSpan interval, CancellationToken cancellationToken) + { + try + { + await ExecuteAutoSynchronizationAsync(cancellationToken).ConfigureAwait(false); + + using var timer = new PeriodicTimer(interval); + + while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false)) + { + await ExecuteAutoSynchronizationAsync(cancellationToken).ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + // no-op + } + catch (Exception ex) + { + LogActivation($"Automatic sync loop failed: {ex.Message}"); + } + } + + private async Task ExecuteAutoSynchronizationAsync(CancellationToken cancellationToken) + { + if (_synchronizationManager == null || _accountService == null) + return; + + bool lockTaken = false; + + try + { + lockTaken = await _autoSynchronizationSemaphore.WaitAsync(0, cancellationToken).ConfigureAwait(false); + if (!lockTaken) + return; + + var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false); + var currentAccountIds = accounts.Select(a => a.Id).ToHashSet(); + foreach (var staleAccountId in _inboxSyncCounters.Keys.Where(a => !currentAccountIds.Contains(a)).ToList()) + { + _inboxSyncCounters.TryRemove(staleAccountId, out _); + } + + var synchronizationTasks = accounts + .Select(account => ExecuteAutoSynchronizationForAccountAsync(account, cancellationToken)) + .ToList(); + + await Task.WhenAll(synchronizationTasks).ConfigureAwait(false); + } + finally + { + if (lockTaken) + { + _autoSynchronizationSemaphore.Release(); + } + } + } + + private async Task ExecuteAutoSynchronizationForAccountAsync(Wino.Core.Domain.Entities.Shared.MailAccount account, CancellationToken cancellationToken) + { + if (_synchronizationManager == null) + return; + + cancellationToken.ThrowIfCancellationRequested(); + + if (_synchronizationManager.IsAccountSynchronizing(account.Id)) + return; + + var inboxSyncOptions = new MailSynchronizationOptions + { + AccountId = account.Id, + Type = MailSynchronizationType.InboxOnly + }; + + var inboxSyncResult = await _synchronizationManager.SynchronizeMailAsync(inboxSyncOptions, cancellationToken).ConfigureAwait(false); + + if (inboxSyncResult.CompletedState is SynchronizationCompletedState.Success or SynchronizationCompletedState.PartiallyCompleted) + { + await ClearInvalidCredentialAttentionIfNeededAsync(account.Id).ConfigureAwait(false); + + var inboxSyncCount = _inboxSyncCounters.AddOrUpdate(account.Id, 1, (_, currentCount) => currentCount + 1); + + if (inboxSyncCount >= InboxSyncsPerFullSync) + { + var fullSyncOptions = new MailSynchronizationOptions + { + AccountId = account.Id, + Type = MailSynchronizationType.FullFolders + }; + + await _synchronizationManager.SynchronizeMailAsync(fullSyncOptions, cancellationToken).ConfigureAwait(false); + _inboxSyncCounters[account.Id] = 0; + } + } + + if (!account.IsCalendarAccessGranted) + return; + + var calendarOptions = new CalendarSynchronizationOptions + { + AccountId = account.Id, + Type = CalendarSynchronizationType.CalendarMetadata + }; + + await _synchronizationManager.SynchronizeCalendarAsync(calendarOptions, cancellationToken).ConfigureAwait(false); + } + + private async Task ClearInvalidCredentialAttentionIfNeededAsync(Guid accountId) + { + if (_accountService == null) + return; + + var account = await _accountService.GetAccountAsync(accountId).ConfigureAwait(false); + + if (account?.AttentionReason != AccountAttentionReason.InvalidCredentials) + return; + + await _accountService.ClearAccountAttentionAsync(accountId).ConfigureAwait(false); + } + + /// + /// Handles activation redirected from another instance (single-instancing). + /// This is called when a second instance tries to launch and redirects to this existing instance. + /// + public void HandleRedirectedActivation(AppActivationArguments args) + { + // Dispatch to UI thread since this is called from Program.OnActivated + MainWindow?.DispatcherQueue.TryEnqueue(async () => + { + // Handle different activation kinds + if (args.Kind == ExtendedActivationKind.AppNotification) + { + // Handle toast notification activation + var toastArgs = (AppNotificationActivatedEventArgs)args.Data; + _ = HandleToastActivationAsync(toastArgs); + } + else + { + if (MainWindow is IWinoShellWindow shellWindow) + { + if (args.Kind == ExtendedActivationKind.Launch && + args.Data is ILaunchActivatedEventArgs launchArgs) + { + var launchArguments = launchArgs.Arguments; + + if (Program.TryConsumeRedirectedAlternateModeOverride()) + { + launchArguments = AppendLaunchArgument(launchArguments, ToggleDefaultModeLaunchArgument); + } + + shellWindow.HandleAppActivation(launchArguments, launchArgs.TileId); + } + else if (TryResolveActivationMode(args, _preferencesService?.DefaultApplicationMode ?? WinoApplicationMode.Mail, out var redirectedMode)) + { + shellWindow.HandleAppActivation(GetModeLaunchArgument(redirectedMode)); + } + } + + await HandlePostActivationAsync(args); + + // Bring the existing window to front after handling redirected activation. + MainWindow?.BringToFront(); + MainWindow?.Activate(); + } + }); + } + + private static string GetModeLaunchArgument(WinoApplicationMode mode) + => mode switch + { + WinoApplicationMode.Calendar => "--mode=calendar", + WinoApplicationMode.Contacts => "--mode=contacts", + WinoApplicationMode.Settings => "--mode=settings", + _ => "--mode=mail" + }; + + private static string AppendLaunchArgument(string? launchArguments, string launchArgument) + { + return string.IsNullOrWhiteSpace(launchArguments) + ? launchArgument + : $"{launchArguments} {launchArgument}"; + } + + private static bool TryResolveActivationMode(AppActivationArguments activationArgs, WinoApplicationMode defaultMode, out WinoApplicationMode mode) + { + mode = defaultMode; + + if (activationArgs.Kind == ExtendedActivationKind.Protocol && + activationArgs.Data is IProtocolActivatedEventArgs protocolArgs) + { + var scheme = protocolArgs.Uri?.Scheme; + + if (string.Equals(scheme, "webcal", StringComparison.OrdinalIgnoreCase) || + string.Equals(scheme, "webcals", StringComparison.OrdinalIgnoreCase)) + { + mode = WinoApplicationMode.Calendar; + return true; + } + + if (string.Equals(scheme, "mailto", StringComparison.OrdinalIgnoreCase) || + string.Equals(scheme, "google.pw.oauth2", StringComparison.OrdinalIgnoreCase)) + { + mode = WinoApplicationMode.Mail; + return true; + } + + if (string.Equals(scheme, WinoProtocolScheme, StringComparison.OrdinalIgnoreCase) && + string.Equals(protocolArgs.Uri?.Host, BillingProtocolHost, StringComparison.OrdinalIgnoreCase)) + { + mode = WinoApplicationMode.Settings; + return true; + } + } + + if (activationArgs.Kind == ExtendedActivationKind.File && + activationArgs.Data is IFileActivatedEventArgs fileArgs) + { + var fileItem = fileArgs.Files?.FirstOrDefault(); + var extension = Path.GetExtension(fileItem?.Name ?? string.Empty); + + if (string.Equals(extension, ".ics", StringComparison.OrdinalIgnoreCase)) + { + mode = WinoApplicationMode.Calendar; + return true; + } + + if (string.Equals(extension, ".eml", StringComparison.OrdinalIgnoreCase)) + { + mode = WinoApplicationMode.Mail; + return true; + } + } + + if (activationArgs.Kind == ExtendedActivationKind.Launch && + activationArgs.Data is ILaunchActivatedEventArgs launchArgs) + { + mode = AppModeActivationResolver.Resolve(launchArgs.Arguments, launchArgs.TileId, null, defaultMode); + return true; + } + + return false; + } + + private static string? GetCurrentLaunchTileId() + { + var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs(); + + if (activationArgs.Kind == ExtendedActivationKind.Launch && + activationArgs.Data is ILaunchActivatedEventArgs launchArgs) + { + return launchArgs.TileId; + } + + return null; + } + + private async Task HandlePostActivationAsync(AppActivationArguments activationArgs) + { + if (await TryHandleBillingProtocolActivationAsync(activationArgs).ConfigureAwait(false)) + { + return; + } + } + + private async Task TryHandleBillingProtocolActivationAsync(AppActivationArguments activationArgs) + { + if (!TryGetBillingCallbackUri(activationArgs, out var callbackUri)) + { + return false; + } + + Services.GetRequiredService().Navigate( + WinoPage.SettingsPage, + WinoPage.WinoAccountManagementPage, + NavigationReferenceFrame.ShellFrame, + NavigationTransitionType.None); + + var winoAccountProfileService = Services.GetRequiredService(); + await winoAccountProfileService.ProcessBillingCallbackAsync(callbackUri).ConfigureAwait(false); + return true; + } + + private static bool TryGetBillingCallbackUri(AppActivationArguments activationArgs, out Uri callbackUri) + { + callbackUri = null!; + + if (activationArgs.Kind != ExtendedActivationKind.Protocol || + activationArgs.Data is not IProtocolActivatedEventArgs protocolArgs || + protocolArgs.Uri == null) + { + return false; + } + + var uri = protocolArgs.Uri; + if (!string.Equals(uri.Scheme, WinoProtocolScheme, StringComparison.OrdinalIgnoreCase) || + !string.Equals(uri.Host, BillingProtocolHost, StringComparison.OrdinalIgnoreCase) || + !string.Equals(uri.AbsolutePath, BillingSuccessPath, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + callbackUri = uri; + return true; + } +} + + diff --git a/Wino.Core.UWP/AppThemes/Acrylic.xaml b/Wino.Mail.WinUI/AppThemes/Acrylic.xaml similarity index 65% rename from Wino.Core.UWP/AppThemes/Acrylic.xaml rename to Wino.Mail.WinUI/AppThemes/Acrylic.xaml index fb84bf43..0d26ea38 100644 --- a/Wino.Core.UWP/AppThemes/Acrylic.xaml +++ b/Wino.Mail.WinUI/AppThemes/Acrylic.xaml @@ -1,11 +1,10 @@ - + xmlns:xaml="using:Microsoft.UI.Xaml"> Acrylic - False Transparent @@ -14,10 +13,12 @@ #ecf0f1 + #B2FCFCFC + #D9ECEFF1 + #4D0078D4 @@ -25,10 +26,12 @@ #2C2C2C + #662C2C2C + #992C2C2C + #66399BFF diff --git a/Wino.Core.UWP/AppThemes/Clouds.xaml b/Wino.Mail.WinUI/AppThemes/Clouds.xaml similarity index 56% rename from Wino.Core.UWP/AppThemes/Clouds.xaml rename to Wino.Mail.WinUI/AppThemes/Clouds.xaml index ca234cc1..7bd2507f 100644 --- a/Wino.Core.UWP/AppThemes/Clouds.xaml +++ b/Wino.Mail.WinUI/AppThemes/Clouds.xaml @@ -1,11 +1,10 @@ - + xmlns:xaml="using:Microsoft.UI.Xaml"> Clouds - ms-appx:///Wino.Core.UWP/BackgroundImages/Clouds.jpg - False + ms-appx:///BackgroundImages/Clouds.jpg Transparent @@ -13,11 +12,17 @@ #b2dffc + #33B2DFFC + #66B2DFFC + #4D0078D4 #222f3e #b2dffc + #33B2DFFC + #66B2DFFC + #66399BFF #222f3e diff --git a/Wino.Core.UWP/AppThemes/Custom.xaml b/Wino.Mail.WinUI/AppThemes/Custom.xaml similarity index 75% rename from Wino.Core.UWP/AppThemes/Custom.xaml rename to Wino.Mail.WinUI/AppThemes/Custom.xaml index 71bbf1a3..fc56711b 100644 --- a/Wino.Core.UWP/AppThemes/Custom.xaml +++ b/Wino.Mail.WinUI/AppThemes/Custom.xaml @@ -1,11 +1,10 @@ - + xmlns:xaml="using:Microsoft.UI.Xaml"> Custom ms-appdata:///local/CustomWallpaper.jpg - False #ecf0f1 #D9FFFFFF + + + #4D0078D4 @@ -34,6 +36,9 @@ #1f1f1f #E61F1F1F + + + #66399BFF diff --git a/Wino.Core.UWP/AppThemes/Mica.xaml b/Wino.Mail.WinUI/AppThemes/Default.xaml similarity index 54% rename from Wino.Core.UWP/AppThemes/Mica.xaml rename to Wino.Mail.WinUI/AppThemes/Default.xaml index 302451b4..0a1c448e 100644 --- a/Wino.Core.UWP/AppThemes/Mica.xaml +++ b/Wino.Mail.WinUI/AppThemes/Default.xaml @@ -1,10 +1,9 @@ - + xmlns:xaml="using:Microsoft.UI.Xaml"> - Mica - True + Default Transparent Transparent @@ -14,9 +13,15 @@ #ecf0f1 + #F7F9FA + #DFE4EA + #4D0078D4 #1f1f1f + #1F1F1F + #262626 + #66399BFF diff --git a/Wino.Core.UWP/AppThemes/Forest.xaml b/Wino.Mail.WinUI/AppThemes/Forest.xaml similarity index 52% rename from Wino.Core.UWP/AppThemes/Forest.xaml rename to Wino.Mail.WinUI/AppThemes/Forest.xaml index 63ab19a1..83288e03 100644 --- a/Wino.Core.UWP/AppThemes/Forest.xaml +++ b/Wino.Mail.WinUI/AppThemes/Forest.xaml @@ -1,11 +1,10 @@ - + xmlns:xaml="using:Microsoft.UI.Xaml"> Forest - ms-appx:///Wino.Core.UWP/BackgroundImages/Forest.jpg - False + ms-appx:///BackgroundImages/Forest.jpg Transparent @@ -13,9 +12,15 @@ #A800D608 + #2200D608 + #4D00D608 + #4D0078D4 #59001C01 + #22001C01 + #59001C01 + #66399BFF diff --git a/Wino.Core.UWP/AppThemes/Garden.xaml b/Wino.Mail.WinUI/AppThemes/Garden.xaml similarity index 55% rename from Wino.Core.UWP/AppThemes/Garden.xaml rename to Wino.Mail.WinUI/AppThemes/Garden.xaml index cf238b79..85b3adbe 100644 --- a/Wino.Core.UWP/AppThemes/Garden.xaml +++ b/Wino.Mail.WinUI/AppThemes/Garden.xaml @@ -1,11 +1,10 @@ - + xmlns:xaml="using:Microsoft.UI.Xaml"> Garden - ms-appx:///Wino.Core.UWP/BackgroundImages/Garden.jpg - False + ms-appx:///BackgroundImages/Garden.jpg Transparent @@ -13,12 +12,18 @@ #dcfad8 + #26DCFAD8 + #59DCFAD8 + #4D0078D4 #576574 #dcfad8 + #26576574 + #59576574 + #66399BFF diff --git a/Wino.Core.UWP/AppThemes/Nighty.xaml b/Wino.Mail.WinUI/AppThemes/Nighty.xaml similarity index 53% rename from Wino.Core.UWP/AppThemes/Nighty.xaml rename to Wino.Mail.WinUI/AppThemes/Nighty.xaml index 51de8e5f..5bfcfaa3 100644 --- a/Wino.Core.UWP/AppThemes/Nighty.xaml +++ b/Wino.Mail.WinUI/AppThemes/Nighty.xaml @@ -1,11 +1,10 @@ - + xmlns:xaml="using:Microsoft.UI.Xaml"> Nighty - ms-appx:///Wino.Core.UWP/BackgroundImages/Nighty.jpg - False + ms-appx:///BackgroundImages/Nighty.jpg Transparent @@ -14,10 +13,16 @@ #fdcb6e + #33FDCB6E + #66FDCB6E + #4D0078D4 #5413191F + #2213191F + #5413191F + #66399BFF diff --git a/Wino.Core.UWP/AppThemes/Snowflake.xaml b/Wino.Mail.WinUI/AppThemes/Snowflake.xaml similarity index 53% rename from Wino.Core.UWP/AppThemes/Snowflake.xaml rename to Wino.Mail.WinUI/AppThemes/Snowflake.xaml index 3358e139..b768b91e 100644 --- a/Wino.Core.UWP/AppThemes/Snowflake.xaml +++ b/Wino.Mail.WinUI/AppThemes/Snowflake.xaml @@ -1,11 +1,10 @@ - + xmlns:xaml="using:Microsoft.UI.Xaml"> Snowflake - ms-appx:///Wino.Core.UWP/BackgroundImages/Snowflake.jpg - False + ms-appx:///BackgroundImages/Snowflake.jpg Transparent @@ -14,10 +13,16 @@ #b0c6dd + #33B0C6DD + #66B0C6DD + #4D0078D4 #b0c6dd + #33B0C6DD + #66B0C6DD + #66399BFF diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/LargeTile.scale-100.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/LargeTile.scale-100.png new file mode 100644 index 00000000..30488703 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/LargeTile.scale-100.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/LargeTile.scale-125.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/LargeTile.scale-125.png new file mode 100644 index 00000000..78a99689 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/LargeTile.scale-125.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/LargeTile.scale-150.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/LargeTile.scale-150.png new file mode 100644 index 00000000..7f923b86 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/LargeTile.scale-150.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/LargeTile.scale-200.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/LargeTile.scale-200.png new file mode 100644 index 00000000..8532ea41 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/LargeTile.scale-200.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/LargeTile.scale-400.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/LargeTile.scale-400.png new file mode 100644 index 00000000..7c65c063 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/LargeTile.scale-400.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SmallTile.scale-100.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SmallTile.scale-100.png new file mode 100644 index 00000000..7a1eaedb Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SmallTile.scale-100.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SmallTile.scale-125.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SmallTile.scale-125.png new file mode 100644 index 00000000..40b3ab4c Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SmallTile.scale-125.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SmallTile.scale-150.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SmallTile.scale-150.png new file mode 100644 index 00000000..cc7da075 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SmallTile.scale-150.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SmallTile.scale-200.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SmallTile.scale-200.png new file mode 100644 index 00000000..26b910f6 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SmallTile.scale-200.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SmallTile.scale-400.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SmallTile.scale-400.png new file mode 100644 index 00000000..cfaabcdb Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SmallTile.scale-400.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SplashScreen.scale-100.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SplashScreen.scale-100.png new file mode 100644 index 00000000..169b6db6 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SplashScreen.scale-100.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SplashScreen.scale-125.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SplashScreen.scale-125.png new file mode 100644 index 00000000..437d10eb Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SplashScreen.scale-125.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SplashScreen.scale-150.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SplashScreen.scale-150.png new file mode 100644 index 00000000..0bafaa6d Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SplashScreen.scale-150.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SplashScreen.scale-200.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SplashScreen.scale-200.png new file mode 100644 index 00000000..4313f73e Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SplashScreen.scale-200.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SplashScreen.scale-400.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SplashScreen.scale-400.png new file mode 100644 index 00000000..7a224a2b Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/SplashScreen.scale-400.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square150x150Logo.scale-100.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square150x150Logo.scale-100.png new file mode 100644 index 00000000..9f36f4eb Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square150x150Logo.scale-100.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square150x150Logo.scale-125.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square150x150Logo.scale-125.png new file mode 100644 index 00000000..65c3d34d Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square150x150Logo.scale-125.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square150x150Logo.scale-150.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square150x150Logo.scale-150.png new file mode 100644 index 00000000..8105f621 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square150x150Logo.scale-150.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square150x150Logo.scale-200.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square150x150Logo.scale-200.png new file mode 100644 index 00000000..5a18cde3 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square150x150Logo.scale-200.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square150x150Logo.scale-400.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square150x150Logo.scale-400.png new file mode 100644 index 00000000..ddd238bc Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square150x150Logo.scale-400.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-lightunplated_targetsize-16.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-lightunplated_targetsize-16.png new file mode 100644 index 00000000..e756568f Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-lightunplated_targetsize-16.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-lightunplated_targetsize-24.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-lightunplated_targetsize-24.png new file mode 100644 index 00000000..290e89c2 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-lightunplated_targetsize-24.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-lightunplated_targetsize-256.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-lightunplated_targetsize-256.png new file mode 100644 index 00000000..d4cf69f0 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-lightunplated_targetsize-256.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-lightunplated_targetsize-32.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-lightunplated_targetsize-32.png new file mode 100644 index 00000000..c1ce2f33 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-lightunplated_targetsize-32.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-lightunplated_targetsize-48.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-lightunplated_targetsize-48.png new file mode 100644 index 00000000..87e80532 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-lightunplated_targetsize-48.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-unplated_targetsize-16.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-unplated_targetsize-16.png new file mode 100644 index 00000000..e756568f Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-unplated_targetsize-16.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-unplated_targetsize-24.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-unplated_targetsize-24.png new file mode 100644 index 00000000..290e89c2 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-unplated_targetsize-24.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-unplated_targetsize-256.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-unplated_targetsize-256.png new file mode 100644 index 00000000..d4cf69f0 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-unplated_targetsize-256.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-unplated_targetsize-32.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-unplated_targetsize-32.png new file mode 100644 index 00000000..c1ce2f33 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-unplated_targetsize-32.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-unplated_targetsize-48.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-unplated_targetsize-48.png new file mode 100644 index 00000000..87e80532 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.altform-unplated_targetsize-48.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.scale-100.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.scale-100.png new file mode 100644 index 00000000..7345b443 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.scale-100.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.scale-125.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.scale-125.png new file mode 100644 index 00000000..2afb3113 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.scale-125.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.scale-150.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.scale-150.png new file mode 100644 index 00000000..8114e111 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.scale-150.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.scale-200.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.scale-200.png new file mode 100644 index 00000000..96b4ccf4 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.scale-200.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.scale-400.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.scale-400.png new file mode 100644 index 00000000..6383ffc3 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.scale-400.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.targetsize-16.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.targetsize-16.png new file mode 100644 index 00000000..e756568f Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.targetsize-16.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.targetsize-24.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.targetsize-24.png new file mode 100644 index 00000000..290e89c2 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.targetsize-24.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.targetsize-24_altform-unplated.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 00000000..290e89c2 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.targetsize-256.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.targetsize-256.png new file mode 100644 index 00000000..d4cf69f0 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.targetsize-256.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.targetsize-32.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.targetsize-32.png new file mode 100644 index 00000000..c1ce2f33 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.targetsize-32.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.targetsize-48.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.targetsize-48.png new file mode 100644 index 00000000..87e80532 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Square44x44Logo.targetsize-48.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/StoreLogo.scale-100.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/StoreLogo.scale-100.png new file mode 100644 index 00000000..640e8767 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/StoreLogo.scale-100.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/StoreLogo.scale-125.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/StoreLogo.scale-125.png new file mode 100644 index 00000000..97cf7cbb Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/StoreLogo.scale-125.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/StoreLogo.scale-150.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/StoreLogo.scale-150.png new file mode 100644 index 00000000..d4a71a12 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/StoreLogo.scale-150.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/StoreLogo.scale-200.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/StoreLogo.scale-200.png new file mode 100644 index 00000000..3512de10 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/StoreLogo.scale-200.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/StoreLogo.scale-400.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/StoreLogo.scale-400.png new file mode 100644 index 00000000..908fd71f Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/StoreLogo.scale-400.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Wide310x150Logo.scale-100.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Wide310x150Logo.scale-100.png new file mode 100644 index 00000000..cb9dbd55 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Wide310x150Logo.scale-100.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Wide310x150Logo.scale-125.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Wide310x150Logo.scale-125.png new file mode 100644 index 00000000..1cf7b58d Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Wide310x150Logo.scale-125.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Wide310x150Logo.scale-150.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Wide310x150Logo.scale-150.png new file mode 100644 index 00000000..2ba7fb5d Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Wide310x150Logo.scale-150.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Wide310x150Logo.scale-200.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Wide310x150Logo.scale-200.png new file mode 100644 index 00000000..169b6db6 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Wide310x150Logo.scale-200.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Wide310x150Logo.scale-400.png b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Wide310x150Logo.scale-400.png new file mode 100644 index 00000000..4313f73e Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/CalendarAssets/Wide310x150Logo.scale-400.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/LargeTile.scale-100.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/LargeTile.scale-100.png new file mode 100644 index 00000000..27dad70a Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/LargeTile.scale-100.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/LargeTile.scale-125.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/LargeTile.scale-125.png new file mode 100644 index 00000000..fd560f24 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/LargeTile.scale-125.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/LargeTile.scale-150.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/LargeTile.scale-150.png new file mode 100644 index 00000000..c92234dc Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/LargeTile.scale-150.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/LargeTile.scale-200.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/LargeTile.scale-200.png new file mode 100644 index 00000000..e503e856 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/LargeTile.scale-200.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/LargeTile.scale-400.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/LargeTile.scale-400.png new file mode 100644 index 00000000..dffa1286 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/LargeTile.scale-400.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SmallTile.scale-100.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SmallTile.scale-100.png new file mode 100644 index 00000000..dc8cdc7c Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SmallTile.scale-100.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SmallTile.scale-125.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SmallTile.scale-125.png new file mode 100644 index 00000000..16279dd5 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SmallTile.scale-125.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SmallTile.scale-150.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SmallTile.scale-150.png new file mode 100644 index 00000000..2ca3b32b Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SmallTile.scale-150.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SmallTile.scale-200.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SmallTile.scale-200.png new file mode 100644 index 00000000..2986f315 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SmallTile.scale-200.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SmallTile.scale-400.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SmallTile.scale-400.png new file mode 100644 index 00000000..0bc1d256 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SmallTile.scale-400.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SplashScreen.scale-100.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SplashScreen.scale-100.png new file mode 100644 index 00000000..4d52d609 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SplashScreen.scale-100.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SplashScreen.scale-125.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SplashScreen.scale-125.png new file mode 100644 index 00000000..59a21e15 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SplashScreen.scale-125.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SplashScreen.scale-150.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SplashScreen.scale-150.png new file mode 100644 index 00000000..2a5ec8c1 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SplashScreen.scale-150.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SplashScreen.scale-200.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SplashScreen.scale-200.png new file mode 100644 index 00000000..e51bafd4 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SplashScreen.scale-200.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SplashScreen.scale-400.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SplashScreen.scale-400.png new file mode 100644 index 00000000..6c28797e Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/SplashScreen.scale-400.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square150x150Logo.scale-100.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square150x150Logo.scale-100.png new file mode 100644 index 00000000..1133e445 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square150x150Logo.scale-100.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square150x150Logo.scale-125.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square150x150Logo.scale-125.png new file mode 100644 index 00000000..2f7bf28e Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square150x150Logo.scale-125.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square150x150Logo.scale-150.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square150x150Logo.scale-150.png new file mode 100644 index 00000000..98fb8d4e Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square150x150Logo.scale-150.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square150x150Logo.scale-200.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square150x150Logo.scale-200.png new file mode 100644 index 00000000..97ed4a03 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square150x150Logo.scale-200.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square150x150Logo.scale-400.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square150x150Logo.scale-400.png new file mode 100644 index 00000000..1a65fbc7 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square150x150Logo.scale-400.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-lightunplated_targetsize-16.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-lightunplated_targetsize-16.png new file mode 100644 index 00000000..0faa7aae Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-lightunplated_targetsize-16.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-lightunplated_targetsize-24.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-lightunplated_targetsize-24.png new file mode 100644 index 00000000..58a0504f Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-lightunplated_targetsize-24.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-lightunplated_targetsize-256.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-lightunplated_targetsize-256.png new file mode 100644 index 00000000..a8e4e336 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-lightunplated_targetsize-256.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-lightunplated_targetsize-32.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-lightunplated_targetsize-32.png new file mode 100644 index 00000000..e041f59b Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-lightunplated_targetsize-32.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-lightunplated_targetsize-48.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-lightunplated_targetsize-48.png new file mode 100644 index 00000000..229cca05 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-lightunplated_targetsize-48.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-unplated_targetsize-16.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-unplated_targetsize-16.png new file mode 100644 index 00000000..0faa7aae Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-unplated_targetsize-16.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-unplated_targetsize-24.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-unplated_targetsize-24.png new file mode 100644 index 00000000..58a0504f Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-unplated_targetsize-24.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-unplated_targetsize-256.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-unplated_targetsize-256.png new file mode 100644 index 00000000..a8e4e336 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-unplated_targetsize-256.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-unplated_targetsize-32.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-unplated_targetsize-32.png new file mode 100644 index 00000000..e041f59b Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-unplated_targetsize-32.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-unplated_targetsize-48.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-unplated_targetsize-48.png new file mode 100644 index 00000000..229cca05 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.altform-unplated_targetsize-48.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.scale-100.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.scale-100.png new file mode 100644 index 00000000..0e9904a0 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.scale-100.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.scale-125.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.scale-125.png new file mode 100644 index 00000000..c9022704 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.scale-125.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.scale-150.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.scale-150.png new file mode 100644 index 00000000..64425f74 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.scale-150.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.scale-200.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.scale-200.png new file mode 100644 index 00000000..93a17783 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.scale-200.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.scale-400.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.scale-400.png new file mode 100644 index 00000000..ca07113e Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.scale-400.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.targetsize-16.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.targetsize-16.png new file mode 100644 index 00000000..0faa7aae Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.targetsize-16.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.targetsize-24.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.targetsize-24.png new file mode 100644 index 00000000..58a0504f Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.targetsize-24.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.targetsize-24_altform-unplated.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 00000000..58a0504f Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.targetsize-256.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.targetsize-256.png new file mode 100644 index 00000000..a8e4e336 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.targetsize-256.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.targetsize-32.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.targetsize-32.png new file mode 100644 index 00000000..e041f59b Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.targetsize-32.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.targetsize-48.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.targetsize-48.png new file mode 100644 index 00000000..229cca05 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Square44x44Logo.targetsize-48.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/StoreLogo.scale-100.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/StoreLogo.scale-100.png new file mode 100644 index 00000000..9881e699 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/StoreLogo.scale-100.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/StoreLogo.scale-125.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/StoreLogo.scale-125.png new file mode 100644 index 00000000..b57a5960 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/StoreLogo.scale-125.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/StoreLogo.scale-150.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/StoreLogo.scale-150.png new file mode 100644 index 00000000..8fa6592f Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/StoreLogo.scale-150.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/StoreLogo.scale-200.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/StoreLogo.scale-200.png new file mode 100644 index 00000000..5ef3b658 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/StoreLogo.scale-200.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/StoreLogo.scale-400.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/StoreLogo.scale-400.png new file mode 100644 index 00000000..cfd23b34 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/StoreLogo.scale-400.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Wide310x150Logo.scale-100.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Wide310x150Logo.scale-100.png new file mode 100644 index 00000000..75a12d76 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Wide310x150Logo.scale-100.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Wide310x150Logo.scale-125.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Wide310x150Logo.scale-125.png new file mode 100644 index 00000000..54d68b2e Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Wide310x150Logo.scale-125.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Wide310x150Logo.scale-150.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Wide310x150Logo.scale-150.png new file mode 100644 index 00000000..8630d7b6 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Wide310x150Logo.scale-150.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Wide310x150Logo.scale-200.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Wide310x150Logo.scale-200.png new file mode 100644 index 00000000..4d52d609 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Wide310x150Logo.scale-200.png differ diff --git a/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Wide310x150Logo.scale-400.png b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Wide310x150Logo.scale-400.png new file mode 100644 index 00000000..e51bafd4 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/AppEntries/MailAssets/Wide310x150Logo.scale-400.png differ diff --git a/Wino.Mail/Assets/EML/eml.png b/Wino.Mail.WinUI/Assets/EML/eml.png similarity index 100% rename from Wino.Mail/Assets/EML/eml.png rename to Wino.Mail.WinUI/Assets/EML/eml.png diff --git a/Wino.Core.UWP/Assets/FileTypes/type_archive.png b/Wino.Mail.WinUI/Assets/FileTypes/type_archive.png similarity index 100% rename from Wino.Core.UWP/Assets/FileTypes/type_archive.png rename to Wino.Mail.WinUI/Assets/FileTypes/type_archive.png diff --git a/Wino.Core.UWP/Assets/FileTypes/type_audio.png b/Wino.Mail.WinUI/Assets/FileTypes/type_audio.png similarity index 100% rename from Wino.Core.UWP/Assets/FileTypes/type_audio.png rename to Wino.Mail.WinUI/Assets/FileTypes/type_audio.png diff --git a/Wino.Core.UWP/Assets/FileTypes/type_executable.png b/Wino.Mail.WinUI/Assets/FileTypes/type_executable.png similarity index 100% rename from Wino.Core.UWP/Assets/FileTypes/type_executable.png rename to Wino.Mail.WinUI/Assets/FileTypes/type_executable.png diff --git a/Wino.Core.UWP/Assets/FileTypes/type_html.png b/Wino.Mail.WinUI/Assets/FileTypes/type_html.png similarity index 100% rename from Wino.Core.UWP/Assets/FileTypes/type_html.png rename to Wino.Mail.WinUI/Assets/FileTypes/type_html.png diff --git a/Wino.Core.UWP/Assets/FileTypes/type_image.png b/Wino.Mail.WinUI/Assets/FileTypes/type_image.png similarity index 100% rename from Wino.Core.UWP/Assets/FileTypes/type_image.png rename to Wino.Mail.WinUI/Assets/FileTypes/type_image.png diff --git a/Wino.Core.UWP/Assets/FileTypes/type_none.png b/Wino.Mail.WinUI/Assets/FileTypes/type_none.png similarity index 100% rename from Wino.Core.UWP/Assets/FileTypes/type_none.png rename to Wino.Mail.WinUI/Assets/FileTypes/type_none.png diff --git a/Wino.Core.UWP/Assets/FileTypes/type_other.png b/Wino.Mail.WinUI/Assets/FileTypes/type_other.png similarity index 100% rename from Wino.Core.UWP/Assets/FileTypes/type_other.png rename to Wino.Mail.WinUI/Assets/FileTypes/type_other.png diff --git a/Wino.Core.UWP/Assets/FileTypes/type_pdf.png b/Wino.Mail.WinUI/Assets/FileTypes/type_pdf.png similarity index 100% rename from Wino.Core.UWP/Assets/FileTypes/type_pdf.png rename to Wino.Mail.WinUI/Assets/FileTypes/type_pdf.png diff --git a/Wino.Core.UWP/Assets/FileTypes/type_rar.png b/Wino.Mail.WinUI/Assets/FileTypes/type_rar.png similarity index 100% rename from Wino.Core.UWP/Assets/FileTypes/type_rar.png rename to Wino.Mail.WinUI/Assets/FileTypes/type_rar.png diff --git a/Wino.Core.UWP/Assets/FileTypes/type_video.png b/Wino.Mail.WinUI/Assets/FileTypes/type_video.png similarity index 100% rename from Wino.Core.UWP/Assets/FileTypes/type_video.png rename to Wino.Mail.WinUI/Assets/FileTypes/type_video.png diff --git a/Wino.Mail/Assets/NotificationIcons/profile-dark.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/profile.png similarity index 100% rename from Wino.Mail/Assets/NotificationIcons/profile-dark.png rename to Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/profile.png diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/calendar-join.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/calendar-join.png new file mode 100644 index 00000000..bc6fc2bc Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/calendar-join.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/calendar-snooze.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/calendar-snooze.png new file mode 100644 index 00000000..618e1d16 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/calendar-snooze.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/dismiss.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/dismiss.png new file mode 100644 index 00000000..e81df3fb Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/dismiss.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/mail-archive.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/mail-archive.png new file mode 100644 index 00000000..ef6b6b8f Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/mail-archive.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/mail-delete.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/mail-delete.png new file mode 100644 index 00000000..e6ea8428 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/mail-delete.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/mail-markread.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/mail-markread.png new file mode 100644 index 00000000..abf2884e Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/mail-markread.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/calendar-join.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/calendar-join.png new file mode 100644 index 00000000..1c23956e Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/calendar-join.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/calendar-snooze.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/calendar-snooze.png new file mode 100644 index 00000000..a568d93d Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/calendar-snooze.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/dismiss.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/dismiss.png new file mode 100644 index 00000000..679d63f3 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/dismiss.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/mail-archive.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/mail-archive.png new file mode 100644 index 00000000..0601580d Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/mail-archive.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/mail-delete.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/mail-delete.png new file mode 100644 index 00000000..c74ce5f9 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/mail-delete.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/mail-markread.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/mail-markread.png new file mode 100644 index 00000000..11cd2dd1 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/mail-markread.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/calendar-join.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/calendar-join.png new file mode 100644 index 00000000..b47963af Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/calendar-join.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/calendar-snooze.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/calendar-snooze.png new file mode 100644 index 00000000..f9968a2b Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/calendar-snooze.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/dismiss.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/dismiss.png new file mode 100644 index 00000000..8ba86ea4 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/dismiss.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/mail-archive.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/mail-archive.png new file mode 100644 index 00000000..b7435c8a Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/mail-archive.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/mail-delete.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/mail-delete.png new file mode 100644 index 00000000..f970df4b Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/mail-delete.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/mail-markread.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/mail-markread.png new file mode 100644 index 00000000..0f16c559 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/mail-markread.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/calendar-join.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/calendar-join.png new file mode 100644 index 00000000..445bef6a Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/calendar-join.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/calendar-snooze.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/calendar-snooze.png new file mode 100644 index 00000000..7357261a Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/calendar-snooze.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/dismiss.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/dismiss.png new file mode 100644 index 00000000..735d6c69 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/dismiss.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/mail-archive.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/mail-archive.png new file mode 100644 index 00000000..785fbb5a Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/mail-archive.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/mail-delete.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/mail-delete.png new file mode 100644 index 00000000..7ecaf105 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/mail-delete.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/mail-markread.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/mail-markread.png new file mode 100644 index 00000000..c6c548d0 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/mail-markread.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/calendar-join.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/calendar-join.png new file mode 100644 index 00000000..fb17ae9a Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/calendar-join.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/calendar-snooze.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/calendar-snooze.png new file mode 100644 index 00000000..0ad818e1 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/calendar-snooze.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/dismiss.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/dismiss.png new file mode 100644 index 00000000..a6591fd2 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/dismiss.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/mail-archive.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/mail-archive.png new file mode 100644 index 00000000..a4e573d9 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/mail-archive.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/mail-delete.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/mail-delete.png new file mode 100644 index 00000000..23702da9 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/mail-delete.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/mail-markread.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/mail-markread.png new file mode 100644 index 00000000..7ead265f Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/mail-markread.png differ diff --git a/Wino.Mail/Assets/NotificationIcons/profile-light.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/profile.png similarity index 100% rename from Wino.Mail/Assets/NotificationIcons/profile-light.png rename to Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/profile.png diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/calendar-join.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/calendar-join.png new file mode 100644 index 00000000..4ba19db7 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/calendar-join.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/calendar-snooze.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/calendar-snooze.png new file mode 100644 index 00000000..8e297571 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/calendar-snooze.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/dismiss.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/dismiss.png new file mode 100644 index 00000000..a635bbfa Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/dismiss.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/mail-archive.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/mail-archive.png new file mode 100644 index 00000000..de9ddc37 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/mail-archive.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/mail-delete.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/mail-delete.png new file mode 100644 index 00000000..9f57ff6f Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/mail-delete.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/mail-markread.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/mail-markread.png new file mode 100644 index 00000000..03cde630 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/mail-markread.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/calendar-join.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/calendar-join.png new file mode 100644 index 00000000..01a4d0ee Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/calendar-join.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/calendar-snooze.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/calendar-snooze.png new file mode 100644 index 00000000..9ec09801 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/calendar-snooze.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/dismiss.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/dismiss.png new file mode 100644 index 00000000..5f2fa277 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/dismiss.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/mail-archive.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/mail-archive.png new file mode 100644 index 00000000..922eb752 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/mail-archive.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/mail-delete.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/mail-delete.png new file mode 100644 index 00000000..8c1c5cf5 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/mail-delete.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/mail-markread.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/mail-markread.png new file mode 100644 index 00000000..559a08e9 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/mail-markread.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/calendar-join.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/calendar-join.png new file mode 100644 index 00000000..1a12fe40 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/calendar-join.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/calendar-snooze.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/calendar-snooze.png new file mode 100644 index 00000000..daebf99b Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/calendar-snooze.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/dismiss.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/dismiss.png new file mode 100644 index 00000000..4cdcf55c Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/dismiss.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/mail-archive.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/mail-archive.png new file mode 100644 index 00000000..edfc3805 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/mail-archive.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/mail-delete.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/mail-delete.png new file mode 100644 index 00000000..1eb3b3bd Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/mail-delete.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/mail-markread.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/mail-markread.png new file mode 100644 index 00000000..403a5962 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/mail-markread.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/calendar-join.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/calendar-join.png new file mode 100644 index 00000000..1905dfb0 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/calendar-join.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/calendar-snooze.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/calendar-snooze.png new file mode 100644 index 00000000..3498f142 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/calendar-snooze.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/dismiss.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/dismiss.png new file mode 100644 index 00000000..24b40c5c Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/dismiss.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/mail-archive.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/mail-archive.png new file mode 100644 index 00000000..1369d7ce Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/mail-archive.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/mail-delete.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/mail-delete.png new file mode 100644 index 00000000..0a3dc404 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/mail-delete.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/mail-markread.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/mail-markread.png new file mode 100644 index 00000000..5d56de68 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/mail-markread.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/calendar-join.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/calendar-join.png new file mode 100644 index 00000000..3be418e3 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/calendar-join.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/calendar-snooze.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/calendar-snooze.png new file mode 100644 index 00000000..0bd4607d Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/calendar-snooze.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/dismiss.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/dismiss.png new file mode 100644 index 00000000..c05ba123 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/dismiss.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/mail-archive.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/mail-archive.png new file mode 100644 index 00000000..fc527f89 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/mail-archive.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/mail-delete.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/mail-delete.png new file mode 100644 index 00000000..7f66c6fa Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/mail-delete.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/mail-markread.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/mail-markread.png new file mode 100644 index 00000000..2ddd86dd Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/mail-markread.png differ diff --git a/Wino.Core.UWP/Assets/Providers/Gmail.png b/Wino.Mail.WinUI/Assets/Providers/Gmail.png similarity index 100% rename from Wino.Core.UWP/Assets/Providers/Gmail.png rename to Wino.Mail.WinUI/Assets/Providers/Gmail.png diff --git a/Wino.Core.UWP/Assets/Providers/IMAP4.png b/Wino.Mail.WinUI/Assets/Providers/IMAP4.png similarity index 100% rename from Wino.Core.UWP/Assets/Providers/IMAP4.png rename to Wino.Mail.WinUI/Assets/Providers/IMAP4.png diff --git a/Wino.Core.UWP/Assets/Providers/Office 365.png b/Wino.Mail.WinUI/Assets/Providers/Office 365.png similarity index 100% rename from Wino.Core.UWP/Assets/Providers/Office 365.png rename to Wino.Mail.WinUI/Assets/Providers/Office 365.png diff --git a/Wino.Core.UWP/Assets/Providers/Outlook.png b/Wino.Mail.WinUI/Assets/Providers/Outlook.png similarity index 100% rename from Wino.Core.UWP/Assets/Providers/Outlook.png rename to Wino.Mail.WinUI/Assets/Providers/Outlook.png diff --git a/Wino.Core.UWP/Assets/Providers/Yahoo.png b/Wino.Mail.WinUI/Assets/Providers/Yahoo.png similarity index 100% rename from Wino.Core.UWP/Assets/Providers/Yahoo.png rename to Wino.Mail.WinUI/Assets/Providers/Yahoo.png diff --git a/Wino.Core.UWP/Assets/Providers/iCloud.png b/Wino.Mail.WinUI/Assets/Providers/iCloud.png similarity index 100% rename from Wino.Core.UWP/Assets/Providers/iCloud.png rename to Wino.Mail.WinUI/Assets/Providers/iCloud.png diff --git a/Wino.Mail/Assets/ReleaseNotes/1102.md b/Wino.Mail.WinUI/Assets/ReleaseNotes/1102.md similarity index 100% rename from Wino.Mail/Assets/ReleaseNotes/1102.md rename to Wino.Mail.WinUI/Assets/ReleaseNotes/1102.md diff --git a/Wino.Mail.WinUI/Assets/ReleaseNotes/vnext.md b/Wino.Mail.WinUI/Assets/ReleaseNotes/vnext.md new file mode 100644 index 00000000..3e014bd0 --- /dev/null +++ b/Wino.Mail.WinUI/Assets/ReleaseNotes/vnext.md @@ -0,0 +1,86 @@ +# 🎉 Welcome to Wino Mail – What's New? + +Thank you for using Wino Mail! This update is one of the biggest yet, bringing a brand-new Wino Calendar, major security improvements, and a ton of quality-of-life upgrades. Here's a tour of everything new: + +--- + +## 📅 Wino Calendar + +Wino now ships with a fully integrated calendar alongside your mail. You can view, create, and manage your events without ever leaving the app. If you use any CalDAV-compatible service (like iCloud, Fastmail, or a self-hosted server), your events will sync automatically and stay up to date. Recurring events, reminders, RSVP responses, and online meeting links are all supported. When someone sends you a calendar invitation by email, Wino will recognize it and let you accept or decline right from the mail reading view. + +- View, create, edit, and delete calendar events +- Sync with any CalDAV-compatible calendar service +- Full recurring event support +- RSVP directly from invitation emails +- Reminders and "Join Online" links for virtual meetings +- Calendar settings integrated into the main Settings page + +--- + +## 🔒 Email Signing & Encryption (S/MIME) + +You can now send digitally signed emails so recipients know a message genuinely came from you, and encrypt outgoing emails so only the intended recipient can read them. When you receive a signed or encrypted email, Wino will verify the signature and decrypt the content automatically. Import your personal certificate once from **Settings → Signature & Encryption**, and Wino takes care of the rest. Each email address (alias) can have its own certificate. + +- Import your personal S/MIME certificate (PKCS#12 / .pfx) +- Sign and/or encrypt outgoing emails with toggle buttons in the compose toolbar +- Visual indicator on received emails that are signed or encrypted +- Automatic signature verification and decryption on incoming mail + +--- + +## 💬 Threaded Mail View + +Emails that belong to the same conversation are now grouped into threads, making it much easier to follow a back-and-forth discussion without scrolling through your entire inbox. Threads expand and collapse smoothly, and you can select or act on individual messages within a conversation. + +--- + +## 📎 Large Attachments for Outlook + +Sending large files via your Outlook or Microsoft 365 account no longer fails. Wino now uses Microsoft's upload session API behind the scenes, which handles big attachments reliably regardless of file size. + +--- + +## 🔔 Smarter Notifications + +Toast notifications now let you act on emails directly from the notification (mark as read, delete, etc.) even if the app is not open. Clicking a calendar reminder notification takes you straight to that event. Notifications for mail and calendar are now routed to the correct app entry automatically. + +--- + +## 🗂️ Folder Management + +You can now create new sub-folders and delete existing folders directly from the sidebar — no need to go to your webmail to organize your mailbox. A new Storage settings page also lets you see how much space Wino is using on your device. + +--- + +## 💫 Swipe Actions + +Swipe left or right on emails in the mail list to quickly archive, delete, or mark them — ideal for touch screen devices or when you want to process your inbox fast. + +--- + +## ⌨️ Keyboard Shortcuts + +A new keyboard shortcuts dialog is available so you can discover all the keyboard shortcuts Wino supports. Press the shortcut or find it in the app menu to open it. + +--- + +## 🖨️ Custom Print Dialog + +Printing an email now uses Wino's own print dialog, giving you a cleaner and more consistent experience. + +--- + +## 🚀 Faster App Startup + +Wino's internals have been modernized to take full advantage of the latest .NET runtime optimizations. While this is a behind-the-scenes change, it means the app starts quicker, uses less memory, and is set up for even better performance in future updates. + +--- + +## 🐛 Bug Fixes & Stability + +- Fixed several issues with Outlook sync reliability and speed +- Improved IMAP synchronization to be more stable and resource-efficient +- Fixed duplicate mail and calendar event issues +- Improved account sign-out and re-authentication handling +- Better error messages when something goes wrong during sync +- Dozens of smaller fixes throughout the app diff --git a/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Calendar.svg b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Calendar.svg new file mode 100644 index 00000000..40bd60e7 --- /dev/null +++ b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Calendar.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MARCH + + + + + + + + + + + + + + + + + + + + diff --git a/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Customize.svg b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Customize.svg new file mode 100644 index 00000000..5667a4f3 --- /dev/null +++ b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Customize.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Mail.svg b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Mail.svg new file mode 100644 index 00000000..7a551879 --- /dev/null +++ b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Mail.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + G + + + O + + + @ + diff --git a/Wino.Mail.WinUI/Assets/UpdateNotes/Images/More.svg b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/More.svg new file mode 100644 index 00000000..e62b98fe --- /dev/null +++ b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/More.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Notification.svg b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Notification.svg new file mode 100644 index 00000000..1a298e70 --- /dev/null +++ b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Notification.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 3 + + + + Archive + + + Delete + diff --git a/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Security.svg b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Security.svg new file mode 100644 index 00000000..8ec6fbfa --- /dev/null +++ b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Security.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Thread.svg b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Thread.svg new file mode 100644 index 00000000..48723e7b --- /dev/null +++ b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Thread.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wino.Mail.WinUI/Assets/UpdateNotes/features.json b/Wino.Mail.WinUI/Assets/UpdateNotes/features.json new file mode 100644 index 00000000..07eb1dad --- /dev/null +++ b/Wino.Mail.WinUI/Assets/UpdateNotes/features.json @@ -0,0 +1,44 @@ +[ + { + "title": "# Outlook, Gmail & IMAP/SMTP", + "description": "Connect all your email accounts in one place. Wino supports Microsoft Outlook (Office 365), Gmail, and any standard IMAP/SMTP server — giving you a unified inbox experience.", + "imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Mail.svg", + "imageWidth": 128, + "imageHeight": 128 + }, + { + "title": "# Calendar with CalDAV Support", + "description": "Manage your schedule alongside your emails. Create local calendars or connect to remote CalDAV-compatible servers. View, create, and respond to events and invitations.", + "imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Calendar.svg", + "imageWidth": 128, + "imageHeight": 128 + }, + { + "title": "# S/MIME Signing & Encryption", + "description": "Keep your communications secure. Sign and encrypt emails with personal certificates to ensure your messages are authentic and private.", + "imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Security.svg", + "imageWidth": 128, + "imageHeight": 128 + }, + { + "title": "# Conversation Threading", + "description": "Follow discussions with ease. Emails are grouped by conversation thread so you never lose context in long email chains.", + "imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Thread.svg", + "imageWidth": 128, + "imageHeight": 128 + }, + { + "title": "# Actionable Notifications", + "description": "Stay productive without switching apps. Mark as read, delete, or archive emails directly from Windows toast notifications.", + "imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Notification.svg", + "imageWidth": 128, + "imageHeight": 128 + }, + { + "title": "# Rich Customization", + "description": "Make Wino yours. Choose from built-in themes, create custom themes with your own wallpapers and accent colors, configure swipe actions, and set keyboard shortcuts.", + "imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Customize.svg", + "imageWidth": 128, + "imageHeight": 128 + } +] diff --git a/Wino.Mail.WinUI/Assets/UpdateNotes/vnext.json b/Wino.Mail.WinUI/Assets/UpdateNotes/vnext.json new file mode 100644 index 00000000..1dba91df --- /dev/null +++ b/Wino.Mail.WinUI/Assets/UpdateNotes/vnext.json @@ -0,0 +1,39 @@ +{ + "sections": [ + { + "title": "Wino Calendar is here!", + "description": "You can now create local or remote CalDAV-compatible calendars, manage recurring events, and respond to invitations — all from within Wino.", + "imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Calendar.svg", + "imageWidth": 128, + "imageHeight": 128 + }, + { + "title": "S/MIME Signing & Encryption", + "description": "Wino now supports signing and encrypting your emails with personal certificates. Keep your communications secure and verifiable.", + "imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Security.svg", + "imageWidth": 128, + "imageHeight": 128 + }, + { + "title": "Threaded Mail View", + "description": "Emails are now grouped by conversation, making it easier to follow long discussions without losing context.", + "imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Thread.svg", + "imageWidth": 128, + "imageHeight": 128 + }, + { + "title": "Smarter Notifications", + "description": "Act on your emails directly from toast notifications — mark as read, delete, or archive without opening the app.", + "imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Notification.svg", + "imageWidth": 128, + "imageHeight": 128 + }, + { + "title": "And much more...", + "description": "Folder management, swipe actions, keyboard shortcuts, a custom print dialog, and significant performance improvements are all included in this release.\n\nThank you for using Wino Mail!", + "imageUrl": "ms-appx:///Assets/UpdateNotes/Images/More.svg", + "imageWidth": 128, + "imageHeight": 128 + } + ] +} diff --git a/Wino.Mail.WinUI/Assets/WinoIcons.ttf b/Wino.Mail.WinUI/Assets/WinoIcons.ttf new file mode 100644 index 00000000..cd38942c Binary files /dev/null and b/Wino.Mail.WinUI/Assets/WinoIcons.ttf differ diff --git a/Wino.Mail.WinUI/Assets/Wino_Icon.ico b/Wino.Mail.WinUI/Assets/Wino_Icon.ico new file mode 100644 index 00000000..f8dd2cd6 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/Wino_Icon.ico differ diff --git a/Wino.Core.UWP/BackgroundImages/Acrylic.jpg b/Wino.Mail.WinUI/BackgroundImages/Acrylic.jpg similarity index 100% rename from Wino.Core.UWP/BackgroundImages/Acrylic.jpg rename to Wino.Mail.WinUI/BackgroundImages/Acrylic.jpg diff --git a/Wino.Core.UWP/BackgroundImages/Clouds.jpg b/Wino.Mail.WinUI/BackgroundImages/Clouds.jpg similarity index 100% rename from Wino.Core.UWP/BackgroundImages/Clouds.jpg rename to Wino.Mail.WinUI/BackgroundImages/Clouds.jpg diff --git a/Wino.Core.UWP/BackgroundImages/Forest.jpg b/Wino.Mail.WinUI/BackgroundImages/Forest.jpg similarity index 100% rename from Wino.Core.UWP/BackgroundImages/Forest.jpg rename to Wino.Mail.WinUI/BackgroundImages/Forest.jpg diff --git a/Wino.Core.UWP/BackgroundImages/Garden.jpg b/Wino.Mail.WinUI/BackgroundImages/Garden.jpg similarity index 100% rename from Wino.Core.UWP/BackgroundImages/Garden.jpg rename to Wino.Mail.WinUI/BackgroundImages/Garden.jpg diff --git a/Wino.Core.UWP/BackgroundImages/Mica.jpg b/Wino.Mail.WinUI/BackgroundImages/Mica.jpg similarity index 100% rename from Wino.Core.UWP/BackgroundImages/Mica.jpg rename to Wino.Mail.WinUI/BackgroundImages/Mica.jpg diff --git a/Wino.Core.UWP/BackgroundImages/Nighty.jpg b/Wino.Mail.WinUI/BackgroundImages/Nighty.jpg similarity index 100% rename from Wino.Core.UWP/BackgroundImages/Nighty.jpg rename to Wino.Mail.WinUI/BackgroundImages/Nighty.jpg diff --git a/Wino.Core.UWP/BackgroundImages/Snowflake.jpg b/Wino.Mail.WinUI/BackgroundImages/Snowflake.jpg similarity index 100% rename from Wino.Core.UWP/BackgroundImages/Snowflake.jpg rename to Wino.Mail.WinUI/BackgroundImages/Snowflake.jpg diff --git a/Wino.Mail.WinUI/BasePage.cs b/Wino.Mail.WinUI/BasePage.cs new file mode 100644 index 00000000..858b929e --- /dev/null +++ b/Wino.Mail.WinUI/BasePage.cs @@ -0,0 +1,123 @@ +using System; +using System.Diagnostics; +using CommunityToolkit.Mvvm.Messaging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Navigation; +using Wino.Core.ViewModels; +using Wino.Messaging.Client.Shell; +using WinoNavigationMode = Wino.Core.Domain.Models.Navigation.NavigationMode; + +namespace Wino.Mail.WinUI; + +public partial class BasePage : Page, IRecipient +{ + private bool _isPreparedForClose; + + public void Receive(LanguageChanged message) + { + OnLanguageChanged(); + } + + public virtual void OnLanguageChanged() { } + + /// + /// Register message recipients for this page. Override to register specific message types. + /// + protected virtual void RegisterRecipients() { } + + /// + /// Unregister message recipients for this page. Override to unregister specific message types. + /// + protected virtual void UnregisterRecipients() { } + + public virtual CoreBaseViewModel? AssociatedViewModel => null; + + public virtual void PrepareForClose() + { + if (_isPreparedForClose) + return; + + _isPreparedForClose = true; + + WeakReferenceMessenger.Default.Unregister(this); + UnregisterRecipients(); + } + + protected void ResetPreparedForCloseState() + { + _isPreparedForClose = false; + } + + protected bool IsPreparedForClose => _isPreparedForClose; +} + +public abstract class BasePage : BasePage where T : CoreBaseViewModel +{ + public T ViewModel { get; } = WinoApplication.Current.Services.GetService() ?? throw new ArgumentException($"Can't resolve '{typeof(T)}' as view model."); + public override CoreBaseViewModel AssociatedViewModel => ViewModel; + + protected BasePage() + { + ViewModel.Dispatcher = new WinUIDispatcher(DispatcherQueue); + + Loaded += PageLoaded; + Unloaded += PageUnloaded; + } + + private void PageUnloaded(object sender, RoutedEventArgs e) + { + Loaded -= PageLoaded; + Unloaded -= PageUnloaded; + } + + private void PageLoaded(object sender, RoutedEventArgs e) => ViewModel.OnPageLoaded(); + + ~BasePage() { Debug.WriteLine($"Disposed {GetType().Name}"); } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + base.OnNavigatedTo(e); + + var mode = GetNavigationMode(e.NavigationMode); + var parameter = e.Parameter; + + ResetPreparedForCloseState(); + WeakReferenceMessenger.Default.Register(this); + RegisterRecipients(); + + ViewModel.OnNavigatedTo(mode, parameter); + } + + protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) + { + base.OnNavigatingFrom(e); + + var mode = GetNavigationMode(e.NavigationMode); + var parameter = e.Parameter; + + PrepareForClose(mode, parameter); + + GC.Collect(); + } + + public override void PrepareForClose() + { + PrepareForClose(WinoNavigationMode.New, null); + } + + private void PrepareForClose(WinoNavigationMode mode, object? parameter) + { + if (IsPreparedForClose) + return; + + base.PrepareForClose(); + ViewModel.OnNavigatedFrom(mode, parameter!); + } + + private WinoNavigationMode GetNavigationMode(NavigationMode mode) + { + return (WinoNavigationMode)mode; + } +} diff --git a/Wino.Mail/Behaviors/CreateMailNavigationItemBehavior.cs b/Wino.Mail.WinUI/Behaviors/CreateMailNavigationItemBehavior.cs similarity index 91% rename from Wino.Mail/Behaviors/CreateMailNavigationItemBehavior.cs rename to Wino.Mail.WinUI/Behaviors/CreateMailNavigationItemBehavior.cs index c741555b..84a8d5e1 100644 --- a/Wino.Mail/Behaviors/CreateMailNavigationItemBehavior.cs +++ b/Wino.Mail.WinUI/Behaviors/CreateMailNavigationItemBehavior.cs @@ -1,8 +1,8 @@ -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using Microsoft.Xaml.Interactivity; -using Windows.UI.Xaml; +using Microsoft.UI.Xaml; using Wino.Core.Domain.Interfaces; -using Wino.Core.UWP.Controls; +using Wino.Mail.WinUI.Controls; namespace Wino.Behaviors; @@ -58,7 +58,7 @@ public class CreateMailNavigationItemBehavior : Behavior } } - private void MenuCollectionUpdated(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) + private void MenuCollectionUpdated(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { ManageAccounts(); } diff --git a/Wino.Core.UWP/Controls/AccountCreationDialogControl.xaml b/Wino.Mail.WinUI/Controls/AccountCreationDialogControl.xaml similarity index 81% rename from Wino.Core.UWP/Controls/AccountCreationDialogControl.xaml rename to Wino.Mail.WinUI/Controls/AccountCreationDialogControl.xaml index ae38dd73..17ea54df 100644 --- a/Wino.Core.UWP/Controls/AccountCreationDialogControl.xaml +++ b/Wino.Mail.WinUI/Controls/AccountCreationDialogControl.xaml @@ -1,10 +1,10 @@ - - + + + + + + + + + diff --git a/Wino.Core.UWP/Controls/AccountCreationDialogControl.xaml.cs b/Wino.Mail.WinUI/Controls/AccountCreationDialogControl.xaml.cs similarity index 69% rename from Wino.Core.UWP/Controls/AccountCreationDialogControl.xaml.cs rename to Wino.Mail.WinUI/Controls/AccountCreationDialogControl.xaml.cs index 0cd1c098..09ddcc55 100644 --- a/Wino.Core.UWP/Controls/AccountCreationDialogControl.xaml.cs +++ b/Wino.Mail.WinUI/Controls/AccountCreationDialogControl.xaml.cs @@ -1,21 +1,22 @@ -using System; +using System; using System.Threading.Tasks; using CommunityToolkit.Mvvm.Messaging; +using CommunityToolkit.WinUI; using Microsoft.Extensions.DependencyInjection; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Messaging.UI; -namespace Wino.Core.UWP.Controls; +namespace Wino.Mail.WinUI.Controls; public sealed partial class AccountCreationDialogControl : UserControl, IRecipient { - private string copyClipboardURL; + private string copyClipboardURL = string.Empty; - public event EventHandler CancelClicked; + public event EventHandler? CancelClicked; public AccountCreationDialogState State { @@ -46,30 +47,30 @@ public sealed partial class AccountCreationDialogControl : UserControl, IRecipie await Task.Delay(2000); - await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => + await DispatcherQueue.EnqueueAsync(async () => { - AuthHelpDialogButton.Visibility = Windows.UI.Xaml.Visibility.Visible; + AuthHelpDialogButton.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed; }); } - private void ControlLoaded(object sender, Windows.UI.Xaml.RoutedEventArgs e) + private void ControlLoaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) { WeakReferenceMessenger.Default.Register(this); } - private void ControlUnloaded(object sender, Windows.UI.Xaml.RoutedEventArgs e) + private void ControlUnloaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) { WeakReferenceMessenger.Default.UnregisterAll(this); } - private async void CopyClicked(object sender, Windows.UI.Xaml.RoutedEventArgs e) + private async void CopyClicked(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) { if (string.IsNullOrEmpty(copyClipboardURL)) return; - var clipboardService = WinoApplication.Current.Services.GetService(); + var clipboardService = WinoApplication.Current.Services.GetRequiredService(); await clipboardService.CopyClipboardAsync(copyClipboardURL); } - private void CancelButtonClicked(object sender, Windows.UI.Xaml.RoutedEventArgs e) => CancelClicked?.Invoke(this, null); + private void CancelButtonClicked(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) => CancelClicked?.Invoke(this, EventArgs.Empty); } diff --git a/Wino.Mail/Controls/AccountNavigationItem.cs b/Wino.Mail.WinUI/Controls/AccountNavigationItem.cs similarity index 81% rename from Wino.Mail/Controls/AccountNavigationItem.cs rename to Wino.Mail.WinUI/Controls/AccountNavigationItem.cs index 9462a088..6b1a65e3 100644 --- a/Wino.Mail/Controls/AccountNavigationItem.cs +++ b/Wino.Mail.WinUI/Controls/AccountNavigationItem.cs @@ -1,8 +1,8 @@ -using System.Numerics; +using System.Numerics; using Microsoft.UI.Xaml.Controls; -using Windows.UI.Xaml; +using Microsoft.UI.Xaml; using Wino.Core.Domain.Interfaces; -using Wino.Core.UWP.Controls; +using Wino.Mail.WinUI.Controls; namespace Wino.Controls; @@ -28,8 +28,8 @@ public partial class AccountNavigationItem : WinoNavigationViewItem private const string PART_NavigationViewItemMenuItemsHost = "NavigationViewItemMenuItemsHost"; private const string PART_SelectionIndicator = "CustomSelectionIndicator"; - private ItemsRepeater _itemsRepeater; - private Windows.UI.Xaml.Shapes.Rectangle _selectionIndicator; + private ItemsRepeater _itemsRepeater = null!; + private Microsoft.UI.Xaml.Shapes.Rectangle _selectionIndicator = null!; public AccountNavigationItem() { @@ -40,8 +40,8 @@ public partial class AccountNavigationItem : WinoNavigationViewItem { base.OnApplyTemplate(); - _itemsRepeater = GetTemplateChild(PART_NavigationViewItemMenuItemsHost) as ItemsRepeater; - _selectionIndicator = GetTemplateChild(PART_SelectionIndicator) as Windows.UI.Xaml.Shapes.Rectangle; + _itemsRepeater = (GetTemplateChild(PART_NavigationViewItemMenuItemsHost) as ItemsRepeater)!; + _selectionIndicator = (GetTemplateChild(PART_SelectionIndicator) as Microsoft.UI.Xaml.Shapes.Rectangle)!; UpdateSelectionBorder(); } diff --git a/Wino.Mail.WinUI/Controls/Advanced/WinoItemsView.cs b/Wino.Mail.WinUI/Controls/Advanced/WinoItemsView.cs new file mode 100644 index 00000000..c3a705b6 --- /dev/null +++ b/Wino.Mail.WinUI/Controls/Advanced/WinoItemsView.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Windows.Input; +using CommunityToolkit.WinUI; +using Microsoft.UI.Xaml.Controls; +using Wino.Mail.ViewModels.Data; + +namespace Wino.Mail.WinUI.Controls.Advanced; + +[Obsolete("ItemsView sucks. Hard to deal with virtualization issues. Use ListView. This control is here to wise up anyone who tries to use it.")] +public partial class WinoItemsView : ItemsView +{ + private const string PART_ScrollView = nameof(PART_ScrollView); + + private ScrollView? _internalScrollView; + + [GeneratedDependencyProperty] + public partial ICommand? LoadMoreCommand { get; set; } + + public IEnumerable? CastedItemsSource => ItemsSource as IEnumerable; + + public WinoItemsView() + { + DefaultStyleKey = typeof(ItemsView); + } + + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + _internalScrollView = GetTemplateChild("PART_ScrollView") as ScrollView ?? throw new System.Exception("Can't find the ScrollView in WinoItemsView."); + + _internalScrollView.ViewChanged -= InternalScrollViewPositionChanged; + _internalScrollView.ViewChanged += InternalScrollViewPositionChanged; + } + + private void InternalScrollViewPositionChanged(ScrollView sender, object args) + { + if (_internalScrollView == null) return; + + // No need to raise init request if there are no items in the list. + if (ItemsSource == null) return; + + double progress = sender.VerticalOffset / sender.ScrollableHeight; + + // Trigger when scrolled past 90% of total height + if (progress >= 0.9) LoadMoreCommand?.Execute(null); + } + + public bool SelectMailItemContainer(MailItemViewModel mailItemViewModel) + { + return true; + } + + /// + /// Recursively clears all selections except the given mail. + /// + /// Exceptional mail item to be not unselected. + /// Whether expansion states of thread containers should stay as it is or not. + public void ClearSelections(MailItemViewModel? exceptViewModel = null, bool preserveThreadExpanding = false) + { + if (CastedItemsSource == null) return; + + foreach (var item in CastedItemsSource) + { + if (item is MailItemViewModel mailItemViewModel) + { + if (mailItemViewModel != exceptViewModel) + { + mailItemViewModel.IsSelected = false; + } + } + else if (item is ThreadMailItemViewModel threadMailItemViewModel) + { + threadMailItemViewModel.IsSelected = false; + + if (!preserveThreadExpanding) threadMailItemViewModel.IsThreadExpanded = false; + + foreach (var childMail in threadMailItemViewModel.ThreadEmails) + { + if (childMail != exceptViewModel) + { + childMail.IsSelected = false; + } + } + } + } + } +} diff --git a/Wino.Mail.WinUI/Controls/AiActionsPanel.xaml b/Wino.Mail.WinUI/Controls/AiActionsPanel.xaml new file mode 100644 index 00000000..cf49e1f9 --- /dev/null +++ b/Wino.Mail.WinUI/Controls/AiActionsPanel.xaml @@ -0,0 +1,328 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wino.Mail.WinUI/Controls/CalendarTitleBarContent.xaml.cs b/Wino.Mail.WinUI/Controls/CalendarTitleBarContent.xaml.cs new file mode 100644 index 00000000..df356b84 --- /dev/null +++ b/Wino.Mail.WinUI/Controls/CalendarTitleBarContent.xaml.cs @@ -0,0 +1,55 @@ +using System; +using System.Windows.Input; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Wino.Calendar.Controls; +using Wino.Core.Domain.Enums; + +namespace Wino.Mail.WinUI.Controls; + +public sealed partial class CalendarTitleBarContent : UserControl +{ + public event EventHandler? PreviousDateRequested; + public event EventHandler? NextDateRequested; + + public CalendarTitleBarContent() + { + InitializeComponent(); + } + + public string VisibleDateRangeText + { + get => VisibleDateRangeTextBlock.Text; + set => VisibleDateRangeTextBlock.Text = value; + } + + public ICommand? TodayClickedCommand + { + get => CalendarTypeSelector.TodayClickedCommand; + set => CalendarTypeSelector.TodayClickedCommand = value; + } + + public int DisplayDayCount + { + get => CalendarTypeSelector.DisplayDayCount; + set => CalendarTypeSelector.DisplayDayCount = value; + } + + public CalendarDisplayType SelectedType + { + get => CalendarTypeSelector.SelectedType; + set => CalendarTypeSelector.SelectedType = value; + } + + public long RegisterSelectedTypeChanged(DependencyPropertyChangedCallback callback) + => CalendarTypeSelector.RegisterPropertyChangedCallback(WinoCalendarTypeSelectorControl.SelectedTypeProperty, callback); + + public void UnregisterSelectedTypeChanged(long token) + => CalendarTypeSelector.UnregisterPropertyChangedCallback(WinoCalendarTypeSelectorControl.SelectedTypeProperty, token); + + private void PreviousDateClicked(object sender, RoutedEventArgs e) + => PreviousDateRequested?.Invoke(this, EventArgs.Empty); + + private void NextDateClicked(object sender, RoutedEventArgs e) + => NextDateRequested?.Invoke(this, EventArgs.Empty); +} diff --git a/Wino.Core.UWP/Controls/ControlConstants.cs b/Wino.Mail.WinUI/Controls/ControlConstants.cs similarity index 88% rename from Wino.Core.UWP/Controls/ControlConstants.cs rename to Wino.Mail.WinUI/Controls/ControlConstants.cs index db693d77..b66f7e43 100644 --- a/Wino.Core.UWP/Controls/ControlConstants.cs +++ b/Wino.Mail.WinUI/Controls/ControlConstants.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; -namespace Wino.Core.UWP.Controls; +namespace Wino.Mail.WinUI.Controls; public static class ControlConstants { @@ -76,6 +76,7 @@ public static class ControlConstants { WinoIconGlyph.CalendarToday, "\uE911" }, { WinoIconGlyph.CalendarDay, "\uE913" }, { WinoIconGlyph.CalendarWeek, "\uE914" }, + { WinoIconGlyph.CalendarWorkWeek, "\uE914" }, { WinoIconGlyph.CalendarMonth, "\uE91c" }, { WinoIconGlyph.CalendarYear, "\uE917" }, { WinoIconGlyph.WeatherBlow, "\uE907" }, @@ -100,6 +101,15 @@ public static class ControlConstants { WinoIconGlyph.EventJoinOnline, "\uE926" }, { WinoIconGlyph.ViewMessageSource, "\uE943" }, { WinoIconGlyph.Apple, "\uE92B" }, - { WinoIconGlyph.Yahoo, "\uE92C" } + { WinoIconGlyph.Yahoo, "\uE92C" }, + { WinoIconGlyph.People, "\uF007" }, + { WinoIconGlyph.AttachmentNew, "\uF006" }, + { WinoIconGlyph.CalendarSettings, "\uF005" }, + { WinoIconGlyph.SettingsNew, "\uF004" }, + { WinoIconGlyph.ManageAccounts, "\uF003" }, + { WinoIconGlyph.SendNew, "\uF002" }, + { WinoIconGlyph.CalendarShowAs, "\uF001" }, + { WinoIconGlyph.EventDecline, "\uF000" }, + { WinoIconGlyph.Dismiss, "\uF008" }, }; } diff --git a/Wino.Core.UWP/Controls/CustomWrapPanel.cs b/Wino.Mail.WinUI/Controls/CustomWrapPanel.cs similarity index 94% rename from Wino.Core.UWP/Controls/CustomWrapPanel.cs rename to Wino.Mail.WinUI/Controls/CustomWrapPanel.cs index af875259..90230165 100644 --- a/Wino.Core.UWP/Controls/CustomWrapPanel.cs +++ b/Wino.Mail.WinUI/Controls/CustomWrapPanel.cs @@ -1,10 +1,10 @@ -using System; +using System; -namespace Wino.Core.UWP.Controls +namespace Wino.Mail.WinUI.Controls { using Windows.Foundation; - using Windows.UI.Xaml; - using Windows.UI.Xaml.Controls; + using Microsoft.UI.Xaml; + using Microsoft.UI.Xaml.Controls; namespace CustomControls { diff --git a/Wino.Mail.WinUI/Controls/EditorCommanding.cs b/Wino.Mail.WinUI/Controls/EditorCommanding.cs new file mode 100644 index 00000000..0be5479d --- /dev/null +++ b/Wino.Mail.WinUI/Controls/EditorCommanding.cs @@ -0,0 +1,252 @@ +using System; +using System.Globalization; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Microsoft.UI; +using Microsoft.UI.Xaml.Media; +using Windows.UI; + +namespace Wino.Mail.Controls; + +public interface IEditorCommandTarget +{ + EditorState CurrentState { get; } + EditorCapabilities Capabilities { get; } + event EventHandler? StateChanged; + event EventHandler? CapabilitiesChanged; + Task ExecuteCommandAsync(EditorCommand command); +} + +public interface IEditorCommandControl +{ + IEditorCommandTarget? CommandTarget { get; set; } + void AttachCommandTarget(IEditorCommandTarget? target); + void DetachCommandTarget(); +} + +public enum EditorCommandKind +{ + ToggleBold, + ToggleItalic, + ToggleUnderline, + ToggleStrikethrough, + ToggleOrderedList, + ToggleUnorderedList, + Indent, + Outdent, + SetAlignment, + SetFontFamily, + SetFontSize, + SetParagraphStyle, + SetTextColor, + SetHighlightColor, + SetLineHeight, + InsertImage, + InsertLink, + RemoveLink, + InsertEmoji, + InsertTable, + ToggleBuiltInToolbar, + ToggleTheme, + ToggleSpellCheck +} + +public enum EditorTextAlignment +{ + Left, + Center, + Right, + Justify +} + +public sealed record class EditorCommand(EditorCommandKind Kind, object? Value = null) +{ + public static EditorCommand ToggleBold() => new(EditorCommandKind.ToggleBold); + public static EditorCommand ToggleItalic() => new(EditorCommandKind.ToggleItalic); + public static EditorCommand ToggleUnderline() => new(EditorCommandKind.ToggleUnderline); + public static EditorCommand ToggleStrikethrough() => new(EditorCommandKind.ToggleStrikethrough); + public static EditorCommand ToggleOrderedList() => new(EditorCommandKind.ToggleOrderedList); + public static EditorCommand ToggleUnorderedList() => new(EditorCommandKind.ToggleUnorderedList); + public static EditorCommand Indent() => new(EditorCommandKind.Indent); + public static EditorCommand Outdent() => new(EditorCommandKind.Outdent); + public static EditorCommand SetAlignment(EditorTextAlignment alignment) => new(EditorCommandKind.SetAlignment, alignment); + public static EditorCommand SetFontFamily(string fontFamily) => new(EditorCommandKind.SetFontFamily, fontFamily); + public static EditorCommand SetFontSize(int fontSize) => new(EditorCommandKind.SetFontSize, fontSize); + public static EditorCommand SetParagraphStyle(string tagName) => new(EditorCommandKind.SetParagraphStyle, tagName); + public static EditorCommand SetTextColor(string color) => new(EditorCommandKind.SetTextColor, color); + public static EditorCommand SetHighlightColor(string color) => new(EditorCommandKind.SetHighlightColor, color); + public static EditorCommand SetLineHeight(string lineHeight) => new(EditorCommandKind.SetLineHeight, lineHeight); + public static EditorCommand InsertImage() => new(EditorCommandKind.InsertImage); + public static EditorCommand InsertEmoji() => new(EditorCommandKind.InsertEmoji); + public static EditorCommand InsertLink(EditorLinkCommandArgs args) => new(EditorCommandKind.InsertLink, args); + public static EditorCommand RemoveLink() => new(EditorCommandKind.RemoveLink); + public static EditorCommand InsertTable(EditorTableCommandArgs args) => new(EditorCommandKind.InsertTable, args); + public static EditorCommand ToggleBuiltInToolbar(bool isVisible) => new(EditorCommandKind.ToggleBuiltInToolbar, isVisible); + public static EditorCommand ToggleTheme(bool isDarkMode) => new(EditorCommandKind.ToggleTheme, isDarkMode); + public static EditorCommand ToggleSpellCheck(bool isEnabled) => new(EditorCommandKind.ToggleSpellCheck, isEnabled); +} + +public sealed record class EditorLinkCommandArgs( + [property: JsonPropertyName("url")] string Url, + [property: JsonPropertyName("text")] string? Text = null, + [property: JsonPropertyName("openInNewWindow")] bool OpenInNewWindow = true); + +public sealed record class EditorTableCommandArgs( + [property: JsonPropertyName("rows")] int Rows, + [property: JsonPropertyName("columns")] int Columns); + +public sealed record class EditorColorOption(string Name, string Value) +{ + public SolidColorBrush Brush => new(ParseColorValue(Value)); + + public static Color ParseColorValue(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return Colors.Transparent; + } + + var normalizedValue = value.Trim(); + + if (string.Equals(normalizedValue, "transparent", StringComparison.OrdinalIgnoreCase)) + { + return Colors.Transparent; + } + + if (TryParseRgbColor(normalizedValue, out var rgbColor)) + { + return rgbColor; + } + + if (TryParseNamedColor(normalizedValue, out var namedColor)) + { + return namedColor; + } + + var hex = normalizedValue.TrimStart('#'); + if (hex.Length == 6) + { + hex = $"FF{hex}"; + } + + if (hex.Length != 8 || !uint.TryParse(hex, System.Globalization.NumberStyles.HexNumber, null, out var argb)) + { + return Colors.Transparent; + } + + return Color.FromArgb( + (byte)((argb >> 24) & 0xFF), + (byte)((argb >> 16) & 0xFF), + (byte)((argb >> 8) & 0xFF), + (byte)(argb & 0xFF)); + } + + private static bool TryParseRgbColor(string value, out Color color) + { + color = Colors.Transparent; + + var isRgba = value.StartsWith("rgba(", StringComparison.OrdinalIgnoreCase); + var isRgb = value.StartsWith("rgb(", StringComparison.OrdinalIgnoreCase); + if (!isRgb && !isRgba) + { + return false; + } + + var startIndex = value.IndexOf('('); + var endIndex = value.LastIndexOf(')'); + if (startIndex < 0 || endIndex <= startIndex) + { + return false; + } + + var segments = value[(startIndex + 1)..endIndex] + .Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + + if ((isRgb && segments.Length != 3) || (isRgba && segments.Length != 4)) + { + return false; + } + + if (!byte.TryParse(segments[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var red) || + !byte.TryParse(segments[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var green) || + !byte.TryParse(segments[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out var blue)) + { + return false; + } + + byte alpha = 255; + if (isRgba) + { + if (!double.TryParse(segments[3], NumberStyles.Float, CultureInfo.InvariantCulture, out var alphaValue)) + { + return false; + } + + alpha = alphaValue <= 1d + ? (byte)Math.Clamp(Math.Round(alphaValue * 255d), 0d, 255d) + : (byte)Math.Clamp(Math.Round(alphaValue), 0d, 255d); + } + + color = Color.FromArgb(alpha, red, green, blue); + return true; + } + + private static bool TryParseNamedColor(string value, out Color color) + { + color = value.ToLowerInvariant() switch + { + "black" => Colors.Black, + "white" => Colors.White, + "gray" or "grey" => Colors.Gray, + "red" => Colors.Red, + "orange" => Colors.Orange, + "yellow" => Colors.Yellow, + "green" => Colors.Green, + "blue" => Colors.Blue, + "purple" => Colors.Purple, + "pink" => Colors.Pink, + _ => Colors.Transparent + }; + + return !color.Equals(Colors.Transparent) || string.Equals(value, "transparent", StringComparison.OrdinalIgnoreCase); + } +} + +public sealed record class EditorParagraphStyleOption(string Name, string Tag); + +public sealed record class EditorCapabilities +{ + public IReadOnlyList Fonts { get; init; } = Array.Empty(); + public IReadOnlyList FontSizes { get; init; } = Array.Empty(); + public IReadOnlyList TextColors { get; init; } = Array.Empty(); + public IReadOnlyList HighlightColors { get; init; } = Array.Empty(); + public IReadOnlyList ParagraphStyles { get; init; } = Array.Empty(); + public IReadOnlyList LineHeights { get; init; } = Array.Empty(); + public IReadOnlyList Alignments { get; init; } = Array.Empty(); +} + +public sealed record class EditorState +{ + public bool IsBold { get; init; } + public bool IsItalic { get; init; } + public bool IsUnderline { get; init; } + public bool IsStrikethrough { get; init; } + public bool IsOrderedList { get; init; } + public bool IsUnorderedList { get; init; } + public bool CanIndent { get; init; } = true; + public bool CanOutdent { get; init; } + public bool HasSelection { get; init; } + public bool IsDarkMode { get; init; } + public bool IsBuiltInToolbarVisible { get; init; } + public bool IsSpellCheckEnabled { get; init; } = true; + public EditorTextAlignment Alignment { get; init; } = EditorTextAlignment.Left; + public string? FontFamily { get; init; } + public int? FontSize { get; init; } + public string? ParagraphStyle { get; init; } + public string? TextColor { get; init; } + public string? HighlightColor { get; init; } + public string? LineHeight { get; init; } + public string? LinkUrl { get; init; } + public string? SelectedText { get; init; } +} diff --git a/Wino.Mail.WinUI/Controls/EditorTabbedCommandBarControl.xaml b/Wino.Mail.WinUI/Controls/EditorTabbedCommandBarControl.xaml new file mode 100644 index 00000000..7a809963 --- /dev/null +++ b/Wino.Mail.WinUI/Controls/EditorTabbedCommandBarControl.xaml @@ -0,0 +1,430 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wino.Mail.WinUI/Controls/EditorTabbedCommandBarControl.xaml.cs b/Wino.Mail.WinUI/Controls/EditorTabbedCommandBarControl.xaml.cs new file mode 100644 index 00000000..befdcb61 --- /dev/null +++ b/Wino.Mail.WinUI/Controls/EditorTabbedCommandBarControl.xaml.cs @@ -0,0 +1,449 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.WinUI; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using Windows.UI; + +namespace Wino.Mail.Controls; + +public sealed partial class EditorTabbedCommandBarControl : UserControl, IEditorCommandControl +{ + [GeneratedDependencyProperty] + public partial IEditorCommandTarget? CommandTarget { get; set; } + + [GeneratedDependencyProperty] + public partial object? PaneCustomContent { get; set; } + + [GeneratedDependencyProperty] + public partial object? InsertCustomContent { get; set; } + + [GeneratedDependencyProperty] + public partial object? OptionsCustomContent { get; set; } + + [GeneratedDependencyProperty] + public partial EditorColorOption? SelectedTextColorOption { get; set; } + + [GeneratedDependencyProperty] + public partial EditorColorOption? SelectedHighlightColorOption { get; set; } + + private bool _isApplyingState; + private IEditorCommandTarget? _subscribedTarget; + private static readonly SolidColorBrush TransparentBrush = new(EditorColorOption.ParseColorValue(null)); + private IReadOnlyList _textColorOptions = Array.Empty(); + private IReadOnlyList _highlightColorOptions = Array.Empty(); + + public Brush SelectedTextColorBrush => SelectedTextColorOption?.Brush ?? TransparentBrush; + public Brush SelectedHighlightColorBrush => SelectedHighlightColorOption?.Brush ?? TransparentBrush; + + public EditorTabbedCommandBarControl() + { + InitializeComponent(); + + Loaded += OnLoaded; + Unloaded += OnUnloaded; + } + + public void AttachCommandTarget(IEditorCommandTarget? target) + { + if (_subscribedTarget == target) + { + return; + } + + if (_subscribedTarget != null) + { + _subscribedTarget.StateChanged -= CommandTarget_StateChanged; + _subscribedTarget.CapabilitiesChanged -= CommandTarget_CapabilitiesChanged; + } + + _subscribedTarget = target; + + if (_subscribedTarget != null) + { + _subscribedTarget.StateChanged += CommandTarget_StateChanged; + _subscribedTarget.CapabilitiesChanged += CommandTarget_CapabilitiesChanged; + ApplyCapabilities(_subscribedTarget.Capabilities); + ApplyState(_subscribedTarget.CurrentState); + } + } + + public void DetachCommandTarget() + { + if (_subscribedTarget == null) + { + return; + } + + _subscribedTarget.StateChanged -= CommandTarget_StateChanged; + _subscribedTarget.CapabilitiesChanged -= CommandTarget_CapabilitiesChanged; + _subscribedTarget = null; + } + + partial void OnCommandTargetChanged(IEditorCommandTarget? newValue) + { + AttachCommandTarget(newValue); + } + + partial void OnSelectedTextColorOptionChanged(EditorColorOption? newValue) => Bindings.Update(); + + partial void OnSelectedHighlightColorOptionChanged(EditorColorOption? newValue) => Bindings.Update(); + + private void OnLoaded(object sender, RoutedEventArgs e) + { + AttachCommandTarget(CommandTarget); + } + + private void OnUnloaded(object sender, RoutedEventArgs e) + { + DetachCommandTarget(); + } + + private void CommandTarget_StateChanged(object? sender, EditorState e) + { + ApplyState(e); + } + + private void CommandTarget_CapabilitiesChanged(object? sender, EditorCapabilities e) + { + ApplyCapabilities(e); + } + + private void ApplyCapabilities(EditorCapabilities capabilities) + { + FontFamilyComboBox.ItemsSource = capabilities.Fonts; + FontSizeComboBox.ItemsSource = capabilities.FontSizes; + AlignmentComboBox.ItemsSource = capabilities.Alignments; + ParagraphStyleComboBox.ItemsSource = capabilities.ParagraphStyles; + _textColorOptions = capabilities.TextColors; + _highlightColorOptions = capabilities.HighlightColors; + TextColorComboBox.ItemsSource = _textColorOptions; + HighlightColorComboBox.ItemsSource = _highlightColorOptions; + LineHeightComboBox.ItemsSource = capabilities.LineHeights; + } + + private void ApplyState(EditorState state) + { + _isApplyingState = true; + + BoldButton.IsChecked = state.IsBold; + ItalicButton.IsChecked = state.IsItalic; + UnderlineButton.IsChecked = state.IsUnderline; + StrikeButton.IsChecked = state.IsStrikethrough; + BulletListButton.IsChecked = state.IsUnorderedList; + OrderedListButton.IsChecked = state.IsOrderedList; + IndentButton.IsEnabled = state.CanIndent; + OutdentButton.IsEnabled = state.CanOutdent; + RemoveLinkButton.IsEnabled = !string.IsNullOrWhiteSpace(state.LinkUrl); + RemoveLinkButton.Visibility = RemoveLinkButton.IsEnabled ? Visibility.Visible : Visibility.Collapsed; + BuiltInToolbarButton.IsChecked = state.IsBuiltInToolbarVisible; + SpellCheckButton.IsChecked = state.IsSpellCheckEnabled; + + AlignmentComboBox.SelectedItem = state.Alignment; + FontFamilyComboBox.SelectedItem = MatchStringItem(FontFamilyComboBox.ItemsSource, state.FontFamily); + FontSizeComboBox.SelectedItem = MatchValueItem(FontSizeComboBox.ItemsSource, state.FontSize); + LineHeightComboBox.SelectedItem = MatchStringItem(LineHeightComboBox.ItemsSource, state.LineHeight); + ParagraphStyleComboBox.SelectedItem = MatchParagraphItem(state.ParagraphStyle); + SelectedTextColorOption = ResolveColorOption(_textColorOptions, state.TextColor); + SelectedHighlightColorOption = ResolveColorOption(_highlightColorOptions, state.HighlightColor); + TextColorComboBox.SelectedItem = SelectedTextColorOption; + HighlightColorComboBox.SelectedItem = SelectedHighlightColorOption; + + _isApplyingState = false; + } + + private static object? MatchStringItem(object? itemsSource, string? value) + { + if (itemsSource is IEnumerable strings) + { + return strings.FirstOrDefault(item => string.Equals(item, value, StringComparison.OrdinalIgnoreCase)); + } + + return null; + } + + private static object? MatchValueItem(object? itemsSource, T? value) where T : struct + { + if (!value.HasValue || itemsSource is not IEnumerable values) + { + return null; + } + + foreach (var item in values) + { + if (EqualityComparer.Default.Equals(item, value.Value)) + { + return item; + } + } + + return null; + } + + private object? MatchParagraphItem(string? tag) + { + if (ParagraphStyleComboBox.ItemsSource is not IEnumerable styles) + { + return null; + } + + return styles.FirstOrDefault(item => string.Equals(item.Tag, tag, StringComparison.OrdinalIgnoreCase)); + } + + private static EditorColorOption? MatchColorItem(IEnumerable colors, string? value) + { + var normalizedValue = value ?? string.Empty; + var matchedByValue = colors.FirstOrDefault(item => string.Equals(item.Value, normalizedValue, StringComparison.OrdinalIgnoreCase)); + if (matchedByValue != null) + { + return matchedByValue; + } + + var targetColor = EditorColorOption.ParseColorValue(value); + return colors.FirstOrDefault(item => item.Brush.Color.Equals(targetColor)); + } + + private static EditorColorOption? ResolveColorOption(IEnumerable colors, string? value) + { + var colorOptions = colors.ToList(); + var matchedColor = MatchColorItem(colors, value); + if (matchedColor != null) + { + return matchedColor; + } + + if (string.IsNullOrWhiteSpace(value)) + { + return colorOptions.FirstOrDefault(item => string.IsNullOrWhiteSpace(item.Value)); + } + + var targetColor = EditorColorOption.ParseColorValue(value); + var selectableColors = colorOptions + .Where(item => !string.IsNullOrWhiteSpace(item.Value)) + .ToList(); + + if (selectableColors.Count == 0) + { + return colorOptions.FirstOrDefault(); + } + + return selectableColors + .OrderBy(item => GetColorDistance(item.Brush.Color, targetColor)) + .First(); + } + + private static int GetColorDistance(Color left, Color right) + { + var redDiff = left.R - right.R; + var greenDiff = left.G - right.G; + var blueDiff = left.B - right.B; + return (redDiff * redDiff) + (greenDiff * greenDiff) + (blueDiff * blueDiff); + } + + private async Task ExecuteAsync(EditorCommand command) + { + if (_isApplyingState || CommandTarget == null) + { + return; + } + + await CommandTarget.ExecuteCommandAsync(command); + } + + private async void BoldButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.ToggleBold()); + private async void ItalicButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.ToggleItalic()); + private async void UnderlineButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.ToggleUnderline()); + private async void StrikeButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.ToggleStrikethrough()); + private async void BulletListButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.ToggleUnorderedList()); + private async void OrderedListButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.ToggleOrderedList()); + private async void IndentButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.Indent()); + private async void OutdentButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.Outdent()); + private async void ImageButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.InsertImage()); + private async void EmojiButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.InsertEmoji()); + private async void RemoveLinkButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.RemoveLink()); + + private async void AlignmentComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (_isApplyingState || AlignmentComboBox.SelectedItem is not EditorTextAlignment alignment) + { + return; + } + + await ExecuteAsync(EditorCommand.SetAlignment(alignment)); + } + + private async void FontFamilyComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (_isApplyingState || FontFamilyComboBox.SelectedItem is not string fontFamily || string.IsNullOrWhiteSpace(fontFamily)) + { + return; + } + + await ExecuteAsync(EditorCommand.SetFontFamily(fontFamily)); + } + + private async void FontSizeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (_isApplyingState || FontSizeComboBox.SelectedItem is not int fontSize) + { + return; + } + + await ExecuteAsync(EditorCommand.SetFontSize(fontSize)); + } + + private async void ParagraphStyleComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (_isApplyingState || ParagraphStyleComboBox.SelectedItem is not EditorParagraphStyleOption paragraphStyle) + { + return; + } + + await ExecuteAsync(EditorCommand.SetParagraphStyle(paragraphStyle.Tag)); + } + + private async void TextColorComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + SelectedTextColorOption = TextColorComboBox.SelectedItem as EditorColorOption; + + if (_isApplyingState || SelectedTextColorOption == null) + { + return; + } + + await ExecuteAsync(EditorCommand.SetTextColor(SelectedTextColorOption.Value)); + } + + private async void HighlightColorComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + SelectedHighlightColorOption = HighlightColorComboBox.SelectedItem as EditorColorOption; + + if (_isApplyingState || SelectedHighlightColorOption == null) + { + return; + } + + await ExecuteAsync(EditorCommand.SetHighlightColor(SelectedHighlightColorOption.Value)); + } + + private async void LineHeightComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (_isApplyingState || LineHeightComboBox.SelectedItem is not string lineHeight || string.IsNullOrWhiteSpace(lineHeight)) + { + return; + } + + await ExecuteAsync(EditorCommand.SetLineHeight(lineHeight)); + } + + private async void BuiltInToolbarButton_Click(object sender, RoutedEventArgs e) + { + await ExecuteAsync(EditorCommand.ToggleBuiltInToolbar(BuiltInToolbarButton.IsChecked == true)); + } + + private async void SpellCheckButton_Click(object sender, RoutedEventArgs e) + { + await ExecuteAsync(EditorCommand.ToggleSpellCheck(SpellCheckButton.IsChecked == true)); + } + + private async void LinkButton_Click(object sender, RoutedEventArgs e) + { + if (CommandTarget == null) + { + return; + } + + var currentState = CommandTarget.CurrentState; + var urlTextBox = new TextBox + { + Header = "URL", + Text = currentState.LinkUrl ?? string.Empty, + PlaceholderText = "https://example.com" + }; + var textTextBox = new TextBox + { + Header = "Text", + Text = currentState.SelectedText ?? string.Empty, + PlaceholderText = "Link text" + }; + var openInNewWindow = new CheckBox + { + Content = "Open in new window", + IsChecked = true + }; + + var dialog = new ContentDialog + { + XamlRoot = XamlRoot, + Title = "Insert link", + PrimaryButtonText = "Apply", + CloseButtonText = "Cancel", + DefaultButton = ContentDialogButton.Primary, + Content = new StackPanel + { + Spacing = 12, + Children = + { + urlTextBox, + textTextBox, + openInNewWindow + } + } + }; + + if (await dialog.ShowAsync() == ContentDialogResult.Primary && !string.IsNullOrWhiteSpace(urlTextBox.Text)) + { + await ExecuteAsync(EditorCommand.InsertLink(new EditorLinkCommandArgs(urlTextBox.Text.Trim(), textTextBox.Text.Trim(), openInNewWindow.IsChecked == true))); + } + } + + private async void TableButton_Click(object sender, RoutedEventArgs e) + { + var rowsBox = new NumberBox + { + Header = "Rows", + Minimum = 1, + Maximum = 10, + SmallChange = 1, + Value = 2 + }; + var columnsBox = new NumberBox + { + Header = "Columns", + Minimum = 1, + Maximum = 10, + SmallChange = 1, + Value = 2 + }; + + var dialog = new ContentDialog + { + XamlRoot = XamlRoot, + Title = "Insert table", + PrimaryButtonText = "Insert", + CloseButtonText = "Cancel", + DefaultButton = ContentDialogButton.Primary, + Content = new StackPanel + { + Spacing = 12, + Children = + { + rowsBox, + columnsBox + } + } + }; + + if (await dialog.ShowAsync() == ContentDialogResult.Primary) + { + await ExecuteAsync(EditorCommand.InsertTable(new EditorTableCommandArgs((int)Math.Max(1, rowsBox.Value), (int)Math.Max(1, columnsBox.Value)))); + } + } + +} + + + diff --git a/Wino.Core.UWP/Controls/EqualGridPanel.cs b/Wino.Mail.WinUI/Controls/EqualGridPanel.cs similarity index 94% rename from Wino.Core.UWP/Controls/EqualGridPanel.cs rename to Wino.Mail.WinUI/Controls/EqualGridPanel.cs index e87eee1f..bba8bc70 100644 --- a/Wino.Core.UWP/Controls/EqualGridPanel.cs +++ b/Wino.Mail.WinUI/Controls/EqualGridPanel.cs @@ -1,8 +1,8 @@ -using Windows.Foundation; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; +using Windows.Foundation; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; -namespace Wino.Core.UWP.Controls; +namespace Wino.Mail.WinUI.Controls; public partial class EqualGridPanel : Panel { diff --git a/Wino.Mail.WinUI/Controls/IAiHtmlActionHost.cs b/Wino.Mail.WinUI/Controls/IAiHtmlActionHost.cs new file mode 100644 index 00000000..24ca2b93 --- /dev/null +++ b/Wino.Mail.WinUI/Controls/IAiHtmlActionHost.cs @@ -0,0 +1,15 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Wino.Mail.WinUI.Controls; + +public interface IAiHtmlActionHost +{ + Task GetCurrentHtmlAsync(CancellationToken cancellationToken); + Task ApplyHtmlResultAsync(string html, CancellationToken cancellationToken); + Task TryGetCachedTranslationHtmlAsync(string languageCode, CancellationToken cancellationToken); + Task SaveCachedTranslationHtmlAsync(string languageCode, string html, CancellationToken cancellationToken); + Task TryGetCachedSummaryTextAsync(CancellationToken cancellationToken); + Task SaveCachedSummaryTextAsync(string summary, CancellationToken cancellationToken); + string GetSuggestedSummaryFileName(); +} diff --git a/Wino.Mail.WinUI/Controls/ImagePreviewControl.cs b/Wino.Mail.WinUI/Controls/ImagePreviewControl.cs new file mode 100644 index 00000000..7bd51b87 --- /dev/null +++ b/Wino.Mail.WinUI/Controls/ImagePreviewControl.cs @@ -0,0 +1,457 @@ +using System; +using System.ComponentModel; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.WinUI; +using EmailValidation; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media.Imaging; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Interfaces; +using Wino.Mail.WinUI; + +namespace Wino.Controls; + +/// +/// Contact avatar control built on top of PersonPicture. +/// Priority: +/// 1) AccountContact file-based picture +/// 2) Gravatar thumbnail (if enabled) +/// 3) Initials from display name fallback +/// +public sealed partial class ImagePreviewControl : PersonPicture +{ + private sealed record RefreshSnapshot(string DisplayName, string Address, Guid? ContactPictureFileId); + + private static readonly TimeSpan RefreshDebounceDuration = TimeSpan.FromMilliseconds(40); + + [GeneratedDependencyProperty] + public partial IMailItemDisplayInformation? MailItemInformation { get; set; } + + [GeneratedDependencyProperty] + public partial AccountContact? PreviewContact { get; set; } + + [GeneratedDependencyProperty] + public partial string? Address { get; set; } + + [GeneratedDependencyProperty] + public partial string? DisplayNameOverride { get; set; } + + private readonly IThumbnailService? _thumbnailService; + private readonly IPreferencesService? _preferencesService; + private readonly IContactPictureFileService? _contactPictureFileService; + private INotifyPropertyChanged? _mailItemInformationPropertySource; + private CancellationTokenSource? _refreshCancellationTokenSource; + private CancellationTokenSource? _scheduledRefreshCancellationTokenSource; + private long _refreshVersion; + + public ImagePreviewControl() + { + DefaultStyleKey = typeof(PersonPicture); + + try + { + _thumbnailService = App.Current.Services.GetService(); + _preferencesService = App.Current.Services.GetService(); + _contactPictureFileService = App.Current.Services.GetService(); + } + catch + { + // Keep control functional in design-time/test contexts without service provider. + } + + Loaded += OnLoaded; + Unloaded += OnUnloaded; + } + + partial void OnMailItemInformationPropertyChanged(DependencyPropertyChangedEventArgs e) + { + if (_mailItemInformationPropertySource != null) + { + _mailItemInformationPropertySource.PropertyChanged -= MailItemInformationPropertyChanged; + _mailItemInformationPropertySource = null; + } + + if (e.NewValue is INotifyPropertyChanged observableMailItemInformation) + { + _mailItemInformationPropertySource = observableMailItemInformation; + _mailItemInformationPropertySource.PropertyChanged += MailItemInformationPropertyChanged; + } + + RequestRefresh(); + } + + partial void OnPreviewContactPropertyChanged(DependencyPropertyChangedEventArgs e) => RequestRefresh(); + partial void OnAddressPropertyChanged(DependencyPropertyChangedEventArgs e) => RequestRefresh(); + partial void OnDisplayNameOverridePropertyChanged(DependencyPropertyChangedEventArgs e) => RequestRefresh(); + + private void OnLoaded(object sender, RoutedEventArgs e) + { + RequestRefresh(); + } + + private void OnUnloaded(object sender, RoutedEventArgs e) + { + if (_mailItemInformationPropertySource != null) + { + _mailItemInformationPropertySource.PropertyChanged -= MailItemInformationPropertyChanged; + _mailItemInformationPropertySource = null; + } + + CancelScheduledRefresh(); + CancelActiveRefresh(); + } + + private void MailItemInformationPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + // Refresh only for fields that affect avatar image or initials. + if (string.IsNullOrEmpty(e.PropertyName) + || e.PropertyName == nameof(IMailItemDisplayInformation.ContactPictureFileId) + || e.PropertyName == nameof(IMailItemDisplayInformation.SenderContact) + || e.PropertyName == nameof(IMailItemDisplayInformation.FromName) + || e.PropertyName == nameof(IMailItemDisplayInformation.FromAddress) + || e.PropertyName == nameof(IMailItemDisplayInformation.ThumbnailUpdatedEvent)) + { + RequestRefresh(); + } + } + + private void RequestRefresh() + { + if (DispatcherQueue == null || DispatcherQueue.HasThreadAccess) + { + QueueRefresh(); + return; + } + + DispatcherQueue.TryEnqueue(QueueRefresh); + } + + private void QueueRefresh() + { + if (!IsLoaded) + return; + + CancelScheduledRefresh(); + + var cts = new CancellationTokenSource(); + _scheduledRefreshCancellationTokenSource = cts; + + _ = DebounceAndRefreshAsync(cts.Token); + } + + private async Task DebounceAndRefreshAsync(CancellationToken cancellationToken) + { + try + { + await Task.Delay(RefreshDebounceDuration, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + return; + } + + StartRefresh(); + } + + private void StartRefresh() + { + CancelActiveRefresh(); + + var cts = new CancellationTokenSource(); + _refreshCancellationTokenSource = cts; + var refreshVersion = Interlocked.Increment(ref _refreshVersion); + _ = RefreshAsync(refreshVersion, cts.Token); + } + + private void CancelScheduledRefresh() + { + var cts = _scheduledRefreshCancellationTokenSource; + _scheduledRefreshCancellationTokenSource = null; + + if (cts != null && !cts.IsCancellationRequested) + { + cts.Cancel(); + } + + cts?.Dispose(); + } + + private void CancelActiveRefresh() + { + var cts = _refreshCancellationTokenSource; + _refreshCancellationTokenSource = null; + + if (cts != null && !cts.IsCancellationRequested) + { + cts.Cancel(); + } + + cts?.Dispose(); + } + + private async Task RefreshAsync(long refreshVersion, CancellationToken cancellationToken) + { + try + { + var snapshot = await CaptureSnapshotAsync(refreshVersion, cancellationToken).ConfigureAwait(false); + if (snapshot == null) + return; + + await ApplyInitialVisualStateAsync(snapshot.DisplayName, refreshVersion, cancellationToken).ConfigureAwait(false); + + // Skip all picture loading if the user has disabled sender pictures. + if (_preferencesService?.IsShowSenderPicturesEnabled == false) + return; + + // 1) File-based contact picture (preferred — native WIC decode, no base64 overhead). + if (snapshot.ContactPictureFileId.HasValue && _contactPictureFileService != null) + { + var filePath = _contactPictureFileService.GetContactPicturePath(snapshot.ContactPictureFileId.Value); + if (!string.IsNullOrEmpty(filePath)) + { + var fileBitmap = await CreateBitmapFromFileAsync(filePath, cancellationToken).ConfigureAwait(false); + if (fileBitmap != null) + { + await ApplyProfilePictureAsync(fileBitmap, refreshVersion, cancellationToken).ConfigureAwait(false); + return; + } + } + } + + // 2) Gravatar lookup through thumbnail service (if enabled). + if (_preferencesService?.IsGravatarEnabled == true && + _thumbnailService != null && + !string.IsNullOrWhiteSpace(snapshot.Address) && + EmailValidator.Validate(snapshot.Address)) + { + var thumbnailBase64 = await _thumbnailService + .GetThumbnailAsync(snapshot.Address.Trim().ToLowerInvariant(), awaitLoad: true) + .ConfigureAwait(false); + + if (!string.IsNullOrWhiteSpace(thumbnailBase64)) + { + var thumbnailBitmap = await CreateBitmapFromBase64Async(thumbnailBase64, cancellationToken).ConfigureAwait(false); + if (thumbnailBitmap != null) + { + await ApplyProfilePictureAsync(thumbnailBitmap, refreshVersion, cancellationToken).ConfigureAwait(false); + return; + } + } + } + + // 3) Initials fallback is already in place via DisplayName + ProfilePicture = null. + } + catch (OperationCanceledException) + { + // Expected during virtualization/recycling. + } + catch + { + // Keep fallback initials if decoding/network fails. + } + } + + // DependencyProperty-backed values must be read on UI thread once, then used off-thread. + private async Task CaptureSnapshotAsync(long refreshVersion, CancellationToken cancellationToken) + { + return await ExecuteOnUiThreadAsync(() => + { + if (!IsActiveRefresh(refreshVersion, cancellationToken)) + return null; + + var address = ResolveAddress(); + var displayName = ResolveDisplayName(address); + var contactPictureFileId = PreviewContact?.ContactPictureFileId + ?? MailItemInformation?.SenderContact?.ContactPictureFileId + ?? MailItemInformation?.ContactPictureFileId; + + return new RefreshSnapshot(displayName, address, contactPictureFileId); + }).ConfigureAwait(false); + } + + private string ResolveAddress() + { + if (!string.IsNullOrWhiteSpace(PreviewContact?.Address)) + return PreviewContact.Address.Trim(); + + if (!string.IsNullOrWhiteSpace(Address)) + return Address.Trim(); + + if (MailItemInformation == null) + return string.Empty; + + var contactAddress = MailItemInformation?.SenderContact?.Address; + if (!string.IsNullOrWhiteSpace(contactAddress)) + return contactAddress.Trim(); + + if (!string.IsNullOrWhiteSpace(MailItemInformation?.FromAddress)) + return MailItemInformation.FromAddress.Trim(); + + return string.Empty; + } + + private string ResolveDisplayName(string resolvedAddress) + { + if (!string.IsNullOrWhiteSpace(PreviewContact?.Name)) + return PreviewContact.Name.Trim(); + + if (!string.IsNullOrWhiteSpace(DisplayNameOverride)) + return DisplayNameOverride.Trim(); + + var contactName = MailItemInformation?.SenderContact?.Name; + if (!string.IsNullOrWhiteSpace(contactName)) + return contactName.Trim(); + + if (!string.IsNullOrWhiteSpace(MailItemInformation?.FromName)) + return MailItemInformation.FromName.Trim(); + + return resolvedAddress.Trim(); + } + private async Task ApplyInitialVisualStateAsync(string displayName, long refreshVersion, CancellationToken cancellationToken) + { + await ExecuteOnUiThreadAsync(() => + { + if (!IsActiveRefresh(refreshVersion, cancellationToken)) + return; + + DisplayName = displayName; + Initials = null; + ProfilePicture = null; + }).ConfigureAwait(false); + } + + private async Task ApplyProfilePictureAsync(BitmapImage bitmapImage, long refreshVersion, CancellationToken cancellationToken) + { + await ExecuteOnUiThreadAsync(() => + { + if (!IsActiveRefresh(refreshVersion, cancellationToken)) + return; + + DisplayName = string.Empty; + Initials = string.Empty; + ProfilePicture = bitmapImage; + }).ConfigureAwait(false); + } + + private bool IsActiveRefresh(long refreshVersion, CancellationToken cancellationToken) + => !cancellationToken.IsCancellationRequested && refreshVersion == _refreshVersion; + + private async Task ExecuteOnUiThreadAsync(Action action) + { + if (DispatcherQueue == null || DispatcherQueue.HasThreadAccess) + { + action(); + return; + } + + var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var enqueued = DispatcherQueue.TryEnqueue(() => + { + try + { + action(); + completion.TrySetResult(null); + } + catch (Exception ex) + { + completion.TrySetException(ex); + } + }); + + if (!enqueued) + { + completion.TrySetException(new InvalidOperationException("Failed to dispatch UI update.")); + } + + await completion.Task.ConfigureAwait(false); + } + + private async Task ExecuteOnUiThreadAsync(Func func) + { + if (DispatcherQueue == null || DispatcherQueue.HasThreadAccess) + { + return func(); + } + + var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var enqueued = DispatcherQueue.TryEnqueue(() => + { + try + { + completion.TrySetResult(func()); + } + catch (Exception ex) + { + completion.TrySetException(ex); + } + }); + + if (!enqueued) + { + completion.TrySetException(new InvalidOperationException("Failed to dispatch UI update.")); + } + + return await completion.Task.ConfigureAwait(false); + } + + private async Task CreateBitmapFromFileAsync(string filePath, CancellationToken cancellationToken) + { + byte[] bytes; + try + { + bytes = await File.ReadAllBytesAsync(filePath, cancellationToken).ConfigureAwait(false); + } + catch + { + return null; + } + + cancellationToken.ThrowIfCancellationRequested(); + + return await ExecuteOnUiThreadAsync(() => + { + cancellationToken.ThrowIfCancellationRequested(); + + using var memoryStream = new MemoryStream(bytes); + var bitmapImage = new BitmapImage(); + bitmapImage.SetSource(memoryStream.AsRandomAccessStream()); + return bitmapImage; + }).ConfigureAwait(false); + } + + private async Task CreateBitmapFromBase64Async(string base64, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(base64)) + return null; + + byte[] bytes; + + try + { + bytes = await Task.Run(() => Convert.FromBase64String(base64), cancellationToken).ConfigureAwait(false); + } + catch + { + return null; + } + + cancellationToken.ThrowIfCancellationRequested(); + + return await ExecuteOnUiThreadAsync(() => + { + cancellationToken.ThrowIfCancellationRequested(); + + using var memoryStream = new MemoryStream(bytes); + var bitmapImage = new BitmapImage(); + bitmapImage.SetSource(memoryStream.AsRandomAccessStream()); + return bitmapImage; + }).ConfigureAwait(false); + } + +} diff --git a/Wino.Mail.WinUI/Controls/ListView/WinoListView.cs b/Wino.Mail.WinUI/Controls/ListView/WinoListView.cs new file mode 100644 index 00000000..fa08b548 --- /dev/null +++ b/Wino.Mail.WinUI/Controls/ListView/WinoListView.cs @@ -0,0 +1,341 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Input; +using CommunityToolkit.WinUI; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Wino.Core.Domain; +using Wino.Core.Domain.Models.MailItem; +using Wino.Mail.ViewModels.Data; + +namespace Wino.Mail.WinUI.Controls.ListView; + +public partial class WinoListView : Microsoft.UI.Xaml.Controls.ListView +{ + private const string PART_ScrollViewer = "ScrollViewer"; + private ScrollViewer? internalScrollviewer; + + [GeneratedDependencyProperty] + public partial bool IsThreadListView { get; set; } + + [GeneratedDependencyProperty] + public partial ICommand? LoadMoreCommand { get; set; } + + public event EventHandler? MailDragStateChanged; + + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + DragItemsStarting -= ItemDragStarting; + DragItemsStarting += ItemDragStarting; + DragItemsCompleted -= ItemDragCompleted; + DragItemsCompleted += ItemDragCompleted; + + internalScrollviewer = GetTemplateChild(PART_ScrollViewer) as ScrollViewer; + + if (internalScrollviewer == null) return; + + internalScrollviewer.ViewChanged -= InternalScrollVeiwerViewChanged; + internalScrollviewer.ViewChanged += InternalScrollVeiwerViewChanged; + } + + private void InternalScrollVeiwerViewChanged(object? sender, ScrollViewerViewChangedEventArgs e) + { + if (internalScrollviewer == null) return; + + // No need to raise init request if there are no items in the list. + if (ItemsSource == null) return; + + double progress = internalScrollviewer.VerticalOffset / internalScrollviewer.ScrollableHeight; + + // Trigger when scrolled past 90% of total height + if (progress >= 0.9) + { + bool canLoadMore = LoadMoreCommand?.CanExecute(null) ?? false; + + if (canLoadMore) + { + LoadMoreCommand?.Execute(null); + } + } + } + + protected override void PrepareContainerForItemOverride(DependencyObject element, object item) + { + base.PrepareContainerForItemOverride(element, item); + + // Ensure the container's selection state matches the model's state + // This is crucial for virtualization scenarios where containers are recycled + + if (item is MailItemViewModel mailItemViewModel + && element is WinoMailItemViewModelListViewItem container + && container.Item != mailItemViewModel) + { + container.Item = mailItemViewModel; + } + else if (item is ThreadMailItemViewModel threadMailItemViewModel + && element is WinoThreadMailItemViewModelListViewItem threadContainer + && threadContainer.Item != threadMailItemViewModel) + { + threadContainer.Item = threadMailItemViewModel; + threadContainer.IsThreadExpanded = threadMailItemViewModel.IsThreadExpanded; + } + } + + protected override void ClearContainerForItemOverride(DependencyObject element, object item) + { + base.ClearContainerForItemOverride(element, item); + + if (item is MailItemViewModel mailItemViewModel && element is WinoMailItemViewModelListViewItem container) + { + container.Item = null; + } + else if (item is ThreadMailItemViewModel threadMailItemViewModel && element is WinoThreadMailItemViewModelListViewItem threadContainer) + { + threadContainer.Item = null; + threadContainer.IsThreadExpanded = false; + } + } + + public WinoMailItemViewModelListViewItem? GetMailItemContainer(MailItemViewModel mailItemViewModel) + { + foreach (var item in Items) + { + if (item is MailItemViewModel mailItem && mailItem.Id == mailItemViewModel.Id) return ContainerFromItem(mailItemViewModel) as WinoMailItemViewModelListViewItem; + if (item is ThreadMailItemViewModel threadMailItem && threadMailItem.GetContainingIds().Contains(mailItemViewModel.MailCopy.UniqueId)) + { + var threadContainer = ContainerFromItem(threadMailItem) as WinoThreadMailItemViewModelListViewItem; + + // Try to get the inner WinoListView. + if (threadContainer != null) + { + var innerListViewControl = threadContainer.GetWinoListViewControl(); + + return innerListViewControl?.ContainerFromItem(mailItemViewModel) as WinoMailItemViewModelListViewItem; + } + } + } + + return null; + } + + public async Task> GetItemContainersAsync(MailItemViewModel mailItemViewModel) + { + WinoMailItemViewModelListViewItem? itemContainer = null; + WinoThreadMailItemViewModelListViewItem? threadContainer = null; + WinoListView? innerListView = null; + + int retryCount = 0; + int maxRetries = 5; + + foreach (var item in Items) + { + if (item is MailItemViewModel mailItem && mailItem.Id == mailItemViewModel.Id) + { + itemContainer = ContainerFromItem(mailItemViewModel) as WinoMailItemViewModelListViewItem; + + // Not realized yet. + if (itemContainer == null) + { + ScrollIntoView(mailItemViewModel); + + // Wait for the container to be generated. + while (itemContainer == null && retryCount < maxRetries) + { + await Task.Delay(100); // Wait a bit for the UI to update + itemContainer = ContainerFromItem(mailItemViewModel) as WinoMailItemViewModelListViewItem; + retryCount++; + } + } + + break; + } + else if (item is ThreadMailItemViewModel threadMailItemViewModel && threadMailItemViewModel.HasUniqueId(mailItemViewModel.MailCopy.UniqueId)) + { + threadContainer = ContainerFromItem(threadMailItemViewModel) as WinoThreadMailItemViewModelListViewItem; + + if (threadContainer == null) + { + ScrollIntoView(threadMailItemViewModel); + + while (threadContainer == null && retryCount < maxRetries) + { + await Task.Delay(100); // Wait a bit for the UI to update + threadContainer = ContainerFromItem(threadMailItemViewModel) as WinoThreadMailItemViewModelListViewItem; + retryCount++; + } + } + + // Try to get the inner WinoListView. + if (threadContainer != null) + { + threadContainer.IsThreadExpanded = true; + + var innerListViewControl = threadContainer.GetWinoListViewControl(); + + if (innerListViewControl != null) + { + innerListView = innerListViewControl; + + itemContainer = innerListViewControl.ContainerFromItem(mailItemViewModel) as WinoMailItemViewModelListViewItem; + + // Item thread has been found but container is not realized yet. + // This could happen when Sent item passed to navigate for Inbox or vice-versa. + // Ideally, we should select the first UniqueId match in the thread in this case. + + if (itemContainer == null) + { + var realThreadItem = innerListViewControl.Items.Cast().FirstOrDefault(a => a.UniqueId == mailItemViewModel.MailCopy.UniqueId); + + if (realThreadItem != null) + { + itemContainer = innerListViewControl.ContainerFromItem(realThreadItem) as WinoMailItemViewModelListViewItem; + } + } + } + } + break; + } + } + + return new Tuple(itemContainer, threadContainer, innerListView); + } + + public void ChangeSelectionMode(ListViewSelectionMode mode) + { + // Not only this control, but also all inner WinoListView controls should change the selection mode. + // TODO: New threads added after this call won't have the correct selection mode. + + SelectionMode = mode; + + foreach (var item in Items) + { + if (item is ThreadMailItemViewModel) + { + var itemContainer = ContainerFromItem(item) as WinoThreadMailItemViewModelListViewItem; + if (itemContainer != null) + { + var innerListViewControl = itemContainer.GetWinoListViewControl(); + innerListViewControl?.ChangeSelectionMode(mode); + } + } + } + } + + public void Cleanup() + { + DragItemsStarting -= ItemDragStarting; + DragItemsCompleted -= ItemDragCompleted; + + if (internalScrollviewer != null) + { + internalScrollviewer.ViewChanged -= InternalScrollVeiwerViewChanged; + } + } + + private void ItemDragStarting(object sender, DragItemsStartingEventArgs args) + { + // Dragging multiple mails from different accounts/folders are supported with the condition below: + // All mails belongs to the drag will be matched on the dropped folder's account. + // Meaning that if users drag 1 mail from Account A/Inbox and 1 mail from Account B/Inbox, + // and drop to Account A/Inbox, the mail from Account B/Inbox will NOT be moved. + + var itemsToDrag = ResolveDraggedMailItems(args); + + if (itemsToDrag.Count == 0) + { + return; + } + + var dragPackage = new MailDragPackage(itemsToDrag.Cast()); + args.Data.Properties.Add(nameof(MailDragPackage), dragPackage); + + var draggingText = string.Format(Translator.MailsDragging, itemsToDrag.Count); + args.Data.SetText(draggingText); + args.Data.Properties.Title = draggingText; + // args.DragUI.SetContentFromDataPackage(); + + MailDragStateChanged?.Invoke(this, new MailDragStateChangedEventArgs(true, itemsToDrag.Count)); + } + + private void ItemDragCompleted(ListViewBase sender, DragItemsCompletedEventArgs args) + { + MailDragStateChanged?.Invoke(this, new MailDragStateChangedEventArgs(false, 0)); + } + + private List ResolveDraggedMailItems(DragItemsStartingEventArgs args) + { + var draggedItems = ExpandDragItems(args.Items.Cast()); + var selectedItems = GetSelectedMailItemsFromCurrentList(); + + if (selectedItems.Count > 1) + { + var selectedIds = selectedItems.Select(a => a.UniqueId).ToHashSet(); + bool dragStartedFromSelection = draggedItems.Any(a => selectedIds.Contains(a.UniqueId)); + + if (dragStartedFromSelection) + { + return selectedItems; + } + } + + return draggedItems.Count > 0 ? draggedItems : selectedItems; + } + + private List GetSelectedMailItemsFromCurrentList() + { + if (IsThreadListView) + { + return Items + .Cast() + .OfType() + .Where(a => a.IsSelected) + .GroupBy(a => a.UniqueId) + .Select(a => a.First()) + .ToList(); + } + + return Items + .Cast() + .OfType() + .SelectMany(a => a.GetSelectedMailItems()) + .GroupBy(a => a.UniqueId) + .Select(a => a.First()) + .ToList(); + } + + private static List ExpandDragItems(IEnumerable dragItems) + { + var result = new List(); + + foreach (var dragItem in dragItems) + { + if (dragItem is MailItemViewModel mailItem) + { + result.Add(mailItem); + } + else if (dragItem is ThreadMailItemViewModel threadItem) + { + result.AddRange(threadItem.ThreadEmails); + } + else if (dragItem is IMailListItem mailListItem) + { + result.AddRange(mailListItem.GetSelectedMailItems()); + } + } + + return result + .GroupBy(a => a.UniqueId) + .Select(a => a.First()) + .ToList(); + } +} + +public sealed class MailDragStateChangedEventArgs(bool isDragging, int draggedItemCount) : EventArgs +{ + public bool IsDragging { get; } = isDragging; + public int DraggedItemCount { get; } = draggedItemCount; +} diff --git a/Wino.Mail.WinUI/Controls/ListView/WinoListViewStyles.xaml b/Wino.Mail.WinUI/Controls/ListView/WinoListViewStyles.xaml new file mode 100644 index 00000000..22338ac8 --- /dev/null +++ b/Wino.Mail.WinUI/Controls/ListView/WinoListViewStyles.xaml @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Wino.Mail.WinUI/Controls/ListView/WinoMailItemContainerStyleSelector.cs b/Wino.Mail.WinUI/Controls/ListView/WinoMailItemContainerStyleSelector.cs new file mode 100644 index 00000000..528c4487 --- /dev/null +++ b/Wino.Mail.WinUI/Controls/ListView/WinoMailItemContainerStyleSelector.cs @@ -0,0 +1,20 @@ +using System; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Wino.Mail.ViewModels.Data; + +namespace Wino.Mail.WinUI.Controls.ListView; + +public partial class WinoMailItemContainerStyleSelector : StyleSelector +{ + public Style? ThreadStyle { get; set; } + public Style? MailItemStyle { get; set; } + protected override Style SelectStyleCore(object item, DependencyObject container) + { + if (item is MailItemViewModel) return MailItemStyle ?? throw new Exception($"Missing style for {nameof(MailItemViewModel)}"); + if (item is ThreadMailItemViewModel) + return ThreadStyle ?? throw new Exception($"Missing style for {nameof(ThreadMailItemViewModel)}"); + + return base.SelectStyleCore(item, container); + } +} diff --git a/Wino.Mail.WinUI/Controls/ListView/WinoMailItemTemplateSelector.cs b/Wino.Mail.WinUI/Controls/ListView/WinoMailItemTemplateSelector.cs new file mode 100644 index 00000000..a079f1dd --- /dev/null +++ b/Wino.Mail.WinUI/Controls/ListView/WinoMailItemTemplateSelector.cs @@ -0,0 +1,30 @@ +using System; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Wino.Core.Domain.Enums; +using Wino.Mail.ViewModels.Data; + +namespace Wino.Mail.WinUI.Controls.ListView; + +public partial class WinoMailItemTemplateSelector : DataTemplateSelector +{ + public DataTemplate? SingleMailItemTemplate { get; set; } + public DataTemplate? ThreadMailItemTemplate { get; set; } + public DataTemplate? CalendarMailItemTemplate { get; set; } + + protected override DataTemplate SelectTemplateCore(object item, DependencyObject container) + { + if (item is MailItemViewModel mailItemViewModel) + { + // Check if it's a calendar-related item + if (mailItemViewModel.MailCopy.ItemType != MailItemType.Mail && CalendarMailItemTemplate != null) + return CalendarMailItemTemplate; + + return SingleMailItemTemplate ?? throw new Exception($"Missing template for single mail items."); + } + else if (item is ThreadMailItemViewModel) + return ThreadMailItemTemplate ?? throw new Exception($"Missing template for thread mail items."); + + return base.SelectTemplateCore(item, container); + } +} diff --git a/Wino.Mail.WinUI/Controls/ListView/WinoMailItemViewModelListViewItem.cs b/Wino.Mail.WinUI/Controls/ListView/WinoMailItemViewModelListViewItem.cs new file mode 100644 index 00000000..91f6816f --- /dev/null +++ b/Wino.Mail.WinUI/Controls/ListView/WinoMailItemViewModelListViewItem.cs @@ -0,0 +1,38 @@ +using CommunityToolkit.WinUI; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Wino.Mail.ViewModels.Data; +using WinRT; + +namespace Wino.Mail.WinUI.Controls.ListView; + +[GeneratedBindableCustomProperty] +public partial class WinoMailItemViewModelListViewItem : ListViewItem +{ + [GeneratedDependencyProperty] + public partial MailItemViewModel? Item { get; set; } + + [GeneratedDependencyProperty] + public partial bool IsCustomSelected { get; set; } + + public WinoMailItemViewModelListViewItem() + { + DefaultStyleKey = typeof(WinoMailItemViewModelListViewItem); + } + + partial void OnItemPropertyChanged(DependencyPropertyChangedEventArgs e) + { + if (e.OldValue is MailItemViewModel oldItem) + oldItem.OnSelectionChanged = null; + + if (e.NewValue is MailItemViewModel newItem) + { + newItem.OnSelectionChanged = (selected) => IsCustomSelected = selected; + IsCustomSelected = newItem.IsSelected; + } + else + { + IsCustomSelected = false; + } + } +} diff --git a/Wino.Mail.WinUI/Controls/ListView/WinoThreadMailItemViewModelListViewItem.cs b/Wino.Mail.WinUI/Controls/ListView/WinoThreadMailItemViewModelListViewItem.cs new file mode 100644 index 00000000..7114eec7 --- /dev/null +++ b/Wino.Mail.WinUI/Controls/ListView/WinoThreadMailItemViewModelListViewItem.cs @@ -0,0 +1,55 @@ +using System.Linq; +using CommunityToolkit.WinUI; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Wino.Controls; +using Wino.Helpers; +using Wino.Mail.ViewModels.Data; +using WinRT; + +namespace Wino.Mail.WinUI.Controls.ListView; + +[GeneratedBindableCustomProperty] +public partial class WinoThreadMailItemViewModelListViewItem : ListViewItem +{ + [GeneratedDependencyProperty] + public partial bool IsThreadExpanded { get; set; } + + [GeneratedDependencyProperty] + public partial ThreadMailItemViewModel? Item { get; set; } + + [GeneratedDependencyProperty] + public partial bool IsCustomSelected { get; set; } + + public WinoThreadMailItemViewModelListViewItem() + { + DefaultStyleKey = typeof(WinoThreadMailItemViewModelListViewItem); + } + + public WinoListView? GetWinoListViewControl() + { + var expander = GetExpander(); + + if (expander?.Content is WinoListView control) return control; + + return null; + } + + public WinoExpander? GetExpander() => WinoVisualTreeHelper.FindDescendants(this).FirstOrDefault(); + + partial void OnItemPropertyChanged(DependencyPropertyChangedEventArgs e) + { + if (e.OldValue is ThreadMailItemViewModel oldItem) + oldItem.OnSelectionChanged = null; + + if (e.NewValue is ThreadMailItemViewModel newItem) + { + newItem.OnSelectionChanged = (selected) => IsCustomSelected = selected; + IsCustomSelected = newItem.IsSelected; + } + else + { + IsCustomSelected = false; + } + } +} diff --git a/Wino.Mail/Controls/MailItemDisplayInformationControl.xaml b/Wino.Mail.WinUI/Controls/MailItemDisplayInformationControl.xaml similarity index 88% rename from Wino.Mail/Controls/MailItemDisplayInformationControl.xaml rename to Wino.Mail.WinUI/Controls/MailItemDisplayInformationControl.xaml index 92d3332a..b08d2f4b 100644 --- a/Wino.Mail/Controls/MailItemDisplayInformationControl.xaml +++ b/Wino.Mail.WinUI/Controls/MailItemDisplayInformationControl.xaml @@ -1,10 +1,10 @@ - + PointerExited="ControlPointerExited" + Unloaded="OnUnloaded"> + + + + + + + + diff --git a/Wino.Core.UWP/Dialogs/AccountPickerDialog.xaml.cs b/Wino.Mail.WinUI/Dialogs/AccountPickerDialog.xaml.cs similarity index 70% rename from Wino.Core.UWP/Dialogs/AccountPickerDialog.xaml.cs rename to Wino.Mail.WinUI/Dialogs/AccountPickerDialog.xaml.cs index f07a4071..1c8c19c6 100644 --- a/Wino.Core.UWP/Dialogs/AccountPickerDialog.xaml.cs +++ b/Wino.Mail.WinUI/Dialogs/AccountPickerDialog.xaml.cs @@ -1,14 +1,14 @@ -using System.Collections.Generic; -using Windows.UI.Xaml.Controls; +using System.Collections.Generic; +using Microsoft.UI.Xaml.Controls; using Wino.Core.Domain.Entities.Shared; namespace Wino.Dialogs; public sealed partial class AccountPickerDialog : ContentDialog { - public MailAccount PickedAccount { get; set; } + public MailAccount? PickedAccount { get; set; } - public List AvailableAccounts { get; set; } + public List AvailableAccounts { get; set; } = []; public AccountPickerDialog(List availableAccounts) { diff --git a/Wino.Mail.WinUI/Dialogs/AccountReorderDialog.xaml b/Wino.Mail.WinUI/Dialogs/AccountReorderDialog.xaml new file mode 100644 index 00000000..16fbea2e --- /dev/null +++ b/Wino.Mail.WinUI/Dialogs/AccountReorderDialog.xaml @@ -0,0 +1,117 @@ + + + + 420 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wino.Mail/Dialogs/AccountReorderDialog.xaml.cs b/Wino.Mail.WinUI/Dialogs/AccountReorderDialog.xaml.cs similarity index 65% rename from Wino.Mail/Dialogs/AccountReorderDialog.xaml.cs rename to Wino.Mail.WinUI/Dialogs/AccountReorderDialog.xaml.cs index 9bbcae64..37f59975 100644 --- a/Wino.Mail/Dialogs/AccountReorderDialog.xaml.cs +++ b/Wino.Mail.WinUI/Dialogs/AccountReorderDialog.xaml.cs @@ -1,23 +1,25 @@ -using System.Collections.ObjectModel; +using System; +using System.Collections.ObjectModel; using System.Linq; using Microsoft.Extensions.DependencyInjection; -using Windows.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls; using Wino.Core.Domain.Interfaces; +using Wino.Mail.WinUI; namespace Wino.Dialogs; public sealed partial class AccountReorderDialog : ContentDialog { - public ObservableCollection Accounts { get; } + public ObservableCollection Accounts { get; set; } = null!; private int count; private bool isOrdering = false; - private readonly IAccountService _accountService = App.Current.Services.GetService(); + private readonly IAccountService? _accountService = App.Current.Services.GetService(); - public AccountReorderDialog(ObservableCollection accounts) + public AccountReorderDialog(ObservableCollection? accounts) { - Accounts = accounts; + Accounts = accounts ?? throw new ArgumentNullException(nameof(accounts)); count = accounts.Count; @@ -32,7 +34,7 @@ public sealed partial class AccountReorderDialog : ContentDialog private void DialogClosed(ContentDialog sender, ContentDialogClosedEventArgs args) => Accounts.CollectionChanged -= AccountsChanged; - private async void AccountsChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) + private async void AccountsChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { if (count - 1 == Accounts.Count) isOrdering = true; @@ -43,7 +45,8 @@ public sealed partial class AccountReorderDialog : ContentDialog var dict = Accounts.ToDictionary(a => a.StartupEntityId, a => Accounts.IndexOf(a)); - await _accountService.UpdateAccountOrdersAsync(dict); + if (_accountService != null) + await _accountService.UpdateAccountOrdersAsync(dict); isOrdering = false; } diff --git a/Wino.Mail.WinUI/Dialogs/ContactEditDialog.xaml b/Wino.Mail.WinUI/Dialogs/ContactEditDialog.xaml new file mode 100644 index 00000000..02da60f2 --- /dev/null +++ b/Wino.Mail.WinUI/Dialogs/ContactEditDialog.xaml @@ -0,0 +1,106 @@ + + + + 400 + + + + + + + + + + + + + + + + + + + + + + +