Skip to content

Commit 5263c39

Browse files
imsyyclaude
andcommitted
修复任务栏歌词评审问题
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 89fe956 commit 5263c39

5 files changed

Lines changed: 130 additions & 22 deletions

File tree

.github/copilot-instructions.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# SPlayer-Next Copilot 指引
2+
3+
本仓库所有 Copilot 会话、Code Review、建议,**一律使用中文作答**(代码、标识符、注释保持原项目语言)。
4+
5+
## 项目概述
6+
7+
SPlayer-Next 是基于 **Electron + Vue 3 + TypeScript** 的桌面音乐播放器,后继 SPlayer,搭配 **Rust NAPI-RS** 原生模块做音频解码、系统媒体集成、任务栏集成。
8+
9+
### 进程架构
10+
11+
- **主进程** `electron/main/`:窗口管理、IPC、原生模块调度
12+
- **Preload** `electron/preload/`:通过 contextBridge 暴露 `window.api`
13+
- **渲染进程** `src/`:Vue 3 SPA;另有三个独立窗口 `windows/desktop-lyric``windows/dynamic-island``windows/taskbar-lyric`
14+
15+
### 原生模块(`native/`
16+
17+
- `audio-engine`:FFmpeg 解码 + rodio 播放 + FFT + 封面提取
18+
- `media-ctrl`:跨平台系统媒体控件(Windows SMTC / Linux MPRIS / macOS MPNowPlaying)+ Discord RPC
19+
- `taskbar-lyric`:Windows 专属,把窗口嵌入任务栏 + RegistryWatcher / UiaWatcher / TrayWatcher 四路监听
20+
21+
所有原生模块通过 `loadNativeModule()``electron/main/utils/nativeLoader.ts`)懒加载。
22+
23+
### 路径别名
24+
25+
| 别名 | 指向 | 使用范围 |
26+
|---|---|---|
27+
| `@/*` | `src/*` | 渲染进程 |
28+
| `@shared/*` | `shared/*` | 主进程 + 渲染进程 + 原生类型 |
29+
| `@main/*` | `electron/main/*` | 主进程 |
30+
| `@server/*` | `electron/server/*` | 主进程 |
31+
| `@windows/*` | `windows/*` | 三个独立歌词窗口(复用 `windows/shared/`|
32+
| `@splayer/audio-engine` | `native/audio-engine` | 主进程,类型从自动生成的 `index.d.ts` 导入 |
33+
| `@splayer/media-ctrl` | `native/media-ctrl` | 同上 |
34+
| `@splayer/taskbar-lyric` | `native/taskbar-lyric` | 同上 |
35+
36+
## 代码约定
37+
38+
- **语言**:注释、Git commit、PR 描述一律中文
39+
- **时间单位**:渲染端所有时间值是**毫秒**;Rust 引擎内部用秒,转换在 `electron/main/ipc/player.ts` 里做(`toMs()`
40+
- **Prettier**:双引号、分号、100 字符宽、末尾逗号
41+
- **Auto-imports**(无需手写 `import`):`vue``pinia``vue-router``@vueuse/core``vue-i18n`;UI 组件自动从 `src/components/` 注入
42+
- **原生模块类型****禁止手写**,必须从 `@splayer/<module>` 的自动生成 `index.d.ts` 导入
43+
- **状态管理双层**`src/stores/status.ts` 走 Pinia 响应式(5Hz 更新);`src/services/playback.ts` 存非响应式时间源,供 60fps 渲染(歌词 / 频谱)用 RAF 采样
44+
- **Reactivity**`Track[]` 等大集合用 `shallowRef`,避免深层代理;响应式代理对象存 IndexedDB 会 `DataCloneError`
45+
- **Store 持久化**:只把轻量数据写 sessionStorage;`TrackDetail`(含歌词大字符串)**禁止**持久化
46+
- **IPC 监听器清理**:preload 的 `onEvent` 必须先 `removeAllListeners``on`,防 HMR 累积;渲染端 composable 在 `onBeforeUnmount` 调用 preload 返回的 `unsubscribe`
47+
- **自动生成文件**(不要编辑):`auto-imports.d.ts``components.d.ts``native/*/index.d.ts`
48+
- **Logger**:主进程统一用 `@main/utils/logger` 的 scoped logger(`coreLog` / `playerLog` / `mediaLog` / `trayLog` / `taskbarLog` / `nativeLog` 等),**不要**直接 `import log from "electron-log"`
49+
50+
## 窗口侧(`windows/`
51+
52+
三个歌词窗口都复用:
53+
54+
- `@windows/shared/composables/useNowPlayingSync` 提供播放同步、歌词索引、锚点插值
55+
- `getNowPlayingCurrentMs()` 非响应式读当前时间,供逐字高亮 RAF 循环用
56+
- 歌词行选择算法按窗口语义选:桌面歌词用 `pickPrimaryIndex`(考虑 overlap)、灵动岛用 `pickLatestStartedIndex`(立即切换)
57+
58+
不要在窗口内重新实现上述逻辑,直接调用共享 composable。
59+
60+
## 设置 schema
61+
62+
- 结构定义在 `src/settings/schema.ts`
63+
- 类型在 `src/types/settings-schema.ts``SettingCategory``SettingSection``SettingItem`
64+
- `SettingSection.tag` / `SettingItem.tag` 支持 `SettingTag = { text: string; type?: "primary" | "warning" | ... }`,用于标题旁的徽标(Beta / 实验等)
65+
- i18n key 约定:`settings.section.{id}` / `settings.{itemKey}.label` / `settings.{itemKey}.description`
66+
- UI 原子组件:`STag` / `SButton` / `SSelect` / `SSlider` / `SSwitch` / `SColor` / `SDialog` 等,统一 7 色 theme token(default / primary / cover / info / success / warning / error)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
applyTo: "**"
3+
---
4+
5+
# 审查指引
6+
7+
所有审查评论、建议、总结一律**用中文**输出。
8+
9+
## 审查关注点
10+
11+
**真实问题优先,风格偏好最后**。优先揪出:
12+
13+
- 运行时 bug、竞态、内存 / 资源泄漏(IPC 监听器、RAF、ResizeObserver、Windows 事件钩子未清理)
14+
- 类型与实现不一致(如 preload API 类型声明与实际 resolve 值不匹配)
15+
- 主进程资源生命周期:窗口 `closed` 事件之外的重复清理会引发双 `stop()` / 双 broadcast
16+
- 硬编码值掩盖用户设置:检查配置项是否真的生效到底层模块
17+
- 原生模块 COM 生命周期(`CoInitializeEx` 的返回值必须判断,`CoUninitialize` 只在真正初始化成功时调用;参考 `native/taskbar-lyric/src/uia.rs``should_uninitialize` 模式)
18+
- 锁的持有时长:回调执行前应先 clone `Arc` 放开 `Mutex`,避免阻塞其它访问与重入死锁
19+
- 渲染端持久化:`TrackDetail` / 大歌词字符串不应进 sessionStorage
20+
- 不同进程对 `@shared/*` 类型的使用是否一致
21+
22+
## 避免的建议
23+
24+
- 不要建议改 Prettier 风格(双引号 / 分号 / 100 列 / 末尾逗号已锁定)
25+
- 不要建议手写原生模块类型,必须从 `@splayer/<module>` 导入
26+
- 不要建议在渲染端直接 `import log from "electron-log"` 或在主进程绕过 `@main/utils/logger`
27+
- 不要建议为"防御性编程"添加内部代码的运行时校验(只在系统边界校验)
28+
- 不要建议拆分三行的类似代码为抽象,除非有三个以上真实用例
29+
30+
## 评论风格
31+
32+
- 直接指出问题所在文件 / 行号 / 变量名,配可执行的修法
33+
- 区分「必修」「建议」「风格」,便于作者决定优先级
34+
- 若和项目现有模式有冲突,**先查 `CLAUDE.md``.github/copilot-instructions.md`** 确认约定再提

electron/main/window/taskbarLyric.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,11 @@ let themeRegWatcher: RegistryWatcher | null = null;
3838
let uiaWatcher: UiaWatcher | null = null;
3939
let trayWatcher: TrayWatcher | null = null;
4040

41-
/** 传给 Rust 的 lyric_width:Win10 据此从 tasklist 划空间,Win11 忽略 */
42-
const currentWidth = 300;
41+
/** 从设置读取当前歌词宽度(Win10 据此从 tasklist 划空间,Win11 忽略) */
42+
const resolveLyricWidth = (): number => {
43+
const width = store.get("taskbarLyric.maxWidth");
44+
return typeof width === "number" && width > 0 ? width : 400;
45+
};
4346

4447
/**
4548
* 初始窗口尺寸——故意设大,覆盖任何可能的任务栏宽度/高度。
@@ -120,7 +123,7 @@ const applyLayout = (layout: JsTaskbarLayout): void => {
120123

121124
/** Watcher 回调——任何任务栏相关变化都回到这里重算布局 */
122125
const onLayoutChange = (): void => {
123-
service?.update(currentWidth);
126+
service?.update(resolveLyricWidth());
124127
};
125128

126129
/** 安全创建原生 watcher,失败只 warn 不中断启动 */
@@ -198,7 +201,7 @@ export const createTaskbarLyricWindow = (): BrowserWindow | null => {
198201
const hwndPtr = Number(win.getNativeWindowHandle().readBigUInt64LE(0));
199202
taskbarLog.info(`嵌入窗口 hwnd=${hwndPtr}`);
200203
svc.embedWindowByPtr(hwndPtr);
201-
svc.update(currentWidth);
204+
svc.update(resolveLyricWidth());
202205

203206
advancedRegWatcher = tryStart(
204207
"RegistryWatcher(Advanced)",
@@ -239,27 +242,24 @@ const cleanupWatchers = (): void => {
239242
service = null;
240243
};
241244

242-
/** 关闭任务栏歌词窗口并清理所有资源 */
245+
/** 请求关闭任务栏歌词窗口,实际清理由 "closed" 事件统一处理 */
243246
export const closeTaskbarLyricWindow = (): void => {
244247
if (taskbarLyricWindow && !taskbarLyricWindow.isDestroyed()) {
245248
taskbarLyricWindow.close();
246249
}
247-
taskbarLyricWindow = null;
248-
cleanupWatchers();
249-
setTrayTaskbarLyric(false);
250-
broadcast("taskbarLyric:visibilityChange", false);
251250
};
252251

253-
/** 切换任务栏歌词窗口显隐 */
254-
export const toggleTaskbarLyricWindow = (): void => {
252+
/** 切换任务栏歌词窗口显隐,返回切换后是否打开 */
253+
export const toggleTaskbarLyricWindow = (): boolean => {
255254
if (taskbarLyricWindow && !taskbarLyricWindow.isDestroyed()) {
256255
closeTaskbarLyricWindow();
257-
} else {
258-
createTaskbarLyricWindow();
256+
return false;
259257
}
258+
createTaskbarLyricWindow();
259+
return true;
260260
};
261261

262-
/** 触发一次布局重算 */
262+
/** 触发一次布局重算(配置变更后调用) */
263263
export const applyTaskbarLyricLayout = (): void => {
264-
service?.update(currentWidth);
264+
service?.update(resolveLyricWidth());
265265
};

native/taskbar-lyric/src/tray_watcher.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,15 @@ unsafe extern "system" fn win_event_proc(
3737
let len = unsafe { GetClassNameW(hwnd, &mut buffer) };
3838
if len > 0 {
3939
let name = String::from_utf16_lossy(&buffer[..len as usize]);
40-
if name == "TrayNotifyWnd"
41-
&& let Ok(guard) = GLOBAL_CALLBACK.lock()
42-
&& let Some(cb) = guard.as_ref()
43-
{
44-
cb();
40+
if name == "TrayNotifyWnd" {
41+
// 先 clone Arc 再释放锁,避免回调执行期间阻塞其它 GLOBAL_CALLBACK 访问
42+
let callback = GLOBAL_CALLBACK
43+
.lock()
44+
.ok()
45+
.and_then(|guard| guard.as_ref().cloned());
46+
if let Some(cb) = callback {
47+
cb();
48+
}
4549
}
4650
}
4751
}

native/taskbar-lyric/src/uia_watcher.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,9 @@ impl UiaWatcher {
9696
let callback_arc = Arc::new(callback);
9797

9898
thread::spawn(move || unsafe {
99-
let _ = CoInitializeEx(None, COINIT_MULTITHREADED);
99+
let hr = CoInitializeEx(None, COINIT_MULTITHREADED);
100+
// S_OK / S_FALSE 需要配对 CoUninitialize;RPC_E_CHANGED_MODE 或其它错误则不应调用
101+
let should_uninitialize = hr.is_ok();
100102

101103
let thread_id = GetCurrentThreadId();
102104
let _ = tx.send(thread_id);
@@ -145,7 +147,9 @@ impl UiaWatcher {
145147
}
146148

147149
drop(_handlers_guard);
148-
CoUninitialize();
150+
if should_uninitialize {
151+
CoUninitialize();
152+
}
149153
});
150154

151155
let thread_id = rx.recv().map_err(|e| anyhow!("获取线程 ID 失败: {e}"))?;

0 commit comments

Comments
 (0)