diff --git a/Source/LibationAvalonia/App.axaml b/Source/LibationAvalonia/App.axaml index da3b6fad..9c4b6240 100644 --- a/Source/LibationAvalonia/App.axaml +++ b/Source/LibationAvalonia/App.axaml @@ -1,7 +1,8 @@  + x:Class="LibationAvalonia.App" + Name="Libation"> @@ -69,4 +70,11 @@ + + + + + + + \ No newline at end of file diff --git a/Source/LibationAvalonia/App.axaml.cs b/Source/LibationAvalonia/App.axaml.cs index c5e309ea..9d1c5159 100644 --- a/Source/LibationAvalonia/App.axaml.cs +++ b/Source/LibationAvalonia/App.axaml.cs @@ -14,6 +14,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; +using ReactiveUI; +using DataLayer; namespace LibationAvalonia { @@ -45,9 +47,6 @@ namespace LibationAvalonia { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - var acceleratorKey = Configuration.IsMacOs ? KeyModifiers.Meta : KeyModifiers.Alt; - AvaloniaLocator.CurrentMutable.Bind().ToFunc(() => new AccessKeyHandlerEx(acceleratorKey)); - var config = Configuration.Instance; if (!config.LibationSettingsAreValid) @@ -219,9 +218,8 @@ namespace LibationAvalonia LoadStyles(); var mainWindow = new MainWindow(); desktop.MainWindow = MainWindow = mainWindow; - mainWindow.RestoreSizeAndLocation(Configuration.Instance); - mainWindow.OnLoad(); mainWindow.OnLibraryLoaded(LibraryTask.GetAwaiter().GetResult()); + mainWindow.RestoreSizeAndLocation(Configuration.Instance); mainWindow.Show(); } diff --git a/Source/LibationAvalonia/Assets/LibationVectorIcons.xaml b/Source/LibationAvalonia/Assets/LibationVectorIcons.xaml index d2e2d278..9c9d7ec2 100644 --- a/Source/LibationAvalonia/Assets/LibationVectorIcons.xaml +++ b/Source/LibationAvalonia/Assets/LibationVectorIcons.xaml @@ -64,6 +64,33 @@ M7.2,0.8 a 0.8,0.8 0 0 1 1.6,0 v8 l0.9929,-0.9929 a 0.8,0.8 0 0 1 1.1314,1.1314 l-2.3586,2.3586 a 0.8,0.8 0 0 1 -1.1314,0 l-2.3586,-2.3586 a 0.8,0.8 0 0 1 1.1314,-1.1314 l0.9929,0.9929 v8 + + + M139,2 + A 192,200 0 0 0 103,84 + A 222,334 41 0 0 241,320 + V478 + H160 + A 16,16 0 0 0 160,510 + H352 + A16 16 0 0 0 352,478 + H271 + V320 + A 222,334 -41 0 0 409,84 + A 192,200 0 0 0 373,2 + M355,32 + A 192,200 0 0 1 381,127 + A 187.5,334 -35 0 1 256,286 + A 187.5,334 35 0 1 131,127 + A 192,200 0 0 1 157,32 + H355 + M146,147 + A 168,300 35 0 0 256,270 + A 168,300 -35 0 0 366,128 + S 360,50 280,110 + S 192,128 147,147 + + diff --git a/Source/LibationAvalonia/Controls/DirectorySelectControl.axaml b/Source/LibationAvalonia/Controls/DirectorySelectControl.axaml index 45f6fe02..c66c7a1b 100644 --- a/Source/LibationAvalonia/Controls/DirectorySelectControl.axaml +++ b/Source/LibationAvalonia/Controls/DirectorySelectControl.axaml @@ -27,7 +27,7 @@ + Text="{Binding Converter={StaticResource KnownDirectoryConverter}}" /> diff --git a/Source/LibationAvalonia/Controls/GroupBox.axaml b/Source/LibationAvalonia/Controls/GroupBox.axaml index 91246b19..c4d23b8f 100644 --- a/Source/LibationAvalonia/Controls/GroupBox.axaml +++ b/Source/LibationAvalonia/Controls/GroupBox.axaml @@ -6,9 +6,6 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="LibationAvalonia.Controls.GroupBox"> - - - + + + + + + + + + + + + + diff --git a/Source/LibationAvalonia/Dialogs/AboutDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/AboutDialog.axaml.cs new file mode 100644 index 00000000..cbcd7d99 --- /dev/null +++ b/Source/LibationAvalonia/Dialogs/AboutDialog.axaml.cs @@ -0,0 +1,80 @@ +using Avalonia.Controls; +using LibationAvalonia.Controls; +using LibationAvalonia.ViewModels; +using LibationFileManager; +using LibationUiBase; +using ReactiveUI; +using System; +using System.Threading.Tasks; + +namespace LibationAvalonia.Dialogs +{ + public partial class AboutDialog : DialogWindow + { + private readonly AboutVM _viewModel; + public AboutDialog() : base(saveAndRestorePosition:false) + { + if (Design.IsDesignMode) + _ = Configuration.Instance.LibationFiles; + + InitializeComponent(); + + DataContext = _viewModel = new AboutVM(); + } + + private async void CheckForUpgrade_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) + { + var mainWindow = Owner as Views.MainWindow; + + var upgrader = new Upgrader(); + upgrader.DownloadProgress += async (_, e) => await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => mainWindow.ViewModel.DownloadProgress = e.ProgressPercentage); + upgrader.DownloadCompleted += async (_, _) => await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => mainWindow.ViewModel.DownloadProgress = null); + + _viewModel.CanCheckForUpgrade = false; + Version latestVersion = null; + await upgrader.CheckForUpgradeAsync(OnUpgradeAvailable); + + _viewModel.CanCheckForUpgrade = latestVersion is null; + + _viewModel.UpgradeButtonText = latestVersion is null ? "Libation is up to date. Check Again." : $"Version {latestVersion:3} is available"; + + async Task OnUpgradeAvailable(UpgradeEventArgs e) + { + var notificationResult = await new UpgradeNotificationDialog(e.UpgradeProperties, e.CapUpgrade).ShowDialogAsync(this); + + e.Ignore = notificationResult == DialogResult.Ignore; + e.InstallUpgrade = notificationResult == DialogResult.OK; + latestVersion = e.UpgradeProperties.LatestRelease; + } + } + + private void Link_GithubUser(object sender, Avalonia.Input.TappedEventArgs e) + { + if (sender is LinkLabel lbl) + { + Dinah.Core.Go.To.Url($"ht" + $"tps://github.com/{lbl.Text.Replace('.','-')}"); + } + } + + private void Link_getlibation(object sender, Avalonia.Input.TappedEventArgs e) => Dinah.Core.Go.To.Url(AppScaffolding.LibationScaffolding.WebsiteUrl); + + private void ViewReleaseNotes_Tapped(object sender, Avalonia.Input.TappedEventArgs e) + => Dinah.Core.Go.To.Url($"{AppScaffolding.LibationScaffolding.RepositoryUrl}/releases/tag/v{AppScaffolding.LibationScaffolding.BuildVersion.ToString(3)}"); + } + + public class AboutVM : ViewModelBase + { + public string Version { get; } + public bool CanCheckForUpgrade { get => canCheckForUpgrade; set => this.RaiseAndSetIfChanged(ref canCheckForUpgrade, value); } + public string UpgradeButtonText { get => upgradeButtonText; set => this.RaiseAndSetIfChanged(ref upgradeButtonText, value); } + + + private bool canCheckForUpgrade = true; + private string upgradeButtonText = "Check for Upgrade"; + + public AboutVM() + { + Version = $"Libation {AppScaffolding.LibationScaffolding.Variety} v{AppScaffolding.LibationScaffolding.BuildVersion}"; + } + } +} diff --git a/Source/LibationAvalonia/Dialogs/LiberatedStatusBatchAutoDialog.axaml b/Source/LibationAvalonia/Dialogs/LiberatedStatusBatchAutoDialog.axaml index 05ffd5d0..426cda5f 100644 --- a/Source/LibationAvalonia/Dialogs/LiberatedStatusBatchAutoDialog.axaml +++ b/Source/LibationAvalonia/Dialogs/LiberatedStatusBatchAutoDialog.axaml @@ -2,17 +2,17 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="120" + mc:Ignorable="d" d:DesignWidth="550" d:DesignHeight="130" xmlns:controls="clr-namespace:LibationAvalonia.Controls" x:Class="LibationAvalonia.Dialogs.LiberatedStatusBatchAutoDialog" Title="Liberated status: Whether the book has been downloaded" - MinHeight="100" MaxHeight="165" - MinWidth="600" MaxWidth="800" - Width="600" + MinHeight="130" MaxHeight="130" + MinWidth="550" MaxWidth="550" + Width="550" Height="130" WindowStartupLocation="CenterOwner" Icon="/Assets/libation.ico"> - + - diff --git a/Source/LibationAvalonia/Dialogs/TrashBinDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/TrashBinDialog.axaml.cs index 07d66868..f97bb09f 100644 --- a/Source/LibationAvalonia/Dialogs/TrashBinDialog.axaml.cs +++ b/Source/LibationAvalonia/Dialogs/TrashBinDialog.axaml.cs @@ -25,12 +25,18 @@ namespace LibationAvalonia.Dialogs DataContext = _viewModel = new(); this.Closing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance); + this.KeyDown += TrashBinDialog_KeyDown; } public async void EmptyTrash_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) => await _viewModel.PermanentlyDeleteCheckedAsync(); public async void Restore_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) => await _viewModel.RestoreCheckedAsync(); + private void TrashBinDialog_KeyDown(object sender, Avalonia.Input.KeyEventArgs e) + { + if (e.Key == Avalonia.Input.Key.Escape) + Close(); + } } public class TrashBinViewModel : ViewModelBase, IDisposable diff --git a/Source/LibationAvalonia/LibationAvalonia.csproj b/Source/LibationAvalonia/LibationAvalonia.csproj index 47eb7468..63ea3003 100644 --- a/Source/LibationAvalonia/LibationAvalonia.csproj +++ b/Source/LibationAvalonia/LibationAvalonia.csproj @@ -51,8 +51,8 @@ LiberateStatusButton.axaml - - MainWindow.axaml + + MainVM.cs diff --git a/Source/LibationAvalonia/AccessKeyHandlerEx.cs b/Source/LibationAvalonia/MacAccessKeyHandler.cs similarity index 52% rename from Source/LibationAvalonia/AccessKeyHandlerEx.cs rename to Source/LibationAvalonia/MacAccessKeyHandler.cs index 9369eb14..bee7eebf 100644 --- a/Source/LibationAvalonia/AccessKeyHandlerEx.cs +++ b/Source/LibationAvalonia/MacAccessKeyHandler.cs @@ -1,54 +1,44 @@ using Avalonia; using Avalonia.Input; -using System.Linq; namespace LibationAvalonia { - internal class AccessKeyHandlerEx : AccessKeyHandler + internal class MacAccessKeyHandler : AccessKeyHandler { - public KeyModifiers KeyModifier { get; } - private readonly Key[] ActivatorKeys; - - public AccessKeyHandlerEx(KeyModifiers menuKeyModifier) - { - KeyModifier = menuKeyModifier; - ActivatorKeys = menuKeyModifier switch - { - KeyModifiers.Alt => new[] { Key.LeftAlt, Key.RightAlt }, - KeyModifiers.Control => new[] { Key.LeftCtrl, Key.RightCtrl }, - KeyModifiers.Meta => new[] { Key.LWin, Key.RWin }, - _ => throw new System.NotSupportedException($"{nameof(KeyModifiers)}.{menuKeyModifier} is not implemented"), - }; - } - protected override void OnPreviewKeyDown(object sender, KeyEventArgs e) { - if (ActivatorKeys.Contains(e.Key) && e.KeyModifiers.HasAllFlags(KeyModifier)) + if (e.Key is Key.LWin or Key.RWin) { var newArgs = new KeyEventArgs { Key = Key.LeftAlt, Handled = e.Handled }; base.OnPreviewKeyDown(sender, newArgs); e.Handled = newArgs.Handled; } - } + else if (e.Key is not Key.LeftAlt and not Key.RightAlt) + base.OnPreviewKeyDown(sender, e); + } protected override void OnPreviewKeyUp(object sender, KeyEventArgs e) { - if (ActivatorKeys.Contains(e.Key) && e.KeyModifiers.HasAllFlags(KeyModifier)) + if (e.Key is Key.LWin or Key.RWin) { var newArgs = new KeyEventArgs { Key = Key.LeftAlt, Handled = e.Handled }; base.OnPreviewKeyUp(sender, newArgs); e.Handled = newArgs.Handled; - } - } + } + else if (e.Key is not Key.LeftAlt and not Key.RightAlt) + base.OnPreviewKeyDown(sender, e); + } protected override void OnKeyDown(object sender, KeyEventArgs e) { - if (e.KeyModifiers.HasAllFlags(KeyModifier)) + if (e.KeyModifiers.HasAllFlags(KeyModifiers.Meta)) { var newArgs = new KeyEventArgs { Key = e.Key, Handled = e.Handled, KeyModifiers = KeyModifiers.Alt }; base.OnKeyDown(sender, newArgs); e.Handled = newArgs.Handled; - } - } + } + else if (!e.KeyModifiers.HasFlag(KeyModifiers.Alt)) + base.OnPreviewKeyDown(sender, e); + } } } diff --git a/Source/LibationAvalonia/ViewModels/MainVM.BackupCounts.cs b/Source/LibationAvalonia/ViewModels/MainVM.BackupCounts.cs new file mode 100644 index 00000000..ed3cfd5f --- /dev/null +++ b/Source/LibationAvalonia/ViewModels/MainVM.BackupCounts.cs @@ -0,0 +1,58 @@ +using ApplicationServices; +using Avalonia.Threading; +using ReactiveUI; +using System.Threading.Tasks; + +namespace LibationAvalonia.ViewModels +{ + partial class MainVM + { + private Task updateCountsTask; + private LibraryCommands.LibraryStats _libraryStats; + + /// The "Begin Book and PDF Backup" menu item header text + public string BookBackupsToolStripText { get; private set; } = "Begin Book and PDF Backups: 0"; + /// The "Begin PDF Only Backup" menu item header text + public string PdfBackupsToolStripText { get; private set; } = "Begin PDF Only Backups: 0"; + + /// The user's library statistics + public LibraryCommands.LibraryStats LibraryStats + { + get => _libraryStats; + set + { + this.RaiseAndSetIfChanged(ref _libraryStats, value); + + BookBackupsToolStripText + = LibraryStats.HasPendingBooks + ? "Begin " + menufyText($"Book and PDF Backups: {LibraryStats.PendingBooks} remaining") + : "All books have been liberated"; + + PdfBackupsToolStripText + = LibraryStats.pdfsNotDownloaded > 0 + ? "Begin " + menufyText($"PDF Only Backups: {LibraryStats.pdfsNotDownloaded} remaining") + : "All PDFs have been downloaded"; + + this.RaisePropertyChanged(nameof(BookBackupsToolStripText)); + this.RaisePropertyChanged(nameof(PdfBackupsToolStripText)); + } + } + + private void Configure_BackupCounts() + { + MainWindow.Loaded += setBackupCounts; + LibraryCommands.LibrarySizeChanged += setBackupCounts; + LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts; + } + + private async void setBackupCounts(object _, object __) + { + if (updateCountsTask?.IsCompleted ?? true) + { + updateCountsTask = Task.Run(() => LibraryCommands.GetCounts()); + var stats = await updateCountsTask; + await Dispatcher.UIThread.InvokeAsync(() => LibraryStats = stats); + } + } + } +} diff --git a/Source/LibationAvalonia/Views/MainWindow.Export.cs b/Source/LibationAvalonia/ViewModels/MainVM.Export.cs similarity index 76% rename from Source/LibationAvalonia/Views/MainWindow.Export.cs rename to Source/LibationAvalonia/ViewModels/MainVM.Export.cs index 7bcd0fb9..c29f90ce 100644 --- a/Source/LibationAvalonia/Views/MainWindow.Export.cs +++ b/Source/LibationAvalonia/ViewModels/MainVM.Export.cs @@ -3,21 +3,22 @@ using Avalonia.Platform.Storage; using FileManager; using LibationFileManager; using System; +using System.Threading.Tasks; -namespace LibationAvalonia.Views +namespace LibationAvalonia.ViewModels { - public partial class MainWindow + partial class MainVM { private void Configure_Export() { } - public async void exportLibraryToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) + public async Task ExportLibraryAsync() { try { var options = new FilePickerSaveOptions { Title = "Where to export Library", - SuggestedStartLocation = await StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books.PathWithoutPrefix), + SuggestedStartLocation = await MainWindow.StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books.PathWithoutPrefix), SuggestedFileName = $"Libation Library Export {DateTime.Now:yyyy-MM-dd}", DefaultExtension = "xlsx", ShowOverwritePrompt = true, @@ -43,7 +44,7 @@ namespace LibationAvalonia.Views } }; - var selectedFile = (await StorageProvider.SaveFilePickerAsync(options))?.TryGetLocalPath(); + var selectedFile = (await MainWindow.StorageProvider.SaveFilePickerAsync(options))?.TryGetLocalPath(); if (selectedFile is null) return; @@ -66,7 +67,7 @@ namespace LibationAvalonia.Views } catch (Exception ex) { - await MessageBox.ShowAdminAlert(this, "Error attempting to export your library.", "Error exporting", ex); + await MessageBox.ShowAdminAlert(MainWindow, "Error attempting to export your library.", "Error exporting", ex); } } } diff --git a/Source/LibationAvalonia/ViewModels/MainVM.Filters.cs b/Source/LibationAvalonia/ViewModels/MainVM.Filters.cs new file mode 100644 index 00000000..76c42e83 --- /dev/null +++ b/Source/LibationAvalonia/ViewModels/MainVM.Filters.cs @@ -0,0 +1,123 @@ +using Avalonia; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Input; +using LibationFileManager; +using ReactiveUI; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace LibationAvalonia.ViewModels +{ + partial class MainVM + { + private string lastGoodFilter = ""; + private string _filterString; + private bool _firstFilterIsDefault = true; + + /// Library filterting query + public string FilterString { get => _filterString; set => this.RaiseAndSetIfChanged(ref _filterString, value); } + public AvaloniaList QuickFilterMenuItems { get; } = new(); + /// Indicates if the first quick filter is the default filter + public bool FirstFilterIsDefault { get => _firstFilterIsDefault; set => QuickFilters.UseDefault = this.RaiseAndSetIfChanged(ref _firstFilterIsDefault, value); } + + + private void Configure_Filters() + { + FirstFilterIsDefault = QuickFilters.UseDefault; + MainWindow.Initialized += updateFiltersMenu; + QuickFilters.Updated += updateFiltersMenu; + + //We need to be able to dynamically add and remove menu items from the Quick Filters menu. + //To do that, we need quick filter's menu items source to be writable, which we can only + //achieve by creating the list ourselves (instead of allowing Avalonia to create it from the xaml) + + QuickFilterMenuItems.Add(new MenuItem + { + + Header = "Start Libation with 1st filter _Default", + Command = ReactiveCommand.Create(ToggleFirstFilterIsDefault), + Icon = new CheckBox + { + BorderThickness = new Thickness(0), + IsHitTestVisible = false, + [!CheckBox.IsCheckedProperty] = new Binding(nameof(FirstFilterIsDefault)) + } + }); + QuickFilterMenuItems.Add(new MenuItem { Header = "_Edit quick filters...", Command = ReactiveCommand.Create(EditQuickFiltersAsync) }); + QuickFilterMenuItems.Add(new Separator()); + } + + public void AddQuickFilterBtn() => QuickFilters.Add(FilterString); + public async Task FilterBtn() => await PerformFilter(FilterString); + public async Task FilterHelpBtn() => await new LibationAvalonia.Dialogs.SearchSyntaxDialog().ShowDialog(MainWindow); + public void ToggleFirstFilterIsDefault() => FirstFilterIsDefault = !FirstFilterIsDefault; + public async Task EditQuickFiltersAsync() => await new LibationAvalonia.Dialogs.EditQuickFilters().ShowDialog(MainWindow); + public async Task PerformFilter(string filterString) + { + FilterString = filterString; + + try + { + await ProductsDisplay.Filter(filterString); + lastGoodFilter = filterString; + } + catch (Exception ex) + { + await MessageBox.Show($"Bad filter string:\r\n\r\n{ex.Message}", "Bad filter string", MessageBoxButtons.OK, MessageBoxIcon.Error); + + // re-apply last good filter + await PerformFilter(lastGoodFilter); + } + } + + private void updateFiltersMenu(object _ = null, object __ = null) + { + //Clear all filters + var quickFilterNativeMenu = (NativeMenuItem)NativeMenu.GetMenu(MainWindow).Items[3]; + for (int i = quickFilterNativeMenu.Menu.Items.Count - 1; i >= 3; i--) + { + var command = ((NativeMenuItem)quickFilterNativeMenu.Menu.Items[i]).Command as IDisposable; + if (command != null) + { + var existingBinding = MainWindow.KeyBindings.FirstOrDefault(kb => kb.Command == command); + if (existingBinding != null) + MainWindow.KeyBindings.Remove(existingBinding); + + command.Dispose(); + } + + quickFilterNativeMenu.Menu.Items.RemoveAt(i); + QuickFilterMenuItems.RemoveAt(i); + } + + // re-populate + var index = 0; + foreach (var filter in QuickFilters.Filters) + { + var command = ReactiveCommand.Create(async () => await PerformFilter(filter)); + + var menuItem = new MenuItem { Header = $"{++index}: {filter}", Command = command }; + var nativeMenuItem = new NativeMenuItem { Header = $"{index}: {filter}", Command = command }; + + if (Configuration.IsMacOs && index <= 10) + { + //Register hotkeys Command + 1 - 0 for quick filters + var key = index == 10 ? Key.D0 : Key.D0 + index; + nativeMenuItem.Gesture = new KeyGesture(key, KeyModifiers.Meta); + } + else if (!Configuration.IsMacOs && index <= 12) + { + //Register hotkeys F1 - F12 for quick filters + menuItem.InputGesture = new KeyGesture(Key.F1 + index - 1); + MainWindow.KeyBindings.Add(new KeyBinding { Command = command, Gesture = menuItem.InputGesture }); + } + + QuickFilterMenuItems.Add(menuItem); + quickFilterNativeMenu.Menu.Items.Add(nativeMenuItem); + } + } + } +} diff --git a/Source/LibationAvalonia/ViewModels/MainVM.Import.cs b/Source/LibationAvalonia/ViewModels/MainVM.Import.cs new file mode 100644 index 00000000..1bae061f --- /dev/null +++ b/Source/LibationAvalonia/ViewModels/MainVM.Import.cs @@ -0,0 +1,254 @@ +using ApplicationServices; +using AudibleUtilities; +using Avalonia.Controls; +using LibationFileManager; +using ReactiveUI; +using System; +using System.Linq; +using System.Threading.Tasks; +using Avalonia.Input; + +namespace LibationAvalonia.ViewModels +{ + public partial class MainVM + { + private bool _autoScanChecked = Configuration.Instance.AutoScan; + private string _removeBooksButtonText = "Remove # Books from Libation"; + private bool _removeBooksButtonEnabled = Design.IsDesignMode; + private bool _removeButtonsVisible = Design.IsDesignMode; + private int _numAccountsScanning = 2; + private int _accountsCount = 0; + + /// Auto scanning accounts is enables + public bool AutoScanChecked { get => _autoScanChecked; set => Configuration.Instance.AutoScan = this.RaiseAndSetIfChanged(ref _autoScanChecked, value); } + /// Display text for the "Remove # Books from Libation" button + public string RemoveBooksButtonText { get => _removeBooksButtonText; set => this.RaiseAndSetIfChanged(ref _removeBooksButtonText, value); } + /// Indicates if the "Remove # Books from Libation" button is enabled + public bool RemoveBooksButtonEnabled { get => _removeBooksButtonEnabled; set { this.RaiseAndSetIfChanged(ref _removeBooksButtonEnabled, value); } } + /// Indicates if the "Remove # Books from Libation" and "Done Removing" buttons should be visible + public bool RemoveButtonsVisible + { + get => _removeButtonsVisible; + set + { + this.RaiseAndSetIfChanged(ref _removeButtonsVisible, value); + this.RaisePropertyChanged(nameof(RemoveMenuItemsEnabled)); + } + } + /// Indicates if Libation is currently scanning account(s) + public bool ActivelyScanning => _numAccountsScanning > 0; + /// Indicates if the "Remove Books" menu items are enabled + public bool RemoveMenuItemsEnabled => !RemoveButtonsVisible && !ActivelyScanning; + /// The library scanning status text + public string ScanningText => _numAccountsScanning == 1 ? "Scanning..." : $"Scanning {_numAccountsScanning} accounts..."; + /// There is at least one Audible account + public bool AnyAccounts => AccountsCount > 0; + /// There is exactly one Audible account + public bool OneAccount => AccountsCount == 1; + /// There are more than 1 Audible accounts + public bool MultipleAccounts => AccountsCount > 1; + /// The number of accounts added to Libation + public int AccountsCount + { + get => _accountsCount; + set + { + this.RaiseAndSetIfChanged(ref _accountsCount, value); + this.RaisePropertyChanged(nameof(AnyAccounts)); + this.RaisePropertyChanged(nameof(OneAccount)); + this.RaisePropertyChanged(nameof(MultipleAccounts)); + } + } + + + public void Configure_Import() + { + MainWindow.Loaded += (_, _) => + { + refreshImportMenu(); + AccountsSettingsPersister.Saved += refreshImportMenu; + }; + + AutoScanChecked = Configuration.Instance.AutoScan; + + setyNumScanningAccounts(0); + LibraryCommands.ScanBegin += (_, accountsLength) => setyNumScanningAccounts(accountsLength); + LibraryCommands.ScanEnd += (_, newCount) => setyNumScanningAccounts(0); + + if (!Design.IsDesignMode) + RemoveButtonsVisible = false; + } + + public void ToggleAutoScan() => AutoScanChecked = !AutoScanChecked; + + public async Task AddAccountsAsync() + { + await MessageBox.Show("To load your Audible library, come back here to the Import menu after adding your account"); + await new LibationAvalonia.Dialogs.AccountsDialog().ShowDialog(MainWindow); + } + + public async Task ScanAccountAsync() + { + using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); + await scanLibrariesAsync(persister.AccountsSettings.GetAll().FirstOrDefault()); + } + + public async Task ScanAllAccountsAsync() + { + using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); + await scanLibrariesAsync(persister.AccountsSettings.GetAll().ToArray()); + } + + public async Task ScanSomeAccountsAsync() + { + var scanAccountsDialog = new LibationAvalonia.Dialogs.ScanAccountsDialog(); + + if (await scanAccountsDialog.ShowDialog(MainWindow) != DialogResult.OK) + return; + + if (!scanAccountsDialog.CheckedAccounts.Any()) + return; + + await scanLibrariesAsync(scanAccountsDialog.CheckedAccounts.ToArray()); + } + + public async Task RemoveBooksAsync() + { + // if 0 accounts, this will not be visible + // if 1 account, run scanLibrariesRemovedBooks() on this account + // if multiple accounts, another menu set will open. do not run scanLibrariesRemovedBooks() + using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); + var accounts = persister.AccountsSettings.GetAll(); + + if (accounts.Count != 1) + return; + + var firstAccount = accounts.Single(); + await scanLibrariesRemovedBooks(firstAccount); + } + + // selectively remove books from all accounts + public async Task RemoveBooksAllAsync() + { + using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); + var allAccounts = persister.AccountsSettings.GetAll(); + await scanLibrariesRemovedBooks(allAccounts.ToArray()); + } + + public async Task RemoveBooksBtn() + { + RemoveBooksButtonEnabled = false; + await ProductsDisplay.RemoveCheckedBooksAsync(); + RemoveBooksButtonEnabled = true; + } + + public async Task DoneRemovingBtn() + { + RemoveButtonsVisible = false; + + ProductsDisplay.DoneRemovingBooks(); + + //Restore the filter + await PerformFilter(lastGoodFilter); + } + + // selectively remove books from some accounts + public async Task RemoveBooksSomeAsync() + { + var scanAccountsDialog = new LibationAvalonia.Dialogs.ScanAccountsDialog(); + + if (await scanAccountsDialog.ShowDialog(MainWindow) != DialogResult.OK) + return; + + if (!scanAccountsDialog.CheckedAccounts.Any()) + return; + + await scanLibrariesRemovedBooks(scanAccountsDialog.CheckedAccounts.ToArray()); + } + + public async Task LocateAudiobooksAsync() + { + var locateDialog = new LibationAvalonia.Dialogs.LocateAudiobooksDialog(); + await locateDialog.ShowDialog(MainWindow); + } + + private void setyNumScanningAccounts(int numScanning) + { + _numAccountsScanning = numScanning; + this.RaisePropertyChanged(nameof(ActivelyScanning)); + this.RaisePropertyChanged(nameof(RemoveMenuItemsEnabled)); + this.RaisePropertyChanged(nameof(ScanningText)); + } + + private async Task scanLibrariesRemovedBooks(params Account[] accounts) + { + //This action is meant to operate on the entire library. + //For removing books within a filter set, use + //Visible Books > Remove from library + + await ProductsDisplay.Filter(null); + + RemoveBooksButtonEnabled = true; + RemoveButtonsVisible = true; + + await ProductsDisplay.ScanAndRemoveBooksAsync(accounts); + } + + private async Task scanLibrariesAsync(params Account[] accounts) + { + try + { + var (totalProcessed, newAdded) = await LibraryCommands.ImportAccountAsync(LibationAvalonia.Dialogs.Login.AvaloniaLoginChoiceEager.ApiExtendedFunc, accounts); + + // this is here instead of ScanEnd so that the following is only possible when it's user-initiated, not automatic loop + if (Configuration.Instance.ShowImportedStats && newAdded > 0) + await MessageBox.Show($"Total processed: {totalProcessed}\r\nNew: {newAdded}"); + } + catch (OperationCanceledException) + { + Serilog.Log.Information("Audible login attempt cancelled by user"); + } + catch (Exception ex) + { + await MessageBox.ShowAdminAlert( + MainWindow, + "Error importing library. Please try again. If this still happens after 2 or 3 tries, stop and contact administrator", + "Error importing library", + ex); + } + } + + private void refreshImportMenu(object _ = null, EventArgs __ = null) + { + using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); + AccountsCount = persister.AccountsSettings.Accounts.Count; + + var importMenuItem = (NativeMenuItem)NativeMenu.GetMenu(MainWindow).Items[0]; + + for (int i = importMenuItem.Menu.Items.Count - 1; i >= 2; i--) + importMenuItem.Menu.Items.RemoveAt(i); + + if (AccountsCount < 1) + { + importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "No accounts yet. Add Account...", Command = ReactiveCommand.Create(AddAccountsAsync) }); + } + else if (AccountsCount == 1) + { + importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Scan Library", Command = ReactiveCommand.Create(ScanAccountAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Alt | KeyModifiers.Meta)}); + importMenuItem.Menu.Items.Add(new NativeMenuItemSeparator()); + importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Remove Library Books", Command = ReactiveCommand.Create(RemoveBooksAsync), Gesture = new KeyGesture(Key.R, KeyModifiers.Alt | KeyModifiers.Meta)}); + } + else + { + importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Scan Library of All Accounts", Command = ReactiveCommand.Create(ScanAllAccountsAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Alt | KeyModifiers.Meta)}); + importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Scan Library of Some Accounts", Command = ReactiveCommand.Create(ScanSomeAccountsAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Alt | KeyModifiers.Meta | KeyModifiers.Shift) }); + importMenuItem.Menu.Items.Add(new NativeMenuItemSeparator()); + importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Remove Books from All Accounts", Command = ReactiveCommand.Create(RemoveBooksAllAsync), Gesture = new KeyGesture(Key.R, KeyModifiers.Alt | KeyModifiers.Meta)}); + importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Remove Books from Some Accounts", Command = ReactiveCommand.Create(RemoveBooksSomeAsync), Gesture = new KeyGesture(Key.R, KeyModifiers.Alt | KeyModifiers.Meta | KeyModifiers.Shift) }); + } + + importMenuItem.Menu.Items.Add(new NativeMenuItemSeparator()); + importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Locate Audiobooks...", Command = ReactiveCommand.Create(LocateAudiobooksAsync) }); + } + } +} diff --git a/Source/LibationAvalonia/ViewModels/MainVM.Liberate.cs b/Source/LibationAvalonia/ViewModels/MainVM.Liberate.cs new file mode 100644 index 00000000..f5487022 --- /dev/null +++ b/Source/LibationAvalonia/ViewModels/MainVM.Liberate.cs @@ -0,0 +1,64 @@ +using ApplicationServices; +using LibationFileManager; +using System; +using System.Linq; +using System.Threading.Tasks; +using DataLayer; + +namespace LibationAvalonia.ViewModels +{ + partial class MainVM + { + public void Configure_Liberate() { } + + public void BackupAllBooks() + { + try + { + setQueueCollapseState(false); + + Serilog.Log.Logger.Information("Begin backing up all library books"); + + ProcessQueue.AddDownloadDecrypt( + DbContexts + .GetLibrary_Flat_NoTracking() + .UnLiberated() + ); + } + catch (Exception ex) + { + Serilog.Log.Logger.Error(ex, "An error occurred while backing up all library books"); + } + } + + public void BackupAllPdfs() + { + setQueueCollapseState(false); + ProcessQueue.AddDownloadPdf(DbContexts.GetLibrary_Flat_NoTracking().Where(lb => lb.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)); + } + + public async Task ConvertAllToMp3Async() + { + var result = await MessageBox.Show(MainWindow, + "This converts all m4b titles in your library to mp3 files. Original files are not deleted." + + "\r\nFor large libraries this will take a long time and will take up more disk space." + + "\r\n\r\nContinue?" + + "\r\n\r\n(To always download titles as mp3 instead of m4b, go to Settings: Download my books as .MP3 files)", + "Convert all M4b => Mp3?", + MessageBoxButtons.YesNo, + MessageBoxIcon.Warning); + if (result == DialogResult.Yes) + { + setQueueCollapseState(false); + ProcessQueue.AddConvertMp3(DbContexts.GetLibrary_Flat_NoTracking().Where(lb => lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated && lb.Book.ContentType is ContentType.Product)); + } + //Only Queue Liberated books for conversion. This isn't a perfect filter, but it's better than nothing. + } + + private void setQueueCollapseState(bool collapsed) + { + QueueOpen = !collapsed; + Configuration.Instance.SetNonString(!collapsed, nameof(QueueOpen)); + } + } +} diff --git a/Source/LibationAvalonia/Views/MainWindow.ProcessQueue.cs b/Source/LibationAvalonia/ViewModels/MainVM.ProcessQueue.cs similarity index 57% rename from Source/LibationAvalonia/Views/MainWindow.ProcessQueue.cs rename to Source/LibationAvalonia/ViewModels/MainVM.ProcessQueue.cs index 7b3699d5..9c30a7cc 100644 --- a/Source/LibationAvalonia/Views/MainWindow.ProcessQueue.cs +++ b/Source/LibationAvalonia/ViewModels/MainVM.ProcessQueue.cs @@ -1,35 +1,52 @@ -using DataLayer; -using Dinah.Core; -using LibationFileManager; -using LibationUiBase.GridView; +using LibationFileManager; using System; using System.Linq; +using DataLayer; +using Dinah.Core; +using LibationUiBase.GridView; +using ReactiveUI; -namespace LibationAvalonia.Views +namespace LibationAvalonia.ViewModels { - public partial class MainWindow + partial class MainVM { - private void Configure_ProcessQueue() + private bool _queueOpen = false; + + /// The Process Queue panel is open + public bool QueueOpen { - var collapseState = !Configuration.Instance.GetNonString(defaultValue: true, nameof(_viewModel.QueueOpen)); - SetQueueCollapseState(collapseState); + get => _queueOpen; + set + { + this.RaiseAndSetIfChanged(ref _queueOpen, value); + QueueButtonAngle = value ? 180 : 0; + this.RaisePropertyChanged(nameof(QueueButtonAngle)); + } } - public async void ProductsDisplay_LiberateClicked(object sender, LibraryBook libraryBook) + public double QueueButtonAngle { get; private set; } + + private void Configure_ProcessQueue() + { + var collapseState = !Configuration.Instance.GetNonString(defaultValue: true, nameof(QueueOpen)); + setQueueCollapseState(collapseState); + } + + public async void LiberateClicked(LibraryBook libraryBook) { try { if (libraryBook.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload) { Serilog.Log.Logger.Information("Begin single book backup of {libraryBook}", libraryBook); - SetQueueCollapseState(false); - _viewModel.ProcessQueue.AddDownloadDecrypt(libraryBook); + setQueueCollapseState(false); + ProcessQueue.AddDownloadDecrypt(libraryBook); } else if (libraryBook.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated) { Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", libraryBook); - SetQueueCollapseState(false); - _viewModel.ProcessQueue.AddDownloadPdf(libraryBook); + setQueueCollapseState(false); + ProcessQueue.AddDownloadPdf(libraryBook); } else if (libraryBook.Book.Audio_Exists()) { @@ -49,15 +66,15 @@ namespace LibationAvalonia.Views } } - public void ProductsDisplay_LiberateSeriesClicked(object sender, ISeriesEntry series) + public void LiberateSeriesClicked(ISeriesEntry series) { try { - SetQueueCollapseState(false); + setQueueCollapseState(false); Serilog.Log.Logger.Information("Begin backing up all {series} episodes", series.LibraryBook); - _viewModel.ProcessQueue.AddDownloadDecrypt(series.Children.Select(c => c.LibraryBook).UnLiberated()); + ProcessQueue.AddDownloadDecrypt(series.Children.Select(c => c.LibraryBook).UnLiberated()); } catch (Exception ex) { @@ -65,15 +82,15 @@ namespace LibationAvalonia.Views } } - public void ProductsDisplay_ConvertToMp3Clicked(object sender, LibraryBook libraryBook) + public void ConvertToMp3Clicked(LibraryBook libraryBook) { try { if (libraryBook.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated) { - Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", libraryBook); - SetQueueCollapseState(false); - _viewModel.ProcessQueue.AddConvertMp3(libraryBook); + Serilog.Log.Logger.Information("Begin convert to mp3 {libraryBook}", libraryBook); + setQueueCollapseState(false); + ProcessQueue.AddConvertMp3(libraryBook); } } catch (Exception ex) @@ -81,15 +98,7 @@ namespace LibationAvalonia.Views Serilog.Log.Logger.Error(ex, "An error occurred while handling the stop light button click for {libraryBook}", libraryBook); } } - private void SetQueueCollapseState(bool collapsed) - { - _viewModel.QueueOpen = !collapsed; - Configuration.Instance.SetNonString(!collapsed, nameof(_viewModel.QueueOpen)); - } - public void ToggleQueueHideBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) - { - SetQueueCollapseState(_viewModel.QueueOpen); - } + public void ToggleQueueHideBtn() => setQueueCollapseState(QueueOpen); } } diff --git a/Source/LibationAvalonia/Views/MainWindow.ScanAuto.cs b/Source/LibationAvalonia/ViewModels/MainVM.ScanAuto.cs similarity index 65% rename from Source/LibationAvalonia/Views/MainWindow.ScanAuto.cs rename to Source/LibationAvalonia/ViewModels/MainVM.ScanAuto.cs index 785202da..2dd4829c 100644 --- a/Source/LibationAvalonia/Views/MainWindow.ScanAuto.cs +++ b/Source/LibationAvalonia/ViewModels/MainVM.ScanAuto.cs @@ -1,23 +1,19 @@ using ApplicationServices; using AudibleUtilities; -using Avalonia.Controls; using Dinah.Core; using LibationFileManager; using System; using System.Collections.Generic; using System.Linq; -namespace LibationAvalonia.Views +namespace LibationAvalonia.ViewModels { - public partial class MainWindow + partial class MainVM { - private InterruptableTimer autoScanTimer; + private readonly InterruptableTimer autoScanTimer = new InterruptableTimer(TimeSpan.FromMinutes(5)); private void Configure_ScanAuto() { - // creating InterruptableTimer inside 'Configure_' is a break from the pattern. As long as no one else needs to access or subscribe to it, this is ok - autoScanTimer = new InterruptableTimer(TimeSpan.FromMinutes(5)); - // subscribe as async/non-blocking. I'd actually rather prefer blocking but real-world testing found that caused a deadlock in the AudibleAPI autoScanTimer.Elapsed += async (_, __) => { @@ -30,7 +26,7 @@ namespace LibationAvalonia.Views // in autoScan, new books SHALL NOT show dialog try { - await LibraryCommands.ImportAccountAsync(Dialogs.Login.AvaloniaLoginChoiceEager.ApiExtendedFunc, accounts); + await LibraryCommands.ImportAccountAsync(LibationAvalonia.Dialogs.Login.AvaloniaLoginChoiceEager.ApiExtendedFunc, accounts); } catch (OperationCanceledException) { @@ -42,10 +38,8 @@ namespace LibationAvalonia.Views } }; - _viewModel.AutoScanChecked = Configuration.Instance.AutoScan; - // if enabled: begin on load - Opened += startAutoScan; + MainWindow.Loaded += startAutoScan; // if new 'default' account is added, run autoscan AccountsSettingsPersister.Saving += accountsPreSave; @@ -55,6 +49,7 @@ namespace LibationAvalonia.Views Configuration.Instance.PropertyChanged += startAutoScan; } + private List<(string AccountId, string LocaleName)> preSaveDefaultAccounts; private List<(string AccountId, string LocaleName)> getDefaultAccounts() { @@ -65,32 +60,24 @@ namespace LibationAvalonia.Views .Select(a => (a.AccountId, a.Locale.Name)) .ToList(); } + private void accountsPreSave(object sender = null, EventArgs e = null) => preSaveDefaultAccounts = getDefaultAccounts(); + private void accountsPostSave(object sender = null, EventArgs e = null) { - var postSaveDefaultAccounts = getDefaultAccounts(); - var newDefaultAccounts = postSaveDefaultAccounts.Except(preSaveDefaultAccounts).ToList(); - - if (newDefaultAccounts.Any()) + if (getDefaultAccounts().Except(preSaveDefaultAccounts).Any()) startAutoScan(); } [PropertyChangeFilter(nameof(Configuration.AutoScan))] private void startAutoScan(object sender = null, EventArgs e = null) { - _viewModel.AutoScanChecked = Configuration.Instance.AutoScan; - if (_viewModel.AutoScanChecked) + AutoScanChecked = Configuration.Instance.AutoScan; + if (AutoScanChecked) autoScanTimer.PerformNow(); else autoScanTimer.Stop(); } - public void autoScanLibraryToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (sender is MenuItem mi && mi.Icon is CheckBox checkBox) - { - checkBox.IsChecked = !(checkBox.IsChecked ?? false); - } - } } } diff --git a/Source/LibationAvalonia/ViewModels/MainVM.Settings.cs b/Source/LibationAvalonia/ViewModels/MainVM.Settings.cs new file mode 100644 index 00000000..715eabc8 --- /dev/null +++ b/Source/LibationAvalonia/ViewModels/MainVM.Settings.cs @@ -0,0 +1,42 @@ +using Avalonia.Controls; +using LibationFileManager; +using ReactiveUI; +using System; +using System.Threading.Tasks; + +namespace LibationAvalonia.ViewModels +{ + partial class MainVM + { + private bool _menuBarVisible = !Configuration.IsMacOs; + public bool MenuBarVisible { get => _menuBarVisible; set => this.RaiseAndSetIfChanged(ref _menuBarVisible, value); } + private void Configure_Settings() + { + ((NativeMenuItem)NativeMenu.GetMenu(App.Current).Items[0]).Command = ReactiveCommand.Create(ShowAboutAsync); + } + + public Task ShowAboutAsync() => new LibationAvalonia.Dialogs.AboutDialog().ShowDialog(MainWindow); + public Task ShowAccountsAsync() => new LibationAvalonia.Dialogs.AccountsDialog().ShowDialog(MainWindow); + public Task ShowSettingsAsync() => new LibationAvalonia.Dialogs.SettingsDialog().ShowDialog(MainWindow); + public Task ShowTrashBinAsync() => new LibationAvalonia.Dialogs.TrashBinDialog().ShowDialog(MainWindow); + + public void LaunchHangover() + { + try + { + System.Diagnostics.Process.Start("Hangover" + (Configuration.IsWindows ? ".exe" : "")); + } + catch (Exception ex) + { + Serilog.Log.Logger.Error(ex, "Failed to launch Hangover"); + } + } + + public async Task StartWalkthroughAsync() + { + MenuBarVisible = true; + await new Walkthrough(MainWindow).RunAsync(); + MenuBarVisible = !Configuration.IsMacOs; + } + } +} diff --git a/Source/LibationAvalonia/ViewModels/MainVM.VisibleBooks.cs b/Source/LibationAvalonia/ViewModels/MainVM.VisibleBooks.cs new file mode 100644 index 00000000..db6fe17b --- /dev/null +++ b/Source/LibationAvalonia/ViewModels/MainVM.VisibleBooks.cs @@ -0,0 +1,203 @@ +using ApplicationServices; +using System; +using System.Threading.Tasks; +using DataLayer; +using Avalonia.Threading; +using LibationAvalonia.Dialogs; +using ReactiveUI; + +namespace LibationAvalonia.ViewModels +{ + partial class MainVM + { + private int _visibleNotLiberated = 1; + private int _visibleCount = 1; + + /// The Bottom-right visible book count status text + public string VisibleCountText => $"Visible: {_visibleCount}"; + /// The Visible Books menu item header text + public string VisibleCountMenuItemText => menufyText($"Visible Books {_visibleCount}"); + /// Indicates if any of the books visible in the Products Display haven't been liberated + public bool AnyVisibleNotLiberated => _visibleNotLiberated > 0; + /// The "Liberate Visible Books" menu item header text (submenu item of the "Liberate Menu" menu item) + public string LiberateVisibleToolStripText { get; private set; } = "Liberate _Visible Books: 0"; + /// The "Liberate" menu item header text (submenu item of the "Visible Books" menu item) + public string LiberateVisibleToolStripText_2 { get; private set; } = menufyText("Liberate: 0"); + + private void Configure_VisibleBooks() + { + LibraryCommands.BookUserDefinedItemCommitted += setLiberatedVisibleMenuItemAsync; + ProductsDisplay.VisibleCountChanged += ProductsDisplay_VisibleCountChanged; + } + + private void setVisibleCount(int visibleCount) + { + _visibleCount = visibleCount; + this.RaisePropertyChanged(nameof(VisibleCountText)); + this.RaisePropertyChanged(nameof(VisibleCountMenuItemText)); + } + + private void setVisibleNotLiberatedCount(int visibleNotLiberated) + { + _visibleNotLiberated = visibleNotLiberated; + + LiberateVisibleToolStripText + = AnyVisibleNotLiberated + ? "Liberate " + menufyText($"Visible Books: {visibleNotLiberated}") + : "All visible books are liberated"; + + LiberateVisibleToolStripText_2 + = AnyVisibleNotLiberated + ? menufyText($"Liberate: {visibleNotLiberated}") + : "All visible books are liberated"; + + this.RaisePropertyChanged(nameof(AnyVisibleNotLiberated)); + this.RaisePropertyChanged(nameof(LiberateVisibleToolStripText)); + this.RaisePropertyChanged(nameof(LiberateVisibleToolStripText_2)); + } + + public async void ProductsDisplay_VisibleCountChanged(object sender, int qty) + { + setVisibleCount(qty); + await Dispatcher.UIThread.InvokeAsync(setLiberatedVisibleMenuItem); + } + + private async void setLiberatedVisibleMenuItemAsync(object _, object __) + => await Dispatcher.UIThread.InvokeAsync(setLiberatedVisibleMenuItem); + + + public void LiberateVisible() + { + try + { + setQueueCollapseState(false); + + Serilog.Log.Logger.Information("Begin backing up visible library books"); + + ProcessQueue.AddDownloadDecrypt( + ProductsDisplay + .GetVisibleBookEntries() + .UnLiberated() + ); + } + catch (Exception ex) + { + Serilog.Log.Logger.Error(ex, "An error occurred while backing up visible library books"); + } + } + + public async Task ReplaceTagsAsync() + { + var dialog = new TagsBatchDialog(); + var result = await dialog.ShowDialog(MainWindow); + if (result != DialogResult.OK) + return; + + var visibleLibraryBooks = ProductsDisplay.GetVisibleBookEntries(); + + var confirmationResult = await MessageBox.ShowConfirmationDialog( + MainWindow, + visibleLibraryBooks, + // do not use `$` string interpolation. See impl. + "Are you sure you want to replace tags in {0}?", + "Replace tags?"); + + if (confirmationResult != DialogResult.Yes) + return; + + visibleLibraryBooks.UpdateTags(dialog.NewTags); + } + + public async Task SetBookDownloadedAsync() + { + var dialog = new LiberatedStatusBatchManualDialog(); + var result = await dialog.ShowDialog(MainWindow); + if (result != DialogResult.OK) + return; + + var visibleLibraryBooks = ProductsDisplay.GetVisibleBookEntries(); + + var confirmationResult = await MessageBox.ShowConfirmationDialog( + MainWindow, + visibleLibraryBooks, + // do not use `$` string interpolation. See impl. + "Are you sure you want to replace book downloaded status in {0}?", + "Replace downloaded status?"); + + if (confirmationResult != DialogResult.Yes) + return; + + visibleLibraryBooks.UpdateBookStatus(dialog.BookLiberatedStatus); + } + + public async Task SetPdfDownloadedAsync() + { + var dialog = new LiberatedStatusBatchManualDialog(isPdf: true); + var result = await dialog.ShowDialog(MainWindow); + if (result != DialogResult.OK) + return; + + var visibleLibraryBooks = ProductsDisplay.GetVisibleBookEntries(); + + var confirmationResult = await MessageBox.ShowConfirmationDialog( + MainWindow, + visibleLibraryBooks, + // do not use `$` string interpolation. See impl. + "Are you sure you want to replace PDF downloaded status in {0}?", + "Replace downloaded status?"); + + if (confirmationResult != DialogResult.Yes) + return; + + visibleLibraryBooks.UpdatePdfStatus(dialog.BookLiberatedStatus); + } + + public async Task SetDownloadedAutoAsync() + { + var dialog = new LiberatedStatusBatchAutoDialog(); + var result = await dialog.ShowDialog(MainWindow); + if (result != DialogResult.OK) + return; + + var bulkSetStatus = new BulkSetDownloadStatus(ProductsDisplay.GetVisibleBookEntries(), dialog.SetDownloaded, dialog.SetNotDownloaded); + var count = await Task.Run(bulkSetStatus.Discover); + + if (count == 0) + return; + + var confirmationResult = await MessageBox.Show( + bulkSetStatus.AggregateMessage, + "Replace downloaded status?", + MessageBoxButtons.YesNo, + MessageBoxIcon.Question, + MessageBoxDefaultButton.Button1); + + if (confirmationResult != DialogResult.Yes) + return; + + bulkSetStatus.Execute(); + } + + public async Task RemoveVisibleAsync() + { + var visibleLibraryBooks = ProductsDisplay.GetVisibleBookEntries(); + + var confirmationResult = await MessageBox.ShowConfirmationDialog( + MainWindow, + visibleLibraryBooks, + // do not use `$` string interpolation. See impl. + "Are you sure you want to remove {0} from Libation's library?", + "Remove books from Libation?", + MessageBoxDefaultButton.Button2); + + if (confirmationResult is DialogResult.Yes) + await visibleLibraryBooks.RemoveBooksAsync(); + } + + private void setLiberatedVisibleMenuItem() + { + var libraryStats = LibraryCommands.GetCounts(ProductsDisplay.GetVisibleBookEntries()); + setVisibleNotLiberatedCount(libraryStats.PendingBooks); + } + } +} diff --git a/Source/LibationAvalonia/Views/MainWindow.NoUI.cs b/Source/LibationAvalonia/ViewModels/MainVM._NoUI.cs similarity index 92% rename from Source/LibationAvalonia/Views/MainWindow.NoUI.cs rename to Source/LibationAvalonia/ViewModels/MainVM._NoUI.cs index 22542e70..9cfd5dd8 100644 --- a/Source/LibationAvalonia/Views/MainWindow.NoUI.cs +++ b/Source/LibationAvalonia/ViewModels/MainVM._NoUI.cs @@ -2,9 +2,9 @@ using LibationUiBase; using System.IO; -namespace LibationAvalonia.Views +namespace LibationAvalonia.ViewModels { - public partial class MainWindow + partial class MainVM { private void Configure_NonUI() { diff --git a/Source/LibationAvalonia/ViewModels/MainVM.cs b/Source/LibationAvalonia/ViewModels/MainVM.cs new file mode 100644 index 00000000..86c69d9d --- /dev/null +++ b/Source/LibationAvalonia/ViewModels/MainVM.cs @@ -0,0 +1,39 @@ +using ApplicationServices; +using LibationAvalonia.Views; +using LibationFileManager; +using ReactiveUI; + +namespace LibationAvalonia.ViewModels +{ + public partial class MainVM : ViewModelBase + { + public ProcessQueueViewModel ProcessQueue { get; } = new ProcessQueueViewModel(); + public ProductsDisplayViewModel ProductsDisplay { get; } = new ProductsDisplayViewModel(); + + private double? _downloadProgress = null; + public double? DownloadProgress { get => _downloadProgress; set => this.RaiseAndSetIfChanged(ref _downloadProgress, value); } + + + private readonly MainWindow MainWindow; + public MainVM(MainWindow mainWindow) + { + MainWindow = mainWindow; + + ProductsDisplay.RemovableCountChanged += (_, removeCount) => RemoveBooksButtonText = removeCount == 1 ? "Remove 1 Book from Libation" : $"Remove {removeCount} Books from Libation"; + LibraryCommands.LibrarySizeChanged += async (_, _) => await ProductsDisplay.UpdateGridAsync(DbContexts.GetLibrary_Flat_NoTracking(includeParents: true)); + + Configure_NonUI(); + Configure_BackupCounts(); + Configure_Export(); + Configure_Filters(); + Configure_Import(); + Configure_Liberate(); + Configure_ProcessQueue(); + Configure_ScanAuto(); + Configure_Settings(); + Configure_VisibleBooks(); + } + + private static string menufyText(string header) => Configuration.IsMacOs ? header : $"_{header}"; + } +} diff --git a/Source/LibationAvalonia/ViewModels/MainWindowViewModel.cs b/Source/LibationAvalonia/ViewModels/MainWindowViewModel.cs deleted file mode 100644 index 367e0702..00000000 --- a/Source/LibationAvalonia/ViewModels/MainWindowViewModel.cs +++ /dev/null @@ -1,226 +0,0 @@ -using ApplicationServices; -using Avalonia.Collections; -using Avalonia.Controls; -using LibationFileManager; -using ReactiveUI; - -namespace LibationAvalonia.ViewModels -{ - public class MainWindowViewModel : ViewModelBase - { - private string _filterString; - private string _removeBooksButtonText = "Remove # Books from Libation"; - private bool _removeBooksButtonEnabled = true; - private bool _autoScanChecked = true; - private bool _firstFilterIsDefault = true; - private bool _removeButtonsVisible = true; - private int _numAccountsScanning = 2; - private int _accountsCount = 0; - private bool _queueOpen = true; - private int _visibleCount = 1; - private LibraryCommands.LibraryStats _libraryStats; - private int _visibleNotLiberated = 1; - public bool IsMp3Supported => Configuration.IsLinux || Configuration.IsWindows; - - /// The Process Queue's viewmodel - public ProcessQueueViewModel ProcessQueue { get; } = new ProcessQueueViewModel(); - public ProductsDisplayViewModel ProductsDisplay { get; } = new ProductsDisplayViewModel(); - - private double? _downloadProgress = null; - public double? DownloadProgress { get => _downloadProgress; set => this.RaiseAndSetIfChanged(ref _downloadProgress, value); } - - - /// Library filterting query - public string FilterString { get => _filterString; set => this.RaiseAndSetIfChanged(ref _filterString, value); } - - - /// Display text for the "Remove # Books from Libation" button - public string RemoveBooksButtonText { get => _removeBooksButtonText; set => this.RaiseAndSetIfChanged(ref _removeBooksButtonText, value); } - - - /// Indicates if the "Remove # Books from Libation" button is enabled - public bool RemoveBooksButtonEnabled { get => _removeBooksButtonEnabled; set { this.RaiseAndSetIfChanged(ref _removeBooksButtonEnabled, value); } } - - - /// Auto scanning accounts is enables - public bool AutoScanChecked - { - get => _autoScanChecked; - set - { - if (value != _autoScanChecked) - Configuration.Instance.AutoScan = value; - this.RaiseAndSetIfChanged(ref _autoScanChecked, value); - } - } - - public AvaloniaList QuickFilterMenuItems { get; } = new(); - - /// Indicates if the first quick filter is the default filter - public bool FirstFilterIsDefault - { - get => _firstFilterIsDefault; - set - { - if (value != _firstFilterIsDefault) - QuickFilters.UseDefault = value; - this.RaiseAndSetIfChanged(ref _firstFilterIsDefault, value); - } - } - - - /// Indicates if the "Remove # Books from Libation" and "Done Removing" buttons should be visible - public bool RemoveButtonsVisible - { - get => _removeButtonsVisible; - set - { - this.RaiseAndSetIfChanged(ref _removeButtonsVisible, value); - this.RaisePropertyChanged(nameof(RemoveMenuItemsEnabled)); - } - } - - - - - /// The number of accounts currently being scanned - public int NumAccountsScanning - { - get => _numAccountsScanning; - set - { - this.RaiseAndSetIfChanged(ref _numAccountsScanning, value); - this.RaisePropertyChanged(nameof(ActivelyScanning)); - this.RaisePropertyChanged(nameof(RemoveMenuItemsEnabled)); - this.RaisePropertyChanged(nameof(ScanningText)); - } - } - - /// Indicates if Libation is currently scanning account(s) - public bool ActivelyScanning => _numAccountsScanning > 0; - /// Indicates if the "Remove Books" menu items are enabled - public bool RemoveMenuItemsEnabled => !RemoveButtonsVisible && !ActivelyScanning; - /// The library scanning status text - public string ScanningText => _numAccountsScanning == 1 ? "Scanning..." : $"Scanning {_numAccountsScanning} accounts..."; - - - - /// The number of accounts added to Libation - public int AccountsCount - { - get => _accountsCount; - set - { - this.RaiseAndSetIfChanged(ref _accountsCount, value); - this.RaisePropertyChanged(nameof(ZeroAccounts)); - this.RaisePropertyChanged(nameof(AnyAccounts)); - this.RaisePropertyChanged(nameof(OneAccount)); - this.RaisePropertyChanged(nameof(MultipleAccounts)); - } - } - - /// There are no Audible accounts - public bool ZeroAccounts => _accountsCount == 0; - /// There is at least one Audible account - public bool AnyAccounts => _accountsCount > 0; - /// There is exactly one Audible account - public bool OneAccount => _accountsCount == 1; - /// There are more than 1 Audible accounts - public bool MultipleAccounts => _accountsCount > 1; - - - - /// The Process Queue panel is open - public bool QueueOpen - { - get => _queueOpen; - set - { - this.RaiseAndSetIfChanged(ref _queueOpen, value); - QueueButtonAngle = value ? 180 : 0; - this.RaisePropertyChanged(nameof(QueueButtonAngle)); - } - } - public double QueueButtonAngle { get; private set; } - - - /// The number of books visible in the Product Display - public int VisibleCount - { - get => _visibleCount; - set - { - this.RaiseAndSetIfChanged(ref _visibleCount, value); - this.RaisePropertyChanged(nameof(VisibleCountText)); - this.RaisePropertyChanged(nameof(VisibleCountMenuItemText)); - } - } - - /// The Bottom-right visible book count status text - public string VisibleCountText => $"Visible: {VisibleCount}"; - /// The Visible Books menu item header text - public string VisibleCountMenuItemText => $"_Visible Books {VisibleCount}"; - - - - /// The user's library statistics - public LibraryCommands.LibraryStats LibraryStats - { - get => _libraryStats; - set - { - this.RaiseAndSetIfChanged(ref _libraryStats, value); - - BookBackupsToolStripText - = LibraryStats.HasPendingBooks - ? $"Begin _Book and PDF Backups: {LibraryStats.PendingBooks} remaining" - : "All books have been liberated"; - - PdfBackupsToolStripText - = LibraryStats.pdfsNotDownloaded > 0 - ? $"Begin _PDF Only Backups: {LibraryStats.pdfsNotDownloaded} remaining" - : "All PDFs have been downloaded"; - - this.RaisePropertyChanged(nameof(BookBackupsToolStripText)); - this.RaisePropertyChanged(nameof(PdfBackupsToolStripText)); - } - } - - /// The "Begin Book and PDF Backup" menu item header text - public string BookBackupsToolStripText { get; private set; } = "Begin _Book and PDF Backups: 0"; - /// The "Begin PDF Only Backup" menu item header text - public string PdfBackupsToolStripText { get; private set; } = "Begin _PDF Only Backups: 0"; - - - /// The number of books visible in the Products Display that have not yet been liberated - public int VisibleNotLiberated - { - get => _visibleNotLiberated; - set - { - this.RaiseAndSetIfChanged(ref _visibleNotLiberated, value); - - LiberateVisibleToolStripText - = AnyVisibleNotLiberated - ? $"Liberate _Visible Books: {VisibleNotLiberated}" - : "All visible books are liberated"; - - LiberateVisibleToolStripText_2 - = AnyVisibleNotLiberated - ? $"_Liberate: {VisibleNotLiberated}" - : "All visible books are liberated"; - - this.RaisePropertyChanged(nameof(AnyVisibleNotLiberated)); - this.RaisePropertyChanged(nameof(LiberateVisibleToolStripText)); - this.RaisePropertyChanged(nameof(LiberateVisibleToolStripText_2)); - } - } - - /// Indicates if any of the books visible in the Products Display haven't been liberated - public bool AnyVisibleNotLiberated => VisibleNotLiberated > 0; - /// The "Liberate Visible Books" menu item header text (submenu item of the "Liberate Menu" menu item) - public string LiberateVisibleToolStripText { get; private set; } = "Liberate _Visible Books: 0"; - /// The "Liberate" menu item header text (submenu item of the "Visible Books" menu item) - public string LiberateVisibleToolStripText_2 { get; private set; } = "_Liberate: 0"; - } -} diff --git a/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs b/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs index 481f1b7b..127a3b00 100644 --- a/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs +++ b/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs @@ -117,7 +117,7 @@ namespace LibationAvalonia.ViewModels } //Create the filtered-in list before adding entries to avoid a refresh - FilteredInGridEntries = QueryResults(geList, FilterString); + FilteredInGridEntries = QueryResults(geList.Union(geList.OfType().SelectMany(s => s.Children)), FilterString); SOURCE.AddRange(geList.OrderByDescending(e => e.DateAdded)); //Add all children beneath their parent diff --git a/Source/LibationAvalonia/ViewModels/RowComparer.cs b/Source/LibationAvalonia/ViewModels/RowComparer.cs index b20dc5de..743a610f 100644 --- a/Source/LibationAvalonia/ViewModels/RowComparer.cs +++ b/Source/LibationAvalonia/ViewModels/RowComparer.cs @@ -70,9 +70,9 @@ namespace LibationAvalonia.ViewModels return InternalCompare(parentA, geB); } - //both are children of the same series, always present in order of series index, ascending + //both are children of the same series if (parentA == parentB) - return geA.SeriesIndex.CompareTo(geB.SeriesIndex) * (sortDirection is ListSortDirection.Ascending ? 1 : -1); + return InternalCompare(geA, geB); //a and b are children of different series. return InternalCompare(parentA, parentB); diff --git a/Source/LibationAvalonia/Views/MainWindow.BackupCounts.cs b/Source/LibationAvalonia/Views/MainWindow.BackupCounts.cs deleted file mode 100644 index 762175e9..00000000 --- a/Source/LibationAvalonia/Views/MainWindow.BackupCounts.cs +++ /dev/null @@ -1,23 +0,0 @@ -using ApplicationServices; -using Avalonia.Threading; -using System.Threading.Tasks; - -namespace LibationAvalonia.Views -{ - public partial class MainWindow - { - private Task updateCountsTask; - private void Configure_BackupCounts() - { - Load += setBackupCounts; - LibraryCommands.LibrarySizeChanged += setBackupCounts; - LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts; - } - - private void setBackupCounts(object _, object __) - { - if (updateCountsTask?.IsCompleted is not false) - updateCountsTask = Dispatcher.UIThread.InvokeAsync(() => _viewModel.LibraryStats = LibraryCommands.GetCounts()); - } - } -} diff --git a/Source/LibationAvalonia/Views/MainWindow.Filter.cs b/Source/LibationAvalonia/Views/MainWindow.Filter.cs deleted file mode 100644 index 9874b586..00000000 --- a/Source/LibationAvalonia/Views/MainWindow.Filter.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Avalonia.Input; -using System; -using System.Threading.Tasks; - -namespace LibationAvalonia.Views -{ - public partial class MainWindow - { - protected void Configure_Filter() { } - - public async void filterHelpBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) - => await (new LibationAvalonia.Dialogs.SearchSyntaxDialog()).ShowDialog(this); - - public async void filterSearchTb_KeyPress(object sender, KeyEventArgs e) - { - if (e.Key == Key.Return) - { - await performFilter(_viewModel.FilterString); - - // silence the 'ding' - e.Handled = true; - } - } - - public async void filterBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) - => await performFilter(_viewModel.FilterString); - - private string lastGoodFilter = ""; - private async Task performFilter(string filterString) - { - _viewModel.FilterString = filterString; - - try - { - await _viewModel.ProductsDisplay.Filter(filterString); - lastGoodFilter = filterString; - } - catch (Exception ex) - { - await MessageBox.Show($"Bad filter string:\r\n\r\n{ex.Message}", "Bad filter string", MessageBoxButtons.OK, MessageBoxIcon.Error); - - // re-apply last good filter - await performFilter(lastGoodFilter); - } - } - } -} diff --git a/Source/LibationAvalonia/Views/MainWindow.Liberate.cs b/Source/LibationAvalonia/Views/MainWindow.Liberate.cs deleted file mode 100644 index c79ba1e9..00000000 --- a/Source/LibationAvalonia/Views/MainWindow.Liberate.cs +++ /dev/null @@ -1,59 +0,0 @@ -using DataLayer; -using System; -using System.Linq; -using System.Threading.Tasks; - -namespace LibationAvalonia.Views -{ - public partial class MainWindow - { - private void Configure_Liberate() { } - - //GetLibrary_Flat_NoTracking() may take a long time on a hugh library. so run in new thread - public void beginBookBackupsToolStripMenuItem_Click(object _ = null, Avalonia.Interactivity.RoutedEventArgs __ = null) - { - try - { - SetQueueCollapseState(false); - - Serilog.Log.Logger.Information("Begin backing up all library books"); - - _viewModel.ProcessQueue.AddDownloadDecrypt( - ApplicationServices.DbContexts - .GetLibrary_Flat_NoTracking() - .UnLiberated() - ); - } - catch (Exception ex) - { - Serilog.Log.Logger.Error(ex, "An error occurred while backing up all library books"); - } - } - - public async void beginPdfBackupsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args) - { - SetQueueCollapseState(false); - await Task.Run(() => _viewModel.ProcessQueue.AddDownloadPdf(ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking() - .Where(lb => lb.Book.UserDefinedItem.PdfStatus is DataLayer.LiberatedStatus.NotLiberated))); - } - - public async void convertAllM4bToMp3ToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args) - { - var result = await MessageBox.Show( - "This converts all m4b titles in your library to mp3 files. Original files are not deleted." - + "\r\nFor large libraries this will take a long time and will take up more disk space." - + "\r\n\r\nContinue?" - + "\r\n\r\n(To always download titles as mp3 instead of m4b, go to Settings: Download my books as .MP3 files)", - "Convert all M4b => Mp3?", - MessageBoxButtons.YesNo, - MessageBoxIcon.Warning); - if (result == DialogResult.Yes) - { - SetQueueCollapseState(false); - await Task.Run(() => _viewModel.ProcessQueue.AddConvertMp3(ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking() - .Where(lb => lb.Book.UserDefinedItem.BookStatus is DataLayer.LiberatedStatus.Liberated && lb.Book.ContentType is DataLayer.ContentType.Product))); - } - //Only Queue Liberated books for conversion. This isn't a perfect filter, but it's better than nothing. - } - } -} diff --git a/Source/LibationAvalonia/Views/MainWindow.QuickFilters.cs b/Source/LibationAvalonia/Views/MainWindow.QuickFilters.cs deleted file mode 100644 index 74220033..00000000 --- a/Source/LibationAvalonia/Views/MainWindow.QuickFilters.cs +++ /dev/null @@ -1,90 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using LibationFileManager; -using System.Linq; -using Avalonia.Data; - -namespace LibationAvalonia.Views -{ - public partial class MainWindow - { - private void Configure_QuickFilters() - { - _viewModel.FirstFilterIsDefault = QuickFilters.UseDefault; - Load += updateFiltersMenu; - QuickFilters.Updated += updateFiltersMenu; - - //We need to be able to dynamically add and remove menu items from the Quick Filters menu. - //To do that, we need quick filter's menu items source to be writable, which we can only - //achieve by creating the list ourselves (instead of allowing Avalonia to create it from the xaml) - - var startWithFilterMenuItem = new MenuItem - { - Header = "Start Libation with 1st filter _Default", - Icon = new CheckBox - { - BorderThickness = new Thickness(0), - IsHitTestVisible = false, - [!CheckBox.IsCheckedProperty] = new Binding(nameof(_viewModel.FirstFilterIsDefault)) - } - }; - - var editFiltersMenuItem = new MenuItem { Header = "_Edit quick filters..." }; - - startWithFilterMenuItem.Click += firstFilterIsDefaultToolStripMenuItem_Click; - editFiltersMenuItem.Click += editQuickFiltersToolStripMenuItem_Click; - - _viewModel.QuickFilterMenuItems.Add(startWithFilterMenuItem); - _viewModel.QuickFilterMenuItems.Add(editFiltersMenuItem); - _viewModel.QuickFilterMenuItems.Add(new Separator()); - } - - private async void QuickFiltersMenuItem_KeyDown(object sender, Avalonia.Input.KeyEventArgs e) - { - int keyNum = (int)e.Key - 34; - - if (keyNum <=9 && keyNum >= 1) - { - var menuItem = _viewModel.QuickFilterMenuItems - .OfType() - .FirstOrDefault(i => i.Header is string h && h.StartsWith($"_{keyNum}")); - - if (menuItem is not null) - { - await performFilter(menuItem.Tag as string); - e.Handled = true; - } - } - } - - private void updateFiltersMenu(object _ = null, object __ = null) - { - //Clear all filters - _viewModel.QuickFilterMenuItems.RemoveAll(_viewModel.QuickFilterMenuItems.Where(i => i.Tag is string).ToList()); - - // re-populate - var index = 0; - foreach (var filter in QuickFilters.Filters) - { - var quickFilterMenuItem = new MenuItem - { - Tag = filter, - Header = $"_{++index}: {filter}" - }; - quickFilterMenuItem.Click += async (_, __) => await performFilter(filter); - _viewModel.QuickFilterMenuItems.Add(quickFilterMenuItem); - } - } - - private void firstFilterIsDefaultToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) - { - _viewModel.FirstFilterIsDefault = !_viewModel.FirstFilterIsDefault; - } - - private void addQuickFilterBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) - => QuickFilters.Add(_viewModel.FilterString); - - private async void editQuickFiltersToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) - => await new Dialogs.EditQuickFilters().ShowDialog(this); - } -} diff --git a/Source/LibationAvalonia/Views/MainWindow.RemoveBooks.cs b/Source/LibationAvalonia/Views/MainWindow.RemoveBooks.cs deleted file mode 100644 index 785bddcb..00000000 --- a/Source/LibationAvalonia/Views/MainWindow.RemoveBooks.cs +++ /dev/null @@ -1,100 +0,0 @@ -using AudibleUtilities; -using LibationAvalonia.Dialogs; -using System.Linq; - -namespace LibationAvalonia.Views -{ - public partial class MainWindow - { - private void Configure_RemoveBooks() - { - if (Avalonia.Controls.Design.IsDesignMode) - return; - - _viewModel.RemoveButtonsVisible = false; - } - - public async void openTrashBinToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) - { - var trash = new TrashBinDialog(); - await trash.ShowDialog(this); - } - - public void removeLibraryBooksToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) - { - // if 0 accounts, this will not be visible - // if 1 account, run scanLibrariesRemovedBooks() on this account - // if multiple accounts, another menu set will open. do not run scanLibrariesRemovedBooks() - using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); - var accounts = persister.AccountsSettings.GetAll(); - - if (accounts.Count != 1) - return; - - var firstAccount = accounts.Single(); - scanLibrariesRemovedBooks(firstAccount); - } - - // selectively remove books from all accounts - public void removeAllAccountsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) - { - using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); - var allAccounts = persister.AccountsSettings.GetAll(); - scanLibrariesRemovedBooks(allAccounts.ToArray()); - } - - // selectively remove books from some accounts - public async void removeSomeAccountsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) - { - var scanAccountsDialog = new Dialogs.ScanAccountsDialog(); - - if (await scanAccountsDialog.ShowDialog(this) != DialogResult.OK) - return; - - if (!scanAccountsDialog.CheckedAccounts.Any()) - return; - - scanLibrariesRemovedBooks(scanAccountsDialog.CheckedAccounts.ToArray()); - } - - private async void scanLibrariesRemovedBooks(params Account[] accounts) - { - //This action is meant to operate on the entire library. - //For removing books within a filter set, use - //Visible Books > Remove from library - - await _viewModel.ProductsDisplay.Filter(null); - - _viewModel.RemoveBooksButtonEnabled = true; - _viewModel.RemoveButtonsVisible = true; - - await _viewModel.ProductsDisplay.ScanAndRemoveBooksAsync(accounts); - } - - public async void removeBooksBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) - { - _viewModel.RemoveBooksButtonEnabled = false; - await _viewModel.ProductsDisplay.RemoveCheckedBooksAsync(); - _viewModel.RemoveBooksButtonEnabled = true; - } - - public async void doneRemovingBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) - { - _viewModel.RemoveButtonsVisible = false; - - _viewModel.ProductsDisplay.DoneRemovingBooks(); - - //Restore the filter - await performFilter(lastGoodFilter); - } - - public void ProductsDisplay_RemovableCountChanged(object sender, int removeCount) - { - _viewModel.RemoveBooksButtonText = removeCount switch - { - 1 => "Remove 1 Book from Libation", - _ => $"Remove {removeCount} Books from Libation" - }; - } - } -} diff --git a/Source/LibationAvalonia/Views/MainWindow.ScanManual.cs b/Source/LibationAvalonia/Views/MainWindow.ScanManual.cs deleted file mode 100644 index 4375c7c3..00000000 --- a/Source/LibationAvalonia/Views/MainWindow.ScanManual.cs +++ /dev/null @@ -1,90 +0,0 @@ -using ApplicationServices; -using AudibleUtilities; -using LibationAvalonia.Dialogs; -using LibationFileManager; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace LibationAvalonia.Views -{ - public partial class MainWindow - { - private void Configure_ScanManual() - { - Load += refreshImportMenu; - AccountsSettingsPersister.Saved += refreshImportMenu; - } - - private void refreshImportMenu(object _, EventArgs __) - { - using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); - _viewModel.AccountsCount = persister.AccountsSettings.Accounts.Count; - } - - public async void noAccountsYetAddAccountToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) - { - await MessageBox.Show("To load your Audible library, come back here to the Import menu after adding your account"); - await new Dialogs.AccountsDialog().ShowDialog(this); - } - - public async void scanLibraryToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) - { - using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); - var firstAccount = persister.AccountsSettings.GetAll().FirstOrDefault(); - await scanLibrariesAsync(firstAccount); - } - - public async void scanLibraryOfAllAccountsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) - { - using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); - var allAccounts = persister.AccountsSettings.GetAll(); - await scanLibrariesAsync(allAccounts); - } - - public async void scanLibraryOfSomeAccountsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) - { - var scanAccountsDialog = new Dialogs.ScanAccountsDialog(); - - if (await scanAccountsDialog.ShowDialog(this) != DialogResult.OK) - return; - - if (!scanAccountsDialog.CheckedAccounts.Any()) - return; - - await scanLibrariesAsync(scanAccountsDialog.CheckedAccounts); - } - - private async Task scanLibrariesAsync(IEnumerable accounts) => await scanLibrariesAsync(accounts.ToArray()); - private async Task scanLibrariesAsync(params Account[] accounts) - { - try - { - var (totalProcessed, newAdded) = await LibraryCommands.ImportAccountAsync(Dialogs.Login.AvaloniaLoginChoiceEager.ApiExtendedFunc, accounts); - - // this is here instead of ScanEnd so that the following is only possible when it's user-initiated, not automatic loop - if (Configuration.Instance.ShowImportedStats && newAdded > 0) - await MessageBox.Show($"Total processed: {totalProcessed}\r\nNew: {newAdded}"); - } - catch (OperationCanceledException) - { - Serilog.Log.Information("Audible login attempt cancelled by user"); - } - catch (Exception ex) - { - await MessageBox.ShowAdminAlert( - this, - "Error importing library. Please try again. If this still happens after 2 or 3 tries, stop and contact administrator", - "Error importing library", - ex); - } - } - - private async void locateAudiobooksToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) - { - var locateDialog = new LocateAudiobooksDialog(); - await locateDialog.ShowDialog(this); - } - } -} diff --git a/Source/LibationAvalonia/Views/MainWindow.ScanNotification.cs b/Source/LibationAvalonia/Views/MainWindow.ScanNotification.cs deleted file mode 100644 index 1c5272a2..00000000 --- a/Source/LibationAvalonia/Views/MainWindow.ScanNotification.cs +++ /dev/null @@ -1,24 +0,0 @@ -using ApplicationServices; -using System; - -namespace LibationAvalonia.Views -{ - public partial class MainWindow - { - private void Configure_ScanNotification() - { - _viewModel.NumAccountsScanning = 0; - LibraryCommands.ScanBegin += LibraryCommands_ScanBegin; - LibraryCommands.ScanEnd += LibraryCommands_ScanEnd; - } - private void LibraryCommands_ScanBegin(object sender, int accountsLength) - { - _viewModel.NumAccountsScanning = accountsLength; - } - - private void LibraryCommands_ScanEnd(object sender, int newCount) - { - _viewModel.NumAccountsScanning = 0; - } - } -} diff --git a/Source/LibationAvalonia/Views/MainWindow.Settings.cs b/Source/LibationAvalonia/Views/MainWindow.Settings.cs deleted file mode 100644 index 0989855d..00000000 --- a/Source/LibationAvalonia/Views/MainWindow.Settings.cs +++ /dev/null @@ -1,34 +0,0 @@ -using LibationFileManager; -using System; - -namespace LibationAvalonia.Views -{ - public partial class MainWindow - { - private void Configure_Settings() { } - - public async void accountsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) - => await new Dialogs.AccountsDialog().ShowDialog(this); - - public async void basicSettingsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) - => await new Dialogs.SettingsDialog().ShowDialog(this); - - public async void aboutToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) - => await MessageBox.Show($"Libation {AppScaffolding.LibationScaffolding.Variety}{Environment.NewLine}Version {AppScaffolding.LibationScaffolding.BuildVersion}", $"Libation v{AppScaffolding.LibationScaffolding.BuildVersion}"); - - public async void tourToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) - => await new Walkthrough(this).RunAsync(); - - public void launchHangoverToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) - { - try - { - System.Diagnostics.Process.Start("Hangover" + (Configuration.IsWindows ? ".exe" : "")); - } - catch (Exception ex) - { - Serilog.Log.Logger.Error(ex, "Failed to launch Hangover"); - } - } - } -} diff --git a/Source/LibationAvalonia/Views/MainWindow.Upgrade.cs b/Source/LibationAvalonia/Views/MainWindow.Upgrade.cs deleted file mode 100644 index ade9bfbc..00000000 --- a/Source/LibationAvalonia/Views/MainWindow.Upgrade.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Avalonia.Threading; -using LibationAvalonia.Dialogs; -using LibationUiBase; -using System.Threading.Tasks; - -namespace LibationAvalonia.Views -{ - public partial class MainWindow - { - private void Configure_Upgrade() - { - setProgressVisible(false); -#if !DEBUG - async Task upgradeAvailable(UpgradeEventArgs e) - { - var notificationResult = await new UpgradeNotificationDialog(e.UpgradeProperties, e.CapUpgrade).ShowDialogAsync(this); - - e.Ignore = notificationResult == DialogResult.Ignore; - e.InstallUpgrade = notificationResult == DialogResult.OK; - } - - var upgrader = new Upgrader(); - upgrader.DownloadProgress += async (_, e) => await Dispatcher.UIThread.InvokeAsync(() => _viewModel.DownloadProgress = e.ProgressPercentage); - upgrader.DownloadBegin += async (_, _) => await Dispatcher.UIThread.InvokeAsync(() => setProgressVisible(true)); - upgrader.DownloadCompleted += async (_, _) => await Dispatcher.UIThread.InvokeAsync(() => setProgressVisible(false)); - - Opened += async (_, _) => await upgrader.CheckForUpgradeAsync(upgradeAvailable); -#endif - } - - private void setProgressVisible(bool visible) => _viewModel.DownloadProgress = visible ? 0 : null; - - } -} diff --git a/Source/LibationAvalonia/Views/MainWindow.VisibleBooks.cs b/Source/LibationAvalonia/Views/MainWindow.VisibleBooks.cs deleted file mode 100644 index 097d26a5..00000000 --- a/Source/LibationAvalonia/Views/MainWindow.VisibleBooks.cs +++ /dev/null @@ -1,158 +0,0 @@ -using ApplicationServices; -using Avalonia.Threading; -using DataLayer; -using System; -using System.Threading.Tasks; - -namespace LibationAvalonia.Views -{ - public partial class MainWindow - { - private void Configure_VisibleBooks() - { - LibraryCommands.BookUserDefinedItemCommitted += setLiberatedVisibleMenuItemAsync; - } - - private async void setLiberatedVisibleMenuItemAsync(object _, object __) - => await Dispatcher.UIThread.InvokeAsync(setLiberatedVisibleMenuItem); - - public void liberateVisible(object sender, Avalonia.Interactivity.RoutedEventArgs args) - { - try - { - SetQueueCollapseState(false); - - Serilog.Log.Logger.Information("Begin backing up visible library books"); - - _viewModel.ProcessQueue.AddDownloadDecrypt( - _viewModel - .ProductsDisplay - .GetVisibleBookEntries() - .UnLiberated() - ); - } - catch (Exception ex) - { - Serilog.Log.Logger.Error(ex, "An error occurred while backing up visible library books"); - } - } - public async void replaceTagsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args) - { - var dialog = new Dialogs.TagsBatchDialog(); - var result = await dialog.ShowDialog(this); - if (result != DialogResult.OK) - return; - - var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries(); - - var confirmationResult = await MessageBox.ShowConfirmationDialog( - this, - visibleLibraryBooks, - // do not use `$` string interpolation. See impl. - "Are you sure you want to replace tags in {0}?", - "Replace tags?"); - - if (confirmationResult != DialogResult.Yes) - return; - - visibleLibraryBooks.UpdateTags(dialog.NewTags); - } - - public async void setBookDownloadedManualToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args) - { - var dialog = new Dialogs.LiberatedStatusBatchManualDialog(); - var result = await dialog.ShowDialog(this); - if (result != DialogResult.OK) - return; - - var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries(); - - var confirmationResult = await MessageBox.ShowConfirmationDialog( - this, - visibleLibraryBooks, - // do not use `$` string interpolation. See impl. - "Are you sure you want to replace book downloaded status in {0}?", - "Replace downloaded status?"); - - if (confirmationResult != DialogResult.Yes) - return; - - visibleLibraryBooks.UpdateBookStatus(dialog.BookLiberatedStatus); - } - - public async void setPdfDownloadedManualToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args) - { - var dialog = new Dialogs.LiberatedStatusBatchManualDialog(isPdf: true); - var result = await dialog.ShowDialog(this); - if (result != DialogResult.OK) - return; - - var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries(); - - var confirmationResult = await MessageBox.ShowConfirmationDialog( - this, - visibleLibraryBooks, - // do not use `$` string interpolation. See impl. - "Are you sure you want to replace PDF downloaded status in {0}?", - "Replace downloaded status?"); - - if (confirmationResult != DialogResult.Yes) - return; - - visibleLibraryBooks.UpdatePdfStatus(dialog.BookLiberatedStatus); - } - - public async void setDownloadedAutoToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args) - { - var dialog = new Dialogs.LiberatedStatusBatchAutoDialog(); - var result = await dialog.ShowDialog(this); - if (result != DialogResult.OK) - return; - - var bulkSetStatus = new BulkSetDownloadStatus(_viewModel.ProductsDisplay.GetVisibleBookEntries(), dialog.SetDownloaded, dialog.SetNotDownloaded); - var count = await Task.Run(bulkSetStatus.Discover); - - if (count == 0) - return; - - var confirmationResult = await MessageBox.Show( - bulkSetStatus.AggregateMessage, - "Replace downloaded status?", - MessageBoxButtons.YesNo, - MessageBoxIcon.Question, - MessageBoxDefaultButton.Button1); - - if (confirmationResult != DialogResult.Yes) - return; - - bulkSetStatus.Execute(); - } - - public async void removeToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args) - { - var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries(); - - var confirmationResult = await MessageBox.ShowConfirmationDialog( - this, - visibleLibraryBooks, - // do not use `$` string interpolation. See impl. - "Are you sure you want to remove {0} from Libation's library?", - "Remove books from Libation?", - MessageBoxDefaultButton.Button2); - - if (confirmationResult is DialogResult.Yes) - await visibleLibraryBooks.RemoveBooksAsync(); - } - public async void ProductsDisplay_VisibleCountChanged(object sender, int qty) - { - _viewModel.VisibleCount = qty; - - await Dispatcher.UIThread.InvokeAsync(setLiberatedVisibleMenuItem); - } - void setLiberatedVisibleMenuItem() - { - var libraryStats = LibraryCommands.GetCounts(_viewModel.ProductsDisplay.GetVisibleBookEntries()); - _viewModel.VisibleNotLiberated = libraryStats.PendingBooks; - } - } -} diff --git a/Source/LibationAvalonia/Views/MainWindow.axaml b/Source/LibationAvalonia/Views/MainWindow.axaml index 13380bbb..6cb02cb5 100644 --- a/Source/LibationAvalonia/Views/MainWindow.axaml +++ b/Source/LibationAvalonia/Views/MainWindow.axaml @@ -9,15 +9,77 @@ mc:Ignorable="d" d:DesignWidth="1850" d:DesignHeight="700" x:Class="LibationAvalonia.Views.MainWindow" Title="Libation: Liberate your Library" + x:DataType="vm:MainVM" Name="Form1" Icon="/Assets/libation.ico"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -35,26 +97,27 @@ - + - + - + - - - - + + + + + - - - + + + - + @@ -67,10 +130,10 @@ - - - - + + + + @@ -82,12 +145,12 @@ - + - + - - - - - - + + + + + + @@ -122,19 +185,19 @@ - - + + - - + + - - + + - + - + @@ -155,23 +218,23 @@ - @@ -179,17 +242,17 @@ - + - + @@ -203,10 +266,10 @@ - - - - + + + + diff --git a/Source/LibationAvalonia/Views/MainWindow.axaml.cs b/Source/LibationAvalonia/Views/MainWindow.axaml.cs index 20dfe906..96e0edb6 100644 --- a/Source/LibationAvalonia/Views/MainWindow.axaml.cs +++ b/Source/LibationAvalonia/Views/MainWindow.axaml.cs @@ -1,58 +1,41 @@ -using ApplicationServices; +using Avalonia.Input; using Avalonia.ReactiveUI; using DataLayer; using LibationAvalonia.ViewModels; using LibationFileManager; +using LibationUiBase.GridView; +using ReactiveUI; using System; using System.Collections.Generic; using System.Linq; namespace LibationAvalonia.Views { - public partial class MainWindow : ReactiveWindow + public partial class MainWindow : ReactiveWindow { - public event EventHandler Load; public event EventHandler> LibraryLoaded; - private readonly MainWindowViewModel _viewModel; - public MainWindow() { - this.DataContext = _viewModel = new MainWindowViewModel(); + DataContext = new MainVM(this); InitializeComponent(); - - // eg: if one of these init'd productsGrid, then another can't reliably subscribe to it - Configure_BackupCounts(); - Configure_ScanAuto(); - Configure_ScanNotification(); - Configure_VisibleBooks(); - Configure_QuickFilters(); - Configure_ScanManual(); - Configure_RemoveBooks(); - Configure_Liberate(); - Configure_Export(); - Configure_Settings(); - Configure_ProcessQueue(); Configure_Upgrade(); - Configure_Filter(); - // misc which belongs in winforms app but doesn't have a UI element - Configure_NonUI(); - _viewModel.ProductsDisplay.RemovableCountChanged += ProductsDisplay_RemovableCountChanged; - _viewModel.ProductsDisplay.VisibleCountChanged += ProductsDisplay_VisibleCountChanged; - - { - this.LibraryLoaded += MainWindow_LibraryLoaded; - - LibraryCommands.LibrarySizeChanged += async (_, _) => await _viewModel.ProductsDisplay.UpdateGridAsync(DbContexts.GetLibrary_Flat_NoTracking(includeParents: true)); - Closing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance); - } + Loaded += MainWindow_Loaded; Closing += MainWindow_Closing; + LibraryLoaded += MainWindow_LibraryLoaded; - Opened += MainWindow_Opened; + KeyBindings.Add(new KeyBinding { Command = ReactiveCommand.Create(selectAndFocusSearchBox), Gesture = new KeyGesture(Key.F, Configuration.IsMacOs ? KeyModifiers.Meta : KeyModifiers.Control) }); + + if (!Configuration.IsMacOs) + { + KeyBindings.Add(new KeyBinding { Command = ReactiveCommand.Create(ViewModel.ShowSettingsAsync), Gesture = new KeyGesture(Key.P, KeyModifiers.Control) }); + KeyBindings.Add(new KeyBinding { Command = ReactiveCommand.Create(ViewModel.ShowAccountsAsync), Gesture = new KeyGesture(Key.A, KeyModifiers.Control | KeyModifiers.Shift) }); + KeyBindings.Add(new KeyBinding { Command = ReactiveCommand.Create(ViewModel.ExportLibraryAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Control) }); + } } - private async void MainWindow_Opened(object sender, EventArgs e) + private async void MainWindow_Loaded(object sender, EventArgs e) { if (Configuration.Instance.FirstLaunch) { @@ -66,21 +49,64 @@ namespace LibationAvalonia.Views Configuration.Instance.FirstLaunch = false; } } - + private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e) { productsDisplay?.CloseImageDisplay(); + this.SaveSizeAndLocation(Configuration.Instance); } private async void MainWindow_LibraryLoaded(object sender, List dbBooks) { if (QuickFilters.UseDefault) - await performFilter(QuickFilters.Filters.FirstOrDefault()); + await ViewModel.PerformFilter(QuickFilters.Filters.FirstOrDefault()); - _viewModel.ProductsDisplay.BindToGrid(dbBooks); + ViewModel.ProductsDisplay.BindToGrid(dbBooks); + } + + private void selectAndFocusSearchBox() + { + filterSearchTb.SelectAll(); + filterSearchTb.Focus(); } - public void OnLoad() => Load?.Invoke(this, EventArgs.Empty); public void OnLibraryLoaded(List initialLibrary) => LibraryLoaded?.Invoke(this, initialLibrary); + public void ProductsDisplay_LiberateClicked(object _, LibraryBook libraryBook) => ViewModel.LiberateClicked(libraryBook); + public void ProductsDisplay_LiberateSeriesClicked(object _, ISeriesEntry series) => ViewModel.LiberateSeriesClicked(series); + public void ProductsDisplay_ConvertToMp3Clicked(object _, LibraryBook libraryBook) => ViewModel.ConvertToMp3Clicked(libraryBook); + + public async void filterSearchTb_KeyPress(object _, KeyEventArgs e) + { + if (e.Key == Key.Return) + { + await ViewModel.PerformFilter(ViewModel.FilterString); + + // silence the 'ding' + e.Handled = true; + } + } + + private void Configure_Upgrade() + { + setProgressVisible(false); +#if !DEBUG + async System.Threading.Tasks.Task upgradeAvailable(LibationUiBase.UpgradeEventArgs e) + { + var notificationResult = await new Dialogs.UpgradeNotificationDialog(e.UpgradeProperties, e.CapUpgrade).ShowDialogAsync(this); + + e.Ignore = notificationResult == DialogResult.Ignore; + e.InstallUpgrade = notificationResult == DialogResult.OK; + } + + var upgrader = new LibationUiBase.Upgrader(); + upgrader.DownloadProgress += async (_, e) => await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => ViewModel.DownloadProgress = e.ProgressPercentage); + upgrader.DownloadBegin += async (_, _) => await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => setProgressVisible(true)); + upgrader.DownloadCompleted += async (_, _) => await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => setProgressVisible(false)); + + Opened += async (_, _) => await upgrader.CheckForUpgradeAsync(upgradeAvailable); +#endif + } + + private void setProgressVisible(bool visible) => ViewModel.DownloadProgress = visible ? 0 : null; } } diff --git a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml index 87b50442..555576dc 100644 --- a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml +++ b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml @@ -11,9 +11,6 @@ - - - diff --git a/Source/LibationAvalonia/Walkthrough.cs b/Source/LibationAvalonia/Walkthrough.cs index b7bd49d1..8cee6272 100644 --- a/Source/LibationAvalonia/Walkthrough.cs +++ b/Source/LibationAvalonia/Walkthrough.cs @@ -149,7 +149,7 @@ namespace LibationAvalonia await displayControlAsync(MainForm.importToolStripMenuItem); await displayControlAsync(scanItem); - scanItem.RaiseEvent(new RoutedEventArgs(MenuItem.ClickEvent)); + scanItem.Command.Execute(null); MainForm.importToolStripMenuItem.Close(); var tcs = new TaskCompletionSource(); @@ -193,7 +193,7 @@ namespace LibationAvalonia await displayControlAsync(MainForm.filterBtn); - MainForm.filterBtn.RaiseEvent(new RoutedEventArgs(Button.ClickEvent)); + MainForm.filterBtn.Command.Execute(null); await Task.Delay(1000); @@ -222,7 +222,7 @@ namespace LibationAvalonia await Task.Delay(750); await displayControlAsync(MainForm.addQuickFilterBtn); - MainForm.addQuickFilterBtn.RaiseEvent(new RoutedEventArgs(Button.ClickEvent)); + MainForm.addQuickFilterBtn.Command.Execute(null); await displayControlAsync(MainForm.quickFiltersToolStripMenuItem); await displayControlAsync(editQuickFiltersToolStripMenuItem); diff --git a/Source/LibationWinForms/Dialogs/AboutDialog.Designer.cs b/Source/LibationWinForms/Dialogs/AboutDialog.Designer.cs new file mode 100644 index 00000000..39aeb4f4 --- /dev/null +++ b/Source/LibationWinForms/Dialogs/AboutDialog.Designer.cs @@ -0,0 +1,329 @@ +namespace LibationWinForms.Dialogs +{ + partial class AboutDialog + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + pictureBox1 = new System.Windows.Forms.PictureBox(); + releaseNotesLbl = new System.Windows.Forms.LinkLabel(); + checkForUpgradeBtn = new System.Windows.Forms.Button(); + getLibationLbl = new System.Windows.Forms.LinkLabel(); + rmcrackanLbl = new System.Windows.Forms.LinkLabel(); + MBucariLbl = new System.Windows.Forms.LinkLabel(); + groupBox1 = new System.Windows.Forms.GroupBox(); + label3 = new System.Windows.Forms.Label(); + label4 = new System.Windows.Forms.Label(); + label2 = new System.Windows.Forms.Label(); + label1 = new System.Windows.Forms.Label(); + flowLayoutPanel1 = new System.Windows.Forms.FlowLayoutPanel(); + linkLabel4 = new System.Windows.Forms.LinkLabel(); + linkLabel2 = new System.Windows.Forms.LinkLabel(); + linkLabel3 = new System.Windows.Forms.LinkLabel(); + linkLabel1 = new System.Windows.Forms.LinkLabel(); + linkLabel5 = new System.Windows.Forms.LinkLabel(); + linkLabel6 = new System.Windows.Forms.LinkLabel(); + ((System.ComponentModel.ISupportInitialize)pictureBox1).BeginInit(); + groupBox1.SuspendLayout(); + flowLayoutPanel1.SuspendLayout(); + SuspendLayout(); + // + // pictureBox1 + // + pictureBox1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + pictureBox1.Image = Properties.Resources.cheers; + pictureBox1.Location = new System.Drawing.Point(12, 91); + pictureBox1.Name = "pictureBox1"; + pictureBox1.Size = new System.Drawing.Size(410, 210); + pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom; + pictureBox1.TabIndex = 0; + pictureBox1.TabStop = false; + // + // releaseNotesLbl + // + releaseNotesLbl.AutoSize = true; + releaseNotesLbl.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + releaseNotesLbl.Location = new System.Drawing.Point(12, 12); + releaseNotesLbl.Name = "releaseNotesLbl"; + releaseNotesLbl.Size = new System.Drawing.Size(171, 20); + releaseNotesLbl.TabIndex = 2; + releaseNotesLbl.TabStop = true; + releaseNotesLbl.Text = "Libation Classic v11.0.0.0"; + // + // checkForUpgradeBtn + // + checkForUpgradeBtn.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + checkForUpgradeBtn.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + checkForUpgradeBtn.Location = new System.Drawing.Point(12, 54); + checkForUpgradeBtn.Name = "checkForUpgradeBtn"; + checkForUpgradeBtn.Size = new System.Drawing.Size(410, 31); + checkForUpgradeBtn.TabIndex = 3; + checkForUpgradeBtn.Text = "Check for Upgrade"; + checkForUpgradeBtn.UseVisualStyleBackColor = true; + // + // getLibationLbl + // + getLibationLbl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right; + getLibationLbl.AutoSize = true; + getLibationLbl.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + getLibationLbl.Location = new System.Drawing.Point(245, 12); + getLibationLbl.Name = "getLibationLbl"; + getLibationLbl.Size = new System.Drawing.Size(162, 20); + getLibationLbl.TabIndex = 7; + getLibationLbl.TabStop = true; + getLibationLbl.Text = "https://getlibation.com"; + getLibationLbl.LinkClicked += getLibationLbl_LinkClicked; + // + // rmcrackanLbl + // + rmcrackanLbl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + rmcrackanLbl.AutoSize = true; + rmcrackanLbl.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point); + rmcrackanLbl.Location = new System.Drawing.Point(6, 19); + rmcrackanLbl.Name = "rmcrackanLbl"; + rmcrackanLbl.Padding = new System.Windows.Forms.Padding(0, 3, 0, 3); + rmcrackanLbl.Size = new System.Drawing.Size(80, 25); + rmcrackanLbl.TabIndex = 8; + rmcrackanLbl.TabStop = true; + rmcrackanLbl.Text = "rmcrackan"; + rmcrackanLbl.LinkClicked += Link_GithubUser; + // + // MBucariLbl + // + MBucariLbl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + MBucariLbl.AutoSize = true; + MBucariLbl.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point); + MBucariLbl.Location = new System.Drawing.Point(6, 40); + MBucariLbl.Name = "MBucariLbl"; + MBucariLbl.Padding = new System.Windows.Forms.Padding(0, 3, 0, 3); + MBucariLbl.Size = new System.Drawing.Size(64, 25); + MBucariLbl.TabIndex = 9; + MBucariLbl.TabStop = true; + MBucariLbl.Text = "Mbucari"; + MBucariLbl.LinkClicked += Link_GithubUser; + // + // groupBox1 + // + groupBox1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + groupBox1.Controls.Add(label3); + groupBox1.Controls.Add(label4); + groupBox1.Controls.Add(label2); + groupBox1.Controls.Add(label1); + groupBox1.Controls.Add(flowLayoutPanel1); + groupBox1.Controls.Add(MBucariLbl); + groupBox1.Controls.Add(rmcrackanLbl); + groupBox1.Location = new System.Drawing.Point(12, 307); + groupBox1.Name = "groupBox1"; + groupBox1.Size = new System.Drawing.Size(410, 172); + groupBox1.TabIndex = 10; + groupBox1.TabStop = false; + groupBox1.Text = "Acknowledgements"; + // + // label3 + // + label3.AutoSize = true; + label3.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + label3.Location = new System.Drawing.Point(92, 43); + label3.Name = "label3"; + label3.Padding = new System.Windows.Forms.Padding(0, 0, 0, 3); + label3.Size = new System.Drawing.Size(71, 22); + label3.TabIndex = 12; + label3.Text = "Developer"; + // + // label4 + // + label4.AutoSize = true; + label4.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + label4.Location = new System.Drawing.Point(92, 22); + label4.Name = "label4"; + label4.Padding = new System.Windows.Forms.Padding(0, 0, 0, 3); + label4.Size = new System.Drawing.Size(55, 22); + label4.TabIndex = 12; + label4.Text = "Creator"; + // + // label2 + // + label2.AutoSize = true; + label2.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + label2.Location = new System.Drawing.Point(92, 22); + label2.Name = "label2"; + label2.Padding = new System.Windows.Forms.Padding(0, 0, 0, 3); + label2.Size = new System.Drawing.Size(45, 22); + label2.TabIndex = 12; + label2.Text = "label2"; + // + // label1 + // + label1.AutoSize = true; + label1.Location = new System.Drawing.Point(6, 82); + label1.Name = "label1"; + label1.Size = new System.Drawing.Size(157, 15); + label1.TabIndex = 11; + label1.Text = "Additional Contributions by:"; + // + // flowLayoutPanel1 + // + flowLayoutPanel1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + flowLayoutPanel1.Controls.Add(linkLabel4); + flowLayoutPanel1.Controls.Add(linkLabel2); + flowLayoutPanel1.Controls.Add(linkLabel3); + flowLayoutPanel1.Controls.Add(linkLabel1); + flowLayoutPanel1.Controls.Add(linkLabel5); + flowLayoutPanel1.Controls.Add(linkLabel6); + flowLayoutPanel1.Location = new System.Drawing.Point(6, 100); + flowLayoutPanel1.Name = "flowLayoutPanel1"; + flowLayoutPanel1.Size = new System.Drawing.Size(398, 66); + flowLayoutPanel1.TabIndex = 10; + // + // linkLabel4 + // + linkLabel4.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + linkLabel4.AutoSize = true; + linkLabel4.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + linkLabel4.Location = new System.Drawing.Point(3, 0); + linkLabel4.Name = "linkLabel4"; + linkLabel4.Padding = new System.Windows.Forms.Padding(0, 3, 0, 3); + linkLabel4.Size = new System.Drawing.Size(41, 21); + linkLabel4.TabIndex = 9; + linkLabel4.TabStop = true; + linkLabel4.Text = "pixil98"; + linkLabel4.LinkClicked += Link_GithubUser; + // + // linkLabel2 + // + linkLabel2.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + linkLabel2.AutoSize = true; + linkLabel2.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + linkLabel2.Location = new System.Drawing.Point(50, 0); + linkLabel2.Name = "linkLabel2"; + linkLabel2.Padding = new System.Windows.Forms.Padding(0, 3, 0, 3); + linkLabel2.Size = new System.Drawing.Size(104, 21); + linkLabel2.TabIndex = 9; + linkLabel2.TabStop = true; + linkLabel2.Text = "hutattedonmyarm"; + linkLabel2.LinkClicked += Link_GithubUser; + // + // linkLabel3 + // + linkLabel3.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + linkLabel3.AutoSize = true; + linkLabel3.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + linkLabel3.Location = new System.Drawing.Point(160, 0); + linkLabel3.Name = "linkLabel3"; + linkLabel3.Padding = new System.Windows.Forms.Padding(0, 3, 0, 3); + linkLabel3.Size = new System.Drawing.Size(43, 21); + linkLabel3.TabIndex = 9; + linkLabel3.TabStop = true; + linkLabel3.Text = "seanke"; + linkLabel3.LinkClicked += Link_GithubUser; + // + // linkLabel1 + // + linkLabel1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + linkLabel1.AutoSize = true; + linkLabel1.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + linkLabel1.Location = new System.Drawing.Point(209, 0); + linkLabel1.Name = "linkLabel1"; + linkLabel1.Padding = new System.Windows.Forms.Padding(0, 3, 0, 3); + linkLabel1.Size = new System.Drawing.Size(66, 21); + linkLabel1.TabIndex = 9; + linkLabel1.TabStop = true; + linkLabel1.Text = "wtanksleyjr"; + linkLabel1.LinkClicked += Link_GithubUser; + // + // linkLabel5 + // + linkLabel5.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + linkLabel5.AutoSize = true; + linkLabel5.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + linkLabel5.Location = new System.Drawing.Point(281, 0); + linkLabel5.Name = "linkLabel5"; + linkLabel5.Padding = new System.Windows.Forms.Padding(0, 3, 0, 3); + linkLabel5.Size = new System.Drawing.Size(51, 21); + linkLabel5.TabIndex = 9; + linkLabel5.TabStop = true; + linkLabel5.Text = "Dr.Blank"; + linkLabel5.LinkClicked += Link_GithubUser; + // + // linkLabel6 + // + linkLabel6.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + linkLabel6.AutoSize = true; + linkLabel6.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + linkLabel6.Location = new System.Drawing.Point(3, 21); + linkLabel6.Name = "linkLabel6"; + linkLabel6.Padding = new System.Windows.Forms.Padding(0, 3, 0, 3); + linkLabel6.Size = new System.Drawing.Size(77, 21); + linkLabel6.TabIndex = 9; + linkLabel6.TabStop = true; + linkLabel6.Text = "CharlieRussel"; + linkLabel6.LinkClicked += Link_GithubUser; + // + // AboutDialog + // + AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + ClientSize = new System.Drawing.Size(434, 491); + Controls.Add(groupBox1); + Controls.Add(getLibationLbl); + Controls.Add(checkForUpgradeBtn); + Controls.Add(releaseNotesLbl); + Controls.Add(pictureBox1); + MinimumSize = new System.Drawing.Size(445, 530); + Name = "AboutDialog"; + StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + Text = "About Libation"; + ((System.ComponentModel.ISupportInitialize)pictureBox1).EndInit(); + groupBox1.ResumeLayout(false); + groupBox1.PerformLayout(); + flowLayoutPanel1.ResumeLayout(false); + flowLayoutPanel1.PerformLayout(); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private System.Windows.Forms.PictureBox pictureBox1; + private System.Windows.Forms.LinkLabel releaseNotesLbl; + private System.Windows.Forms.Button checkForUpgradeBtn; + private System.Windows.Forms.LinkLabel getLibationLbl; + private System.Windows.Forms.LinkLabel rmcrackanLbl; + private System.Windows.Forms.LinkLabel MBucariLbl; + private System.Windows.Forms.GroupBox groupBox1; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.FlowLayoutPanel flowLayoutPanel1; + private System.Windows.Forms.LinkLabel linkLabel1; + private System.Windows.Forms.LinkLabel linkLabel4; + private System.Windows.Forms.LinkLabel linkLabel2; + private System.Windows.Forms.LinkLabel linkLabel3; + private System.Windows.Forms.LinkLabel linkLabel5; + private System.Windows.Forms.LinkLabel linkLabel6; + private System.Windows.Forms.Label label3; + private System.Windows.Forms.Label label4; + private System.Windows.Forms.Label label2; + } +} \ No newline at end of file diff --git a/Source/LibationWinForms/Dialogs/AboutDialog.cs b/Source/LibationWinForms/Dialogs/AboutDialog.cs new file mode 100644 index 00000000..85ba4746 --- /dev/null +++ b/Source/LibationWinForms/Dialogs/AboutDialog.cs @@ -0,0 +1,62 @@ +using LibationUiBase; +using System; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace LibationWinForms.Dialogs +{ + public partial class AboutDialog : Form + { + public AboutDialog() + { + InitializeComponent(); + this.SetLibationIcon(); + releaseNotesLbl.Text = $"Libation {AppScaffolding.LibationScaffolding.Variety} v{AppScaffolding.LibationScaffolding.BuildVersion}"; + + var toolTip = new ToolTip(); + toolTip.SetToolTip(releaseNotesLbl, "View Release Notes"); + } + + private void releaseNotesLbl_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) + => Dinah.Core.Go.To.Url($"{AppScaffolding.LibationScaffolding.RepositoryUrl}/releases/tag/v{AppScaffolding.LibationScaffolding.BuildVersion.ToString(3)}"); + + private async void checkForUpgradeBtn_Click(object sender, EventArgs e) + { + var form1 = Owner as Form1; + var upgrader = new Upgrader(); + upgrader.DownloadBegin += (_, _) => form1.Invoke(() => form1.upgradeLbl.Visible = form1.upgradePb.Visible = true); + upgrader.DownloadProgress += (_, e) => form1.Invoke(() => form1.upgradePb.Value = int.Max(0, int.Min(100, (int)(e.ProgressPercentage ?? 0)))); + upgrader.DownloadCompleted += (_, _) => form1.Invoke(() => form1.upgradeLbl.Visible = form1.upgradePb.Visible = false); + + checkForUpgradeBtn.Enabled = false; + Version latestVersion = null; + await upgrader.CheckForUpgradeAsync(OnUpgradeAvailable); + + checkForUpgradeBtn.Enabled = latestVersion is null; + + checkForUpgradeBtn.Text = latestVersion is null ? "Libation is up to date. Check Again." : $"Version {latestVersion:3} is available"; + + Task OnUpgradeAvailable(UpgradeEventArgs e) + { + var notificationResult = new UpgradeNotificationDialog(e.UpgradeProperties).ShowDialog(this); + + e.Ignore = notificationResult == DialogResult.Ignore; + e.InstallUpgrade = notificationResult == DialogResult.Yes; + latestVersion = e.UpgradeProperties.LatestRelease; + + return Task.CompletedTask; + } + } + + private void getLibationLbl_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) + => Dinah.Core.Go.To.Url(AppScaffolding.LibationScaffolding.WebsiteUrl); + + private void Link_GithubUser(object sender, LinkLabelLinkClickedEventArgs e) + { + if (sender is LinkLabel lbl) + { + Dinah.Core.Go.To.Url($"ht" + $"tps://github.com/{lbl.Text.Replace('.', '-')}"); + } + } + } +} diff --git a/Source/LibationWinForms/Dialogs/AboutDialog.resx b/Source/LibationWinForms/Dialogs/AboutDialog.resx new file mode 100644 index 00000000..f298a7be --- /dev/null +++ b/Source/LibationWinForms/Dialogs/AboutDialog.resx @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Source/LibationWinForms/Form1.Designer.cs b/Source/LibationWinForms/Form1.Designer.cs index 7546ae76..bf6eb017 100644 --- a/Source/LibationWinForms/Form1.Designer.cs +++ b/Source/LibationWinForms/Form1.Designer.cs @@ -697,7 +697,7 @@ private System.Windows.Forms.Button removeBooksBtn; private System.Windows.Forms.Button doneRemovingBtn; private System.Windows.Forms.ToolStripMenuItem setPdfDownloadedManualToolStripMenuItem; - private System.Windows.Forms.ToolStripProgressBar upgradePb; - private System.Windows.Forms.ToolStripStatusLabel upgradeLbl; + public System.Windows.Forms.ToolStripProgressBar upgradePb; + public System.Windows.Forms.ToolStripStatusLabel upgradeLbl; } } diff --git a/Source/LibationWinForms/Form1.Settings.cs b/Source/LibationWinForms/Form1.Settings.cs index ce8b61c5..07ce09c0 100644 --- a/Source/LibationWinForms/Form1.Settings.cs +++ b/Source/LibationWinForms/Form1.Settings.cs @@ -12,9 +12,7 @@ namespace LibationWinForms private void basicSettingsToolStripMenuItem_Click(object sender, EventArgs e) => new SettingsDialog().ShowDialog(); - private void aboutToolStripMenuItem_Click(object sender, EventArgs e) - => MessageBox.Show($"Libation {AppScaffolding.LibationScaffolding.Variety}{Environment.NewLine}Version {AppScaffolding.LibationScaffolding.BuildVersion}", $"Libation v{AppScaffolding.LibationScaffolding.BuildVersion}"); - + private void aboutToolStripMenuItem_Click(object sender, EventArgs e) => new AboutDialog().ShowDialog(this); private async void tourToolStripMenuItem_Click(object sender, EventArgs e) => await new Walkthrough(this).RunAsync(); diff --git a/Source/LibationWinForms/GridView/GridEntryBindingList.cs b/Source/LibationWinForms/GridView/GridEntryBindingList.cs index 9203ad53..b5b10c5e 100644 --- a/Source/LibationWinForms/GridView/GridEntryBindingList.cs +++ b/Source/LibationWinForms/GridView/GridEntryBindingList.cs @@ -201,8 +201,8 @@ namespace LibationWinForms.GridView { var pIndex = itemsList.IndexOf(parent); - //children should always be sorted by series index. - foreach (var c in children.Where(c => c.Parent == parent).OrderBy(c => c.SeriesIndex)) + //children are sorted beneath their series parent + foreach (var c in children.Where(c => c.Parent == parent).OrderBy(c => c, Comparer)) itemsList.Insert(++pIndex, c); } } diff --git a/Source/LibationWinForms/Properties/Resources.Designer.cs b/Source/LibationWinForms/Properties/Resources.Designer.cs index 280a5d69..a86128bc 100644 --- a/Source/LibationWinForms/Properties/Resources.Designer.cs +++ b/Source/LibationWinForms/Properties/Resources.Designer.cs @@ -60,6 +60,16 @@ namespace LibationWinForms.Properties { } } + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap cheers { + get { + object obj = ResourceManager.GetObject("cheers", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// diff --git a/Source/LibationWinForms/Properties/Resources.resx b/Source/LibationWinForms/Properties/Resources.resx index a78be5b3..d970d5e6 100644 --- a/Source/LibationWinForms/Properties/Resources.resx +++ b/Source/LibationWinForms/Properties/Resources.resx @@ -118,6 +118,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\Resources\cheers.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Resources\img-coverart-prod-unavailable_300x300.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a diff --git a/Source/LibationWinForms/Resources/cheers.png b/Source/LibationWinForms/Resources/cheers.png new file mode 100644 index 00000000..2a1930f4 Binary files /dev/null and b/Source/LibationWinForms/Resources/cheers.png differ