Skip to content

refactor(front): 曲目伪装和开场视频改成独立页面 - #79

Merged
clansty merged 26 commits into
mainfrom
feat/fancy-tool-page
Oct 29, 2026
Merged

clansty merged 26 commits into
mainfrom
feat/fancy-tool-page

Conversation

@clansty

@clansty clansty commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

View guided diff

Sourcery 摘要

将开场视频与曲目伪装工具重构为独立页面,并完善资源管理、预览和文件事务处理。

新功能:

  • 将开场视频与曲目伪装工具改为独立窗口页面,支持统一曲目列表、搜索筛选及分标签编辑。
  • 新增开场视频、音频和伪装封面的预览、上传、替换与删除功能,并支持左右屏视频同步预览。
  • 新增开场视频与曲目伪装的合并列表接口,以及独立的伪装封面删除接口。

错误修复:

  • 改进开场资源和伪装封面的文件更新回滚、占用检测与缓存清理,降低操作失败时遗留不完整文件的风险。
  • 修正视频转换的画质配置,使开场视频采用高质量编码参数。
  • 避免将无意义的空字段写入伪装配置,并统一曲目搜索匹配逻辑。

功能增强:

  • 将授权校验集中到打开工具窗口时执行,移除工具接口中的逐项授权判断。
  • 抽取通用的独立窗口打开逻辑,并复用曲目搜索判定。

维护工作:

  • 更新前端 API 类型、路由及多语言文案,以适配新的独立工具页面。
Original summary in English

Sourcery 总结

将开场视频与曲目伪装重构为独立工具页面,并完善资源预览、编辑及可靠文件管理。

新功能:

  • 将开场视频与曲目伪装整合为独立工具页面,提供统一曲目列表、搜索筛选和分标签编辑。
  • 支持开场视频、音频及伪装封面的预览、上传、替换和删除,并支持左右屏视频同步预览及转码进度显示。
  • 新增开场视频与曲目伪装的合并列表接口,以及独立删除伪装封面的接口。

错误修复:

  • 改进资源更新的事务回滚、文件占用处理和预览缓存清理,减少失败操作留下不完整资源的风险。
  • 提高开场视频转码质量,并避免向伪装配置写入无效空字段。

改进:

  • 集中在打开工具页面时执行授权校验,抽取通用独立窗口打开逻辑,并统一曲目搜索匹配规则。

文档:

  • 更新相关多语言界面文案,以适配独立工具页面和新的资源管理功能。

杂项:

  • 更新前端路由、API 类型和工具页面结构,以支持新的独立页面。
Original summary in English

Sourcery 摘要

将开场视频与曲目伪装重构为独立工具页面,并完善资源预览、编辑和可靠文件管理。

新功能:

  • 将开场视频与曲目伪装整合为独立工具页面,提供统一曲目列表、搜索筛选和分标签编辑。
  • 支持开场视频、音频及伪装封面的预览、上传、替换和删除,并为视频转换提供实时进度及左右屏同步预览。
  • 新增开场视频与曲目伪装的合并列表接口,以及独立删除伪装封面的接口。

问题修复:

  • 改进资源更新、删除和回滚流程,处理文件占用并清理预览缓存,降低失败操作留下不完整文件的风险。
  • 提高开场视频转换质量,并避免向伪装配置写入无效空字段。

改进:

  • 集中在打开工具页面时执行授权校验,抽取通用独立窗口打开逻辑,并统一曲目搜索和分类筛选规则。

文档:

  • 更新中英文及繁体中文界面文案,以适配独立工具页面和新的资源管理功能。

杂项:

  • 更新前端路由、API 类型和工具页面结构,移除原有开场视频与曲目伪装弹窗。
Original summary in English

Sourcery 摘要

将开场视频与曲目伪装重构为独立工具页面,并完善资源预览、编辑及可靠的文件管理。

新功能:

  • 将开场视频与曲目伪装整合到独立工具页面,提供统一的曲目列表、搜索筛选和分标签编辑。
  • 支持预览、上传、替换和删除开场视频、音频及伪装封面,并提供左右屏同步预览和视频转换进度显示。
  • 新增开场视频与曲目伪装的合并列表接口,以及独立的伪装封面删除接口。

问题修复:

  • 改进资源更新、删除和回滚流程,处理文件占用并清理预览缓存,降低操作失败后留下不完整文件的风险。
  • 提高开场视频转换质量,并避免向伪装配置写入无效的空字段。

功能优化:

  • 将授权校验集中到工具页面打开时执行,提取通用的独立窗口打开逻辑,并统一曲目搜索和分类筛选规则。

文档:

  • 更新简体中文、英文及繁体中文界面文案,以适配独立工具页面和新的资源管理功能。

杂项:

  • 更新前端路由、API 类型和工具页面结构,并移除原有的开场视频与曲目伪装弹窗。
Original summary in English

Sourcery 摘要

将开场视频与曲目伪装整合为独立工具页面,并完善资源管理、预览和可靠的文件处理。

新功能:

  • 将开场视频与曲目伪装功能重构为独立工具页面,提供统一的曲目列表、搜索筛选和分标签编辑。
  • 新增开场视频、音频及伪装封面的预览、上传、替换和删除功能,并支持左右屏视频同步预览与转码进度显示。
  • 新增开场视频与曲目伪装的合并列表接口,以及独立的伪装封面删除接口。

问题修复:

  • 改进资源文件的占用检测、事务回滚和预览缓存清理,降低操作失败后遗留不完整文件的风险。
  • 提高开场视频的转码质量,避免向伪装配置写入无效的空字段,并统一资源状态处理。

改进:

  • 将授权校验集中在打开工具页面时执行,提取通用的独立窗口打开逻辑,并统一曲目搜索和分类筛选规则。

文档:

  • 更新简体中文、英文和繁体中文界面文案,以适配独立工具页面和新的资源管理功能。

杂项:

  • 更新前端路由、API 类型和工具页面结构,并移除原有的开场视频与曲目伪装弹窗。
Original summary in English

Sourcery 总结

将开场视频与曲目伪装重构为独立工具页面,并完善资源管理、预览和可靠文件处理。

新功能:

  • 将开场视频与曲目伪装整合为独立工具页面,提供统一曲目列表、搜索筛选和分标签编辑。
  • 支持开场视频、音频及伪装封面的预览、上传、替换和删除,并提供左右屏同步预览与视频转码进度显示。
  • 新增开场视频与曲目伪装的合并列表接口及独立的伪装封面删除接口。

问题修复:

  • 改进资源更新、删除和回滚流程,处理文件占用并清理预览缓存,降低失败操作留下不完整文件的风险。
  • 提高开场视频转码质量,并避免向伪装配置写入无效的空字段。

改进:

  • 集中在打开工具页面时执行授权校验,抽取通用的独立窗口打开逻辑,并统一曲目搜索和分类筛选规则。

文档:

  • 更新简体中文、英文和繁体中文界面文案,以适配独立工具页面和新的资源管理功能。

杂项:

  • 更新前端路由、API 类型和工具页面结构,并移除原有的开场视频与曲目伪装弹窗。
Original summary in English

Summary by Sourcery

将开场视频与曲目伪装重构为独立工具页面,并完善资源管理、预览和可靠文件处理。

New Features:

  • 将开场视频与曲目伪装整合为独立工具页面,提供统一曲目列表、搜索筛选和分标签编辑。
  • 支持开场视频、音频及伪装封面的预览、上传、替换和删除,并提供左右屏同步预览与视频转码进度显示。
  • 新增开场视频与曲目伪装的合并列表接口及独立的伪装封面删除接口。

