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());
|
PictureStorage.SetDefaultImage(PictureSize._80x80, ms1.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
QueuedBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Queued };
|
QueuedBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Queued };
|
||||||
WorkingBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Working };
|
WorkingBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Working };
|
||||||
CompletedBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Completed };
|
CompletedBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Completed };
|
||||||
CancelledBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Cancelled };
|
CancelledBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Cancelled };
|
||||||
FailedBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Failed };
|
FailedBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Failed };
|
||||||
|
|
||||||
//Set the current processable so that the empty queue doesn't try to advance.
|
//Set the current processable so that the empty queue doesn't try to advance.
|
||||||
QueuedBook.AddDownloadPdf();
|
QueuedBook.AddDownloadPdf();
|
||||||
|
|||||||
@ -8,7 +8,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
{
|
{
|
||||||
partial class MainVM
|
partial class MainVM
|
||||||
{
|
{
|
||||||
private void Configure_NonUI()
|
public static void Configure_NonUI()
|
||||||
{
|
{
|
||||||
using var ms1 = new MemoryStream();
|
using var ms1 = new MemoryStream();
|
||||||
App.OpenAsset("img-coverart-prod-unavailable_80x80.jpg").CopyTo(ms1);
|
App.OpenAsset("img-coverart-prod-unavailable_80x80.jpg").CopyTo(ms1);
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
using DataLayer;
|
using DataLayer;
|
||||||
using LibationAvalonia.Views;
|
using LibationAvalonia.Views;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
|
using LibationUiBase.ProcessQueue;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|||||||
@ -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:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:vm="clr-namespace:LibationAvalonia.ViewModels"
|
xmlns:vm="clr-namespace:LibationUiBase.ProcessQueue;assembly=LibationUiBase"
|
||||||
xmlns:views="clr-namespace:LibationAvalonia.Views"
|
xmlns:views="clr-namespace:LibationAvalonia.Views"
|
||||||
x:DataType="vm:ProcessBookViewModel"
|
x:DataType="vm:ProcessBookViewModel"
|
||||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="87" MaxHeight="87" MinHeight="87" MinWidth="300"
|
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="87" MaxHeight="87" MinHeight="87" MinWidth="300"
|
||||||
|
|||||||
@ -2,7 +2,6 @@ using ApplicationServices;
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using DataLayer;
|
using DataLayer;
|
||||||
using LibationAvalonia.ViewModels;
|
|
||||||
using LibationUiBase;
|
using LibationUiBase;
|
||||||
using LibationUiBase.ProcessQueue;
|
using LibationUiBase.ProcessQueue;
|
||||||
|
|
||||||
@ -31,10 +30,8 @@ namespace LibationAvalonia.Views
|
|||||||
if (Design.IsDesignMode)
|
if (Design.IsDesignMode)
|
||||||
{
|
{
|
||||||
using var context = DbContexts.GetContext();
|
using var context = DbContexts.GetContext();
|
||||||
DataContext = new ProcessBookViewModel(
|
ViewModels.MainVM.Configure_NonUI();
|
||||||
context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"),
|
DataContext = new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"));
|
||||||
LogMe.RegisterForm(default(ILogForm))
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -44,7 +41,7 @@ namespace LibationAvalonia.Views
|
|||||||
public void Cancel_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
public void Cancel_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
=> CancelButtonClicked?.Invoke(DataItem);
|
=> CancelButtonClicked?.Invoke(DataItem);
|
||||||
public void MoveFirst_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
public void MoveFirst_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
=> PositionButtonClicked?.Invoke(DataItem, QueuePosition.Fisrt);
|
=> PositionButtonClicked?.Invoke(DataItem, QueuePosition.First);
|
||||||
public void MoveUp_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
public void MoveUp_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
=> PositionButtonClicked?.Invoke(DataItem, QueuePosition.OneUp);
|
=> PositionButtonClicked?.Invoke(DataItem, QueuePosition.OneUp);
|
||||||
public void MoveDown_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
public void MoveDown_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
|
|||||||
@ -34,7 +34,7 @@
|
|||||||
HorizontalScrollBarVisibility="Disabled"
|
HorizontalScrollBarVisibility="Disabled"
|
||||||
VerticalScrollBarVisibility="Auto"
|
VerticalScrollBarVisibility="Auto"
|
||||||
AllowAutoHide="False">
|
AllowAutoHide="False">
|
||||||
<ItemsControl ItemsSource="{Binding Items}">
|
<ItemsControl ItemsSource="{Binding Queue}">
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl.ItemsPanel>
|
||||||
<ItemsPanelTemplate>
|
<ItemsPanelTemplate>
|
||||||
<VirtualizingStackPanel />
|
<VirtualizingStackPanel />
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
using ApplicationServices;
|
using ApplicationServices;
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Data.Converters;
|
using Avalonia.Data.Converters;
|
||||||
using DataLayer;
|
using DataLayer;
|
||||||
using LibationAvalonia.ViewModels;
|
|
||||||
using LibationUiBase;
|
using LibationUiBase;
|
||||||
using LibationUiBase.ProcessQueue;
|
using LibationUiBase.ProcessQueue;
|
||||||
using System;
|
using System;
|
||||||
@ -17,7 +15,7 @@ namespace LibationAvalonia.Views
|
|||||||
{
|
{
|
||||||
public partial class ProcessQueueControl : UserControl
|
public partial class ProcessQueueControl : UserControl
|
||||||
{
|
{
|
||||||
private TrackedQueue<ProcessBookViewModelBase>? Queue => _viewModel?.Queue;
|
private TrackedQueue<ProcessBookViewModel>? Queue => _viewModel?.Queue;
|
||||||
private ProcessQueueViewModel? _viewModel => DataContext as ProcessQueueViewModel;
|
private ProcessQueueViewModel? _viewModel => DataContext as ProcessQueueViewModel;
|
||||||
|
|
||||||
public ProcessQueueControl()
|
public ProcessQueueControl()
|
||||||
@ -31,48 +29,49 @@ namespace LibationAvalonia.Views
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
if (Design.IsDesignMode)
|
if (Design.IsDesignMode)
|
||||||
{
|
{
|
||||||
|
_ = LibationFileManager.Configuration.Instance.LibationFiles;
|
||||||
|
ViewModels.MainVM.Configure_NonUI();
|
||||||
var vm = new ProcessQueueViewModel();
|
var vm = new ProcessQueueViewModel();
|
||||||
var Logger = LogMe.RegisterForm(vm);
|
|
||||||
DataContext = vm;
|
DataContext = vm;
|
||||||
using var context = DbContexts.GetContext();
|
using var context = DbContexts.GetContext();
|
||||||
List<ProcessBookViewModel> testList = new()
|
List<ProcessBookViewModel> testList = new()
|
||||||
{
|
{
|
||||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"), Logger)
|
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"))
|
||||||
{
|
{
|
||||||
Result = ProcessBookResult.FailedAbort,
|
Result = ProcessBookResult.FailedAbort,
|
||||||
Status = ProcessBookStatus.Failed,
|
Status = ProcessBookStatus.Failed,
|
||||||
},
|
},
|
||||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IWVG"), Logger)
|
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IWVG"))
|
||||||
{
|
{
|
||||||
Result = ProcessBookResult.FailedSkip,
|
Result = ProcessBookResult.FailedSkip,
|
||||||
Status = ProcessBookStatus.Failed,
|
Status = ProcessBookStatus.Failed,
|
||||||
},
|
},
|
||||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q"), Logger)
|
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q"))
|
||||||
{
|
{
|
||||||
Result = ProcessBookResult.FailedRetry,
|
Result = ProcessBookResult.FailedRetry,
|
||||||
Status = ProcessBookStatus.Failed,
|
Status = ProcessBookStatus.Failed,
|
||||||
},
|
},
|
||||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NUPO"), Logger)
|
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NUPO"))
|
||||||
{
|
{
|
||||||
Result = ProcessBookResult.ValidationFail,
|
Result = ProcessBookResult.ValidationFail,
|
||||||
Status = ProcessBookStatus.Failed,
|
Status = ProcessBookStatus.Failed,
|
||||||
},
|
},
|
||||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NMX4"), Logger)
|
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NMX4"))
|
||||||
{
|
{
|
||||||
Result = ProcessBookResult.Cancelled,
|
Result = ProcessBookResult.Cancelled,
|
||||||
Status = ProcessBookStatus.Cancelled,
|
Status = ProcessBookStatus.Cancelled,
|
||||||
},
|
},
|
||||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0"), Logger)
|
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0"))
|
||||||
{
|
{
|
||||||
Result = ProcessBookResult.Success,
|
Result = ProcessBookResult.Success,
|
||||||
Status = ProcessBookStatus.Completed,
|
Status = ProcessBookStatus.Completed,
|
||||||
},
|
},
|
||||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6"), Logger)
|
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6"))
|
||||||
{
|
{
|
||||||
Result = ProcessBookResult.None,
|
Result = ProcessBookResult.None,
|
||||||
Status = ProcessBookStatus.Working,
|
Status = ProcessBookStatus.Working,
|
||||||
},
|
},
|
||||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"), Logger)
|
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"))
|
||||||
{
|
{
|
||||||
Result = ProcessBookResult.None,
|
Result = ProcessBookResult.None,
|
||||||
Status = ProcessBookStatus.Queued,
|
Status = ProcessBookStatus.Queued,
|
||||||
|
|||||||
@ -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;
|
||||||
using AudibleApi.Common;
|
using AudibleApi.Common;
|
||||||
using DataLayer;
|
using DataLayer;
|
||||||
@ -40,9 +40,8 @@ public enum ProcessBookStatus
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// This is the viewmodel for queued processables
|
/// This is the viewmodel for queued processables
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class ProcessBookViewModelBase : ReactiveObject
|
public class ProcessBookViewModel : ReactiveObject
|
||||||
{
|
{
|
||||||
private readonly LogMe Logger;
|
|
||||||
public LibraryBook LibraryBook { get; protected set; }
|
public LibraryBook LibraryBook { get; protected set; }
|
||||||
|
|
||||||
private ProcessBookResult _result = ProcessBookResult.None;
|
private ProcessBookResult _result = ProcessBookResult.None;
|
||||||
@ -84,6 +83,21 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Process Queue Logging
|
||||||
|
|
||||||
|
public event EventHandler<string>? LogWritten;
|
||||||
|
private void OnLogWritten(string text) => LogWritten?.Invoke(this, text.Trim());
|
||||||
|
|
||||||
|
private void LogError(string? message, Exception? ex = null)
|
||||||
|
{
|
||||||
|
OnLogWritten(message ?? "Automated backup: error");
|
||||||
|
if (ex is not null)
|
||||||
|
OnLogWritten("ERROR: " + ex.Message);
|
||||||
|
}
|
||||||
|
private void LogInfo(string text) => OnLogWritten(text);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
protected Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke();
|
protected Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke();
|
||||||
protected void NextProcessable() => _currentProcessable = null;
|
protected void NextProcessable() => _currentProcessable = null;
|
||||||
private Processable? _currentProcessable;
|
private Processable? _currentProcessable;
|
||||||
@ -91,10 +105,9 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
|||||||
/// <summary> A series of Processable actions to perform on this book </summary>
|
/// <summary> A series of Processable actions to perform on this book </summary>
|
||||||
protected Queue<Func<Processable>> Processes { get; } = new();
|
protected Queue<Func<Processable>> Processes { get; } = new();
|
||||||
|
|
||||||
protected ProcessBookViewModelBase(LibraryBook libraryBook, LogMe logme)
|
public ProcessBookViewModel(LibraryBook libraryBook)
|
||||||
{
|
{
|
||||||
LibraryBook = libraryBook;
|
LibraryBook = libraryBook;
|
||||||
Logger = logme;
|
|
||||||
|
|
||||||
_title = LibraryBook.Book.TitleWithSubtitle;
|
_title = LibraryBook.Book.TitleWithSubtitle;
|
||||||
_author = LibraryBook.Book.AuthorNames();
|
_author = LibraryBook.Book.AuthorNames();
|
||||||
@ -106,15 +119,14 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
|||||||
PictureStorage.PictureCached += PictureStorage_PictureCached;
|
PictureStorage.PictureCached += PictureStorage_PictureCached;
|
||||||
|
|
||||||
// Mutable property. Set the field so PropertyChanged isn't fired.
|
// Mutable property. Set the field so PropertyChanged isn't fired.
|
||||||
_cover = LoadImageFromBytes(picture, PictureSize._80x80);
|
_cover = BaseUtil.LoadImage(picture, PictureSize._80x80);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract object? LoadImageFromBytes(byte[] bytes, PictureSize pictureSize);
|
|
||||||
private void PictureStorage_PictureCached(object? sender, PictureCachedEventArgs e)
|
private void PictureStorage_PictureCached(object? sender, PictureCachedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Definition.PictureId == LibraryBook.Book.PictureId)
|
if (e.Definition.PictureId == LibraryBook.Book.PictureId)
|
||||||
{
|
{
|
||||||
Cover = LoadImageFromBytes(e.Picture, PictureSize._80x80);
|
Cover = BaseUtil.LoadImage(e.Picture, PictureSize._80x80);
|
||||||
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -133,36 +145,36 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
|||||||
result = ProcessBookResult.Success;
|
result = ProcessBookResult.Success;
|
||||||
else if (statusHandler.Errors.Contains("Cancelled"))
|
else if (statusHandler.Errors.Contains("Cancelled"))
|
||||||
{
|
{
|
||||||
Logger.Info($"{procName}: Process was cancelled - {LibraryBook.Book}");
|
LogInfo($"{procName}: Process was cancelled - {LibraryBook.Book}");
|
||||||
result = ProcessBookResult.Cancelled;
|
result = ProcessBookResult.Cancelled;
|
||||||
}
|
}
|
||||||
else if (statusHandler.Errors.Contains("Validation failed"))
|
else if (statusHandler.Errors.Contains("Validation failed"))
|
||||||
{
|
{
|
||||||
Logger.Info($"{procName}: Validation failed - {LibraryBook.Book}");
|
LogInfo($"{procName}: Validation failed - {LibraryBook.Book}");
|
||||||
result = ProcessBookResult.ValidationFail;
|
result = ProcessBookResult.ValidationFail;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
foreach (var errorMessage in statusHandler.Errors)
|
foreach (var errorMessage in statusHandler.Errors)
|
||||||
Logger.Error($"{procName}: {errorMessage}");
|
LogError($"{procName}: {errorMessage}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (ContentLicenseDeniedException ldex)
|
catch (ContentLicenseDeniedException ldex)
|
||||||
{
|
{
|
||||||
if (ldex.AYCL?.RejectionReason is null or RejectionReason.GenericError)
|
if (ldex.AYCL?.RejectionReason is null or RejectionReason.GenericError)
|
||||||
{
|
{
|
||||||
Logger.Info($"{procName}: Content license was denied, but this error appears to be caused by a temporary interruption of service. - {LibraryBook.Book}");
|
LogInfo($"{procName}: Content license was denied, but this error appears to be caused by a temporary interruption of service. - {LibraryBook.Book}");
|
||||||
result = ProcessBookResult.LicenseDeniedPossibleOutage;
|
result = ProcessBookResult.LicenseDeniedPossibleOutage;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger.Info($"{procName}: Content license denied. Check your Audible account to see if you have access to this title. - {LibraryBook.Book}");
|
LogInfo($"{procName}: Content license denied. Check your Audible account to see if you have access to this title. - {LibraryBook.Book}");
|
||||||
result = ProcessBookResult.LicenseDenied;
|
result = ProcessBookResult.LicenseDenied;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.Error(ex, procName);
|
LogError(procName, ex);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@ -192,15 +204,15 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.Error(ex, $"{CurrentProcessable.Name}: Error while cancelling");
|
LogError($"{CurrentProcessable.Name}: Error while cancelling", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProcessBookViewModelBase AddDownloadPdf() => AddProcessable<DownloadPdf>();
|
public ProcessBookViewModel AddDownloadPdf() => AddProcessable<DownloadPdf>();
|
||||||
public ProcessBookViewModelBase AddDownloadDecryptBook() => AddProcessable<DownloadDecryptBook>();
|
public ProcessBookViewModel AddDownloadDecryptBook() => AddProcessable<DownloadDecryptBook>();
|
||||||
public ProcessBookViewModelBase AddConvertToMp3() => AddProcessable<ConvertToMp3>();
|
public ProcessBookViewModel AddConvertToMp3() => AddProcessable<ConvertToMp3>();
|
||||||
|
|
||||||
private ProcessBookViewModelBase AddProcessable<T>() where T : Processable, new()
|
private ProcessBookViewModel AddProcessable<T>() where T : Processable, new()
|
||||||
{
|
{
|
||||||
Processes.Enqueue(() => new T());
|
Processes.Enqueue(() => new T());
|
||||||
return this;
|
return this;
|
||||||
@ -252,7 +264,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
|||||||
private void AudioDecodable_AuthorsDiscovered(object? sender, string authors) => Author = authors;
|
private void AudioDecodable_AuthorsDiscovered(object? sender, string authors) => Author = authors;
|
||||||
private void AudioDecodable_NarratorsDiscovered(object? sender, string narrators) => Narrator = narrators;
|
private void AudioDecodable_NarratorsDiscovered(object? sender, string narrators) => Narrator = narrators;
|
||||||
private void AudioDecodable_CoverImageDiscovered(object? sender, byte[] coverArt)
|
private void AudioDecodable_CoverImageDiscovered(object? sender, byte[] coverArt)
|
||||||
=> Cover = LoadImageFromBytes(coverArt, PictureSize._80x80);
|
=> Cover = BaseUtil.LoadImage(coverArt, PictureSize._80x80);
|
||||||
|
|
||||||
private byte[] AudioDecodable_RequestCoverArt(object? sender, EventArgs e)
|
private byte[] AudioDecodable_RequestCoverArt(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
@ -292,7 +304,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
|||||||
Status = ProcessBookStatus.Working;
|
Status = ProcessBookStatus.Working;
|
||||||
|
|
||||||
if (sender is Processable processable)
|
if (sender is Processable processable)
|
||||||
Logger.Info($"{Environment.NewLine}{processable.Name} Step, Begin: {libraryBook.Book}");
|
LogInfo($"{Environment.NewLine}{processable.Name} Step, Begin: {libraryBook.Book}");
|
||||||
|
|
||||||
Title = libraryBook.Book.TitleWithSubtitle;
|
Title = libraryBook.Book.TitleWithSubtitle;
|
||||||
Author = libraryBook.Book.AuthorNames();
|
Author = libraryBook.Book.AuthorNames();
|
||||||
@ -303,7 +315,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
|||||||
{
|
{
|
||||||
if (sender is Processable processable)
|
if (sender is Processable processable)
|
||||||
{
|
{
|
||||||
Logger.Info($"{processable.Name} Step, Completed: {libraryBook.Book}");
|
LogInfo($"{processable.Name} Step, Completed: {libraryBook.Book}");
|
||||||
UnlinkProcessable(processable);
|
UnlinkProcessable(processable);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -329,7 +341,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
|||||||
if (result.HasErrors)
|
if (result.HasErrors)
|
||||||
{
|
{
|
||||||
foreach (var errorMessage in result.Errors.Where(e => e != "Validation failed"))
|
foreach (var errorMessage in result.Errors.Where(e => e != "Validation failed"))
|
||||||
Logger.Error(errorMessage);
|
LogError(errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,7 +352,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
|||||||
protected async Task<ProcessBookResult> GetFailureActionAsync(LibraryBook libraryBook)
|
protected async Task<ProcessBookResult> GetFailureActionAsync(LibraryBook libraryBook)
|
||||||
{
|
{
|
||||||
const DialogResult SkipResult = DialogResult.Ignore;
|
const DialogResult SkipResult = DialogResult.Ignore;
|
||||||
Logger.Error($"ERROR. All books have not been processed. Book failed: {libraryBook.Book}");
|
LogError($"ERROR. All books have not been processed. Book failed: {libraryBook.Book}");
|
||||||
|
|
||||||
DialogResult? dialogResult = Configuration.Instance.BadBook switch
|
DialogResult? dialogResult = Configuration.Instance.BadBook switch
|
||||||
{
|
{
|
||||||
@ -353,7 +365,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
|||||||
if (dialogResult == SkipResult)
|
if (dialogResult == SkipResult)
|
||||||
{
|
{
|
||||||
libraryBook.UpdateBookStatus(LiberatedStatus.Error);
|
libraryBook.UpdateBookStatus(LiberatedStatus.Error);
|
||||||
Logger.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.TitleWithSubtitle}");
|
LogInfo($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.TitleWithSubtitle}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return dialogResult is SkipResult ? ProcessBookResult.FailedSkip
|
return dialogResult is SkipResult ? ProcessBookResult.FailedSkip
|
||||||
@ -1,30 +1,32 @@
|
|||||||
using DataLayer;
|
using ApplicationServices;
|
||||||
|
using DataLayer;
|
||||||
using LibationUiBase.Forms;
|
using LibationUiBase.Forms;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using ApplicationServices;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
namespace LibationUiBase.ProcessQueue;
|
namespace LibationUiBase.ProcessQueue;
|
||||||
|
|
||||||
public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
|
public record LogEntry(DateTime LogDate, string LogMessage)
|
||||||
{
|
{
|
||||||
public abstract void WriteLine(string text);
|
public string LogDateString => LogDate.ToShortTimeString();
|
||||||
protected abstract ProcessBookViewModelBase CreateNewProcessBook(LibraryBook libraryBook);
|
}
|
||||||
|
|
||||||
public TrackedQueue<ProcessBookViewModelBase> Queue { get; }
|
public class ProcessQueueViewModel : ReactiveObject
|
||||||
|
{
|
||||||
|
public ObservableCollection<LogEntry> LogEntries { get; } = new();
|
||||||
|
public TrackedQueue<ProcessBookViewModel> Queue { get; } = new();
|
||||||
public Task? QueueRunner { get; private set; }
|
public Task? QueueRunner { get; private set; }
|
||||||
public bool Running => !QueueRunner?.IsCompleted ?? false;
|
public bool Running => !QueueRunner?.IsCompleted ?? false;
|
||||||
protected LogMe Logger { get; }
|
|
||||||
|
|
||||||
public ProcessQueueViewModelBase(ICollection<ProcessBookViewModelBase>? underlyingList)
|
public ProcessQueueViewModel()
|
||||||
{
|
{
|
||||||
Logger = LogMe.RegisterForm(this);
|
|
||||||
Queue = new(underlyingList);
|
|
||||||
Queue.QueuedCountChanged += Queue_QueuedCountChanged;
|
Queue.QueuedCountChanged += Queue_QueuedCountChanged;
|
||||||
Queue.CompletedCountChanged += Queue_CompletedCountChanged;
|
Queue.CompletedCountChanged += Queue_CompletedCountChanged;
|
||||||
|
SpeedLimit = LibationFileManager.Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int _completedCount;
|
private int _completedCount;
|
||||||
@ -32,6 +34,7 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
|
|||||||
private int _queuedCount;
|
private int _queuedCount;
|
||||||
private string? _runningTime;
|
private string? _runningTime;
|
||||||
private bool _progressBarVisible;
|
private bool _progressBarVisible;
|
||||||
|
private decimal _speedLimit;
|
||||||
|
|
||||||
public int CompletedCount { get => _completedCount; private set { RaiseAndSetIfChanged(ref _completedCount, value); RaisePropertyChanged(nameof(AnyCompleted)); } }
|
public int CompletedCount { get => _completedCount; private set { RaiseAndSetIfChanged(ref _completedCount, value); RaisePropertyChanged(nameof(AnyCompleted)); } }
|
||||||
public int QueuedCount { get => _queuedCount; private set { this.RaiseAndSetIfChanged(ref _queuedCount, value); RaisePropertyChanged(nameof(AnyQueued)); } }
|
public int QueuedCount { get => _queuedCount; private set { this.RaiseAndSetIfChanged(ref _queuedCount, value); RaisePropertyChanged(nameof(AnyQueued)); } }
|
||||||
@ -42,6 +45,32 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
|
|||||||
public bool AnyQueued => QueuedCount > 0;
|
public bool AnyQueued => QueuedCount > 0;
|
||||||
public bool AnyErrors => ErrorCount > 0;
|
public bool AnyErrors => ErrorCount > 0;
|
||||||
public double Progress => 100d * Queue.Completed.Count / Queue.Count;
|
public double Progress => 100d * Queue.Completed.Count / Queue.Count;
|
||||||
|
public decimal SpeedLimitIncrement { get; private set; }
|
||||||
|
public decimal SpeedLimit
|
||||||
|
{
|
||||||
|
get => _speedLimit;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
var newValue = Math.Min(999 * 1024 * 1024, (long)Math.Ceiling(value * 1024 * 1024));
|
||||||
|
var config = LibationFileManager.Configuration.Instance;
|
||||||
|
config.DownloadSpeedLimit = newValue;
|
||||||
|
|
||||||
|
_speedLimit
|
||||||
|
= config.DownloadSpeedLimit <= newValue ? value
|
||||||
|
: value == 0.01m ? config.DownloadSpeedLimit / 1024m / 1024
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
config.DownloadSpeedLimit = (long)(_speedLimit * 1024 * 1024);
|
||||||
|
|
||||||
|
SpeedLimitIncrement = _speedLimit > 100 ? 10
|
||||||
|
: _speedLimit > 10 ? 1
|
||||||
|
: _speedLimit > 1 ? 0.1m
|
||||||
|
: 0.01m;
|
||||||
|
|
||||||
|
RaisePropertyChanged(nameof(SpeedLimitIncrement));
|
||||||
|
RaisePropertyChanged(nameof(SpeedLimit));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void Queue_CompletedCountChanged(object? sender, int e)
|
private void Queue_CompletedCountChanged(object? sender, int e)
|
||||||
{
|
{
|
||||||
@ -59,6 +88,9 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
|
|||||||
RaisePropertyChanged(nameof(Progress));
|
RaisePropertyChanged(nameof(Progress));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ProcessBook_LogWritten(object? sender, string logMessage)
|
||||||
|
=> Invoke(() => LogEntries.Add(new(DateTime.Now, logMessage.Trim())));
|
||||||
|
|
||||||
#region Add Books to Queue
|
#region Add Books to Queue
|
||||||
|
|
||||||
public bool QueueDownloadPdf(IList<LibraryBook> libraryBooks)
|
public bool QueueDownloadPdf(IList<LibraryBook> libraryBooks)
|
||||||
@ -124,47 +156,50 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
|
|||||||
}
|
}
|
||||||
|
|
||||||
private bool IsBookInQueue(LibraryBook libraryBook)
|
private bool IsBookInQueue(LibraryBook libraryBook)
|
||||||
=> Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is not ProcessBookViewModelBase entry ? false
|
=> Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is not ProcessBookViewModel entry ? false
|
||||||
: entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed ? !Queue.RemoveCompleted(entry)
|
: entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed ? !Queue.RemoveCompleted(entry)
|
||||||
: true;
|
: true;
|
||||||
|
|
||||||
private bool RemoveCompleted(LibraryBook libraryBook)
|
private bool RemoveCompleted(LibraryBook libraryBook)
|
||||||
=> Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is ProcessBookViewModelBase entry
|
=> Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is ProcessBookViewModel entry
|
||||||
&& entry.Status is ProcessBookStatus.Completed
|
&& entry.Status is ProcessBookStatus.Completed
|
||||||
&& Queue.RemoveCompleted(entry);
|
&& Queue.RemoveCompleted(entry);
|
||||||
|
|
||||||
private void AddDownloadPdf(IEnumerable<LibraryBook> entries)
|
private void AddDownloadPdf(IList<LibraryBook> entries)
|
||||||
{
|
{
|
||||||
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
|
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
|
||||||
Serilog.Log.Logger.Information("Queueing {count} books for PDF-only download", procs.Length);
|
Serilog.Log.Logger.Information("Queueing {count} books for PDF-only download", procs.Length);
|
||||||
AddToQueue(procs);
|
AddToQueue(procs);
|
||||||
|
|
||||||
ProcessBookViewModelBase Create(LibraryBook entry)
|
ProcessBookViewModel Create(LibraryBook entry)
|
||||||
=> CreateNewProcessBook(entry).AddDownloadPdf();
|
=> new ProcessBookViewModel(entry).AddDownloadPdf();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddDownloadDecrypt(IEnumerable<LibraryBook> entries)
|
private void AddDownloadDecrypt(IList<LibraryBook> entries)
|
||||||
{
|
{
|
||||||
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
|
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
|
||||||
Serilog.Log.Logger.Information("Queueing {count} books ofr download/decrypt", procs.Length);
|
Serilog.Log.Logger.Information("Queueing {count} books ofr download/decrypt", procs.Length);
|
||||||
AddToQueue(procs);
|
AddToQueue(procs);
|
||||||
|
|
||||||
ProcessBookViewModelBase Create(LibraryBook entry)
|
ProcessBookViewModel Create(LibraryBook entry)
|
||||||
=> CreateNewProcessBook(entry).AddDownloadDecryptBook().AddDownloadPdf();
|
=> new ProcessBookViewModel(entry).AddDownloadDecryptBook().AddDownloadPdf();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddConvertMp3(IEnumerable<LibraryBook> entries)
|
private void AddConvertMp3(IList<LibraryBook> entries)
|
||||||
{
|
{
|
||||||
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
|
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
|
||||||
Serilog.Log.Logger.Information("Queueing {count} books for mp3 conversion", procs.Length);
|
Serilog.Log.Logger.Information("Queueing {count} books for mp3 conversion", procs.Length);
|
||||||
AddToQueue(procs);
|
AddToQueue(procs);
|
||||||
|
|
||||||
ProcessBookViewModelBase Create(LibraryBook entry)
|
ProcessBookViewModel Create(LibraryBook entry)
|
||||||
=> CreateNewProcessBook(entry).AddConvertToMp3();
|
=> new ProcessBookViewModel(entry).AddConvertToMp3();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddToQueue(IEnumerable<ProcessBookViewModelBase> pbook)
|
private void AddToQueue(IList<ProcessBookViewModel> pbook)
|
||||||
{
|
{
|
||||||
|
foreach (var book in pbook)
|
||||||
|
book.LogWritten += ProcessBook_LogWritten;
|
||||||
|
|
||||||
Queue.Enqueue(pbook);
|
Queue.Enqueue(pbook);
|
||||||
if (!Running)
|
if (!Running)
|
||||||
QueueRunner = Task.Run(QueueLoop);
|
QueueRunner = Task.Run(QueueLoop);
|
||||||
@ -187,7 +222,7 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
|
|||||||
|
|
||||||
while (Queue.MoveNext())
|
while (Queue.MoveNext())
|
||||||
{
|
{
|
||||||
if (Queue.Current is not ProcessBookViewModelBase nextBook)
|
if (Queue.Current is not ProcessBookViewModel nextBook)
|
||||||
{
|
{
|
||||||
Serilog.Log.Logger.Information("Current queue item is empty.");
|
Serilog.Log.Logger.Information("Current queue item is empty.");
|
||||||
continue;
|
continue;
|
||||||
@ -1,5 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Specialized;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
@ -7,7 +9,7 @@ namespace LibationUiBase
|
|||||||
{
|
{
|
||||||
public enum QueuePosition
|
public enum QueuePosition
|
||||||
{
|
{
|
||||||
Fisrt,
|
First,
|
||||||
OneUp,
|
OneUp,
|
||||||
OneDown,
|
OneDown,
|
||||||
Last,
|
Last,
|
||||||
@ -23,37 +25,20 @@ namespace LibationUiBase
|
|||||||
*
|
*
|
||||||
* The index is the link position from the first link you lifted to the
|
* The index is the link position from the first link you lifted to the
|
||||||
* last one in the chain.
|
* last one in the chain.
|
||||||
*
|
|
||||||
*
|
|
||||||
* For this to work with Avalonia's ItemsRepeater, it must be an ObservableCollection
|
|
||||||
* (not merely a Collection with INotifyCollectionChanged, INotifyPropertyChanged).
|
|
||||||
* So TrackedQueue maintains 2 copies of the list. The primary copy of the list is
|
|
||||||
* split into Completed, Current and Queued and is used by ProcessQueue to keep track
|
|
||||||
* of what's what. The secondary copy is a concatenation of primary's three sources
|
|
||||||
* and is stored in ObservableCollection.Items. When the primary list changes, the
|
|
||||||
* secondary list is cleared and reset to match the primary.
|
|
||||||
*/
|
*/
|
||||||
public class TrackedQueue<T> where T : class
|
public class TrackedQueue<T> : IReadOnlyCollection<T>, IList, INotifyCollectionChanged where T : class
|
||||||
{
|
{
|
||||||
public event EventHandler<int>? CompletedCountChanged;
|
public event EventHandler<int>? CompletedCountChanged;
|
||||||
public event EventHandler<int>? QueuedCountChanged;
|
public event EventHandler<int>? QueuedCountChanged;
|
||||||
|
public event NotifyCollectionChangedEventHandler? CollectionChanged;
|
||||||
|
|
||||||
public T? Current { get; private set; }
|
public T? Current { get; private set; }
|
||||||
|
|
||||||
public IReadOnlyList<T> Queued => _queued;
|
|
||||||
public IReadOnlyList<T> Completed => _completed;
|
public IReadOnlyList<T> Completed => _completed;
|
||||||
|
private List<T> Queued { get; } = new();
|
||||||
|
|
||||||
private readonly List<T> _queued = new();
|
|
||||||
private readonly List<T> _completed = new();
|
private readonly List<T> _completed = new();
|
||||||
private readonly object lockObject = new();
|
private readonly object lockObject = new();
|
||||||
|
private int QueueStartIndex => Completed.Count + (Current is null ? 0 : 1);
|
||||||
private readonly ICollection<T>? _underlyingList;
|
|
||||||
public ICollection<T>? UnderlyingList => _underlyingList;
|
|
||||||
|
|
||||||
public TrackedQueue(ICollection<T>? underlyingList = null)
|
|
||||||
{
|
|
||||||
_underlyingList = underlyingList;
|
|
||||||
}
|
|
||||||
|
|
||||||
public T this[int index]
|
public T this[int index]
|
||||||
{
|
{
|
||||||
@ -61,17 +46,10 @@ namespace LibationUiBase
|
|||||||
{
|
{
|
||||||
lock (lockObject)
|
lock (lockObject)
|
||||||
{
|
{
|
||||||
if (index < _completed.Count)
|
return index < Completed.Count ? Completed[index]
|
||||||
return _completed[index];
|
: index == Completed.Count && Current is not null ? Current
|
||||||
index -= _completed.Count;
|
: index < Count ? Queued[index - QueueStartIndex]
|
||||||
|
: throw new IndexOutOfRangeException();
|
||||||
if (index == 0 && Current != null) return Current;
|
|
||||||
|
|
||||||
if (Current != null) index--;
|
|
||||||
|
|
||||||
if (index < _queued.Count) return _queued.ElementAt(index);
|
|
||||||
|
|
||||||
throw new IndexOutOfRangeException();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -82,7 +60,7 @@ namespace LibationUiBase
|
|||||||
{
|
{
|
||||||
lock (lockObject)
|
lock (lockObject)
|
||||||
{
|
{
|
||||||
return _queued.Count + _completed.Count + (Current == null ? 0 : 1);
|
return QueueStartIndex + Queued.Count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -91,131 +69,117 @@ namespace LibationUiBase
|
|||||||
{
|
{
|
||||||
lock (lockObject)
|
lock (lockObject)
|
||||||
{
|
{
|
||||||
if (_completed.Contains(item))
|
int index = _completed.IndexOf(item);
|
||||||
return _completed.IndexOf(item);
|
if (index < 0 && item == Current)
|
||||||
|
index = Completed.Count;
|
||||||
if (Current == item) return _completed.Count;
|
if (index < 0)
|
||||||
|
{
|
||||||
if (_queued.Contains(item))
|
index = Queued.IndexOf(item);
|
||||||
return _queued.IndexOf(item) + (Current is null ? 0 : 1);
|
if (index >= 0)
|
||||||
return -1;
|
index += QueueStartIndex;
|
||||||
|
}
|
||||||
|
return index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool RemoveQueued(T item)
|
public bool RemoveQueued(T item)
|
||||||
{
|
{
|
||||||
bool itemsRemoved;
|
int queuedCount, queueIndex;
|
||||||
int queuedCount;
|
|
||||||
|
|
||||||
lock (lockObject)
|
lock (lockObject)
|
||||||
{
|
{
|
||||||
itemsRemoved = _queued.Remove(item);
|
queueIndex = Queued.IndexOf(item);
|
||||||
queuedCount = _queued.Count;
|
if (queueIndex >= 0)
|
||||||
|
Queued.RemoveAt(queueIndex);
|
||||||
|
queuedCount = Queued.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemsRemoved)
|
if (queueIndex >= 0)
|
||||||
{
|
{
|
||||||
QueuedCountChanged?.Invoke(this, queuedCount);
|
QueuedCountChanged?.Invoke(this, queuedCount);
|
||||||
RebuildSecondary();
|
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, QueueStartIndex + queueIndex));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return itemsRemoved;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
public void ClearCurrent()
|
|
||||||
{
|
|
||||||
lock (lockObject)
|
|
||||||
Current = null;
|
|
||||||
RebuildSecondary();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool RemoveCompleted(T item)
|
public bool RemoveCompleted(T item)
|
||||||
{
|
{
|
||||||
bool itemsRemoved;
|
int completedCount, completedIndex;
|
||||||
int completedCount;
|
|
||||||
|
|
||||||
lock (lockObject)
|
lock (lockObject)
|
||||||
{
|
{
|
||||||
itemsRemoved = _completed.Remove(item);
|
completedIndex = _completed.IndexOf(item);
|
||||||
|
if (completedIndex >= 0)
|
||||||
|
_completed.RemoveAt(completedIndex);
|
||||||
completedCount = _completed.Count;
|
completedCount = _completed.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemsRemoved)
|
if (completedIndex >= 0)
|
||||||
{
|
{
|
||||||
CompletedCountChanged?.Invoke(this, completedCount);
|
CompletedCountChanged?.Invoke(this, completedCount);
|
||||||
RebuildSecondary();
|
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, completedIndex));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return itemsRemoved;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearCurrent()
|
||||||
|
{
|
||||||
|
T? current;
|
||||||
|
lock (lockObject)
|
||||||
|
{
|
||||||
|
current = Current;
|
||||||
|
Current = null;
|
||||||
|
}
|
||||||
|
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, current, _completed.Count));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearQueue()
|
public void ClearQueue()
|
||||||
{
|
{
|
||||||
|
List<T> queuedItems;
|
||||||
lock (lockObject)
|
lock (lockObject)
|
||||||
_queued.Clear();
|
{
|
||||||
|
queuedItems = Queued.ToList();
|
||||||
|
Queued.Clear();
|
||||||
|
}
|
||||||
QueuedCountChanged?.Invoke(this, 0);
|
QueuedCountChanged?.Invoke(this, 0);
|
||||||
RebuildSecondary();
|
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, queuedItems, QueueStartIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearCompleted()
|
public void ClearCompleted()
|
||||||
{
|
{
|
||||||
|
List<T> completedItems;
|
||||||
lock (lockObject)
|
lock (lockObject)
|
||||||
|
{
|
||||||
|
completedItems = _completed.ToList();
|
||||||
_completed.Clear();
|
_completed.Clear();
|
||||||
|
}
|
||||||
CompletedCountChanged?.Invoke(this, 0);
|
CompletedCountChanged?.Invoke(this, 0);
|
||||||
RebuildSecondary();
|
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, completedItems, 0));
|
||||||
}
|
|
||||||
|
|
||||||
public bool Any(Func<T, bool> predicate)
|
|
||||||
{
|
|
||||||
lock (lockObject)
|
|
||||||
{
|
|
||||||
return (Current != null && predicate(Current)) || _completed.Any(predicate) || _queued.Any(predicate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public T? FirstOrDefault(Func<T, bool> predicate)
|
|
||||||
{
|
|
||||||
lock (lockObject)
|
|
||||||
{
|
|
||||||
return Current != null && predicate(Current) ? Current
|
|
||||||
: _completed.FirstOrDefault(predicate) is T completed ? completed
|
|
||||||
: _queued.FirstOrDefault(predicate);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void MoveQueuePosition(T item, QueuePosition requestedPosition)
|
public void MoveQueuePosition(T item, QueuePosition requestedPosition)
|
||||||
{
|
{
|
||||||
|
int oldIndex, newIndex;
|
||||||
lock (lockObject)
|
lock (lockObject)
|
||||||
{
|
{
|
||||||
if (_queued.Count == 0 || !_queued.Contains(item)) return;
|
oldIndex = Queued.IndexOf(item);
|
||||||
|
newIndex = requestedPosition switch
|
||||||
|
{
|
||||||
|
QueuePosition.First => 0,
|
||||||
|
QueuePosition.OneUp => oldIndex - 1,
|
||||||
|
QueuePosition.OneDown => oldIndex + 1,
|
||||||
|
QueuePosition.Last or _ => Queued.Count - 1
|
||||||
|
};
|
||||||
|
|
||||||
if ((requestedPosition == QueuePosition.Fisrt || requestedPosition == QueuePosition.OneUp) && _queued[0] == item)
|
if (oldIndex < 0 || newIndex < 0 || newIndex >= Queued.Count || newIndex == oldIndex)
|
||||||
return;
|
|
||||||
if ((requestedPosition == QueuePosition.Last || requestedPosition == QueuePosition.OneDown) && _queued[^1] == item)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
int queueIndex = _queued.IndexOf(item);
|
Queued.RemoveAt(oldIndex);
|
||||||
|
Queued.Insert(newIndex, item);
|
||||||
if (requestedPosition == QueuePosition.OneUp)
|
|
||||||
{
|
|
||||||
_queued.RemoveAt(queueIndex);
|
|
||||||
_queued.Insert(queueIndex - 1, item);
|
|
||||||
}
|
}
|
||||||
else if (requestedPosition == QueuePosition.OneDown)
|
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, item, QueueStartIndex + newIndex, QueueStartIndex + oldIndex));
|
||||||
{
|
|
||||||
_queued.RemoveAt(queueIndex);
|
|
||||||
_queued.Insert(queueIndex + 1, item);
|
|
||||||
}
|
|
||||||
else if (requestedPosition == QueuePosition.Fisrt)
|
|
||||||
{
|
|
||||||
_queued.RemoveAt(queueIndex);
|
|
||||||
_queued.Insert(0, item);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_queued.RemoveAt(queueIndex);
|
|
||||||
_queued.Insert(_queued.Count, item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RebuildSecondary();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool MoveNext()
|
public bool MoveNext()
|
||||||
@ -232,15 +196,15 @@ namespace LibationUiBase
|
|||||||
completedCount = _completed.Count;
|
completedCount = _completed.Count;
|
||||||
completedChanged = true;
|
completedChanged = true;
|
||||||
}
|
}
|
||||||
if (_queued.Count == 0)
|
if (Queued.Count == 0)
|
||||||
{
|
{
|
||||||
Current = null;
|
Current = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Current = _queued[0];
|
Current = Queued[0];
|
||||||
_queued.RemoveAt(0);
|
Queued.RemoveAt(0);
|
||||||
|
|
||||||
queuedCount = _queued.Count;
|
queuedCount = Queued.Count;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -249,34 +213,48 @@ namespace LibationUiBase
|
|||||||
if (completedChanged)
|
if (completedChanged)
|
||||||
CompletedCountChanged?.Invoke(this, completedCount);
|
CompletedCountChanged?.Invoke(this, completedCount);
|
||||||
QueuedCountChanged?.Invoke(this, queuedCount);
|
QueuedCountChanged?.Invoke(this, queuedCount);
|
||||||
RebuildSecondary();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Enqueue(IEnumerable<T> item)
|
public void Enqueue(IList<T> item)
|
||||||
{
|
{
|
||||||
int queueCount;
|
int queueCount;
|
||||||
lock (lockObject)
|
lock (lockObject)
|
||||||
{
|
{
|
||||||
_queued.AddRange(item);
|
Queued.AddRange(item);
|
||||||
queueCount = _queued.Count;
|
queueCount = Queued.Count;
|
||||||
}
|
}
|
||||||
foreach (var i in item)
|
|
||||||
_underlyingList?.Add(i);
|
|
||||||
QueuedCountChanged?.Invoke(this, queueCount);
|
QueuedCountChanged?.Invoke(this, queueCount);
|
||||||
}
|
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, QueueStartIndex + Queued.Count));
|
||||||
|
|
||||||
private void RebuildSecondary()
|
|
||||||
{
|
|
||||||
_underlyingList?.Clear();
|
|
||||||
foreach (var item in GetAllItems())
|
|
||||||
_underlyingList?.Add(item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<T> GetAllItems()
|
public IEnumerable<T> GetAllItems()
|
||||||
|
{
|
||||||
|
lock (lockObject)
|
||||||
{
|
{
|
||||||
if (Current is null) return Completed.Concat(Queued);
|
if (Current is null) return Completed.Concat(Queued);
|
||||||
return Completed.Concat(new List<T> { Current }).Concat(Queued);
|
return Completed.Concat([Current]).Concat(Queued);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IEnumerator<T> GetEnumerator() => GetAllItems().GetEnumerator();
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||||
|
|
||||||
|
#region IList interface implementation
|
||||||
|
object? IList.this[int index] { get => this[index]; set => throw new NotSupportedException(); }
|
||||||
|
public bool IsReadOnly => true;
|
||||||
|
public bool IsFixedSize => false;
|
||||||
|
public bool IsSynchronized => false;
|
||||||
|
public object SyncRoot => this;
|
||||||
|
public int IndexOf(object? value) => value is T t ? IndexOf(t) : -1;
|
||||||
|
public bool Contains(object? value) => IndexOf(value) >= 0;
|
||||||
|
//These aren't used by anything, but they are IList interface members and this class needs to be an IList for Avalonia
|
||||||
|
public int Add(object? value) => throw new NotSupportedException();
|
||||||
|
public void Clear() => throw new NotSupportedException();
|
||||||
|
public void Insert(int index, object? value) => throw new NotSupportedException();
|
||||||
|
public void Remove(object? value) => throw new NotSupportedException();
|
||||||
|
public void RemoveAt(int index) => throw new NotSupportedException();
|
||||||
|
public void CopyTo(Array array, int index) => throw new NotSupportedException();
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,33 +3,20 @@ using System;
|
|||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationWinForms.ProcessQueue
|
namespace LibationWinForms.ProcessQueue
|
||||||
{
|
{
|
||||||
internal partial class ProcessBookControl : UserControl
|
internal partial class ProcessBookControl : UserControl
|
||||||
{
|
{
|
||||||
private readonly int CancelBtnDistanceFromEdge;
|
private readonly int CancelBtnDistanceFromEdge;
|
||||||
private readonly int ProgressBarDistanceFromEdge;
|
private readonly int ProgressBarDistanceFromEdge;
|
||||||
|
private object? m_OldContext;
|
||||||
|
|
||||||
private static Color FailedColor { get; } = Color.LightCoral;
|
private static Color FailedColor { get; } = Color.LightCoral;
|
||||||
private static Color CancelledColor { get; } = Color.Khaki;
|
private static Color CancelledColor { get; } = Color.Khaki;
|
||||||
private static Color QueuedColor { get; } = SystemColors.Control;
|
private static Color QueuedColor { get; } = SystemColors.Control;
|
||||||
private static Color SuccessColor { get; } = Color.PaleGreen;
|
private static Color SuccessColor { get; } = Color.PaleGreen;
|
||||||
|
|
||||||
private ProcessBookViewModelBase m_Context;
|
|
||||||
public ProcessBookViewModelBase Context
|
|
||||||
{
|
|
||||||
get => m_Context;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (m_Context != value)
|
|
||||||
{
|
|
||||||
OnContextChanging();
|
|
||||||
m_Context = value;
|
|
||||||
OnContextChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ProcessBookControl()
|
public ProcessBookControl()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
@ -41,35 +28,41 @@ namespace LibationWinForms.ProcessQueue
|
|||||||
ProgressBarDistanceFromEdge = Width - progressBar1.Location.X - progressBar1.Width;
|
ProgressBarDistanceFromEdge = Width - progressBar1.Location.X - progressBar1.Width;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnContextChanging()
|
protected override void OnDataContextChanged(EventArgs e)
|
||||||
{
|
{
|
||||||
if (Context is not null)
|
if (m_OldContext is ProcessBookViewModel oldContext)
|
||||||
Context.PropertyChanged -= Context_PropertyChanged;
|
oldContext.PropertyChanged -= DataContext_PropertyChanged;
|
||||||
|
|
||||||
|
if (DataContext is ProcessBookViewModel newContext)
|
||||||
|
{
|
||||||
|
m_OldContext = newContext;
|
||||||
|
newContext.PropertyChanged += DataContext_PropertyChanged;
|
||||||
|
DataContext_PropertyChanged(DataContext, new System.ComponentModel.PropertyChangedEventArgs(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnContextChanged()
|
base.OnDataContextChanged(e);
|
||||||
{
|
|
||||||
Context.PropertyChanged += Context_PropertyChanged;
|
|
||||||
Context_PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(null));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Context_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
private void DataContext_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
|
if (sender is not ProcessBookViewModel vm)
|
||||||
|
return;
|
||||||
|
|
||||||
SuspendLayout();
|
SuspendLayout();
|
||||||
if (e.PropertyName is null or nameof(Context.Cover))
|
if (e.PropertyName is null or nameof(vm.Cover))
|
||||||
SetCover(Context.Cover as Image);
|
SetCover(vm.Cover as Image);
|
||||||
if (e.PropertyName is null or nameof(Context.Title) or nameof(Context.Author) or nameof(Context.Narrator))
|
if (e.PropertyName is null or nameof(vm.Title) or nameof(vm.Author) or nameof(vm.Narrator))
|
||||||
SetBookInfo($"{Context.Title}\r\nBy {Context.Author}\r\nNarrated by {Context.Narrator}");
|
SetBookInfo($"{vm.Title}\r\nBy {vm.Author}\r\nNarrated by {vm.Narrator}");
|
||||||
if (e.PropertyName is null or nameof(Context.Status) or nameof(Context.StatusText))
|
if (e.PropertyName is null or nameof(vm.Status) or nameof(vm.StatusText))
|
||||||
SetStatus(Context.Status, Context.StatusText);
|
SetStatus(vm.Status, vm.StatusText);
|
||||||
if (e.PropertyName is null or nameof(Context.Progress))
|
if (e.PropertyName is null or nameof(vm.Progress))
|
||||||
SetProgress(Context.Progress);
|
SetProgress(vm.Progress);
|
||||||
if (e.PropertyName is null or nameof(Context.TimeRemaining))
|
if (e.PropertyName is null or nameof(vm.TimeRemaining))
|
||||||
SetRemainingTime(Context.TimeRemaining);
|
SetRemainingTime(vm.TimeRemaining);
|
||||||
ResumeLayout();
|
ResumeLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetCover(Image cover) => pictureBox1.Image = cover;
|
private void SetCover(Image? cover) => pictureBox1.Image = cover;
|
||||||
private void SetBookInfo(string title) => bookInfoLbl.Text = title;
|
private void SetBookInfo(string title) => bookInfoLbl.Text = title;
|
||||||
private void SetRemainingTime(TimeSpan remaining)
|
private void SetRemainingTime(TimeSpan remaining)
|
||||||
=> remainingTimeLbl.Text = $"{remaining:mm\\:ss}";
|
=> remainingTimeLbl.Text = $"{remaining:mm\\:ss}";
|
||||||
|
|||||||
@ -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 LibationFileManager;
|
||||||
using LibationUiBase;
|
using LibationUiBase;
|
||||||
|
using LibationUiBase.ProcessQueue;
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
@ -27,49 +28,47 @@ internal partial class ProcessQueueControl : UserControl
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
var speedLimitMBps = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
|
|
||||||
numericUpDown1.Value = speedLimitMBps > numericUpDown1.Maximum || speedLimitMBps < numericUpDown1.Minimum ? 0 : speedLimitMBps;
|
|
||||||
statusStrip1.Items.Add(PopoutButton);
|
statusStrip1.Items.Add(PopoutButton);
|
||||||
|
|
||||||
virtualFlowControl2.ButtonClicked += VirtualFlowControl2_ButtonClicked;
|
virtualFlowControl2.ButtonClicked += VirtualFlowControl2_ButtonClicked;
|
||||||
|
virtualFlowControl2.DataContext = ViewModel.Queue;
|
||||||
|
|
||||||
ViewModel.LogWritten += (_, text) => WriteLine(text);
|
|
||||||
ViewModel.PropertyChanged += ProcessQueue_PropertyChanged;
|
ViewModel.PropertyChanged += ProcessQueue_PropertyChanged;
|
||||||
virtualFlowControl2.Items = ViewModel.Items;
|
ViewModel.LogEntries.CollectionChanged += LogEntries_CollectionChanged;
|
||||||
Load += ProcessQueueControl_Load;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessQueueControl_Load(object? sender, EventArgs e)
|
private void LogEntries_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!IsDisposed && e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
|
||||||
|
{
|
||||||
|
foreach(var entry in e.NewItems?.OfType<LogEntry>() ?? [])
|
||||||
|
logDGV.Rows.Add(entry.LogDate, entry.LogMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnLoad(EventArgs e)
|
||||||
{
|
{
|
||||||
if (DesignMode) return;
|
if (DesignMode) return;
|
||||||
ProcessQueue_PropertyChanged(this, new PropertyChangedEventArgs(null));
|
ProcessQueue_PropertyChanged(this, new PropertyChangedEventArgs(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void WriteLine(string text)
|
|
||||||
{
|
|
||||||
if (!IsDisposed)
|
|
||||||
logDGV.Rows.Add(DateTime.Now, text.Trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void cancelAllBtn_Click(object? sender, EventArgs e)
|
private async void cancelAllBtn_Click(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
ViewModel.Queue.ClearQueue();
|
ViewModel.Queue.ClearQueue();
|
||||||
if (ViewModel.Queue.Current is not null)
|
if (ViewModel.Queue.Current is not null)
|
||||||
await ViewModel.Queue.Current.CancelAsync();
|
await ViewModel.Queue.Current.CancelAsync();
|
||||||
virtualFlowControl2.RefreshDisplay();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void btnClearFinished_Click(object? sender, EventArgs e)
|
private void btnClearFinished_Click(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
ViewModel.Queue.ClearCompleted();
|
ViewModel.Queue.ClearCompleted();
|
||||||
virtualFlowControl2.RefreshDisplay();
|
|
||||||
|
|
||||||
if (!ViewModel.Running)
|
if (!ViewModel.Running)
|
||||||
runningTimeLbl.Text = string.Empty;
|
runningTimeLbl.Text = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clearLogBtn_Click(object? sender, EventArgs e)
|
private void clearLogBtn_Click(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
|
ViewModel.LogEntries.Clear();
|
||||||
logDGV.Rows.Clear();
|
logDGV.Rows.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,7 +91,6 @@ internal partial class ProcessQueueControl : UserControl
|
|||||||
{
|
{
|
||||||
queueNumberLbl.Text = ViewModel.QueuedCount.ToString();
|
queueNumberLbl.Text = ViewModel.QueuedCount.ToString();
|
||||||
queueNumberLbl.Visible = ViewModel.QueuedCount > 0;
|
queueNumberLbl.Visible = ViewModel.QueuedCount > 0;
|
||||||
virtualFlowControl2.RefreshDisplay();
|
|
||||||
}
|
}
|
||||||
if (e.PropertyName is null or nameof(ViewModel.ErrorCount))
|
if (e.PropertyName is null or nameof(ViewModel.ErrorCount))
|
||||||
{
|
{
|
||||||
@ -117,16 +115,22 @@ internal partial class ProcessQueueControl : UserControl
|
|||||||
{
|
{
|
||||||
runningTimeLbl.Text = ViewModel.RunningTime;
|
runningTimeLbl.Text = ViewModel.RunningTime;
|
||||||
}
|
}
|
||||||
|
if (e.PropertyName is null or nameof(ViewModel.SpeedLimit))
|
||||||
|
{
|
||||||
|
numericUpDown1.Value = ViewModel.SpeedLimit;
|
||||||
|
numericUpDown1.Increment = ViewModel.SpeedLimitIncrement;
|
||||||
|
numericUpDown1.DecimalPlaces = ViewModel.SpeedLimit >= 10 ? 0 : ViewModel.SpeedLimit >= 1 ? 1 : 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// View notified the model that a botton was clicked
|
/// View notified the model that a button was clicked
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sender">the <see cref="ProcessBookControl"/> whose button was clicked</param>
|
/// <param name="sender">the <see cref="ProcessBookControl"/> whose button was clicked</param>
|
||||||
/// <param name="buttonName">The name of the button clicked</param>
|
/// <param name="buttonName">The name of the button clicked</param>
|
||||||
private async void VirtualFlowControl2_ButtonClicked(object? sender, string buttonName)
|
private async void VirtualFlowControl2_ButtonClicked(object? sender, string buttonName)
|
||||||
{
|
{
|
||||||
if (sender is not ProcessBookControl control || control.Context is not ProcessBookViewModel item)
|
if (sender is not ProcessBookControl control || control.DataContext is not ProcessBookViewModel item)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
try
|
try
|
||||||
@ -135,13 +139,12 @@ internal partial class ProcessQueueControl : UserControl
|
|||||||
{
|
{
|
||||||
await item.CancelAsync();
|
await item.CancelAsync();
|
||||||
ViewModel.Queue.RemoveQueued(item);
|
ViewModel.Queue.RemoveQueued(item);
|
||||||
virtualFlowControl2.RefreshDisplay();
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
QueuePosition? position = buttonName switch
|
QueuePosition? position = buttonName switch
|
||||||
{
|
{
|
||||||
nameof(ProcessBookControl.moveFirstBtn) => QueuePosition.Fisrt,
|
nameof(ProcessBookControl.moveFirstBtn) => QueuePosition.First,
|
||||||
nameof(ProcessBookControl.moveUpBtn) => QueuePosition.OneUp,
|
nameof(ProcessBookControl.moveUpBtn) => QueuePosition.OneUp,
|
||||||
nameof(ProcessBookControl.moveDownBtn) => QueuePosition.OneDown,
|
nameof(ProcessBookControl.moveDownBtn) => QueuePosition.OneDown,
|
||||||
nameof(ProcessBookControl.moveLastBtn) => QueuePosition.Last,
|
nameof(ProcessBookControl.moveLastBtn) => QueuePosition.Last,
|
||||||
@ -149,10 +152,7 @@ internal partial class ProcessQueueControl : UserControl
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (position is not null)
|
if (position is not null)
|
||||||
{
|
|
||||||
ViewModel.Queue.MoveQueuePosition(item, position.Value);
|
ViewModel.Queue.MoveQueuePosition(item, position.Value);
|
||||||
virtualFlowControl2.RefreshDisplay();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(Exception ex)
|
catch(Exception ex)
|
||||||
@ -164,27 +164,7 @@ internal partial class ProcessQueueControl : UserControl
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private void numericUpDown1_ValueChanged(object? sender, EventArgs e)
|
private void numericUpDown1_ValueChanged(object? sender, EventArgs e)
|
||||||
{
|
=> ViewModel.SpeedLimit = numericUpDown1.Value;
|
||||||
var newValue = (long)(numericUpDown1.Value * 1024 * 1024);
|
|
||||||
|
|
||||||
var config = Configuration.Instance;
|
|
||||||
config.DownloadSpeedLimit = newValue;
|
|
||||||
if (config.DownloadSpeedLimit > newValue)
|
|
||||||
numericUpDown1.Value =
|
|
||||||
numericUpDown1.Value == 0.01m ? config.DownloadSpeedLimit / 1024m / 1024
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
numericUpDown1.Increment =
|
|
||||||
numericUpDown1.Value > 100 ? 10
|
|
||||||
: numericUpDown1.Value > 10 ? 1
|
|
||||||
: numericUpDown1.Value > 1 ? 0.1m
|
|
||||||
: 0.01m;
|
|
||||||
|
|
||||||
numericUpDown1.DecimalPlaces =
|
|
||||||
numericUpDown1.Value >= 10 ? 0
|
|
||||||
: numericUpDown1.Value >= 1 ? 1
|
|
||||||
: 2;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class NumericUpDownSuffix : NumericUpDown
|
public class NumericUpDownSuffix : NumericUpDown
|
||||||
|
|||||||
@ -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.Generic;
|
||||||
|
using System.Collections.Specialized;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationWinForms.ProcessQueue
|
namespace LibationWinForms.ProcessQueue
|
||||||
{
|
{
|
||||||
internal partial class VirtualFlowControl : UserControl
|
internal partial class VirtualFlowControl : UserControl
|
||||||
@ -11,18 +13,28 @@ namespace LibationWinForms.ProcessQueue
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Triggered when one of the <see cref="ProcessBookControl"/>'s buttons has been clicked
|
/// Triggered when one of the <see cref="ProcessBookControl"/>'s buttons has been clicked
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event EventHandler<string> ButtonClicked;
|
public event EventHandler<string>? ButtonClicked;
|
||||||
|
public IList? Items { get; private set; }
|
||||||
|
|
||||||
private List<ProcessBookViewModelBase> m_Items;
|
private object? m_OldContext;
|
||||||
public List<ProcessBookViewModelBase> Items
|
protected override void OnDataContextChanged(EventArgs e)
|
||||||
{
|
{
|
||||||
get => m_Items;
|
if (m_OldContext is INotifyCollectionChanged oldNotify)
|
||||||
set
|
oldNotify.CollectionChanged -= Items_CollectionChanged;
|
||||||
|
|
||||||
|
if (DataContext is INotifyCollectionChanged newNotify)
|
||||||
{
|
{
|
||||||
m_Items = value;
|
m_OldContext = newNotify;
|
||||||
if (m_Items is not null)
|
newNotify.CollectionChanged += Items_CollectionChanged;
|
||||||
RefreshDisplay();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Items = DataContext as IList;
|
||||||
|
base.OnDataContextChanged(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Items_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
RefreshDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RefreshDisplay()
|
public void RefreshDisplay()
|
||||||
@ -65,7 +77,7 @@ namespace LibationWinForms.ProcessQueue
|
|||||||
#region Instance variables
|
#region Instance variables
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The total height, inclusing margins, of the repeated <see cref="ProcessBookControl"/>
|
/// The total height, including margins, of the repeated <see cref="ProcessBookControl"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly int VirtualControlHeight;
|
private readonly int VirtualControlHeight;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -137,14 +149,15 @@ namespace LibationWinForms.ProcessQueue
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles all button clicks from all <see cref="ProcessBookControl"/>, detects which one sent the click, and fires <see cref="ButtonClicked"/> to notify the model of the click
|
/// Handles all button clicks from all <see cref="ProcessBookControl"/>, detects which one sent the click, and fires <see cref="ButtonClicked"/> to notify the model of the click
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void ControlButton_Click(object sender, EventArgs e)
|
private void ControlButton_Click(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
Control button = sender as Control;
|
Control? button = sender as Control;
|
||||||
Control form = button.Parent;
|
Control? form = button?.Parent;
|
||||||
while (form is not ProcessBookControl)
|
while (form is not null and not ProcessBookControl)
|
||||||
form = form.Parent;
|
form = form?.Parent;
|
||||||
|
|
||||||
ButtonClicked?.Invoke(form, button.Name);
|
if (form is not null && button?.Name is string buttonText)
|
||||||
|
ButtonClicked?.Invoke(form, buttonText);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -176,7 +189,7 @@ namespace LibationWinForms.ProcessQueue
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculated the virtual controls that are in view at the currrent scroll position and windows size,
|
/// Calculated the virtual controls that are in view at the current scroll position and windows size,
|
||||||
/// positions <see cref="panel1"/> to simulate scroll activity, then fires updates the controls with
|
/// positions <see cref="panel1"/> to simulate scroll activity, then fires updates the controls with
|
||||||
/// the context corresponding to the virtual scroll position
|
/// the context corresponding to the virtual scroll position
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -195,9 +208,10 @@ namespace LibationWinForms.ProcessQueue
|
|||||||
numVisible = Math.Min(numVisible, VirtualControlCount);
|
numVisible = Math.Min(numVisible, VirtualControlCount);
|
||||||
numVisible = Math.Min(numVisible, VirtualControlCount - firstVisible);
|
numVisible = Math.Min(numVisible, VirtualControlCount - firstVisible);
|
||||||
|
|
||||||
for (int i = 0; i < numVisible; i++)
|
if (Items is IList items)
|
||||||
{
|
{
|
||||||
BookControls[i].Context = Items[firstVisible + i];
|
for (int i = 0; i < numVisible; i++)
|
||||||
|
BookControls[i].DataContext = items[firstVisible + i];
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < BookControls.Count; i++)
|
for (int i = 0; i < BookControls.Count; i++)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user