Merge pull request #1161 from Mbucari/master

Performance and UI tweaks
This commit is contained in:
rmcrackan 2025-02-28 22:48:49 -05:00 committed by GitHub
commit 51aabe5dd4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 492 additions and 160 deletions

View File

@ -90,6 +90,7 @@ namespace AppScaffolding
Migrations.migrate_to_v6_6_9(config); Migrations.migrate_to_v6_6_9(config);
Migrations.migrate_to_v11_5_0(config); Migrations.migrate_to_v11_5_0(config);
Migrations.migrate_to_v11_6_5(config); Migrations.migrate_to_v11_6_5(config);
Migrations.migrate_to_v12_0_1(config);
} }
/// <summary>Initialize logging. Wire-up events. Run after migration</summary> /// <summary>Initialize logging. Wire-up events. Run after migration</summary>
@ -417,6 +418,82 @@ namespace AppScaffolding
public List<string> Filters { get; set; } = new(); public List<string> Filters { get; set; } = new();
} }
public static void migrate_to_v12_0_1(Configuration config)
{
#nullable enable
//Migrate from version 1 file cache to the dictionary-based version 2 cache
const string FILENAME_V1 = "FileLocations.json";
const string FILENAME_V2 = "FileLocationsV2.json";
var jsonFileV1 = Path.Combine(Configuration.Instance.LibationFiles, FILENAME_V1);
var jsonFileV2 = Path.Combine(Configuration.Instance.LibationFiles, FILENAME_V2);
if (!File.Exists(jsonFileV2) && File.Exists(jsonFileV1))
{
try
{
//FilePathCache loads the cache in its static constructor,
//so perform migration without using FilePathCache.CacheEntry
if (JArray.Parse(File.ReadAllText(jsonFileV1)) is not JArray v1Cache || v1Cache.Count == 0)
return;
Dictionary<string, JArray> cache = new();
//Convert to c# objects to speed up searching by ID inside the iterator
var allItems
= v1Cache
.Select(i => new
{
Id = i["Id"]?.Value<string>(),
Path = i["Path"]?["Path"]?.Value<string>()
}).Where(i => i.Id != null)
.ToArray();
foreach (var id in allItems.Select(i => i.Id).OfType<string>().Distinct())
{
//Use this opportunity to purge non-existent files and re-classify file types
//(due to *.aax files previously not being classified as FileType.AAXC)
var items = allItems
.Where(i => i.Id == id && File.Exists(i.Path))
.Select(i => new JObject
{
{ "Id", i.Id },
{ "FileType", (int)FileTypes.GetFileTypeFromPath(i.Path) },
{ "Path", new JObject{ { "Path", i.Path } } }
})
.ToArray();
if (items.Length == 0)
continue;
cache[id] = new JArray(items);
}
var cacheJson = new JObject { { "Dictionary", JObject.FromObject(cache) } };
var cacheFileText = cacheJson.ToString(Formatting.Indented);
void migrate()
{
File.WriteAllText(jsonFileV2, cacheFileText);
File.Delete(jsonFileV1);
}
try { migrate(); }
catch (IOException)
{
try { migrate(); }
catch (IOException)
{
migrate();
}
}
}
catch { /* eat */ }
}
#nullable restore
}
public static void migrate_to_v11_6_5(Configuration config) public static void migrate_to_v11_6_5(Configuration config)
{ {
//Settings migration for unsupported sample rates (#1116) //Settings migration for unsupported sample rates (#1116)

View File

@ -144,7 +144,7 @@ namespace ApplicationServices
PictureId = a.Book.PictureId, PictureId = a.Book.PictureId,
IsAbridged = a.Book.IsAbridged, IsAbridged = a.Book.IsAbridged,
DatePublished = a.Book.DatePublished, DatePublished = a.Book.DatePublished,
CategoriesNames = a.Book.LowestCategoryNames().Any() ? a.Book.LowestCategoryNames().Aggregate((a, b) => $"{a}, {b}") : "", CategoriesNames = string.Join("; ", a.Book.LowestCategoryNames()),
MyRatingOverall = a.Book.UserDefinedItem.Rating.OverallRating, MyRatingOverall = a.Book.UserDefinedItem.Rating.OverallRating,
MyRatingPerformance = a.Book.UserDefinedItem.Rating.PerformanceRating, MyRatingPerformance = a.Book.UserDefinedItem.Rating.PerformanceRating,
MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating, MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating,

View File

@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:LibationAvalonia" xmlns:local="using:LibationAvalonia"
xmlns:controls="using:LibationAvalonia.Controls" xmlns:controls="using:LibationAvalonia.Controls"
xmlns:dialogs="using:LibationAvalonia.Dialogs"
x:Class="LibationAvalonia.App" x:Class="LibationAvalonia.App"
Name="Libation"> Name="Libation">
@ -12,6 +13,10 @@
<Application.Resources> <Application.Resources>
<ResourceDictionary> <ResourceDictionary>
<ControlTheme x:Key="{x:Type TextBlock}" TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource TextControlForeground}" />
<Setter Property="Background" Value="Transparent" />
</ControlTheme>
<ResourceDictionary.ThemeDictionaries> <ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light"> <ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="SeriesEntryGridBackgroundBrush" Opacity="0.3" Color="#abffab" /> <SolidColorBrush x:Key="SeriesEntryGridBackgroundBrush" Opacity="0.3" Color="#abffab" />
@ -81,6 +86,60 @@
<!-- It's called AutoHide, but this is really the mouseover shrink/expand. --> <!-- It's called AutoHide, but this is really the mouseover shrink/expand. -->
<Setter Property="AllowAutoHide" Value="false"/> <Setter Property="AllowAutoHide" Value="false"/>
</Style> </Style>
<Style Selector="dialogs|DialogWindow">
<Style Selector="^[UseCustomTitleBar=false]">
<Setter Property="SystemDecorations" Value="Full"/>
<Setter Property="Template">
<ControlTemplate>
<ContentPresenter Background="{DynamicResource SystemControlBackgroundAltHighBrush}" Content="{TemplateBinding Content}" />
</ControlTemplate>
</Setter>
</Style>
<Style Selector="^[UseCustomTitleBar=true]">
<Setter Property="SystemDecorations" Value="BorderOnly"/>
<Setter Property="Template">
<ControlTemplate>
<Panel Background="{DynamicResource SystemControlBackgroundAltHighBrush}">
<Grid RowDefinitions="30,*">
<Border Name="DialogWindowTitleBorder" Margin="5,0" Background="{DynamicResource SystemAltMediumColor}">
<Border.Styles>
<Style Selector="Button#DialogCloseButton">
<Style Selector="^:pointerover">
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Red" />
</Style>
<Style Selector="^ Path">
<Setter Property="Fill" Value="{DynamicResource IconFill}" />
</Style>
</Style>
<Style Selector="^:not(:pointerover) /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
</Style>
</Style>
</Border.Styles>
<Grid ColumnDefinitions="Auto,*,Auto">
<Path Name="DialogWindowTitleIcon" Margin="3,5,0,5" Fill="{DynamicResource IconFill}" Stretch="Uniform" Data="{StaticResource LibationGlassIcon}"/>
<TextBlock Name="DialogWindowTitleTextBlock" Margin="8,0,0,0" VerticalAlignment="Center" FontWeight="DemiBold" FontSize="12" Grid.Column="1" Text="{TemplateBinding Title}" />
<Button Name="DialogCloseButton" Grid.Column="2">
<Path Fill="{DynamicResource SystemControlBackgroundBaseLowBrush}" VerticalAlignment="Center" Stretch="Uniform" RenderTransform="{StaticResource Rotate45Transform}" Data="{StaticResource CancelButtonIcon}" />
</Button>
</Grid>
</Border>
<Path Stroke="{DynamicResource SystemBaseMediumLowColor}" StrokeThickness="1" VerticalAlignment="Bottom" Stretch="Fill" Data="M0,0 L1,0" />
<ContentPresenter Grid.Row="1" Content="{TemplateBinding Content}" />
</Grid>
</Panel>
</ControlTemplate>
</Setter>
</Style>
</Style>
</Application.Styles> </Application.Styles>
<NativeMenu.Menu> <NativeMenu.Menu>

View File

@ -44,7 +44,7 @@ namespace LibationAvalonia
if (!config.LibationSettingsAreValid) if (!config.LibationSettingsAreValid)
{ {
var defaultLibationFilesDir = Configuration.UserProfile; var defaultLibationFilesDir = Configuration.DefaultLibationFilesDirectory;
// check for existing settings in default location // check for existing settings in default location
var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json"); var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json");
@ -82,8 +82,8 @@ namespace LibationAvalonia
// - error message, Exit() // - error message, Exit()
if (setupDialog.IsNewUser) if (setupDialog.IsNewUser)
{ {
Configuration.SetLibationFiles(Configuration.UserProfile); Configuration.SetLibationFiles(Configuration.DefaultLibationFilesDirectory);
setupDialog.Config.Books = Path.Combine(Configuration.UserProfile, nameof(Configuration.Books)); setupDialog.Config.Books = Configuration.DefaultBooksDirectory;
if (setupDialog.Config.LibationSettingsAreValid) if (setupDialog.Config.LibationSettingsAreValid)
{ {
@ -174,7 +174,7 @@ namespace LibationAvalonia
if (continueResult == DialogResult.Yes) if (continueResult == DialogResult.Yes)
{ {
config.Books = Path.Combine(libationFilesDialog.SelectedDirectory, nameof(Configuration.Books)); config.Books = Configuration.DefaultBooksDirectory;
if (config.LibationSettingsAreValid) if (config.LibationSettingsAreValid)
{ {

View File

@ -91,6 +91,32 @@
S 192,128 147,147 S 192,128 147,147
</StreamGeometry> </StreamGeometry>
<StreamGeometry x:Key="LibationGlassIcon">
M262,8
h-117
a 192,200 0 0 0 -36,82
a 222,334 41 0 0 138,236
v158
h-81
a 16,16 0 0 0 0,32
h192
a 16 16 0 0 0 0,-32
h-81
v-158
a 222,334 -41 0 0 138,-236
a 192,200 0 0 0 -36,-82
h-117
m-99,30
a 192,200 0 0 0 -26,95
a 187.5,334 35 0 0 125,159
a 187.5,334 -35 0 0 125,-159
a 192,200 0 0 0 -26,-95
h-198
M158,136
a 168,305 35 0 0 104,136
a 168,305 -35 0 0 104,-136
</StreamGeometry>
</ResourceDictionary> </ResourceDictionary>
</Styles.Resources> </Styles.Resources>
</Styles> </Styles>

View File

@ -51,7 +51,9 @@ namespace LibationAvalonia.Controls
{ {
Configuration.KnownDirectories.WinTemp, Configuration.KnownDirectories.WinTemp,
Configuration.KnownDirectories.UserProfile, Configuration.KnownDirectories.UserProfile,
Configuration.KnownDirectories.ApplicationData,
Configuration.KnownDirectories.AppDir, Configuration.KnownDirectories.AppDir,
Configuration.KnownDirectories.MyMusic,
Configuration.KnownDirectories.MyDocs, Configuration.KnownDirectories.MyDocs,
Configuration.KnownDirectories.LibationFiles Configuration.KnownDirectories.LibationFiles
}; };

View File

@ -167,15 +167,6 @@
SelectedItem="{CompiledBinding ThemeVariant, Mode=TwoWay}" SelectedItem="{CompiledBinding ThemeVariant, Mode=TwoWay}"
ItemsSource="{CompiledBinding Themes}"/> ItemsSource="{CompiledBinding Themes}"/>
<TextBlock
Grid.Column="2"
FontSize="16"
FontWeight="Bold"
Margin="10,0"
VerticalAlignment="Center"
IsVisible="{CompiledBinding SelectionChanged}"
Text="Theme change takes effect on restart"/>
</Grid> </Grid>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@ -1,4 +1,6 @@
using Avalonia.Controls; using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using LibationFileManager; using LibationFileManager;
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -9,19 +11,98 @@ namespace LibationAvalonia.Dialogs
{ {
public bool SaveAndRestorePosition { get; set; } = true; public bool SaveAndRestorePosition { get; set; } = true;
public Control ControlToFocusOnShow { get; set; } public Control ControlToFocusOnShow { get; set; }
protected override Type StyleKeyOverride => typeof(DialogWindow);
public static readonly StyledProperty<bool> UseCustomTitleBarProperty =
AvaloniaProperty.Register<DialogWindow, bool>(nameof(UseCustomTitleBar));
public bool UseCustomTitleBar
{
get { return GetValue(UseCustomTitleBarProperty); }
set { SetValue(UseCustomTitleBarProperty, value); }
}
public DialogWindow() public DialogWindow()
{ {
this.HideMinMaxBtns(); KeyDown += DialogWindow_KeyDown;
this.KeyDown += DialogWindow_KeyDown; Initialized += DialogWindow_Initialized;
this.Initialized += DialogWindow_Initialized; Opened += DialogWindow_Opened;
this.Opened += DialogWindow_Opened; Closing += DialogWindow_Closing;
this.Closing += DialogWindow_Closing;
UseCustomTitleBar = Configuration.IsWindows;
} }
private bool fixedMinHeight = false;
private bool fixedMaxHeight = false;
private bool fixedHeight = false;
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
const int customTitleBarHeight = 30;
if (UseCustomTitleBar)
{
if (change.Property == MinHeightProperty && !fixedMinHeight)
{
fixedMinHeight = true;
MinHeight += customTitleBarHeight;
fixedMinHeight = false;
}
if (change.Property == MaxHeightProperty && !fixedMaxHeight)
{
fixedMaxHeight = true;
MaxHeight += customTitleBarHeight;
fixedMaxHeight = false;
}
if (change.Property == HeightProperty && !fixedHeight)
{
fixedHeight = true;
Height += customTitleBarHeight;
fixedHeight = false;
}
}
base.OnPropertyChanged(change);
}
public DialogWindow(bool saveAndRestorePosition) : this() public DialogWindow(bool saveAndRestorePosition) : this()
{ {
SaveAndRestorePosition = saveAndRestorePosition; SaveAndRestorePosition = saveAndRestorePosition;
} }
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (!UseCustomTitleBar)
return;
var closeButton = e.NameScope.Find<Button>("DialogCloseButton");
var border = e.NameScope.Get<Border>("DialogWindowTitleBorder");
var titleBlock = e.NameScope.Get<TextBlock>("DialogWindowTitleTextBlock");
var icon = e.NameScope.Get<Avalonia.Controls.Shapes.Path>("DialogWindowTitleIcon");
closeButton.Click += CloseButton_Click;
border.PointerPressed += Border_PointerPressed;
icon.IsVisible = Icon != null;
if (MinHeight == MaxHeight && MinWidth == MaxWidth)
{
CanResize = false;
border.Margin = new Thickness(0);
icon.Margin = new Thickness(8, 5, 0, 5);
}
}
private void Border_PointerPressed(object sender, Avalonia.Input.PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(null).Properties.IsLeftButtonPressed)
BeginMoveDrag(e);
}
private void CloseButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
CancelAndClose();
}
private void DialogWindow_Initialized(object sender, EventArgs e) private void DialogWindow_Initialized(object sender, EventArgs e)
{ {
this.WindowStartupLocation = WindowStartupLocation.CenterOwner; this.WindowStartupLocation = WindowStartupLocation.CenterOwner;

View File

@ -25,7 +25,6 @@ namespace LibationAvalonia.Dialogs
{ {
InitializeComponent(); InitializeComponent();
this.HideMinMaxBtns();
ControlToFocusOnShow = this.FindControl<Button>(nameof(ImportButton)); ControlToFocusOnShow = this.FindControl<Button>(nameof(ImportButton));
LoadAccounts(); LoadAccounts();

View File

@ -12,8 +12,6 @@ namespace LibationAvalonia.Dialogs
{ {
InitializeComponent(); InitializeComponent();
this.HideMinMaxBtns();
StringFields = @" StringFields = @"
Search for wizard of oz: Search for wizard of oz:
title:oz title:oz

View File

@ -109,23 +109,5 @@ namespace LibationAvalonia
public int Width; public int Width;
public bool IsMaximized; public bool IsMaximized;
} }
public static void HideMinMaxBtns(this Window form)
{
if (Design.IsDesignMode || !Configuration.IsWindows || form.TryGetPlatformHandle() is not IPlatformHandle handle)
return;
var currentStyle = GetWindowLong(handle.Handle, GWL_STYLE);
SetWindowLong(handle.Handle, GWL_STYLE, currentStyle & ~WS_MAXIMIZEBOX & ~WS_MINIMIZEBOX);
}
const long WS_MINIMIZEBOX = 0x00020000L;
const long WS_MAXIMIZEBOX = 0x10000L;
const int GWL_STYLE = -16;
[System.Runtime.InteropServices.DllImport("user32.dll", EntryPoint = "GetWindowLong")]
static extern long GetWindowLong(IntPtr hWnd, int nIndex);
[System.Runtime.InteropServices.DllImport("user32.dll")]
static extern int SetWindowLong(IntPtr hWnd, int nIndex, long dwNewLong);
} }
} }

View File

@ -165,8 +165,6 @@ Libation.
var dialog = new MessageBoxWindow(saveAndRestorePosition); var dialog = new MessageBoxWindow(saveAndRestorePosition);
dialog.HideMinMaxBtns();
var vm = new MessageBoxViewModel(message, caption, buttons, icon, defaultButton); var vm = new MessageBoxViewModel(message, caption, buttons, icon, defaultButton);
dialog.DataContext = vm; dialog.DataContext = vm;
dialog.ControlToFocusOnShow = dialog.FindControl<Control>(defaultButton.ToString()); dialog.ControlToFocusOnShow = dialog.FindControl<Control>(defaultButton.ToString());
@ -190,11 +188,13 @@ Libation.
tbx.Height = tbx.DesiredSize.Height; tbx.Height = tbx.DesiredSize.Height;
tbx.Width = tbx.DesiredSize.Width; tbx.Width = tbx.DesiredSize.Width;
dialog.MinHeight = vm.FormHeightFromTboxHeight((int)tbx.DesiredSize.Height);
var absoluteHeight = vm.FormHeightFromTboxHeight((int)tbx.DesiredSize.Height);
dialog.MinHeight = absoluteHeight;
dialog.MinWidth = vm.FormWidthFromTboxWidth((int)tbx.DesiredSize.Width); dialog.MinWidth = vm.FormWidthFromTboxWidth((int)tbx.DesiredSize.Width);
dialog.MaxHeight = dialog.MinHeight; dialog.MaxHeight = absoluteHeight;
dialog.MaxWidth = dialog.MinWidth; dialog.MaxWidth = dialog.MinWidth;
dialog.Height = dialog.MinHeight; dialog.Height = absoluteHeight;
dialog.Width = dialog.MinWidth; dialog.Width = dialog.MinWidth;
return dialog; return dialog;
} }

View File

@ -4,7 +4,6 @@ using DataLayer;
using LibationFileManager; using LibationFileManager;
using ReactiveUI; using ReactiveUI;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace LibationAvalonia.ViewModels namespace LibationAvalonia.ViewModels
@ -44,9 +43,6 @@ namespace LibationAvalonia.ViewModels
private void Configure_BackupCounts() private void Configure_BackupCounts()
{ {
LibraryCommands.LibrarySizeChanged += async (object _, List<LibraryBook> libraryBooks)
=> await SetBackupCountsAsync(libraryBooks);
//Pass null to the setup count to get the whole library. //Pass null to the setup count to get the whole library.
LibraryCommands.BookUserDefinedItemCommitted += async (_, _) LibraryCommands.BookUserDefinedItemCommitted += async (_, _)
=> await SetBackupCountsAsync(null); => await SetBackupCountsAsync(null);

View File

@ -10,8 +10,8 @@ namespace LibationAvalonia.ViewModels
{ {
partial class MainVM partial class MainVM
{ {
private int _visibleNotLiberated = 1; private int _visibleNotLiberated = 0;
private int _visibleCount = 1; private int _visibleCount = 0;
/// <summary> The Bottom-right visible book count status text </summary> /// <summary> The Bottom-right visible book count status text </summary>
public string VisibleCountText => $"Visible: {_visibleCount}"; public string VisibleCountText => $"Visible: {_visibleCount}";

View File

@ -4,6 +4,7 @@ using LibationAvalonia.Views;
using LibationFileManager; using LibationFileManager;
using ReactiveUI; using ReactiveUI;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
namespace LibationAvalonia.ViewModels namespace LibationAvalonia.ViewModels
{ {
@ -38,7 +39,9 @@ namespace LibationAvalonia.ViewModels
private async void LibraryCommands_LibrarySizeChanged(object sender, List<LibraryBook> fullLibrary) private async void LibraryCommands_LibrarySizeChanged(object sender, List<LibraryBook> fullLibrary)
{ {
await ProductsDisplay.UpdateGridAsync(fullLibrary); await Task.WhenAll(
SetBackupCountsAsync(fullLibrary),
ProductsDisplay.UpdateGridAsync(fullLibrary));
} }
private static string menufyText(string header) => Configuration.IsMacOs ? header : $"_{header}"; private static string menufyText(string header) => Configuration.IsMacOs ? header : $"_{header}";

View File

@ -118,7 +118,15 @@ namespace LibationAvalonia.ViewModels
#region Add Books to Queue #region Add Books to Queue
private bool isBookInQueue(LibraryBook libraryBook) private bool isBookInQueue(LibraryBook libraryBook)
=> Queue.Any(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId); {
var entry = Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId);
if (entry == null)
return false;
else if (entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed)
return !Queue.RemoveCompleted(entry);
else
return true;
}
public void AddDownloadPdf(LibraryBook libraryBook) public void AddDownloadPdf(LibraryBook libraryBook)
=> AddDownloadPdf(new List<LibraryBook>() { libraryBook }); => AddDownloadPdf(new List<LibraryBook>() { libraryBook });

View File

@ -21,6 +21,7 @@ namespace LibationAvalonia.ViewModels.Settings
public List<Configuration.KnownDirectories> KnownDirectories { get; } = new() public List<Configuration.KnownDirectories> KnownDirectories { get; } = new()
{ {
Configuration.KnownDirectories.WinTemp, Configuration.KnownDirectories.WinTemp,
Configuration.KnownDirectories.ApplicationData,
Configuration.KnownDirectories.UserProfile, Configuration.KnownDirectories.UserProfile,
Configuration.KnownDirectories.AppDir, Configuration.KnownDirectories.AppDir,
Configuration.KnownDirectories.MyDocs, Configuration.KnownDirectories.MyDocs,

View File

@ -67,7 +67,8 @@ namespace LibationAvalonia.ViewModels.Settings
{ {
Configuration.KnownDirectories.UserProfile, Configuration.KnownDirectories.UserProfile,
Configuration.KnownDirectories.AppDir, Configuration.KnownDirectories.AppDir,
Configuration.KnownDirectories.MyDocs Configuration.KnownDirectories.MyDocs,
Configuration.KnownDirectories.MyMusic,
}; };
public string BooksText { get; } = Configuration.GetDescription(nameof(Configuration.Books)); public string BooksText { get; } = Configuration.GetDescription(nameof(Configuration.Books));
@ -100,11 +101,14 @@ namespace LibationAvalonia.ViewModels.Settings
set set
{ {
this.RaiseAndSetIfChanged(ref themeVariant, value); this.RaiseAndSetIfChanged(ref themeVariant, value);
App.Current.RequestedThemeVariant = themeVariant switch
SelectionChanged = ThemeVariant != initialThemeVariant; {
this.RaisePropertyChanged(nameof(SelectionChanged)); nameof(Avalonia.Styling.ThemeVariant.Dark) => Avalonia.Styling.ThemeVariant.Dark,
nameof(Avalonia.Styling.ThemeVariant.Light) => Avalonia.Styling.ThemeVariant.Light,
// "System"
_ => Avalonia.Styling.ThemeVariant.Default
};
} }
} }
public bool SelectionChanged { get; private set; }
} }
} }

View File

@ -8,6 +8,7 @@ using ReactiveUI;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
namespace LibationAvalonia.Views namespace LibationAvalonia.Views
{ {
@ -60,13 +61,14 @@ namespace LibationAvalonia.Views
filterSearchTb.Focus(); filterSearchTb.Focus();
} }
public async System.Threading.Tasks.Task OnLibraryLoadedAsync(List<LibraryBook> initialLibrary) public async Task OnLibraryLoadedAsync(List<LibraryBook> initialLibrary)
{ {
if (QuickFilters.UseDefault) if (QuickFilters.UseDefault)
await ViewModel.PerformFilter(QuickFilters.Filters.FirstOrDefault()); await ViewModel.PerformFilter(QuickFilters.Filters.FirstOrDefault());
await ViewModel.SetBackupCountsAsync(initialLibrary); await Task.WhenAll(
await ViewModel.ProductsDisplay.BindToGridAsync(initialLibrary); ViewModel.SetBackupCountsAsync(initialLibrary),
ViewModel.ProductsDisplay.BindToGridAsync(initialLibrary));
} }
public void ProductsDisplay_LiberateClicked(object _, LibraryBook libraryBook) => ViewModel.LiberateClicked(libraryBook); public void ProductsDisplay_LiberateClicked(object _, LibraryBook libraryBook) => ViewModel.LiberateClicked(libraryBook);

View File

@ -6,7 +6,7 @@
xmlns:views="clr-namespace:LibationAvalonia.Views" xmlns:views="clr-namespace:LibationAvalonia.Views"
xmlns:viewModels="clr-namespace:LibationAvalonia.ViewModels" xmlns:viewModels="clr-namespace:LibationAvalonia.ViewModels"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="850" mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="650"
x:Class="LibationAvalonia.Views.ProcessQueueControl"> x:Class="LibationAvalonia.Views.ProcessQueueControl">
<UserControl.Resources> <UserControl.Resources>
@ -39,7 +39,8 @@
<ScrollViewer <ScrollViewer
Name="scroller" Name="scroller"
HorizontalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto"> VerticalScrollBarVisibility="Auto"
AllowAutoHide="False">
<ItemsRepeater IsVisible="True" <ItemsRepeater IsVisible="True"
Grid.Column="0" Grid.Column="0"
Name="repeater" Name="repeater"

View File

@ -50,7 +50,7 @@ namespace LibationFileManager
get get
{ {
if (string.IsNullOrWhiteSpace(Configuration.Instance.Books)) if (string.IsNullOrWhiteSpace(Configuration.Instance.Books))
Configuration.Instance.Books = Path.Combine(Configuration.UserProfile, "Books"); Configuration.Instance.Books = Configuration.DefaultBooksDirectory;
return Directory.CreateDirectory(Configuration.Instance.Books).FullName; return Directory.CreateDirectory(Configuration.Instance.Books).FullName;
} }
} }

View File

@ -14,8 +14,12 @@ namespace LibationFileManager
public static string AppDir_Relative => $@".{Path.PathSeparator}{LIBATION_FILES_KEY}"; public static string AppDir_Relative => $@".{Path.PathSeparator}{LIBATION_FILES_KEY}";
public static string AppDir_Absolute => Path.GetFullPath(Path.Combine(ProcessDirectory, LIBATION_FILES_KEY)); public static string AppDir_Absolute => Path.GetFullPath(Path.Combine(ProcessDirectory, LIBATION_FILES_KEY));
public static string MyDocs => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Libation")); public static string MyDocs => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Libation"));
public static string MyMusic => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyMusic), "Libation"));
public static string WinTemp => Path.GetFullPath(Path.Combine(Path.GetTempPath(), "Libation")); public static string WinTemp => Path.GetFullPath(Path.Combine(Path.GetTempPath(), "Libation"));
public static string UserProfile => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Libation")); public static string UserProfile => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Libation"));
public static string LocalAppData => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Libation"));
public static string DefaultLibationFilesDirectory => !IsWindows ? LocalAppData : UserProfile;
public static string DefaultBooksDirectory => Path.Combine(!IsWindows ? MyMusic : UserProfile, nameof(Books));
public enum KnownDirectories public enum KnownDirectories
{ {
@ -34,19 +38,27 @@ namespace LibationFileManager
MyDocs = 4, MyDocs = 4,
[Description("Your settings folder (aka: Libation Files)")] [Description("Your settings folder (aka: Libation Files)")]
LibationFiles = 5 LibationFiles = 5,
}
// use func calls so we always get the latest value of LibationFiles [Description("User Application Data Folder")]
private static List<(KnownDirectories directory, Func<string?> getPathFunc)> directoryOptionsPaths { get; } = new() ApplicationData = 6,
[Description("My Music")]
MyMusic = 7,
}
// use func calls so we always get the latest value of LibationFiles
private static List<(KnownDirectories directory, Func<string?> getPathFunc)> directoryOptionsPaths { get; } = new()
{ {
(KnownDirectories.None, () => null), (KnownDirectories.None, () => null),
(KnownDirectories.ApplicationData, () => LocalAppData),
(KnownDirectories.MyMusic, () => MyMusic),
(KnownDirectories.UserProfile, () => UserProfile), (KnownDirectories.UserProfile, () => UserProfile),
(KnownDirectories.AppDir, () => AppDir_Relative), (KnownDirectories.AppDir, () => AppDir_Relative),
(KnownDirectories.WinTemp, () => WinTemp), (KnownDirectories.WinTemp, () => WinTemp),
(KnownDirectories.MyDocs, () => MyDocs), (KnownDirectories.MyDocs, () => MyDocs),
// this is important to not let very early calls try to accidentally load LibationFiles too early. // this is important to not let very early calls try to accidentally load LibationFiles too early.
// also, keep this at bottom of this list // also, keep this at bottom of this list
(KnownDirectories.LibationFiles, () => libationFilesPathCache) (KnownDirectories.LibationFiles, () => LibationSettingsDirectory)
}; };
public static string? GetKnownDirectoryPath(KnownDirectories directory) public static string? GetKnownDirectoryPath(KnownDirectories directory)
{ {

View File

@ -22,11 +22,11 @@ namespace LibationFileManager
{ {
get get
{ {
if (libationFilesPathCache is not null) if (LibationSettingsDirectory is not null)
return libationFilesPathCache; return LibationSettingsDirectory;
// FIRST: must write here before SettingsFilePath in next step reads cache // FIRST: must write here before SettingsFilePath in next step reads cache
libationFilesPathCache = getLibationFilesSettingFromJson(); LibationSettingsDirectory = getLibationFilesSettingFromJson();
// SECOND. before setting to json file with SetWithJsonPath, PersistentDictionary must exist // SECOND. before setting to json file with SetWithJsonPath, PersistentDictionary must exist
persistentDictionary = new PersistentDictionary(SettingsFilePath); persistentDictionary = new PersistentDictionary(SettingsFilePath);
@ -42,11 +42,14 @@ namespace LibationFileManager
SetWithJsonPath(jsonpath, "path", logPath, true); SetWithJsonPath(jsonpath, "path", logPath, true);
return libationFilesPathCache; return LibationSettingsDirectory;
} }
} }
private static string? libationFilesPathCache { get; set; } /// <summary>
/// Directory pointed to by appsettings.json
/// </summary>
private static string? LibationSettingsDirectory { get; set; }
/// <summary> /// <summary>
/// Try to find appsettings.json in the following locations: /// Try to find appsettings.json in the following locations:
@ -79,7 +82,7 @@ namespace LibationFileManager
string[] possibleAppsettingsDirectories = new[] string[] possibleAppsettingsDirectories = new[]
{ {
ProcessDirectory, ProcessDirectory,
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Libation"), LocalAppData,
UserProfile, UserProfile,
Path.Combine(Path.GetTempPath(), "Libation") Path.Combine(Path.GetTempPath(), "Libation")
}; };
@ -106,9 +109,15 @@ namespace LibationFileManager
} }
//Valid appsettings.json not found. Try to create it in each folder. //Valid appsettings.json not found. Try to create it in each folder.
var endingContents = new JObject { { LIBATION_FILES_KEY, UserProfile } }.ToString(Formatting.Indented); var endingContents = new JObject { { LIBATION_FILES_KEY, DefaultLibationFilesDirectory } }.ToString(Formatting.Indented);
foreach (var dir in possibleAppsettingsDirectories) foreach (var dir in possibleAppsettingsDirectories)
{ {
//Don't try to create appsettings.json in the program files directory on *.nix systems.
//However, still _look_ for one there for backwards compatibility with previous installations
if (!IsWindows && dir == ProcessDirectory)
continue;
var appsettingsFile = Path.Combine(dir, appsettings_filename); var appsettingsFile = Path.Combine(dir, appsettings_filename);
try try
@ -180,7 +189,7 @@ namespace LibationFileManager
public static void SetLibationFiles(string directory) public static void SetLibationFiles(string directory)
{ {
libationFilesPathCache = null; LibationSettingsDirectory = null;
var startingContents = File.ReadAllText(AppsettingsJsonFile); var startingContents = File.ReadAllText(AppsettingsJsonFile);
var jObj = JObject.Parse(startingContents); var jObj = JObject.Parse(startingContents);

View File

@ -18,9 +18,8 @@ namespace LibationFileManager
var pDic = new PersistentDictionary(settingsFile, isReadOnly: false); var pDic = new PersistentDictionary(settingsFile, isReadOnly: false);
var booksDir = pDic.GetString(nameof(Books)); if (pDic.GetString(nameof(Books)) is not string booksDir)
return false;
if (booksDir is null) return false;
if (!Directory.Exists(booksDir)) if (!Directory.Exists(booksDir))
{ {
@ -28,17 +27,21 @@ namespace LibationFileManager
throw new DirectoryNotFoundException(settingsFile); throw new DirectoryNotFoundException(settingsFile);
//"Books" is not null, so setup has already been run. //"Books" is not null, so setup has already been run.
//Since Books can't be found, try to create it in Libation settings folder //Since Books can't be found, try to create it
booksDir = Path.Combine(dir, nameof(Books)); //and then revert to the default books directory
try foreach (string d in new string[] { booksDir, DefaultBooksDirectory })
{ {
Directory.CreateDirectory(booksDir); try
{
Directory.CreateDirectory(d);
pDic.SetString(nameof(Books), booksDir); pDic.SetString(nameof(Books), d);
return booksDir is not null && Directory.Exists(booksDir); return Directory.Exists(d);
}
catch { /* Do Nothing */ }
} }
catch { return false; } return false;
} }
return true; return true;

View File

@ -1,8 +1,8 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Dinah.Core.Collections.Immutable;
using FileManager; using FileManager;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -13,78 +13,96 @@ namespace LibationFileManager
{ {
public record CacheEntry(string Id, FileType FileType, LongPath Path); public record CacheEntry(string Id, FileType FileType, LongPath Path);
private const string FILENAME = "FileLocations.json"; private const string FILENAME_V2 = "FileLocationsV2.json";
public static event EventHandler<CacheEntry>? Inserted; public static event EventHandler<CacheEntry>? Inserted;
public static event EventHandler<CacheEntry>? Removed; public static event EventHandler<CacheEntry>? Removed;
private static Cache<CacheEntry> cache { get; } = new Cache<CacheEntry>(); private static LongPath jsonFileV2 => Path.Combine(Configuration.Instance.LibationFiles, FILENAME_V2);
private static LongPath jsonFile => Path.Combine(Configuration.Instance.LibationFiles, FILENAME); private static readonly FileCacheV2<CacheEntry> Cache = new();
static FilePathCache() static FilePathCache()
{ {
// load json into memory. if file doesn't exist, nothing to do. save() will create if needed // load json into memory. if file doesn't exist, nothing to do. save() will create if needed
if (!File.Exists(jsonFile)) if (!File.Exists(jsonFileV2))
return; return;
try try
{
var list = JsonConvert.DeserializeObject<List<CacheEntry>>(File.ReadAllText(jsonFile));
if (list is null)
throw new NullReferenceException("File exists but deserialize is null. This will never happen when file is healthy.");
cache = new Cache<CacheEntry>(list);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error deserializing file. Wrong format. Possibly corrupt. Deleting file. {@DebugInfo}", new { jsonFile });
lock (locker)
File.Delete(jsonFile);
return;
}
}
public static bool Exists(string id, FileType type) => GetFirstPath(id, type) is not null;
public static List<(FileType fileType, LongPath path)> GetFiles(string id)
=> getEntries(entry => entry.Id == id)
.Select(entry => (entry.FileType, entry.Path))
.ToList();
public static LongPath? GetFirstPath(string id, FileType type)
=> getEntries(entry => entry.Id == id && entry.FileType == type)
?.FirstOrDefault()
?.Path;
private static IEnumerable<CacheEntry> getEntries(Func<CacheEntry, bool> predicate)
{
var entries = cache.Where(predicate).ToList();
if (entries is null || !entries.Any())
return Enumerable.Empty<CacheEntry>();
remove(entries.Where(e => !File.Exists(e.Path)).ToList());
return cache.Where(predicate).ToList();
}
private static void remove(List<CacheEntry> entries)
{
if (entries is null)
return;
lock (locker)
{ {
foreach (var entry in entries) Cache = JsonConvert.DeserializeObject<FileCacheV2<CacheEntry>>(File.ReadAllText(jsonFileV2))
{ ?? throw new NullReferenceException("File exists but deserialize is null. This will never happen when file is healthy.");
cache.Remove(entry); }
Removed?.Invoke(null, entry); catch (Exception ex)
} {
save(); Serilog.Log.Logger.Error(ex, "Error deserializing file. Wrong format. Possibly corrupt. Deleting file. {@DebugInfo}", new { jsonFileV2 });
lock (locker)
File.Delete(jsonFileV2);
return;
} }
} }
public static void Insert(string id, string path) public static bool Exists(string id, FileType type) => GetFirstPath(id, type) is not null;
public static List<(FileType fileType, LongPath path)> GetFiles(string id)
{
var matchingFiles = Cache.GetIdEntries(id);
bool cacheChanged = false;
//Verify all entries exist
for (int i = 0; i < matchingFiles.Count; i++)
{
if (!File.Exists(matchingFiles[i].Path))
{
matchingFiles.RemoveAt(i);
cacheChanged |= Remove(matchingFiles[i]);
}
}
if (cacheChanged)
save();
return matchingFiles.Select(e => (e.FileType, e.Path)).ToList();
}
public static LongPath? GetFirstPath(string id, FileType type)
{
var matchingFiles = Cache.GetIdEntries(id).Where(e => e.FileType == type).ToList();
bool cacheChanged = false;
try
{
//Verify entries exist, but return first matching 'type'
for (int i = 0; i < matchingFiles.Count; i++)
{
if (File.Exists(matchingFiles[i].Path))
return matchingFiles[i].Path;
else
{
matchingFiles.RemoveAt(i);
cacheChanged |= Remove(matchingFiles[i]);
}
}
return null;
}
finally
{
if (cacheChanged)
save();
}
}
private static bool Remove(CacheEntry entry)
{
if (Cache.Remove(entry.Id, entry))
{
Removed?.Invoke(null, entry);
return true;
}
return false;
}
public static void Insert(string id, string path)
{ {
var type = FileTypes.GetFileTypeFromPath(path); var type = FileTypes.GetFileTypeFromPath(path);
Insert(new CacheEntry(id, type, path)); Insert(new CacheEntry(id, type, path));
@ -92,7 +110,7 @@ namespace LibationFileManager
public static void Insert(CacheEntry entry) public static void Insert(CacheEntry entry)
{ {
cache.Add(entry); Cache.Add(entry.Id, entry);
Inserted?.Invoke(null, entry); Inserted?.Invoke(null, entry);
save(); save();
} }
@ -102,7 +120,7 @@ namespace LibationFileManager
private static void save() private static void save()
{ {
// create json if not exists // create json if not exists
static void resave() => File.WriteAllText(jsonFile, JsonConvert.SerializeObject(cache.ToList(), Formatting.Indented)); static void resave() => File.WriteAllText(jsonFileV2, JsonConvert.SerializeObject(Cache, Formatting.Indented));
lock (locker) lock (locker)
{ {
@ -112,11 +130,41 @@ namespace LibationFileManager
try { resave(); } try { resave(); }
catch (IOException ex) catch (IOException ex)
{ {
Serilog.Log.Logger.Error(ex, $"Error saving {FILENAME}"); Serilog.Log.Logger.Error(ex, $"Error saving {FILENAME_V2}");
throw; throw;
} }
} }
} }
} }
}
private class FileCacheV2<TEntry>
{
[JsonProperty]
private readonly ConcurrentDictionary<string, List<TEntry>> Dictionary = new();
public List<TEntry> GetIdEntries(string id)
{
static List<TEntry> empty() => new();
return Dictionary.TryGetValue(id, out var entries) ? entries.ToList() : empty();
}
public void Add(string id, TEntry entry)
{
Dictionary.AddOrUpdate(id, [entry], (id, entries) => { entries.Add(entry); return entries; });
}
public void AddRange(string id, IEnumerable<TEntry> entries)
{
Dictionary.AddOrUpdate(id, entries.ToList(), (id, entries) =>
{
entries.AddRange(entries);
return entries;
});
}
public bool Remove(string id, TEntry entry)
=> Dictionary.TryGetValue(id, out List<TEntry>? entries) && entries.Remove(entry);
}
}
} }

View File

@ -11,6 +11,7 @@ namespace LibationFileManager
{ {
private static Dictionary<string, FileType> dic => new() private static Dictionary<string, FileType> dic => new()
{ {
["aax"] = FileType.AAXC,
["aaxc"] = FileType.AAXC, ["aaxc"] = FileType.AAXC,
["cue"] = FileType.Cue, ["cue"] = FileType.Cue,
["pdf"] = FileType.PDF, ["pdf"] = FileType.PDF,

View File

@ -169,6 +169,16 @@ namespace LibationUiBase
} }
} }
public T FirstOrDefault(Func<T, bool> predicate)
{
lock (lockObject)
{
return Current != null && predicate(Current) ? Current
: _completed.FirstOrDefault(predicate) is T completed ? completed
: _queued.FirstOrDefault(predicate);
}
}
public void MoveQueuePosition(T item, QueuePosition requestedPosition) public void MoveQueuePosition(T item, QueuePosition requestedPosition)
{ {
lock (lockObject) lock (lockObject)

View File

@ -37,6 +37,7 @@ namespace LibationWinForms.Dialogs
inProgressSelectControl.SetDirectoryItems(new() inProgressSelectControl.SetDirectoryItems(new()
{ {
Configuration.KnownDirectories.WinTemp, Configuration.KnownDirectories.WinTemp,
Configuration.KnownDirectories.ApplicationData,
Configuration.KnownDirectories.UserProfile, Configuration.KnownDirectories.UserProfile,
Configuration.KnownDirectories.AppDir, Configuration.KnownDirectories.AppDir,
Configuration.KnownDirectories.MyDocs, Configuration.KnownDirectories.MyDocs,

View File

@ -44,7 +44,8 @@ namespace LibationWinForms.Dialogs
{ {
Configuration.KnownDirectories.UserProfile, Configuration.KnownDirectories.UserProfile,
Configuration.KnownDirectories.AppDir, Configuration.KnownDirectories.AppDir,
Configuration.KnownDirectories.MyDocs Configuration.KnownDirectories.MyDocs,
Configuration.KnownDirectories.MyMusic,
}, },
Configuration.KnownDirectories.UserProfile, Configuration.KnownDirectories.UserProfile,
"Books"); "Books");

View File

@ -105,13 +105,14 @@ namespace LibationWinForms
splitContainer1.Panel2Collapsed = false; splitContainer1.Panel2Collapsed = false;
processBookQueue1.popoutBtn.Visible = true; processBookQueue1.popoutBtn.Visible = true;
} }
Configuration.Instance.SetNonString(splitContainer1.Panel2Collapsed, nameof(splitContainer1.Panel2Collapsed));
toggleQueueHideBtn.Text = splitContainer1.Panel2Collapsed ? "❰❰❰" : "❱❱❱"; toggleQueueHideBtn.Text = splitContainer1.Panel2Collapsed ? "❰❰❰" : "❱❱❱";
} }
private void ToggleQueueHideBtn_Click(object sender, EventArgs e) private void ToggleQueueHideBtn_Click(object sender, EventArgs e)
{ {
SetQueueCollapseState(!splitContainer1.Panel2Collapsed); SetQueueCollapseState(!splitContainer1.Panel2Collapsed);
Configuration.Instance.SetNonString(splitContainer1.Panel2Collapsed, nameof(splitContainer1.Panel2Collapsed));
} }
private void ProcessBookQueue1_PopOut(object sender, EventArgs e) private void ProcessBookQueue1_PopOut(object sender, EventArgs e)

View File

@ -17,7 +17,7 @@ namespace LibationWinForms
//Set this size before restoring form size and position //Set this size before restoring form size and position
splitContainer1.Panel2MinSize = this.DpiScale(350); splitContainer1.Panel2MinSize = this.DpiScale(350);
this.RestoreSizeAndLocation(Configuration.Instance); this.RestoreSizeAndLocation(Configuration.Instance);
this.FormClosing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance); FormClosing += Form1_FormClosing;
// this looks like a perfect opportunity to refactor per below. // this looks like a perfect opportunity to refactor per below.
// since this loses design-time tooling and internal access, for now I'm opting for partial classes // since this loses design-time tooling and internal access, for now I'm opting for partial classes
@ -58,6 +58,14 @@ namespace LibationWinForms
Shown += Form1_Shown; Shown += Form1_Shown;
} }
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
//Always close the queue before saving the form to prevent
//Form1 from getting excessively wide when it's restored.
SetQueueCollapseState(true);
this.SaveSizeAndLocation(Configuration.Instance);
}
private async void Form1_Shown(object sender, EventArgs e) private async void Form1_Shown(object sender, EventArgs e)
{ {
if (Configuration.Instance.FirstLaunch) if (Configuration.Instance.FirstLaunch)

View File

@ -82,7 +82,15 @@ namespace LibationWinForms.ProcessQueue
} }
private bool isBookInQueue(DataLayer.LibraryBook libraryBook) private bool isBookInQueue(DataLayer.LibraryBook libraryBook)
=> Queue.Any(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId); {
var entry = Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId);
if (entry == null)
return false;
else if (entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed)
return !Queue.RemoveCompleted(entry);
else
return true;
}
public void AddDownloadPdf(DataLayer.LibraryBook libraryBook) public void AddDownloadPdf(DataLayer.LibraryBook libraryBook)
=> AddDownloadPdf(new List<DataLayer.LibraryBook>() { libraryBook }); => AddDownloadPdf(new List<DataLayer.LibraryBook>() { libraryBook });

View File

@ -98,7 +98,7 @@ namespace LibationWinForms
if (config.LibationSettingsAreValid) if (config.LibationSettingsAreValid)
return; return;
var defaultLibationFilesDir = Configuration.UserProfile; var defaultLibationFilesDir = Configuration.DefaultLibationFilesDirectory;
// check for existing settings in default location // check for existing settings in default location
var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json"); var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json");
@ -154,7 +154,7 @@ namespace LibationWinForms
// INIT DEFAULT SETTINGS // INIT DEFAULT SETTINGS
// if 'new user' was clicked, or if 'returning user' chose new install: show basic settings dialog // if 'new user' was clicked, or if 'returning user' chose new install: show basic settings dialog
config.Books ??= Path.Combine(defaultLibationFilesDir, "Books"); config.Books ??= Configuration.DefaultBooksDirectory;
if (config.LibationSettingsAreValid) if (config.LibationSettingsAreValid)
return; return;