mirror of
https://github.com/yaobiao131/downkyicore.git
synced 2025-08-10 00:52:31 +00:00
Merge pull request #249 from NLick47/fac_04
feat: 个人空间-历史记录 加入增量加载,支持无限滚动
This commit is contained in:
55
DownKyi/Commands/AsyncDelegateCommand.cs
Normal file
55
DownKyi/Commands/AsyncDelegateCommand.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
63
DownKyi/CustomAction/InfiniteScrollBehavior.cs
Normal file
63
DownKyi/CustomAction/InfiniteScrollBehavior.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user