Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
adea9f0
refactor(front): 抽出 openWindow 工具函数
clansty Oct 28, 2026
c4ab14c
refactor(front): 曲目搜索判定抽成公共函数
clansty Oct 28, 2026
d1e8425
fix(tools): 曲目伪装的 toml 只写需要的字段
clansty Oct 28, 2026
a5c1396
fix(video): H264 画质参数改成质量档位,替掉 ExtraArgs
clansty Oct 28, 2026
82e27c4
fix(tools): 去掉逐接口的授权校验,改在打开工具窗口时校验
clansty Oct 28, 2026
bf071ca
fix(tools): 开场视频与伪装的写文件改成同卷事务,并调整接口
clansty Oct 28, 2026
1f608cd
feat(tools): 开场视频与曲目伪装改成全屏工具窗口
clansty Oct 28, 2026
10819a3
fix(tools): 开场视频按侧替换不再互相覆盖,并回报转码进度
clansty Oct 28, 2026
6a91acd
feat(tools): 工具界面按 review 调整
clansty Oct 28, 2026
e492101
fix(tools): 只替换一侧开场视频时保留另一侧
clansty Oct 29, 2026
035fbc1
fix(tools): 回滚失败时保留 stage 目录,不再删掉最后一份原件
clansty Oct 29, 2026
4ca4948
fix(tools): 事务中断后能从遗留的 stage 目录把原文件搬回去
clansty Oct 29, 2026
6282163
fix(tools): 只有真的被占用才报「文件被占用」
clansty Oct 29, 2026
b75f5ad
fix(tools): 列表遇到重复曲目 id 不再整体 500
clansty Oct 29, 2026
44a3b0c
fix(tools): 转码进度改成单写者队列
clansty Oct 29, 2026
f54ab2e
fix(tools): 伪装表单切曲目时清空,加载完成前不许保存
clansty Oct 29, 2026
fab58a5
fix(front): 删除失败的文案和文件占用分开
clansty Oct 29, 2026
8448353
fix(front): 读 AquaMai 配置不再挡首屏
clansty Oct 29, 2026
bae5c2b
refactor(front): 曲目 ID 分段判定抽成公共函数
clansty Oct 29, 2026
1e7143f
docs(video): 注明 Quality.High 只对 H264 生效
clansty Oct 29, 2026
e401c25
fix(tools): 事务恢复改成按日志判断有没有提交过
clansty Oct 29, 2026
26546e8
fix(front): 伪装表单读失败后不再一直锁死
clansty Oct 29, 2026
0b250a2
fix(front): 去掉开场视频的说明段落,压缩内部机制类文案
clansty Oct 29, 2026
4d9ef84
docs(front): 记下界面文案的取舍标准
clansty Oct 29, 2026
8ea233e
fix(front): 删除伪装 / 封面后先就地更新状态,读取失败也不会把已删内容写回去
clansty Oct 29, 2026
5c3f378
fix(front): 切曲目后旧请求的返回不再改到当前表单上
clansty Oct 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,173 changes: 810 additions & 363 deletions MaiChartManager/Controllers/Tools/FancyToolController.cs

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions MaiChartManager/Front/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,18 @@ pnpm --filter @munet/ui typecheck # 只检查 UI 包
- 代码注释使用中文
- `.editorconfig`:2 空格、LF、UTF-8

## 界面文案

界面上的文字只服务于「用户接下来要做什么」。默认不写说明段落,逐条自问,答不上来就删:

- **删掉它,用户会做错什么吗?** 不会做错、只是「知道了」的信息一律不写。
- **有没有实现细节?** 文件格式(mp4 / acb / awb / toml)、转码步骤、编码方式、目录和文件名规则、缓存策略、内部流程,都不该出现在界面上;用户只关心结果(有没有声音、能不能用)。
- **UI 本身表达了吗?** 状态能用「有 / 无」这种短标记就别写句子;能靠禁用按钮、警告条、预览表达的就别写文字。
- **是需要用户在别处操作的吗?** 比如「要在 AquaMai 里开启某个选项」——用检测 + 警告条(只在没开启时出现)表达,不要在静态说明里重复一遍。
- **同一句话是不是在别的地方又说了一遍?** 重复即删除。

