Merge pull request #565 from Mbucari/master

About Dialog, mac menus, and hotkeys
This commit is contained in:
rmcrackan 2023-04-03 15:54:40 -04:00 committed by GitHub
commit 7ada0082a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 1714 additions and 1093 deletions

View File

@ -1,7 +1,8 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:LibationAvalonia"
x:Class="LibationAvalonia.App">
x:Class="LibationAvalonia.App"
Name="Libation">
<Application.DataTemplates>
<local:ViewLocator/>
@ -69,4 +70,11 @@
</Style>
</Style>
</Application.Styles>
<NativeMenu.Menu>
<NativeMenu>
<NativeMenuItem Header="About Libation" />
</NativeMenu>
</NativeMenu.Menu>
</Application>

View File

@ -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<IAccessKeyHandler>().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();
}

View File

@ -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
</StreamGeometry>
<StreamGeometry x:Key="LibationCheersIcon">
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
</StreamGeometry>
</ResourceDictionary>
</Styles.Resources>
</Styles>

View File

@ -27,7 +27,7 @@
<DataTemplate>
<TextBlock
Text="{Binding, Converter={StaticResource KnownDirectoryConverter}}" />
Text="{Binding Converter={StaticResource KnownDirectoryConverter}}" />
</DataTemplate>
</ComboBox.ItemTemplate>

View File

@ -6,9 +6,6 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="LibationAvalonia.Controls.GroupBox">
<Design.DataContext>
</Design.DataContext>
<ContentControl.Styles>
<Style Selector="controls|GroupBox Border">
<Setter Property="BorderBrush" Value="DarkGray" />
@ -16,7 +13,7 @@
<Style Selector="controls|GroupBox">
<Setter Property="Template">
<ControlTemplate>
<Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="7,10,*,Auto">
<Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="8,12,*,Auto">
<Grid
ZIndex="1"

View File

@ -0,0 +1,73 @@
<Window xmlns="https://github.com/avaloniaui"
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="520"
MinWidth="400" MinHeight="520"
Width="400" Height="520"
x:Class="LibationAvalonia.Dialogs.AboutDialog"
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
Title="About Libation"
Icon="/Assets/libation.ico">
<Grid Margin="10" ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto,*">
<controls:LinkLabel Grid.ColumnSpan="2" FontSize="16" FontWeight="Bold" Text="{Binding Version}" ToolTip.Tip="View Release Notes" Tapped="ViewReleaseNotes_Tapped" />
<controls:LinkLabel Grid.Column="1" FontSize="14" VerticalAlignment="Center" HorizontalAlignment="Right" Text="https://getlibation.com" Tapped="Link_getlibation"/>
<Button Grid.Row="1" Grid.ColumnSpan="2" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" Margin="0,20,0,0" IsEnabled="{Binding CanCheckForUpgrade}" Content="{Binding UpgradeButtonText}" Click="CheckForUpgrade_Click" />
<Canvas Grid.Row="2" Grid.ColumnSpan="2" Margin="0,30,0,20" Width="280" Height="220">
<Path Stretch="None" Fill="{DynamicResource IconFill}" Data="{DynamicResource LibationCheersIcon}">
<Path.RenderTransform>
<TransformGroup>
<RotateTransform Angle="12" />
<ScaleTransform ScaleX="0.4" ScaleY="0.4" />
<TranslateTransform X="-160" Y="-150" />
</TransformGroup>
</Path.RenderTransform>
</Path>
<Path Stretch="None" Fill="{DynamicResource IconFill}" Data="{DynamicResource LibationCheersIcon}">
<Path.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="-1" ScaleY="1" />
<RotateTransform Angle="-12" />
<ScaleTransform ScaleX="0.4" ScaleY="0.4" />
<TranslateTransform X="23" Y="-150" />
</TransformGroup>
</Path.RenderTransform>
</Path>
</Canvas>
<controls:GroupBox Grid.Row="3" BorderWidth="2" Label="Acknowledgements" Grid.ColumnSpan="2">
<StackPanel>
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto">
<controls:LinkLabel FontWeight="Bold" Text="rmcrackan" Tapped="Link_GithubUser" />
<TextBlock Grid.Column="1" Margin="10,0" Text="Creator" />
<controls:LinkLabel Grid.Row="1" FontWeight="Bold" Text="Mbucari" Tapped="Link_GithubUser" />
<TextBlock Grid.Row="1" Grid.Column="1" Margin="10,0" Text="Developer" />
</Grid>
<TextBlock Margin="0,10" FontSize="12" Text="Additional Contributions by:" TextDecorations="Underline"/>
<WrapPanel>
<WrapPanel.Styles>
<Style Selector="TextBlock">
<Setter Property="Margin" Value="5,0" />
<Setter Property="FontSize" Value="13" />
</Style>
</WrapPanel.Styles>
<controls:LinkLabel Text="pixil98" Tapped="Link_GithubUser" />
<controls:LinkLabel Text="hutattedonmyarm" Tapped="Link_GithubUser" />
<controls:LinkLabel Text="seanke" Tapped="Link_GithubUser" />
<controls:LinkLabel Text="wtanksleyjr" Tapped="Link_GithubUser" />
<controls:LinkLabel Text="Dr.Blank" Tapped="Link_GithubUser" />
<controls:LinkLabel Text="CharlieRussel" Tapped="Link_GithubUser" />
</WrapPanel>
</StackPanel>
</controls:GroupBox>
</Grid>
</Window>

View File

@ -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}";
}
}
}

View File

@ -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">
<Grid RowDefinitions="Auto,Auto,Auto">
<Grid Margin="10" RowDefinitions="Auto,Auto,Auto">
<StackPanel
Grid.Row="0"
@ -45,7 +45,6 @@
<Button
Grid.Row="2"
Padding="30,0,30,0"
Margin="10,0,10,10"
HorizontalAlignment="Right"
Height="25"
Content="Save"

View File

@ -40,7 +40,7 @@
<Button Grid.Column="1" IsVisible="{Binding HasButton2}" MinWidth="75" MinHeight="28" Name="Button2" Click="Button2_Click" Margin="5">
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Text="{Binding Button2Text}"/>
</Button>
<Button Grid.Column="2" IsVisible="{Binding HasButton3}" MinWidth="75" MinHeight="28" Name="Button3" Click="Button3_Click" Content="Cancel" Margin="5">
<Button Grid.Column="2" IsVisible="{Binding HasButton3}" MinWidth="75" MinHeight="28" Name="Button3" Click="Button3_Click" Margin="5">
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Text="{Binding Button3Text}"/>
</Button>
</StackPanel>

View File

@ -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

View File

