using System; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Serilog; using Wino.Core.Domain; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Navigation; using Wino.Core.Extensions; using Wino.Mail.ViewModels.Data; namespace Wino.Mail.ViewModels; public partial class StoragePageViewModel( IAccountService accountService, IMimeStorageService mimeStorageService, IMailDialogService dialogService) : MailBaseViewModel { private readonly ILogger _logger = Log.ForContext(); private readonly IAccountService _accountService = accountService; private readonly IMimeStorageService _mimeStorageService = mimeStorageService; private readonly IMailDialogService _dialogService = dialogService; public ObservableCollection AccountStorageItems { get; } = []; [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsBusy))] public partial bool IsLoading { get; set; } [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsBusy))] public partial bool IsCleaning { get; set; } [ObservableProperty] public partial string MimeRootPath { get; set; } = string.Empty; [ObservableProperty] public partial string SummaryText { get; set; } = ""; public bool IsBusy => IsLoading || IsCleaning; public override async void OnNavigatedTo(NavigationMode mode, object parameters) { base.OnNavigatedTo(mode, parameters); await ExecuteUIThread(() => { SummaryText = Translator.SettingsStorage_NoLocalMimeDataFound; }); await RefreshStorageAsync(); } partial void OnIsLoadingChanged(bool value) { UpdateAccountBusyState(); } partial void OnIsCleaningChanged(bool value) { UpdateAccountBusyState(); } private void UpdateAccountBusyState() { Dispatcher.ExecuteOnUIThread(() => { foreach (var item in AccountStorageItems) { item.IsBusy = IsBusy; } }); } [RelayCommand] private async Task RefreshStorageAsync() { if (IsBusy) return; await ExecuteUIThread(() => { IsLoading = true; }); try { var mimeRootPath = await _mimeStorageService.GetMimeRootPathAsync().ConfigureAwait(false); var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false); var sizeMap = await _mimeStorageService.GetAccountsMimeStorageSizesAsync(accounts.Select(a => a.Id)).ConfigureAwait(false); var storageItems = accounts .Select(account => { sizeMap.TryGetValue(account.Id, out var accountSize); var viewModel = new AccountStorageItemViewModel(account, accountSize, DeleteAllCommand, DeleteOlderThanOneMonthCommand, DeleteOlderThanThreeMonthsCommand, DeleteOlderThanSixMonthsCommand, DeleteOlderThanOneYearCommand); viewModel.SizeDescription = string.Format(Translator.SettingsStorage_AccountUsageDescription, viewModel.SizeText); return viewModel; }) .OrderByDescending(a => a.SizeBytes) .ToList(); await ExecuteUIThread(() => { MimeRootPath = mimeRootPath; AccountStorageItems.Clear(); foreach (var item in storageItems) { AccountStorageItems.Add(item); } var total = storageItems.Sum(a => a.SizeBytes); SummaryText = storageItems.Count == 0 ? Translator.SettingsStorage_NoAccountsFound : string.Format(Translator.SettingsStorage_TotalUsage, total.GetBytesReadable()); }); } catch (Exception ex) { _logger.Error(ex, "Failed to refresh storage data."); await ExecuteUIThread(() => { _dialogService.InfoBarMessage(Translator.GeneralTitle_Error, ex.Message, Core.Domain.Enums.InfoBarMessageType.Error); }); } finally { await ExecuteUIThread(() => { IsLoading = false; }); } } [RelayCommand] private async Task DeleteAllAsync(AccountStorageItemViewModel accountItem) { if (accountItem == null || IsBusy) return; bool approved = await _dialogService.ShowConfirmationDialogAsync( string.Format(Translator.SettingsStorage_DeleteAll_Confirm_Message, accountItem.AccountName), Translator.SettingsStorage_DeleteAll_Confirm_Title, Translator.Buttons_Delete); if (!approved) return; await ExecuteUIThread(() => { IsCleaning = true; }); try { await _mimeStorageService.DeleteAccountMimeStorageAsync(accountItem.Account.Id).ConfigureAwait(false); await ExecuteUIThread(() => { _dialogService.InfoBarMessage(Translator.GeneralTitle_Info, Translator.SettingsStorage_DeleteAll_Success, Core.Domain.Enums.InfoBarMessageType.Success); }); await RefreshStorageAsync(); } catch (Exception ex) { _logger.Error(ex, "Failed to delete all MIME content for account {AccountId}", accountItem.Account.Id); await ExecuteUIThread(() => { _dialogService.InfoBarMessage(Translator.GeneralTitle_Error, ex.Message, Core.Domain.Enums.InfoBarMessageType.Error); }); } finally { await ExecuteUIThread(() => { IsCleaning = false; }); } } [RelayCommand] private Task DeleteOlderThanOneMonthAsync(AccountStorageItemViewModel accountItem) => DeleteOlderThanAsync(accountItem, 1); [RelayCommand] private Task DeleteOlderThanThreeMonthsAsync(AccountStorageItemViewModel accountItem) => DeleteOlderThanAsync(accountItem, 3); [RelayCommand] private Task DeleteOlderThanSixMonthsAsync(AccountStorageItemViewModel accountItem) => DeleteOlderThanAsync(accountItem, 6); [RelayCommand] private Task DeleteOlderThanOneYearAsync(AccountStorageItemViewModel accountItem) => DeleteOlderThanAsync(accountItem, 12); private async Task DeleteOlderThanAsync(AccountStorageItemViewModel accountItem, int months) { if (accountItem == null || IsBusy) return; string rangeText = GetRangeText(months); bool approved = await _dialogService.ShowConfirmationDialogAsync( string.Format(Translator.SettingsStorage_DeleteOld_Confirm_Message, rangeText, accountItem.AccountName), Translator.SettingsStorage_DeleteOld_Confirm_Title, Translator.Buttons_Delete); if (!approved) return; await ExecuteUIThread(() => { IsCleaning = true; }); try { var cutoffDateUtc = DateTime.UtcNow.AddMonths(-months); var deletedDirectoryCount = await _mimeStorageService .DeleteAccountMimeStorageOlderThanAsync(accountItem.Account.Id, cutoffDateUtc) .ConfigureAwait(false); await ExecuteUIThread(() => { _dialogService.InfoBarMessage( Translator.GeneralTitle_Info, string.Format(Translator.SettingsStorage_DeleteOld_Success, deletedDirectoryCount, rangeText), Core.Domain.Enums.InfoBarMessageType.Success); }); await RefreshStorageAsync(); } catch (Exception ex) { _logger.Error(ex, "Failed to delete MIME content by cutoff for account {AccountId}", accountItem.Account.Id); await ExecuteUIThread(() => { _dialogService.InfoBarMessage(Translator.GeneralTitle_Error, ex.Message, Core.Domain.Enums.InfoBarMessageType.Error); }); } finally { await ExecuteUIThread(() => { IsCleaning = false; }); } } private static string GetRangeText(int months) { return months switch { 1 => Translator.SettingsStorage_1Month, 3 => Translator.SettingsStorage_3Months, 6 => Translator.SettingsStorage_6Months, 12 => Translator.SettingsStorage_1Year, _ => string.Format(Translator.SettingsStorage_Months, months) }; } }