Bug Fixes:

  • 改进资源更新、删除和回滚流程,处理文件占用并清理预览缓存,降低失败操作留下不完整文件的风险。
  • 提高开场视频转码质量,并避免向伪装配置写入无效的空字段。

Enhancements:

  • 集中在打开工具页面时执行授权校验,抽取通用的独立窗口打开逻辑,并统一曲目搜索和分类筛选规则。

Documentation:

  • 更新简体中文、英文和繁体中文界面文案,以适配独立工具页面和新的资源管理功能。

Chores:

  • 更新前端路由、API 类型和工具页面结构,并移除原有的开场视频与曲目伪装弹窗。

- 新增 utils/openWindow.ts,统一处理 WebView2 的 window.open 和 Photino 的 open-window 消息

- 谱面预览改用这个工具函数,后续新窗口(工具页)可以直接复用
- 新增 utils/musicSearch.ts,统一按 名称 / 曲师 / 谱师 / ID 匹配

- 批量操作的曲目选择器改用这个判定,和新的工具页搜索保持同一口径
- 不再把空的 NoteDesigner、值为 0 的 UnlockScore、false 的 HideNoteDesigner 写进配置文件

- 这些字段缺失时 AquaMai 本来就按「不伪装」处理,但那是对方实现细节,toml 作为跨项目契约不应该依赖它
- VideoConvertOptions 新增 Quality 档位(Default / High),High 时按选中的编码器给显式画质参数

- 删掉上一版临时加的 ExtraArgs 逃生口,画质参数归位到 VideoConvert 内部,不再依赖调用方传 ffmpeg 参数

- 顺便把 H264QualityArgs 收回 private,只有这里的代码该关心它
- 开场视频 / 开场音频 / 伪装三个写入接口不再各自判断授权,和「视频转换」等工具的入口校验方式保持一致

- 授权校验挪到前端打开工具的入口,拦一次就够,开源项目没必要把复杂度堆到每个接口上
- 临时文件和备份目录改到目标目录里的 .mcm-tmp,所有搬迁都是一次同卷 rename:修掉临时目录在别的文件系统时 File.Move 直接失败(Linux 上 /tmp 是 tmpfs 必踩),也不用再复制几百 MB 旧文件做备份,峰值占用从 3 倍回到 1 倍

- 被预览占用搬不动时单独返回 file-locked,前端据此提示「文件仍被占用」,不再是笼统的删除失败

- 新增 GetFancyListApi 返回开场视频与曲目伪装的并集,替掉 GetIntroMovieListApi / GetCamouflageListApi

- 新增 DeleteCamouflageJacketApi:只删封面保留 toml,游戏侧没有封面时用默认空封面,是合法状态

- 删除开场视频/音频时顺手清掉 acb 解码出来的预览缓存,不再在 temp 里留几十 MB 死文件

- 列表解析只认 Enter_<id>[_L|_R].mp4,中间产物不会被当成交付文件
- 工具从弹窗改成独立的 hash 路由页 /fancy-tool,用 openWindow 打开(和谱面预览同一套机制):要放 1:1 视频预览和上千首的曲目搜索,弹窗宽度和高度都不够

- 左侧列已做过开场视频或曲目伪装的曲目(按 id 并集),带开场视频/伪装图标徽标;顶部是选择乐曲按钮和本次列表的本地过滤

- 右侧用 WhateverNaviBar 切「开场视频 / 曲目伪装」;点选择乐曲时右侧整块切到搜索页,选中即回到编辑界面并自动落到有内容的那个 tab

- 搜索页复用批量操作的表格范式:虚拟滚动 + 名称/曲师/谱师/ID 搜索,带封面列和「已有」标记

- 拆成 MusicSidebar / MusicSearch / IntroTab / CamouflageTab / refs / errors,原来 742 行的单文件拆完

- 新增「删除封面」,曲名或曲师留空时不允许保存(游戏侧缺字段会显示 ???)

- 页面顶部提示改动需要重启游戏才生效(AquaMai 只在启动时扫一次目录)

- 授权校验挪到工具卡片入口,未授权直接弹购买对话框

- 清理上一版没用到的 6 个文案 key(三种语言),操作状态改成每个 tab 各自管理,不再一操作就锁整页
@sourcery-ai

sourcery-ai Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

审阅者指南

本次 PR 将开场视频与曲目伪装从原有模态框重构为可独立打开的曲目管理页面,通过后端并集列表和统一文件事务管理关联资产,同时完善媒体预览、音频编码、封面操作、授权入口、搜索复用、API 类型及本地化支持。

打开并加载 Fancy 工具的时序图

sequenceDiagram
    participant User
    participant ToolsPage
    participant LicenseGate
    participant Window as FancyToolWindow
    participant API as FancyToolController
    participant Assets as LocalAssets

    User->>ToolsPage: openFancyTool()
    ToolsPage->>LicenseGate: check version.license
    alt license active
        LicenseGate-->>ToolsPage: allowed
        ToolsPage->>Window: openWindow(/fancy-tool)
        Window->>API: GetFancyList()
        API->>Assets: scan IntroMovies and Camouflages
        Assets-->>API: intro and camouflage entries
        API-->>Window: FancyEntry[]
        Window->>API: load music, genres, and versions
        API-->>Window: initialize search and editor
    else license inactive
        LicenseGate-->>ToolsPage: denied
    end
Loading

Fancy 资产事务更新时序图

sequenceDiagram
    participant User
    participant FancyPage
    participant API as FancyToolController
    participant Transaction as FancyFileTransaction
    participant Converter as VideoConvert
    participant Assets as LocalAssets

    User->>FancyPage: uploadMovie or uploadAudio
    FancyPage->>FancyPage: releaseMedia()
    FancyPage->>API: SetIntroMovie or SetIntroAudio
    API->>Transaction: create stage directory
    API->>Converter: ConvertVideo or BuildIntroAudio
    Converter-->>Transaction: staged media files
    API->>Transaction: Backup existing files
    API->>Transaction: Place staged files
    API->>Transaction: Commit()
    Transaction->>Assets: rename staged files and clean backups
    API-->>FancyPage: FancyToolResult
    FancyPage->>API: GetIntroMovie or refreshEntries
    API-->>FancyPage: updated preview state
Loading

统一 Fancy 条目的实体关系图

erDiagram
    FancyEntry {
        int id PK
        bool hasSingle
        bool hasLeft
        bool hasRight
        bool hasAudio
        bool hasCamouflage
        string camouflageName
        string camouflageArtist
        bool hasJacket
    }
    IntroMovieAsset {
        int id PK
        bool hasVideo
        bool hasAudio
    }
    CamouflageAsset {
        int id PK
        string name
        string artist
        bool hasJacket
    }
    FancyEntry ||--o| IntroMovieAsset : summarizes
    FancyEntry ||--o| CamouflageAsset : summarizes
Loading

文件级变更

变更 详情 文件
将开场视频与曲目伪装重构为独立页面,并以曲目为中心合并管理两类资产。
  • 新增独立路由和窗口打开流程,工具页提供曲目列表、搜索、状态标记及开场视频/伪装双标签编辑。
  • 新增并集列表接口,合并扫描视频、音频、伪装配置和封面状态;移除原有分离的列表接口。
  • 抽取统一曲目搜索匹配逻辑,并为独立窗口补充独立 SPA 初始化所需的曲目、版本和流派数据加载。
