mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-09-13 06:07:30 -04:00
The docs page was linked from docs/index.md but missing from the VitePress sidebar, where every sibling feature page is listed. In the dispatch test fake, Started.Enqueue ran before the counter incremented, so WaitForStarted could return in the gap and the HighWaterMark assertion could observe one book fewer than had started. Comments still named TrackedQueue.Current and .Active after both were deleted, the Configuration doc comment still called itself the bound on a spinner when nothing binds it any more, and the ClaimAbort comment said the winner was the book that answered the dialog when it is really whichever book gets there first.
240 lines
6.7 KiB
C#
240 lines
6.7 KiB
C#
using Avalonia.Controls;
|
|
using Avalonia.Data.Converters;
|
|
using Avalonia.Input.Platform;
|
|
using Avalonia.Threading;
|
|
using DataLayer;
|
|
using LibationUiBase;
|
|
using LibationUiBase.ProcessQueue;
|
|
using System;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace LibationAvalonia.Views;
|
|
|
|
public partial class ProcessQueueControl : UserControl
|
|
{
|
|
private TrackedQueue<ProcessBookViewModel>? Queue => _viewModel?.Queue;
|
|
private ProcessQueueViewModel? _viewModel => DataContext as ProcessQueueViewModel;
|
|
|
|
public ProcessQueueControl()
|
|
{
|
|
InitializeComponent();
|
|
|
|
ProcessBookControl.PositionButtonClicked += ProcessBookControl2_ButtonClicked;
|
|
ProcessBookControl.CancelButtonClicked += ProcessBookControl2_CancelButtonClicked;
|
|
|
|
#region Design Mode Testing
|
|
#if DEBUG
|
|
if (Design.IsDesignMode)
|
|
{
|
|
ViewModels.MainVM.Configure_NonUI();
|
|
var config = LibationFileManager.Configuration.CreateMockInstance();
|
|
var vm = new ProcessQueueViewModel();
|
|
DataContext = vm;
|
|
|
|
var trialBook = MockLibraryBook.CreateBook();
|
|
System.Collections.Generic.List<ProcessBookViewModel> testList = new()
|
|
{
|
|
new ProcessBookViewModel(trialBook, config)
|
|
{
|
|
Result = ProcessBookResult.FailedAbort,
|
|
Status = ProcessBookStatus.Failed,
|
|
},
|
|
new ProcessBookViewModel(trialBook, config)
|
|
{
|
|
Result = ProcessBookResult.FailedSkip,
|
|
Status = ProcessBookStatus.Failed,
|
|
},
|
|
new ProcessBookViewModel(trialBook, config)
|
|
{
|
|
Result = ProcessBookResult.FailedRetry,
|
|
Status = ProcessBookStatus.Failed,
|
|
},
|
|
new ProcessBookViewModel(trialBook, config)
|
|
{
|
|
Result = ProcessBookResult.ValidationFail,
|
|
Status = ProcessBookStatus.Failed,
|
|
},
|
|
new ProcessBookViewModel(trialBook, config)
|
|
{
|
|
Result = ProcessBookResult.Cancelled,
|
|
Status = ProcessBookStatus.Cancelled,
|
|
},
|
|
new ProcessBookViewModel(trialBook, config)
|
|
{
|
|
Result = ProcessBookResult.Success,
|
|
Status = ProcessBookStatus.Completed,
|
|
},
|
|
new ProcessBookViewModel(trialBook, config)
|
|
{
|
|
Result = ProcessBookResult.None,
|
|
Status = ProcessBookStatus.Working,
|
|
},
|
|
new ProcessBookViewModel(trialBook, config)
|
|
{
|
|
Result = ProcessBookResult.None,
|
|
Status = ProcessBookStatus.Queued,
|
|
},
|
|
};
|
|
|
|
vm.Queue.Enqueue(testList);
|
|
|
|
// Six completed, one active, one still queued - the mix this preview exists to show.
|
|
// Driven through the same calls the queue loop makes, rather than the sequential
|
|
// MoveNext() that used to live here for this one caller.
|
|
for (int i = 0; i < 6 && vm.Queue.TryDequeueNext(out var finished); i++)
|
|
vm.Queue.MarkCompleted(finished);
|
|
vm.Queue.TryDequeueNext(out _);
|
|
return;
|
|
}
|
|
#endif
|
|
#endregion
|
|
}
|
|
|
|
#region Auto-Scroll Current Item Into View
|
|
protected override void OnDataContextBeginUpdate()
|
|
{
|
|
if (DataContext is ProcessQueueViewModel vm)
|
|
{
|
|
vm.ProcessStart -= Book_ProcessStart;
|
|
}
|
|
base.OnDataContextBeginUpdate();
|
|
}
|
|
|
|
protected override void OnDataContextEndUpdate()
|
|
{
|
|
if (DataContext is ProcessQueueViewModel vm)
|
|
{
|
|
vm.ProcessStart += Book_ProcessStart;
|
|
}
|
|
base.OnDataContextEndUpdate();
|
|
}
|
|
|
|
private void Book_ProcessStart(object? sender, ProcessBookViewModel e)
|
|
{
|
|
// The checkbox gates the same logic in both UIs, so auto-scroll means one thing.
|
|
if (sender is ProcessQueueViewModel vm && !vm.AutoScrollQueue) return;
|
|
|
|
Dispatcher.UIThread.Invoke(() =>
|
|
{
|
|
if (Queue?.IndexOf(e) is int newtBookIndex && newtBookIndex > 0 && QueueListControl.Presenter?.Panel is VirtualizingStackPanel panel && itemIsVisible(newtBookIndex - 1, panel))
|
|
{
|
|
// Only scroll the new item into view if the previous item is visible.
|
|
// This allows users to scroll through the queue without being interrupted.
|
|
QueueListControl.ScrollIntoView(newtBookIndex);
|
|
}
|
|
});
|
|
|
|
static bool itemIsVisible(int newtBookIndex, VirtualizingStackPanel panel)
|
|
=> panel.FirstRealizedIndex <= newtBookIndex && panel.LastRealizedIndex >= newtBookIndex;
|
|
}
|
|
#endregion
|
|
|
|
public void NumericUpDown_KeyDown(object sender, Avalonia.Input.KeyEventArgs e)
|
|
{
|
|
if (e.Key == Avalonia.Input.Key.Enter && sender is Avalonia.Input.IInputElement input) input.Focus();
|
|
}
|
|
|
|
#region Control event handlers
|
|
|
|
private async void ProcessBookControl2_CancelButtonClicked(ProcessBookViewModel? item)
|
|
{
|
|
if (item is not null)
|
|
{
|
|
await item.CancelAsync();
|
|
Queue?.RemoveQueued(item);
|
|
}
|
|
}
|
|
|
|
private void ProcessBookControl2_ButtonClicked(ProcessBookViewModel? item, QueuePosition queueButton)
|
|
{
|
|
if (item is not null)
|
|
Queue?.MoveQueuePosition(item, queueButton);
|
|
}
|
|
|
|
public async void CancelAllBtn_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
|
{
|
|
// Cancels every active book, not just the first one - with parallel downloads
|
|
// there can be several running at once.
|
|
if (_viewModel is ProcessQueueViewModel vm)
|
|
await vm.CancelAllAsync();
|
|
}
|
|
|
|
public void ClearFinishedBtn_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
|
{
|
|
Queue?.ClearCompleted();
|
|
|
|
if (_viewModel?.Running is false)
|
|
_viewModel.RunningTime = string.Empty;
|
|
}
|
|
|
|
public void ClearLogBtn_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
|
{
|
|
_viewModel?.LogEntries.Clear();
|
|
}
|
|
|
|
private async void LogCopyBtn_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
|
{
|
|
if (_viewModel is ProcessQueueViewModel vm)
|
|
{
|
|
string logText = string.Join("\r\n", vm.LogEntries.Select(r => $"{r.LogDate.ToShortDateString()} {r.LogDate.ToShortTimeString()}\t{r.LogMessage}"));
|
|
if (App.MainWindow?.Clipboard?.SetTextAsync(logText) is Task setter)
|
|
await setter;
|
|
}
|
|
}
|
|
|
|
private async void cancelAllBtn_Click(object? sender, EventArgs e)
|
|
{
|
|
if (_viewModel is ProcessQueueViewModel vm)
|
|
await vm.CancelAllAsync();
|
|
}
|
|
|
|
private void btnClearFinished_Click(object? sender, EventArgs e)
|
|
{
|
|
Queue?.ClearCompleted();
|
|
|
|
if (_viewModel?.Running is false)
|
|
_viewModel.RunningTime = string.Empty;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
public class DecimalConverter : IValueConverter
|
|
{
|
|
public static readonly DecimalConverter Instance = new();
|
|
|
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
|
{
|
|
if (value is string sourceText && targetType.IsAssignableTo(typeof(decimal?)))
|
|
{
|
|
if (sourceText == "∞") return 0;
|
|
|
|
for (int i = sourceText.Length; i > 0; i--)
|
|
{
|
|
if (decimal.TryParse(sourceText[..i], out var val))
|
|
return val;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
|
{
|
|
if (value is decimal val)
|
|
{
|
|
return
|
|
val == 0 ? "∞"
|
|
: (
|
|
val >= 10 ? ((long)val).ToString()
|
|
: val >= 1 ? val.ToString("F1")
|
|
: val.ToString("F2")
|
|
) + " MB/s";
|
|
}
|
|
return value?.ToString();
|
|
}
|
|
}
|