Wino accounts settings.
This commit is contained in:
@@ -144,6 +144,7 @@ private string searchQuery = string.Empty;
|
||||
- In `EventDetailsPageViewModel.LoadAttendeesAsync`, never mutate `CurrentEvent.Attendees` outside `ExecuteUIThread(...)`.
|
||||
- Never create pure C# controls or controls that heavily manipulate UI structure from `.cs` files. Define controls in XAML and keep UI composition in XAML.
|
||||
- Never subscribe to framework events like `Loaded`, `Unloaded`, or input events from constructors in `.xaml.cs` for XAML-backed controls and pages; wire them directly in XAML instead.
|
||||
- If you use `x:Load` in XAML, always give that `UIElement` an `x:Name`.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
// For updating account calendars asynchronously.
|
||||
private SemaphoreSlim _accountCalendarUpdateSemaphoreSlim = new(1);
|
||||
private bool _runtimeSubscriptionsAttached;
|
||||
private bool _hasRegisteredPersistentRecipients;
|
||||
|
||||
public CalendarAppShellViewModel(IPreferencesService preferencesService,
|
||||
IStatePersistanceService statePersistanceService,
|
||||
@@ -151,7 +152,12 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
|
||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
if (!_hasRegisteredPersistentRecipients)
|
||||
{
|
||||
RegisterRecipients();
|
||||
_hasRegisteredPersistentRecipients = true;
|
||||
}
|
||||
|
||||
AttachRuntimeSubscriptions();
|
||||
|
||||
var activationContext = parameters as ShellModeActivationContext;
|
||||
@@ -177,8 +183,6 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
|
||||
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedFrom(mode, parameters);
|
||||
|
||||
DetachRuntimeSubscriptions();
|
||||
PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
|
||||
_ = ExecuteUIThread(() =>
|
||||
|
||||
@@ -36,6 +36,7 @@ public enum WinoPage
|
||||
EmailTemplatesPage,
|
||||
CreateEmailTemplatePage,
|
||||
StoragePage,
|
||||
WinoAccountManagementPage,
|
||||
WelcomePageV2,
|
||||
WelcomeHostPage,
|
||||
ProviderSelectionPage,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#nullable enable
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Mail.Api.Contracts.Ai;
|
||||
using Wino.Mail.Api.Contracts.Auth;
|
||||
using Wino.Mail.Api.Contracts.Common;
|
||||
|
||||
@@ -12,4 +14,8 @@ public interface IWinoAccountApiClient
|
||||
Task<ApiEnvelope<AuthResultDto>> LoginAsync(string email, string password, CancellationToken cancellationToken = default);
|
||||
Task<ApiEnvelope<AuthResultDto>> RefreshAsync(string refreshToken, CancellationToken cancellationToken = default);
|
||||
Task<ApiEnvelope<JsonElement>> LogoutAsync(string refreshToken, CancellationToken cancellationToken = default);
|
||||
Task<ApiEnvelope<AuthUserDto>> GetCurrentUserAsync(CancellationToken cancellationToken = default);
|
||||
Task<ApiEnvelope<AiStatusResultDto>> GetAiStatusAsync(CancellationToken cancellationToken = default);
|
||||
Task<string?> GetSettingsAsync(CancellationToken cancellationToken = default);
|
||||
Task<bool> SaveSettingsAsync(string settingsJson, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,11 @@ public static class SettingsNavigationInfoProvider
|
||||
manageAccountsDescription,
|
||||
"\uE77B",
|
||||
searchKeywords: Translator.SettingsSearch_ManageAccounts_Keywords),
|
||||
new(WinoPage.WinoAccountManagementPage,
|
||||
Translator.WinoAccount_SettingsSection_Title,
|
||||
Translator.WinoAccount_SettingsSection_Description,
|
||||
"\uE77B",
|
||||
searchKeywords: Translator.SettingsSearch_WinoAccount_Keywords),
|
||||
new(null, Translator.SettingsOptions_GeneralSection, string.Empty, "\uE713", isSeparator: true),
|
||||
new(WinoPage.AppPreferencesPage,
|
||||
Translator.SettingsAppPreferences_Title,
|
||||
@@ -130,6 +135,7 @@ public static class SettingsNavigationInfoProvider
|
||||
WinoPage.SettingOptionsPage => Translator.MenuSettings,
|
||||
WinoPage.ManageAccountsPage => Translator.SettingsManageAccountSettings_Title,
|
||||
WinoPage.AccountManagementPage => Translator.SettingsManageAccountSettings_Title,
|
||||
WinoPage.WinoAccountManagementPage => Translator.WinoAccount_SettingsSection_Title,
|
||||
WinoPage.PersonalizationPage => Translator.SettingsPersonalization_Title,
|
||||
WinoPage.AboutPage => Translator.SettingsAbout_Title,
|
||||
WinoPage.MessageListPage => Translator.SettingsMessageList_Title,
|
||||
|
||||
@@ -829,6 +829,12 @@
|
||||
"SettingsHome_StorageCard_Description": "See how much local MIME content Wino keeps on this device and clean it up when needed.",
|
||||
"SettingsHome_StorageEmptySummary": "No cached MIME content detected yet.",
|
||||
"SettingsHome_StorageLoading": "Checking local MIME usage...",
|
||||
"SettingsHome_WinoAccount_Title": "Wino Account",
|
||||
"SettingsHome_WinoAccount_SignedOutDescription": "Sign in to sync settings across devices and access add-ons like Wino AI Pack.",
|
||||
"SettingsHome_WinoAccount_ManageLink": "Manage Wino Account",
|
||||
"SettingsHome_AiPack_Title": "Wino AI Pack",
|
||||
"SettingsHome_SettingsSync_Title": "Settings sync",
|
||||
"SettingsHome_SettingsSync_Description": "Export or import your preferences to keep them in sync across devices.",
|
||||
"SettingsHome_Tips_Title": "Tips & tricks",
|
||||
"SettingsHome_Tips_Description": "A few small changes can make Wino feel much more personal.",
|
||||
"SettingsHome_Tip_Theme": "Want dark mode or accent changes? Open Personalization.",
|
||||
@@ -849,6 +855,32 @@
|
||||
"SettingsSearch_Personalization_Keywords": "theme;dark;light;appearance;accent;color;colour;mode;layout;density",
|
||||
"SettingsSearch_About_Keywords": "about;version;website;privacy;github;donate;store;support",
|
||||
"SettingsSearch_KeyboardShortcuts_Keywords": "shortcut;shortcuts;hotkey;hotkeys;keyboard;keys",
|
||||
"SettingsSearch_WinoAccount_Keywords": "winoaccount;account;accounts;wino",
|
||||
"WinoAccount_Management_Description": "Manage your Wino Account, AI Pack access, and synchronized settings.",
|
||||
"WinoAccount_Management_SignedOutTitle": "Sign in to Wino Account",
|
||||
"WinoAccount_Management_SignedOutDescription": "Sign in or create a Wino Account to sync your settings and manage Wino add-ons.",
|
||||
"WinoAccount_Management_AccountCardTitle": "Account",
|
||||
"WinoAccount_Management_AccountCardDescription": "Your Wino Account email address and current account state.",
|
||||
"WinoAccount_Management_AiPackCardTitle": "Wino AI Pack",
|
||||
"WinoAccount_Management_AiPackCardDescription": "See whether Wino AI Pack is active and how much usage is left.",
|
||||
"WinoAccount_Management_AiPackActive": "AI Pack is active",
|
||||
"WinoAccount_Management_AiPackInactive": "AI Pack is not active",
|
||||
"WinoAccount_Management_AiPackUsage": "{0} of {1} uses consumed. {2} remaining.",
|
||||
"WinoAccount_Management_AiPackBillingPeriod": "Billing period: {0:d} - {1:d}",
|
||||
"WinoAccount_Management_AiPackUnknownUsage": "Usage details are not available yet.",
|
||||
"WinoAccount_Management_AiPackBuyDescription": "Buy Wino AI Pack to translate, rewrite or summarize emails with AI.",
|
||||
"WinoAccount_Management_SettingsCardTitle": "Settings sync",
|
||||
"WinoAccount_Management_SettingsCardDescription": "Export your current settings to your Wino Account or import them back on this device.",
|
||||
"WinoAccount_Management_StatusLabel": "Status: {0}",
|
||||
"WinoAccount_Management_NoRemoteSettings": "There are no synchronized settings stored for this account yet.",
|
||||
"WinoAccount_Management_ExportSucceeded": "Your settings were exported to your Wino Account.",
|
||||
"WinoAccount_Management_ImportSucceeded": "Imported {0} settings from your Wino Account.",
|
||||
"WinoAccount_Management_ImportPartial": "Imported {0} settings. {1} settings could not be restored.",
|
||||
"WinoAccount_Management_SerializeFailed": "Wino could not serialize your current preferences.",
|
||||
"WinoAccount_Management_EmptyExport": "There are no preference values to export.",
|
||||
"WinoAccount_Management_ImportEmpty": "The synchronized settings payload does not contain any values to restore.",
|
||||
"WinoAccount_Management_LoadFailed": "Wino could not load the latest Wino Account information.",
|
||||
"WinoAccount_Management_ActionFailed": "The Wino Account request could not be completed.",
|
||||
"SettingsSearch_MessageList_Keywords": "message;messages;list;threading;threads;avatar;preview;sender",
|
||||
"SettingsSearch_ReadComposePane_Keywords": "reader;compose;composer;font;fonts;external content;display;reading",
|
||||
"SettingsSearch_SignatureAndEncryption_Keywords": "signature;signatures;encryption;certificate;certificates;s mime;smime;security",
|
||||
|
||||
@@ -16,13 +16,19 @@ using Wino.Core.Domain.Models.Settings;
|
||||
using Wino.Core.Domain.Models.Translations;
|
||||
using Wino.Core.Extensions;
|
||||
using Wino.Core.ViewModels.Data;
|
||||
using Wino.Mail.Api.Contracts.Ai;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
using Wino.Messaging.Client.Navigation;
|
||||
using Wino.Messaging.UI;
|
||||
|
||||
namespace Wino.Core.ViewModels;
|
||||
|
||||
public partial class SettingOptionsPageViewModel : CoreBaseViewModel
|
||||
public partial class SettingOptionsPageViewModel : CoreBaseViewModel,
|
||||
IRecipient<WinoAccountSignedInMessage>,
|
||||
IRecipient<WinoAccountSignedOutMessage>
|
||||
{
|
||||
private const string BuyAiPackUrl = "https://example.com/wino-ai-pack";
|
||||
|
||||
private readonly INativeAppService _nativeAppService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly IMimeStorageService _mimeStorageService;
|
||||
@@ -31,6 +37,9 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel
|
||||
private readonly INewThemeService _newThemeService;
|
||||
private readonly IPreferencesService _preferencesService;
|
||||
private readonly IProviderService _providerService;
|
||||
private readonly IWinoAccountProfileService _profileService;
|
||||
private readonly IWinoAccountApiClient _apiClient;
|
||||
private readonly IMailDialogService _dialogService;
|
||||
private bool _isInitializingSettings;
|
||||
private bool _isAppearanceSelectionPaused;
|
||||
|
||||
@@ -82,6 +91,43 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel
|
||||
[ObservableProperty]
|
||||
public partial bool UseAccentColor { get; set; }
|
||||
|
||||
// Wino Account hero card properties
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsWinoAccountBusy { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsWinoAccountSignedOut))]
|
||||
[NotifyPropertyChangedFor(nameof(CanShowBuyAiPack))]
|
||||
public partial bool IsWinoAccountSignedIn { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string WinoAccountEmail { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string WinoAccountStatusText { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(CanShowAiUsage))]
|
||||
[NotifyPropertyChangedFor(nameof(CanShowBuyAiPack))]
|
||||
public partial bool HasAiPack { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string AiPackStateText { get; set; } = Translator.WinoAccount_Management_AiPackInactive;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string AiUsageSummary { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string AiBillingPeriodSummary { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double AiUsagePercent { get; set; }
|
||||
|
||||
public bool IsWinoAccountSignedOut => !IsWinoAccountSignedIn;
|
||||
public bool CanShowAiUsage => HasAiPack;
|
||||
public bool CanShowBuyAiPack => IsWinoAccountSignedIn && !HasAiPack;
|
||||
|
||||
public SettingOptionsPageViewModel(INativeAppService nativeAppService,
|
||||
IAccountService accountService,
|
||||
IMimeStorageService mimeStorageService,
|
||||
@@ -89,7 +135,10 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel
|
||||
ITranslationService translationService,
|
||||
INewThemeService newThemeService,
|
||||
IPreferencesService preferencesService,
|
||||
IProviderService providerService)
|
||||
IProviderService providerService,
|
||||
IWinoAccountProfileService profileService,
|
||||
IWinoAccountApiClient apiClient,
|
||||
IMailDialogService dialogService)
|
||||
{
|
||||
_nativeAppService = nativeAppService;
|
||||
_accountService = accountService;
|
||||
@@ -99,6 +148,9 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel
|
||||
_newThemeService = newThemeService;
|
||||
_preferencesService = preferencesService;
|
||||
_providerService = providerService;
|
||||
_profileService = profileService;
|
||||
_apiClient = apiClient;
|
||||
_dialogService = dialogService;
|
||||
}
|
||||
|
||||
public override void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
@@ -112,6 +164,7 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel
|
||||
InitializeQuickSettings();
|
||||
|
||||
_ = LoadDashboardAsync();
|
||||
_ = LoadWinoAccountAsync();
|
||||
}
|
||||
|
||||
public void UpdateSearchSuggestions(string query)
|
||||
@@ -365,6 +418,181 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel
|
||||
await _translationService.InitializeLanguageAsync(language.Language);
|
||||
}
|
||||
|
||||
// Wino Account message recipients
|
||||
|
||||
protected override void RegisterRecipients()
|
||||
{
|
||||
base.RegisterRecipients();
|
||||
|
||||
Messenger.Register<WinoAccountSignedInMessage>(this);
|
||||
Messenger.Register<WinoAccountSignedOutMessage>(this);
|
||||
}
|
||||
|
||||
protected override void UnregisterRecipients()
|
||||
{
|
||||
base.UnregisterRecipients();
|
||||
|
||||
Messenger.Unregister<WinoAccountSignedInMessage>(this);
|
||||
Messenger.Unregister<WinoAccountSignedOutMessage>(this);
|
||||
}
|
||||
|
||||
public void Receive(WinoAccountSignedInMessage message)
|
||||
=> _ = LoadWinoAccountAsync();
|
||||
|
||||
public void Receive(WinoAccountSignedOutMessage message)
|
||||
=> _ = ResetWinoAccountStateAsync();
|
||||
|
||||
// Wino Account hero card commands and helpers
|
||||
|
||||
[RelayCommand]
|
||||
private async Task WinoAccountRegisterAsync()
|
||||
{
|
||||
var account = await _dialogService.ShowWinoAccountRegistrationDialogAsync();
|
||||
if (account == null) return;
|
||||
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Info,
|
||||
string.Format(Translator.WinoAccount_RegisterSuccessMessage, account.Email),
|
||||
InfoBarMessageType.Success);
|
||||
await LoadWinoAccountAsync();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task WinoAccountSignInAsync()
|
||||
{
|
||||
var account = await _dialogService.ShowWinoAccountLoginDialogAsync();
|
||||
if (account == null) return;
|
||||
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Info,
|
||||
string.Format(Translator.WinoAccount_LoginSuccessMessage, account.Email),
|
||||
InfoBarMessageType.Success);
|
||||
await LoadWinoAccountAsync();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task WinoAccountSignOutAsync()
|
||||
{
|
||||
var account = await _profileService.GetActiveAccountAsync().ConfigureAwait(false);
|
||||
if (account == null) return;
|
||||
|
||||
await _profileService.SignOutAsync().ConfigureAwait(false);
|
||||
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Info,
|
||||
string.Format(Translator.WinoAccount_SignOut_SuccessMessage, account.Email),
|
||||
InfoBarMessageType.Success);
|
||||
await ResetWinoAccountStateAsync();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task OpenBuyAiPackPageAsync() => await _nativeAppService.LaunchUriAsync(new Uri(BuyAiPackUrl));
|
||||
|
||||
[RelayCommand]
|
||||
private void NavigateToWinoAccountManagement()
|
||||
=> NavigateSubDetail(WinoPage.WinoAccountManagementPage);
|
||||
|
||||
private async Task LoadWinoAccountAsync()
|
||||
{
|
||||
await ExecuteUIThread(() => IsWinoAccountBusy = true);
|
||||
|
||||
try
|
||||
{
|
||||
var account = await EnsureAuthenticatedAccountAsync().ConfigureAwait(false);
|
||||
if (account == null)
|
||||
{
|
||||
await ResetWinoAccountStateAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var currentUserResponse = await _apiClient.GetCurrentUserAsync().ConfigureAwait(false);
|
||||
var aiStatusResponse = await _apiClient.GetAiStatusAsync().ConfigureAwait(false);
|
||||
|
||||
var resolvedEmail = currentUserResponse.IsSuccess && currentUserResponse.Result != null
|
||||
? currentUserResponse.Result.Email
|
||||
: account.Email;
|
||||
|
||||
var resolvedStatus = currentUserResponse.IsSuccess && currentUserResponse.Result != null
|
||||
? currentUserResponse.Result.AccountStatus
|
||||
: account.AccountStatus;
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
IsWinoAccountSignedIn = true;
|
||||
WinoAccountEmail = resolvedEmail;
|
||||
WinoAccountStatusText = string.Format(Translator.WinoAccount_Management_StatusLabel, resolvedStatus);
|
||||
});
|
||||
|
||||
UpdateAiPackState(aiStatusResponse.IsSuccess ? aiStatusResponse.Result : null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await ResetWinoAccountStateAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
await ExecuteUIThread(() => IsWinoAccountBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ResetWinoAccountStateAsync()
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
IsWinoAccountSignedIn = false;
|
||||
WinoAccountEmail = string.Empty;
|
||||
WinoAccountStatusText = string.Empty;
|
||||
HasAiPack = false;
|
||||
AiPackStateText = Translator.WinoAccount_Management_AiPackInactive;
|
||||
AiUsageSummary = string.Empty;
|
||||
AiBillingPeriodSummary = string.Empty;
|
||||
AiUsagePercent = 0;
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<WinoAccount> EnsureAuthenticatedAccountAsync()
|
||||
{
|
||||
var account = await _profileService.GetActiveAccountAsync().ConfigureAwait(false);
|
||||
if (account == null) return null;
|
||||
|
||||
if (account.AccessTokenExpiresAtUtc <= DateTime.UtcNow.AddMinutes(1))
|
||||
{
|
||||
var refreshResult = await _profileService.RefreshAsync().ConfigureAwait(false);
|
||||
if (!refreshResult.IsSuccess) return null;
|
||||
|
||||
account = refreshResult.Account ?? await _profileService.GetActiveAccountAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return account != null && !string.IsNullOrWhiteSpace(account.AccessToken) ? account : null;
|
||||
}
|
||||
|
||||
private void UpdateAiPackState(AiStatusResultDto aiStatus)
|
||||
{
|
||||
var hasAiPack = aiStatus?.HasAiPack == true;
|
||||
var usageText = Translator.WinoAccount_Management_AiPackUnknownUsage;
|
||||
var billingText = string.Empty;
|
||||
var usagePercent = 0d;
|
||||
|
||||
if (hasAiPack && aiStatus?.Used is int used && aiStatus.MonthlyLimit is int limit && aiStatus.Remaining is int remaining)
|
||||
{
|
||||
usageText = string.Format(Translator.WinoAccount_Management_AiPackUsage, used, limit, remaining);
|
||||
usagePercent = limit > 0 ? (double)used / limit * 100 : 0;
|
||||
}
|
||||
|
||||
if (hasAiPack && aiStatus?.CurrentPeriodStartUtc is DateTimeOffset periodStart && aiStatus.CurrentPeriodEndUtc is DateTimeOffset periodEnd)
|
||||
{
|
||||
billingText = string.Format(Translator.WinoAccount_Management_AiPackBillingPeriod, periodStart.LocalDateTime, periodEnd.LocalDateTime);
|
||||
}
|
||||
|
||||
_ = ExecuteUIThread(() =>
|
||||
{
|
||||
HasAiPack = hasAiPack;
|
||||
AiPackStateText = hasAiPack
|
||||
? Translator.WinoAccount_Management_AiPackActive
|
||||
: Translator.WinoAccount_Management_AiPackInactive;
|
||||
AiUsageSummary = hasAiPack ? usageText : string.Empty;
|
||||
AiBillingPeriodSummary = hasAiPack ? billingText : string.Empty;
|
||||
AiUsagePercent = usagePercent;
|
||||
});
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void NavigateSubDetail(object type)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,554 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Mail.Api.Contracts.Ai;
|
||||
using Wino.Mail.Api.Contracts.Auth;
|
||||
|
||||
namespace Wino.Core.ViewModels;
|
||||
|
||||
public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel
|
||||
{
|
||||
private const string BuyAiPackUrl = "https://example.com/wino-ai-pack";
|
||||
|
||||
private readonly IWinoAccountProfileService _profileService;
|
||||
private readonly IWinoAccountApiClient _apiClient;
|
||||
private readonly IPreferencesService _preferencesService;
|
||||
private readonly IMailDialogService _dialogService;
|
||||
private readonly INativeAppService _nativeAppService;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsBusy { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsSignedOut))]
|
||||
[NotifyPropertyChangedFor(nameof(CanShowBuyAiPack))]
|
||||
public partial bool IsSignedIn { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string AccountEmail { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string AccountStatusText { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(CanShowAiUsage))]
|
||||
[NotifyPropertyChangedFor(nameof(CanShowBuyAiPack))]
|
||||
public partial bool HasAiPack { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string AiPackStateText { get; set; } = Translator.WinoAccount_Management_AiPackInactive;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string AiUsageSummary { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string AiBillingPeriodSummary { get; set; } = string.Empty;
|
||||
|
||||
public bool IsSignedOut => !IsSignedIn;
|
||||
public bool CanShowAiUsage => HasAiPack;
|
||||
public bool CanShowBuyAiPack => IsSignedIn && !HasAiPack;
|
||||
|
||||
public WinoAccountManagementPageViewModel(IWinoAccountProfileService profileService,
|
||||
IWinoAccountApiClient apiClient,
|
||||
IPreferencesService preferencesService,
|
||||
IMailDialogService dialogService,
|
||||
INativeAppService nativeAppService)
|
||||
{
|
||||
_profileService = profileService;
|
||||
_apiClient = apiClient;
|
||||
_preferencesService = preferencesService;
|
||||
_dialogService = dialogService;
|
||||
_nativeAppService = nativeAppService;
|
||||
}
|
||||
|
||||
public override void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
_ = LoadAsync();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task RegisterAsync()
|
||||
{
|
||||
var account = await _dialogService.ShowWinoAccountRegistrationDialogAsync();
|
||||
if (account == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Info,
|
||||
string.Format(Translator.WinoAccount_RegisterSuccessMessage, account.Email),
|
||||
InfoBarMessageType.Success);
|
||||
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SignInAsync()
|
||||
{
|
||||
var account = await _dialogService.ShowWinoAccountLoginDialogAsync();
|
||||
if (account == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Info,
|
||||
string.Format(Translator.WinoAccount_LoginSuccessMessage, account.Email),
|
||||
InfoBarMessageType.Success);
|
||||
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SignOutAsync()
|
||||
{
|
||||
var account = await _profileService.GetActiveAccountAsync().ConfigureAwait(false);
|
||||
if (account == null)
|
||||
{
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Warning,
|
||||
Translator.WinoAccount_SignOut_NoAccountMessage,
|
||||
InfoBarMessageType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
await _profileService.SignOutAsync().ConfigureAwait(false);
|
||||
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Info,
|
||||
string.Format(Translator.WinoAccount_SignOut_SuccessMessage, account.Email),
|
||||
InfoBarMessageType.Success);
|
||||
|
||||
await ResetSignedOutStateAsync();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task OpenBuyPageAsync() => await _nativeAppService.LaunchUriAsync(new Uri(BuyAiPackUrl));
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ExportSettingsAsync()
|
||||
{
|
||||
string settingsJson;
|
||||
|
||||
try
|
||||
{
|
||||
if (await EnsureAuthenticatedAccountAsync().ConfigureAwait(false) == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var settings = CollectPreferencesSnapshot();
|
||||
if (settings.Count == 0)
|
||||
{
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Warning,
|
||||
Translator.WinoAccount_Management_EmptyExport,
|
||||
InfoBarMessageType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
settingsJson = SerializePreferencesSnapshot(settings);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(settingsJson) || settingsJson == "{}")
|
||||
{
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Warning,
|
||||
Translator.WinoAccount_Management_EmptyExport,
|
||||
InfoBarMessageType.Warning);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
|
||||
Translator.WinoAccount_Management_SerializeFailed,
|
||||
InfoBarMessageType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (await EnsureAuthenticatedAccountAsync().ConfigureAwait(false) == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var isSaved = await _apiClient.SaveSettingsAsync(settingsJson).ConfigureAwait(false);
|
||||
|
||||
if (!isSaved)
|
||||
{
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
|
||||
Translator.WinoAccount_Management_ActionFailed,
|
||||
InfoBarMessageType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Info,
|
||||
Translator.WinoAccount_Management_ExportSucceeded,
|
||||
InfoBarMessageType.Success);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
|
||||
Translator.WinoAccount_Management_ActionFailed,
|
||||
InfoBarMessageType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ImportSettingsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (await EnsureAuthenticatedAccountAsync().ConfigureAwait(false) == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var settingsJson = await _apiClient.GetSettingsAsync().ConfigureAwait(false);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(settingsJson))
|
||||
{
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Warning,
|
||||
Translator.WinoAccount_Management_NoRemoteSettings,
|
||||
InfoBarMessageType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
using var document = JsonDocument.Parse(settingsJson);
|
||||
if (document.RootElement.ValueKind != JsonValueKind.Object || !document.RootElement.EnumerateObject().Any())
|
||||
{
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Warning,
|
||||
Translator.WinoAccount_Management_ImportEmpty,
|
||||
InfoBarMessageType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var (appliedCount, failedCount) = ApplyPreferencesSnapshot(document.RootElement);
|
||||
|
||||
if (appliedCount == 0)
|
||||
{
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Warning,
|
||||
Translator.WinoAccount_Management_ImportEmpty,
|
||||
InfoBarMessageType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (failedCount > 0)
|
||||
{
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Warning,
|
||||
string.Format(Translator.WinoAccount_Management_ImportPartial, appliedCount, failedCount),
|
||||
InfoBarMessageType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Info,
|
||||
string.Format(Translator.WinoAccount_Management_ImportSucceeded, appliedCount),
|
||||
InfoBarMessageType.Success);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
|
||||
Translator.WinoAccount_Management_ActionFailed,
|
||||
InfoBarMessageType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadAsync()
|
||||
{
|
||||
await ExecuteUIThread(() => IsBusy = true);
|
||||
|
||||
try
|
||||
{
|
||||
var account = await EnsureAuthenticatedAccountAsync().ConfigureAwait(false);
|
||||
|
||||
if (account == null)
|
||||
{
|
||||
await ResetSignedOutStateAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var currentUserResponse = await _apiClient.GetCurrentUserAsync().ConfigureAwait(false);
|
||||
var aiStatusResponse = await _apiClient.GetAiStatusAsync().ConfigureAwait(false);
|
||||
|
||||
var resolvedUser = currentUserResponse.IsSuccess && currentUserResponse.Result != null
|
||||
? currentUserResponse.Result
|
||||
: new AuthUserDto(account.Id, account.Email, account.AccountStatus, account.HasPassword, account.HasGoogleLogin, account.HasFacebookLogin);
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
IsSignedIn = true;
|
||||
AccountEmail = resolvedUser.Email;
|
||||
AccountStatusText = string.Format(Translator.WinoAccount_Management_StatusLabel, resolvedUser.AccountStatus);
|
||||
});
|
||||
|
||||
UpdateAiPackState(aiStatusResponse.IsSuccess ? aiStatusResponse.Result : null);
|
||||
|
||||
if (!currentUserResponse.IsSuccess || !aiStatusResponse.IsSuccess)
|
||||
{
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Warning,
|
||||
Translator.WinoAccount_Management_LoadFailed,
|
||||
InfoBarMessageType.Warning);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
|
||||
Translator.WinoAccount_Management_LoadFailed,
|
||||
InfoBarMessageType.Error);
|
||||
await ResetSignedOutStateAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
await ExecuteUIThread(() => IsBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ResetSignedOutStateAsync()
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
IsSignedIn = false;
|
||||
AccountEmail = string.Empty;
|
||||
AccountStatusText = string.Empty;
|
||||
HasAiPack = false;
|
||||
AiPackStateText = Translator.WinoAccount_Management_AiPackInactive;
|
||||
AiUsageSummary = string.Empty;
|
||||
AiBillingPeriodSummary = string.Empty;
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<WinoAccount?> EnsureAuthenticatedAccountAsync()
|
||||
{
|
||||
var account = await _profileService.GetActiveAccountAsync().ConfigureAwait(false);
|
||||
if (account == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (account.AccessTokenExpiresAtUtc <= DateTime.UtcNow.AddMinutes(1))
|
||||
{
|
||||
var refreshResult = await _profileService.RefreshAsync().ConfigureAwait(false);
|
||||
|
||||
if (!refreshResult.IsSuccess)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
account = refreshResult.Account ?? await _profileService.GetActiveAccountAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return account != null && !string.IsNullOrWhiteSpace(account.AccessToken)
|
||||
? account
|
||||
: null;
|
||||
}
|
||||
|
||||
private void UpdateAiPackState(AiStatusResultDto? aiStatus)
|
||||
{
|
||||
var hasAiPack = aiStatus?.HasAiPack == true;
|
||||
var usageText = Translator.WinoAccount_Management_AiPackUnknownUsage;
|
||||
var billingText = string.Empty;
|
||||
|
||||
if (hasAiPack && aiStatus?.Used is int used && aiStatus.MonthlyLimit is int limit && aiStatus.Remaining is int remaining)
|
||||
{
|
||||
usageText = string.Format(Translator.WinoAccount_Management_AiPackUsage, used, limit, remaining);
|
||||
}
|
||||
|
||||
if (hasAiPack && aiStatus?.CurrentPeriodStartUtc is DateTimeOffset periodStart && aiStatus.CurrentPeriodEndUtc is DateTimeOffset periodEnd)
|
||||
{
|
||||
billingText = string.Format(Translator.WinoAccount_Management_AiPackBillingPeriod, periodStart.LocalDateTime, periodEnd.LocalDateTime);
|
||||
}
|
||||
|
||||
_ = ExecuteUIThread(() =>
|
||||
{
|
||||
HasAiPack = hasAiPack;
|
||||
AiPackStateText = hasAiPack
|
||||
? Translator.WinoAccount_Management_AiPackActive
|
||||
: Translator.WinoAccount_Management_AiPackInactive;
|
||||
AiUsageSummary = hasAiPack ? usageText : string.Empty;
|
||||
AiBillingPeriodSummary = hasAiPack ? billingText : string.Empty;
|
||||
});
|
||||
}
|
||||
|
||||
private Dictionary<string, object?> CollectPreferencesSnapshot()
|
||||
{
|
||||
var settings = new Dictionary<string, object?>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var property in GetSyncablePreferenceProperties())
|
||||
{
|
||||
settings[property.Name] = property.GetValue(_preferencesService);
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
private static string SerializePreferencesSnapshot(Dictionary<string, object?> settings)
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
using (var writer = new Utf8JsonWriter(stream))
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
|
||||
foreach (var setting in settings)
|
||||
{
|
||||
WritePreferenceValue(writer, setting.Key, setting.Value);
|
||||
}
|
||||
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
return Encoding.UTF8.GetString(stream.ToArray());
|
||||
}
|
||||
|
||||
private (int appliedCount, int failedCount) ApplyPreferencesSnapshot(JsonElement rootElement)
|
||||
{
|
||||
var appliedCount = 0;
|
||||
var failedCount = 0;
|
||||
|
||||
foreach (var property in GetSyncablePreferenceProperties())
|
||||
{
|
||||
if (!rootElement.TryGetProperty(property.Name, out var value) || value.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
property.SetValue(_preferencesService, ReadPreferenceValue(property.PropertyType, value));
|
||||
appliedCount++;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
failedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return (appliedCount, failedCount);
|
||||
}
|
||||
|
||||
private static void WritePreferenceValue(Utf8JsonWriter writer, string propertyName, object? value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteNull(propertyName);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case string stringValue:
|
||||
writer.WriteString(propertyName, stringValue);
|
||||
return;
|
||||
case bool boolValue:
|
||||
writer.WriteBoolean(propertyName, boolValue);
|
||||
return;
|
||||
case int intValue:
|
||||
writer.WriteNumber(propertyName, intValue);
|
||||
return;
|
||||
case long longValue:
|
||||
writer.WriteNumber(propertyName, longValue);
|
||||
return;
|
||||
case double doubleValue:
|
||||
writer.WriteNumber(propertyName, doubleValue);
|
||||
return;
|
||||
case float floatValue:
|
||||
writer.WriteNumber(propertyName, floatValue);
|
||||
return;
|
||||
case Guid guidValue:
|
||||
writer.WriteString(propertyName, guidValue);
|
||||
return;
|
||||
case TimeSpan timeSpanValue:
|
||||
writer.WriteString(propertyName, timeSpanValue.ToString("c", CultureInfo.InvariantCulture));
|
||||
return;
|
||||
}
|
||||
|
||||
var valueType = Nullable.GetUnderlyingType(value.GetType()) ?? value.GetType();
|
||||
if (valueType.IsEnum)
|
||||
{
|
||||
writer.WriteString(propertyName, value.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteString(propertyName, Convert.ToString(value, CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
private static object? ReadPreferenceValue(Type propertyType, JsonElement value)
|
||||
{
|
||||
var targetType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
|
||||
|
||||
if (value.ValueKind == JsonValueKind.Null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (targetType == typeof(string))
|
||||
{
|
||||
return value.GetString() ?? string.Empty;
|
||||
}
|
||||
|
||||
if (targetType == typeof(bool))
|
||||
{
|
||||
return value.GetBoolean();
|
||||
}
|
||||
|
||||
if (targetType == typeof(int))
|
||||
{
|
||||
return value.GetInt32();
|
||||
}
|
||||
|
||||
if (targetType == typeof(long))
|
||||
{
|
||||
return value.GetInt64();
|
||||
}
|
||||
|
||||
if (targetType == typeof(double))
|
||||
{
|
||||
return value.GetDouble();
|
||||
}
|
||||
|
||||
if (targetType == typeof(float))
|
||||
{
|
||||
return value.GetSingle();
|
||||
}
|
||||
|
||||
if (targetType == typeof(Guid))
|
||||
{
|
||||
return Guid.Parse(value.GetString() ?? string.Empty);
|
||||
}
|
||||
|
||||
if (targetType == typeof(TimeSpan))
|
||||
{
|
||||
return TimeSpan.Parse(value.GetString() ?? string.Empty, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (targetType.IsEnum)
|
||||
{
|
||||
return Enum.Parse(targetType, value.GetString() ?? string.Empty, true);
|
||||
}
|
||||
|
||||
return Convert.ChangeType(value.GetString(), targetType, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static IEnumerable<PropertyInfo> GetSyncablePreferenceProperties()
|
||||
{
|
||||
foreach (var property in typeof(IPreferencesService).GetProperties(BindingFlags.Instance | BindingFlags.Public))
|
||||
{
|
||||
if (!property.CanRead || !property.CanWrite || property.GetIndexParameters().Length > 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return property;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ namespace Wino.Mail.ViewModels;
|
||||
|
||||
public partial class MailAppShellViewModel : MailBaseViewModel,
|
||||
IMailShellClient,
|
||||
IRecipient<AccountCreatedMessage>,
|
||||
IRecipient<MailtoProtocolMessageRequested>,
|
||||
IRecipient<RefreshUnreadCountsMessage>,
|
||||
IRecipient<AccountsMenuRefreshRequested>,
|
||||
@@ -86,6 +87,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
||||
|
||||
private readonly INativeAppService _nativeAppService;
|
||||
private readonly IMailService _mailService;
|
||||
private bool _hasRegisteredPersistentRecipients;
|
||||
|
||||
private readonly SemaphoreSlim accountInitFolderUpdateSlim = new SemaphoreSlim(1);
|
||||
|
||||
@@ -226,7 +228,11 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
||||
|
||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
if (!_hasRegisteredPersistentRecipients)
|
||||
{
|
||||
RegisterRecipients();
|
||||
_hasRegisteredPersistentRecipients = true;
|
||||
}
|
||||
|
||||
var activationContext = parameters as ShellModeActivationContext;
|
||||
var shouldRunStartupFlows = activationContext?.IsInitialActivation ?? true;
|
||||
@@ -269,8 +275,6 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
||||
|
||||
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedFrom(mode, parameters);
|
||||
|
||||
PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
|
||||
}
|
||||
|
||||
@@ -1155,6 +1159,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
||||
|
||||
Messenger.Register<AccountRemovedMessage>(this);
|
||||
Messenger.Register<AccountUpdatedMessage>(this);
|
||||
Messenger.Register<AccountCreatedMessage>(this);
|
||||
Messenger.Register<MailtoProtocolMessageRequested>(this);
|
||||
Messenger.Register<RefreshUnreadCountsMessage>(this);
|
||||
Messenger.Register<AccountsMenuRefreshRequested>(this);
|
||||
@@ -1172,6 +1177,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
||||
|
||||
Messenger.Unregister<AccountRemovedMessage>(this);
|
||||
Messenger.Unregister<AccountUpdatedMessage>(this);
|
||||
Messenger.Unregister<AccountCreatedMessage>(this);
|
||||
Messenger.Unregister<MailtoProtocolMessageRequested>(this);
|
||||
Messenger.Unregister<RefreshUnreadCountsMessage>(this);
|
||||
Messenger.Unregister<AccountsMenuRefreshRequested>(this);
|
||||
@@ -1195,6 +1201,9 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
||||
await RestoreSelectedAccountAfterMenuRefreshAsync(false);
|
||||
}
|
||||
|
||||
public async void Receive(AccountCreatedMessage message)
|
||||
=> await HandleAccountCreatedAsync(message.Account);
|
||||
|
||||
public async Task HandleAccountCreatedAsync(MailAccount createdAccount)
|
||||
{
|
||||
latestSelectedAccountMenuItem = null;
|
||||
|
||||
@@ -23,14 +23,15 @@ using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Core.ViewModels;
|
||||
using Wino.Mail.Services;
|
||||
using Wino.Mail.WinUI.ViewModels;
|
||||
using Wino.Mail.ViewModels;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
using Wino.Mail.WinUI.Activation;
|
||||
using Wino.Mail.WinUI.Interfaces;
|
||||
using Wino.Mail.WinUI.Models;
|
||||
using Wino.Mail.WinUI.Services;
|
||||
using Wino.Mail.WinUI.ViewModels;
|
||||
using Wino.Messaging.Client.Accounts;
|
||||
using Wino.Messaging.Client.Navigation;
|
||||
using Wino.Messaging.Server;
|
||||
@@ -174,6 +175,7 @@ public partial class App : WinoApplication,
|
||||
services.AddTransient(typeof(LanguageTimePageViewModel));
|
||||
services.AddTransient(typeof(AppPreferencesPageViewModel));
|
||||
services.AddTransient(typeof(StoragePageViewModel));
|
||||
services.AddTransient(typeof(WinoAccountManagementPageViewModel));
|
||||
services.AddTransient(typeof(AliasManagementPageViewModel));
|
||||
services.AddTransient(typeof(ContactsPageViewModel));
|
||||
services.AddTransient(typeof(SignatureAndEncryptionPageViewModel));
|
||||
|
||||
@@ -89,6 +89,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
||||
WinoPage.EmailTemplatesPage,
|
||||
WinoPage.CreateEmailTemplatePage,
|
||||
WinoPage.StoragePage,
|
||||
WinoPage.WinoAccountManagementPage,
|
||||
WinoPage.CalendarSettingsPage,
|
||||
WinoPage.CalendarAccountSettingsPage
|
||||
];
|
||||
@@ -155,6 +156,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
||||
WinoPage.EmailTemplatesPage => typeof(EmailTemplatesPage),
|
||||
WinoPage.CreateEmailTemplatePage => typeof(CreateEmailTemplatePage),
|
||||
WinoPage.StoragePage => typeof(StoragePage),
|
||||
WinoPage.WinoAccountManagementPage => typeof(WinoAccountManagementPage),
|
||||
WinoPage.WelcomeHostPage => typeof(WelcomeHostPage),
|
||||
WinoPage.ProviderSelectionPage => typeof(ProviderSelectionPage),
|
||||
WinoPage.AccountSetupProgressPage => typeof(AccountSetupProgressPage),
|
||||
|
||||
@@ -35,7 +35,7 @@ public class StatePersistenceService : ObservableObject, IStatePersistanceServic
|
||||
public bool IsBackButtonVisible =>
|
||||
ApplicationMode == WinoApplicationMode.Mail
|
||||
? (IsReadingMail && IsReaderNarrowed) || HasCurrentModeBackStack
|
||||
: IsEventDetailsVisible || HasCurrentModeBackStack;
|
||||
: HasCurrentModeBackStack;
|
||||
|
||||
private WinoApplicationMode applicationMode = WinoApplicationMode.Mail;
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ public partial class SettingsShellClient(INavigationService navigationService) :
|
||||
IRecipient<ActiveSettingsPageChanged>,
|
||||
IRecipient<LanguageChanged>
|
||||
{
|
||||
private bool _hasRegisteredPersistentRecipients;
|
||||
|
||||
public WinoApplicationMode Mode => WinoApplicationMode.Settings;
|
||||
public MenuItemCollection? MenuItems { get; private set; }
|
||||
|
||||
@@ -37,18 +39,22 @@ public partial class SettingsShellClient(INavigationService navigationService) :
|
||||
|
||||
public void Activate(ShellModeActivationContext activationContext)
|
||||
{
|
||||
if (!_hasRegisteredPersistentRecipients)
|
||||
{
|
||||
RegisterRecipients();
|
||||
_hasRegisteredPersistentRecipients = true;
|
||||
}
|
||||
|
||||
RebuildMenuItems();
|
||||
|
||||
var targetPage = activationContext.Parameter as WinoPage? ?? WinoPage.SettingOptionsPage;
|
||||
SetSelectedRootPage(SettingsNavigationInfoProvider.GetRootPage(targetPage));
|
||||
OnNavigatedTo(NavigationMode.New, activationContext);
|
||||
|
||||
navigationService.Navigate(WinoPage.SettingsPage, targetPage, NavigationReferenceFrame.InnerShellFrame);
|
||||
}
|
||||
|
||||
public void Deactivate()
|
||||
{
|
||||
OnNavigatedFrom(NavigationMode.New, null!);
|
||||
}
|
||||
|
||||
public Task HandleNavigationItemInvokedAsync(IMenuItem? menuItem)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
using Wino.Core.ViewModels;
|
||||
|
||||
namespace Wino.Views.Abstract;
|
||||
|
||||
public abstract class WinoAccountManagementPageAbstract : SettingsPageBase<WinoAccountManagementPageViewModel>
|
||||
{
|
||||
}
|
||||
@@ -180,6 +180,8 @@
|
||||
MenuItemsSource="{x:Bind ViewModel.MenuItems, Mode=OneWay}"
|
||||
OpenPaneLength="{x:Bind ViewModel.StatePersistenceService.OpenPaneLength, Mode=TwoWay}"
|
||||
PaneDisplayMode="Auto"
|
||||
PaneOpened="NavigationPaneOpened"
|
||||
PaneClosed="NavigationPaneClosed"
|
||||
Style="{StaticResource CalendarShellNavigationViewStyle}">
|
||||
<muxc:NavigationView.PaneCustomContent>
|
||||
<Grid x:Name="PaneCustomContent" Padding="0,0,0,6">
|
||||
|
||||
@@ -79,6 +79,12 @@ public sealed partial class CalendarAppShell : CalendarAppShellAbstract,
|
||||
private void NavigationViewDisplayModeChanged(NavigationView sender, NavigationViewDisplayModeChangedEventArgs args)
|
||||
=> UpdateNavigationPaneLayout(args.DisplayMode);
|
||||
|
||||
private void NavigationPaneOpened(NavigationView sender, object args)
|
||||
=> UpdateNavigationPaneLayout(sender.DisplayMode);
|
||||
|
||||
private void NavigationPaneClosed(NavigationView sender, object args)
|
||||
=> UpdateNavigationPaneLayout(sender.DisplayMode);
|
||||
|
||||
private Task InvokeNewCalendarEventAsync()
|
||||
=> ViewModel.HandleNavigationItemInvokedAsync(new NewCalendarEventMenuItem());
|
||||
|
||||
|
||||
@@ -80,22 +80,18 @@
|
||||
</TransitionCollection>
|
||||
</StackPanel.ChildrenTransitions>
|
||||
|
||||
<!-- About hero card. -->
|
||||
<!-- App About hero card -->
|
||||
<Border
|
||||
Padding="24,20"
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8">
|
||||
<Grid ColumnSpacing="16" RowSpacing="16">
|
||||
<Grid ColumnSpacing="16">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel
|
||||
VerticalAlignment="Center"
|
||||
@@ -108,12 +104,8 @@
|
||||
Source="ms-appx:///Assets/AppEntries/MailAssets/Square150x150Logo.scale-100.png"
|
||||
Stretch="Uniform" />
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<TextBlock Style="{StaticResource TitleTextBlockStyle}" Text="Wino Mail" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource TitleTextBlockStyle}"
|
||||
Text="Wino Mail" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Top"
|
||||
Foreground="{ThemeResource TextFillColorTertiaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.VersionText, Mode=OneWay}" />
|
||||
@@ -138,7 +130,6 @@
|
||||
Stretch="Uniform" />
|
||||
</Viewbox>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
Command="{x:Bind ViewModel.NavigateExternalCommand}"
|
||||
CommandParameter="{x:Bind ViewModel.GitHubUrl, Mode=OneWay}"
|
||||
@@ -151,7 +142,6 @@
|
||||
Stretch="Uniform" />
|
||||
</Viewbox>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
Command="{x:Bind ViewModel.NavigateExternalCommand}"
|
||||
CommandParameter="Store"
|
||||
@@ -167,6 +157,106 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Loading indicator -->
|
||||
<ProgressRing
|
||||
x:Name="WinoAccountBusyRing"
|
||||
Width="24"
|
||||
Height="24"
|
||||
HorizontalAlignment="Center"
|
||||
x:Load="{x:Bind ViewModel.IsWinoAccountBusy, Mode=OneWay}"
|
||||
IsActive="True" />
|
||||
|
||||
<!-- Wino Account: Signed-out state -->
|
||||
<controls:SettingsCard
|
||||
x:Name="SignedOutCard"
|
||||
Description="{x:Bind domain:Translator.SettingsHome_WinoAccount_SignedOutDescription}"
|
||||
Header="{x:Bind domain:Translator.SettingsHome_WinoAccount_Title}"
|
||||
x:Load="{x:Bind ViewModel.IsWinoAccountSignedOut, Mode=OneWay}">
|
||||
<controls:SettingsCard.HeaderIcon>
|
||||
<FontIcon Glyph="" />
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button
|
||||
Command="{x:Bind ViewModel.WinoAccountSignInCommand}"
|
||||
Content="{x:Bind domain:Translator.Buttons_SignIn}"
|
||||
Style="{StaticResource AccentButtonStyle}" />
|
||||
<Button Command="{x:Bind ViewModel.WinoAccountRegisterCommand}" Content="{x:Bind domain:Translator.Buttons_CreateAccount}" />
|
||||
</StackPanel>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Wino Account: Signed-in state -->
|
||||
<controls:SettingsExpander
|
||||
x:Name="SignedInExpander"
|
||||
Description="{x:Bind ViewModel.WinoAccountStatusText, Mode=OneWay}"
|
||||
Header="{x:Bind ViewModel.WinoAccountEmail, Mode=OneWay}"
|
||||
x:Load="{x:Bind ViewModel.IsWinoAccountSignedIn, Mode=OneWay}">
|
||||
<controls:SettingsExpander.HeaderIcon>
|
||||
<FontIcon Glyph="" />
|
||||
</controls:SettingsExpander.HeaderIcon>
|
||||
<Button
|
||||
Command="{x:Bind ViewModel.WinoAccountSignOutCommand}"
|
||||
Content="{x:Bind domain:Translator.WinoAccount_SignOutButton_Action}" />
|
||||
|
||||
<controls:SettingsExpander.Items>
|
||||
<!-- AI Pack active (with progress bar) -->
|
||||
<controls:SettingsCard
|
||||
x:Name="AiPackActiveCard"
|
||||
Description="{x:Bind ViewModel.AiUsageSummary, Mode=OneWay}"
|
||||
Header="{x:Bind domain:Translator.SettingsHome_AiPack_Title}"
|
||||
x:Load="{x:Bind ViewModel.CanShowAiUsage, Mode=OneWay}">
|
||||
<controls:SettingsCard.HeaderIcon>
|
||||
<ImageIcon Source="ms-appx:///Assets/AIPackIcon.png" />
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
<StackPanel MinWidth="200" Spacing="4">
|
||||
<ProgressBar
|
||||
Height="8"
|
||||
Maximum="100"
|
||||
Value="{x:Bind ViewModel.AiUsagePercent, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorTertiaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.AiBillingPeriodSummary, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- AI Pack not purchased -->
|
||||
<controls:SettingsCard
|
||||
x:Name="AiPackBuyCard"
|
||||
Description="{x:Bind domain:Translator.WinoAccount_Management_AiPackBuyDescription}"
|
||||
Header="{x:Bind domain:Translator.SettingsHome_AiPack_Title}"
|
||||
x:Load="{x:Bind ViewModel.CanShowBuyAiPack, Mode=OneWay}">
|
||||
<controls:SettingsCard.HeaderIcon>
|
||||
<ImageIcon Source="ms-appx:///Assets/AIPackIcon.png" />
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
<Button
|
||||
Command="{x:Bind ViewModel.OpenBuyAiPackPageCommand}"
|
||||
Content="{x:Bind domain:Translator.Buttons_Purchase}"
|
||||
Style="{StaticResource AccentButtonStyle}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Settings sync -->
|
||||
<controls:SettingsCard
|
||||
Click="WinoAccountManagementClicked"
|
||||
Description="{x:Bind domain:Translator.SettingsHome_SettingsSync_Description}"
|
||||
Header="{x:Bind domain:Translator.SettingsHome_SettingsSync_Title}"
|
||||
IsClickEnabled="True">
|
||||
<controls:SettingsCard.HeaderIcon>
|
||||
<FontIcon Glyph="" />
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Manage Wino Account -->
|
||||
<controls:SettingsCard
|
||||
Click="WinoAccountManagementClicked"
|
||||
Header="{x:Bind domain:Translator.SettingsHome_WinoAccount_ManageLink}"
|
||||
IsClickEnabled="True">
|
||||
<controls:SettingsCard.HeaderIcon>
|
||||
<FontIcon Glyph="" />
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
</controls:SettingsCard>
|
||||
</controls:SettingsExpander.Items>
|
||||
</controls:SettingsExpander>
|
||||
|
||||
|
||||
<Border
|
||||
Padding="12,0,34,20"
|
||||
|
||||
@@ -51,6 +51,11 @@ public sealed partial class SettingOptionsPage : SettingOptionsPageAbstract
|
||||
ViewModel.NavigateToAddAccount();
|
||||
}
|
||||
|
||||
private void WinoAccountManagementClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.NavigateToWinoAccountManagementCommand.Execute(null);
|
||||
}
|
||||
|
||||
private void SettingsSearchTextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
|
||||
{
|
||||
if (args.Reason == AutoSuggestionBoxTextChangeReason.UserInput || string.IsNullOrWhiteSpace(sender.Text))
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
<abstract:WinoAccountManagementPageAbstract
|
||||
x:Class="Wino.Views.Settings.WinoAccountManagementPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:abstract="using:Wino.Views.Abstract"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:domain="using:Wino.Core.Domain"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
Title="{x:Bind domain:Translator.WinoAccount_SettingsSection_Title}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<ScrollViewer>
|
||||
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
|
||||
<TextBlock
|
||||
Margin="0,0,0,8"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
Text="{x:Bind domain:Translator.WinoAccount_Management_Description}" />
|
||||
|
||||
<StackPanel
|
||||
x:Name="BusyPanel"
|
||||
HorizontalAlignment="Center"
|
||||
x:Load="{x:Bind ViewModel.IsBusy, Mode=OneWay}"
|
||||
Spacing="8">
|
||||
<ProgressRing
|
||||
Width="32"
|
||||
Height="32"
|
||||
IsActive="True" />
|
||||
<TextBlock HorizontalAlignment="Center" Text="{x:Bind domain:Translator.Busy}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel
|
||||
x:Name="SignedOutPanel"
|
||||
x:Load="{x:Bind ViewModel.IsSignedOut, Mode=OneWay}"
|
||||
Spacing="{StaticResource SettingsCardSpacing}">
|
||||
<controls:SettingsCard Description="{x:Bind domain:Translator.WinoAccount_Management_SignedOutDescription}" Header="{x:Bind domain:Translator.WinoAccount_Management_SignedOutTitle}">
|
||||
<controls:SettingsCard.HeaderIcon>
|
||||
<SymbolIcon Symbol="Contact" />
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||
<Button Command="{x:Bind ViewModel.RegisterCommand}" Content="{x:Bind domain:Translator.Buttons_CreateAccount}" />
|
||||
<Button Command="{x:Bind ViewModel.SignInCommand}" Content="{x:Bind domain:Translator.Buttons_SignIn}" />
|
||||
</StackPanel>
|
||||
</controls:SettingsCard>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel
|
||||
x:Name="SignedInPanel"
|
||||
x:Load="{x:Bind ViewModel.IsSignedIn, Mode=OneWay}"
|
||||
Spacing="{StaticResource SettingsCardSpacing}">
|
||||
<controls:SettingsCard Description="{x:Bind domain:Translator.WinoAccount_Management_AccountCardDescription}" Header="{x:Bind domain:Translator.WinoAccount_Management_AccountCardTitle}">
|
||||
<controls:SettingsCard.HeaderIcon>
|
||||
<SymbolIcon Symbol="Contact" />
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock FontWeight="SemiBold" Text="{x:Bind ViewModel.AccountEmail, Mode=OneWay}" />
|
||||
<TextBlock Text="{x:Bind ViewModel.AccountStatusText, Mode=OneWay}" />
|
||||
<Button
|
||||
HorizontalAlignment="Left"
|
||||
Command="{x:Bind ViewModel.SignOutCommand}"
|
||||
Content="{x:Bind domain:Translator.WinoAccount_SignOutButton_Action}" />
|
||||
</StackPanel>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard Description="{x:Bind domain:Translator.WinoAccount_Management_AiPackCardDescription}" Header="{x:Bind domain:Translator.WinoAccount_Management_AiPackCardTitle}">
|
||||
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock FontWeight="SemiBold" Text="{x:Bind ViewModel.AiPackStateText, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
x:Name="CanShowAITextBlock"
|
||||
x:Load="{x:Bind ViewModel.CanShowAiUsage, Mode=OneWay}"
|
||||
Text="{x:Bind ViewModel.AiUsageSummary, Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
<TextBlock
|
||||
x:Name="BillingBlock"
|
||||
x:Load="{x:Bind ViewModel.CanShowAiUsage, Mode=OneWay}"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind ViewModel.AiBillingPeriodSummary, Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
<TextBlock
|
||||
x:Name="CanBuyAIPack"
|
||||
x:Load="{x:Bind ViewModel.CanShowBuyAiPack, Mode=OneWay}"
|
||||
Text="{x:Bind domain:Translator.WinoAccount_Management_AiPackBuyDescription}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
<Button
|
||||
x:Name="OpenBuyPageButton"
|
||||
HorizontalAlignment="Left"
|
||||
x:Load="{x:Bind ViewModel.CanShowBuyAiPack, Mode=OneWay}"
|
||||
Command="{x:Bind ViewModel.OpenBuyPageCommand}"
|
||||
Content="{x:Bind domain:Translator.Buttons_Purchase}" />
|
||||
</StackPanel>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard Description="{x:Bind domain:Translator.WinoAccount_Management_SettingsCardDescription}" Header="{x:Bind domain:Translator.WinoAccount_Management_SettingsCardTitle}">
|
||||
<controls:SettingsCard.HeaderIcon>
|
||||
<SymbolIcon Symbol="Sync" />
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||
<Button Command="{x:Bind ViewModel.ExportSettingsCommand}" Content="{x:Bind domain:Translator.Buttons_Export}" />
|
||||
<Button Command="{x:Bind ViewModel.ImportSettingsCommand}" Content="{x:Bind domain:Translator.Buttons_Import}" />
|
||||
</StackPanel>
|
||||
</controls:SettingsCard>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</abstract:WinoAccountManagementPageAbstract>
|
||||
@@ -0,0 +1,11 @@
|
||||
using Wino.Views.Abstract;
|
||||
|
||||
namespace Wino.Views.Settings;
|
||||
|
||||
public sealed partial class WinoAccountManagementPage : WinoAccountManagementPageAbstract
|
||||
{
|
||||
public WinoAccountManagementPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,13 @@ public sealed partial class SettingsPage : SettingsPageAbstract,
|
||||
manageAccountsEntry.Title = Translator.SettingsManageAccountSettings_Title;
|
||||
}
|
||||
|
||||
var winoAccountEntry = PageHistory.FirstOrDefault(a => a.Request.PageType == WinoPage.WinoAccountManagementPage);
|
||||
|
||||
if (winoAccountEntry != null)
|
||||
{
|
||||
winoAccountEntry.Title = Translator.WinoAccount_SettingsSection_Title;
|
||||
}
|
||||
|
||||
_ = RefreshCurrentPageStateAsync();
|
||||
UpdateWindowTitle();
|
||||
}
|
||||
|
||||
@@ -525,6 +525,8 @@
|
||||
OpenPaneLength="{x:Bind ViewModel.StatePersistenceService.OpenPaneLength, Mode=TwoWay}"
|
||||
PaneDisplayMode="Auto"
|
||||
PaneOpening="NavigationPaneOpening"
|
||||
PaneOpened="NavigationPaneOpened"
|
||||
PaneClosed="NavigationPaneClosed"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Hidden"
|
||||
SelectionChanged="MenuSelectionChanged"
|
||||
Style="{StaticResource CalendarShellNavigationViewStyle}">
|
||||
|
||||
@@ -471,6 +471,12 @@ public sealed partial class WinoAppShell : Views.Abstract.WinoAppShellAbstract,
|
||||
}
|
||||
}
|
||||
|
||||
private void NavigationPaneOpened(NavigationView sender, object args)
|
||||
=> UpdateNavigationPaneLayout(sender.DisplayMode);
|
||||
|
||||
private void NavigationPaneClosed(NavigationView sender, object args)
|
||||
=> UpdateNavigationPaneLayout(sender.DisplayMode);
|
||||
|
||||
private void NavigationViewDisplayModeChanged(NavigationView sender, NavigationViewDisplayModeChangedEventArgs args)
|
||||
=> UpdateNavigationPaneLayout(args.DisplayMode);
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
<Content Remove="Assets\AppEntries\CalendarAssets\Square44x44Logo.altform-unplated_targetsize-24.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Assets\AIPackIcon.png" />
|
||||
<None Remove="Assets\CalendarWide310x150Logo.png" />
|
||||
<None Remove="Assets\UpdateNotes\Images\Calendar.svg" />
|
||||
<None Remove="Assets\UpdateNotes\Images\Security.svg" />
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Mail.Api.Contracts.Ai;
|
||||
using Wino.Mail.Api.Contracts.Auth;
|
||||
using Wino.Mail.Api.Contracts.Common;
|
||||
|
||||
@@ -17,10 +21,13 @@ namespace Wino.Services;
|
||||
public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly IDatabaseService _databaseService;
|
||||
private readonly bool _ownsHttpClient;
|
||||
|
||||
public WinoAccountApiClient(HttpClient? httpClient = null)
|
||||
public WinoAccountApiClient(IDatabaseService databaseService, HttpClient? httpClient = null)
|
||||
{
|
||||
_databaseService = databaseService;
|
||||
|
||||
if (httpClient != null)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
@@ -71,6 +78,55 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public Task<ApiEnvelope<AuthUserDto>> GetCurrentUserAsync(CancellationToken cancellationToken = default)
|
||||
=> SendAuthorizedRequestAsync("api/v1/auth/me", WinoAccountApiJsonContext.Default.ApiEnvelopeAuthUserDto, cancellationToken);
|
||||
|
||||
public Task<ApiEnvelope<AiStatusResultDto>> GetAiStatusAsync(CancellationToken cancellationToken = default)
|
||||
=> SendAuthorizedRequestAsync("api/v1/ai/status", WinoAccountApiJsonContext.Default.ApiEnvelopeAiStatusResultDto, cancellationToken);
|
||||
|
||||
public async Task<string?> GetSettingsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var request = await CreateAuthorizedRequestAsync(HttpMethod.Get, "api/v1/users/me/settings").ConfigureAwait(false);
|
||||
if (request == null)
|
||||
return null;
|
||||
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NoContent)
|
||||
return null;
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return null;
|
||||
|
||||
return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> SaveSettingsAsync(string settingsJson, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var request = await CreateAuthorizedRequestAsync(HttpMethod.Put, "api/v1/users/me/settings").ConfigureAwait(false);
|
||||
if (request == null)
|
||||
return false;
|
||||
|
||||
request.Content = new StringContent(settingsJson, Encoding.UTF8, "application/json");
|
||||
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ApiEnvelope<AuthResultDto>> SendAuthRequestAsync<TRequest>(string endpoint, TRequest request, JsonTypeInfo<TRequest> typeInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
@@ -94,6 +150,46 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ApiEnvelope<TResponse>> SendAuthorizedRequestAsync<TResponse>(string endpoint, JsonTypeInfo<ApiEnvelope<TResponse>> typeInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var request = await CreateAuthorizedRequestAsync(HttpMethod.Get, endpoint).ConfigureAwait(false);
|
||||
if (request == null)
|
||||
return ApiEnvelope<TResponse>.Failure("MissingAccessToken");
|
||||
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var payload = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
var envelope = string.IsNullOrWhiteSpace(payload)
|
||||
? null
|
||||
: JsonSerializer.Deserialize(payload, typeInfo);
|
||||
|
||||
return envelope ?? ApiEnvelope<TResponse>.Failure($"HTTP {(int)response.StatusCode} {response.ReasonPhrase}".Trim());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ApiEnvelope<TResponse>.Failure(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<HttpRequestMessage?> CreateAuthorizedRequestAsync(HttpMethod method, string endpoint)
|
||||
{
|
||||
var accessToken = await GetAccessTokenAsync().ConfigureAwait(false);
|
||||
if (string.IsNullOrWhiteSpace(accessToken))
|
||||
return null;
|
||||
|
||||
var request = new HttpRequestMessage(method, endpoint);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
return request;
|
||||
}
|
||||
|
||||
private async Task<string?> GetAccessTokenAsync()
|
||||
{
|
||||
var account = await _databaseService.Connection.Table<WinoAccount>().FirstOrDefaultAsync().ConfigureAwait(false);
|
||||
return string.IsNullOrWhiteSpace(account?.AccessToken) ? null : account.AccessToken;
|
||||
}
|
||||
|
||||
private static bool ValidateCertificate(HttpRequestMessage requestMessage, X509Certificate2? certificate, X509Chain? chain, System.Net.Security.SslPolicyErrors sslPolicyErrors)
|
||||
{
|
||||
if (requestMessage.RequestUri?.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase) == true)
|
||||
@@ -119,5 +215,7 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
|
||||
[JsonSerializable(typeof(RefreshRequest))]
|
||||
[JsonSerializable(typeof(LogoutRequest))]
|
||||
[JsonSerializable(typeof(ApiEnvelope<AuthResultDto>))]
|
||||
[JsonSerializable(typeof(ApiEnvelope<AuthUserDto>))]
|
||||
[JsonSerializable(typeof(ApiEnvelope<AiStatusResultDto>))]
|
||||
[JsonSerializable(typeof(ApiEnvelope<JsonElement>))]
|
||||
internal sealed partial class WinoAccountApiJsonContext : JsonSerializerContext;
|
||||
|
||||
Reference in New Issue
Block a user