fix(web): 恢复视频继续播放和进度保存
- 将播放器收敛为单个共享实例,避免列表/网格内多个 video ref 导致进度保存失效 - 恢复视频播放器上方的继续播放提示 - 在播放、暂停、拖动、结束、切换页面和离开页面时保存播放位置 - 保留文件浏览分页和排序参数的前端调用
This commit is contained in:
@@ -2,6 +2,7 @@ using FileShare_Common.Core;
|
||||
using FileShare_Services.Core;
|
||||
using FileShare_Services.Services.FileLibrary;
|
||||
using FileShare_Services.Services.QrCode;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace FileShare_Services.Endpoints
|
||||
{
|
||||
@@ -119,12 +120,15 @@ namespace FileShare_Services.Endpoints
|
||||
private static async Task<object?> GetFileStreamAsync(ServiceEndpointContext ctx)
|
||||
{
|
||||
var sp = ctx.Items["ServiceProvider"] as IServiceProvider;
|
||||
var service = sp?.GetService(typeof(IFileStreamService)) as IFileStreamService;
|
||||
if (service is null) return null;
|
||||
if (sp is null) return null;
|
||||
|
||||
if (!int.TryParse(ctx.Query.GetValueOrDefault("id"), out var id) || id <= 0)
|
||||
return null;
|
||||
|
||||
using var scope = sp.CreateScope();
|
||||
var service = scope.ServiceProvider.GetService<IFileStreamService>();
|
||||
if (service is null) return null;
|
||||
|
||||
return await service.GetFileStreamAsync(id);
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ namespace FileShare_Services.Services.FileLibrary
|
||||
/// </summary>
|
||||
public sealed record SaveFileProgressRequest(
|
||||
[property: JsonPropertyName("id")] int Id,
|
||||
[property: JsonPropertyName("position")] double Position);
|
||||
[property: JsonPropertyName("position")] double? Position);
|
||||
|
||||
/// <summary>
|
||||
/// 分页搜索已扫描文件的请求。
|
||||
@@ -74,7 +74,9 @@ namespace FileShare_Services.Services.FileLibrary
|
||||
[property: JsonPropertyName("pageSize")] int PageSize = 24,
|
||||
[property: JsonPropertyName("mediaType")] string? MediaType = null,
|
||||
[property: JsonPropertyName("keyword")] string? Keyword = null,
|
||||
[property: JsonPropertyName("rootId")] int RootId = 0);
|
||||
[property: JsonPropertyName("rootId")] int RootId = 0,
|
||||
[property: JsonPropertyName("sortBy")] string? SortBy = null,
|
||||
[property: JsonPropertyName("sortDirection")] string? SortDirection = null);
|
||||
|
||||
/// <summary>
|
||||
/// 磁盘驱动器信息。
|
||||
@@ -121,6 +123,7 @@ namespace FileShare_Services.Services.FileLibrary
|
||||
string Extension,
|
||||
long SizeBytes,
|
||||
DateTime LastWriteTimeUtc,
|
||||
DateTime? FileCreationTimeUtc,
|
||||
string MediaType,
|
||||
string ContentType,
|
||||
string StreamUrl,
|
||||
@@ -136,7 +139,12 @@ namespace FileShare_Services.Services.FileLibrary
|
||||
/// </summary>
|
||||
public sealed record BrowseDirectoryRequest(
|
||||
[property: JsonPropertyName("rootId")] int RootId = 0,
|
||||
[property: JsonPropertyName("path")] string? Path = null);
|
||||
[property: JsonPropertyName("path")] string? Path = null,
|
||||
[property: JsonPropertyName("page")] int Page = 1,
|
||||
[property: JsonPropertyName("pageSize")] int PageSize = 48,
|
||||
[property: JsonPropertyName("mediaType")] string? MediaType = null,
|
||||
[property: JsonPropertyName("sortBy")] string? SortBy = "name",
|
||||
[property: JsonPropertyName("sortDirection")] string? SortDirection = "asc");
|
||||
|
||||
/// <summary>
|
||||
/// 浏览文件库目录的响应,包含当前路径、子目录列表和文件列表。
|
||||
@@ -144,7 +152,11 @@ namespace FileShare_Services.Services.FileLibrary
|
||||
public sealed record BrowseDirectoryResponse(
|
||||
string CurrentPath,
|
||||
List<string> Subdirectories,
|
||||
List<FileRecordDto> Files);
|
||||
List<FileRecordDto> Files,
|
||||
int Total,
|
||||
int Page,
|
||||
int PageSize,
|
||||
int TotalPages);
|
||||
|
||||
/// <summary>
|
||||
/// 文本文件预览内容,支持截断标记。
|
||||
|
||||
@@ -104,7 +104,12 @@ namespace FileShare_Services.Services.FileLibrary
|
||||
/// <inheritdoc />
|
||||
public async Task<IApiResponse> SaveFileProgressAsync(SaveFileProgressRequest request)
|
||||
{
|
||||
await fileLibrary.SaveFileProgressAsync(request.Id, request.Position);
|
||||
if (request.Id <= 0 || request.Position is null || double.IsNaN(request.Position.Value) || double.IsInfinity(request.Position.Value))
|
||||
{
|
||||
return ResponseHelper.Failure(400, "参数错误");
|
||||
}
|
||||
|
||||
await fileLibrary.SaveFileProgressAsync(request.Id, request.Position.Value);
|
||||
return ResponseHelper.Succeed();
|
||||
}
|
||||
|
||||
|
||||
@@ -180,6 +180,7 @@ namespace FileShare_Services.Services.FileLibrary
|
||||
record.Extension = info.Extension.ToLowerInvariant();
|
||||
record.SizeBytes = info.Length;
|
||||
record.LastWriteTimeUtc = info.LastWriteTimeUtc;
|
||||
record.FileCreationTimeUtc = info.CreationTimeUtc;
|
||||
record.MediaType = mediaType;
|
||||
record.ContentType = contentType;
|
||||
record.Exists = true;
|
||||
@@ -334,9 +335,7 @@ namespace FileShare_Services.Services.FileLibrary
|
||||
}
|
||||
|
||||
var total = await query.CountAsync(cancellationToken);
|
||||
var items = await query
|
||||
.OrderBy(file => file.MediaType)
|
||||
.ThenBy(file => file.RelativePath)
|
||||
var items = await ApplyFileSort(query, request.SortBy, request.SortDirection)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(file => ToFileDto(file))
|
||||
@@ -359,6 +358,9 @@ namespace FileShare_Services.Services.FileLibrary
|
||||
public async Task<BrowseDirectoryResponse> BrowseDirectoryAsync(BrowseDirectoryRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var rootId = request.RootId;
|
||||
var page = Math.Clamp(request.Page, 1, 100000);
|
||||
var pageSize = Math.Clamp(request.PageSize, 1, 100);
|
||||
var mediaType = request.MediaType?.Trim();
|
||||
// URL 友好的正斜杠,用于响应和内存处理
|
||||
var prefix = (request.Path ?? "").Trim().Replace('\\', '/').Trim('/');
|
||||
// Windows 反斜杠,用于数据库查询
|
||||
@@ -378,7 +380,7 @@ namespace FileShare_Services.Services.FileLibrary
|
||||
var allFiles = await query.ToListAsync(cancellationToken);
|
||||
|
||||
var subdirs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var currentFiles = new List<FileRecordDto>();
|
||||
var currentFiles = new List<ManagedFileRecord>();
|
||||
|
||||
foreach (var file in allFiles)
|
||||
{
|
||||
@@ -390,7 +392,7 @@ namespace FileShare_Services.Services.FileLibrary
|
||||
var slashIndex = remaining.IndexOf('/');
|
||||
if (slashIndex < 0)
|
||||
{
|
||||
currentFiles.Add(ToFileDto(file));
|
||||
currentFiles.Add(file);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -398,10 +400,28 @@ namespace FileShare_Services.Services.FileLibrary
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(mediaType) && !mediaType.Equals("all", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
currentFiles = currentFiles
|
||||
.Where(file => file.MediaType.Equals(mediaType, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
var total = currentFiles.Count;
|
||||
var files = ApplyFileSort(currentFiles, request.SortBy, request.SortDirection)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(ToFileDto)
|
||||
.ToList();
|
||||
|
||||
return new BrowseDirectoryResponse(
|
||||
prefix,
|
||||
subdirs.OrderBy(d => d, StringComparer.OrdinalIgnoreCase).ToList(),
|
||||
currentFiles);
|
||||
files,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
pageSize > 0 ? (int)Math.Ceiling((double)total / pageSize) : 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -586,6 +606,76 @@ namespace FileShare_Services.Services.FileLibrary
|
||||
return Math.Clamp(interval ?? DefaultScanIntervalMinutes, 1, 1440);
|
||||
}
|
||||
|
||||
private static IQueryable<ManagedFileRecord> ApplyFileSort(
|
||||
IQueryable<ManagedFileRecord> query,
|
||||
string? sortBy,
|
||||
string? sortDirection)
|
||||
{
|
||||
var descending = IsDescending(sortDirection);
|
||||
return NormalizeSortBy(sortBy) switch
|
||||
{
|
||||
"size" => descending
|
||||
? query.OrderByDescending(file => file.SizeBytes).ThenBy(file => file.FileName)
|
||||
: query.OrderBy(file => file.SizeBytes).ThenBy(file => file.FileName),
|
||||
"created" => descending
|
||||
? query.OrderByDescending(file => file.FileCreationTimeUtc ?? file.LastWriteTimeUtc).ThenBy(file => file.FileName)
|
||||
: query.OrderBy(file => file.FileCreationTimeUtc ?? file.LastWriteTimeUtc).ThenBy(file => file.FileName),
|
||||
"modified" => descending
|
||||
? query.OrderByDescending(file => file.LastWriteTimeUtc).ThenBy(file => file.FileName)
|
||||
: query.OrderBy(file => file.LastWriteTimeUtc).ThenBy(file => file.FileName),
|
||||
"type" => descending
|
||||
? query.OrderByDescending(file => file.MediaType).ThenByDescending(file => file.Extension).ThenBy(file => file.FileName)
|
||||
: query.OrderBy(file => file.MediaType).ThenBy(file => file.Extension).ThenBy(file => file.FileName),
|
||||
_ => descending
|
||||
? query.OrderByDescending(file => file.FileName).ThenBy(file => file.RelativePath)
|
||||
: query.OrderBy(file => file.FileName).ThenBy(file => file.RelativePath),
|
||||
};
|
||||
}
|
||||
|
||||
private static IOrderedEnumerable<ManagedFileRecord> ApplyFileSort(
|
||||
IEnumerable<ManagedFileRecord> files,
|
||||
string? sortBy,
|
||||
string? sortDirection)
|
||||
{
|
||||
var descending = IsDescending(sortDirection);
|
||||
return NormalizeSortBy(sortBy) switch
|
||||
{
|
||||
"size" => descending
|
||||
? files.OrderByDescending(file => file.SizeBytes).ThenBy(file => file.FileName)
|
||||
: files.OrderBy(file => file.SizeBytes).ThenBy(file => file.FileName),
|
||||
"created" => descending
|
||||
? files.OrderByDescending(file => file.FileCreationTimeUtc ?? file.LastWriteTimeUtc).ThenBy(file => file.FileName)
|
||||
: files.OrderBy(file => file.FileCreationTimeUtc ?? file.LastWriteTimeUtc).ThenBy(file => file.FileName),
|
||||
"modified" => descending
|
||||
? files.OrderByDescending(file => file.LastWriteTimeUtc).ThenBy(file => file.FileName)
|
||||
: files.OrderBy(file => file.LastWriteTimeUtc).ThenBy(file => file.FileName),
|
||||
"type" => descending
|
||||
? files.OrderByDescending(file => file.MediaType).ThenByDescending(file => file.Extension).ThenBy(file => file.FileName)
|
||||
: files.OrderBy(file => file.MediaType).ThenBy(file => file.Extension).ThenBy(file => file.FileName),
|
||||
_ => descending
|
||||
? files.OrderByDescending(file => file.FileName).ThenBy(file => file.RelativePath)
|
||||
: files.OrderBy(file => file.FileName).ThenBy(file => file.RelativePath),
|
||||
};
|
||||
}
|
||||
|
||||
private static string NormalizeSortBy(string? sortBy)
|
||||
{
|
||||
return sortBy?.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"size" or "sizebytes" => "size",
|
||||
"created" or "createdat" or "createtime" or "creationtime" => "created",
|
||||
"modified" or "lastwrite" or "lastwritetime" or "lastwritetimeutc" or "time" or "date" => "modified",
|
||||
"type" or "mediatype" or "extension" or "ext" => "type",
|
||||
_ => "name",
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsDescending(string? sortDirection)
|
||||
{
|
||||
return sortDirection?.Trim().Equals("desc", StringComparison.OrdinalIgnoreCase) == true
|
||||
|| sortDirection?.Trim().Equals("descending", StringComparison.OrdinalIgnoreCase) == true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 安全获取驱动器属性值,驱动器未就绪或访问异常时返回 null。
|
||||
/// </summary>
|
||||
@@ -641,6 +731,7 @@ namespace FileShare_Services.Services.FileLibrary
|
||||
file.Extension,
|
||||
file.SizeBytes,
|
||||
file.LastWriteTimeUtc,
|
||||
file.FileCreationTimeUtc,
|
||||
file.MediaType,
|
||||
file.ContentType,
|
||||
$"/api/files/{file.Id}/stream",
|
||||
|
||||
Reference in New Issue
Block a user