Add local JSON account import and export
This commit is contained in:
@@ -7,5 +7,7 @@ namespace Wino.Core.Domain.Interfaces;
|
||||
public interface IWinoAccountDataSyncService
|
||||
{
|
||||
Task<WinoAccountSyncExportResult> ExportAsync(WinoAccountSyncSelection selection, CancellationToken cancellationToken = default);
|
||||
Task<WinoAccountSyncFileExportResult> ExportToJsonAsync(WinoAccountSyncSelection selection, CancellationToken cancellationToken = default);
|
||||
Task<WinoAccountSyncImportResult> ImportAsync(WinoAccountSyncSelection selection, CancellationToken cancellationToken = default);
|
||||
Task<WinoAccountSyncImportResult> ImportFromJsonAsync(string jsonContent, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Wino.Core.Domain.Models.Accounts;
|
||||
|
||||
public sealed class WinoAccountSyncFileExportResult
|
||||
{
|
||||
public string JsonContent { get; init; } = string.Empty;
|
||||
public WinoAccountSyncExportResult ExportResult { get; init; } = new();
|
||||
}
|
||||
@@ -1339,9 +1339,10 @@
|
||||
"WelcomeWindow_GetStartedButton": "Get started by adding an account",
|
||||
"WelcomeWindow_GetStartedDescription": "Add your Outlook, Gmail, or IMAP account to get started with Wino Mail.",
|
||||
"WelcomeWindow_ImportFromWinoAccount": "Import from your Wino Account",
|
||||
"WelcomeWindow_ImportInProgress": "Importing your synchronized preferences and accounts...",
|
||||
"WelcomeWindow_ImportNoAccountsFound": "No synced accounts were found in your Wino Account. If preferences were available, they were restored. Use Get started to add an account manually.",
|
||||
"WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} synced accounts are already available on this device. Use Get started to add another account manually if needed.",
|
||||
"WelcomeWindow_ImportFromJsonFile": "Import from a JSON file",
|
||||
"WelcomeWindow_ImportInProgress": "Importing preferences and accounts...",
|
||||
"WelcomeWindow_ImportNoAccountsFound": "No accounts were found to import. If preferences were available, they were restored. Use Get started to add an account manually.",
|
||||
"WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} imported accounts are already available on this device. Use Get started to add another account manually if needed.",
|
||||
"WelcomeWindow_SetupTitle": "Set up your account",
|
||||
"WelcomeWindow_SetupSubtitle": "Choose your email provider to get started",
|
||||
"WelcomeWindow_AddAccountButton": "Add account",
|
||||
@@ -1400,13 +1401,13 @@
|
||||
"WinoAccount_Management_StatusLabel": "Status: {0}",
|
||||
"WinoAccount_Management_NoRemoteSettings": "There is no synchronized data stored for this account yet.",
|
||||
"WinoAccount_Management_ExportSucceeded": "Your selected Wino data was exported successfully.",
|
||||
"WinoAccount_Management_ExportPreferencesSucceeded": "Your preferences were exported to your Wino Account.",
|
||||
"WinoAccount_Management_ExportAccountsSucceeded": "Exported {0} account details to your Wino Account.",
|
||||
"WinoAccount_Management_ExportPreferencesSucceeded": "Your preferences were exported.",
|
||||
"WinoAccount_Management_ExportAccountsSucceeded": "Exported {0} account details.",
|
||||
"WinoAccount_Management_ImportSucceeded": "Imported synchronized data from your Wino Account.",
|
||||
"WinoAccount_Management_ImportPreferencesSucceeded": "Applied {0} synchronized preferences.",
|
||||
"WinoAccount_Management_ImportPreferencesSucceeded": "Applied {0} preferences.",
|
||||
"WinoAccount_Management_ImportAccountsSucceeded": "Imported {0} accounts.",
|
||||
"WinoAccount_Management_ImportDuplicateAccountsSkipped": "Skipped {0} accounts that already exist on this device.",
|
||||
"WinoAccount_Management_ImportPartial": "Applied {0} synchronized preferences. {1} preferences could not be restored.",
|
||||
"WinoAccount_Management_ImportPartial": "Applied {0} preferences. {1} preferences could not be restored.",
|
||||
"WinoAccount_Management_ImportReloginReminder": "Passwords, tokens, and other sensitive information were not imported. Sign in again for each account on this device before using it.",
|
||||
"WinoAccount_Management_SerializeFailed": "Wino could not serialize your current preferences.",
|
||||
"WinoAccount_Management_EmptyExport": "There are no preference values to export.",
|
||||
@@ -1418,6 +1419,12 @@
|
||||
"WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Passwords, tokens, and other sensitive information are not synced.",
|
||||
"WinoAccount_Management_ExportDialog_AccountsRelogin": "Imported accounts on another PC will still need you to sign in again before they can be used.",
|
||||
"WinoAccount_Management_ExportDialog_InProgress": "Exporting your selected Wino data...",
|
||||
"WinoAccount_Management_LocalDataSectionTitle": "Transfer with a JSON file",
|
||||
"WinoAccount_Management_LocalDataSectionDescription": "Import from or export to a local JSON file. Passwords, tokens, and other sensitive information are not included.",
|
||||
"WinoAccount_Management_LocalDataImportAction": "Import JSON",
|
||||
"WinoAccount_Management_LocalDataExportAction": "Export JSON",
|
||||
"WinoAccount_Management_LocalDataSaved": "Saved your exported Wino data to {0}.",
|
||||
"WinoAccount_Management_LocalDataInvalidFile": "The selected JSON file doesn't contain a valid Wino export.",
|
||||
"WinoAccount_Management_LoadFailed": "Wino could not load the latest Wino Account information.",
|
||||
"WinoAccount_Management_ActionFailed": "The Wino Account request could not be completed.",
|
||||
"WinoAccount_SettingsSection_Title": "Wino Account",
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Serilog;
|
||||
@@ -26,6 +31,10 @@ namespace Wino.Mail.ViewModels;
|
||||
|
||||
public partial class AccountManagementViewModel : AccountManagementPageViewModelBase
|
||||
{
|
||||
private const string LocalExportFileName = "wino-data-export.json";
|
||||
private static readonly UTF8Encoding Utf8WithoutBom = new(false);
|
||||
|
||||
private readonly IWinoAccountDataSyncService _syncService;
|
||||
private readonly IWinoLogger _winoLogger;
|
||||
private readonly ISpecialImapProviderConfigResolver _specialImapProviderConfigResolver;
|
||||
private readonly ICalDavClient _calDavClient;
|
||||
@@ -38,6 +47,7 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel
|
||||
IProviderService providerService,
|
||||
IStoreManagementService storeManagementService,
|
||||
IWinoAccountProfileService winoAccountProfileService,
|
||||
IWinoAccountDataSyncService syncService,
|
||||
IWinoLogger winoLogger,
|
||||
ISpecialImapProviderConfigResolver specialImapProviderConfigResolver,
|
||||
ICalDavClient calDavClient,
|
||||
@@ -45,11 +55,17 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel
|
||||
IPreferencesService preferencesService) : base(dialogService, navigationService, accountService, providerService, storeManagementService, winoAccountProfileService, authenticationProvider, preferencesService)
|
||||
{
|
||||
MailDialogService = dialogService;
|
||||
_syncService = syncService;
|
||||
_winoLogger = winoLogger;
|
||||
_specialImapProviderConfigResolver = specialImapProviderConfigResolver;
|
||||
_calDavClient = calDavClient;
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(ExportLocalDataCommand))]
|
||||
[NotifyCanExecuteChangedFor(nameof(ImportLocalDataCommand))]
|
||||
public partial bool IsDataTransferInProgress { get; set; }
|
||||
|
||||
[RelayCommand]
|
||||
private async Task CreateMergedAccountAsync()
|
||||
{
|
||||
@@ -208,6 +224,95 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel
|
||||
[RelayCommand(CanExecute = nameof(CanReorderAccounts))]
|
||||
private Task ReorderAccountsAsync() => MailDialogService.ShowAccountReorderDialogAsync(availableAccounts: Accounts);
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanTransferLocalData))]
|
||||
private async Task ExportLocalDataAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var exportPath = await ExecuteUIThreadTaskAsync(
|
||||
() => MailDialogService.PickFilePathAsync(LocalExportFileName))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(exportPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await ExecuteUIThread(() => IsDataTransferInProgress = true);
|
||||
|
||||
var exportResult = await _syncService.ExportToJsonAsync(new()).ConfigureAwait(false);
|
||||
await File.WriteAllTextAsync(exportPath, exportResult.JsonContent, Utf8WithoutBom).ConfigureAwait(false);
|
||||
|
||||
DialogService.InfoBarMessage(
|
||||
Translator.GeneralTitle_Info,
|
||||
$"{BuildExportSuccessMessage(exportResult.ExportResult)} {string.Format(Translator.WinoAccount_Management_LocalDataSaved, exportPath)}",
|
||||
InfoBarMessageType.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DialogService.InfoBarMessage(
|
||||
Translator.GeneralTitle_Error,
|
||||
ex.Message,
|
||||
InfoBarMessageType.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await ExecuteUIThread(() => IsDataTransferInProgress = false);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanTransferLocalData))]
|
||||
private async Task ImportLocalDataAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var fileContent = await ExecuteUIThreadTaskAsync(
|
||||
() => MailDialogService.PickWindowsFileContentAsync(".json"))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (fileContent.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await ExecuteUIThread(() => IsDataTransferInProgress = true);
|
||||
|
||||
var jsonContent = Encoding.UTF8.GetString(fileContent);
|
||||
var result = await _syncService.ImportFromJsonAsync(jsonContent).ConfigureAwait(false);
|
||||
|
||||
await InitializeAccountsAsync().ConfigureAwait(false);
|
||||
|
||||
var messageType = result.FailedPreferenceCount > 0
|
||||
? InfoBarMessageType.Warning
|
||||
: InfoBarMessageType.Success;
|
||||
|
||||
DialogService.InfoBarMessage(
|
||||
result.FailedPreferenceCount > 0 ? Translator.GeneralTitle_Warning : Translator.GeneralTitle_Info,
|
||||
BuildImportMessage(result),
|
||||
messageType);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
DialogService.InfoBarMessage(
|
||||
Translator.GeneralTitle_Error,
|
||||
Translator.WinoAccount_Management_LocalDataInvalidFile,
|
||||
InfoBarMessageType.Error);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DialogService.InfoBarMessage(
|
||||
Translator.GeneralTitle_Error,
|
||||
ex.Message,
|
||||
InfoBarMessageType.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await ExecuteUIThread(() => IsDataTransferInProgress = false);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanTransferLocalData() => !IsDataTransferInProgress;
|
||||
|
||||
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedFrom(mode, parameters);
|
||||
@@ -294,4 +399,60 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel
|
||||
|
||||
await ManageStorePurchasesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static string BuildExportSuccessMessage(Wino.Core.Domain.Models.Accounts.WinoAccountSyncExportResult result)
|
||||
{
|
||||
var parts = new Collection<string>();
|
||||
|
||||
if (result.IncludedPreferences)
|
||||
{
|
||||
parts.Add(Translator.WinoAccount_Management_ExportPreferencesSucceeded);
|
||||
}
|
||||
|
||||
if (result.IncludedAccounts)
|
||||
{
|
||||
parts.Add(string.Format(Translator.WinoAccount_Management_ExportAccountsSucceeded, result.ExportedMailboxCount));
|
||||
}
|
||||
|
||||
if (parts.Count == 0)
|
||||
{
|
||||
parts.Add(Translator.WinoAccount_Management_ExportSucceeded);
|
||||
}
|
||||
|
||||
return string.Join(" ", parts);
|
||||
}
|
||||
|
||||
private static string BuildImportMessage(Wino.Core.Domain.Models.Accounts.WinoAccountSyncImportResult result)
|
||||
{
|
||||
var parts = new Collection<string>();
|
||||
|
||||
if (result.HadRemotePreferences)
|
||||
{
|
||||
parts.Add(result.FailedPreferenceCount > 0
|
||||
? string.Format(Translator.WinoAccount_Management_ImportPartial, result.AppliedPreferenceCount, result.FailedPreferenceCount)
|
||||
: string.Format(Translator.WinoAccount_Management_ImportPreferencesSucceeded, result.AppliedPreferenceCount));
|
||||
}
|
||||
|
||||
if (result.ImportedMailboxCount > 0)
|
||||
{
|
||||
parts.Add(string.Format(Translator.WinoAccount_Management_ImportAccountsSucceeded, result.ImportedMailboxCount));
|
||||
}
|
||||
|
||||
if (result.SkippedDuplicateMailboxCount > 0)
|
||||
{
|
||||
parts.Add(string.Format(Translator.WinoAccount_Management_ImportDuplicateAccountsSkipped, result.SkippedDuplicateMailboxCount));
|
||||
}
|
||||
|
||||
if (parts.Count == 0)
|
||||
{
|
||||
parts.Add(Translator.WinoAccount_Management_ImportEmpty);
|
||||
}
|
||||
|
||||
if (result.ImportedMailboxCount > 0)
|
||||
{
|
||||
parts.Add(Translator.WinoAccount_Management_ImportReloginReminder);
|
||||
}
|
||||
|
||||
return string.Join(" ", parts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
@@ -27,6 +30,7 @@ public partial class WelcomePageV2ViewModel : MailBaseViewModel
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(GetStartedCommand))]
|
||||
[NotifyCanExecuteChangedFor(nameof(ImportFromWinoAccountCommand))]
|
||||
[NotifyCanExecuteChangedFor(nameof(ImportFromJsonCommand))]
|
||||
public partial bool IsImportInProgress { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
@@ -101,6 +105,49 @@ public partial class WelcomePageV2ViewModel : MailBaseViewModel
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanOpenWelcomeActions))]
|
||||
private async Task ImportFromJsonAsync()
|
||||
{
|
||||
await ExecuteUIThread(() => ImportStatusMessage = string.Empty);
|
||||
|
||||
try
|
||||
{
|
||||
var fileContent = await _dialogService.PickWindowsFileContentAsync(".json");
|
||||
if (fileContent.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await ExecuteUIThread(() => IsImportInProgress = true);
|
||||
|
||||
var jsonContent = Encoding.UTF8.GetString(fileContent);
|
||||
var result = await _syncService.ImportFromJsonAsync(jsonContent);
|
||||
if (result.ImportedMailboxCount > 0)
|
||||
{
|
||||
ReportUIChange(new WelcomeImportCompletedMessage(result.ImportedMailboxCount));
|
||||
return;
|
||||
}
|
||||
|
||||
await ExecuteUIThread(() => ImportStatusMessage = BuildInlineImportMessage(result));
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
Debug.WriteLine(ex.Message);
|
||||
await _dialogService.ShowMessageAsync(
|
||||
Translator.WinoAccount_Management_LocalDataInvalidFile,
|
||||
Translator.GeneralTitle_Error,
|
||||
WinoCustomMessageDialogIcon.Error);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await _dialogService.ShowMessageAsync(ex.Message, Translator.GeneralTitle_Error, WinoCustomMessageDialogIcon.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await ExecuteUIThread(() => IsImportInProgress = false);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanOpenWelcomeActions() => !IsImportInProgress;
|
||||
|
||||
private static string BuildInlineImportMessage(WinoAccountSyncImportResult result)
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<Identity
|
||||
Name="58272BurakKSE.WinoMailPreview"
|
||||
Publisher="CN=bkaan"
|
||||
Version="2.0.3.0" />
|
||||
Version="2.0.4.0" />
|
||||
|
||||
<mp:PhoneIdentity PhoneProductId="3879fcfb-a561-4599-9103-e0c9b35a271f" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Wino.Core.ViewModels;
|
||||
using Wino.Mail.ViewModels;
|
||||
|
||||
namespace Wino.Mail.WinUI.Views.Abstract;
|
||||
|
||||
public abstract class ManageAccountsPageAbstract : BasePage<ManageAccountsPagePageViewModel>
|
||||
public abstract class ManageAccountsPageAbstract : BasePage<AccountManagementViewModel>
|
||||
{
|
||||
}
|
||||
|
||||
@@ -220,6 +220,21 @@
|
||||
<SymbolIcon Symbol="Account" />
|
||||
</winuiControls:SettingsCard.HeaderIcon>
|
||||
</winuiControls:SettingsCard>
|
||||
<winuiControls:SettingsCard
|
||||
Description="{x:Bind domain:Translator.WinoAccount_Management_LocalDataSectionDescription}"
|
||||
Header="{x:Bind domain:Translator.WinoAccount_Management_LocalDataSectionTitle}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||
<Button
|
||||
Command="{x:Bind ViewModel.ImportLocalDataCommand}"
|
||||
Content="{x:Bind domain:Translator.WinoAccount_Management_LocalDataImportAction}" />
|
||||
<Button
|
||||
Command="{x:Bind ViewModel.ExportLocalDataCommand}"
|
||||
Content="{x:Bind domain:Translator.WinoAccount_Management_LocalDataExportAction}" />
|
||||
</StackPanel>
|
||||
<winuiControls:SettingsCard.HeaderIcon>
|
||||
<SymbolIcon Symbol="Sync" />
|
||||
</winuiControls:SettingsCard.HeaderIcon>
|
||||
</winuiControls:SettingsCard>
|
||||
<winuiControls:SettingsCard
|
||||
Command="{x:Bind ViewModel.CreateMergedAccountCommand}"
|
||||
Description="{x:Bind domain:Translator.SettingsLinkAccounts_Description}"
|
||||
|
||||
@@ -162,12 +162,12 @@
|
||||
</AppBarButton>
|
||||
<AppBarToggleButton
|
||||
x:Name="ComposeAiActionsToggleButton"
|
||||
Checked="ComposeAiActionsToggleButton_Checked"
|
||||
MinWidth="40"
|
||||
HorizontalContentAlignment="Center"
|
||||
Checked="ComposeAiActionsToggleButton_Checked"
|
||||
LabelPosition="Collapsed"
|
||||
Visibility="{x:Bind GetAiActionsToggleVisibility(ViewModel.PreferencesService.IsAiActionsPanelHidden), Mode=OneWay}"
|
||||
ToolTipService.ToolTip="{x:Bind domain:Translator.Composer_AiActions}">
|
||||
ToolTipService.ToolTip="{x:Bind domain:Translator.Composer_AiActions}"
|
||||
Visibility="{x:Bind GetAiActionsToggleVisibility(ViewModel.PreferencesService.IsAiActionsPanelHidden), Mode=OneWay}">
|
||||
<AppBarToggleButton.Icon>
|
||||
<FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="" />
|
||||
</AppBarToggleButton.Icon>
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:abstract="using:Wino.Mail.WinUI.Views.Abstract"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:domain="using:Wino.Core.Domain"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:winuiControls="using:CommunityToolkit.WinUI.Controls"
|
||||
Style="{StaticResource PageStyle}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
@@ -12,6 +14,22 @@
|
||||
<Grid
|
||||
MaxWidth="900"
|
||||
Padding="20"
|
||||
HorizontalAlignment="Stretch" />
|
||||
HorizontalAlignment="Stretch">
|
||||
<winuiControls:SettingsCard
|
||||
Description="{x:Bind domain:Translator.WinoAccount_Management_LocalDataSectionDescription}"
|
||||
Header="{x:Bind domain:Translator.WinoAccount_Management_LocalDataSectionTitle}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||
<Button
|
||||
Command="{x:Bind ViewModel.ImportLocalDataCommand}"
|
||||
Content="{x:Bind domain:Translator.WinoAccount_Management_LocalDataImportAction}" />
|
||||
<Button
|
||||
Command="{x:Bind ViewModel.ExportLocalDataCommand}"
|
||||
Content="{x:Bind domain:Translator.WinoAccount_Management_LocalDataExportAction}" />
|
||||
</StackPanel>
|
||||
<winuiControls:SettingsCard.HeaderIcon>
|
||||
<SymbolIcon Symbol="Sync" />
|
||||
</winuiControls:SettingsCard.HeaderIcon>
|
||||
</winuiControls:SettingsCard>
|
||||
</Grid>
|
||||
</Border>
|
||||
</abstract:ManageAccountsPageAbstract>
|
||||
|
||||
@@ -126,13 +126,17 @@
|
||||
<StackPanel
|
||||
Grid.Row="3"
|
||||
MaxWidth="600"
|
||||
HorizontalAlignment="Center"
|
||||
Spacing="8">
|
||||
HorizontalAlignment="Center">
|
||||
<HyperlinkButton
|
||||
HorizontalAlignment="Center"
|
||||
Command="{x:Bind ViewModel.ImportFromWinoAccountCommand}"
|
||||
Content="{x:Bind domain:Translator.WelcomeWindow_ImportFromWinoAccount}" />
|
||||
|
||||
<HyperlinkButton
|
||||
HorizontalAlignment="Center"
|
||||
Command="{x:Bind ViewModel.ImportFromJsonCommand}"
|
||||
Content="{x:Bind domain:Translator.WelcomeWindow_ImportFromJsonFile}" />
|
||||
|
||||
<StackPanel
|
||||
x:Name="ImportProgressPanel"
|
||||
HorizontalAlignment="Stretch"
|
||||
@@ -157,6 +161,7 @@
|
||||
Visibility="{x:Bind ViewModel.HasImportStatus, Mode=OneWay}" />
|
||||
|
||||
<TextBlock
|
||||
Margin="0,4"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
@@ -164,6 +169,7 @@
|
||||
|
||||
<Button
|
||||
MinWidth="240"
|
||||
Margin="0,12,0,0"
|
||||
Padding="12,10"
|
||||
HorizontalAlignment="Center"
|
||||
Command="{x:Bind ViewModel.GetStartedCommand}"
|
||||
|
||||
@@ -378,7 +378,6 @@
|
||||
<AppxBundle>Always</AppxBundle>
|
||||
<AppxBundlePlatforms>x86|x64|arm64</AppxBundlePlatforms>
|
||||
<HoursBetweenUpdateChecks>0</HoursBetweenUpdateChecks>
|
||||
<PackageCertificateThumbprint>58CE57FA3E5BF2393C86FC3B3161A925679FB3D4</PackageCertificateThumbprint>
|
||||
<PackageCertificateKeyFile>Wino.Mail.WinUI_TemporaryKey.pfx</PackageCertificateKeyFile>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
@@ -600,6 +601,7 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
|
||||
[JsonSerializable(typeof(ApiEnvelope<UserMailboxSyncListDto>))]
|
||||
[JsonSerializable(typeof(ApiEnvelope<JsonElement>))]
|
||||
[JsonSerializable(typeof(ReplaceUserMailboxesRequestDto))]
|
||||
[JsonSerializable(typeof(List<UserMailboxSyncItemDto>))]
|
||||
internal sealed partial class WinoAccountApiJsonContext : JsonSerializerContext;
|
||||
|
||||
internal sealed record SyncStoreEntitlementsRequest(string? StoreIdKey, string? PurchaseIdKey);
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
@@ -18,6 +21,7 @@ namespace Wino.Services;
|
||||
public sealed class WinoAccountDataSyncService : IWinoAccountDataSyncService
|
||||
{
|
||||
private const int DefaultMaxConcurrentClients = 5;
|
||||
private const int LocalExportVersion = 1;
|
||||
|
||||
private readonly IWinoAccountProfileService _profileService;
|
||||
private readonly IPreferencesService _preferencesService;
|
||||
@@ -35,37 +39,159 @@ public sealed class WinoAccountDataSyncService : IWinoAccountDataSyncService
|
||||
|
||||
public async Task<WinoAccountSyncExportResult> ExportAsync(WinoAccountSyncSelection selection, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var exportedMailboxCount = 0;
|
||||
var preparedExport = await PrepareExportAsync(selection).ConfigureAwait(false);
|
||||
|
||||
if (selection.IncludePreferences)
|
||||
if (selection.IncludePreferences && preparedExport.PreferencesJson != null)
|
||||
{
|
||||
await _profileService.SaveSettingsAsync(_preferencesService.ExportPreferences(), cancellationToken).ConfigureAwait(false);
|
||||
await _profileService.SaveSettingsAsync(preparedExport.PreferencesJson, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (selection.IncludeAccounts)
|
||||
{
|
||||
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
|
||||
var request = new ReplaceUserMailboxesRequestDto
|
||||
{
|
||||
Mailboxes = accounts
|
||||
.OrderBy(a => a.Order)
|
||||
.Select(MapMailbox)
|
||||
.ToList()
|
||||
Mailboxes = preparedExport.Mailboxes
|
||||
};
|
||||
|
||||
await _profileService.ReplaceMailboxesAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
exportedMailboxCount = request.Mailboxes.Count;
|
||||
}
|
||||
|
||||
return new WinoAccountSyncExportResult
|
||||
return preparedExport.ExportResult;
|
||||
}
|
||||
|
||||
public async Task<WinoAccountSyncFileExportResult> ExportToJsonAsync(WinoAccountSyncSelection selection, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var preparedExport = await PrepareExportAsync(selection).ConfigureAwait(false);
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }))
|
||||
{
|
||||
IncludedPreferences = selection.IncludePreferences,
|
||||
IncludedAccounts = selection.IncludeAccounts,
|
||||
ExportedMailboxCount = exportedMailboxCount
|
||||
writer.WriteStartObject();
|
||||
writer.WriteNumber("version", LocalExportVersion);
|
||||
writer.WriteString("exportedAtUtc", DateTime.UtcNow);
|
||||
writer.WriteBoolean("includesPreferences", preparedExport.ExportResult.IncludedPreferences);
|
||||
writer.WriteBoolean("includesAccounts", preparedExport.ExportResult.IncludedAccounts);
|
||||
|
||||
writer.WritePropertyName("preferences");
|
||||
if (!string.IsNullOrWhiteSpace(preparedExport.PreferencesJson))
|
||||
{
|
||||
using var preferencesDocument = JsonDocument.Parse(preparedExport.PreferencesJson);
|
||||
preferencesDocument.RootElement.WriteTo(writer);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
}
|
||||
|
||||
writer.WritePropertyName("mailboxes");
|
||||
JsonSerializer.Serialize(writer, preparedExport.Mailboxes, WinoAccountApiJsonContext.Default.ListUserMailboxSyncItemDto);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
return new WinoAccountSyncFileExportResult
|
||||
{
|
||||
JsonContent = Encoding.UTF8.GetString(stream.ToArray()),
|
||||
ExportResult = preparedExport.ExportResult
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<WinoAccountSyncImportResult> ImportAsync(WinoAccountSyncSelection selection, CancellationToken cancellationToken = default)
|
||||
{
|
||||
string? settingsJson = null;
|
||||
List<UserMailboxSyncItemDto> orderedMailboxes = [];
|
||||
|
||||
if (selection.IncludePreferences)
|
||||
{
|
||||
settingsJson = await _profileService.GetSettingsAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (selection.IncludeAccounts)
|
||||
{
|
||||
var mailboxes = await _profileService.GetMailboxesAsync(cancellationToken).ConfigureAwait(false);
|
||||
orderedMailboxes = mailboxes.Mailboxes
|
||||
.OrderBy(a => a.SortOrder)
|
||||
.ThenBy(a => a.Address, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
return await ImportDataAsync(selection, settingsJson, orderedMailboxes, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<WinoAccountSyncImportResult> ImportFromJsonAsync(string jsonContent, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
jsonContent = TrimUtf8Bom(jsonContent);
|
||||
|
||||
using var document = JsonDocument.Parse(jsonContent);
|
||||
if (document.RootElement.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
throw new JsonException("Invalid root element.");
|
||||
}
|
||||
|
||||
string? settingsJson = null;
|
||||
if (document.RootElement.TryGetProperty("preferences", out var preferencesElement))
|
||||
{
|
||||
settingsJson = preferencesElement.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Object => preferencesElement.GetRawText(),
|
||||
JsonValueKind.String => preferencesElement.GetString(),
|
||||
JsonValueKind.Null or JsonValueKind.Undefined => null,
|
||||
_ => throw new JsonException("Invalid preferences payload.")
|
||||
};
|
||||
}
|
||||
|
||||
var mailboxes = new List<UserMailboxSyncItemDto>();
|
||||
if (document.RootElement.TryGetProperty("mailboxes", out var mailboxesElement))
|
||||
{
|
||||
if (mailboxesElement.ValueKind is not (JsonValueKind.Array or JsonValueKind.Null or JsonValueKind.Undefined))
|
||||
{
|
||||
throw new JsonException("Invalid mailboxes payload.");
|
||||
}
|
||||
|
||||
if (mailboxesElement.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
mailboxes = JsonSerializer.Deserialize(mailboxesElement.GetRawText(), WinoAccountApiJsonContext.Default.ListUserMailboxSyncItemDto) ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
var selection = new WinoAccountSyncSelection(
|
||||
IncludePreferences: !string.IsNullOrWhiteSpace(settingsJson),
|
||||
IncludeAccounts: mailboxes.Count > 0);
|
||||
|
||||
return await ImportDataAsync(selection, settingsJson, mailboxes, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<PreparedSyncExport> PrepareExportAsync(WinoAccountSyncSelection selection)
|
||||
{
|
||||
var preferencesJson = selection.IncludePreferences
|
||||
? _preferencesService.ExportPreferences()
|
||||
: null;
|
||||
|
||||
var mailboxes = selection.IncludeAccounts
|
||||
? (await _accountService.GetAccountsAsync().ConfigureAwait(false))
|
||||
.OrderBy(a => a.Order)
|
||||
.Select(MapMailbox)
|
||||
.ToList()
|
||||
: [];
|
||||
|
||||
return new PreparedSyncExport(
|
||||
preferencesJson,
|
||||
mailboxes,
|
||||
new WinoAccountSyncExportResult
|
||||
{
|
||||
IncludedPreferences = selection.IncludePreferences,
|
||||
IncludedAccounts = selection.IncludeAccounts,
|
||||
ExportedMailboxCount = mailboxes.Count
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<WinoAccountSyncImportResult> ImportDataAsync(
|
||||
WinoAccountSyncSelection selection,
|
||||
string? settingsJson,
|
||||
List<UserMailboxSyncItemDto> mailboxes,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new WinoAccountSyncImportResult
|
||||
{
|
||||
@@ -73,30 +199,25 @@ public sealed class WinoAccountDataSyncService : IWinoAccountDataSyncService
|
||||
IncludedAccounts = selection.IncludeAccounts
|
||||
};
|
||||
|
||||
if (selection.IncludePreferences)
|
||||
if (selection.IncludePreferences && !string.IsNullOrWhiteSpace(settingsJson))
|
||||
{
|
||||
var settingsJson = await _profileService.GetSettingsAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (!string.IsNullOrWhiteSpace(settingsJson))
|
||||
var (appliedCount, failedCount) = _preferencesService.ImportPreferences(settingsJson);
|
||||
result = new WinoAccountSyncImportResult
|
||||
{
|
||||
var (appliedCount, failedCount) = _preferencesService.ImportPreferences(settingsJson);
|
||||
result = new WinoAccountSyncImportResult
|
||||
{
|
||||
IncludedPreferences = result.IncludedPreferences,
|
||||
IncludedAccounts = result.IncludedAccounts,
|
||||
HadRemotePreferences = true,
|
||||
AppliedPreferenceCount = appliedCount,
|
||||
FailedPreferenceCount = failedCount,
|
||||
ImportedMailboxCount = result.ImportedMailboxCount,
|
||||
SkippedDuplicateMailboxCount = result.SkippedDuplicateMailboxCount,
|
||||
RemoteMailboxCount = result.RemoteMailboxCount
|
||||
};
|
||||
}
|
||||
IncludedPreferences = result.IncludedPreferences,
|
||||
IncludedAccounts = result.IncludedAccounts,
|
||||
HadRemotePreferences = true,
|
||||
AppliedPreferenceCount = appliedCount,
|
||||
FailedPreferenceCount = failedCount,
|
||||
ImportedMailboxCount = result.ImportedMailboxCount,
|
||||
SkippedDuplicateMailboxCount = result.SkippedDuplicateMailboxCount,
|
||||
RemoteMailboxCount = result.RemoteMailboxCount
|
||||
};
|
||||
}
|
||||
|
||||
if (selection.IncludeAccounts)
|
||||
{
|
||||
var mailboxes = await _profileService.GetMailboxesAsync(cancellationToken).ConfigureAwait(false);
|
||||
var orderedMailboxes = mailboxes.Mailboxes
|
||||
var orderedMailboxes = mailboxes
|
||||
.OrderBy(a => a.SortOrder)
|
||||
.ThenBy(a => a.Address, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
@@ -288,4 +409,14 @@ public sealed class WinoAccountDataSyncService : IWinoAccountDataSyncService
|
||||
|
||||
private static string CreateMailboxKey(string? address, int providerType)
|
||||
=> $"{address?.Trim().ToLowerInvariant()}|{providerType}";
|
||||
|
||||
private static string TrimUtf8Bom(string jsonContent)
|
||||
=> !string.IsNullOrEmpty(jsonContent) && jsonContent[0] == '\uFEFF'
|
||||
? jsonContent[1..]
|
||||
: jsonContent;
|
||||
|
||||
private sealed record PreparedSyncExport(
|
||||
string? PreferencesJson,
|
||||
List<UserMailboxSyncItemDto> Mailboxes,
|
||||
WinoAccountSyncExportResult ExportResult);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user