Consolidate process queue view models

Remove classic and chardonnay-specific implementations
Refactor TrackedQueue into an IList with INotifyCollectionChanged
This commit is contained in:
Michael Bucari-Tovo 2025-07-21 12:25:25 -06:00
parent bff9b67b72
commit 80b86086ca
19 changed files with 314 additions and 497 deletions

View File

@ -36,11 +36,11 @@ public partial class ThemePreviewControl : UserControl
PictureStorage.SetDefaultImage(PictureSize._80x80, ms1.ToArray()); PictureStorage.SetDefaultImage(PictureSize._80x80, ms1.ToArray());
} }
QueuedBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Queued }; QueuedBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Queued };
WorkingBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Working }; WorkingBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Working };
CompletedBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Completed }; CompletedBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Completed };
CancelledBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Cancelled }; CancelledBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Cancelled };
FailedBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Failed }; FailedBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Failed };
//Set the current processable so that the empty queue doesn't try to advance. //Set the current processable so that the empty queue doesn't try to advance.
QueuedBook.AddDownloadPdf(); QueuedBook.AddDownloadPdf();

View File

@ -8,7 +8,7 @@ namespace LibationAvalonia.ViewModels
{ {
partial class MainVM partial class MainVM
{ {
private void Configure_NonUI() public static void Configure_NonUI()
{ {
using var ms1 = new MemoryStream(); using var ms1 = new MemoryStream();
App.OpenAsset("img-coverart-prod-unavailable_80x80.jpg").CopyTo(ms1); App.OpenAsset("img-coverart-prod-unavailable_80x80.jpg").CopyTo(ms1);

View File

@ -2,6 +2,7 @@
using DataLayer; using DataLayer;
using LibationAvalonia.Views; using LibationAvalonia.Views;
using LibationFileManager; using LibationFileManager;
using LibationUiBase.ProcessQueue;
using ReactiveUI; using ReactiveUI;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;

View File

@ -1,17 +0,0 @@
using DataLayer;
using LibationFileManager;
using LibationUiBase;
using LibationUiBase.ProcessQueue;
#nullable enable
namespace LibationAvalonia.ViewModels;
public class ProcessBookViewModel : ProcessBookViewModelBase
{
public ProcessBookViewModel(LibraryBook libraryBook, LogMe logme) : base(libraryBook, logme) { }
protected override object? LoadImageFromBytes(byte[] bytes, PictureSize pictureSize)
=> AvaloniaUtils.TryLoadImageOrDefault(bytes, pictureSize);
}

View File

@ -1,74 +0,0 @@
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Threading;
using DataLayer;
using LibationFileManager;
using LibationUiBase.ProcessQueue;
using System;
using System.Collections.ObjectModel;
#nullable enable
namespace LibationAvalonia.ViewModels;
public record LogEntry(DateTime LogDate, string? LogMessage)
{
public string LogDateString => LogDate.ToShortTimeString();
}
public class ProcessQueueViewModel : ProcessQueueViewModelBase
{
public ProcessQueueViewModel() : base(CreateEmptyList())
{
Items = Queue.UnderlyingList as AvaloniaList<ProcessBookViewModelBase>
?? throw new ArgumentNullException(nameof(Queue.UnderlyingList));
SpeedLimit = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
}
private decimal _speedLimit;
public decimal SpeedLimitIncrement { get; private set; }
public ObservableCollection<LogEntry> LogEntries { get; } = new();
public AvaloniaList<ProcessBookViewModelBase> Items { get; }
public decimal SpeedLimit
{
get
{
return _speedLimit;
}
set
{
var newValue = Math.Min(999 * 1024 * 1024, (long)(value * 1024 * 1024));
var config = Configuration.Instance;
config.DownloadSpeedLimit = newValue;
_speedLimit
= config.DownloadSpeedLimit <= newValue ? value
: value == 0.01m ? config.DownloadSpeedLimit / 1024m / 1024
: 0;
config.DownloadSpeedLimit = (long)(_speedLimit * 1024 * 1024);
SpeedLimitIncrement = _speedLimit > 100 ? 10
: _speedLimit > 10 ? 1
: _speedLimit > 1 ? 0.1m
: 0.01m;
RaisePropertyChanged(nameof(SpeedLimitIncrement));
RaisePropertyChanged(nameof(SpeedLimit));
}
}
public override void WriteLine(string text)
=> Dispatcher.UIThread.Invoke(() => LogEntries.Add(new(DateTime.Now, text.Trim())));
protected override ProcessBookViewModelBase CreateNewProcessBook(LibraryBook libraryBook)
=> new ProcessBookViewModel(libraryBook, Logger);
private static AvaloniaList<ProcessBookViewModelBase> CreateEmptyList()
{
if (Design.IsDesignMode)
_ = Configuration.Instance.LibationFiles;
return new AvaloniaList<ProcessBookViewModelBase>();
}
}

View File

@ -2,7 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:LibationAvalonia.ViewModels" xmlns:vm="clr-namespace:LibationUiBase.ProcessQueue;assembly=LibationUiBase"
xmlns:views="clr-namespace:LibationAvalonia.Views" xmlns:views="clr-namespace:LibationAvalonia.Views"
x:DataType="vm:ProcessBookViewModel" x:DataType="vm:ProcessBookViewModel"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="87" MaxHeight="87" MinHeight="87" MinWidth="300" mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="87" MaxHeight="87" MinHeight="87" MinWidth="300"

View File

@ -2,7 +2,6 @@ using ApplicationServices;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using DataLayer; using DataLayer;
using LibationAvalonia.ViewModels;
using LibationUiBase; using LibationUiBase;
using LibationUiBase.ProcessQueue; using LibationUiBase.ProcessQueue;
@ -31,10 +30,8 @@ namespace LibationAvalonia.Views
if (Design.IsDesignMode) if (Design.IsDesignMode)
{ {
using var context = DbContexts.GetContext(); using var context = DbContexts.GetContext();
DataContext = new ProcessBookViewModel( ViewModels.MainVM.Configure_NonUI();
context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"), DataContext = new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"));
LogMe.RegisterForm(default(ILogForm))
);
return; return;
} }
} }
@ -44,7 +41,7 @@ namespace LibationAvalonia.Views
public void Cancel_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public void Cancel_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> CancelButtonClicked?.Invoke(DataItem); => CancelButtonClicked?.Invoke(DataItem);
public void MoveFirst_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public void MoveFirst_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> PositionButtonClicked?.Invoke(DataItem, QueuePosition.Fisrt); => PositionButtonClicked?.Invoke(DataItem, QueuePosition.First);
public void MoveUp_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public void MoveUp_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> PositionButtonClicked?.Invoke(DataItem, QueuePosition.OneUp); => PositionButtonClicked?.Invoke(DataItem, QueuePosition.OneUp);
public void MoveDown_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public void MoveDown_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)

View File

@ -34,7 +34,7 @@
HorizontalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
AllowAutoHide="False"> AllowAutoHide="False">
<ItemsControl ItemsSource="{Binding Items}"> <ItemsControl ItemsSource="{Binding Queue}">
<ItemsControl.ItemsPanel> <ItemsControl.ItemsPanel>
<ItemsPanelTemplate> <ItemsPanelTemplate>
<VirtualizingStackPanel /> <VirtualizingStackPanel />

View File

@ -1,9 +1,7 @@
using ApplicationServices; using ApplicationServices;
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using DataLayer; using DataLayer;
using LibationAvalonia.ViewModels;
using LibationUiBase; using LibationUiBase;
using LibationUiBase.ProcessQueue; using LibationUiBase.ProcessQueue;
using System; using System;
@ -17,7 +15,7 @@ namespace LibationAvalonia.Views
{ {
public partial class ProcessQueueControl : UserControl public partial class ProcessQueueControl : UserControl
{ {
private TrackedQueue<ProcessBookViewModelBase>? Queue => _viewModel?.Queue; private TrackedQueue<ProcessBookViewModel>? Queue => _viewModel?.Queue;
private ProcessQueueViewModel? _viewModel => DataContext as ProcessQueueViewModel; private ProcessQueueViewModel? _viewModel => DataContext as ProcessQueueViewModel;
public ProcessQueueControl() public ProcessQueueControl()
@ -31,48 +29,49 @@ namespace LibationAvalonia.Views
#if DEBUG #if DEBUG
if (Design.IsDesignMode) if (Design.IsDesignMode)
{ {
_ = LibationFileManager.Configuration.Instance.LibationFiles;
ViewModels.MainVM.Configure_NonUI();
var vm = new ProcessQueueViewModel(); var vm = new ProcessQueueViewModel();
var Logger = LogMe.RegisterForm(vm);
DataContext = vm; DataContext = vm;
using var context = DbContexts.GetContext(); using var context = DbContexts.GetContext();
List<ProcessBookViewModel> testList = new() List<ProcessBookViewModel> testList = new()
{ {
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"), Logger) new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"))
{ {
Result = ProcessBookResult.FailedAbort, Result = ProcessBookResult.FailedAbort,
Status = ProcessBookStatus.Failed, Status = ProcessBookStatus.Failed,
}, },
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IWVG"), Logger) new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IWVG"))
{ {
Result = ProcessBookResult.FailedSkip, Result = ProcessBookResult.FailedSkip,
Status = ProcessBookStatus.Failed, Status = ProcessBookStatus.Failed,
}, },
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q"), Logger) new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q"))
{ {
Result = ProcessBookResult.FailedRetry, Result = ProcessBookResult.FailedRetry,
Status = ProcessBookStatus.Failed, Status = ProcessBookStatus.Failed,
}, },
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NUPO"), Logger) new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NUPO"))
{ {
Result = ProcessBookResult.ValidationFail, Result = ProcessBookResult.ValidationFail,
Status = ProcessBookStatus.Failed, Status = ProcessBookStatus.Failed,
}, },
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NMX4"), Logger) new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NMX4"))
{ {
Result = ProcessBookResult.Cancelled, Result = ProcessBookResult.Cancelled,
Status = ProcessBookStatus.Cancelled, Status = ProcessBookStatus.Cancelled,
}, },
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0"), Logger) new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0"))
{ {
Result = ProcessBookResult.Success, Result = ProcessBookResult.Success,
Status = ProcessBookStatus.Completed, Status = ProcessBookStatus.Completed,
}, },
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6"), Logger) new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6"))
{ {
Result = ProcessBookResult.None, Result = ProcessBookResult.None,
Status = ProcessBookStatus.Working, Status = ProcessBookStatus.Working,
}, },
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"), Logger) new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"))
{ {
Result = ProcessBookResult.None, Result = ProcessBookResult.None,
Status = ProcessBookStatus.Queued, Status = ProcessBookStatus.Queued,

View File

@ -1,7 +0,0 @@
namespace LibationUiBase
{
public interface ILogForm
{
void WriteLine(string text);
}
}

View File

@ -1,56 +0,0 @@
using System;
using System.Threading.Tasks;
namespace LibationUiBase
{
// decouple serilog and form. include convenience factory method
public class LogMe
{
public event EventHandler<string> LogInfo;
public event EventHandler<string> LogErrorString;
public event EventHandler<(Exception, string)> LogError;
private LogMe()
{
LogInfo += (_, text) => Serilog.Log.Logger.Information($"Automated backup: {text}");
LogErrorString += (_, text) => Serilog.Log.Logger.Error(text);
LogError += (_, tuple) => Serilog.Log.Logger.Error(tuple.Item1, tuple.Item2 ?? "Automated backup: error");
}
private static ILogForm LogForm;
public static LogMe RegisterForm<T>(T form) where T : ILogForm
{
var logMe = new LogMe();
if (form is null)
return logMe;
LogForm = form;
logMe.LogInfo += LogMe_LogInfo;
logMe.LogErrorString += LogMe_LogErrorString;
logMe.LogError += LogMe_LogError;
return logMe;
}
private static async void LogMe_LogError(object sender, (Exception, string) tuple)
{
await Task.Run(() => LogForm?.WriteLine(tuple.Item2 ?? "Automated backup: error"));
await Task.Run(() => LogForm?.WriteLine("ERROR: " + tuple.Item1.Message));
}
private static async void LogMe_LogErrorString(object sender, string text)
{
await Task.Run(() => LogForm?.WriteLine(text));
}
private static async void LogMe_LogInfo(object sender, string text)
{
await Task.Run(() => LogForm?.WriteLine(text));
}
public void Info(string text) => LogInfo?.Invoke(this, text);
public void Error(string text) => LogErrorString?.Invoke(this, text);
public void Error(Exception ex, string text = null) => LogError?.Invoke(this, (ex, text));
}
}

View File

@ -1,4 +1,4 @@
using ApplicationServices; using ApplicationServices;
using AudibleApi; using AudibleApi;
using AudibleApi.Common; using AudibleApi.Common;
using DataLayer; using DataLayer;
@ -40,9 +40,8 @@ public enum ProcessBookStatus
/// <summary> /// <summary>
/// This is the viewmodel for queued processables /// This is the viewmodel for queued processables
/// </summary> /// </summary>
public abstract class ProcessBookViewModelBase : ReactiveObject public class ProcessBookViewModel : ReactiveObject
{ {
private readonly LogMe Logger;
public LibraryBook LibraryBook { get; protected set; } public LibraryBook LibraryBook { get; protected set; }
private ProcessBookResult _result = ProcessBookResult.None; private ProcessBookResult _result = ProcessBookResult.None;
@ -84,6 +83,21 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
#endregion #endregion
#region Process Queue Logging
public event EventHandler<string>? LogWritten;
private void OnLogWritten(string text) => LogWritten?.Invoke(this, text.Trim());
private void LogError(string? message, Exception? ex = null)
{
OnLogWritten(message ?? "Automated backup: error");
if (ex is not null)
OnLogWritten("ERROR: " + ex.Message);
}
private void LogInfo(string text) => OnLogWritten(text);
#endregion
protected Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke(); protected Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke();
protected void NextProcessable() => _currentProcessable = null; protected void NextProcessable() => _currentProcessable = null;
private Processable? _currentProcessable; private Processable? _currentProcessable;
@ -91,10 +105,9 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
/// <summary> A series of Processable actions to perform on this book </summary> /// <summary> A series of Processable actions to perform on this book </summary>
protected Queue<Func<Processable>> Processes { get; } = new(); protected Queue<Func<Processable>> Processes { get; } = new();
protected ProcessBookViewModelBase(LibraryBook libraryBook, LogMe logme) public ProcessBookViewModel(LibraryBook libraryBook)
{ {
LibraryBook = libraryBook; LibraryBook = libraryBook;
Logger = logme;
_title = LibraryBook.Book.TitleWithSubtitle; _title = LibraryBook.Book.TitleWithSubtitle;
_author = LibraryBook.Book.AuthorNames(); _author = LibraryBook.Book.AuthorNames();
@ -106,15 +119,14 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
PictureStorage.PictureCached += PictureStorage_PictureCached; PictureStorage.PictureCached += PictureStorage_PictureCached;
// Mutable property. Set the field so PropertyChanged isn't fired. // Mutable property. Set the field so PropertyChanged isn't fired.
_cover = LoadImageFromBytes(picture, PictureSize._80x80); _cover = BaseUtil.LoadImage(picture, PictureSize._80x80);
} }
protected abstract object? LoadImageFromBytes(byte[] bytes, PictureSize pictureSize);
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)
{ {
Cover = LoadImageFromBytes(e.Picture, PictureSize._80x80); Cover = BaseUtil.LoadImage(e.Picture, PictureSize._80x80);
PictureStorage.PictureCached -= PictureStorage_PictureCached; PictureStorage.PictureCached -= PictureStorage_PictureCached;
} }
} }
@ -133,36 +145,36 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
result = ProcessBookResult.Success; result = ProcessBookResult.Success;
else if (statusHandler.Errors.Contains("Cancelled")) else if (statusHandler.Errors.Contains("Cancelled"))
{ {
Logger.Info($"{procName}: Process was cancelled - {LibraryBook.Book}"); LogInfo($"{procName}: Process was cancelled - {LibraryBook.Book}");
result = ProcessBookResult.Cancelled; result = ProcessBookResult.Cancelled;
} }
else if (statusHandler.Errors.Contains("Validation failed")) else if (statusHandler.Errors.Contains("Validation failed"))
{ {
Logger.Info($"{procName}: Validation failed - {LibraryBook.Book}"); LogInfo($"{procName}: Validation failed - {LibraryBook.Book}");
result = ProcessBookResult.ValidationFail; result = ProcessBookResult.ValidationFail;
} }
else else
{ {
foreach (var errorMessage in statusHandler.Errors) foreach (var errorMessage in statusHandler.Errors)
Logger.Error($"{procName}: {errorMessage}"); LogError($"{procName}: {errorMessage}");
} }
} }
catch (ContentLicenseDeniedException ldex) catch (ContentLicenseDeniedException ldex)
{ {
if (ldex.AYCL?.RejectionReason is null or RejectionReason.GenericError) if (ldex.AYCL?.RejectionReason is null or RejectionReason.GenericError)
{ {
Logger.Info($"{procName}: Content license was denied, but this error appears to be caused by a temporary interruption of service. - {LibraryBook.Book}"); LogInfo($"{procName}: Content license was denied, but this error appears to be caused by a temporary interruption of service. - {LibraryBook.Book}");
result = ProcessBookResult.LicenseDeniedPossibleOutage; result = ProcessBookResult.LicenseDeniedPossibleOutage;
} }
else else
{ {
Logger.Info($"{procName}: Content license denied. Check your Audible account to see if you have access to this title. - {LibraryBook.Book}"); LogInfo($"{procName}: Content license denied. Check your Audible account to see if you have access to this title. - {LibraryBook.Book}");
result = ProcessBookResult.LicenseDenied; result = ProcessBookResult.LicenseDenied;
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.Error(ex, procName); LogError(procName, ex);
} }
finally finally
{ {
@ -192,15 +204,15 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.Error(ex, $"{CurrentProcessable.Name}: Error while cancelling"); LogError($"{CurrentProcessable.Name}: Error while cancelling", ex);
} }
} }
public ProcessBookViewModelBase AddDownloadPdf() => AddProcessable<DownloadPdf>(); public ProcessBookViewModel AddDownloadPdf() => AddProcessable<DownloadPdf>();
public ProcessBookViewModelBase AddDownloadDecryptBook() => AddProcessable<DownloadDecryptBook>(); public ProcessBookViewModel AddDownloadDecryptBook() => AddProcessable<DownloadDecryptBook>();
public ProcessBookViewModelBase AddConvertToMp3() => AddProcessable<ConvertToMp3>(); public ProcessBookViewModel AddConvertToMp3() => AddProcessable<ConvertToMp3>();
private ProcessBookViewModelBase AddProcessable<T>() where T : Processable, new() private ProcessBookViewModel AddProcessable<T>() where T : Processable, new()
{ {
Processes.Enqueue(() => new T()); Processes.Enqueue(() => new T());
return this; return this;
@ -252,7 +264,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
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 void AudioDecodable_CoverImageDiscovered(object? sender, byte[] coverArt) private void AudioDecodable_CoverImageDiscovered(object? sender, byte[] coverArt)
=> Cover = LoadImageFromBytes(coverArt, PictureSize._80x80); => Cover = BaseUtil.LoadImage(coverArt, PictureSize._80x80);
private byte[] AudioDecodable_RequestCoverArt(object? sender, EventArgs e) private byte[] AudioDecodable_RequestCoverArt(object? sender, EventArgs e)
{ {
@ -292,7 +304,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
Status = ProcessBookStatus.Working; Status = ProcessBookStatus.Working;
if (sender is Processable processable) if (sender is Processable processable)
Logger.Info($"{Environment.NewLine}{processable.Name} Step, Begin: {libraryBook.Book}"); LogInfo($"{Environment.NewLine}{processable.Name} Step, Begin: {libraryBook.Book}");
Title = libraryBook.Book.TitleWithSubtitle; Title = libraryBook.Book.TitleWithSubtitle;
Author = libraryBook.Book.AuthorNames(); Author = libraryBook.Book.AuthorNames();
@ -303,7 +315,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
{ {
if (sender is Processable processable) if (sender is Processable processable)
{ {
Logger.Info($"{processable.Name} Step, Completed: {libraryBook.Book}"); LogInfo($"{processable.Name} Step, Completed: {libraryBook.Book}");
UnlinkProcessable(processable); UnlinkProcessable(processable);
} }
@ -329,7 +341,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
if (result.HasErrors) if (result.HasErrors)
{ {
foreach (var errorMessage in result.Errors.Where(e => e != "Validation failed")) foreach (var errorMessage in result.Errors.Where(e => e != "Validation failed"))
Logger.Error(errorMessage); LogError(errorMessage);
} }
} }
@ -340,7 +352,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
protected async Task<ProcessBookResult> GetFailureActionAsync(LibraryBook libraryBook) protected async Task<ProcessBookResult> GetFailureActionAsync(LibraryBook libraryBook)
{ {
const DialogResult SkipResult = DialogResult.Ignore; const DialogResult SkipResult = DialogResult.Ignore;
Logger.Error($"ERROR. All books have not been processed. Book failed: {libraryBook.Book}"); LogError($"ERROR. All books have not been processed. Book failed: {libraryBook.Book}");
DialogResult? dialogResult = Configuration.Instance.BadBook switch DialogResult? dialogResult = Configuration.Instance.BadBook switch
{ {
@ -353,7 +365,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
if (dialogResult == SkipResult) if (dialogResult == SkipResult)
{ {
libraryBook.UpdateBookStatus(LiberatedStatus.Error); libraryBook.UpdateBookStatus(LiberatedStatus.Error);
Logger.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.TitleWithSubtitle}"); LogInfo($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.TitleWithSubtitle}");
} }
return dialogResult is SkipResult ? ProcessBookResult.FailedSkip return dialogResult is SkipResult ? ProcessBookResult.FailedSkip

View File

@ -1,30 +1,32 @@
using DataLayer; using ApplicationServices;
using DataLayer;
using LibationUiBase.Forms; using LibationUiBase.Forms;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using ApplicationServices;
using System.Threading.Tasks; using System.Threading.Tasks;
#nullable enable #nullable enable
namespace LibationUiBase.ProcessQueue; namespace LibationUiBase.ProcessQueue;
public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm public record LogEntry(DateTime LogDate, string LogMessage)
{ {
public abstract void WriteLine(string text); public string LogDateString => LogDate.ToShortTimeString();
protected abstract ProcessBookViewModelBase CreateNewProcessBook(LibraryBook libraryBook); }
public TrackedQueue<ProcessBookViewModelBase> Queue { get; } public class ProcessQueueViewModel : ReactiveObject
{
public ObservableCollection<LogEntry> LogEntries { get; } = new();
public TrackedQueue<ProcessBookViewModel> Queue { get; } = new();
public Task? QueueRunner { get; private set; } public Task? QueueRunner { get; private set; }
public bool Running => !QueueRunner?.IsCompleted ?? false; public bool Running => !QueueRunner?.IsCompleted ?? false;
protected LogMe Logger { get; }
public ProcessQueueViewModelBase(ICollection<ProcessBookViewModelBase>? underlyingList) public ProcessQueueViewModel()
{ {
Logger = LogMe.RegisterForm(this);
Queue = new(underlyingList);
Queue.QueuedCountChanged += Queue_QueuedCountChanged; Queue.QueuedCountChanged += Queue_QueuedCountChanged;
Queue.CompletedCountChanged += Queue_CompletedCountChanged; Queue.CompletedCountChanged += Queue_CompletedCountChanged;
SpeedLimit = LibationFileManager.Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
} }
private int _completedCount; private int _completedCount;
@ -32,6 +34,7 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
private int _queuedCount; private int _queuedCount;
private string? _runningTime; private string? _runningTime;
private bool _progressBarVisible; private bool _progressBarVisible;
private decimal _speedLimit;
public int CompletedCount { get => _completedCount; private set { RaiseAndSetIfChanged(ref _completedCount, value); RaisePropertyChanged(nameof(AnyCompleted)); } } public int CompletedCount { get => _completedCount; private set { RaiseAndSetIfChanged(ref _completedCount, value); RaisePropertyChanged(nameof(AnyCompleted)); } }
public int QueuedCount { get => _queuedCount; private set { this.RaiseAndSetIfChanged(ref _queuedCount, value); RaisePropertyChanged(nameof(AnyQueued)); } } public int QueuedCount { get => _queuedCount; private set { this.RaiseAndSetIfChanged(ref _queuedCount, value); RaisePropertyChanged(nameof(AnyQueued)); } }
@ -42,6 +45,32 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
public bool AnyQueued => QueuedCount > 0; public bool AnyQueued => QueuedCount > 0;
public bool AnyErrors => ErrorCount > 0; public bool AnyErrors => ErrorCount > 0;
public double Progress => 100d * Queue.Completed.Count / Queue.Count; public double Progress => 100d * Queue.Completed.Count / Queue.Count;
public decimal SpeedLimitIncrement { get; private set; }
public decimal SpeedLimit
{
get => _speedLimit;
set
{
var newValue = Math.Min(999 * 1024 * 1024, (long)Math.Ceiling(value * 1024 * 1024));
var config = LibationFileManager.Configuration.Instance;
config.DownloadSpeedLimit = newValue;
_speedLimit
= config.DownloadSpeedLimit <= newValue ? value
: value == 0.01m ? config.DownloadSpeedLimit / 1024m / 1024
: 0;
config.DownloadSpeedLimit = (long)(_speedLimit * 1024 * 1024);
SpeedLimitIncrement = _speedLimit > 100 ? 10
: _speedLimit > 10 ? 1
: _speedLimit > 1 ? 0.1m
: 0.01m;
RaisePropertyChanged(nameof(SpeedLimitIncrement));
RaisePropertyChanged(nameof(SpeedLimit));
}
}
private void Queue_CompletedCountChanged(object? sender, int e) private void Queue_CompletedCountChanged(object? sender, int e)
{ {
@ -59,6 +88,9 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
RaisePropertyChanged(nameof(Progress)); RaisePropertyChanged(nameof(Progress));
} }
private void ProcessBook_LogWritten(object? sender, string logMessage)
=> Invoke(() => LogEntries.Add(new(DateTime.Now, logMessage.Trim())));
#region Add Books to Queue #region Add Books to Queue
public bool QueueDownloadPdf(IList<LibraryBook> libraryBooks) public bool QueueDownloadPdf(IList<LibraryBook> libraryBooks)
@ -124,47 +156,50 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
} }
private bool IsBookInQueue(LibraryBook libraryBook) private bool IsBookInQueue(LibraryBook libraryBook)
=> Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is not ProcessBookViewModelBase entry ? false => Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is not ProcessBookViewModel entry ? false
: entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed ? !Queue.RemoveCompleted(entry) : entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed ? !Queue.RemoveCompleted(entry)
: true; : true;
private bool RemoveCompleted(LibraryBook libraryBook) private bool RemoveCompleted(LibraryBook libraryBook)
=> Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is ProcessBookViewModelBase entry => Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is ProcessBookViewModel entry
&& entry.Status is ProcessBookStatus.Completed && entry.Status is ProcessBookStatus.Completed
&& Queue.RemoveCompleted(entry); && Queue.RemoveCompleted(entry);
private void AddDownloadPdf(IEnumerable<LibraryBook> entries) private void AddDownloadPdf(IList<LibraryBook> entries)
{ {
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray(); var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
Serilog.Log.Logger.Information("Queueing {count} books for PDF-only download", procs.Length); Serilog.Log.Logger.Information("Queueing {count} books for PDF-only download", procs.Length);
AddToQueue(procs); AddToQueue(procs);
ProcessBookViewModelBase Create(LibraryBook entry) ProcessBookViewModel Create(LibraryBook entry)
=> CreateNewProcessBook(entry).AddDownloadPdf(); => new ProcessBookViewModel(entry).AddDownloadPdf();
} }
private void AddDownloadDecrypt(IEnumerable<LibraryBook> entries) private void AddDownloadDecrypt(IList<LibraryBook> entries)
{ {
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray(); var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
Serilog.Log.Logger.Information("Queueing {count} books ofr download/decrypt", procs.Length); Serilog.Log.Logger.Information("Queueing {count} books ofr download/decrypt", procs.Length);
AddToQueue(procs); AddToQueue(procs);
ProcessBookViewModelBase Create(LibraryBook entry) ProcessBookViewModel Create(LibraryBook entry)
=> CreateNewProcessBook(entry).AddDownloadDecryptBook().AddDownloadPdf(); => new ProcessBookViewModel(entry).AddDownloadDecryptBook().AddDownloadPdf();
} }
private void AddConvertMp3(IEnumerable<LibraryBook> entries) private void AddConvertMp3(IList<LibraryBook> entries)
{ {
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray(); var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
Serilog.Log.Logger.Information("Queueing {count} books for mp3 conversion", procs.Length); Serilog.Log.Logger.Information("Queueing {count} books for mp3 conversion", procs.Length);
AddToQueue(procs); AddToQueue(procs);
ProcessBookViewModelBase Create(LibraryBook entry) ProcessBookViewModel Create(LibraryBook entry)
=> CreateNewProcessBook(entry).AddConvertToMp3(); => new ProcessBookViewModel(entry).AddConvertToMp3();
} }
private void AddToQueue(IEnumerable<ProcessBookViewModelBase> pbook) private void AddToQueue(IList<ProcessBookViewModel> pbook)
{ {
foreach (var book in pbook)
book.LogWritten += ProcessBook_LogWritten;
Queue.Enqueue(pbook); Queue.Enqueue(pbook);
if (!Running) if (!Running)
QueueRunner = Task.Run(QueueLoop); QueueRunner = Task.Run(QueueLoop);
@ -187,7 +222,7 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
while (Queue.MoveNext()) while (Queue.MoveNext())
{ {
if (Queue.Current is not ProcessBookViewModelBase nextBook) if (Queue.Current is not ProcessBookViewModel nextBook)
{ {
Serilog.Log.Logger.Information("Current queue item is empty."); Serilog.Log.Logger.Information("Current queue item is empty.");
continue; continue;

View File

@ -1,5 +1,7 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq; using System.Linq;
#nullable enable #nullable enable
@ -7,7 +9,7 @@ namespace LibationUiBase
{ {
public enum QueuePosition public enum QueuePosition
{ {
Fisrt, First,
OneUp, OneUp,
OneDown, OneDown,
Last, Last,
@ -23,37 +25,20 @@ namespace LibationUiBase
* *
* The index is the link position from the first link you lifted to the * The index is the link position from the first link you lifted to the
* last one in the chain. * last one in the chain.
*
*
* For this to work with Avalonia's ItemsRepeater, it must be an ObservableCollection
* (not merely a Collection with INotifyCollectionChanged, INotifyPropertyChanged).
* So TrackedQueue maintains 2 copies of the list. The primary copy of the list is
* split into Completed, Current and Queued and is used by ProcessQueue to keep track
* of what's what. The secondary copy is a concatenation of primary's three sources
* and is stored in ObservableCollection.Items. When the primary list changes, the
* secondary list is cleared and reset to match the primary.
*/ */
public class TrackedQueue<T> where T : class public class TrackedQueue<T> : IReadOnlyCollection<T>, IList, INotifyCollectionChanged where T : class
{ {
public event EventHandler<int>? CompletedCountChanged; public event EventHandler<int>? CompletedCountChanged;
public event EventHandler<int>? QueuedCountChanged; public event EventHandler<int>? QueuedCountChanged;
public event NotifyCollectionChangedEventHandler? CollectionChanged;
public T? Current { get; private set; } public T? Current { get; private set; }
public IReadOnlyList<T> Queued => _queued;
public IReadOnlyList<T> Completed => _completed; public IReadOnlyList<T> Completed => _completed;
private List<T> Queued { get; } = new();
private readonly List<T> _queued = new();
private readonly List<T> _completed = new(); private readonly List<T> _completed = new();
private readonly object lockObject = new(); private readonly object lockObject = new();
private int QueueStartIndex => Completed.Count + (Current is null ? 0 : 1);
private readonly ICollection<T>? _underlyingList;
public ICollection<T>? UnderlyingList => _underlyingList;
public TrackedQueue(ICollection<T>? underlyingList = null)
{
_underlyingList = underlyingList;
}
public T this[int index] public T this[int index]
{ {
@ -61,17 +46,10 @@ namespace LibationUiBase
{ {
lock (lockObject) lock (lockObject)
{ {
if (index < _completed.Count) return index < Completed.Count ? Completed[index]
return _completed[index]; : index == Completed.Count && Current is not null ? Current
index -= _completed.Count; : index < Count ? Queued[index - QueueStartIndex]
: throw new IndexOutOfRangeException();
if (index == 0 && Current != null) return Current;
if (Current != null) index--;
if (index < _queued.Count) return _queued.ElementAt(index);
throw new IndexOutOfRangeException();
} }
} }
} }
@ -82,7 +60,7 @@ namespace LibationUiBase
{ {
lock (lockObject) lock (lockObject)
{ {
return _queued.Count + _completed.Count + (Current == null ? 0 : 1); return QueueStartIndex + Queued.Count;
} }
} }
} }
@ -91,131 +69,117 @@ namespace LibationUiBase
{ {
lock (lockObject) lock (lockObject)
{ {
if (_completed.Contains(item)) int index = _completed.IndexOf(item);
return _completed.IndexOf(item); if (index < 0 && item == Current)
index = Completed.Count;
if (Current == item) return _completed.Count; if (index < 0)
{
if (_queued.Contains(item)) index = Queued.IndexOf(item);
return _queued.IndexOf(item) + (Current is null ? 0 : 1); if (index >= 0)
return -1; index += QueueStartIndex;
}
return index;
} }
} }
public bool RemoveQueued(T item) public bool RemoveQueued(T item)
{ {
bool itemsRemoved; int queuedCount, queueIndex;
int queuedCount;
lock (lockObject) lock (lockObject)
{ {
itemsRemoved = _queued.Remove(item); queueIndex = Queued.IndexOf(item);
queuedCount = _queued.Count; if (queueIndex >= 0)
Queued.RemoveAt(queueIndex);
queuedCount = Queued.Count;
} }
if (itemsRemoved) if (queueIndex >= 0)
{ {
QueuedCountChanged?.Invoke(this, queuedCount); QueuedCountChanged?.Invoke(this, queuedCount);
RebuildSecondary(); CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, QueueStartIndex + queueIndex));
return true;
} }
return itemsRemoved; return false;
}
public void ClearCurrent()
{
lock (lockObject)
Current = null;
RebuildSecondary();
} }
public bool RemoveCompleted(T item) public bool RemoveCompleted(T item)
{ {
bool itemsRemoved; int completedCount, completedIndex;
int completedCount;
lock (lockObject) lock (lockObject)
{ {
itemsRemoved = _completed.Remove(item); completedIndex = _completed.IndexOf(item);
if (completedIndex >= 0)
_completed.RemoveAt(completedIndex);
completedCount = _completed.Count; completedCount = _completed.Count;
} }
if (itemsRemoved) if (completedIndex >= 0)
{ {
CompletedCountChanged?.Invoke(this, completedCount); CompletedCountChanged?.Invoke(this, completedCount);
RebuildSecondary(); CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, completedIndex));
return true;
} }
return itemsRemoved; return false;
}
public void ClearCurrent()
{
T? current;
lock (lockObject)
{
current = Current;
Current = null;
}
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, current, _completed.Count));
} }
public void ClearQueue() public void ClearQueue()
{ {
List<T> queuedItems;
lock (lockObject) lock (lockObject)
_queued.Clear(); {
queuedItems = Queued.ToList();
Queued.Clear();
}
QueuedCountChanged?.Invoke(this, 0); QueuedCountChanged?.Invoke(this, 0);
RebuildSecondary(); CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, queuedItems, QueueStartIndex));
} }
public void ClearCompleted() public void ClearCompleted()
{ {
List<T> completedItems;
lock (lockObject) lock (lockObject)
{
completedItems = _completed.ToList();
_completed.Clear(); _completed.Clear();
}
CompletedCountChanged?.Invoke(this, 0); CompletedCountChanged?.Invoke(this, 0);
RebuildSecondary(); CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, completedItems, 0));
}
public bool Any(Func<T, bool> predicate)
{
lock (lockObject)
{
return (Current != null && predicate(Current)) || _completed.Any(predicate) || _queued.Any(predicate);
}
}
public T? FirstOrDefault(Func<T, bool> predicate)
{
lock (lockObject)
{
return Current != null && predicate(Current) ? Current
: _completed.FirstOrDefault(predicate) is T completed ? completed
: _queued.FirstOrDefault(predicate);
}
} }
public void MoveQueuePosition(T item, QueuePosition requestedPosition) public void MoveQueuePosition(T item, QueuePosition requestedPosition)
{ {
int oldIndex, newIndex;
lock (lockObject) lock (lockObject)
{ {
if (_queued.Count == 0 || !_queued.Contains(item)) return; oldIndex = Queued.IndexOf(item);
newIndex = requestedPosition switch
{
QueuePosition.First => 0,
QueuePosition.OneUp => oldIndex - 1,
QueuePosition.OneDown => oldIndex + 1,
QueuePosition.Last or _ => Queued.Count - 1
};
if ((requestedPosition == QueuePosition.Fisrt || requestedPosition == QueuePosition.OneUp) && _queued[0] == item) if (oldIndex < 0 || newIndex < 0 || newIndex >= Queued.Count || newIndex == oldIndex)
return;
if ((requestedPosition == QueuePosition.Last || requestedPosition == QueuePosition.OneDown) && _queued[^1] == item)
return; return;
int queueIndex = _queued.IndexOf(item); Queued.RemoveAt(oldIndex);
Queued.Insert(newIndex, item);
if (requestedPosition == QueuePosition.OneUp)
{
_queued.RemoveAt(queueIndex);
_queued.Insert(queueIndex - 1, item);
} }
else if (requestedPosition == QueuePosition.OneDown) CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, item, QueueStartIndex + newIndex, QueueStartIndex + oldIndex));
{
_queued.RemoveAt(queueIndex);
_queued.Insert(queueIndex + 1, item);
}
else if (requestedPosition == QueuePosition.Fisrt)
{
_queued.RemoveAt(queueIndex);
_queued.Insert(0, item);
}
else
{
_queued.RemoveAt(queueIndex);
_queued.Insert(_queued.Count, item);
}
}
RebuildSecondary();
} }
public bool MoveNext() public bool MoveNext()
@ -232,15 +196,15 @@ namespace LibationUiBase
completedCount = _completed.Count; completedCount = _completed.Count;
completedChanged = true; completedChanged = true;
} }
if (_queued.Count == 0) if (Queued.Count == 0)
{ {
Current = null; Current = null;
return false; return false;
} }
Current = _queued[0]; Current = Queued[0];
_queued.RemoveAt(0); Queued.RemoveAt(0);
queuedCount = _queued.Count; queuedCount = Queued.Count;
return true; return true;
} }
} }
@ -249,34 +213,48 @@ namespace LibationUiBase
if (completedChanged) if (completedChanged)
CompletedCountChanged?.Invoke(this, completedCount); CompletedCountChanged?.Invoke(this, completedCount);
QueuedCountChanged?.Invoke(this, queuedCount); QueuedCountChanged?.Invoke(this, queuedCount);
RebuildSecondary();
} }
} }
public void Enqueue(IEnumerable<T> item) public void Enqueue(IList<T> item)
{ {
int queueCount; int queueCount;
lock (lockObject) lock (lockObject)
{ {
_queued.AddRange(item); Queued.AddRange(item);
queueCount = _queued.Count; queueCount = Queued.Count;
} }
foreach (var i in item)
_underlyingList?.Add(i);
QueuedCountChanged?.Invoke(this, queueCount); QueuedCountChanged?.Invoke(this, queueCount);
} CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, QueueStartIndex + Queued.Count));
private void RebuildSecondary()
{
_underlyingList?.Clear();
foreach (var item in GetAllItems())
_underlyingList?.Add(item);
} }
public IEnumerable<T> GetAllItems() public IEnumerable<T> GetAllItems()
{
lock (lockObject)
{ {
if (Current is null) return Completed.Concat(Queued); if (Current is null) return Completed.Concat(Queued);
return Completed.Concat(new List<T> { Current }).Concat(Queued); return Completed.Concat([Current]).Concat(Queued);
} }
} }
public IEnumerator<T> GetEnumerator() => GetAllItems().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
#region IList interface implementation
object? IList.this[int index] { get => this[index]; set => throw new NotSupportedException(); }
public bool IsReadOnly => true;
public bool IsFixedSize => false;
public bool IsSynchronized => false;
public object SyncRoot => this;
public int IndexOf(object? value) => value is T t ? IndexOf(t) : -1;
public bool Contains(object? value) => IndexOf(value) >= 0;
//These aren't used by anything, but they are IList interface members and this class needs to be an IList for Avalonia
public int Add(object? value) => throw new NotSupportedException();
public void Clear() => throw new NotSupportedException();
public void Insert(int index, object? value) => throw new NotSupportedException();
public void Remove(object? value) => throw new NotSupportedException();
public void RemoveAt(int index) => throw new NotSupportedException();
public void CopyTo(Array array, int index) => throw new NotSupportedException();
#endregion
}
} }

