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
@@ -82,12 +145,12 @@
-
+
-
-
+
-
+
@@ -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