优化下载逻辑,提示下载进度 (#513)

* 修复颜色不更新bug

* 优化下载组逻辑,允许排队时暂停

* 更新信号灯控件

* 仅重试失败项
This commit is contained in:
Poker 2024-07-17 00:28:34 +08:00 committed by GitHub
parent 48c6c40b30
commit e10ef1a357
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 268 additions and 158 deletions

View File

@ -16,14 +16,14 @@
HorizontalAlignment="Left"
controls:DockPanel.Dock="Top"
FontWeight="SemiBold"
Foreground="{StaticResource TextSecondaryAccentColor}"
Foreground="{StaticResource TextFillColorSecondaryBrush}"
Style="{StaticResource BaseTextBlockStyle}"
Text="{x:Bind Title, Mode=OneWay}" />
<fluent:SymbolIcon
VerticalAlignment="Center"
controls:DockPanel.Dock="Left"
FontSize="{StaticResource SmallIconFontSize}"
Foreground="{StaticResource TextSecondaryAccentColor}"
Foreground="{StaticResource TextFillColorSecondaryBrush}"
Symbol="{x:Bind Symbol, Mode=OneWay}" />
<TextBlock
HorizontalAlignment="Center"

View File

@ -53,6 +53,8 @@ public static class C
public static bool IsNotZeroL(long value) => value is not 0;
public static Visibility IsNotZeroToVisibility(int value) => value is not 0 ? Visibility.Visible : Visibility.Collapsed;
public static Visibility IsNotZeroDToVisibility(double value) => value is not 0 ? Visibility.Visible : Visibility.Collapsed;
public static unsafe Color ToAlphaColor(uint color)

View File

@ -0,0 +1,19 @@
<controls:DockPanel
x:Class="Pixeval.Controls.DigitalSignalItem"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Pixeval.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
HorizontalSpacing="5"
mc:Ignorable="d">
<Ellipse
Width="7"
Height="7"
Fill="{x:Bind Fill, Mode=OneWay}" />
<TextBlock
Foreground="{StaticResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Text, Mode=OneWay}" />
</controls:DockPanel>

View File

@ -0,0 +1,11 @@
using Microsoft.UI.Xaml.Media;
using WinUI3Utilities.Attributes;
namespace Pixeval.Controls;
[DependencyProperty<string>("Text")]
[DependencyProperty<Brush>("Fill")]
public sealed partial class DigitalSignalItem
{
public DigitalSignalItem() => InitializeComponent();
}

View File

@ -30,7 +30,7 @@
Text="{x:Bind PersonNickname, Mode=OneWay}" />
<TextBlock
HorizontalAlignment="Center"
Foreground="{ThemeResource PixevalTipTextForeground}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind PersonName, Mode=OneWay}" />
</StackPanel>

View File

@ -16,14 +16,14 @@
<ItemGroup>
<PackageReference Include="CommunityToolkit.Labs.WinUI.Shimmer" Version="0.1.240517-build.1678" />
<PackageReference Include="FluentIcons.WinUI" Version="1.1.244" />
<PackageReference Include="FluentIcons.WinUI" Version="1.1.247" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.2.0" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.3233" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.5.240607001" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.5.240627000" />
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
<PackageReference Include="CommunityToolkit.WinUI.Triggers" Version="8.1.240606-rc" />
<PackageReference Include="CommunityToolkit.WinUI.Collections" Version="8.1.240606-rc" />
@ -49,4 +49,16 @@
<ItemGroup>
<PRIResource Include="Strings\*\*.resjson" />
</ItemGroup>
<ItemGroup>
<CustomAdditionalCompileInputs Remove="DigitalSignalItem.xaml" />
</ItemGroup>
<ItemGroup>
<None Remove="DigitalSignalItem.xaml" />
</ItemGroup>
<ItemGroup>
<Resource Remove="DigitalSignalItem.xaml" />
</ItemGroup>
</Project>

View File

@ -12,7 +12,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="WebApiClientCore" Version="2.1.2" PrivateAssets="True" />
<PackageReference Include="WebApiClientCore" Version="2.1.4" PrivateAssets="True" />
</ItemGroup>
<ItemGroup>

View File

@ -88,7 +88,7 @@ public partial record AppSettings() : IWindowSettings
/// The max download tasks that are allowed to run concurrently
/// </summary>
[SettingsEntry(Symbol.DeveloperBoardLightning, nameof(MaxDownloadConcurrencyLevelEntryHeader), nameof(MaxDownloadConcurrencyLevelEntryDescription))]
public int MaxDownloadTaskConcurrencyLevel { get; set; } = Environment.ProcessorCount / 2;
public int MaxDownloadTaskConcurrencyLevel { get; set; } = Environment.ProcessorCount / 4;
[SettingsEntry(Symbol.SaveEdit, nameof(DownloadWhenBookmarkedEntryHeader), nameof(DownloadWhenBookmarkedEntryDescription))]
public bool DownloadWhenBookmarked { get; set; }

View File

@ -69,7 +69,7 @@
<!-- TipTextColor -->
<TextBlock
controls:DockPanel.Dock="Bottom"
Foreground="{ThemeResource PixevalTipTextForeground}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind local:C.CultureDateTimeOffsetDateFormatter(ViewModel.PostDate, pixeval:AppSettings.CurrentCulture), Mode=OneWay}" />
</controls:DockPanel>

View File

@ -50,8 +50,25 @@
ShowError="{x:Bind ViewModel.IsError(ViewModel.DownloadTask.CurrentState), Mode=OneWay}"
ShowPaused="{x:Bind ViewModel.IsPaused(ViewModel.DownloadTask.CurrentState), Mode=OneWay}"
Value="{x:Bind ViewModel.DownloadTask.ProgressPercentage, Mode=OneWay}" />
<StackPanel
Orientation="Horizontal"
Spacing="5"
Visibility="{x:Bind ViewModel.IsGroup(ViewModel.DownloadTask), Mode=OneWay}">
<local:DigitalSignalItem
Fill="{x:Bind ViewModel.CurrentStateBrush(ViewModel.DownloadTask.CurrentState), Mode=OneWay}"
Text="{x:Bind ViewModel.DownloadTask.ActiveCount, Mode=OneWay}"
Visibility="{x:Bind local:C.IsNotZeroToVisibility(ViewModel.DownloadTask.ActiveCount), Mode=OneWay}" />
<local:DigitalSignalItem
Fill="{StaticResource SystemFillColorSuccessBrush}"
Text="{x:Bind ViewModel.DownloadTask.CompletedCount, Mode=OneWay}"
Visibility="{x:Bind local:C.IsNotZeroToVisibility(ViewModel.DownloadTask.CompletedCount), Mode=OneWay}" />
<local:DigitalSignalItem
Fill="{StaticResource SystemFillColorCriticalBrush}"
Text="{x:Bind ViewModel.DownloadTask.ErrorCount, Mode=OneWay}"
Visibility="{x:Bind local:C.IsNotZeroToVisibility(ViewModel.DownloadTask.ErrorCount), Mode=OneWay}" />
</StackPanel>
<TextBlock
Foreground="{ThemeResource PixevalTipTextForeground}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ViewModel.ProgressMessage(ViewModel.DownloadTask.CurrentState, ViewModel.DownloadTask.ProgressPercentage, ViewModel.DownloadTask), Mode=OneWay}"
TextTrimming="CharacterEllipsis"

View File

@ -23,10 +23,12 @@ using System.IO;
using Windows.Foundation;
using Windows.System;
using Microsoft.UI.Xaml;
using Pixeval.Download;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Documents;
using Pixeval.Util.UI;
using WinUI3Utilities;
using WinUI3Utilities.Attributes;
using Symbol = FluentIcons.Common.Symbol;
namespace Pixeval.Controls;
@ -47,24 +49,22 @@ public sealed partial class DownloadItem
private async void ActionButton_OnClicked(object sender, RoutedEventArgs e)
{
switch (ViewModel.DownloadTask.CurrentState)
switch (ViewModel.ActionButtonSymbol(ViewModel.DownloadTask.CurrentState))
{
case DownloadState.Queued:
case DownloadState.Pending:
case Symbol.Dismiss:
ViewModel.DownloadTask.Cancel();
break;
case DownloadState.Running:
case Symbol.Pause:
ViewModel.DownloadTask.Pause();
break;
case DownloadState.Error:
case DownloadState.Cancelled:
case Symbol.ArrowRepeatAll:
ViewModel.DownloadTask.TryReset();
break;
case DownloadState.Completed:
case Symbol.Open:
if (!await Launcher.LaunchUriAsync(new Uri(ViewModel.DownloadTask.OpenLocalDestination)))
_ = await this.CreateAcknowledgementAsync(MiscResources.DownloadItemOpenFailed, MiscResources.DownloadItemMaybeDeleted);
break;
case DownloadState.Paused:
case Symbol.Play:
ViewModel.DownloadTask.TryResume();
break;
default:

View File

@ -23,6 +23,8 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using FluentIcons.Common;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Pixeval.Database;
using Pixeval.Download;
using Pixeval.Download.Models;
@ -87,20 +89,20 @@ public sealed partial class DownloadItemViewModel(IDownloadTaskGroup downloadTas
_ => ThrowHelper.ArgumentOutOfRange<DownloadState, string>(state)
};
public string ActionButtonContent(DownloadState state) => state switch
public string ActionButtonContent(DownloadState state) => ActionButtonSymbol(state) switch
{
DownloadState.Queued or DownloadState.Pending => DownloadItemResources.ActionDownloadCancelled,
DownloadState.Running => DownloadItemResources.ActionButtonContentPause,
DownloadState.Cancelled or DownloadState.Error => DownloadItemResources.ActionButtonContentRetry,
DownloadState.Completed => DownloadItemResources.ActionButtonContentOpen,
DownloadState.Paused => DownloadItemResources.ActionButtonContentResume,
Symbol.Dismiss => DownloadItemResources.ActionDownloadCancelled,
Symbol.Pause => DownloadItemResources.ActionButtonContentPause,
Symbol.ArrowRepeatAll => DownloadItemResources.ActionButtonContentRetry,
Symbol.Open => DownloadItemResources.ActionButtonContentOpen,
Symbol.Play => DownloadItemResources.ActionButtonContentResume,
_ => ThrowHelper.ArgumentOutOfRange<DownloadState, string>(state)
};
public Symbol ActionButtonSymbol(DownloadState state) => state switch
{
DownloadState.Queued or DownloadState.Pending => Symbol.Dismiss,
DownloadState.Running => Symbol.Pause,
DownloadState.Pending => Symbol.Dismiss,
DownloadState.Queued or DownloadState.Running => Symbol.Pause,
DownloadState.Cancelled or DownloadState.Error => Symbol.ArrowRepeatAll,
DownloadState.Completed => Symbol.Open,
DownloadState.Paused => Symbol.Play,
@ -119,6 +121,15 @@ public sealed partial class DownloadItemViewModel(IDownloadTaskGroup downloadTas
public bool IsPaused(DownloadState state) => state is DownloadState.Paused;
public Visibility IsGroup(IDownloadTaskGroup group) => C.ToVisibility(group is DownloadTaskGroup);
public Brush CurrentStateBrush(DownloadState state) => Application.Current.GetResource<Brush>(state switch
{
DownloadState.Paused => "SystemFillColorCautionBrush",
DownloadState.Cancelled => "SystemFillColorNeutralBrush",
_ => "SystemFillColorAttentionBrush"
});
#pragma warning restore CA1822
#region Not supported

View File

@ -45,10 +45,10 @@
VerticalAlignment="Center"
Content="{fluent:SymbolIcon Symbol=Guest,
FontSize=12}"
Foreground="{StaticResource TextSecondaryAccentColor}" />
Foreground="{StaticResource TextFillColorSecondaryBrush}" />
<TextBlock
VerticalAlignment="Center"
Foreground="{StaticResource TextSecondaryAccentColor}"
Foreground="{StaticResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionStrongTextBlockStyle}"
Text="{x:Bind ViewModel.UserId, Mode=OneWay}" />
<local:PixevalBadge

View File

@ -24,5 +24,5 @@
<controls:ColorPickerButton
ColorPickerStyle="{StaticResource ColorPickerStyle}"
Loaded="ColorPicker_OnLoaded"
SelectedColor="{x:Bind controls1:C.ToAlphaColor(Entry.Value), BindBack=controls1:C.ToAlphaUInt, Mode=TwoWay}" />
SelectedColor="{x:Bind controls1:C.ToAlphaColor(Entry.Value), BindBack=ColorBindBack, Mode=TwoWay}" />
</controls:SettingsCard>

View File

@ -1,3 +1,4 @@
using Windows.UI;
using CommunityToolkit.WinUI.Controls;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@ -22,4 +23,6 @@ public sealed partial class ColorSettingsCard
{
Entry.ValueChanged?.Invoke(Entry.Value);
}
private void ColorBindBack(Color color) => Entry.Value = C.ToAlphaUInt(color);
}

View File

@ -28,16 +28,15 @@ using System.Threading.Tasks;
using Pixeval.CoreApi.Net;
using Pixeval.Download.Models;
using Pixeval.Utilities.Threading;
using WinUI3Utilities;
namespace Pixeval.Download;
public partial class DownloadManager : IDisposable
{
/// <summary>
/// 正在下载的任务队列,数量不超过<see cref="ConcurrencyDegree"/>
/// 正在排队的任务队列
/// </summary>
private readonly Channel<IDownloadTaskBase> _downloadTaskChannel = Channel.CreateUnbounded<IDownloadTaskBase>();
private readonly Channel<DownloadToken> _downloadTaskChannel = Channel.CreateUnbounded<DownloadToken>();
/// <summary>
/// 使用<see cref="MakoApiKind.ImageApi"/>的<see cref="HttpClient"/>
@ -53,7 +52,7 @@ public partial class DownloadManager : IDisposable
private readonly ReenterableAwaiter<bool> _throttle = new(true, true);
/// <summary>
/// <see cref="_downloadTaskChannel"/>中的数量
/// 正在下载中的数量
/// </summary>
private int _workingTasks;
@ -93,16 +92,17 @@ public partial class DownloadManager : IDisposable
/// <remarks>
/// intrinsic download task are not counted
/// </remarks>
public void QueueTask(IDownloadTaskGroup task)
public void QueueTask(IDownloadTaskGroup taskGroup)
{
if (_taskQuerySet.TryGetValue(task.Destination, out var v) && v == task)
if (_taskQuerySet.TryGetValue(taskGroup.Destination, out var v) && v == taskGroup)
return;
_taskQuerySet[task.Destination] = task;
_taskQuerySet[taskGroup.Destination] = taskGroup;
if (v is not null)
_ = QueuedTasks.Remove(v);
QueuedTasks.Insert(0, task);
_ = _downloadTaskChannel.Writer.TryWrite(task);
QueuedTasks.Insert(0, taskGroup);
taskGroup.SubscribeProgress(_downloadTaskChannel.Writer);
_ = _downloadTaskChannel.Writer.TryWrite(taskGroup.GetToken());
}
/// <summary>
@ -111,40 +111,30 @@ public partial class DownloadManager : IDisposable
private async void PollTask()
{
while (await _downloadTaskChannel.Reader.WaitToReadAsync())
while (await _throttle && _downloadTaskChannel.Reader.TryRead(out var queuedTask))
switch (queuedTask)
{
case ImageDownloadTask imageTask:
imageTask.DownloadTryResume += t => _downloadTaskChannel.Writer.TryWrite(t);
imageTask.DownloadTryReset += t => _downloadTaskChannel.Writer.TryWrite(t);
// 子任务入列时一定是处于Queued状态需要直接运行的
_ = DownloadAsync(imageTask);
break;
case IDownloadTaskGroup taskGroup:
await taskGroup.InitializeTaskGroupAsync();
foreach (var subTask in taskGroup)
{
subTask.DownloadTryResume += t => _downloadTaskChannel.Writer.TryWrite(t);
subTask.DownloadTryReset += t => _downloadTaskChannel.Writer.TryWrite(t);
// 任务组中的任务需要判断是否处于Queued状态才能运行
if (subTask.CurrentState is DownloadState.Queued)
{
_ = DownloadAsync(subTask);
_ = await _throttle;
}
}
break;
}
while (await _throttle
&& _downloadTaskChannel.Reader.TryRead(out var taskToken)
&& taskToken is { Token.IsCancellationRequested: false, Task: var taskGroup })
{
await taskGroup.InitializeTaskGroupAsync();
foreach (var subTask in taskGroup)
// 需要判断是否处于Queued状态才能运行
if (subTask.CurrentState is DownloadState.Queued)
{
await DownloadAsync(subTask);
_ = await _throttle;
}
}
}
/// <summary>
/// 清除指定任务
/// </summary>
/// <param name="task"></param>
public void RemoveTask(IDownloadTaskGroup task)
/// <param name="taskGroup"></param>
public void RemoveTask(IDownloadTaskGroup taskGroup)
{
_ = _taskQuerySet.Remove(task.Destination);
_ = QueuedTasks.Remove(task);
taskGroup.Cancel();
_ = _taskQuerySet.Remove(taskGroup.Destination);
_ = QueuedTasks.Remove(taskGroup);
}
/// <summary>
@ -152,9 +142,8 @@ public partial class DownloadManager : IDisposable
/// </summary>
public void ClearTasks()
{
// foreach (var task in QueuedTasks)
// await task.CancelAsync();
foreach (var task in QueuedTasks)
task.Cancel();
QueuedTasks.Clear();
_taskQuerySet.Clear();
}
@ -165,11 +154,12 @@ public partial class DownloadManager : IDisposable
/// <remarks>
/// Execute the task only if it's already queued
/// </remarks>
public bool TryExecuteTaskGroupInline(IDownloadTaskGroup task)
public bool TryExecuteTaskGroupInline(IDownloadTaskGroup taskGroup)
{
if (QueuedTasks.Contains(task) && task.CurrentState is DownloadState.Queued)
if (QueuedTasks.Contains(taskGroup) && taskGroup.CurrentState is DownloadState.Queued)
{
_ = _downloadTaskChannel.Writer.TryWrite(task);
taskGroup.SubscribeProgress(_downloadTaskChannel.Writer);
_ = _downloadTaskChannel.Writer.TryWrite(taskGroup.GetToken());
return true;
}
@ -179,8 +169,21 @@ public partial class DownloadManager : IDisposable
private async Task DownloadAsync(ImageDownloadTask task)
{
await IncrementCounterAsync();
await DownloadInternalAsync(task);
await DecrementCounterAsync();
_ = Download();
return;
async Task Download()
{
try
{
await task.StartAsync(_httpClient);
}
catch
{
// ignored
}
await DecrementCounterAsync();
}
}
private async Task IncrementCounterAsync()
@ -194,21 +197,4 @@ public partial class DownloadManager : IDisposable
_ = Interlocked.Decrement(ref _workingTasks);
await _throttle.SetResultAsync(true);
}
private async Task DownloadInternalAsync(ImageDownloadTask task)
{
try
{
await (task.CurrentState switch
{
DownloadState.Queued => task.StartAsync(_httpClient),
DownloadState.Paused => task.ResumeAsync(_httpClient),
_ => ThrowHelper.ArgumentOutOfRange<DownloadState, Task>(task.CurrentState)
});
}
catch
{
// ignored
}
}
}

View File

@ -25,6 +25,7 @@ using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.Extensions.DependencyInjection;
@ -32,7 +33,6 @@ using Pixeval.Controls.Windowing;
using Pixeval.CoreApi.Model;
using Pixeval.Database;
using Pixeval.Database.Managers;
using Pixeval.Util.UI;
using Pixeval.Utilities;
using WinUI3Utilities;
@ -58,6 +58,10 @@ public abstract partial class DownloadTaskGroup(DownloadHistoryEntry entry) : Ob
var g = sender.To<DownloadTaskGroup>();
if (e.PropertyName is not nameof(CurrentState))
return;
// 子任务状态变化时,不一定需要任务组状态变化,也会进入此分支
OnPropertyChanged(nameof(ActiveCount));
OnPropertyChanged(nameof(CompletedCount));
OnPropertyChanged(nameof(ErrorCount));
if (g.CurrentState is DownloadState.Running or DownloadState.Paused or DownloadState.Pending)
return;
g.DatabaseEntry.State = g.CurrentState;
@ -108,18 +112,23 @@ public abstract partial class DownloadTaskGroup(DownloadHistoryEntry entry) : Ob
if (CurrentState is not (DownloadState.Completed or DownloadState.Error or DownloadState.Cancelled))
return;
IsProcessing = true;
TasksSet.ForEach(t => t.TryReset());
(CurrentState is DownloadState.Error
? TasksSet.Where(t => t.CurrentState is DownloadState.Error)
: TasksSet)
.ForEach(t => t.TryReset());
if (CancellationTokenSource.IsCancellationRequested)
{
CancellationTokenSource.Dispose();
CancellationTokenSource = new();
}
DownloadTryReset?.Invoke(this);
IsProcessing = false;
}
public void Pause()
{
IsProcessing = true;
CancellationTokenSource.Cancel();
TasksSet.ForEach(t => t.Pause());
IsProcessing = false;
}
@ -127,7 +136,13 @@ public abstract partial class DownloadTaskGroup(DownloadHistoryEntry entry) : Ob
public void TryResume()
{
IsProcessing = true;
if (CancellationTokenSource.IsCancellationRequested)
{
CancellationTokenSource.Dispose();
CancellationTokenSource = new();
}
TasksSet.ForEach(t => t.TryResume());
DownloadTryResume?.Invoke(this);
IsProcessing = false;
}
@ -146,9 +161,8 @@ public abstract partial class DownloadTaskGroup(DownloadHistoryEntry entry) : Ob
public abstract void Delete();
/// <summary>
/// 用于<see cref="DownloadState.Pending"/>的取消令牌
/// </summary>
public DownloadToken GetToken() => new(this, CancellationTokenSource.Token);
private CancellationTokenSource CancellationTokenSource { get; set; } = new();
/// <summary>
@ -219,6 +233,10 @@ public abstract partial class DownloadTaskGroup(DownloadHistoryEntry entry) : Ob
public event Func<DownloadTaskGroup, CancellationToken, Task>? AfterAllDownloadAsync;
private event Action<DownloadTaskGroup>? DownloadTryResume;
private event Action<DownloadTaskGroup>? DownloadTryReset;
protected void AddToTasksSet(ImageDownloadTask task)
{
_tasksSet.Add(task);
@ -228,31 +246,24 @@ public abstract partial class DownloadTaskGroup(DownloadHistoryEntry entry) : Ob
task.DownloadErrorAsync += x => ItemDownloadErrorAsync?.Invoke(x) ?? Task.CompletedTask;
task.PropertyChanged += (_, e) =>
{
switch (e.PropertyName)
OnPropertyChanged(e.PropertyName switch
{
case nameof(ImageDownloadTask.ProgressPercentage):
{
var time = DateTime.Now;
if (time - _lastReportedTime < TimeSpan.FromMilliseconds(500))
return;
_lastReportedTime = time;
OnPropertyChanged(nameof(ProgressPercentage));
break;
}
case nameof(ImageDownloadTask.CurrentState):
OnPropertyChanged(nameof(CurrentState));
break;
case nameof(ImageDownloadTask.ErrorCause):
OnPropertyChanged(nameof(ErrorCause));
break;
}
nameof(ImageDownloadTask.ProgressPercentage) => nameof(ProgressPercentage),
nameof(ImageDownloadTask.CurrentState) => nameof(CurrentState),
nameof(ImageDownloadTask.ErrorCause) => nameof(ErrorCause),
_ => ""
});
};
}
/// <summary>
/// 降低汇报频率
/// </summary>
private DateTime _lastReportedTime = DateTime.MinValue;
public void SubscribeProgress(ChannelWriter<DownloadToken> writer)
{
DownloadTryResume += OnDownloadWrite;
DownloadTryReset += OnDownloadWrite;
return;
void OnDownloadWrite(DownloadTaskGroup o) => writer.TryWrite(o.GetToken());
}
public Exception? ErrorCause => TasksSet.FirstOrDefault(t => t.ErrorCause is not null)?.ErrorCause;
@ -260,7 +271,20 @@ public abstract partial class DownloadTaskGroup(DownloadHistoryEntry entry) : Ob
public bool IsAnyError => TasksSet.Any(t => t.CurrentState is DownloadState.Error);
public double ProgressPercentage => TasksSet.Count is 0 ? 100 : TasksSet.Average(t => t.ProgressPercentage);
public int ActiveCount => TasksSet.Count(t => t.CurrentState is DownloadState.Queued or DownloadState.Running or DownloadState.Pending or DownloadState.Paused or DownloadState.Cancelled);
public int CompletedCount => TasksSet.Count(t => t.CurrentState is DownloadState.Completed);
public int ErrorCount => TasksSet.Count(t => t.CurrentState is DownloadState.Error);
public double ProgressPercentage =>
IsCreateFromEntry
? DatabaseEntry.State is DownloadState.Queued
? 0
: 100
: TasksSet.Count is 0
? 100
: TasksSet.Average(t => t.ProgressPercentage);
public IEnumerator<ImageDownloadTask> GetEnumerator() => TasksSet.GetEnumerator();

View File

@ -23,6 +23,8 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using Pixeval.CoreApi.Model;
using Pixeval.Database;
@ -34,4 +36,16 @@ public interface IDownloadTaskGroup : IDownloadTaskBase, IIdEntry, INotifyProper
DownloadHistoryEntry DatabaseEntry { get; }
ValueTask InitializeTaskGroupAsync();
void SubscribeProgress(ChannelWriter<DownloadToken> writer);
DownloadToken GetToken();
int ActiveCount { get; }
int CompletedCount { get; }
int ErrorCount { get; }
}
public readonly record struct DownloadToken(IDownloadTaskGroup Task, CancellationToken Token);

