commit
85b6792468
@ -2,7 +2,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Version>12.0.2.1</Version>
|
<Version>12.0.2.2</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Octokit" Version="14.0.0" />
|
<PackageReference Include="Octokit" Version="14.0.0" />
|
||||||
|
|||||||
@ -15,12 +15,13 @@ using Newtonsoft.Json.Linq;
|
|||||||
using Serilog;
|
using Serilog;
|
||||||
using static DtoImporterService.PerfLogger;
|
using static DtoImporterService.PerfLogger;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace ApplicationServices
|
namespace ApplicationServices
|
||||||
{
|
{
|
||||||
public static class LibraryCommands
|
public static class LibraryCommands
|
||||||
{
|
{
|
||||||
public static event EventHandler<int> ScanBegin;
|
public static event EventHandler<int>? ScanBegin;
|
||||||
public static event EventHandler<int> ScanEnd;
|
public static event EventHandler<int>? ScanEnd;
|
||||||
|
|
||||||
public static bool Scanning { get; private set; }
|
public static bool Scanning { get; private set; }
|
||||||
private static object _lock { get; } = new();
|
private static object _lock { get; } = new();
|
||||||
@ -100,7 +101,7 @@ namespace ApplicationServices
|
|||||||
}
|
}
|
||||||
|
|
||||||
#region FULL LIBRARY scan and import
|
#region FULL LIBRARY scan and import
|
||||||
public static async Task<(int totalCount, int newCount)> ImportAccountAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, params Account[] accounts)
|
public static async Task<(int totalCount, int newCount)> ImportAccountAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, params Account[]? accounts)
|
||||||
{
|
{
|
||||||
logRestart();
|
logRestart();
|
||||||
|
|
||||||
@ -236,7 +237,7 @@ namespace ApplicationServices
|
|||||||
{
|
{
|
||||||
var tasks = new List<Task<List<ImportItem>>>();
|
var tasks = new List<Task<List<ImportItem>>>();
|
||||||
|
|
||||||
await using LogArchiver archiver
|
await using LogArchiver? archiver
|
||||||
= Log.Logger.IsDebugEnabled()
|
= Log.Logger.IsDebugEnabled()
|
||||||
? new LogArchiver(System.IO.Path.Combine(Configuration.Instance.LibationFiles, "LibraryScans.zip"))
|
? new LogArchiver(System.IO.Path.Combine(Configuration.Instance.LibationFiles, "LibraryScans.zip"))
|
||||||
: default;
|
: default;
|
||||||
@ -266,13 +267,13 @@ namespace ApplicationServices
|
|||||||
return importItems;
|
return importItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<List<ImportItem>> scanAccountAsync(ApiExtended apiExtended, Account account, LibraryOptions libraryOptions, LogArchiver archiver)
|
private static async Task<List<ImportItem>> scanAccountAsync(ApiExtended apiExtended, Account account, LibraryOptions libraryOptions, LogArchiver? archiver)
|
||||||
{
|
{
|
||||||
ArgumentValidator.EnsureNotNull(account, nameof(account));
|
ArgumentValidator.EnsureNotNull(account, nameof(account));
|
||||||
|
|
||||||
Log.Logger.Information("ImportLibraryAsync. {@DebugInfo}", new
|
Log.Logger.Information("ImportLibraryAsync. {@DebugInfo}", new
|
||||||
{
|
{
|
||||||
Account = account?.MaskedLogEntry ?? "[null]"
|
Account = account.MaskedLogEntry ?? "[null]"
|
||||||
});
|
});
|
||||||
|
|
||||||
logTime($"pre scanAccountAsync {account.AccountName}");
|
logTime($"pre scanAccountAsync {account.AccountName}");
|
||||||
@ -294,7 +295,7 @@ namespace ApplicationServices
|
|||||||
throw new AggregateException(ex.InnerExceptions);
|
throw new AggregateException(ex.InnerExceptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task logDtoItemsAsync(IEnumerable<AudibleApi.Common.Item> dtoItems, IEnumerable<Exception> exceptions = null)
|
async Task logDtoItemsAsync(IEnumerable<AudibleApi.Common.Item> dtoItems, IEnumerable<Exception>? exceptions = null)
|
||||||
{
|
{
|
||||||
if (archiver is not null)
|
if (archiver is not null)
|
||||||
{
|
{
|
||||||
@ -452,28 +453,28 @@ namespace ApplicationServices
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Occurs when the size of the library changes. ie: books are added or removed</summary>
|
/// <summary>Occurs when the size of the library changes. ie: books are added or removed</summary>
|
||||||
public static event EventHandler<List<LibraryBook>> LibrarySizeChanged;
|
public static event EventHandler<List<LibraryBook>>? LibrarySizeChanged;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Occurs when the size of the library does not change but book(s) details do. Especially when <see cref="UserDefinedItem.Tags"/>, <see cref="UserDefinedItem.BookStatus"/>, or <see cref="UserDefinedItem.PdfStatus"/> changed values are successfully persisted.
|
/// Occurs when the size of the library does not change but book(s) details do. Especially when <see cref="UserDefinedItem.Tags"/>, <see cref="UserDefinedItem.BookStatus"/>, or <see cref="UserDefinedItem.PdfStatus"/> changed values are successfully persisted.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static event EventHandler<IEnumerable<LibraryBook>> BookUserDefinedItemCommitted;
|
public static event EventHandler<IEnumerable<LibraryBook>>? BookUserDefinedItemCommitted;
|
||||||
|
|
||||||
#region Update book details
|
#region Update book details
|
||||||
public static int UpdateUserDefinedItem(
|
public static int UpdateUserDefinedItem(
|
||||||
this LibraryBook lb,
|
this LibraryBook lb,
|
||||||
string tags = null,
|
string? tags = null,
|
||||||
LiberatedStatus? bookStatus = null,
|
LiberatedStatus? bookStatus = null,
|
||||||
LiberatedStatus? pdfStatus = null,
|
LiberatedStatus? pdfStatus = null,
|
||||||
Rating rating = null)
|
Rating? rating = null)
|
||||||
=> new[] { lb }.UpdateUserDefinedItem(tags, bookStatus, pdfStatus, rating);
|
=> new[] { lb }.UpdateUserDefinedItem(tags, bookStatus, pdfStatus, rating);
|
||||||
|
|
||||||
public static int UpdateUserDefinedItem(
|
public static int UpdateUserDefinedItem(
|
||||||
this IEnumerable<LibraryBook> lb,
|
this IEnumerable<LibraryBook> lb,
|
||||||
string tags = null,
|
string? tags = null,
|
||||||
LiberatedStatus? bookStatus = null,
|
LiberatedStatus? bookStatus = null,
|
||||||
LiberatedStatus? pdfStatus = null,
|
LiberatedStatus? pdfStatus = null,
|
||||||
Rating rating = null)
|
Rating? rating = null)
|
||||||
=> updateUserDefinedItem(
|
=> updateUserDefinedItem(
|
||||||
lb,
|
lb,
|
||||||
udi => {
|
udi => {
|
||||||
@ -532,7 +533,8 @@ namespace ApplicationServices
|
|||||||
var udiEntity = context.Entry(book.Book.UserDefinedItem);
|
var udiEntity = context.Entry(book.Book.UserDefinedItem);
|
||||||
|
|
||||||
udiEntity.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
udiEntity.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||||
udiEntity.Reference(udi => udi.Rating).TargetEntry.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
if (udiEntity.Reference(udi => udi.Rating).TargetEntry is Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry<Rating> ratingEntry)
|
||||||
|
ratingEntry.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||||
}
|
}
|
||||||
|
|
||||||
var qtyChanges = context.SaveChanges();
|
var qtyChanges = context.SaveChanges();
|
||||||
@ -598,7 +600,8 @@ namespace ApplicationServices
|
|||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public static LibraryStats GetCounts(IEnumerable<LibraryBook> libraryBooks = null)
|
|
||||||
|
public static LibraryStats GetCounts(IEnumerable<LibraryBook>? libraryBooks = null)
|
||||||
{
|
{
|
||||||
libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking();
|
libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking();
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,9 @@
|
|||||||
<Setter Property="Foreground" Value="{DynamicResource TextControlForeground}" />
|
<Setter Property="Foreground" Value="{DynamicResource TextControlForeground}" />
|
||||||
<Setter Property="Background" Value="Transparent" />
|
<Setter Property="Background" Value="Transparent" />
|
||||||
</ControlTheme>
|
</ControlTheme>
|
||||||
|
<ControlTheme x:Key="{x:Type DataGridCell}" TargetType="DataGridCell" BasedOn="{StaticResource {x:Type DataGridCell}}">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextControlForeground}" />
|
||||||
|
</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" />
|
||||||
|
|||||||
@ -6,6 +6,7 @@ using LibationAvalonia.Dialogs;
|
|||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia
|
namespace LibationAvalonia
|
||||||
{
|
{
|
||||||
internal static class AvaloniaUtils
|
internal static class AvaloniaUtils
|
||||||
@ -14,18 +15,18 @@ namespace LibationAvalonia
|
|||||||
=> GetBrushFromResources(name, Brushes.Transparent);
|
=> GetBrushFromResources(name, Brushes.Transparent);
|
||||||
public static IBrush GetBrushFromResources(string name, IBrush defaultBrush)
|
public static IBrush GetBrushFromResources(string name, IBrush defaultBrush)
|
||||||
{
|
{
|
||||||
if (App.Current.TryGetResource(name, App.Current.ActualThemeVariant, out var value) && value is IBrush brush)
|
if ((App.Current?.TryGetResource(name, App.Current.ActualThemeVariant, out var value) ?? false) && value is IBrush brush)
|
||||||
return brush;
|
return brush;
|
||||||
return defaultBrush;
|
return defaultBrush;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Task<DialogResult> ShowDialogAsync(this DialogWindow dialogWindow, Window owner = null)
|
public static Task<DialogResult> ShowDialogAsync(this DialogWindow dialogWindow, Window? owner = null)
|
||||||
=> dialogWindow.ShowDialog<DialogResult>(owner ?? App.MainWindow);
|
=> dialogWindow.ShowDialog<DialogResult>(owner ?? App.MainWindow);
|
||||||
|
|
||||||
public static Window GetParentWindow(this Control control) => control.GetVisualRoot() as Window;
|
public static Window? GetParentWindow(this Control control) => control.GetVisualRoot() as Window;
|
||||||
|
|
||||||
|
|
||||||
private static Bitmap defaultImage;
|
private static Bitmap? defaultImage;
|
||||||
public static Bitmap TryLoadImageOrDefault(byte[] picture, PictureSize defaultSize = PictureSize.Native)
|
public static Bitmap TryLoadImageOrDefault(byte[] picture, PictureSize defaultSize = PictureSize.Native)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.Primitives;
|
using Avalonia.Controls.Primitives;
|
||||||
|
using Avalonia.Styling;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -30,6 +31,9 @@ namespace LibationAvalonia.Dialogs
|
|||||||
Closing += DialogWindow_Closing;
|
Closing += DialogWindow_Closing;
|
||||||
|
|
||||||
UseCustomTitleBar = Configuration.IsWindows;
|
UseCustomTitleBar = Configuration.IsWindows;
|
||||||
|
|
||||||
|
if (Design.IsDesignMode)
|
||||||
|
RequestedThemeVariant = ThemeVariant.Dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool fixedMinHeight = false;
|
private bool fixedMinHeight = false;
|
||||||
|
|||||||
@ -42,6 +42,17 @@ namespace LibationAvalonia.Dialogs
|
|||||||
public EditQuickFilters()
|
public EditQuickFilters()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
if (Design.IsDesignMode)
|
||||||
|
{
|
||||||
|
Filters = new ObservableCollection<Filter>([
|
||||||
|
new Filter { Name = "Filter 1", FilterString = "[filter1 string]" },
|
||||||
|
new Filter { Name = "Filter 2", FilterString = "[filter2 string]" },
|
||||||
|
new Filter { Name = "Filter 3", FilterString = "[filter3 string]" },
|
||||||
|
new Filter { Name = "Filter 4", FilterString = "[filter4 string]" }
|
||||||
|
]);
|
||||||
|
DataContext = this;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// WARNING: accounts persister will write ANY EDIT to object immediately to file
|
// WARNING: accounts persister will write ANY EDIT to object immediately to file
|
||||||
// here: copy strings and dispose of persister
|
// here: copy strings and dispose of persister
|
||||||
|
|||||||
@ -51,7 +51,7 @@
|
|||||||
<DataGridTemplateColumn Width="Auto" Header="Tag">
|
<DataGridTemplateColumn Width="Auto" Header="Tag">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<TextPresenter Height="18" Margin="10,0,10,0" VerticalAlignment="Center" Text="{Binding Item1}" />
|
<TextBlock Height="18" Margin="10,0,10,0" VerticalAlignment="Center" Text="{Binding Item1}" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</DataGridTemplateColumn>
|
</DataGridTemplateColumn>
|
||||||
@ -59,7 +59,7 @@
|
|||||||
<DataGridTemplateColumn Width="Auto" Header="Description">
|
<DataGridTemplateColumn Width="Auto" Header="Description">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<TextPresenter
|
<TextBlock
|
||||||
Height="18"
|
Height="18"
|
||||||
Margin="10,0,10,0"
|
Margin="10,0,10,0"
|
||||||
VerticalAlignment="Center" Text="{Binding Item2}" />
|
VerticalAlignment="Center" Text="{Binding Item2}" />
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
using Avalonia.Collections;
|
using Avalonia.Collections;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.Documents;
|
using Avalonia.Controls.Documents;
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Styling;
|
||||||
using Dinah.Core;
|
using Dinah.Core;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
@ -11,175 +11,175 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace LibationAvalonia.Dialogs
|
namespace LibationAvalonia.Dialogs;
|
||||||
|
|
||||||
|
public partial class EditTemplateDialog : DialogWindow
|
||||||
{
|
{
|
||||||
public partial class EditTemplateDialog : DialogWindow
|
private EditTemplateViewModel _viewModel;
|
||||||
|
|
||||||
|
public EditTemplateDialog()
|
||||||
{
|
{
|
||||||
private EditTemplateViewModel _viewModel;
|
InitializeComponent();
|
||||||
|
|
||||||
public EditTemplateDialog()
|
if (Design.IsDesignMode)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
_ = Configuration.Instance.LibationFiles;
|
||||||
|
RequestedThemeVariant = ThemeVariant.Dark;
|
||||||
if (Design.IsDesignMode)
|
var editor = TemplateEditor<Templates.FileTemplate>.CreateFilenameEditor(Configuration.Instance.Books, Configuration.Instance.FileTemplate);
|
||||||
{
|
_viewModel = new(Configuration.Instance, editor);
|
||||||
_ = Configuration.Instance.LibationFiles;
|
_viewModel.ResetTextBox(editor.EditingTemplate.TemplateText);
|
||||||
var editor = TemplateEditor<Templates.FileTemplate>.CreateFilenameEditor(Configuration.Instance.Books, Configuration.Instance.FileTemplate);
|
Title = $"Edit {editor.TemplateName}";
|
||||||
_viewModel = new(Configuration.Instance, editor);
|
|
||||||
_viewModel.ResetTextBox(editor.EditingTemplate.TemplateText);
|
|
||||||
Title = $"Edit {editor.TemplateName}";
|
|
||||||
DataContext = _viewModel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public EditTemplateDialog(ITemplateEditor templateEditor) : this()
|
|
||||||
{
|
|
||||||
ArgumentValidator.EnsureNotNull(templateEditor, nameof(templateEditor));
|
|
||||||
|
|
||||||
_viewModel = new EditTemplateViewModel(Configuration.Instance, templateEditor);
|
|
||||||
_viewModel.ResetTextBox(templateEditor.EditingTemplate.TemplateText);
|
|
||||||
Title = $"Edit {templateEditor.TemplateName}";
|
|
||||||
DataContext = _viewModel;
|
DataContext = _viewModel;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public EditTemplateDialog(ITemplateEditor templateEditor) : this()
|
||||||
|
{
|
||||||
|
ArgumentValidator.EnsureNotNull(templateEditor, nameof(templateEditor));
|
||||||
|
|
||||||
|
_viewModel = new EditTemplateViewModel(Configuration.Instance, templateEditor);
|
||||||
|
_viewModel.ResetTextBox(templateEditor.EditingTemplate.TemplateText);
|
||||||
|
Title = $"Edit {templateEditor.TemplateName}";
|
||||||
|
DataContext = _viewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public void EditTemplateViewModel_DoubleTapped(object sender, Avalonia.Input.TappedEventArgs e)
|
public void EditTemplateViewModel_DoubleTapped(object sender, Avalonia.Input.TappedEventArgs e)
|
||||||
|
{
|
||||||
|
var dataGrid = sender as DataGrid;
|
||||||
|
|
||||||
|
var item = (dataGrid.SelectedItem as Tuple<string, string, string>).Item3;
|
||||||
|
if (string.IsNullOrWhiteSpace(item)) return;
|
||||||
|
|
||||||
|
var text = userEditTbox.Text;
|
||||||
|
|
||||||
|
userEditTbox.Text = text.Insert(Math.Min(Math.Max(0, userEditTbox.CaretIndex), text.Length), item);
|
||||||
|
userEditTbox.CaretIndex += item.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task SaveAndCloseAsync()
|
||||||
|
{
|
||||||
|
if (!await _viewModel.Validate())
|
||||||
|
return;
|
||||||
|
|
||||||
|
await base.SaveAndCloseAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void SaveButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
|
=> await SaveAndCloseAsync();
|
||||||
|
|
||||||
|
private class EditTemplateViewModel : ViewModels.ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly Configuration config;
|
||||||
|
public InlineCollection Inlines { get; } = new();
|
||||||
|
public ITemplateEditor TemplateEditor { get; }
|
||||||
|
public EditTemplateViewModel(Configuration configuration, ITemplateEditor templates)
|
||||||
{
|
{
|
||||||
var dataGrid = sender as DataGrid;
|
config = configuration;
|
||||||
|
TemplateEditor = templates;
|
||||||
|
Description = templates.TemplateDescription;
|
||||||
|
ListItems
|
||||||
|
= new AvaloniaList<Tuple<string, string, string>>(
|
||||||
|
TemplateEditor
|
||||||
|
.EditingTemplate
|
||||||
|
.TagsRegistered
|
||||||
|
.Cast<TemplateTags>()
|
||||||
|
.Select(
|
||||||
|
t => new Tuple<string, string, string>(
|
||||||
|
$"<{t.TagName}>",
|
||||||
|
t.Description,
|
||||||
|
t.DefaultValue)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
var item = (dataGrid.SelectedItem as Tuple<string, string, string>).Item3;
|
|
||||||
if (string.IsNullOrWhiteSpace(item)) return;
|
|
||||||
|
|
||||||
var text = userEditTbox.Text;
|
|
||||||
|
|
||||||
userEditTbox.Text = text.Insert(Math.Min(Math.Max(0, userEditTbox.CaretIndex), text.Length), item);
|
|
||||||
userEditTbox.CaretIndex += item.Length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task SaveAndCloseAsync()
|
// hold the work-in-progress value. not guaranteed to be valid
|
||||||
|
private string _userTemplateText;
|
||||||
|
public string UserTemplateText
|
||||||
{
|
{
|
||||||
if (!await _viewModel.Validate())
|
get => _userTemplateText;
|
||||||
return;
|
set
|
||||||
|
|
||||||
await base.SaveAndCloseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void SaveButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
|
||||||
=> await SaveAndCloseAsync();
|
|
||||||
|
|
||||||
private class EditTemplateViewModel : ViewModels.ViewModelBase
|
|
||||||
{
|
|
||||||
private readonly Configuration config;
|
|
||||||
public InlineCollection Inlines { get; } = new();
|
|
||||||
public ITemplateEditor TemplateEditor { get; }
|
|
||||||
public EditTemplateViewModel(Configuration configuration, ITemplateEditor templates)
|
|
||||||
{
|
{
|
||||||
config = configuration;
|
this.RaiseAndSetIfChanged(ref _userTemplateText, value);
|
||||||
TemplateEditor = templates;
|
templateTb_TextChanged();
|
||||||
Description = templates.TemplateDescription;
|
}
|
||||||
ListItems
|
}
|
||||||
= new AvaloniaList<Tuple<string, string, string>>(
|
|
||||||
|
private string _warningText;
|
||||||
|
public string WarningText { get => _warningText; set => this.RaiseAndSetIfChanged(ref _warningText, value); }
|
||||||
|
|
||||||
|
public string Description { get; }
|
||||||
|
|
||||||
|
public AvaloniaList<Tuple<string, string, string>> ListItems { get; set; }
|
||||||
|
|
||||||
|
public void ResetTextBox(string value) => UserTemplateText = value;
|
||||||
|
public void ResetToDefault() => ResetTextBox(TemplateEditor.DefaultTemplate);
|
||||||
|
|
||||||
|
public async Task<bool> Validate()
|
||||||
|
{
|
||||||
|
if (TemplateEditor.EditingTemplate.IsValid)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var errors
|
||||||
|
= TemplateEditor
|
||||||
|
.EditingTemplate
|
||||||
|
.Errors
|
||||||
|
.Select(err => $"- {err}")
|
||||||
|
.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||||
|
await MessageBox.Show($"This template text is not valid. Errors:\r\n{errors}", "Invalid", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void templateTb_TextChanged()
|
||||||
|
{
|
||||||
|
TemplateEditor.SetTemplateText(UserTemplateText);
|
||||||
|
|
||||||
|
const char ZERO_WIDTH_SPACE = '\u200B';
|
||||||
|
var sing = $"{Path.DirectorySeparatorChar}";
|
||||||
|
|
||||||
|
// result: can wrap long paths. eg:
|
||||||
|
// |-- LINE WRAP BOUNDARIES --|
|
||||||
|
// \books\author with a very <= normal line break on space between words
|
||||||
|
// long name\narrator narrator
|
||||||
|
// \title <= line break on the zero-with space we added before slashes
|
||||||
|
string slashWrap(string val) => val.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}");
|
||||||
|
|
||||||
|
WarningText
|
||||||
|
= !TemplateEditor.EditingTemplate.HasWarnings
|
||||||
|
? ""
|
||||||
|
: "Warning:\r\n" +
|
||||||
TemplateEditor
|
TemplateEditor
|
||||||
.EditingTemplate
|
.EditingTemplate
|
||||||
.TagsRegistered
|
.Warnings
|
||||||
.Cast<TemplateTags>()
|
.Select(err => $"- {err}")
|
||||||
.Select(
|
.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||||
t => new Tuple<string, string, string>(
|
|
||||||
$"<{t.TagName}>",
|
|
||||||
t.Description,
|
|
||||||
t.DefaultValue)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
var bold = FontWeight.Bold;
|
||||||
|
var reg = FontWeight.Normal;
|
||||||
|
|
||||||
// hold the work-in-progress value. not guaranteed to be valid
|
Inlines.Clear();
|
||||||
private string _userTemplateText;
|
|
||||||
public string UserTemplateText
|
if (!TemplateEditor.IsFilePath)
|
||||||
{
|
{
|
||||||
get => _userTemplateText;
|
Inlines.Add(new Run(TemplateEditor.GetName()) { FontWeight = bold });
|
||||||
set
|
return;
|
||||||
{
|
|
||||||
this.RaiseAndSetIfChanged(ref _userTemplateText, value);
|
|
||||||
templateTb_TextChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _warningText;
|
var folder = TemplateEditor.GetFolderName();
|
||||||
public string WarningText { get => _warningText; set => this.RaiseAndSetIfChanged(ref _warningText, value); }
|
var file = TemplateEditor.GetFileName();
|
||||||
|
var ext = config.DecryptToLossy ? "mp3" : "m4b";
|
||||||
|
|
||||||
public string Description { get; }
|
Inlines.Add(new Run(slashWrap(TemplateEditor.BaseDirectory.PathWithoutPrefix)) { FontWeight = reg });
|
||||||
|
Inlines.Add(new Run(sing) { FontWeight = reg });
|
||||||
|
|
||||||
public AvaloniaList<Tuple<string, string, string>> ListItems { get; set; }
|
Inlines.Add(new Run(slashWrap(folder)) { FontWeight = TemplateEditor.IsFolder ? bold : reg });
|
||||||
|
|
||||||
public void ResetTextBox(string value) => UserTemplateText = value;
|
Inlines.Add(new Run(sing));
|
||||||
public void ResetToDefault() => ResetTextBox(TemplateEditor.DefaultTemplate);
|
|
||||||
|
|
||||||
public async Task<bool> Validate()
|
Inlines.Add(new Run(slashWrap(file)) { FontWeight = TemplateEditor.IsFolder ? reg : bold });
|
||||||
{
|
|
||||||
if (TemplateEditor.EditingTemplate.IsValid)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
var errors
|
Inlines.Add(new Run($".{ext}"));
|
||||||
= TemplateEditor
|
|
||||||
.EditingTemplate
|
|
||||||
.Errors
|
|
||||||
.Select(err => $"- {err}")
|
|
||||||
.Aggregate((a, b) => $"{a}\r\n{b}");
|
|
||||||
await MessageBox.Show($"This template text is not valid. Errors:\r\n{errors}", "Invalid", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void templateTb_TextChanged()
|
|
||||||
{
|
|
||||||
TemplateEditor.SetTemplateText(UserTemplateText);
|
|
||||||
|
|
||||||
const char ZERO_WIDTH_SPACE = '\u200B';
|
|
||||||
var sing = $"{Path.DirectorySeparatorChar}";
|
|
||||||
|
|
||||||
// result: can wrap long paths. eg:
|
|
||||||
// |-- LINE WRAP BOUNDARIES --|
|
|
||||||
// \books\author with a very <= normal line break on space between words
|
|
||||||
// long name\narrator narrator
|
|
||||||
// \title <= line break on the zero-with space we added before slashes
|
|
||||||
string slashWrap(string val) => val.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}");
|
|
||||||
|
|
||||||
WarningText
|
|
||||||
= !TemplateEditor.EditingTemplate.HasWarnings
|
|
||||||
? ""
|
|
||||||
: "Warning:\r\n" +
|
|
||||||
TemplateEditor
|
|
||||||
.EditingTemplate
|
|
||||||
.Warnings
|
|
||||||
.Select(err => $"- {err}")
|
|
||||||
.Aggregate((a, b) => $"{a}\r\n{b}");
|
|
||||||
|
|
||||||
var bold = FontWeight.Bold;
|
|
||||||
var reg = FontWeight.Normal;
|
|
||||||
|
|
||||||
Inlines.Clear();
|
|
||||||
|
|
||||||
if (!TemplateEditor.IsFilePath)
|
|
||||||
{
|
|
||||||
Inlines.Add(new Run(TemplateEditor.GetName()) { FontWeight = bold });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var folder = TemplateEditor.GetFolderName();
|
|
||||||
var file = TemplateEditor.GetFileName();
|
|
||||||
var ext = config.DecryptToLossy ? "mp3" : "m4b";
|
|
||||||
|
|
||||||
Inlines.Add(new Run(slashWrap(TemplateEditor.BaseDirectory.PathWithoutPrefix)) { FontWeight = reg });
|
|
||||||
Inlines.Add(new Run(sing) { FontWeight = reg });
|
|
||||||
|
|
||||||
Inlines.Add(new Run(slashWrap(folder)) { FontWeight = TemplateEditor.IsFolder ? bold : reg });
|
|
||||||
|
|
||||||
Inlines.Add(new Run(sing));
|
|
||||||
|
|
||||||
Inlines.Add(new Run(slashWrap(file)) { FontWeight = TemplateEditor.IsFolder ? reg : bold });
|
|
||||||
|
|
||||||
Inlines.Add(new Run($".{ext}"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,17 +6,17 @@ using LibationFileManager;
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia
|
namespace LibationAvalonia
|
||||||
{
|
{
|
||||||
public static class FormSaveExtension
|
public static class FormSaveExtension
|
||||||
{
|
{
|
||||||
static readonly WindowIcon WindowIcon;
|
static readonly WindowIcon? WindowIcon;
|
||||||
static FormSaveExtension()
|
static FormSaveExtension()
|
||||||
{
|
{
|
||||||
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow is not null)
|
WindowIcon = Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow?.Icon is WindowIcon icon
|
||||||
WindowIcon = desktop.MainWindow.Icon;
|
? icon
|
||||||
else
|
: null;
|
||||||
WindowIcon = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SetLibationIcon(this Window form)
|
public static void SetLibationIcon(this Window form)
|
||||||
@ -29,7 +29,7 @@ namespace LibationAvalonia
|
|||||||
if (Design.IsDesignMode) return;
|
if (Design.IsDesignMode) return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var savedState = config.GetNonString<FormSizeAndPosition>(defaultValue: null, form.GetType().Name);
|
var savedState = config.GetNonString<FormSizeAndPosition?>(defaultValue: null, form.GetType().Name);
|
||||||
|
|
||||||
if (savedState is null)
|
if (savedState is null)
|
||||||
return;
|
return;
|
||||||
@ -40,12 +40,14 @@ namespace LibationAvalonia
|
|||||||
savedState.Width = (int)form.Width;
|
savedState.Width = (int)form.Width;
|
||||||
savedState.Height = (int)form.Height;
|
savedState.Height = (int)form.Height;
|
||||||
}
|
}
|
||||||
|
if (form.Screens.Primary is Screen primaryScreen)
|
||||||
// Fit to the current screen size in case the screen resolution changed since the size was last persisted
|
{
|
||||||
if (savedState.Width > form.Screens.Primary.WorkingArea.Width)
|
// Fit to the current screen size in case the screen resolution changed since the size was last persisted
|
||||||
savedState.Width = form.Screens.Primary.WorkingArea.Width;
|
if (savedState.Width > primaryScreen.WorkingArea.Width)
|
||||||
if (savedState.Height > form.Screens.Primary.WorkingArea.Height)
|
savedState.Width = primaryScreen.WorkingArea.Width;
|
||||||
savedState.Height = form.Screens.Primary.WorkingArea.Height;
|
if (savedState.Height > primaryScreen.WorkingArea.Height)
|
||||||
|
savedState.Height = primaryScreen.WorkingArea.Height;
|
||||||
|
}
|
||||||
|
|
||||||
var rect = new PixelRect(savedState.X, savedState.Y, savedState.Width, savedState.Height);
|
var rect = new PixelRect(savedState.X, savedState.Y, savedState.Width, savedState.Height);
|
||||||
|
|
||||||
|
|||||||
@ -5,10 +5,10 @@ using System.Threading.Tasks;
|
|||||||
using ApplicationServices;
|
using ApplicationServices;
|
||||||
using AppScaffolding;
|
using AppScaffolding;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia
|
namespace LibationAvalonia
|
||||||
{
|
{
|
||||||
static class Program
|
static class Program
|
||||||
@ -57,7 +57,7 @@ namespace LibationAvalonia
|
|||||||
App.LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
App.LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
||||||
}
|
}
|
||||||
|
|
||||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(null);
|
BuildAvaloniaApp().StartWithClassicDesktopLifetime([]);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -77,27 +77,27 @@ namespace LibationAvalonia
|
|||||||
private static void LogError(object exceptionObject)
|
private static void LogError(object exceptionObject)
|
||||||
{
|
{
|
||||||
var logError = $"""
|
var logError = $"""
|
||||||
{DateTime.Now} - Libation Crash
|
{DateTime.Now} - Libation Crash
|
||||||
OS {Configuration.OS}
|
OS {Configuration.OS}
|
||||||
Version {LibationScaffolding.BuildVersion}
|
Version {LibationScaffolding.BuildVersion}
|
||||||
ReleaseIdentifier {LibationScaffolding.ReleaseIdentifier}
|
ReleaseIdentifier {LibationScaffolding.ReleaseIdentifier}
|
||||||
InteropFunctionsType {InteropFactory.InteropFunctionsType}
|
InteropFunctionsType {InteropFactory.InteropFunctionsType}
|
||||||
LibationFiles {getConfigValue(c => c.LibationFiles)}
|
LibationFiles {getConfigValue(c => c.LibationFiles)}
|
||||||
Books Folder {getConfigValue(c => c.Books)}
|
Books Folder {getConfigValue(c => c.Books)}
|
||||||
=== EXCEPTION ===
|
=== EXCEPTION ===
|
||||||
{exceptionObject}
|
{exceptionObject}
|
||||||
""";
|
""";
|
||||||
|
|
||||||
var crashLog = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "LibationCrash.log");
|
var crashLog = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "LibationCrash.log");
|
||||||
|
|
||||||
using var sw = new StreamWriter(crashLog, true);
|
using var sw = new StreamWriter(crashLog, true);
|
||||||
sw.WriteLine(logError);
|
sw.WriteLine(logError);
|
||||||
|
|
||||||
static string getConfigValue(Func<Configuration, string> selector)
|
static string getConfigValue(Func<Configuration, string?> selector)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return selector(Configuration.Instance);
|
return selector(Configuration.Instance) ?? "[null]";
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -4,6 +4,7 @@ using DataLayer;
|
|||||||
using LibationUiBase.GridView;
|
using LibationUiBase.GridView;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
public class AvaloniaEntryStatus : EntryStatus, IEntryStatus, IComparable
|
public class AvaloniaEntryStatus : EntryStatus, IEntryStatus, IComparable
|
||||||
@ -17,6 +18,6 @@ namespace LibationAvalonia.ViewModels
|
|||||||
=> AvaloniaUtils.TryLoadImageOrDefault(picture, LibationFileManager.PictureSize._80x80);
|
=> AvaloniaUtils.TryLoadImageOrDefault(picture, LibationFileManager.PictureSize._80x80);
|
||||||
|
|
||||||
//Button icons are handled by LiberateStatusButton
|
//Button icons are handled by LiberateStatusButton
|
||||||
protected override Bitmap GetResourceImage(string rescName) => null;
|
protected override Bitmap? GetResourceImage(string rescName) => null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
public class LiberateStatusButtonViewModel : ViewModelBase
|
public class LiberateStatusButtonViewModel : ViewModelBase
|
||||||
|
|||||||
@ -6,12 +6,13 @@ using ReactiveUI;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
partial class MainVM
|
partial class MainVM
|
||||||
{
|
{
|
||||||
private Task<LibraryCommands.LibraryStats> updateCountsTask;
|
private Task<LibraryCommands.LibraryStats>? updateCountsTask;
|
||||||
private LibraryCommands.LibraryStats _libraryStats;
|
private LibraryCommands.LibraryStats? _libraryStats;
|
||||||
|
|
||||||
/// <summary> The "Begin Book and PDF Backup" menu item header text </summary>
|
/// <summary> The "Begin Book and PDF Backup" menu item header text </summary>
|
||||||
public string BookBackupsToolStripText { get; private set; } = "Begin Book and PDF Backups: 0";
|
public string BookBackupsToolStripText { get; private set; } = "Begin Book and PDF Backups: 0";
|
||||||
@ -19,7 +20,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
public string PdfBackupsToolStripText { get; private set; } = "Begin PDF Only Backups: 0";
|
public string PdfBackupsToolStripText { get; private set; } = "Begin PDF Only Backups: 0";
|
||||||
|
|
||||||
/// <summary> The user's library statistics </summary>
|
/// <summary> The user's library statistics </summary>
|
||||||
public LibraryCommands.LibraryStats LibraryStats
|
public LibraryCommands.LibraryStats? LibraryStats
|
||||||
{
|
{
|
||||||
get => _libraryStats;
|
get => _libraryStats;
|
||||||
set
|
set
|
||||||
@ -27,12 +28,12 @@ namespace LibationAvalonia.ViewModels
|
|||||||
this.RaiseAndSetIfChanged(ref _libraryStats, value);
|
this.RaiseAndSetIfChanged(ref _libraryStats, value);
|
||||||
|
|
||||||
BookBackupsToolStripText
|
BookBackupsToolStripText
|
||||||
= LibraryStats.HasPendingBooks
|
= LibraryStats?.HasPendingBooks ?? false
|
||||||
? "Begin " + menufyText($"Book and PDF Backups: {LibraryStats.PendingBooks} remaining")
|
? "Begin " + menufyText($"Book and PDF Backups: {LibraryStats.PendingBooks} remaining")
|
||||||
: "All books have been liberated";
|
: "All books have been liberated";
|
||||||
|
|
||||||
PdfBackupsToolStripText
|
PdfBackupsToolStripText
|
||||||
= LibraryStats.pdfsNotDownloaded > 0
|
= LibraryStats?.pdfsNotDownloaded > 0
|
||||||
? "Begin " + menufyText($"PDF Only Backups: {LibraryStats.pdfsNotDownloaded} remaining")
|
? "Begin " + menufyText($"PDF Only Backups: {LibraryStats.pdfsNotDownloaded} remaining")
|
||||||
: "All PDFs have been downloaded";
|
: "All PDFs have been downloaded";
|
||||||
|
|
||||||
@ -48,7 +49,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
=> await SetBackupCountsAsync(null);
|
=> await SetBackupCountsAsync(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetBackupCountsAsync(IEnumerable<LibraryBook> libraryBooks)
|
public async Task SetBackupCountsAsync(IEnumerable<LibraryBook>? libraryBooks)
|
||||||
{
|
{
|
||||||
if (updateCountsTask?.IsCompleted ?? true)
|
if (updateCountsTask?.IsCompleted ?? true)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -5,6 +5,7 @@ using LibationFileManager;
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
partial class MainVM
|
partial class MainVM
|
||||||
@ -18,7 +19,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
var options = new FilePickerSaveOptions
|
var options = new FilePickerSaveOptions
|
||||||
{
|
{
|
||||||
Title = "Where to export Library",
|
Title = "Where to export Library",
|
||||||
SuggestedStartLocation = await MainWindow.StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books.PathWithoutPrefix),
|
SuggestedStartLocation = await MainWindow.StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books?.PathWithoutPrefix ?? Configuration.DefaultBooksDirectory),
|
||||||
SuggestedFileName = $"Libation Library Export {DateTime.Now:yyyy-MM-dd}",
|
SuggestedFileName = $"Libation Library Export {DateTime.Now:yyyy-MM-dd}",
|
||||||
DefaultExtension = "xlsx",
|
DefaultExtension = "xlsx",
|
||||||
ShowOverwritePrompt = true,
|
ShowOverwritePrompt = true,
|
||||||
@ -41,7 +42,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
AppleUniformTypeIdentifiers = new[] { "public.json" }
|
AppleUniformTypeIdentifiers = new[] { "public.json" }
|
||||||
},
|
},
|
||||||
new("All files (*.*)") { Patterns = new[] { "*" } }
|
new("All files (*.*)") { Patterns = new[] { "*" } }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var selectedFile = (await MainWindow.StorageProvider.SaveFilePickerAsync(options))?.TryGetLocalPath();
|
var selectedFile = (await MainWindow.StorageProvider.SaveFilePickerAsync(options))?.TryGetLocalPath();
|
||||||
|
|||||||
@ -9,16 +9,17 @@ using System;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
partial class MainVM
|
partial class MainVM
|
||||||
{
|
{
|
||||||
private QuickFilters.NamedFilter lastGoodFilter = new(string.Empty, null);
|
private QuickFilters.NamedFilter? lastGoodFilter = new(string.Empty, null);
|
||||||
private QuickFilters.NamedFilter _selectedNamedFilter = new(string.Empty, null);
|
private QuickFilters.NamedFilter? _selectedNamedFilter = new(string.Empty, null);
|
||||||
private bool _firstFilterIsDefault = true;
|
private bool _firstFilterIsDefault = true;
|
||||||
|
|
||||||
/// <summary> Library filterting query </summary>
|
/// <summary> Library filterting query </summary>
|
||||||
public QuickFilters.NamedFilter SelectedNamedFilter { get => _selectedNamedFilter; set => this.RaiseAndSetIfChanged(ref _selectedNamedFilter, value); }
|
public QuickFilters.NamedFilter? SelectedNamedFilter { get => _selectedNamedFilter; set => this.RaiseAndSetIfChanged(ref _selectedNamedFilter, value); }
|
||||||
public AvaloniaList<Control> QuickFilterMenuItems { get; } = new();
|
public AvaloniaList<Control> QuickFilterMenuItems { get; } = new();
|
||||||
/// <summary> Indicates if the first quick filter is the default filter </summary>
|
/// <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); }
|
public bool FirstFilterIsDefault { get => _firstFilterIsDefault; set => QuickFilters.UseDefault = this.RaiseAndSetIfChanged(ref _firstFilterIsDefault, value); }
|
||||||
@ -50,36 +51,44 @@ namespace LibationAvalonia.ViewModels
|
|||||||
QuickFilterMenuItems.Add(new Separator());
|
QuickFilterMenuItems.Add(new Separator());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddQuickFilterBtn() => QuickFilters.Add(SelectedNamedFilter);
|
public void AddQuickFilterBtn() { if (SelectedNamedFilter != null) QuickFilters.Add(SelectedNamedFilter); }
|
||||||
public async Task FilterBtn() => await PerformFilter(SelectedNamedFilter);
|
public async Task FilterBtn() => await PerformFilter(SelectedNamedFilter);
|
||||||
public async Task FilterHelpBtn() => await new LibationAvalonia.Dialogs.SearchSyntaxDialog().ShowDialog(MainWindow);
|
public async Task FilterHelpBtn() => await new LibationAvalonia.Dialogs.SearchSyntaxDialog().ShowDialog(MainWindow);
|
||||||
public void ToggleFirstFilterIsDefault() => FirstFilterIsDefault = !FirstFilterIsDefault;
|
public void ToggleFirstFilterIsDefault() => FirstFilterIsDefault = !FirstFilterIsDefault;
|
||||||
public async Task EditQuickFiltersAsync() => await new LibationAvalonia.Dialogs.EditQuickFilters().ShowDialog(MainWindow);
|
public async Task EditQuickFiltersAsync() => await new LibationAvalonia.Dialogs.EditQuickFilters().ShowDialog(MainWindow);
|
||||||
public async Task PerformFilter(QuickFilters.NamedFilter namedFilter)
|
public async Task PerformFilter(QuickFilters.NamedFilter? namedFilter)
|
||||||
{
|
{
|
||||||
SelectedNamedFilter = namedFilter;
|
SelectedNamedFilter = namedFilter;
|
||||||
|
var tryFilter = namedFilter?.Filter;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await ProductsDisplay.Filter(namedFilter.Filter);
|
await ProductsDisplay.Filter(tryFilter);
|
||||||
lastGoodFilter = namedFilter;
|
lastGoodFilter = namedFilter;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
await MessageBox.Show($"Bad filter string:\r\n\r\n{ex.Message}", "Bad filter string", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
Serilog.Log.Logger.Error(ex, "Error performing filtering. {@namedFilter} {@lastGoodFilter}", namedFilter, lastGoodFilter);
|
||||||
|
await MessageBox.Show($"Bad filter string: \"{tryFilter}\"\r\n\r\n{ex.Message}", "Bad filter string", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
|
||||||
// re-apply last good filter
|
// re-apply last good filter
|
||||||
await PerformFilter(lastGoodFilter);
|
await PerformFilter(lastGoodFilter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateFiltersMenu(object _ = null, object __ = null)
|
private void updateFiltersMenu(object? _ = null, object? __ = null)
|
||||||
{
|
{
|
||||||
//Clear all filters
|
if (NativeMenu.GetMenu(MainWindow)?.Items[3] is not NativeMenuItem ss ||
|
||||||
var quickFilterNativeMenu = (NativeMenuItem)NativeMenu.GetMenu(MainWindow).Items[3];
|
ss.Menu is not NativeMenu quickFilterNativeMenu)
|
||||||
for (int i = quickFilterNativeMenu.Menu.Items.Count - 1; i >= 3; i--)
|
|
||||||
{
|
{
|
||||||
var command = ((NativeMenuItem)quickFilterNativeMenu.Menu.Items[i]).Command as IDisposable;
|
Serilog.Log.Logger.Error($"Unable to find {nameof(quickFilterNativeMenu)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Clear all filters
|
||||||
|
for (int i = quickFilterNativeMenu.Items.Count - 1; i >= 3; i--)
|
||||||
|
{
|
||||||
|
var command = ((NativeMenuItem)quickFilterNativeMenu.Items[i]).Command as IDisposable;
|
||||||
if (command != null)
|
if (command != null)
|
||||||
{
|
{
|
||||||
var existingBinding = MainWindow.KeyBindings.FirstOrDefault(kb => kb.Command == command);
|
var existingBinding = MainWindow.KeyBindings.FirstOrDefault(kb => kb.Command == command);
|
||||||
@ -89,7 +98,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
command.Dispose();
|
command.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
quickFilterNativeMenu.Menu.Items.RemoveAt(i);
|
quickFilterNativeMenu.Items.RemoveAt(i);
|
||||||
QuickFilterMenuItems.RemoveAt(i);
|
QuickFilterMenuItems.RemoveAt(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,7 +125,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
}
|
}
|
||||||
|
|
||||||
QuickFilterMenuItems.Add(menuItem);
|
QuickFilterMenuItems.Add(menuItem);
|
||||||
quickFilterNativeMenu.Menu.Items.Add(nativeMenuItem);
|
quickFilterNativeMenu.Items.Add(nativeMenuItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
public partial class MainVM
|
public partial class MainVM
|
||||||
@ -90,7 +91,9 @@ namespace LibationAvalonia.ViewModels
|
|||||||
public async Task ScanAccountAsync()
|
public async Task ScanAccountAsync()
|
||||||
{
|
{
|
||||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||||
await scanLibrariesAsync(persister.AccountsSettings.GetAll().FirstOrDefault());
|
var firstAccount = persister.AccountsSettings.GetAll().FirstOrDefault();
|
||||||
|
if (firstAccount != null)
|
||||||
|
await scanLibrariesAsync(firstAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ScanAllAccountsAsync()
|
public async Task ScanAllAccountsAsync()
|
||||||
@ -194,7 +197,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
await ProductsDisplay.ScanAndRemoveBooksAsync(accounts);
|
await ProductsDisplay.ScanAndRemoveBooksAsync(accounts);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task scanLibrariesAsync(params Account[] accounts)
|
private async Task scanLibrariesAsync(params Account[]? accounts)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -218,37 +221,44 @@ namespace LibationAvalonia.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void refreshImportMenu(object _ = null, EventArgs __ = null)
|
private void refreshImportMenu(object? _ = null, EventArgs? __ = null)
|
||||||
{
|
{
|
||||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||||
AccountsCount = persister.AccountsSettings.Accounts.Count;
|
AccountsCount = persister.AccountsSettings.Accounts.Count;
|
||||||
|
|
||||||
var importMenuItem = (NativeMenuItem)NativeMenu.GetMenu(MainWindow).Items[0];
|
|
||||||
|
|
||||||
for (int i = importMenuItem.Menu.Items.Count - 1; i >= 2; i--)
|
if (NativeMenu.GetMenu(MainWindow)?.Items[0] is not NativeMenuItem ss ||
|
||||||
importMenuItem.Menu.Items.RemoveAt(i);
|
ss.Menu is not NativeMenu importMenuItem)
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Error($"Unable to find {nameof(importMenuItem)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for (int i = importMenuItem.Items.Count - 1; i >= 2; i--)
|
||||||
|
importMenuItem.Items.RemoveAt(i);
|
||||||
|
|
||||||
if (AccountsCount < 1)
|
if (AccountsCount < 1)
|
||||||
{
|
{
|
||||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "No accounts yet. Add Account...", Command = ReactiveCommand.Create(AddAccountsAsync) });
|
importMenuItem.Items.Add(new NativeMenuItem { Header = "No accounts yet. Add Account...", Command = ReactiveCommand.Create(AddAccountsAsync) });
|
||||||
}
|
}
|
||||||
else if (AccountsCount == 1)
|
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.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.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)});
|
importMenuItem.Items.Add(new NativeMenuItem { Header = "Remove Library Books", Command = ReactiveCommand.Create(RemoveBooksAsync), Gesture = new KeyGesture(Key.R, KeyModifiers.Alt | KeyModifiers.Meta) });
|
||||||
}
|
}
|
||||||
else
|
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.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.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.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.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.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.Items.Add(new NativeMenuItemSeparator());
|
||||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Locate Audiobooks...", Command = ReactiveCommand.Create(LocateAudiobooksAsync) });
|
importMenuItem.Items.Add(new NativeMenuItem { Header = "Locate Audiobooks...", Command = ReactiveCommand.Create(LocateAudiobooksAsync) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DataLayer;
|
using DataLayer;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
partial class MainVM
|
partial class MainVM
|
||||||
|
|||||||
@ -6,6 +6,7 @@ using Dinah.Core;
|
|||||||
using LibationUiBase.GridView;
|
using LibationUiBase.GridView;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
partial class MainVM
|
partial class MainVM
|
||||||
|
|||||||
@ -6,6 +6,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
partial class MainVM
|
partial class MainVM
|
||||||
@ -50,7 +51,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private List<(string AccountId, string LocaleName)> preSaveDefaultAccounts;
|
private List<(string AccountId, string LocaleName)>? preSaveDefaultAccounts;
|
||||||
private List<(string AccountId, string LocaleName)> getDefaultAccounts()
|
private List<(string AccountId, string LocaleName)> getDefaultAccounts()
|
||||||
{
|
{
|
||||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||||
@ -61,17 +62,17 @@ namespace LibationAvalonia.ViewModels
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void accountsPreSave(object sender = null, EventArgs e = null)
|
private void accountsPreSave(object? sender = null, EventArgs? e = null)
|
||||||
=> preSaveDefaultAccounts = getDefaultAccounts();
|
=> preSaveDefaultAccounts = getDefaultAccounts();
|
||||||
|
|
||||||
private void accountsPostSave(object sender = null, EventArgs e = null)
|
private void accountsPostSave(object? sender = null, EventArgs? e = null)
|
||||||
{
|
{
|
||||||
if (getDefaultAccounts().Except(preSaveDefaultAccounts).Any())
|
if (getDefaultAccounts().Except(preSaveDefaultAccounts ?? Enumerable.Empty<(string AccountId, string LocaleName)>()).Any())
|
||||||
startAutoScan();
|
startAutoScan();
|
||||||
}
|
}
|
||||||
|
|
||||||
[PropertyChangeFilter(nameof(Configuration.AutoScan))]
|
[PropertyChangeFilter(nameof(Configuration.AutoScan))]
|
||||||
private void startAutoScan(object sender = null, EventArgs e = null)
|
private void startAutoScan(object? sender = null, EventArgs? e = null)
|
||||||
{
|
{
|
||||||
AutoScanChecked = Configuration.Instance.AutoScan;
|
AutoScanChecked = Configuration.Instance.AutoScan;
|
||||||
if (AutoScanChecked)
|
if (AutoScanChecked)
|
||||||
|
|||||||
@ -4,6 +4,7 @@ using ReactiveUI;
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
partial class MainVM
|
partial class MainVM
|
||||||
@ -12,7 +13,9 @@ namespace LibationAvalonia.ViewModels
|
|||||||
public bool MenuBarVisible { get => _menuBarVisible; set => this.RaiseAndSetIfChanged(ref _menuBarVisible, value); }
|
public bool MenuBarVisible { get => _menuBarVisible; set => this.RaiseAndSetIfChanged(ref _menuBarVisible, value); }
|
||||||
private void Configure_Settings()
|
private void Configure_Settings()
|
||||||
{
|
{
|
||||||
((NativeMenuItem)NativeMenu.GetMenu(App.Current).Items[0]).Command = ReactiveCommand.Create(ShowAboutAsync);
|
if (App.Current is Avalonia.Application app &&
|
||||||
|
NativeMenu.GetMenu(app)?.Items[0] is NativeMenuItem aboutMenu)
|
||||||
|
aboutMenu.Command = ReactiveCommand.Create(ShowAboutAsync);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task ShowAboutAsync() => new LibationAvalonia.Dialogs.AboutDialog().ShowDialog(MainWindow);
|
public Task ShowAboutAsync() => new LibationAvalonia.Dialogs.AboutDialog().ShowDialog(MainWindow);
|
||||||
|
|||||||
@ -6,6 +6,7 @@ using Avalonia.Threading;
|
|||||||
using LibationAvalonia.Dialogs;
|
using LibationAvalonia.Dialogs;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
partial class MainVM
|
partial class MainVM
|
||||||
@ -56,13 +57,13 @@ namespace LibationAvalonia.ViewModels
|
|||||||
this.RaisePropertyChanged(nameof(LiberateVisibleToolStripText_2));
|
this.RaisePropertyChanged(nameof(LiberateVisibleToolStripText_2));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void ProductsDisplay_VisibleCountChanged(object sender, int qty)
|
public async void ProductsDisplay_VisibleCountChanged(object? sender, int qty)
|
||||||
{
|
{
|
||||||
setVisibleCount(qty);
|
setVisibleCount(qty);
|
||||||
await Dispatcher.UIThread.InvokeAsync(setLiberatedVisibleMenuItem);
|
await Dispatcher.UIThread.InvokeAsync(setLiberatedVisibleMenuItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void setLiberatedVisibleMenuItemAsync(object _, object __)
|
private async void setLiberatedVisibleMenuItemAsync(object? _, object __)
|
||||||
=> await Dispatcher.UIThread.InvokeAsync(setLiberatedVisibleMenuItem);
|
=> await Dispatcher.UIThread.InvokeAsync(setLiberatedVisibleMenuItem);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
using LibationUiBase;
|
using LibationUiBase;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
partial class MainVM
|
partial class MainVM
|
||||||
|
|||||||
@ -6,6 +6,7 @@ using ReactiveUI;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
public partial class MainVM : ViewModelBase
|
public partial class MainVM : ViewModelBase
|
||||||
@ -37,11 +38,18 @@ namespace LibationAvalonia.ViewModels
|
|||||||
Configure_VisibleBooks();
|
Configure_VisibleBooks();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void LibraryCommands_LibrarySizeChanged(object sender, List<LibraryBook> fullLibrary)
|
private async void LibraryCommands_LibrarySizeChanged(object? sender, List<LibraryBook> fullLibrary)
|
||||||
{
|
{
|
||||||
await Task.WhenAll(
|
try
|
||||||
SetBackupCountsAsync(fullLibrary),
|
{
|
||||||
Task.Run(() => ProductsDisplay.UpdateGridAsync(fullLibrary)));
|
await Task.WhenAll(
|
||||||
|
SetBackupCountsAsync(fullLibrary),
|
||||||
|
Task.Run(() => ProductsDisplay.UpdateGridAsync(fullLibrary)));
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
await MessageBox.ShowAdminAlert(MainWindow, "An error occurred while updating the library.", "Library Size Change Error", ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string menufyText(string header) => Configuration.IsMacOs ? header : $"_{header}";
|
private static string menufyText(string header) => Configuration.IsMacOs ? header : $"_{header}";
|
||||||
|
|||||||
@ -16,6 +16,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
public enum ProcessBookResult
|
public enum ProcessBookResult
|
||||||
@ -45,28 +46,28 @@ namespace LibationAvalonia.ViewModels
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class ProcessBookViewModel : ViewModelBase
|
public class ProcessBookViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
public event EventHandler Completed;
|
public event EventHandler? Completed;
|
||||||
|
|
||||||
public LibraryBook LibraryBook { get; private set; }
|
public LibraryBook LibraryBook { get; private set; }
|
||||||
|
|
||||||
private ProcessBookResult _result = ProcessBookResult.None;
|
private ProcessBookResult _result = ProcessBookResult.None;
|
||||||
private ProcessBookStatus _status = ProcessBookStatus.Queued;
|
private ProcessBookStatus _status = ProcessBookStatus.Queued;
|
||||||
private string _narrator;
|
private string? _narrator;
|
||||||
private string _author;
|
private string? _author;
|
||||||
private string _title;
|
private string? _title;
|
||||||
private int _progress;
|
private int _progress;
|
||||||
private string _eta;
|
private string? _eta;
|
||||||
private Bitmap _cover;
|
private Bitmap? _cover;
|
||||||
|
|
||||||
#region Properties exposed to the view
|
#region Properties exposed to the view
|
||||||
public ProcessBookResult Result { get => _result; set { this.RaiseAndSetIfChanged(ref _result, value); this.RaisePropertyChanged(nameof(StatusText)); } }
|
public ProcessBookResult Result { get => _result; set { this.RaiseAndSetIfChanged(ref _result, value); this.RaisePropertyChanged(nameof(StatusText)); } }
|
||||||
public ProcessBookStatus Status { get => _status; set { this.RaiseAndSetIfChanged(ref _status, value); this.RaisePropertyChanged(nameof(BackgroundColor)); this.RaisePropertyChanged(nameof(IsFinished)); this.RaisePropertyChanged(nameof(IsDownloading)); this.RaisePropertyChanged(nameof(Queued)); } }
|
public ProcessBookStatus Status { get => _status; set { this.RaiseAndSetIfChanged(ref _status, value); this.RaisePropertyChanged(nameof(BackgroundColor)); this.RaisePropertyChanged(nameof(IsFinished)); this.RaisePropertyChanged(nameof(IsDownloading)); this.RaisePropertyChanged(nameof(Queued)); } }
|
||||||
public string Narrator { get => _narrator; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _narrator, value)); }
|
public string? Narrator { get => _narrator; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _narrator, value)); }
|
||||||
public string Author { get => _author; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _author, value)); }
|
public string? Author { get => _author; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _author, value)); }
|
||||||
public string Title { get => _title; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _title, value)); }
|
public string? Title { get => _title; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _title, value)); }
|
||||||
public int Progress { get => _progress; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _progress, value)); }
|
public int Progress { get => _progress; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _progress, value)); }
|
||||||
public string ETA { get => _eta; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _eta, value)); }
|
public string? ETA { get => _eta; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _eta, value)); }
|
||||||
public Bitmap Cover { get => _cover; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _cover, value)); }
|
public Bitmap? Cover { get => _cover; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _cover, value)); }
|
||||||
public bool IsFinished => Status is not ProcessBookStatus.Queued and not ProcessBookStatus.Working;
|
public bool IsFinished => Status is not ProcessBookStatus.Queued and not ProcessBookStatus.Working;
|
||||||
public bool IsDownloading => Status is ProcessBookStatus.Working;
|
public bool IsDownloading => Status is ProcessBookStatus.Working;
|
||||||
public bool Queued => Status is ProcessBookStatus.Queued;
|
public bool Queued => Status is ProcessBookStatus.Queued;
|
||||||
@ -95,8 +96,8 @@ namespace LibationAvalonia.ViewModels
|
|||||||
|
|
||||||
private TimeSpan TimeRemaining { set { ETA = $"ETA: {value:mm\\:ss}"; } }
|
private TimeSpan TimeRemaining { set { ETA = $"ETA: {value:mm\\:ss}"; } }
|
||||||
private Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke();
|
private Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke();
|
||||||
private Processable NextProcessable() => _currentProcessable = null;
|
private Processable? NextProcessable() => _currentProcessable = null;
|
||||||
private Processable _currentProcessable;
|
private Processable? _currentProcessable;
|
||||||
private readonly Queue<Func<Processable>> Processes = new();
|
private readonly Queue<Func<Processable>> Processes = new();
|
||||||
private readonly LogMe Logger;
|
private readonly LogMe Logger;
|
||||||
|
|
||||||
@ -118,7 +119,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
_cover = AvaloniaUtils.TryLoadImageOrDefault(picture, PictureSize._80x80);
|
_cover = AvaloniaUtils.TryLoadImageOrDefault(picture, PictureSize._80x80);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
|
private void PictureStorage_PictureCached(object? sender, PictureCachedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Definition.PictureId == LibraryBook.Book.PictureId)
|
if (e.Definition.PictureId == LibraryBook.Book.PictureId)
|
||||||
{
|
{
|
||||||
@ -255,14 +256,14 @@ namespace LibationAvalonia.ViewModels
|
|||||||
|
|
||||||
#region AudioDecodable event handlers
|
#region AudioDecodable event handlers
|
||||||
|
|
||||||
private void AudioDecodable_TitleDiscovered(object sender, string title) => Title = title;
|
private void AudioDecodable_TitleDiscovered(object? sender, string title) => Title = title;
|
||||||
|
|
||||||
private void AudioDecodable_AuthorsDiscovered(object sender, string authors) => Author = authors;
|
private void AudioDecodable_AuthorsDiscovered(object? sender, string authors) => Author = authors;
|
||||||
|
|
||||||
private void AudioDecodable_NarratorsDiscovered(object sender, string narrators) => Narrator = narrators;
|
private void AudioDecodable_NarratorsDiscovered(object? sender, string narrators) => Narrator = narrators;
|
||||||
|
|
||||||
|
|
||||||
private byte[] AudioDecodable_RequestCoverArt(object sender, EventArgs e)
|
private byte[] AudioDecodable_RequestCoverArt(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
var quality
|
var quality
|
||||||
= Configuration.Instance.FileDownloadQuality == Configuration.DownloadQuality.High && LibraryBook.Book.PictureLarge is not null
|
= Configuration.Instance.FileDownloadQuality == Configuration.DownloadQuality.High && LibraryBook.Book.PictureLarge is not null
|
||||||
@ -275,7 +276,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
return coverData;
|
return coverData;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AudioDecodable_CoverImageDiscovered(object sender, byte[] coverArt)
|
private void AudioDecodable_CoverImageDiscovered(object? sender, byte[] coverArt)
|
||||||
{
|
{
|
||||||
using var ms = new System.IO.MemoryStream(coverArt);
|
using var ms = new System.IO.MemoryStream(coverArt);
|
||||||
Cover = new Avalonia.Media.Imaging.Bitmap(ms);
|
Cover = new Avalonia.Media.Imaging.Bitmap(ms);
|
||||||
@ -284,10 +285,10 @@ namespace LibationAvalonia.ViewModels
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Streamable event handlers
|
#region Streamable event handlers
|
||||||
private void Streamable_StreamingTimeRemaining(object sender, TimeSpan timeRemaining) => TimeRemaining = timeRemaining;
|
private void Streamable_StreamingTimeRemaining(object? sender, TimeSpan timeRemaining) => TimeRemaining = timeRemaining;
|
||||||
|
|
||||||
|
|
||||||
private void Streamable_StreamingProgressChanged(object sender, Dinah.Core.Net.Http.DownloadProgress downloadProgress)
|
private void Streamable_StreamingProgressChanged(object? sender, Dinah.Core.Net.Http.DownloadProgress downloadProgress)
|
||||||
{
|
{
|
||||||
if (!downloadProgress.ProgressPercentage.HasValue)
|
if (!downloadProgress.ProgressPercentage.HasValue)
|
||||||
return;
|
return;
|
||||||
@ -302,21 +303,25 @@ namespace LibationAvalonia.ViewModels
|
|||||||
|
|
||||||
#region Processable event handlers
|
#region Processable event handlers
|
||||||
|
|
||||||
private async void Processable_Begin(object sender, LibraryBook libraryBook)
|
private async void Processable_Begin(object? sender, LibraryBook libraryBook)
|
||||||
{
|
{
|
||||||
await Dispatcher.UIThread.InvokeAsync(() => Status = ProcessBookStatus.Working);
|
await Dispatcher.UIThread.InvokeAsync(() => Status = ProcessBookStatus.Working);
|
||||||
|
|
||||||
Logger.Info($"{Environment.NewLine}{((Processable)sender).Name} Step, Begin: {libraryBook.Book}");
|
if (sender is Processable processable)
|
||||||
|
Logger.Info($"{Environment.NewLine}{processable.Name} Step, Begin: {libraryBook.Book}");
|
||||||
|
|
||||||
Title = libraryBook.Book.TitleWithSubtitle;
|
Title = libraryBook.Book.TitleWithSubtitle;
|
||||||
Author = libraryBook.Book.AuthorNames();
|
Author = libraryBook.Book.AuthorNames();
|
||||||
Narrator = libraryBook.Book.NarratorNames();
|
Narrator = libraryBook.Book.NarratorNames();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void Processable_Completed(object sender, LibraryBook libraryBook)
|
private async void Processable_Completed(object? sender, LibraryBook libraryBook)
|
||||||
{
|
{
|
||||||
Logger.Info($"{((Processable)sender).Name} Step, Completed: {libraryBook.Book}");
|
if (sender is Processable processable)
|
||||||
UnlinkProcessable((Processable)sender);
|
{
|
||||||
|
Logger.Info($"{processable.Name} Step, Completed: {libraryBook.Book}");
|
||||||
|
UnlinkProcessable(processable);
|
||||||
|
}
|
||||||
|
|
||||||
if (Processes.Count == 0)
|
if (Processes.Count == 0)
|
||||||
{
|
{
|
||||||
@ -375,7 +380,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
: str;
|
: str;
|
||||||
|
|
||||||
details =
|
details =
|
||||||
$@" Title: {libraryBook.Book.TitleWithSubtitle}
|
$@" Title: {libraryBook.Book.TitleWithSubtitle}
|
||||||
ID: {libraryBook.Book.AudibleProductId}
|
ID: {libraryBook.Book.AudibleProductId}
|
||||||
Author: {trunc(libraryBook.Book.AuthorNames())}
|
Author: {trunc(libraryBook.Book.AuthorNames())}
|
||||||
Narr: {trunc(libraryBook.Book.NarratorNames())}";
|
Narr: {trunc(libraryBook.Book.NarratorNames())}";
|
||||||
|
|||||||
@ -12,15 +12,17 @@ using System.Collections.ObjectModel;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
|
|
||||||
public class ProcessQueueViewModel : ViewModelBase, ILogForm
|
public class ProcessQueueViewModel : ViewModelBase, ILogForm
|
||||||
{
|
{
|
||||||
public ObservableCollection<LogEntry> LogEntries { get; } = new();
|
public ObservableCollection<LogEntry> LogEntries { get; } = new();
|
||||||
public AvaloniaList<ProcessBookViewModel> Items { get; } = new();
|
public AvaloniaList<ProcessBookViewModel> Items { get; } = new();
|
||||||
public TrackedQueue<ProcessBookViewModel> Queue { get; }
|
public TrackedQueue<ProcessBookViewModel> Queue { get; }
|
||||||
public ProcessBookViewModel SelectedItem { get; set; }
|
public ProcessBookViewModel? SelectedItem { get; set; }
|
||||||
public Task QueueRunner { get; private set; }
|
public Task? QueueRunner { get; private set; }
|
||||||
public bool Running => !QueueRunner?.IsCompleted ?? false;
|
public bool Running => !QueueRunner?.IsCompleted ?? false;
|
||||||
|
|
||||||
private readonly LogMe Logger;
|
private readonly LogMe Logger;
|
||||||
@ -41,14 +43,14 @@ namespace LibationAvalonia.ViewModels
|
|||||||
private int _completedCount;
|
private int _completedCount;
|
||||||
private int _errorCount;
|
private int _errorCount;
|
||||||
private int _queuedCount;
|
private int _queuedCount;
|
||||||
private string _runningTime;
|
private string? _runningTime;
|
||||||
private bool _progressBarVisible;
|
private bool _progressBarVisible;
|
||||||
private decimal _speedLimit;
|
private decimal _speedLimit;
|
||||||
|
|
||||||
public int CompletedCount { get => _completedCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _completedCount, value); this.RaisePropertyChanged(nameof(AnyCompleted)); }); }
|
public int CompletedCount { get => _completedCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _completedCount, value); this.RaisePropertyChanged(nameof(AnyCompleted)); }); }
|
||||||
public int QueuedCount { get => _queuedCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _queuedCount, value); this.RaisePropertyChanged(nameof(AnyQueued)); }); }
|
public int QueuedCount { get => _queuedCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _queuedCount, value); this.RaisePropertyChanged(nameof(AnyQueued)); }); }
|
||||||
public int ErrorCount { get => _errorCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _errorCount, value); this.RaisePropertyChanged(nameof(AnyErrors)); }); }
|
public int ErrorCount { get => _errorCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _errorCount, value); this.RaisePropertyChanged(nameof(AnyErrors)); }); }
|
||||||
public string RunningTime { get => _runningTime; set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _runningTime, value); }); }
|
public string? RunningTime { get => _runningTime; set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _runningTime, value); }); }
|
||||||
public bool ProgressBarVisible { get => _progressBarVisible; set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _progressBarVisible, value); }); }
|
public bool ProgressBarVisible { get => _progressBarVisible; set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _progressBarVisible, value); }); }
|
||||||
public bool AnyCompleted => CompletedCount > 0;
|
public bool AnyCompleted => CompletedCount > 0;
|
||||||
public bool AnyQueued => QueuedCount > 0;
|
public bool AnyQueued => QueuedCount > 0;
|
||||||
@ -89,7 +91,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
|
|
||||||
public decimal SpeedLimitIncrement { get; private set; }
|
public decimal SpeedLimitIncrement { get; private set; }
|
||||||
|
|
||||||
private async void Queue_CompletedCountChanged(object sender, int e)
|
private async void Queue_CompletedCountChanged(object? sender, int e)
|
||||||
{
|
{
|
||||||
int errCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.FailedAbort or ProcessBookResult.FailedSkip or ProcessBookResult.FailedRetry or ProcessBookResult.ValidationFail);
|
int errCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.FailedAbort or ProcessBookResult.FailedSkip or ProcessBookResult.FailedRetry or ProcessBookResult.ValidationFail);
|
||||||
int completeCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.Success);
|
int completeCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.Success);
|
||||||
@ -98,7 +100,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
CompletedCount = completeCount;
|
CompletedCount = completeCount;
|
||||||
await Dispatcher.UIThread.InvokeAsync(() => this.RaisePropertyChanged(nameof(Progress)));
|
await Dispatcher.UIThread.InvokeAsync(() => this.RaisePropertyChanged(nameof(Progress)));
|
||||||
}
|
}
|
||||||
private async void Queue_QueuededCountChanged(object sender, int cueCount)
|
private async void Queue_QueuededCountChanged(object? sender, int cueCount)
|
||||||
{
|
{
|
||||||
QueuedCount = cueCount;
|
QueuedCount = cueCount;
|
||||||
await Dispatcher.UIThread.InvokeAsync(() => this.RaisePropertyChanged(nameof(Progress)));
|
await Dispatcher.UIThread.InvokeAsync(() => this.RaisePropertyChanged(nameof(Progress)));
|
||||||
@ -218,13 +220,17 @@ namespace LibationAvalonia.ViewModels
|
|||||||
|
|
||||||
while (Queue.MoveNext())
|
while (Queue.MoveNext())
|
||||||
{
|
{
|
||||||
var nextBook = Queue.Current;
|
if (Queue.Current is not ProcessBookViewModel nextBook)
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Information("Current queue item is empty.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
Serilog.Log.Logger.Information("Begin processing queued item. {item_LibraryBook}", nextBook?.LibraryBook);
|
Serilog.Log.Logger.Information("Begin processing queued item. {item_LibraryBook}", nextBook.LibraryBook);
|
||||||
|
|
||||||
var result = await nextBook.ProcessOneAsync();
|
var result = await nextBook.ProcessOneAsync();
|
||||||
|
|
||||||
Serilog.Log.Logger.Information("Completed processing queued item: {item_LibraryBook}\r\nResult: {result}", nextBook?.LibraryBook, result);
|
Serilog.Log.Logger.Information("Completed processing queued item: {item_LibraryBook}\r\nResult: {result}", nextBook.LibraryBook, result);
|
||||||
|
|
||||||
if (result == ProcessBookResult.ValidationFail)
|
if (result == ProcessBookResult.ValidationFail)
|
||||||
Queue.ClearCurrent();
|
Queue.ClearCurrent();
|
||||||
@ -256,7 +262,7 @@ This error appears to be caused by a temporary interruption of service that some
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CounterTimer_Tick(object state)
|
private void CounterTimer_Tick(object? state)
|
||||||
{
|
{
|
||||||
string timeToStr(TimeSpan time)
|
string timeToStr(TimeSpan time)
|
||||||
{
|
{
|
||||||
@ -273,6 +279,6 @@ This error appears to be caused by a temporary interruption of service that some
|
|||||||
{
|
{
|
||||||
public DateTime LogDate { get; init; }
|
public DateTime LogDate { get; init; }
|
||||||
public string LogDateString => LogDate.ToShortTimeString();
|
public string LogDateString => LogDate.ToShortTimeString();
|
||||||
public string LogMessage { get; init; }
|
public string? LogMessage { get; init; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -104,6 +104,9 @@ namespace LibationAvalonia.ViewModels
|
|||||||
|
|
||||||
internal async Task BindToGridAsync(List<LibraryBook> dbBooks)
|
internal async Task BindToGridAsync(List<LibraryBook> dbBooks)
|
||||||
{
|
{
|
||||||
|
if (dbBooks == null)
|
||||||
|
throw new ArgumentNullException(nameof(dbBooks));
|
||||||
|
|
||||||
//Get the UI thread's synchronization context and set it on the current thread to ensure
|
//Get the UI thread's synchronization context and set it on the current thread to ensure
|
||||||
//it's available for GetAllProductsAsync and GetAllSeriesEntriesAsync
|
//it's available for GetAllProductsAsync and GetAllSeriesEntriesAsync
|
||||||
var sc = await Dispatcher.UIThread.InvokeAsync(() => AvaloniaSynchronizationContext.Current);
|
var sc = await Dispatcher.UIThread.InvokeAsync(() => AvaloniaSynchronizationContext.Current);
|
||||||
@ -155,12 +158,11 @@ namespace LibationAvalonia.ViewModels
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal async Task UpdateGridAsync(List<LibraryBook> dbBooks)
|
internal async Task UpdateGridAsync(List<LibraryBook> dbBooks)
|
||||||
{
|
{
|
||||||
|
if (dbBooks == null)
|
||||||
|
throw new ArgumentNullException(nameof(dbBooks));
|
||||||
|
|
||||||
if (GridEntries == null)
|
if (GridEntries == null)
|
||||||
{
|
throw new InvalidOperationException($"Must call {nameof(BindToGridAsync)} before calling {nameof(UpdateGridAsync)}");
|
||||||
//always bind before updating. Binding creates GridEntries.
|
|
||||||
await BindToGridAsync(dbBooks);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Add new or update existing grid entries
|
#region Add new or update existing grid entries
|
||||||
|
|
||||||
|
|||||||
@ -3,17 +3,18 @@ using LibationUiBase.GridView;
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
internal class RowComparer : RowComparerBase
|
internal class RowComparer : RowComparerBase
|
||||||
{
|
{
|
||||||
private static readonly PropertyInfo HeaderCellPi = typeof(DataGridColumn).GetProperty("HeaderCell", BindingFlags.NonPublic | BindingFlags.Instance);
|
private static readonly PropertyInfo? HeaderCellPi = typeof(DataGridColumn).GetProperty("HeaderCell", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
private static readonly PropertyInfo CurrentSortingStatePi = typeof(DataGridColumnHeader).GetProperty("CurrentSortingState", BindingFlags.NonPublic | BindingFlags.Instance);
|
private static readonly PropertyInfo? CurrentSortingStatePi = typeof(DataGridColumnHeader).GetProperty("CurrentSortingState", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
|
||||||
private DataGridColumn Column { get; init; }
|
private DataGridColumn? Column { get; }
|
||||||
public override string PropertyName { get; set; }
|
public override string? PropertyName { get; set; }
|
||||||
|
|
||||||
public RowComparer(DataGridColumn column)
|
public RowComparer(DataGridColumn? column)
|
||||||
{
|
{
|
||||||
Column = column;
|
Column = column;
|
||||||
PropertyName = Column?.SortMemberPath ?? nameof(IGridEntry.DateAdded);
|
PropertyName = Column?.SortMemberPath ?? nameof(IGridEntry.DateAdded);
|
||||||
@ -22,7 +23,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
//Avalonia doesn't expose the column's CurrentSortingState, so we must get it through reflection
|
//Avalonia doesn't expose the column's CurrentSortingState, so we must get it through reflection
|
||||||
protected override ListSortDirection GetSortOrder()
|
protected override ListSortDirection GetSortOrder()
|
||||||
=> Column is null ? ListSortDirection.Descending
|
=> Column is null ? ListSortDirection.Descending
|
||||||
: CurrentSortingStatePi.GetValue(HeaderCellPi.GetValue(Column)) is ListSortDirection lsd ? lsd
|
: CurrentSortingStatePi?.GetValue(HeaderCellPi?.GetValue(Column)) is ListSortDirection lsd ? lsd
|
||||||
: ListSortDirection.Descending;
|
: ListSortDirection.Descending;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ using ReactiveUI;
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels.Settings
|
namespace LibationAvalonia.ViewModels.Settings
|
||||||
{
|
{
|
||||||
public class AudioSettingsVM : ViewModelBase
|
public class AudioSettingsVM : ViewModelBase
|
||||||
@ -33,17 +34,13 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
= new(
|
= new(
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
NAudio.Lame.EncoderQuality.High,
|
NAudio.Lame.EncoderQuality.High,
|
||||||
NAudio.Lame.EncoderQuality.Standard,
|
NAudio.Lame.EncoderQuality.Standard,
|
||||||
NAudio.Lame.EncoderQuality.Fast,
|
NAudio.Lame.EncoderQuality.Fast,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
public AudioSettingsVM(Configuration config)
|
public AudioSettingsVM(Configuration config)
|
||||||
{
|
|
||||||
LoadSettings(config);
|
|
||||||
}
|
|
||||||
public void LoadSettings(Configuration config)
|
|
||||||
{
|
{
|
||||||
CreateCueSheet = config.CreateCueSheet;
|
CreateCueSheet = config.CreateCueSheet;
|
||||||
CombineNestedChapterTitles = config.CombineNestedChapterTitles;
|
CombineNestedChapterTitles = config.CombineNestedChapterTitles;
|
||||||
@ -57,7 +54,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
MergeOpeningAndEndCredits = config.MergeOpeningAndEndCredits;
|
MergeOpeningAndEndCredits = config.MergeOpeningAndEndCredits;
|
||||||
StripAudibleBrandAudio = config.StripAudibleBrandAudio;
|
StripAudibleBrandAudio = config.StripAudibleBrandAudio;
|
||||||
StripUnabridged = config.StripUnabridged;
|
StripUnabridged = config.StripUnabridged;
|
||||||
ChapterTitleTemplate = config.ChapterTitleTemplate;
|
_chapterTitleTemplate = config.ChapterTitleTemplate;
|
||||||
DecryptToLossy = config.DecryptToLossy;
|
DecryptToLossy = config.DecryptToLossy;
|
||||||
MoveMoovToBeginning = config.MoveMoovToBeginning;
|
MoveMoovToBeginning = config.MoveMoovToBeginning;
|
||||||
LameTargetBitrate = config.LameTargetBitrate;
|
LameTargetBitrate = config.LameTargetBitrate;
|
||||||
@ -67,7 +64,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
LameBitrate = config.LameBitrate;
|
LameBitrate = config.LameBitrate;
|
||||||
LameVBRQuality = config.LameVBRQuality;
|
LameVBRQuality = config.LameVBRQuality;
|
||||||
|
|
||||||
SelectedSampleRate = SampleRates.SingleOrDefault(s => s.Value == config.MaxSampleRate);
|
SelectedSampleRate = SampleRates.SingleOrDefault(s => s.Value == config.MaxSampleRate) ?? SampleRates[0];
|
||||||
SelectedEncoderQuality = config.LameEncoderQuality;
|
SelectedEncoderQuality = config.LameEncoderQuality;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ using LibationFileManager;
|
|||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels.Settings
|
namespace LibationAvalonia.ViewModels.Settings
|
||||||
{
|
{
|
||||||
public class DownloadDecryptSettingsVM : ViewModelBase
|
public class DownloadDecryptSettingsVM : ViewModelBase
|
||||||
@ -15,7 +16,16 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
public DownloadDecryptSettingsVM(Configuration config)
|
public DownloadDecryptSettingsVM(Configuration config)
|
||||||
{
|
{
|
||||||
Config = config;
|
Config = config;
|
||||||
LoadSettings(config);
|
BadBookAsk = config.BadBook is Configuration.BadBookAction.Ask;
|
||||||
|
BadBookAbort = config.BadBook is Configuration.BadBookAction.Abort;
|
||||||
|
BadBookRetry = config.BadBook is Configuration.BadBookAction.Retry;
|
||||||
|
BadBookIgnore = config.BadBook is Configuration.BadBookAction.Ignore;
|
||||||
|
_folderTemplate = config.FolderTemplate;
|
||||||
|
_fileTemplate = config.FileTemplate;
|
||||||
|
_chapterFileTemplate = config.ChapterFileTemplate;
|
||||||
|
InProgressDirectory = config.InProgress;
|
||||||
|
UseCoverAsFolderIcon = config.UseCoverAsFolderIcon;
|
||||||
|
SaveMetadataToFile = config.SaveMetadataToFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Configuration.KnownDirectories> KnownDirectories { get; } = new()
|
public List<Configuration.KnownDirectories> KnownDirectories { get; } = new()
|
||||||
@ -28,20 +38,6 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
Configuration.KnownDirectories.LibationFiles
|
Configuration.KnownDirectories.LibationFiles
|
||||||
};
|
};
|
||||||
|
|
||||||
public void LoadSettings(Configuration config)
|
|
||||||
{
|
|
||||||
BadBookAsk = config.BadBook is Configuration.BadBookAction.Ask;
|
|
||||||
BadBookAbort = config.BadBook is Configuration.BadBookAction.Abort;
|
|
||||||
BadBookRetry = config.BadBook is Configuration.BadBookAction.Retry;
|
|
||||||
BadBookIgnore = config.BadBook is Configuration.BadBookAction.Ignore;
|
|
||||||
FolderTemplate = config.FolderTemplate;
|
|
||||||
FileTemplate = config.FileTemplate;
|
|
||||||
ChapterFileTemplate = config.ChapterFileTemplate;
|
|
||||||
InProgressDirectory = config.InProgress;
|
|
||||||
UseCoverAsFolderIcon = config.UseCoverAsFolderIcon;
|
|
||||||
SaveMetadataToFile = config.SaveMetadataToFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SaveSettings(Configuration config)
|
public void SaveSettings(Configuration config)
|
||||||
{
|
{
|
||||||
config.BadBook
|
config.BadBook
|
||||||
@ -62,10 +58,10 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
public string UseCoverAsFolderIconText { get; } = Configuration.GetDescription(nameof(Configuration.UseCoverAsFolderIcon));
|
public string UseCoverAsFolderIconText { get; } = Configuration.GetDescription(nameof(Configuration.UseCoverAsFolderIcon));
|
||||||
public string SaveMetadataToFileText { get; } = Configuration.GetDescription(nameof(Configuration.SaveMetadataToFile));
|
public string SaveMetadataToFileText { get; } = Configuration.GetDescription(nameof(Configuration.SaveMetadataToFile));
|
||||||
public string BadBookGroupboxText { get; } = Configuration.GetDescription(nameof(Configuration.BadBook));
|
public string BadBookGroupboxText { get; } = Configuration.GetDescription(nameof(Configuration.BadBook));
|
||||||
public string BadBookAskText { get; } = Configuration.BadBookAction.Ask.GetDescription();
|
public string BadBookAskText { get; } = Configuration.BadBookAction.Ask.GetDescription() ?? nameof(Configuration.BadBookAction.Ask);
|
||||||
public string BadBookAbortText { get; } = Configuration.BadBookAction.Abort.GetDescription();
|
public string BadBookAbortText { get; } = Configuration.BadBookAction.Abort.GetDescription() ?? nameof(Configuration.BadBookAction.Abort);
|
||||||
public string BadBookRetryText { get; } = Configuration.BadBookAction.Retry.GetDescription();
|
public string BadBookRetryText { get; } = Configuration.BadBookAction.Retry.GetDescription() ?? nameof(Configuration.BadBookAction.Retry);
|
||||||
public string BadBookIgnoreText { get; } = Configuration.BadBookAction.Ignore.GetDescription();
|
public string BadBookIgnoreText { get; } = Configuration.BadBookAction.Ignore.GetDescription() ?? nameof(Configuration.BadBookAction.Ignore);
|
||||||
public string FolderTemplateText { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate));
|
public string FolderTemplateText { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate));
|
||||||
public string FileTemplateText { get; } = Configuration.GetDescription(nameof(Configuration.FileTemplate));
|
public string FileTemplateText { get; } = Configuration.GetDescription(nameof(Configuration.FileTemplate));
|
||||||
public string ChapterFileTemplateText { get; } = Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate));
|
public string ChapterFileTemplateText { get; } = Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate));
|
||||||
|
|||||||
@ -1,15 +1,11 @@
|
|||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels.Settings
|
namespace LibationAvalonia.ViewModels.Settings
|
||||||
{
|
{
|
||||||
public class ImportSettingsVM
|
public class ImportSettingsVM
|
||||||
{
|
{
|
||||||
public ImportSettingsVM(Configuration config)
|
public ImportSettingsVM(Configuration config)
|
||||||
{
|
|
||||||
LoadSettings(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LoadSettings(Configuration config)
|
|
||||||
{
|
{
|
||||||
AutoScan = config.AutoScan;
|
AutoScan = config.AutoScan;
|
||||||
ShowImportedStats = config.ShowImportedStats;
|
ShowImportedStats = config.ShowImportedStats;
|
||||||
|
|||||||
@ -7,6 +7,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels.Settings
|
namespace LibationAvalonia.ViewModels.Settings
|
||||||
{
|
{
|
||||||
public class ImportantSettingsVM : ViewModelBase
|
public class ImportantSettingsVM : ViewModelBase
|
||||||
@ -18,12 +19,8 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
public ImportantSettingsVM(Configuration config)
|
public ImportantSettingsVM(Configuration config)
|
||||||
{
|
{
|
||||||
this.config = config;
|
this.config = config;
|
||||||
LoadSettings(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LoadSettings(Configuration config)
|
BooksDirectory = config.Books?.PathWithoutPrefix ?? Configuration.DefaultBooksDirectory;
|
||||||
{
|
|
||||||
BooksDirectory = config.Books.PathWithoutPrefix;
|
|
||||||
SavePodcastsToParentFolder = config.SavePodcastsToParentFolder;
|
SavePodcastsToParentFolder = config.SavePodcastsToParentFolder;
|
||||||
OverwriteExisting = config.OverwriteExisting;
|
OverwriteExisting = config.OverwriteExisting;
|
||||||
CreationTime = DateTimeSources.SingleOrDefault(v => v.Value == config.CreationTime) ?? DateTimeSources[0];
|
CreationTime = DateTimeSources.SingleOrDefault(v => v.Value == config.CreationTime) ?? DateTimeSources[0];
|
||||||
@ -32,9 +29,9 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
GridScaleFactor = scaleFactorToLinearRange(config.GridScaleFactor);
|
GridScaleFactor = scaleFactorToLinearRange(config.GridScaleFactor);
|
||||||
GridFontScaleFactor = scaleFactorToLinearRange(config.GridFontScaleFactor);
|
GridFontScaleFactor = scaleFactorToLinearRange(config.GridFontScaleFactor);
|
||||||
|
|
||||||
ThemeVariant = initialThemeVariant = Configuration.Instance.GetString(propertyName: nameof(ThemeVariant));
|
themeVariant = initialThemeVariant = Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)) ?? "";
|
||||||
if (string.IsNullOrWhiteSpace(initialThemeVariant))
|
if (string.IsNullOrWhiteSpace(initialThemeVariant))
|
||||||
ThemeVariant = initialThemeVariant = "System";
|
themeVariant = initialThemeVariant = "System";
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SaveSettings(Configuration config)
|
public void SaveSettings(Configuration config)
|
||||||
@ -100,14 +97,17 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
get => themeVariant;
|
get => themeVariant;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
|
var changed = !value.Equals(themeVariant);
|
||||||
this.RaiseAndSetIfChanged(ref themeVariant, value);
|
this.RaiseAndSetIfChanged(ref themeVariant, value);
|
||||||
App.Current.RequestedThemeVariant = themeVariant switch
|
|
||||||
{
|
if (changed && App.Current is Avalonia.Application app)
|
||||||
nameof(Avalonia.Styling.ThemeVariant.Dark) => Avalonia.Styling.ThemeVariant.Dark,
|
app.RequestedThemeVariant = themeVariant switch
|
||||||
nameof(Avalonia.Styling.ThemeVariant.Light) => Avalonia.Styling.ThemeVariant.Light,
|
{
|
||||||
// "System"
|
nameof(Avalonia.Styling.ThemeVariant.Dark) => Avalonia.Styling.ThemeVariant.Dark,
|
||||||
_ => Avalonia.Styling.ThemeVariant.Default
|
nameof(Avalonia.Styling.ThemeVariant.Light) => Avalonia.Styling.ThemeVariant.Light,
|
||||||
};
|
// "System"
|
||||||
|
_ => Avalonia.Styling.ThemeVariant.Default
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,7 +30,7 @@ namespace LibationFileManager
|
|||||||
}
|
}
|
||||||
.AsReadOnly();
|
.AsReadOnly();
|
||||||
|
|
||||||
public static string? GetHelpText(string settingName)
|
public static string GetHelpText(string settingName)
|
||||||
=> HelpText.TryGetValue(settingName, out var value) ? value : null;
|
=> HelpText.TryGetValue(settingName, out var value) ? value : "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -82,7 +82,7 @@ namespace LibationFileManager
|
|||||||
|
|
||||||
public string SettingsFilePath => Path.Combine(LibationFiles, "Settings.json");
|
public string SettingsFilePath => Path.Combine(LibationFiles, "Settings.json");
|
||||||
|
|
||||||
public static string? GetDescription(string propertyName)
|
public static string GetDescription(string propertyName)
|
||||||
{
|
{
|
||||||
var attribute = typeof(Configuration)
|
var attribute = typeof(Configuration)
|
||||||
.GetProperty(propertyName)
|
.GetProperty(propertyName)
|
||||||
@ -90,7 +90,7 @@ namespace LibationFileManager
|
|||||||
.SingleOrDefault()
|
.SingleOrDefault()
|
||||||
as DescriptionAttribute;
|
as DescriptionAttribute;
|
||||||
|
|
||||||
return attribute?.Description;
|
return attribute?.Description ?? $"[{propertyName}]";
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Exists(string propertyName) => Settings.Exists(propertyName);
|
public bool Exists(string propertyName) => Settings.Exists(propertyName);
|
||||||
@ -118,12 +118,15 @@ namespace LibationFileManager
|
|||||||
|
|
||||||
// temp/working dir(s) should be outside of dropbox
|
// temp/working dir(s) should be outside of dropbox
|
||||||
[Description("Temporary location of files while they're in process of being downloaded and decrypted.\r\nWhen decryption is complete, the final file will be in Books location\r\nRecommend not using a folder which is backed up real time. Eg: Dropbox, iCloud, Google Drive")]
|
[Description("Temporary location of files while they're in process of being downloaded and decrypted.\r\nWhen decryption is complete, the final file will be in Books location\r\nRecommend not using a folder which is backed up real time. Eg: Dropbox, iCloud, Google Drive")]
|
||||||
public string InProgress { get
|
public string InProgress
|
||||||
|
{
|
||||||
|
get
|
||||||
{
|
{
|
||||||
var tempDir = GetString();
|
var tempDir = GetString();
|
||||||
return string.IsNullOrWhiteSpace(tempDir) ? WinTemp : tempDir;
|
return string.IsNullOrWhiteSpace(tempDir) ? WinTemp : tempDir;
|
||||||
}
|
}
|
||||||
set => SetString(value); }
|
set => SetString(value);
|
||||||
|
}
|
||||||
|
|
||||||
[Description("Allow Libation to fix up audiobook metadata")]
|
[Description("Allow Libation to fix up audiobook metadata")]
|
||||||
public bool AllowLibationFixup { get => GetNonString(defaultValue: true); set => SetNonString(value); }
|
public bool AllowLibationFixup { get => GetNonString(defaultValue: true); set => SetNonString(value); }
|
||||||
@ -162,10 +165,10 @@ namespace LibationFileManager
|
|||||||
public NAudio.Lame.EncoderQuality LameEncoderQuality { get => GetNonString(defaultValue: NAudio.Lame.EncoderQuality.High); set => SetNonString(value); }
|
public NAudio.Lame.EncoderQuality LameEncoderQuality { get => GetNonString(defaultValue: NAudio.Lame.EncoderQuality.High); set => SetNonString(value); }
|
||||||
|
|
||||||
[Description("Lame encoder downsamples to mono")]
|
[Description("Lame encoder downsamples to mono")]
|
||||||
public bool LameDownsampleMono { get => GetNonString(defaultValue: true); set => SetNonString(value); }
|
public bool LameDownsampleMono { get => GetNonString(defaultValue: true); set => SetNonString(value); }
|
||||||
|
|
||||||
[Description("Lame target bitrate [16,320]")]
|
[Description("Lame target bitrate [16,320]")]
|
||||||
public int LameBitrate { get => GetNonString(defaultValue: 64); set => SetNonString(value); }
|
public int LameBitrate { get => GetNonString(defaultValue: 64); set => SetNonString(value); }
|
||||||
|
|
||||||
[Description("Restrict encoder to constant bitrate?")]
|
[Description("Restrict encoder to constant bitrate?")]
|
||||||
public bool LameConstantBitrate { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
public bool LameConstantBitrate { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||||
@ -179,8 +182,8 @@ namespace LibationFileManager
|
|||||||
private static readonly EquatableDictionary<string, bool> DefaultColumns = new(
|
private static readonly EquatableDictionary<string, bool> DefaultColumns = new(
|
||||||
new KeyValuePair<string, bool>[]
|
new KeyValuePair<string, bool>[]
|
||||||
{
|
{
|
||||||
new ("SeriesOrder", false),
|
new ("SeriesOrder", false),
|
||||||
new ("LastDownload", false)
|
new ("LastDownload", false)
|
||||||
});
|
});
|
||||||
|
|
||||||
[Description("A Dictionary of GridView data property names and bool indicating its column's visibility in ProductsGrid")]
|
[Description("A Dictionary of GridView data property names and bool indicating its column's visibility in ProductsGrid")]
|
||||||
|
|||||||
@ -46,7 +46,7 @@ namespace LibationFileManager
|
|||||||
|
|
||||||
// Note that records overload equality automagically, so should be able to
|
// Note that records overload equality automagically, so should be able to
|
||||||
// compare these the same way as comparing simple strings.
|
// compare these the same way as comparing simple strings.
|
||||||
public record NamedFilter(string Filter, string Name)
|
public record NamedFilter(string Filter, string? Name)
|
||||||
{
|
{
|
||||||
public string Filter { get; set; } = Filter;
|
public string Filter { get; set; } = Filter;
|
||||||
public string? Name { get; set; } = Name;
|
public string? Name { get; set; } = Name;
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationUiBase.GridView
|
namespace LibationUiBase.GridView
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -11,67 +12,10 @@ namespace LibationUiBase.GridView
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class RowComparerBase : IComparer, IComparer<IGridEntry>, IComparer<object>
|
public abstract class RowComparerBase : IComparer, IComparer<IGridEntry>, IComparer<object>
|
||||||
{
|
{
|
||||||
public abstract string PropertyName { get; set; }
|
public abstract string? PropertyName { get; set; }
|
||||||
|
|
||||||
public int Compare(object x, object y)
|
public int Compare(object? x, object? y)
|
||||||
{
|
=> Compare(x as IGridEntry, y as IGridEntry);
|
||||||
if (x is null && y is not null) return -1;
|
|
||||||
if (x is not null && y is null) return 1;
|
|
||||||
if (x is null && y is null) return 0;
|
|
||||||
|
|
||||||
var geA = (IGridEntry)x;
|
|
||||||
var geB = (IGridEntry)y;
|
|
||||||
|
|
||||||
var sortDirection = GetSortOrder();
|
|
||||||
|
|
||||||
ISeriesEntry parentA = null;
|
|
||||||
ISeriesEntry parentB = null;
|
|
||||||
|
|
||||||
if (geA is ILibraryBookEntry lbA && lbA.Parent is ISeriesEntry seA)
|
|
||||||
parentA = seA;
|
|
||||||
if (geB is ILibraryBookEntry lbB && lbB.Parent is ISeriesEntry seB)
|
|
||||||
parentB = seB;
|
|
||||||
|
|
||||||
//both a and b are top-level grid entries
|
|
||||||
if (parentA is null && parentB is null)
|
|
||||||
return InternalCompare(geA, geB);
|
|
||||||
|
|
||||||
//a is top-level, b is a child
|
|
||||||
if (parentA is null && parentB is not null)
|
|
||||||
{
|
|
||||||
// b is a child of a, parent is always first
|
|
||||||
if (parentB == geA)
|
|
||||||
return sortDirection is ListSortDirection.Ascending ? -1 : 1;
|
|
||||||
else
|
|
||||||
return InternalCompare(geA, parentB);
|
|
||||||
}
|
|
||||||
|
|
||||||
//a is a child, b is a top-level
|
|
||||||
if (parentA is not null && parentB is null)
|
|
||||||
{
|
|
||||||
// a is a child of b, parent is always first
|
|
||||||
if (parentA == geB)
|
|
||||||
return sortDirection is ListSortDirection.Ascending ? 1 : -1;
|
|
||||||
else
|
|
||||||
return InternalCompare(parentA, geB);
|
|
||||||
}
|
|
||||||
|
|
||||||
//both are children of the same series
|
|
||||||
if (parentA == parentB)
|
|
||||||
{
|
|
||||||
//Podcast episodes usually all have the same PurchaseDate and DateAdded property:
|
|
||||||
//the date that the series was added to the library. So when sorting by PurchaseDate
|
|
||||||
//and DateAdded, compare SeriesOrder instead..
|
|
||||||
return PropertyName switch
|
|
||||||
{
|
|
||||||
nameof(IGridEntry.DateAdded) or nameof (IGridEntry.PurchaseDate) => geA.SeriesOrder.CompareTo(geB.SeriesOrder),
|
|
||||||
_ => InternalCompare(geA, geB),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//a and b are children of different series.
|
|
||||||
return InternalCompare(parentA, parentB);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract ListSortDirection GetSortOrder();
|
protected abstract ListSortDirection GetSortOrder();
|
||||||
|
|
||||||
@ -80,7 +24,7 @@ namespace LibationUiBase.GridView
|
|||||||
var val1 = x.GetMemberValue(PropertyName);
|
var val1 = x.GetMemberValue(PropertyName);
|
||||||
var val2 = y.GetMemberValue(PropertyName);
|
var val2 = y.GetMemberValue(PropertyName);
|
||||||
|
|
||||||
var compare = x.GetMemberComparer(val1.GetType()).Compare(val1, val2);
|
var compare = x.GetMemberComparer(val1.GetType()).Compare(val1, val2);
|
||||||
|
|
||||||
return compare == 0 && x.Liberate.IsSeries && y.Liberate.IsSeries
|
return compare == 0 && x.Liberate.IsSeries && y.Liberate.IsSeries
|
||||||
//Both a and b are series parents and compare as equal, so break the tie.
|
//Both a and b are series parents and compare as equal, so break the tie.
|
||||||
@ -88,9 +32,66 @@ namespace LibationUiBase.GridView
|
|||||||
: compare;
|
: compare;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int Compare(IGridEntry x, IGridEntry y)
|
public int Compare(IGridEntry? geA, IGridEntry? geB)
|
||||||
{
|
{
|
||||||
return Compare((object)x, y);
|
if (geA is null && geB is not null) return -1;
|
||||||
|
if (geA is not null && geB is null) return 1;
|
||||||
|
if (geA is null || geB is null) return 0;
|
||||||
|
|
||||||
|
var sortDirection = GetSortOrder();
|
||||||
|
|
||||||
|
ISeriesEntry? parentA = null;
|
||||||
|
ISeriesEntry? parentB = null;
|
||||||
|
|
||||||
|
if (geA is ILibraryBookEntry lbA && lbA.Parent is ISeriesEntry seA)
|
||||||
|
parentA = seA;
|
||||||
|
if (geB is ILibraryBookEntry lbB && lbB.Parent is ISeriesEntry seB)
|
||||||
|
parentB = seB;
|
||||||
|
|
||||||
|
//both entries are children
|
||||||
|
if (parentA != null && parentB != null)
|
||||||
|
{
|
||||||
|
//both are children of the same series
|
||||||
|
if (parentA == parentB)
|
||||||
|
{
|
||||||
|
//Podcast episodes usually all have the same PurchaseDate and DateAdded property:
|
||||||
|
//the date that the series was added to the library. So when sorting by PurchaseDate
|
||||||
|
//and DateAdded, compare SeriesOrder instead..
|
||||||
|
return PropertyName switch
|
||||||
|
{
|
||||||
|
nameof(IGridEntry.DateAdded) or nameof(IGridEntry.PurchaseDate) => geA.SeriesOrder.CompareTo(geB.SeriesOrder),
|
||||||
|
_ => InternalCompare(geA, geB),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
//a and b are children of different series.
|
||||||
|
return InternalCompare(parentA, parentB);
|
||||||
|
}
|
||||||
|
|
||||||
|
//a is top-level, b is a child
|
||||||
|
else if (parentA is null && parentB is not null)
|
||||||
|
{
|
||||||
|
// b is a child of a, parent is always first
|
||||||
|
if (parentB == geA)
|
||||||
|
return sortDirection is ListSortDirection.Ascending ? -1 : 1;
|
||||||
|
else
|
||||||
|
return InternalCompare(geA, parentB);
|
||||||
|
}
|
||||||
|
//a is a child, b is a top-level
|
||||||
|
else if (parentA is not null && parentB is null)
|
||||||
|
{
|
||||||
|
// a is a child of b, parent is always first
|
||||||
|
if (parentA == geB)
|
||||||
|
return sortDirection is ListSortDirection.Ascending ? 1 : -1;
|
||||||
|
else
|
||||||
|
return InternalCompare(parentA, geB);
|
||||||
|
}
|
||||||
|
//parentA and parentB are null
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//both a and b are top-level grid entries
|
||||||
|
return InternalCompare(geA, geB);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user