@ -51,8 +51,8 @@
<Compile Update="Views\LiberateStatusButton.axaml.cs">
<DependentUpon>LiberateStatusButton.axaml</DependentUpon>
</Compile>
<Compile Update="Views\MainWindow.*.cs">
<DependentUpon>MainWindow.axaml</DependentUpon>
<Compile Update="ViewModels\MainVM.*.cs">
<DependentUpon>MainVM.cs</DependentUpon>
</Compile>
</ItemGroup>

View File

@ -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);
}
}
}

View File

@ -0,0 +1,58 @@
using ApplicationServices;
using Avalonia.Threading;
using ReactiveUI;
using System.Threading.Tasks;
namespace LibationAvalonia.ViewModels
{
partial class MainVM
{
private Task<LibraryCommands.LibraryStats> updateCountsTask;
private LibraryCommands.LibraryStats _libraryStats;
/// <summary> The "Begin Book and PDF Backup" menu item header text </summary>
public string BookBackupsToolStripText { get; private set; } = "Begin Book and PDF Backups: 0";
/// <summary> The "Begin PDF Only Backup" menu item header text </summary>
public string PdfBackupsToolStripText { get; private set; } = "Begin PDF Only Backups: 0";
/// <summary> The user's library statistics </summary>
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);
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
/// <summary> Library filterting query </summary>
public string FilterString { get => _filterString; set => this.RaiseAndSetIfChanged(ref _filterString, value); }
public AvaloniaList<Control> QuickFilterMenuItems { get; } = new();
/// <summary> Indicates if the first quick filter is the default filter </summary>
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);
}
}
}
}

View File

@ -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;
/// <summary> Auto scanning accounts is enables </summary>
public bool AutoScanChecked { get => _autoScanChecked; set => Configuration.Instance.AutoScan = this.RaiseAndSetIfChanged(ref _autoScanChecked, value); }
/// <summary> Display text for the "Remove # Books from Libation" button </summary>
public string RemoveBooksButtonText { get => _removeBooksButtonText; set => this.RaiseAndSetIfChanged(ref _removeBooksButtonText, value); }
/// <summary> Indicates if the "Remove # Books from Libation" button is enabled </summary>
public bool RemoveBooksButtonEnabled { get => _removeBooksButtonEnabled; set { this.RaiseAndSetIfChanged(ref _removeBooksButtonEnabled, value); } }
/// <summary> Indicates if the "Remove # Books from Libation" and "Done Removing" buttons should be visible </summary>
public bool RemoveButtonsVisible
{
get => _removeButtonsVisible;
set
{
this.RaiseAndSetIfChanged(ref _removeButtonsVisible, value);
this.RaisePropertyChanged(nameof(RemoveMenuItemsEnabled));
}
}
/// <summary> Indicates if Libation is currently scanning account(s) </summary>
public bool ActivelyScanning => _numAccountsScanning > 0;
/// <summary> Indicates if the "Remove Books" menu items are enabled</summary>
public bool RemoveMenuItemsEnabled => !RemoveButtonsVisible && !ActivelyScanning;
/// <summary> The library scanning status text </summary>
public string ScanningText => _numAccountsScanning == 1 ? "Scanning..." : $"Scanning {_numAccountsScanning} accounts...";
/// <summary> There is at least one Audible account </summary>
public bool AnyAccounts => AccountsCount > 0;
/// <summary> There is exactly one Audible account </summary>
public bool OneAccount => AccountsCount == 1;
/// <summary> There are more than 1 Audible accounts </summary>
public bool MultipleAccounts => AccountsCount > 1;
/// <summary> The number of accounts added to Libation </summary>
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<DialogResult>(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<DialogResult>(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) });
}
}
}

View File

@ -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));
}
}
}

View File

@ -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;
/// <summary> The Process Queue panel is open </summary>
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);
}
}

View File

@ -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);
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
/// <summary> The Bottom-right visible book count status text </summary>
public string VisibleCountText => $"Visible: {_visibleCount}";
/// <summary> The Visible Books menu item header text </summary>
public string VisibleCountMenuItemText => menufyText($"Visible Books {_visibleCount}");
/// <summary> Indicates if any of the books visible in the Products Display haven't been liberated </summary>
public bool AnyVisibleNotLiberated => _visibleNotLiberated > 0;
/// <summary> The "Liberate Visible Books" menu item header text (submenu item of the "Liberate Menu" menu item) </summary>
public string LiberateVisibleToolStripText { get; private set; } = "Liberate _Visible Books: 0";
/// <summary> The "Liberate" menu item header text (submenu item of the "Visible Books" menu item) </summary>
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<DialogResult>(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<DialogResult>(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<DialogResult>(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<DialogResult>(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);
}
}
}

View File

@ -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()
{

View File

@ -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}";
}
}

View File