View File

@ -39,6 +39,8 @@ public partial class ImageDownloadTask : ObservableObject, IDownloadTaskBase, IP
Uri = uri;
Destination = destination;
CurrentState = initState;
if (initState is DownloadState.Completed or DownloadState.Cancelled or DownloadState.Error)
ProgressPercentage = 100;
DownloadStartedAsync += DownloadStartedAsyncOverride;
DownloadStoppedAsync += DownloadStoppedAsyncOverride;
DownloadErrorAsync += DownloadErrorAsyncOverride;
@ -66,13 +68,13 @@ public partial class ImageDownloadTask : ObservableObject, IDownloadTaskBase, IP
[ObservableProperty] private DownloadState _currentState;
[ObservableProperty] private double _progressPercentage = 100;
[ObservableProperty] private double _progressPercentage;
[ObservableProperty] private Exception? _errorCause;
[ObservableProperty] private bool _isProcessing;
private CancellationTokenSource CancellationTokenSource { get; set; } = new();
protected CancellationTokenSource CancellationTokenSource { get; private set; } = new();
private bool _isRunning;
@ -199,11 +201,12 @@ public partial class ImageDownloadTask : ObservableObject, IDownloadTaskBase, IP
public void Pause()
{
if (CurrentState is not DownloadState.Running)
if (CurrentState is not (DownloadState.Queued or DownloadState.Running))
return;
IsProcessing = true;
CancellationTokenSource.Cancel();
CurrentState = DownloadState.Paused;
DownloadPaused?.Invoke(this);
IsProcessing = false;
}
@ -222,11 +225,6 @@ public partial class ImageDownloadTask : ObservableObject, IDownloadTaskBase, IP
IsProcessing = false;
}
public async Task ResumeAsync(HttpClient httpClient)
{
await StartAsync(httpClient, true);
}
public void Cancel()
{
if (CurrentState is not (DownloadState.Paused or DownloadState.Pending or DownloadState.Running or DownloadState.Queued))
@ -234,6 +232,7 @@ public partial class ImageDownloadTask : ObservableObject, IDownloadTaskBase, IP
IsProcessing = true;
CancellationTokenSource.Cancel();
CurrentState = DownloadState.Cancelled;
DownloadCancelled?.Invoke(this);
IsProcessing = false;
}
@ -255,6 +254,10 @@ public partial class ImageDownloadTask : ObservableObject, IDownloadTaskBase, IP
public event Action<ImageDownloadTask>? DownloadTryReset;
public event Action<ImageDownloadTask>? DownloadPaused;
public event Action<ImageDownloadTask>? DownloadCancelled;
public event Func<ImageDownloadTask, CancellationToken, Task> AfterDownloadAsync;
void IProgress<double>.Report(double value) => ProgressPercentage = value;