View File

@ -3,33 +3,20 @@ using System;
using System.Drawing; using System.Drawing;
using System.Windows.Forms; using System.Windows.Forms;
#nullable enable
namespace LibationWinForms.ProcessQueue namespace LibationWinForms.ProcessQueue
{ {
internal partial class ProcessBookControl : UserControl internal partial class ProcessBookControl : UserControl
{ {
private readonly int CancelBtnDistanceFromEdge; private readonly int CancelBtnDistanceFromEdge;
private readonly int ProgressBarDistanceFromEdge; private readonly int ProgressBarDistanceFromEdge;
private object? m_OldContext;
private static Color FailedColor { get; } = Color.LightCoral; private static Color FailedColor { get; } = Color.LightCoral;
private static Color CancelledColor { get; } = Color.Khaki; private static Color CancelledColor { get; } = Color.Khaki;
private static Color QueuedColor { get; } = SystemColors.Control; private static Color QueuedColor { get; } = SystemColors.Control;
private static Color SuccessColor { get; } = Color.PaleGreen; private static Color SuccessColor { get; } = Color.PaleGreen;
private ProcessBookViewModelBase m_Context;
public ProcessBookViewModelBase Context
{
get => m_Context;
set
{
if (m_Context != value)
{
OnContextChanging();
m_Context = value;
OnContextChanged();
}
}
}
public ProcessBookControl() public ProcessBookControl()
{ {
InitializeComponent(); InitializeComponent();
@ -41,35 +28,41 @@ namespace LibationWinForms.ProcessQueue
ProgressBarDistanceFromEdge = Width - progressBar1.Location.X - progressBar1.Width; ProgressBarDistanceFromEdge = Width - progressBar1.Location.X - progressBar1.Width;
} }
private void OnContextChanging() protected override void OnDataContextChanged(EventArgs e)
{ {
if (Context is not null) if (m_OldContext is ProcessBookViewModel oldContext)
Context.PropertyChanged -= Context_PropertyChanged; oldContext.PropertyChanged -= DataContext_PropertyChanged;
if (DataContext is ProcessBookViewModel newContext)
{
m_OldContext = newContext;
newContext.PropertyChanged += DataContext_PropertyChanged;
DataContext_PropertyChanged(DataContext, new System.ComponentModel.PropertyChangedEventArgs(null));
} }
private void OnContextChanged() base.OnDataContextChanged(e);
{
Context.PropertyChanged += Context_PropertyChanged;
Context_PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(null));
} }
private void Context_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) private void DataContext_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{ {
if (sender is not ProcessBookViewModel vm)
return;
SuspendLayout(); SuspendLayout();
if (e.PropertyName is null or nameof(Context.Cover)) if (e.PropertyName is null or nameof(vm.Cover))
SetCover(Context.Cover as Image); SetCover(vm.Cover as Image);
if (e.PropertyName is null or nameof(Context.Title) or nameof(Context.Author) or nameof(Context.Narrator)) if (e.PropertyName is null or nameof(vm.Title) or nameof(vm.Author) or nameof(vm.Narrator))
SetBookInfo($"{Context.Title}\r\nBy {Context.Author}\r\nNarrated by {Context.Narrator}"); SetBookInfo($"{vm.Title}\r\nBy {vm.Author}\r\nNarrated by {vm.Narrator}");
if (e.PropertyName is null or nameof(Context.Status) or nameof(Context.StatusText)) if (e.PropertyName is null or nameof(vm.Status) or nameof(vm.StatusText))
SetStatus(Context.Status, Context.StatusText); SetStatus(vm.Status, vm.StatusText);
if (e.PropertyName is null or nameof(Context.Progress)) if (e.PropertyName is null or nameof(vm.Progress))
SetProgress(Context.Progress); SetProgress(vm.Progress);
if (e.PropertyName is null or nameof(Context.TimeRemaining)) if (e.PropertyName is null or nameof(vm.TimeRemaining))
SetRemainingTime(Context.TimeRemaining); SetRemainingTime(vm.TimeRemaining);
ResumeLayout(); ResumeLayout();
} }
private void SetCover(Image cover) => pictureBox1.Image = cover; private void SetCover(Image? cover) => pictureBox1.Image = cover;
private void SetBookInfo(string title) => bookInfoLbl.Text = title; private void SetBookInfo(string title) => bookInfoLbl.Text = title;
private void SetRemainingTime(TimeSpan remaining) private void SetRemainingTime(TimeSpan remaining)
=> remainingTimeLbl.Text = $"{remaining:mm\\:ss}"; => remainingTimeLbl.Text = $"{remaining:mm\\:ss}";

View File

@ -1,14 +0,0 @@
using DataLayer;
using LibationFileManager;
using LibationUiBase;
using LibationUiBase.ProcessQueue;
namespace LibationWinForms.ProcessQueue;
public class ProcessBookViewModel : ProcessBookViewModelBase
{
public ProcessBookViewModel(LibraryBook libraryBook, LogMe logme) : base(libraryBook, logme) { }
protected override object LoadImageFromBytes(byte[] bytes, PictureSize pictureSize)
=> WinFormsUtil.TryLoadImageOrDefault(bytes, PictureSize._80x80);
}

View File

@ -1,5 +1,6 @@
using LibationFileManager; using LibationFileManager;
using LibationUiBase; using LibationUiBase;
using LibationUiBase.ProcessQueue;
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
@ -27,49 +28,47 @@ internal partial class ProcessQueueControl : UserControl
{ {
InitializeComponent(); InitializeComponent();
var speedLimitMBps = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
numericUpDown1.Value = speedLimitMBps > numericUpDown1.Maximum || speedLimitMBps < numericUpDown1.Minimum ? 0 : speedLimitMBps;
statusStrip1.Items.Add(PopoutButton); statusStrip1.Items.Add(PopoutButton);
virtualFlowControl2.ButtonClicked += VirtualFlowControl2_ButtonClicked; virtualFlowControl2.ButtonClicked += VirtualFlowControl2_ButtonClicked;
virtualFlowControl2.DataContext = ViewModel.Queue;
ViewModel.LogWritten += (_, text) => WriteLine(text);
ViewModel.PropertyChanged += ProcessQueue_PropertyChanged; ViewModel.PropertyChanged += ProcessQueue_PropertyChanged;
virtualFlowControl2.Items = ViewModel.Items; ViewModel.LogEntries.CollectionChanged += LogEntries_CollectionChanged;
Load += ProcessQueueControl_Load;
} }
private void ProcessQueueControl_Load(object? sender, EventArgs e) private void LogEntries_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (!IsDisposed && e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
{
foreach(var entry in e.NewItems?.OfType<LogEntry>() ?? [])
logDGV.Rows.Add(entry.LogDate, entry.LogMessage);
}
}
protected override void OnLoad(EventArgs e)
{ {
if (DesignMode) return; if (DesignMode) return;
ProcessQueue_PropertyChanged(this, new PropertyChangedEventArgs(null)); ProcessQueue_PropertyChanged(this, new PropertyChangedEventArgs(null));
} }
public void WriteLine(string text)
{
if (!IsDisposed)
logDGV.Rows.Add(DateTime.Now, text.Trim());
}
private async void cancelAllBtn_Click(object? sender, EventArgs e) private async void cancelAllBtn_Click(object? sender, EventArgs e)
{ {
ViewModel.Queue.ClearQueue(); ViewModel.Queue.ClearQueue();
if (ViewModel.Queue.Current is not null) if (ViewModel.Queue.Current is not null)
await ViewModel.Queue.Current.CancelAsync(); await ViewModel.Queue.Current.CancelAsync();
virtualFlowControl2.RefreshDisplay();
} }
private void btnClearFinished_Click(object? sender, EventArgs e) private void btnClearFinished_Click(object? sender, EventArgs e)
{ {
ViewModel.Queue.ClearCompleted(); ViewModel.Queue.ClearCompleted();
virtualFlowControl2.RefreshDisplay();
if (!ViewModel.Running) if (!ViewModel.Running)
runningTimeLbl.Text = string.Empty; runningTimeLbl.Text = string.Empty;
} }
private void clearLogBtn_Click(object? sender, EventArgs e) private void clearLogBtn_Click(object? sender, EventArgs e)
{ {
ViewModel.LogEntries.Clear();
logDGV.Rows.Clear(); logDGV.Rows.Clear();
} }
@ -92,7 +91,6 @@ internal partial class ProcessQueueControl : UserControl
{ {
queueNumberLbl.Text = ViewModel.QueuedCount.ToString(); queueNumberLbl.Text = ViewModel.QueuedCount.ToString();
queueNumberLbl.Visible = ViewModel.QueuedCount > 0; queueNumberLbl.Visible = ViewModel.QueuedCount > 0;
virtualFlowControl2.RefreshDisplay();
} }
if (e.PropertyName is null or nameof(ViewModel.ErrorCount)) if (e.PropertyName is null or nameof(ViewModel.ErrorCount))
{ {
@ -117,16 +115,22 @@ internal partial class ProcessQueueControl : UserControl
{ {
runningTimeLbl.Text = ViewModel.RunningTime; runningTimeLbl.Text = ViewModel.RunningTime;
} }
if (e.PropertyName is null or nameof(ViewModel.SpeedLimit))
{
numericUpDown1.Value = ViewModel.SpeedLimit;
numericUpDown1.Increment = ViewModel.SpeedLimitIncrement;
numericUpDown1.DecimalPlaces = ViewModel.SpeedLimit >= 10 ? 0 : ViewModel.SpeedLimit >= 1 ? 1 : 2;
}
} }
/// <summary> /// <summary>
/// View notified the model that a botton was clicked /// View notified the model that a button was clicked
/// </summary> /// </summary>
/// <param name="sender">the <see cref="ProcessBookControl"/> whose button was clicked</param> /// <param name="sender">the <see cref="ProcessBookControl"/> whose button was clicked</param>
/// <param name="buttonName">The name of the button clicked</param> /// <param name="buttonName">The name of the button clicked</param>
private async void VirtualFlowControl2_ButtonClicked(object? sender, string buttonName) private async void VirtualFlowControl2_ButtonClicked(object? sender, string buttonName)
{ {
if (sender is not ProcessBookControl control || control.Context is not ProcessBookViewModel item) if (sender is not ProcessBookControl control || control.DataContext is not ProcessBookViewModel item)
return; return;
try try
@ -135,13 +139,12 @@ internal partial class ProcessQueueControl : UserControl
{ {
await item.CancelAsync(); await item.CancelAsync();
ViewModel.Queue.RemoveQueued(item); ViewModel.Queue.RemoveQueued(item);
virtualFlowControl2.RefreshDisplay();
} }
else else
{ {
QueuePosition? position = buttonName switch QueuePosition? position = buttonName switch
{ {
nameof(ProcessBookControl.moveFirstBtn) => QueuePosition.Fisrt, nameof(ProcessBookControl.moveFirstBtn) => QueuePosition.First,
nameof(ProcessBookControl.moveUpBtn) => QueuePosition.OneUp, nameof(ProcessBookControl.moveUpBtn) => QueuePosition.OneUp,
nameof(ProcessBookControl.moveDownBtn) => QueuePosition.OneDown, nameof(ProcessBookControl.moveDownBtn) => QueuePosition.OneDown,
nameof(ProcessBookControl.moveLastBtn) => QueuePosition.Last, nameof(ProcessBookControl.moveLastBtn) => QueuePosition.Last,
@ -149,10 +152,7 @@ internal partial class ProcessQueueControl : UserControl
}; };
if (position is not null) if (position is not null)
{
ViewModel.Queue.MoveQueuePosition(item, position.Value); ViewModel.Queue.MoveQueuePosition(item, position.Value);
virtualFlowControl2.RefreshDisplay();
}
} }
} }
catch(Exception ex) catch(Exception ex)
@ -164,27 +164,7 @@ internal partial class ProcessQueueControl : UserControl
#endregion #endregion
private void numericUpDown1_ValueChanged(object? sender, EventArgs e) private void numericUpDown1_ValueChanged(object? sender, EventArgs e)
{ => ViewModel.SpeedLimit = numericUpDown1.Value;
var newValue = (long)(numericUpDown1.Value * 1024 * 1024);
var config = Configuration.Instance;
config.DownloadSpeedLimit = newValue;
if (config.DownloadSpeedLimit > newValue)
numericUpDown1.Value =
numericUpDown1.Value == 0.01m ? config.DownloadSpeedLimit / 1024m / 1024
: 0;
numericUpDown1.Increment =
numericUpDown1.Value > 100 ? 10
: numericUpDown1.Value > 10 ? 1
: numericUpDown1.Value > 1 ? 0.1m
: 0.01m;
numericUpDown1.DecimalPlaces =
numericUpDown1.Value >= 10 ? 0
: numericUpDown1.Value >= 1 ? 1
: 2;
}
} }
public class NumericUpDownSuffix : NumericUpDown public class NumericUpDownSuffix : NumericUpDown