@ -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;
/// <summary> The Process Queue's viewmodel </summary>
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); }
/// <summary> Library filterting query </summary>
public string FilterString { get => _filterString; set => this.RaiseAndSetIfChanged(ref _filterString, value); }
/// <summary> Display text for the "Remove # Books from Libation" button </summary>
public string RemoveBooksButtonText { get => _removeBooksButtonText; set => this.RaiseAndSetIfChanged(ref _removeBooksButtonText, value); }
/// <summary> Indicates if the "Remove # Books from Libation" button is enabled </summary>
public bool RemoveBooksButtonEnabled { get => _removeBooksButtonEnabled; set { this.RaiseAndSetIfChanged(ref _removeBooksButtonEnabled, value); } }
/// <summary> Auto scanning accounts is enables </summary>
public bool AutoScanChecked
{
get => _autoScanChecked;
set
{
if (value != _autoScanChecked)
Configuration.Instance.AutoScan = value;
this.RaiseAndSetIfChanged(ref _autoScanChecked, value);
}
}
public AvaloniaList<Control> QuickFilterMenuItems { get; } = new();
/// <summary> Indicates if the first quick filter is the default filter </summary>
public bool FirstFilterIsDefault
{
get => _firstFilterIsDefault;
set
{
if (value != _firstFilterIsDefault)
QuickFilters.UseDefault = value;
this.RaiseAndSetIfChanged(ref _firstFilterIsDefault, value);
}
}
/// <summary> Indicates if the "Remove # Books from Libation" and "Done Removing" buttons should be visible </summary>
public bool RemoveButtonsVisible
{
get => _removeButtonsVisible;
set
{
this.RaiseAndSetIfChanged(ref _removeButtonsVisible, value);
this.RaisePropertyChanged(nameof(RemoveMenuItemsEnabled));
}
}
/// <summary> The number of accounts currently being scanned </summary>
public int NumAccountsScanning
{
get => _numAccountsScanning;
set
{
this.RaiseAndSetIfChanged(ref _numAccountsScanning, value);
this.RaisePropertyChanged(nameof(ActivelyScanning));
this.RaisePropertyChanged(nameof(RemoveMenuItemsEnabled));
this.RaisePropertyChanged(nameof(ScanningText));
}
}
/// <summary> Indicates if Libation is currently scanning account(s) </summary>
public bool ActivelyScanning => _numAccountsScanning > 0;
/// <summary> Indicates if the "Remove Books" menu items are enabled</summary>
public bool RemoveMenuItemsEnabled => !RemoveButtonsVisible && !ActivelyScanning;
/// <summary> The library scanning status text </summary>
public string ScanningText => _numAccountsScanning == 1 ? "Scanning..." : $"Scanning {_numAccountsScanning} accounts...";
/// <summary> The number of accounts added to Libation </summary>
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));
}
}
/// <summary> There are no Audible accounts </summary>
public bool ZeroAccounts => _accountsCount == 0;
/// <summary> There is at least one Audible account </summary>
public bool AnyAccounts => _accountsCount > 0;
/// <summary> There is exactly one Audible account </summary>
public bool OneAccount => _accountsCount == 1;
/// <summary> There are more than 1 Audible accounts </summary>
public bool MultipleAccounts => _accountsCount > 1;
/// <summary> The Process Queue panel is open </summary>
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; }
/// <summary> The number of books visible in the Product Display </summary>
public int VisibleCount
{
get => _visibleCount;
set
{
this.RaiseAndSetIfChanged(ref _visibleCount, value);
this.RaisePropertyChanged(nameof(VisibleCountText));
this.RaisePropertyChanged(nameof(VisibleCountMenuItemText));
}
}
/// <summary> The Bottom-right visible book count status text </summary>
public string VisibleCountText => $"Visible: {VisibleCount}";
/// <summary> The Visible Books menu item header text </summary>
public string VisibleCountMenuItemText => $"_Visible Books {VisibleCount}";
/// <summary> The user's library statistics </summary>
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));
}
}
/// <summary> The "Begin Book and PDF Backup" menu item header text </summary>
public string BookBackupsToolStripText { get; private set; } = "Begin _Book and PDF Backups: 0";
/// <summary> The "Begin PDF Only Backup" menu item header text </summary>
public string PdfBackupsToolStripText { get; private set; } = "Begin _PDF Only Backups: 0";
/// <summary> The number of books visible in the Products Display that have not yet been liberated </summary>
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));
}
}
/// <summary> Indicates if any of the books visible in the Products Display haven't been liberated </summary>
public bool AnyVisibleNotLiberated => VisibleNotLiberated > 0;
/// <summary> The "Liberate Visible Books" menu item header text (submenu item of the "Liberate Menu" menu item) </summary>
public string LiberateVisibleToolStripText { get; private set; } = "Liberate _Visible Books: 0";
/// <summary> The "Liberate" menu item header text (submenu item of the "Visible Books" menu item) </summary>
public string LiberateVisibleToolStripText_2 { get; private set; } = "_Liberate: 0";
}
}

View File

@ -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<ISeriesEntry>().SelectMany(s => s.Children)), FilterString);
SOURCE.AddRange(geList.OrderByDescending(e => e.DateAdded));
//Add all children beneath their parent

View File

@ -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);

View File

@ -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());
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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.
}
}
}

View File

@ -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<MenuItem>()
.FirstOrDefault(i => i.Header is string h && h.StartsWith($"_{keyNum}"));
if (menuItem is not null)
{
await performFilter(menuItem.Tag as string);
e.Handled = true;
}
}
}
private void updateFiltersMenu(object _ = null, object __ = null)
{
//Clear all filters
_viewModel.QuickFilterMenuItems.RemoveAll(_viewModel.QuickFilterMenuItems.Where(i => i.Tag is string).ToList());
// re-populate
var index = 0;
foreach (var filter in QuickFilters.Filters)
{
var quickFilterMenuItem = new MenuItem
{
Tag = filter,
Header = $"_{++index}: {filter}"
};
quickFilterMenuItem.Click += async (_, __) => await performFilter(filter);
_viewModel.QuickFilterMenuItems.Add(quickFilterMenuItem);
}
}
private void firstFilterIsDefaultToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
_viewModel.FirstFilterIsDefault = !_viewModel.FirstFilterIsDefault;
}
private void addQuickFilterBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> QuickFilters.Add(_viewModel.FilterString);
private async void editQuickFiltersToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await new Dialogs.EditQuickFilters().ShowDialog(this);
}
}

View File

@ -1,100 +0,0 @@
using AudibleUtilities;
using LibationAvalonia.Dialogs;
using System.Linq;
namespace LibationAvalonia.Views
{
public partial class MainWindow
{
private void Configure_RemoveBooks()
{
if (Avalonia.Controls.Design.IsDesignMode)
return;
_viewModel.RemoveButtonsVisible = false;
}
public async void openTrashBinToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var trash = new TrashBinDialog();
await trash.ShowDialog(this);
}
public void removeLibraryBooksToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
// if 0 accounts, this will not be visible
// if 1 account, run scanLibrariesRemovedBooks() on this account
// if multiple accounts, another menu set will open. do not run scanLibrariesRemovedBooks()
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings.GetAll();
if (accounts.Count != 1)
return;
var firstAccount = accounts.Single();
scanLibrariesRemovedBooks(firstAccount);
}
// selectively remove books from all accounts
public void removeAllAccountsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var allAccounts = persister.AccountsSettings.GetAll();
scanLibrariesRemovedBooks(allAccounts.ToArray());
}
// selectively remove books from some accounts
public async void removeSomeAccountsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var scanAccountsDialog = new Dialogs.ScanAccountsDialog();
if (await scanAccountsDialog.ShowDialog<DialogResult>(this) != DialogResult.OK)
return;
if (!scanAccountsDialog.CheckedAccounts.Any())
return;
scanLibrariesRemovedBooks(scanAccountsDialog.CheckedAccounts.ToArray());
}
private async void scanLibrariesRemovedBooks(params Account[] accounts)
{
//This action is meant to operate on the entire library.
//For removing books within a filter set, use
//Visible Books > Remove from library
await _viewModel.ProductsDisplay.Filter(null);
_viewModel.RemoveBooksButtonEnabled = true;
_viewModel.RemoveButtonsVisible = true;
await _viewModel.ProductsDisplay.ScanAndRemoveBooksAsync(accounts);
}
public async void removeBooksBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
_viewModel.RemoveBooksButtonEnabled = false;
await _viewModel.ProductsDisplay.RemoveCheckedBooksAsync();
_viewModel.RemoveBooksButtonEnabled = true;
}
public async void doneRemovingBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
_viewModel.RemoveButtonsVisible = false;
_viewModel.ProductsDisplay.DoneRemovingBooks();
//Restore the filter
await performFilter(lastGoodFilter);
}
public void ProductsDisplay_RemovableCountChanged(object sender, int removeCount)
{
_viewModel.RemoveBooksButtonText = removeCount switch
{
1 => "Remove 1 Book from Libation",
_ => $"Remove {removeCount} Books from Libation"
};
}
}
}

