Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new “Dynamic Island” (灵动岛) floating lyric window for Windows/Electron, and refactors lyric timing utilities to support high-frequency word highlighting with less reactive overhead.
Changes:
- Introduces a new
windows/dynamic-islandrenderer entry (Vue) with drag/mode behavior and lyric rendering. - Adds main-process window management + IPC + tray + settings wiring for Dynamic Island enable/config/visibility.
- Extracts lyric sync helpers into
shared/utils/lyricSync.tsand updates desktop-lyric to use the shared utilities and a non-reactive playback cursor getter.
Reviewed changes
Copilot reviewed 31 out of 32 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| windows/dynamic-island/main.ts | New Vue entrypoint mounting the Dynamic Island app. |
| windows/dynamic-island/index.html | New HTML entry for the Dynamic Island renderer window. |
| windows/dynamic-island/composables/useNowPlayingSync.ts | Snapshot + position sync + RAF anchor interpolation for Dynamic Island. |
| windows/dynamic-island/composables/useDragWindow.ts | Pointer-based window dragging with rAF throttling and state save. |
| windows/dynamic-island/components/IslandLyricLine.vue | Word-by-word gradient highlighting driven by a non-reactive currentMs getter. |
| windows/dynamic-island/App.vue | Dynamic Island UI layout, sizing, mode transitions, and config application. |
| windows/desktop-lyric/utils.ts | Removes lyric sync helpers now moved to shared utils. |
| windows/desktop-lyric/composables/useNowPlayingSync.ts | Switches to shared lyric sync helpers; exposes non-reactive currentMs getter and removes currentMs ref. |
| windows/desktop-lyric/components/LyricLine.vue | Moves scroll + word highlight to rAF-driven DOM style updates using currentMs getter; removes currentMs prop. |
| windows/desktop-lyric/App.vue | Updates component import/name and removes passing currentMs; updates settings deep-link category. |
| src/stores/settings.ts | Tracks Dynamic Island open state and subscribes to Dynamic Island config + visibility updates. |
| src/settings/virtualBindings.ts | Adds virtual binding to toggle Dynamic Island window from settings. |
| src/settings/schema.ts | Adds “externalLyric” category and Dynamic Island settings section/items. |
| src/i18n/locales/zh-CN.json | Adds category/section labels + Dynamic Island setting strings (CN). |
| src/i18n/locales/en-US.json | Adds category/section labels + Dynamic Island setting strings (EN). |
| src/components/player/FullPlayer/index.vue | Removes debug/extra classes and adjusts comments for lyric ref/mount state. |
| shared/utils/lyricSync.ts | New shared lyric sync utilities: primary/latest index selection + last-line end clamping. |
| shared/types/window.ts | Extends preload Window API types to include Dynamic Island controls + DynamicIslandApi. |
| shared/types/settings.ts | Adds DynamicIslandSettings and persisted windowStates.dynamicIsland. |
| shared/defaults/settings.ts | Adds base height constant + default Dynamic Island config and window state. |
| package.json | Updates pnpm version in packageManager. |
| electron/preload/index.ts | Adds preload APIs for Dynamic Island window + config/mode/cursor events; removes removeAllListeners in subscribe helper. |
| electron/preload/index.d.ts | Exposes window.api.dynamicIsland typings. |
| electron/main/window/main.ts | Adjusts tray/thumbar init timing and passes main window into thumbar initializer. |
| electron/main/window/index.ts | Re-exports Dynamic Island window functions. |
| electron/main/window/dynamicIsland.ts | Implements main-process Dynamic Island window creation, sizing, snapping, cursor polling, IPC handlers, and persistence. |
| electron/main/utils/i18n.ts | Adds tray strings for opening/closing Dynamic Island. |
| electron/main/services/tray.ts | Adds tray menu item + state sync for Dynamic Island window. |
| electron/main/services/thumbar.ts | Changes initThumbar signature to accept a BrowserWindow parameter. |
| electron/main/ipc/window.ts | Adds IPC handlers for Dynamic Island toggle/close/open state + drag/resize/height/mode query. |
| electron/main/ipc/config.ts | Applies/broadcasts Dynamic Island config changes (always-on-top, snapCentered, nonOcclusive). |
| electron.vite.config.ts | Adds Dynamic Island window HTML as a renderer build input. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| wordByWord: true, | ||
| playedColor: "#rgba(255, 255, 255, 1)", | ||
| unplayedColor: "rgba(255, 255, 255, 0.5)", | ||
| backgroundColor: "rgba(0, 0, 0, 1)", |
There was a problem hiding this comment.
dynamicIsland.playedColor is set to #rgba(255, 255, 255, 1), which is not a valid CSS color. This will break the CSS variable --di-played and can make the Dynamic Island text invisible or incorrectly colored. Use rgba(255, 255, 255, 1) or a hex color like #ffffff.
There was a problem hiding this comment.
@copilot apply changes based on this feedback
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 31 out of 32 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // 订阅灵动岛配置变化 | ||
| onConfigChange: (callback: (config: unknown) => void) => | ||
| subscribe("dynamicIsland:configChange", callback), |
There was a problem hiding this comment.
dynamicIsland.onConfigChange currently types the callback payload as unknown, which loses type-safety and is incompatible with the DynamicIslandApi contract (expects DynamicIslandSettings). Import and use the proper type for the callback payload so renderer consumers get correct typing.
| const renderFrame = (): void => { | ||
| if (props.wordByWord) { | ||
| const currentMs = getNowPlayingCurrentMs(); | ||
| for (let i = 0; i < props.line.words.length; i++) { | ||
| const el = wordRefs[i]; | ||
| if (!el) continue; | ||
| const progress = getWordProgress(props.line.words[i], currentMs); | ||
| if (lastWordProgress[i] !== progress) { | ||
| lastWordProgress[i] = progress; | ||
| el.style.setProperty("--p", progress); | ||
| } | ||
| } | ||
| } | ||
| rafId = requestAnimationFrame(renderFrame); | ||
| }; | ||
|
|
||
| watch(() => [props.wordByWord, props.line], resetRenderCache); | ||
|
|
||
| onMounted(() => { | ||
| renderFrame(); | ||
| }); | ||
|
|
||
| onBeforeUnmount(() => { | ||
| if (rafId !== 0) { | ||
| cancelAnimationFrame(rafId); | ||
| rafId = 0; | ||
| } |
There was a problem hiding this comment.
renderFrame() schedules a new requestAnimationFrame unconditionally, even when wordByWord is false (no per-frame updates occur). This keeps a permanent 60fps loop running for no benefit. Consider starting/stopping the RAF loop based on wordByWord (and canceling when disabled) to reduce CPU usage.
| const renderFrame = (): void => { | |
| if (props.wordByWord) { | |
| const currentMs = getNowPlayingCurrentMs(); | |
| for (let i = 0; i < props.line.words.length; i++) { | |
| const el = wordRefs[i]; | |
| if (!el) continue; | |
| const progress = getWordProgress(props.line.words[i], currentMs); | |
| if (lastWordProgress[i] !== progress) { | |
| lastWordProgress[i] = progress; | |
| el.style.setProperty("--p", progress); | |
| } | |
| } | |
| } | |
| rafId = requestAnimationFrame(renderFrame); | |
| }; | |
| watch(() => [props.wordByWord, props.line], resetRenderCache); | |
| onMounted(() => { | |
| renderFrame(); | |
| }); | |
| onBeforeUnmount(() => { | |
| if (rafId !== 0) { | |
| cancelAnimationFrame(rafId); | |
| rafId = 0; | |
| } | |
| const stopRenderLoop = (): void => { | |
| if (rafId !== 0) { | |
| cancelAnimationFrame(rafId); | |
| rafId = 0; | |
| } | |
| }; | |
| const renderFrame = (): void => { | |
| if (!props.wordByWord) { | |
| rafId = 0; | |
| return; | |
| } | |
| const currentMs = getNowPlayingCurrentMs(); | |
| for (let i = 0; i < props.line.words.length; i++) { | |
| const el = wordRefs[i]; | |
| if (!el) continue; | |
| const progress = getWordProgress(props.line.words[i], currentMs); | |
| if (lastWordProgress[i] !== progress) { | |
| lastWordProgress[i] = progress; | |
| el.style.setProperty("--p", progress); | |
| } | |
| } | |
| rafId = requestAnimationFrame(renderFrame); | |
| }; | |
| const startRenderLoop = (): void => { | |
| if (rafId === 0 && props.wordByWord) { | |
| renderFrame(); | |
| } | |
| }; | |
| watch( | |
| () => props.wordByWord, | |
| (wordByWord) => { | |
| resetRenderCache(); | |
| if (wordByWord) { | |
| startRenderLoop(); | |
| } else { | |
| stopRenderLoop(); | |
| } | |
| }, | |
| ); | |
| watch(() => props.line, resetRenderCache); | |
| onMounted(() => { | |
| startRenderLoop(); | |
| }); | |
| onBeforeUnmount(() => { | |
| stopRenderLoop(); |
| measureCtx.font = `${config.fontWeight} ${sizePx}px ${family}`; | ||
| return Math.ceil(measureCtx.measureText(text).width); | ||
| }; | ||
|
|
There was a problem hiding this comment.
config.scale and config.fontWeight affect fontSize/padding/cover size and also the measured text width (measureTextWidth), but there is no watcher that recomputes lyricWidth + calls resizeWindow() when these values change. As a result, adjusting scale/font weight from settings can leave the window width stale and cause clipping/misalignment. Consider watching the relevant sizing inputs (e.g. config.scale, config.fontWeight, maybe fontSize) and re-running measureTarget() + resizeWindow() (ideally without triggering the shrink/expand swap animation).
| const refreshMeasuredLayout = async () => { | |
| await nextTick(); | |
| if (typeof measureTarget === "function") { | |
| measureTarget(); | |
| } | |
| if (typeof resizeWindow === "function") { | |
| resizeWindow(); | |
| } | |
| }; | |
| watch( | |
| () => [config.scale, config.fontWeight, fontSize.value], | |
| () => { | |
| refreshMeasuredLayout(); | |
| }, | |
| { flush: "post" }, | |
| ); |
Agent-Logs-Url: https://github.com/SPlayer-Dev/SPlayer-Next/sessions/d7d72eb9-46a2-4858-abda-901a0ab14060 Co-authored-by: imsyy <42232682+imsyy@users.noreply.github.com>
|
Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details. Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
- IslandLyricLine: wordByWord=false 时停止 RAF,避免 60fps 空转 - App: 监听 scale/fontWeight 变化,即时重测并 resize 窗口宽度 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 31 out of 32 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

No description provided.