commit a32169199f4b3d5d1a22c16056cbd0479699f7c9 Author: yyc12345 Date: Thu Oct 22 15:24:15 2026 +0800 docs: add project plan (PLAN.md) and .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..21e814b --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio cache/options +.vs/ +.idea/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# NuGet +*.nupkg +*.snupkg +**/[Pp]ackages/* +!**/[Pp]ackages/build/ + +# Rider +*.sln.iml + +# OS files +Thumbs.db +.DS_Store diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..867e325 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,198 @@ +# SongTidy 项目计划 + +## 1. 项目概述 + +跨平台桌面应用,用于批量整理音频文件名:扫描/选择音频文件 → 解析文件名提取**歌手/歌曲名/注释** → 界面编辑(含一键交换歌手与歌曲名)→ 一键批量重命名。支持将编辑进度暂存为 XML「计划」文件,下次继续。 + +## 2. 技术栈 + +| 项 | 选型 | +|---|---| +| 框架 | .NET 10(`net10.0`,本地 SDK 10.0.401) | +| UI | Avalonia **11.3.22**(以本地 `avalonia.mvvm` 模板生成骨架后升级至此版本) | +| 主题/控件库 | **Semi.Avalonia 11.3.14**(替代模板自带主题,不使用默认界面库) | +| 图标库 | **IconPacks.Avalonia.Lucide 2.0.0** | +| MVVM | ReactiveUI + `Avalonia.ReactiveUI`(11.3.x,与 Avalonia 对齐) | +| 依赖注入 | 不引入 DI 容器,轻量组合(服务在 ViewModel 中直接构造) | +| 测试 | MSTest,独立项目 `SongTidy.Tests` | + +以上库版本已验证可共存。`App.axaml` 中引入 Semi 主题替换模板默认主题;图标通过 IconPacks 的 Lucide 图标控件用于菜单项、交换按钮等处。 + +## 3. 领域规则 + +### 3.1 字段约束 + +- **歌曲名**:必选 +- **歌手**:可选(对应不知道歌手的歌曲) +- **注释**:可选 + +### 3.2 文件名解析算法(纯字符串操作,不用正则) + +输入为**不含扩展名**的文件名。 + +**步骤一 · 括号抽取(严格模式)** + +1. 识别中文括号 `()` 与英文括号 `()` 两类,**同类才配对**(`(a)` 属落单) +2. 栈式扫描配对;**出现任何落单括号(未匹配的开或闭)→ 放弃整个抽取步骤**,全串原样进入步骤二(此时注释为空) +3. 每对括号:删去两侧括号符,内容 `Trim()`(仅标准空白,不含全角空格)作为注释素材 +4. 抽到多个注释素材:以**单个半角空格**连接 +5. 嵌套括号(罕见):最外层配对生效,内部括号作字面字符保留 +6. 已抽取的括号对从源串中移除,剩余部分进入步骤二 + +**步骤二 · 连字符分割** + +- 仅认半角 `-`(全角 `-`、破折号 `—`/`–` 等不认) +- 无 `-`:歌手为空,整串 `Trim()` 为歌曲名 +- 有 `-`(多个时**以最后一个分割**):前段归歌手、后段归歌曲名(粗暴分配,交换按钮兜底),各自 `Trim()` + +**示例**:`A - B (Live - Tokyo)(修正版).mp3` → 歌手 `A`,歌曲名 `B`,注释 `Live - Tokyo 修正版` + +### 3.3 目标文件名格式(预设注册表,代码留扩展点) + +| 歌手 | 注释 | 歌手在前 | 歌曲在前 | +|------|------|----------|----------| +| 有 | 有 | `A - N (C)` | `N (C) - A` | +| 有 | 无 | `A - N` | `N - A` | +| 无 | 有 | `N (C)` | `N (C)` | +| 无 | 无 | `N` | `N` | + +- 注释括号固定半角 `()` +- **扩展名保留原样**,不参与解析与生成(flac 重命名后仍是 `.flac`) +- 实现为格式定义注册表(占位符 A/N/C + 各缺省分支模板),后续新增格式只需注册新定义 + +### 3.4 重命名校验关卡(全有或全无) + +`输出 > 重命名` 点击后先做**全量校验**,任一问题即弹窗列出**全部**问题(含行号与原因)并拒绝执行: + +1. 歌曲名为空 +2. 字段含文件名非法字符(`\ / : * ? " < > |` 及控制字符),或目标名以点/空格结尾(Windows 限制) +3. 源文件已不存在(含 XML 加载后文件丢失的情形,在此统一拦截) +4. 列表中存在重复源路径(区分大小写的直接字符串比较,与合并规则一致) +5. 目标名冲突:多个条目生成相同目标名;或目标与磁盘既有文件冲突(自身源文件除外)——此项保持**大小写不敏感**,与文件系统实际行为一致(同目录不能同时存在 `a.mp3` 与 `A.mp3`),可挡住 `A→B` 且 `B→A` 这类顺序性冲突链 + +### 3.5 重命名执行 + +校验通过 → **MessageBox 式确认弹窗**(显示条目数与当前格式,防误触)→ 确认后批量执行 → 弹窗报告结果。执行成功后**条目保持不动**(仍指向旧文件名,再次重命名会被「文件缺失」拦截,属预期行为)。 + +## 4. 功能规格 + +### 4.1 主窗口 + +**菜单栏**: + +- **文件**:新建计划|打开计划|—|保存计划|另存为计划|—|关闭计划|—|退出 +- **输入**:从文件夹扫描|从文件选择 +- **输出**:重命名 +- **关于**:关于 + +**信息条**(DataGrid 上方): + +- 左:`共 N 个条目` +- 右:命名格式选择器(歌手在前 / 歌曲在前),驱动目标文件名预览列实时刷新 + +**主区域 DataGrid**: + +`#`|`原文件名`(只读)|`歌手`|`歌曲名`|`注释`(可编辑)|`目标文件名`(只读实时预览)|`⇄`(行内交换按钮,仅交换歌手与歌曲名,无批量操作、无重新解析) + +无底部状态栏。 + +### 4.2 输入 + +- **从文件夹扫描**:系统文件夹选择器;**仅当前文件夹不递归**;按固定扩展名列表过滤:`mp3 flac wav ogg m4a aac wma opus`(代码常量) +- **从文件选择**:系统文件选择器,多选,过滤器允许任意文件(`所有文件 (*.*)`) +- **合并规则**:按完整路径判重(**区分大小写的直接字符串比较**,不做 Windows 大小写不敏感特判);出现重复条目时**新的解析结果直接丢弃,保留用户原条目(含已编辑内容)**;非重复条目追加到列表末尾 + +### 4.3 计划文档(XML 暂存) + +**序列化实现** + +- 使用 .NET 内置 `System.Xml.Serialization.XmlSerializer` +- 模型类**显式标注** `[XmlRoot]`、`[XmlElement]`、`[XmlAttribute]`、`[XmlArray]`/`[XmlArrayItem]`、`[XmlEnum]` 等 attribute 指定 node name,**不依赖类名/属性名推导** + +**文件格式**(属性代替子节点,UTF-8,过滤器 `*.xml`): + +```xml + + + ArtistFirst + + + + + +``` + +- `version`:文件版本号(`[XmlAttribute]`,当前 `1`,预留后续演进) +- `Format`:当前命名格式,以枚举名序列化(枚举成员用 `[XmlEnum]` 显式标注,如 `ArtistFirst`/`TitleFirst`) +- `Item`:`Path`/`Artist`/`Title`/`Comment` 全部为 XML 属性,空字段写空字符串(属性始终写出) +- 特殊字符(`&`、`"`、`<` 等)由 XmlSerializer 自动转义 + +**文档模型** + +- 数据模型:全部条目(原始文件绝对路径 + 三字段当前值)+ 当前命名格式;无其他状态概念 +- **加载计划时恢复主窗口格式选择器**;**切换格式也计入脏标记** +- **新建/关闭计划**:清空列表、清除路径 +- **保存计划**:已有路径则原路径覆盖,否则走另存为;**另存为计划**:选择新路径保存 +- **脏提示**:新建/打开/关闭计划及退出时,若有未保存修改弹窗三选:保存 / 不保存 / 取消 +- 加载时**不校验**文件存在性(重命名关卡统一拦截) + +### 4.4 弹窗基础设施 + +Avalonia 无内置 MessageBox,项目自建**通用 MessageBox 样式对话框组件**(标题 + 多行正文 + 可配置按钮组合,如 `确定`、`是/否`、`保存/不保存/取消`),全项目共用: + +- 重命名前确认弹窗(防误触) +- 脏提示弹窗(保存 / 不保存 / 取消) +- 校验错误报告(正文为全部问题的多行清单) +- 重命名结果报告 + +(「关于」对话框为独立窗口,不套用 MessageBox) + +## 5. 项目结构 + +``` +SongTidy/ +├── SongTidy.csproj # net10.0;Avalonia 11.3.22、Semi.Avalonia 11.3.14、 +│ # IconPacks.Avalonia.Lucide 2.0.0、Avalonia.ReactiveUI +├── Program.cs / App.axaml(.cs) / ViewLocator.cs # 模板骨架(App.axaml 换用 Semi 主题) +├── Models/ +│ ├── SongPlan.cs # [XmlRoot("Plan")]:version 属性、Format、Items +│ ├── PlanItem.cs # [XmlArrayItem("Item")]:Path/Artist/Title/Comment 均为 [XmlAttribute] +│ └── RenameFormats.cs # 格式预设注册表(可扩展)+ 带 [XmlEnum] 的格式枚举 +├── Services/ +│ ├── FilenameParser.cs # 3.2 解析算法(纯静态) +│ ├── AudioFileScanner.cs # 文件夹扫描(非递归、扩展名过滤) +│ ├── RenameValidator.cs # 3.4 全量校验 +│ ├── RenameExecutor.cs # 批量重命名 +│ └── PlanXmlIO.cs # 计划 XML 读写(XmlSerializer) +├── ViewModels/ +│ ├── ViewModelBase.cs +│ ├── MainWindowViewModel.cs # 菜单命令、计划文档状态(路径/脏标记)、格式选择、条目集合 +│ └── SongEntryViewModel.cs # 行 VM:三字段、交换命令、目标名预览 +├── Views/ +│ ├── MainWindow.axaml(.cs) +│ └── Dialogs/ +│ └── MessageBoxWindow.axaml(.cs) # 通用 MessageBox 组件 + 轻量调用辅助 +SongTidy.Tests/ # MSTest:解析、格式生成、校验、XML IO +``` + +响应式要点:`ReactiveObject` + `ReactiveCommand`;预览列与信息条计数用 `WhenAnyValue`/`ObservableAsPropertyHelper` 派生;脏标记由字段编辑、集合变更、格式切换驱动;文件与文件夹选择器走 `StorageProvider`。 + +## 6. 实施步骤 + +1. `dotnet new avalonia.mvvm` 生成骨架,升级包版本至 Avalonia 11.3.22 / Semi.Avalonia 11.3.14 / IconPacks.Avalonia.Lucide 2.0.0 / Avalonia.ReactiveUI,`App.axaml` 接入 Semi 主题;建 `SongTidy.Tests`(MSTest) +2. 实现 `FilenameParser` + 解析单测(覆盖落单括号、多注释、多连字符、无连字符、全半角混合、嵌套括号等边界) +3. 实现 `RenameFormats` 注册表与生成逻辑 + 单测(四种组合 × 两种格式) +4. 实现 `PlanXmlIO` + 单测(格式枚举读写、空字段、路径含空格/特殊字符、`version` 保留) +5. 实现 `RenameValidator` + 单测(五类问题、冲突链) +6. 实现 ViewModel 层(文档模型、命令、脏跟踪、合并规则) +7. 实现主窗口视图(菜单栏、信息条、DataGrid 绑定、交换按钮、Lucide 图标) +8. 实现 `RenameExecutor`、MessageBox 通用组件与确认/报告/错误/脏提示弹窗 +9. 「关于」对话框、窗口标题图标等收尾;全量测试通过 + +## 7. 非目标 + +- 不读写音频元数据标签(ID3 等) +- 不递归扫描子文件夹 +- 无撤销功能(以执行前校验 + 确认弹窗兜底) +- 无批量交换、无重新解析 +- 单窗口单计划,不支持多计划并行