diff --git a/DownKyi/Commands/AsyncDelegateCommand.cs b/DownKyi/Commands/AsyncDelegateCommand.cs new file mode 100644 index 0000000..895a005 --- /dev/null +++ b/DownKyi/Commands/AsyncDelegateCommand.cs @@ -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 _execute; + private readonly Func _canExecute; + private CancellationTokenSource _cancellationTokenSource; + private bool _isExecuting; + + public event EventHandler CanExecuteChanged; + + public AsyncDelegateCommand(Func execute, Func 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); + } +} \ No newline at end of file diff --git a/DownKyi/CustomAction/InfiniteScrollBehavior.cs b/DownKyi/CustomAction/InfiniteScrollBehavior.cs new file mode 100644 index 0000000..7d70bcf --- /dev/null +++ b/DownKyi/CustomAction/InfiniteScrollBehavior.cs @@ -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 +{ + private bool _isExecuting; + + public static readonly StyledProperty LoadMoreCommandProperty = + AvaloniaProperty.Register( + 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; + } + } + +} \ No newline at end of file diff --git a/DownKyi/ViewModels/ViewMyHistoryViewModel.cs b/DownKyi/ViewModels/ViewMyHistoryViewModel.cs index ad6392c..16f70cf 100644 --- a/DownKyi/ViewModels/ViewMyHistoryViewModel.cs +++ b/DownKyi/ViewModels/ViewMyHistoryViewModel.cs @@ -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); + + /// /// 返回事件 @@ -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.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; + } /// /// 添加所有视频到下载列表事件 /// @@ -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; + }); + } + }); } /// @@ -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" + }; + } } \ No newline at end of file diff --git a/DownKyi/Views/ViewMyHistory.axaml b/DownKyi/Views/ViewMyHistory.axaml index dde5420..23a9e4b 100644 --- a/DownKyi/Views/ViewMyHistory.axaml +++ b/DownKyi/Views/ViewMyHistory.axaml @@ -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"> @@ -41,7 +42,8 @@ CornerRadius="5"> - + @@ -144,7 +146,8 @@ VerticalAlignment="Center" CornerRadius="12"> - + + + + + @@ -258,7 +267,7 @@ - +