mirror of
https://github.com/yaobiao131/downkyicore.git
synced 2025-08-10 00:52:31 +00:00
feat: 个人空间-历史记录 加入增量加载,支持无限滚动
This commit is contained in:
61
DownKyi/CustomAction/IncrementalLoadingBehavior.cs
Normal file
61
DownKyi/CustomAction/IncrementalLoadingBehavior.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Xaml.Interactivity;
|
||||
|
||||
namespace DownKyi.CustomAction;
|
||||
|
||||
public class IncrementalLoadingBehavior<T>: Behavior<ListBox>
|
||||
{
|
||||
|
||||
public static readonly StyledProperty<Func<int, Task<T[]>>> LoadPageFuncProperty =
|
||||
AvaloniaProperty.Register<IncrementalLoadingBehavior<T>, Func<int, Task<T[]>>>(nameof(LoadPageFunc));
|
||||
|
||||
private int currentPage = 1;
|
||||
private bool isLoading = false;
|
||||
public Func<int, Task<T[]>> LoadPageFunc
|
||||
{
|
||||
get => GetValue(LoadPageFuncProperty);
|
||||
set => SetValue(LoadPageFuncProperty, value);
|
||||
}
|
||||
|
||||
protected override void OnAttached()
|
||||
{
|
||||
base.OnAttached();
|
||||
AssociatedObject.AddHandler(ScrollViewer.ScrollChangedEvent, OnScrollChanged);
|
||||
LoadNextPageAsync();
|
||||
}
|
||||
|
||||
protected override void OnDetaching()
|
||||
{
|
||||
base.OnDetaching();
|
||||
AssociatedObject.RemoveHandler(ScrollViewer.ScrollChangedEvent, OnScrollChanged);
|
||||
}
|
||||
|
||||
private async void OnScrollChanged(object sender, ScrollChangedEventArgs e)
|
||||
{
|
||||
var scrollViewer = e.Source as ScrollViewer;
|
||||
if (scrollViewer == null) return;
|
||||
|
||||
if (scrollViewer.Offset.Y >= scrollViewer.Extent.Height - scrollViewer.Viewport.Height)
|
||||
{
|
||||
await LoadNextPageAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadNextPageAsync()
|
||||
{
|
||||
if (isLoading || LoadPageFunc == null ) return;
|
||||
|
||||
isLoading = true;
|
||||
var items = await LoadPageFunc(currentPage);
|
||||
foreach (var item in items)
|
||||
{
|
||||
AssociatedObject.Items.Add(item);
|
||||
}
|
||||
currentPage++;
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
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.Core.BiliApi.History;
|
||||
using DownKyi.Core.BiliApi.History.Models;
|
||||
using DownKyi.Core.BiliApi.VideoStream;
|
||||
using DownKyi.Core.Utils;
|
||||
using DownKyi.Events;
|
||||
@@ -246,6 +249,32 @@ public class ViewMyHistoryViewModel : ViewModelBase
|
||||
public DelegateCommand AddAllToDownloadCommand =>
|
||||
_addAllToDownloadCommand ??= new DelegateCommand(ExecuteAddAllToDownloadCommand);
|
||||
|
||||
|
||||
private long _nextMax = 0;
|
||||
|
||||
private long _nextViewAt = 0;
|
||||
|
||||
public Func<int, Task<HistoryMedia[]>> LoadPageFunc => (page) =>
|
||||
{
|
||||
int startIndex = (page - 1) * VideoNumberInPage;
|
||||
return Task.Run<HistoryMedia[]>(() =>
|
||||
{
|
||||
var result = History.GetHistory(_nextMax, _nextViewAt, VideoNumberInPage);
|
||||
foreach (var item in result.List)
|
||||
{
|
||||
var history = Convert(item, EventAggregator);
|
||||
if (history != null)
|
||||
{
|
||||
Medias.Add(history);
|
||||
}
|
||||
}
|
||||
|
||||
_nextMax = result.Cursor.Max;
|
||||
_nextViewAt = result.Cursor.ViewAt;
|
||||
return Medias.Skip(startIndex).Take(VideoNumberInPage).ToArray();
|
||||
});
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 添加所有视频到下载列表事件
|
||||
/// </summary>
|
||||
@@ -327,8 +356,6 @@ 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)
|
||||
{
|
||||
@@ -336,118 +363,13 @@ public class ViewMyHistoryViewModel : ViewModelBase
|
||||
NoDataVisibility = true;
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var history in historyList.List)
|
||||
App.PropertyChangeAsync(() =>
|
||||
{
|
||||
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);
|
||||
ContentVisibility = true;
|
||||
LoadingVisibility = false;
|
||||
NoDataVisibility = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -502,4 +424,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"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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}" />
|
||||
@@ -246,11 +247,13 @@
|
||||
x:Name="NameMedias"
|
||||
Grid.Row="0"
|
||||
BorderThickness="0"
|
||||
ItemsSource="{Binding Medias}"
|
||||
ItemContainerTheme="{StaticResource MediaListStyle}"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
SelectionMode="Multiple">
|
||||
<i:Interaction.Behaviors>
|
||||
<customAction:IncrementalLoadingBehavior
|
||||
x:TypeArguments="vmp:HistoryMedia"
|
||||
LoadPageFunc="{Binding LoadPageFunc}"/>
|
||||
<ia:EventTriggerBehavior EventName="SelectionChanged">
|
||||
<ia:InvokeCommandAction Command="{Binding MediasCommand}"
|
||||
CommandParameter="{Binding ElementName=NameMedias, Path=SelectedItems}" />
|
||||
@@ -258,7 +261,7 @@
|
||||
</i:Interaction.Behaviors>
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel />
|
||||
<VirtualizingStackPanel />
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
<ListBox.Theme>
|
||||
|
||||
Reference in New Issue
Block a user