View File

@ -85,7 +85,15 @@ public class MangaDownloadTaskGroup : DownloadTaskGroup, IImageDownloadTaskGroup
private IllustrationDownloadFormat IllustrationDownloadFormat { get; }
public override string OpenLocalDestination => Path.GetDirectoryName(TasksSet[0].Destination)!;
public override string OpenLocalDestination
{
get
{
if (TasksSet.Count is 0)
SetTasksSet();
return Path.GetDirectoryName(TasksSet[0].Destination)!;
}
}
public override void Delete()
{

View File

@ -24,6 +24,7 @@ using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Pixeval.CoreApi.Model;
@ -40,7 +41,11 @@ public class SingleImageDownloadTaskGroup : ImageDownloadTask, IImageDownloadTas
{
public DownloadHistoryEntry DatabaseEntry { get; }
public ValueTask InitializeTaskGroupAsync() => ValueTask.CompletedTask;
public ValueTask InitializeTaskGroupAsync()
{
SetNotCreateFromEntry();
return ValueTask.CompletedTask;
}
public Illustration Entry => DatabaseEntry.Entry.To<Illustration>();
@ -60,6 +65,8 @@ public class SingleImageDownloadTaskGroup : ImageDownloadTask, IImageDownloadTas
{
DatabaseEntry = entry;
CurrentState = entry.State;
if (entry.State is DownloadState.Completed or DownloadState.Cancelled or DownloadState.Error)
ProgressPercentage = 100;
IllustrationDownloadFormat = IoHelper.GetIllustrationFormat(Path.GetExtension(Destination));
}
@ -92,6 +99,23 @@ public class SingleImageDownloadTaskGroup : ImageDownloadTask, IImageDownloadTas
await TagsManager.SetTagsAsync(Destination, Entry, token);
}
public DownloadToken GetToken() => new(this, CancellationTokenSource.Token);
public int ActiveCount => CurrentState is DownloadState.Queued or DownloadState.Running or DownloadState.Pending or DownloadState.Paused or DownloadState.Cancelled ? 1 : 0;
public int CompletedCount => CurrentState is DownloadState.Completed ? 1 : 0;
public int ErrorCount => CurrentState is DownloadState.Error ? 1 : 0;
public void SubscribeProgress(ChannelWriter<DownloadToken> writer)
{
DownloadTryResume += OnDownloadWrite;
DownloadTryReset += OnDownloadWrite;
return;
void OnDownloadWrite(ImageDownloadTask o) => writer.TryWrite(o.To<SingleImageDownloadTaskGroup>().GetToken());
}
public int Count => 1;
public IEnumerator<ImageDownloadTask> GetEnumerator() => ((IReadOnlyList<ImageDownloadTask>)[this]).GetEnumerator();

View File

@ -30,7 +30,7 @@
TextWrapping="WrapWholeWords" />
<TextBlock
MaxHeight="15"
Foreground="{ThemeResource PixevalTipTextForeground}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind TranslatedName}"
TextTrimming="CharacterEllipsis"

View File

@ -37,25 +37,25 @@
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.1.240606-rc" />
<PackageReference Include="CommunityToolkit.Diagnostics" Version="8.2.2" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="FluentIcons.WinUI" Version="1.1.244" />
<PackageReference Include="FluentIcons.WinUI" Version="1.1.247" />
<PackageReference Include="GitVersion.MsBuild" Version="5.12.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="LiteDB" Version="5.0.20" />
<PackageReference Include="LiteDB" Version="5.0.21" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.3233" />
<PackageReference Condition="'$(PublishAot)' == 'true'" Include="Microsoft.Windows.CsWinRT" Version="2.1.0-prerelease.240602.1" />
<PackageReference Condition="'$(PublishAot)' == 'true'" Include="Microsoft.WindowsAppSDK" Version="1.6.240531000-experimental1" />
<PackageReference Condition="'$(PublishAot)' == 'false'" Include="Microsoft.WindowsAppSDK" Version="1.5.240607001" />
<PackageReference Condition="'$(PublishAot)' == 'false'" Include="Microsoft.WindowsAppSDK" Version="1.5.240627000" />
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
<PackageReference Include="PininSharp" Version="1.2.0" />
<!--<PackageReference Include="Pixeval.Bypass" Version="1.1.7" />-->
<PackageReference Include="Pixeval.QRCoder" Version="1.4.5" />
<PackageReference Include="QuestPDF" Version="2024.6.2" />
<PackageReference Include="ReverseMarkdown" Version="4.5.0" />
<PackageReference Include="QuestPDF" Version="2024.6.4" />
<PackageReference Include="ReverseMarkdown" Version="4.6.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.4" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.3" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />

View File

@ -12,12 +12,6 @@
TintOpacity="0.3" />
<SolidColorBrush x:Key="PixevalTranslucentBackgroundBrush" Color="#8FFFFFFF" />
<SolidColorBrush x:Key="PixevalPanelBackgroundThemeBrush" Color="#F2F2F2" />
<SolidColorBrush x:Key="PixevalBorderBrush" Color="#7C7B7B" />
<SolidColorBrush x:Key="PixevalTipTextForeground" Color="#999AA1" />
<SolidColorBrush x:Key="CardBackground" Color="#FAFCFD" />
<SolidColorBrush x:Key="CardStrokeBrush" Color="#0F000000" />
<SolidColorBrush x:Key="TextSecondaryAccentColor" Color="#404252" />
<SolidColorBrush x:Key="SecondaryAccentBrush" Color="#FAFAFA" />
<SolidColorBrush x:Key="SecondaryAccentBorderBrush" Color="#939393" />
<ui:Color x:Key="SecondaryAccentColor">#FAFAFA</ui:Color>
<ui:Color x:Key="ExpanderHeaderCardPointerOverBackgroundColor">#F6F6F6</ui:Color>
@ -36,12 +30,6 @@
TintOpacity="0.3" />
<SolidColorBrush x:Key="PixevalTranslucentBackgroundBrush" Color="#8FFFFFFF" />
<SolidColorBrush x:Key="PixevalPanelBackgroundThemeBrush" Color="#F2F2F2" />
<SolidColorBrush x:Key="PixevalBorderBrush" Color="#7C7B7B" />
<SolidColorBrush x:Key="PixevalTipTextForeground" Color="#999AA1" />
<SolidColorBrush x:Key="CardBackground" Color="#FAFCFD" />
<SolidColorBrush x:Key="CardStrokeBrush" Color="#0F000000" />
<SolidColorBrush x:Key="TextSecondaryAccentColor" Color="#404252" />
<SolidColorBrush x:Key="SecondaryAccentBrush" Color="#FAFAFA" />
<SolidColorBrush x:Key="SecondaryAccentBorderBrush" Color="#939393" />
<ui:Color x:Key="SecondaryAccentColor">#FAFAFA</ui:Color>
<ui:Color x:Key="ExpanderHeaderCardPointerOverBackgroundColor">#F6F6F6</ui:Color>
@ -60,13 +48,7 @@
TintOpacity="0.3" />
<SolidColorBrush x:Key="PixevalTranslucentBackgroundBrush" Color="#8F000000" />
<SolidColorBrush x:Key="PixevalPanelBackgroundThemeBrush" Color="#232323" />
<SolidColorBrush x:Key="PixevalBorderBrush" Color="#ACADAD" />
<SolidColorBrush x:Key="PixevalTipTextForeground" Color="#828492" />
<SolidColorBrush x:Key="CardBackground" Color="#2B2A2F" />
<SolidColorBrush x:Key="CardStrokeBrush" Color="#19000000" />
<SolidColorBrush x:Key="TextSecondaryAccentColor" Color="#D2D4DA" />
<SolidColorBrush x:Key="SecondaryAccentBorderBrush" Color="#141414" />
<SolidColorBrush x:Key="SecondaryAccentBrush" Color="#302C2D" />
<ui:Color x:Key="SecondaryAccentColor">#302C2D</ui:Color>
<ui:Color x:Key="ExpanderHeaderCardPointerOverBackgroundColor">#323232</ui:Color>
<ui:Color x:Key="ActionableCardPointerOverBackgroundColor">#323232</ui:Color>
@ -78,12 +60,6 @@
<ResourceDictionary x:Key="HighContrast">
<SolidColorBrush x:Key="PixevalAppAcrylicBrush" Color="{ThemeResource SystemColorWindowColor}" />
<SolidColorBrush x:Key="PixevalPanelBackgroundThemeBrush" Color="#323232" />
<SolidColorBrush x:Key="PixevalBorderBrush" Color="#ACADAD" />
<SolidColorBrush x:Key="PixevalTipTextForeground" Color="#828492" />
<SolidColorBrush x:Key="CardBackground" Color="{StaticResource SystemColorWindowColor}" />
<SolidColorBrush x:Key="CardStrokeBrush" Color="Red" />
<SolidColorBrush x:Key="TextSecondaryAccentColor" Color="#D2D4DA" />
<SolidColorBrush x:Key="SecondaryAccentBrush" Color="Cyan" />
<SolidColorBrush x:Key="SecondaryAccentBorderBrush" Color="Red" />
<ui:Color x:Key="SecondaryAccentColor">Cyan</ui:Color>
<ui:Color x:Key="ExpanderHeaderCardPointerOverBackgroundColor">#323232</ui:Color>