MaiChartManager/Controllers/Tools/FancyToolController.cs
MaiChartManager/Front/src/client/apiGen.ts
MaiChartManager/Front/src/router.ts
MaiChartManager/Front/src/utils/musicSearch.ts
MaiChartManager/Front/src/utils/openWindow.ts
MaiChartManager/Front/src/views/FancyTool/index.tsx
MaiChartManager/Front/src/views/FancyTool/refs.ts
MaiChartManager/Front/src/views/FancyTool/MusicSidebar.tsx
MaiChartManager/Front/src/views/FancyTool/MusicSearch.tsx
MaiChartManager/Front/src/views/BatchAction/MusicSelector.tsx
MaiChartManager/Front/src/views/Tools/index.tsx
MaiChartManager/Front/src/views/Tools/IntroCamouflageModal.tsx
完善独立工具页的开场视频与音频编辑体验,并强化媒体预览和文件状态处理。
  • 支持单视频与左右分屏视频预览、播放/进度同步、独立音频播放,以及上传、替换、删除操作。
  • 上传视频时自动提取音频并生成 ACB/AWB,统一使用高画质 H.264 编码;删除或更新后清理预览缓存。
  • 通过停止媒体预览、缓存 token 和过期响应检查,降低 Windows 文件占用及异步切歌导致状态错乱的问题。
MaiChartManager/Controllers/Tools/FancyToolController.cs
MaiChartManager/Front/src/views/FancyTool/IntroTab.tsx
MaiChartManager/Front/src/client/apiGen.ts
MaiChartManager/Front/src/locales/en.yaml
MaiChartManager/Front/src/locales/zh.yaml
MaiChartManager/Front/src/locales/zh-TW.yaml
MaiChartManager/Utils/VideoConvert.cs
扩展曲目伪装编辑能力并规范跨项目配置文件输出。
  • 支持单独删除伪装封面而保留伪装配置,并在列表中展示伪装名称、作者和封面状态。
  • 仅写入有意义的 TOML 字段,避免空值和默认值影响游戏侧伪装判定。
  • 为封面替换、视频和音频写入统一引入同目录 stage/backup/commit/rollback 事务,减少跨盘复制和半套文件风险。
MaiChartManager/Controllers/Tools/FancyToolController.cs
MaiChartManager/Front/src/views/FancyTool/CamouflageTab.tsx
MaiChartManager/Front/src/client/apiGen.ts
MaiChartManager/Front/src/locales/en.yaml
MaiChartManager/Front/src/locales/zh.yaml
MaiChartManager/Front/src/locales/zh-TW.yaml
将授权校验从后端工具接口移至打开独立工具页时执行。
  • 工具入口在打开页面前检查授权状态并弹出购买提示;后端不再对各个 FancyTool 接口逐一校验。
  • 统一处理文件占用、删除、回滚和未知错误的前端提示。
MaiChartManager/Front/src/views/Tools/index.tsx
MaiChartManager/Controllers/Tools/FancyToolController.cs
MaiChartManager/Front/src/views/FancyTool/errors.ts
MaiChartManager/Front/src/locales/en.yaml
MaiChartManager/Front/src/locales/zh.yaml
MaiChartManager/Front/src/locales/zh-TW.yaml
统一独立窗口创建逻辑,并复用到谱面预览页面。
  • 抽取兼容 WebView2/Photino 的站内窗口打开工具,集中处理 hash 路由、查询参数、窗口尺寸和宿主消息。
  • 谱面预览按钮改用统一窗口工具,保持原有参数和窗口行为。
MaiChartManager/Front/src/utils/openWindow.ts
MaiChartManager/Front/src/views/Charts/MusicEdit/PreviewChartButton.tsx
更新前端 API 类型与本地化文案以匹配新的页面和接口。
  • 生成 FancyEntry、GetFancyList 和 DeleteCamouflageJacket API 定义,移除旧列表类型和接口定义。
  • 新增独立页面、搜索、媒体操作、封面操作、文件占用及重启提示相关的中英文文案。
  • 补充 MusicXmlWithABJacket 的 utageBaseId 字段并更新生成客户端标题。
MaiChartManager/Front/src/client/apiGen.ts
MaiChartManager/Front/src/locales/en.yaml
MaiChartManager/Front/src/locales/zh.yaml
MaiChartManager/Front/src/locales/zh-TW.yaml

提示与命令

与 Sourcery 互动

  • 触发新一轮审阅: 在 pull request 中评论 @sourcery-ai review。
  • 继续讨论: 直接回复 Sourcery 的审阅评论。
  • 根据审阅评论创建 GitHub issue: 回复审阅评论,请 Sourcery 根据该评论创建 issue。也可以回复审阅评论并输入 @sourcery-ai issue 来创建 issue。
  • 生成 pull request 标题: 在 pull request 标题中的任意位置写入 @sourcery-ai,即可随时生成标题。也可以在 pull request 中评论 @sourcery-ai title,随时(重新)生成标题。
  • 生成 pull request 摘要: 在 pull request 正文中需要生成摘要的位置写入 @sourcery-ai summary,即可随时在该位置生成 PR 摘要。也可以在 pull request 中评论 @sourcery-ai summary,随时(重新)生成摘要。
  • 生成审阅者指南: 在 pull request 中评论 @sourcery-ai guide,即可随时(重新)生成审阅者指南。
  • 解决所有 Sourcery 评论: 在 pull request 中评论 @sourcery-ai resolve,即可解决所有 Sourcery 评论。如果你已经处理完所有评论且不想再看到它们,这会很有用。
  • 隐藏所有 Sourcery 审阅: 在 pull request 中评论 @sourcery-ai dismiss,即可隐藏所有现有的 Sourcery 审阅。当你想从头开始进行新一轮审阅时尤其有用——别忘了评论 @sourcery-ai review 来触发新一轮审阅!

自定义使用体验

访问你的控制面板,以便:

  • 启用或禁用审阅功能,例如 Sourcery 生成的 pull request 摘要、审阅者指南等。
  • 更改审阅语言。
  • 添加、删除或编辑自定义审阅说明。
  • 调整其他审阅设置。

获取帮助

Original review guide in English

Reviewer's Guide

本次 PR 将开场视频与曲目伪装从原有模态框重构为可独立打开的曲目管理页面,通过后端并集列表和统一文件事务管理关联资产,同时完善媒体预览、音频编码、封面操作、授权入口、搜索复用、API 类型及本地化支持。

Sequence diagram for opening and loading the fancy tool

sequenceDiagram
    participant User
    participant ToolsPage
    participant LicenseGate
    participant Window as FancyToolWindow
    participant API as FancyToolController
    participant Assets as LocalAssets

    User->>ToolsPage: openFancyTool()
    ToolsPage->>LicenseGate: check version.license
    alt license active
        LicenseGate-->>ToolsPage: allowed
        ToolsPage->>Window: openWindow(/fancy-tool)
        Window->>API: GetFancyList()
        API->>Assets: scan IntroMovies and Camouflages
        Assets-->>API: intro and camouflage entries
        API-->>Window: FancyEntry[]
        Window->>API: load music, genres, and versions
        API-->>Window: initialize search and editor
    else license inactive
        LicenseGate-->>ToolsPage: denied
    end
Loading

Sequence diagram for transactional fancy asset updates

sequenceDiagram
    participant User
    participant FancyPage
    participant API as FancyToolController
    participant Transaction as FancyFileTransaction
    participant Converter as VideoConvert
    participant Assets as LocalAssets

    User->>FancyPage: uploadMovie or uploadAudio
    FancyPage->>FancyPage: releaseMedia()
    FancyPage->>API: SetIntroMovie or SetIntroAudio
    API->>Transaction: create stage directory
    API->>Converter: ConvertVideo or BuildIntroAudio
    Converter-->>Transaction: staged media files
    API->>Transaction: Backup existing files
    API->>Transaction: Place staged files
    API->>Transaction: Commit()
    Transaction->>Assets: rename staged files and clean backups
    API-->>FancyPage: FancyToolResult
    FancyPage->>API: GetIntroMovie or refreshEntries
    API-->>FancyPage: updated preview state
