Merge pull request #249 from NLick47/fac_04

feat: 个人空间-历史记录 加入增量加载,支持无限滚动
This commit is contained in:
yaobiao131
2025-03-23 19:30:38 +08:00
committed by GitHub
4 changed files with 238 additions and 118 deletions

View File

@@ -0,0 +1,55 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
namespace DownKyi.Commands;
public class AsyncDelegateCommand : ICommand
{
private readonly Func<object, CancellationToken, Task> _execute;
private readonly Func<object, bool> _canExecute;
private CancellationTokenSource _cancellationTokenSource;
private bool _isExecuting;
public event EventHandler CanExecuteChanged;
public AsyncDelegateCommand(Func<object, CancellationToken, Task> execute, Func<object, bool> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return !_isExecuting && (_canExecute?.Invoke(parameter) ?? true);
}
public async void Execute(object parameter)
{
_isExecuting = true;
RaiseCanExecuteChanged();
_cancellationTokenSource = new CancellationTokenSource();
try
{
await _execute(parameter, _cancellationTokenSource.Token);
}
finally
{
_isExecuting = false;
RaiseCanExecuteChanged();
}
}
public void Cancel()
{
_cancellationTokenSource?.Cancel();
}
protected void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}

View File

@@ -0,0 +1,63 @@
using Avalonia.Controls;
using System.Windows.Input;
using Avalonia;
using Avalonia.Threading;
using Avalonia.Xaml.Interactivity;
namespace DownKyi.CustomAction;
public class InfiniteScrollBehavior : Behavior<ListBox>
{
private bool _isExecuting;
public static readonly StyledProperty<ICommand?> LoadMoreCommandProperty =
AvaloniaProperty.Register<InfiniteScrollBehavior, ICommand?>(
nameof(LoadMoreCommand));
public ICommand? LoadMoreCommand
{
get => GetValue(LoadMoreCommandProperty);
set => SetValue(LoadMoreCommandProperty, value);
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.AddHandler(
ScrollViewer.ScrollChangedEvent,
HandleScrollChanged);
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.RemoveHandler(ScrollViewer.ScrollChangedEvent, HandleScrollChanged);
}
private void HandleScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (_isExecuting || LoadMoreCommand == null)
return;
var scrollViewer = e.Source as ScrollViewer;
if (scrollViewer == null ||
scrollViewer.Offset.Y + scrollViewer.Viewport.Height < scrollViewer.Extent.Height - 50)
return;
_isExecuting = true;
try
{
if (LoadMoreCommand?.CanExecute(null) == true)
{
LoadMoreCommand.Execute(null);
}
}
finally
{
_isExecuting = false;
}
}
}

View File

