From 55ed3bae237022c033ac62d753b3029614937f5e Mon Sep 17 00:00:00 2001 From: huimanQAQ Date: Wed, 28 Oct 2026 23:57:57 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat(tools):=20=E6=96=B0=E5=A2=9E=E5=BC=80?= =?UTF-8?q?=E5=9C=BA=E8=A7=86=E9=A2=91=E4=B8=8E=E6=9B=B2=E7=9B=AE=E4=BC=AA?= =?UTF-8?q?=E8=A3=85=E7=9A=84=E7=BC=96=E8=BE=91=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端新增 FancyToolController,配合 AquaMai 的 Fancy.CustomIntroCinematic / Fancy.TrackCamouflage: - 开场视频:GetIntroMovie / GetIntroMovieFile(支持 Range)/ GetIntroMovieList / SetIntroMovie / DeleteIntroMovie - 上传视频统一转 H.264 MP4,并按当前编码器给画质参数(h264_mf 默认画质极低,实测同源 2.86 Mbps → 13 Mbps) - 支持左右屏同一视频(L=R)与左右分开;替换单侧时自动补齐另一侧 - 音轨自动抽取成 Enter_.acb / .awb(游戏里由 acb/awb 发声);另提供 SetIntroAudio / DeleteIntroAudio 单独替换音频 - 曲目伪装:GetCamouflage / GetCamouflageJacket / GetCamouflageList / SetCamouflage(Toml)/ SetCamouflageJacket / DeleteCamouflage - 删除与覆盖统一走占用重试(DeleteFilesWithRetry / MoveFileWithRetry),文件被占用时不会卡死或误报成功 - VideoConvert 新增 KeepAudio / ExtraArgs 选项与 H264QualityArgs,其他调用方行为不变 - 前端:工具页新增入口 + IntroCamouflageModal - 使用 MuNET-UI 的 Tabs / Select / TextInput / CheckBox,曲目选择带搜索框过滤 - 左右分屏 1:2 预览,播放/暂停/拖动进度双向同步,显示实际分辨率与文件路径 - 未选择乐曲时所有输入与按钮禁用 - 已有列表显示真实曲名,载入时清空搜索并定位到该曲目 --- .../Controllers/Tools/FancyToolController.cs | 605 +++++++++++++++ MaiChartManager/Front/src/client/apiGen.ts | 333 ++++++++- MaiChartManager/Front/src/locales/en.yaml | 64 ++ MaiChartManager/Front/src/locales/zh-TW.yaml | 64 ++ MaiChartManager/Front/src/locales/zh.yaml | 64 ++ .../src/views/Tools/IntroCamouflageModal.tsx | 698 ++++++++++++++++++ .../Front/src/views/Tools/index.tsx | 9 + MaiChartManager/Utils/VideoConvert.cs | 43 +- 8 files changed, 1878 insertions(+), 2 deletions(-) create mode 100644 MaiChartManager/Controllers/Tools/FancyToolController.cs create mode 100644 MaiChartManager/Front/src/views/Tools/IntroCamouflageModal.tsx diff --git a/MaiChartManager/Controllers/Tools/FancyToolController.cs b/MaiChartManager/Controllers/Tools/FancyToolController.cs new file mode 100644 index 00000000..bf3d4cc3 --- /dev/null +++ b/MaiChartManager/Controllers/Tools/FancyToolController.cs @@ -0,0 +1,605 @@ +using MaiChartManager.Platform; +using MaiChartManager.Utils; +using Microsoft.AspNetCore.Mvc; +using Tomlyn; +using Tomlyn.Model; + +namespace MaiChartManager.Controllers.Tools; + +/// +/// 配合 AquaMai 的两个花活功能的编辑工具: +/// +/// 开场视频 CustomIntroCinematic:LocalAssets/IntroMovies/Enter_<id>.mp4(或 Enter_<id>_L/_R.mp4 左右分开) +/// 曲目伪装 TrackCamouflage:LocalAssets/Camouflages/<id>.toml + <id>_jacket.jpg/png +/// +/// +[ApiController] +[Route("MaiChartManagerServlet/[action]Api")] +public class FancyToolController(ILogger logger) : ControllerBase +{ + private static readonly TomlModelOptions TomlOptions = new() { ConvertPropertyName = name => name }; + + private static string IntroMovieDir => Path.Combine(StaticSettings.GamePath, "LocalAssets", "IntroMovies"); + private static string CamouflageDir => Path.Combine(StaticSettings.GamePath, "LocalAssets", "Camouflages"); + + public record FancyToolResult(bool Success, string? Error); + + public record IntroMovieInfo(bool HasSingle, bool HasLeft, bool HasRight, bool HasAudio, + string? SinglePath, string? LeftPath, string? RightPath); + + public record CamouflageInfoDto(string? Name, string? Artist, string? NoteDesigner, bool HideNoteDesigner, + double UnlockScore, bool HasJacket, string? JacketPath); + + public class CamouflageModel + { + public string Name { get; set; } = ""; + public string Artist { get; set; } = ""; + public string? NoteDesigner { get; set; } + public bool HideNoteDesigner { get; set; } + public double UnlockScore { get; set; } + } + + #region 开场视频 + + [HttpGet] + public IntroMovieInfo GetIntroMovie(int id) + { + var singlePath = ExistingPath(VideoFileName(id, null)); + var leftPath = ExistingPath(VideoFileName(id, "L")); + var rightPath = ExistingPath(VideoFileName(id, "R")); + return new IntroMovieInfo(singlePath is not null, leftPath is not null, rightPath is not null, + System.IO.File.Exists(Path.Combine(IntroMovieDir, $"Enter_{id}.acb")) + && System.IO.File.Exists(Path.Combine(IntroMovieDir, $"Enter_{id}.awb")), + singlePath, leftPath, rightPath); + } + + /// + /// 取开场视频文件供前端预览。未指定 side 时依次取单文件 / 左 / 右。 + /// + [HttpGet] + public ActionResult GetIntroMovieFile(int id, string? side) + { + var path = side?.ToUpperInvariant() switch + { + "L" => ExistingPath(VideoFileName(id, "L")), + "R" => ExistingPath(VideoFileName(id, "R")), + _ => ExistingPath(VideoFileName(id, null)) + ?? ExistingPath(VideoFileName(id, "L")) + ?? ExistingPath(VideoFileName(id, "R")), + }; + + if (path is null) return NotFound(); + // enableRangeProcessing: