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(...)`.
|
- 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 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.
|
- 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`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -76,4 +76,4 @@
|
|||||||
<PackageVersion Include="FluentAssertions" Version="8.8.0" />
|
<PackageVersion Include="FluentAssertions" Version="8.8.0" />
|
||||||
<PackageVersion Include="Moq" Version="4.20.72" />
|
<PackageVersion Include="Moq" Version="4.20.72" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
// For updating account calendars asynchronously.
|
// For updating account calendars asynchronously.
|
||||||
private SemaphoreSlim _accountCalendarUpdateSemaphoreSlim = new(1);
|
private SemaphoreSlim _accountCalendarUpdateSemaphoreSlim = new(1);
|
||||||
private bool _runtimeSubscriptionsAttached;
|
private bool _runtimeSubscriptionsAttached;
|
||||||
|
private bool _hasRegisteredPersistentRecipients;
|
||||||
|
|
||||||
public CalendarAppShellViewModel(IPreferencesService preferencesService,
|
public CalendarAppShellViewModel(IPreferencesService preferencesService,
|
||||||
IStatePersistanceService statePersistanceService,
|
IStatePersistanceService statePersistanceService,
|
||||||
@@ -151,7 +152,12 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
|
|
||||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||||
{
|
{
|
||||||
base.OnNavigatedTo(mode, parameters);
|
if (!_hasRegisteredPersistentRecipients)
|
||||||
|
{
|
||||||
|
RegisterRecipients();
|
||||||
|
_hasRegisteredPersistentRecipients = true;
|
||||||
|
}
|
||||||
|
|
||||||
AttachRuntimeSubscriptions();
|
AttachRuntimeSubscriptions();
|
||||||
|
|
||||||
var activationContext = parameters as ShellModeActivationContext;
|
var activationContext = parameters as ShellModeActivationContext;
|
||||||
@@ -177,8 +183,6 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
|
|
||||||
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
|
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
|
||||||
{
|
{
|
||||||
base.OnNavigatedFrom(mode, parameters);
|
|
||||||
|
|
||||||
DetachRuntimeSubscriptions();
|
DetachRuntimeSubscriptions();
|
||||||
PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
|
PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
|
||||||
_ = ExecuteUIThread(() =>
|
_ = ExecuteUIThread(() =>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ public enum WinoPage
|
|||||||
EmailTemplatesPage,
|
EmailTemplatesPage,
|
||||||
CreateEmailTemplatePage,
|
CreateEmailTemplatePage,
|
||||||
StoragePage,
|
StoragePage,
|
||||||
|
WinoAccountManagementPage,
|
||||||
WelcomePageV2,
|
WelcomePageV2,
|
||||||
WelcomeHostPage,
|
WelcomeHostPage,
|
||||||
ProviderSelectionPage,
|
ProviderSelectionPage,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
#nullable enable
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Wino.Mail.Api.Contracts.Ai;
|
||||||
using Wino.Mail.Api.Contracts.Auth;
|
using Wino.Mail.Api.Contracts.Auth;
|
||||||
using Wino.Mail.Api.Contracts.Common;
|
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>> LoginAsync(string email, string password, CancellationToken cancellationToken = default);
|
||||||
Task<ApiEnvelope<AuthResultDto>> RefreshAsync(string refreshToken, CancellationToken cancellationToken = default);
|
Task<ApiEnvelope<AuthResultDto>> RefreshAsync(string refreshToken, CancellationToken cancellationToken = default);
|
||||||
Task<ApiEnvelope<JsonElement>> LogoutAsync(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,
|
manageAccountsDescription,
|
||||||
"\uE77B",
|
"\uE77B",
|
||||||
searchKeywords: Translator.SettingsSearch_ManageAccounts_Keywords),
|
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(null, Translator.SettingsOptions_GeneralSection, string.Empty, "\uE713", isSeparator: true),
|
||||||
new(WinoPage.AppPreferencesPage,
|
new(WinoPage.AppPreferencesPage,
|
||||||
Translator.SettingsAppPreferences_Title,
|
Translator.SettingsAppPreferences_Title,
|
||||||
@@ -130,6 +135,7 @@ public static class SettingsNavigationInfoProvider
|
|||||||
WinoPage.SettingOptionsPage => Translator.MenuSettings,
|
WinoPage.SettingOptionsPage => Translator.MenuSettings,
|
||||||
WinoPage.ManageAccountsPage => Translator.SettingsManageAccountSettings_Title,
|
WinoPage.ManageAccountsPage => Translator.SettingsManageAccountSettings_Title,
|
||||||
WinoPage.AccountManagementPage => Translator.SettingsManageAccountSettings_Title,
|
WinoPage.AccountManagementPage => Translator.SettingsManageAccountSettings_Title,
|
||||||
|
WinoPage.WinoAccountManagementPage => Translator.WinoAccount_SettingsSection_Title,
|
||||||
WinoPage.PersonalizationPage => Translator.SettingsPersonalization_Title,
|
WinoPage.PersonalizationPage => Translator.SettingsPersonalization_Title,
|
||||||
WinoPage.AboutPage => Translator.SettingsAbout_Title,
|
WinoPage.AboutPage => Translator.SettingsAbout_Title,
|
||||||
WinoPage.MessageListPage => Translator.SettingsMessageList_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_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_StorageEmptySummary": "No cached MIME content detected yet.",
|
||||||
"SettingsHome_StorageLoading": "Checking local MIME usage...",
|
"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_Title": "Tips & tricks",
|
||||||
"SettingsHome_Tips_Description": "A few small changes can make Wino feel much more personal.",
|
"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.",
|
"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_Personalization_Keywords": "theme;dark;light;appearance;accent;color;colour;mode;layout;density",
|
||||||
"SettingsSearch_About_Keywords": "about;version;website;privacy;github;donate;store;support",
|
"SettingsSearch_About_Keywords": "about;version;website;privacy;github;donate;store;support",
|
||||||
"SettingsSearch_KeyboardShortcuts_Keywords": "shortcut;shortcuts;hotkey;hotkeys;keyboard;keys",
|
"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_MessageList_Keywords": "message;messages;list;threading;threads;avatar;preview;sender",
|
||||||
"SettingsSearch_ReadComposePane_Keywords": "reader;compose;composer;font;fonts;external content;display;reading",
|
"SettingsSearch_ReadComposePane_Keywords": "reader;compose;composer;font;fonts;external content;display;reading",
|
||||||
"SettingsSearch_SignatureAndEncryption_Keywords": "signature;signatures;encryption;certificate;certificates;s mime;smime;security",
|
"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.Domain.Models.Translations;
|
||||||
using Wino.Core.Extensions;
|
using Wino.Core.Extensions;
|
||||||
using Wino.Core.ViewModels.Data;
|
using Wino.Core.ViewModels.Data;
|
||||||
|
using Wino.Mail.Api.Contracts.Ai;
|
||||||
using Wino.Mail.ViewModels.Data;
|
using Wino.Mail.ViewModels.Data;
|
||||||
using Wino.Messaging.Client.Navigation;
|
using Wino.Messaging.Client.Navigation;
|
||||||
|
using Wino.Messaging.UI;
|
||||||
|
|
||||||
namespace Wino.Core.ViewModels;
|
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 INativeAppService _nativeAppService;
|
||||||
private readonly IAccountService _accountService;
|
private readonly IAccountService _accountService;
|
||||||
private readonly IMimeStorageService _mimeStorageService;
|
private readonly IMimeStorageService _mimeStorageService;
|
||||||
@@ -31,6 +37,9 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel
|
|||||||
private readonly INewThemeService _newThemeService;
|
private readonly INewThemeService _newThemeService;
|
||||||
private readonly IPreferencesService _preferencesService;
|
private readonly IPreferencesService _preferencesService;
|
||||||
private readonly IProviderService _providerService;
|
private readonly IProviderService _providerService;
|
||||||
|
private readonly IWinoAccountProfileService _profileService;
|
||||||
|
private readonly IWinoAccountApiClient _apiClient;
|
||||||
|
private readonly IMailDialogService _dialogService;
|
||||||
private bool _isInitializingSettings;
|
private bool _isInitializingSettings;
|
||||||
private bool _isAppearanceSelectionPaused;
|
private bool _isAppearanceSelectionPaused;
|
||||||
|
|
||||||
@@ -82,6 +91,43 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial bool UseAccentColor { get; set; }
|
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,
|
public SettingOptionsPageViewModel(INativeAppService nativeAppService,
|
||||||
IAccountService accountService,
|
IAccountService accountService,
|
||||||
IMimeStorageService mimeStorageService,
|
IMimeStorageService mimeStorageService,
|
||||||
@@ -89,7 +135,10 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel
|
|||||||
ITranslationService translationService,
|
ITranslationService translationService,
|
||||||
INewThemeService newThemeService,
|
INewThemeService newThemeService,
|
||||||
IPreferencesService preferencesService,
|
IPreferencesService preferencesService,
|
||||||
IProviderService providerService)
|
IProviderService providerService,
|
||||||
|
IWinoAccountProfileService profileService,
|
||||||
|
IWinoAccountApiClient apiClient,
|
||||||
|
IMailDialogService dialogService)
|
||||||
{
|
{
|
||||||
_nativeAppService = nativeAppService;
|
_nativeAppService = nativeAppService;
|
||||||
_accountService = accountService;
|
_accountService = accountService;
|
||||||
@@ -99,6 +148,9 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel
|
|||||||
_newThemeService = newThemeService;
|
_newThemeService = newThemeService;
|
||||||
_preferencesService = preferencesService;
|
_preferencesService = preferencesService;
|
||||||
_providerService = providerService;
|
_providerService = providerService;
|
||||||
|
_profileService = profileService;
|
||||||
|
_apiClient = apiClient;
|
||||||
|
_dialogService = dialogService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnNavigatedTo(NavigationMode mode, object parameters)
|
public override void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||||
@@ -112,6 +164,7 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel
|
|||||||
InitializeQuickSettings();
|
InitializeQuickSettings();
|
||||||
|
|
||||||
_ = LoadDashboardAsync();
|
_ = LoadDashboardAsync();
|
||||||
|
_ = LoadWinoAccountAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateSearchSuggestions(string query)
|
public void UpdateSearchSuggestions(string query)
|
||||||
@@ -365,6 +418,181 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel
|
|||||||
await _translationService.InitializeLanguageAsync(language.Language);
|
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]
|
[RelayCommand]
|
||||||
public void NavigateSubDetail(object type)
|
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,
|
public partial class MailAppShellViewModel : MailBaseViewModel,
|
||||||
IMailShellClient,
|
IMailShellClient,
|
||||||
|
IRecipient<AccountCreatedMessage>,
|
||||||
IRecipient<MailtoProtocolMessageRequested>,
|
IRecipient<MailtoProtocolMessageRequested>,
|
||||||
IRecipient<RefreshUnreadCountsMessage>,
|
IRecipient<RefreshUnreadCountsMessage>,
|
||||||
IRecipient<AccountsMenuRefreshRequested>,
|
IRecipient<AccountsMenuRefreshRequested>,
|
||||||
@@ -86,6 +87,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
|
|
||||||
private readonly INativeAppService _nativeAppService;
|
private readonly INativeAppService _nativeAppService;
|
||||||
private readonly IMailService _mailService;
|
private readonly IMailService _mailService;
|
||||||
|
private bool _hasRegisteredPersistentRecipients;
|
||||||
|
|
||||||
private readonly SemaphoreSlim accountInitFolderUpdateSlim = new SemaphoreSlim(1);
|
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)
|
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||||
{
|
{
|
||||||
base.OnNavigatedTo(mode, parameters);
|
if (!_hasRegisteredPersistentRecipients)
|
||||||
|
{
|
||||||
|
RegisterRecipients();
|
||||||
|
_hasRegisteredPersistentRecipients = true;
|
||||||
|
}
|
||||||
|
|
||||||
var activationContext = parameters as ShellModeActivationContext;
|
var activationContext = parameters as ShellModeActivationContext;
|
||||||
var shouldRunStartupFlows = activationContext?.IsInitialActivation ?? true;
|
var shouldRunStartupFlows = activationContext?.IsInitialActivation ?? true;
|
||||||
@@ -269,8 +275,6 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
|
|
||||||
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
|
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
|
||||||
{
|
{
|
||||||
base.OnNavigatedFrom(mode, parameters);
|
|
||||||
|
|
||||||
PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
|
PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1155,6 +1159,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
|
|
||||||
Messenger.Register<AccountRemovedMessage>(this);
|
Messenger.Register<AccountRemovedMessage>(this);
|
||||||
Messenger.Register<AccountUpdatedMessage>(this);
|
Messenger.Register<AccountUpdatedMessage>(this);
|
||||||
|
Messenger.Register<AccountCreatedMessage>(this);
|
||||||
Messenger.Register<MailtoProtocolMessageRequested>(this);
|
Messenger.Register<MailtoProtocolMessageRequested>(this);
|
||||||
Messenger.Register<RefreshUnreadCountsMessage>(this);
|
Messenger.Register<RefreshUnreadCountsMessage>(this);
|
||||||
Messenger.Register<AccountsMenuRefreshRequested>(this);
|
Messenger.Register<AccountsMenuRefreshRequested>(this);
|
||||||
@@ -1172,6 +1177,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
|
|
||||||
Messenger.Unregister<AccountRemovedMessage>(this);
|
Messenger.Unregister<AccountRemovedMessage>(this);
|
||||||
Messenger.Unregister<AccountUpdatedMessage>(this);
|
Messenger.Unregister<AccountUpdatedMessage>(this);
|
||||||
|
Messenger.Unregister<AccountCreatedMessage>(this);
|
||||||
Messenger.Unregister<MailtoProtocolMessageRequested>(this);
|
Messenger.Unregister<MailtoProtocolMessageRequested>(this);
|
||||||
Messenger.Unregister<RefreshUnreadCountsMessage>(this);
|
Messenger.Unregister<RefreshUnreadCountsMessage>(this);
|
||||||
Messenger.Unregister<AccountsMenuRefreshRequested>(this);
|
Messenger.Unregister<AccountsMenuRefreshRequested>(this);
|
||||||
@@ -1195,6 +1201,9 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
await RestoreSelectedAccountAfterMenuRefreshAsync(false);
|
await RestoreSelectedAccountAfterMenuRefreshAsync(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async void Receive(AccountCreatedMessage message)
|
||||||
|
=> await HandleAccountCreatedAsync(message.Account);
|
||||||
|
|
||||||
public async Task HandleAccountCreatedAsync(MailAccount createdAccount)
|
public async Task HandleAccountCreatedAsync(MailAccount createdAccount)
|
||||||
{
|
{
|
||||||
latestSelectedAccountMenuItem = null;
|
latestSelectedAccountMenuItem = null;
|
||||||
|
|||||||
@@ -23,14 +23,15 @@ using Wino.Core.Domain.Models.Calendar;
|
|||||||
using Wino.Core.Domain.Models.MailItem;
|
using Wino.Core.Domain.Models.MailItem;
|
||||||
using Wino.Core.Domain.Models.Navigation;
|
using Wino.Core.Domain.Models.Navigation;
|
||||||
using Wino.Core.Domain.Models.Synchronization;
|
using Wino.Core.Domain.Models.Synchronization;
|
||||||
|
using Wino.Core.ViewModels;
|
||||||
using Wino.Mail.Services;
|
using Wino.Mail.Services;
|
||||||
using Wino.Mail.WinUI.ViewModels;
|
|
||||||
using Wino.Mail.ViewModels;
|
using Wino.Mail.ViewModels;
|
||||||
using Wino.Mail.ViewModels.Data;
|
using Wino.Mail.ViewModels.Data;
|
||||||
using Wino.Mail.WinUI.Activation;
|
using Wino.Mail.WinUI.Activation;
|
||||||
using Wino.Mail.WinUI.Interfaces;
|
using Wino.Mail.WinUI.Interfaces;
|
||||||
using Wino.Mail.WinUI.Models;
|
using Wino.Mail.WinUI.Models;
|
||||||
using Wino.Mail.WinUI.Services;
|
using Wino.Mail.WinUI.Services;
|
||||||
|
using Wino.Mail.WinUI.ViewModels;
|
||||||
using Wino.Messaging.Client.Accounts;
|
using Wino.Messaging.Client.Accounts;
|
||||||
using Wino.Messaging.Client.Navigation;
|
using Wino.Messaging.Client.Navigation;
|
||||||
using Wino.Messaging.Server;
|
using Wino.Messaging.Server;
|
||||||
@@ -174,6 +175,7 @@ public partial class App : WinoApplication,
|
|||||||
services.AddTransient(typeof(LanguageTimePageViewModel));
|
services.AddTransient(typeof(LanguageTimePageViewModel));
|
||||||
services.AddTransient(typeof(AppPreferencesPageViewModel));
|
services.AddTransient(typeof(AppPreferencesPageViewModel));
|
||||||
services.AddTransient(typeof(StoragePageViewModel));
|
services.AddTransient(typeof(StoragePageViewModel));
|
||||||
|
services.AddTransient(typeof(WinoAccountManagementPageViewModel));
|
||||||
services.AddTransient(typeof(AliasManagementPageViewModel));
|
services.AddTransient(typeof(AliasManagementPageViewModel));
|
||||||
services.AddTransient(typeof(ContactsPageViewModel));
|
services.AddTransient(typeof(ContactsPageViewModel));
|
||||||
services.AddTransient(typeof(SignatureAndEncryptionPageViewModel));
|
services.AddTransient(typeof(SignatureAndEncryptionPageViewModel));
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
|||||||
WinoPage.EmailTemplatesPage,
|
WinoPage.EmailTemplatesPage,
|
||||||
WinoPage.CreateEmailTemplatePage,
|
WinoPage.CreateEmailTemplatePage,
|
||||||
WinoPage.StoragePage,
|
WinoPage.StoragePage,
|
||||||
|
WinoPage.WinoAccountManagementPage,
|
||||||
WinoPage.CalendarSettingsPage,
|
WinoPage.CalendarSettingsPage,
|
||||||
WinoPage.CalendarAccountSettingsPage
|
WinoPage.CalendarAccountSettingsPage
|
||||||
];
|
];
|
||||||
@@ -155,6 +156,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
|||||||
WinoPage.EmailTemplatesPage => typeof(EmailTemplatesPage),
|
WinoPage.EmailTemplatesPage => typeof(EmailTemplatesPage),
|
||||||
WinoPage.CreateEmailTemplatePage => typeof(CreateEmailTemplatePage),
|
WinoPage.CreateEmailTemplatePage => typeof(CreateEmailTemplatePage),
|
||||||
WinoPage.StoragePage => typeof(StoragePage),
|
WinoPage.StoragePage => typeof(StoragePage),
|
||||||
|
WinoPage.WinoAccountManagementPage => typeof(WinoAccountManagementPage),
|
||||||
WinoPage.WelcomeHostPage => typeof(WelcomeHostPage),
|
WinoPage.WelcomeHostPage => typeof(WelcomeHostPage),
|
||||||
WinoPage.ProviderSelectionPage => typeof(ProviderSelectionPage),
|
WinoPage.ProviderSelectionPage => typeof(ProviderSelectionPage),
|
||||||
WinoPage.AccountSetupProgressPage => typeof(AccountSetupProgressPage),
|
WinoPage.AccountSetupProgressPage => typeof(AccountSetupProgressPage),
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ public class StatePersistenceService : ObservableObject, IStatePersistanceServic
|
|||||||
public bool IsBackButtonVisible =>
|
public bool IsBackButtonVisible =>
|
||||||
ApplicationMode == WinoApplicationMode.Mail
|
ApplicationMode == WinoApplicationMode.Mail
|
||||||
? (IsReadingMail && IsReaderNarrowed) || HasCurrentModeBackStack
|
? (IsReadingMail && IsReaderNarrowed) || HasCurrentModeBackStack
|
||||||
: IsEventDetailsVisible || HasCurrentModeBackStack;
|
: HasCurrentModeBackStack;
|
||||||
|
|
||||||
private WinoApplicationMode applicationMode = WinoApplicationMode.Mail;
|
private WinoApplicationMode applicationMode = WinoApplicationMode.Mail;
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ public partial class SettingsShellClient(INavigationService navigationService) :
|
|||||||
IRecipient<ActiveSettingsPageChanged>,
|
IRecipient<ActiveSettingsPageChanged>,
|
||||||
IRecipient<LanguageChanged>
|
IRecipient<LanguageChanged>
|
||||||
{
|
{
|
||||||
|
private bool _hasRegisteredPersistentRecipients;
|
||||||
|
|
||||||
public WinoApplicationMode Mode => WinoApplicationMode.Settings;
|
public WinoApplicationMode Mode => WinoApplicationMode.Settings;
|
||||||
public MenuItemCollection? MenuItems { get; private set; }
|
public MenuItemCollection? MenuItems { get; private set; }
|
||||||
|
|
||||||
@@ -37,18 +39,22 @@ public partial class SettingsShellClient(INavigationService navigationService) :
|
|||||||
|
|
||||||
public void Activate(ShellModeActivationContext activationContext)
|
public void Activate(ShellModeActivationContext activationContext)
|
||||||
{
|
{
|
||||||
|
if (!_hasRegisteredPersistentRecipients)
|
||||||
|
{
|
||||||
|
RegisterRecipients();
|
||||||
|
_hasRegisteredPersistentRecipients = true;
|
||||||
|
}
|
||||||
|
|
||||||
RebuildMenuItems();
|
RebuildMenuItems();
|
||||||
|
|
||||||
var targetPage = activationContext.Parameter as WinoPage? ?? WinoPage.SettingOptionsPage;
|
var targetPage = activationContext.Parameter as WinoPage? ?? WinoPage.SettingOptionsPage;
|
||||||
SetSelectedRootPage(SettingsNavigationInfoProvider.GetRootPage(targetPage));
|
SetSelectedRootPage(SettingsNavigationInfoProvider.GetRootPage(targetPage));
|
||||||
OnNavigatedTo(NavigationMode.New, activationContext);
|
|
||||||
|
|
||||||
navigationService.Navigate(WinoPage.SettingsPage, targetPage, NavigationReferenceFrame.InnerShellFrame);
|
navigationService.Navigate(WinoPage.SettingsPage, targetPage, NavigationReferenceFrame.InnerShellFrame);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Deactivate()
|
public void Deactivate()
|
||||||
{
|
{
|
||||||
OnNavigatedFrom(NavigationMode.New, null!);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task HandleNavigationItemInvokedAsync(IMenuItem? menuItem)
|
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}"
|
MenuItemsSource="{x:Bind ViewModel.MenuItems, Mode=OneWay}"
|
||||||
OpenPaneLength="{x:Bind ViewModel.StatePersistenceService.OpenPaneLength, Mode=TwoWay}"
|
OpenPaneLength="{x:Bind ViewModel.StatePersistenceService.OpenPaneLength, Mode=TwoWay}"
|
||||||
PaneDisplayMode="Auto"
|
PaneDisplayMode="Auto"
|
||||||
|
PaneOpened="NavigationPaneOpened"
|
||||||
|
PaneClosed="NavigationPaneClosed"
|
||||||
Style="{StaticResource CalendarShellNavigationViewStyle}">
|
Style="{StaticResource CalendarShellNavigationViewStyle}">
|
||||||
<muxc:NavigationView.PaneCustomContent>
|
<muxc:NavigationView.PaneCustomContent>
|
||||||
<Grid x:Name="PaneCustomContent" Padding="0,0,0,6">
|
<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)
|
private void NavigationViewDisplayModeChanged(NavigationView sender, NavigationViewDisplayModeChangedEventArgs args)
|
||||||
=> UpdateNavigationPaneLayout(args.DisplayMode);
|
=> 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()
|
private Task InvokeNewCalendarEventAsync()
|
||||||
=> ViewModel.HandleNavigationItemInvokedAsync(new NewCalendarEventMenuItem());
|
=> ViewModel.HandleNavigationItemInvokedAsync(new NewCalendarEventMenuItem());
|
||||||
|
|
||||||
|
|||||||
@@ -80,22 +80,18 @@
|
|||||||
</TransitionCollection>
|
</TransitionCollection>
|
||||||
</StackPanel.ChildrenTransitions>
|
</StackPanel.ChildrenTransitions>
|
||||||
|
|
||||||
<!-- About hero card. -->
|
<!-- App About hero card -->
|
||||||
<Border
|
<Border
|
||||||
Padding="24,20"
|
Padding="24,20"
|
||||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
CornerRadius="8">
|
CornerRadius="8">
|
||||||
<Grid ColumnSpacing="16" RowSpacing="16">
|
<Grid ColumnSpacing="16">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<StackPanel
|
<StackPanel
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
@@ -108,12 +104,8 @@
|
|||||||
Source="ms-appx:///Assets/AppEntries/MailAssets/Square150x150Logo.scale-100.png"
|
Source="ms-appx:///Assets/AppEntries/MailAssets/Square150x150Logo.scale-100.png"
|
||||||
Stretch="Uniform" />
|
Stretch="Uniform" />
|
||||||
<StackPanel VerticalAlignment="Center">
|
<StackPanel VerticalAlignment="Center">
|
||||||
|
<TextBlock Style="{StaticResource TitleTextBlockStyle}" Text="Wino Mail" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
VerticalAlignment="Center"
|
|
||||||
Style="{StaticResource TitleTextBlockStyle}"
|
|
||||||
Text="Wino Mail" />
|
|
||||||
<TextBlock
|
|
||||||
VerticalAlignment="Top"
|
|
||||||
Foreground="{ThemeResource TextFillColorTertiaryBrush}"
|
Foreground="{ThemeResource TextFillColorTertiaryBrush}"
|
||||||
Style="{StaticResource CaptionTextBlockStyle}"
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
Text="{x:Bind ViewModel.VersionText, Mode=OneWay}" />
|
Text="{x:Bind ViewModel.VersionText, Mode=OneWay}" />
|
||||||
@@ -138,7 +130,6 @@
|
|||||||
Stretch="Uniform" />
|
Stretch="Uniform" />
|
||||||
</Viewbox>
|
</Viewbox>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
Command="{x:Bind ViewModel.NavigateExternalCommand}"
|
Command="{x:Bind ViewModel.NavigateExternalCommand}"
|
||||||
CommandParameter="{x:Bind ViewModel.GitHubUrl, Mode=OneWay}"
|
CommandParameter="{x:Bind ViewModel.GitHubUrl, Mode=OneWay}"
|
||||||
@@ -151,7 +142,6 @@
|
|||||||
Stretch="Uniform" />
|
Stretch="Uniform" />
|
||||||
</Viewbox>
|
</Viewbox>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
Command="{x:Bind ViewModel.NavigateExternalCommand}"
|
Command="{x:Bind ViewModel.NavigateExternalCommand}"
|
||||||
CommandParameter="Store"
|
CommandParameter="Store"
|
||||||
@@ -167,6 +157,106 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</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
|
<Border
|
||||||
Padding="12,0,34,20"
|
Padding="12,0,34,20"
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ public sealed partial class SettingOptionsPage : SettingOptionsPageAbstract
|
|||||||
ViewModel.NavigateToAddAccount();
|
ViewModel.NavigateToAddAccount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void WinoAccountManagementClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
ViewModel.NavigateToWinoAccountManagementCommand.Execute(null);
|
||||||
|
}
|
||||||
|
|
||||||
private void SettingsSearchTextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
|
private void SettingsSearchTextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
|
||||||
{
|
{
|
||||||
if (args.Reason == AutoSuggestionBoxTextChangeReason.UserInput || string.IsNullOrWhiteSpace(sender.Text))
|
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;
|
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();
|
_ = RefreshCurrentPageStateAsync();
|
||||||
UpdateWindowTitle();
|
UpdateWindowTitle();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -525,6 +525,8 @@
|
|||||||
OpenPaneLength="{x:Bind ViewModel.StatePersistenceService.OpenPaneLength, Mode=TwoWay}"
|
OpenPaneLength="{x:Bind ViewModel.StatePersistenceService.OpenPaneLength, Mode=TwoWay}"
|
||||||
PaneDisplayMode="Auto"
|
PaneDisplayMode="Auto"
|
||||||
PaneOpening="NavigationPaneOpening"
|
PaneOpening="NavigationPaneOpening"
|
||||||
|
PaneOpened="NavigationPaneOpened"
|
||||||
|
PaneClosed="NavigationPaneClosed"
|
||||||
ScrollViewer.VerticalScrollBarVisibility="Hidden"
|
ScrollViewer.VerticalScrollBarVisibility="Hidden"
|
||||||
SelectionChanged="MenuSelectionChanged"
|
SelectionChanged="MenuSelectionChanged"
|
||||||
Style="{StaticResource CalendarShellNavigationViewStyle}">
|
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)
|
private void NavigationViewDisplayModeChanged(NavigationView sender, NavigationViewDisplayModeChangedEventArgs args)
|
||||||
=> UpdateNavigationPaneLayout(args.DisplayMode);
|
=> UpdateNavigationPaneLayout(args.DisplayMode);
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,7 @@
|
|||||||
<Content Remove="Assets\AppEntries\CalendarAssets\Square44x44Logo.altform-unplated_targetsize-24.png" />
|
<Content Remove="Assets\AppEntries\CalendarAssets\Square44x44Logo.altform-unplated_targetsize-24.png" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<None Remove="Assets\AIPackIcon.png" />
|
||||||
<None Remove="Assets\CalendarWide310x150Logo.png" />
|
<None Remove="Assets\CalendarWide310x150Logo.png" />
|
||||||
<None Remove="Assets\UpdateNotes\Images\Calendar.svg" />
|
<None Remove="Assets\UpdateNotes\Images\Calendar.svg" />
|
||||||
<None Remove="Assets\UpdateNotes\Images\Security.svg" />
|
<None Remove="Assets\UpdateNotes\Images\Security.svg" />
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System;
|
using System;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Text.Json.Serialization.Metadata;
|
using System.Text.Json.Serialization.Metadata;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Mail.Api.Contracts.Ai;
|
||||||
using Wino.Mail.Api.Contracts.Auth;
|
using Wino.Mail.Api.Contracts.Auth;
|
||||||
using Wino.Mail.Api.Contracts.Common;
|
using Wino.Mail.Api.Contracts.Common;
|
||||||
|
|
||||||
@@ -17,10 +21,13 @@ namespace Wino.Services;
|
|||||||
public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
|
public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
|
||||||
{
|
{
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly IDatabaseService _databaseService;
|
||||||
private readonly bool _ownsHttpClient;
|
private readonly bool _ownsHttpClient;
|
||||||
|
|
||||||
public WinoAccountApiClient(HttpClient? httpClient = null)
|
public WinoAccountApiClient(IDatabaseService databaseService, HttpClient? httpClient = null)
|
||||||
{
|
{
|
||||||
|
_databaseService = databaseService;
|
||||||
|
|
||||||
if (httpClient != null)
|
if (httpClient != null)
|
||||||
{
|
{
|
||||||
_httpClient = httpClient;
|
_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)
|
private async Task<ApiEnvelope<AuthResultDto>> SendAuthRequestAsync<TRequest>(string endpoint, TRequest request, JsonTypeInfo<TRequest> typeInfo, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
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)
|
private static bool ValidateCertificate(HttpRequestMessage requestMessage, X509Certificate2? certificate, X509Chain? chain, System.Net.Security.SslPolicyErrors sslPolicyErrors)
|
||||||
{
|
{
|
||||||
if (requestMessage.RequestUri?.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase) == true)
|
if (requestMessage.RequestUri?.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase) == true)
|
||||||
@@ -119,5 +215,7 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
|
|||||||
[JsonSerializable(typeof(RefreshRequest))]
|
[JsonSerializable(typeof(RefreshRequest))]
|
||||||
[JsonSerializable(typeof(LogoutRequest))]
|
[JsonSerializable(typeof(LogoutRequest))]
|
||||||
[JsonSerializable(typeof(ApiEnvelope<AuthResultDto>))]
|
[JsonSerializable(typeof(ApiEnvelope<AuthResultDto>))]
|
||||||
|
[JsonSerializable(typeof(ApiEnvelope<AuthUserDto>))]
|
||||||
|
[JsonSerializable(typeof(ApiEnvelope<AiStatusResultDto>))]
|
||||||
[JsonSerializable(typeof(ApiEnvelope<JsonElement>))]
|
[JsonSerializable(typeof(ApiEnvelope<JsonElement>))]
|
||||||
internal sealed partial class WinoAccountApiJsonContext : JsonSerializerContext;
|
internal sealed partial class WinoAccountApiJsonContext : JsonSerializerContext;
|
||||||
|
|||||||
Reference in New Issue
Block a user