commit
85b6792468
@ -2,7 +2,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Version>12.0.2.1</Version>
|
||||
<Version>12.0.2.2</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Octokit" Version="14.0.0" />
|
||||
|
||||
@ -15,12 +15,13 @@ using Newtonsoft.Json.Linq;
|
||||
using Serilog;
|
||||
using static DtoImporterService.PerfLogger;
|
||||
|
||||
#nullable enable
|
||||
namespace ApplicationServices
|
||||
{
|
||||
public static class LibraryCommands
|
||||
{
|
||||
public static event EventHandler<int> ScanBegin;
|
||||
public static event EventHandler<int> ScanEnd;
|
||||
public static event EventHandler<int>? ScanBegin;
|
||||
public static event EventHandler<int>? ScanEnd;
|
||||
|
||||
public static bool Scanning { get; private set; }
|
||||
private static object _lock { get; } = new();
|
||||
@ -100,7 +101,7 @@ namespace ApplicationServices
|
||||
}
|
||||
|
||||
#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();
|
||||
|
||||
@ -236,7 +237,7 @@ namespace ApplicationServices
|
||||
{
|
||||
var tasks = new List<Task<List<ImportItem>>>();
|
||||
|
||||
await using LogArchiver archiver
|
||||
await using LogArchiver? archiver
|
||||
= Log.Logger.IsDebugEnabled()
|
||||
? new LogArchiver(System.IO.Path.Combine(Configuration.Instance.LibationFiles, "LibraryScans.zip"))
|
||||
: default;
|
||||
@ -266,13 +267,13 @@ namespace ApplicationServices
|
||||
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));
|
||||
|
||||
Log.Logger.Information("ImportLibraryAsync. {@DebugInfo}", new
|
||||
{
|
||||
Account = account?.MaskedLogEntry ?? "[null]"
|
||||
Account = account.MaskedLogEntry ?? "[null]"
|
||||
});
|
||||
|
||||
logTime($"pre scanAccountAsync {account.AccountName}");
|
||||
@ -294,7 +295,7 @@ namespace ApplicationServices
|
||||
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)
|
||||
{
|
||||
@ -452,28 +453,28 @@ namespace ApplicationServices
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// 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>
|
||||
public static event EventHandler<IEnumerable<LibraryBook>> BookUserDefinedItemCommitted;
|
||||
public static event EventHandler<IEnumerable<LibraryBook>>? BookUserDefinedItemCommitted;
|
||||
|
||||
#region Update book details
|
||||
public static int UpdateUserDefinedItem(
|
||||
this LibraryBook lb,
|
||||
string tags = null,
|
||||
string? tags = null,
|
||||
LiberatedStatus? bookStatus = null,
|
||||
LiberatedStatus? pdfStatus = null,
|
||||
Rating rating = null)
|
||||
Rating? rating = null)
|
||||
=> new[] { lb }.UpdateUserDefinedItem(tags, bookStatus, pdfStatus, rating);
|
||||
|
||||
public static int UpdateUserDefinedItem(
|
||||
this IEnumerable<LibraryBook> lb,
|
||||
string tags = null,
|
||||
string? tags = null,
|
||||
LiberatedStatus? bookStatus = null,
|
||||
LiberatedStatus? pdfStatus = null,
|
||||
Rating rating = null)
|
||||
Rating? rating = null)
|
||||
=> updateUserDefinedItem(
|
||||
lb,
|
||||
udi => {
|
||||
@ -532,7 +533,8 @@ namespace ApplicationServices
|
||||
var udiEntity = context.Entry(book.Book.UserDefinedItem);
|
||||
|
||||
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();
|
||||
@ -598,7 +600,8 @@ namespace ApplicationServices
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
public static LibraryStats GetCounts(IEnumerable<LibraryBook> libraryBooks = null)
|
||||
|
||||
public static LibraryStats GetCounts(IEnumerable<LibraryBook>? libraryBooks = null)
|
||||
{
|
||||
libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking();
|
||||
|
||||
|
||||
@ -17,6 +17,9 @@
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextControlForeground}" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
</ControlTheme>
|
||||
<ControlTheme x:Key="{x:Type DataGridCell}" TargetType="DataGridCell" BasedOn="{StaticResource {x:Type DataGridCell}}">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextControlForeground}" />
|
||||
</ControlTheme>
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<SolidColorBrush x:Key="SeriesEntryGridBackgroundBrush" Opacity="0.3" Color="#abffab" />
|
||||
|
||||
@ -6,6 +6,7 @@ using LibationAvalonia.Dialogs;
|
||||
using LibationFileManager;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia
|
||||
{
|
||||
internal static class AvaloniaUtils
|
||||
@ -14,18 +15,18 @@ namespace LibationAvalonia
|
||||
=> GetBrushFromResources(name, Brushes.Transparent);
|
||||
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 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);
|
||||
|
||||
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)
|
||||
{
|
||||
try
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Styling;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
@ -30,6 +31,9 @@ namespace LibationAvalonia.Dialogs
|
||||
Closing += DialogWindow_Closing;
|
||||
|
||||
UseCustomTitleBar = Configuration.IsWindows;
|
||||
|
||||
if (Design.IsDesignMode)
|
||||
RequestedThemeVariant = ThemeVariant.Dark;
|
||||
}
|
||||
|
||||
private bool fixedMinHeight = false;
|
||||
|
||||
@ -42,6 +42,17 @@ namespace LibationAvalonia.Dialogs
|
||||
public EditQuickFilters()
|
||||
{
|
||||
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
|
||||
// here: copy strings and dispose of persister
|
||||
|
||||
@ -51,7 +51,7 @@
|
||||
<DataGridTemplateColumn Width="Auto" Header="Tag">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<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>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
@ -59,7 +59,7 @@
|
||||
<DataGridTemplateColumn Width="Auto" Header="Description">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextPresenter
|
||||
<TextBlock
|
||||
Height="18"
|
||||
Margin="10,0,10,0"
|
||||
VerticalAlignment="Center" Text="{Binding Item2}" />
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Documents;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using ReactiveUI;
|
||||
@ -11,175 +11,175 @@ using System.IO;
|
||||
using System.Linq;
|
||||
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();
|
||||
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
_ = Configuration.Instance.LibationFiles;
|
||||
var editor = TemplateEditor<Templates.FileTemplate>.CreateFilenameEditor(Configuration.Instance.Books, Configuration.Instance.FileTemplate);
|
||||
_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}";
|
||||
_ = Configuration.Instance.LibationFiles;
|
||||
RequestedThemeVariant = ThemeVariant.Dark;
|
||||
var editor = TemplateEditor<Templates.FileTemplate>.CreateFilenameEditor(Configuration.Instance.Books, Configuration.Instance.FileTemplate);
|
||||
_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;
|
||||
}
|
||||
|
||||
|
||||
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())
|
||||
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)
|
||||
get => _userTemplateText;
|
||||
set
|
||||
{
|
||||
config = configuration;
|
||||
TemplateEditor = templates;
|
||||
Description = templates.TemplateDescription;
|
||||
ListItems
|
||||
= new AvaloniaList<Tuple<string, string, string>>(
|
||||
this.RaiseAndSetIfChanged(ref _userTemplateText, value);
|
||||
templateTb_TextChanged();
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
.EditingTemplate
|
||||
.TagsRegistered
|
||||
.Cast<TemplateTags>()
|
||||
.Select(
|
||||
t => new Tuple<string, string, string>(
|
||||
$"<{t.TagName}>",
|
||||
t.Description,
|
||||
t.DefaultValue)
|
||||
)
|
||||
);
|
||||
.Warnings
|
||||
.Select(err => $"- {err}")
|
||||
.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||
|
||||
}
|
||||
var bold = FontWeight.Bold;
|
||||
var reg = FontWeight.Normal;
|
||||
|
||||
// hold the work-in-progress value. not guaranteed to be valid
|
||||
private string _userTemplateText;
|
||||
public string UserTemplateText
|
||||
Inlines.Clear();
|
||||
|
||||
if (!TemplateEditor.IsFilePath)
|
||||
{
|
||||
get => _userTemplateText;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _userTemplateText, value);
|
||||
templateTb_TextChanged();
|
||||
}
|
||||
Inlines.Add(new Run(TemplateEditor.GetName()) { FontWeight = bold });
|
||||
return;
|
||||
}
|
||||
|
||||
private string _warningText;
|
||||
public string WarningText { get => _warningText; set => this.RaiseAndSetIfChanged(ref _warningText, value); }
|
||||
var folder = TemplateEditor.GetFolderName();
|
||||
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;
|
||||
public void ResetToDefault() => ResetTextBox(TemplateEditor.DefaultTemplate);
|
||||
Inlines.Add(new Run(sing));
|
||||
|
||||
public async Task<bool> Validate()
|
||||
{
|
||||
if (TemplateEditor.EditingTemplate.IsValid)
|
||||
return true;
|
||||
Inlines.Add(new Run(slashWrap(file)) { FontWeight = TemplateEditor.IsFolder ? reg : bold });
|
||||
|
||||
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
|
||||
.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}"));
|
||||
}
|
||||
Inlines.Add(new Run($".{ext}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,17 +6,17 @@ using LibationFileManager;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia
|
||||
{
|
||||
public static class FormSaveExtension
|
||||
{
|
||||
static readonly WindowIcon WindowIcon;
|
||||
static readonly WindowIcon? WindowIcon;
|
||||
static FormSaveExtension()
|
||||
{
|
||||
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow is not null)
|
||||
WindowIcon = desktop.MainWindow.Icon;
|
||||
else
|
||||
WindowIcon = null;
|
||||
WindowIcon = Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow?.Icon is WindowIcon icon
|
||||
? icon
|
||||
: null;
|
||||
}
|
||||
|
||||
public static void SetLibationIcon(this Window form)
|
||||
@ -29,7 +29,7 @@ namespace LibationAvalonia
|
||||
if (Design.IsDesignMode) return;
|
||||
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)
|
||||
return;
|
||||
@ -40,12 +40,14 @@ namespace LibationAvalonia
|
||||
savedState.Width = (int)form.Width;
|
||||
savedState.Height = (int)form.Height;
|
||||
}
|
||||
|
||||
// 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)
|
||||
savedState.Width = form.Screens.Primary.WorkingArea.Width;
|
||||
if (savedState.Height > form.Screens.Primary.WorkingArea.Height)
|
||||
savedState.Height = form.Screens.Primary.WorkingArea.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 > primaryScreen.WorkingArea.Width)
|
||||
savedState.Width = primaryScreen.WorkingArea.Width;
|
||||
if (savedState.Height > primaryScreen.WorkingArea.Height)
|
||||
savedState.Height = primaryScreen.WorkingArea.Height;
|
||||
}
|
||||
|
||||
var rect = new PixelRect(savedState.X, savedState.Y, savedState.Width, savedState.Height);
|
||||
|
||||
|
||||
@ -5,10 +5,10 @@ using System.Threading.Tasks;
|
||||
using ApplicationServices;
|
||||
using AppScaffolding;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.ReactiveUI;
|
||||
using LibationFileManager;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia
|
||||
{
|
||||
static class Program
|
||||
@ -57,7 +57,7 @@ namespace LibationAvalonia
|
||||
App.LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
||||
}
|
||||
|
||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(null);
|
||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime([]);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -77,27 +77,27 @@ namespace LibationAvalonia
|
||||
private static void LogError(object exceptionObject)
|
||||
{
|
||||
var logError = $"""
|
||||
{DateTime.Now} - Libation Crash
|
||||
OS {Configuration.OS}
|
||||
Version {LibationScaffolding.BuildVersion}
|
||||
ReleaseIdentifier {LibationScaffolding.ReleaseIdentifier}
|
||||
InteropFunctionsType {InteropFactory.InteropFunctionsType}
|
||||
LibationFiles {getConfigValue(c => c.LibationFiles)}
|
||||
Books Folder {getConfigValue(c => c.Books)}
|
||||
=== EXCEPTION ===
|
||||
{exceptionObject}
|
||||
""";
|
||||
{DateTime.Now} - Libation Crash
|
||||
OS {Configuration.OS}
|
||||
Version {LibationScaffolding.BuildVersion}
|
||||
ReleaseIdentifier {LibationScaffolding.ReleaseIdentifier}
|
||||
InteropFunctionsType {InteropFactory.InteropFunctionsType}
|
||||
LibationFiles {getConfigValue(c => c.LibationFiles)}
|
||||
Books Folder {getConfigValue(c => c.Books)}
|
||||
=== EXCEPTION ===
|
||||
{exceptionObject}
|
||||
""";
|
||||
|
||||
var crashLog = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "LibationCrash.log");
|
||||
|
||||
using var sw = new StreamWriter(crashLog, true);
|
||||
sw.WriteLine(logError);
|
||||
|
||||
static string getConfigValue(Func<Configuration, string> selector)
|
||||
static string getConfigValue(Func<Configuration, string?> selector)
|
||||
{
|
||||
try
|
||||
{
|
||||
return selector(Configuration.Instance);
|
||||
return selector(Configuration.Instance) ?? "[null]";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@ -4,6 +4,7 @@ using DataLayer;
|
||||
using LibationUiBase.GridView;
|
||||
using System;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
public class AvaloniaEntryStatus : EntryStatus, IEntryStatus, IComparable
|
||||
@ -17,6 +18,6 @@ namespace LibationAvalonia.ViewModels
|
||||
=> AvaloniaUtils.TryLoadImageOrDefault(picture, LibationFileManager.PictureSize._80x80);
|
||||
|
||||
//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;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
public class LiberateStatusButtonViewModel : ViewModelBase
|
||||
|
||||
@ -6,12 +6,13 @@ using ReactiveUI;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
{
|
||||
private Task<LibraryCommands.LibraryStats> updateCountsTask;
|
||||
private LibraryCommands.LibraryStats _libraryStats;
|
||||
private Task<LibraryCommands.LibraryStats>? updateCountsTask;
|
||||
private LibraryCommands.LibraryStats? _libraryStats;
|
||||
|
||||
/// <summary> The "Begin Book and PDF Backup" menu item header text </summary>
|
||||
public string BookBackupsToolStripText { get; private set; } = "Begin Book and PDF Backups: 0";
|
||||
@ -19,7 +20,7 @@ namespace LibationAvalonia.ViewModels
|
||||
public string PdfBackupsToolStripText { get; private set; } = "Begin PDF Only Backups: 0";
|
||||
|
||||
/// <summary> The user's library statistics </summary>
|
||||
public LibraryCommands.LibraryStats LibraryStats
|
||||
public LibraryCommands.LibraryStats? LibraryStats
|
||||
{
|
||||
get => _libraryStats;
|
||||
set
|
||||
@ -27,12 +28,12 @@ namespace LibationAvalonia.ViewModels
|
||||
this.RaiseAndSetIfChanged(ref _libraryStats, value);
|
||||
|
||||
BookBackupsToolStripText
|
||||
= LibraryStats.HasPendingBooks
|
||||
= LibraryStats?.HasPendingBooks ?? false
|
||||
? "Begin " + menufyText($"Book and PDF Backups: {LibraryStats.PendingBooks} remaining")
|
||||
: "All books have been liberated";
|
||||
|
||||
PdfBackupsToolStripText
|
||||
= LibraryStats.pdfsNotDownloaded > 0
|
||||
= LibraryStats?.pdfsNotDownloaded > 0
|
||||
? "Begin " + menufyText($"PDF Only Backups: {LibraryStats.pdfsNotDownloaded} remaining")
|
||||
: "All PDFs have been downloaded";
|
||||
|
||||
@ -48,14 +49,14 @@ namespace LibationAvalonia.ViewModels
|
||||
=> await SetBackupCountsAsync(null);
|
||||
}
|
||||
|
||||
public async Task SetBackupCountsAsync(IEnumerable<LibraryBook> libraryBooks)
|
||||
public async Task SetBackupCountsAsync(IEnumerable<LibraryBook>? libraryBooks)
|
||||
{
|
||||
if (updateCountsTask?.IsCompleted ?? true)
|
||||
{
|
||||
updateCountsTask = Task.Run(() => LibraryCommands.GetCounts(libraryBooks));
|
||||
var stats = await updateCountsTask;
|
||||
await Dispatcher.UIThread.InvokeAsync(() => LibraryStats = stats);
|
||||
|
||||
|
||||
if (Configuration.Instance.AutoDownloadEpisodes
|
||||
&& stats.booksNoProgress + stats.pdfsNotDownloaded > 0)
|
||||
await Dispatcher.UIThread.InvokeAsync(BackupAllBooks);
|
||||
|
||||
@ -5,6 +5,7 @@ using LibationFileManager;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
@ -18,7 +19,7 @@ namespace LibationAvalonia.ViewModels
|
||||
var options = new FilePickerSaveOptions
|
||||
{
|
||||
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}",
|
||||
DefaultExtension = "xlsx",
|
||||
ShowOverwritePrompt = true,
|
||||
@ -41,7 +42,7 @@ namespace LibationAvalonia.ViewModels
|
||||
AppleUniformTypeIdentifiers = new[] { "public.json" }
|
||||
},
|
||||
new("All files (*.*)") { Patterns = new[] { "*" } }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var selectedFile = (await MainWindow.StorageProvider.SaveFilePickerAsync(options))?.TryGetLocalPath();
|
||||
|
||||
@ -9,16 +9,17 @@ using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
{
|
||||
private QuickFilters.NamedFilter lastGoodFilter = new(string.Empty, null);
|
||||
private QuickFilters.NamedFilter _selectedNamedFilter = new(string.Empty, null);
|
||||
private QuickFilters.NamedFilter? lastGoodFilter = new(string.Empty, null);
|
||||
private QuickFilters.NamedFilter? _selectedNamedFilter = new(string.Empty, null);
|
||||
private bool _firstFilterIsDefault = true;
|
||||
|
||||
/// <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();
|
||||
/// <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); }
|
||||
@ -50,36 +51,44 @@ namespace LibationAvalonia.ViewModels
|
||||
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 FilterHelpBtn() => await new LibationAvalonia.Dialogs.SearchSyntaxDialog().ShowDialog(MainWindow);
|
||||
public void ToggleFirstFilterIsDefault() => FirstFilterIsDefault = !FirstFilterIsDefault;
|
||||
public async Task EditQuickFiltersAsync() => await new LibationAvalonia.Dialogs.EditQuickFilters().ShowDialog(MainWindow);
|
||||
public async Task PerformFilter(QuickFilters.NamedFilter namedFilter)
|
||||
public async Task PerformFilter(QuickFilters.NamedFilter? namedFilter)
|
||||
{
|
||||
SelectedNamedFilter = namedFilter;
|
||||
var tryFilter = namedFilter?.Filter;
|
||||
|
||||
try
|
||||
{
|
||||
await ProductsDisplay.Filter(namedFilter.Filter);
|
||||
await ProductsDisplay.Filter(tryFilter);
|
||||
lastGoodFilter = namedFilter;
|
||||
}
|
||||
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
|
||||
await PerformFilter(lastGoodFilter);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateFiltersMenu(object _ = null, object __ = null)
|
||||
private void updateFiltersMenu(object? _ = null, object? __ = null)
|
||||
{
|
||||
//Clear all filters
|
||||
var quickFilterNativeMenu = (NativeMenuItem)NativeMenu.GetMenu(MainWindow).Items[3];
|
||||
for (int i = quickFilterNativeMenu.Menu.Items.Count - 1; i >= 3; i--)
|
||||
if (NativeMenu.GetMenu(MainWindow)?.Items[3] is not NativeMenuItem ss ||
|
||||
ss.Menu is not NativeMenu quickFilterNativeMenu)
|
||||
{
|
||||
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)
|
||||
{
|
||||
var existingBinding = MainWindow.KeyBindings.FirstOrDefault(kb => kb.Command == command);
|
||||
@ -89,7 +98,7 @@ namespace LibationAvalonia.ViewModels
|
||||
command.Dispose();
|
||||
}
|
||||
|
||||
quickFilterNativeMenu.Menu.Items.RemoveAt(i);
|
||||
quickFilterNativeMenu.Items.RemoveAt(i);
|
||||
QuickFilterMenuItems.RemoveAt(i);
|
||||
}
|
||||
|
||||
@ -116,7 +125,7 @@ namespace LibationAvalonia.ViewModels
|
||||
}
|
||||
|
||||
QuickFilterMenuItems.Add(menuItem);
|
||||
quickFilterNativeMenu.Menu.Items.Add(nativeMenuItem);
|
||||
quickFilterNativeMenu.Items.Add(nativeMenuItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Input;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
public partial class MainVM
|
||||
@ -90,7 +91,9 @@ namespace LibationAvalonia.ViewModels
|
||||
public async Task ScanAccountAsync()
|
||||
{
|
||||
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()
|
||||
@ -194,7 +197,7 @@ namespace LibationAvalonia.ViewModels
|
||||
await ProductsDisplay.ScanAndRemoveBooksAsync(accounts);
|
||||
}
|
||||
|
||||
private async Task scanLibrariesAsync(params Account[] accounts)
|
||||
private async Task scanLibrariesAsync(params Account[]? accounts)
|
||||
{
|
||||
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();
|
||||
AccountsCount = persister.AccountsSettings.Accounts.Count;
|
||||
|
||||
var importMenuItem = (NativeMenuItem)NativeMenu.GetMenu(MainWindow).Items[0];
|
||||
|
||||
for (int i = importMenuItem.Menu.Items.Count - 1; i >= 2; i--)
|
||||
importMenuItem.Menu.Items.RemoveAt(i);
|
||||
if (NativeMenu.GetMenu(MainWindow)?.Items[0] is not NativeMenuItem ss ||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Scan Library", Command = ReactiveCommand.Create(ScanAccountAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Alt | KeyModifiers.Meta)});
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItemSeparator());
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Remove Library Books", Command = ReactiveCommand.Create(RemoveBooksAsync), Gesture = new KeyGesture(Key.R, KeyModifiers.Alt | KeyModifiers.Meta)});
|
||||
importMenuItem.Items.Add(new NativeMenuItem { Header = "Scan Library", Command = ReactiveCommand.Create(ScanAccountAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Alt | KeyModifiers.Meta) });
|
||||
importMenuItem.Items.Add(new NativeMenuItemSeparator());
|
||||
importMenuItem.Items.Add(new NativeMenuItem { Header = "Remove Library Books", Command = ReactiveCommand.Create(RemoveBooksAsync), Gesture = new KeyGesture(Key.R, KeyModifiers.Alt | KeyModifiers.Meta) });
|
||||
}
|
||||
else
|
||||
{
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Scan Library of All Accounts", Command = ReactiveCommand.Create(ScanAllAccountsAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Alt | KeyModifiers.Meta)});
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Scan Library of Some Accounts", Command = ReactiveCommand.Create(ScanSomeAccountsAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Alt | KeyModifiers.Meta | KeyModifiers.Shift) });
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItemSeparator());
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Remove Books from All Accounts", Command = ReactiveCommand.Create(RemoveBooksAllAsync), Gesture = new KeyGesture(Key.R, KeyModifiers.Alt | KeyModifiers.Meta)});
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Remove Books from Some Accounts", Command = ReactiveCommand.Create(RemoveBooksSomeAsync), Gesture = new KeyGesture(Key.R, KeyModifiers.Alt | KeyModifiers.Meta | KeyModifiers.Shift) });
|
||||
importMenuItem.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 Some Accounts", Command = ReactiveCommand.Create(ScanSomeAccountsAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Alt | KeyModifiers.Meta | KeyModifiers.Shift) });
|
||||
importMenuItem.Items.Add(new NativeMenuItemSeparator());
|
||||
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.Items.Add(new NativeMenuItem { Header = "Remove Books from Some Accounts", Command = ReactiveCommand.Create(RemoveBooksSomeAsync), Gesture = new KeyGesture(Key.R, KeyModifiers.Alt | KeyModifiers.Meta | KeyModifiers.Shift) });
|
||||
}
|
||||
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItemSeparator());
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Locate Audiobooks...", Command = ReactiveCommand.Create(LocateAudiobooksAsync) });
|
||||
importMenuItem.Items.Add(new NativeMenuItemSeparator());
|
||||
importMenuItem.Items.Add(new NativeMenuItem { Header = "Locate Audiobooks...", Command = ReactiveCommand.Create(LocateAudiobooksAsync) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using DataLayer;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
|
||||
@ -6,6 +6,7 @@ using Dinah.Core;
|
||||
using LibationUiBase.GridView;
|
||||
using ReactiveUI;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
|
||||
@ -6,6 +6,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
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()
|
||||
{
|
||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
@ -61,17 +62,17 @@ namespace LibationAvalonia.ViewModels
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private void accountsPreSave(object sender = null, EventArgs e = null)
|
||||
private void accountsPreSave(object? sender = null, EventArgs? e = null)
|
||||
=> 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();
|
||||
}
|
||||
|
||||
[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;
|
||||
if (AutoScanChecked)
|
||||
|
||||
@ -4,6 +4,7 @@ using ReactiveUI;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
@ -12,7 +13,9 @@ namespace LibationAvalonia.ViewModels
|
||||
public bool MenuBarVisible { get => _menuBarVisible; set => this.RaiseAndSetIfChanged(ref _menuBarVisible, value); }
|
||||
private void Configure_Settings()
|
||||
{
|
||||
((NativeMenuItem)NativeMenu.GetMenu(App.Current).Items[0]).Command = ReactiveCommand.Create(ShowAboutAsync);
|
||||
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);
|
||||
|
||||
@ -6,6 +6,7 @@ using Avalonia.Threading;
|
||||
using LibationAvalonia.Dialogs;
|
||||
using ReactiveUI;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
@ -56,13 +57,13 @@ namespace LibationAvalonia.ViewModels
|
||||
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);
|
||||
await Dispatcher.UIThread.InvokeAsync(setLiberatedVisibleMenuItem);
|
||||
}
|
||||
|
||||
private async void setLiberatedVisibleMenuItemAsync(object _, object __)
|
||||
private async void setLiberatedVisibleMenuItemAsync(object? _, object __)
|
||||
=> await Dispatcher.UIThread.InvokeAsync(setLiberatedVisibleMenuItem);
|
||||
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
using LibationUiBase;
|
||||
using System.IO;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
|
||||
@ -6,6 +6,7 @@ using ReactiveUI;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
public partial class MainVM : ViewModelBase
|
||||
@ -37,11 +38,18 @@ namespace LibationAvalonia.ViewModels
|
||||
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(
|
||||
SetBackupCountsAsync(fullLibrary),
|
||||
Task.Run(() => ProductsDisplay.UpdateGridAsync(fullLibrary)));
|
||||
try
|
||||
{
|
||||
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}";
|
||||
|
||||
@ -16,6 +16,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
public enum ProcessBookResult
|
||||
@ -45,28 +46,28 @@ namespace LibationAvalonia.ViewModels
|
||||
/// </summary>
|
||||
public class ProcessBookViewModel : ViewModelBase
|
||||
{
|
||||
public event EventHandler Completed;
|
||||
public event EventHandler? Completed;
|
||||
|
||||
public LibraryBook LibraryBook { get; private set; }
|
||||
|
||||
private ProcessBookResult _result = ProcessBookResult.None;
|
||||
private ProcessBookStatus _status = ProcessBookStatus.Queued;
|
||||
private string _narrator;
|
||||
private string _author;
|
||||
private string _title;
|
||||
private string? _narrator;
|
||||
private string? _author;
|
||||
private string? _title;
|
||||
private int _progress;
|
||||
private string _eta;
|
||||
private Bitmap _cover;
|
||||
private string? _eta;
|
||||
private Bitmap? _cover;
|
||||
|
||||
#region Properties exposed to the view
|
||||
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 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 Title { get => _title; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _title, 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? 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 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 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 bool IsFinished => Status is not ProcessBookStatus.Queued and not ProcessBookStatus.Working;
|
||||
public bool IsDownloading => Status is ProcessBookStatus.Working;
|
||||
public bool Queued => Status is ProcessBookStatus.Queued;
|
||||
@ -95,8 +96,8 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
private TimeSpan TimeRemaining { set { ETA = $"ETA: {value:mm\\:ss}"; } }
|
||||
private Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke();
|
||||
private Processable NextProcessable() => _currentProcessable = null;
|
||||
private Processable _currentProcessable;
|
||||
private Processable? NextProcessable() => _currentProcessable = null;
|
||||
private Processable? _currentProcessable;
|
||||
private readonly Queue<Func<Processable>> Processes = new();
|
||||
private readonly LogMe Logger;
|
||||
|
||||
@ -118,7 +119,7 @@ namespace LibationAvalonia.ViewModels
|
||||
_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)
|
||||
{
|
||||
@ -255,14 +256,14 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
#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
|
||||
= Configuration.Instance.FileDownloadQuality == Configuration.DownloadQuality.High && LibraryBook.Book.PictureLarge is not null
|
||||
@ -275,7 +276,7 @@ namespace LibationAvalonia.ViewModels
|
||||
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);
|
||||
Cover = new Avalonia.Media.Imaging.Bitmap(ms);
|
||||
@ -284,10 +285,10 @@ namespace LibationAvalonia.ViewModels
|
||||
#endregion
|
||||
|
||||
#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)
|
||||
return;
|
||||
@ -302,21 +303,25 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
#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);
|
||||
|
||||
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;
|
||||
Author = libraryBook.Book.AuthorNames();
|
||||
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}");
|
||||
UnlinkProcessable((Processable)sender);
|
||||
if (sender is Processable processable)
|
||||
{
|
||||
Logger.Info($"{processable.Name} Step, Completed: {libraryBook.Book}");
|
||||
UnlinkProcessable(processable);
|
||||
}
|
||||
|
||||
if (Processes.Count == 0)
|
||||
{
|
||||
@ -375,7 +380,7 @@ namespace LibationAvalonia.ViewModels
|
||||
: str;
|
||||
|
||||
details =
|
||||
$@" Title: {libraryBook.Book.TitleWithSubtitle}
|
||||
$@" Title: {libraryBook.Book.TitleWithSubtitle}
|
||||
ID: {libraryBook.Book.AudibleProductId}
|
||||
Author: {trunc(libraryBook.Book.AuthorNames())}
|
||||
Narr: {trunc(libraryBook.Book.NarratorNames())}";
|
||||
|
||||
@ -12,15 +12,17 @@ using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
|
||||
public class ProcessQueueViewModel : ViewModelBase, ILogForm
|
||||
{
|
||||
public ObservableCollection<LogEntry> LogEntries { get; } = new();
|
||||
public AvaloniaList<ProcessBookViewModel> Items { get; } = new();
|
||||
public TrackedQueue<ProcessBookViewModel> Queue { get; }
|
||||
public ProcessBookViewModel SelectedItem { get; set; }
|
||||
public Task QueueRunner { get; private set; }
|
||||
public ProcessBookViewModel? SelectedItem { get; set; }
|
||||
public Task? QueueRunner { get; private set; }
|
||||
public bool Running => !QueueRunner?.IsCompleted ?? false;
|
||||
|
||||
private readonly LogMe Logger;
|
||||
@ -41,14 +43,14 @@ namespace LibationAvalonia.ViewModels
|
||||
private int _completedCount;
|
||||
private int _errorCount;
|
||||
private int _queuedCount;
|
||||
private string _runningTime;
|
||||
private string? _runningTime;
|
||||
private bool _progressBarVisible;
|
||||
private decimal _speedLimit;
|
||||
|
||||
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 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 AnyCompleted => CompletedCount > 0;
|
||||
public bool AnyQueued => QueuedCount > 0;
|
||||
@ -89,7 +91,7 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
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 completeCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.Success);
|
||||
@ -98,7 +100,7 @@ namespace LibationAvalonia.ViewModels
|
||||
CompletedCount = completeCount;
|
||||
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;
|
||||
await Dispatcher.UIThread.InvokeAsync(() => this.RaisePropertyChanged(nameof(Progress)));
|
||||
@ -120,7 +122,7 @@ namespace LibationAvalonia.ViewModels
|
||||
private bool isBookInQueue(LibraryBook libraryBook)
|
||||
{
|
||||
var entry = Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId);
|
||||
if (entry == null)
|
||||
if (entry == null)
|
||||
return false;
|
||||
else if (entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed)
|
||||
return !Queue.RemoveCompleted(entry);
|
||||
@ -218,13 +220,17 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
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();
|
||||
|
||||
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)
|
||||
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)
|
||||
{
|
||||
@ -273,6 +279,6 @@ This error appears to be caused by a temporary interruption of service that some
|
||||
{
|
||||
public DateTime LogDate { get; init; }
|
||||
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)
|
||||
{
|
||||
if (dbBooks == null)
|
||||
throw new ArgumentNullException(nameof(dbBooks));
|
||||
|
||||
//Get the UI thread's synchronization context and set it on the current thread to ensure
|
||||
//it's available for GetAllProductsAsync and GetAllSeriesEntriesAsync
|
||||
var sc = await Dispatcher.UIThread.InvokeAsync(() => AvaloniaSynchronizationContext.Current);
|
||||
@ -155,12 +158,11 @@ namespace LibationAvalonia.ViewModels
|
||||
/// </summary>
|
||||
internal async Task UpdateGridAsync(List<LibraryBook> dbBooks)
|
||||
{
|
||||
if (dbBooks == null)
|
||||
throw new ArgumentNullException(nameof(dbBooks));
|
||||
|
||||
if (GridEntries == null)
|
||||
{
|
||||
//always bind before updating. Binding creates GridEntries.
|
||||
await BindToGridAsync(dbBooks);
|
||||
return;
|
||||
}
|
||||
throw new InvalidOperationException($"Must call {nameof(BindToGridAsync)} before calling {nameof(UpdateGridAsync)}");
|
||||
|
||||
#region Add new or update existing grid entries
|
||||
|
||||
|
||||
@ -3,17 +3,18 @@ using LibationUiBase.GridView;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
internal class RowComparer : RowComparerBase
|
||||
{
|
||||
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? HeaderCellPi = typeof(DataGridColumn).GetProperty("HeaderCell", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
private static readonly PropertyInfo? CurrentSortingStatePi = typeof(DataGridColumnHeader).GetProperty("CurrentSortingState", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
private DataGridColumn Column { get; init; }
|
||||
public override string PropertyName { get; set; }
|
||||
private DataGridColumn? Column { get; }
|
||||
public override string? PropertyName { get; set; }
|
||||
|
||||
public RowComparer(DataGridColumn column)
|
||||
public RowComparer(DataGridColumn? column)
|
||||
{
|
||||
Column = column;
|
||||
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
|
||||
protected override ListSortDirection GetSortOrder()
|
||||
=> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ using ReactiveUI;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels.Settings
|
||||
{
|
||||
public class AudioSettingsVM : ViewModelBase
|
||||
@ -33,17 +34,13 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
= new(
|
||||
new[]
|
||||
{
|
||||
NAudio.Lame.EncoderQuality.High,
|
||||
NAudio.Lame.EncoderQuality.Standard,
|
||||
NAudio.Lame.EncoderQuality.Fast,
|
||||
NAudio.Lame.EncoderQuality.High,
|
||||
NAudio.Lame.EncoderQuality.Standard,
|
||||
NAudio.Lame.EncoderQuality.Fast,
|
||||
});
|
||||
|
||||
|
||||
public AudioSettingsVM(Configuration config)
|
||||
{
|
||||
LoadSettings(config);
|
||||
}
|
||||
public void LoadSettings(Configuration config)
|
||||
{
|
||||
CreateCueSheet = config.CreateCueSheet;
|
||||
CombineNestedChapterTitles = config.CombineNestedChapterTitles;
|
||||
@ -57,7 +54,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
MergeOpeningAndEndCredits = config.MergeOpeningAndEndCredits;
|
||||
StripAudibleBrandAudio = config.StripAudibleBrandAudio;
|
||||
StripUnabridged = config.StripUnabridged;
|
||||
ChapterTitleTemplate = config.ChapterTitleTemplate;
|
||||
_chapterTitleTemplate = config.ChapterTitleTemplate;
|
||||
DecryptToLossy = config.DecryptToLossy;
|
||||
MoveMoovToBeginning = config.MoveMoovToBeginning;
|
||||
LameTargetBitrate = config.LameTargetBitrate;
|
||||
@ -67,7 +64,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
LameBitrate = config.LameBitrate;
|
||||
LameVBRQuality = config.LameVBRQuality;
|
||||
|
||||
SelectedSampleRate = SampleRates.SingleOrDefault(s => s.Value == config.MaxSampleRate);
|
||||
SelectedSampleRate = SampleRates.SingleOrDefault(s => s.Value == config.MaxSampleRate) ?? SampleRates[0];
|
||||
SelectedEncoderQuality = config.LameEncoderQuality;
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ using LibationFileManager;
|
||||
using ReactiveUI;
|
||||
using System.Collections.Generic;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels.Settings
|
||||
{
|
||||
public class DownloadDecryptSettingsVM : ViewModelBase
|
||||
@ -15,7 +16,16 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public DownloadDecryptSettingsVM(Configuration 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()
|
||||
@ -28,20 +38,6 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
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)
|
||||
{
|
||||
config.BadBook
|
||||
@ -62,10 +58,10 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public string UseCoverAsFolderIconText { get; } = Configuration.GetDescription(nameof(Configuration.UseCoverAsFolderIcon));
|
||||
public string SaveMetadataToFileText { get; } = Configuration.GetDescription(nameof(Configuration.SaveMetadataToFile));
|
||||
public string BadBookGroupboxText { get; } = Configuration.GetDescription(nameof(Configuration.BadBook));
|
||||
public string BadBookAskText { get; } = Configuration.BadBookAction.Ask.GetDescription();
|
||||
public string BadBookAbortText { get; } = Configuration.BadBookAction.Abort.GetDescription();
|
||||
public string BadBookRetryText { get; } = Configuration.BadBookAction.Retry.GetDescription();
|
||||
public string BadBookIgnoreText { get; } = Configuration.BadBookAction.Ignore.GetDescription();
|
||||
public string BadBookAskText { get; } = Configuration.BadBookAction.Ask.GetDescription() ?? nameof(Configuration.BadBookAction.Ask);
|
||||
public string BadBookAbortText { get; } = Configuration.BadBookAction.Abort.GetDescription() ?? nameof(Configuration.BadBookAction.Abort);
|
||||
public string BadBookRetryText { get; } = Configuration.BadBookAction.Retry.GetDescription() ?? nameof(Configuration.BadBookAction.Retry);
|
||||
public string BadBookIgnoreText { get; } = Configuration.BadBookAction.Ignore.GetDescription() ?? nameof(Configuration.BadBookAction.Ignore);
|
||||
public string FolderTemplateText { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate));
|
||||
public string FileTemplateText { get; } = Configuration.GetDescription(nameof(Configuration.FileTemplate));
|
||||
public string ChapterFileTemplateText { get; } = Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate));
|
||||
|
||||
@ -1,15 +1,11 @@
|
||||
using LibationFileManager;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels.Settings
|
||||
{
|
||||
public class ImportSettingsVM
|
||||
{
|
||||
public ImportSettingsVM(Configuration config)
|
||||
{
|
||||
LoadSettings(config);
|
||||
}
|
||||
|
||||
public void LoadSettings(Configuration config)
|
||||
{
|
||||
AutoScan = config.AutoScan;
|
||||
ShowImportedStats = config.ShowImportedStats;
|
||||
|
||||
@ -7,6 +7,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels.Settings
|
||||
{
|
||||
public class ImportantSettingsVM : ViewModelBase
|
||||
@ -18,12 +19,8 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public ImportantSettingsVM(Configuration config)
|
||||
{
|
||||
this.config = config;
|
||||
LoadSettings(config);
|
||||
}
|
||||
|
||||
public void LoadSettings(Configuration config)
|
||||
{
|
||||
BooksDirectory = config.Books.PathWithoutPrefix;
|
||||
BooksDirectory = config.Books?.PathWithoutPrefix ?? Configuration.DefaultBooksDirectory;
|
||||
SavePodcastsToParentFolder = config.SavePodcastsToParentFolder;
|
||||
OverwriteExisting = config.OverwriteExisting;
|
||||
CreationTime = DateTimeSources.SingleOrDefault(v => v.Value == config.CreationTime) ?? DateTimeSources[0];
|
||||
@ -32,9 +29,9 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
GridScaleFactor = scaleFactorToLinearRange(config.GridScaleFactor);
|
||||
GridFontScaleFactor = scaleFactorToLinearRange(config.GridFontScaleFactor);
|
||||
|
||||
ThemeVariant = initialThemeVariant = Configuration.Instance.GetString(propertyName: nameof(ThemeVariant));
|
||||
if (string.IsNullOrWhiteSpace(initialThemeVariant))
|
||||
ThemeVariant = initialThemeVariant = "System";
|
||||
themeVariant = initialThemeVariant = Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)) ?? "";
|
||||
if (string.IsNullOrWhiteSpace(initialThemeVariant))
|
||||
themeVariant = initialThemeVariant = "System";
|
||||
}
|
||||
|
||||
public void SaveSettings(Configuration config)
|
||||
@ -100,14 +97,17 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
get => themeVariant;
|
||||
set
|
||||
{
|
||||
var changed = !value.Equals(themeVariant);
|
||||
this.RaiseAndSetIfChanged(ref themeVariant, value);
|
||||
App.Current.RequestedThemeVariant = themeVariant switch
|
||||
{
|
||||
nameof(Avalonia.Styling.ThemeVariant.Dark) => Avalonia.Styling.ThemeVariant.Dark,
|
||||
nameof(Avalonia.Styling.ThemeVariant.Light) => Avalonia.Styling.ThemeVariant.Light,
|
||||
// "System"
|
||||
_ => Avalonia.Styling.ThemeVariant.Default
|
||||
};
|
||||
|
||||
if (changed && App.Current is Avalonia.Application app)
|
||||
app.RequestedThemeVariant = themeVariant switch
|
||||
{
|
||||
nameof(Avalonia.Styling.ThemeVariant.Dark) => Avalonia.Styling.ThemeVariant.Dark,
|
||||
nameof(Avalonia.Styling.ThemeVariant.Light) => Avalonia.Styling.ThemeVariant.Light,
|
||||
// "System"
|
||||
_ => Avalonia.Styling.ThemeVariant.Default
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@ namespace LibationFileManager
|
||||
{nameof(AllowLibationFixup), """
|
||||
In addition to the options that are enabled if you allow
|
||||
"fixing up" the audiobook, it does the following:
|
||||
|
||||
|
||||
* Sets the ©gen metadata tag for the genres.
|
||||
* Adds the TCOM (@wrt in M4B files) metadata tag for the narrators.
|
||||
* Unescapes the copyright symbol (replace © with ©)
|
||||
@ -30,7 +30,7 @@ namespace LibationFileManager
|
||||
}
|
||||
.AsReadOnly();
|
||||
|
||||
public static string? GetHelpText(string settingName)
|
||||
=> HelpText.TryGetValue(settingName, out var value) ? value : null;
|
||||
}
|
||||
public static string GetHelpText(string settingName)
|
||||
=> HelpText.TryGetValue(settingName, out var value) ? value : "";
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,7 +82,7 @@ namespace LibationFileManager
|
||||
|
||||
public string SettingsFilePath => Path.Combine(LibationFiles, "Settings.json");
|
||||
|
||||
public static string? GetDescription(string propertyName)
|
||||
public static string GetDescription(string propertyName)
|
||||
{
|
||||
var attribute = typeof(Configuration)
|
||||
.GetProperty(propertyName)
|
||||
@ -90,7 +90,7 @@ namespace LibationFileManager
|
||||
.SingleOrDefault()
|
||||
as DescriptionAttribute;
|
||||
|
||||
return attribute?.Description;
|
||||
return attribute?.Description ?? $"[{propertyName}]";
|
||||
}
|
||||
|
||||
public bool Exists(string propertyName) => Settings.Exists(propertyName);
|
||||
@ -118,12 +118,15 @@ namespace LibationFileManager
|
||||
|
||||
// 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")]
|
||||
public string InProgress { get
|
||||
public string InProgress
|
||||
{
|
||||
get
|
||||
{
|
||||
var tempDir = GetString();
|
||||
return string.IsNullOrWhiteSpace(tempDir) ? WinTemp : tempDir;
|
||||
}
|
||||
set => SetString(value); }
|
||||
set => SetString(value);
|
||||
}
|
||||
|
||||
[Description("Allow Libation to fix up audiobook metadata")]
|
||||
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); }
|
||||
|
||||
[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]")]
|
||||
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?")]
|
||||
public bool LameConstantBitrate { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
@ -179,8 +182,8 @@ namespace LibationFileManager
|
||||
private static readonly EquatableDictionary<string, bool> DefaultColumns = new(
|
||||
new KeyValuePair<string, bool>[]
|
||||
{
|
||||
new ("SeriesOrder", false),
|
||||
new ("LastDownload", false)
|
||||
new ("SeriesOrder", false),
|
||||
new ("LastDownload", false)
|
||||
});
|
||||
|
||||
[Description("A Dictionary of GridView data property names and bool indicating its column's visibility in ProductsGrid")]
|
||||
@ -200,7 +203,7 @@ namespace LibationFileManager
|
||||
|
||||
[Description("Download clips and bookmarks?")]
|
||||
public bool DownloadClipsBookmarks { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
|
||||
|
||||
[Description("File format to save clips and bookmarks")]
|
||||
public ClipBookmarkFormat ClipsBookmarksFileFormat { get => GetNonString(defaultValue: ClipBookmarkFormat.CSV); set => SetNonString(value); }
|
||||
|
||||
|
||||
@ -46,7 +46,7 @@ namespace LibationFileManager
|
||||
|
||||
// Note that records overload equality automagically, so should be able to
|
||||
// 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? Name { get; set; } = Name;
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationUiBase.GridView
|
||||
{
|
||||
/// <summary>
|
||||
@ -11,67 +12,10 @@ namespace LibationUiBase.GridView
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
public int Compare(object? x, object? y)
|
||||
=> Compare(x as IGridEntry, y as IGridEntry);
|
||||
|
||||
protected abstract ListSortDirection GetSortOrder();
|
||||
|
||||
@ -80,17 +24,74 @@ namespace LibationUiBase.GridView
|
||||
var val1 = x.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
|
||||
//Both a and b are series parents and compare as equal, so break the tie.
|
||||
? x.AudibleProductId.CompareTo(y.AudibleProductId)
|
||||
: 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