View File

@ -1,90 +0,0 @@
using ApplicationServices;
using AudibleUtilities;
using LibationAvalonia.Dialogs;
using LibationFileManager;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace LibationAvalonia.Views
{
public partial class MainWindow
{
private void Configure_ScanManual()
{
Load += refreshImportMenu;
AccountsSettingsPersister.Saved += refreshImportMenu;
}
private void refreshImportMenu(object _, EventArgs __)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
_viewModel.AccountsCount = persister.AccountsSettings.Accounts.Count;
}
public async void noAccountsYetAddAccountToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
await MessageBox.Show("To load your Audible library, come back here to the Import menu after adding your account");
await new Dialogs.AccountsDialog().ShowDialog(this);
}
public async void scanLibraryToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var firstAccount = persister.AccountsSettings.GetAll().FirstOrDefault();
await scanLibrariesAsync(firstAccount);
}
public async void scanLibraryOfAllAccountsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var allAccounts = persister.AccountsSettings.GetAll();
await scanLibrariesAsync(allAccounts);
}
public async void scanLibraryOfSomeAccountsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var scanAccountsDialog = new Dialogs.ScanAccountsDialog();
if (await scanAccountsDialog.ShowDialog<DialogResult>(this) != DialogResult.OK)
return;
if (!scanAccountsDialog.CheckedAccounts.Any())
return;
await scanLibrariesAsync(scanAccountsDialog.CheckedAccounts);
}
private async Task scanLibrariesAsync(IEnumerable<Account> accounts) => await scanLibrariesAsync(accounts.ToArray());
private async Task scanLibrariesAsync(params Account[] accounts)
{
try
{
var (totalProcessed, newAdded) = await LibraryCommands.ImportAccountAsync(Dialogs.Login.AvaloniaLoginChoiceEager.ApiExtendedFunc, accounts);
// this is here instead of ScanEnd so that the following is only possible when it's user-initiated, not automatic loop
if (Configuration.Instance.ShowImportedStats && newAdded > 0)
await MessageBox.Show($"Total processed: {totalProcessed}\r\nNew: {newAdded}");
}
catch (OperationCanceledException)
{
Serilog.Log.Information("Audible login attempt cancelled by user");
}
catch (Exception ex)
{
await MessageBox.ShowAdminAlert(
this,
"Error importing library. Please try again. If this still happens after 2 or 3 tries, stop and contact administrator",
"Error importing library",
ex);
}
}
private async void locateAudiobooksToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var locateDialog = new LocateAudiobooksDialog();
await locateDialog.ShowDialog(this);
}
}
}

View File

@ -1,24 +0,0 @@
using ApplicationServices;
using System;
namespace LibationAvalonia.Views
{
public partial class MainWindow
{
private void Configure_ScanNotification()
{
_viewModel.NumAccountsScanning = 0;
LibraryCommands.ScanBegin += LibraryCommands_ScanBegin;
LibraryCommands.ScanEnd += LibraryCommands_ScanEnd;
}
private void LibraryCommands_ScanBegin(object sender, int accountsLength)
{
_viewModel.NumAccountsScanning = accountsLength;
}
private void LibraryCommands_ScanEnd(object sender, int newCount)
{
_viewModel.NumAccountsScanning = 0;
}
}
}

View File

@ -1,34 +0,0 @@
using LibationFileManager;
using System;
namespace LibationAvalonia.Views
{
public partial class MainWindow
{
private void Configure_Settings() { }
public async void accountsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await new Dialogs.AccountsDialog().ShowDialog(this);
public async void basicSettingsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await new Dialogs.SettingsDialog().ShowDialog(this);
public async void aboutToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await MessageBox.Show($"Libation {AppScaffolding.LibationScaffolding.Variety}{Environment.NewLine}Version {AppScaffolding.LibationScaffolding.BuildVersion}", $"Libation v{AppScaffolding.LibationScaffolding.BuildVersion}");
public async void tourToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await new Walkthrough(this).RunAsync();
public void launchHangoverToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
try
{
System.Diagnostics.Process.Start("Hangover" + (Configuration.IsWindows ? ".exe" : ""));
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Failed to launch Hangover");
}
}
}
}

View File

@ -1,34 +0,0 @@
using Avalonia.Threading;
using LibationAvalonia.Dialogs;
using LibationUiBase;
using System.Threading.Tasks;
namespace LibationAvalonia.Views
{
public partial class MainWindow
{
private void Configure_Upgrade()
{
setProgressVisible(false);
#if !DEBUG
async Task upgradeAvailable(UpgradeEventArgs e)
{
var notificationResult = await new UpgradeNotificationDialog(e.UpgradeProperties, e.CapUpgrade).ShowDialogAsync(this);
e.Ignore = notificationResult == DialogResult.Ignore;
e.InstallUpgrade = notificationResult == DialogResult.OK;
}
var upgrader = new Upgrader();
upgrader.DownloadProgress += async (_, e) => await Dispatcher.UIThread.InvokeAsync(() => _viewModel.DownloadProgress = e.ProgressPercentage);
upgrader.DownloadBegin += async (_, _) => await Dispatcher.UIThread.InvokeAsync(() => setProgressVisible(true));
upgrader.DownloadCompleted += async (_, _) => await Dispatcher.UIThread.InvokeAsync(() => setProgressVisible(false));
Opened += async (_, _) => await upgrader.CheckForUpgradeAsync(upgradeAvailable);
#endif
}
private void setProgressVisible(bool visible) => _viewModel.DownloadProgress = visible ? 0 : null;
}
}

View File

