Migration plan v1

This commit is contained in:
Burak Kaan Köse
2026-04-23 14:52:52 +02:00
parent 6f82cd4f26
commit c1bda75d9f
17 changed files with 2087 additions and 25 deletions
@@ -17,6 +17,7 @@ using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.Calendar;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Synchronization;
@@ -35,6 +36,7 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel
private static readonly UTF8Encoding Utf8WithoutBom = new(false);
private readonly IWinoAccountDataSyncService _syncService;
private readonly ILegacyLocalMigrationService _legacyLocalMigrationService;
private readonly IWinoLogger _winoLogger;
private readonly ISpecialImapProviderConfigResolver _specialImapProviderConfigResolver;
private readonly ICalDavClient _calDavClient;
@@ -48,6 +50,7 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel
IStoreManagementService storeManagementService,
IWinoAccountProfileService winoAccountProfileService,
IWinoAccountDataSyncService syncService,
ILegacyLocalMigrationService legacyLocalMigrationService,
IWinoLogger winoLogger,
ISpecialImapProviderConfigResolver specialImapProviderConfigResolver,
ICalDavClient calDavClient,
@@ -56,6 +59,7 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel
{
MailDialogService = dialogService;
_syncService = syncService;
_legacyLocalMigrationService = legacyLocalMigrationService;
_winoLogger = winoLogger;
_specialImapProviderConfigResolver = specialImapProviderConfigResolver;
_calDavClient = calDavClient;
@@ -64,8 +68,26 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(ExportLocalDataCommand))]
[NotifyCanExecuteChangedFor(nameof(ImportLocalDataCommand))]
[NotifyCanExecuteChangedFor(nameof(ImportLegacyDatabaseCommand))]
public partial bool IsDataTransferInProgress { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasLegacyImportAvailable))]
[NotifyPropertyChangedFor(nameof(HasLegacyImportWarnings))]
[NotifyPropertyChangedFor(nameof(LegacyMigrationSummary))]
[NotifyPropertyChangedFor(nameof(LegacyMigrationWarningSummary))]
[NotifyCanExecuteChangedFor(nameof(ImportLegacyDatabaseCommand))]
public partial LegacyLocalMigrationPreview LegacyMigrationPreview { get; set; }
public bool HasLegacyImportAvailable => LegacyMigrationPreview?.HasImportableData == true;
public bool HasLegacyImportWarnings => !string.IsNullOrWhiteSpace(LegacyMigrationWarningSummary);
public string LegacyMigrationSummary => HasLegacyImportAvailable
? LegacyLocalMigrationFormatter.BuildPreviewSummary(LegacyMigrationPreview)
: string.Empty;
public string LegacyMigrationWarningSummary => HasLegacyImportAvailable
? LegacyLocalMigrationFormatter.BuildWarningSummary(LegacyMigrationPreview)
: string.Empty;
[RelayCommand]
private async Task CreateMergedAccountAsync()
{
@@ -314,7 +336,42 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel
}
}
[RelayCommand(CanExecute = nameof(CanImportLegacyDatabase))]
private async Task ImportLegacyDatabaseAsync()
{
try
{
await ExecuteUIThread(() => IsDataTransferInProgress = true);
var result = await _legacyLocalMigrationService.ImportAsync().ConfigureAwait(false);
await InitializeAccountsAsync().ConfigureAwait(false);
await RefreshLegacyMigrationPreviewAsync().ConfigureAwait(false);
var messageType = result.FailedAccountCount > 0
? InfoBarMessageType.Warning
: InfoBarMessageType.Success;
DialogService.InfoBarMessage(
result.FailedAccountCount > 0 ? Translator.GeneralTitle_Warning : Translator.GeneralTitle_Info,
LegacyLocalMigrationFormatter.BuildImportMessage(result),
messageType);
}
catch (Exception ex)
{
DialogService.InfoBarMessage(
Translator.GeneralTitle_Error,
ex.Message,
InfoBarMessageType.Error);
}
finally
{
await ExecuteUIThread(() => IsDataTransferInProgress = false);
}
}
private bool CanTransferLocalData() => !IsDataTransferInProgress;
private bool CanImportLegacyDatabase() => !IsDataTransferInProgress && HasLegacyImportAvailable;
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
{
@@ -350,6 +407,7 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel
Accounts.CollectionChanged += AccountCollectionChanged;
await InitializeAccountsAsync();
await RefreshLegacyMigrationPreviewAsync();
PropertyChanged -= PagePropertyChanged;
PropertyChanged += PagePropertyChanged;
@@ -403,6 +461,19 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel
await ManageStorePurchasesAsync().ConfigureAwait(false);
}
private async Task RefreshLegacyMigrationPreviewAsync()
{
try
{
var preview = await _legacyLocalMigrationService.DetectAsync().ConfigureAwait(false);
await ExecuteUIThread(() => LegacyMigrationPreview = preview);
}
catch (Exception)
{
await ExecuteUIThread(() => LegacyMigrationPreview = null);
}
}
private static string BuildExportSuccessMessage(Wino.Core.Domain.Models.Accounts.WinoAccountSyncExportResult result)
{
var parts = new Collection<string>();
@@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Wino.Core.Domain;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Accounts;
namespace Wino.Mail.ViewModels.Data;
internal static class LegacyLocalMigrationFormatter
{
public static string BuildPreviewSummary(LegacyLocalMigrationPreview preview)
{
if (!preview.HasImportableData)
{
return Translator.LegacyLocalMigration_ImportEmpty;
}
var providerSummary = string.Join(", ", preview.ProviderCounts
.Where(a => a.ImportableAccountCount > 0)
.Select(a => $"{a.ImportableAccountCount} {GetProviderName(a.ProviderType)}"));
var parts = new List<string>
{
string.Format(Translator.LegacyLocalMigration_PreviewSummary, preview.ImportableAccountCount, providerSummary)
};
if (preview.DuplicateAccountCount > 0)
{
parts.Add(string.Format(Translator.LegacyLocalMigration_PreviewDuplicateSummary, preview.DuplicateAccountCount));
}
if (preview.ImportableMergedInboxCount > 0)
{
parts.Add(string.Format(Translator.LegacyLocalMigration_PreviewMergedSummary, preview.ImportableMergedInboxCount));
}
return string.Join(" ", parts.Where(a => !string.IsNullOrWhiteSpace(a)));
}
public static string BuildWarningSummary(LegacyLocalMigrationPreview preview)
=> string.Join(Environment.NewLine, preview.Warnings.Where(a => !string.IsNullOrWhiteSpace(a)).Distinct(StringComparer.Ordinal));
public static string BuildImportMessage(LegacyLocalMigrationResult result)
{
var parts = new List<string>();
if (result.ImportedAccountCount > 0)
{
parts.Add(string.Format(Translator.LegacyLocalMigration_ImportAccountsSucceeded, result.ImportedAccountCount));
}
if (result.SkippedDuplicateAccountCount > 0)
{
parts.Add(string.Format(Translator.WinoAccount_Management_ImportDuplicateAccountsSkipped, result.SkippedDuplicateAccountCount));
}
if (result.ImportedMergedInboxCount > 0)
{
parts.Add(string.Format(Translator.LegacyLocalMigration_ImportMergedInboxesSucceeded, result.ImportedMergedInboxCount));
}
if (result.SkippedMergedInboxCount > 0)
{
parts.Add(string.Format(Translator.LegacyLocalMigration_ImportMergedInboxesSkipped, result.SkippedMergedInboxCount));
}
if (result.FailedAccountCount > 0)
{
parts.Add(string.Format(Translator.LegacyLocalMigration_ImportFailedAccounts, result.FailedAccountCount));
}
if (parts.Count == 0)
{
parts.Add(Translator.LegacyLocalMigration_ImportEmpty);
}
if (result.ImportedAccountCount > 0)
{
parts.Add(Translator.WinoAccount_Management_ImportReloginReminder);
}
return string.Join(" ", parts);
}
public static string BuildPromptMessage(LegacyLocalMigrationPreview preview)
{
var summary = BuildPreviewSummary(preview);
var warnings = BuildWarningSummary(preview);
return string.IsNullOrWhiteSpace(warnings)
? summary
: $"{summary}{Environment.NewLine}{Environment.NewLine}{warnings}";
}
private static string GetProviderName(MailProviderType providerType)
{
return providerType switch
{
MailProviderType.Outlook => Translator.LegacyLocalMigration_Provider_Outlook,
MailProviderType.Gmail => Translator.LegacyLocalMigration_Provider_Gmail,
MailProviderType.IMAP4 => Translator.LegacyLocalMigration_Provider_Imap,
_ => providerType.ToString()
};
}
}
+53 -1
View File
@@ -90,10 +90,12 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
private readonly IWebView2RuntimeValidatorService _webView2RuntimeValidatorService;
private readonly IStoreUpdateService _storeUpdateService;
private readonly IShareActivationService _shareActivationService;
private readonly ILegacyLocalMigrationService _legacyLocalMigrationService;
private readonly INativeAppService _nativeAppService;
private readonly IMailService _mailService;
private bool _hasRegisteredPersistentRecipients;
private bool _hasHandledLegacyMigrationPrompt;
private readonly SemaphoreSlim _menuRefreshSemaphore = new(1, 1);
private readonly SemaphoreSlim accountInitFolderUpdateSlim = new SemaphoreSlim(1);
@@ -117,7 +119,8 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
IStartupBehaviorService startupBehaviorService,
IWebView2RuntimeValidatorService webView2RuntimeValidatorService,
IStoreUpdateService storeUpdateService,
IShareActivationService shareActivationService)
IShareActivationService shareActivationService,
ILegacyLocalMigrationService legacyLocalMigrationService)
{
StatePersistenceService = statePersistanceService;
@@ -141,6 +144,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
_webView2RuntimeValidatorService = webView2RuntimeValidatorService;
_storeUpdateService = storeUpdateService;
_shareActivationService = shareActivationService;
_legacyLocalMigrationService = legacyLocalMigrationService;
}
protected override void OnDispatcherAssigned()
@@ -286,6 +290,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
await ProcessLaunchOptionsAsync();
await HandlePendingShareRequestAsync();
await ValidateWebView2RuntimeAsync();
await PromptLegacyMigrationIfNeededAsync(shouldRunStartupFlows);
if (shouldRunStartupFlows && !Debugger.IsAttached)
{
@@ -298,6 +303,53 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
}
}
private async Task PromptLegacyMigrationIfNeededAsync(bool shouldRunStartupFlows)
{
if (!shouldRunStartupFlows || _hasHandledLegacyMigrationPrompt)
{
return;
}
_hasHandledLegacyMigrationPrompt = true;
var currentAccounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
if (!currentAccounts.Any())
{
return;
}
var preview = await _legacyLocalMigrationService.DetectAsync().ConfigureAwait(false);
if (!preview.ShouldPrompt || !preview.HasImportableData)
{
return;
}
var shouldImport = await _dialogService.ShowConfirmationDialogAsync(
LegacyLocalMigrationFormatter.BuildPromptMessage(preview),
Translator.LegacyLocalMigration_PromptTitle,
Translator.LegacyLocalMigration_ImportAction);
if (!shouldImport)
{
_legacyLocalMigrationService.MarkPromptDeferred();
return;
}
var result = await _legacyLocalMigrationService.ImportAsync().ConfigureAwait(false);
await RecreateMenuItemsAsync().ConfigureAwait(false);
await RestoreSelectedAccountAfterMenuRefreshAsync(false).ConfigureAwait(false);
var messageType = result.FailedAccountCount > 0
? InfoBarMessageType.Warning
: InfoBarMessageType.Success;
_dialogService.InfoBarMessage(
result.FailedAccountCount > 0 ? Translator.GeneralTitle_Warning : Translator.GeneralTitle_Info,
LegacyLocalMigrationFormatter.BuildImportMessage(result),
messageType);
}
private async Task ValidateWebView2RuntimeAsync()
{
var isRuntimeAvailable = await _webView2RuntimeValidatorService.IsRuntimeAvailableAsync();
+75 -1
View File
@@ -24,6 +24,7 @@ public partial class WelcomePageV2ViewModel : MailBaseViewModel
private readonly IUpdateManager _updateManager;
private readonly IMailDialogService _dialogService;
private readonly IWinoAccountDataSyncService _syncService;
private readonly ILegacyLocalMigrationService _legacyLocalMigrationService;
[ObservableProperty]
public partial List<UpdateNoteSection> UpdateSections { get; set; } = [];
@@ -32,21 +33,40 @@ public partial class WelcomePageV2ViewModel : MailBaseViewModel
[NotifyCanExecuteChangedFor(nameof(GetStartedCommand))]
[NotifyCanExecuteChangedFor(nameof(ImportFromWinoAccountCommand))]
[NotifyCanExecuteChangedFor(nameof(ImportFromJsonCommand))]
[NotifyCanExecuteChangedFor(nameof(ImportLegacyDatabaseCommand))]
public partial bool IsImportInProgress { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasImportStatus))]
public partial string ImportStatusMessage { get; set; } = string.Empty;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasLegacyImportPreview))]
[NotifyPropertyChangedFor(nameof(HasLegacyImportWarnings))]
[NotifyPropertyChangedFor(nameof(LegacyImportSummary))]
[NotifyPropertyChangedFor(nameof(LegacyImportWarnings))]
[NotifyCanExecuteChangedFor(nameof(ImportLegacyDatabaseCommand))]
public partial LegacyLocalMigrationPreview LegacyMigrationPreview { get; set; }
public bool HasImportStatus => !string.IsNullOrWhiteSpace(ImportStatusMessage);
public bool HasLegacyImportPreview => LegacyMigrationPreview?.HasImportableData == true;
public bool HasLegacyImportWarnings => !string.IsNullOrWhiteSpace(LegacyImportWarnings);
public string LegacyImportSummary => HasLegacyImportPreview
? LegacyLocalMigrationFormatter.BuildPreviewSummary(LegacyMigrationPreview)
: string.Empty;
public string LegacyImportWarnings => HasLegacyImportPreview
? LegacyLocalMigrationFormatter.BuildWarningSummary(LegacyMigrationPreview)
: string.Empty;
public WelcomePageV2ViewModel(IUpdateManager updateManager,
IMailDialogService dialogService,
IWinoAccountDataSyncService syncService)
IWinoAccountDataSyncService syncService,
ILegacyLocalMigrationService legacyLocalMigrationService)
{
_updateManager = updateManager;
_dialogService = dialogService;
_syncService = syncService;
_legacyLocalMigrationService = legacyLocalMigrationService;
}
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
@@ -62,6 +82,15 @@ public partial class WelcomePageV2ViewModel : MailBaseViewModel
{
UpdateSections = [];
}
try
{
LegacyMigrationPreview = await _legacyLocalMigrationService.DetectAsync();
}
catch (Exception)
{
LegacyMigrationPreview = null;
}
}
[RelayCommand(CanExecute = nameof(CanOpenWelcomeActions))]
@@ -150,7 +179,52 @@ public partial class WelcomePageV2ViewModel : MailBaseViewModel
}
}
[RelayCommand(CanExecute = nameof(CanImportLegacyDatabase))]
private async Task ImportLegacyDatabaseAsync()
{
await ExecuteUIThread(() => ImportStatusMessage = string.Empty);
try
{
await ExecuteUIThread(() => IsImportInProgress = true);
var result = await _legacyLocalMigrationService.ImportAsync().ConfigureAwait(false);
if (result.ImportedAccountCount > 0)
{
ReportUIChange(new WelcomeImportCompletedMessage(
result.ImportedAccountCount,
LegacyLocalMigrationFormatter.BuildImportMessage(result)));
return;
}
await ExecuteUIThread(() =>
{
ImportStatusMessage = LegacyLocalMigrationFormatter.BuildImportMessage(result);
LegacyMigrationPreview = result.Preview;
});
}
catch (Exception ex)
{
await _dialogService.ShowMessageAsync(ex.Message, Translator.GeneralTitle_Error, WinoCustomMessageDialogIcon.Error);
}
finally
{
try
{
var preview = await _legacyLocalMigrationService.DetectAsync().ConfigureAwait(false);
await ExecuteUIThread(() => LegacyMigrationPreview = preview);
}
catch (Exception)
{
// Keep the current preview if detection fails after import.
}
await ExecuteUIThread(() => IsImportInProgress = false);
}
}
private bool CanOpenWelcomeActions() => !IsImportInProgress;
private bool CanImportLegacyDatabase() => !IsImportInProgress && HasLegacyImportPreview;
private static string BuildInlineImportMessage(WinoAccountSyncImportResult result)
{