Removed migrations. New onboarding screen and wizard like steps.
This commit is contained in:
@@ -0,0 +1,160 @@
|
|||||||
|
---
|
||||||
|
description: 'WinUI 3 and Windows App SDK coding guidelines. Prevents common UWP API misuse, enforces correct XAML namespaces, threading, windowing, and MVVM patterns for desktop Windows apps.'
|
||||||
|
applyTo: '**/*.xaml, **/*.cs, **/*.csproj'
|
||||||
|
---
|
||||||
|
|
||||||
|
# WinUI 3 / Windows App SDK
|
||||||
|
|
||||||
|
## Critical Rules — NEVER Use Legacy UWP APIs
|
||||||
|
|
||||||
|
These UWP patterns are **wrong** for WinUI 3 desktop apps. Always use the Windows App SDK equivalent.
|
||||||
|
|
||||||
|
- **NEVER** use `Windows.UI.Popups.MessageDialog`. Use `ContentDialog` with `XamlRoot` set.
|
||||||
|
- **NEVER** show a `ContentDialog` without setting `dialog.XamlRoot = this.Content.XamlRoot` first.
|
||||||
|
- **NEVER** use `CoreDispatcher.RunAsync` or `Dispatcher.RunAsync`. Use `DispatcherQueue.TryEnqueue`.
|
||||||
|
- **NEVER** use `Window.Current`. Track the main window via a static `App.MainWindow` property.
|
||||||
|
- **NEVER** use `Windows.UI.Xaml.*` namespaces. Use `Microsoft.UI.Xaml.*`.
|
||||||
|
- **NEVER** use `Windows.UI.Composition`. Use `Microsoft.UI.Composition`.
|
||||||
|
- **NEVER** use `Windows.UI.Colors`. Use `Microsoft.UI.Colors`.
|
||||||
|
- **NEVER** use `ApplicationView` or `CoreWindow` for window management. Use `Microsoft.UI.Windowing.AppWindow`.
|
||||||
|
- **NEVER** use `CoreApplicationViewTitleBar`. Use `AppWindowTitleBar`.
|
||||||
|
- **NEVER** use `GetForCurrentView()` patterns (e.g., `UIViewSettings.GetForCurrentView()`). These do not exist in desktop WinUI 3. Use `AppWindow` APIs instead.
|
||||||
|
- **NEVER** use UWP `PrintManager` directly. Use `IPrintManagerInterop` with a window handle.
|
||||||
|
- **NEVER** use `DataTransferManager` directly for sharing. Use `IDataTransferManagerInterop` with a window handle.
|
||||||
|
- **NEVER** use UWP `IBackgroundTask`. Use `Microsoft.Windows.AppLifecycle` activation.
|
||||||
|
- **NEVER** use `WebAuthenticationBroker`. Use `OAuth2Manager` (Windows App SDK 1.7+).
|
||||||
|
|
||||||
|
## XAML Patterns
|
||||||
|
|
||||||
|
- The default XAML namespace maps to `Microsoft.UI.Xaml`, not `Windows.UI.Xaml`.
|
||||||
|
- Prefer `{x:Bind}` over `{Binding}` for compiled, type-safe, higher-performance bindings.
|
||||||
|
- Set `x:DataType` on `DataTemplate` elements when using `{x:Bind}` — this is required for compiled bindings in templates. On Page/UserControl, `x:DataType` enables compile-time binding validation but is not strictly required if the DataContext does not change.
|
||||||
|
- Use `Mode=OneWay` for dynamic values, `Mode=OneTime` for static, `Mode=TwoWay` only for editable inputs.
|
||||||
|
- Do not bind static constants — set them directly in XAML.
|
||||||
|
|
||||||
|
## Threading
|
||||||
|
|
||||||
|
- Use `DispatcherQueue.TryEnqueue(() => { ... })` to update UI from background threads.
|
||||||
|
- `TryEnqueue` returns `bool`, not a `Task` — it is fire-and-forget.
|
||||||
|
- Check thread access with `DispatcherQueue.HasThreadAccess` before dispatching.
|
||||||
|
- WinUI 3 uses standard STA (not ASTA). No built-in reentrancy protection — be cautious with async code that pumps messages.
|
||||||
|
|
||||||
|
## Windowing
|
||||||
|
|
||||||
|
- Get the `AppWindow` from a WinUI 3 `Window` via `WindowNative.GetWindowHandle` → `Win32Interop.GetWindowIdFromWindow` → `AppWindow.GetFromWindowId`.
|
||||||
|
- Use `AppWindow` for resize, move, title, and presenter operations.
|
||||||
|
- Custom title bar: use `AppWindow.TitleBar` properties, not `CoreApplicationViewTitleBar`.
|
||||||
|
- Track the main window as `App.MainWindow` (a static property set in `OnLaunched`).
|
||||||
|
|
||||||
|
## Dialogs and Pickers
|
||||||
|
|
||||||
|
- **ContentDialog**: Always set `dialog.XamlRoot = this.Content.XamlRoot` before calling `ShowAsync()`.
|
||||||
|
- **File/Folder Pickers**: Initialize with `WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd)` where `hwnd` comes from `WindowNative.GetWindowHandle(App.MainWindow)`.
|
||||||
|
- **Share/Print**: Use COM interop interfaces (`IDataTransferManagerInterop`, `IPrintManagerInterop`) with window handles.
|
||||||
|
|
||||||
|
## MVVM and Data Binding
|
||||||
|
|
||||||
|
- Prefer `CommunityToolkit.Mvvm` (`[ObservableProperty]`, `[RelayCommand]`) for MVVM infrastructure.
|
||||||
|
- Use `Microsoft.Extensions.DependencyInjection` for service registration and injection.
|
||||||
|
- Keep UI (Views) focused on layout and bindings; keep logic in ViewModels and services.
|
||||||
|
- Use `async`/`await` for I/O and long-running work to keep the UI responsive.
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
- Target `net10.0-windows10.0.22621.0` (or appropriate TFM for the project's target SDK).
|
||||||
|
- Set `<UseWinUI>true</UseWinUI>` in the project file.
|
||||||
|
- Reference the latest stable `Microsoft.WindowsAppSDK` NuGet package.
|
||||||
|
- Use `System.Text.Json` with source generators for JSON serialization.
|
||||||
|
|
||||||
|
## C# Code Style
|
||||||
|
|
||||||
|
- Use file-scoped namespaces.
|
||||||
|
- Enable nullable reference types. Use `is null` / `is not null` instead of `== null`.
|
||||||
|
- Prefer pattern matching over `as`/`is` with null checks.
|
||||||
|
- PascalCase for types, methods, properties. camelCase for private fields.
|
||||||
|
- Allman brace style (opening brace on its own line).
|
||||||
|
- Prefer explicit types for built-in types; use `var` only when the type is obvious.
|
||||||
|
|
||||||
|
## Accessibility
|
||||||
|
|
||||||
|
- Set `AutomationProperties.Name` on all interactive controls.
|
||||||
|
- Use `AutomationProperties.HeadingLevel` on section headers.
|
||||||
|
- Hide decorative elements with `AutomationProperties.AccessibilityView="Raw"`.
|
||||||
|
- Ensure full keyboard navigation (Tab, Enter, Space, arrow keys).
|
||||||
|
- Meet WCAG color contrast requirements.
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- Prefer `{x:Bind}` (compiled) over `{Binding}` (reflection-based).
|
||||||
|
- **NativeAOT:** Under Native AOT compilation, `{Binding}` (reflection-based) does not work at all. Only `{x:Bind}` (compiled bindings) is supported. If the project uses NativeAOT, use `{x:Bind}` exclusively.
|
||||||
|
- Use `x:Load` or `x:DeferLoadStrategy` for UI elements that are not immediately needed.
|
||||||
|
- Use `ItemsRepeater` with virtualization for large lists.
|
||||||
|
- Avoid deep layout nesting — prefer `Grid` over nested `StackPanel` chains.
|
||||||
|
- Use `async`/`await` for all I/O; never block the UI thread.
|
||||||
|
|
||||||
|
## App Settings (Packaged vs Unpackaged)
|
||||||
|
|
||||||
|
- **Packaged apps**: `ApplicationData.Current.LocalSettings` works as expected.
|
||||||
|
- **Unpackaged apps**: Use a custom settings file (e.g., JSON in `Environment.GetFolderPath(SpecialFolder.LocalApplicationData)`).
|
||||||
|
- Do not assume `ApplicationData` is always available — check packaging status first.
|
||||||
|
|
||||||
|
## Typography
|
||||||
|
|
||||||
|
- **Always** use built-in TextBlock styles (`CaptionTextBlockStyle`, `BodyTextBlockStyle`, `BodyStrongTextBlockStyle`, `SubtitleTextBlockStyle`, `TitleTextBlockStyle`, `TitleLargeTextBlockStyle`, `DisplayTextBlockStyle`).
|
||||||
|
- Prefer using the built-in TextBlock styles over hardcoding `FontSize`, `FontWeight`, or `FontFamily`.
|
||||||
|
- Font: Segoe UI Variable is the default — do not change it.
|
||||||
|
- Use sentence casing for all UI text.
|
||||||
|
|
||||||
|
|
||||||
|
## Theming & Colors
|
||||||
|
|
||||||
|
- **Always** use `{ThemeResource}` for brushes and colors to support Light, Dark, and High Contrast themes automatically.
|
||||||
|
- **Never** hardcode color values (`#FFFFFF`, `Colors.White`, etc.) for UI elements. Use theme resources like `TextFillColorPrimaryBrush`, `CardBackgroundFillColorDefaultBrush`, `CardStrokeColorDefaultBrush`.
|
||||||
|
- Use `SystemAccentColor` (and `Light1`–`Light3`, `Dark1`–`Dark3` variants) for the user's accent color palette.
|
||||||
|
- For borders: use `CardStrokeColorDefaultBrush` or `ControlStrokeColorDefaultBrush`.
|
||||||
|
|
||||||
|
## Spacing & Layout
|
||||||
|
|
||||||
|
- Use a **4px grid system**: all margins, padding, and spacing values must be multiples of 4px.
|
||||||
|
- Standard spacing: 4 (compact), 8 (controls), 12 (small gutters), 16 (content padding), 24 (large gutters).
|
||||||
|
- Prefer `Grid` over deeply nested `StackPanel` chains for performance.
|
||||||
|
- Use `Auto` for content-sized rows/columns, `*` for proportional sizing. Avoid fixed pixel sizes.
|
||||||
|
- Use `VisualStateManager` with `AdaptiveTrigger` for responsive layouts at breakpoints (640px, 1008px).
|
||||||
|
- Use `ControlCornerRadius` (4px) for small controls and `OverlayCornerRadius` (8px) for cards, dialogs, flyouts.
|
||||||
|
|
||||||
|
## Materials & Elevation
|
||||||
|
|
||||||
|
- Use **Mica** (`MicaBackdrop`) for the app window backdrop. Requires transparent layers above to show through.
|
||||||
|
- Use **Acrylic** for transient surfaces only (flyouts, menus, navigation panes).
|
||||||
|
- Use `LayerFillColorDefaultBrush` for content layers above Mica.
|
||||||
|
- Use `ThemeShadow` with Z-axis `Translation` for elevation. Cards: 4–8 px, Flyouts: 32 px, Dialogs: 128 px.
|
||||||
|
|
||||||
|
## Motion & Transitions
|
||||||
|
|
||||||
|
- Use built-in theme transitions (`EntranceThemeTransition`, `RepositionThemeTransition`, `ContentThemeTransition`, `AddDeleteThemeTransition`).
|
||||||
|
- Avoid custom storyboard animations when a built-in transition exists.
|
||||||
|
|
||||||
|
## Control Selection
|
||||||
|
|
||||||
|
- Use `NavigationView` for primary app navigation (not custom sidebars).
|
||||||
|
- Use `InfoBar` for persistent in-app notifications (not custom banners).
|
||||||
|
- Use `TeachingTip` for contextual guidance (not custom popups).
|
||||||
|
- Use `NumberBox` for numeric input (not TextBox with manual validation).
|
||||||
|
- Use `ToggleSwitch` for on/off settings (not CheckBox).
|
||||||
|
- Use `ItemsView` as the modern collection control for displaying data with built-in selection, virtualization, and layout flexibility.
|
||||||
|
- Use `ListView`/`GridView` for standard virtualized lists and grids, especially when built-in selection support is needed.
|
||||||
|
- Use `ItemsRepeater` only for fully custom virtualizing layouts where you need complete control over rendering and do not need built-in selection or interaction handling.
|
||||||
|
- Use `Expander` for collapsible sections (not custom visibility toggling).
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- Always wrap `async void` event handlers in try/catch to prevent unhandled crashes.
|
||||||
|
- Use `InfoBar` (with `Severity = Error`) for user-facing error messages, not `ContentDialog` for routine errors.
|
||||||
|
- Handle `App.UnhandledException` for logging and graceful recovery.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- **NEVER** use a plain MSTest or xUnit project for tests that instantiate WinUI 3 XAML types. Use a **Unit Test App (WinUI in Desktop)** project, which provides the Xaml runtime and UI thread.
|
||||||
|
- Use `[TestMethod]` for pure logic tests. Use `[UITestMethod]` for any test that creates or interacts with `Microsoft.UI.Xaml` types (controls, pages, user controls).
|
||||||
|
- Place testable business logic in a **Class Library (WinUI in Desktop)** project, separate from the main app.
|
||||||
|
- Build the solution before running tests to enable Visual Studio test discovery.
|
||||||
@@ -10,6 +10,5 @@ namespace Wino.Core.Domain;
|
|||||||
[JsonSerializable(typeof(List<string>))]
|
[JsonSerializable(typeof(List<string>))]
|
||||||
[JsonSerializable(typeof(bool))]
|
[JsonSerializable(typeof(bool))]
|
||||||
[JsonSerializable(typeof(UpdateNotes))]
|
[JsonSerializable(typeof(UpdateNotes))]
|
||||||
[JsonSerializable(typeof(UpdateMigration))]
|
|
||||||
[JsonSerializable(typeof(List<UpdateNoteSection>))]
|
[JsonSerializable(typeof(List<UpdateNoteSection>))]
|
||||||
public partial class BasicTypesJsonContext : JsonSerializerContext;
|
public partial class BasicTypesJsonContext : JsonSerializerContext;
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ public static class Constants
|
|||||||
public const string ToastModeKey = nameof(ToastModeKey);
|
public const string ToastModeKey = nameof(ToastModeKey);
|
||||||
public const string ToastModeMail = nameof(ToastModeMail);
|
public const string ToastModeMail = nameof(ToastModeMail);
|
||||||
public const string ToastModeCalendar = nameof(ToastModeCalendar);
|
public const string ToastModeCalendar = nameof(ToastModeCalendar);
|
||||||
public const string ToastMigrationRequiredKey = nameof(ToastMigrationRequiredKey);
|
|
||||||
|
|
||||||
public const string ClientLogFile = "Client_.log";
|
public const string ClientLogFile = "Client_.log";
|
||||||
public const string ServerLogFile = "Server_.log";
|
public const string ServerLogFile = "Server_.log";
|
||||||
public const string LogArchiveFileName = "WinoLogs.zip";
|
public const string LogArchiveFileName = "WinoLogs.zip";
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Wino.Core.Domain.Enums;
|
||||||
|
|
||||||
|
public enum AccountSetupStepStatus
|
||||||
|
{
|
||||||
|
Pending,
|
||||||
|
InProgress,
|
||||||
|
Succeeded,
|
||||||
|
Failed
|
||||||
|
}
|
||||||
@@ -35,5 +35,9 @@ public enum WinoPage
|
|||||||
EventDetailsPage,
|
EventDetailsPage,
|
||||||
SignatureAndEncryptionPage,
|
SignatureAndEncryptionPage,
|
||||||
StoragePage,
|
StoragePage,
|
||||||
WelcomePageV2
|
WelcomePageV2,
|
||||||
|
WelcomeHostPage,
|
||||||
|
ProviderSelectionPage,
|
||||||
|
AccountSetupProgressPage,
|
||||||
|
SpecialImapCredentialsPage
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Interfaces;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a one-time app or data migration that runs when a user updates to a new version.
|
|
||||||
/// </summary>
|
|
||||||
public interface IAppMigration
|
|
||||||
{
|
|
||||||
/// <summary>Gets the unique identifier for this migration, used to track completion in local settings.</summary>
|
|
||||||
string MigrationId { get; }
|
|
||||||
|
|
||||||
/// <summary>Executes the migration logic.</summary>
|
|
||||||
Task ExecuteAsync();
|
|
||||||
}
|
|
||||||
@@ -40,10 +40,4 @@ public interface INotificationBuilder
|
|||||||
/// Creates a calendar reminder toast for the specified calendar item.
|
/// Creates a calendar reminder toast for the specified calendar item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task CreateCalendarReminderNotificationAsync(CalendarItem calendarItem, long reminderDurationInSeconds);
|
Task CreateCalendarReminderNotificationAsync(CalendarItem calendarItem, long reminderDurationInSeconds);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Shows a notification that a migration is required for the new app version.
|
|
||||||
/// Synchronization is stopped and the user is prompted to open the app.
|
|
||||||
/// </summary>
|
|
||||||
void CreateMigrationRequiredNotification();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Wino.Core.Domain.Models.Updates;
|
using Wino.Core.Domain.Models.Updates;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Interfaces;
|
namespace Wino.Core.Domain.Interfaces;
|
||||||
|
|
||||||
@@ -18,12 +18,4 @@ public interface IUpdateManager
|
|||||||
/// <summary>Stores a flag in local settings indicating the update notes for the current version have been seen.</summary>
|
/// <summary>Stores a flag in local settings indicating the update notes for the current version have been seen.</summary>
|
||||||
void MarkUpdateNotesAsSeen();
|
void MarkUpdateNotesAsSeen();
|
||||||
|
|
||||||
/// <summary>Returns true if any registered migration has not yet been completed.</summary>
|
|
||||||
bool HasPendingMigrations();
|
|
||||||
|
|
||||||
/// <summary>Runs all pending migrations in order and marks each as completed in local settings.</summary>
|
|
||||||
Task RunPendingMigrationsAsync();
|
|
||||||
|
|
||||||
/// <summary>Registers migrations to be tracked and executed by this manager.</summary>
|
|
||||||
void RegisterMigrations(IEnumerable<IAppMigration> migrations);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Models.Updates;
|
|
||||||
|
|
||||||
public class UpdateMigration
|
|
||||||
{
|
|
||||||
[JsonPropertyName("titleKey")]
|
|
||||||
public string TitleKey { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
[JsonPropertyName("descriptionKey")]
|
|
||||||
public string DescriptionKey { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Models.Updates;
|
namespace Wino.Core.Domain.Models.Updates;
|
||||||
|
|
||||||
public class UpdateNotes
|
public class UpdateNotes
|
||||||
{
|
{
|
||||||
[JsonPropertyName("hasPendingMigrations")]
|
|
||||||
public bool HasPendingMigrations { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("migration")]
|
|
||||||
public UpdateMigration Migration { get; set; } = new();
|
|
||||||
|
|
||||||
[JsonPropertyName("sections")]
|
|
||||||
public List<UpdateNoteSection> Sections { get; set; } = [];
|
public List<UpdateNoteSection> Sections { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1004,5 +1004,45 @@
|
|||||||
"WelcomeWindow_SetupSubtitle": "Choose your email provider to get started",
|
"WelcomeWindow_SetupSubtitle": "Choose your email provider to get started",
|
||||||
"WelcomeWindow_AddAccountButton": "Add account",
|
"WelcomeWindow_AddAccountButton": "Add account",
|
||||||
"WelcomeWindow_SkipForNow": "Skip for now — I'll set it up later",
|
"WelcomeWindow_SkipForNow": "Skip for now — I'll set it up later",
|
||||||
"WelcomeWindow_AppDescription": "A fast, focused inbox — redesigned for Windows 11"
|
"WelcomeWindow_AppDescription": "A fast, focused inbox — redesigned for Windows 11",
|
||||||
|
"WelcomeWizard_Step1Title": "Welcome",
|
||||||
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,15 @@ public partial class BreadcrumbNavigationItemViewModel : ObservableObject
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool isActive;
|
private bool isActive;
|
||||||
|
|
||||||
|
public int StepNumber { get; set; }
|
||||||
|
|
||||||
public BreadcrumbNavigationRequested Request { get; set; }
|
public BreadcrumbNavigationRequested Request { get; set; }
|
||||||
|
|
||||||
public BreadcrumbNavigationItemViewModel(BreadcrumbNavigationRequested request, bool isActive)
|
public BreadcrumbNavigationItemViewModel(BreadcrumbNavigationRequested request, bool isActive, int stepNumber = 0)
|
||||||
{
|
{
|
||||||
Request = request;
|
Request = request;
|
||||||
Title = request.PageTitle;
|
Title = request.PageTitle;
|
||||||
IsActive = isActive;
|
IsActive = isActive;
|
||||||
|
StepNumber = stepNumber;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -86,249 +86,7 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
MailAccount createdAccount = null;
|
Messenger.Send(new BreadcrumbNavigationRequested(Translator.WelcomeWizard_Step2Title, WinoPage.ProviderSelectionPage));
|
||||||
IAccountCreationDialog creationDialog = null;
|
|
||||||
bool creationDialogClosed = false;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var providers = ProviderService.GetAvailableProviders();
|
|
||||||
|
|
||||||
// Select provider.
|
|
||||||
var accountCreationDialogResult = await ExecuteUIThreadTaskAsync(() => MailDialogService.ShowAccountProviderSelectionDialogAsync(providers));
|
|
||||||
|
|
||||||
if (accountCreationDialogResult != null)
|
|
||||||
{
|
|
||||||
CustomServerInformation customServerInformation = null;
|
|
||||||
|
|
||||||
createdAccount = new MailAccount()
|
|
||||||
{
|
|
||||||
ProviderType = accountCreationDialogResult.ProviderType,
|
|
||||||
Name = accountCreationDialogResult.AccountName,
|
|
||||||
SpecialImapProvider = accountCreationDialogResult.SpecialImapProviderDetails?.SpecialImapProvider ?? SpecialImapProvider.None,
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
AccountColorHex = accountCreationDialogResult.AccountColorHex,
|
|
||||||
IsCalendarAccessGranted = true // New accounts have calendar scopes
|
|
||||||
};
|
|
||||||
|
|
||||||
if (accountCreationDialogResult.ProviderType == MailProviderType.IMAP4)
|
|
||||||
{
|
|
||||||
if (createdAccount.SpecialImapProvider == SpecialImapProvider.iCloud || createdAccount.SpecialImapProvider == SpecialImapProvider.Yahoo)
|
|
||||||
{
|
|
||||||
var accountCreationCancellationTokenSource = new CancellationTokenSource();
|
|
||||||
creationDialog = MailDialogService.GetAccountCreationDialog(accountCreationDialogResult);
|
|
||||||
|
|
||||||
await ExecuteUIThreadTaskAsync(() => creationDialog.ShowDialogAsync(accountCreationCancellationTokenSource));
|
|
||||||
await Task.Delay(500);
|
|
||||||
|
|
||||||
await ExecuteUIThread(() => creationDialog.State = AccountCreationDialogState.SigningIn);
|
|
||||||
|
|
||||||
customServerInformation = _specialImapProviderConfigResolver.GetServerInformation(createdAccount, accountCreationDialogResult)
|
|
||||||
?? throw new AccountSetupCanceledException();
|
|
||||||
|
|
||||||
customServerInformation.Id = Guid.NewGuid();
|
|
||||||
customServerInformation.AccountId = createdAccount.Id;
|
|
||||||
|
|
||||||
createdAccount.Address = accountCreationDialogResult.SpecialImapProviderDetails.Address;
|
|
||||||
createdAccount.SenderName = accountCreationDialogResult.SpecialImapProviderDetails.SenderName;
|
|
||||||
createdAccount.IsCalendarAccessGranted = customServerInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled;
|
|
||||||
createdAccount.ServerInformation = customServerInformation;
|
|
||||||
|
|
||||||
await ValidateSpecialImapConnectivityAsync(customServerInformation).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var completionSource = new TaskCompletionSource<ImapCalDavSetupResult>();
|
|
||||||
var setupContext = ImapCalDavSettingsNavigationContext.CreateForCreateMode(accountCreationDialogResult, completionSource);
|
|
||||||
|
|
||||||
await ExecuteUIThread(() => Messenger.Send(new BreadcrumbNavigationRequested(
|
|
||||||
Translator.ImapCalDavSettingsPage_TitleCreate,
|
|
||||||
WinoPage.ImapCalDavSettingsPage,
|
|
||||||
setupContext)));
|
|
||||||
|
|
||||||
var setupResult = await completionSource.Task.ConfigureAwait(false)
|
|
||||||
?? throw new AccountSetupCanceledException();
|
|
||||||
|
|
||||||
customServerInformation = setupResult.ServerInformation ?? throw new AccountSetupCanceledException();
|
|
||||||
customServerInformation.Id = Guid.NewGuid();
|
|
||||||
customServerInformation.AccountId = createdAccount.Id;
|
|
||||||
|
|
||||||
createdAccount.Address = setupResult.EmailAddress;
|
|
||||||
createdAccount.SenderName = setupResult.DisplayName;
|
|
||||||
createdAccount.IsCalendarAccessGranted = setupResult.IsCalendarAccessGranted;
|
|
||||||
createdAccount.ServerInformation = customServerInformation;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var accountCreationCancellationTokenSource = new CancellationTokenSource();
|
|
||||||
creationDialog = MailDialogService.GetAccountCreationDialog(accountCreationDialogResult);
|
|
||||||
|
|
||||||
await ExecuteUIThreadTaskAsync(() => creationDialog.ShowDialogAsync(accountCreationCancellationTokenSource));
|
|
||||||
await Task.Delay(500);
|
|
||||||
|
|
||||||
await ExecuteUIThread(() => creationDialog.State = AccountCreationDialogState.SigningIn);
|
|
||||||
|
|
||||||
// OAuth authentication is handled here.
|
|
||||||
// Use SynchronizationManager to handle OAuth authentication.
|
|
||||||
|
|
||||||
var authTokenInfo = await SynchronizationManager.Instance.HandleAuthorizationAsync(
|
|
||||||
accountCreationDialogResult.ProviderType,
|
|
||||||
createdAccount,
|
|
||||||
createdAccount.ProviderType == MailProviderType.Gmail);
|
|
||||||
|
|
||||||
bool creationCanceled = false;
|
|
||||||
await ExecuteUIThread(() => creationCanceled = creationDialog.State == AccountCreationDialogState.Canceled);
|
|
||||||
|
|
||||||
if (creationCanceled)
|
|
||||||
throw new AccountSetupCanceledException();
|
|
||||||
|
|
||||||
// Update account address with authenticated user information
|
|
||||||
createdAccount.Address = authTokenInfo.AccountAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 profileSynchronizationResult = await SynchronizationManager.Instance.SynchronizeProfileAsync(createdAccount.Id);
|
|
||||||
|
|
||||||
if (profileSynchronizationResult.CompletedState != SynchronizationCompletedState.Success)
|
|
||||||
throw new Exception(Translator.Exception_FailedToSynchronizeProfileInformation);
|
|
||||||
|
|
||||||
if (profileSynchronizationResult.ProfileInformation != null)
|
|
||||||
{
|
|
||||||
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 != null)
|
|
||||||
await ExecuteUIThread(() => creationDialog.State = AccountCreationDialogState.PreparingFolders);
|
|
||||||
|
|
||||||
var folderSynchronizationResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(createdAccount.Id);
|
|
||||||
|
|
||||||
if (folderSynchronizationResult == null || folderSynchronizationResult.CompletedState != SynchronizationCompletedState.Success)
|
|
||||||
throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
|
|
||||||
|
|
||||||
if (createdAccount.IsCalendarAccessGranted)
|
|
||||||
{
|
|
||||||
if (creationDialog != null)
|
|
||||||
await ExecuteUIThread(() => creationDialog.State = AccountCreationDialogState.CalendarMetadataFetch);
|
|
||||||
|
|
||||||
var calendarMetadataSynchronizationResult = await SynchronizationManager.Instance.SynchronizeCalendarAsync(new CalendarSynchronizationOptions
|
|
||||||
{
|
|
||||||
AccountId = createdAccount.Id,
|
|
||||||
Type = CalendarSynchronizationType.CalendarMetadata
|
|
||||||
});
|
|
||||||
|
|
||||||
if (calendarMetadataSynchronizationResult == null || calendarMetadataSynchronizationResult.CompletedState != SynchronizationCompletedState.Success)
|
|
||||||
throw new Exception(Translator.Exception_FailedToSynchronizeCalendarMetadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync aliases if supported.
|
|
||||||
if (createdAccount.IsAliasSyncSupported)
|
|
||||||
{
|
|
||||||
// Try to synchronize aliases for the account.
|
|
||||||
var aliasSynchronizationResult = await SynchronizationManager.Instance.SynchronizeAliasesAsync(createdAccount.Id);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (creationDialog != null)
|
|
||||||
{
|
|
||||||
await ExecuteUIThread(() => creationDialog.Complete(false));
|
|
||||||
creationDialogClosed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send changes to listeners.
|
|
||||||
await ExecuteUIThread(() => ReportUIChange(new AccountCreatedMessage(createdAccount)));
|
|
||||||
|
|
||||||
// Notify success.
|
|
||||||
await ExecuteUIThread(() => DialogService.InfoBarMessage(Translator.Info_AccountCreatedTitle, string.Format(Translator.Info_AccountCreatedMessage, createdAccount.Address), InfoBarMessageType.Success));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex) when (ex.Message.Contains(nameof(GmailServiceDisabledException)))
|
|
||||||
{
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
await ExecuteUIThread(() => DialogService.InfoBarMessage(Translator.GmailServiceDisabled_Title, Translator.GmailServiceDisabled_Message, InfoBarMessageType.Error));
|
|
||||||
|
|
||||||
if (createdAccount != null)
|
|
||||||
{
|
|
||||||
await AccountService.DeleteAccountAsync(createdAccount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
|
|
||||||
await ExecuteUIThread(() => DialogService.InfoBarMessage(Translator.Info_AccountCreationFailedTitle, testClientPoolException.Message, InfoBarMessageType.Error));
|
|
||||||
}
|
|
||||||
catch (ImapClientPoolException clientPoolException) when (clientPoolException.InnerException != null)
|
|
||||||
{
|
|
||||||
await ExecuteUIThread(() => DialogService.InfoBarMessage(Translator.Info_AccountCreationFailedTitle, clientPoolException.InnerException.Message, InfoBarMessageType.Error));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Error(ex, "Failed to create account.");
|
|
||||||
|
|
||||||
await ExecuteUIThread(() => DialogService.InfoBarMessage(Translator.Info_AccountCreationFailedTitle, ex.Message, InfoBarMessageType.Error));
|
|
||||||
|
|
||||||
// Delete account in case of failure.
|
|
||||||
if (createdAccount != null)
|
|
||||||
{
|
|
||||||
await AccountService.DeleteAccountAsync(createdAccount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (creationDialog != null && !creationDialogClosed)
|
|
||||||
{
|
|
||||||
bool isCanceled = false;
|
|
||||||
await ExecuteUIThread(() => isCanceled = creationDialog.State == AccountCreationDialogState.Canceled);
|
|
||||||
await ExecuteUIThread(() => creationDialog.Complete(isCanceled));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StartAddNewAccountAsync() => AddNewAccountAsync();
|
public Task StartAddNewAccountAsync() => AddNewAccountAsync();
|
||||||
|
|||||||
@@ -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<AccountSetupStepModel> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,8 @@ namespace Wino.Mail.ViewModels.Data;
|
|||||||
public enum ImapCalDavSettingsPageMode
|
public enum ImapCalDavSettingsPageMode
|
||||||
{
|
{
|
||||||
Create,
|
Create,
|
||||||
Edit
|
Edit,
|
||||||
|
Wizard
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ImapCalDavSettingsNavigationContext
|
public sealed class ImapCalDavSettingsNavigationContext
|
||||||
@@ -35,6 +36,16 @@ public sealed class ImapCalDavSettingsNavigationContext
|
|||||||
Mode = ImapCalDavSettingsPageMode.Edit,
|
Mode = ImapCalDavSettingsPageMode.Edit,
|
||||||
AccountId = accountId
|
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 sealed class ImapCalDavSetupResult
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
|
|||||||
private readonly ICalDavClient _calDavClient;
|
private readonly ICalDavClient _calDavClient;
|
||||||
private readonly IAccountService _accountService;
|
private readonly IAccountService _accountService;
|
||||||
private readonly IMailDialogService _mailDialogService;
|
private readonly IMailDialogService _mailDialogService;
|
||||||
|
private readonly WelcomeWizardContext _wizardContext;
|
||||||
|
|
||||||
private ImapCalDavSettingsPageMode _pageMode;
|
private ImapCalDavSettingsPageMode _pageMode;
|
||||||
private Guid _editingAccountId;
|
private Guid _editingAccountId;
|
||||||
@@ -256,12 +257,14 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
|
|||||||
public ImapCalDavSettingsPageViewModel(IAutoDiscoveryService autoDiscoveryService,
|
public ImapCalDavSettingsPageViewModel(IAutoDiscoveryService autoDiscoveryService,
|
||||||
ICalDavClient calDavClient,
|
ICalDavClient calDavClient,
|
||||||
IAccountService accountService,
|
IAccountService accountService,
|
||||||
IMailDialogService mailDialogService)
|
IMailDialogService mailDialogService,
|
||||||
|
WelcomeWizardContext wizardContext)
|
||||||
{
|
{
|
||||||
_autoDiscoveryService = autoDiscoveryService;
|
_autoDiscoveryService = autoDiscoveryService;
|
||||||
_calDavClient = calDavClient;
|
_calDavClient = calDavClient;
|
||||||
_accountService = accountService;
|
_accountService = accountService;
|
||||||
_mailDialogService = mailDialogService;
|
_mailDialogService = mailDialogService;
|
||||||
|
_wizardContext = wizardContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||||
@@ -278,7 +281,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
|
|||||||
_localOnlyInfoShown = false;
|
_localOnlyInfoShown = false;
|
||||||
SelectedSetupTabIndex = 0;
|
SelectedSetupTabIndex = 0;
|
||||||
|
|
||||||
if (_pageMode == ImapCalDavSettingsPageMode.Create)
|
if (_pageMode == ImapCalDavSettingsPageMode.Create || _pageMode == ImapCalDavSettingsPageMode.Wizard)
|
||||||
{
|
{
|
||||||
PageTitle = Translator.ImapCalDavSettingsPage_TitleCreate;
|
PageTitle = Translator.ImapCalDavSettingsPage_TitleCreate;
|
||||||
ApplyCreateContextDefaults(context.AccountCreationDialogResult);
|
ApplyCreateContextDefaults(context.AccountCreationDialogResult);
|
||||||
@@ -301,6 +304,8 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
|
|||||||
base.OnNavigatedFrom(mode, parameters);
|
base.OnNavigatedFrom(mode, parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsWizardMode => _pageMode == ImapCalDavSettingsPageMode.Wizard;
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task AutoDiscoverSettingsAsync()
|
private async Task AutoDiscoverSettingsAsync()
|
||||||
{
|
{
|
||||||
@@ -407,6 +412,12 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
|
|||||||
IsCalDavValidationSucceeded = false;
|
IsCalDavValidationSucceeded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_pageMode == ImapCalDavSettingsPageMode.Wizard)
|
||||||
|
{
|
||||||
|
CompleteWizardFlow(serverInformation);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (_pageMode == ImapCalDavSettingsPageMode.Create)
|
if (_pageMode == ImapCalDavSettingsPageMode.Create)
|
||||||
{
|
{
|
||||||
CompleteCreateFlow(serverInformation);
|
CompleteCreateFlow(serverInformation);
|
||||||
@@ -436,6 +447,22 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
|
|||||||
Messenger.Send(new BackBreadcrumNavigationRequested());
|
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]
|
[RelayCommand]
|
||||||
private Task ShowLocalCalendarExplanationAsync()
|
private Task ShowLocalCalendarExplanationAsync()
|
||||||
=> _mailDialogService.ShowMessageAsync(
|
=> _mailDialogService.ShowMessageAsync(
|
||||||
|
|||||||
@@ -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<IProviderDetail> Providers { get; private set; } = [];
|
||||||
|
public List<AppColorViewModel> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<SpecialImapProvider, string> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ using System.Collections.Generic;
|
|||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
using Wino.Core.Domain;
|
||||||
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
using Wino.Core.Domain.Models.Navigation;
|
using Wino.Core.Domain.Models.Navigation;
|
||||||
using Wino.Core.Domain.Models.Updates;
|
using Wino.Core.Domain.Models.Updates;
|
||||||
@@ -40,6 +42,8 @@ public partial class WelcomePageV2ViewModel : MailBaseViewModel
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void GetStarted()
|
private void GetStarted()
|
||||||
{
|
{
|
||||||
Messenger.Send(new GetStartedFromWelcomeRequested());
|
Messenger.Send(new BreadcrumbNavigationRequested(
|
||||||
|
Translator.WelcomeWizard_Step2Title,
|
||||||
|
WinoPage.ProviderSelectionPage));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-51
@@ -23,9 +23,9 @@ using Wino.Core.Domain.Models.Calendar;
|
|||||||
using Wino.Core.Domain.Models.MailItem;
|
using Wino.Core.Domain.Models.MailItem;
|
||||||
using Wino.Core.Domain.Models.Navigation;
|
using Wino.Core.Domain.Models.Navigation;
|
||||||
using Wino.Core.Domain.Models.Synchronization;
|
using Wino.Core.Domain.Models.Synchronization;
|
||||||
using Wino.Core.Domain.Models.Updates;
|
|
||||||
using Wino.Mail.Services;
|
using Wino.Mail.Services;
|
||||||
using Wino.Mail.ViewModels;
|
using Wino.Mail.ViewModels;
|
||||||
|
using Wino.Mail.ViewModels.Data;
|
||||||
using Wino.Mail.WinUI.Activation;
|
using Wino.Mail.WinUI.Activation;
|
||||||
using Wino.Mail.WinUI.Interfaces;
|
using Wino.Mail.WinUI.Interfaces;
|
||||||
using Wino.Mail.WinUI.Models;
|
using Wino.Mail.WinUI.Models;
|
||||||
@@ -42,6 +42,7 @@ public partial class App : WinoApplication,
|
|||||||
IRecipient<NewMailSynchronizationRequested>,
|
IRecipient<NewMailSynchronizationRequested>,
|
||||||
IRecipient<NewCalendarSynchronizationRequested>,
|
IRecipient<NewCalendarSynchronizationRequested>,
|
||||||
IRecipient<AccountCreatedMessage>,
|
IRecipient<AccountCreatedMessage>,
|
||||||
|
IRecipient<AccountRemovedMessage>,
|
||||||
IRecipient<GetStartedFromWelcomeRequested>
|
IRecipient<GetStartedFromWelcomeRequested>
|
||||||
{
|
{
|
||||||
private const int InboxSyncsPerFullSync = 20;
|
private const int InboxSyncsPerFullSync = 20;
|
||||||
@@ -146,6 +147,10 @@ public partial class App : WinoApplication,
|
|||||||
services.AddTransient(typeof(AccountManagementViewModel));
|
services.AddTransient(typeof(AccountManagementViewModel));
|
||||||
services.AddTransient(typeof(WelcomePageViewModel));
|
services.AddTransient(typeof(WelcomePageViewModel));
|
||||||
services.AddTransient(typeof(WelcomePageV2ViewModel));
|
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(ComposePageViewModel));
|
||||||
services.AddTransient(typeof(IdlePageViewModel));
|
services.AddTransient(typeof(IdlePageViewModel));
|
||||||
@@ -218,18 +223,10 @@ public partial class App : WinoApplication,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check whether the new version requires a migration before starting sync.
|
|
||||||
var updateManager = Services.GetRequiredService<IUpdateManager>();
|
|
||||||
var updateNotes = await updateManager.GetLatestUpdateNotesAsync();
|
|
||||||
bool hasPendingMigration = updateNotes.HasPendingMigrations && updateManager.HasPendingMigrations();
|
|
||||||
|
|
||||||
_preferencesService.PreferenceChanged -= PreferencesServiceChanged;
|
_preferencesService.PreferenceChanged -= PreferencesServiceChanged;
|
||||||
_preferencesService.PreferenceChanged += PreferencesServiceChanged;
|
_preferencesService.PreferenceChanged += PreferencesServiceChanged;
|
||||||
|
|
||||||
// Hold off sync loop when a migration is required in startup-task (tray-only) mode.
|
RestartAutoSynchronizationLoop();
|
||||||
// In foreground mode the sync loop starts normally; the ViewModel dialog handles migrations before sync kicks in.
|
|
||||||
if (!hasPendingMigration || !IsStartupTaskLaunch())
|
|
||||||
RestartAutoSynchronizationLoop();
|
|
||||||
|
|
||||||
// Check if launched from toast notification.
|
// Check if launched from toast notification.
|
||||||
if (IsNotificationActivation(out AppNotificationActivatedEventArgs toastArgs))
|
if (IsNotificationActivation(out AppNotificationActivatedEventArgs toastArgs))
|
||||||
@@ -253,16 +250,7 @@ public partial class App : WinoApplication,
|
|||||||
// Otherwise, activate the window normally.
|
// Otherwise, activate the window normally.
|
||||||
if (isStartupTaskLaunch)
|
if (isStartupTaskLaunch)
|
||||||
{
|
{
|
||||||
if (hasPendingMigration)
|
LogActivation("Launched by startup task. Window created but hidden (system tray only).");
|
||||||
{
|
|
||||||
// Notify the user to open the app to complete the update before sync can resume.
|
|
||||||
Services.GetRequiredService<INotificationBuilder>().CreateMigrationRequiredNotification();
|
|
||||||
LogActivation("Migration required for new version. Sync skipped. User notified via toast.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
LogActivation("Launched by startup task. Window created but hidden (system tray only).");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -305,13 +293,6 @@ public partial class App : WinoApplication,
|
|||||||
{
|
{
|
||||||
var toastArguments = ToastArguments.Parse(toastArgs.Argument);
|
var toastArguments = ToastArguments.Parse(toastArgs.Argument);
|
||||||
|
|
||||||
// Check migration notification activation first.
|
|
||||||
if (toastArguments.Contains(Constants.ToastMigrationRequiredKey))
|
|
||||||
{
|
|
||||||
await HandleMigrationToastActivationAsync();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check calendar reminder toast activation first.
|
// Check calendar reminder toast activation first.
|
||||||
if (toastArguments.TryGetValue(Constants.ToastCalendarActionKey, out string calendarAction) &&
|
if (toastArguments.TryGetValue(Constants.ToastCalendarActionKey, out string calendarAction) &&
|
||||||
calendarAction == Constants.ToastCalendarNavigateAction &&
|
calendarAction == Constants.ToastCalendarNavigateAction &&
|
||||||
@@ -341,29 +322,6 @@ public partial class App : WinoApplication,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles activation from the migration-required toast notification.
|
|
||||||
/// Opens the app so the shell ViewModel can show the What's New dialog and run migrations.
|
|
||||||
/// </summary>
|
|
||||||
private async Task HandleMigrationToastActivationAsync()
|
|
||||||
{
|
|
||||||
LogActivation("Handling migration toast activation.");
|
|
||||||
|
|
||||||
if (!IsAppRunning())
|
|
||||||
{
|
|
||||||
await CreateAndActivateWindow(null!);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
EnsureMainWindowVisibleAndForeground();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The MailAppShellViewModel.OnNavigatedTo will detect ShouldShowUpdateNotes() == true
|
|
||||||
// and display the What's New dialog (including running migrations) once the XamlRoot is ready.
|
|
||||||
// Restart sync in case it was blocked.
|
|
||||||
RestartAutoSynchronizationLoop();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleCalendarToastNavigationAsync(Guid calendarItemId)
|
private async Task HandleCalendarToastNavigationAsync(Guid calendarItemId)
|
||||||
{
|
{
|
||||||
var calendarService = Services.GetRequiredService<ICalendarService>();
|
var calendarService = Services.GetRequiredService<ICalendarService>();
|
||||||
@@ -593,7 +551,7 @@ public partial class App : WinoApplication,
|
|||||||
InitializeNavigationDispatcher();
|
InitializeNavigationDispatcher();
|
||||||
|
|
||||||
Services.GetRequiredService<INavigationService>()
|
Services.GetRequiredService<INavigationService>()
|
||||||
.Navigate(WinoPage.WelcomePageV2, null, NavigationReferenceFrame.ShellFrame, NavigationTransitionType.None);
|
.Navigate(WinoPage.WelcomeHostPage, null, NavigationReferenceFrame.ShellFrame, NavigationTransitionType.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeNavigationDispatcher()
|
private void InitializeNavigationDispatcher()
|
||||||
@@ -627,6 +585,7 @@ public partial class App : WinoApplication,
|
|||||||
WeakReferenceMessenger.Default.Register<NewMailSynchronizationRequested>(this);
|
WeakReferenceMessenger.Default.Register<NewMailSynchronizationRequested>(this);
|
||||||
WeakReferenceMessenger.Default.Register<NewCalendarSynchronizationRequested>(this);
|
WeakReferenceMessenger.Default.Register<NewCalendarSynchronizationRequested>(this);
|
||||||
WeakReferenceMessenger.Default.Register<AccountCreatedMessage>(this);
|
WeakReferenceMessenger.Default.Register<AccountCreatedMessage>(this);
|
||||||
|
WeakReferenceMessenger.Default.Register<AccountRemovedMessage>(this);
|
||||||
WeakReferenceMessenger.Default.Register<GetStartedFromWelcomeRequested>(this);
|
WeakReferenceMessenger.Default.Register<GetStartedFromWelcomeRequested>(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,6 +658,29 @@ public partial class App : WinoApplication,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Receive(AccountRemovedMessage message)
|
||||||
|
{
|
||||||
|
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
if (accounts.Any()) return;
|
||||||
|
|
||||||
|
// All accounts removed — go back to welcome wizard from step 1
|
||||||
|
Services.GetRequiredService<WelcomeWizardContext>().Reset();
|
||||||
|
StopAutoSynchronizationLoop();
|
||||||
|
CreateWelcomeWindow();
|
||||||
|
windowManager.HideWindow(WinoWindowKind.Shell);
|
||||||
|
await NewThemeService.ApplyThemeToActiveWindowAsync();
|
||||||
|
MainWindow?.Activate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public void Receive(GetStartedFromWelcomeRequested message)
|
public void Receive(GetStartedFromWelcomeRequested message)
|
||||||
{
|
{
|
||||||
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
xmlns:xaml="using:Microsoft.UI.Xaml">
|
xmlns:xaml="using:Microsoft.UI.Xaml">
|
||||||
|
|
||||||
<x:String x:Key="ThemeName">Clouds</x:String>
|
<x:String x:Key="ThemeName">Clouds</x:String>
|
||||||
<x:String x:Key="ThemeBackgroundImage">ms-appx:///Wino.Mail.WinUI/BackgroundImages/Clouds.jpg</x:String>
|
<x:String x:Key="ThemeBackgroundImage">ms-appx:///BackgroundImages/Clouds.jpg</x:String>
|
||||||
|
|
||||||
<ImageBrush x:Key="WinoApplicationBackgroundColor" ImageSource="{StaticResource ThemeBackgroundImage}" />
|
<ImageBrush x:Key="WinoApplicationBackgroundColor" ImageSource="{StaticResource ThemeBackgroundImage}" />
|
||||||
<SolidColorBrush x:Key="AppBarBackgroundColor">Transparent</SolidColorBrush>
|
<SolidColorBrush x:Key="AppBarBackgroundColor">Transparent</SolidColorBrush>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
xmlns:xaml="using:Microsoft.UI.Xaml">
|
xmlns:xaml="using:Microsoft.UI.Xaml">
|
||||||
|
|
||||||
<x:String x:Key="ThemeName">Forest</x:String>
|
<x:String x:Key="ThemeName">Forest</x:String>
|
||||||
<x:String x:Key="ThemeBackgroundImage">ms-appx:///Wino.Mail.WinUI/BackgroundImages/Forest.jpg</x:String>
|
<x:String x:Key="ThemeBackgroundImage">ms-appx:///BackgroundImages/Forest.jpg</x:String>
|
||||||
|
|
||||||
<ImageBrush x:Key="WinoApplicationBackgroundColor" ImageSource="{StaticResource ThemeBackgroundImage}" />
|
<ImageBrush x:Key="WinoApplicationBackgroundColor" ImageSource="{StaticResource ThemeBackgroundImage}" />
|
||||||
<SolidColorBrush x:Key="AppBarBackgroundColor">Transparent</SolidColorBrush>
|
<SolidColorBrush x:Key="AppBarBackgroundColor">Transparent</SolidColorBrush>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
xmlns:xaml="using:Microsoft.UI.Xaml">
|
xmlns:xaml="using:Microsoft.UI.Xaml">
|
||||||
|
|
||||||
<x:String x:Key="ThemeName">Garden</x:String>
|
<x:String x:Key="ThemeName">Garden</x:String>
|
||||||
<x:String x:Key="ThemeBackgroundImage">ms-appx:///Wino.Mail.WinUI/BackgroundImages/Garden.jpg</x:String>
|
<x:String x:Key="ThemeBackgroundImage">ms-appx:///BackgroundImages/Garden.jpg</x:String>
|
||||||
|
|
||||||
<ImageBrush x:Key="WinoApplicationBackgroundColor" ImageSource="{StaticResource ThemeBackgroundImage}" />
|
<ImageBrush x:Key="WinoApplicationBackgroundColor" ImageSource="{StaticResource ThemeBackgroundImage}" />
|
||||||
<SolidColorBrush x:Key="AppBarBackgroundColor">Transparent</SolidColorBrush>
|
<SolidColorBrush x:Key="AppBarBackgroundColor">Transparent</SolidColorBrush>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
xmlns:xaml="using:Microsoft.UI.Xaml">
|
xmlns:xaml="using:Microsoft.UI.Xaml">
|
||||||
|
|
||||||
<x:String x:Key="ThemeName">Nighty</x:String>
|
<x:String x:Key="ThemeName">Nighty</x:String>
|
||||||
<x:String x:Key="ThemeBackgroundImage">ms-appx:///Wino.Mail.WinUI/BackgroundImages/Nighty.jpg</x:String>
|
<x:String x:Key="ThemeBackgroundImage">ms-appx:///BackgroundImages/Nighty.jpg</x:String>
|
||||||
|
|
||||||
<ImageBrush x:Key="WinoApplicationBackgroundColor" ImageSource="{StaticResource ThemeBackgroundImage}" />
|
<ImageBrush x:Key="WinoApplicationBackgroundColor" ImageSource="{StaticResource ThemeBackgroundImage}" />
|
||||||
<SolidColorBrush x:Key="AppBarBackgroundColor">Transparent</SolidColorBrush>
|
<SolidColorBrush x:Key="AppBarBackgroundColor">Transparent</SolidColorBrush>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
xmlns:xaml="using:Microsoft.UI.Xaml">
|
xmlns:xaml="using:Microsoft.UI.Xaml">
|
||||||
|
|
||||||
<x:String x:Key="ThemeName">Snowflake</x:String>
|
<x:String x:Key="ThemeName">Snowflake</x:String>
|
||||||
<x:String x:Key="ThemeBackgroundImage">ms-appx:///Wino.Mail.WinUI/BackgroundImages/Snowflake.jpg</x:String>
|
<x:String x:Key="ThemeBackgroundImage">ms-appx:///BackgroundImages/Snowflake.jpg</x:String>
|
||||||
|
|
||||||
<ImageBrush x:Key="WinoApplicationBackgroundColor" ImageSource="{StaticResource ThemeBackgroundImage}" />
|
<ImageBrush x:Key="WinoApplicationBackgroundColor" ImageSource="{StaticResource ThemeBackgroundImage}" />
|
||||||
<SolidColorBrush x:Key="AppBarBackgroundColor">Transparent</SolidColorBrush>
|
<SolidColorBrush x:Key="AppBarBackgroundColor">Transparent</SolidColorBrush>
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
{
|
{
|
||||||
"hasPendingMigrations": true,
|
|
||||||
"migration": {
|
|
||||||
"titleKey": "WhatIsNew_MigrationPreparing_Title",
|
|
||||||
"descriptionKey": "WhatIsNew_MigrationPreparing_Description"
|
|
||||||
},
|
|
||||||
"sections": [
|
"sections": [
|
||||||
{
|
{
|
||||||
"title": "# Wino Calendar is here!",
|
"title": "# Wino Calendar is here!",
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ public static class CoreUWPContainerSetup
|
|||||||
services.AddTransient(typeof(AboutPageViewModel));
|
services.AddTransient(typeof(AboutPageViewModel));
|
||||||
services.AddTransient(typeof(SettingsPageViewModel));
|
services.AddTransient(typeof(SettingsPageViewModel));
|
||||||
services.AddTransient(typeof(ManageAccountsPagePageViewModel));
|
services.AddTransient(typeof(ManageAccountsPagePageViewModel));
|
||||||
|
services.AddTransient(typeof(WelcomeHostPageViewModel));
|
||||||
services.AddTransient(typeof(KeyboardShortcutsPageViewModel));
|
services.AddTransient(typeof(KeyboardShortcutsPageViewModel));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,40 +22,16 @@
|
|||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<controls:UpdateNotesFlipViewControl x:Name="UpdateNotesControl" Sections="{x:Bind Sections, Mode=OneTime}" />
|
<controls:UpdateNotesFlipViewControl x:Name="UpdateNotesControl" Sections="{x:Bind Sections, Mode=OneTime}" />
|
||||||
|
|
||||||
<StackPanel
|
<StackPanel
|
||||||
x:Name="MigrationPanel"
|
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Spacing="8"
|
|
||||||
Visibility="Collapsed">
|
|
||||||
<TextBlock x:Name="MigrationTitleText" Style="{StaticResource BodyStrongTextBlockStyle}" />
|
|
||||||
<TextBlock x:Name="MigrationDescriptionText" TextWrapping="WrapWholeWords" />
|
|
||||||
<ProgressBar
|
|
||||||
x:Name="MigrationProgressBar"
|
|
||||||
IsIndeterminate="True"
|
|
||||||
Visibility="Collapsed" />
|
|
||||||
<TextBlock
|
|
||||||
x:Name="MigrationErrorText"
|
|
||||||
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
|
|
||||||
TextWrapping="WrapWholeWords"
|
|
||||||
Visibility="Collapsed" />
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<StackPanel
|
|
||||||
Grid.Row="2"
|
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Orientation="Horizontal"
|
Orientation="Horizontal"
|
||||||
Spacing="8">
|
Spacing="8">
|
||||||
<Button
|
|
||||||
x:Name="ContinueAnywayButton"
|
|
||||||
Click="OnContinueAnywayClicked"
|
|
||||||
Content="{x:Bind domain:Translator.WhatIsNew_ContinueAnywayButton}"
|
|
||||||
Visibility="Collapsed" />
|
|
||||||
<Button
|
<Button
|
||||||
x:Name="GetStartedButton"
|
x:Name="GetStartedButton"
|
||||||
Click="OnGetStartedClicked"
|
Click="OnGetStartedClicked"
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Wino.Core.Domain;
|
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
using Wino.Core.Domain.Models.Updates;
|
using Wino.Core.Domain.Models.Updates;
|
||||||
|
|
||||||
@@ -11,7 +9,6 @@ namespace Wino.Dialogs;
|
|||||||
public sealed partial class WhatIsNewDialog : ContentDialog
|
public sealed partial class WhatIsNewDialog : ContentDialog
|
||||||
{
|
{
|
||||||
private readonly IUpdateManager _updateManager;
|
private readonly IUpdateManager _updateManager;
|
||||||
private readonly UpdateNotes _notes;
|
|
||||||
|
|
||||||
public List<UpdateNoteSection> Sections { get; }
|
public List<UpdateNoteSection> Sections { get; }
|
||||||
|
|
||||||
@@ -21,15 +18,12 @@ public sealed partial class WhatIsNewDialog : ContentDialog
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
_notes = notes;
|
|
||||||
_updateManager = updateManager;
|
_updateManager = updateManager;
|
||||||
Sections = notes.Sections;
|
Sections = notes.Sections;
|
||||||
|
|
||||||
// Show the Get Started button immediately when there is only one page.
|
// Show the Get Started button immediately when there is only one page.
|
||||||
UpdateNotesControl.SelectedIndexChanged += OnUpdateSectionChanged;
|
UpdateNotesControl.SelectedIndexChanged += OnUpdateSectionChanged;
|
||||||
UpdateGetStartedButtonVisibility(UpdateNotesControl.SelectedIndex);
|
UpdateGetStartedButtonVisibility(UpdateNotesControl.SelectedIndex);
|
||||||
|
|
||||||
InitializeMigrationStatus();
|
|
||||||
Closing += OnDialogClosing;
|
Closing += OnDialogClosing;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,17 +37,6 @@ public sealed partial class WhatIsNewDialog : ContentDialog
|
|||||||
: Visibility.Collapsed;
|
: Visibility.Collapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeMigrationStatus()
|
|
||||||
{
|
|
||||||
if (!_notes.HasPendingMigrations ||
|
|
||||||
string.IsNullOrWhiteSpace(_notes.Migration.TitleKey) ||
|
|
||||||
string.IsNullOrWhiteSpace(_notes.Migration.DescriptionKey))
|
|
||||||
return;
|
|
||||||
|
|
||||||
MigrationTitleText.Text = Translator.GetTranslatedString(_notes.Migration.TitleKey);
|
|
||||||
MigrationDescriptionText.Text = Translator.GetTranslatedString(_notes.Migration.DescriptionKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDialogClosing(ContentDialog sender, ContentDialogClosingEventArgs args)
|
private void OnDialogClosing(ContentDialog sender, ContentDialogClosingEventArgs args)
|
||||||
{
|
{
|
||||||
// Only allow closing when Get Started button was clicked.
|
// Only allow closing when Get Started button was clicked.
|
||||||
@@ -61,41 +44,9 @@ public sealed partial class WhatIsNewDialog : ContentDialog
|
|||||||
args.Cancel = true;
|
args.Cancel = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnGetStartedClicked(object sender, RoutedEventArgs e)
|
private void OnGetStartedClicked(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
GetStartedButton.IsEnabled = false;
|
GetStartedButton.IsEnabled = false;
|
||||||
ContinueAnywayButton.Visibility = Visibility.Collapsed;
|
|
||||||
MigrationErrorText.Visibility = Visibility.Collapsed;
|
|
||||||
|
|
||||||
if (_notes.HasPendingMigrations)
|
|
||||||
{
|
|
||||||
GetStartedButton.Content = Translator.WhatIsNew_PreparingForNewVersionButton;
|
|
||||||
MigrationPanel.Visibility = Visibility.Visible;
|
|
||||||
MigrationProgressBar.Visibility = Visibility.Visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _updateManager.RunPendingMigrationsAsync();
|
|
||||||
_updateManager.MarkUpdateNotesAsSeen();
|
|
||||||
}
|
|
||||||
catch (System.Exception ex)
|
|
||||||
{
|
|
||||||
MigrationProgressBar.Visibility = Visibility.Collapsed;
|
|
||||||
MigrationErrorText.Text = string.Format(Translator.WhatIsNew_MigrationFailedMessage, ex.GetType().Name);
|
|
||||||
MigrationErrorText.Visibility = Visibility.Visible;
|
|
||||||
ContinueAnywayButton.Visibility = Visibility.Visible;
|
|
||||||
GetStartedButton.IsEnabled = true;
|
|
||||||
GetStartedButton.Content = Translator.WhatIsNew_GetStartedButton;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_canClose = true;
|
|
||||||
Hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnContinueAnywayClicked(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
_updateManager.MarkUpdateNotesAsSeen();
|
_updateManager.MarkUpdateNotesAsSeen();
|
||||||
_canClose = true;
|
_canClose = true;
|
||||||
Hide();
|
Hide();
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ public static class XamlHelpers
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Visibility BoolToVisibilityConverter(bool value) => value ? Visibility.Visible : Visibility.Collapsed;
|
||||||
public static Visibility ReverseBoolToVisibilityConverter(bool value) => value ? Visibility.Collapsed : Visibility.Visible;
|
public static Visibility ReverseBoolToVisibilityConverter(bool value) => value ? Visibility.Collapsed : Visibility.Visible;
|
||||||
public static Visibility ReverseVisibilityConverter(Visibility visibility) => visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible;
|
public static Visibility ReverseVisibilityConverter(Visibility visibility) => visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible;
|
||||||
public static bool ReverseBoolConverter(bool value) => !value;
|
public static bool ReverseBoolConverter(bool value) => !value;
|
||||||
@@ -129,6 +130,16 @@ public static class XamlHelpers
|
|||||||
|
|
||||||
public static SolidColorBrush GetSolidColorBrushFromHex(string colorHex) => string.IsNullOrEmpty(colorHex) ? new SolidColorBrush(Colors.Transparent) : new SolidColorBrush(colorHex.ToColor());
|
public static SolidColorBrush GetSolidColorBrushFromHex(string colorHex) => string.IsNullOrEmpty(colorHex) ? new SolidColorBrush(Colors.Transparent) : new SolidColorBrush(colorHex.ToColor());
|
||||||
public static FontWeight GetFontWeightBySyncState(bool isSyncing) => isSyncing ? FontWeights.SemiBold : FontWeights.Normal;
|
public static FontWeight GetFontWeightBySyncState(bool isSyncing) => isSyncing ? FontWeights.SemiBold : FontWeights.Normal;
|
||||||
|
|
||||||
|
public static Brush GetWizardStepBadgeBrush(bool isActive)
|
||||||
|
=> isActive
|
||||||
|
? (Brush)Application.Current.Resources["AccentFillColorDefaultBrush"]
|
||||||
|
: new SolidColorBrush(Color.FromArgb(30, 128, 128, 128));
|
||||||
|
|
||||||
|
public static Brush GetWizardStepNumberForeground(bool isActive)
|
||||||
|
=> isActive
|
||||||
|
? new SolidColorBrush(Colors.White)
|
||||||
|
: (Brush)Application.Current.Resources["TextFillColorSecondaryBrush"];
|
||||||
public static FontWeight GetFontWeightByChildSelectedState(bool isChildSelected) => isChildSelected ? FontWeights.SemiBold : FontWeights.Normal;
|
public static FontWeight GetFontWeightByChildSelectedState(bool isChildSelected) => isChildSelected ? FontWeights.SemiBold : FontWeights.Normal;
|
||||||
public static FontWeight GetFontWeightByReadState(bool isChildSelected) => isChildSelected ? FontWeights.Normal : FontWeights.SemiBold;
|
public static FontWeight GetFontWeightByReadState(bool isChildSelected) => isChildSelected ? FontWeights.Normal : FontWeights.SemiBold;
|
||||||
public static Visibility StringToVisibilityConverter(string value) => string.IsNullOrWhiteSpace(value) ? Visibility.Collapsed : Visibility.Visible;
|
public static Visibility StringToVisibilityConverter(string value) => string.IsNullOrWhiteSpace(value) ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public class CustomAppTheme : AppThemeBase
|
|||||||
|
|
||||||
public override async Task<string> GetThemeResourceDictionaryContentAsync()
|
public override async Task<string> GetThemeResourceDictionaryContentAsync()
|
||||||
{
|
{
|
||||||
var customAppThemeFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Wino.Mail.WinUI/AppThemes/Custom.xaml"));
|
var customAppThemeFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///AppThemes/Custom.xaml"));
|
||||||
return await FileIO.ReadTextAsync(customAppThemeFile);
|
return await FileIO.ReadTextAsync(customAppThemeFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ public class PreDefinedAppTheme : AppThemeBase
|
|||||||
public override AppThemeType AppThemeType => AppThemeType.PreDefined;
|
public override AppThemeType AppThemeType => AppThemeType.PreDefined;
|
||||||
|
|
||||||
public override string GetBackgroundPreviewImagePath()
|
public override string GetBackgroundPreviewImagePath()
|
||||||
=> $"ms-appx:///Wino.Mail.WinUI/BackgroundImages/{ThemeName}.jpg";
|
=> $"ms-appx:///BackgroundImages/{ThemeName}.jpg";
|
||||||
|
|
||||||
public override async Task<string> GetThemeResourceDictionaryContentAsync()
|
public override async Task<string> GetThemeResourceDictionaryContentAsync()
|
||||||
{
|
{
|
||||||
var xamlDictionaryFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri($"ms-appx://AppThemes/{ThemeName}.xaml"));
|
var xamlDictionaryFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri($"ms-appx:///AppThemes/{ThemeName}.xaml"));
|
||||||
return await FileIO.ReadTextAsync(xamlDictionaryFile);
|
return await FileIO.ReadTextAsync(xamlDictionaryFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,11 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
|||||||
WinoPage.ComposePage,
|
WinoPage.ComposePage,
|
||||||
WinoPage.IdlePage,
|
WinoPage.IdlePage,
|
||||||
WinoPage.WelcomePage,
|
WinoPage.WelcomePage,
|
||||||
WinoPage.WelcomePageV2
|
WinoPage.WelcomePageV2,
|
||||||
|
WinoPage.WelcomeHostPage,
|
||||||
|
WinoPage.ProviderSelectionPage,
|
||||||
|
WinoPage.AccountSetupProgressPage,
|
||||||
|
WinoPage.SpecialImapCredentialsPage
|
||||||
];
|
];
|
||||||
|
|
||||||
private static readonly WinoPage[] CalendarOnlyPages =
|
private static readonly WinoPage[] CalendarOnlyPages =
|
||||||
@@ -116,6 +120,10 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
|||||||
WinoPage.ContactsPage => typeof(ContactsPage),
|
WinoPage.ContactsPage => typeof(ContactsPage),
|
||||||
WinoPage.SignatureAndEncryptionPage => typeof(SignatureAndEncryptionPage),
|
WinoPage.SignatureAndEncryptionPage => typeof(SignatureAndEncryptionPage),
|
||||||
WinoPage.StoragePage => typeof(StoragePage),
|
WinoPage.StoragePage => typeof(StoragePage),
|
||||||
|
WinoPage.WelcomeHostPage => typeof(WelcomeHostPage),
|
||||||
|
WinoPage.ProviderSelectionPage => typeof(ProviderSelectionPage),
|
||||||
|
WinoPage.AccountSetupProgressPage => typeof(AccountSetupProgressPage),
|
||||||
|
WinoPage.SpecialImapCredentialsPage => typeof(SpecialImapCredentialsPage),
|
||||||
WinoPage.CalendarPage => typeof(CalendarPage),
|
WinoPage.CalendarPage => typeof(CalendarPage),
|
||||||
WinoPage.EventDetailsPage => typeof(EventDetailsPage),
|
WinoPage.EventDetailsPage => typeof(EventDetailsPage),
|
||||||
WinoPage.CalendarSettingsPage => typeof(CalendarSettingsPage),
|
WinoPage.CalendarSettingsPage => typeof(CalendarSettingsPage),
|
||||||
|
|||||||
@@ -317,20 +317,6 @@ public class NotificationBuilder : INotificationBuilder
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CreateMigrationRequiredNotification()
|
|
||||||
{
|
|
||||||
var builder = new ToastContentBuilder();
|
|
||||||
builder.SetToastScenario(ToastScenario.Default);
|
|
||||||
|
|
||||||
builder.AddText(Translator.WhatIsNew_MigrationNotification_Title);
|
|
||||||
builder.AddText(Translator.WhatIsNew_MigrationNotification_Message);
|
|
||||||
|
|
||||||
builder.AddArgument(Constants.ToastMigrationRequiredKey, bool.TrueString);
|
|
||||||
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
|
||||||
|
|
||||||
ShowToast(builder);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ShowToast(ToastContentBuilder builder, string? tag = null)
|
private static void ShowToast(ToastContentBuilder builder, string? tag = null)
|
||||||
{
|
{
|
||||||
var toastNotification = new ToastNotification(builder.GetToastContent().GetXml());
|
var toastNotification = new ToastNotification(builder.GetToastContent().GetXml());
|
||||||
|
|||||||
@@ -107,6 +107,9 @@
|
|||||||
</controls:Segmented>
|
</controls:Segmented>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</TitleBar.RightHeader>
|
</TitleBar.RightHeader>
|
||||||
|
<TitleBar.IconSource>
|
||||||
|
<ImageIconSource ImageSource="/Assets/Wino_Icon.ico" />
|
||||||
|
</TitleBar.IconSource>
|
||||||
</TitleBar>
|
</TitleBar>
|
||||||
|
|
||||||
<Frame
|
<Frame
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
|
|||||||
|
using Wino.Mail.ViewModels;
|
||||||
|
using Wino.Mail.WinUI;
|
||||||
|
|
||||||
|
namespace Wino.Mail.WinUI.Views.Abstract;
|
||||||
|
|
||||||
|
public abstract class AccountSetupProgressPageAbstract : BasePage<AccountSetupProgressPageViewModel>
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using Wino.Mail.ViewModels;
|
||||||
|
using Wino.Mail.WinUI;
|
||||||
|
|
||||||
|
namespace Wino.Mail.WinUI.Views.Abstract;
|
||||||
|
|
||||||
|
public abstract class ProviderSelectionPageAbstract : BasePage<ProviderSelectionPageViewModel>
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using Wino.Mail.ViewModels;
|
||||||
|
using Wino.Mail.WinUI;
|
||||||
|
|
||||||
|
namespace Wino.Mail.WinUI.Views.Abstract;
|
||||||
|
|
||||||
|
public abstract class SpecialImapCredentialsPageAbstract : BasePage<SpecialImapCredentialsPageViewModel>
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using Wino.Core.ViewModels;
|
||||||
|
using Wino.Mail.WinUI;
|
||||||
|
|
||||||
|
namespace Wino.Mail.WinUI.Views.Abstract;
|
||||||
|
|
||||||
|
public abstract class WelcomeHostPageAbstract : BasePage<WelcomeHostPageViewModel>
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -78,13 +78,9 @@
|
|||||||
Text="{x:Bind ViewModel.EmailAddress, Mode=TwoWay}" />
|
Text="{x:Bind ViewModel.EmailAddress, Mode=TwoWay}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<PasswordBox
|
<PasswordBox Header="{x:Bind ViewModel.PasswordHeaderText, Mode=OneWay}" Password="{x:Bind ViewModel.Password, Mode=TwoWay}" />
|
||||||
Header="{x:Bind ViewModel.PasswordHeaderText, Mode=OneWay}"
|
|
||||||
Password="{x:Bind ViewModel.Password, Mode=TwoWay}" />
|
|
||||||
|
|
||||||
<CheckBox
|
<CheckBox Content="{x:Bind ViewModel.EnableCalendarSupportText, Mode=OneWay}" IsChecked="{x:Bind ViewModel.IsCalendarSupportEnabled, Mode=TwoWay}" />
|
||||||
Content="{x:Bind ViewModel.EnableCalendarSupportText, Mode=OneWay}"
|
|
||||||
IsChecked="{x:Bind ViewModel.IsCalendarSupportEnabled, Mode=TwoWay}" />
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
@@ -131,18 +127,10 @@
|
|||||||
<FontIcon FontSize="14" Glyph="" />
|
<FontIcon FontSize="14" Glyph="" />
|
||||||
<TextBlock FontWeight="SemiBold" Text="{x:Bind ViewModel.IncomingSectionTitleText, Mode=OneWay}" />
|
<TextBlock FontWeight="SemiBold" Text="{x:Bind ViewModel.IncomingSectionTitleText, Mode=OneWay}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<TextBox
|
<TextBox Header="{x:Bind ViewModel.IncomingServerHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.IncomingServer, Mode=TwoWay}" />
|
||||||
Header="{x:Bind ViewModel.IncomingServerHeaderText, Mode=OneWay}"
|
<TextBox Header="{x:Bind ViewModel.PortHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.IncomingServerPort, Mode=TwoWay}" />
|
||||||
Text="{x:Bind ViewModel.IncomingServer, Mode=TwoWay}" />
|
<TextBox Header="{x:Bind ViewModel.IncomingUsernameHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.IncomingServerUsername, Mode=TwoWay}" />
|
||||||
<TextBox
|
<PasswordBox Header="{x:Bind ViewModel.IncomingPasswordHeaderText, Mode=OneWay}" Password="{x:Bind ViewModel.IncomingServerPassword, Mode=TwoWay}" />
|
||||||
Header="{x:Bind ViewModel.PortHeaderText, Mode=OneWay}"
|
|
||||||
Text="{x:Bind ViewModel.IncomingServerPort, Mode=TwoWay}" />
|
|
||||||
<TextBox
|
|
||||||
Header="{x:Bind ViewModel.IncomingUsernameHeaderText, Mode=OneWay}"
|
|
||||||
Text="{x:Bind ViewModel.IncomingServerUsername, Mode=TwoWay}" />
|
|
||||||
<PasswordBox
|
|
||||||
Header="{x:Bind ViewModel.IncomingPasswordHeaderText, Mode=OneWay}"
|
|
||||||
Password="{x:Bind ViewModel.IncomingServerPassword, Mode=TwoWay}" />
|
|
||||||
<ComboBox
|
<ComboBox
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
Header="{x:Bind ViewModel.ConnectionSecurityHeaderText, Mode=OneWay}"
|
Header="{x:Bind ViewModel.ConnectionSecurityHeaderText, Mode=OneWay}"
|
||||||
@@ -167,18 +155,10 @@
|
|||||||
<FontIcon FontSize="14" Glyph="" />
|
<FontIcon FontSize="14" Glyph="" />
|
||||||
<TextBlock FontWeight="SemiBold" Text="{x:Bind ViewModel.OutgoingSectionTitleText, Mode=OneWay}" />
|
<TextBlock FontWeight="SemiBold" Text="{x:Bind ViewModel.OutgoingSectionTitleText, Mode=OneWay}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<TextBox
|
<TextBox Header="{x:Bind ViewModel.OutgoingServerHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.OutgoingServer, Mode=TwoWay}" />
|
||||||
Header="{x:Bind ViewModel.OutgoingServerHeaderText, Mode=OneWay}"
|
<TextBox Header="{x:Bind ViewModel.PortHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.OutgoingServerPort, Mode=TwoWay}" />
|
||||||
Text="{x:Bind ViewModel.OutgoingServer, Mode=TwoWay}" />
|
<TextBox Header="{x:Bind ViewModel.OutgoingUsernameHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.OutgoingServerUsername, Mode=TwoWay}" />
|
||||||
<TextBox
|
<PasswordBox Header="{x:Bind ViewModel.OutgoingPasswordHeaderText, Mode=OneWay}" Password="{x:Bind ViewModel.OutgoingServerPassword, Mode=TwoWay}" />
|
||||||
Header="{x:Bind ViewModel.PortHeaderText, Mode=OneWay}"
|
|
||||||
Text="{x:Bind ViewModel.OutgoingServerPort, Mode=TwoWay}" />
|
|
||||||
<TextBox
|
|
||||||
Header="{x:Bind ViewModel.OutgoingUsernameHeaderText, Mode=OneWay}"
|
|
||||||
Text="{x:Bind ViewModel.OutgoingServerUsername, Mode=TwoWay}" />
|
|
||||||
<PasswordBox
|
|
||||||
Header="{x:Bind ViewModel.OutgoingPasswordHeaderText, Mode=OneWay}"
|
|
||||||
Password="{x:Bind ViewModel.OutgoingServerPassword, Mode=TwoWay}" />
|
|
||||||
<ComboBox
|
<ComboBox
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
Header="{x:Bind ViewModel.ConnectionSecurityHeaderText, Mode=OneWay}"
|
Header="{x:Bind ViewModel.ConnectionSecurityHeaderText, Mode=OneWay}"
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
<abstract:AccountSetupProgressPageAbstract
|
||||||
|
x:Class="Wino.Views.AccountSetupProgressPage"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:abstract="using:Wino.Mail.WinUI.Views.Abstract"
|
||||||
|
xmlns:accounts="using:Wino.Core.Domain.Models.Accounts"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:domain="using:Wino.Core.Domain"
|
||||||
|
xmlns:helpers="using:Wino.Helpers"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:winuiControls="using:Microsoft.UI.Xaml.Controls"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<Grid Padding="0,8">
|
||||||
|
<StackPanel
|
||||||
|
MaxWidth="500"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Spacing="24">
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<TextBlock
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||||
|
Text="{x:Bind domain:Translator.AccountSetup_Title}" />
|
||||||
|
|
||||||
|
<!-- Steps List -->
|
||||||
|
<ListView
|
||||||
|
IsItemClickEnabled="False"
|
||||||
|
ItemsSource="{x:Bind ViewModel.Steps, Mode=OneWay}"
|
||||||
|
SelectionMode="None">
|
||||||
|
<ListView.ItemContainerStyle>
|
||||||
|
<Style TargetType="ListViewItem">
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||||
|
<Setter Property="Padding" Value="0,6" />
|
||||||
|
<Setter Property="MinHeight" Value="0" />
|
||||||
|
</Style>
|
||||||
|
</ListView.ItemContainerStyle>
|
||||||
|
<ListView.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="accounts:AccountSetupStepModel">
|
||||||
|
<Grid ColumnSpacing="12">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="28" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- Status indicator -->
|
||||||
|
<Grid
|
||||||
|
Width="20"
|
||||||
|
Height="20"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
|
||||||
|
<!-- Pending: gray circle -->
|
||||||
|
<FontIcon
|
||||||
|
FontSize="16"
|
||||||
|
Foreground="{ThemeResource TextFillColorDisabledBrush}"
|
||||||
|
Glyph=""
|
||||||
|
Visibility="{x:Bind helpers:XamlHelpers.BoolToVisibilityConverter(IsPending), Mode=OneWay}" />
|
||||||
|
|
||||||
|
<!-- InProgress: progress ring -->
|
||||||
|
<winuiControls:ProgressRing
|
||||||
|
Width="16"
|
||||||
|
Height="16"
|
||||||
|
IsActive="{x:Bind IsInProgress, Mode=OneWay}"
|
||||||
|
Visibility="{x:Bind helpers:XamlHelpers.BoolToVisibilityConverter(IsInProgress), Mode=OneWay}" />
|
||||||
|
|
||||||
|
<!-- Succeeded: green check -->
|
||||||
|
<FontIcon
|
||||||
|
FontSize="16"
|
||||||
|
Foreground="#107C10"
|
||||||
|
Glyph=""
|
||||||
|
Visibility="{x:Bind helpers:XamlHelpers.BoolToVisibilityConverter(IsSucceeded), Mode=OneWay}" />
|
||||||
|
|
||||||
|
<!-- Failed: red error -->
|
||||||
|
<FontIcon
|
||||||
|
FontSize="16"
|
||||||
|
Foreground="#D13438"
|
||||||
|
Glyph=""
|
||||||
|
Visibility="{x:Bind helpers:XamlHelpers.BoolToVisibilityConverter(IsFailed), Mode=OneWay}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Step text -->
|
||||||
|
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{x:Bind Title}" />
|
||||||
|
<TextBlock
|
||||||
|
Foreground="#D13438"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind ErrorMessage, Mode=OneWay}"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Visibility="{x:Bind helpers:XamlHelpers.BoolToVisibilityConverter(IsFailed), Mode=OneWay}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListView.ItemTemplate>
|
||||||
|
</ListView>
|
||||||
|
|
||||||
|
<!-- Success InfoBar -->
|
||||||
|
<winuiControls:InfoBar
|
||||||
|
IsClosable="False"
|
||||||
|
IsOpen="{x:Bind ViewModel.IsSetupComplete, Mode=OneWay}"
|
||||||
|
Message="{x:Bind domain:Translator.AccountSetup_SuccessMessage}"
|
||||||
|
Severity="Success" />
|
||||||
|
|
||||||
|
<!-- Failure InfoBar + Buttons -->
|
||||||
|
<StackPanel
|
||||||
|
Spacing="12"
|
||||||
|
Visibility="{x:Bind helpers:XamlHelpers.BoolToVisibilityConverter(ViewModel.IsSetupFailed), Mode=OneWay}">
|
||||||
|
<winuiControls:InfoBar
|
||||||
|
IsClosable="False"
|
||||||
|
IsOpen="True"
|
||||||
|
Message="{x:Bind ViewModel.FailureMessage, Mode=OneWay}"
|
||||||
|
Severity="Error" />
|
||||||
|
|
||||||
|
<StackPanel
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="8">
|
||||||
|
<Button
|
||||||
|
Command="{x:Bind ViewModel.GoBackCommand}"
|
||||||
|
Content="{x:Bind domain:Translator.AccountSetup_GoBackButton}" />
|
||||||
|
<Button
|
||||||
|
Command="{x:Bind ViewModel.TryAgainCommand}"
|
||||||
|
Content="{x:Bind domain:Translator.AccountSetup_TryAgainButton}"
|
||||||
|
Style="{StaticResource AccentButtonStyle}" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</abstract:AccountSetupProgressPageAbstract>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using Wino.Mail.WinUI.Views.Abstract;
|
||||||
|
|
||||||
|
namespace Wino.Views;
|
||||||
|
|
||||||
|
public sealed partial class AccountSetupProgressPage : AccountSetupProgressPageAbstract
|
||||||
|
{
|
||||||
|
public AccountSetupProgressPage()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,125 @@
|
|||||||
|
<abstract:ProviderSelectionPageAbstract
|
||||||
|
x:Class="Wino.Views.ProviderSelectionPage"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:abstract="using:Wino.Mail.WinUI.Views.Abstract"
|
||||||
|
xmlns:accounts="using:Wino.Core.Domain.Models.Accounts"
|
||||||
|
xmlns:coreViewModelData="using:Wino.Core.ViewModels.Data"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:domain="using:Wino.Core.Domain"
|
||||||
|
xmlns:helpers="using:Wino.Helpers"
|
||||||
|
xmlns:interfaces="using:Wino.Core.Domain.Interfaces"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<ScrollViewer
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel
|
||||||
|
MaxWidth="480"
|
||||||
|
Margin="0,24,0,24"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Spacing="20">
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}" Text="{x:Bind domain:Translator.ProviderSelection_Title}" />
|
||||||
|
<TextBlock
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource BodyTextBlockStyle}"
|
||||||
|
Text="{x:Bind domain:Translator.ProviderSelection_Subtitle}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Account Name + Color Picker -->
|
||||||
|
<Grid ColumnSpacing="12">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<TextBox
|
||||||
|
Header="{x:Bind domain:Translator.ProviderSelection_AccountNameHeader}"
|
||||||
|
PlaceholderText="{x:Bind domain:Translator.ProviderSelection_AccountNamePlaceholder}"
|
||||||
|
Text="{x:Bind ViewModel.AccountName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
|
||||||
|
<Button Grid.Column="1" VerticalAlignment="Bottom">
|
||||||
|
<Grid>
|
||||||
|
<!-- No color selected placeholder -->
|
||||||
|
<FontIcon
|
||||||
|
FontSize="16"
|
||||||
|
Glyph=""
|
||||||
|
Visibility="{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(ViewModel.IsColorSelected), Mode=OneWay}" />
|
||||||
|
<!-- Color swatch -->
|
||||||
|
<Ellipse
|
||||||
|
Width="16"
|
||||||
|
Height="16"
|
||||||
|
Fill="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(ViewModel.SelectedColor.Hex), Mode=OneWay}"
|
||||||
|
Visibility="{x:Bind helpers:XamlHelpers.BoolToVisibilityConverter(ViewModel.IsColorSelected), Mode=OneWay}" />
|
||||||
|
</Grid>
|
||||||
|
<Button.Flyout>
|
||||||
|
<Flyout Placement="TopEdgeAlignedLeft">
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<GridView
|
||||||
|
Width="150"
|
||||||
|
ItemTemplate="{StaticResource AccountColorTemplate}"
|
||||||
|
ItemsSource="{x:Bind ViewModel.AvailableColors, Mode=OneWay}"
|
||||||
|
SelectedItem="{x:Bind ViewModel.SelectedColor, Mode=TwoWay}" />
|
||||||
|
<HyperlinkButton
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Command="{x:Bind ViewModel.ClearColorCommand}"
|
||||||
|
Content="{x:Bind domain:Translator.ProviderSelection_ClearColor}"
|
||||||
|
Visibility="{x:Bind helpers:XamlHelpers.BoolToVisibilityConverter(ViewModel.IsColorSelected), Mode=OneWay}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Flyout>
|
||||||
|
</Button.Flyout>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Provider List -->
|
||||||
|
<ItemsView
|
||||||
|
ItemsSource="{x:Bind ViewModel.Providers, Mode=OneWay}"
|
||||||
|
SelectionChanged="ProviderSelectionChanged"
|
||||||
|
SelectionMode="Single">
|
||||||
|
<ItemsView.Layout>
|
||||||
|
<UniformGridLayout Orientation="Vertical" />
|
||||||
|
</ItemsView.Layout>
|
||||||
|
<ItemsView.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="interfaces:IProviderDetail">
|
||||||
|
<ItemContainer Padding="12,10">
|
||||||
|
<Grid Padding="16" ColumnSpacing="12">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Image
|
||||||
|
Width="32"
|
||||||
|
Height="32"
|
||||||
|
Source="{x:Bind ProviderImage}" />
|
||||||
|
<StackPanel
|
||||||
|
Grid.Column="1"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Spacing="2">
|
||||||
|
<TextBlock FontWeight="SemiBold" Text="{x:Bind Name}" />
|
||||||
|
<TextBlock
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind Description}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</ItemContainer>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsView.ItemTemplate>
|
||||||
|
</ItemsView>
|
||||||
|
|
||||||
|
<!-- Continue Button -->
|
||||||
|
<Button
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Command="{x:Bind ViewModel.ProceedCommand}"
|
||||||
|
Content="{x:Bind domain:Translator.ProviderSelection_ContinueButton}"
|
||||||
|
IsEnabled="{x:Bind ViewModel.CanProceed, Mode=OneWay}"
|
||||||
|
Style="{StaticResource AccentButtonStyle}" />
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</abstract:ProviderSelectionPageAbstract>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Wino.Mail.WinUI.Views.Abstract;
|
||||||
|
|
||||||
|
namespace Wino.Views;
|
||||||
|
|
||||||
|
public sealed partial class ProviderSelectionPage : ProviderSelectionPageAbstract
|
||||||
|
{
|
||||||
|
public ProviderSelectionPage()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProviderSelectionChanged(ItemsView sender, ItemsViewSelectionChangedEventArgs args)
|
||||||
|
{
|
||||||
|
ViewModel.SelectedProvider = sender.SelectedItem as Wino.Core.Domain.Interfaces.IProviderDetail;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
<abstract:SpecialImapCredentialsPageAbstract
|
||||||
|
x:Class="Wino.Views.SpecialImapCredentialsPage"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:abstract="using:Wino.Mail.WinUI.Views.Abstract"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:domain="using:Wino.Core.Domain"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<ScrollViewer
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel
|
||||||
|
MaxWidth="440"
|
||||||
|
Margin="0,24,0,24"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Spacing="20">
|
||||||
|
|
||||||
|
<!-- Provider logo -->
|
||||||
|
<Image
|
||||||
|
Height="48"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Source="{x:Bind ViewModel.WizardContext.SelectedProvider.ProviderImage, Mode=OneWay}"
|
||||||
|
Stretch="Uniform" />
|
||||||
|
|
||||||
|
<!-- Title / subtitle -->
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}" Text="{x:Bind ViewModel.WizardContext.SelectedProvider.Name, Mode=OneWay}" />
|
||||||
|
<TextBlock
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource BodyTextBlockStyle}"
|
||||||
|
Text="{x:Bind domain:Translator.ProviderSelection_SpecialImap_Subtitle}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Display Name -->
|
||||||
|
<TextBox
|
||||||
|
Header="{x:Bind domain:Translator.ProviderSelection_DisplayNameHeader}"
|
||||||
|
PlaceholderText="{x:Bind domain:Translator.ProviderSelection_DisplayNamePlaceholder}"
|
||||||
|
Text="{x:Bind ViewModel.DisplayName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
|
||||||
|
<!-- Email -->
|
||||||
|
<TextBox
|
||||||
|
Header="{x:Bind domain:Translator.ProviderSelection_EmailHeader}"
|
||||||
|
PlaceholderText="{x:Bind domain:Translator.ProviderSelection_EmailPlaceholder}"
|
||||||
|
Text="{x:Bind ViewModel.EmailAddress, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
|
||||||
|
<!-- App-Specific Password -->
|
||||||
|
<PasswordBox
|
||||||
|
x:Name="AppPasswordBox"
|
||||||
|
Header="{x:Bind domain:Translator.ProviderSelection_AppPasswordHeader}"
|
||||||
|
PasswordChanged="AppPasswordChanged" />
|
||||||
|
|
||||||
|
<HyperlinkButton
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Command="{x:Bind ViewModel.OpenAppPasswordHelpCommand}"
|
||||||
|
Content="{x:Bind domain:Translator.ProviderSelection_AppPasswordHelp}" />
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
|
<Rectangle Height="1" Fill="{ThemeResource CardStrokeColorDefaultBrush}" />
|
||||||
|
|
||||||
|
<!-- Calendar Mode -->
|
||||||
|
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{x:Bind domain:Translator.ProviderSelection_CalendarModeHeader}" />
|
||||||
|
|
||||||
|
<ListView
|
||||||
|
x:Name="CalendarModeListView"
|
||||||
|
IsItemClickEnabled="False"
|
||||||
|
SelectionChanged="CalendarModeSelectionChanged"
|
||||||
|
SelectionMode="Single">
|
||||||
|
<!-- Disabled -->
|
||||||
|
<ListViewItem>
|
||||||
|
<Grid Padding="12" ColumnSpacing="10">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<FontIcon
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontSize="18"
|
||||||
|
Glyph="" />
|
||||||
|
<StackPanel Grid.Column="1" Spacing="2">
|
||||||
|
<TextBlock FontWeight="SemiBold" Text="{x:Bind domain:Translator.ProviderSelection_CalendarMode_DisabledTitle}" />
|
||||||
|
<TextBlock
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind domain:Translator.ProviderSelection_CalendarMode_DisabledDescription}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</ListViewItem>
|
||||||
|
<!-- CalDAV -->
|
||||||
|
<ListViewItem>
|
||||||
|
<Grid Padding="12" ColumnSpacing="10">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<FontIcon
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontSize="18"
|
||||||
|
Glyph="" />
|
||||||
|
<StackPanel Grid.Column="1" Spacing="2">
|
||||||
|
<TextBlock FontWeight="SemiBold" Text="{x:Bind domain:Translator.ProviderSelection_CalendarMode_CalDavTitle}" />
|
||||||
|
<TextBlock
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind ViewModel.CalendarModeCalDavDescription, Mode=OneWay}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</ListViewItem>
|
||||||
|
<!-- Local -->
|
||||||
|
<ListViewItem>
|
||||||
|
<Grid Padding="12" ColumnSpacing="10">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<FontIcon
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontSize="18"
|
||||||
|
Glyph="" />
|
||||||
|
<StackPanel Grid.Column="1" Spacing="2">
|
||||||
|
<TextBlock FontWeight="SemiBold" Text="{x:Bind domain:Translator.ProviderSelection_CalendarMode_LocalTitle}" />
|
||||||
|
<TextBlock
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind domain:Translator.ProviderSelection_CalendarMode_LocalDescription}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</ListViewItem>
|
||||||
|
</ListView>
|
||||||
|
|
||||||
|
<!-- Continue Button -->
|
||||||
|
<Button
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Command="{x:Bind ViewModel.ProceedCommand}"
|
||||||
|
Content="{x:Bind domain:Translator.ProviderSelection_ContinueButton}"
|
||||||
|
IsEnabled="{x:Bind ViewModel.CanProceed, Mode=OneWay}"
|
||||||
|
Style="{StaticResource AccentButtonStyle}" />
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
</abstract:SpecialImapCredentialsPageAbstract>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Wino.Mail.WinUI.Views.Abstract;
|
||||||
|
|
||||||
|
namespace Wino.Views;
|
||||||
|
|
||||||
|
public sealed partial class SpecialImapCredentialsPage : SpecialImapCredentialsPageAbstract
|
||||||
|
{
|
||||||
|
public SpecialImapCredentialsPage()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CalendarModeSelectionChanged(object sender, SelectionChangedEventArgs args)
|
||||||
|
{
|
||||||
|
if (sender is ListView lv)
|
||||||
|
ViewModel.SelectedCalendarModeIndex = lv.SelectedIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AppPasswordChanged(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is PasswordBox pb)
|
||||||
|
ViewModel.AppSpecificPassword = pb.Password;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<abstract:WelcomeHostPageAbstract
|
||||||
|
x:Class="Wino.Views.WelcomeHostPage"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:abstract="using:Wino.Mail.WinUI.Views.Abstract"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:helpers="using:Wino.Helpers"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:viewModelData="using:Wino.Mail.ViewModels.Data"
|
||||||
|
xmlns:winuiControls="using:Microsoft.UI.Xaml.Controls"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<Grid RowSpacing="0">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Wizard step indicator bar -->
|
||||||
|
<Border
|
||||||
|
Padding="24,20"
|
||||||
|
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||||
|
BorderThickness="0,0,0,1">
|
||||||
|
<winuiControls:BreadcrumbBar
|
||||||
|
x:Name="Breadcrumb"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
ItemClicked="BreadItemClicked"
|
||||||
|
ItemsSource="{x:Bind PageHistory, Mode=OneWay}">
|
||||||
|
<winuiControls:BreadcrumbBar.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="viewModelData:BreadcrumbNavigationItemViewModel">
|
||||||
|
<winuiControls:BreadcrumbBarItem>
|
||||||
|
<winuiControls:BreadcrumbBarItem.ContentTemplate>
|
||||||
|
<DataTemplate x:DataType="viewModelData:BreadcrumbNavigationItemViewModel">
|
||||||
|
<StackPanel
|
||||||
|
Margin="8,0"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="8">
|
||||||
|
<Border
|
||||||
|
Width="24"
|
||||||
|
Height="24"
|
||||||
|
Background="{x:Bind helpers:XamlHelpers.GetWizardStepBadgeBrush(IsActive), Mode=OneWay}"
|
||||||
|
CornerRadius="12">
|
||||||
|
<TextBlock
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontSize="11"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{x:Bind helpers:XamlHelpers.GetWizardStepNumberForeground(IsActive), Mode=OneWay}"
|
||||||
|
Text="{x:Bind StepNumber, Mode=OneWay}" />
|
||||||
|
</Border>
|
||||||
|
<TextBlock
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontWeight="{x:Bind helpers:XamlHelpers.GetFontWeightBySyncState(IsActive), Mode=OneWay}"
|
||||||
|
Style="{StaticResource BodyTextBlockStyle}"
|
||||||
|
Text="{x:Bind Title, Mode=OneWay}" />
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</winuiControls:BreadcrumbBarItem.ContentTemplate>
|
||||||
|
</winuiControls:BreadcrumbBarItem>
|
||||||
|
</DataTemplate>
|
||||||
|
</winuiControls:BreadcrumbBar.ItemTemplate>
|
||||||
|
</winuiControls:BreadcrumbBar>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Frame
|
||||||
|
x:Name="WizardFrame"
|
||||||
|
Grid.Row="1" />
|
||||||
|
</Grid>
|
||||||
|
</abstract:WelcomeHostPageAbstract>
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
using Microsoft.UI.Xaml.Media.Animation;
|
||||||
|
using Microsoft.UI.Xaml.Navigation;
|
||||||
|
using MoreLinq;
|
||||||
|
using Wino.Core.Domain.Enums;
|
||||||
|
using Wino.Mail.ViewModels.Data;
|
||||||
|
using Wino.Mail.WinUI.Views.Abstract;
|
||||||
|
using Wino.Messaging.Client.Navigation;
|
||||||
|
|
||||||
|
namespace Wino.Views;
|
||||||
|
|
||||||
|
public sealed partial class WelcomeHostPage : WelcomeHostPageAbstract,
|
||||||
|
IRecipient<BreadcrumbNavigationRequested>,
|
||||||
|
IRecipient<BackBreadcrumNavigationRequested>
|
||||||
|
{
|
||||||
|
public ObservableCollection<BreadcrumbNavigationItemViewModel> PageHistory { get; set; } = [];
|
||||||
|
|
||||||
|
public WelcomeHostPage()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnNavigatedTo(e);
|
||||||
|
|
||||||
|
WeakReferenceMessenger.Default.Register<BreadcrumbNavigationRequested>(this);
|
||||||
|
WeakReferenceMessenger.Default.Register<BackBreadcrumNavigationRequested>(this);
|
||||||
|
|
||||||
|
// Navigate to the welcome/get-started page without adding it to the wizard breadcrumb.
|
||||||
|
// Breadcrumb steps only start after the user clicks "Get Started".
|
||||||
|
var welcomePageType = ViewModel.NavigationService.GetPageType(WinoPage.WelcomePageV2);
|
||||||
|
WizardFrame.Navigate(welcomePageType, null, new SuppressNavigationTransitionInfo());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
|
||||||
|
{
|
||||||
|
WeakReferenceMessenger.Default.Unregister<BreadcrumbNavigationRequested>(this);
|
||||||
|
WeakReferenceMessenger.Default.Unregister<BackBreadcrumNavigationRequested>(this);
|
||||||
|
|
||||||
|
base.OnNavigatingFrom(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Receive(BreadcrumbNavigationRequested message)
|
||||||
|
{
|
||||||
|
var pageType = ViewModel.NavigationService.GetPageType(message.PageType);
|
||||||
|
if (pageType == null) return;
|
||||||
|
|
||||||
|
WizardFrame.Navigate(pageType, message.Parameter, new SlideNavigationTransitionInfo
|
||||||
|
{
|
||||||
|
Effect = SlideNavigationTransitionEffect.FromRight
|
||||||
|
});
|
||||||
|
|
||||||
|
PageHistory.ForEach(a => a.IsActive = false);
|
||||||
|
PageHistory.Add(new BreadcrumbNavigationItemViewModel(message, isActive: true, stepNumber: PageHistory.Count + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Receive(BackBreadcrumNavigationRequested message)
|
||||||
|
{
|
||||||
|
GoBackFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GoBackFrame()
|
||||||
|
{
|
||||||
|
if (!WizardFrame.CanGoBack) return;
|
||||||
|
|
||||||
|
PageHistory.RemoveAt(PageHistory.Count - 1);
|
||||||
|
WizardFrame.GoBack(new SlideNavigationTransitionInfo
|
||||||
|
{
|
||||||
|
Effect = SlideNavigationTransitionEffect.FromLeft
|
||||||
|
});
|
||||||
|
|
||||||
|
if (PageHistory.Count > 0)
|
||||||
|
{
|
||||||
|
PageHistory.ForEach(a => a.IsActive = false);
|
||||||
|
PageHistory[PageHistory.Count - 1].IsActive = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BreadItemClicked(Microsoft.UI.Xaml.Controls.BreadcrumbBar sender, Microsoft.UI.Xaml.Controls.BreadcrumbBarItemClickedEventArgs args)
|
||||||
|
{
|
||||||
|
var clickedItem = PageHistory[args.Index];
|
||||||
|
var currentActive = PageHistory.FirstOrDefault(a => a.IsActive);
|
||||||
|
|
||||||
|
// Only allow navigating backwards (clicking items before current)
|
||||||
|
if (currentActive == null || args.Index >= PageHistory.IndexOf(currentActive))
|
||||||
|
return;
|
||||||
|
|
||||||
|
while (PageHistory.FirstOrDefault(a => a.IsActive) != clickedItem && WizardFrame.CanGoBack)
|
||||||
|
{
|
||||||
|
GoBackFrame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,9 +29,6 @@ public sealed partial class WelcomeWindow : WindowEx
|
|||||||
Width = 980;
|
Width = 980;
|
||||||
Height = 720;
|
Height = 720;
|
||||||
|
|
||||||
//this.IsResizable = false;
|
|
||||||
//this.IsMaximizable = false;
|
|
||||||
|
|
||||||
this.CenterOnScreen();
|
this.CenterOnScreen();
|
||||||
|
|
||||||
var themeService = WinoApplication.Current.Services.GetService<INewThemeService>();
|
var themeService = WinoApplication.Current.Services.GetService<INewThemeService>();
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using Wino.Core.Domain.Interfaces;
|
|
||||||
|
|
||||||
namespace Wino.Services.Migrations;
|
|
||||||
|
|
||||||
public class VNextDelayMigration : IAppMigration
|
|
||||||
{
|
|
||||||
public string MigrationId => "vnext-delay";
|
|
||||||
|
|
||||||
public Task ExecuteAsync() => Task.Delay(3000);
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Wino.Core.Domain;
|
using Wino.Core.Domain;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
using Wino.Core.Domain.Models.Updates;
|
using Wino.Core.Domain.Models.Updates;
|
||||||
using Wino.Services.Migrations;
|
|
||||||
|
|
||||||
namespace Wino.Services;
|
namespace Wino.Services;
|
||||||
|
|
||||||
@@ -15,12 +13,10 @@ public class UpdateManager : IUpdateManager
|
|||||||
private const string UpdateNotesResourcePath = "ms-appx:///Assets/UpdateNotes/vnext.json";
|
private const string UpdateNotesResourcePath = "ms-appx:///Assets/UpdateNotes/vnext.json";
|
||||||
private const string FeaturesResourcePath = "ms-appx:///Assets/UpdateNotes/features.json";
|
private const string FeaturesResourcePath = "ms-appx:///Assets/UpdateNotes/features.json";
|
||||||
private const string UpdateNotesSeenKeyFormat = "UpdateNotes_{0}_Shown";
|
private const string UpdateNotesSeenKeyFormat = "UpdateNotes_{0}_Shown";
|
||||||
private const string MigrationCompletedKeyFormat = "Migration_{0}_Completed";
|
|
||||||
|
|
||||||
private readonly IFileService _fileService;
|
private readonly IFileService _fileService;
|
||||||
private readonly IConfigurationService _configurationService;
|
private readonly IConfigurationService _configurationService;
|
||||||
private readonly INativeAppService _nativeAppService;
|
private readonly INativeAppService _nativeAppService;
|
||||||
private readonly List<IAppMigration> _migrations = [];
|
|
||||||
|
|
||||||
private string _versionSeenKey = string.Empty;
|
private string _versionSeenKey = string.Empty;
|
||||||
private UpdateNotes _latestUpdateNotes = new();
|
private UpdateNotes _latestUpdateNotes = new();
|
||||||
@@ -32,7 +28,6 @@ public class UpdateManager : IUpdateManager
|
|||||||
_fileService = fileService;
|
_fileService = fileService;
|
||||||
_configurationService = configurationService;
|
_configurationService = configurationService;
|
||||||
_nativeAppService = nativeAppService;
|
_nativeAppService = nativeAppService;
|
||||||
_migrations.Add(new VNextDelayMigration());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetVersionSeenKey()
|
private string GetVersionSeenKey()
|
||||||
@@ -69,7 +64,6 @@ public class UpdateManager : IUpdateManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// T
|
|
||||||
public bool ShouldShowUpdateNotes()
|
public bool ShouldShowUpdateNotes()
|
||||||
=> !_configurationService.Get(GetVersionSeenKey(), false);
|
=> !_configurationService.Get(GetVersionSeenKey(), false);
|
||||||
|
|
||||||
@@ -92,35 +86,4 @@ public class UpdateManager : IUpdateManager
|
|||||||
|
|
||||||
public void MarkUpdateNotesAsSeen()
|
public void MarkUpdateNotesAsSeen()
|
||||||
=> _configurationService.Set(GetVersionSeenKey(), true);
|
=> _configurationService.Set(GetVersionSeenKey(), true);
|
||||||
|
|
||||||
public bool HasPendingMigrations()
|
|
||||||
{
|
|
||||||
if (!_latestUpdateNotes.HasPendingMigrations)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return _migrations.Any(m => !_configurationService.Get(string.Format(MigrationCompletedKeyFormat, m.MigrationId), false));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RunPendingMigrationsAsync()
|
|
||||||
{
|
|
||||||
if (!_latestUpdateNotes.HasPendingMigrations)
|
|
||||||
_latestUpdateNotes = await GetLatestUpdateNotesAsync();
|
|
||||||
|
|
||||||
if (!_latestUpdateNotes.HasPendingMigrations)
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (var migration in _migrations)
|
|
||||||
{
|
|
||||||
var key = string.Format(MigrationCompletedKeyFormat, migration.MigrationId);
|
|
||||||
|
|
||||||
if (!_configurationService.Get(key, false))
|
|
||||||
{
|
|
||||||
await migration.ExecuteAsync();
|
|
||||||
_configurationService.Set(key, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RegisterMigrations(IEnumerable<IAppMigration> migrations)
|
|
||||||
=> _migrations.AddRange(migrations);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user