This commit is contained in:
Burak Kaan Köse
2026-03-06 03:43:06 +01:00
86 changed files with 3824 additions and 438 deletions
+172 -11
View File
@@ -21,13 +21,17 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Mail.Services;
using Wino.Mail.ViewModels;
using Wino.Mail.ViewModels.Data;
using Wino.Mail.WinUI.Activation;
using Wino.Mail.WinUI.Interfaces;
using Wino.Mail.WinUI.Models;
using Wino.Mail.WinUI.Services;
using Wino.Messaging.Client.Accounts;
using Wino.Messaging.Client.Navigation;
using Wino.Messaging.Server;
using Wino.Messaging.UI;
using Wino.Services;
@@ -36,13 +40,17 @@ namespace Wino.Mail.WinUI;
public partial class App : WinoApplication,
IRecipient<NewMailSynchronizationRequested>,
IRecipient<NewCalendarSynchronizationRequested>
IRecipient<NewCalendarSynchronizationRequested>,
IRecipient<AccountCreatedMessage>,
IRecipient<AccountRemovedMessage>,
IRecipient<GetStartedFromWelcomeRequested>
{
private const int InboxSyncsPerFullSync = 20;
private const string ToggleDefaultModeLaunchArgument = "--mode=toggle-default";
private ISynchronizationManager? _synchronizationManager;
private IPreferencesService? _preferencesService;
private IAccountService? _accountService;
private bool _windowManagerConfigured;
private CancellationTokenSource? _autoSynchronizationLoopCts;
private readonly SemaphoreSlim _autoSynchronizationSemaphore = new(1, 1);
private readonly Dictionary<Guid, int> _inboxSyncCounters = [];
@@ -57,6 +65,52 @@ public partial class App : WinoApplication,
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)
{
var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
@@ -92,6 +146,11 @@ public partial class App : WinoApplication,
services.AddTransient(typeof(MailRenderingPageViewModel));
services.AddTransient(typeof(AccountManagementViewModel));
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(IdlePageViewModel));
@@ -152,9 +211,21 @@ public partial class App : WinoApplication,
_synchronizationManager = Services.GetRequiredService<ISynchronizationManager>();
_preferencesService = Services.GetRequiredService<IPreferencesService>();
_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;
RestartAutoSynchronizationLoop();
// Check if launched from toast notification.
@@ -180,11 +251,11 @@ public partial class App : WinoApplication,
if (isStartupTaskLaunch)
{
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
{
// Normal launch - show and activate the window.
// The What's New dialog is shown from MailAppShellViewModel.OnNavigatedTo once XamlRoot is ready.
MainWindow?.Activate();
LogActivation("Window created and activated.");
}
@@ -450,10 +521,19 @@ public partial class App : WinoApplication,
// Initialize theme service after window is created.
await NewThemeService.InitializeAsync();
MainWindow?.Activate();
if (MainWindow != null)
Services.GetRequiredService<IWinoWindowManager>().ActivateWindow(MainWindow);
LogActivation("Window created and activated.");
}
public Task OpenManageAccountsFromWelcomeAsync()
{
Services.GetRequiredService<INavigationService>()
.Navigate(WinoPage.ManageAccountsPage, null, NavigationReferenceFrame.ShellFrame, NavigationTransitionType.DrillIn);
return Task.CompletedTask;
}
/// <summary>
/// Creates the main window without activating it.
/// Used for both normal launch and startup task launch (tray only).
@@ -462,15 +542,15 @@ public partial class App : WinoApplication,
{
LogActivation("Creating main window.");
MainWindow = new ShellWindow();
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
MainWindow = windowManager.CreateWindow(WinoWindowKind.Shell, () => new ShellWindow());
InitializeNavigationDispatcher();
var nativeAppService = Services.GetRequiredService<INativeAppService>();
nativeAppService.GetCoreWindowHwnd = () => WinRT.Interop.WindowNative.GetWindowHandle(MainWindow);
if (MainWindow is not IWinoShellWindow shellWindow)
throw new ArgumentException("MainWindow must implement IWinoShellWindow");
windowManager.SetPrimaryNavigationFrame(WinoWindowKind.Shell, shellWindow.GetMainFrame());
var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
if (activationArgs.Kind == ExtendedActivationKind.Launch &&
@@ -496,6 +576,21 @@ public partial class App : WinoApplication,
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()
{
if (MainWindow == null)
@@ -509,18 +604,26 @@ public partial class App : WinoApplication,
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;
MainWindow.Show();
MainWindow.BringToFront();
MainWindow.Activate();
MainWindow = currentWindow;
windowManager.ActivateWindow(currentWindow);
}
private void RegisterRecipients()
{
WeakReferenceMessenger.Default.Register<NewMailSynchronizationRequested>(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)
@@ -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)
{
if (!string.IsNullOrWhiteSpace(exceptionMessage))
+1 -1
View File
@@ -4,7 +4,7 @@
xmlns:xaml="using:Microsoft.UI.Xaml">
<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}" />
<SolidColorBrush x:Key="AppBarBackgroundColor">Transparent</SolidColorBrush>
+1 -1
View File
@@ -4,7 +4,7 @@
xmlns:xaml="using:Microsoft.UI.Xaml">
<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}" />
<SolidColorBrush x:Key="AppBarBackgroundColor">Transparent</SolidColorBrush>
+1 -1
View File
@@ -4,7 +4,7 @@
xmlns:xaml="using:Microsoft.UI.Xaml">
<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}" />
<SolidColorBrush x:Key="AppBarBackgroundColor">Transparent</SolidColorBrush>
+1 -1
View File
@@ -4,7 +4,7 @@
xmlns:xaml="using:Microsoft.UI.Xaml">
<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}" />
<SolidColorBrush x:Key="AppBarBackgroundColor">Transparent</SolidColorBrush>
+1 -1
View File
@@ -4,7 +4,7 @@
xmlns:xaml="using:Microsoft.UI.Xaml">
<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}" />
<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;
}
}
+2
View File
@@ -18,6 +18,7 @@ public static class CoreUWPContainerSetup
services.AddSingleton<IDispatcher>(provider => provider.GetRequiredService<WinUIDispatcher>());
services.AddSingleton<IUnderlyingThemeService, UnderlyingThemeService>();
services.AddSingleton<IWinoWindowManager, WinoWindowManager>();
services.AddSingleton<INativeAppService, NativeAppService>();
services.AddSingleton<IStoreManagementService, StoreManagementService>();
services.AddSingleton<IPreferencesService, PreferencesService>();
@@ -48,6 +49,7 @@ public static class CoreUWPContainerSetup
services.AddTransient(typeof(AboutPageViewModel));
services.AddTransient(typeof(SettingsPageViewModel));
services.AddTransient(typeof(ManageAccountsPagePageViewModel));
services.AddTransient(typeof(WelcomeHostPageViewModel));
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();
}
}
+11
View File
@@ -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 ReverseVisibilityConverter(Visibility visibility) => visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible;
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 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 GetFontWeightByReadState(bool isChildSelected) => isChildSelected ? FontWeights.Normal : FontWeights.SemiBold;
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()
{
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);
}
}
@@ -23,11 +23,11 @@ public class PreDefinedAppTheme : AppThemeBase
public override AppThemeType AppThemeType => AppThemeType.PreDefined;
public override string GetBackgroundPreviewImagePath()
=> $"ms-appx:///Wino.Mail.WinUI/BackgroundImages/{ThemeName}.jpg";
=> $"ms-appx:///BackgroundImages/{ThemeName}.jpg";
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);
}
}
+7
View File
@@ -0,0 +1,7 @@
namespace Wino.Mail.WinUI.Models;
public enum WinoWindowKind
{
Shell,
Welcome
}
+2 -2
View File
@@ -27,9 +27,9 @@ public class DialogService : DialogServiceBase, IMailDialogService
{
public DialogService(INewThemeService themeService,
IConfigurationService configurationService,
IApplicationResourceManager<ResourceDictionary> applicationResourceManager) : base(themeService, configurationService, applicationResourceManager)
IApplicationResourceManager<ResourceDictionary> applicationResourceManager,
IUpdateManager updateManager) : base(themeService, configurationService, applicationResourceManager, updateManager)
{
}
public async Task<ICreateAccountAliasDialog> ShowCreateAccountAliasDialogAsync()
+14 -1
View File
@@ -16,6 +16,7 @@ using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.Common;
using Wino.Core.Domain.Models.Printing;
using Wino.Core.Domain.Models.Updates;
using Wino.Dialogs;
using Wino.Mail.WinUI.Dialogs;
using Wino.Mail.WinUI.Extensions;
@@ -30,14 +31,16 @@ public class DialogServiceBase : IDialogServiceBase
protected INewThemeService ThemeService { get; }
protected IConfigurationService ConfigurationService { get; }
protected IUpdateManager UpdateManager { 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;
ConfigurationService = configurationService;
ApplicationResourceManager = applicationResourceManager;
UpdateManager = updateManager;
}
protected XamlRoot? GetXamlRoot()
@@ -355,4 +358,14 @@ public class DialogServiceBase : IDialogServiceBase
return null!;
}
}
public async Task ShowWhatIsNewDialogAsync(UpdateNotes notes)
{
var dialog = new WhatIsNewDialog(notes, UpdateManager)
{
RequestedTheme = ThemeService.RootTheme.ToWindowsElementTheme()
};
await HandleDialogPresentationAsync(dialog);
}
}
+56 -22
View File
@@ -13,6 +13,7 @@ using Wino.Mail.ViewModels.Data;
using Wino.Mail.ViewModels.Messages;
using Wino.Mail.WinUI;
using Wino.Mail.WinUI.Interfaces;
using Wino.Mail.WinUI.Models;
using Wino.Mail.WinUI.Services;
using Wino.Mail.WinUI.Views.Calendar;
using Wino.Messaging.Client.Mails;
@@ -29,6 +30,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
{
private readonly IStatePersistanceService _statePersistanceService;
private readonly IDispatcher _dispatcher;
private readonly IWinoWindowManager _windowManager;
private WinoPage[] _renderingPageTypes = new WinoPage[]
{
@@ -42,7 +44,12 @@ public class NavigationService : NavigationServiceBase, INavigationService
WinoPage.MailRenderingPage,
WinoPage.ComposePage,
WinoPage.IdlePage,
WinoPage.WelcomePage
WinoPage.WelcomePage,
WinoPage.WelcomePageV2,
WinoPage.WelcomeHostPage,
WinoPage.ProviderSelectionPage,
WinoPage.AccountSetupProgressPage,
WinoPage.SpecialImapCredentialsPage
];
private static readonly WinoPage[] CalendarOnlyPages =
@@ -51,10 +58,11 @@ public class NavigationService : NavigationServiceBase, INavigationService
WinoPage.EventDetailsPage
];
public NavigationService(IStatePersistanceService statePersistanceService, IDispatcher dispatcher)
public NavigationService(IStatePersistanceService statePersistanceService, IDispatcher dispatcher, IWinoWindowManager windowManager)
{
_statePersistanceService = statePersistanceService;
_dispatcher = dispatcher;
_windowManager = windowManager;
}
private bool IsOnNavigationThread()
@@ -101,6 +109,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
WinoPage.MailListPage => typeof(MailListPage),
WinoPage.SettingsPage => typeof(SettingsPage),
WinoPage.WelcomePage => typeof(WelcomePage),
WinoPage.WelcomePageV2 => typeof(WelcomePageV2),
WinoPage.SettingOptionsPage => typeof(SettingOptionsPage),
WinoPage.AppPreferencesPage => typeof(AppPreferencesPage),
WinoPage.AliasManagementPage => typeof(AliasManagementPage),
@@ -111,6 +120,10 @@ public class NavigationService : NavigationServiceBase, INavigationService
WinoPage.ContactsPage => typeof(ContactsPage),
WinoPage.SignatureAndEncryptionPage => typeof(SignatureAndEncryptionPage),
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.EventDetailsPage => typeof(EventDetailsPage),
WinoPage.CalendarSettingsPage => typeof(CalendarSettingsPage),
@@ -120,32 +133,45 @@ public class NavigationService : NavigationServiceBase, INavigationService
}
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 (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 (frameType == NavigationReferenceFrame.ShellFrame)
{
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 calendarAppShell.GetShellFrame();
}
return _windowManager.GetPrimaryNavigationFrame(WinoWindowKind.Shell)
?? _windowManager.GetPrimaryNavigationFrame(WinoWindowKind.Welcome);
}
var contentRoot = mainFrame.Content as UIElement;
if (contentRoot == null) return mainFrame;
var mainFrame = _windowManager.GetPrimaryNavigationFrame(WinoWindowKind.Shell);
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)
@@ -224,14 +250,22 @@ public class NavigationService : NavigationServiceBase, INavigationService
_statePersistanceService.IsReadingMail = _renderingPageTypes.Contains(page);
_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)
{
// Calendar navigations.
if (currentApplicationMode == WinoApplicationMode.Calendar)
{
var currentFrameType = GetCurrentFrameType(ref innerShellFrame);
var currentFrameType = GetCurrentFrameType(innerShellFrame);
if (page == WinoPage.CalendarPage &&
parameter is CalendarPageNavigationArgs calendarNavigationArgs)
@@ -260,7 +294,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
else
{
// Mail navigations.
var currentFrameType = GetCurrentFrameType(ref innerShellFrame);
var currentFrameType = GetCurrentFrameType(innerShellFrame);
bool isMailListingPageActive = currentFrameType != null && currentFrameType == typeof(MailListPage);
// 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)
return _frame.Content.GetType();
if (frame != null && frame.Content != null)
return frame.Content.GetType();
return null;
}
+60 -15
View File
@@ -62,6 +62,7 @@ public class NewThemeService : INewThemeService
private readonly IConfigurationService _configurationService;
private readonly IUnderlyingThemeService _underlyingThemeService;
private readonly IApplicationResourceManager<ResourceDictionary> _applicationResourceManager;
private readonly IWinoWindowManager _windowManager;
private List<AppThemeBase> preDefinedThemes { get; set; } = new List<AppThemeBase>()
{
@@ -75,11 +76,13 @@ public class NewThemeService : INewThemeService
public NewThemeService(IConfigurationService configurationService,
IUnderlyingThemeService underlyingThemeService,
IApplicationResourceManager<ResourceDictionary> applicationResourceManager)
IApplicationResourceManager<ResourceDictionary> applicationResourceManager,
IWinoWindowManager windowManager)
{
_configurationService = configurationService;
_underlyingThemeService = underlyingThemeService;
_applicationResourceManager = applicationResourceManager;
_windowManager = windowManager;
}
/// <summary>
@@ -89,11 +92,17 @@ public class NewThemeService : INewThemeService
{
get
{
return GetShellRootContent().RequestedTheme.ToWinoElementTheme();
var rootContent = TryGetShellRootContent();
if (rootContent == null)
return _configurationService.Get(UnderlyingThemeService.SelectedAppThemeKey, ApplicationElementTheme.Default);
return rootContent.RequestedTheme.ToWinoElementTheme();
}
set
{
GetShellRootContent().RequestedTheme = value.ToWindowsElementTheme();
var rootContent = TryGetShellRootContent();
if (rootContent != null)
rootContent.RequestedTheme = value.ToWindowsElementTheme();
_configurationService.Set(UnderlyingThemeService.SelectedAppThemeKey, value);
@@ -115,9 +124,10 @@ public class NewThemeService : INewThemeService
_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);
});
@@ -154,9 +164,10 @@ public class NewThemeService : INewThemeService
currentBackdropType = 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);
});
@@ -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;
@@ -210,9 +231,9 @@ public class NewThemeService : INewThemeService
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;
}
@@ -267,7 +288,7 @@ public class NewThemeService : INewThemeService
private void NotifyThemeUpdate()
{
if (GetShellRootContent() is not UIElement rootContent) return;
if (TryGetShellRootContent() is not UIElement rootContent) return;
_ = rootContent.DispatcherQueue.EnqueueAsync(() =>
{
@@ -283,9 +304,12 @@ public class NewThemeService : INewThemeService
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;
if (titleBar == null) return;
@@ -353,8 +377,7 @@ public class NewThemeService : INewThemeService
private void RefreshThemeResource()
{
var mainApplicationFrame = GetShellRootContent();
var mainApplicationFrame = TryGetShellRootContent();
if (mainApplicationFrame == null) return;
if (mainApplicationFrame.RequestedTheme == ElementTheme.Dark)
@@ -648,4 +671,26 @@ public class NewThemeService : INewThemeService
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();
}
+3
View File
@@ -107,6 +107,9 @@
</controls:Segmented>
</StackPanel>
</TitleBar.RightHeader>
<TitleBar.IconSource>
<ImageIconSource ImageSource="/Assets/Wino_Icon.ico" />
</TitleBar.IconSource>
</TitleBar>
<Frame
+2 -2
View File
@@ -291,8 +291,8 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow,
UnregisterRecipients();
// Close the window
Close();
var windowManager = WinoApplication.Current.Services.GetService<IWinoWindowManager>();
windowManager?.CloseAllWindows();
// Exit the application
Application.Current.Exit();
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
using Wino.Mail.ViewModels;
using Wino.Mail.WinUI;
namespace Wino.Mail.WinUI.Views.Abstract;
public abstract class AccountSetupProgressPageAbstract : BasePage<AccountSetupProgressPageViewModel>
{
}
@@ -0,0 +1,8 @@
using Wino.Mail.ViewModels;
using Wino.Mail.WinUI;
namespace Wino.Mail.WinUI.Views.Abstract;
public abstract class ProviderSelectionPageAbstract : BasePage<ProviderSelectionPageViewModel>
{
}
@@ -0,0 +1,8 @@
using Wino.Mail.ViewModels;
using Wino.Mail.WinUI;
namespace Wino.Mail.WinUI.Views.Abstract;
public abstract class SpecialImapCredentialsPageAbstract : BasePage<SpecialImapCredentialsPageViewModel>
{
}
@@ -0,0 +1,8 @@
using Wino.Core.ViewModels;
using Wino.Mail.WinUI;
namespace Wino.Mail.WinUI.Views.Abstract;
public abstract class WelcomeHostPageAbstract : BasePage<WelcomeHostPageViewModel>
{
}
@@ -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>
<StackPanel
MaxWidth="1040"
Padding="24,20,24,24"
Spacing="16">
MaxWidth="860"
Padding="36,28,36,36"
HorizontalAlignment="Center"
Spacing="24">
<!-- Page Header -->
<StackPanel Spacing="4">
<TextBlock
FontSize="30"
FontSize="28"
FontWeight="SemiBold"
Text="{x:Bind ViewModel.PageTitle, Mode=OneWay}" />
<TextBlock
Opacity="0.85"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource BodyTextBlockStyle}"
Text="{x:Bind ViewModel.SubtitleText, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
<TextBlock
Opacity="0.85"
Foreground="{ThemeResource TextFillColorTertiaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ViewModel.ProviderHint, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
TextWrapping="WrapWholeWords"
Visibility="{x:Bind ViewModel.HasProviderHint, Mode=OneWay}" />
</StackPanel>
<!-- Setup Mode Selector -->
<SelectorBar x:Name="SetupModeSelector" SelectionChanged="OnSetupModeSelectionChanged">
<SelectorBarItem Icon="Library" Text="{x:Bind ViewModel.BasicTabText, Mode=OneWay}" />
<SelectorBarItem Icon="Setting" Text="{x:Bind ViewModel.AdvancedTabText, Mode=OneWay}" />
</SelectorBar>
<!-- Basic Setup Card -->
<Border
Padding="16"
Padding="20"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="12"
CornerRadius="8"
Visibility="{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(ViewModel.IsAdvancedSetupSelected), Mode=OneWay}">
<StackPanel Spacing="12">
<TextBlock
FontSize="19"
FontWeight="SemiBold"
Text="{x:Bind ViewModel.BasicSectionTitleText, Mode=OneWay}" />
<TextBlock
Opacity="0.75"
Text="{x:Bind ViewModel.BasicSectionDescriptionText, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
<StackPanel Spacing="16">
<StackPanel Spacing="2">
<TextBlock
FontSize="16"
FontWeight="SemiBold"
Text="{x:Bind ViewModel.BasicSectionTitleText, Mode=OneWay}" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ViewModel.BasicSectionDescriptionText, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
</StackPanel>
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
@@ -65,103 +77,146 @@
PlaceholderText="{x:Bind ViewModel.EmailAddressPlaceholderText, Mode=OneWay}"
Text="{x:Bind ViewModel.EmailAddress, Mode=TwoWay}" />
</Grid>
<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}" />
<Button
HorizontalAlignment="Left"
Command="{x:Bind ViewModel.AutoDiscoverSettingsCommand}"
Content="{x:Bind ViewModel.AutoDiscoverButtonText, Mode=OneWay}" />
Content="{x:Bind ViewModel.AutoDiscoverButtonText, Mode=OneWay}"
Style="{ThemeResource AccentButtonStyle}" />
</StackPanel>
</Border>
<!-- Advanced Setup Card -->
<Border
Padding="16"
Padding="20"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="12"
CornerRadius="8"
Visibility="{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(ViewModel.IsBasicSetupSelected), Mode=OneWay}">
<StackPanel Spacing="14">
<TextBlock
FontSize="19"
FontWeight="SemiBold"
Text="{x:Bind ViewModel.AdvancedSectionTitleText, Mode=OneWay}" />
<TextBlock
Opacity="0.75"
Text="{x:Bind ViewModel.AdvancedSectionDescriptionText, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
<StackPanel Spacing="20">
<StackPanel Spacing="2">
<TextBlock
FontSize="16"
FontWeight="SemiBold"
Text="{x:Bind ViewModel.AdvancedSectionTitleText, Mode=OneWay}" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ViewModel.AdvancedSectionDescriptionText, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
</StackPanel>
<Grid ColumnSpacing="18">
<Grid ColumnSpacing="24">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Spacing="8">
<TextBlock FontWeight="SemiBold" Text="{x:Bind ViewModel.IncomingSectionTitleText, Mode=OneWay}" />
<TextBox Header="{x:Bind ViewModel.IncomingServerHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.IncomingServer, Mode=TwoWay}" />
<TextBox Header="{x:Bind ViewModel.PortHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.IncomingServerPort, Mode=TwoWay}" />
<TextBox Header="{x:Bind ViewModel.IncomingUsernameHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.IncomingServerUsername, Mode=TwoWay}" />
<PasswordBox Header="{x:Bind ViewModel.IncomingPasswordHeaderText, Mode=OneWay}" Password="{x:Bind ViewModel.IncomingServerPassword, Mode=TwoWay}" />
<ComboBox
Header="{x:Bind ViewModel.ConnectionSecurityHeaderText, Mode=OneWay}"
ItemsSource="{x:Bind ViewModel.AvailableConnectionSecurityDisplayNames}"
SelectedIndex="{x:Bind ViewModel.SelectedIncomingServerConnectionSecurityIndex, Mode=TwoWay}" />
<ComboBox
Header="{x:Bind ViewModel.AuthenticationMethodHeaderText, Mode=OneWay}"
ItemsSource="{x:Bind ViewModel.AvailableAuthenticationMethodDisplayNames}"
SelectedIndex="{x:Bind ViewModel.SelectedIncomingServerAuthenticationMethodIndex, Mode=TwoWay}" />
</StackPanel>
<!-- Incoming (IMAP) Settings -->
<Border
Padding="16"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
CornerRadius="6">
<StackPanel Spacing="10">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="14" Glyph="&#xE896;" />
<TextBlock FontWeight="SemiBold" Text="{x:Bind ViewModel.IncomingSectionTitleText, Mode=OneWay}" />
</StackPanel>
<TextBox Header="{x:Bind ViewModel.IncomingServerHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.IncomingServer, Mode=TwoWay}" />
<TextBox Header="{x:Bind ViewModel.PortHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.IncomingServerPort, Mode=TwoWay}" />
<TextBox Header="{x:Bind ViewModel.IncomingUsernameHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.IncomingServerUsername, Mode=TwoWay}" />
<PasswordBox Header="{x:Bind ViewModel.IncomingPasswordHeaderText, Mode=OneWay}" Password="{x:Bind ViewModel.IncomingServerPassword, Mode=TwoWay}" />
<ComboBox
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">
<TextBlock FontWeight="SemiBold" Text="{x:Bind ViewModel.OutgoingSectionTitleText, Mode=OneWay}" />
<TextBox Header="{x:Bind ViewModel.OutgoingServerHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.OutgoingServer, Mode=TwoWay}" />
<TextBox Header="{x:Bind ViewModel.PortHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.OutgoingServerPort, Mode=TwoWay}" />
<TextBox Header="{x:Bind ViewModel.OutgoingUsernameHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.OutgoingServerUsername, Mode=TwoWay}" />
<PasswordBox Header="{x:Bind ViewModel.OutgoingPasswordHeaderText, Mode=OneWay}" Password="{x:Bind ViewModel.OutgoingServerPassword, Mode=TwoWay}" />
<ComboBox
Header="{x:Bind ViewModel.ConnectionSecurityHeaderText, Mode=OneWay}"
ItemsSource="{x:Bind ViewModel.AvailableConnectionSecurityDisplayNames}"
SelectedIndex="{x:Bind ViewModel.SelectedOutgoingServerConnectionSecurityIndex, Mode=TwoWay}" />
<ComboBox
Header="{x:Bind ViewModel.AuthenticationMethodHeaderText, Mode=OneWay}"
ItemsSource="{x:Bind ViewModel.AvailableAuthenticationMethodDisplayNames}"
SelectedIndex="{x:Bind ViewModel.SelectedOutgoingServerAuthenticationMethodIndex, Mode=TwoWay}" />
</StackPanel>
<!-- Outgoing (SMTP) Settings -->
<Border
Grid.Column="1"
Padding="16"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
CornerRadius="6">
<StackPanel Spacing="10">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="14" Glyph="&#xE898;" />
<TextBlock FontWeight="SemiBold" Text="{x:Bind ViewModel.OutgoingSectionTitleText, Mode=OneWay}" />
</StackPanel>
<TextBox Header="{x:Bind ViewModel.OutgoingServerHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.OutgoingServer, Mode=TwoWay}" />
<TextBox Header="{x:Bind ViewModel.PortHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.OutgoingServerPort, Mode=TwoWay}" />
<TextBox Header="{x:Bind ViewModel.OutgoingUsernameHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.OutgoingServerUsername, Mode=TwoWay}" />
<PasswordBox Header="{x:Bind ViewModel.OutgoingPasswordHeaderText, Mode=OneWay}" Password="{x:Bind ViewModel.OutgoingServerPassword, Mode=TwoWay}" />
<ComboBox
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>
</StackPanel>
</Border>
<!-- Calendar Settings Card -->
<Border
Padding="16"
Padding="20"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="12">
<StackPanel Spacing="12">
<TextBlock
FontSize="19"
FontWeight="SemiBold"
Text="{x:Bind ViewModel.CalendarSectionTitleText, Mode=OneWay}" />
<TextBlock
Opacity="0.75"
Text="{x:Bind ViewModel.CalendarSectionDescriptionText, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
CornerRadius="8">
<StackPanel Spacing="16">
<StackPanel Spacing="2">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="16" Glyph="&#xE787;" />
<TextBlock
FontSize="16"
FontWeight="SemiBold"
Text="{x:Bind ViewModel.CalendarSectionTitleText, Mode=OneWay}" />
</StackPanel>
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ViewModel.CalendarSectionDescriptionText, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
</StackPanel>
<ComboBox
HorizontalAlignment="Stretch"
Header="{x:Bind ViewModel.CalendarModeHeaderText, Mode=OneWay}"
IsEnabled="{x:Bind ViewModel.IsCalendarModeSelectionVisible, Mode=OneWay}"
ItemsSource="{x:Bind ViewModel.AvailableCalendarSupportModeTitles}"
SelectedIndex="{x:Bind ViewModel.SelectedCalendarSupportModeIndex, Mode=TwoWay}" />
<TextBlock
Opacity="0.8"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ViewModel.SelectedCalendarSupportDescription, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
<Button
HorizontalAlignment="Left"
<HyperlinkButton
Command="{x:Bind ViewModel.ShowLocalCalendarExplanationCommand}"
Content="{x:Bind ViewModel.LocalCalendarLearnMoreText, Mode=OneWay}"
IsEnabled="{x:Bind ViewModel.IsLocalCalendarModeSelected, Mode=OneWay}" />
<Grid ColumnSpacing="12">
<Grid ColumnSpacing="12" Visibility="{x:Bind ViewModel.IsCalDavSettingsVisible, Mode=OneWay}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="*" />
@@ -169,22 +224,22 @@
<TextBox
Grid.Column="0"
Header="{x:Bind ViewModel.CalDavServiceUrlHeaderText, Mode=OneWay}"
IsEnabled="{x:Bind ViewModel.IsCalDavSettingsVisible, Mode=OneWay}"
Text="{x:Bind ViewModel.CalDavServiceUrl, Mode=TwoWay}" />
<TextBox
Grid.Column="1"
Header="{x:Bind ViewModel.CalDavUsernameHeaderText, Mode=OneWay}"
IsEnabled="{x:Bind ViewModel.IsCalDavSettingsVisible, Mode=OneWay}"
Text="{x:Bind ViewModel.CalDavUsername, Mode=TwoWay}" />
</Grid>
<PasswordBox
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>
</Border>
<Grid ColumnSpacing="8">
<!-- Action Bar -->
<Grid Margin="0,4,0,0" ColumnSpacing="8">
<Grid.ColumnDefinitions>
<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="&#xEA3A;"
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="&#xE73E;"
Visibility="{x:Bind helpers:XamlHelpers.BoolToVisibilityConverter(IsSucceeded), Mode=OneWay}" />
<!-- Failed: red error -->
<FontIcon
FontSize="16"
Foreground="#D13438"
Glyph="&#xEA39;"
Visibility="{x:Bind helpers:XamlHelpers.BoolToVisibilityConverter(IsFailed), Mode=OneWay}" />
</Grid>
<!-- Step text -->
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock Text="{x:Bind Title}" />
<TextBlock
Foreground="#D13438"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ErrorMessage, Mode=OneWay}"
TextWrapping="Wrap"
Visibility="{x:Bind helpers:XamlHelpers.BoolToVisibilityConverter(IsFailed), Mode=OneWay}" />
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<!-- Success InfoBar -->
<winuiControls:InfoBar
IsClosable="False"
IsOpen="{x:Bind ViewModel.IsSetupComplete, Mode=OneWay}"
Message="{x:Bind domain:Translator.AccountSetup_SuccessMessage}"
Severity="Success" />
<!-- Failure InfoBar + Buttons -->
<StackPanel
Spacing="12"
Visibility="{x:Bind helpers:XamlHelpers.BoolToVisibilityConverter(ViewModel.IsSetupFailed), Mode=OneWay}">
<winuiControls:InfoBar
IsClosable="False"
IsOpen="True"
Message="{x:Bind ViewModel.FailureMessage, Mode=OneWay}"
Severity="Error" />
<StackPanel
HorizontalAlignment="Center"
Orientation="Horizontal"
Spacing="8">
<Button
Command="{x:Bind ViewModel.GoBackCommand}"
Content="{x:Bind domain:Translator.AccountSetup_GoBackButton}" />
<Button
Command="{x:Bind ViewModel.TryAgainCommand}"
Content="{x:Bind domain:Translator.AccountSetup_TryAgainButton}"
Style="{StaticResource AccentButtonStyle}" />
</StackPanel>
</StackPanel>
</StackPanel>
</Grid>
</abstract:AccountSetupProgressPageAbstract>
@@ -0,0 +1,11 @@
using Wino.Mail.WinUI.Views.Abstract;
namespace Wino.Views;
public sealed partial class AccountSetupProgressPage : AccountSetupProgressPageAbstract
{
public AccountSetupProgressPage()
{
InitializeComponent();
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,125 @@
<abstract:ProviderSelectionPageAbstract
x:Class="Wino.Views.ProviderSelectionPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:abstract="using:Wino.Mail.WinUI.Views.Abstract"
xmlns:accounts="using:Wino.Core.Domain.Models.Accounts"
xmlns:coreViewModelData="using:Wino.Core.ViewModels.Data"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:domain="using:Wino.Core.Domain"
xmlns:helpers="using:Wino.Helpers"
xmlns:interfaces="using:Wino.Core.Domain.Interfaces"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<ScrollViewer
HorizontalAlignment="Center"
VerticalAlignment="Center"
VerticalScrollBarVisibility="Auto">
<StackPanel
MaxWidth="480"
Margin="0,24,0,24"
HorizontalAlignment="Center"
Spacing="20">
<!-- Title -->
<StackPanel Spacing="4">
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}" Text="{x:Bind domain:Translator.ProviderSelection_Title}" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource BodyTextBlockStyle}"
Text="{x:Bind domain:Translator.ProviderSelection_Subtitle}" />
</StackPanel>
<!-- Account Name + Color Picker -->
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox
Header="{x:Bind domain:Translator.ProviderSelection_AccountNameHeader}"
PlaceholderText="{x:Bind domain:Translator.ProviderSelection_AccountNamePlaceholder}"
Text="{x:Bind ViewModel.AccountName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Button Grid.Column="1" VerticalAlignment="Bottom">
<Grid>
<!-- No color selected placeholder -->
<FontIcon
FontSize="16"
Glyph="&#xE771;"
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="&#xEA67;" />
<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="&#xE753;" />
<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="&#xE7F8;" />
<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();
}
}
}
+101 -11
View File
@@ -5,20 +5,110 @@
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:localControls="using:Wino.Mail.WinUI.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Style="{StaticResource PageStyle}"
mc:Ignorable="d">
<Border Style="{StaticResource PageRootBorderStyle}">
<Grid Padding="15">
<ScrollViewer>
<controls:MarkdownTextBlock
Margin="0,0,16,0"
CharacterSpacing="12"
Config="{x:Bind _config, Mode=OneTime}"
FontSize="16"
Text="{x:Bind ViewModel.CurrentVersionNotes, Mode=OneWay}" />
</ScrollViewer>
<Grid Padding="32,24" RowSpacing="16">
<Grid.RowDefinitions>
<!-- Header -->
<RowDefinition Height="Auto" />
<!-- CTA Button -->
<RowDefinition Height="Auto" />
<!-- Segmented Tabs + FlipView content -->
<RowDefinition Height="*" />
<!-- Footer -->
<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>
</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>
+12 -3
View File
@@ -1,3 +1,5 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using CommunityToolkit.WinUI.Controls;
using Wino.Views.Abstract;
@@ -5,12 +7,19 @@ namespace Wino.Views;
public sealed partial class WelcomePage : WelcomePageAbstract
{
private readonly MarkdownConfig _config;
public WelcomePage()
{
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;
}
}
+152
View File
@@ -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;
}
}
+14
View File
@@ -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>
+37
View File
@@ -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();
}
}
+16
View File
@@ -158,6 +158,13 @@
<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-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\Clouds.jpg" />
<None Remove="BackgroundImages\Forest.jpg" />
@@ -202,6 +209,15 @@
<Content Include="AppThemes\Snowflake.xaml" />
<Content Include="AppThemes\TestTheme.xaml" />
<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="BackgroundImages\Acrylic.jpg" />
<Content Include="BackgroundImages\Clouds.jpg" />