@@ -1,9 +1,13 @@
using System.Collections;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DownKyi.Commands;
using DownKyi.Core.BiliApi.History;
using DownKyi.Core.BiliApi.History.Models;
using DownKyi.Core.BiliApi.VideoStream;
using DownKyi.Core.Utils;
using DownKyi.Events;
@@ -136,6 +140,8 @@ public class ViewMyHistoryViewModel : ViewModelBase
private DelegateCommand? _backSpaceCommand;
public DelegateCommand BackSpaceCommand => _backSpaceCommand ??= new DelegateCommand(ExecuteBackSpace);
/// <summary>
/// 返回事件
@@ -245,7 +251,30 @@ public class ViewMyHistoryViewModel : ViewModelBase
public DelegateCommand AddAllToDownloadCommand =>
_addAllToDownloadCommand ??= new DelegateCommand(ExecuteAddAllToDownloadCommand);
public AsyncDelegateCommand LoadMoreCommand => new (ExecuteLoadMoreCommand);
private long _nextMax = 0;
private long _nextViewAt = 0;
public async Task ExecuteLoadMoreCommand(object obj,CancellationToken token)
{
if(NoDataVisibility) return;
LoadingVisibility = true;
var result = await Task<HistoryData>.Run(() =>
{
return History.GetHistory(_nextMax, _nextViewAt, VideoNumberInPage);
});
if (result?.List?.Count > 0)
{
Medias.AddRange(result.List.Select(x => Convert(x,EventAggregator))
.Where(v => v != null && !string.IsNullOrEmpty(v.Title)).ToList());
_nextMax = result.Cursor.Max;
_nextViewAt = result.Cursor.ViewAt;
}
LoadingVisibility = false;
}
/// <summary>
/// 添加所有视频到下载列表事件
/// </summary>
@@ -327,127 +356,25 @@ public class ViewMyHistoryViewModel : ViewModelBase
await Task.Run(() =>
{
var cancellationToken = _tokenSource?.Token;
var historyList = History.GetHistory(0, 0, VideoNumberInPage);
if (historyList?.List == null || historyList.List.Count == 0)
if (historyList?.List?.Count > 0)
{
LoadingVisibility = false;
NoDataVisibility = true;
return;
}
foreach (var history in historyList.List)
{
if (history.History == null)
{
continue;
}
if (history.History.Business != "archive" && history.History.Business != "pgc")
{
continue;
}
// 播放url
var url = history.History.Business switch
{
"archive" => "https://www.bilibili.com/video/" + history.History.Bvid,
"pgc" => history.Uri,
_ => "https://www.bilibili.com"
};
// 查询、保存封面
var coverUrl = history.Cover;
if (!coverUrl.ToLower().StartsWith("http"))
{
coverUrl = $"https:{history.Cover}";
}
// 获取用户头像
var upName = history.AuthorFace != null ? history.AuthorName : "";
// 观看平台
var platform = history.History.Dt switch
{
1 or 3 or 5 or 7 =>
// 手机端
NormalIcon.Instance().PlatformMobile,
2 =>
// web端
NormalIcon.Instance().PlatformPC,
4 or 6 =>
// pad端
NormalIcon.Instance().PlatformIpad,
33 =>
// TV端
NormalIcon.Instance().PlatformTV,
_ => null
};
// 是否显示Partdesc
var partdescVisibility = history.NewDesc != "";
// 是否显示UP主信息和分区信息
var upAndTagVisibility = history.History.Business == "archive";
App.PropertyChangeAsync(() =>
{
// 观看进度
// -1 已看完
// 0 刚开始
// >0 看到 progress
string progress;
if (history.Progress == -1)
{
progress = DictionaryResource.GetString("HistoryFinished");
}
else if (history.Progress == 0)
{
progress = DictionaryResource.GetString("HistoryStarted");
}
else
{
progress = DictionaryResource.GetString("HistoryWatch") + " " +
Format.FormatDuration3(history.Progress);
}
var media = new HistoryMedia(EventAggregator)
{
Business = history.History.Business,
Bvid = history.History.Bvid,
Url = url,
UpMid = history.AuthorMid,
Cover = coverUrl ?? "avares://DownKyi/Resources/video-placeholder.png",
Title = history.Title,
SubTitle = history.ShowTitle,
Duration = history.Duration,
TagName = history.TagName,
Partdesc = history.NewDesc,
Progress = progress,
Platform = platform,
UpName = upName,
UpHeader = history.AuthorFace ?? "",
PartdescVisibility = partdescVisibility,
UpAndTagVisibility = upAndTagVisibility,
};
Medias.Add(media);
ContentVisibility = true;
LoadingVisibility = false;
NoDataVisibility = false;
});
// 判断是否该结束线程若为true跳出循环
if (cancellationToken?.IsCancellationRequested == true)
{
break;
}
}
}, (_tokenSource = new CancellationTokenSource()).Token);
else
{
App.PropertyChangeAsync(() =>
{
LoadingVisibility = false;
NoDataVisibility = true;
});
}
});
}
/// <summary>
@@ -466,6 +393,8 @@ public class ViewMyHistoryViewModel : ViewModelBase
LoadingVisibility = false;
NoDataVisibility = false;
_nextMax = 0;
_nextViewAt = 0;
Medias.Clear();
IsSelectAll = false;
}
@@ -502,4 +431,68 @@ public class ViewMyHistoryViewModel : ViewModelBase
UpdateHistoryMediaList();
}
private static bool IsValidBusiness(string business)
=> business is "archive" or "pgc";
private static string BuildMediaUrl(HistoryList history) =>
history.History.Business switch
{
"archive" => $"https://www.bilibili.com/video/{history.History.Bvid}",
"pgc" => history.Uri,
_ => "https://www.bilibili.com"
};
private static string ProcessCoverUrl(string originalUrl) =>
!string.IsNullOrEmpty(originalUrl) && !originalUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase)
? $"https:{originalUrl}"
: originalUrl;
private static VectorImage? GetPlatformIcon(int dt) =>
dt switch
{
1 or 3 or 5 or 7 => NormalIcon.Instance().PlatformMobile,
2 => NormalIcon.Instance().PlatformPC,
4 or 6 => NormalIcon.Instance().PlatformIpad,
33 => NormalIcon.Instance().PlatformTV,
_ => null
};
private static string BuildProgressText(long progress) =>
progress switch
{
-1 => DictionaryResource.GetString("HistoryFinished"),
0 => DictionaryResource.GetString("HistoryStarted"),
_ => $"{DictionaryResource.GetString("HistoryWatch")} {Format.FormatDuration3(progress)}"
};
public static HistoryMedia? Convert(HistoryList history, IEventAggregator eventAggregator)
{
if (history?.History == null || !IsValidBusiness(history.History.Business))
return null;
var url = BuildMediaUrl(history);
var coverUrl = ProcessCoverUrl(history.Cover);
var platform = GetPlatformIcon(history.History.Dt);
return new HistoryMedia(eventAggregator)
{
Business = history.History.Business,
Bvid = history.History.Bvid,
Url = url,
UpMid = history.AuthorMid,
Cover = coverUrl ?? "avares://DownKyi/Resources/video-placeholder.png",
Title = history.Title,
SubTitle = history.ShowTitle,
Duration = history.Duration,
TagName = history.TagName,
Partdesc = history.NewDesc,
Progress = BuildProgressText(history.Progress),
Platform = platform,
UpName = history.AuthorFace != null ? history.AuthorName : "",
UpHeader = history.AuthorFace ?? "",
PartdescVisibility = !string.IsNullOrEmpty(history.NewDesc),
UpAndTagVisibility = history.History.Business == "archive"
};
}
}

