Skip to content

feat: 任务栏歌词#7

Merged
imsyy merged 14 commits intodevfrom
taskbar-lyric
Apr 19, 2026
Merged

feat: 任务栏歌词#7
imsyy merged 14 commits intodevfrom
taskbar-lyric

Conversation

@imsyy
Copy link
Copy Markdown
Collaborator

@imsyy imsyy commented Apr 18, 2026

No description provided.

Copilot AI review requested due to automatic review settings April 18, 2026 16:04
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new Windows “Taskbar Lyric” feature by embedding an Electron BrowserWindow into the Windows taskbar via a new Rust N-API module, plus associated settings/UI wiring and some shared lyric-sync refactoring.

Changes:

  • Introduce windows/taskbar-lyric renderer entry (Vue UI + karaoke/scrolling line component).
  • Add a new native module native/taskbar-lyric (Win10/Win11 strategies, UIA/tray/registry watchers) and integrate it into build/packaging.
  • Refactor useNowPlayingSync into a shared composable with pluggable index-picking + log tag, and update Desktop Lyric / Dynamic Island to use it.

Reviewed changes

Copilot reviewed 51 out of 55 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
windows/taskbar-lyric/main.ts New Vue entrypoint for taskbar lyric window
windows/taskbar-lyric/index.html New HTML entry for taskbar lyric window
windows/taskbar-lyric/components/TaskbarLyricLine.vue New scrolling + word-by-word highlight lyric line renderer
windows/taskbar-lyric/App.vue New taskbar lyric UI (cover, controls, double-line rendering, theme alignment)
windows/shared/composables/useNowPlayingSync.ts Shared now-playing sync with configurable index picker + log tag
windows/dynamic-island/components/IslandLyricLine.vue Update to use shared getNowPlayingCurrentMs import
windows/dynamic-island/App.vue Switch to shared useNowPlayingSync with pickLatestStartedIndex
windows/desktop-lyric/composables/useNowPlayingSync.ts Remove local now-playing sync implementation
windows/desktop-lyric/components/LyricLine.vue Update to use shared getNowPlayingCurrentMs import
windows/desktop-lyric/App.vue Switch to shared useNowPlayingSync with pickPrimaryIndex
tsconfig.web.json Add @windows/* path alias for renderer
tsconfig.node.json Add @splayer/taskbar-lyric path alias for node/main tooling
src/types/settings-schema.ts Add tag support to setting items/sections
src/stores/settings.ts Track isTaskbarLyricOpen and subscribe to visibility updates
src/settings/virtualBindings.ts Add virtual binding to toggle taskbar lyric window
src/settings/schema.ts Add Taskbar Lyric settings section (Windows-only) + Beta tags
src/i18n/locales/zh-CN.json Add zh-CN strings for taskbar lyric settings
src/i18n/locales/en-US.json Add en-US strings for taskbar lyric settings
src/components/ui/STag.vue New UI tag/badge component
src/components/settings/SettingsSection.vue Render section-level tag badge
src/components/settings/SettingsItem.vue Render item-level tag badge
shared/types/window.ts Extend window API types for taskbar lyric + events
shared/types/settings.ts Add TaskbarLyricSettings + attach to SystemConfig
shared/defaults/settings.ts Add default taskbar lyric config
scripts/build-native.ts Add native build target for taskbar-lyric (win32-only)
native/taskbar-lyric/src/utils.rs Win32 helpers (registry, taskbar HWND, style mutation, theme detection)
native/taskbar-lyric/src/uia_watcher.rs UIA-based taskbar layout change watcher
native/taskbar-lyric/src/uia.rs UIA taskbar scanner for Win11 bridge content bounds
native/taskbar-lyric/src/tray_watcher.rs WinEvent hook watcher for tray location changes
native/taskbar-lyric/src/strategy/win11.rs Win11 embedding + layout computation strategy
native/taskbar-lyric/src/strategy/win10.rs Win10 embedding + rebar/tasklist carving strategy
native/taskbar-lyric/src/strategy/mod.rs Strategy trait + shared layout structs
native/taskbar-lyric/src/logger.rs No-op logging macros for the native module
native/taskbar-lyric/src/lib.rs N-API exports (TaskbarService, RegistryWatcher, UIA/Tray watchers) + worker loop
native/taskbar-lyric/package.json Native module package metadata/scripts
native/taskbar-lyric/index.d.ts Generated TS typings for the native module
native/taskbar-lyric/build.rs NAPI build setup
native/taskbar-lyric/Cargo.toml New Rust crate definition + deps/features
electron/preload/index.ts Expose taskbar lyric window + taskbar lyric event APIs to renderer
electron/preload/index.d.ts Add TaskbarLyricApi to window.api typing
electron/main/window/taskbarLyric.ts Main-process window creation/embedding + watcher wiring + layout application
electron/main/window/index.ts Export taskbar lyric window helpers from window module
electron/main/window/dynamicIsland.ts Update BrowserWindow flags (disable min/max/fullscreen)
electron/main/window/desktopLyric.ts Update BrowserWindow flags (disable min/max/fullscreen)
electron/main/utils/nativeLoader.ts Use centralized nativeLog logger scope
electron/main/utils/logger.ts Add taskbarLog and nativeLog scopes
electron/main/utils/i18n.ts Add tray labels for taskbar lyric open/close
electron/main/services/tray.ts Add tray menu item for taskbar lyric (Windows-only)
electron/main/ipc/window.ts Add IPC handlers for taskbar lyric toggling/closing/query
electron/main/ipc/config.ts Apply/broadcast taskbar lyric config changes + trigger relayout
electron.vite.config.ts Add new renderer entry + aliases (@windows, native module alias)
electron-builder.config.ts Package taskbar-lyric .node into app resources
components.d.ts Register STag for auto component typing
Cargo.toml Add taskbar-lyric crate to workspace
Cargo.lock Lock new dependencies for taskbar-lyric crate

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread electron/main/window/taskbarLyric.ts Outdated
Comment on lines +242 to +250
/** 关闭任务栏歌词窗口并清理所有资源 */
export const closeTaskbarLyricWindow = (): void => {
if (taskbarLyricWindow && !taskbarLyricWindow.isDestroyed()) {
taskbarLyricWindow.close();
}
taskbarLyricWindow = null;
cleanupWatchers();
setTrayTaskbarLyric(false);
broadcast("taskbarLyric:visibilityChange", false);
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

closeTaskbarLyricWindow() sets taskbarLyricWindow = null, stops watchers, updates tray state, and broadcasts visibility change immediately after calling win.close(), but the window may still be alive until the "closed" event fires. This also duplicates the same cleanup/broadcast logic already registered in the "closed" handler, which can lead to double stop() calls and inconsistent state if the close is prevented. Align this with the Desktop Lyric / Dynamic Island patterns: let the "closed" handler own teardown/state updates (or call destroy() if immediate teardown is required).

Suggested change
/** 关闭任务栏歌词窗口并清理所有资源 */
export const closeTaskbarLyricWindow = (): void => {
if (taskbarLyricWindow && !taskbarLyricWindow.isDestroyed()) {
taskbarLyricWindow.close();
}
taskbarLyricWindow = null;
cleanupWatchers();
setTrayTaskbarLyric(false);
broadcast("taskbarLyric:visibilityChange", false);
/** 请求关闭任务栏歌词窗口,实际清理由窗口的 "closed" 事件统一处理 */
export const closeTaskbarLyricWindow = (): void => {
if (taskbarLyricWindow && !taskbarLyricWindow.isDestroyed()) {
taskbarLyricWindow.close();
}

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +44
if name == "TrayNotifyWnd"
&& let Ok(guard) = GLOBAL_CALLBACK.lock()
&& let Some(cb) = guard.as_ref()
{
cb();
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

The tray event hook calls cb() while holding the GLOBAL_CALLBACK mutex. If the callback is slow or re-entrant, this can unnecessarily block other lock users and increases deadlock risk. Prefer cloning the Arc (or extracting a raw function pointer) while locked, then dropping the guard before invoking the callback.

Suggested change
if name == "TrayNotifyWnd"
&& let Ok(guard) = GLOBAL_CALLBACK.lock()
&& let Some(cb) = guard.as_ref()
{
cb();
if name == "TrayNotifyWnd" {
let callback = GLOBAL_CALLBACK
.lock()
.ok()
.and_then(|guard| guard.as_ref().cloned());
if let Some(cb) = callback {
cb();
}

Copilot uses AI. Check for mistakes.
Comment on lines +98 to +100
thread::spawn(move || unsafe {
let _ = CoInitializeEx(None, COINIT_MULTITHREADED);

Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

CoInitializeEx's result is ignored, but CoUninitialize() is called unconditionally at the end of the thread. CoUninitialize should only be called if COM initialization succeeded (and not when CoInitializeEx returns errors like RPC_E_CHANGED_MODE). Track the HRESULT/boolean and only uninitialize when appropriate (similar to TaskbarScanner's should_uninitialize handling).

Copilot uses AI. Check for mistakes.
import { t } from "@main/utils/i18n";
import { trayLog } from "@main/utils/logger";
import { toggleDesktopLyricWindow, toggleDynamicIslandWindow, focusMainWindow } from "@main/window";
import {
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

This import line is significantly longer than the surrounding imports and likely to be reformatted by the project's formatter/linter. Splitting the named imports across multiple lines will improve readability and avoid churn in future diffs.

Copilot uses AI. Check for mistakes.
});

// 切换任务栏歌词窗口
ipcMain.handle("window:toggleTaskbarLyric", () => toggleTaskbarLyricWindow());
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

ipcMain.handle("window:toggleTaskbarLyric") currently returns the result of toggleTaskbarLyricWindow(), but that function returns void. This makes window.api.window.toggleTaskbarLyric() resolve to undefined even though WindowApi.toggleTaskbarLyric is typed as Promise<boolean>. Please return a boolean open/close result (consistent with toggleDesktopLyricWindow / toggleDynamicIslandWindow) and forward that value from the IPC handler.

Suggested change
ipcMain.handle("window:toggleTaskbarLyric", () => toggleTaskbarLyricWindow());
ipcMain.handle("window:toggleTaskbarLyric", () => {
toggleTaskbarLyricWindow();
return !!getTaskbarLyricWindow();
});

Copilot uses AI. Check for mistakes.
Comment thread electron/main/window/taskbarLyric.ts Outdated
Comment on lines +42 to +43
const currentWidth = 300;

Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

currentWidth is a hard-coded constant, but it's used as the lyric_width parameter for the native module (Win10 uses it to carve space). This means user settings like taskbarLyric.maxWidth / taskbarLyric.autoMaxWidth won't actually affect the reserved taskbar space on Win10, and applyTaskbarLyricLayout() will never reflect those config changes. Consider deriving the update width from the current config (e.g., use maxWidth when autoMaxWidth is false) and updating the native layout whenever config changes.

Suggested change
const currentWidth = 300;
const DEFAULT_LYRIC_WIDTH = 300;
type TaskbarLyricStoreSettings = {
autoMaxWidth?: boolean;
maxWidth?: number;
};
const readTaskbarLyricSettings = (): TaskbarLyricStoreSettings => {
const typedStore = store as {
get?: (key: string) => unknown;
[key: string]: unknown;
};
const settings =
typeof typedStore.get === "function"
? typedStore.get("taskbarLyric")
: typedStore.taskbarLyric;
return settings && typeof settings === "object"
? (settings as TaskbarLyricStoreSettings)
: {};
};
const resolveCurrentWidth = (): number => {
const { autoMaxWidth, maxWidth } = readTaskbarLyricSettings();
if (autoMaxWidth === false) {
const width = Number(maxWidth);
if (Number.isFinite(width) && width > 0) {
return width;
}
}
return DEFAULT_LYRIC_WIDTH;
};
let currentWidth = resolveCurrentWidth();
const refreshTaskbarLyricWidth = (): void => {
currentWidth = resolveCurrentWidth();
};
const applyTaskbarLyricLayoutIfAvailable = (): void => {
const layoutApplier = (
globalThis as typeof globalThis & {
applyTaskbarLyricLayout?: () => void | Promise<void>;
}
).applyTaskbarLyricLayout;
if (typeof layoutApplier === "function") {
void layoutApplier();
}
};
const registerTaskbarLyricConfigListener = (): void => {
const typedStore = store as {
onDidChange?: (key: string, listener: () => void) => unknown;
onDidAnyChange?: (listener: () => void) => unknown;
subscribe?: (listener: () => void) => unknown;
};
const onChange = (): void => {
refreshTaskbarLyricWidth();
applyTaskbarLyricLayoutIfAvailable();
};
if (typeof typedStore.onDidChange === "function") {
typedStore.onDidChange("taskbarLyric", onChange);
return;
}
if (typeof typedStore.onDidAnyChange === "function") {
typedStore.onDidAnyChange(onChange);
return;
}
if (typeof typedStore.subscribe === "function") {
typedStore.subscribe(onChange);
}
};
registerTaskbarLyricConfigListener();

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings April 19, 2026 05:56
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 54 out of 58 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +60 to +64
let callback_arc = Arc::new(callback);

if let Ok(mut guard) = GLOBAL_CALLBACK.lock() {
*guard = Some(callback_arc);
}
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

TrayWatcher::new() 会把回调存到全局 GLOBAL_CALLBACK,但 stop()/Drop 没有把它清空,导致 watcher 停止后回调闭包仍被静态 Arc 持有(资源无法释放)。建议在 stop()(或 Drop)里把 GLOBAL_CALLBACK 置回 None(与 TaskbarCreatedWatcher 的清理逻辑对齐)。

Copilot uses AI. Check for mistakes.
Comment thread native/taskbar-lyric/src/lib.rs Outdated
Comment on lines +188 to +192
unsafe {
let hr = CoInitializeEx(None, COINIT_MULTITHREADED);
if hr.is_err() {
return;
}
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

worker_loopCoInitializeEx 只要返回 Err 就直接退出,但 RPC_E_CHANGED_MODE 在其它实现(如 uia.rs)属于“可继续运行但不需要 CoUninitialize”的情况。这里直接 return 会让服务在少数环境下静默失效。建议参照 TaskbarScanner::new()should_uninitialize 模式:对 RPC_E_CHANGED_MODE 允许继续,同时只在真正初始化成功时才调用 CoUninitialize()

Copilot uses AI. Check for mistakes.
Comment thread electron/main/window/taskbarLyric.ts Outdated
Comment on lines +289 to +290
createTaskbarLyricWindow();
return true;
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

toggleTaskbarLyricWindow() 在非 Windows 平台会走到 createTaskbarLyricWindow(); return true;,但 createTaskbarLyricWindow() 此时返回 null,导致 IPC 调用得到“已打开”的假状态。建议在 toggle 内先判 process.platform !== "win32" 直接返回 false,或根据 createTaskbarLyricWindow() 的返回值决定最终返回值。

Suggested change
createTaskbarLyricWindow();
return true;
if (process.platform !== "win32") {
return false;
}
return createTaskbarLyricWindow() !== null;

Copilot uses AI. Check for mistakes.
Comment thread electron/main/window/taskbarLyric.ts Outdated
Comment on lines +241 to +243
// Windows 上 HWND 是 64 位指针(x64),读为 BigInt 再转 number
const hwndPtr = Number(win.getNativeWindowHandle().readBigUInt64LE(0));
taskbarLog.info(`嵌入窗口 hwnd=${hwndPtr}`);
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

这里把 HWND(u64)从 BigInt 转成 Number 再传给原生模块,存在精度丢失风险(超过 2^53 时会截断),可能导致嵌入失败且难定位。建议把 NAPI 方法参数改为 BigInt,或直接接收 Buffer/Uint8Array(从 getNativeWindowHandle() 直接传递)并在 Rust 侧解析为 usize,避免 JS number 精度问题。

Suggested change
// Windows 上 HWND 是 64 位指针(x64),读为 BigInt 再转 number
const hwndPtr = Number(win.getNativeWindowHandle().readBigUInt64LE(0));
taskbarLog.info(`嵌入窗口 hwnd=${hwndPtr}`);
// Windows 上 HWND 可能是 64 位值,先保留为 BigInt,避免直接转成 number 产生静默精度丢失
const hwndPtrBigInt = win.getNativeWindowHandle().readBigUInt64LE(0);
if (hwndPtrBigInt > BigInt(Number.MAX_SAFE_INTEGER)) {
taskbarLog.error(
`嵌入窗口失败:hwnd=${hwndPtrBigInt.toString()} 超出 JS Number 安全整数范围,无法安全传递给原生模块`,
);
return;
}
const hwndPtr = Number(hwndPtrBigInt);
taskbarLog.info(`嵌入窗口 hwnd=${hwndPtrBigInt.toString()}`);

Copilot uses AI. Check for mistakes.
Comment thread electron/preload/index.ts Outdated
Comment on lines +211 to +212
onConfigChange: (callback: (config: unknown) => void) =>
subscribe("taskbarLyric:configChange", callback),
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

taskbarLyric.onConfigChange 这里把回调参数标成 unknown,但 electron/preload/index.d.tsTaskbarLyricApi.onConfigChange 的回调类型是 TaskbarLyricSettings。实现与类型声明不一致,后续改动时更容易把非预期数据透传到渲染端而不自知。建议在 preload 侧引入 TaskbarLyricSettings 并使用 subscribe<TaskbarLyricSettings>("taskbarLyric:configChange", ...),保持实现与共享类型一致。

Suggested change
onConfigChange: (callback: (config: unknown) => void) =>
subscribe("taskbarLyric:configChange", callback),
onConfigChange: (callback: (config: TaskbarLyricSettings) => void) =>
subscribe<TaskbarLyricSettings>("taskbarLyric:configChange", callback),

Copilot uses AI. Check for mistakes.
@imsyy
Copy link
Copy Markdown
Collaborator Author

imsyy commented Apr 19, 2026

@copilot 审查

Copilot AI review requested due to automatic review settings April 19, 2026 07:35
@imsyy imsyy review requested due to automatic review settings April 19, 2026 07:35
Copilot AI review requested due to automatic review settings April 19, 2026 07:36
@imsyy imsyy review requested due to automatic review settings April 19, 2026 07:36
Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 19, 2026

@copilot 审查

审查完毕,共审查 58 个变更文件。发现并修复了 3 个问题(97d5f4f):

必修:

  1. HWND BigInt → Number 精度丢失taskbarLyric.ts:241-248):64 位 HWND 指针直接 Number() 转换可能超出 MAX_SAFE_INTEGER 导致精度截断,添加了安全检查。
  2. uia_watcher.rs CoInitializeEx 错误处理不一致uia_watcher.rs:99-111):原代码未区分 RPC_E_CHANGED_MODE(可继续)与其它 COM 错误(应退出),现已对齐 lib.rsshould_uninitialize 模式。
  3. taskbar_created_watcher.rs 锁内 Arc 创建taskbar_created_watcher.rs:61):Arc::new() 在持锁期间执行,已移到锁外,与 tray_watcher.rs 模式对齐。

确认已处理的历史审查项:

  • closeTaskbarLyricWindow() 清理交给 closed 事件处理
  • ✅ Tray watcher 回调先 clone Arc 再释放锁
  • ✅ UIA watcher should_uninitialize 模式
  • ✅ worker_loop RPC_E_CHANGED_MODE 处理
  • ✅ toggle 非 Windows 平台返回 false
  • ✅ TrayWatcher stop() 清空 GLOBAL_CALLBACK
  • ✅ preload onConfigChange 使用 TaskbarLyricSettings 类型
  • ✅ 配置宽度从 store 读取(resolveLyricWidth

@imsyy imsyy merged commit a9f6aff into dev Apr 19, 2026
1 of 2 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.

3 participants