Whats new implementation.
This commit is contained in:
@@ -72,10 +72,14 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
IAccountService accountService,
|
||||
ICalendarService calendarService,
|
||||
IAccountCalendarStateService accountCalendarStateService,
|
||||
INavigationService navigationService)
|
||||
INavigationService navigationService,
|
||||
IDialogServiceBase dialogService,
|
||||
IUpdateManager updateManager)
|
||||
{
|
||||
_accountService = accountService;
|
||||
_calendarService = calendarService;
|
||||
_dialogService = dialogService;
|
||||
_updateManager = updateManager;
|
||||
|
||||
AccountCalendarStateService = accountCalendarStateService;
|
||||
AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested;
|
||||
@@ -123,9 +127,24 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
|
||||
await InitializeAccountCalendarsAsync();
|
||||
|
||||
await ShowWhatIsNewIfNeededAsync();
|
||||
|
||||
TodayClicked();
|
||||
}
|
||||
|
||||
private async Task ShowWhatIsNewIfNeededAsync()
|
||||
{
|
||||
if (!_updateManager.ShouldShowUpdateNotes())
|
||||
return;
|
||||
|
||||
var notes = await _updateManager.GetLatestUpdateNotesAsync();
|
||||
|
||||
if (notes.Sections.Count == 0)
|
||||
return;
|
||||
|
||||
await _dialogService.ShowWhatIsNewDialogAsync(notes);
|
||||
}
|
||||
|
||||
private async void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e)
|
||||
{
|
||||
// When using three-state checkbox, multiple accounts will be selected/unselected at the same time.
|
||||
@@ -271,6 +290,8 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
private DateTime? _navigationDate;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly ICalendarService _calendarService;
|
||||
private readonly IDialogServiceBase _dialogService;
|
||||
private readonly IUpdateManager _updateManager;
|
||||
|
||||
#region Commands
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Wino.Core.Domain.Models.Updates;
|
||||
|
||||
namespace Wino.Core.Domain;
|
||||
|
||||
@@ -8,4 +9,6 @@ namespace Wino.Core.Domain;
|
||||
[JsonSerializable(typeof(int))]
|
||||
[JsonSerializable(typeof(List<string>))]
|
||||
[JsonSerializable(typeof(bool))]
|
||||
[JsonSerializable(typeof(UpdateNotes))]
|
||||
[JsonSerializable(typeof(List<UpdateNoteSection>))]
|
||||
public partial class BasicTypesJsonContext : JsonSerializerContext;
|
||||
|
||||
@@ -19,6 +19,7 @@ public static class Constants
|
||||
public const string ToastModeKey = nameof(ToastModeKey);
|
||||
public const string ToastModeMail = nameof(ToastModeMail);
|
||||
public const string ToastModeCalendar = nameof(ToastModeCalendar);
|
||||
public const string ToastMigrationRequiredKey = nameof(ToastMigrationRequiredKey);
|
||||
|
||||
public const string ClientLogFile = "Client_.log";
|
||||
public const string ServerLogFile = "Server_.log";
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Wino.Core.Domain.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a one-time app or data migration that runs when a user updates to a new version.
|
||||
/// </summary>
|
||||
public interface IAppMigration
|
||||
{
|
||||
/// <summary>Gets the unique identifier for this migration, used to track completion in local settings.</summary>
|
||||
string MigrationId { get; }
|
||||
|
||||
/// <summary>Executes the migration logic.</summary>
|
||||
Task ExecuteAsync();
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
using Wino.Core.Domain.Models.Common;
|
||||
using Wino.Core.Domain.Models.Printing;
|
||||
using Wino.Core.Domain.Models.Updates;
|
||||
|
||||
namespace Wino.Core.Domain.Interfaces;
|
||||
|
||||
@@ -30,4 +31,10 @@ public interface IDialogServiceBase
|
||||
Task<List<SharedFile>> PickFilesAsync(params object[] typeFilters);
|
||||
Task<string> PickFilePathAsync(string saveFileName);
|
||||
Task<WebView2PrintSettingsModel> ShowPrintDialogAsync(WebView2PrintSettingsModel initialSettings = null);
|
||||
|
||||
/// <summary>
|
||||
/// Presents the "What's New" dialog for the current version.
|
||||
/// This dialog is undismissable and runs any pending migrations when the user clicks "Get Started".
|
||||
/// </summary>
|
||||
Task ShowWhatIsNewDialogAsync(UpdateNotes notes);
|
||||
}
|
||||
|
||||
@@ -40,4 +40,10 @@ public interface INotificationBuilder
|
||||
/// Creates a calendar reminder toast for the specified calendar item.
|
||||
/// </summary>
|
||||
Task CreateCalendarReminderNotificationAsync(CalendarItem calendarItem, long reminderDurationInSeconds);
|
||||
|
||||
/// <summary>
|
||||
/// Shows a notification that a migration is required for the new app version.
|
||||
/// Synchronization is stopped and the user is prompted to open the app.
|
||||
/// </summary>
|
||||
void CreateMigrationRequiredNotification();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Domain.Models.Updates;
|
||||
|
||||
namespace Wino.Core.Domain.Interfaces;
|
||||
|
||||
public interface IUpdateManager
|
||||
{
|
||||
/// <summary>Loads and parses the update notes for the current version from the bundled asset file.</summary>
|
||||
Task<UpdateNotes> GetLatestUpdateNotesAsync();
|
||||
|
||||
/// <summary>Returns true if the current version's update notes have not yet been shown to the user.</summary>
|
||||
bool ShouldShowUpdateNotes();
|
||||
|
||||
/// <summary>Stores a flag in local settings indicating the update notes for the current version have been seen.</summary>
|
||||
void MarkUpdateNotesAsSeen();
|
||||
|
||||
/// <summary>Returns true if any registered migration has not yet been completed.</summary>
|
||||
bool HasPendingMigrations();
|
||||
|
||||
/// <summary>Runs all pending migrations in order and marks each as completed in local settings.</summary>
|
||||
Task RunPendingMigrationsAsync();
|
||||
|
||||
/// <summary>Registers migrations to be tracked and executed by this manager.</summary>
|
||||
void RegisterMigrations(IEnumerable<IAppMigration> migrations);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Updates;
|
||||
|
||||
public class UpdateNoteSection
|
||||
{
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("imageUrl")]
|
||||
public string ImageUrl { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("imageWidth")]
|
||||
public double? ImageWidth { get; set; }
|
||||
|
||||
[JsonPropertyName("imageHeight")]
|
||||
public double? ImageHeight { get; set; }
|
||||
|
||||
/// <summary>Gets the image width for binding, returning NaN for auto-sizing when not specified.</summary>
|
||||
public double ActualImageWidth => ImageWidth ?? double.NaN;
|
||||
|
||||
/// <summary>Gets the image height for binding, returning NaN for auto-sizing when not specified.</summary>
|
||||
public double ActualImageHeight => ImageHeight ?? double.NaN;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Updates;
|
||||
|
||||
public class UpdateNotes
|
||||
{
|
||||
[JsonPropertyName("hasMigrations")]
|
||||
public bool HasMigrations { get; set; }
|
||||
|
||||
[JsonPropertyName("sections")]
|
||||
public List<UpdateNoteSection> Sections { get; set; } = [];
|
||||
}
|
||||
@@ -983,5 +983,8 @@
|
||||
"CalendarAccountSettings_DefaultShowAs": "Default Show As Status",
|
||||
"CalendarAccountSettings_DefaultShowAsDescription": "Default availability status for new events created with this account",
|
||||
"CalendarAccountSettings_PrimaryCalendar": "Primary Calendar",
|
||||
"CalendarAccountSettings_PrimaryCalendarDescription": "Mark this calendar as the primary calendar for the account"
|
||||
"CalendarAccountSettings_PrimaryCalendarDescription": "Mark this calendar as the primary calendar for the account",
|
||||
"WhatIsNew_GetStartedButton": "Get Started",
|
||||
"WhatIsNew_MigrationNotification_Title": "Wino Mail Updated",
|
||||
"WhatIsNew_MigrationNotification_Message": "Open the app to complete the update and see what's new."
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
||||
private readonly IMailDialogService _dialogService;
|
||||
private readonly IMimeFileService _mimeFileService;
|
||||
private readonly IWebView2RuntimeValidatorService _webView2RuntimeValidatorService;
|
||||
private readonly IUpdateManager _updateManager;
|
||||
|
||||
private readonly INativeAppService _nativeAppService;
|
||||
private readonly IMailService _mailService;
|
||||
@@ -100,7 +101,8 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
||||
IStatePersistanceService statePersistanceService,
|
||||
IConfigurationService configurationService,
|
||||
IStartupBehaviorService startupBehaviorService,
|
||||
IWebView2RuntimeValidatorService webView2RuntimeValidatorService)
|
||||
IWebView2RuntimeValidatorService webView2RuntimeValidatorService,
|
||||
IUpdateManager updateManager)
|
||||
{
|
||||
StatePersistenceService = statePersistanceService;
|
||||
|
||||
@@ -121,6 +123,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
||||
_notificationBuilder = notificationBuilder;
|
||||
_winoRequestDelegator = winoRequestDelegator;
|
||||
_webView2RuntimeValidatorService = webView2RuntimeValidatorService;
|
||||
_updateManager = updateManager;
|
||||
}
|
||||
|
||||
protected override void OnDispatcherAssigned()
|
||||
@@ -251,6 +254,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
||||
await ForceAllAccountSynchronizationsAsync();
|
||||
}
|
||||
|
||||
await ShowWhatIsNewIfNeededAsync();
|
||||
await MakeSureEnableStartupLaunchAsync();
|
||||
}
|
||||
|
||||
@@ -264,6 +268,19 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowWhatIsNewIfNeededAsync()
|
||||
{
|
||||
if (!_updateManager.ShouldShowUpdateNotes())
|
||||
return;
|
||||
|
||||
var notes = await _updateManager.GetLatestUpdateNotesAsync();
|
||||
|
||||
if (notes.Sections.Count == 0)
|
||||
return;
|
||||
|
||||
await _dialogService.ShowWhatIsNewDialogAsync(notes);
|
||||
}
|
||||
|
||||
private async Task MakeSureEnableStartupLaunchAsync()
|
||||
{
|
||||
if (!_configurationService.Get<bool>(IsActivateStartupLaunchAskedKey, false))
|
||||
|
||||
@@ -22,6 +22,7 @@ using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
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;
|
||||
@@ -153,8 +154,17 @@ public partial class App : WinoApplication,
|
||||
_preferencesService = Services.GetRequiredService<IPreferencesService>();
|
||||
_accountService = Services.GetRequiredService<IAccountService>();
|
||||
|
||||
// Check whether the new version requires a migration before starting sync.
|
||||
var updateManager = Services.GetRequiredService<IUpdateManager>();
|
||||
var updateNotes = await updateManager.GetLatestUpdateNotesAsync();
|
||||
bool hasPendingMigration = updateNotes.HasMigrations && updateManager.HasPendingMigrations();
|
||||
|
||||
_preferencesService.PreferenceChanged -= PreferencesServiceChanged;
|
||||
_preferencesService.PreferenceChanged += PreferencesServiceChanged;
|
||||
|
||||
// Hold off sync loop when a migration is required in startup-task (tray-only) mode.
|
||||
// In foreground mode the sync loop starts normally; the ViewModel dialog handles migrations before sync kicks in.
|
||||
if (!hasPendingMigration || !IsStartupTaskLaunch())
|
||||
RestartAutoSynchronizationLoop();
|
||||
|
||||
// Check if launched from toast notification.
|
||||
@@ -178,13 +188,22 @@ public partial class App : WinoApplication,
|
||||
// If startup task launch, keep window hidden (system tray only).
|
||||
// Otherwise, activate the window normally.
|
||||
if (isStartupTaskLaunch)
|
||||
{
|
||||
if (hasPendingMigration)
|
||||
{
|
||||
// Notify the user to open the app to complete the update before sync can resume.
|
||||
Services.GetRequiredService<INotificationBuilder>().CreateMigrationRequiredNotification();
|
||||
LogActivation("Migration required for new version. Sync skipped. User notified via toast.");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogActivation("Launched by startup task. Window created but hidden (system tray only).");
|
||||
// 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.");
|
||||
}
|
||||
@@ -222,6 +241,13 @@ public partial class App : WinoApplication,
|
||||
{
|
||||
var toastArguments = ToastArguments.Parse(toastArgs.Argument);
|
||||
|
||||
// Check migration notification activation first.
|
||||
if (toastArguments.Contains(Constants.ToastMigrationRequiredKey))
|
||||
{
|
||||
await HandleMigrationToastActivationAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check calendar reminder toast activation first.
|
||||
if (toastArguments.TryGetValue(Constants.ToastCalendarActionKey, out string calendarAction) &&
|
||||
calendarAction == Constants.ToastCalendarNavigateAction &&
|
||||
@@ -251,6 +277,29 @@ public partial class App : WinoApplication,
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles activation from the migration-required toast notification.
|
||||
/// Opens the app so the shell ViewModel can show the What's New dialog and run migrations.
|
||||
/// </summary>
|
||||
private async Task HandleMigrationToastActivationAsync()
|
||||
{
|
||||
LogActivation("Handling migration toast activation.");
|
||||
|
||||
if (!IsAppRunning())
|
||||
{
|
||||
await CreateAndActivateWindow(null!);
|
||||
}
|
||||
else
|
||||
{
|
||||
EnsureMainWindowVisibleAndForeground();
|
||||
}
|
||||
|
||||
// The MailAppShellViewModel.OnNavigatedTo will detect ShouldShowUpdateNotes() == true
|
||||
// and display the What's New dialog (including running migrations) once the XamlRoot is ready.
|
||||
// Restart sync in case it was blocked.
|
||||
RestartAutoSynchronizationLoop();
|
||||
}
|
||||
|
||||
private async Task HandleCalendarToastNavigationAsync(Guid calendarItemId)
|
||||
{
|
||||
var calendarService = Services.GetRequiredService<ICalendarService>();
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"hasMigrations": false,
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"imageWidth": 128,
|
||||
"imageHeight": 128
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<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: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"
|
||||
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" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<FlipView
|
||||
x:Name="UpdateFlipView"
|
||||
ItemsSource="{x:Bind Sections, Mode=OneTime}"
|
||||
SelectionChanged="OnFlipViewSelectionChanged">
|
||||
<FlipView.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:UpdateNoteSection">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Spacing="16" Padding="12,8">
|
||||
<controls:MarkdownTextBlock
|
||||
Text="{x:Bind Title, Mode=OneTime}"
|
||||
HorizontalAlignment="Center" />
|
||||
<Image
|
||||
Source="{x:Bind ImageUrl, Mode=OneTime}"
|
||||
Width="{x:Bind ActualImageWidth, Mode=OneTime}"
|
||||
Height="{x:Bind ActualImageHeight, Mode=OneTime}"
|
||||
HorizontalAlignment="Center"
|
||||
Stretch="Uniform" />
|
||||
<controls:MarkdownTextBlock
|
||||
Text="{x:Bind Description, Mode=OneTime}"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</DataTemplate>
|
||||
</FlipView.ItemTemplate>
|
||||
</FlipView>
|
||||
|
||||
<PipsPager
|
||||
x:Name="FlipViewPager"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Center"
|
||||
SelectedPageIndex="0"
|
||||
SelectedIndexChanged="OnPipsPagerSelectedIndexChanged" />
|
||||
|
||||
<Button
|
||||
x:Name="GetStartedButton"
|
||||
Grid.Row="2"
|
||||
HorizontalAlignment="Right"
|
||||
Content="{x:Bind domain:Translator.WhatIsNew_GetStartedButton}"
|
||||
Visibility="Collapsed"
|
||||
Click="OnGetStartedClicked" />
|
||||
</Grid>
|
||||
</ContentDialog>
|
||||
@@ -0,0 +1,72 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Windows.System;
|
||||
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; }
|
||||
|
||||
public WhatIsNewDialog(UpdateNotes notes, IUpdateManager updateManager)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_updateManager = updateManager;
|
||||
Sections = notes.Sections;
|
||||
|
||||
// Set the number of pages in the pip pager after sections are assigned.
|
||||
FlipViewPager.NumberOfPages = Sections.Count;
|
||||
|
||||
// Show the Get Started button immediately when there is only one page.
|
||||
if (Sections.Count <= 1)
|
||||
GetStartedButton.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
protected override void OnKeyDown(KeyRoutedEventArgs e)
|
||||
{
|
||||
// Block ESC key to prevent accidental dismissal.
|
||||
if (e.Key == VirtualKey.Escape)
|
||||
{
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
private void OnFlipViewSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
int selectedIndex = UpdateFlipView.SelectedIndex;
|
||||
|
||||
// Keep pip pager in sync with the flip view.
|
||||
FlipViewPager.SelectedPageIndex = selectedIndex;
|
||||
|
||||
// Show Get Started button only on the last page.
|
||||
GetStartedButton.Visibility = selectedIndex == Sections.Count - 1
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void OnPipsPagerSelectedIndexChanged(PipsPager sender, PipsPagerSelectedIndexChangedEventArgs args)
|
||||
{
|
||||
UpdateFlipView.SelectedIndex = sender.SelectedPageIndex;
|
||||
}
|
||||
|
||||
private async void OnGetStartedClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
GetStartedButton.IsEnabled = false;
|
||||
|
||||
await _updateManager.RunPendingMigrationsAsync();
|
||||
_updateManager.MarkUpdateNotesAsSeen();
|
||||
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,6 +317,20 @@ public class NotificationBuilder : INotificationBuilder
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void CreateMigrationRequiredNotification()
|
||||
{
|
||||
var builder = new ToastContentBuilder();
|
||||
builder.SetToastScenario(ToastScenario.Default);
|
||||
|
||||
builder.AddText(Translator.WhatIsNew_MigrationNotification_Title);
|
||||
builder.AddText(Translator.WhatIsNew_MigrationNotification_Message);
|
||||
|
||||
builder.AddArgument(Constants.ToastMigrationRequiredKey, bool.TrueString);
|
||||
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||
|
||||
ShowToast(builder);
|
||||
}
|
||||
|
||||
private static void ShowToast(ToastContentBuilder builder, string? tag = null)
|
||||
{
|
||||
var toastNotification = new ToastNotification(builder.GetToastContent().GetXml());
|
||||
|
||||
@@ -142,6 +142,7 @@
|
||||
<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\Wino_Icon.ico" />
|
||||
<Content Include="BackgroundImages\Acrylic.jpg" />
|
||||
<Content Include="BackgroundImages\Clouds.jpg" />
|
||||
|
||||
@@ -29,5 +29,6 @@ public static class ServicesContainerSetup
|
||||
services.AddSingleton<IContactPictureFileService, ContactPictureFileService>();
|
||||
|
||||
services.AddTransient<ICalDavClient, CalDavClient>();
|
||||
services.AddSingleton<IUpdateManager, UpdateManager>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Updates;
|
||||
|
||||
namespace Wino.Services;
|
||||
|
||||
public class UpdateManager : IUpdateManager
|
||||
{
|
||||
private const string UpdateNotesResourcePath = "ms-appx:///Assets/UpdateNotes/vnext.json";
|
||||
private const string UpdateNotesSeenKeyFormat = "UpdateNotes_{0}_Shown";
|
||||
private const string MigrationCompletedKeyFormat = "Migration_{0}_Completed";
|
||||
|
||||
private readonly IFileService _fileService;
|
||||
private readonly IConfigurationService _configurationService;
|
||||
private readonly INativeAppService _nativeAppService;
|
||||
private readonly List<IAppMigration> _migrations = [];
|
||||
|
||||
private string _versionSeenKey = string.Empty;
|
||||
|
||||
public UpdateManager(IFileService fileService,
|
||||
IConfigurationService configurationService,
|
||||
INativeAppService nativeAppService)
|
||||
{
|
||||
_fileService = fileService;
|
||||
_configurationService = configurationService;
|
||||
_nativeAppService = nativeAppService;
|
||||
}
|
||||
|
||||
private string GetVersionSeenKey()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_versionSeenKey))
|
||||
{
|
||||
var version = _nativeAppService.GetFullAppVersion();
|
||||
var sanitized = version.Replace(".", "_");
|
||||
_versionSeenKey = string.Format(UpdateNotesSeenKeyFormat, sanitized);
|
||||
}
|
||||
|
||||
return _versionSeenKey;
|
||||
}
|
||||
|
||||
public async Task<UpdateNotes> GetLatestUpdateNotesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = await _fileService.GetFileContentByApplicationUriAsync(UpdateNotesResourcePath);
|
||||
|
||||
if (string.IsNullOrEmpty(json))
|
||||
return new UpdateNotes();
|
||||
|
||||
return JsonSerializer.Deserialize(json, BasicTypesJsonContext.Default.UpdateNotes) ?? new UpdateNotes();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return new UpdateNotes();
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShouldShowUpdateNotes()
|
||||
=> !_configurationService.Get(GetVersionSeenKey(), false);
|
||||
|
||||
public void MarkUpdateNotesAsSeen()
|
||||
=> _configurationService.Set(GetVersionSeenKey(), true);
|
||||
|
||||
public bool HasPendingMigrations()
|
||||
=> _migrations.Any(m => !_configurationService.Get(string.Format(MigrationCompletedKeyFormat, m.MigrationId), false));
|
||||
|
||||
public async Task RunPendingMigrationsAsync()
|
||||
{
|
||||
foreach (var migration in _migrations)
|
||||
{
|
||||
var key = string.Format(MigrationCompletedKeyFormat, migration.MigrationId);
|
||||
|
||||
if (!_configurationService.Get(key, false))
|
||||
{
|
||||
await migration.ExecuteAsync();
|
||||
_configurationService.Set(key, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterMigrations(IEnumerable<IAppMigration> migrations)
|
||||
=> _migrations.AddRange(migrations);
|
||||
}
|
||||
Reference in New Issue
Block a user