Loading

Entity relationship diagram for unified fancy entries

erDiagram
    FancyEntry {
        int id PK
        bool hasSingle
        bool hasLeft
        bool hasRight
        bool hasAudio
        bool hasCamouflage
        string camouflageName
        string camouflageArtist
        bool hasJacket
    }
    IntroMovieAsset {
        int id PK
        bool hasVideo
        bool hasAudio
    }
    CamouflageAsset {
        int id PK
        string name
        string artist
        bool hasJacket
    }
    FancyEntry ||--o| IntroMovieAsset : summarizes
    FancyEntry ||--o| CamouflageAsset : summarizes
Loading

File-Level Changes

Change Details Files
将开场视频与曲目伪装重构为独立页面,并以曲目为中心合并管理两类资产。
  • 新增独立路由和窗口打开流程,工具页提供曲目列表、搜索、状态标记及开场视频/伪装双标签编辑。
  • 新增并集列表接口,合并扫描视频、音频、伪装配置和封面状态;移除原有分离的列表接口。
  • 抽取统一曲目搜索匹配逻辑,并为独立窗口补充独立 SPA 初始化所需的曲目、版本和流派数据加载。
MaiChartManager/Controllers/Tools/FancyToolController.cs
MaiChartManager/Front/src/client/apiGen.ts
MaiChartManager/Front/src/router.ts
MaiChartManager/Front/src/utils/musicSearch.ts
MaiChartManager/Front/src/utils/openWindow.ts
MaiChartManager/Front/src/views/FancyTool/index.tsx
MaiChartManager/Front/src/views/FancyTool/refs.ts
MaiChartManager/Front/src/views/FancyTool/MusicSidebar.tsx
MaiChartManager/Front/src/views/FancyTool/MusicSearch.tsx
MaiChartManager/Front/src/views/BatchAction/MusicSelector.tsx
MaiChartManager/Front/src/views/Tools/index.tsx
MaiChartManager/Front/src/views/Tools/IntroCamouflageModal.tsx
完善独立工具页的开场视频与音频编辑体验,并强化媒体预览和文件状态处理。
  • 支持单视频与左右分屏视频预览、播放/进度同步、独立音频播放,以及上传、替换、删除操作。
  • 上传视频时自动提取音频并生成 ACB/AWB,统一使用高画质 H.264 编码;删除或更新后清理预览缓存。
  • 通过停止媒体预览、缓存 token 和过期响应检查,降低 Windows 文件占用及异步切歌导致状态错乱的问题。
MaiChartManager/Controllers/Tools/FancyToolController.cs
MaiChartManager/Front/src/views/FancyTool/IntroTab.tsx
MaiChartManager/Front/src/client/apiGen.ts
MaiChartManager/Front/src/locales/en.yaml
MaiChartManager/Front/src/locales/zh.yaml
MaiChartManager/Front/src/locales/zh-TW.yaml
MaiChartManager/Utils/VideoConvert.cs
扩展曲目伪装编辑能力并规范跨项目配置文件输出。
  • 支持单独删除伪装封面而保留伪装配置,并在列表中展示伪装名称、作者和封面状态。
  • 仅写入有意义的 TOML 字段,避免空值和默认值影响游戏侧伪装判定。
  • 为封面替换、视频和音频写入统一引入同目录 stage/backup/commit/rollback 事务,减少跨盘复制和半套文件风险。
MaiChartManager/Controllers/Tools/FancyToolController.cs
MaiChartManager/Front/src/views/FancyTool/CamouflageTab.tsx
MaiChartManager/Front/src/client/apiGen.ts
MaiChartManager/Front/src/locales/en.yaml
MaiChartManager/Front/src/locales/zh.yaml
MaiChartManager/Front/src/locales/zh-TW.yaml
将授权校验从后端工具接口移至打开独立工具页时执行。
  • 工具入口在打开页面前检查授权状态并弹出购买提示;后端不再对各个 FancyTool 接口逐一校验。
  • 统一处理文件占用、删除、回滚和未知错误的前端提示。
MaiChartManager/Front/src/views/Tools/index.tsx
MaiChartManager/Controllers/Tools/FancyToolController.cs
MaiChartManager/Front/src/views/FancyTool/errors.ts
MaiChartManager/Front/src/locales/en.yaml
MaiChartManager/Front/src/locales/zh.yaml
MaiChartManager/Front/src/locales/zh-TW.yaml
统一独立窗口创建逻辑,并复用到谱面预览页面。
  • 抽取 WebView2/Photino 兼容的站内窗口打开工具,集中处理 hash 路由、查询参数、窗口尺寸和宿主消息。
  • 谱面预览按钮改用统一窗口工具,保持原有参数和窗口行为。
MaiChartManager/Front/src/utils/openWindow.ts
MaiChartManager/Front/src/views/Charts/MusicEdit/PreviewChartButton.tsx
更新前端 API 类型与本地化文案以匹配新的页面和接口。
  • 生成 FancyEntry、GetFancyList 和 DeleteCamouflageJacket API 定义,移除旧列表类型和接口定义。
  • 新增独立页面、搜索、媒体操作、封面操作、文件占用及重启提示相关的中英文文案。
  • 补充 MusicXmlWithABJacket 的 utageBaseId 字段并更新生成客户端标题。
MaiChartManager/Front/src/client/apiGen.ts
MaiChartManager/Front/src/locales/en.yaml
MaiChartManager/Front/src/locales/zh.yaml
MaiChartManager/Front/src/locales/zh-TW.yaml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

嗨——我发现了 12 个问题

给 AI 智能体的提示
请处理此代码审查中的评论:

## 单项评论

### 评论 1
<location path="MaiChartManager/Controllers/Tools/FancyToolController.cs" line_range="166" />
<code_context>
         if (gamePathError is not null) return new FancyToolResult(false, gamePathError);
-#if WINDOWS
-        if (IapManager.License != IapManager.LicenseStatus.Active)
-            return new FancyToolResult(false, "license-required");
-#endif

</code_context>
<issue_to_address>
**未授权上传会修改游戏文件**

未授权的调用方直接调用 fancy-tool 写入 API,或不经工具卡片而直接打开工具路由时,写入处理程序不再检查 `IapManager.License`;而 `openFancyTool` 检查只限制工具卡片,因此直接请求仍会转换上传内容并替换受许可保护的游戏资源。

请在后端写入处理程序中恢复许可证检查,并限制对工具路由的直接访问。

另见 `MaiChartManager/Controllers/Tools/FancyToolController.cs:15`、`MaiChartManager/Controllers/Tools/FancyToolController.cs:295-297`、`MaiChartManager/Front/src/views/Tools/index.tsx:28-31`、`MaiChartManager/Front/src/router.ts:12`。
</issue_to_address>

### 评论 2
<location path="MaiChartManager/Controllers/Tools/FancyToolController.cs" line_range="302-315" />
<code_context>
+            foreach (var path in IntroFilePaths(id))
</code_context>
<issue_to_address>
**替换一侧会覆盖两侧**

如果一首歌的 L、R 视频不同,而用户替换其中一侧,`SetIntroMovieCore` 会在检查另一侧视频之前备份现有开场文件,导致另一侧视频看起来不存在;随后新上传的视频会被复制到两侧,提交时原视频也会被丢弃。

备份时保留现有的另一侧视频,并且只替换请求指定的一侧。
</issue_to_address>

