fix(web): 恢复视频继续播放和进度保存

- 将播放器收敛为单个共享实例,避免列表/网格内多个 video ref 导致进度保存失效
- 恢复视频播放器上方的继续播放提示
- 在播放、暂停、拖动、结束、切换页面和离开页面时保存播放位置
- 保留文件浏览分页和排序参数的前端调用
This commit is contained in:
2026-05-23 11:03:51 +08:00
parent 27e4029f4a
commit c6b05c12e5
22 changed files with 2449 additions and 165 deletions
+6 -2
View File
@@ -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",