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());
}
QueuedBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Queued };
WorkingBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Working };
CompletedBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Completed };
CancelledBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Cancelled };
FailedBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Failed };
QueuedBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Queued };
WorkingBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Working };
CompletedBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Completed };
CancelledBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Cancelled };
FailedBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Failed };
//Set the current processable so that the empty queue doesn't try to advance.
QueuedBook.AddDownloadPdf();

View File

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

View File

@ -2,6 +2,7 @@
using DataLayer;
using LibationAvalonia.Views;
using LibationFileManager;
using LibationUiBase.ProcessQueue;
using ReactiveUI;
using System.Collections.Generic;
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:d="http://schemas.microsoft.com/expression/blend/2008"
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"
x:DataType="vm:ProcessBookViewModel"
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.Controls;
using DataLayer;
using LibationAvalonia.ViewModels;
using LibationUiBase;
using LibationUiBase.ProcessQueue;
@ -31,10 +30,8 @@ namespace LibationAvalonia.Views
if (Design.IsDesignMode)
{
using var context = DbContexts.GetContext();
DataContext = new ProcessBookViewModel(
context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"),
LogMe.RegisterForm(default(ILogForm))
);
ViewModels.MainVM.Configure_NonUI();
DataContext = new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"));
return;
}
}
@ -44,7 +41,7 @@ namespace LibationAvalonia.Views
public void Cancel_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> CancelButtonClicked?.Invoke(DataItem);
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)
=> PositionButtonClicked?.Invoke(DataItem, QueuePosition.OneUp);
public void MoveDown_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)

View File

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

View File

@ -1,9 +1,7 @@
using ApplicationServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data.Converters;
using DataLayer;
using LibationAvalonia.ViewModels;
using LibationUiBase;
using LibationUiBase.ProcessQueue;
using System;
@ -17,7 +15,7 @@ namespace LibationAvalonia.Views
{
public partial class ProcessQueueControl : UserControl
{
private TrackedQueue<ProcessBookViewModelBase>? Queue => _viewModel?.Queue;
private TrackedQueue<ProcessBookViewModel>? Queue => _viewModel?.Queue;
private ProcessQueueViewModel? _viewModel => DataContext as ProcessQueueViewModel;
public ProcessQueueControl()
@ -31,48 +29,49 @@ namespace LibationAvalonia.Views
#if DEBUG
if (Design.IsDesignMode)
{
_ = LibationFileManager.Configuration.Instance.LibationFiles;
ViewModels.MainVM.Configure_NonUI();
var vm = new ProcessQueueViewModel();
var Logger = LogMe.RegisterForm(vm);
DataContext = vm;
using var context = DbContexts.GetContext();
List<ProcessBookViewModel> testList = new()
{
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"), Logger)
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"))
{
Result = ProcessBookResult.FailedAbort,
Status = ProcessBookStatus.Failed,
},
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IWVG"), Logger)
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IWVG"))
{
Result = ProcessBookResult.FailedSkip,
Status = ProcessBookStatus.Failed,
},
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q"), Logger)
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q"))
{
Result = ProcessBookResult.FailedRetry,
Status = ProcessBookStatus.Failed,
},
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NUPO"), Logger)
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NUPO"))
{
Result = ProcessBookResult.ValidationFail,
Status = ProcessBookStatus.Failed,
},
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NMX4"), Logger)
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NMX4"))
{
Result = ProcessBookResult.Cancelled,
Status = ProcessBookStatus.Cancelled,
},
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0"), Logger)
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0"))
{
Result = ProcessBookResult.Success,
Status = ProcessBookStatus.Completed,
},
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6"), Logger)
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6"))
{
Result = ProcessBookResult.None,
Status = ProcessBookStatus.Working,
},
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"), Logger)
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"))
{
Result = ProcessBookResult.None,
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.Common;
using DataLayer;
@ -40,9 +40,8 @@ public enum ProcessBookStatus
/// <summary>
/// This is the viewmodel for queued processables
/// </summary>
public abstract class ProcessBookViewModelBase : ReactiveObject
public class ProcessBookViewModel : ReactiveObject
{
private readonly LogMe Logger;
public LibraryBook LibraryBook { get; protected set; }
private ProcessBookResult _result = ProcessBookResult.None;
@ -84,6 +83,21 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
#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 void NextProcessable() => _currentProcessable = null;
private Processable? _currentProcessable;
@ -91,10 +105,9 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
/// <summary> A series of Processable actions to perform on this book </summary>
protected Queue<Func<Processable>> Processes { get; } = new();
protected ProcessBookViewModelBase(LibraryBook libraryBook, LogMe logme)
public ProcessBookViewModel(LibraryBook libraryBook)
{
LibraryBook = libraryBook;
Logger = logme;
_title = LibraryBook.Book.TitleWithSubtitle;
_author = LibraryBook.Book.AuthorNames();
@ -106,15 +119,14 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
PictureStorage.PictureCached += PictureStorage_PictureCached;
// 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)
{
if (e.Definition.PictureId == LibraryBook.Book.PictureId)
{
Cover = LoadImageFromBytes(e.Picture, PictureSize._80x80);
Cover = BaseUtil.LoadImage(e.Picture, PictureSize._80x80);
PictureStorage.PictureCached -= PictureStorage_PictureCached;
}
}
@ -133,36 +145,36 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
result = ProcessBookResult.Success;
else if (statusHandler.Errors.Contains("Cancelled"))
{
Logger.Info($"{procName}: Process was cancelled - {LibraryBook.Book}");
LogInfo($"{procName}: Process was cancelled - {LibraryBook.Book}");
result = ProcessBookResult.Cancelled;
}
else if (statusHandler.Errors.Contains("Validation failed"))
{
Logger.Info($"{procName}: Validation failed - {LibraryBook.Book}");
LogInfo($"{procName}: Validation failed - {LibraryBook.Book}");
result = ProcessBookResult.ValidationFail;
}
else
{
foreach (var errorMessage in statusHandler.Errors)
Logger.Error($"{procName}: {errorMessage}");
LogError($"{procName}: {errorMessage}");
}
}
catch (ContentLicenseDeniedException ldex)
{
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;
}
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;
}
}
catch (Exception ex)
{
Logger.Error(ex, procName);
LogError(procName, ex);
}
finally
{
@ -192,15 +204,15 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
}
catch (Exception ex)
{
Logger.Error(ex, $"{CurrentProcessable.Name}: Error while cancelling");
LogError($"{CurrentProcessable.Name}: Error while cancelling", ex);
}
}
public ProcessBookViewModelBase AddDownloadPdf() => AddProcessable<DownloadPdf>();
public ProcessBookViewModelBase AddDownloadDecryptBook() => AddProcessable<DownloadDecryptBook>();
public ProcessBookViewModelBase AddConvertToMp3() => AddProcessable<ConvertToMp3>();
public ProcessBookViewModel AddDownloadPdf() => AddProcessable<DownloadPdf>();
public ProcessBookViewModel AddDownloadDecryptBook() => AddProcessable<DownloadDecryptBook>();
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());
return this;
@ -252,7 +264,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
private void AudioDecodable_AuthorsDiscovered(object? sender, string authors) => Author = authors;
private void AudioDecodable_NarratorsDiscovered(object? sender, string narrators) => Narrator = narrators;
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)
{
@ -292,7 +304,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
Status = ProcessBookStatus.Working;
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;
Author = libraryBook.Book.AuthorNames();
@ -303,7 +315,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
{
if (sender is Processable processable)
{
Logger.Info($"{processable.Name} Step, Completed: {libraryBook.Book}");
LogInfo($"{processable.Name} Step, Completed: {libraryBook.Book}");
UnlinkProcessable(processable);
}
@ -329,7 +341,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
if (result.HasErrors)
{
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)
{
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
{
@ -353,7 +365,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
if (dialogResult == SkipResult)
{
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

View File

@ -1,30 +1,32 @@
using DataLayer;
using ApplicationServices;
using DataLayer;
using LibationUiBase.Forms;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using ApplicationServices;
using System.Threading.Tasks;
#nullable enable
namespace LibationUiBase.ProcessQueue;
public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
public record LogEntry(DateTime LogDate, string LogMessage)
{
public abstract void WriteLine(string text);
protected abstract ProcessBookViewModelBase CreateNewProcessBook(LibraryBook libraryBook);
public string LogDateString => LogDate.ToShortTimeString();
}
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 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.CompletedCountChanged += Queue_CompletedCountChanged;
SpeedLimit = LibationFileManager.Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
}
private int _completedCount;
@ -32,6 +34,7 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
private int _queuedCount;
private string? _runningTime;
private bool _progressBarVisible;
private decimal _speedLimit;
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)); } }
@ -42,6 +45,32 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
public bool AnyQueued => QueuedCount > 0;
public bool AnyErrors => ErrorCount > 0;
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)
{
@ -59,6 +88,9 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
RaisePropertyChanged(nameof(Progress));
}
private void ProcessBook_LogWritten(object? sender, string logMessage)
=> Invoke(() => LogEntries.Add(new(DateTime.Now, logMessage.Trim())));
#region Add Books to Queue
public bool QueueDownloadPdf(IList<LibraryBook> libraryBooks)
@ -124,47 +156,50 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
}
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)
: true;
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
&& 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();
Serilog.Log.Logger.Information("Queueing {count} books for PDF-only download", procs.Length);
AddToQueue(procs);
ProcessBookViewModelBase Create(LibraryBook entry)
=> CreateNewProcessBook(entry).AddDownloadPdf();
ProcessBookViewModel Create(LibraryBook entry)
=> 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();
Serilog.Log.Logger.Information("Queueing {count} books ofr download/decrypt", procs.Length);
AddToQueue(procs);
ProcessBookViewModelBase Create(LibraryBook entry)
=> CreateNewProcessBook(entry).AddDownloadDecryptBook().AddDownloadPdf();
ProcessBookViewModel Create(LibraryBook entry)
=> 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();
Serilog.Log.Logger.Information("Queueing {count} books for mp3 conversion", procs.Length);
AddToQueue(procs);
ProcessBookViewModelBase Create(LibraryBook entry)
=> CreateNewProcessBook(entry).AddConvertToMp3();
ProcessBookViewModel Create(LibraryBook entry)
=> 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);
if (!Running)
QueueRunner = Task.Run(QueueLoop);
@ -187,7 +222,7 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
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.");
continue;

View File

@ -1,5 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
#nullable enable
@ -7,7 +9,7 @@ namespace LibationUiBase
{
public enum QueuePosition
{
Fisrt,
First,
OneUp,
OneDown,
Last,
@ -23,37 +25,20 @@ namespace LibationUiBase
*
* The index is the link position from the first link you lifted to the
* 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>? QueuedCountChanged;
public event NotifyCollectionChangedEventHandler? CollectionChanged;
public T? Current { get; private set; }
public IReadOnlyList<T> Queued => _queued;
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 object lockObject = new();
private readonly ICollection<T>? _underlyingList;
public ICollection<T>? UnderlyingList => _underlyingList;
public TrackedQueue(ICollection<T>? underlyingList = null)
{
_underlyingList = underlyingList;
}
private int QueueStartIndex => Completed.Count + (Current is null ? 0 : 1);
public T this[int index]
{
@ -61,17 +46,10 @@ namespace LibationUiBase
{
lock (lockObject)
{
if (index < _completed.Count)
return _completed[index];
index -= _completed.Count;
if (index == 0 && Current != null) return Current;
if (Current != null) index--;
if (index < _queued.Count) return _queued.ElementAt(index);
throw new IndexOutOfRangeException();
return index < Completed.Count ? Completed[index]
: index == Completed.Count && Current is not null ? Current
: index < Count ? Queued[index - QueueStartIndex]
: throw new IndexOutOfRangeException();
}
}
}
@ -82,7 +60,7 @@ namespace LibationUiBase
{
lock (lockObject)
{
return _queued.Count + _completed.Count + (Current == null ? 0 : 1);
return QueueStartIndex + Queued.Count;
}
}
}
@ -91,131 +69,117 @@ namespace LibationUiBase
{
lock (lockObject)
{
if (_completed.Contains(item))
return _completed.IndexOf(item);
if (Current == item) return _completed.Count;
if (_queued.Contains(item))
return _queued.IndexOf(item) + (Current is null ? 0 : 1);
return -1;
int index = _completed.IndexOf(item);
if (index < 0 && item == Current)
index = Completed.Count;
if (index < 0)
{
index = Queued.IndexOf(item);
if (index >= 0)
index += QueueStartIndex;
}
return index;
}
}
public bool RemoveQueued(T item)
{
bool itemsRemoved;
int queuedCount;
int queuedCount, queueIndex;
lock (lockObject)
{
itemsRemoved = _queued.Remove(item);
queuedCount = _queued.Count;
queueIndex = Queued.IndexOf(item);
if (queueIndex >= 0)
Queued.RemoveAt(queueIndex);
queuedCount = Queued.Count;
}
if (itemsRemoved)
if (queueIndex >= 0)
{
QueuedCountChanged?.Invoke(this, queuedCount);
RebuildSecondary();
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, QueueStartIndex + queueIndex));
return true;
}
return itemsRemoved;
}
public void ClearCurrent()
{
lock (lockObject)
Current = null;
RebuildSecondary();
return false;
}
public bool RemoveCompleted(T item)
{
bool itemsRemoved;
int completedCount;
int completedCount, completedIndex;
lock (lockObject)
{
itemsRemoved = _completed.Remove(item);
completedIndex = _completed.IndexOf(item);
if (completedIndex >= 0)
_completed.RemoveAt(completedIndex);
completedCount = _completed.Count;
}
if (itemsRemoved)
if (completedIndex >= 0)
{
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()
{
List<T> queuedItems;
lock (lockObject)
_queued.Clear();
{
queuedItems = Queued.ToList();
Queued.Clear();
}
QueuedCountChanged?.Invoke(this, 0);
RebuildSecondary();
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, queuedItems, QueueStartIndex));
}
public void ClearCompleted()
{
List<T> completedItems;
lock (lockObject)
{
completedItems = _completed.ToList();
_completed.Clear();
}
CompletedCountChanged?.Invoke(this, 0);
RebuildSecondary();
}
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);
}
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, completedItems, 0));
}
public void MoveQueuePosition(T item, QueuePosition requestedPosition)
{
int oldIndex, newIndex;
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)
return;
if ((requestedPosition == QueuePosition.Last || requestedPosition == QueuePosition.OneDown) && _queued[^1] == item)
if (oldIndex < 0 || newIndex < 0 || newIndex >= Queued.Count || newIndex == oldIndex)
return;
int queueIndex = _queued.IndexOf(item);
if (requestedPosition == QueuePosition.OneUp)
{
_queued.RemoveAt(queueIndex);
_queued.Insert(queueIndex - 1, item);
Queued.RemoveAt(oldIndex);
Queued.Insert(newIndex, item);
}
else if (requestedPosition == QueuePosition.OneDown)
{
_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();
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, item, QueueStartIndex + newIndex, QueueStartIndex + oldIndex));
}
public bool MoveNext()
@ -232,15 +196,15 @@ namespace LibationUiBase
completedCount = _completed.Count;
completedChanged = true;
}
if (_queued.Count == 0)
if (Queued.Count == 0)
{
Current = null;
return false;
}
Current = _queued[0];
_queued.RemoveAt(0);
Current = Queued[0];
Queued.RemoveAt(0);
queuedCount = _queued.Count;
queuedCount = Queued.Count;
return true;
}
}
@ -249,34 +213,48 @@ namespace LibationUiBase
if (completedChanged)
CompletedCountChanged?.Invoke(this, completedCount);
QueuedCountChanged?.Invoke(this, queuedCount);
RebuildSecondary();
}
}
public void Enqueue(IEnumerable<T> item)
public void Enqueue(IList<T> item)
{
int queueCount;
lock (lockObject)
{
_queued.AddRange(item);
queueCount = _queued.Count;
Queued.AddRange(item);
queueCount = Queued.Count;
}
foreach (var i in item)
_underlyingList?.Add(i);
QueuedCountChanged?.Invoke(this, queueCount);
}
private void RebuildSecondary()
{
_underlyingList?.Clear();
foreach (var item in GetAllItems())
_underlyingList?.Add(item);
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, QueueStartIndex + Queued.Count));
}
public IEnumerable<T> GetAllItems()
{
lock (lockObject)
{
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.Windows.Forms;
#nullable enable
namespace LibationWinForms.ProcessQueue
{
internal partial class ProcessBookControl : UserControl
{
private readonly int CancelBtnDistanceFromEdge;
private readonly int ProgressBarDistanceFromEdge;
private object? m_OldContext;
private static Color FailedColor { get; } = Color.LightCoral;
private static Color CancelledColor { get; } = Color.Khaki;
private static Color QueuedColor { get; } = SystemColors.Control;
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()
{
InitializeComponent();
@ -41,35 +28,41 @@ namespace LibationWinForms.ProcessQueue
ProgressBarDistanceFromEdge = Width - progressBar1.Location.X - progressBar1.Width;
}
private void OnContextChanging()
protected override void OnDataContextChanged(EventArgs e)
{
if (Context is not null)
Context.PropertyChanged -= Context_PropertyChanged;
if (m_OldContext is ProcessBookViewModel oldContext)
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()
{
Context.PropertyChanged += Context_PropertyChanged;
Context_PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(null));
base.OnDataContextChanged(e);
}
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();
if (e.PropertyName is null or nameof(Context.Cover))
SetCover(Context.Cover as Image);
if (e.PropertyName is null or nameof(Context.Title) or nameof(Context.Author) or nameof(Context.Narrator))
SetBookInfo($"{Context.Title}\r\nBy {Context.Author}\r\nNarrated by {Context.Narrator}");
if (e.PropertyName is null or nameof(Context.Status) or nameof(Context.StatusText))
SetStatus(Context.Status, Context.StatusText);
if (e.PropertyName is null or nameof(Context.Progress))
SetProgress(Context.Progress);
if (e.PropertyName is null or nameof(Context.TimeRemaining))
SetRemainingTime(Context.TimeRemaining);
if (e.PropertyName is null or nameof(vm.Cover))
SetCover(vm.Cover as Image);
if (e.PropertyName is null or nameof(vm.Title) or nameof(vm.Author) or nameof(vm.Narrator))
SetBookInfo($"{vm.Title}\r\nBy {vm.Author}\r\nNarrated by {vm.Narrator}");
if (e.PropertyName is null or nameof(vm.Status) or nameof(vm.StatusText))
SetStatus(vm.Status, vm.StatusText);
if (e.PropertyName is null or nameof(vm.Progress))
SetProgress(vm.Progress);
if (e.PropertyName is null or nameof(vm.TimeRemaining))
SetRemainingTime(vm.TimeRemaining);
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 SetRemainingTime(TimeSpan remaining)
=> 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 LibationUiBase;
using LibationUiBase.ProcessQueue;
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
@ -27,49 +28,47 @@ internal partial class ProcessQueueControl : UserControl
{
InitializeComponent();
var speedLimitMBps = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
numericUpDown1.Value = speedLimitMBps > numericUpDown1.Maximum || speedLimitMBps < numericUpDown1.Minimum ? 0 : speedLimitMBps;
statusStrip1.Items.Add(PopoutButton);
virtualFlowControl2.ButtonClicked += VirtualFlowControl2_ButtonClicked;
virtualFlowControl2.DataContext = ViewModel.Queue;
ViewModel.LogWritten += (_, text) => WriteLine(text);
ViewModel.PropertyChanged += ProcessQueue_PropertyChanged;
virtualFlowControl2.Items = ViewModel.Items;
Load += ProcessQueueControl_Load;
ViewModel.LogEntries.CollectionChanged += LogEntries_CollectionChanged;
}
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;
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)
{
ViewModel.Queue.ClearQueue();
if (ViewModel.Queue.Current is not null)
await ViewModel.Queue.Current.CancelAsync();
virtualFlowControl2.RefreshDisplay();
}
private void btnClearFinished_Click(object? sender, EventArgs e)
{
ViewModel.Queue.ClearCompleted();
virtualFlowControl2.RefreshDisplay();
if (!ViewModel.Running)
runningTimeLbl.Text = string.Empty;
}
private void clearLogBtn_Click(object? sender, EventArgs e)
{
ViewModel.LogEntries.Clear();
logDGV.Rows.Clear();
}
@ -92,7 +91,6 @@ internal partial class ProcessQueueControl : UserControl
{
queueNumberLbl.Text = ViewModel.QueuedCount.ToString();
queueNumberLbl.Visible = ViewModel.QueuedCount > 0;
virtualFlowControl2.RefreshDisplay();
}
if (e.PropertyName is null or nameof(ViewModel.ErrorCount))
{
@ -117,16 +115,22 @@ internal partial class ProcessQueueControl : UserControl
{
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>
/// View notified the model that a botton was clicked
/// View notified the model that a button was clicked
/// </summary>
/// <param name="sender">the <see cref="ProcessBookControl"/> whose button was clicked</param>
/// <param name="buttonName">The name of the button clicked</param>
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;
try
@ -135,13 +139,12 @@ internal partial class ProcessQueueControl : UserControl
{
await item.CancelAsync();
ViewModel.Queue.RemoveQueued(item);
virtualFlowControl2.RefreshDisplay();
}
else
{
QueuePosition? position = buttonName switch
{
nameof(ProcessBookControl.moveFirstBtn) => QueuePosition.Fisrt,
nameof(ProcessBookControl.moveFirstBtn) => QueuePosition.First,
nameof(ProcessBookControl.moveUpBtn) => QueuePosition.OneUp,
nameof(ProcessBookControl.moveDownBtn) => QueuePosition.OneDown,
nameof(ProcessBookControl.moveLastBtn) => QueuePosition.Last,
@ -149,10 +152,7 @@ internal partial class ProcessQueueControl : UserControl
};
if (position is not null)
{
ViewModel.Queue.MoveQueuePosition(item, position.Value);
virtualFlowControl2.RefreshDisplay();
}
}
}
catch(Exception ex)
@ -164,27 +164,7 @@ internal partial class ProcessQueueControl : UserControl
#endregion
private void numericUpDown1_ValueChanged(object? sender, EventArgs e)
{
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;
}
=> ViewModel.SpeedLimit = numericUpDown1.Value;
}
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.Specialized;
using System.Drawing;
using System.Windows.Forms;
#nullable enable
namespace LibationWinForms.ProcessQueue
{
internal partial class VirtualFlowControl : UserControl
@ -11,18 +13,28 @@ namespace LibationWinForms.ProcessQueue
/// <summary>
/// Triggered when one of the <see cref="ProcessBookControl"/>'s buttons has been clicked
/// </summary>
public event EventHandler<string> ButtonClicked;
public event EventHandler<string>? ButtonClicked;
public IList? Items { get; private set; }
private List<ProcessBookViewModelBase> m_Items;
public List<ProcessBookViewModelBase> Items
private object? m_OldContext;
protected override void OnDataContextChanged(EventArgs e)
{
get => m_Items;
set
if (m_OldContext is INotifyCollectionChanged oldNotify)
oldNotify.CollectionChanged -= Items_CollectionChanged;
if (DataContext is INotifyCollectionChanged newNotify)
{
m_Items = value;
if (m_Items is not null)
RefreshDisplay();
m_OldContext = newNotify;
newNotify.CollectionChanged += Items_CollectionChanged;
}
Items = DataContext as IList;
base.OnDataContextChanged(e);
}
private void Items_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
RefreshDisplay();
}
public void RefreshDisplay()
@ -65,7 +77,7 @@ namespace LibationWinForms.ProcessQueue
#region Instance variables
/// <summary>
/// The total height, inclusing margins, of the repeated <see cref="ProcessBookControl"/>
/// The total height, including margins, of the repeated <see cref="ProcessBookControl"/>
/// </summary>
private readonly int VirtualControlHeight;
/// <summary>
@ -137,14 +149,15 @@ namespace LibationWinForms.ProcessQueue
/// <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
/// </summary>
private void ControlButton_Click(object sender, EventArgs e)
private void ControlButton_Click(object? sender, EventArgs e)
{
Control button = sender as Control;
Control form = button.Parent;
while (form is not ProcessBookControl)
form = form.Parent;
Control? button = sender as Control;
Control? form = button?.Parent;
while (form is not null and not ProcessBookControl)
form = form?.Parent;
ButtonClicked?.Invoke(form, button.Name);
if (form is not null && button?.Name is string buttonText)
ButtonClicked?.Invoke(form, buttonText);
}
/// <summary>
@ -176,7 +189,7 @@ namespace LibationWinForms.ProcessQueue
}
/// <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
/// the context corresponding to the virtual scroll position
/// </summary>
@ -195,9 +208,10 @@ namespace LibationWinForms.ProcessQueue
numVisible = Math.Min(numVisible, VirtualControlCount);
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++)