### 评论 3
<location path="MaiChartManager/Controllers/Tools/FancyToolController.cs" line_range="907-916" />
<code_context>
+                try
+                {
+                    if (!System.IO.File.Exists(backupPath)) continue;
+                    if (!MoveWithRetry(backupPath, target)) PlatformFile.CopyFile(backupPath, target);
+                }
+                catch (Exception)
+                {
</code_context>
<issue_to_address>
**回滚失败会删除原始资源**

回滚期间恢复备份失败时,`Rollback` 会压制恢复错误,随后 `DropStageDir` 递归删除暂存目录,其中包含唯一剩余的备份,导致原始资源被永久删除。

只要恢复失败,就保留暂存目录及其中的备份,并报告失败,而不是删除它们。
</issue_to_address>

### 评论 4
<location path="MaiChartManager/Controllers/Tools/FancyToolController.cs" line_range="844" />
<code_context>
+            if (!System.IO.File.Exists(path)) return true;
+
+            var backupPath = StagePath($"bak_{Guid.NewGuid():N}");
+            if (!MoveWithRetry(path, backupPath)) return false;
+
+            _backups.Add((path, backupPath));
</code_context>
<issue_to_address>
**更新中断会使原始资源无法恢复**

如果进程或机器在备份移动完成后、事务提交前停止,`Backup` 会将原始文件移入 `.mcm-tmp`;若进程在提交或回滚前停止,启动时没有恢复机制将其还原,导致规范位置上的资源丢失。

添加启动恢复机制,以还原被遗弃事务目录中的备份。
</issue_to_address>

### 评论 5
<location path="MaiChartManager/Controllers/Tools/FancyToolController.cs" line_range="315" />
<code_context>
+                // 只替换一侧时,另一侧还没有的话先复制一份,保证左右都在(游戏要求 L、R 同时存在)
                 var otherSide = normalizedSide == "L" ? "R" : "L";
                 var otherPath = Path.Combine(IntroMovieDir, VideoFileName(id, otherSide));
                 if (!System.IO.File.Exists(otherPath)) PlatformFile.CopyFile(targetPath, otherPath);
             }

</code_context>
<issue_to_address>
**上传失败后会留下多余的视频**

按侧上传时,如果创建了缺失的另一侧视频,之后文件放置又失败,`PlatformFile.CopyFile` 创建的另一侧视频并未登记到 `FancyFileTransaction` 中。因此,`Rollback` 移除已跟踪的输出并还原备份后,仍会留下该副本;调用方看到的是上传失败、文件布局却不完整。

将另一侧的副本登记到事务中,以便上传失败时回滚将其删除。

另见 `MaiChartManager/Controllers/Tools/FancyToolController.cs:312`、`MaiChartManager/Controllers/Tools/FancyToolController.cs:891`。
</issue_to_address>

