Review #1885: Avalonia parity, and cap concurrency by processor count

Chardonnay had parallel downloads with no way to configure them, since
the queue logic lives in shared UI code but each UI supplies its own
controls. Adds the Auto-scroll toggle and the 'At once' spinner to
Chardonnay's queue panel, bound to the same view model properties the
WinForms panel uses.

Also uses Environment.ProcessorCount as the spinner's ceiling rather
than its default: min(ProcessorCount, 10). Downloading is bound by
Audible's license throttling rather than local CPU, so core count says
nothing about how many concurrent downloads will succeed - it only
bounds how many decrypts can usefully run at once. The default stays 3.

Spinner bounds are bound rather than hardcoded, so the two UIs cannot
drift apart.
This commit is contained in:
Allamagoosa committed 2026-08-16 14:08:49 -07:00
1 parent 2db63a8edd
commit 986dda5eaa
4 files changed
+46 -12

No files matched your search

@@ -50,9 +50,32 @@
</ItemsControl>
</ScrollViewer>
</Border>
<Grid Grid.Column="0" Grid.Row="1" ColumnDefinitions="*,Auto,Auto">
<Grid Grid.Column="0" Grid.Row="1" ColumnDefinitions="*,Auto,Auto,Auto">
<Button Name="cancelAllBtn" Grid.Column="0" FontSize="12" HorizontalAlignment="Left" Click="CancelAllBtn_Click">Cancel All</Button>
<StackPanel Orientation="Horizontal" Grid.Column="1" Margin="0,0,10,0" >
<StackPanel Orientation="Horizontal" Grid.Column="1" Margin="0,0,10,0" VerticalAlignment="Center">
<CheckBox
FontSize="12"
Margin="0,0,10,0"
VerticalAlignment="Center"
IsChecked="{Binding AutoScrollQueue, Mode=TwoWay}"
ToolTip.Tip="Keep active downloads scrolled into view."
Content="Auto-scroll" />
<TextBlock Margin="0,0,6,0" FontSize="11" Text="At&#xA;once" VerticalAlignment="Center" />
<NumericUpDown
Classes="SmallNumericUpDown"
MinWidth="90"
FontSize="12"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Height="{Binding #cancelAllBtn.DesiredSize.Height}"
ToolTip.Tip="How many books to download and decrypt at the same time. 1 downloads one at a time."
Value="{Binding MaxConcurrentDownloads, Mode=TwoWay}"
Minimum="{Binding MinConcurrentDownloads}"
Maximum="{Binding MaxAllowedConcurrentDownloads}"
Increment="1"
FormatString="0" />
</StackPanel>
<StackPanel Orientation="Horizontal" Grid.Column="2" Margin="0,0,10,0" >
<StackPanel.Styles>
<Style Selector="NumericUpDown#PART_Spinner">
<Setter Property="Background" Value="Black"/>
@@ -73,7 +96,7 @@
Increment="{Binding SpeedLimitIncrement}"
Maximum="999" />
</StackPanel>
<Button Grid.Column="2" FontSize="12" HorizontalAlignment="Right" Click="ClearFinishedBtn_Click">Clear Finished</Button>
<Button Grid.Column="3" FontSize="12" HorizontalAlignment="Right" Click="ClearFinishedBtn_Click">Clear Finished</Button>
</Grid>
</Grid>
</TabItem>
@@ -422,15 +422,26 @@ public partial class Configuration
public const int MinConcurrentDownloads = 1;
/// <summary>
/// The highest <see cref="MaxConcurrentDownloads"/> can go. Audible throttles license
/// requests, so more concurrency stops helping well before this and starts producing
/// license denials instead.
/// Hard ceiling on <see cref="MaxConcurrentDownloads"/>. Audible throttles license requests,
/// so concurrency stops helping well before this and starts producing license denials instead.
/// </summary>
public const int MaxAllowedConcurrentDownloads = 10;
public const int ConcurrentDownloadsHardLimit = 10;
/// <summary>Deliberately conservative. See <see cref="MaxAllowedConcurrentDownloads"/>.</summary>
/// <summary>Deliberately conservative. See <see cref="ConcurrentDownloadsHardLimit"/>.</summary>
public const int DefaultConcurrentDownloads = 3;
/// <summary>
/// The highest <see cref="MaxConcurrentDownloads"/> can go on this machine.
/// </summary>
/// <remarks>
/// Processor count is deliberately <em>not</em> the default. Downloading is bound by Audible's
/// license throttling rather than by local CPU, so core count says nothing about how many
/// concurrent downloads will succeed - it only bounds how many decrypts can usefully run at
/// once. It is therefore used as a ceiling and nothing more.
/// </remarks>
public static int MaxAllowedConcurrentDownloads
=> Math.Clamp(Environment.ProcessorCount, MinConcurrentDownloads, ConcurrentDownloadsHardLimit);
[Description("Maximum number of books to download and decrypt simultaneously. Set to 1 to download one book at a time.")]
public int MaxConcurrentDownloads
{
@@ -66,8 +66,8 @@ public class ProcessQueueViewModel : ReactiveObject
public bool AutoScrollQueue { get => field; set { RaiseAndSetIfChanged(ref field, value); Configuration.Instance.AutoScrollQueue = value; } }
/// <summary>Exposed so UI controls can bind their spinner bounds rather than hardcoding them.</summary>
public static int MinConcurrentDownloads => Configuration.MinConcurrentDownloads;
public static int MaxAllowedConcurrentDownloads => Configuration.MaxAllowedConcurrentDownloads;
public int MinConcurrentDownloads => Configuration.MinConcurrentDownloads;
public int MaxAllowedConcurrentDownloads => Configuration.MaxAllowedConcurrentDownloads;
public string? RunningTime { get => field; set => RaiseAndSetIfChanged(ref field, value); }
public bool ProgressBarVisible { get => field; set => RaiseAndSetIfChanged(ref field, value); }
public bool AnyCompleted => CompletedCount > 0;
@@ -40,8 +40,8 @@ internal partial class ProcessQueueControl : UserControl
ViewModel.ProcessStart += Book_ProcessStart;
autoScrollChk.CheckedChanged += (s, e) => ViewModel.AutoScrollQueue = autoScrollChk.Checked;
concurrencyNum.Minimum = ProcessQueueViewModel.MinConcurrentDownloads;
concurrencyNum.Maximum = ProcessQueueViewModel.MaxAllowedConcurrentDownloads;
concurrencyNum.Minimum = ViewModel.MinConcurrentDownloads;
concurrencyNum.Maximum = ViewModel.MaxAllowedConcurrentDownloads;
concurrencyNum.ValueChanged += (s, e) => ViewModel.MaxConcurrentDownloads = (int)concurrencyNum.Value;
ProcessQueue_PropertyChanged(this, new PropertyChangedEventArgs(null));
}