@ -1,158 +0,0 @@
using ApplicationServices;
using Avalonia.Threading;
using DataLayer;
using System;
using System.Threading.Tasks;
namespace LibationAvalonia.Views
{
public partial class MainWindow
{
private void Configure_VisibleBooks()
{
LibraryCommands.BookUserDefinedItemCommitted += setLiberatedVisibleMenuItemAsync;
}
private async void setLiberatedVisibleMenuItemAsync(object _, object __)
=> await Dispatcher.UIThread.InvokeAsync(setLiberatedVisibleMenuItem);
public void liberateVisible(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
try
{
SetQueueCollapseState(false);
Serilog.Log.Logger.Information("Begin backing up visible library books");
_viewModel.ProcessQueue.AddDownloadDecrypt(
_viewModel
.ProductsDisplay
.GetVisibleBookEntries()
.UnLiberated()
);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "An error occurred while backing up visible library books");
}
}
public async void replaceTagsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var dialog = new Dialogs.TagsBatchDialog();
var result = await dialog.ShowDialog<DialogResult>(this);
if (result != DialogResult.OK)
return;
var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries();
var confirmationResult = await MessageBox.ShowConfirmationDialog(
this,
visibleLibraryBooks,
// do not use `$` string interpolation. See impl.
"Are you sure you want to replace tags in {0}?",
"Replace tags?");
if (confirmationResult != DialogResult.Yes)
return;
visibleLibraryBooks.UpdateTags(dialog.NewTags);
}
public async void setBookDownloadedManualToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var dialog = new Dialogs.LiberatedStatusBatchManualDialog();
var result = await dialog.ShowDialog<DialogResult>(this);
if (result != DialogResult.OK)
return;
var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries();
var confirmationResult = await MessageBox.ShowConfirmationDialog(
this,
visibleLibraryBooks,
// do not use `$` string interpolation. See impl.
"Are you sure you want to replace book downloaded status in {0}?",
"Replace downloaded status?");
if (confirmationResult != DialogResult.Yes)
return;
visibleLibraryBooks.UpdateBookStatus(dialog.BookLiberatedStatus);
}
public async void setPdfDownloadedManualToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var dialog = new Dialogs.LiberatedStatusBatchManualDialog(isPdf: true);
var result = await dialog.ShowDialog<DialogResult>(this);
if (result != DialogResult.OK)
return;
var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries();
var confirmationResult = await MessageBox.ShowConfirmationDialog(
this,
visibleLibraryBooks,
// do not use `$` string interpolation. See impl.
"Are you sure you want to replace PDF downloaded status in {0}?",
"Replace downloaded status?");
if (confirmationResult != DialogResult.Yes)
return;
visibleLibraryBooks.UpdatePdfStatus(dialog.BookLiberatedStatus);
}
public async void setDownloadedAutoToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var dialog = new Dialogs.LiberatedStatusBatchAutoDialog();
var result = await dialog.ShowDialog<DialogResult>(this);
if (result != DialogResult.OK)
return;
var bulkSetStatus = new BulkSetDownloadStatus(_viewModel.ProductsDisplay.GetVisibleBookEntries(), dialog.SetDownloaded, dialog.SetNotDownloaded);
var count = await Task.Run(bulkSetStatus.Discover);
if (count == 0)
return;
var confirmationResult = await MessageBox.Show(
bulkSetStatus.AggregateMessage,
"Replace downloaded status?",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question,
MessageBoxDefaultButton.Button1);
if (confirmationResult != DialogResult.Yes)
return;
bulkSetStatus.Execute();
}
public async void removeToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries();
var confirmationResult = await MessageBox.ShowConfirmationDialog(
this,
visibleLibraryBooks,
// do not use `$` string interpolation. See impl.
"Are you sure you want to remove {0} from Libation's library?",
"Remove books from Libation?",
MessageBoxDefaultButton.Button2);
if (confirmationResult is DialogResult.Yes)
await visibleLibraryBooks.RemoveBooksAsync();
}
public async void ProductsDisplay_VisibleCountChanged(object sender, int qty)
{
_viewModel.VisibleCount = qty;
await Dispatcher.UIThread.InvokeAsync(setLiberatedVisibleMenuItem);
}
void setLiberatedVisibleMenuItem()
{
var libraryStats = LibraryCommands.GetCounts(_viewModel.ProductsDisplay.GetVisibleBookEntries());
_viewModel.VisibleNotLiberated = libraryStats.PendingBooks;
}
}
}

View File

