From db5ecd60e4a046c85ff871953892ed888e2264ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Thu, 5 Mar 2026 10:12:03 +0100 Subject: [PATCH] New startup window. --- Instructions/WelcomePageInstructions.md | 16 ++ Wino.Core.Domain/BasicTypesJsonContext.cs | 1 + Wino.Core.Domain/Enums/WinoPage.cs | 3 +- .../Interfaces/INewThemeService.cs | 6 + Wino.Core.Domain/Interfaces/IUpdateManager.cs | 3 + .../Models/Updates/UpdateMigration.cs | 12 + .../Models/Updates/UpdateNotes.cs | 7 +- .../Translations/en_US/resources.json | 20 +- Wino.Core.Domain/Translator.cs | 5 +- .../AccountManagementViewModel.cs | 2 + .../WelcomePageV2ViewModel.cs | 45 ++++ Wino.Mail.ViewModels/WelcomePageViewModel.cs | 52 +++- Wino.Mail.WinUI/App.xaml.cs | 152 ++++++++++- .../Assets/UpdateNotes/Images/Calendar.svg | 54 ++++ .../Assets/UpdateNotes/Images/Customize.svg | 49 ++++ .../Assets/UpdateNotes/Images/Mail.svg | 51 ++++ .../Assets/UpdateNotes/Images/More.svg | 48 ++++ .../UpdateNotes/Images/Notification.svg | 45 ++++ .../Assets/UpdateNotes/Images/Security.svg | 43 ++++ .../Assets/UpdateNotes/Images/Thread.svg | 49 ++++ .../Assets/UpdateNotes/features.json | 44 ++++ Wino.Mail.WinUI/Assets/UpdateNotes/vnext.json | 16 +- .../Controls/UpdateNotesFlipViewControl.xaml | 50 ++++ .../UpdateNotesFlipViewControl.xaml.cs | 54 ++++ Wino.Mail.WinUI/CoreUWPContainerSetup.cs | 1 + Wino.Mail.WinUI/Dialogs/WhatIsNewDialog.xaml | 72 +++--- .../Dialogs/WhatIsNewDialog.xaml.cs | 91 ++++--- .../Interfaces/IWinoWindowManager.cs | 24 ++ Wino.Mail.WinUI/Models/WinoWindowKind.cs | 7 + Wino.Mail.WinUI/Services/NavigationService.cs | 65 ++++- .../Services/NavigationServiceBase.cs | 6 +- Wino.Mail.WinUI/Services/NewThemeService.cs | 75 ++++-- Wino.Mail.WinUI/Services/WinoWindowManager.cs | 235 +++++++++++++++++ Wino.Mail.WinUI/ShellWindow.xaml.cs | 4 +- .../Views/Abstract/WelcomePageV2Abstract.cs | 9 + .../Views/Account/ImapCalDavSettingsPage.xaml | 241 ++++++++++++------ Wino.Mail.WinUI/Views/WelcomePage.xaml | 112 +++++++- Wino.Mail.WinUI/Views/WelcomePage.xaml.cs | 15 +- Wino.Mail.WinUI/Views/WelcomePageV2.xaml | 152 +++++++++++ Wino.Mail.WinUI/Views/WelcomePageV2.xaml.cs | 22 ++ Wino.Mail.WinUI/WelcomeWindow.xaml | 14 + Wino.Mail.WinUI/WelcomeWindow.xaml.cs | 40 +++ Wino.Mail.WinUI/Wino.Mail.WinUI.csproj | 15 ++ .../GetStartedFromWelcomeRequested.cs | 7 + .../Migrations/VNextDelayMigration.cs | 11 + Wino.Services/UpdateManager.cs | 46 +++- 46 files changed, 1857 insertions(+), 234 deletions(-) create mode 100644 Instructions/WelcomePageInstructions.md create mode 100644 Wino.Core.Domain/Models/Updates/UpdateMigration.cs create mode 100644 Wino.Mail.ViewModels/WelcomePageV2ViewModel.cs create mode 100644 Wino.Mail.WinUI/Assets/UpdateNotes/Images/Calendar.svg create mode 100644 Wino.Mail.WinUI/Assets/UpdateNotes/Images/Customize.svg create mode 100644 Wino.Mail.WinUI/Assets/UpdateNotes/Images/Mail.svg create mode 100644 Wino.Mail.WinUI/Assets/UpdateNotes/Images/More.svg create mode 100644 Wino.Mail.WinUI/Assets/UpdateNotes/Images/Notification.svg create mode 100644 Wino.Mail.WinUI/Assets/UpdateNotes/Images/Security.svg create mode 100644 Wino.Mail.WinUI/Assets/UpdateNotes/Images/Thread.svg create mode 100644 Wino.Mail.WinUI/Assets/UpdateNotes/features.json create mode 100644 Wino.Mail.WinUI/Controls/UpdateNotesFlipViewControl.xaml create mode 100644 Wino.Mail.WinUI/Controls/UpdateNotesFlipViewControl.xaml.cs create mode 100644 Wino.Mail.WinUI/Interfaces/IWinoWindowManager.cs create mode 100644 Wino.Mail.WinUI/Models/WinoWindowKind.cs create mode 100644 Wino.Mail.WinUI/Services/WinoWindowManager.cs create mode 100644 Wino.Mail.WinUI/Views/Abstract/WelcomePageV2Abstract.cs create mode 100644 Wino.Mail.WinUI/Views/WelcomePageV2.xaml create mode 100644 Wino.Mail.WinUI/Views/WelcomePageV2.xaml.cs create mode 100644 Wino.Mail.WinUI/WelcomeWindow.xaml create mode 100644 Wino.Mail.WinUI/WelcomeWindow.xaml.cs create mode 100644 Wino.Messages/Client/Navigation/GetStartedFromWelcomeRequested.cs create mode 100644 Wino.Services/Migrations/VNextDelayMigration.cs diff --git a/Instructions/WelcomePageInstructions.md b/Instructions/WelcomePageInstructions.md new file mode 100644 index 00000000..3dc70d2e --- /dev/null +++ b/Instructions/WelcomePageInstructions.md @@ -0,0 +1,16 @@ +This is a design document for new welcome page for users who have no accounts defined in the database. + +Right now we welcome our users in WelcomePage if they have no accounts defined. Navigation is like ShellWindow -> MailAppShell -> WelcomePage. This allows users to access ManageAccountsPage from the side bar since mail app shell has this navigation option. +We don't want this anymore. Here are the instructions to define new welcome page (or initial startup wizard page): + +- ShellWindow should not be used to navigate this new welcome page. +- Create a new WelcomeWindow and navigate to WelcomePage. You can remove all the content inside the existing welcome page because we are doing a new welcome page. +- In App.xaml.cs, we must check if the user has accounts defined or not. If no accounts, create this new window and activate it. +- In this Window I want to highlight a few things. It should clearly say that this is a native Windows application that supports Mail and Calendar. +- Design is not important at the moment, but make sure to follow Windows fluent design guidelines. +- In this page users must be able to + ++ Go to manage accounts page ++ Show the latest version changelog. We have this new "What's new" dialog, but resolve the latest json for the update and show it on the page as well using the same FlipView. You can make this a UserControl maybe. It'll be exactly the same, except "Get started" button should not be visible when the control is loaded in WelcomePage. ++ Some details from the About page like version, github page, donation link etc. would be great. We can show it here in new WelcomePage too. + diff --git a/Wino.Core.Domain/BasicTypesJsonContext.cs b/Wino.Core.Domain/BasicTypesJsonContext.cs index f6085c1c..dbe88a7f 100644 --- a/Wino.Core.Domain/BasicTypesJsonContext.cs +++ b/Wino.Core.Domain/BasicTypesJsonContext.cs @@ -10,5 +10,6 @@ namespace Wino.Core.Domain; [JsonSerializable(typeof(List))] [JsonSerializable(typeof(bool))] [JsonSerializable(typeof(UpdateNotes))] +[JsonSerializable(typeof(UpdateMigration))] [JsonSerializable(typeof(List))] public partial class BasicTypesJsonContext : JsonSerializerContext; diff --git a/Wino.Core.Domain/Enums/WinoPage.cs b/Wino.Core.Domain/Enums/WinoPage.cs index 2787ba3f..c44b0f47 100644 --- a/Wino.Core.Domain/Enums/WinoPage.cs +++ b/Wino.Core.Domain/Enums/WinoPage.cs @@ -34,5 +34,6 @@ public enum WinoPage CalendarAccountSettingsPage, EventDetailsPage, SignatureAndEncryptionPage, - StoragePage + StoragePage, + WelcomePageV2 } diff --git a/Wino.Core.Domain/Interfaces/INewThemeService.cs b/Wino.Core.Domain/Interfaces/INewThemeService.cs index d571d7ac..037b4bee 100644 --- a/Wino.Core.Domain/Interfaces/INewThemeService.cs +++ b/Wino.Core.Domain/Interfaces/INewThemeService.cs @@ -37,4 +37,10 @@ public interface INewThemeService : IInitializeAsync // Backdrop management List GetAvailableBackdropTypes(); + + /// + /// Re-applies the current theme (backdrop, root theme, accent, caption colors) + /// to the currently active window. Use after a window transition. + /// + Task ApplyThemeToActiveWindowAsync(); } diff --git a/Wino.Core.Domain/Interfaces/IUpdateManager.cs b/Wino.Core.Domain/Interfaces/IUpdateManager.cs index 9a593439..cb5ecef9 100644 --- a/Wino.Core.Domain/Interfaces/IUpdateManager.cs +++ b/Wino.Core.Domain/Interfaces/IUpdateManager.cs @@ -9,6 +9,9 @@ public interface IUpdateManager /// Loads and parses the update notes for the current version from the bundled asset file. Task GetLatestUpdateNotesAsync(); + /// Loads and parses the app feature highlights from the bundled asset file. + Task> GetFeaturesAsync(); + /// Returns true if the current version's update notes have not yet been shown to the user. bool ShouldShowUpdateNotes(); diff --git a/Wino.Core.Domain/Models/Updates/UpdateMigration.cs b/Wino.Core.Domain/Models/Updates/UpdateMigration.cs new file mode 100644 index 00000000..205e4dc4 --- /dev/null +++ b/Wino.Core.Domain/Models/Updates/UpdateMigration.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Wino.Core.Domain.Models.Updates; + +public class UpdateMigration +{ + [JsonPropertyName("titleKey")] + public string TitleKey { get; set; } = string.Empty; + + [JsonPropertyName("descriptionKey")] + public string DescriptionKey { get; set; } = string.Empty; +} diff --git a/Wino.Core.Domain/Models/Updates/UpdateNotes.cs b/Wino.Core.Domain/Models/Updates/UpdateNotes.cs index 50e3e708..abab57f1 100644 --- a/Wino.Core.Domain/Models/Updates/UpdateNotes.cs +++ b/Wino.Core.Domain/Models/Updates/UpdateNotes.cs @@ -5,8 +5,11 @@ namespace Wino.Core.Domain.Models.Updates; public class UpdateNotes { - [JsonPropertyName("hasMigrations")] - public bool HasMigrations { get; set; } + [JsonPropertyName("hasPendingMigrations")] + public bool HasPendingMigrations { get; set; } + + [JsonPropertyName("migration")] + public UpdateMigration Migration { get; set; } = new(); [JsonPropertyName("sections")] public List Sections { get; set; } = []; diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index 51de54c8..6a819ed5 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -985,6 +985,24 @@ "CalendarAccountSettings_PrimaryCalendar": "Primary Calendar", "CalendarAccountSettings_PrimaryCalendarDescription": "Mark this calendar as the primary calendar for the account", "WhatIsNew_GetStartedButton": "Get Started", + "WhatIsNew_ContinueAnywayButton": "Continue anyway", + "WhatIsNew_PreparingForNewVersionButton": "Preparing for new version...", + "WhatIsNew_MigrationPreparing_Title": "Preparing your data", + "WhatIsNew_MigrationPreparing_Description": "Wino is applying update migrations. Please wait while we prepare your account data for this release.", + "WhatIsNew_MigrationFailedMessage": "Applying migrations failed with error code {0}. You may continue to use the application. However, if you encounter serious issues please re-install the application.", "WhatIsNew_MigrationNotification_Title": "Wino Mail Updated", - "WhatIsNew_MigrationNotification_Message": "Open the app to complete the update and see what's new." + "WhatIsNew_MigrationNotification_Message": "Open the app to complete the update and see what's new.", + "WelcomeWindow_Title": "Welcome to Wino Mail", + "WelcomeWindow_Subtitle": "A native Windows experience for Mail and Calendar.", + "WelcomeWindow_WhatsNewTitle": "Latest changes", + "WelcomeWindow_FeaturesTitle": "Features", + "WelcomeWindow_WhatsNewTab": "What's New", + "WelcomeWindow_FeaturesTab": "Features", + "WelcomeWindow_GetStartedButton": "Get started by adding an account", + "WelcomeWindow_GetStartedDescription": "Add your Outlook, Gmail, or IMAP account to get started with Wino Mail.", + "WelcomeWindow_SetupTitle": "Set up your account", + "WelcomeWindow_SetupSubtitle": "Choose your email provider to get started", + "WelcomeWindow_AddAccountButton": "Add account", + "WelcomeWindow_SkipForNow": "Skip for now — I'll set it up later", + "WelcomeWindow_AppDescription": "A fast, focused inbox — redesigned for Windows 11" } diff --git a/Wino.Core.Domain/Translator.cs b/Wino.Core.Domain/Translator.cs index 1fc2fb4c..9ada00c6 100644 --- a/Wino.Core.Domain/Translator.cs +++ b/Wino.Core.Domain/Translator.cs @@ -7,4 +7,7 @@ namespace Wino.Core.Domain; /// All translations generated automatically by the source generator. /// [TranslatorGen] -public partial class Translator; +public partial class Translator +{ + public static string GetTranslatedString(string key) => Resources.GetTranslatedString(key); +} diff --git a/Wino.Mail.ViewModels/AccountManagementViewModel.cs b/Wino.Mail.ViewModels/AccountManagementViewModel.cs index 63f1e875..ba566492 100644 --- a/Wino.Mail.ViewModels/AccountManagementViewModel.cs +++ b/Wino.Mail.ViewModels/AccountManagementViewModel.cs @@ -331,6 +331,8 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel } } + public Task StartAddNewAccountAsync() => AddNewAccountAsync(); + private async Task ValidateSpecialImapConnectivityAsync(CustomServerInformation serverInformation) { var connectivityResult = await SynchronizationManager.Instance diff --git a/Wino.Mail.ViewModels/WelcomePageV2ViewModel.cs b/Wino.Mail.ViewModels/WelcomePageV2ViewModel.cs new file mode 100644 index 00000000..3e086a92 --- /dev/null +++ b/Wino.Mail.ViewModels/WelcomePageV2ViewModel.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Navigation; +using Wino.Core.Domain.Models.Updates; +using Wino.Messaging.Client.Navigation; + +namespace Wino.Mail.ViewModels; + +public partial class WelcomePageV2ViewModel : MailBaseViewModel +{ + private readonly IUpdateManager _updateManager; + + [ObservableProperty] + public partial List UpdateSections { get; set; } = []; + + public WelcomePageV2ViewModel(IUpdateManager updateManager) + { + _updateManager = updateManager; + } + + public override async void OnNavigatedTo(NavigationMode mode, object parameters) + { + base.OnNavigatedTo(mode, parameters); + + try + { + var updateNotes = await _updateManager.GetLatestUpdateNotesAsync(); + UpdateSections = updateNotes.Sections; + } + catch (Exception) + { + UpdateSections = []; + } + } + + [RelayCommand] + private void GetStarted() + { + Messenger.Send(new GetStartedFromWelcomeRequested()); + } +} diff --git a/Wino.Mail.ViewModels/WelcomePageViewModel.cs b/Wino.Mail.ViewModels/WelcomePageViewModel.cs index 4893d0e1..9a38fb66 100644 --- a/Wino.Mail.ViewModels/WelcomePageViewModel.cs +++ b/Wino.Mail.ViewModels/WelcomePageViewModel.cs @@ -1,37 +1,69 @@ -using System; +using System; +using System.Collections.Generic; using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using Wino.Core.Domain; using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Enums; using Wino.Core.Domain.Models.Navigation; +using Wino.Core.Domain.Models.Updates; namespace Wino.Mail.ViewModels; public partial class WelcomePageViewModel : MailBaseViewModel { - public const string VersionFile = "vnext.md"; - private readonly IMailDialogService _dialogService; - private readonly IFileService _fileService; + private readonly IUpdateManager _updateManager; + private readonly INativeAppService _nativeAppService; + private readonly INavigationService _navigationService; [ObservableProperty] - public partial string CurrentVersionNotes { get; set; } = string.Empty; + public partial string VersionDisplay { get; set; } = string.Empty; - public WelcomePageViewModel(IMailDialogService dialogService, IFileService fileService) + [ObservableProperty] + public partial List UpdateSections { get; set; } = []; + + [ObservableProperty] + public partial List FeatureSections { get; set; } = []; + + public string GitHubUrl => "https://github.com/bkaankose/Wino-Mail/"; + public string PaypalUrl => "https://paypal.me/bkaankose?country.x=PL&locale.x=en_US"; + + public WelcomePageViewModel(IUpdateManager updateManager, + INativeAppService nativeAppService, + INavigationService navigationService) { - _dialogService = dialogService; - _fileService = fileService; + _updateManager = updateManager; + _nativeAppService = nativeAppService; + _navigationService = navigationService; } public override async void OnNavigatedTo(NavigationMode mode, object parameters) { base.OnNavigatedTo(mode, parameters); + VersionDisplay = $"{Translator.SettingsAboutVersion}{_nativeAppService.GetFullAppVersion()}"; + try { - CurrentVersionNotes = await _fileService.GetFileContentByApplicationUriAsync($"ms-appx:///Assets/ReleaseNotes/{VersionFile}"); + var updateNotes = await _updateManager.GetLatestUpdateNotesAsync(); + UpdateSections = updateNotes.Sections; } catch (Exception) { - _dialogService.InfoBarMessage(Translator.GeneralTitle_Error, "Can't find the patch notes.", Core.Domain.Enums.InfoBarMessageType.Information); + UpdateSections = []; + } + + try + { + FeatureSections = await _updateManager.GetFeaturesAsync(); + } + catch (Exception) + { + FeatureSections = []; } } + + [RelayCommand] + private void NavigateManageAccounts() + => _navigationService.Navigate(WinoPage.ManageAccountsPage, null, NavigationReferenceFrame.ShellFrame, NavigationTransitionType.DrillIn); } diff --git a/Wino.Mail.WinUI/App.xaml.cs b/Wino.Mail.WinUI/App.xaml.cs index 870bb2a3..526c0b13 100644 --- a/Wino.Mail.WinUI/App.xaml.cs +++ b/Wino.Mail.WinUI/App.xaml.cs @@ -21,14 +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.Core.Domain.Models.Updates; using Wino.Mail.Services; using Wino.Mail.ViewModels; 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; @@ -37,13 +40,16 @@ namespace Wino.Mail.WinUI; public partial class App : WinoApplication, IRecipient, - IRecipient + IRecipient, + IRecipient, + IRecipient { 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 _inboxSyncCounters = []; @@ -58,6 +64,52 @@ public partial class App : WinoApplication, RegisterRecipients(); } + private void EnsureWindowManagerConfigured() + { + if (_windowManagerConfigured) + return; + + var windowManager = Services.GetRequiredService(); + windowManager.ActiveWindowChanged -= OnActiveWindowChanged; + windowManager.ActiveWindowChanged += OnActiveWindowChanged; + windowManager.WindowRemoved -= OnManagedWindowRemoved; + windowManager.WindowRemoved += OnManagedWindowRemoved; + + var nativeAppService = Services.GetRequiredService(); + nativeAppService.GetCoreWindowHwnd = () => + { + var window = windowManager.ActiveWindow + ?? windowManager.GetWindow(WinoWindowKind.Shell) + ?? windowManager.GetWindow(WinoWindowKind.Welcome) + ?? MainWindow; + + return window == null + ? IntPtr.Zero + : WinRT.Interop.WindowNative.GetWindowHandle(window); + }; + + _windowManagerConfigured = true; + } + + private void OnActiveWindowChanged(object? sender, WindowEx? window) + { + if (window == null) + return; + + MainWindow = window; + InitializeNavigationDispatcher(); + } + + private void OnManagedWindowRemoved(object? sender, WindowEx window) + { + var windowManager = Services.GetRequiredService(); + MainWindow = windowManager.ActiveWindow + ?? windowManager.GetWindow(WinoWindowKind.Shell) + ?? windowManager.GetWindow(WinoWindowKind.Welcome); + + InitializeNavigationDispatcher(); + } + public bool IsNotificationActivation(out AppNotificationActivatedEventArgs args) { var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs(); @@ -93,6 +145,7 @@ public partial class App : WinoApplication, services.AddTransient(typeof(MailRenderingPageViewModel)); services.AddTransient(typeof(AccountManagementViewModel)); services.AddTransient(typeof(WelcomePageViewModel)); + services.AddTransient(typeof(WelcomePageV2ViewModel)); services.AddTransient(typeof(ComposePageViewModel)); services.AddTransient(typeof(IdlePageViewModel)); @@ -153,11 +206,22 @@ public partial class App : WinoApplication, _synchronizationManager = Services.GetRequiredService(); _preferencesService = Services.GetRequiredService(); _accountService = Services.GetRequiredService(); + EnsureWindowManagerConfigured(); + + var hasAnyAccount = (await _accountService.GetAccountsAsync()).Any(); + if (!IsStartupTaskLaunch() && !hasAnyAccount) + { + CreateWelcomeWindow(); + await NewThemeService.InitializeAsync(); + MainWindow?.Activate(); + LogActivation("Welcome window created and activated."); + return; + } // Check whether the new version requires a migration before starting sync. var updateManager = Services.GetRequiredService(); var updateNotes = await updateManager.GetLatestUpdateNotesAsync(); - bool hasPendingMigration = updateNotes.HasMigrations && updateManager.HasPendingMigrations(); + bool hasPendingMigration = updateNotes.HasPendingMigrations && updateManager.HasPendingMigrations(); _preferencesService.PreferenceChanged -= PreferencesServiceChanged; _preferencesService.PreferenceChanged += PreferencesServiceChanged; @@ -462,10 +526,19 @@ public partial class App : WinoApplication, // Initialize theme service after window is created. await NewThemeService.InitializeAsync(); - MainWindow?.Activate(); + if (MainWindow != null) + Services.GetRequiredService().ActivateWindow(MainWindow); + LogActivation("Window created and activated."); } + public Task OpenManageAccountsFromWelcomeAsync() + { + Services.GetRequiredService() + .Navigate(WinoPage.ManageAccountsPage, null, NavigationReferenceFrame.ShellFrame, NavigationTransitionType.DrillIn); + return Task.CompletedTask; + } + /// /// Creates the main window without activating it. /// Used for both normal launch and startup task launch (tray only). @@ -474,15 +547,15 @@ public partial class App : WinoApplication, { LogActivation("Creating main window."); - MainWindow = new ShellWindow(); + var windowManager = Services.GetRequiredService(); + MainWindow = windowManager.CreateWindow(WinoWindowKind.Shell, () => new ShellWindow()); InitializeNavigationDispatcher(); - var nativeAppService = Services.GetRequiredService(); - 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 && @@ -508,6 +581,21 @@ public partial class App : WinoApplication, shellWindow.HandleAppActivation(args?.Arguments, GetCurrentLaunchTileId(), Environment.CommandLine); } + private void CreateWelcomeWindow() + { + LogActivation("Creating welcome window."); + + var windowManager = Services.GetRequiredService(); + MainWindow = windowManager.CreateWindow(WinoWindowKind.Welcome, () => new WelcomeWindow()); + if (MainWindow is WelcomeWindow welcomeWindow) + windowManager.SetPrimaryNavigationFrame(WinoWindowKind.Welcome, welcomeWindow.GetRootFrame()); + + InitializeNavigationDispatcher(); + + Services.GetRequiredService() + .Navigate(WinoPage.WelcomePageV2, null, NavigationReferenceFrame.ShellFrame, NavigationTransitionType.None); + } + private void InitializeNavigationDispatcher() { if (MainWindow == null) @@ -521,18 +609,25 @@ public partial class App : WinoApplication, private void EnsureMainWindowVisibleAndForeground() { - if (MainWindow == null) + var windowManager = Services.GetRequiredService(); + 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(this); WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); } public async void Receive(NewMailSynchronizationRequested message) @@ -585,6 +680,41 @@ public partial class App : WinoApplication, } } + public void Receive(AccountCreatedMessage message) + { + var windowManager = Services.GetRequiredService(); + + // Only transition when the account was created from the WelcomeWindow. + if (windowManager.GetWindow(WinoWindowKind.Welcome) == null) + return; + + MainWindow?.DispatcherQueue?.TryEnqueue(async () => + { + // Create and activate ShellWindow — ActiveWindowChanged fires and rebinds the dispatcher. + CreateWindow(null); + windowManager.HideWindow(WinoWindowKind.Welcome); + await NewThemeService.ApplyThemeToActiveWindowAsync(); + MainWindow?.Activate(); + RestartAutoSynchronizationLoop(); + }); + } + + public void Receive(GetStartedFromWelcomeRequested message) + { + var windowManager = Services.GetRequiredService(); + + if (windowManager.GetWindow(WinoWindowKind.Welcome) == null) + return; + + MainWindow?.DispatcherQueue?.TryEnqueue(async () => + { + CreateWindow(null); + windowManager.HideWindow(WinoWindowKind.Welcome); + await NewThemeService.ApplyThemeToActiveWindowAsync(); + MainWindow?.Activate(); + }); + } + private static string GetSynchronizationFailureMessage(MailSynchronizationType synchronizationType, string? exceptionMessage) { if (!string.IsNullOrWhiteSpace(exceptionMessage)) diff --git a/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Calendar.svg b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Calendar.svg new file mode 100644 index 00000000..40bd60e7 --- /dev/null +++ b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Calendar.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MARCH + + + + + + + + + + + + + + + + + + + + diff --git a/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Customize.svg b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Customize.svg new file mode 100644 index 00000000..5667a4f3 --- /dev/null +++ b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Customize.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Mail.svg b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Mail.svg new file mode 100644 index 00000000..7a551879 --- /dev/null +++ b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Mail.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + G + + + O + + + @ + diff --git a/Wino.Mail.WinUI/Assets/UpdateNotes/Images/More.svg b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/More.svg new file mode 100644 index 00000000..e62b98fe --- /dev/null +++ b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/More.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Notification.svg b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Notification.svg new file mode 100644 index 00000000..1a298e70 --- /dev/null +++ b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Notification.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 3 + + + + Archive + + + Delete + diff --git a/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Security.svg b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Security.svg new file mode 100644 index 00000000..8ec6fbfa --- /dev/null +++ b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Security.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Thread.svg b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Thread.svg new file mode 100644 index 00000000..48723e7b --- /dev/null +++ b/Wino.Mail.WinUI/Assets/UpdateNotes/Images/Thread.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wino.Mail.WinUI/Assets/UpdateNotes/features.json b/Wino.Mail.WinUI/Assets/UpdateNotes/features.json new file mode 100644 index 00000000..07eb1dad --- /dev/null +++ b/Wino.Mail.WinUI/Assets/UpdateNotes/features.json @@ -0,0 +1,44 @@ +[ + { + "title": "# Outlook, Gmail & IMAP/SMTP", + "description": "Connect all your email accounts in one place. Wino supports Microsoft Outlook (Office 365), Gmail, and any standard IMAP/SMTP server — giving you a unified inbox experience.", + "imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Mail.svg", + "imageWidth": 128, + "imageHeight": 128 + }, + { + "title": "# Calendar with CalDAV Support", + "description": "Manage your schedule alongside your emails. Create local calendars or connect to remote CalDAV-compatible servers. View, create, and respond to events and invitations.", + "imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Calendar.svg", + "imageWidth": 128, + "imageHeight": 128 + }, + { + "title": "# S/MIME Signing & Encryption", + "description": "Keep your communications secure. Sign and encrypt emails with personal certificates to ensure your messages are authentic and private.", + "imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Security.svg", + "imageWidth": 128, + "imageHeight": 128 + }, + { + "title": "# Conversation Threading", + "description": "Follow discussions with ease. Emails are grouped by conversation thread so you never lose context in long email chains.", + "imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Thread.svg", + "imageWidth": 128, + "imageHeight": 128 + }, + { + "title": "# Actionable Notifications", + "description": "Stay productive without switching apps. Mark as read, delete, or archive emails directly from Windows toast notifications.", + "imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Notification.svg", + "imageWidth": 128, + "imageHeight": 128 + }, + { + "title": "# Rich Customization", + "description": "Make Wino yours. Choose from built-in themes, create custom themes with your own wallpapers and accent colors, configure swipe actions, and set keyboard shortcuts.", + "imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Customize.svg", + "imageWidth": 128, + "imageHeight": 128 + } +] diff --git a/Wino.Mail.WinUI/Assets/UpdateNotes/vnext.json b/Wino.Mail.WinUI/Assets/UpdateNotes/vnext.json index 628505ad..a1509ff4 100644 --- a/Wino.Mail.WinUI/Assets/UpdateNotes/vnext.json +++ b/Wino.Mail.WinUI/Assets/UpdateNotes/vnext.json @@ -1,38 +1,42 @@ { - "hasMigrations": false, + "hasPendingMigrations": true, + "migration": { + "titleKey": "WhatIsNew_MigrationPreparing_Title", + "descriptionKey": "WhatIsNew_MigrationPreparing_Description" + }, "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/AboutPageTile.png", + "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/AboutPageTile.png", + "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/AboutPageTile.png", + "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/AboutPageTile.png", + "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/AboutPageTile.png", + "imageUrl": "ms-appx:///Assets/UpdateNotes/Images/More.svg", "imageWidth": 128, "imageHeight": 128 } diff --git a/Wino.Mail.WinUI/Controls/UpdateNotesFlipViewControl.xaml b/Wino.Mail.WinUI/Controls/UpdateNotesFlipViewControl.xaml new file mode 100644 index 00000000..6e8f1203 --- /dev/null +++ b/Wino.Mail.WinUI/Controls/UpdateNotesFlipViewControl.xaml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wino.Mail.WinUI/Controls/UpdateNotesFlipViewControl.xaml.cs b/Wino.Mail.WinUI/Controls/UpdateNotesFlipViewControl.xaml.cs new file mode 100644 index 00000000..75f236ff --- /dev/null +++ b/Wino.Mail.WinUI/Controls/UpdateNotesFlipViewControl.xaml.cs @@ -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? SelectedIndexChanged; + + public int SelectedIndex => UpdateFlipView.SelectedIndex; + + public IList? Sections + { + get { return (IList?)GetValue(SectionsProperty); } + set { SetValue(SectionsProperty, value); } + } + + public static readonly DependencyProperty SectionsProperty = + DependencyProperty.Register(nameof(Sections), + typeof(IList), + 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; + } +} diff --git a/Wino.Mail.WinUI/CoreUWPContainerSetup.cs b/Wino.Mail.WinUI/CoreUWPContainerSetup.cs index 6cbae79b..12780859 100644 --- a/Wino.Mail.WinUI/CoreUWPContainerSetup.cs +++ b/Wino.Mail.WinUI/CoreUWPContainerSetup.cs @@ -18,6 +18,7 @@ public static class CoreUWPContainerSetup services.AddSingleton(provider => provider.GetRequiredService()); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/Wino.Mail.WinUI/Dialogs/WhatIsNewDialog.xaml b/Wino.Mail.WinUI/Dialogs/WhatIsNewDialog.xaml index fb7c1c14..c9dcfffe 100644 --- a/Wino.Mail.WinUI/Dialogs/WhatIsNewDialog.xaml +++ b/Wino.Mail.WinUI/Dialogs/WhatIsNewDialog.xaml @@ -2,11 +2,10 @@ 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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:domain="using:Wino.Core.Domain" - xmlns:controls="using:CommunityToolkit.WinUI.Controls" - xmlns:models="using:Wino.Core.Domain.Models.Updates" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" Style="{StaticResource WinoDialogStyle}" @@ -26,45 +25,42 @@ - - - - - - - - - - - - - + - + Spacing="8" + Visibility="Collapsed"> + + + + + - + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + diff --git a/Wino.Mail.WinUI/Views/WelcomePage.xaml.cs b/Wino.Mail.WinUI/Views/WelcomePage.xaml.cs index 0e7c14e7..93440684 100644 --- a/Wino.Mail.WinUI/Views/WelcomePage.xaml.cs +++ b/Wino.Mail.WinUI/Views/WelcomePage.xaml.cs @@ -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; } } diff --git a/Wino.Mail.WinUI/Views/WelcomePageV2.xaml b/Wino.Mail.WinUI/Views/WelcomePageV2.xaml new file mode 100644 index 00000000..989513e2 --- /dev/null +++ b/Wino.Mail.WinUI/Views/WelcomePageV2.xaml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wino.Mail.WinUI/Views/WelcomePageV2.xaml.cs b/Wino.Mail.WinUI/Views/WelcomePageV2.xaml.cs new file mode 100644 index 00000000..908f3e01 --- /dev/null +++ b/Wino.Mail.WinUI/Views/WelcomePageV2.xaml.cs @@ -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; + } +} diff --git a/Wino.Mail.WinUI/WelcomeWindow.xaml b/Wino.Mail.WinUI/WelcomeWindow.xaml new file mode 100644 index 00000000..f4b46e4e --- /dev/null +++ b/Wino.Mail.WinUI/WelcomeWindow.xaml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/Wino.Mail.WinUI/WelcomeWindow.xaml.cs b/Wino.Mail.WinUI/WelcomeWindow.xaml.cs new file mode 100644 index 00000000..0f192d14 --- /dev/null +++ b/Wino.Mail.WinUI/WelcomeWindow.xaml.cs @@ -0,0 +1,40 @@ +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.IsResizable = false; + //this.IsMaximizable = false; + + this.CenterOnScreen(); + + var themeService = WinoApplication.Current.Services.GetService(); + themeService?.UpdateSystemCaptionButtonColors(); + } +} diff --git a/Wino.Mail.WinUI/Wino.Mail.WinUI.csproj b/Wino.Mail.WinUI/Wino.Mail.WinUI.csproj index 5ebfc99d..da387108 100644 --- a/Wino.Mail.WinUI/Wino.Mail.WinUI.csproj +++ b/Wino.Mail.WinUI/Wino.Mail.WinUI.csproj @@ -98,6 +98,13 @@ + + + + + + + @@ -143,6 +150,14 @@ + + + + + + + + diff --git a/Wino.Messages/Client/Navigation/GetStartedFromWelcomeRequested.cs b/Wino.Messages/Client/Navigation/GetStartedFromWelcomeRequested.cs new file mode 100644 index 00000000..58b11c00 --- /dev/null +++ b/Wino.Messages/Client/Navigation/GetStartedFromWelcomeRequested.cs @@ -0,0 +1,7 @@ +namespace Wino.Messaging.Client.Navigation; + +/// +/// User clicked "Get Started" on the welcome page. +/// App should close the welcome window and open the shell window. +/// +public record GetStartedFromWelcomeRequested; diff --git a/Wino.Services/Migrations/VNextDelayMigration.cs b/Wino.Services/Migrations/VNextDelayMigration.cs new file mode 100644 index 00000000..16790661 --- /dev/null +++ b/Wino.Services/Migrations/VNextDelayMigration.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Wino.Core.Domain.Interfaces; + +namespace Wino.Services.Migrations; + +public class VNextDelayMigration : IAppMigration +{ + public string MigrationId => "vnext-delay"; + + public Task ExecuteAsync() => Task.Delay(3000); +} diff --git a/Wino.Services/UpdateManager.cs b/Wino.Services/UpdateManager.cs index f7bff55a..b5be9131 100644 --- a/Wino.Services/UpdateManager.cs +++ b/Wino.Services/UpdateManager.cs @@ -6,12 +6,14 @@ using System.Threading.Tasks; using Wino.Core.Domain; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Updates; +using Wino.Services.Migrations; namespace Wino.Services; public class UpdateManager : IUpdateManager { private const string UpdateNotesResourcePath = "ms-appx:///Assets/UpdateNotes/vnext.json"; + private const string FeaturesResourcePath = "ms-appx:///Assets/UpdateNotes/features.json"; private const string UpdateNotesSeenKeyFormat = "UpdateNotes_{0}_Shown"; private const string MigrationCompletedKeyFormat = "Migration_{0}_Completed"; @@ -21,6 +23,7 @@ public class UpdateManager : IUpdateManager private readonly List _migrations = []; private string _versionSeenKey = string.Empty; + private UpdateNotes _latestUpdateNotes = new(); public UpdateManager(IFileService fileService, IConfigurationService configurationService, @@ -29,6 +32,7 @@ public class UpdateManager : IUpdateManager _fileService = fileService; _configurationService = configurationService; _nativeAppService = nativeAppService; + _migrations.Add(new VNextDelayMigration()); } private string GetVersionSeenKey() @@ -50,27 +54,61 @@ public class UpdateManager : IUpdateManager var json = await _fileService.GetFileContentByApplicationUriAsync(UpdateNotesResourcePath); if (string.IsNullOrEmpty(json)) - return new UpdateNotes(); + { + _latestUpdateNotes = new UpdateNotes(); + return _latestUpdateNotes; + } - return JsonSerializer.Deserialize(json, BasicTypesJsonContext.Default.UpdateNotes) ?? new UpdateNotes(); + _latestUpdateNotes = JsonSerializer.Deserialize(json, BasicTypesJsonContext.Default.UpdateNotes) ?? new UpdateNotes(); + return _latestUpdateNotes; } catch (Exception) { - return new UpdateNotes(); + _latestUpdateNotes = new UpdateNotes(); + return _latestUpdateNotes; } } + // T public bool ShouldShowUpdateNotes() => !_configurationService.Get(GetVersionSeenKey(), false); + public async Task> GetFeaturesAsync() + { + try + { + var json = await _fileService.GetFileContentByApplicationUriAsync(FeaturesResourcePath); + + if (string.IsNullOrEmpty(json)) + return []; + + return JsonSerializer.Deserialize(json, BasicTypesJsonContext.Default.ListUpdateNoteSection) ?? []; + } + catch (Exception) + { + return []; + } + } + public void MarkUpdateNotesAsSeen() => _configurationService.Set(GetVersionSeenKey(), true); public bool HasPendingMigrations() - => _migrations.Any(m => !_configurationService.Get(string.Format(MigrationCompletedKeyFormat, m.MigrationId), false)); + { + if (!_latestUpdateNotes.HasPendingMigrations) + return false; + + return _migrations.Any(m => !_configurationService.Get(string.Format(MigrationCompletedKeyFormat, m.MigrationId), false)); + } public async Task RunPendingMigrationsAsync() { + if (!_latestUpdateNotes.HasPendingMigrations) + _latestUpdateNotes = await GetLatestUpdateNotesAsync(); + + if (!_latestUpdateNotes.HasPendingMigrations) + return; + foreach (var migration in _migrations) { var key = string.Format(MigrationCompletedKeyFormat, migration.MigrationId);