View File

@@ -9,7 +9,8 @@
x:DataType="vm:ViewMyHistoryViewModel"
xmlns:i="using:Avalonia.Xaml.Interactivity"
xmlns:ia="clr-namespace:Avalonia.Xaml.Interactions.Core;assembly=Avalonia.Xaml.Interactions"
xmlns:asyncImageLoader="clr-namespace:DownKyi.CustomControl.AsyncImageLoader">
xmlns:asyncImageLoader="clr-namespace:DownKyi.CustomControl.AsyncImageLoader"
xmlns:customAction="clr-namespace:DownKyi.CustomAction">
<UserControl.Resources>
<ControlTheme x:Key="MediaListStyle" TargetType="{x:Type ListBoxItem}" x:DataType="vmp:HistoryMedia">
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
@@ -41,7 +42,8 @@
CornerRadius="5">
<Border.Background>
<!-- 长宽比1.6 -->
<ImageBrush asyncImageLoader:ImageBrushLoader.Source="{Binding Cover}" Stretch="UniformToFill" />
<ImageBrush asyncImageLoader:ImageBrushLoader.Source="{Binding Cover}"
Stretch="UniformToFill" />
</Border.Background>
</Border>
@@ -144,7 +146,8 @@
VerticalAlignment="Center"
CornerRadius="12">
<Border.Background>
<ImageBrush asyncImageLoader:ImageBrushLoader.Source="{Binding UpHeader}" />
<ImageBrush
asyncImageLoader:ImageBrushLoader.Source="{Binding UpHeader}" />
</Border.Background>
</Border>
<TextBlock
@@ -243,14 +246,20 @@
<Grid Grid.Row="2" IsVisible="{Binding ContentVisibility}" RowDefinitions="*,1,50">
<ListBox
ItemsSource="{Binding Medias}"
x:Name="NameMedias"
Grid.Row="0"
BorderThickness="0"
ItemsSource="{Binding Medias}"
ItemContainerTheme="{StaticResource MediaListStyle}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
SelectionMode="Multiple">
<i:Interaction.Behaviors>
<customAction:InfiniteScrollBehavior
LoadMoreCommand="{Binding LoadMoreCommand}">
</customAction:InfiniteScrollBehavior>
<ia:EventTriggerBehavior EventName="SelectionChanged">
<ia:InvokeCommandAction Command="{Binding MediasCommand}"
CommandParameter="{Binding ElementName=NameMedias, Path=SelectedItems}" />
@@ -258,7 +267,7 @@
</i:Interaction.Behaviors>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.Theme>