using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; 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.Core.Domain.Models.Accounts; using Wino.Core.Domain.Models.Personalization; 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, IRecipient, IRecipient, IRecipient { private const string BuyAiPackUrl = "https://example.com/wino-ai-pack"; private readonly INativeAppService _nativeAppService; private readonly IAccountService _accountService; private readonly IMimeStorageService _mimeStorageService; private readonly IStoreRatingService _storeRatingService; private readonly ITranslationService _translationService; private readonly INewThemeService _newThemeService; private readonly IPreferencesService _preferencesService; private readonly IProviderService _providerService; private readonly IWinoAccountProfileService _profileService; private readonly IMailDialogService _dialogService; private bool _isInitializingSettings; private bool _isAppearanceSelectionPaused; public string GitHubUrl => AppUrls.GitHub; public string PaypalUrl => AppUrls.Paypal; public string WebsiteUrl => AppUrls.Website; public string PrivacyPolicyUrl => AppUrls.PrivacyPolicy; public ObservableCollection SearchSuggestions { get; } = []; public ObservableCollection Accounts { get; } = []; public ObservableCollection Colors { get; } = []; public List ElementThemes { get; } = [ new(ApplicationElementTheme.Light, Translator.ElementTheme_Light), new(ApplicationElementTheme.Dark, Translator.ElementTheme_Dark), new(ApplicationElementTheme.Default, Translator.ElementTheme_Default), ]; public bool HasAccounts => Accounts.Count > 0; [ObservableProperty] public partial string VersionText { get; set; } = string.Empty; [ObservableProperty] public partial string AccountSummaryText { get; set; } = string.Empty; [ObservableProperty] public partial int AccountCount { get; set; } [ObservableProperty] public partial string StorageSummaryText { get; set; } = string.Empty; [ObservableProperty] public partial string SearchQuery { get; set; } = string.Empty; [ObservableProperty] public partial List AvailableLanguages { get; set; } = []; [ObservableProperty] public partial AppLanguageModel SelectedLanguage { get; set; } [ObservableProperty] public partial ElementThemeContainer SelectedElementTheme { get; set; } [ObservableProperty] public partial AppColorViewModel SelectedAppColor { get; set; } [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(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, IStoreRatingService storeRatingService, ITranslationService translationService, INewThemeService newThemeService, IPreferencesService preferencesService, IProviderService providerService, IWinoAccountProfileService profileService, IMailDialogService dialogService) { _nativeAppService = nativeAppService; _accountService = accountService; _mimeStorageService = mimeStorageService; _storeRatingService = storeRatingService; _translationService = translationService; _newThemeService = newThemeService; _preferencesService = preferencesService; _providerService = providerService; _profileService = profileService; _dialogService = dialogService; } public override void OnNavigatedTo(NavigationMode mode, object parameters) { base.OnNavigatedTo(mode, parameters); VersionText = string.Format("{0}{1}", Translator.SettingsAboutVersion, _nativeAppService.GetFullAppVersion()); SearchQuery = string.Empty; SearchSuggestions.Clear(); StorageSummaryText = Translator.SettingsHome_StorageLoading; InitializeQuickSettings(); _ = LoadDashboardAsync(); _ = LoadWinoAccountAsync(); } public void UpdateSearchSuggestions(string query) { SearchQuery = query; SearchSuggestions.Clear(); foreach (var result in SettingsNavigationInfoProvider.Search(query, AccountSummaryText).Take(6)) { SearchSuggestions.Add(result); } } public SettingsNavigationItemInfo GetBestSearchSuggestion(string query) => SettingsNavigationInfoProvider.Search(query, AccountSummaryText).FirstOrDefault(); public void NavigateToSetting(SettingsNavigationItemInfo item) { if (item?.PageType is WinoPage pageType) { NavigateSubDetail(pageType); } } public void NavigateToAccount(IAccountProviderDetailViewModel account) { if (account == null) { return; } Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsManageAccountSettings_Title, WinoPage.ManageAccountsPage)); switch (account) { case AccountProviderDetailViewModel accountDetails: Messenger.Send(new BreadcrumbNavigationRequested(accountDetails.Account.Name, WinoPage.AccountDetailsPage, accountDetails.Account.Id)); break; case MergedAccountProviderDetailViewModel mergedAccount: Messenger.Send(new BreadcrumbNavigationRequested(mergedAccount.MergedInbox.Name, WinoPage.MergedAccountDetailsPage, mergedAccount)); break; } } public void NavigateToAddAccount() { Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsManageAccountSettings_Title, WinoPage.ManageAccountsPage)); Messenger.Send(new BreadcrumbNavigationRequested(Translator.WelcomeWizard_Step2Title, WinoPage.ProviderSelectionPage)); } private async Task LoadDashboardAsync() { var accounts = (await _accountService.GetAccountsAsync().ConfigureAwait(false) ?? []).ToList(); var count = accounts.Count; Dictionary storageSizeMap = count == 0 ? [] : await _mimeStorageService.GetAccountsMimeStorageSizesAsync(accounts.Select(account => account.Id)).ConfigureAwait(false); var totalStorageBytes = storageSizeMap.Values.Sum(); var groupedAccountItems = CreateAccountItems(accounts); await ExecuteUIThread(() => { AccountCount = count; AccountSummaryText = string.Format(Translator.SettingsOptions_AccountsSummary, count); Accounts.Clear(); foreach (var account in groupedAccountItems) { Accounts.Add(account); } OnPropertyChanged(nameof(HasAccounts)); StorageSummaryText = totalStorageBytes == 0 ? Translator.SettingsHome_StorageEmptySummary : string.Format(Translator.SettingsStorage_TotalUsage, totalStorageBytes.GetBytesReadable()); if (!string.IsNullOrWhiteSpace(SearchQuery)) { UpdateSearchSuggestions(SearchQuery); } }); } private void InitializeQuickSettings() { _isInitializingSettings = true; InitializeColors(); InitializeLanguageOptions(); InitializeAppearanceOptions(); _isInitializingSettings = false; } private void InitializeLanguageOptions() { AvailableLanguages = _translationService.GetAvailableLanguages(); SelectedLanguage = AvailableLanguages.FirstOrDefault(language => language.Language == _preferencesService.CurrentLanguage) ?? AvailableLanguages.FirstOrDefault(); } private void InitializeColors() { Colors.Clear(); foreach (var color in _newThemeService.GetAvailableAccountColors().Distinct(StringComparer.OrdinalIgnoreCase)) { Colors.Add(new AppColorViewModel(color)); } var systemAccentColor = _newThemeService.GetSystemAccentColorHex(); if (Colors.All(color => !string.Equals(color.Hex, systemAccentColor, StringComparison.OrdinalIgnoreCase))) { Colors.Add(new AppColorViewModel(systemAccentColor, true)); } else { var matchingAccentColor = Colors.First(color => string.Equals(color.Hex, systemAccentColor, StringComparison.OrdinalIgnoreCase)); Colors.Remove(matchingAccentColor); Colors.Add(new AppColorViewModel(systemAccentColor, true)); } } private void InitializeAppearanceOptions() { _isAppearanceSelectionPaused = true; var currentAccentColor = _newThemeService.AccentColor; if (!string.IsNullOrWhiteSpace(currentAccentColor) && Colors.All(color => !string.Equals(color.Hex, currentAccentColor, StringComparison.OrdinalIgnoreCase))) { Colors.Insert(0, new AppColorViewModel(currentAccentColor)); } SelectedElementTheme = ElementThemes.FirstOrDefault(theme => theme.NativeTheme == _newThemeService.RootTheme) ?? ElementThemes.LastOrDefault(); if (string.IsNullOrWhiteSpace(currentAccentColor)) { SelectedAppColor = Colors.LastOrDefault(color => color.IsAccentColor) ?? Colors.LastOrDefault(); UseAccentColor = true; } else { SelectedAppColor = Colors.FirstOrDefault(color => string.Equals(color.Hex, currentAccentColor, StringComparison.OrdinalIgnoreCase)) ?? Colors.FirstOrDefault(); UseAccentColor = SelectedAppColor?.IsAccentColor == true; } _isAppearanceSelectionPaused = false; } private List CreateAccountItems(List accounts) { var groupedAccounts = accounts .OrderBy(account => account.MergedInboxId == null ? 1 : 0) .ThenBy(account => account.Order) .ThenBy(account => account.Name) .GroupBy(account => account.MergedInboxId); var accountItems = new List(); foreach (var accountGroup in groupedAccounts) { if (accountGroup.Key == null) { accountItems.AddRange(accountGroup.Select(CreateAccountProviderDetails)); continue; } var mergedInbox = accountGroup.First().MergedInbox; var holdingAccounts = accountGroup .Select(CreateAccountProviderDetails) .ToList(); var mergedAccount = new MergedAccountProviderDetailViewModel(mergedInbox, holdingAccounts) { ProviderDetail = holdingAccounts.FirstOrDefault()?.ProviderDetail }; accountItems.Add(mergedAccount); } return accountItems; } private AccountProviderDetailViewModel CreateAccountProviderDetails(MailAccount account) { var provider = _providerService.GetProviderDetail(account.ProviderType); return new AccountProviderDetailViewModel(provider, account); } partial void OnSelectedLanguageChanged(AppLanguageModel value) { if (_isInitializingSettings || value == null) { return; } _ = ApplyLanguageAsync(value); } partial void OnSelectedElementThemeChanged(ElementThemeContainer value) { if (_isInitializingSettings || value == null) { return; } _newThemeService.RootTheme = value.NativeTheme; } partial void OnSelectedAppColorChanged(AppColorViewModel value) { if (_isInitializingSettings || _isAppearanceSelectionPaused || value == null) { return; } _isAppearanceSelectionPaused = true; UseAccentColor = value.IsAccentColor; _isAppearanceSelectionPaused = false; _newThemeService.AccentColor = value.Hex; } partial void OnUseAccentColorChanged(bool value) { if (_isInitializingSettings || _isAppearanceSelectionPaused || Colors.Count == 0) { return; } var accentColor = Colors.LastOrDefault(color => color.IsAccentColor); var fallbackColor = Colors.FirstOrDefault(color => !color.IsAccentColor) ?? Colors.FirstOrDefault(); var targetColor = value ? accentColor : SelectedAppColor?.IsAccentColor == true ? fallbackColor : SelectedAppColor; if (targetColor == null || ReferenceEquals(targetColor, SelectedAppColor)) { return; } _isAppearanceSelectionPaused = true; SelectedAppColor = targetColor; _isAppearanceSelectionPaused = false; _newThemeService.AccentColor = targetColor.Hex; } private async Task ApplyLanguageAsync(AppLanguageModel language) { await _translationService.InitializeLanguageAsync(language.Language); } // Wino Account message recipients protected override void RegisterRecipients() { base.RegisterRecipients(); Messenger.Register(this); Messenger.Register(this); Messenger.Register(this); } protected override void UnregisterRecipients() { base.UnregisterRecipients(); Messenger.Unregister(this); Messenger.Unregister(this); Messenger.Unregister(this); } public void Receive(WinoAccountProfileUpdatedMessage message) => _ = LoadWinoAccountAsync(); public void Receive(WinoAccountProfileDeletedMessage message) => _ = ResetWinoAccountStateAsync(); public void Receive(WinoAccountAddOnPurchasedMessage message) => _ = LoadWinoAccountAsync(); // 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() { try { var cachedAccount = await _profileService.GetActiveAccountAsync().ConfigureAwait(false); var cachedAddOns = await _profileService.GetCachedAddOnSnapshotAsync().ConfigureAwait(false); if (cachedAccount != null) { await ApplyWinoAccountStateAsync(cachedAccount.Email, cachedAccount.AccountStatus).ConfigureAwait(false); UpdateAiPackState(cachedAddOns); } await ExecuteUIThread(() => IsWinoAccountBusy = cachedAccount == null); var account = await _profileService.GetAuthenticatedAccountAsync().ConfigureAwait(false); if (account == null) { await ResetWinoAccountStateAsync(); return; } var aiStatusResponse = await _profileService.GetAiStatusAsync().ConfigureAwait(false); var profileRefreshResult = await _profileService.RefreshProfileAsync().ConfigureAwait(false); var resolvedAccount = profileRefreshResult.IsSuccess && profileRefreshResult.Account != null ? profileRefreshResult.Account : account; await ApplyWinoAccountStateAsync(resolvedAccount.Email, resolvedAccount.AccountStatus).ConfigureAwait(false); if (aiStatusResponse.IsSuccess) { UpdateAiPackState(aiStatusResponse.Result); } else { UpdateAiPackState(cachedAddOns); } } catch { await ResetWinoAccountStateAsync(); } finally { await ExecuteUIThread(() => IsWinoAccountBusy = false); } } private async Task ApplyWinoAccountStateAsync(string email, string status) { await ExecuteUIThread(() => { IsWinoAccountSignedIn = true; WinoAccountEmail = email; WinoAccountStatusText = string.Format(Translator.WinoAccount_Management_StatusLabel, status); }); } 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 void UpdateAiPackState(AiStatusResultDto aiStatus) { UpdateAiPackState( aiStatus?.HasAiPack == true, aiStatus?.Used, aiStatus?.MonthlyLimit, aiStatus?.Remaining, aiStatus?.CurrentPeriodStartUtc, aiStatus?.CurrentPeriodEndUtc); } private void UpdateAiPackState(WinoAccountAddOnSnapshot addOnSnapshot) { var remaining = addOnSnapshot?.HasAiPack == true && addOnSnapshot.UsageLimit is int limit && addOnSnapshot.UsageCount is int used ? limit - used : (int?)null; UpdateAiPackState( addOnSnapshot?.HasAiPack == true, addOnSnapshot?.UsageCount, addOnSnapshot?.UsageLimit, remaining, addOnSnapshot?.BillingPeriodStartUtc, addOnSnapshot?.BillingPeriodEndUtc); } private void UpdateAiPackState(bool hasAiPack, int? used, int? limit, int? remaining, DateTimeOffset? periodStart, DateTimeOffset? periodEnd) { var usageText = Translator.WinoAccount_Management_AiPackUnknownUsage; var billingText = string.Empty; var usagePercent = 0d; if (hasAiPack && used is int usageCount && limit is int usageLimit && remaining is int remainingCount) { usageText = string.Format(Translator.WinoAccount_Management_AiPackUsage, usageCount, usageLimit, remainingCount); usagePercent = usageLimit > 0 ? (double)usageCount / usageLimit * 100 : 0; } if (hasAiPack && periodStart is DateTimeOffset billingStart && periodEnd is DateTimeOffset billingEnd) { billingText = string.Format(Translator.WinoAccount_Management_AiPackBillingPeriod, billingStart.LocalDateTime, billingEnd.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) { if (type is WinoPage pageType) { var pageInfo = SettingsNavigationInfoProvider.GetInfo(pageType, AccountSummaryText); Messenger.Send(new BreadcrumbNavigationRequested(pageInfo.Title, pageType)); } } [RelayCommand] private async Task NavigateExternalAsync(object target) { if (target is not string stringTarget || string.IsNullOrWhiteSpace(stringTarget)) return; if (stringTarget == "Store") { await _storeRatingService.LaunchStorePageForReviewAsync(); return; } await _nativeAppService.LaunchUriAsync(new Uri(stringTarget)); } }