@ -9,15 +9,77 @@
mc:Ignorable="d" d:DesignWidth="1850" d:DesignHeight="700"
x:Class="LibationAvalonia.Views.MainWindow"
Title="Libation: Liberate your Library"
x:DataType="vm:MainVM"
Name="Form1"
Icon="/Assets/libation.ico">
<NativeMenu.Menu>
<NativeMenu>
<NativeMenuItem Header="Import">
<NativeMenu>
<NativeMenuItem
Header="Auto Scan Library"
Command="{CompiledBinding ToggleAutoScan}"
IsChecked="{CompiledBinding AutoScanChecked}"
ToggleType="CheckBox" />
<NativeMenuItemSeparator />
</NativeMenu>
</NativeMenuItem>
<NativeMenuItem Header="Liberate">
<NativeMenu>
<NativeMenuItem Command="{CompiledBinding BackupAllBooks}" Header="{CompiledBinding BookBackupsToolStripText}" Gesture="{OnPlatform macOS='alt+⌘+B'}" />
<NativeMenuItem Command="{CompiledBinding BackupAllPdfs}" Header="{CompiledBinding PdfBackupsToolStripText}" Gesture="{OnPlatform macOS='alt+⌘+P'}"/>
<NativeMenuItem Command="{CompiledBinding ConvertAllToMp3Async}" Header="Convert all M4b to Mp3 [Long-running]..." />
<NativeMenuItem Command="{CompiledBinding LiberateVisible}" Header="{CompiledBinding LiberateVisibleToolStripText}" IsEnabled="{CompiledBinding AnyVisibleNotLiberated}" />
</NativeMenu>
</NativeMenuItem>
<NativeMenuItem Header="Export">
<NativeMenu>
<NativeMenuItem Command="{CompiledBinding ExportLibraryAsync}" Header="Export Library" Gesture="{OnPlatform macOS='alt+⌘+X'}"/>
</NativeMenu>
</NativeMenuItem>
<NativeMenuItem Header="Quick Filters">
<NativeMenu>
<NativeMenuItem
Header="Start Libation with 1st filter Default"
Command="{CompiledBinding ToggleFirstFilterIsDefault}"
IsChecked="{CompiledBinding FirstFilterIsDefault}"
ToggleType="CheckBox" />
<NativeMenuItem Command="{CompiledBinding EditQuickFiltersAsync}" Header="Edit quick filters..." Gesture="{OnPlatform macOS='alt+⌘+Q'}" />
<NativeMenuItemSeparator />
</NativeMenu>
</NativeMenuItem>
<NativeMenuItem Header="Visible Books">
<NativeMenu>
<NativeMenuItem Command="{CompiledBinding LiberateVisible}" Header="{CompiledBinding LiberateVisibleToolStripText_2}" IsEnabled="{CompiledBinding AnyVisibleNotLiberated}" Gesture="{OnPlatform macOS='alt+⌘+V'}" />
<NativeMenuItem Command="{CompiledBinding ReplaceTagsAsync}" Header="Replace Tags..." />
<NativeMenuItem Command="{CompiledBinding SetBookDownloadedAsync}" Header="Set book 'Downloaded' status manually..." />
<NativeMenuItem Command="{CompiledBinding SetPdfDownloadedAsync}" Header="Set PDF 'Downloaded' status manually..." />
<NativeMenuItem Command="{CompiledBinding SetDownloadedAutoAsync}" Header="Set 'Downloaded' status automatically..." />
<NativeMenuItem Command="{CompiledBinding RemoveVisibleAsync}" Header="Remove from library..." />
</NativeMenu>
</NativeMenuItem>
<NativeMenuItem Header="Settings">
<NativeMenu>
<NativeMenuItem Command="{CompiledBinding ShowAccountsAsync}" Header="Accounts..." Gesture="{OnPlatform macOS='⌘+.'}" />
<NativeMenuItem Command="{CompiledBinding ShowSettingsAsync}" Header="Settings..." Gesture="{OnPlatform macOS='⌘+,'}" />
<NativeMenuItemSeparator />
<NativeMenuItem Command="{CompiledBinding ShowTrashBinAsync}" Header="Trash Bin" Gesture="{OnPlatform macOS='alt+⌘+T'}" />
<NativeMenuItem Command="{CompiledBinding LaunchHangover}" Header="Launch Hangover" />
<NativeMenuItemSeparator />
<NativeMenuItem Command="{CompiledBinding StartWalkthroughAsync}" Header="Take a Guided Tour of Libation" />
<NativeMenuItem Command="{CompiledBinding ShowAboutAsync}" Header="About..." />
</NativeMenu>
</NativeMenuItem>
</NativeMenu>
</NativeMenu.Menu>
<Border BorderBrush="{DynamicResource DataGridGridLinesBrush}" BorderThickness="2" Padding="10,0,10,10">
<Grid RowDefinitions="Auto,Auto,*,Auto">
<Grid Grid.Row="0" ColumnDefinitions="1*,Auto">
<!-- Menu Strip -->
<Menu Grid.Column="0" VerticalAlignment="Top">
<Menu Grid.Column="0" VerticalAlignment="Top" IsVisible="{CompiledBinding MenuBarVisible}">
<!-- Decrease height of menu strop -->
<Menu.Styles>
@ -35,26 +97,27 @@
<Setter Property="Height" Value="NaN"/>
</Style>
</MenuItem.Styles>
<MenuItem IsVisible="{Binding AnyAccounts}" Click="autoScanLibraryToolStripMenuItem_Click" Header="A_uto Scan Library">
<MenuItem IsVisible="{CompiledBinding AnyAccounts}" Command="{CompiledBinding ToggleAutoScan}" Header="A_uto Scan Library">
<MenuItem.Icon>
<CheckBox BorderThickness="0" IsChecked="{Binding AutoScanChecked, Mode=TwoWay}" IsHitTestVisible="False" />
<CheckBox BorderThickness="0" IsChecked="{CompiledBinding AutoScanChecked, Mode=TwoWay}" IsHitTestVisible="False" />
</MenuItem.Icon>
</MenuItem>
<MenuItem IsVisible="{Binding !AnyAccounts}" Click="noAccountsYetAddAccountToolStripMenuItem_Click" Header="No accounts yet. A_dd Account..." />
<MenuItem IsVisible="{CompiledBinding !AnyAccounts}" Command="{CompiledBinding AddAccountsAsync}" Header="No accounts yet. A_dd Account..." />
<!-- Scan Library -->
<MenuItem IsVisible="{Binding OneAccount}" IsEnabled="{Binding !ActivelyScanning}" Name="scanLibraryToolStripMenuItem" Click="scanLibraryToolStripMenuItem_Click" Header="Scan _Library" />
<MenuItem IsVisible="{Binding MultipleAccounts}" IsEnabled="{Binding !ActivelyScanning}" Name="scanLibraryOfAllAccountsToolStripMenuItem" Click="scanLibraryOfAllAccountsToolStripMenuItem_Click" Header="Scan Library of _All Accounts" />
<MenuItem IsVisible="{Binding MultipleAccounts}" IsEnabled="{Binding !ActivelyScanning}" Click="scanLibraryOfSomeAccountsToolStripMenuItem_Click" Header="Scan Library of _Some Accounts" />
<Separator />
<MenuItem IsVisible="{CompiledBinding OneAccount}" IsEnabled="{CompiledBinding !ActivelyScanning}" Name="scanLibraryToolStripMenuItem" Command="{CompiledBinding ScanAccountAsync}" Header="Scan _Library" />
<MenuItem IsVisible="{CompiledBinding MultipleAccounts}" IsEnabled="{CompiledBinding !ActivelyScanning}" Name="scanLibraryOfAllAccountsToolStripMenuItem" Command="{CompiledBinding ScanAllAccountsAsync}" Header="Scan Library of _All Accounts" />
<MenuItem IsVisible="{CompiledBinding MultipleAccounts}" IsEnabled="{CompiledBinding !ActivelyScanning}" Command="{CompiledBinding ScanSomeAccountsAsync}" Header="Scan Library of _Some Accounts" />
<Separator IsVisible="{CompiledBinding AnyAccounts}" />
<!-- Remove Books -->
<MenuItem IsVisible="{Binding OneAccount}" IsEnabled="{Binding RemoveMenuItemsEnabled}" Click="removeLibraryBooksToolStripMenuItem_Click" Header="_Remove Library Books" />
<MenuItem IsVisible="{Binding MultipleAccounts}" IsEnabled="{Binding RemoveMenuItemsEnabled}" Click="removeAllAccountsToolStripMenuItem_Click" Header="_Remove Books from All Accounts" />
<MenuItem IsVisible="{Binding MultipleAccounts}" IsEnabled="{Binding RemoveMenuItemsEnabled}" Click="removeSomeAccountsToolStripMenuItem_Click" Header="_Remove Books from Some Accounts" />
<MenuItem IsVisible="{CompiledBinding OneAccount}" IsEnabled="{CompiledBinding RemoveMenuItemsEnabled}" Command="{CompiledBinding RemoveBooksAsync}" Header="_Remove Library Books" />
<MenuItem IsVisible="{CompiledBinding MultipleAccounts}" IsEnabled="{CompiledBinding RemoveMenuItemsEnabled}" Command="{CompiledBinding RemoveBooksAllAsync}" Header="_Remove Books from All Accounts" />
<MenuItem IsVisible="{CompiledBinding MultipleAccounts}" IsEnabled="{CompiledBinding RemoveMenuItemsEnabled}" Command="{CompiledBinding RemoveBooksSomeAsync}" Header="_Remove Books from Some Accounts" />
<Separator />
<MenuItem Click="locateAudiobooksToolStripMenuItem_Click" Header="L_ocate Audiobooks" />
<MenuItem Command="{CompiledBinding LocateAudiobooksAsync}" Header="L_ocate Audiobooks..." />
</MenuItem>
@ -67,10 +130,10 @@
<Setter Property="Height" Value="NaN"/>
</Style>
</MenuItem.Styles>
<MenuItem Click="beginBookBackupsToolStripMenuItem_Click" Header="{Binding BookBackupsToolStripText}" />
<MenuItem Click="beginPdfBackupsToolStripMenuItem_Click" Header="{Binding PdfBackupsToolStripText}" />
<MenuItem Click="convertAllM4bToMp3ToolStripMenuItem_Click" Header="Convert all _M4b to Mp3 [Long-running]..." IsVisible="{Binding IsMp3Supported}" />
<MenuItem Click="liberateVisible" Header="{Binding LiberateVisibleToolStripText}" IsEnabled="{Binding AnyVisibleNotLiberated}" />
<MenuItem Command="{CompiledBinding BackupAllBooks}" Header="{CompiledBinding BookBackupsToolStripText}" />
<MenuItem Command="{CompiledBinding BackupAllPdfs}" Header="{CompiledBinding PdfBackupsToolStripText}" />
<MenuItem Command="{CompiledBinding ConvertAllToMp3Async}" Header="Convert all _M4b to Mp3 [Long-running]..." />
<MenuItem Command="{CompiledBinding LiberateVisible}" Header="{CompiledBinding LiberateVisibleToolStripText}" IsEnabled="{CompiledBinding AnyVisibleNotLiberated}" />
</MenuItem>
<!-- Export Menu -->
@ -82,12 +145,12 @@
<Setter Property="Height" Value="NaN"/>
</Style>
</MenuItem.Styles>
<MenuItem IsEnabled="{Binding LibraryStats.HasBookResults}" Click="exportLibraryToolStripMenuItem_Click" Header="E_xport Library" />
<MenuItem IsEnabled="{CompiledBinding LibraryStats.HasBookResults}" Command="{CompiledBinding ExportLibraryAsync}" Header="E_xport Library" InputGesture="ctrl+S" />
</MenuItem>
<!-- Quick Filters Menu -->
<MenuItem Name="quickFiltersToolStripMenuItem" Header="Quick _Filters" ItemsSource="{Binding QuickFilterMenuItems}" KeyDown="QuickFiltersMenuItem_KeyDown">
<MenuItem Name="quickFiltersToolStripMenuItem" Header="Quick _Filters" ItemsSource="{CompiledBinding QuickFilterMenuItems}">
<!-- Remove height style property for menu item -->
<MenuItem.Styles>
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
@ -98,19 +161,19 @@
<!-- Visible Books Menu -->
<MenuItem Header="{Binding VisibleCountMenuItemText}" >
<MenuItem Header="{CompiledBinding VisibleCountMenuItemText}" >
<!-- Remove height style property for menu item -->
<MenuItem.Styles>
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Height" Value="NaN"/>
</Style>
</MenuItem.Styles>
<MenuItem Click="liberateVisible" Header="{Binding LiberateVisibleToolStripText_2}" IsEnabled="{Binding AnyVisibleNotLiberated}" />
<MenuItem Click="replaceTagsToolStripMenuItem_Click" Header="Replace _Tags..." />
<MenuItem Click="setBookDownloadedManualToolStripMenuItem_Click" Header="Set book '_Downloaded' status manually..." />
<MenuItem Click="setPdfDownloadedManualToolStripMenuItem_Click" Header="Set _PDF 'Downloaded' status manually..." />
<MenuItem Click="setDownloadedAutoToolStripMenuItem_Click" Header="Set '_Downloaded' status automatically..." />
<MenuItem Click="removeToolStripMenuItem_Click" Header="_Remove from library..." />
<MenuItem Command="{CompiledBinding LiberateVisible}" Header="{CompiledBinding LiberateVisibleToolStripText_2}" IsEnabled="{CompiledBinding AnyVisibleNotLiberated}" />
<MenuItem Command="{CompiledBinding ReplaceTagsAsync}" Header="Replace _Tags..." />
<MenuItem Command="{CompiledBinding SetBookDownloadedAsync}" Header="Set book '_Downloaded' status manually..." />
<MenuItem Command="{CompiledBinding SetPdfDownloadedAsync}" Header="Set _PDF 'Downloaded' status manually..." />
<MenuItem Command="{CompiledBinding SetDownloadedAutoAsync}" Header="Set '_Downloaded' status automatically..." />
<MenuItem Command="{CompiledBinding RemoveVisibleAsync}" Header="_Remove from library..." />
</MenuItem>
<!-- Settings Menu -->
@ -122,19 +185,19 @@
<Setter Property="Height" Value="NaN"/>
</Style>
</MenuItem.Styles>
<MenuItem Name="accountsToolStripMenuItem" Click="accountsToolStripMenuItem_Click" Header="_Accounts..." />
<MenuItem Name="basicSettingsToolStripMenuItem" Click="basicSettingsToolStripMenuItem_Click" Header="_Settings..." />
<MenuItem Name="accountsToolStripMenuItem" Command="{CompiledBinding ShowAccountsAsync}" Header="_Accounts..." InputGesture="ctrl+shift+A"/>
<MenuItem Name="basicSettingsToolStripMenuItem" Command="{CompiledBinding ShowSettingsAsync}" Header="_Settings..." InputGesture="ctrl+P" />
<Separator />
<MenuItem Click="openTrashBinToolStripMenuItem_Click" Header="Trash Bin" />
<MenuItem Click="launchHangoverToolStripMenuItem_Click" Header="Launch _Hangover" />
<MenuItem Command="{CompiledBinding ShowTrashBinAsync}" Header="Trash Bin" />
<MenuItem Command="{CompiledBinding LaunchHangover}" Header="Launch _Hangover" />
<Separator />
<MenuItem Click="tourToolStripMenuItem_Click" Header="Take a Guided _Tour of Libation" />
<MenuItem Click="aboutToolStripMenuItem_Click" Header="A_bout..." />
<MenuItem Command="{CompiledBinding StartWalkthroughAsync}" Header="Take a Guided _Tour of Libation" />
<MenuItem Command="{CompiledBinding ShowAboutAsync}" Header="A_bout..." />
</MenuItem>
</Menu>
<StackPanel IsVisible="{Binding ActivelyScanning}" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
<StackPanel IsVisible="{CompiledBinding ActivelyScanning}" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
<Path VerticalAlignment="Center" Fill="{StaticResource IconFill}" Data="{StaticResource ImportIcon}" />
<TextBlock Margin="5,0,5,0" VerticalAlignment="Center" Text="{Binding ScanningText}"/>
<TextBlock Margin="5,0,5,0" VerticalAlignment="Center" Text="{CompiledBinding ScanningText}"/>
</StackPanel>
</Grid>
@ -155,23 +218,23 @@
</Grid.Styles>
<StackPanel Grid.Column="0" Orientation="Horizontal">
<Button Name="filterHelpBtn" Margin="0" Click="filterHelpBtn_Click" Content="?"/>
<Button Name="addQuickFilterBtn" Click="addQuickFilterBtn_Click" Content="Add To Quick Filters"/>
<Button Name="filterHelpBtn" Margin="0" Command="{CompiledBinding FilterHelpBtn}" Content="?"/>
<Button Name="addQuickFilterBtn" Command="{CompiledBinding AddQuickFilterBtn}" Content="Add To Quick Filters"/>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Button IsVisible="{Binding RemoveButtonsVisible}" IsEnabled="{Binding RemoveBooksButtonEnabled}" Click="removeBooksBtn_Click" Content="{Binding RemoveBooksButtonText}"/>
<Button IsVisible="{Binding RemoveButtonsVisible}" Click="doneRemovingBtn_Click" Content="Done Removing Books"/>
<Button IsVisible="{CompiledBinding RemoveButtonsVisible}" IsEnabled="{CompiledBinding RemoveBooksButtonEnabled}" Command="{CompiledBinding RemoveBooksBtn}" Content="{CompiledBinding RemoveBooksButtonText}"/>
<Button IsVisible="{CompiledBinding RemoveButtonsVisible}" Command="{CompiledBinding DoneRemovingBtn}" Content="Done Removing Books"/>
</StackPanel>
<TextBox Grid.Column="1" Margin="10,0,0,0" Name="filterSearchTb" IsVisible="{Binding !RemoveButtonsVisible}" Text="{Binding FilterString, Mode=TwoWay}" KeyDown="filterSearchTb_KeyPress" />
<TextBox Grid.Column="1" Margin="10,0,0,0" Name="filterSearchTb" IsVisible="{CompiledBinding !RemoveButtonsVisible}" Text="{CompiledBinding FilterString, Mode=TwoWay}" KeyDown="filterSearchTb_KeyPress" />
<StackPanel Grid.Column="2" Height="30" Orientation="Horizontal">
<Button Name="filterBtn" Click="filterBtn_Click" VerticalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Content="Filter"/>
<Button Padding="2,6,2,6" VerticalAlignment="Stretch" Click="ToggleQueueHideBtn_Click">
<Button Name="filterBtn" Command="{CompiledBinding FilterBtn}" VerticalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Content="Filter"/>
<Button Padding="2,6,2,6" VerticalAlignment="Stretch" Command="{CompiledBinding ToggleQueueHideBtn}">
<Path Stretch="Uniform" Fill="{DynamicResource IconFill}" Data="{StaticResource LeftArrows}">
<Path.RenderTransform>
<RotateTransform Angle="{Binding QueueButtonAngle}"/>
<RotateTransform Angle="{CompiledBinding QueueButtonAngle}"/>
</Path.RenderTransform>
</Path>
</Button>
@ -179,17 +242,17 @@
</Grid>
<Border Grid.Row="2" BorderThickness="1" BorderBrush="{DynamicResource DataGridGridLinesBrush}">
<SplitView IsPaneOpen="{Binding QueueOpen}" DisplayMode="Inline" OpenPaneLength="400" MinWidth="400" PanePlacement="Right">
<SplitView IsPaneOpen="{CompiledBinding QueueOpen}" DisplayMode="Inline" OpenPaneLength="400" MinWidth="400" PanePlacement="Right">
<!-- Process Queue -->
<SplitView.Pane>
<views:ProcessQueueControl DataContext="{Binding ProcessQueue}"/>
<views:ProcessQueueControl DataContext="{CompiledBinding ProcessQueue}"/>
</SplitView.Pane>
<!-- Product Display Grid -->
<views:ProductsDisplay
Name="productsDisplay"
DataContext="{Binding ProductsDisplay}"
DataContext="{CompiledBinding ProductsDisplay}"
LiberateClicked="ProductsDisplay_LiberateClicked"
LiberateSeriesClicked="ProductsDisplay_LiberateSeriesClicked"
ConvertToMp3Clicked="ProductsDisplay_ConvertToMp3Clicked" />
@ -203,10 +266,10 @@
<Setter Property="MinWidth" Value="100" />
</Style>
</Grid.Styles>
<TextBlock FontSize="14" Grid.Column="0" Text="Upgrading:" VerticalAlignment="Center" IsVisible="{Binding DownloadProgress, Converter={x:Static ObjectConverters.IsNotNull}}" />
<ProgressBar Grid.Column="1" Margin="5,0,10,0" VerticalAlignment="Stretch" Width="100" Value="{Binding DownloadProgress}" IsVisible="{Binding DownloadProgress, Converter={x:Static ObjectConverters.IsNotNull}}"/>
<TextBlock FontSize="14" Grid.Column="2" Text="{Binding VisibleCountText}" VerticalAlignment="Center" />
<TextBlock FontSize="14" Grid.Column="3" Text="{Binding LibraryStats.StatusString}" VerticalAlignment="Center" />
<TextBlock FontSize="14" Grid.Column="0" Text="Upgrading:" VerticalAlignment="Center" IsVisible="{CompiledBinding DownloadProgress, Converter={x:Static ObjectConverters.IsNotNull}}" />
<ProgressBar Grid.Column="1" Margin="5,0,10,0" VerticalAlignment="Stretch" Width="100" Value="{CompiledBinding DownloadProgress}" IsVisible="{CompiledBinding DownloadProgress, Converter={x:Static ObjectConverters.IsNotNull}}"/>
<TextBlock FontSize="14" Grid.Column="2" Text="{CompiledBinding VisibleCountText}" VerticalAlignment="Center" />
<TextBlock FontSize="14" Grid.Column="3" Text="{CompiledBinding LibraryStats.StatusString}" VerticalAlignment="Center" />
</Grid>
</Grid>
</Border>

