merged
@@ -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.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
This is a design document for new welcome page for users who have no accounts defined in the database.
|
||||||
|
|
||||||
|
Right now we welcome our users in WelcomePage if they have no accounts defined. Navigation is like ShellWindow -> MailAppShell -> WelcomePage. This allows users to access ManageAccountsPage from the side bar since mail app shell has this navigation option.
|
||||||
|
We don't want this anymore. Here are the instructions to define new welcome page (or initial startup wizard page):
|
||||||
|
|
||||||
|
- ShellWindow should not be used to navigate this new welcome page.
|
||||||
|
- Create a new WelcomeWindow and navigate to WelcomePage. You can remove all the content inside the existing welcome page because we are doing a new welcome page.
|
||||||
|
- In App.xaml.cs, we must check if the user has accounts defined or not. If no accounts, create this new window and activate it.
|
||||||
|
- In this Window I want to highlight a few things. It should clearly say that this is a native Windows application that supports Mail and Calendar.
|
||||||
|
- Design is not important at the moment, but make sure to follow Windows fluent design guidelines.
|
||||||
|
- In this page users must be able to
|
||||||
|
|
||||||
|
+ Go to manage accounts page
|
||||||
|
+ Show the latest version changelog. We have this new "What's new" dialog, but resolve the latest json for the update and show it on the page as well using the same FlipView. You can make this a UserControl maybe. It'll be exactly the same, except "Get started" button should not be visible when the control is loaded in WelcomePage.
|
||||||
|
+ Some details from the About page like version, github page, donation link etc. would be great. We can show it here in new WelcomePage too.
|
||||||
|
|
||||||
@@ -72,10 +72,14 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
IAccountService accountService,
|
IAccountService accountService,
|
||||||
ICalendarService calendarService,
|
ICalendarService calendarService,
|
||||||
IAccountCalendarStateService accountCalendarStateService,
|
IAccountCalendarStateService accountCalendarStateService,
|
||||||
INavigationService navigationService)
|
INavigationService navigationService,
|
||||||
|
IDialogServiceBase dialogService,
|
||||||
|
IUpdateManager updateManager)
|
||||||
{
|
{
|
||||||
_accountService = accountService;
|
_accountService = accountService;
|
||||||
_calendarService = calendarService;
|
_calendarService = calendarService;
|
||||||
|
_dialogService = dialogService;
|
||||||
|
_updateManager = updateManager;
|
||||||
|
|
||||||
AccountCalendarStateService = accountCalendarStateService;
|
AccountCalendarStateService = accountCalendarStateService;
|
||||||
AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested;
|
AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested;
|
||||||
@@ -123,9 +127,24 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
|
|
||||||
await InitializeAccountCalendarsAsync();
|
await InitializeAccountCalendarsAsync();
|
||||||
|
|
||||||
|
await ShowWhatIsNewIfNeededAsync();
|
||||||
|
|
||||||
TodayClicked();
|
TodayClicked();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ShowWhatIsNewIfNeededAsync()
|
||||||
|
{
|
||||||
|
if (!_updateManager.ShouldShowUpdateNotes())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var notes = await _updateManager.GetLatestUpdateNotesAsync();
|
||||||
|
|
||||||
|
if (notes.Sections.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await _dialogService.ShowWhatIsNewDialogAsync(notes);
|
||||||
|
}
|
||||||
|
|
||||||
private async void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e)
|
private async void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e)
|
||||||
{
|
{
|
||||||
// When using three-state checkbox, multiple accounts will be selected/unselected at the same time.
|
// When using three-state checkbox, multiple accounts will be selected/unselected at the same time.
|
||||||
@@ -271,6 +290,8 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
private DateTime? _navigationDate;
|
private DateTime? _navigationDate;
|
||||||
private readonly IAccountService _accountService;
|
private readonly IAccountService _accountService;
|
||||||
private readonly ICalendarService _calendarService;
|
private readonly ICalendarService _calendarService;
|
||||||
|
private readonly IDialogServiceBase _dialogService;
|
||||||
|
private readonly IUpdateManager _updateManager;
|
||||||
|
|
||||||
#region Commands
|
#region Commands
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using Wino.Core.Domain.Models.Updates;
|
||||||
|
|
||||||
namespace Wino.Core.Domain;
|
namespace Wino.Core.Domain;
|
||||||
|
|
||||||
@@ -8,4 +9,6 @@ namespace Wino.Core.Domain;
|
|||||||
[JsonSerializable(typeof(int))]
|
[JsonSerializable(typeof(int))]
|
||||||
[JsonSerializable(typeof(List<string>))]
|
[JsonSerializable(typeof(List<string>))]
|
||||||
[JsonSerializable(typeof(bool))]
|
[JsonSerializable(typeof(bool))]
|
||||||
|
[JsonSerializable(typeof(UpdateNotes))]
|
||||||
|
[JsonSerializable(typeof(List<UpdateNoteSection>))]
|
||||||
public partial class BasicTypesJsonContext : JsonSerializerContext;
|
public partial class BasicTypesJsonContext : JsonSerializerContext;
|
||||||
|
|||||||
@@ -21,7 +21,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 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
|
||||||
|
}
|
||||||
@@ -34,5 +34,10 @@ public enum WinoPage
|
|||||||
CalendarAccountSettingsPage,
|
CalendarAccountSettingsPage,
|
||||||
EventDetailsPage,
|
EventDetailsPage,
|
||||||
SignatureAndEncryptionPage,
|
SignatureAndEncryptionPage,
|
||||||
StoragePage
|
StoragePage,
|
||||||
|
WelcomePageV2,
|
||||||
|
WelcomeHostPage,
|
||||||
|
ProviderSelectionPage,
|
||||||
|
AccountSetupProgressPage,
|
||||||
|
SpecialImapCredentialsPage
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Wino.Core.Domain.Enums;
|
|||||||
using Wino.Core.Domain.Models.Accounts;
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
using Wino.Core.Domain.Models.Common;
|
using Wino.Core.Domain.Models.Common;
|
||||||
using Wino.Core.Domain.Models.Printing;
|
using Wino.Core.Domain.Models.Printing;
|
||||||
|
using Wino.Core.Domain.Models.Updates;
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Interfaces;
|
namespace Wino.Core.Domain.Interfaces;
|
||||||
|
|
||||||
@@ -30,4 +31,10 @@ public interface IDialogServiceBase
|
|||||||
Task<List<SharedFile>> PickFilesAsync(params object[] typeFilters);
|
Task<List<SharedFile>> PickFilesAsync(params object[] typeFilters);
|
||||||
Task<string> PickFilePathAsync(string saveFileName);
|
Task<string> PickFilePathAsync(string saveFileName);
|
||||||
Task<WebView2PrintSettingsModel> ShowPrintDialogAsync(WebView2PrintSettingsModel initialSettings = null);
|
Task<WebView2PrintSettingsModel> ShowPrintDialogAsync(WebView2PrintSettingsModel initialSettings = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Presents the "What's New" dialog for the current version.
|
||||||
|
/// This dialog is undismissable and runs any pending migrations when the user clicks "Get Started".
|
||||||
|
/// </summary>
|
||||||
|
Task ShowWhatIsNewDialogAsync(UpdateNotes notes);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,4 +37,10 @@ public interface INewThemeService : IInitializeAsync
|
|||||||
|
|
||||||
// Backdrop management
|
// Backdrop management
|
||||||
List<BackdropTypeWrapper> GetAvailableBackdropTypes();
|
List<BackdropTypeWrapper> GetAvailableBackdropTypes();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Re-applies the current theme (backdrop, root theme, accent, caption colors)
|
||||||
|
/// to the currently active window. Use after a window transition.
|
||||||
|
/// </summary>
|
||||||
|
Task ApplyThemeToActiveWindowAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Wino.Core.Domain.Models.Updates;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Wino.Core.Domain.Interfaces;
|
||||||
|
|
||||||
|
public interface IUpdateManager
|
||||||
|
{
|
||||||
|
/// <summary>Loads and parses the update notes for the current version from the bundled asset file.</summary>
|
||||||
|
Task<UpdateNotes> GetLatestUpdateNotesAsync();
|
||||||
|
|
||||||
|
/// <summary>Loads and parses the app feature highlights from the bundled asset file.</summary>
|
||||||
|
Task<List<UpdateNoteSection>> GetFeaturesAsync();
|
||||||
|
|
||||||
|
/// <summary>Returns true if the current version's update notes have not yet been shown to the user.</summary>
|
||||||
|
bool ShouldShowUpdateNotes();
|
||||||
|
|
||||||
|
/// <summary>Stores a flag in local settings indicating the update notes for the current version have been seen.</summary>
|
||||||
|
void MarkUpdateNotesAsSeen();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Wino.Core.Domain.Models.Updates;
|
||||||
|
|
||||||
|
public class UpdateNoteSection
|
||||||
|
{
|
||||||
|
[JsonPropertyName("title")]
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("description")]
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("imageUrl")]
|
||||||
|
public string ImageUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("imageWidth")]
|
||||||
|
public double? ImageWidth { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("imageHeight")]
|
||||||
|
public double? ImageHeight { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Gets the image width for binding, returning NaN for auto-sizing when not specified.</summary>
|
||||||
|
public double ActualImageWidth => ImageWidth ?? double.NaN;
|
||||||
|
|
||||||
|
/// <summary>Gets the image height for binding, returning NaN for auto-sizing when not specified.</summary>
|
||||||
|
public double ActualImageHeight => ImageHeight ?? double.NaN;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
namespace Wino.Core.Domain.Models.Updates;
|
||||||
|
|
||||||
|
public class UpdateNotes
|
||||||
|
{
|
||||||
|
public List<UpdateNoteSection> Sections { get; set; } = [];
|
||||||
|
}
|
||||||
@@ -993,5 +993,66 @@
|
|||||||
"CalendarAccountSettings_DefaultShowAs": "Default Show As Status",
|
"CalendarAccountSettings_DefaultShowAs": "Default Show As Status",
|
||||||
"CalendarAccountSettings_DefaultShowAsDescription": "Default availability status for new events created with this account",
|
"CalendarAccountSettings_DefaultShowAsDescription": "Default availability status for new events created with this account",
|
||||||
"CalendarAccountSettings_PrimaryCalendar": "Primary Calendar",
|
"CalendarAccountSettings_PrimaryCalendar": "Primary Calendar",
|
||||||
"CalendarAccountSettings_PrimaryCalendarDescription": "Mark this calendar as the primary calendar for the account"
|
"CalendarAccountSettings_PrimaryCalendarDescription": "Mark this calendar as the primary calendar for the account",
|
||||||
|
"WhatIsNew_GetStartedButton": "Get Started",
|
||||||
|
"WhatIsNew_ContinueAnywayButton": "Continue anyway",
|
||||||
|
"WhatIsNew_PreparingForNewVersionButton": "Preparing for new version...",
|
||||||
|
"WhatIsNew_MigrationPreparing_Title": "Preparing your data",
|
||||||
|
"WhatIsNew_MigrationPreparing_Description": "Wino is applying update migrations. Please wait while we prepare your account data for this release.",
|
||||||
|
"WhatIsNew_MigrationFailedMessage": "Applying migrations failed with error code {0}. You may continue to use the application. However, if you encounter serious issues please re-install the application.",
|
||||||
|
"WhatIsNew_MigrationNotification_Title": "Wino Mail Updated",
|
||||||
|
"WhatIsNew_MigrationNotification_Message": "Open the app to complete the update and see what's new.",
|
||||||
|
"WelcomeWindow_Title": "Welcome to Wino Mail",
|
||||||
|
"WelcomeWindow_Subtitle": "A native Windows experience for Mail and Calendar.",
|
||||||
|
"WelcomeWindow_WhatsNewTitle": "Latest changes",
|
||||||
|
"WelcomeWindow_FeaturesTitle": "Features",
|
||||||
|
"WelcomeWindow_WhatsNewTab": "What's New",
|
||||||
|
"WelcomeWindow_FeaturesTab": "Features",
|
||||||
|
"WelcomeWindow_GetStartedButton": "Get started by adding an account",
|
||||||
|
"WelcomeWindow_GetStartedDescription": "Add your Outlook, Gmail, or IMAP account to get started with Wino Mail.",
|
||||||
|
"WelcomeWindow_SetupTitle": "Set up your account",
|
||||||
|
"WelcomeWindow_SetupSubtitle": "Choose your email provider to get started",
|
||||||
|
"WelcomeWindow_AddAccountButton": "Add account",
|
||||||
|
"WelcomeWindow_SkipForNow": "Skip for now — I'll set it up later",
|
||||||
|
"WelcomeWindow_AppDescription": "A fast, focused inbox — redesigned for Windows 11",
|
||||||
|
"WelcomeWizard_Step1Title": "Welcome",
|
||||||
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,4 +7,7 @@ namespace Wino.Core.Domain;
|
|||||||
/// All translations generated automatically by the source generator.
|
/// All translations generated automatically by the source generator.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[TranslatorGen]
|
[TranslatorGen]
|
||||||
public partial class Translator;
|
public partial class Translator
|
||||||
|
{
|
||||||
|
public static string GetTranslatedString(string key) => Resources.GetTranslatedString(key);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,251 +86,11 @@ 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();
|
||||||
|
|
||||||
private async Task ValidateSpecialImapConnectivityAsync(CustomServerInformation serverInformation)
|
private async Task ValidateSpecialImapConnectivityAsync(CustomServerInformation serverInformation)
|
||||||
{
|
{
|
||||||
var connectivityResult = await SynchronizationManager.Instance
|
var connectivityResult = await SynchronizationManager.Instance
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
private readonly IMailDialogService _dialogService;
|
private readonly IMailDialogService _dialogService;
|
||||||
private readonly IMimeFileService _mimeFileService;
|
private readonly IMimeFileService _mimeFileService;
|
||||||
private readonly IWebView2RuntimeValidatorService _webView2RuntimeValidatorService;
|
private readonly IWebView2RuntimeValidatorService _webView2RuntimeValidatorService;
|
||||||
|
private readonly IUpdateManager _updateManager;
|
||||||
|
|
||||||
private readonly INativeAppService _nativeAppService;
|
private readonly INativeAppService _nativeAppService;
|
||||||
private readonly IMailService _mailService;
|
private readonly IMailService _mailService;
|
||||||
@@ -100,7 +101,8 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
IStatePersistanceService statePersistanceService,
|
IStatePersistanceService statePersistanceService,
|
||||||
IConfigurationService configurationService,
|
IConfigurationService configurationService,
|
||||||
IStartupBehaviorService startupBehaviorService,
|
IStartupBehaviorService startupBehaviorService,
|
||||||
IWebView2RuntimeValidatorService webView2RuntimeValidatorService)
|
IWebView2RuntimeValidatorService webView2RuntimeValidatorService,
|
||||||
|
IUpdateManager updateManager)
|
||||||
{
|
{
|
||||||
StatePersistenceService = statePersistanceService;
|
StatePersistenceService = statePersistanceService;
|
||||||
|
|
||||||
@@ -121,6 +123,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
_notificationBuilder = notificationBuilder;
|
_notificationBuilder = notificationBuilder;
|
||||||
_winoRequestDelegator = winoRequestDelegator;
|
_winoRequestDelegator = winoRequestDelegator;
|
||||||
_webView2RuntimeValidatorService = webView2RuntimeValidatorService;
|
_webView2RuntimeValidatorService = webView2RuntimeValidatorService;
|
||||||
|
_updateManager = updateManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnDispatcherAssigned()
|
protected override void OnDispatcherAssigned()
|
||||||
@@ -251,6 +254,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
await ForceAllAccountSynchronizationsAsync();
|
await ForceAllAccountSynchronizationsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await ShowWhatIsNewIfNeededAsync();
|
||||||
await MakeSureEnableStartupLaunchAsync();
|
await MakeSureEnableStartupLaunchAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,6 +268,19 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ShowWhatIsNewIfNeededAsync()
|
||||||
|
{
|
||||||
|
if (!_updateManager.ShouldShowUpdateNotes())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var notes = await _updateManager.GetLatestUpdateNotesAsync();
|
||||||
|
|
||||||
|
if (notes.Sections.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await _dialogService.ShowWhatIsNewDialogAsync(notes);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task MakeSureEnableStartupLaunchAsync()
|
private async Task MakeSureEnableStartupLaunchAsync()
|
||||||
{
|
{
|
||||||
if (!_configurationService.Get<bool>(IsActivateStartupLaunchAskedKey, false))
|
if (!_configurationService.Get<bool>(IsActivateStartupLaunchAskedKey, false))
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
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.Domain.Models.Updates;
|
||||||
|
using Wino.Messaging.Client.Navigation;
|
||||||
|
|
||||||
|
namespace Wino.Mail.ViewModels;
|
||||||
|
|
||||||
|
public partial class WelcomePageV2ViewModel : MailBaseViewModel
|
||||||
|
{
|
||||||
|
private readonly IUpdateManager _updateManager;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial List<UpdateNoteSection> UpdateSections { get; set; } = [];
|
||||||
|
|
||||||
|
public WelcomePageV2ViewModel(IUpdateManager updateManager)
|
||||||
|
{
|
||||||
|
_updateManager = updateManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||||
|
{
|
||||||
|
base.OnNavigatedTo(mode, parameters);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var updateNotes = await _updateManager.GetLatestUpdateNotesAsync();
|
||||||
|
UpdateSections = updateNotes.Sections;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
UpdateSections = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void GetStarted()
|
||||||
|
{
|
||||||
|
Messenger.Send(new BreadcrumbNavigationRequested(
|
||||||
|
Translator.WelcomeWizard_Step2Title,
|
||||||
|
WinoPage.ProviderSelectionPage));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,37 +1,69 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using Wino.Core.Domain;
|
using Wino.Core.Domain;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Models.Navigation;
|
using Wino.Core.Domain.Models.Navigation;
|
||||||
|
using Wino.Core.Domain.Models.Updates;
|
||||||
|
|
||||||
namespace Wino.Mail.ViewModels;
|
namespace Wino.Mail.ViewModels;
|
||||||
|
|
||||||
public partial class WelcomePageViewModel : MailBaseViewModel
|
public partial class WelcomePageViewModel : MailBaseViewModel
|
||||||
{
|
{
|
||||||
public const string VersionFile = "vnext.md";
|
private readonly IUpdateManager _updateManager;
|
||||||
private readonly IMailDialogService _dialogService;
|
private readonly INativeAppService _nativeAppService;
|
||||||
private readonly IFileService _fileService;
|
private readonly INavigationService _navigationService;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial string CurrentVersionNotes { get; set; } = string.Empty;
|
public partial string VersionDisplay { get; set; } = string.Empty;
|
||||||
|
|
||||||
public WelcomePageViewModel(IMailDialogService dialogService, IFileService fileService)
|
[ObservableProperty]
|
||||||
|
public partial List<UpdateNoteSection> UpdateSections { get; set; } = [];
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial List<UpdateNoteSection> FeatureSections { get; set; } = [];
|
||||||
|
|
||||||
|
public string GitHubUrl => "https://github.com/bkaankose/Wino-Mail/";
|
||||||
|
public string PaypalUrl => "https://paypal.me/bkaankose?country.x=PL&locale.x=en_US";
|
||||||
|
|
||||||
|
public WelcomePageViewModel(IUpdateManager updateManager,
|
||||||
|
INativeAppService nativeAppService,
|
||||||
|
INavigationService navigationService)
|
||||||
{
|
{
|
||||||
_dialogService = dialogService;
|
_updateManager = updateManager;
|
||||||
_fileService = fileService;
|
_nativeAppService = nativeAppService;
|
||||||
|
_navigationService = navigationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||||
{
|
{
|
||||||
base.OnNavigatedTo(mode, parameters);
|
base.OnNavigatedTo(mode, parameters);
|
||||||
|
|
||||||
|
VersionDisplay = $"{Translator.SettingsAboutVersion}{_nativeAppService.GetFullAppVersion()}";
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
CurrentVersionNotes = await _fileService.GetFileContentByApplicationUriAsync($"ms-appx:///Assets/ReleaseNotes/{VersionFile}");
|
var updateNotes = await _updateManager.GetLatestUpdateNotesAsync();
|
||||||
|
UpdateSections = updateNotes.Sections;
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error, "Can't find the patch notes.", Core.Domain.Enums.InfoBarMessageType.Information);
|
UpdateSections = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
FeatureSections = await _updateManager.GetFeaturesAsync();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
FeatureSections = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void NavigateManageAccounts()
|
||||||
|
=> _navigationService.Navigate(WinoPage.ManageAccountsPage, null, NavigationReferenceFrame.ShellFrame, NavigationTransitionType.DrillIn);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,13 +21,17 @@ using Wino.Core.Domain.Enums;
|
|||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
using Wino.Core.Domain.Models.Calendar;
|
using Wino.Core.Domain.Models.Calendar;
|
||||||
using Wino.Core.Domain.Models.MailItem;
|
using Wino.Core.Domain.Models.MailItem;
|
||||||
|
using Wino.Core.Domain.Models.Navigation;
|
||||||
using Wino.Core.Domain.Models.Synchronization;
|
using Wino.Core.Domain.Models.Synchronization;
|
||||||
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.Services;
|
using Wino.Mail.WinUI.Services;
|
||||||
using Wino.Messaging.Client.Accounts;
|
using Wino.Messaging.Client.Accounts;
|
||||||
|
using Wino.Messaging.Client.Navigation;
|
||||||
using Wino.Messaging.Server;
|
using Wino.Messaging.Server;
|
||||||
using Wino.Messaging.UI;
|
using Wino.Messaging.UI;
|
||||||
using Wino.Services;
|
using Wino.Services;
|
||||||
@@ -36,13 +40,17 @@ namespace Wino.Mail.WinUI;
|
|||||||
|
|
||||||
public partial class App : WinoApplication,
|
public partial class App : WinoApplication,
|
||||||
IRecipient<NewMailSynchronizationRequested>,
|
IRecipient<NewMailSynchronizationRequested>,
|
||||||
IRecipient<NewCalendarSynchronizationRequested>
|
IRecipient<NewCalendarSynchronizationRequested>,
|
||||||
|
IRecipient<AccountCreatedMessage>,
|
||||||
|
IRecipient<AccountRemovedMessage>,
|
||||||
|
IRecipient<GetStartedFromWelcomeRequested>
|
||||||
{
|
{
|
||||||
private const int InboxSyncsPerFullSync = 20;
|
private const int InboxSyncsPerFullSync = 20;
|
||||||
private const string ToggleDefaultModeLaunchArgument = "--mode=toggle-default";
|
private const string ToggleDefaultModeLaunchArgument = "--mode=toggle-default";
|
||||||
private ISynchronizationManager? _synchronizationManager;
|
private ISynchronizationManager? _synchronizationManager;
|
||||||
private IPreferencesService? _preferencesService;
|
private IPreferencesService? _preferencesService;
|
||||||
private IAccountService? _accountService;
|
private IAccountService? _accountService;
|
||||||
|
private bool _windowManagerConfigured;
|
||||||
private CancellationTokenSource? _autoSynchronizationLoopCts;
|
private CancellationTokenSource? _autoSynchronizationLoopCts;
|
||||||
private readonly SemaphoreSlim _autoSynchronizationSemaphore = new(1, 1);
|
private readonly SemaphoreSlim _autoSynchronizationSemaphore = new(1, 1);
|
||||||
private readonly Dictionary<Guid, int> _inboxSyncCounters = [];
|
private readonly Dictionary<Guid, int> _inboxSyncCounters = [];
|
||||||
@@ -57,6 +65,52 @@ public partial class App : WinoApplication,
|
|||||||
RegisterRecipients();
|
RegisterRecipients();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void EnsureWindowManagerConfigured()
|
||||||
|
{
|
||||||
|
if (_windowManagerConfigured)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
||||||
|
windowManager.ActiveWindowChanged -= OnActiveWindowChanged;
|
||||||
|
windowManager.ActiveWindowChanged += OnActiveWindowChanged;
|
||||||
|
windowManager.WindowRemoved -= OnManagedWindowRemoved;
|
||||||
|
windowManager.WindowRemoved += OnManagedWindowRemoved;
|
||||||
|
|
||||||
|
var nativeAppService = Services.GetRequiredService<INativeAppService>();
|
||||||
|
nativeAppService.GetCoreWindowHwnd = () =>
|
||||||
|
{
|
||||||
|
var window = windowManager.ActiveWindow
|
||||||
|
?? windowManager.GetWindow(WinoWindowKind.Shell)
|
||||||
|
?? windowManager.GetWindow(WinoWindowKind.Welcome)
|
||||||
|
?? MainWindow;
|
||||||
|
|
||||||
|
return window == null
|
||||||
|
? IntPtr.Zero
|
||||||
|
: WinRT.Interop.WindowNative.GetWindowHandle(window);
|
||||||
|
};
|
||||||
|
|
||||||
|
_windowManagerConfigured = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnActiveWindowChanged(object? sender, WindowEx? window)
|
||||||
|
{
|
||||||
|
if (window == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
MainWindow = window;
|
||||||
|
InitializeNavigationDispatcher();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnManagedWindowRemoved(object? sender, WindowEx window)
|
||||||
|
{
|
||||||
|
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
||||||
|
MainWindow = windowManager.ActiveWindow
|
||||||
|
?? windowManager.GetWindow(WinoWindowKind.Shell)
|
||||||
|
?? windowManager.GetWindow(WinoWindowKind.Welcome);
|
||||||
|
|
||||||
|
InitializeNavigationDispatcher();
|
||||||
|
}
|
||||||
|
|
||||||
public bool IsNotificationActivation(out AppNotificationActivatedEventArgs args)
|
public bool IsNotificationActivation(out AppNotificationActivatedEventArgs args)
|
||||||
{
|
{
|
||||||
var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
||||||
@@ -92,6 +146,11 @@ public partial class App : WinoApplication,
|
|||||||
services.AddTransient(typeof(MailRenderingPageViewModel));
|
services.AddTransient(typeof(MailRenderingPageViewModel));
|
||||||
services.AddTransient(typeof(AccountManagementViewModel));
|
services.AddTransient(typeof(AccountManagementViewModel));
|
||||||
services.AddTransient(typeof(WelcomePageViewModel));
|
services.AddTransient(typeof(WelcomePageViewModel));
|
||||||
|
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));
|
||||||
@@ -152,9 +211,21 @@ public partial class App : WinoApplication,
|
|||||||
_synchronizationManager = Services.GetRequiredService<ISynchronizationManager>();
|
_synchronizationManager = Services.GetRequiredService<ISynchronizationManager>();
|
||||||
_preferencesService = Services.GetRequiredService<IPreferencesService>();
|
_preferencesService = Services.GetRequiredService<IPreferencesService>();
|
||||||
_accountService = Services.GetRequiredService<IAccountService>();
|
_accountService = Services.GetRequiredService<IAccountService>();
|
||||||
|
EnsureWindowManagerConfigured();
|
||||||
|
|
||||||
|
var hasAnyAccount = (await _accountService.GetAccountsAsync()).Any();
|
||||||
|
if (!IsStartupTaskLaunch() && !hasAnyAccount)
|
||||||
|
{
|
||||||
|
CreateWelcomeWindow();
|
||||||
|
await NewThemeService.InitializeAsync();
|
||||||
|
MainWindow?.Activate();
|
||||||
|
LogActivation("Welcome window created and activated.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_preferencesService.PreferenceChanged -= PreferencesServiceChanged;
|
_preferencesService.PreferenceChanged -= PreferencesServiceChanged;
|
||||||
_preferencesService.PreferenceChanged += PreferencesServiceChanged;
|
_preferencesService.PreferenceChanged += PreferencesServiceChanged;
|
||||||
|
|
||||||
RestartAutoSynchronizationLoop();
|
RestartAutoSynchronizationLoop();
|
||||||
|
|
||||||
// Check if launched from toast notification.
|
// Check if launched from toast notification.
|
||||||
@@ -180,11 +251,11 @@ public partial class App : WinoApplication,
|
|||||||
if (isStartupTaskLaunch)
|
if (isStartupTaskLaunch)
|
||||||
{
|
{
|
||||||
LogActivation("Launched by startup task. Window created but hidden (system tray only).");
|
LogActivation("Launched by startup task. Window created but hidden (system tray only).");
|
||||||
// Window is created but not activated. User can show it from system tray.
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Normal launch - show and activate the window.
|
// Normal launch - show and activate the window.
|
||||||
|
// The What's New dialog is shown from MailAppShellViewModel.OnNavigatedTo once XamlRoot is ready.
|
||||||
MainWindow?.Activate();
|
MainWindow?.Activate();
|
||||||
LogActivation("Window created and activated.");
|
LogActivation("Window created and activated.");
|
||||||
}
|
}
|
||||||
@@ -450,10 +521,19 @@ public partial class App : WinoApplication,
|
|||||||
// Initialize theme service after window is created.
|
// Initialize theme service after window is created.
|
||||||
await NewThemeService.InitializeAsync();
|
await NewThemeService.InitializeAsync();
|
||||||
|
|
||||||
MainWindow?.Activate();
|
if (MainWindow != null)
|
||||||
|
Services.GetRequiredService<IWinoWindowManager>().ActivateWindow(MainWindow);
|
||||||
|
|
||||||
LogActivation("Window created and activated.");
|
LogActivation("Window created and activated.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task OpenManageAccountsFromWelcomeAsync()
|
||||||
|
{
|
||||||
|
Services.GetRequiredService<INavigationService>()
|
||||||
|
.Navigate(WinoPage.ManageAccountsPage, null, NavigationReferenceFrame.ShellFrame, NavigationTransitionType.DrillIn);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates the main window without activating it.
|
/// Creates the main window without activating it.
|
||||||
/// Used for both normal launch and startup task launch (tray only).
|
/// Used for both normal launch and startup task launch (tray only).
|
||||||
@@ -462,15 +542,15 @@ public partial class App : WinoApplication,
|
|||||||
{
|
{
|
||||||
LogActivation("Creating main window.");
|
LogActivation("Creating main window.");
|
||||||
|
|
||||||
MainWindow = new ShellWindow();
|
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
||||||
|
MainWindow = windowManager.CreateWindow(WinoWindowKind.Shell, () => new ShellWindow());
|
||||||
InitializeNavigationDispatcher();
|
InitializeNavigationDispatcher();
|
||||||
|
|
||||||
var nativeAppService = Services.GetRequiredService<INativeAppService>();
|
|
||||||
nativeAppService.GetCoreWindowHwnd = () => WinRT.Interop.WindowNative.GetWindowHandle(MainWindow);
|
|
||||||
|
|
||||||
if (MainWindow is not IWinoShellWindow shellWindow)
|
if (MainWindow is not IWinoShellWindow shellWindow)
|
||||||
throw new ArgumentException("MainWindow must implement IWinoShellWindow");
|
throw new ArgumentException("MainWindow must implement IWinoShellWindow");
|
||||||
|
|
||||||
|
windowManager.SetPrimaryNavigationFrame(WinoWindowKind.Shell, shellWindow.GetMainFrame());
|
||||||
|
|
||||||
var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
||||||
|
|
||||||
if (activationArgs.Kind == ExtendedActivationKind.Launch &&
|
if (activationArgs.Kind == ExtendedActivationKind.Launch &&
|
||||||
@@ -496,6 +576,21 @@ public partial class App : WinoApplication,
|
|||||||
shellWindow.HandleAppActivation(args?.Arguments, GetCurrentLaunchTileId(), Environment.CommandLine);
|
shellWindow.HandleAppActivation(args?.Arguments, GetCurrentLaunchTileId(), Environment.CommandLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CreateWelcomeWindow()
|
||||||
|
{
|
||||||
|
LogActivation("Creating welcome window.");
|
||||||
|
|
||||||
|
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
||||||
|
MainWindow = windowManager.CreateWindow(WinoWindowKind.Welcome, () => new WelcomeWindow());
|
||||||
|
if (MainWindow is WelcomeWindow welcomeWindow)
|
||||||
|
windowManager.SetPrimaryNavigationFrame(WinoWindowKind.Welcome, welcomeWindow.GetRootFrame());
|
||||||
|
|
||||||
|
InitializeNavigationDispatcher();
|
||||||
|
|
||||||
|
Services.GetRequiredService<INavigationService>()
|
||||||
|
.Navigate(WinoPage.WelcomeHostPage, null, NavigationReferenceFrame.ShellFrame, NavigationTransitionType.None);
|
||||||
|
}
|
||||||
|
|
||||||
private void InitializeNavigationDispatcher()
|
private void InitializeNavigationDispatcher()
|
||||||
{
|
{
|
||||||
if (MainWindow == null)
|
if (MainWindow == null)
|
||||||
@@ -509,18 +604,26 @@ public partial class App : WinoApplication,
|
|||||||
|
|
||||||
private void EnsureMainWindowVisibleAndForeground()
|
private void EnsureMainWindowVisibleAndForeground()
|
||||||
{
|
{
|
||||||
if (MainWindow == null)
|
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
||||||
|
var currentWindow = windowManager.ActiveWindow
|
||||||
|
?? windowManager.GetWindow(WinoWindowKind.Shell)
|
||||||
|
?? windowManager.GetWindow(WinoWindowKind.Welcome)
|
||||||
|
?? MainWindow;
|
||||||
|
|
||||||
|
if (currentWindow == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
MainWindow.Show();
|
MainWindow = currentWindow;
|
||||||
MainWindow.BringToFront();
|
windowManager.ActivateWindow(currentWindow);
|
||||||
MainWindow.Activate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RegisterRecipients()
|
private void RegisterRecipients()
|
||||||
{
|
{
|
||||||
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<AccountRemovedMessage>(this);
|
||||||
|
WeakReferenceMessenger.Default.Register<GetStartedFromWelcomeRequested>(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void Receive(NewMailSynchronizationRequested message)
|
public async void Receive(NewMailSynchronizationRequested message)
|
||||||
@@ -573,6 +676,64 @@ public partial class App : WinoApplication,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Receive(AccountCreatedMessage message)
|
||||||
|
{
|
||||||
|
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
||||||
|
|
||||||
|
// Only transition when the account was created from the WelcomeWindow.
|
||||||
|
if (windowManager.GetWindow(WinoWindowKind.Welcome) == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
MainWindow?.DispatcherQueue?.TryEnqueue(async () =>
|
||||||
|
{
|
||||||
|
// Create and activate ShellWindow — ActiveWindowChanged fires and rebinds the dispatcher.
|
||||||
|
CreateWindow(null);
|
||||||
|
windowManager.HideWindow(WinoWindowKind.Welcome);
|
||||||
|
await NewThemeService.ApplyThemeToActiveWindowAsync();
|
||||||
|
MainWindow?.Activate();
|
||||||
|
RestartAutoSynchronizationLoop();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
||||||
|
|
||||||
|
if (windowManager.GetWindow(WinoWindowKind.Welcome) == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
MainWindow?.DispatcherQueue?.TryEnqueue(async () =>
|
||||||
|
{
|
||||||
|
CreateWindow(null);
|
||||||
|
windowManager.HideWindow(WinoWindowKind.Welcome);
|
||||||
|
await NewThemeService.ApplyThemeToActiveWindowAsync();
|
||||||
|
MainWindow?.Activate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetSynchronizationFailureMessage(MailSynchronizationType synchronizationType, string? exceptionMessage)
|
private static string GetSynchronizationFailureMessage(MailSynchronizationType synchronizationType, string? exceptionMessage)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(exceptionMessage))
|
if (!string.IsNullOrWhiteSpace(exceptionMessage))
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="cal-body" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#EFF6FF"/>
|
||||||
|
<stop offset="100%" stop-color="#DBEAFE"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="cal-header" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#60A5FA"/>
|
||||||
|
<stop offset="100%" stop-color="#2563EB"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="cal-today" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#818CF8"/>
|
||||||
|
<stop offset="100%" stop-color="#4F46E5"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="cal-shadow" x="-10%" y="-10%" width="120%" height="130%">
|
||||||
|
<feDropShadow dx="0" dy="4" stdDeviation="6" flood-color="#2563EB" flood-opacity="0.18"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<rect x="10" y="22" width="108" height="96" rx="14" fill="url(#cal-body)" filter="url(#cal-shadow)"/>
|
||||||
|
|
||||||
|
<!-- Header band -->
|
||||||
|
<rect x="10" y="22" width="108" height="34" rx="14" fill="url(#cal-header)"/>
|
||||||
|
<rect x="10" y="42" width="108" height="14" fill="url(#cal-header)"/>
|
||||||
|
|
||||||
|
<!-- Ring binders -->
|
||||||
|
<rect x="37" y="12" width="10" height="24" rx="5" fill="#93C5FD"/>
|
||||||
|
<rect x="81" y="12" width="10" height="24" rx="5" fill="#93C5FD"/>
|
||||||
|
<rect x="39" y="14" width="6" height="20" rx="3" fill="#1D4ED8"/>
|
||||||
|
<rect x="83" y="14" width="6" height="20" rx="3" fill="#1D4ED8"/>
|
||||||
|
|
||||||
|
<!-- Month label -->
|
||||||
|
<text x="64" y="43" text-anchor="middle" font-family="Segoe UI Variable, Segoe UI, sans-serif" font-size="13" font-weight="600" fill="white" opacity="0.95">MARCH</text>
|
||||||
|
|
||||||
|
<!-- Grid: row 1 -->
|
||||||
|
<rect x="18" y="64" width="16" height="14" rx="5" fill="white" opacity="0.55"/>
|
||||||
|
<rect x="40" y="64" width="16" height="14" rx="5" fill="white" opacity="0.55"/>
|
||||||
|
<rect x="62" y="64" width="16" height="14" rx="5" fill="white" opacity="0.55"/>
|
||||||
|
<rect x="84" y="64" width="16" height="14" rx="5" fill="white" opacity="0.55"/>
|
||||||
|
<rect x="106" y="64" width="12" height="14" rx="5" fill="white" opacity="0.55"/>
|
||||||
|
|
||||||
|
<!-- Grid: row 2 (today highlighted) -->
|
||||||
|
<rect x="18" y="84" width="16" height="14" rx="5" fill="white" opacity="0.55"/>
|
||||||
|
<rect x="40" y="84" width="16" height="14" rx="5" fill="url(#cal-today)"/>
|
||||||
|
<rect x="62" y="84" width="16" height="14" rx="5" fill="white" opacity="0.55"/>
|
||||||
|
<rect x="84" y="84" width="16" height="14" rx="5" fill="white" opacity="0.55"/>
|
||||||
|
<rect x="106" y="84" width="12" height="14" rx="5" fill="white" opacity="0.55"/>
|
||||||
|
|
||||||
|
<!-- Grid: row 3 (partial) -->
|
||||||
|
<rect x="18" y="104" width="16" height="10" rx="5" fill="white" opacity="0.35"/>
|
||||||
|
<rect x="40" y="104" width="16" height="10" rx="5" fill="white" opacity="0.35"/>
|
||||||
|
<rect x="62" y="104" width="16" height="10" rx="5" fill="white" opacity="0.35"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,49 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="cu-palette" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#FDF4FF"/>
|
||||||
|
<stop offset="100%" stop-color="#F3E8FF"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="cu-handle" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#A78BFA"/>
|
||||||
|
<stop offset="100%" stop-color="#7C3AED"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="cu-shadow" x="-10%" y="-10%" width="130%" height="130%">
|
||||||
|
<feDropShadow dx="0" dy="4" stdDeviation="7" flood-color="#7C3AED" flood-opacity="0.2"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Palette body -->
|
||||||
|
<path d="M64 12 C38 12 18 30 18 54 C18 68 26 76 36 80 C46 84 46 92 50 98 C54 104 60 108 68 106 C82 104 110 88 110 60 C110 34 90 12 64 12 Z"
|
||||||
|
fill="url(#cu-palette)" filter="url(#cu-shadow)"/>
|
||||||
|
|
||||||
|
<!-- Palette thumb hole -->
|
||||||
|
<circle cx="78" cy="96" r="10" fill="#E9D5FF"/>
|
||||||
|
<circle cx="78" cy="96" r="6" fill="white" opacity="0.6"/>
|
||||||
|
|
||||||
|
<!-- Color swatches -->
|
||||||
|
<!-- Red -->
|
||||||
|
<circle cx="38" cy="44" r="11" fill="#F87171"/>
|
||||||
|
<circle cx="38" cy="44" r="11" fill="white" opacity="0.15"/>
|
||||||
|
|
||||||
|
<!-- Orange -->
|
||||||
|
<circle cx="62" cy="28" r="11" fill="#FB923C"/>
|
||||||
|
<circle cx="62" cy="28" r="11" fill="white" opacity="0.15"/>
|
||||||
|
|
||||||
|
<!-- Blue -->
|
||||||
|
<circle cx="87" cy="34" r="11" fill="#60A5FA"/>
|
||||||
|
<circle cx="87" cy="34" r="11" fill="white" opacity="0.15"/>
|
||||||
|
|
||||||
|
<!-- Green -->
|
||||||
|
<circle cx="100" cy="56" r="11" fill="#4ADE80"/>
|
||||||
|
<circle cx="100" cy="56" r="11" fill="white" opacity="0.15"/>
|
||||||
|
|
||||||
|
<!-- Purple -->
|
||||||
|
<circle cx="92" cy="80" r="11" fill="#A78BFA"/>
|
||||||
|
<circle cx="92" cy="80" r="11" fill="white" opacity="0.15"/>
|
||||||
|
|
||||||
|
<!-- Brush -->
|
||||||
|
<rect x="22" y="72" width="8" height="36" rx="4" fill="url(#cu-handle)" transform="rotate(-40 26 90)"/>
|
||||||
|
<path d="M8 110 C8 110 14 106 18 112 C16 116 10 116 8 110 Z" fill="#F87171"/>
|
||||||
|
<path d="M10 112 C10 112 14 109 17 113" fill="none" stroke="#FCA5A5" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,51 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="ml-env" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#F0F9FF"/>
|
||||||
|
<stop offset="100%" stop-color="#BAE6FD"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="ml-flap" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#38BDF8"/>
|
||||||
|
<stop offset="100%" stop-color="#0284C7"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="ml-dot1" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#4ADE80"/>
|
||||||
|
<stop offset="100%" stop-color="#16A34A"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="ml-dot2" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#F87171"/>
|
||||||
|
<stop offset="100%" stop-color="#DC2626"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="ml-dot3" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#A78BFA"/>
|
||||||
|
<stop offset="100%" stop-color="#7C3AED"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="ml-shadow" x="-10%" y="-10%" width="130%" height="130%">
|
||||||
|
<feDropShadow dx="0" dy="4" stdDeviation="7" flood-color="#0284C7" flood-opacity="0.2"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Envelope body -->
|
||||||
|
<rect x="14" y="30" width="100" height="70" rx="12" fill="url(#ml-env)" filter="url(#ml-shadow)"/>
|
||||||
|
|
||||||
|
<!-- Envelope flap (closed) -->
|
||||||
|
<path d="M14 42 L64 68 L114 42 L114 38 A12 12 0 0 0 102 26 L26 26 A12 12 0 0 0 14 38 Z"
|
||||||
|
fill="url(#ml-flap)"/>
|
||||||
|
|
||||||
|
<!-- Fold line on front -->
|
||||||
|
<path d="M14 46 L64 71 L114 46" fill="none" stroke="#7DD3FC" stroke-width="1.2" opacity="0.6"/>
|
||||||
|
|
||||||
|
<!-- Body content lines -->
|
||||||
|
<rect x="28" y="76" width="50" height="5" rx="2.5" fill="#0EA5E9" opacity="0.3"/>
|
||||||
|
<rect x="28" y="86" width="36" height="5" rx="2.5" fill="#0EA5E9" opacity="0.2"/>
|
||||||
|
|
||||||
|
<!-- Provider dots (Outlook=blue, Gmail=red, IMAP=purple) -->
|
||||||
|
<circle cx="90" cy="81" r="11" fill="url(#ml-dot1)" stroke="white" stroke-width="2"/>
|
||||||
|
<text x="90" y="86" text-anchor="middle" font-family="Segoe UI Variable, Segoe UI, sans-serif" font-size="11" font-weight="700" fill="white">G</text>
|
||||||
|
|
||||||
|
<circle cx="90" cy="81" r="11" fill="url(#ml-dot2)" stroke="white" stroke-width="2" transform="translate(-26,0)"/>
|
||||||
|
<text x="64" y="86" text-anchor="middle" font-family="Segoe UI Variable, Segoe UI, sans-serif" font-size="10" font-weight="700" fill="white">O</text>
|
||||||
|
|
||||||
|
<circle cx="90" cy="81" r="11" fill="url(#ml-dot3)" stroke="white" stroke-width="2" transform="translate(-52,0)"/>
|
||||||
|
<text x="38" y="86" text-anchor="middle" font-family="Segoe UI Variable, Segoe UI, sans-serif" font-size="10" font-weight="700" fill="white">@</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,48 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="mo-star1" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#FCD34D"/>
|
||||||
|
<stop offset="100%" stop-color="#F59E0B"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="mo-star2" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#93C5FD"/>
|
||||||
|
<stop offset="100%" stop-color="#3B82F6"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="mo-star3" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#C4B5FD"/>
|
||||||
|
<stop offset="100%" stop-color="#7C3AED"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="mo-glow1" x="-30%" y="-30%" width="160%" height="160%">
|
||||||
|
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#F59E0B" flood-opacity="0.4"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="mo-glow2" x="-30%" y="-30%" width="160%" height="160%">
|
||||||
|
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#3B82F6" flood-opacity="0.35"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="mo-glow3" x="-30%" y="-30%" width="160%" height="160%">
|
||||||
|
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#7C3AED" flood-opacity="0.35"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Large center star -->
|
||||||
|
<polygon points="64,14 70,50 106,56 70,62 64,98 58,62 22,56 58,50"
|
||||||
|
fill="url(#mo-star1)" filter="url(#mo-glow1)"/>
|
||||||
|
|
||||||
|
<!-- Star highlight -->
|
||||||
|
<polygon points="64,20 68.5,48 92,52 68.5,56 64,84 59.5,56 36,52 59.5,48"
|
||||||
|
fill="white" opacity="0.18"/>
|
||||||
|
|
||||||
|
<!-- Small star top-right -->
|
||||||
|
<polygon points="98,18 100.5,28 110,30 100.5,32 98,42 95.5,32 86,30 95.5,28"
|
||||||
|
fill="url(#mo-star2)" filter="url(#mo-glow2)"/>
|
||||||
|
|
||||||
|
<!-- Small star bottom-left -->
|
||||||
|
<polygon points="28,82 30,90 38,92 30,94 28,102 26,94 18,92 26,90"
|
||||||
|
fill="url(#mo-star3)" filter="url(#mo-glow3)"/>
|
||||||
|
|
||||||
|
<!-- Tiny sparkle dots -->
|
||||||
|
<circle cx="104" cy="66" r="3" fill="#FCD34D" opacity="0.8"/>
|
||||||
|
<circle cx="22" cy="44" r="2.5" fill="#93C5FD" opacity="0.8"/>
|
||||||
|
<circle cx="110" cy="90" r="2" fill="#C4B5FD" opacity="0.7"/>
|
||||||
|
<circle cx="18" cy="106" r="2" fill="#FCD34D" opacity="0.6"/>
|
||||||
|
<circle cx="64" cy="108" r="2.5" fill="#93C5FD" opacity="0.6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,45 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="nb-bell" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#FCD34D"/>
|
||||||
|
<stop offset="100%" stop-color="#D97706"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="nb-badge" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#F87171"/>
|
||||||
|
<stop offset="100%" stop-color="#DC2626"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="nb-action" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#60A5FA"/>
|
||||||
|
<stop offset="100%" stop-color="#2563EB"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="nb-glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
|
<feDropShadow dx="0" dy="4" stdDeviation="7" flood-color="#D97706" flood-opacity="0.3"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Bell body -->
|
||||||
|
<path d="M64 14 C64 14 40 18 38 52 L34 80 L94 80 L90 52 C88 18 64 14 64 14 Z"
|
||||||
|
fill="url(#nb-bell)" filter="url(#nb-glow)"/>
|
||||||
|
|
||||||
|
<!-- Bell highlight -->
|
||||||
|
<path d="M64 18 C64 18 52 22 50 46 L48 64 L72 64 C72 46 66 24 64 18 Z"
|
||||||
|
fill="white" opacity="0.2"/>
|
||||||
|
|
||||||
|
<!-- Bell base / clapper mount -->
|
||||||
|
<rect x="30" y="78" width="68" height="10" rx="5" fill="#B45309"/>
|
||||||
|
|
||||||
|
<!-- Clapper -->
|
||||||
|
<circle cx="64" cy="100" r="8" fill="#92400E"/>
|
||||||
|
<rect x="61" y="88" width="6" height="14" rx="3" fill="#B45309"/>
|
||||||
|
|
||||||
|
<!-- Notification badge -->
|
||||||
|
<circle cx="92" cy="26" r="13" fill="url(#nb-badge)"/>
|
||||||
|
<text x="92" y="31" text-anchor="middle" font-family="Segoe UI Variable, Segoe UI, sans-serif" font-size="13" font-weight="700" fill="white">3</text>
|
||||||
|
|
||||||
|
<!-- Action buttons (toast-like) -->
|
||||||
|
<rect x="20" y="98" width="38" height="18" rx="7" fill="url(#nb-action)"/>
|
||||||
|
<text x="39" y="111" text-anchor="middle" font-family="Segoe UI Variable, Segoe UI, sans-serif" font-size="9" font-weight="600" fill="white">Archive</text>
|
||||||
|
|
||||||
|
<rect x="64" y="98" width="38" height="18" rx="7" fill="white" stroke="#E5E7EB" stroke-width="1"/>
|
||||||
|
<text x="83" y="111" text-anchor="middle" font-family="Segoe UI Variable, Segoe UI, sans-serif" font-size="9" font-weight="600" fill="#374151">Delete</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,43 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="sh-outer" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#6EE7B7"/>
|
||||||
|
<stop offset="100%" stop-color="#059669"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="sh-inner" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#A7F3D0" stop-opacity="0.9"/>
|
||||||
|
<stop offset="100%" stop-color="#34D399" stop-opacity="0.6"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="sh-lock" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#ECFDF5"/>
|
||||||
|
<stop offset="100%" stop-color="#D1FAE5"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="sh-glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
|
<feDropShadow dx="0" dy="4" stdDeviation="8" flood-color="#059669" flood-opacity="0.25"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Shield outer -->
|
||||||
|
<path d="M64 10 L106 28 L106 68 C106 92 86 112 64 118 C42 112 22 92 22 68 L22 28 Z"
|
||||||
|
fill="url(#sh-outer)" filter="url(#sh-glow)"/>
|
||||||
|
|
||||||
|
<!-- Shield inner highlight -->
|
||||||
|
<path d="M64 18 L100 34 L100 68 C100 88 83 106 64 112 C45 106 28 88 28 68 L28 34 Z"
|
||||||
|
fill="url(#sh-inner)" opacity="0.6"/>
|
||||||
|
|
||||||
|
<!-- Lock body -->
|
||||||
|
<rect x="50" y="62" width="28" height="22" rx="6" fill="url(#sh-lock)"/>
|
||||||
|
|
||||||
|
<!-- Lock shackle -->
|
||||||
|
<path d="M56 62 L56 54 A8 8 0 0 1 72 54 L72 62"
|
||||||
|
fill="none" stroke="white" stroke-width="5" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<!-- Keyhole -->
|
||||||
|
<circle cx="64" cy="71" r="4" fill="#059669"/>
|
||||||
|
<rect x="62" y="72" width="4" height="7" rx="2" fill="#059669"/>
|
||||||
|
|
||||||
|
<!-- Check sparkles -->
|
||||||
|
<circle cx="94" cy="28" r="3" fill="white" opacity="0.7"/>
|
||||||
|
<circle cx="106" cy="44" r="2" fill="white" opacity="0.5"/>
|
||||||
|
<circle cx="34" cy="44" r="2" fill="white" opacity="0.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,49 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="th-env3" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#E0E7FF"/>
|
||||||
|
<stop offset="100%" stop-color="#C7D2FE"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="th-env2" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#EDE9FE"/>
|
||||||
|
<stop offset="100%" stop-color="#DDD6FE"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="th-env1" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#818CF8"/>
|
||||||
|
<stop offset="100%" stop-color="#4F46E5"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="th-flap" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#6366F1"/>
|
||||||
|
<stop offset="100%" stop-color="#4338CA"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="th-shadow" x="-10%" y="-10%" width="130%" height="130%">
|
||||||
|
<feDropShadow dx="0" dy="4" stdDeviation="6" flood-color="#4F46E5" flood-opacity="0.2"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Back envelope (bottom-most) -->
|
||||||
|
<rect x="18" y="38" width="80" height="52" rx="8" fill="url(#th-env3)" opacity="0.8"/>
|
||||||
|
<path d="M18 46 L58 66 L98 46" fill="none" stroke="#A5B4FC" stroke-width="1.5" stroke-linejoin="round"/>
|
||||||
|
|
||||||
|
<!-- Middle envelope -->
|
||||||
|
<rect x="24" y="30" width="80" height="52" rx="8" fill="url(#th-env2)" opacity="0.9"/>
|
||||||
|
<path d="M24 38 L64 58 L104 38" fill="none" stroke="#C4B5FD" stroke-width="1.5" stroke-linejoin="round"/>
|
||||||
|
|
||||||
|
<!-- Front envelope -->
|
||||||
|
<rect x="30" y="22" width="80" height="52" rx="10" fill="url(#th-env1)" filter="url(#th-shadow)"/>
|
||||||
|
|
||||||
|
<!-- Front flap (triangle) -->
|
||||||
|
<path d="M30 32 L70 52 L110 32 L110 30 A10 10 0 0 0 100 22 L40 22 A10 10 0 0 0 30 30 Z"
|
||||||
|
fill="url(#th-flap)"/>
|
||||||
|
|
||||||
|
<!-- Dividing line on front -->
|
||||||
|
<path d="M30 38 L70 57 L110 38" fill="none" stroke="#818CF8" stroke-width="1" opacity="0.5"/>
|
||||||
|
|
||||||
|
<!-- Lines in body -->
|
||||||
|
<rect x="42" y="56" width="40" height="4" rx="2" fill="white" opacity="0.35"/>
|
||||||
|
<rect x="42" y="64" width="28" height="4" rx="2" fill="white" opacity="0.25"/>
|
||||||
|
|
||||||
|
<!-- Reply indicator dot -->
|
||||||
|
<circle cx="100" cy="84" r="12" fill="#4F46E5"/>
|
||||||
|
<path d="M95 84 L101 78 M101 78 L101 90 M101 78 L107 84" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,44 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"title": "# Outlook, Gmail & IMAP/SMTP",
|
||||||
|
"description": "Connect all your email accounts in one place. Wino supports Microsoft Outlook (Office 365), Gmail, and any standard IMAP/SMTP server — giving you a unified inbox experience.",
|
||||||
|
"imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Mail.svg",
|
||||||
|
"imageWidth": 128,
|
||||||
|
"imageHeight": 128
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "# Calendar with CalDAV Support",
|
||||||
|
"description": "Manage your schedule alongside your emails. Create local calendars or connect to remote CalDAV-compatible servers. View, create, and respond to events and invitations.",
|
||||||
|
"imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Calendar.svg",
|
||||||
|
"imageWidth": 128,
|
||||||
|
"imageHeight": 128
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "# S/MIME Signing & Encryption",
|
||||||
|
"description": "Keep your communications secure. Sign and encrypt emails with personal certificates to ensure your messages are authentic and private.",
|
||||||
|
"imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Security.svg",
|
||||||
|
"imageWidth": 128,
|
||||||
|
"imageHeight": 128
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "# Conversation Threading",
|
||||||
|
"description": "Follow discussions with ease. Emails are grouped by conversation thread so you never lose context in long email chains.",
|
||||||
|
"imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Thread.svg",
|
||||||
|
"imageWidth": 128,
|
||||||
|
"imageHeight": 128
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "# Actionable Notifications",
|
||||||
|
"description": "Stay productive without switching apps. Mark as read, delete, or archive emails directly from Windows toast notifications.",
|
||||||
|
"imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Notification.svg",
|
||||||
|
"imageWidth": 128,
|
||||||
|
"imageHeight": 128
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "# Rich Customization",
|
||||||
|
"description": "Make Wino yours. Choose from built-in themes, create custom themes with your own wallpapers and accent colors, configure swipe actions, and set keyboard shortcuts.",
|
||||||
|
"imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Customize.svg",
|
||||||
|
"imageWidth": 128,
|
||||||
|
"imageHeight": 128
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"title": "# Wino Calendar is here!",
|
||||||
|
"description": "You can now create local or remote CalDAV-compatible calendars, manage recurring events, and respond to invitations — all from within Wino.",
|
||||||
|
"imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Calendar.svg",
|
||||||
|
"imageWidth": 128,
|
||||||
|
"imageHeight": 128
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "# S/MIME Signing & Encryption",
|
||||||
|
"description": "Wino now supports signing and encrypting your emails with personal certificates. Keep your communications secure and verifiable.",
|
||||||
|
"imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Security.svg",
|
||||||
|
"imageWidth": 128,
|
||||||
|
"imageHeight": 128
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "# Threaded Mail View",
|
||||||
|
"description": "Emails are now grouped by conversation, making it easier to follow long discussions without losing context.",
|
||||||
|
"imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Thread.svg",
|
||||||
|
"imageWidth": 128,
|
||||||
|
"imageHeight": 128
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "# Smarter Notifications",
|
||||||
|
"description": "Act on your emails directly from toast notifications — mark as read, delete, or archive without opening the app.",
|
||||||
|
"imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Notification.svg",
|
||||||
|
"imageWidth": 128,
|
||||||
|
"imageHeight": 128
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "# And much more...",
|
||||||
|
"description": "Folder management, swipe actions, keyboard shortcuts, a custom print dialog, and significant performance improvements are all included in this release.\n\nThank you for using Wino Mail!",
|
||||||
|
"imageUrl": "ms-appx:///Assets/UpdateNotes/Images/More.svg",
|
||||||
|
"imageWidth": 128,
|
||||||
|
"imageHeight": 128
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<UserControl
|
||||||
|
x:Class="Wino.Mail.WinUI.Controls.UpdateNotesFlipViewControl"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:models="using:Wino.Core.Domain.Models.Updates"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<Grid RowSpacing="12">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<FlipView
|
||||||
|
x:Name="UpdateFlipView"
|
||||||
|
ItemsSource="{x:Bind Sections, Mode=OneWay}"
|
||||||
|
SelectionChanged="OnFlipViewSelectionChanged">
|
||||||
|
<FlipView.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="models:UpdateNoteSection">
|
||||||
|
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel Padding="8" Spacing="12">
|
||||||
|
<controls:MarkdownTextBlock
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Text="{x:Bind Title, Mode=OneTime}" />
|
||||||
|
<Image
|
||||||
|
Width="{x:Bind ActualImageWidth, Mode=OneTime}"
|
||||||
|
Height="{x:Bind ActualImageHeight, Mode=OneTime}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Source="{x:Bind ImageUrl, Mode=OneTime}"
|
||||||
|
Stretch="Uniform" />
|
||||||
|
<controls:MarkdownTextBlock
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Text="{x:Bind Description, Mode=OneTime}" />
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</DataTemplate>
|
||||||
|
</FlipView.ItemTemplate>
|
||||||
|
</FlipView>
|
||||||
|
|
||||||
|
<PipsPager
|
||||||
|
x:Name="FlipViewPager"
|
||||||
|
Grid.Row="1"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
SelectedPageIndex="0"
|
||||||
|
SelectedIndexChanged="OnPipsPagerSelectedIndexChanged" />
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Wino.Core.Domain.Models.Updates;
|
||||||
|
|
||||||
|
namespace Wino.Mail.WinUI.Controls;
|
||||||
|
|
||||||
|
public sealed partial class UpdateNotesFlipViewControl : UserControl
|
||||||
|
{
|
||||||
|
public event EventHandler<int>? SelectedIndexChanged;
|
||||||
|
|
||||||
|
public int SelectedIndex => UpdateFlipView.SelectedIndex;
|
||||||
|
|
||||||
|
public IList<UpdateNoteSection>? Sections
|
||||||
|
{
|
||||||
|
get { return (IList<UpdateNoteSection>?)GetValue(SectionsProperty); }
|
||||||
|
set { SetValue(SectionsProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly DependencyProperty SectionsProperty =
|
||||||
|
DependencyProperty.Register(nameof(Sections),
|
||||||
|
typeof(IList<UpdateNoteSection>),
|
||||||
|
typeof(UpdateNotesFlipViewControl),
|
||||||
|
new PropertyMetadata(null, OnSectionsChanged));
|
||||||
|
|
||||||
|
public UpdateNotesFlipViewControl()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnSectionsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (d is UpdateNotesFlipViewControl control)
|
||||||
|
control.UpdatePager();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFlipViewSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
FlipViewPager.SelectedPageIndex = UpdateFlipView.SelectedIndex;
|
||||||
|
SelectedIndexChanged?.Invoke(this, UpdateFlipView.SelectedIndex);
|
||||||
|
UpdatePager();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPipsPagerSelectedIndexChanged(PipsPager sender, PipsPagerSelectedIndexChangedEventArgs args)
|
||||||
|
{
|
||||||
|
UpdateFlipView.SelectedIndex = sender.SelectedPageIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdatePager()
|
||||||
|
{
|
||||||
|
FlipViewPager.NumberOfPages = Sections?.Count ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ public static class CoreUWPContainerSetup
|
|||||||
services.AddSingleton<IDispatcher>(provider => provider.GetRequiredService<WinUIDispatcher>());
|
services.AddSingleton<IDispatcher>(provider => provider.GetRequiredService<WinUIDispatcher>());
|
||||||
|
|
||||||
services.AddSingleton<IUnderlyingThemeService, UnderlyingThemeService>();
|
services.AddSingleton<IUnderlyingThemeService, UnderlyingThemeService>();
|
||||||
|
services.AddSingleton<IWinoWindowManager, WinoWindowManager>();
|
||||||
services.AddSingleton<INativeAppService, NativeAppService>();
|
services.AddSingleton<INativeAppService, NativeAppService>();
|
||||||
services.AddSingleton<IStoreManagementService, StoreManagementService>();
|
services.AddSingleton<IStoreManagementService, StoreManagementService>();
|
||||||
services.AddSingleton<IPreferencesService, PreferencesService>();
|
services.AddSingleton<IPreferencesService, PreferencesService>();
|
||||||
@@ -48,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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<ContentDialog
|
||||||
|
x:Class="Wino.Dialogs.WhatIsNewDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:controls="using:Wino.Mail.WinUI.Controls"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:domain="using:Wino.Core.Domain"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
HorizontalContentAlignment="Stretch"
|
||||||
|
VerticalContentAlignment="Stretch"
|
||||||
|
Style="{StaticResource WinoDialogStyle}"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<ContentDialog.Resources>
|
||||||
|
<x:Double x:Key="ContentDialogMinWidth">480</x:Double>
|
||||||
|
<x:Double x:Key="ContentDialogMaxWidth">560</x:Double>
|
||||||
|
<x:Double x:Key="ContentDialogMinHeight">480</x:Double>
|
||||||
|
<x:Double x:Key="ContentDialogMaxHeight">700</x:Double>
|
||||||
|
</ContentDialog.Resources>
|
||||||
|
|
||||||
|
<Grid RowSpacing="12">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<controls:UpdateNotesFlipViewControl x:Name="UpdateNotesControl" Sections="{x:Bind Sections, Mode=OneTime}" />
|
||||||
|
|
||||||
|
<StackPanel
|
||||||
|
Grid.Row="1"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="8">
|
||||||
|
<Button
|
||||||
|
x:Name="GetStartedButton"
|
||||||
|
Click="OnGetStartedClicked"
|
||||||
|
Content="{x:Bind domain:Translator.WhatIsNew_GetStartedButton}"
|
||||||
|
Visibility="Collapsed" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</ContentDialog>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Core.Domain.Models.Updates;
|
||||||
|
|
||||||
|
namespace Wino.Dialogs;
|
||||||
|
|
||||||
|
public sealed partial class WhatIsNewDialog : ContentDialog
|
||||||
|
{
|
||||||
|
private readonly IUpdateManager _updateManager;
|
||||||
|
|
||||||
|
public List<UpdateNoteSection> Sections { get; }
|
||||||
|
|
||||||
|
private bool _canClose = false;
|
||||||
|
|
||||||
|
public WhatIsNewDialog(UpdateNotes notes, IUpdateManager updateManager)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
_updateManager = updateManager;
|
||||||
|
Sections = notes.Sections;
|
||||||
|
|
||||||
|
// Show the Get Started button immediately when there is only one page.
|
||||||
|
UpdateNotesControl.SelectedIndexChanged += OnUpdateSectionChanged;
|
||||||
|
UpdateGetStartedButtonVisibility(UpdateNotesControl.SelectedIndex);
|
||||||
|
Closing += OnDialogClosing;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUpdateSectionChanged(object? sender, int selectedIndex)
|
||||||
|
=> UpdateGetStartedButtonVisibility(selectedIndex);
|
||||||
|
|
||||||
|
private void UpdateGetStartedButtonVisibility(int selectedIndex)
|
||||||
|
{
|
||||||
|
GetStartedButton.Visibility = selectedIndex == Sections.Count - 1
|
||||||
|
? Visibility.Visible
|
||||||
|
: Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDialogClosing(ContentDialog sender, ContentDialogClosingEventArgs args)
|
||||||
|
{
|
||||||
|
// Only allow closing when Get Started button was clicked.
|
||||||
|
if (!_canClose)
|
||||||
|
args.Cancel = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGetStartedClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
GetStartedButton.IsEnabled = false;
|
||||||
|
_updateManager.MarkUpdateNotesAsSeen();
|
||||||
|
_canClose = true;
|
||||||
|
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;
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using WinUIEx;
|
||||||
|
using Wino.Mail.WinUI.Models;
|
||||||
|
|
||||||
|
namespace Wino.Mail.WinUI.Interfaces;
|
||||||
|
|
||||||
|
public interface IWinoWindowManager
|
||||||
|
{
|
||||||
|
event EventHandler<WindowEx?> ActiveWindowChanged;
|
||||||
|
event EventHandler<WindowEx> WindowRemoved;
|
||||||
|
|
||||||
|
WindowEx? ActiveWindow { get; }
|
||||||
|
WindowEx CreateWindow(WinoWindowKind kind, Func<WindowEx> factory, string? name = null);
|
||||||
|
WindowEx? GetWindow(WinoWindowKind kind, string? name = null);
|
||||||
|
WindowEx? GetWindow(string name);
|
||||||
|
void ActivateWindow(WindowEx window);
|
||||||
|
bool ActivateWindow(WinoWindowKind kind, string? name = null);
|
||||||
|
void HideWindow(WindowEx window);
|
||||||
|
bool HideWindow(WinoWindowKind kind, string? name = null);
|
||||||
|
void SetPrimaryNavigationFrame(WinoWindowKind kind, Frame frame, string? name = null);
|
||||||
|
Frame? GetPrimaryNavigationFrame(WinoWindowKind kind, string? name = null);
|
||||||
|
void CloseAllWindows();
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Wino.Mail.WinUI.Models;
|
||||||
|
|
||||||
|
public enum WinoWindowKind
|
||||||
|
{
|
||||||
|
Shell,
|
||||||
|
Welcome
|
||||||
|
}
|
||||||
@@ -27,9 +27,9 @@ public class DialogService : DialogServiceBase, IMailDialogService
|
|||||||
{
|
{
|
||||||
public DialogService(INewThemeService themeService,
|
public DialogService(INewThemeService themeService,
|
||||||
IConfigurationService configurationService,
|
IConfigurationService configurationService,
|
||||||
IApplicationResourceManager<ResourceDictionary> applicationResourceManager) : base(themeService, configurationService, applicationResourceManager)
|
IApplicationResourceManager<ResourceDictionary> applicationResourceManager,
|
||||||
|
IUpdateManager updateManager) : base(themeService, configurationService, applicationResourceManager, updateManager)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ICreateAccountAliasDialog> ShowCreateAccountAliasDialogAsync()
|
public async Task<ICreateAccountAliasDialog> ShowCreateAccountAliasDialogAsync()
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ using Wino.Core.Domain.Interfaces;
|
|||||||
using Wino.Core.Domain.Models.Accounts;
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
using Wino.Core.Domain.Models.Common;
|
using Wino.Core.Domain.Models.Common;
|
||||||
using Wino.Core.Domain.Models.Printing;
|
using Wino.Core.Domain.Models.Printing;
|
||||||
|
using Wino.Core.Domain.Models.Updates;
|
||||||
using Wino.Dialogs;
|
using Wino.Dialogs;
|
||||||
using Wino.Mail.WinUI.Dialogs;
|
using Wino.Mail.WinUI.Dialogs;
|
||||||
using Wino.Mail.WinUI.Extensions;
|
using Wino.Mail.WinUI.Extensions;
|
||||||
@@ -30,14 +31,16 @@ public class DialogServiceBase : IDialogServiceBase
|
|||||||
|
|
||||||
protected INewThemeService ThemeService { get; }
|
protected INewThemeService ThemeService { get; }
|
||||||
protected IConfigurationService ConfigurationService { get; }
|
protected IConfigurationService ConfigurationService { get; }
|
||||||
|
protected IUpdateManager UpdateManager { get; }
|
||||||
|
|
||||||
protected IApplicationResourceManager<ResourceDictionary> ApplicationResourceManager { get; }
|
protected IApplicationResourceManager<ResourceDictionary> ApplicationResourceManager { get; }
|
||||||
|
|
||||||
public DialogServiceBase(INewThemeService themeService, IConfigurationService configurationService, IApplicationResourceManager<ResourceDictionary> applicationResourceManager)
|
public DialogServiceBase(INewThemeService themeService, IConfigurationService configurationService, IApplicationResourceManager<ResourceDictionary> applicationResourceManager, IUpdateManager updateManager)
|
||||||
{
|
{
|
||||||
ThemeService = themeService;
|
ThemeService = themeService;
|
||||||
ConfigurationService = configurationService;
|
ConfigurationService = configurationService;
|
||||||
ApplicationResourceManager = applicationResourceManager;
|
ApplicationResourceManager = applicationResourceManager;
|
||||||
|
UpdateManager = updateManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected XamlRoot? GetXamlRoot()
|
protected XamlRoot? GetXamlRoot()
|
||||||
@@ -355,4 +358,14 @@ public class DialogServiceBase : IDialogServiceBase
|
|||||||
return null!;
|
return null!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task ShowWhatIsNewDialogAsync(UpdateNotes notes)
|
||||||
|
{
|
||||||
|
var dialog = new WhatIsNewDialog(notes, UpdateManager)
|
||||||
|
{
|
||||||
|
RequestedTheme = ThemeService.RootTheme.ToWindowsElementTheme()
|
||||||
|
};
|
||||||
|
|
||||||
|
await HandleDialogPresentationAsync(dialog);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ using Wino.Mail.ViewModels.Data;
|
|||||||
using Wino.Mail.ViewModels.Messages;
|
using Wino.Mail.ViewModels.Messages;
|
||||||
using Wino.Mail.WinUI;
|
using Wino.Mail.WinUI;
|
||||||
using Wino.Mail.WinUI.Interfaces;
|
using Wino.Mail.WinUI.Interfaces;
|
||||||
|
using Wino.Mail.WinUI.Models;
|
||||||
using Wino.Mail.WinUI.Services;
|
using Wino.Mail.WinUI.Services;
|
||||||
using Wino.Mail.WinUI.Views.Calendar;
|
using Wino.Mail.WinUI.Views.Calendar;
|
||||||
using Wino.Messaging.Client.Mails;
|
using Wino.Messaging.Client.Mails;
|
||||||
@@ -29,6 +30,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
|||||||
{
|
{
|
||||||
private readonly IStatePersistanceService _statePersistanceService;
|
private readonly IStatePersistanceService _statePersistanceService;
|
||||||
private readonly IDispatcher _dispatcher;
|
private readonly IDispatcher _dispatcher;
|
||||||
|
private readonly IWinoWindowManager _windowManager;
|
||||||
|
|
||||||
private WinoPage[] _renderingPageTypes = new WinoPage[]
|
private WinoPage[] _renderingPageTypes = new WinoPage[]
|
||||||
{
|
{
|
||||||
@@ -42,7 +44,12 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
|||||||
WinoPage.MailRenderingPage,
|
WinoPage.MailRenderingPage,
|
||||||
WinoPage.ComposePage,
|
WinoPage.ComposePage,
|
||||||
WinoPage.IdlePage,
|
WinoPage.IdlePage,
|
||||||
WinoPage.WelcomePage
|
WinoPage.WelcomePage,
|
||||||
|
WinoPage.WelcomePageV2,
|
||||||
|
WinoPage.WelcomeHostPage,
|
||||||
|
WinoPage.ProviderSelectionPage,
|
||||||
|
WinoPage.AccountSetupProgressPage,
|
||||||
|
WinoPage.SpecialImapCredentialsPage
|
||||||
];
|
];
|
||||||
|
|
||||||
private static readonly WinoPage[] CalendarOnlyPages =
|
private static readonly WinoPage[] CalendarOnlyPages =
|
||||||
@@ -51,10 +58,11 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
|||||||
WinoPage.EventDetailsPage
|
WinoPage.EventDetailsPage
|
||||||
];
|
];
|
||||||
|
|
||||||
public NavigationService(IStatePersistanceService statePersistanceService, IDispatcher dispatcher)
|
public NavigationService(IStatePersistanceService statePersistanceService, IDispatcher dispatcher, IWinoWindowManager windowManager)
|
||||||
{
|
{
|
||||||
_statePersistanceService = statePersistanceService;
|
_statePersistanceService = statePersistanceService;
|
||||||
_dispatcher = dispatcher;
|
_dispatcher = dispatcher;
|
||||||
|
_windowManager = windowManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsOnNavigationThread()
|
private bool IsOnNavigationThread()
|
||||||
@@ -101,6 +109,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
|||||||
WinoPage.MailListPage => typeof(MailListPage),
|
WinoPage.MailListPage => typeof(MailListPage),
|
||||||
WinoPage.SettingsPage => typeof(SettingsPage),
|
WinoPage.SettingsPage => typeof(SettingsPage),
|
||||||
WinoPage.WelcomePage => typeof(WelcomePage),
|
WinoPage.WelcomePage => typeof(WelcomePage),
|
||||||
|
WinoPage.WelcomePageV2 => typeof(WelcomePageV2),
|
||||||
WinoPage.SettingOptionsPage => typeof(SettingOptionsPage),
|
WinoPage.SettingOptionsPage => typeof(SettingOptionsPage),
|
||||||
WinoPage.AppPreferencesPage => typeof(AppPreferencesPage),
|
WinoPage.AppPreferencesPage => typeof(AppPreferencesPage),
|
||||||
WinoPage.AliasManagementPage => typeof(AliasManagementPage),
|
WinoPage.AliasManagementPage => typeof(AliasManagementPage),
|
||||||
@@ -111,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),
|
||||||
@@ -120,32 +133,45 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Frame GetCoreFrame(NavigationReferenceFrame frameType)
|
public Frame GetCoreFrame(NavigationReferenceFrame frameType)
|
||||||
=> ExecuteOnNavigationThread(() => GetCoreFrameInternal(frameType));
|
=> ExecuteOnNavigationThread(() => GetCoreFrameInternal(frameType) ?? throw new ArgumentException($"Frame '{frameType}' cannot be resolved."));
|
||||||
|
|
||||||
private Frame GetCoreFrameInternal(NavigationReferenceFrame frameType)
|
private Frame? GetCoreFrameInternal(NavigationReferenceFrame frameType, WinoWindowKind? requestedWindowKind = null)
|
||||||
{
|
{
|
||||||
if (WinoApplication.MainWindow is not IWinoShellWindow shellWindow) throw new ArgumentException("MainWindow must implement IWinoShellWindow");
|
if (frameType == NavigationReferenceFrame.ShellFrame)
|
||||||
if (shellWindow.GetMainFrame() is not Frame mainFrame) throw new ArgumentException("MainFrame cannot be null.");
|
|
||||||
|
|
||||||
if (frameType == NavigationReferenceFrame.ShellFrame) return shellWindow.GetMainFrame();
|
|
||||||
|
|
||||||
if (frameType == NavigationReferenceFrame.InnerShellFrame)
|
|
||||||
{
|
{
|
||||||
if (mainFrame.Content is MailAppShell mailAppShell)
|
if (requestedWindowKind.HasValue)
|
||||||
|
return _windowManager.GetPrimaryNavigationFrame(requestedWindowKind.Value);
|
||||||
|
|
||||||
|
var activeWindow = _windowManager.ActiveWindow;
|
||||||
|
if (activeWindow != null)
|
||||||
{
|
{
|
||||||
return mailAppShell.GetShellFrame();
|
var activeShellWindow = _windowManager.GetWindow(WinoWindowKind.Shell);
|
||||||
|
if (ReferenceEquals(activeWindow, activeShellWindow))
|
||||||
|
return _windowManager.GetPrimaryNavigationFrame(WinoWindowKind.Shell);
|
||||||
|
|
||||||
|
var activeWelcomeWindow = _windowManager.GetWindow(WinoWindowKind.Welcome);
|
||||||
|
if (ReferenceEquals(activeWindow, activeWelcomeWindow))
|
||||||
|
return _windowManager.GetPrimaryNavigationFrame(WinoWindowKind.Welcome);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mainFrame.Content is CalendarAppShell calendarAppShell)
|
return _windowManager.GetPrimaryNavigationFrame(WinoWindowKind.Shell)
|
||||||
{
|
?? _windowManager.GetPrimaryNavigationFrame(WinoWindowKind.Welcome);
|
||||||
return calendarAppShell.GetShellFrame();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentRoot = mainFrame.Content as UIElement;
|
var mainFrame = _windowManager.GetPrimaryNavigationFrame(WinoWindowKind.Shell);
|
||||||
if (contentRoot == null) return mainFrame;
|
if (mainFrame == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
return WinoVisualTreeHelper.GetChildObject<Frame>(contentRoot, frameType.ToString()) ?? mainFrame;
|
var contentRoot = mainFrame.Content as FrameworkElement;
|
||||||
|
if (contentRoot == null) return null;
|
||||||
|
|
||||||
|
// Use FindName first — it works immediately after InitializeComponent(),
|
||||||
|
// before the visual tree is built by the layout pass.
|
||||||
|
if (contentRoot.FindName(frameType.ToString()) is Frame namedFrame)
|
||||||
|
return namedFrame;
|
||||||
|
|
||||||
|
// Fall back to visual tree search for deeply nested frames (e.g. RenderingFrame).
|
||||||
|
return WinoVisualTreeHelper.GetChildObject<Frame>(contentRoot, frameType.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ChangeApplicationMode(WinoApplicationMode mode)
|
public bool ChangeApplicationMode(WinoApplicationMode mode)
|
||||||
@@ -224,14 +250,22 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
|||||||
_statePersistanceService.IsReadingMail = _renderingPageTypes.Contains(page);
|
_statePersistanceService.IsReadingMail = _renderingPageTypes.Contains(page);
|
||||||
_statePersistanceService.IsEventDetailsVisible = page == WinoPage.EventDetailsPage;
|
_statePersistanceService.IsEventDetailsVisible = page == WinoPage.EventDetailsPage;
|
||||||
|
|
||||||
Frame innerShellFrame = GetCoreFrameInternal(NavigationReferenceFrame.InnerShellFrame);
|
Frame? innerShellFrame = GetCoreFrameInternal(NavigationReferenceFrame.InnerShellFrame);
|
||||||
|
if (innerShellFrame == null && frame == NavigationReferenceFrame.ShellFrame)
|
||||||
|
{
|
||||||
|
var requestedFrame = GetCoreFrameInternal(NavigationReferenceFrame.ShellFrame, WinoWindowKind.Welcome);
|
||||||
|
if (requestedFrame == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return requestedFrame.Navigate(pageType, parameter, GetNavigationTransitionInfo(transition));
|
||||||
|
}
|
||||||
|
|
||||||
if (innerShellFrame != null)
|
if (innerShellFrame != null)
|
||||||
{
|
{
|
||||||
// Calendar navigations.
|
// Calendar navigations.
|
||||||
if (currentApplicationMode == WinoApplicationMode.Calendar)
|
if (currentApplicationMode == WinoApplicationMode.Calendar)
|
||||||
{
|
{
|
||||||
var currentFrameType = GetCurrentFrameType(ref innerShellFrame);
|
var currentFrameType = GetCurrentFrameType(innerShellFrame);
|
||||||
|
|
||||||
if (page == WinoPage.CalendarPage &&
|
if (page == WinoPage.CalendarPage &&
|
||||||
parameter is CalendarPageNavigationArgs calendarNavigationArgs)
|
parameter is CalendarPageNavigationArgs calendarNavigationArgs)
|
||||||
@@ -260,7 +294,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Mail navigations.
|
// Mail navigations.
|
||||||
var currentFrameType = GetCurrentFrameType(ref innerShellFrame);
|
var currentFrameType = GetCurrentFrameType(innerShellFrame);
|
||||||
bool isMailListingPageActive = currentFrameType != null && currentFrameType == typeof(MailListPage);
|
bool isMailListingPageActive = currentFrameType != null && currentFrameType == typeof(MailListPage);
|
||||||
|
|
||||||
// Active page is mail list page and we are refreshing the folder.
|
// Active page is mail list page and we are refreshing the folder.
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ public class NavigationServiceBase
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public Type? GetCurrentFrameType(ref Frame _frame)
|
public Type? GetCurrentFrameType(Frame frame)
|
||||||
{
|
{
|
||||||
if (_frame != null && _frame.Content != null)
|
if (frame != null && frame.Content != null)
|
||||||
return _frame.Content.GetType();
|
return frame.Content.GetType();
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ public class NewThemeService : INewThemeService
|
|||||||
private readonly IConfigurationService _configurationService;
|
private readonly IConfigurationService _configurationService;
|
||||||
private readonly IUnderlyingThemeService _underlyingThemeService;
|
private readonly IUnderlyingThemeService _underlyingThemeService;
|
||||||
private readonly IApplicationResourceManager<ResourceDictionary> _applicationResourceManager;
|
private readonly IApplicationResourceManager<ResourceDictionary> _applicationResourceManager;
|
||||||
|
private readonly IWinoWindowManager _windowManager;
|
||||||
|
|
||||||
private List<AppThemeBase> preDefinedThemes { get; set; } = new List<AppThemeBase>()
|
private List<AppThemeBase> preDefinedThemes { get; set; } = new List<AppThemeBase>()
|
||||||
{
|
{
|
||||||
@@ -75,11 +76,13 @@ public class NewThemeService : INewThemeService
|
|||||||
|
|
||||||
public NewThemeService(IConfigurationService configurationService,
|
public NewThemeService(IConfigurationService configurationService,
|
||||||
IUnderlyingThemeService underlyingThemeService,
|
IUnderlyingThemeService underlyingThemeService,
|
||||||
IApplicationResourceManager<ResourceDictionary> applicationResourceManager)
|
IApplicationResourceManager<ResourceDictionary> applicationResourceManager,
|
||||||
|
IWinoWindowManager windowManager)
|
||||||
{
|
{
|
||||||
_configurationService = configurationService;
|
_configurationService = configurationService;
|
||||||
_underlyingThemeService = underlyingThemeService;
|
_underlyingThemeService = underlyingThemeService;
|
||||||
_applicationResourceManager = applicationResourceManager;
|
_applicationResourceManager = applicationResourceManager;
|
||||||
|
_windowManager = windowManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -89,11 +92,17 @@ public class NewThemeService : INewThemeService
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
return GetShellRootContent().RequestedTheme.ToWinoElementTheme();
|
var rootContent = TryGetShellRootContent();
|
||||||
|
if (rootContent == null)
|
||||||
|
return _configurationService.Get(UnderlyingThemeService.SelectedAppThemeKey, ApplicationElementTheme.Default);
|
||||||
|
|
||||||
|
return rootContent.RequestedTheme.ToWinoElementTheme();
|
||||||
}
|
}
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
GetShellRootContent().RequestedTheme = value.ToWindowsElementTheme();
|
var rootContent = TryGetShellRootContent();
|
||||||
|
if (rootContent != null)
|
||||||
|
rootContent.RequestedTheme = value.ToWindowsElementTheme();
|
||||||
|
|
||||||
_configurationService.Set(UnderlyingThemeService.SelectedAppThemeKey, value);
|
_configurationService.Set(UnderlyingThemeService.SelectedAppThemeKey, value);
|
||||||
|
|
||||||
@@ -115,9 +124,10 @@ public class NewThemeService : INewThemeService
|
|||||||
|
|
||||||
_configurationService.Set(CurrentApplicationThemeKey, value);
|
_configurationService.Set(CurrentApplicationThemeKey, value);
|
||||||
|
|
||||||
if (WinoApplication.MainWindow != null)
|
var window = GetThemeWindow();
|
||||||
|
if (window != null)
|
||||||
{
|
{
|
||||||
WinoApplication.MainWindow.DispatcherQueue.TryEnqueue(async () =>
|
window.DispatcherQueue.TryEnqueue(async () =>
|
||||||
{
|
{
|
||||||
await ApplyCustomThemeAsync(false);
|
await ApplyCustomThemeAsync(false);
|
||||||
});
|
});
|
||||||
@@ -154,9 +164,10 @@ public class NewThemeService : INewThemeService
|
|||||||
currentBackdropType = value;
|
currentBackdropType = value;
|
||||||
_configurationService.Set(WindowBackdropTypeKey, (int)value);
|
_configurationService.Set(WindowBackdropTypeKey, (int)value);
|
||||||
|
|
||||||
if (WinoApplication.MainWindow != null)
|
var window = GetThemeWindow();
|
||||||
|
if (window != null)
|
||||||
{
|
{
|
||||||
WinoApplication.MainWindow.DispatcherQueue.TryEnqueue(() =>
|
window.DispatcherQueue.TryEnqueue(() =>
|
||||||
{
|
{
|
||||||
ApplyBackdrop(value);
|
ApplyBackdrop(value);
|
||||||
});
|
});
|
||||||
@@ -176,7 +187,17 @@ public class NewThemeService : INewThemeService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public FrameworkElement GetShellRootContent() => (WinoApplication.MainWindow as IWinoShellWindow)?.GetRootContent() ?? throw new Exception("No root content found");
|
public FrameworkElement GetShellRootContent()
|
||||||
|
{
|
||||||
|
var window = GetThemeWindow();
|
||||||
|
if (window is IWinoShellWindow shellWindow)
|
||||||
|
return shellWindow.GetRootContent();
|
||||||
|
|
||||||
|
if (window?.Content is FrameworkElement frameworkElement)
|
||||||
|
return frameworkElement;
|
||||||
|
|
||||||
|
throw new Exception("No root content found");
|
||||||
|
}
|
||||||
|
|
||||||
private bool isInitialized = false;
|
private bool isInitialized = false;
|
||||||
|
|
||||||
@@ -210,9 +231,9 @@ public class NewThemeService : INewThemeService
|
|||||||
|
|
||||||
public void ApplyBackdrop(WindowBackdropType backdropType)
|
public void ApplyBackdrop(WindowBackdropType backdropType)
|
||||||
{
|
{
|
||||||
if (WinoApplication.MainWindow is not WindowEx windowEx)
|
if (GetThemeWindow() is not WindowEx windowEx)
|
||||||
{
|
{
|
||||||
Debug.WriteLine("MainWindow is not WindowEx, cannot apply backdrop");
|
Debug.WriteLine("No active WindowEx found, cannot apply backdrop");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +288,7 @@ public class NewThemeService : INewThemeService
|
|||||||
|
|
||||||
private void NotifyThemeUpdate()
|
private void NotifyThemeUpdate()
|
||||||
{
|
{
|
||||||
if (GetShellRootContent() is not UIElement rootContent) return;
|
if (TryGetShellRootContent() is not UIElement rootContent) return;
|
||||||
|
|
||||||
_ = rootContent.DispatcherQueue.EnqueueAsync(() =>
|
_ = rootContent.DispatcherQueue.EnqueueAsync(() =>
|
||||||
{
|
{
|
||||||
@@ -283,9 +304,12 @@ public class NewThemeService : INewThemeService
|
|||||||
|
|
||||||
public void UpdateSystemCaptionButtonColors()
|
public void UpdateSystemCaptionButtonColors()
|
||||||
{
|
{
|
||||||
GetShellRootContent().DispatcherQueue.TryEnqueue(() =>
|
var rootContent = TryGetShellRootContent();
|
||||||
|
if (rootContent == null) return;
|
||||||
|
|
||||||
|
rootContent.DispatcherQueue.TryEnqueue(() =>
|
||||||
{
|
{
|
||||||
if (WinoApplication.MainWindow is not WindowEx mainWindow) return;
|
if (GetThemeWindow() is not WindowEx mainWindow) return;
|
||||||
|
|
||||||
var titleBar = mainWindow.AppWindow.TitleBar;
|
var titleBar = mainWindow.AppWindow.TitleBar;
|
||||||
if (titleBar == null) return;
|
if (titleBar == null) return;
|
||||||
@@ -353,8 +377,7 @@ public class NewThemeService : INewThemeService
|
|||||||
|
|
||||||
private void RefreshThemeResource()
|
private void RefreshThemeResource()
|
||||||
{
|
{
|
||||||
var mainApplicationFrame = GetShellRootContent();
|
var mainApplicationFrame = TryGetShellRootContent();
|
||||||
|
|
||||||
if (mainApplicationFrame == null) return;
|
if (mainApplicationFrame == null) return;
|
||||||
|
|
||||||
if (mainApplicationFrame.RequestedTheme == ElementTheme.Dark)
|
if (mainApplicationFrame.RequestedTheme == ElementTheme.Dark)
|
||||||
@@ -648,4 +671,26 @@ public class NewThemeService : INewThemeService
|
|||||||
new BackdropTypeWrapper(WindowBackdropType.AcrylicThin, "Acrylic Thin")
|
new BackdropTypeWrapper(WindowBackdropType.AcrylicThin, "Acrylic Thin")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private WindowEx? GetThemeWindow() => _windowManager.ActiveWindow ?? WinoApplication.MainWindow;
|
||||||
|
|
||||||
|
private FrameworkElement? TryGetShellRootContent()
|
||||||
|
{
|
||||||
|
var window = GetThemeWindow();
|
||||||
|
if (window == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (window is IWinoShellWindow shellWindow)
|
||||||
|
return shellWindow.GetRootContent();
|
||||||
|
|
||||||
|
return window.Content as FrameworkElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ApplyThemeToActiveWindowAsync()
|
||||||
|
{
|
||||||
|
ApplyBackdrop(currentBackdropType);
|
||||||
|
RootTheme = _configurationService.Get(UnderlyingThemeService.SelectedAppThemeKey, ApplicationElementTheme.Default);
|
||||||
|
await ApplyCustomThemeAsync(false);
|
||||||
|
UpdateSystemCaptionButtonColors();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using WinUIEx;
|
||||||
|
using Wino.Mail.WinUI.Interfaces;
|
||||||
|
using Wino.Mail.WinUI.Models;
|
||||||
|
|
||||||
|
namespace Wino.Mail.WinUI.Services;
|
||||||
|
|
||||||
|
public class WinoWindowManager : IWinoWindowManager
|
||||||
|
{
|
||||||
|
public event EventHandler<WindowEx?>? ActiveWindowChanged;
|
||||||
|
public event EventHandler<WindowEx>? WindowRemoved;
|
||||||
|
|
||||||
|
private readonly object _syncLock = new();
|
||||||
|
private readonly Dictionary<(WinoWindowKind Kind, string Name), WindowEx> _windows = [];
|
||||||
|
private readonly Dictionary<WindowEx, (WinoWindowKind Kind, string Name)> _windowKeys = [];
|
||||||
|
private readonly Dictionary<(WinoWindowKind Kind, string Name), Frame> _primaryNavigationFrames = [];
|
||||||
|
|
||||||
|
public WindowEx? ActiveWindow { get; private set; }
|
||||||
|
|
||||||
|
public WindowEx CreateWindow(WinoWindowKind kind, Func<WindowEx> factory, string? name = null)
|
||||||
|
{
|
||||||
|
var key = CreateKey(kind, name);
|
||||||
|
|
||||||
|
lock (_syncLock)
|
||||||
|
{
|
||||||
|
if (_windows.TryGetValue(key, out var existingWindow))
|
||||||
|
{
|
||||||
|
ActiveWindow = existingWindow;
|
||||||
|
ActiveWindowChanged?.Invoke(this, existingWindow);
|
||||||
|
return existingWindow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var newWindow = factory();
|
||||||
|
|
||||||
|
lock (_syncLock)
|
||||||
|
{
|
||||||
|
if (_windows.TryGetValue(key, out var existingWindow))
|
||||||
|
{
|
||||||
|
ActiveWindow = existingWindow;
|
||||||
|
ActiveWindowChanged?.Invoke(this, existingWindow);
|
||||||
|
return existingWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackWindow(key, newWindow);
|
||||||
|
ActiveWindow = newWindow;
|
||||||
|
ActiveWindowChanged?.Invoke(this, newWindow);
|
||||||
|
return newWindow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public WindowEx? GetWindow(WinoWindowKind kind, string? name = null)
|
||||||
|
{
|
||||||
|
lock (_syncLock)
|
||||||
|
{
|
||||||
|
_windows.TryGetValue(CreateKey(kind, name), out var window);
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public WindowEx? GetWindow(string name)
|
||||||
|
{
|
||||||
|
var normalizedName = NormalizeName(name);
|
||||||
|
|
||||||
|
lock (_syncLock)
|
||||||
|
{
|
||||||
|
return _windows
|
||||||
|
.Where(x => x.Key.Name.Equals(normalizedName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Select(x => x.Value)
|
||||||
|
.FirstOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ActivateWindow(WindowEx window)
|
||||||
|
{
|
||||||
|
window.Show();
|
||||||
|
window.BringToFront();
|
||||||
|
window.Activate();
|
||||||
|
|
||||||
|
lock (_syncLock)
|
||||||
|
{
|
||||||
|
ActiveWindow = window;
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveWindowChanged?.Invoke(this, window);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ActivateWindow(WinoWindowKind kind, string? name = null)
|
||||||
|
{
|
||||||
|
var window = GetWindow(kind, name);
|
||||||
|
if (window == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
ActivateWindow(window);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void HideWindow(WindowEx window)
|
||||||
|
{
|
||||||
|
window.Hide();
|
||||||
|
|
||||||
|
lock (_syncLock)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(ActiveWindow, window))
|
||||||
|
{
|
||||||
|
ActiveWindow = null;
|
||||||
|
ActiveWindowChanged?.Invoke(this, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HideWindow(WinoWindowKind kind, string? name = null)
|
||||||
|
{
|
||||||
|
var window = GetWindow(kind, name);
|
||||||
|
if (window == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
HideWindow(window);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetPrimaryNavigationFrame(WinoWindowKind kind, Frame frame, string? name = null)
|
||||||
|
{
|
||||||
|
lock (_syncLock)
|
||||||
|
{
|
||||||
|
_primaryNavigationFrames[CreateKey(kind, name)] = frame;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Frame? GetPrimaryNavigationFrame(WinoWindowKind kind, string? name = null)
|
||||||
|
{
|
||||||
|
lock (_syncLock)
|
||||||
|
{
|
||||||
|
_primaryNavigationFrames.TryGetValue(CreateKey(kind, name), out var frame);
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TrackWindow((WinoWindowKind Kind, string Name) key, WindowEx window)
|
||||||
|
{
|
||||||
|
_windows[key] = window;
|
||||||
|
_windowKeys[window] = key;
|
||||||
|
window.Activated += WindowActivated;
|
||||||
|
window.Closed += WindowClosed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WindowActivated(object sender, WindowActivatedEventArgs args)
|
||||||
|
{
|
||||||
|
if (sender is not WindowEx window)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (args.WindowActivationState == WindowActivationState.Deactivated)
|
||||||
|
return;
|
||||||
|
|
||||||
|
lock (_syncLock)
|
||||||
|
{
|
||||||
|
if (_windowKeys.ContainsKey(window))
|
||||||
|
{
|
||||||
|
ActiveWindow = window;
|
||||||
|
ActiveWindowChanged?.Invoke(this, window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WindowClosed(object sender, WindowEventArgs args)
|
||||||
|
{
|
||||||
|
if (sender is not WindowEx window)
|
||||||
|
return;
|
||||||
|
|
||||||
|
lock (_syncLock)
|
||||||
|
{
|
||||||
|
if (!_windowKeys.TryGetValue(window, out var key))
|
||||||
|
return;
|
||||||
|
|
||||||
|
window.Activated -= WindowActivated;
|
||||||
|
window.Closed -= WindowClosed;
|
||||||
|
|
||||||
|
_windowKeys.Remove(window);
|
||||||
|
_windows.Remove(key);
|
||||||
|
_primaryNavigationFrames.Remove(key);
|
||||||
|
WindowRemoved?.Invoke(this, window);
|
||||||
|
|
||||||
|
if (ReferenceEquals(ActiveWindow, window))
|
||||||
|
{
|
||||||
|
ActiveWindow = null;
|
||||||
|
ActiveWindowChanged?.Invoke(this, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CloseAllWindows()
|
||||||
|
{
|
||||||
|
List<WindowEx> windows;
|
||||||
|
lock (_syncLock)
|
||||||
|
{
|
||||||
|
windows = _windows.Values.Distinct().ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var window in windows)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
window.Activated -= WindowActivated;
|
||||||
|
window.Closed -= WindowClosed;
|
||||||
|
window.Close();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Best effort shutdown for all tracked windows.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_syncLock)
|
||||||
|
{
|
||||||
|
_windowKeys.Clear();
|
||||||
|
_windows.Clear();
|
||||||
|
_primaryNavigationFrames.Clear();
|
||||||
|
ActiveWindow = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveWindowChanged?.Invoke(this, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (WinoWindowKind Kind, string Name) CreateKey(WinoWindowKind kind, string? name)
|
||||||
|
{
|
||||||
|
var resolvedName = NormalizeName(name ?? kind.ToString());
|
||||||
|
return (kind, string.IsNullOrWhiteSpace(resolvedName) ? kind.ToString() : resolvedName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeName(string name) => name.Trim();
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -291,8 +291,8 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow,
|
|||||||
|
|
||||||
UnregisterRecipients();
|
UnregisterRecipients();
|
||||||
|
|
||||||
// Close the window
|
var windowManager = WinoApplication.Current.Services.GetService<IWinoWindowManager>();
|
||||||
Close();
|
windowManager?.CloseAllWindows();
|
||||||
|
|
||||||
// Exit the application
|
// Exit the application
|
||||||
Application.Current.Exit();
|
Application.Current.Exit();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using Wino.Mail.WinUI;
|
||||||
|
using Wino.Mail.ViewModels;
|
||||||
|
|
||||||
|
namespace Wino.Views.Abstract;
|
||||||
|
|
||||||
|
public abstract class WelcomePageV2Abstract : BasePage<WelcomePageV2ViewModel>
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -10,45 +10,57 @@
|
|||||||
|
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
<StackPanel
|
<StackPanel
|
||||||
MaxWidth="1040"
|
MaxWidth="860"
|
||||||
Padding="24,20,24,24"
|
Padding="36,28,36,36"
|
||||||
Spacing="16">
|
HorizontalAlignment="Center"
|
||||||
|
Spacing="24">
|
||||||
|
|
||||||
|
<!-- Page Header -->
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4">
|
||||||
<TextBlock
|
<TextBlock
|
||||||
FontSize="30"
|
FontSize="28"
|
||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
Text="{x:Bind ViewModel.PageTitle, Mode=OneWay}" />
|
Text="{x:Bind ViewModel.PageTitle, Mode=OneWay}" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Opacity="0.85"
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource BodyTextBlockStyle}"
|
||||||
Text="{x:Bind ViewModel.SubtitleText, Mode=OneWay}"
|
Text="{x:Bind ViewModel.SubtitleText, Mode=OneWay}"
|
||||||
TextWrapping="WrapWholeWords" />
|
TextWrapping="WrapWholeWords" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Opacity="0.85"
|
Foreground="{ThemeResource TextFillColorTertiaryBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
Text="{x:Bind ViewModel.ProviderHint, Mode=OneWay}"
|
Text="{x:Bind ViewModel.ProviderHint, Mode=OneWay}"
|
||||||
TextWrapping="WrapWholeWords" />
|
TextWrapping="WrapWholeWords"
|
||||||
|
Visibility="{x:Bind ViewModel.HasProviderHint, Mode=OneWay}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Setup Mode Selector -->
|
||||||
<SelectorBar x:Name="SetupModeSelector" SelectionChanged="OnSetupModeSelectionChanged">
|
<SelectorBar x:Name="SetupModeSelector" SelectionChanged="OnSetupModeSelectionChanged">
|
||||||
<SelectorBarItem Icon="Library" Text="{x:Bind ViewModel.BasicTabText, Mode=OneWay}" />
|
<SelectorBarItem Icon="Library" Text="{x:Bind ViewModel.BasicTabText, Mode=OneWay}" />
|
||||||
<SelectorBarItem Icon="Setting" Text="{x:Bind ViewModel.AdvancedTabText, Mode=OneWay}" />
|
<SelectorBarItem Icon="Setting" Text="{x:Bind ViewModel.AdvancedTabText, Mode=OneWay}" />
|
||||||
</SelectorBar>
|
</SelectorBar>
|
||||||
|
|
||||||
|
<!-- Basic Setup Card -->
|
||||||
<Border
|
<Border
|
||||||
Padding="16"
|
Padding="20"
|
||||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
CornerRadius="12"
|
CornerRadius="8"
|
||||||
Visibility="{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(ViewModel.IsAdvancedSetupSelected), Mode=OneWay}">
|
Visibility="{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(ViewModel.IsAdvancedSetupSelected), Mode=OneWay}">
|
||||||
<StackPanel Spacing="12">
|
<StackPanel Spacing="16">
|
||||||
<TextBlock
|
<StackPanel Spacing="2">
|
||||||
FontSize="19"
|
<TextBlock
|
||||||
FontWeight="SemiBold"
|
FontSize="16"
|
||||||
Text="{x:Bind ViewModel.BasicSectionTitleText, Mode=OneWay}" />
|
FontWeight="SemiBold"
|
||||||
<TextBlock
|
Text="{x:Bind ViewModel.BasicSectionTitleText, Mode=OneWay}" />
|
||||||
Opacity="0.75"
|
<TextBlock
|
||||||
Text="{x:Bind ViewModel.BasicSectionDescriptionText, Mode=OneWay}"
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
TextWrapping="WrapWholeWords" />
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind ViewModel.BasicSectionDescriptionText, Mode=OneWay}"
|
||||||
|
TextWrapping="WrapWholeWords" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<Grid ColumnSpacing="12">
|
<Grid ColumnSpacing="12">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
@@ -65,103 +77,146 @@
|
|||||||
PlaceholderText="{x:Bind ViewModel.EmailAddressPlaceholderText, Mode=OneWay}"
|
PlaceholderText="{x:Bind ViewModel.EmailAddressPlaceholderText, Mode=OneWay}"
|
||||||
Text="{x:Bind ViewModel.EmailAddress, Mode=TwoWay}" />
|
Text="{x:Bind ViewModel.EmailAddress, Mode=TwoWay}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<PasswordBox Header="{x:Bind ViewModel.PasswordHeaderText, Mode=OneWay}" Password="{x:Bind ViewModel.Password, Mode=TwoWay}" />
|
<PasswordBox Header="{x:Bind ViewModel.PasswordHeaderText, Mode=OneWay}" Password="{x:Bind ViewModel.Password, Mode=TwoWay}" />
|
||||||
|
|
||||||
<CheckBox Content="{x:Bind ViewModel.EnableCalendarSupportText, Mode=OneWay}" IsChecked="{x:Bind ViewModel.IsCalendarSupportEnabled, Mode=TwoWay}" />
|
<CheckBox Content="{x:Bind ViewModel.EnableCalendarSupportText, Mode=OneWay}" IsChecked="{x:Bind ViewModel.IsCalendarSupportEnabled, Mode=TwoWay}" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
Command="{x:Bind ViewModel.AutoDiscoverSettingsCommand}"
|
Command="{x:Bind ViewModel.AutoDiscoverSettingsCommand}"
|
||||||
Content="{x:Bind ViewModel.AutoDiscoverButtonText, Mode=OneWay}" />
|
Content="{x:Bind ViewModel.AutoDiscoverButtonText, Mode=OneWay}"
|
||||||
|
Style="{ThemeResource AccentButtonStyle}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<!-- Advanced Setup Card -->
|
||||||
<Border
|
<Border
|
||||||
Padding="16"
|
Padding="20"
|
||||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
CornerRadius="12"
|
CornerRadius="8"
|
||||||
Visibility="{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(ViewModel.IsBasicSetupSelected), Mode=OneWay}">
|
Visibility="{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(ViewModel.IsBasicSetupSelected), Mode=OneWay}">
|
||||||
<StackPanel Spacing="14">
|
<StackPanel Spacing="20">
|
||||||
<TextBlock
|
<StackPanel Spacing="2">
|
||||||
FontSize="19"
|
<TextBlock
|
||||||
FontWeight="SemiBold"
|
FontSize="16"
|
||||||
Text="{x:Bind ViewModel.AdvancedSectionTitleText, Mode=OneWay}" />
|
FontWeight="SemiBold"
|
||||||
<TextBlock
|
Text="{x:Bind ViewModel.AdvancedSectionTitleText, Mode=OneWay}" />
|
||||||
Opacity="0.75"
|
<TextBlock
|
||||||
Text="{x:Bind ViewModel.AdvancedSectionDescriptionText, Mode=OneWay}"
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
TextWrapping="WrapWholeWords" />
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind ViewModel.AdvancedSectionDescriptionText, Mode=OneWay}"
|
||||||
|
TextWrapping="WrapWholeWords" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<Grid ColumnSpacing="18">
|
<Grid ColumnSpacing="24">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<StackPanel Spacing="8">
|
<!-- Incoming (IMAP) Settings -->
|
||||||
<TextBlock FontWeight="SemiBold" Text="{x:Bind ViewModel.IncomingSectionTitleText, Mode=OneWay}" />
|
<Border
|
||||||
<TextBox Header="{x:Bind ViewModel.IncomingServerHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.IncomingServer, Mode=TwoWay}" />
|
Padding="16"
|
||||||
<TextBox Header="{x:Bind ViewModel.PortHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.IncomingServerPort, Mode=TwoWay}" />
|
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||||
<TextBox Header="{x:Bind ViewModel.IncomingUsernameHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.IncomingServerUsername, Mode=TwoWay}" />
|
CornerRadius="6">
|
||||||
<PasswordBox Header="{x:Bind ViewModel.IncomingPasswordHeaderText, Mode=OneWay}" Password="{x:Bind ViewModel.IncomingServerPassword, Mode=TwoWay}" />
|
<StackPanel Spacing="10">
|
||||||
<ComboBox
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
Header="{x:Bind ViewModel.ConnectionSecurityHeaderText, Mode=OneWay}"
|
<FontIcon FontSize="14" Glyph="" />
|
||||||
ItemsSource="{x:Bind ViewModel.AvailableConnectionSecurityDisplayNames}"
|
<TextBlock FontWeight="SemiBold" Text="{x:Bind ViewModel.IncomingSectionTitleText, Mode=OneWay}" />
|
||||||
SelectedIndex="{x:Bind ViewModel.SelectedIncomingServerConnectionSecurityIndex, Mode=TwoWay}" />
|
</StackPanel>
|
||||||
<ComboBox
|
<TextBox Header="{x:Bind ViewModel.IncomingServerHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.IncomingServer, Mode=TwoWay}" />
|
||||||
Header="{x:Bind ViewModel.AuthenticationMethodHeaderText, Mode=OneWay}"
|
<TextBox Header="{x:Bind ViewModel.PortHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.IncomingServerPort, Mode=TwoWay}" />
|
||||||
ItemsSource="{x:Bind ViewModel.AvailableAuthenticationMethodDisplayNames}"
|
<TextBox Header="{x:Bind ViewModel.IncomingUsernameHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.IncomingServerUsername, Mode=TwoWay}" />
|
||||||
SelectedIndex="{x:Bind ViewModel.SelectedIncomingServerAuthenticationMethodIndex, Mode=TwoWay}" />
|
<PasswordBox Header="{x:Bind ViewModel.IncomingPasswordHeaderText, Mode=OneWay}" Password="{x:Bind ViewModel.IncomingServerPassword, Mode=TwoWay}" />
|
||||||
</StackPanel>
|
<ComboBox
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Header="{x:Bind ViewModel.ConnectionSecurityHeaderText, Mode=OneWay}"
|
||||||
|
ItemsSource="{x:Bind ViewModel.AvailableConnectionSecurityDisplayNames}"
|
||||||
|
SelectedIndex="{x:Bind ViewModel.SelectedIncomingServerConnectionSecurityIndex, Mode=TwoWay}" />
|
||||||
|
<ComboBox
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Header="{x:Bind ViewModel.AuthenticationMethodHeaderText, Mode=OneWay}"
|
||||||
|
ItemsSource="{x:Bind ViewModel.AvailableAuthenticationMethodDisplayNames}"
|
||||||
|
SelectedIndex="{x:Bind ViewModel.SelectedIncomingServerAuthenticationMethodIndex, Mode=TwoWay}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<StackPanel Grid.Column="1" Spacing="8">
|
<!-- Outgoing (SMTP) Settings -->
|
||||||
<TextBlock FontWeight="SemiBold" Text="{x:Bind ViewModel.OutgoingSectionTitleText, Mode=OneWay}" />
|
<Border
|
||||||
<TextBox Header="{x:Bind ViewModel.OutgoingServerHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.OutgoingServer, Mode=TwoWay}" />
|
Grid.Column="1"
|
||||||
<TextBox Header="{x:Bind ViewModel.PortHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.OutgoingServerPort, Mode=TwoWay}" />
|
Padding="16"
|
||||||
<TextBox Header="{x:Bind ViewModel.OutgoingUsernameHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.OutgoingServerUsername, Mode=TwoWay}" />
|
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||||
<PasswordBox Header="{x:Bind ViewModel.OutgoingPasswordHeaderText, Mode=OneWay}" Password="{x:Bind ViewModel.OutgoingServerPassword, Mode=TwoWay}" />
|
CornerRadius="6">
|
||||||
<ComboBox
|
<StackPanel Spacing="10">
|
||||||
Header="{x:Bind ViewModel.ConnectionSecurityHeaderText, Mode=OneWay}"
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
ItemsSource="{x:Bind ViewModel.AvailableConnectionSecurityDisplayNames}"
|
<FontIcon FontSize="14" Glyph="" />
|
||||||
SelectedIndex="{x:Bind ViewModel.SelectedOutgoingServerConnectionSecurityIndex, Mode=TwoWay}" />
|
<TextBlock FontWeight="SemiBold" Text="{x:Bind ViewModel.OutgoingSectionTitleText, Mode=OneWay}" />
|
||||||
<ComboBox
|
</StackPanel>
|
||||||
Header="{x:Bind ViewModel.AuthenticationMethodHeaderText, Mode=OneWay}"
|
<TextBox Header="{x:Bind ViewModel.OutgoingServerHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.OutgoingServer, Mode=TwoWay}" />
|
||||||
ItemsSource="{x:Bind ViewModel.AvailableAuthenticationMethodDisplayNames}"
|
<TextBox Header="{x:Bind ViewModel.PortHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.OutgoingServerPort, Mode=TwoWay}" />
|
||||||
SelectedIndex="{x:Bind ViewModel.SelectedOutgoingServerAuthenticationMethodIndex, Mode=TwoWay}" />
|
<TextBox Header="{x:Bind ViewModel.OutgoingUsernameHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.OutgoingServerUsername, Mode=TwoWay}" />
|
||||||
</StackPanel>
|
<PasswordBox Header="{x:Bind ViewModel.OutgoingPasswordHeaderText, Mode=OneWay}" Password="{x:Bind ViewModel.OutgoingServerPassword, Mode=TwoWay}" />
|
||||||
|
<ComboBox
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Header="{x:Bind ViewModel.ConnectionSecurityHeaderText, Mode=OneWay}"
|
||||||
|
ItemsSource="{x:Bind ViewModel.AvailableConnectionSecurityDisplayNames}"
|
||||||
|
SelectedIndex="{x:Bind ViewModel.SelectedOutgoingServerConnectionSecurityIndex, Mode=TwoWay}" />
|
||||||
|
<ComboBox
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Header="{x:Bind ViewModel.AuthenticationMethodHeaderText, Mode=OneWay}"
|
||||||
|
ItemsSource="{x:Bind ViewModel.AvailableAuthenticationMethodDisplayNames}"
|
||||||
|
SelectedIndex="{x:Bind ViewModel.SelectedOutgoingServerAuthenticationMethodIndex, Mode=TwoWay}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<!-- Calendar Settings Card -->
|
||||||
<Border
|
<Border
|
||||||
Padding="16"
|
Padding="20"
|
||||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
CornerRadius="12">
|
CornerRadius="8">
|
||||||
<StackPanel Spacing="12">
|
<StackPanel Spacing="16">
|
||||||
<TextBlock
|
<StackPanel Spacing="2">
|
||||||
FontSize="19"
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
FontWeight="SemiBold"
|
<FontIcon FontSize="16" Glyph="" />
|
||||||
Text="{x:Bind ViewModel.CalendarSectionTitleText, Mode=OneWay}" />
|
<TextBlock
|
||||||
<TextBlock
|
FontSize="16"
|
||||||
Opacity="0.75"
|
FontWeight="SemiBold"
|
||||||
Text="{x:Bind ViewModel.CalendarSectionDescriptionText, Mode=OneWay}"
|
Text="{x:Bind ViewModel.CalendarSectionTitleText, Mode=OneWay}" />
|
||||||
TextWrapping="WrapWholeWords" />
|
</StackPanel>
|
||||||
|
<TextBlock
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind ViewModel.CalendarSectionDescriptionText, Mode=OneWay}"
|
||||||
|
TextWrapping="WrapWholeWords" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<ComboBox
|
<ComboBox
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
Header="{x:Bind ViewModel.CalendarModeHeaderText, Mode=OneWay}"
|
Header="{x:Bind ViewModel.CalendarModeHeaderText, Mode=OneWay}"
|
||||||
IsEnabled="{x:Bind ViewModel.IsCalendarModeSelectionVisible, Mode=OneWay}"
|
IsEnabled="{x:Bind ViewModel.IsCalendarModeSelectionVisible, Mode=OneWay}"
|
||||||
ItemsSource="{x:Bind ViewModel.AvailableCalendarSupportModeTitles}"
|
ItemsSource="{x:Bind ViewModel.AvailableCalendarSupportModeTitles}"
|
||||||
SelectedIndex="{x:Bind ViewModel.SelectedCalendarSupportModeIndex, Mode=TwoWay}" />
|
SelectedIndex="{x:Bind ViewModel.SelectedCalendarSupportModeIndex, Mode=TwoWay}" />
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Opacity="0.8"
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
Text="{x:Bind ViewModel.SelectedCalendarSupportDescription, Mode=OneWay}"
|
Text="{x:Bind ViewModel.SelectedCalendarSupportDescription, Mode=OneWay}"
|
||||||
TextWrapping="WrapWholeWords" />
|
TextWrapping="WrapWholeWords" />
|
||||||
<Button
|
|
||||||
HorizontalAlignment="Left"
|
<HyperlinkButton
|
||||||
Command="{x:Bind ViewModel.ShowLocalCalendarExplanationCommand}"
|
Command="{x:Bind ViewModel.ShowLocalCalendarExplanationCommand}"
|
||||||
Content="{x:Bind ViewModel.LocalCalendarLearnMoreText, Mode=OneWay}"
|
Content="{x:Bind ViewModel.LocalCalendarLearnMoreText, Mode=OneWay}"
|
||||||
IsEnabled="{x:Bind ViewModel.IsLocalCalendarModeSelected, Mode=OneWay}" />
|
IsEnabled="{x:Bind ViewModel.IsLocalCalendarModeSelected, Mode=OneWay}" />
|
||||||
<Grid ColumnSpacing="12">
|
|
||||||
|
<Grid ColumnSpacing="12" Visibility="{x:Bind ViewModel.IsCalDavSettingsVisible, Mode=OneWay}">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="2*" />
|
<ColumnDefinition Width="2*" />
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
@@ -169,22 +224,22 @@
|
|||||||
<TextBox
|
<TextBox
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Header="{x:Bind ViewModel.CalDavServiceUrlHeaderText, Mode=OneWay}"
|
Header="{x:Bind ViewModel.CalDavServiceUrlHeaderText, Mode=OneWay}"
|
||||||
IsEnabled="{x:Bind ViewModel.IsCalDavSettingsVisible, Mode=OneWay}"
|
|
||||||
Text="{x:Bind ViewModel.CalDavServiceUrl, Mode=TwoWay}" />
|
Text="{x:Bind ViewModel.CalDavServiceUrl, Mode=TwoWay}" />
|
||||||
<TextBox
|
<TextBox
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Header="{x:Bind ViewModel.CalDavUsernameHeaderText, Mode=OneWay}"
|
Header="{x:Bind ViewModel.CalDavUsernameHeaderText, Mode=OneWay}"
|
||||||
IsEnabled="{x:Bind ViewModel.IsCalDavSettingsVisible, Mode=OneWay}"
|
|
||||||
Text="{x:Bind ViewModel.CalDavUsername, Mode=TwoWay}" />
|
Text="{x:Bind ViewModel.CalDavUsername, Mode=TwoWay}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<PasswordBox
|
<PasswordBox
|
||||||
Header="{x:Bind ViewModel.CalDavPasswordHeaderText, Mode=OneWay}"
|
Header="{x:Bind ViewModel.CalDavPasswordHeaderText, Mode=OneWay}"
|
||||||
IsEnabled="{x:Bind ViewModel.IsCalDavSettingsVisible, Mode=OneWay}"
|
Password="{x:Bind ViewModel.CalDavPassword, Mode=TwoWay}"
|
||||||
Password="{x:Bind ViewModel.CalDavPassword, Mode=TwoWay}" />
|
Visibility="{x:Bind ViewModel.IsCalDavSettingsVisible, Mode=OneWay}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Grid ColumnSpacing="8">
|
<!-- Action Bar -->
|
||||||
|
<Grid Margin="0,4,0,0" ColumnSpacing="8">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,20 +5,110 @@
|
|||||||
xmlns:abstract="using:Wino.Views.Abstract"
|
xmlns:abstract="using:Wino.Views.Abstract"
|
||||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:domain="using:Wino.Core.Domain"
|
||||||
|
xmlns:localControls="using:Wino.Mail.WinUI.Controls"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
Style="{StaticResource PageStyle}"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
|
|
||||||
<Border Style="{StaticResource PageRootBorderStyle}">
|
<Grid Padding="32,24" RowSpacing="16">
|
||||||
<Grid Padding="15">
|
<Grid.RowDefinitions>
|
||||||
<ScrollViewer>
|
<!-- Header -->
|
||||||
<controls:MarkdownTextBlock
|
<RowDefinition Height="Auto" />
|
||||||
Margin="0,0,16,0"
|
<!-- CTA Button -->
|
||||||
CharacterSpacing="12"
|
<RowDefinition Height="Auto" />
|
||||||
Config="{x:Bind _config, Mode=OneTime}"
|
<!-- Segmented Tabs + FlipView content -->
|
||||||
FontSize="16"
|
<RowDefinition Height="*" />
|
||||||
Text="{x:Bind ViewModel.CurrentVersionNotes, Mode=OneWay}" />
|
<!-- Footer -->
|
||||||
</ScrollViewer>
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<StackPanel Grid.Row="0" Spacing="4">
|
||||||
|
<TextBlock
|
||||||
|
FontSize="28"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Text="{x:Bind domain:Translator.WelcomeWindow_Title}" />
|
||||||
|
<TextBlock
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource BodyTextBlockStyle}"
|
||||||
|
Text="{x:Bind domain:Translator.WelcomeWindow_Subtitle}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Get Started CTA -->
|
||||||
|
<Button
|
||||||
|
Grid.Row="1"
|
||||||
|
Padding="24,12"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Command="{x:Bind ViewModel.NavigateManageAccountsCommand}"
|
||||||
|
Style="{StaticResource AccentButtonStyle}">
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock
|
||||||
|
FontSize="14"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
HorizontalTextAlignment="Center"
|
||||||
|
Text="{x:Bind domain:Translator.WelcomeWindow_GetStartedButton}" />
|
||||||
|
<TextBlock
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="{ThemeResource TextOnAccentFillColorPrimaryBrush}"
|
||||||
|
Opacity="0.8"
|
||||||
|
Text="{x:Bind domain:Translator.WelcomeWindow_GetStartedDescription}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<!-- Tabs + Content -->
|
||||||
|
<Grid Grid.Row="2" RowSpacing="12">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Segmented Control -->
|
||||||
|
<controls:Segmented
|
||||||
|
x:Name="TabSegmented"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
SelectionChanged="OnTabSelectionChanged">
|
||||||
|
<controls:SegmentedItem Content="{x:Bind domain:Translator.WelcomeWindow_FeaturesTab}" />
|
||||||
|
<controls:SegmentedItem Content="{x:Bind domain:Translator.WelcomeWindow_WhatsNewTab}" />
|
||||||
|
</controls:Segmented>
|
||||||
|
|
||||||
|
<!-- Features FlipView -->
|
||||||
|
<localControls:UpdateNotesFlipViewControl
|
||||||
|
x:Name="FeaturesFlipView"
|
||||||
|
Grid.Row="1"
|
||||||
|
Sections="{x:Bind ViewModel.FeatureSections, Mode=OneWay}"
|
||||||
|
Visibility="Visible" />
|
||||||
|
|
||||||
|
<!-- What's New FlipView -->
|
||||||
|
<localControls:UpdateNotesFlipViewControl
|
||||||
|
x:Name="WhatsNewFlipView"
|
||||||
|
Grid.Row="1"
|
||||||
|
Sections="{x:Bind ViewModel.UpdateSections, Mode=OneWay}"
|
||||||
|
Visibility="Collapsed" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<Grid Grid.Row="3" Padding="0,8,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{ThemeResource TextFillColorTertiaryBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind ViewModel.VersionDisplay, Mode=OneWay}" />
|
||||||
|
|
||||||
|
<StackPanel
|
||||||
|
Grid.Column="1"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="8">
|
||||||
|
<HyperlinkButton Content="{x:Bind domain:Translator.SettingsAboutGithub_Title}" NavigateUri="{x:Bind ViewModel.GitHubUrl, Mode=OneWay}" />
|
||||||
|
<HyperlinkButton Content="{x:Bind domain:Translator.SettingsPaypal_Title}" NavigateUri="{x:Bind ViewModel.PaypalUrl, Mode=OneWay}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
</abstract:WelcomePageAbstract>
|
</abstract:WelcomePageAbstract>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using CommunityToolkit.WinUI.Controls;
|
using CommunityToolkit.WinUI.Controls;
|
||||||
using Wino.Views.Abstract;
|
using Wino.Views.Abstract;
|
||||||
|
|
||||||
@@ -5,12 +7,19 @@ namespace Wino.Views;
|
|||||||
|
|
||||||
public sealed partial class WelcomePage : WelcomePageAbstract
|
public sealed partial class WelcomePage : WelcomePageAbstract
|
||||||
{
|
{
|
||||||
private readonly MarkdownConfig _config;
|
|
||||||
|
|
||||||
public WelcomePage()
|
public WelcomePage()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
_config = new MarkdownConfig();
|
private void OnTabSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not Segmented segmented)
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool isFeaturesTab = segmented.SelectedIndex == 0;
|
||||||
|
|
||||||
|
FeaturesFlipView.Visibility = isFeaturesTab ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
WhatsNewFlipView.Visibility = isFeaturesTab ? Visibility.Collapsed : Visibility.Visible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
<abstract:WelcomePageV2Abstract
|
||||||
|
x:Class="Wino.Views.WelcomePageV2"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:abstract="using:Wino.Views.Abstract"
|
||||||
|
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:domain="using:Wino.Core.Domain"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:models="using:Wino.Core.Domain.Models.Updates"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||||
|
<Grid
|
||||||
|
MaxWidth="900"
|
||||||
|
Margin="0,48,0,40"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
RowSpacing="32">
|
||||||
|
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Brand -->
|
||||||
|
<StackPanel HorizontalAlignment="Center" Spacing="4">
|
||||||
|
<Image
|
||||||
|
Width="128"
|
||||||
|
Height="128"
|
||||||
|
Margin="0,0,0,12"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Source="ms-appx:///Assets/AppEntries/MailAssets/Square150x150Logo.scale-100.png"
|
||||||
|
Stretch="Uniform" />
|
||||||
|
<TextBlock
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
FontSize="28"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Text="{x:Bind domain:Translator.WelcomeWindow_Title}" />
|
||||||
|
<TextBlock
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource BodyTextBlockStyle}"
|
||||||
|
Text="{x:Bind domain:Translator.WelcomeWindow_AppDescription}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- FlipView -->
|
||||||
|
<Grid
|
||||||
|
Grid.Row="1"
|
||||||
|
MaxHeight="300"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Border
|
||||||
|
Padding="0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||||
|
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="12">
|
||||||
|
<FlipView
|
||||||
|
x:Name="UpdateFlipView"
|
||||||
|
MinHeight="200"
|
||||||
|
Background="Transparent"
|
||||||
|
ItemsSource="{x:Bind ViewModel.UpdateSections, Mode=OneWay}"
|
||||||
|
SelectionChanged="OnFlipViewSelectionChanged">
|
||||||
|
<FlipView.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="models:UpdateNoteSection">
|
||||||
|
<Grid Padding="48,40" ColumnSpacing="40">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="200" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- Illustration -->
|
||||||
|
<Border Height="160" CornerRadius="8">
|
||||||
|
<Image
|
||||||
|
Width="{x:Bind ActualImageWidth, Mode=OneTime}"
|
||||||
|
Height="{x:Bind ActualImageHeight, Mode=OneTime}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Source="{x:Bind ImageUrl, Mode=OneTime}"
|
||||||
|
Stretch="Uniform" />
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<StackPanel
|
||||||
|
Grid.Column="1"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Spacing="8">
|
||||||
|
<controls:MarkdownTextBlock Text="{x:Bind Title, Mode=OneTime}" />
|
||||||
|
<controls:MarkdownTextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind Description, Mode=OneTime}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</FlipView.ItemTemplate>
|
||||||
|
</FlipView>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- PipsPager -->
|
||||||
|
<PipsPager
|
||||||
|
x:Name="FlipViewPager"
|
||||||
|
Grid.Row="1"
|
||||||
|
Margin="0,16,0,0"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
NumberOfPages="{x:Bind ViewModel.UpdateSections.Count, Mode=OneWay}"
|
||||||
|
SelectedIndexChanged="OnPipsPagerSelectedIndexChanged"
|
||||||
|
SelectedPageIndex="0" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
|
<Border
|
||||||
|
Grid.Row="2"
|
||||||
|
Height="1"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Background="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||||
|
|
||||||
|
<!-- Get Started -->
|
||||||
|
<StackPanel
|
||||||
|
Grid.Row="3"
|
||||||
|
MaxWidth="600"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Spacing="8">
|
||||||
|
<TextBlock
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource BodyTextBlockStyle}"
|
||||||
|
Text="{x:Bind domain:Translator.WelcomeWindow_GetStartedDescription}" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
MinWidth="240"
|
||||||
|
Padding="12,10"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Command="{x:Bind ViewModel.GetStartedCommand}"
|
||||||
|
Style="{StaticResource AccentButtonStyle}">
|
||||||
|
<TextBlock
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
FontSize="14"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Text="{x:Bind domain:Translator.WelcomeWindow_GetStartedButton}" />
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
</abstract:WelcomePageV2Abstract>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Wino.Views.Abstract;
|
||||||
|
|
||||||
|
namespace Wino.Views;
|
||||||
|
|
||||||
|
public sealed partial class WelcomePageV2 : WelcomePageV2Abstract
|
||||||
|
{
|
||||||
|
public WelcomePageV2()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFlipViewSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
FlipViewPager.SelectedPageIndex = UpdateFlipView.SelectedIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPipsPagerSelectedIndexChanged(PipsPager sender, PipsPagerSelectedIndexChangedEventArgs args)
|
||||||
|
{
|
||||||
|
UpdateFlipView.SelectedIndex = sender.SelectedPageIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<winuiex:WindowEx
|
||||||
|
x:Class="Wino.Mail.WinUI.WelcomeWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:winuiex="using:WinUIEx"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<Grid Background="{ThemeResource WinoApplicationBackgroundColor}">
|
||||||
|
<Frame x:Name="RootFrame" />
|
||||||
|
</Grid>
|
||||||
|
</winuiex:WindowEx>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.UI.Windowing;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using WinUIEx;
|
||||||
|
|
||||||
|
namespace Wino.Mail.WinUI;
|
||||||
|
|
||||||
|
public sealed partial class WelcomeWindow : WindowEx
|
||||||
|
{
|
||||||
|
public Frame GetRootFrame() => RootFrame;
|
||||||
|
|
||||||
|
public WelcomeWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
MinWidth = 980;
|
||||||
|
MinHeight = 900;
|
||||||
|
Title = "Wino Mail";
|
||||||
|
this.SetIcon("Assets/Wino_Icon.ico");
|
||||||
|
|
||||||
|
ConfigureWindowChrome();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConfigureWindowChrome()
|
||||||
|
{
|
||||||
|
AppWindow.TitleBar.ExtendsContentIntoTitleBar = true;
|
||||||
|
|
||||||
|
Width = 980;
|
||||||
|
Height = 720;
|
||||||
|
|
||||||
|
this.CenterOnScreen();
|
||||||
|
|
||||||
|
var themeService = WinoApplication.Current.Services.GetService<INewThemeService>();
|
||||||
|
themeService?.UpdateSystemCaptionButtonColors();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -158,6 +158,13 @@
|
|||||||
<None Remove="Assets\NotificationIcons\mail-markread.theme-light.scale-150.png" />
|
<None Remove="Assets\NotificationIcons\mail-markread.theme-light.scale-150.png" />
|
||||||
<None Remove="Assets\NotificationIcons\mail-markread.theme-light.scale-200.png" />
|
<None Remove="Assets\NotificationIcons\mail-markread.theme-light.scale-200.png" />
|
||||||
<None Remove="Assets\NotificationIcons\mail-markread.theme-light.scale-400.png" />
|
<None Remove="Assets\NotificationIcons\mail-markread.theme-light.scale-400.png" />
|
||||||
|
<None Remove="Assets\UpdateNotes\Images\Calendar.svg" />
|
||||||
|
<None Remove="Assets\UpdateNotes\Images\Security.svg" />
|
||||||
|
<None Remove="Assets\UpdateNotes\Images\Thread.svg" />
|
||||||
|
<None Remove="Assets\UpdateNotes\Images\Notification.svg" />
|
||||||
|
<None Remove="Assets\UpdateNotes\Images\More.svg" />
|
||||||
|
<None Remove="Assets\UpdateNotes\Images\Mail.svg" />
|
||||||
|
<None Remove="Assets\UpdateNotes\Images\Customize.svg" />
|
||||||
<None Remove="BackgroundImages\Acrylic.jpg" />
|
<None Remove="BackgroundImages\Acrylic.jpg" />
|
||||||
<None Remove="BackgroundImages\Clouds.jpg" />
|
<None Remove="BackgroundImages\Clouds.jpg" />
|
||||||
<None Remove="BackgroundImages\Forest.jpg" />
|
<None Remove="BackgroundImages\Forest.jpg" />
|
||||||
@@ -202,6 +209,15 @@
|
|||||||
<Content Include="AppThemes\Snowflake.xaml" />
|
<Content Include="AppThemes\Snowflake.xaml" />
|
||||||
<Content Include="AppThemes\TestTheme.xaml" />
|
<Content Include="AppThemes\TestTheme.xaml" />
|
||||||
<Content Include="Assets\ReleaseNotes\vnext.md" />
|
<Content Include="Assets\ReleaseNotes\vnext.md" />
|
||||||
|
<Content Include="Assets\UpdateNotes\vnext.json" />
|
||||||
|
<Content Include="Assets\UpdateNotes\features.json" />
|
||||||
|
<Content Include="Assets\UpdateNotes\Images\Calendar.svg" />
|
||||||
|
<Content Include="Assets\UpdateNotes\Images\Security.svg" />
|
||||||
|
<Content Include="Assets\UpdateNotes\Images\Thread.svg" />
|
||||||
|
<Content Include="Assets\UpdateNotes\Images\Notification.svg" />
|
||||||
|
<Content Include="Assets\UpdateNotes\Images\More.svg" />
|
||||||
|
<Content Include="Assets\UpdateNotes\Images\Mail.svg" />
|
||||||
|
<Content Include="Assets\UpdateNotes\Images\Customize.svg" />
|
||||||
<Content Include="Assets\Wino_Icon.ico" />
|
<Content Include="Assets\Wino_Icon.ico" />
|
||||||
<Content Include="BackgroundImages\Acrylic.jpg" />
|
<Content Include="BackgroundImages\Acrylic.jpg" />
|
||||||
<Content Include="BackgroundImages\Clouds.jpg" />
|
<Content Include="BackgroundImages\Clouds.jpg" />
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Wino.Messaging.Client.Navigation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User clicked "Get Started" on the welcome page.
|
||||||
|
/// App should close the welcome window and open the shell window.
|
||||||
|
/// </summary>
|
||||||
|
public record GetStartedFromWelcomeRequested;
|
||||||
@@ -29,5 +29,6 @@ public static class ServicesContainerSetup
|
|||||||
services.AddSingleton<IContactPictureFileService, ContactPictureFileService>();
|
services.AddSingleton<IContactPictureFileService, ContactPictureFileService>();
|
||||||
|
|
||||||
services.AddTransient<ICalDavClient, CalDavClient>();
|
services.AddTransient<ICalDavClient, CalDavClient>();
|
||||||
|
services.AddSingleton<IUpdateManager, UpdateManager>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Wino.Core.Domain;
|
||||||
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Core.Domain.Models.Updates;
|
||||||
|
|
||||||
|
namespace Wino.Services;
|
||||||
|
|
||||||
|
public class UpdateManager : IUpdateManager
|
||||||
|
{
|
||||||
|
private const string UpdateNotesResourcePath = "ms-appx:///Assets/UpdateNotes/vnext.json";
|
||||||
|
private const string FeaturesResourcePath = "ms-appx:///Assets/UpdateNotes/features.json";
|
||||||
|
private const string UpdateNotesSeenKeyFormat = "UpdateNotes_{0}_Shown";
|
||||||
|
|
||||||
|
private readonly IFileService _fileService;
|
||||||
|
private readonly IConfigurationService _configurationService;
|
||||||
|
private readonly INativeAppService _nativeAppService;
|
||||||
|
|
||||||
|
private string _versionSeenKey = string.Empty;
|
||||||
|
private UpdateNotes _latestUpdateNotes = new();
|
||||||
|
|
||||||
|
public UpdateManager(IFileService fileService,
|
||||||
|
IConfigurationService configurationService,
|
||||||
|
INativeAppService nativeAppService)
|
||||||
|
{
|
||||||
|
_fileService = fileService;
|
||||||
|
_configurationService = configurationService;
|
||||||
|
_nativeAppService = nativeAppService;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetVersionSeenKey()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_versionSeenKey))
|
||||||
|
{
|
||||||
|
var version = _nativeAppService.GetFullAppVersion();
|
||||||
|
var sanitized = version.Replace(".", "_");
|
||||||
|
_versionSeenKey = string.Format(UpdateNotesSeenKeyFormat, sanitized);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _versionSeenKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UpdateNotes> GetLatestUpdateNotesAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = await _fileService.GetFileContentByApplicationUriAsync(UpdateNotesResourcePath);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(json))
|
||||||
|
{
|
||||||
|
_latestUpdateNotes = new UpdateNotes();
|
||||||
|
return _latestUpdateNotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
_latestUpdateNotes = JsonSerializer.Deserialize(json, BasicTypesJsonContext.Default.UpdateNotes) ?? new UpdateNotes();
|
||||||
|
return _latestUpdateNotes;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
_latestUpdateNotes = new UpdateNotes();
|
||||||
|
return _latestUpdateNotes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShouldShowUpdateNotes()
|
||||||
|
=> !_configurationService.Get(GetVersionSeenKey(), false);
|
||||||
|
|
||||||
|
public async Task<List<UpdateNoteSection>> GetFeaturesAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = await _fileService.GetFileContentByApplicationUriAsync(FeaturesResourcePath);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(json))
|
||||||
|
return [];
|
||||||
|
|
||||||
|
return JsonSerializer.Deserialize(json, BasicTypesJsonContext.Default.ListUpdateNoteSection) ?? [];
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MarkUpdateNotesAsSeen()
|
||||||
|
=> _configurationService.Set(GetVersionSeenKey(), true);
|
||||||
|
}
|
||||||