Consolidate process queue view models
Remove classic and chardonnay-specific implementations Refactor TrackedQueue into an IList with INotifyCollectionChanged
This commit is contained in:
parent
bff9b67b72
commit
80b86086ca
@ -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();
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
using DataLayer;
|
||||
using LibationAvalonia.Views;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase.ProcessQueue;
|
||||
using ReactiveUI;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -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);
|
||||
|
||||
}
|
||||
@ -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>();
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -34,7 +34,7 @@
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
AllowAutoHide="False">
|
||||
<ItemsControl ItemsSource="{Binding Items}">
|
||||
<ItemsControl ItemsSource="{Binding Queue}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<VirtualizingStackPanel />
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
namespace LibationUiBase
|
||||
{
|
||||
public interface ILogForm
|
||||
{
|
||||
void WriteLine(string text);
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -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;
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}";
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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++)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user