允许留下的:当前状态、按钮动作、用户自己解决不了的错误(哪个文件被占用、缺什么前置条件)、必须知道的外部事实(改完要重启游戏才生效)。

## ANTI-PATTERNS

- 禁止手动编辑 `src/client/apiGen.ts` 和 `aquaMaiVersionConfigApiGen.ts`,修改会在下次生成时被覆盖
Expand Down
92 changes: 48 additions & 44 deletions MaiChartManager/Front/src/client/apiGen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,14 +125,6 @@ export interface BatchSetPropsRequest {
version?: number;
}

export interface CamouflageEntry {
/** @format int32 */
id?: number;
name?: string | null;
artist?: string | null;
hasJacket?: boolean;
}

export interface CamouflageInfoDto {
name?: string | null;
artist?: string | null;
Expand Down Expand Up @@ -238,6 +230,19 @@ export interface EntryState {
value?: any;
}

export interface FancyEntry {
/** @format int32 */
id?: number;
hasSingle?: boolean;
hasLeft?: boolean;
hasRight?: boolean;
hasAudio?: boolean;
hasCamouflage?: boolean;
camouflageName?: string | null;
camouflageArtist?: string | null;
hasJacket?: boolean;
}

export interface FancyToolResult {
success?: boolean;
error?: string | null;
Expand Down Expand Up @@ -360,15 +365,6 @@ export interface InstallMmlLibsDto {
useLegacy?: boolean;
}

export interface IntroMovieEntry {
/** @format int32 */
id?: number;
hasSingle?: boolean;
hasLeft?: boolean;
hasRight?: boolean;
hasAudio?: boolean;
}

export interface IntroMovieInfo {
hasSingle?: boolean;
hasLeft?: boolean;
Expand Down Expand Up @@ -417,6 +413,8 @@ export interface MusicXmlWithABJacket {
disable?: boolean;
longMusic?: boolean;
shiftMethod?: string | null;
/** @format int32 */
utageBaseId?: number | null;
charts?: Chart[] | null;
assetBundleJacket?: string | null;
pseudoAssetBundleJacket?: string | null;
Expand Down Expand Up @@ -1474,6 +1472,21 @@ export class Api<
...params,
}),

/**
* No description
*
* @tags FancyTool
* @name GetFancyList
* @request GET:/MaiChartManagerServlet/GetFancyListApi
*/
GetFancyList: (params: RequestParams = {}) =>
this.request<FancyEntry[], any>({
path: `/MaiChartManagerServlet/GetFancyListApi`,
method: "GET",
format: "json",
...params,
}),

/**
* No description
*
Expand Down Expand Up @@ -1556,12 +1569,11 @@ export class Api<
},
params: RequestParams = {},
) =>
this.request<FancyToolResult, any>({
this.request<void, any>({
path: `/MaiChartManagerServlet/SetIntroMovieApi`,
method: "POST",
body: data,
type: ContentType.FormData,
format: "json",
...params,
}),

Expand Down Expand Up @@ -1634,21 +1646,6 @@ export class Api<
...params,
}),

/**
* No description
*
* @tags FancyTool
* @name GetIntroMovieList
* @request GET:/MaiChartManagerServlet/GetIntroMovieListApi
*/
GetIntroMovieList: (params: RequestParams = {}) =>
this.request<IntroMovieEntry[], any>({
path: `/MaiChartManagerServlet/GetIntroMovieListApi`,
method: "GET",
format: "json",
...params,
}),

/**
* No description
*
Expand Down Expand Up @@ -1750,18 +1747,18 @@ export class Api<
* No description
*
* @tags FancyTool
* @name DeleteCamouflage
* @request POST:/MaiChartManagerServlet/DeleteCamouflageApi
* @name DeleteCamouflageJacket
* @request POST:/MaiChartManagerServlet/DeleteCamouflageJacketApi
*/
DeleteCamouflage: (
DeleteCamouflageJacket: (
query?: {
/** @format int32 */
id?: number;
},
params: RequestParams = {},
) =>
this.request<FancyToolResult, any>({
path: `/MaiChartManagerServlet/DeleteCamouflageApi`,
path: `/MaiChartManagerServlet/DeleteCamouflageJacketApi`,
method: "POST",
query: query,
format: "json",
Expand All @@ -1772,13 +1769,20 @@ export class Api<
* No description
*
* @tags FancyTool
* @name GetCamouflageList
* @request GET:/MaiChartManagerServlet/GetCamouflageListApi
* @name DeleteCamouflage
* @request POST:/MaiChartManagerServlet/DeleteCamouflageApi
*/
GetCamouflageList: (params: RequestParams = {}) =>
this.request<CamouflageEntry[], any>({
path: `/MaiChartManagerServlet/GetCamouflageListApi`,
method: "GET",
DeleteCamouflage: (
query?: {
/** @format int32 */
id?: number;
},
params: RequestParams = {},
) =>
this.request<FancyToolResult, any>({
path: `/MaiChartManagerServlet/DeleteCamouflageApi`,
method: "POST",
query: query,
format: "json",
...params,
}),
Expand Down
59 changes: 28 additions & 31 deletions MaiChartManager/Front/src/locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -454,45 +454,40 @@ mod:
Leave empty to use the first detected device as 1P
tools:
fancy:
label: Intro Movie & Camouflage
pickMusic: Choose a song
filterPlaceholder: Filter existing songs
noEntries: No song has an intro movie or camouflage yet
noMatch: No matching song
searchHint: Click any row to edit that song
restartHint: Restart the game for changes to take effect
badgeIntro: Has an intro movie
badgeCamouflage: Has camouflage
preview: Preview
path: File path
noPreview: No file yet
existing: Existing
none: None
load: Load
delete: Delete
introReplace: Replace video
camoJacketShort: with jacket
label: Intro movie & camo
title: Intro movie & track camouflage
music: Music
musicPlaceholder: Search by ID or name
musicPick: Choose a song
pickMusicFirst: Select a song first
introTab: Intro movie
camouflageTab: Camouflage
introHint: "Play a custom video before a track starts (AquaMai's CustomIntroCinematic). Both screens can share one video or use separate ones. Uploads are converted to a silent mp4; the audio track is extracted into Enter_<id>.acb / .awb and played separately by the game."
introSingle: Single file
introAudio: Audio
introSide: Apply to
introModDisabled: Fancy.CustomIntroCinematic is not enabled in AquaMai, so this has no effect in game. Enable it in the Mod settings first.
introSame: Same video for both screens
introSameHint: "When off, each screen has its own video and the preview plays both side by side at 1:2. Toggling the switch does not touch the files; they are only written when you replace a video."
introLeft: Replace left video (L)
introRight: Replace right video (R)
introAudioGenerated: "Saved: audio track extracted to acb/awb"
introAudioSilent: "Saved: no audio track, the intro will be silent"
introAudioReplace: Replace audio
introAudioAdd: Add audio
introAudioRemove: Remove audio
introAudioRemoveConfirm: "Remove the intro audio for song {id}? (both screens share one acb/awb)"
introAudioShared: shared by both screens
sideBoth: Same video on both screens
sideLeft: Left screen only (L)
sideRight: Right screen only (R)
introUpload: Choose video
introReplace: Replace video
introRemove: Delete intro movie
introRemoveConfirm: Delete the intro movie of music {id}?
camouflageHint: Show another name, artist and jacket for this track in the music select and track start screens (AquaMai TrackCamouflage). Works well together with the intro movie.
introAudio: Audio
introAudioYes: Present
introAudioNo: None
introAudioAdd: Add audio
introAudioReplace: Replace audio
introAudioRemove: Remove audio
introAudioRemoveConfirm: Remove the intro audio for song {id}?
introAudioGenerated: "Saved: the video will have sound in game"
introAudioSilent: "Saved: no audio, the intro will be silent"
introEncodingAudio: Encoding audio…
camouflageHint: Shows another name, artist and jacket for this track in the music select and track start screens.
camoModDisabled: Fancy.TrackCamouflage is not enabled in AquaMai, so this has no effect in game. Enable it in the Mod settings first.
camoName: Disguised name
camoArtist: Disguised artist
camoNoteDesigner: Disguised note designer
Expand All @@ -501,6 +496,8 @@ tools:
camoUnlockScoreHint: 0 means the camouflage disappears once the player has any score; use 102 to keep it forever
camoJacketUpload: Choose jacket (jpg / png)
camoJacketReplace: Replace jacket
camoJacketRemove: Remove jacket
camoJacketRemoveConfirm: Remove the camouflage jacket of music {id}? Name and other info are kept
camoRemove: Clear camouflage
camoRemoveConfirm: Clear the camouflage info and jacket of music {id}?
save: Save
Expand All @@ -511,12 +508,12 @@ tools:
errorInvalidSide: Invalid video side
errorInvalidText: Name and artist are required
errorInvalidImage: Only jpg / png images are supported
errorLicense: A valid license is required to convert video
errorInvalidAudio: Unsupported or invalid audio file
errorGamePath: Game directory is not configured
errorConvert: Video conversion failed
errorWrite: Failed to write file
errorDeleteLocked: "Delete failed: the file is still in use. Close the preview and try again."
errorInvalidAudio: "Unsupported or invalid audio file"
errorFileLocked: The file is still in use, close the preview and try again
errorDeleteFailed: Failed to delete the file, close the preview and try again
title: Tools
audioConvert: Audio Convert (ACB + AWB)
videoConvert: Video Convert (DAT)
Expand Down
63 changes: 30 additions & 33 deletions MaiChartManager/Front/src/locales/zh-TW.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -417,45 +417,40 @@ mod:
pathDesc: USB 序號或連接埠路徑,例如 2.2。請使用設定工具中顯示的識別。留空則使用第一個偵測到的設備作為 1P
tools:
fancy:
label: 開場影片與曲目偽裝
pickMusic: 選擇樂曲
filterPlaceholder: 過濾已有樂曲
noEntries: 還沒有做過開場影片或曲目偽裝的樂曲
noMatch: 沒有符合的樂曲
searchHint: 點擊任意一行即可編輯這首樂曲
restartHint: 修改後需重啟遊戲才會生效
badgeIntro: 有開場影片
badgeCamouflage: 有曲目偽裝
preview: 預覽
path: 檔案路徑
noPreview: 尚無檔案
existing: 已有
none: 暫無
load: 載入
delete: 刪除
introReplace: 更換影片
camoJacketShort: 含封面
label: 開場影片與曲目偽裝
title: 開場影片與曲目偽裝
music: 樂曲
musicPlaceholder: 輸入 ID 或名稱搜尋
musicPick: 選擇樂曲
pickMusicFirst: 請先選擇一首樂曲
introTab: 開場影片
camouflageTab: 曲目偽裝
introHint: 在遊戲內開始樂曲前播放自訂影片(AquaMai 的 CustomIntroCinematic)。左右螢幕可以共用一個影片,也可以分開兩個。上傳後影片會轉成靜音 mp4,音軌自動擷取成 Enter_<id>.acb / .awb,由遊戲單獨播放。
introSingle: 單一檔案
introAudio: 音訊
introSide: 套用到
introModDisabled: AquaMai 的 Fancy.CustomIntroCinematic 沒有開啟,遊戲中不會生效。請到「Mod 設定」開啟後再來修改。
introSame: 左右螢幕使用同一影片
introSameHint: 關閉後左右螢幕各用一個影片,預覽以 1:2 並排播放,可分別替換某一側;切換開關本身不會改動檔案,替換影片時才會寫入
introLeft: 更換左側影片(L)
introRight: 更換右側影片(R)
introAudioGenerated: 已儲存:音軌已產生 acb/awb,遊戲中由它發聲
introAudioSilent: 已儲存:影片沒有音軌,將靜音播放
introAudioReplace: 更換音訊
introAudioAdd: 新增音訊
introAudioRemove: 刪除音訊
introAudioRemoveConfirm: 確定要刪除樂曲 {id} 的開場音訊嗎?(左右螢幕共用一組 acb/awb)
introAudioShared: 左右螢幕共用
sideBoth: 左右螢幕同一影片
sideLeft: 僅左螢幕(L)
sideRight: 僅右螢幕(R)
introLeft: 替換左側影片(L)
introRight: 替換右側影片(R)
introUpload: 選擇影片
introReplace: 更換影片
introRemove: 刪除開場影片
introRemoveConfirm: 確定要刪除樂曲 {id} 的開場影片嗎?
camouflageHint: 在選曲與開始畫面把樂曲顯示成別的曲名、曲師與封面(AquaMai 的 TrackCamouflage),適合搭配開場影片使用。
introAudio: 音訊
introAudioYes: 有
introAudioNo: 無
introAudioAdd: 新增音訊
introAudioReplace: 更換音訊
introAudioRemove: 刪除音訊
introAudioRemoveConfirm: 確定要刪除樂曲 {id} 的開場音訊嗎?
introAudioGenerated: 已儲存:遊戲內會有聲音
introAudioSilent: 已儲存:沒有音訊,將靜音播放
introEncodingAudio: 正在產生音訊…
camouflageHint: 在選曲與開始畫面把樂曲顯示成別的曲名、曲師與封面。
camoModDisabled: AquaMai 的 Fancy.TrackCamouflage 沒有開啟,遊戲中不會生效。請到「Mod 設定」開啟後再來修改。
camoName: 偽裝後的曲名
camoArtist: 偽裝後的曲師
camoNoteDesigner: 偽裝後的譜師
Expand All @@ -464,6 +459,8 @@ tools:
camoUnlockScoreHint: 0 表示有成績後就不再偽裝;填 102 可以永久偽裝
camoJacketUpload: 選擇封面(jpg / png)
camoJacketReplace: 更換封面
camoJacketRemove: 刪除封面
camoJacketRemoveConfirm: 確定要刪除樂曲 {id} 的偽裝封面嗎?曲名等資訊會保留
camoRemove: 清除偽裝
camoRemoveConfirm: 確定要清除樂曲 {id} 的偽裝資訊與封面嗎?
save: 儲存
Expand All @@ -474,12 +471,12 @@ tools:
errorInvalidSide: 影片位置無效
errorInvalidText: 曲名與曲師不能為空
errorInvalidImage: 只支援 jpg / png 圖片
errorLicense: 需要有效的授權才能轉換影片
errorInvalidAudio: 音訊檔案無法辨識或轉換失敗
errorGamePath: 未設定遊戲目錄
errorConvert: 影片轉換失敗
errorWrite: 寫入檔案失敗
errorDeleteLocked: 刪除失敗:檔案仍被占用,請關閉預覽後重試
errorInvalidAudio: 音訊檔案無法辨識或轉換失敗
errorFileLocked: 檔案仍被占用,請關閉預覽後重試
errorDeleteFailed: 刪除檔案失敗,請關閉預覽後重試
title: 工具
audioConvert: 音訊轉換(ACB + AWB)
videoConvert: 影片轉換(DAT)
Expand Down
Loading
Loading