View File

@ -1,24 +0,0 @@
using DataLayer;
using LibationUiBase.ProcessQueue;
using System;
using System.Collections.Generic;
#nullable enable
namespace LibationWinForms.ProcessQueue;
internal class ProcessQueueViewModel : ProcessQueueViewModelBase
{
public event EventHandler<string>? LogWritten;
public List<ProcessBookViewModelBase> Items { get; }
public ProcessQueueViewModel() : base(new List<ProcessBookViewModelBase>())
{
Items = Queue.UnderlyingList as List<ProcessBookViewModelBase>
?? throw new ArgumentNullException(nameof(Queue.UnderlyingList));
}
public override void WriteLine(string text) => Invoke(() => LogWritten?.Invoke(this, text.Trim()));
protected override ProcessBookViewModelBase CreateNewProcessBook(LibraryBook libraryBook)
=> new ProcessBookViewModel(libraryBook, Logger);
}

View File

@ -1,9 +1,11 @@
using LibationUiBase.ProcessQueue; using System;
using System; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using System.Drawing; using System.Drawing;
using System.Windows.Forms; using System.Windows.Forms;
#nullable enable
namespace LibationWinForms.ProcessQueue namespace LibationWinForms.ProcessQueue
{ {
internal partial class VirtualFlowControl : UserControl internal partial class VirtualFlowControl : UserControl
@ -11,18 +13,28 @@ namespace LibationWinForms.ProcessQueue
/// <summary> /// <summary>
/// Triggered when one of the <see cref="ProcessBookControl"/>'s buttons has been clicked /// Triggered when one of the <see cref="ProcessBookControl"/>'s buttons has been clicked
/// </summary> /// </summary>
public event EventHandler<string> ButtonClicked; public event EventHandler<string>? ButtonClicked;
public IList? Items { get; private set; }
private List<ProcessBookViewModelBase> m_Items; private object? m_OldContext;
public List<ProcessBookViewModelBase> Items protected override void OnDataContextChanged(EventArgs e)
{ {
get => m_Items; if (m_OldContext is INotifyCollectionChanged oldNotify)
set oldNotify.CollectionChanged -= Items_CollectionChanged;
if (DataContext is INotifyCollectionChanged newNotify)
{ {
m_Items = value; m_OldContext = newNotify;
if (m_Items is not null) newNotify.CollectionChanged += Items_CollectionChanged;
RefreshDisplay();
} }
Items = DataContext as IList;
base.OnDataContextChanged(e);
}
private void Items_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
RefreshDisplay();
} }
public void RefreshDisplay() public void RefreshDisplay()
@ -65,7 +77,7 @@ namespace LibationWinForms.ProcessQueue
#region Instance variables #region Instance variables
/// <summary> /// <summary>
/// The total height, inclusing margins, of the repeated <see cref="ProcessBookControl"/> /// The total height, including margins, of the repeated <see cref="ProcessBookControl"/>
/// </summary> /// </summary>
private readonly int VirtualControlHeight; private readonly int VirtualControlHeight;
/// <summary> /// <summary>
@ -137,14 +149,15 @@ namespace LibationWinForms.ProcessQueue
/// <summary> /// <summary>
/// Handles all button clicks from all <see cref="ProcessBookControl"/>, detects which one sent the click, and fires <see cref="ButtonClicked"/> to notify the model of the click /// Handles all button clicks from all <see cref="ProcessBookControl"/>, detects which one sent the click, and fires <see cref="ButtonClicked"/> to notify the model of the click
/// </summary> /// </summary>
private void ControlButton_Click(object sender, EventArgs e) private void ControlButton_Click(object? sender, EventArgs e)
{ {
Control button = sender as Control; Control? button = sender as Control;
Control form = button.Parent; Control? form = button?.Parent;
while (form is not ProcessBookControl) while (form is not null and not ProcessBookControl)
form = form.Parent; form = form?.Parent;
ButtonClicked?.Invoke(form, button.Name); if (form is not null && button?.Name is string buttonText)
ButtonClicked?.Invoke(form, buttonText);
} }
/// <summary> /// <summary>
@ -176,7 +189,7 @@ namespace LibationWinForms.ProcessQueue
} }
/// <summary> /// <summary>
/// Calculated the virtual controls that are in view at the currrent scroll position and windows size, /// Calculated the virtual controls that are in view at the current scroll position and windows size,
/// positions <see cref="panel1"/> to simulate scroll activity, then fires updates the controls with /// positions <see cref="panel1"/> to simulate scroll activity, then fires updates the controls with
/// the context corresponding to the virtual scroll position /// the context corresponding to the virtual scroll position
/// </summary> /// </summary>
@ -195,9 +208,10 @@ namespace LibationWinForms.ProcessQueue
numVisible = Math.Min(numVisible, VirtualControlCount); numVisible = Math.Min(numVisible, VirtualControlCount);
numVisible = Math.Min(numVisible, VirtualControlCount - firstVisible); numVisible = Math.Min(numVisible, VirtualControlCount - firstVisible);
for (int i = 0; i < numVisible; i++) if (Items is IList items)
{ {
BookControls[i].Context = Items[firstVisible + i]; for (int i = 0; i < numVisible; i++)
BookControls[i].DataContext = items[firstVisible + i];
} }
for (int i = 0; i < BookControls.Count; i++) for (int i = 0; i < BookControls.Count; i++)