Files
Libation/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs
T
Cursor Agentandrmcrackan 818510d1b0 feat(limit): add opt-in daily download limit core, history store and enforcement
Records every successful audiobook download in the library database (a new
DownloadHistory table) and, when the user opts in, stops downloading once the
rolling 24 hour window is full.

The history lives in the database rather than a file under LibationFiles
because in Docker only the database is on a volume; a file there is discarded
on every container restart.

The limit is checked immediately before each book downloads, never at queueing
time, so a full queue stays full and the user can raise or disable the limit
mid-run. When nothing in the queue can proceed the queue pauses and re-checks
every 15 seconds, recomputing settings, history and clock from scratch, so a
queue left running for days drip-feeds itself as downloads age out. The CLI
never waits: it skips covered titles and reports a count.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-14 17:16:47 +00:00

235 lines
6.2 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);
vm.Queue.MoveNext();
vm.Queue.MoveNext();
vm.Queue.MoveNext();
vm.Queue.MoveNext();
vm.Queue.MoveNext();
vm.Queue.MoveNext();
vm.Queue.MoveNext();
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)
{
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)
{
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();
}
}