View File

@ -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<MainWindowViewModel>
public partial class MainWindow : ReactiveWindow<MainVM>
{
public event EventHandler Load;
public event EventHandler<List<LibraryBook>> 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)
{
@ -70,17 +53,60 @@ namespace LibationAvalonia.Views
private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
productsDisplay?.CloseImageDisplay();
this.SaveSizeAndLocation(Configuration.Instance);
}
private async void MainWindow_LibraryLoaded(object sender, List<LibraryBook> 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<LibraryBook> 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;
}
}

View File

@ -11,9 +11,6 @@
<UserControl.Resources>
<views:DecimalConverter x:Key="myConverter" />
</UserControl.Resources>
<UserControl.Resources>
<RecyclePool x:Key="RecyclePool" />
<DataTemplate x:Key="queuedBook">
<views:ProcessBookControl />

View File

@ -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);

View File

@ -0,0 +1,329 @@
namespace LibationWinForms.Dialogs
{
partial class AboutDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
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;
}
}

View File

@ -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('.', '-')}");
}
}
}
}

View File

@ -0,0 +1,60 @@
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -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;
}
}

View File

@ -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();

View File

@ -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);
}
}

View File

@ -60,6 +60,16 @@ namespace LibationWinForms.Properties {
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap cheers {
get {
object obj = ResourceManager.GetObject("cheers", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>

View File

@ -118,6 +118,9 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="cheers" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\cheers.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="default_cover_300x300" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\img-coverart-prod-unavailable_300x300.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB