diff --git a/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs b/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs index 214ea33b..ee35a855 100644 --- a/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs +++ b/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs @@ -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(); diff --git a/Source/LibationAvalonia/ViewModels/MainVM._NoUI.cs b/Source/LibationAvalonia/ViewModels/MainVM._NoUI.cs index 0d361117..384d247e 100644 --- a/Source/LibationAvalonia/ViewModels/MainVM._NoUI.cs +++ b/Source/LibationAvalonia/ViewModels/MainVM._NoUI.cs @@ -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); diff --git a/Source/LibationAvalonia/ViewModels/MainVM.cs b/Source/LibationAvalonia/ViewModels/MainVM.cs index a57bf9c5..d369f064 100644 --- a/Source/LibationAvalonia/ViewModels/MainVM.cs +++ b/Source/LibationAvalonia/ViewModels/MainVM.cs @@ -2,6 +2,7 @@ using DataLayer; using LibationAvalonia.Views; using LibationFileManager; +using LibationUiBase.ProcessQueue; using ReactiveUI; using System.Collections.Generic; using System.Threading.Tasks; diff --git a/Source/LibationAvalonia/ViewModels/ProcessBookViewModel.cs b/Source/LibationAvalonia/ViewModels/ProcessBookViewModel.cs deleted file mode 100644 index 3bc4b30f..00000000 --- a/Source/LibationAvalonia/ViewModels/ProcessBookViewModel.cs +++ /dev/null @@ -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); - -} \ No newline at end of file diff --git a/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs b/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs deleted file mode 100644 index de329357..00000000 --- a/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs +++ /dev/null @@ -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 - ?? throw new ArgumentNullException(nameof(Queue.UnderlyingList)); - - SpeedLimit = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024; - } - - private decimal _speedLimit; - public decimal SpeedLimitIncrement { get; private set; } - public ObservableCollection LogEntries { get; } = new(); - public AvaloniaList 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 CreateEmptyList() - { - if (Design.IsDesignMode) - _ = Configuration.Instance.LibationFiles; - return new AvaloniaList(); - } -} diff --git a/Source/LibationAvalonia/Views/ProcessBookControl.axaml b/Source/LibationAvalonia/Views/ProcessBookControl.axaml index 864ce3ee..1796257f 100644 --- a/Source/LibationAvalonia/Views/ProcessBookControl.axaml +++ b/Source/LibationAvalonia/Views/ProcessBookControl.axaml @@ -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" diff --git a/Source/LibationAvalonia/Views/ProcessBookControl.axaml.cs b/Source/LibationAvalonia/Views/ProcessBookControl.axaml.cs index 9e66987d..f7adccf5 100644 --- a/Source/LibationAvalonia/Views/ProcessBookControl.axaml.cs +++ b/Source/LibationAvalonia/Views/ProcessBookControl.axaml.cs @@ -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) diff --git a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml index f65a400e..163a6d3b 100644 --- a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml +++ b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml @@ -34,7 +34,7 @@ HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" AllowAutoHide="False"> - + diff --git a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs index 97ae56f9..35333cf4 100644 --- a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs +++ b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs @@ -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? Queue => _viewModel?.Queue; + private TrackedQueue? 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 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, diff --git a/Source/LibationUiBase/ILogForm.cs b/Source/LibationUiBase/ILogForm.cs deleted file mode 100644 index a0553fba..00000000 --- a/Source/LibationUiBase/ILogForm.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace LibationUiBase -{ - public interface ILogForm - { - void WriteLine(string text); - } -} diff --git a/Source/LibationUiBase/LogMe.cs b/Source/LibationUiBase/LogMe.cs deleted file mode 100644 index 08e6dfad..00000000 --- a/Source/LibationUiBase/LogMe.cs +++ /dev/null @@ -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 LogInfo; - public event EventHandler 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 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)); - } -} diff --git a/Source/LibationUiBase/ProcessQueue/ProcessBookViewModelBase.cs b/Source/LibationUiBase/ProcessQueue/ProcessBookViewModel.cs similarity index 85% rename from Source/LibationUiBase/ProcessQueue/ProcessBookViewModelBase.cs rename to Source/LibationUiBase/ProcessQueue/ProcessBookViewModel.cs index 62b00e7d..c47da12e 100644 --- a/Source/LibationUiBase/ProcessQueue/ProcessBookViewModelBase.cs +++ b/Source/LibationUiBase/ProcessQueue/ProcessBookViewModel.cs @@ -1,4 +1,4 @@ -using ApplicationServices; +using ApplicationServices; using AudibleApi; using AudibleApi.Common; using DataLayer; @@ -40,9 +40,8 @@ public enum ProcessBookStatus /// /// This is the viewmodel for queued processables /// -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? 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 /// A series of Processable actions to perform on this book protected Queue> 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(); - public ProcessBookViewModelBase AddDownloadDecryptBook() => AddProcessable(); - public ProcessBookViewModelBase AddConvertToMp3() => AddProcessable(); + public ProcessBookViewModel AddDownloadPdf() => AddProcessable(); + public ProcessBookViewModel AddDownloadDecryptBook() => AddProcessable(); + public ProcessBookViewModel AddConvertToMp3() => AddProcessable(); - private ProcessBookViewModelBase AddProcessable() where T : Processable, new() + private ProcessBookViewModel AddProcessable() 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 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 @@ -411,4 +423,4 @@ public abstract class ProcessBookViewModelBase : ReactiveObject } #endregion -} +} \ No newline at end of file diff --git a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModelBase.cs b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs similarity index 77% rename from Source/LibationUiBase/ProcessQueue/ProcessQueueViewModelBase.cs rename to Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs index a6452ceb..d3172b40 100644 --- a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModelBase.cs +++ b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs @@ -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 Queue { get; } +public class ProcessQueueViewModel : ReactiveObject +{ + public ObservableCollection LogEntries { get; } = new(); + public TrackedQueue Queue { get; } = new(); public Task? QueueRunner { get; private set; } public bool Running => !QueueRunner?.IsCompleted ?? false; - protected LogMe Logger { get; } - public ProcessQueueViewModelBase(ICollection? 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 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 entries) + private void AddDownloadPdf(IList 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 entries) + private void AddDownloadDecrypt(IList 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 entries) + private void AddConvertMp3(IList 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 pbook) + private void AddToQueue(IList 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; diff --git a/Source/LibationUiBase/TrackedQueue[T].cs b/Source/LibationUiBase/TrackedQueue[T].cs index 746f8c6b..e90c63b2 100644 --- a/Source/LibationUiBase/TrackedQueue[T].cs +++ b/Source/LibationUiBase/TrackedQueue[T].cs @@ -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, @@ -22,38 +24,21 @@ namespace LibationUiBase * 3) the pile of chain at your feet grows by 1 link (Completed) * * 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. + * last one in the chain. */ - public class TrackedQueue where T : class + public class TrackedQueue : IReadOnlyCollection, IList, INotifyCollectionChanged where T : class { public event EventHandler? CompletedCountChanged; public event EventHandler? QueuedCountChanged; + public event NotifyCollectionChangedEventHandler? CollectionChanged; public T? Current { get; private set; } - - public IReadOnlyList Queued => _queued; public IReadOnlyList Completed => _completed; + private List Queued { get; } = new(); - private readonly List _queued = new(); private readonly List _completed = new(); private readonly object lockObject = new(); - - private readonly ICollection? _underlyingList; - public ICollection? UnderlyingList => _underlyingList; - - public TrackedQueue(ICollection? 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 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 completedItems; lock (lockObject) + { + completedItems = _completed.ToList(); _completed.Clear(); + } CompletedCountChanged?.Invoke(this, 0); - RebuildSecondary(); - } - - public bool Any(Func predicate) - { - lock (lockObject) - { - return (Current != null && predicate(Current)) || _completed.Any(predicate) || _queued.Any(predicate); - } - } - - public T? FirstOrDefault(Func 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); - } - 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); - } + Queued.RemoveAt(oldIndex); + Queued.Insert(newIndex, 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 item) + public void Enqueue(IList 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 GetAllItems() { - if (Current is null) return Completed.Concat(Queued); - return Completed.Concat(new List { Current }).Concat(Queued); + lock (lockObject) + { + if (Current is null) return Completed.Concat(Queued); + return Completed.Concat([Current]).Concat(Queued); + } } + + public IEnumerator 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 } } diff --git a/Source/LibationWinForms/ProcessQueue/ProcessBookControl.cs b/Source/LibationWinForms/ProcessQueue/ProcessBookControl.cs index 8bcfd6a2..35718afa 100644 --- a/Source/LibationWinForms/ProcessQueue/ProcessBookControl.cs +++ b/Source/LibationWinForms/ProcessQueue/ProcessBookControl.cs @@ -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)); + } + + base.OnDataContextChanged(e); } - private void OnContextChanged() + private void DataContext_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { - Context.PropertyChanged += Context_PropertyChanged; - Context_PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(null)); - } + if (sender is not ProcessBookViewModel vm) + return; - private void Context_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) - { 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}"; diff --git a/Source/LibationWinForms/ProcessQueue/ProcessBookViewModel.cs b/Source/LibationWinForms/ProcessQueue/ProcessBookViewModel.cs deleted file mode 100644 index f302b4b8..00000000 --- a/Source/LibationWinForms/ProcessQueue/ProcessBookViewModel.cs +++ /dev/null @@ -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); -} diff --git a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs index 7a67d31f..76a7d43d 100644 --- a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs +++ b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs @@ -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() ?? []) + 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; + } } /// - /// View notified the model that a botton was clicked + /// View notified the model that a button was clicked /// /// the whose button was clicked /// The name of the button clicked 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 diff --git a/Source/LibationWinForms/ProcessQueue/ProcessQueueViewModel.cs b/Source/LibationWinForms/ProcessQueue/ProcessQueueViewModel.cs deleted file mode 100644 index e8fb4d1e..00000000 --- a/Source/LibationWinForms/ProcessQueue/ProcessQueueViewModel.cs +++ /dev/null @@ -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? LogWritten; - public List Items { get; } - - public ProcessQueueViewModel() : base(new List()) - { - Items = Queue.UnderlyingList as List - ?? 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); -} diff --git a/Source/LibationWinForms/ProcessQueue/VirtualFlowControl.cs b/Source/LibationWinForms/ProcessQueue/VirtualFlowControl.cs index 5733ba48..007c732a 100644 --- a/Source/LibationWinForms/ProcessQueue/VirtualFlowControl.cs +++ b/Source/LibationWinForms/ProcessQueue/VirtualFlowControl.cs @@ -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 /// /// Triggered when one of the 's buttons has been clicked /// - public event EventHandler ButtonClicked; + public event EventHandler? ButtonClicked; + public IList? Items { get; private set; } - private List m_Items; - public List 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 /// - /// The total height, inclusing margins, of the repeated + /// The total height, including margins, of the repeated /// private readonly int VirtualControlHeight; /// @@ -137,14 +149,15 @@ namespace LibationWinForms.ProcessQueue /// /// Handles all button clicks from all , detects which one sent the click, and fires to notify the model of the click /// - 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); } /// @@ -176,7 +189,7 @@ namespace LibationWinForms.ProcessQueue } /// - /// 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 to simulate scroll activity, then fires updates the controls with /// the context corresponding to the virtual scroll position /// @@ -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++)