Whats new implementation.
This commit is contained in:
@@ -72,10 +72,14 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
IAccountService accountService,
|
IAccountService accountService,
|
||||||
ICalendarService calendarService,
|
ICalendarService calendarService,
|
||||||
IAccountCalendarStateService accountCalendarStateService,
|
IAccountCalendarStateService accountCalendarStateService,
|
||||||
INavigationService navigationService)
|
INavigationService navigationService,
|
||||||
|
IDialogServiceBase dialogService,
|
||||||
|
IUpdateManager updateManager)
|
||||||
{
|
{
|
||||||
_accountService = accountService;
|
_accountService = accountService;
|
||||||
_calendarService = calendarService;
|
_calendarService = calendarService;
|
||||||
|
_dialogService = dialogService;
|
||||||
|
_updateManager = updateManager;
|
||||||
|
|
||||||
AccountCalendarStateService = accountCalendarStateService;
|
AccountCalendarStateService = accountCalendarStateService;
|
||||||
AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested;
|
AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested;
|
||||||
@@ -123,9 +127,24 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
|
|
||||||
await InitializeAccountCalendarsAsync();
|
await InitializeAccountCalendarsAsync();
|
||||||
|
|
||||||
|
await ShowWhatIsNewIfNeededAsync();
|
||||||
|
|
||||||
TodayClicked();
|
TodayClicked();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ShowWhatIsNewIfNeededAsync()
|
||||||
|
{
|
||||||
|
if (!_updateManager.ShouldShowUpdateNotes())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var notes = await _updateManager.GetLatestUpdateNotesAsync();
|
||||||
|
|
||||||
|
if (notes.Sections.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await _dialogService.ShowWhatIsNewDialogAsync(notes);
|
||||||
|
}
|
||||||
|
|
||||||
private async void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e)
|
private async void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e)
|
||||||
{
|
{
|
||||||
// When using three-state checkbox, multiple accounts will be selected/unselected at the same time.
|
// When using three-state checkbox, multiple accounts will be selected/unselected at the same time.
|
||||||
@@ -271,6 +290,8 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
private DateTime? _navigationDate;
|
private DateTime? _navigationDate;
|
||||||
private readonly IAccountService _accountService;
|
private readonly IAccountService _accountService;
|
||||||
private readonly ICalendarService _calendarService;
|
private readonly ICalendarService _calendarService;
|
||||||
|
private readonly IDialogServiceBase _dialogService;
|
||||||
|
private readonly IUpdateManager _updateManager;
|
||||||
|
|
||||||
#region Commands
|
#region Commands
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using Wino.Core.Domain.Models.Updates;
|
||||||
|
|
||||||
namespace Wino.Core.Domain;
|
namespace Wino.Core.Domain;
|
||||||
|
|
||||||
@@ -8,4 +9,6 @@ namespace Wino.Core.Domain;
|
|||||||
[JsonSerializable(typeof(int))]
|
[JsonSerializable(typeof(int))]
|
||||||
[JsonSerializable(typeof(List<string>))]
|
[JsonSerializable(typeof(List<string>))]
|
||||||
[JsonSerializable(typeof(bool))]
|
[JsonSerializable(typeof(bool))]
|
||||||
|
[JsonSerializable(typeof(UpdateNotes))]
|
||||||
|
[JsonSerializable(typeof(List<UpdateNoteSection>))]
|
||||||
public partial class BasicTypesJsonContext : JsonSerializerContext;
|
public partial class BasicTypesJsonContext : JsonSerializerContext;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ public static class Constants
|
|||||||
public const string ToastModeKey = nameof(ToastModeKey);
|
public const string ToastModeKey = nameof(ToastModeKey);
|
||||||
public const string ToastModeMail = nameof(ToastModeMail);
|
public const string ToastModeMail = nameof(ToastModeMail);
|
||||||
public const string ToastModeCalendar = nameof(ToastModeCalendar);
|
public const string ToastModeCalendar = nameof(ToastModeCalendar);
|
||||||
|
public const string ToastMigrationRequiredKey = nameof(ToastMigrationRequiredKey);
|
||||||
|
|
||||||
public const string ClientLogFile = "Client_.log";
|
public const string ClientLogFile = "Client_.log";
|
||||||
public const string ServerLogFile = "Server_.log";
|
public const string ServerLogFile = "Server_.log";
|
||||||
|
|||||||
@@ -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.Accounts;
|
||||||
using Wino.Core.Domain.Models.Common;
|
using Wino.Core.Domain.Models.Common;
|
||||||
using Wino.Core.Domain.Models.Printing;
|
using Wino.Core.Domain.Models.Printing;
|
||||||
|
using Wino.Core.Domain.Models.Updates;
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Interfaces;
|
namespace Wino.Core.Domain.Interfaces;
|
||||||
|
|
||||||
@@ -30,4 +31,10 @@ public interface IDialogServiceBase
|
|||||||
Task<List<SharedFile>> PickFilesAsync(params object[] typeFilters);
|
Task<List<SharedFile>> PickFilesAsync(params object[] typeFilters);
|
||||||
Task<string> PickFilePathAsync(string saveFileName);
|
Task<string> PickFilePathAsync(string saveFileName);
|
||||||
Task<WebView2PrintSettingsModel> ShowPrintDialogAsync(WebView2PrintSettingsModel initialSettings = null);
|
Task<WebView2PrintSettingsModel> ShowPrintDialogAsync(WebView2PrintSettingsModel initialSettings = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Presents the "What's New" dialog for the current version.
|
||||||
|
/// This dialog is undismissable and runs any pending migrations when the user clicks "Get Started".
|
||||||
|
/// </summary>
|
||||||
|
Task ShowWhatIsNewDialogAsync(UpdateNotes notes);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,4 +40,10 @@ public interface INotificationBuilder
|
|||||||
/// Creates a calendar reminder toast for the specified calendar item.
|
/// Creates a calendar reminder toast for the specified calendar item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task CreateCalendarReminderNotificationAsync(CalendarItem calendarItem, long reminderDurationInSeconds);
|
Task CreateCalendarReminderNotificationAsync(CalendarItem calendarItem, long reminderDurationInSeconds);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows a notification that a migration is required for the new app version.
|
||||||
|
/// Synchronization is stopped and the user is prompted to open the app.
|
||||||
|
/// </summary>
|
||||||
|
void CreateMigrationRequiredNotification();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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_DefaultShowAs": "Default Show As Status",
|
||||||
"CalendarAccountSettings_DefaultShowAsDescription": "Default availability status for new events created with this account",
|
"CalendarAccountSettings_DefaultShowAsDescription": "Default availability status for new events created with this account",
|
||||||
"CalendarAccountSettings_PrimaryCalendar": "Primary Calendar",
|
"CalendarAccountSettings_PrimaryCalendar": "Primary Calendar",
|
||||||
"CalendarAccountSettings_PrimaryCalendarDescription": "Mark this calendar as the primary calendar for the account"
|
"CalendarAccountSettings_PrimaryCalendarDescription": "Mark this calendar as the primary calendar for the account",
|
||||||
|
"WhatIsNew_GetStartedButton": "Get Started",
|
||||||
|
"WhatIsNew_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 IMailDialogService _dialogService;
|
||||||
private readonly IMimeFileService _mimeFileService;
|
private readonly IMimeFileService _mimeFileService;
|
||||||
private readonly IWebView2RuntimeValidatorService _webView2RuntimeValidatorService;
|
private readonly IWebView2RuntimeValidatorService _webView2RuntimeValidatorService;
|
||||||
|
private readonly IUpdateManager _updateManager;
|
||||||
|
|
||||||
private readonly INativeAppService _nativeAppService;
|
private readonly INativeAppService _nativeAppService;
|
||||||
private readonly IMailService _mailService;
|
private readonly IMailService _mailService;
|
||||||
@@ -100,7 +101,8 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
IStatePersistanceService statePersistanceService,
|
IStatePersistanceService statePersistanceService,
|
||||||
IConfigurationService configurationService,
|
IConfigurationService configurationService,
|
||||||
IStartupBehaviorService startupBehaviorService,
|
IStartupBehaviorService startupBehaviorService,
|
||||||
IWebView2RuntimeValidatorService webView2RuntimeValidatorService)
|
IWebView2RuntimeValidatorService webView2RuntimeValidatorService,
|
||||||
|
IUpdateManager updateManager)
|
||||||
{
|
{
|
||||||
StatePersistenceService = statePersistanceService;
|
StatePersistenceService = statePersistanceService;
|
||||||
|
|
||||||
@@ -121,6 +123,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
_notificationBuilder = notificationBuilder;
|
_notificationBuilder = notificationBuilder;
|
||||||
_winoRequestDelegator = winoRequestDelegator;
|
_winoRequestDelegator = winoRequestDelegator;
|
||||||
_webView2RuntimeValidatorService = webView2RuntimeValidatorService;
|
_webView2RuntimeValidatorService = webView2RuntimeValidatorService;
|
||||||
|
_updateManager = updateManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnDispatcherAssigned()
|
protected override void OnDispatcherAssigned()
|
||||||
@@ -251,6 +254,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
await ForceAllAccountSynchronizationsAsync();
|
await ForceAllAccountSynchronizationsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await ShowWhatIsNewIfNeededAsync();
|
||||||
await MakeSureEnableStartupLaunchAsync();
|
await MakeSureEnableStartupLaunchAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,6 +268,19 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ShowWhatIsNewIfNeededAsync()
|
||||||
|
{
|
||||||
|
if (!_updateManager.ShouldShowUpdateNotes())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var notes = await _updateManager.GetLatestUpdateNotesAsync();
|
||||||
|
|
||||||
|
if (notes.Sections.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await _dialogService.ShowWhatIsNewDialogAsync(notes);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task MakeSureEnableStartupLaunchAsync()
|
private async Task MakeSureEnableStartupLaunchAsync()
|
||||||
{
|
{
|
||||||
if (!_configurationService.Get<bool>(IsActivateStartupLaunchAskedKey, false))
|
if (!_configurationService.Get<bool>(IsActivateStartupLaunchAskedKey, false))
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ using Wino.Core.Domain.Interfaces;
|
|||||||
using Wino.Core.Domain.Models.Calendar;
|
using Wino.Core.Domain.Models.Calendar;
|
||||||
using Wino.Core.Domain.Models.MailItem;
|
using Wino.Core.Domain.Models.MailItem;
|
||||||
using Wino.Core.Domain.Models.Synchronization;
|
using Wino.Core.Domain.Models.Synchronization;
|
||||||
|
using Wino.Core.Domain.Models.Updates;
|
||||||
using Wino.Mail.Services;
|
using Wino.Mail.Services;
|
||||||
using Wino.Mail.ViewModels;
|
using Wino.Mail.ViewModels;
|
||||||
using Wino.Mail.WinUI.Activation;
|
using Wino.Mail.WinUI.Activation;
|
||||||
@@ -153,9 +154,18 @@ public partial class App : WinoApplication,
|
|||||||
_preferencesService = Services.GetRequiredService<IPreferencesService>();
|
_preferencesService = Services.GetRequiredService<IPreferencesService>();
|
||||||
_accountService = Services.GetRequiredService<IAccountService>();
|
_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;
|
||||||
_preferencesService.PreferenceChanged += PreferencesServiceChanged;
|
_preferencesService.PreferenceChanged += PreferencesServiceChanged;
|
||||||
RestartAutoSynchronizationLoop();
|
|
||||||
|
// 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.
|
// Check if launched from toast notification.
|
||||||
if (IsNotificationActivation(out AppNotificationActivatedEventArgs toastArgs))
|
if (IsNotificationActivation(out AppNotificationActivatedEventArgs toastArgs))
|
||||||
@@ -179,12 +189,21 @@ public partial class App : WinoApplication,
|
|||||||
// Otherwise, activate the window normally.
|
// Otherwise, activate the window normally.
|
||||||
if (isStartupTaskLaunch)
|
if (isStartupTaskLaunch)
|
||||||
{
|
{
|
||||||
LogActivation("Launched by startup task. Window created but hidden (system tray only).");
|
if (hasPendingMigration)
|
||||||
// Window is created but not activated. User can show it from system tray.
|
{
|
||||||
|
// Notify the user to open the app to complete the update before sync can resume.
|
||||||
|
Services.GetRequiredService<INotificationBuilder>().CreateMigrationRequiredNotification();
|
||||||
|
LogActivation("Migration required for new version. Sync skipped. User notified via toast.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LogActivation("Launched by startup task. Window created but hidden (system tray only).");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Normal launch - show and activate the window.
|
// Normal launch - show and activate the window.
|
||||||
|
// The What's New dialog is shown from MailAppShellViewModel.OnNavigatedTo once XamlRoot is ready.
|
||||||
MainWindow?.Activate();
|
MainWindow?.Activate();
|
||||||
LogActivation("Window created and activated.");
|
LogActivation("Window created and activated.");
|
||||||
}
|
}
|
||||||
@@ -222,6 +241,13 @@ public partial class App : WinoApplication,
|
|||||||
{
|
{
|
||||||
var toastArguments = ToastArguments.Parse(toastArgs.Argument);
|
var toastArguments = ToastArguments.Parse(toastArgs.Argument);
|
||||||
|
|
||||||
|
// Check migration notification activation first.
|
||||||
|
if (toastArguments.Contains(Constants.ToastMigrationRequiredKey))
|
||||||
|
{
|
||||||
|
await HandleMigrationToastActivationAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check calendar reminder toast activation first.
|
// Check calendar reminder toast activation first.
|
||||||
if (toastArguments.TryGetValue(Constants.ToastCalendarActionKey, out string calendarAction) &&
|
if (toastArguments.TryGetValue(Constants.ToastCalendarActionKey, out string calendarAction) &&
|
||||||
calendarAction == Constants.ToastCalendarNavigateAction &&
|
calendarAction == Constants.ToastCalendarNavigateAction &&
|
||||||
@@ -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)
|
private async Task HandleCalendarToastNavigationAsync(Guid calendarItemId)
|
||||||
{
|
{
|
||||||
var calendarService = Services.GetRequiredService<ICalendarService>();
|
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,
|
public DialogService(INewThemeService themeService,
|
||||||
IConfigurationService configurationService,
|
IConfigurationService configurationService,
|
||||||
IApplicationResourceManager<ResourceDictionary> applicationResourceManager) : base(themeService, configurationService, applicationResourceManager)
|
IApplicationResourceManager<ResourceDictionary> applicationResourceManager,
|
||||||
|
IUpdateManager updateManager) : base(themeService, configurationService, applicationResourceManager, updateManager)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ICreateAccountAliasDialog> ShowCreateAccountAliasDialogAsync()
|
public async Task<ICreateAccountAliasDialog> ShowCreateAccountAliasDialogAsync()
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ using Wino.Core.Domain.Interfaces;
|
|||||||
using Wino.Core.Domain.Models.Accounts;
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
using Wino.Core.Domain.Models.Common;
|
using Wino.Core.Domain.Models.Common;
|
||||||
using Wino.Core.Domain.Models.Printing;
|
using Wino.Core.Domain.Models.Printing;
|
||||||
|
using Wino.Core.Domain.Models.Updates;
|
||||||
using Wino.Dialogs;
|
using Wino.Dialogs;
|
||||||
using Wino.Mail.WinUI.Dialogs;
|
using Wino.Mail.WinUI.Dialogs;
|
||||||
using Wino.Mail.WinUI.Extensions;
|
using Wino.Mail.WinUI.Extensions;
|
||||||
@@ -30,14 +31,16 @@ public class DialogServiceBase : IDialogServiceBase
|
|||||||
|
|
||||||
protected INewThemeService ThemeService { get; }
|
protected INewThemeService ThemeService { get; }
|
||||||
protected IConfigurationService ConfigurationService { get; }
|
protected IConfigurationService ConfigurationService { get; }
|
||||||
|
protected IUpdateManager UpdateManager { get; }
|
||||||
|
|
||||||
protected IApplicationResourceManager<ResourceDictionary> ApplicationResourceManager { get; }
|
protected IApplicationResourceManager<ResourceDictionary> ApplicationResourceManager { get; }
|
||||||
|
|
||||||
public DialogServiceBase(INewThemeService themeService, IConfigurationService configurationService, IApplicationResourceManager<ResourceDictionary> applicationResourceManager)
|
public DialogServiceBase(INewThemeService themeService, IConfigurationService configurationService, IApplicationResourceManager<ResourceDictionary> applicationResourceManager, IUpdateManager updateManager)
|
||||||
{
|
{
|
||||||
ThemeService = themeService;
|
ThemeService = themeService;
|
||||||
ConfigurationService = configurationService;
|
ConfigurationService = configurationService;
|
||||||
ApplicationResourceManager = applicationResourceManager;
|
ApplicationResourceManager = applicationResourceManager;
|
||||||
|
UpdateManager = updateManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected XamlRoot? GetXamlRoot()
|
protected XamlRoot? GetXamlRoot()
|
||||||
@@ -355,4 +358,14 @@ public class DialogServiceBase : IDialogServiceBase
|
|||||||
return null!;
|
return null!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task ShowWhatIsNewDialogAsync(UpdateNotes notes)
|
||||||
|
{
|
||||||
|
var dialog = new WhatIsNewDialog(notes, UpdateManager)
|
||||||
|
{
|
||||||
|
RequestedTheme = ThemeService.RootTheme.ToWindowsElementTheme()
|
||||||
|
};
|
||||||
|
|
||||||
|
await HandleDialogPresentationAsync(dialog);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -317,6 +317,20 @@ public class NotificationBuilder : INotificationBuilder
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void CreateMigrationRequiredNotification()
|
||||||
|
{
|
||||||
|
var builder = new ToastContentBuilder();
|
||||||
|
builder.SetToastScenario(ToastScenario.Default);
|
||||||
|
|
||||||
|
builder.AddText(Translator.WhatIsNew_MigrationNotification_Title);
|
||||||
|
builder.AddText(Translator.WhatIsNew_MigrationNotification_Message);
|
||||||
|
|
||||||
|
builder.AddArgument(Constants.ToastMigrationRequiredKey, bool.TrueString);
|
||||||
|
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||||
|
|
||||||
|
ShowToast(builder);
|
||||||
|
}
|
||||||
|
|
||||||
private static void ShowToast(ToastContentBuilder builder, string? tag = null)
|
private static void ShowToast(ToastContentBuilder builder, string? tag = null)
|
||||||
{
|
{
|
||||||
var toastNotification = new ToastNotification(builder.GetToastContent().GetXml());
|
var toastNotification = new ToastNotification(builder.GetToastContent().GetXml());
|
||||||
|
|||||||
@@ -142,6 +142,7 @@
|
|||||||
<Content Include="AppThemes\Snowflake.xaml" />
|
<Content Include="AppThemes\Snowflake.xaml" />
|
||||||
<Content Include="AppThemes\TestTheme.xaml" />
|
<Content Include="AppThemes\TestTheme.xaml" />
|
||||||
<Content Include="Assets\ReleaseNotes\vnext.md" />
|
<Content Include="Assets\ReleaseNotes\vnext.md" />
|
||||||
|
<Content Include="Assets\UpdateNotes\vnext.json" />
|
||||||
<Content Include="Assets\Wino_Icon.ico" />
|
<Content Include="Assets\Wino_Icon.ico" />
|
||||||
<Content Include="BackgroundImages\Acrylic.jpg" />
|
<Content Include="BackgroundImages\Acrylic.jpg" />
|
||||||
<Content Include="BackgroundImages\Clouds.jpg" />
|
<Content Include="BackgroundImages\Clouds.jpg" />
|
||||||
|
|||||||
@@ -29,5 +29,6 @@ public static class ServicesContainerSetup
|
|||||||
services.AddSingleton<IContactPictureFileService, ContactPictureFileService>();
|
services.AddSingleton<IContactPictureFileService, ContactPictureFileService>();
|
||||||
|
|
||||||
services.AddTransient<ICalDavClient, CalDavClient>();
|
services.AddTransient<ICalDavClient, CalDavClient>();
|
||||||
|
services.AddSingleton<IUpdateManager, UpdateManager>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,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