docs: add project plan (PLAN.md) and .gitignore

This commit is contained in:
yyc12345 committed 2026-10-22 15:24:15 +08:00
commit a32169199f
2 files changed
+244

No files matched your search

+198
View File
@@ -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
<?xml version="1.0" encoding="utf-8"?>
<Plan version="1">
<Format>ArtistFirst</Format>
<Items>
<Item Path="D:\Music\A - B (Live).mp3" Artist="A" Title="B" Comment="Live"/>
<Item Path="D:\Music\C.mp3" Artist="" Title="C" Comment=""/>
</Items>
</Plan>
```
- `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 等)
- 不递归扫描子文件夹
- 无撤销功能(以执行前校验 + 确认弹窗兜底)
- 无批量交换、无重新解析
- 单窗口单计划,不支持多计划并行