### 评论 6
<location path="MaiChartManager/Controllers/Tools/FancyToolController.cs" line_range="68" />
<code_context>
+    public IEnumerable<FancyEntry> GetFancyList()
+    {
+        var introMovies = ReadIntroMovieEntries().ToDictionary(entry => entry.Id);
+        var camouflages = ReadCamouflageEntries().ToDictionary(entry => entry.Id);
+
+        return introMovies.Keys.Union(camouflages.Keys)
</code_context>
<issue_to_address>
**重复 ID 会导致列表无法加载**

如果迷彩目录中存在不同的数字 TOML 文件名,但解析后得到相同 ID,`ReadCamouflageEntries` 就会产生重复 ID,导致 `GetFancyList` 中的 `ToDictionary` 抛出异常,整个合并列表请求失败,无法返回条目。

将迷彩条目转换为字典之前,先处理解析后重复的 ID。
</issue_to_address>

### 评论 7
<location path="MaiChartManager/Controllers/Tools/FancyToolController.cs" line_range="102" />
<code_context>
+        if (!Directory.Exists(IntroMovieDir)) return [];
+
+        var ids = new HashSet<int>();
+        foreach (var file in Directory.EnumerateFiles(IntroMovieDir, "*.mp4"))
+        {
+            var name = Path.GetFileNameWithoutExtension(file);
</code_context>
<issue_to_address>
**已保存的资源不会显示在列表中**

用户保存了没有视频的开场音频,或保存了没有迷彩 TOML 的封面时,`ReadIntroMovieEntries` 只从 MP4 文件中发现 ID,而 `ReadCamouflageEntries` 只从 TOML 文件中发现 ID。因此,`GetFancyList` 会将仅有音频或仅有封面的资源排除在侧边栏和搜索状态之外。

构建合并列表时,也纳入音频和封面资源中的 ID。

另见 `MaiChartManager/Front/src/views/FancyTool/IntroTab.tsx:366-367`、`MaiChartManager/Controllers/Tools/FancyToolController.cs:70`、`MaiChartManager/Controllers/Tools/FancyToolController.cs:103`、`MaiChartManager/Controllers/Tools/FancyToolController.cs:128`。
</issue_to_address>

### 评论 8
<location path="MaiChartManager/Controllers/Tools/FancyToolController.cs" line_range="67" />
<code_context>
+    [HttpGet]
+    public IEnumerable<FancyEntry> GetFancyList()
+    {
+        var introMovies = ReadIntroMovieEntries().ToDictionary(entry => entry.Id);
+        var camouflages = ReadCamouflageEntries().ToDictionary(entry => entry.Id);
+
</code_context>
<issue_to_address>
**列表读取到更新过程中的状态**

另一个工具窗口发起列表请求时,如果上传正处于备份歌曲文件之后、放置文件之前,`GetFancyList` 会在未获取每首歌的锁的情况下扫描文件;与此同时,上传会临时将现有文件移入 `.mcm-tmp`。因此,另一个工具窗口收到的列表可能不包含该歌曲,或只显示部分信息;上传完成后,它还可能继续显示这个过时结果。

确保列表读取看到每首歌的稳定状态,或在进行中的事务完成后刷新列表。
</issue_to_address>

### 评论 9
<location path="MaiChartManager/Controllers/Tools/FancyToolController.cs" line_range="331" />
<code_context>
             logger.LogError(e, "转换开场视频失败: {id}", id);
-            tx.Rollback();
-            return new FancyToolResult(false, "convert-failed");
+            return new FancyToolResult(false, e is IOException ? "file-locked" : "convert-failed");
         }
         finally
</code_context>
<issue_to_address>
**I/O 错误被误报为文件锁定**

上传遇到与文件锁定无关的 `IOException` 时,上传处理程序会将所有 `IOException` 都映射为 `file-locked`,包括转换或写入期间磁盘已满或磁盘故障等错误。随后前端会提示用户关闭预览并重试,从而掩盖视频、音频和封面上传中的实际故障。

仅在检测到锁定或移动失败时返回 `file-locked`;其他 I/O 错误应返回转换或写入错误。

另见 `MaiChartManager/Controllers/Tools/FancyToolController.cs:401`、`MaiChartManager/Controllers/Tools/FancyToolController.cs:718`、`MaiChartManager/Front/src/views/FancyTool/errors.ts:15`。
</issue_to_address>

### 评论 10
<location path="MaiChartManager/Front/src/views/FancyTool/index.tsx" line_range="71" />
<code_context>
+              <WhateverNaviBar items={naviItems.value}/>
+            </div>
+            <div class="grow-1 of-y-auto cst px-4">
+              {activeTab.value === FancyTab.Intro ? <IntroTab/> : <CamouflageTab/>}
+            </div>
+          </>}
</code_context>
<issue_to_address>
**切换标签页会丢弃编辑内容**

用户编辑迷彩字段后,在保存前切换标签页或打开歌曲搜索时,条件渲染会卸载 `CamouflageTab`。表单状态保存在组件本地,因此再次挂载时会调用 `load()`,用服务器上已保存的值覆盖未保存的更改。

切换标签页时保持组件挂载、仅隐藏非活动组件;或者在卸载后保留脏表单状态,并在丢弃前发出警告。

另见 `MaiChartManager/Front/src/views/FancyTool/CamouflageTab.tsx:49`。
</issue_to_address>

### 评论 11
<location path="MaiChartManager/Front/src/views/FancyTool/CamouflageTab.tsx" line_range="38" />
<code_context>
+      // 切曲目后旧请求可能才回来,丢弃过期响应
+      if (id !== selectedId.value) return;
+
+      camouflage.value = {
+        name: data.name ?? '',
+        artist: data.artist ?? '',
</code_context>
<issue_to_address>
**较晚返回的响应会覆盖当前状态**

如果用户编辑时迷彩加载仍在进行,或多个列表刷新请求以乱序完成,`CamouflageTab.load` 可能会覆盖用户已经编辑的字段;较早发起的 `GetFancyList` 响应也可能覆盖更新的 `fancyEntries`。用户会丢失未保存的更改,或看到过期条目和状态。

为异步响应添加保护,避免加载结果覆盖用户编辑,并阻止较早的列表请求覆盖较新的结果。

另见 `MaiChartManager/Front/src/views/FancyTool/refs.ts:38`。
</issue_to_address>

### 评论 12
<location path="MaiChartManager/Front/src/views/FancyTool/CamouflageTab.tsx" line_range="34-37" />
<code_context>
+        camouflage.value = emptyCamouflage();
+        return;
+      }
+      const data: CamouflageInfoDto = (await api.GetCamouflage({ id })).data;
+      // 切曲目后旧请求可能才回来,丢弃过期响应
+      if (id !== selectedId.value) return;
+
+      camouflage.value = {
+        name: data.name ?? '',
</code_context>
<issue_to_address>
**旧歌曲的迷彩数据会保存到新歌曲**

用户选择另一首歌曲后,如果在新歌曲的迷彩请求返回前就点击保存,`load` 尚未完成时 `selectedId` 已经改变,但表单仍保留上一首歌曲的值,而且保存按钮仍可用。随后 `save` 会使用新 ID 提交旧值,从而用上一首歌曲的数据覆盖新歌曲的迷彩设置。

跟踪表单当前对应的 ID,并在该 ID 的数据加载完成前禁用编辑和保存。
</issue_to_address>

Sourcery 评估

需要人工审查。 目前有 12 个待处理问题;视频和音频上传处理程序中的许可证检查已被移除,现在只在 UI 打开时检查,因此调用方可以直接调用 API 绕过检查,在没有有效许可证的情况下写入开场资源。恢复检查可以重新启用服务器端验证,但无法撤销端点未受保护期间写入的文件。

阻断问题:MaiChartManager/Controllers/Tools/FancyToolController.cs:166、MaiChartManager/Controllers/Tools/FancyToolController.cs:315、MaiChartManager/Controllers/Tools/FancyToolController.cs:916、MaiChartManager/Controllers/Tools/FancyToolController.cs:844、MaiChartManager/Controllers/Tools/FancyToolController.cs:315,以及另外 7 处


Sourcery 对开源项目免费——如果您喜欢我们的审查,欢迎分享 ✨
Original comment in English

Hey - I've found 12 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="MaiChartManager/Controllers/Tools/FancyToolController.cs" line_range="166" />
<code_context>
         if (gamePathError is not null) return new FancyToolResult(false, gamePathError);
-#if WINDOWS
-        if (IapManager.License != IapManager.LicenseStatus.Active)
-            return new FancyToolResult(false, "license-required");
-#endif

</code_context>
<issue_to_address>
**Unlicensed uploads change game files**

When an unlicensed caller invokes a fancy-tool write API directly or opens the tool route without using the tool card, the write handlers no longer check `IapManager.License`, and the `openFancyTool` check only gates the tool card, so direct requests still convert uploads and replace licensed game assets.

Restore the license check in the backend write handlers and guard direct access to the tool route.

Also at `MaiChartManager/Controllers/Tools/FancyToolController.cs:15`, `MaiChartManager/Controllers/Tools/FancyToolController.cs:295-297`, `MaiChartManager/Front/src/views/Tools/index.tsx:28-31`, `MaiChartManager/Front/src/router.ts:12`.
</issue_to_address>

### Comment 2
<location path="MaiChartManager/Controllers/Tools/FancyToolController.cs" line_range="302-315" />
<code_context>
+            foreach (var path in IntroFilePaths(id))
</code_context>
<issue_to_address>
**Replacing one side overwrites both**

When a song has distinct L and R videos and the user replaces either side, `SetIntroMovieCore` backs up the existing intro files before checking for the opposite-side video, so that video appears missing and the newly uploaded video is copied to both sides; committing discards the original.

Preserve the existing opposite-side video when backing up and replacing only the requested side.
</issue_to_address>

### Comment 3
<location path="MaiChartManager/Controllers/Tools/FancyToolController.cs" line_range="907-916" />
<code_context>
+                try
+                {
+                    if (!System.IO.File.Exists(backupPath)) continue;
+                    if (!MoveWithRetry(backupPath, target)) PlatformFile.CopyFile(backupPath, target);
+                }
+                catch (Exception)
+                {
</code_context>
<issue_to_address>
**Failed rollback deletes original assets**

When restoring a backup fails during rollback, `Rollback` suppresses the restore error, then `DropStageDir` recursively deletes the staging directory containing the only remaining backup, permanently removing the original asset.

Keep the staging directory and its backups whenever a restore fails, and report the failure instead of deleting them.
</issue_to_address>

### Comment 4
<location path="MaiChartManager/Controllers/Tools/FancyToolController.cs" line_range="844" />
<code_context>
+            if (!System.IO.File.Exists(path)) return true;
+
+            var backupPath = StagePath($"bak_{Guid.NewGuid():N}");
+            if (!MoveWithRetry(path, backupPath)) return false;
+
+            _backups.Add((path, backupPath));
</code_context>
<issue_to_address>
**Interrupted updates strand original assets**

When the process or machine stops after a backup move and before transaction commit, `Backup` moves the original into `.mcm-tmp`; if the process stops before commit or rollback, no startup recovery restores it, leaving the canonical asset missing.

Add startup recovery that restores backups from abandoned transaction directories.
</issue_to_address>

### Comment 5
<location path="MaiChartManager/Controllers/Tools/FancyToolController.cs" line_range="315" />
<code_context>
+                // 只替换一侧时,另一侧还没有的话先复制一份,保证左右都在(游戏要求 L、R 同时存在)
                 var otherSide = normalizedSide == "L" ? "R" : "L";
                 var otherPath = Path.Combine(IntroMovieDir, VideoFileName(id, otherSide));
                 if (!System.IO.File.Exists(otherPath)) PlatformFile.CopyFile(targetPath, otherPath);
             }

</code_context>
<issue_to_address>
**Failed uploads leave an extra video**

When a side-specific upload creates a missing opposite-side video and a later placement fails, `PlatformFile.CopyFile` creates the opposite-side video without registering it with `FancyFileTransaction`, so `Rollback` leaves that copy behind after removing tracked outputs and restoring backups; the caller sees a failed upload with a partial layout.

Register the opposite-side copy with the transaction so rollback removes it if the upload fails.

Also at `MaiChartManager/Controllers/Tools/FancyToolController.cs:312`, `MaiChartManager/Controllers/Tools/FancyToolController.cs:891`.
</issue_to_address>

### Comment 6
<location path="MaiChartManager/Controllers/Tools/FancyToolController.cs" line_range="68" />
<code_context>
+    public IEnumerable<FancyEntry> GetFancyList()
+    {
+        var introMovies = ReadIntroMovieEntries().ToDictionary(entry => entry.Id);
+        var camouflages = ReadCamouflageEntries().ToDictionary(entry => entry.Id);
+
+        return introMovies.Keys.Union(camouflages.Keys)
</code_context>
<issue_to_address>
**Duplicate IDs prevent the list loading**

When the camouflage directory contains distinct numeric TOML filenames that parse to the same ID, `ReadCamouflageEntries` produces duplicate IDs, so `ToDictionary` in `GetFancyList` throws and the combined list request fails instead of returning entries.

Handle duplicate parsed IDs before converting camouflage entries to a dictionary.
</issue_to_address>

### Comment 7
<location path="MaiChartManager/Controllers/Tools/FancyToolController.cs" line_range="102" />
<code_context>
+        if (!Directory.Exists(IntroMovieDir)) return [];
+
+        var ids = new HashSet<int>();
+        foreach (var file in Directory.EnumerateFiles(IntroMovieDir, "*.mp4"))
+        {
+            var name = Path.GetFileNameWithoutExtension(file);
</code_context>
<issue_to_address>
**Saved assets disappear from the list**

When a user saves intro audio without a video, or a jacket without camouflage TOML, `ReadIntroMovieEntries` discovers IDs only from MP4 files and `ReadCamouflageEntries` only from TOML files, so `GetFancyList` omits audio-only and jacket-only assets from the sidebar and search status.

Include IDs from audio and jacket assets when building the combined list.

Also at `MaiChartManager/Front/src/views/FancyTool/IntroTab.tsx:366-367`, `MaiChartManager/Controllers/Tools/FancyToolController.cs:70`, `MaiChartManager/Controllers/Tools/FancyToolController.cs:103`, `MaiChartManager/Controllers/Tools/FancyToolController.cs:128`.
</issue_to_address>

### Comment 8
<location path="MaiChartManager/Controllers/Tools/FancyToolController.cs" line_range="67" />
<code_context>
+    [HttpGet]
+    public IEnumerable<FancyEntry> GetFancyList()
+    {
+        var introMovies = ReadIntroMovieEntries().ToDictionary(entry => entry.Id);
+        var camouflages = ReadCamouflageEntries().ToDictionary(entry => entry.Id);
+
</code_context>
<issue_to_address>
**List reads mid-update state**

When a list request from another tool window runs while an upload is between backing up and placing the song's files, `GetFancyList` scans files without taking the per-song gate, while an upload temporarily moves the existing files into `.mcm-tmp`. A second tool window can therefore receive a list with the song missing or only partially represented, and keep showing that stale result after the upload completes.

Make list reads observe a stable per-song state, or refresh them after an in-progress transaction completes.
</issue_to_address>

### Comment 9
<location path="MaiChartManager/Controllers/Tools/FancyToolController.cs" line_range="331" />
<code_context>
             logger.LogError(e, "转换开场视频失败: {id}", id);
-            tx.Rollback();
-            return new FancyToolResult(false, "convert-failed");
+            return new FancyToolResult(false, e is IOException ? "file-locked" : "convert-failed");
         }
         finally
</code_context>
<issue_to_address>
**I/O failures misreported as locks**

When an upload encounters an IOException unrelated to a file being locked, the upload handlers map every `IOException` to `file-locked`, including errors such as a full or failing disk during conversion or writing. The frontend then tells users to close the preview and retry, masking the actual failure across video, audio, and jacket uploads.

Return `file-locked` only for detected lock/move failures and use the conversion or write error for other I/O failures.

Also at `MaiChartManager/Controllers/Tools/FancyToolController.cs:401`, `MaiChartManager/Controllers/Tools/FancyToolController.cs:718`, `MaiChartManager/Front/src/views/FancyTool/errors.ts:15`.
</issue_to_address>

### Comment 10
<location path="MaiChartManager/Front/src/views/FancyTool/index.tsx" line_range="71" />
<code_context>
+              <WhateverNaviBar items={naviItems.value}/>
+            </div>
+            <div class="grow-1 of-y-auto cst px-4">
+              {activeTab.value === FancyTab.Intro ? <IntroTab/> : <CamouflageTab/>}
+            </div>
+          </>}
</code_context>
<issue_to_address>
**Tab changes discard edits**

When a user edits camouflage fields and switches tabs or opens song search before saving, the conditional render unmounts `CamouflageTab` when the user switches tabs or opens song search. Its form state is local, so mounting it again calls `load()` and replaces unsaved changes with the saved server values.

Keep the tab components mounted while hiding the inactive one, or preserve dirty form state across unmounts and warn before discarding it.

Also at `MaiChartManager/Front/src/views/FancyTool/CamouflageTab.tsx:49`.
</issue_to_address>

### Comment 11
<location path="MaiChartManager/Front/src/views/FancyTool/CamouflageTab.tsx" line_range="38" />
<code_context>
+      // 切曲目后旧请求可能才回来,丢弃过期响应
+      if (id !== selectedId.value) return;
+
+      camouflage.value = {
+        name: data.name ?? '',
+        artist: data.artist ?? '',
</code_context>
<issue_to_address>
**Late responses overwrite current state**

When a camouflage load is pending while the user edits, or overlapping list refreshes resolve out of order, `CamouflageTab.load` can replace fields the user has already edited, while an older `GetFancyList` response can replace newer `fancyEntries`; users lose unsaved changes or see stale entries and statuses.

Guard asynchronous responses so loads do not overwrite edits and older list requests cannot replace newer results.

Also at `MaiChartManager/Front/src/views/FancyTool/refs.ts:38`.
</issue_to_address>

### Comment 12
<location path="MaiChartManager/Front/src/views/FancyTool/CamouflageTab.tsx" line_range="34-37" />
<code_context>
+        camouflage.value = emptyCamouflage();
+        return;
+      }
+      const data: CamouflageInfoDto = (await api.GetCamouflage({ id })).data;
+      // 切曲目后旧请求可能才回来,丢弃过期响应
+      if (id !== selectedId.value) return;
+
+      camouflage.value = {
+        name: data.name ?? '',
</code_context>
<issue_to_address>
**Old camouflage saves to new song**

When a user selects another song and saves before its camouflage request returns, `selectedId` changes before `load` finishes, while the form retains the previous song’s values and Save remains enabled. `save` then submits those values using the new ID, overwriting that song’s camouflage with the previous song’s data.

Track the ID currently represented by the form and disable editing and saving until that ID’s load completes.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 12 findings to address first, and the license check was removed from the video and audio upload handlers and now runs only when the UI opens, so a caller can bypass it by invoking the APIs directly and write intro assets without an active license. Reverting restores the server-side check, but it does not undo files written while the endpoints were unguarded.

Blocking findings: MaiChartManager/Controllers/Tools/FancyToolController.cs:166, MaiChartManager/Controllers/Tools/FancyToolController.cs:315, MaiChartManager/Controllers/Tools/FancyToolController.cs:916, MaiChartManager/Controllers/Tools/FancyToolController.cs:844, MaiChartManager/Controllers/Tools/FancyToolController.cs:315, and 7 more


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

- 从「左右屏同一个视频」切到分屏、只换某一侧时,另一侧改用原来那个单文件补齐:之前是从这次上传的视频复制过去,结果两边文件变成同一个(预览也变成两个一样的新视频)

- 只换一侧时音频是两边共用的,新视频没有音轨就保留原来的 acb/awb,不再跟着视频一起清掉

- 上传接口改成 SSE 回报转码进度(Progress / Success / Error,Error 的 data 是错误码):转码要几十秒到几分钟,之前只能干转圈没有任何反馈

- 视频和音频的备份分开处理,只换视频时不去动 acb/awb
- 搜索页表格改成父容器给死高度(grow + h-0 + flexHeight),表体不再被吃掉一块;ID / 版本 / 流派补回列过滤

- 上传转码时显示进度条(转码占 0-90%,之后显示「正在生成音频…」)

- 左侧曲目栏加上曲绘,id 放在名称上面,开场视频 / 曲目伪装的图标挪到右上

- WhateverNaviBar 去掉图标

- 预览不再显示文件路径和 acb 文件名,音频只显示「有 / 无」

- 删掉左右分屏和伪装字段两段说明文字

- AquaMai 的 Fancy.CustomIntroCinematic / Fancy.TrackCamouflage 没开启时,在对应 tab 顶部给警告,说明里也写明需要开启;读不到配置时不提示,避免误报

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 20 files

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

View guided diff | Re-trigger cubic

Comment thread MaiChartManager/Utils/VideoConvert.cs Resolved

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 12 files (changes from recent commits).

Requires human review: Auto-approval blocked because this review re-detected 2 unresolved issues already reported by Cubic.
Tip: Get a Slack DM when cubic finishes reviewing your PRs. Connect Slack

View guided diff | Re-trigger cubic

- 备份集合按 side 收窄到单文件 + 目标侧:之前把 L、R 都挪进 stage,导致「另一侧不存在就补一份」的判定永远成立,另一侧被替换成这次上传的视频(磁盘上的文件也跟着变)

- 另一侧的补齐文件改成先复制进 stage 再 Place,后续步骤失败回滚时会跟着撤掉,不再留下多余文件
- Rollback 现在会检查每个备份是否真的还原成功,有任何一个失败就把 stage 目录留下并记录日志路径

- Dispose 之前会无条件删 stage 目录,等于把刚失败的那份唯一副本删掉;改成回滚失败时跳过清理
- 备份文件名从 bak_<guid> 改成 bak_<原文件名>,否则谁也不知道它原本叫什么

- 新建事务时扫一遍 .mcm-tmp 下的遗留目录:正式路径上缺文件就把备份搬回去,已经有了就不覆盖(那说明中断没造成损失)

- 用 ActiveStageDirs 跳过正在运行的事务目录,避免把别的并发事务的中间状态当成遗留备份
- MoveWithRetry 改成返回最后一次异常,位置拿不到原因(之前所有异常都被吞成 false)

- Place 失败时先看目标是不是还被独占着,是才抛 FancyFileLockedException;磁盘满、权限不足之类按普通写入失败报

- 三个 catch 的映射从 e is IOException 收窄到 FancyFileLockedException
- 伪装目录里同时有 1.toml 和 01.toml 时,两个都会解析成 id 1,ToDictionary 直接抛异常,整个列表接口挂掉

- 改成按 id 分组取第一条,一条脏文件不影响其它曲目
- 进度回调是 ffmpeg 读取线程上 fire-and-forget 写 Response,会和请求线程上的 Success / Error 帧并发写同一个 HttpResponse,帧顺序也没保证

- 照着 VideoConvertToolController 的做法引入无界 Channel:回调只 TryWrite 入队,由一个循环按顺序写出

- 客户端中途断开时按 RequestAborted 结束写循环
- 切曲目时先清空表单:之前上一首的内容会留在表单里,读取失败时一直留着,点保存就写进了当前曲目

- 用 loadedId 记录表单里装的是哪一首,没就绪时所有输入和按钮都禁用,避免切曲目瞬间把旧表单提交给新 id

- 保存成功后的刷新失败改用读取错误文案,文件本身已经写进去了,不该报「写入失败」
- delete-failed 不一定是占用导致的(可能是权限、磁盘),映射到「文件仍被占用」会误导用户

- 新增 tools.fancy.errorDeleteFailed,补了简中 / 繁中 / 英文三条文案
- loadModFlags 之前放在 Promise.all 里被 await,解析 AquaMai.dll 期间 Splash 不消失、首条曲目的自动选中也被推迟,和注释写反了

- 改成在列表和曲目数据就绪之后再调用,不 await
- 标准 / DX / 宴会场 的阈值之前在两处各写了一套(搜索页还硬编码 1e5),改成 utils/musicSearch 里的 musicIdKind,宴会场起点走 consts 的 isUtageId

- 批量操作页的 ID 筛选顺带改成用 key 当筛选值:原来拿翻译后的文案本身比较,切语言或改文案就会失效

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 11 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

- 光看正式路径上有没有文件判断不出上次是「还没写新文件」还是「新文件已经写好但没提交」:崩溃在 Place 之后、Commit 之前时会跳过旧备份,留下音画不匹配的半成品

- stage 目录里记一份事务日志(bak: / placed: / commit),恢复时先看有没有 commit 标记:没有就撤掉已生效的新文件再还原备份,有就只清残留

- 清理时把整个遗留 stage 目录删掉,没来得及启用的新视频 / 音频不会再留在 .mcm-tmp 里

- 回滚没能撤干净时把剩下的条目重写回日志,下次接着撤;已经处理掉的行必须去掉,否则下次会删掉刚还原好的文件

- 用一份脚本把事务类原样抽出来在 Linux 上跑了 7 个场景(正常提交、备份后 / Place 后 / 打标记后断电、未提交半成品、老目录无日志、并发事务不被误恢复),全部通过
- loadedId 之前每次 load 开头都清零,保存成功后刷新失败就会让表单永久处于「没就绪」,只能靠切曲目解锁

- 改成只在切换曲目(watch selectedId)时清零;刷新失败时表单内容就是刚保存的内容,不需要锁
- 删掉开场视频 tab 顶部那段说明文字(转成静音 mp4、音轨编码成 acb/awb 这些是内部实现,用户不需要知道)

- 曲目伪装的说明去掉「需要在 AquaMai 里开启 …」:检测到没开启时本来就有警告条,重复一遍只是噪音

- 上传成功后的提示不再提 acb/awb,只说明游戏内有没有声音

- 三个语言同步调整
- 这一轮反复在删「用户不需要知道的内部机制」和「UI 已经表达过的重复说明」,把判断顺序写进 Front/AGENTS.md,改界面时先照着自问一遍

- 明确允许留在界面上的只有:当前状态、按钮动作、用户解决不了的错误、必须知道的外部事实

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 7 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

- 清除伪装成功后立刻清空表单:之前若紧接着的 load() 失败,表单留着刚删掉的曲名曲师且仍可编辑,一按保存就把伪装写回去了

- 删封面 / 换封面成功时先改 hasJacket,避免读取失败时界面还显示已经不存在的封面(或明明有封面却提示「选择封面」)

- 这两处的刷新失败也改用读取错误文案

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 1 file (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

- 上传 / 删除封面的请求在途时用户切了曲目,返回后仍会把 hasJacket、空表单写到新曲目的表单上(尤其读取失败时,界面显示的封面状态就是错的)

- 三处本地状态更新和随后的刷新都先判断 id === selectedId,切走了只刷新列表、不动表单

- 开场视频转码同理:切走之后不再弹「有没有声音」的提示,那句文案取的是当前曲目的音频状态

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 2 files (changes from recent commits).

Requires human review: Refactors intro/camouflage tools into a standalone window with merged list and previews, but drops backend license checks, letting direct API calls bypass the paid feature. The human judgment is whether frontend-only enforcement is acceptable.

View guided diff | Re-trigger cubic

@clansty
clansty merged commit 0198835 into main Oct 29, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant