From b7a3663078e5833c28579d1464da6feb50b7ee74 Mon Sep 17 00:00:00 2001 From: lyswhut Date: Tue, 5 May 2026 14:50:34 +0800 Subject: [PATCH 01/49] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20kg=20=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E7=BB=93=E6=9E=9C=E6=98=BE=E7=A4=BA=E9=97=AE=E9=A2=98?= =?UTF-8?q?=EF=BC=88#2782=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- publish/changeLog.md | 16 +--------------- src/renderer/utils/musicSdk/kg/musicSearch.js | 2 +- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/publish/changeLog.md b/publish/changeLog.md index 5f98247cdc..8949e79008 100644 --- a/publish/changeLog.md +++ b/publish/changeLog.md @@ -1,17 +1,3 @@ -我们很高兴地宣布新项目 Any Listen 的桌面版已发布,目前已支持列表跟随本地文件自动更新、加载并播放WebDAV上的歌曲等功能,更多功能仍在积极开发中,桌面版与Web版将同步更新。 -对于有播放本地音乐或播放服务器上音乐需求的人可以试试,若遇到任何问题可以发 issue 反馈。 - -### 优化 - -- 优化歌单内歌曲搜索结果排序 (#2734) - ### 修复 -- 修复桌面歌词的 鼠标移入歌词区域时提高透明度 设置不稳定的问题(#2679, @Little100) -- 修复某些情况下可能播放没有声音的问题(#2693) -- 修复 tx 搜索结果显示异常的问题(#2753) -- 修复音乐名称和歌手信息格式化问题(#2733) - -### 其他 - -- 更新 Electron 到 40.8.3 +- 修复 kg 搜索结果显示问题(#2782) diff --git a/src/renderer/utils/musicSdk/kg/musicSearch.js b/src/renderer/utils/musicSdk/kg/musicSearch.js index 9fa60ba148..dabb7073cd 100644 --- a/src/renderer/utils/musicSdk/kg/musicSearch.js +++ b/src/renderer/utils/musicSdk/kg/musicSearch.js @@ -49,7 +49,7 @@ export default { } return { singer: decodeName(formatSingerName(rawData.Singers, 'name')), - name: decodeName(rawData.SongName), + name: decodeName(`${rawData.OriSongName}${rawData.Suffix ? ` ${rawData.Suffix}` : ''}`), albumName: decodeName(rawData.AlbumName), albumId: rawData.AlbumID, songmid: rawData.Audioid, From be48e78a6fbed2159d5081c4d2743316007c0846 Mon Sep 17 00:00:00 2001 From: lyswhut Date: Tue, 5 May 2026 15:31:34 +0800 Subject: [PATCH 02/49] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BE=9D=E8=B5=96&?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E5=8F=B7=EF=BC=882.12.3-beta.0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 36 ++++++++++++++++++------------------ package.json | 10 +++++----- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1beb8ee6c8..371796d258 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "lx-music-desktop", - "version": "2.12.2", + "version": "2.12.3-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lx-music-desktop", - "version": "2.12.2", + "version": "2.12.3-beta.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -40,7 +40,7 @@ "@types/node": "^20.19.39", "@types/tunnel": "^0.0.7", "@types/ws": "8.5.4", - "@vue/language-plugin-pug": "^3.2.7", + "@vue/language-plugin-pug": "^3.2.8", "browserslist": "^4.28.2", "chalk": "^4.1.2", "changelog-parser": "^3.0.1", @@ -50,11 +50,11 @@ "css-loader": "^7.1.4", "css-minimizer-webpack-plugin": "^8.0.0", "del": "^6.1.1", - "electron": "40.9.2", + "electron": "40.9.3", "electron-builder": "^26.9.0", "electron-debug": "^3.2.0", "electron-devtools-installer": "github:lyswhut/electron-devtools-installer#64596d615c1fc891eefd8aef1dfcb2c87aaadf03", - "electron-to-chromium": "^1.5.345", + "electron-to-chromium": "^1.5.349", "electron-updater": "6.8.4", "eslint": "^8.57.1", "eslint-config-standard": "^17.1.0", @@ -69,7 +69,7 @@ "less-loader": "^12.3.2", "mini-css-extract-plugin": "^2.10.2", "node-loader": "^2.1.0", - "postcss": "^8.5.12", + "postcss": "^8.5.14", "postcss-loader": "^8.2.1", "postcss-pxtorem": "^6.1.0", "pug": "^3.0.4", @@ -2297,9 +2297,9 @@ "license": "MIT" }, "node_modules/@vue/language-plugin-pug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vue/language-plugin-pug/-/language-plugin-pug-3.2.7.tgz", - "integrity": "sha512-5D01iE73WYNZX38wGeU7qo0srDc1ATX6K+CAahJdDpxUIHPLE4AbUHP1lS790oT+ZLmlTqSBOFbTYDNtuKqA3A==", + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/@vue/language-plugin-pug/-/language-plugin-pug-3.2.8.tgz", + "integrity": "sha512-wF0RShZf/oY5wZjVZNsrNIuqNIKIxKA7UEVmGUIjRpHh0q6RwqzawaAeUu7Zp/TozO0rqGp0rfZRSyydhUF/9g==", "dev": true, "license": "MIT", "dependencies": { @@ -5529,9 +5529,9 @@ } }, "node_modules/electron": { - "version": "40.9.2", - "resolved": "https://registry.npmjs.org/electron/-/electron-40.9.2.tgz", - "integrity": "sha512-gTLLTlfMyORZDj+03tkxsstQOQlmu6dYl0X8cwlmFb+gMmCM9Gc+rmBGSaCb5KI11IMUWHu4hvKA/spP8oJe+w==", + "version": "40.9.3", + "resolved": "https://registry.npmjs.org/electron/-/electron-40.9.3.tgz", + "integrity": "sha512-rDcJOT6BBE689Ada+4jD3rVr05pMv9MZOgT0x/rIMVDF9c4ttx4RTb6lVARTyxZC7uqpirttCtcli1eg1DX5qg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5760,9 +5760,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.347", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.347.tgz", - "integrity": "sha512-BqbKWR67PjxFypgOFcDevD6j8N8GCPkSnQQRuqQIBh3GYCwr0xsLqw2EtSn83oq5iTqJ/wabM/YHV7KgvWGz7Q==", + "version": "1.5.349", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.349.tgz", + "integrity": "sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A==", "dev": true, "license": "ISC" }, @@ -10969,9 +10969,9 @@ } }, "node_modules/postcss": { - "version": "8.5.13", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz", - "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "funding": [ { "type": "opencollective", diff --git a/package.json b/package.json index cc041c3a5e..8a92c3e7df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lx-music-desktop", - "version": "2.12.2", + "version": "2.12.3-beta.0", "description": "一个免费的音乐查找助手", "main": "./dist/main.js", "scripts": { @@ -113,7 +113,7 @@ "@types/node": "^20.19.39", "@types/tunnel": "^0.0.7", "@types/ws": "8.5.4", - "@vue/language-plugin-pug": "^3.2.7", + "@vue/language-plugin-pug": "^3.2.8", "browserslist": "^4.28.2", "chalk": "^4.1.2", "changelog-parser": "^3.0.1", @@ -123,11 +123,11 @@ "css-loader": "^7.1.4", "css-minimizer-webpack-plugin": "^8.0.0", "del": "^6.1.1", - "electron": "40.9.2", + "electron": "40.9.3", "electron-builder": "^26.9.0", "electron-debug": "^3.2.0", "electron-devtools-installer": "github:lyswhut/electron-devtools-installer#64596d615c1fc891eefd8aef1dfcb2c87aaadf03", - "electron-to-chromium": "^1.5.345", + "electron-to-chromium": "^1.5.349", "electron-updater": "6.8.4", "eslint": "^8.57.1", "eslint-config-standard": "^17.1.0", @@ -142,7 +142,7 @@ "less-loader": "^12.3.2", "mini-css-extract-plugin": "^2.10.2", "node-loader": "^2.1.0", - "postcss": "^8.5.12", + "postcss": "^8.5.14", "postcss-loader": "^8.2.1", "postcss-pxtorem": "^6.1.0", "pug": "^3.0.4", From 8c4d5a15088cb8e05fbcbd7411290961e4d5a8c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E6=B3=A2?= Date: Thu, 7 May 2026 16:03:09 +0800 Subject: [PATCH 03/49] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E5=88=97=E8=A1=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build-config/tests/player-queue.test.mjs | 71 +++ src/lang/en-us.json | 5 + src/lang/zh-cn.json | 5 + src/lang/zh-tw.json | 5 + .../components/common/PlayQueueBtn.vue | 449 ++++++++++++++++++ .../components/layout/PlayBar/ControlBtns.vue | 1 + src/renderer/core/player/action.ts | 7 + src/renderer/core/player/queue.mjs | 67 +++ src/renderer/store/player/action.ts | 11 + src/renderer/store/player/state.ts | 2 + src/renderer/utils/compositions/useDrag.js | 5 +- .../songList/List/components/TagList.vue | 1 + 12 files changed, 628 insertions(+), 1 deletion(-) create mode 100644 build-config/tests/player-queue.test.mjs create mode 100644 src/renderer/components/common/PlayQueueBtn.vue create mode 100644 src/renderer/core/player/queue.mjs diff --git a/build-config/tests/player-queue.test.mjs b/build-config/tests/player-queue.test.mjs new file mode 100644 index 0000000000..f39e86428e --- /dev/null +++ b/build-config/tests/player-queue.test.mjs @@ -0,0 +1,71 @@ +import assert from 'node:assert/strict' + +import { + buildPlayQueueSections, + moveTempQueueItem, +} from '../../src/renderer/core/player/queue.mjs' + +const createMusic = (id, name) => ({ + id, + name, + singer: `${name} singer`, + source: 'kw', + interval: '03:00', + meta: { + albumName: `${name} album`, + }, +}) + +const run = (name, fn) => { + try { + fn() + console.log(`PASS ${name}`) + } catch (error) { + console.error(`FAIL ${name}`) + throw error + } +} + +run('buildPlayQueueSections creates temp and base sections with active item metadata', () => { + const tempPlayList = [ + { listId: 'list_a', musicInfo: createMusic('temp_1', 'Temp 1'), isTempPlay: true }, + { listId: 'list_b', musicInfo: createMusic('temp_2', 'Temp 2'), isTempPlay: true }, + ] + const baseList = [ + createMusic('song_1', 'Song 1'), + createMusic('song_2', 'Song 2'), + ] + + const sections = buildPlayQueueSections({ + tempPlayList, + baseList, + baseListId: 'list_a', + playMusicInfo: { + listId: 'list_b', + musicInfo: tempPlayList[1].musicInfo, + isTempPlay: true, + }, + }) + + assert.equal(sections.length, 2) + assert.deepEqual(sections.map(section => section.key), ['temp', 'base']) + assert.equal(sections[0].items[1].isActive, true) + assert.equal(sections[0].items[1].canRemove, true) + assert.equal(sections[0].items[1].canDrag, true) + assert.equal(sections[1].items[0].canRemove, false) + assert.equal(sections[1].items[0].canDrag, false) + assert.equal(sections[1].items[0].isActive, false) +}) + +run('moveTempQueueItem reorders temp queue immutably', () => { + const tempPlayList = [ + { listId: 'list_a', musicInfo: createMusic('temp_1', 'Temp 1'), isTempPlay: true }, + { listId: 'list_b', musicInfo: createMusic('temp_2', 'Temp 2'), isTempPlay: true }, + { listId: 'list_c', musicInfo: createMusic('temp_3', 'Temp 3'), isTempPlay: true }, + ] + + const movedList = moveTempQueueItem(tempPlayList, 2, 0) + + assert.deepEqual(movedList.map(item => item.musicInfo.id), ['temp_3', 'temp_1', 'temp_2']) + assert.deepEqual(tempPlayList.map(item => item.musicInfo.id), ['temp_1', 'temp_2', 'temp_3']) +}) diff --git a/src/lang/en-us.json b/src/lang/en-us.json index f04971a59c..62df12fb17 100644 --- a/src/lang/en-us.json +++ b/src/lang/en-us.json @@ -236,6 +236,7 @@ "player__play_toggle_mode_off": "Disable", "player__play_toggle_mode_random": "Shuffle", "player__play_toggle_mode_single_loop": "Repeat", + "player__play_queue": "Current queue", "player__playback_preserves_pitch": "Pitch compensation", "player__playback_rate": "Current playback rate: ", "player__playback_rate_reset_btn": "Reset", @@ -282,6 +283,10 @@ "player__sound_effect_pitch_shifter_reset_btn": "Reset", "player__sound_effect_pitch_shifter_tip": "This results in additional CPU usage, as raising/lowering the pitch requires real-time processing of audio data.\n\nKnown issues:\nInsufficient CPU resources will cause processing tasks to pile up, and a sound exception will occur.\nAt this time, it is necessary to pause the playback for a period of time and wait for the accumulated tasks to be processed before playing.", "player__stop": "Paused", + "player__queue_clear": "Clear", + "player__queue_current": "Current list", + "player__queue_empty": "There is no playable queue to show right now.", + "player__queue_temp": "Play later", "player__volume": "Volume: ", "player__volume_mute_label": "Mute", "player__volume_muted": "Muted", diff --git a/src/lang/zh-cn.json b/src/lang/zh-cn.json index c5a5242380..bf26daa86d 100644 --- a/src/lang/zh-cn.json +++ b/src/lang/zh-cn.json @@ -236,6 +236,7 @@ "player__play_toggle_mode_off": "禁用歌曲切换", "player__play_toggle_mode_random": "列表随机播放", "player__play_toggle_mode_single_loop": "单曲循环播放", + "player__play_queue": "当前播放列表", "player__playback_preserves_pitch": "音调补偿", "player__playback_rate": "当前播放速率:", "player__playback_rate_reset_btn": "重置", @@ -282,6 +283,10 @@ "player__sound_effect_pitch_shifter_reset_btn": "重置", "player__sound_effect_pitch_shifter_tip": "由于升降调需要实时处理音频数据,这会导致额外的 CPU 占用。\n\n已知问题:\n如果 CPU 资源不够将导致处理任务堆积而出现声音异常。\n这时需要暂停播放一段时间等堆积的任务处理完毕再播放。", "player__stop": "暂停播放", + "player__queue_clear": "清空", + "player__queue_current": "当前歌单", + "player__queue_empty": "当前没有可显示的播放队列", + "player__queue_temp": "稍后播放", "player__volume": "当前音量:", "player__volume_mute_label": "静音", "player__volume_muted": "已静音", diff --git a/src/lang/zh-tw.json b/src/lang/zh-tw.json index 491563fa56..8669f358e5 100644 --- a/src/lang/zh-tw.json +++ b/src/lang/zh-tw.json @@ -236,6 +236,7 @@ "player__play_toggle_mode_off": "停用歌曲切換", "player__play_toggle_mode_random": "隨機播放", "player__play_toggle_mode_single_loop": "重複播放", + "player__play_queue": "目前播放清單", "player__playback_preserves_pitch": "音調補償", "player__playback_rate": "目前播放速率:", "player__playback_rate_reset_btn": "重設", @@ -282,6 +283,10 @@ "player__sound_effect_pitch_shifter_reset_btn": "重設", "player__sound_effect_pitch_shifter_tip": "由於升降調需要即時處理音訊資料,這會導致額外的 CPU 占用。\n\n已知問題:\n如果 CPU 資源不夠將導致處理任務堆積而出現聲音異常。\n這時需要暫停播放一段時間等堆積的任務處理完畢再播放。", "player__stop": "暫停播放", + "player__queue_clear": "清空", + "player__queue_current": "目前歌單", + "player__queue_empty": "目前沒有可顯示的播放佇列", + "player__queue_temp": "稍後播放", "player__volume": "目前音量:", "player__volume_mute_label": "靜音", "player__volume_muted": "已靜音", diff --git a/src/renderer/components/common/PlayQueueBtn.vue b/src/renderer/components/common/PlayQueueBtn.vue new file mode 100644 index 0000000000..c458447b06 --- /dev/null +++ b/src/renderer/components/common/PlayQueueBtn.vue @@ -0,0 +1,449 @@ + + + + + diff --git a/src/renderer/components/layout/PlayBar/ControlBtns.vue b/src/renderer/components/layout/PlayBar/ControlBtns.vue index 4601342fd7..afc64c9f6d 100644 --- a/src/renderer/components/layout/PlayBar/ControlBtns.vue +++ b/src/renderer/components/layout/PlayBar/ControlBtns.vue @@ -14,6 +14,7 @@ + diff --git a/src/renderer/core/player/action.ts b/src/renderer/core/player/action.ts index 88995d0a99..e95d5809a3 100644 --- a/src/renderer/core/player/action.ts +++ b/src/renderer/core/player/action.ts @@ -369,6 +369,13 @@ const handlePlayNext = (playMusicInfo: LX.Player.PlayMusicInfo) => { setPlayMusicInfo(playMusicInfo.listId, playMusicInfo.musicInfo, playMusicInfo.isTempPlay) handlePlay() } + +export const playTempPlayItem = (index: number) => { + const target = tempPlayList[index] + if (!target) return + removeTempPlayList(index) + handlePlayNext(target) +} /** * 下一曲 * @param isAutoToggle 是否自动切换 diff --git a/src/renderer/core/player/queue.mjs b/src/renderer/core/player/queue.mjs new file mode 100644 index 0000000000..828286e548 --- /dev/null +++ b/src/renderer/core/player/queue.mjs @@ -0,0 +1,67 @@ +const isSamePlayItem = (queueItem, playMusicInfo) => { + if (!queueItem || !playMusicInfo?.musicInfo) return false + if (queueItem.musicInfo.id !== playMusicInfo.musicInfo.id) return false + return queueItem.isTempPlay + ? playMusicInfo.isTempPlay + : !playMusicInfo.isTempPlay && queueItem.listId === playMusicInfo.listId +} + +const createQueueItem = (queueItem, index, section) => ({ + key: `${section}_${queueItem.musicInfo.id}_${index}`, + index, + listId: queueItem.listId, + musicInfo: queueItem.musicInfo, + isTempPlay: queueItem.isTempPlay, + isActive: false, + canRemove: section === 'temp', + canDrag: section === 'temp', +}) + +export const buildPlayQueueSections = ({ + tempPlayList = [], + baseList = [], + baseListId = null, + playMusicInfo = null, +}) => { + const sections = [] + + if (tempPlayList.length) { + sections.push({ + key: 'temp', + items: tempPlayList.map((item, index) => { + const queueItem = createQueueItem(item, index, 'temp') + queueItem.isActive = isSamePlayItem(item, playMusicInfo) + return queueItem + }), + }) + } + + if (baseList.length) { + sections.push({ + key: 'base', + items: baseList.map((musicInfo, index) => { + const item = { + listId: baseListId, + musicInfo, + isTempPlay: false, + } + const queueItem = createQueueItem(item, index, 'base') + queueItem.isActive = isSamePlayItem(item, playMusicInfo) + return queueItem + }), + }) + } + + return sections +} + +export const moveTempQueueItem = (list, oldIndex, newIndex) => { + if (oldIndex === newIndex) return [...list] + if (oldIndex < 0 || oldIndex >= list.length) return [...list] + if (newIndex < 0 || newIndex >= list.length) return [...list] + + const nextList = [...list] + const [target] = nextList.splice(oldIndex, 1) + nextList.splice(newIndex, 0, target) + return nextList +} diff --git a/src/renderer/store/player/action.ts b/src/renderer/store/player/action.ts index 87977fc1a1..536e1f1798 100644 --- a/src/renderer/store/player/action.ts +++ b/src/renderer/store/player/action.ts @@ -8,6 +8,7 @@ import { isShowPlayerDetail, isShowPlayComment, isShowLrcSelectContent, + isShowPlayQueue, playInfo, playMusicInfo, playedList, @@ -17,6 +18,7 @@ import { getListMusicsFromCache } from '@renderer/store/list/action' import { downloadList } from '@renderer/store/download/state' import { setProgress } from './playProgress' import { playNext } from '@renderer/core/player' +import { moveTempQueueItem } from '@renderer/core/player/queue.mjs' import { LIST_IDS } from '@common/constants' import { toRaw } from '@common/utils/vueTools' import { arrPush, arrUnshift } from '@common/utils/common' @@ -69,6 +71,10 @@ export const setShowPlayLrcSelectContentLrc = (val: boolean) => { isShowLrcSelectContent.value = val } +export const setShowPlayQueue = (val: boolean) => { + isShowPlayQueue.value = val +} + export const setPlayListId = (listId: string | null) => { playInfo.playerListId = listId } @@ -245,6 +251,11 @@ export const addTempPlayList = (list: LX.Player.TempPlayListItem[]) => { export const removeTempPlayList = (index: number) => { tempPlayList.splice(index, 1) } + +export const moveTempPlayList = (oldIndex: number, newIndex: number) => { + const list = moveTempQueueItem(tempPlayList, oldIndex, newIndex) + tempPlayList.splice(0, tempPlayList.length, ...list) +} /** * 清空稍后播放列表 */ diff --git a/src/renderer/store/player/state.ts b/src/renderer/store/player/state.ts index 48aea238bb..8b0b205465 100644 --- a/src/renderer/store/player/state.ts +++ b/src/renderer/store/player/state.ts @@ -40,6 +40,8 @@ export const isShowPlayComment = ref(false) export const isShowLrcSelectContent = ref(false) +export const isShowPlayQueue = ref(false) + export const playMusicInfo = shallowReactive<{ /** * 当前播放歌曲的列表 id diff --git a/src/renderer/utils/compositions/useDrag.js b/src/renderer/utils/compositions/useDrag.js index 6204e2f395..3f4fb684af 100644 --- a/src/renderer/utils/compositions/useDrag.js +++ b/src/renderer/utils/compositions/useDrag.js @@ -2,7 +2,10 @@ import Sortable, { AutoScroll } from 'sortablejs/modular/sortable.core.esm' import { onMounted } from '@common/utils/vueTools' import { clearDownKeys } from '@renderer/event' -Sortable.mount(new AutoScroll()) +if (!window.__lx_sortableAutoScrollMounted) { + Sortable.mount(new AutoScroll()) + window.__lx_sortableAutoScrollMounted = true +} const noop = () => {} diff --git a/src/renderer/views/songList/List/components/TagList.vue b/src/renderer/views/songList/List/components/TagList.vue index 20c8f6b327..cbd5cbe17f 100644 --- a/src/renderer/views/songList/List/components/TagList.vue +++ b/src/renderer/views/songList/List/components/TagList.vue @@ -86,6 +86,7 @@ const popupStyle = reactive({ const setTagPopupWidth = () => { window.setTimeout(() => { const dom_view = document.getElementById('view') + if (!dom_view) return popupStyle.width = dom_view.clientWidth * 0.96 + 'px' popupStyle.maxHeight = dom_view.clientHeight * 0.65 + 'px' }, 50) From 3f5e50d85c0ecf8f6c5e02301af951891fdd916c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E6=B3=A2?= Date: Thu, 7 May 2026 16:43:22 +0800 Subject: [PATCH 04/49] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=A8=8D=E5=90=8E?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E6=8B=96=E6=8B=BD=E6=8E=92=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/common/PlayQueueBtn.vue | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/renderer/components/common/PlayQueueBtn.vue b/src/renderer/components/common/PlayQueueBtn.vue index c458447b06..45b472eae8 100644 --- a/src/renderer/components/common/PlayQueueBtn.vue +++ b/src/renderer/components/common/PlayQueueBtn.vue @@ -32,7 +32,7 @@ @click="handlePlayItem(tempSection.key, item.index)" >
-
+
@@ -121,6 +121,7 @@ const styles = useCssModule() const dom_btn = ref(null) const dom_tempList = ref(null) let sortable = null +let suppressPlayUntil = 0 const visible = computed({ get: () => isShowPlayQueue.value, @@ -162,15 +163,20 @@ watch(dom_tempList, (element) => { sortable = Sortable.create(element, { animation: 150, disabled: true, + forceFallback: true, + fallbackOnBody: true, + fallbackTolerance: 4, filter: `.${styles.noDrag}`, + handle: `.${styles.dragHandle}`, ghostClass: styles.dragingItem, - onUpdate(event) { - moveTempPlayList(event.oldIndex, event.newIndex) - }, onStart() { window.app_event.dragStart() }, - onEnd() { + onEnd(event) { + if (event.oldIndex != null && event.newIndex != null && event.oldIndex !== event.newIndex) { + moveTempPlayList(event.oldIndex, event.newIndex) + } + suppressPlayUntil = Date.now() + 200 window.app_event.dragEnd() }, }) @@ -220,6 +226,7 @@ const getSectionTitle = (key) => { } const handlePlayItem = (sectionKey, index) => { + if (Date.now() < suppressPlayUntil) return if (sectionKey === 'temp') { playTempPlayItem(index) } else if (playInfo.playerListId) { @@ -390,6 +397,14 @@ onBeforeUnmount(() => { } } +.dragHandle { + cursor: grab; + + &:active { + cursor: grabbing; + } +} + .itemText { min-width: 0; flex: auto; From f273791618daab6499d1549296ca2518d1091de9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E6=B3=A2?= Date: Thu, 7 May 2026 16:03:09 +0800 Subject: [PATCH 05/49] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E5=88=97=E8=A1=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build-config/tests/player-queue.test.mjs | 71 +++ src/lang/en-us.json | 5 + src/lang/zh-cn.json | 5 + src/lang/zh-tw.json | 5 + .../components/common/PlayQueueBtn.vue | 449 ++++++++++++++++++ .../components/layout/PlayBar/ControlBtns.vue | 1 + src/renderer/core/player/action.ts | 7 + src/renderer/core/player/queue.mjs | 67 +++ src/renderer/store/player/action.ts | 11 + src/renderer/store/player/state.ts | 2 + src/renderer/utils/compositions/useDrag.js | 5 +- .../songList/List/components/TagList.vue | 1 + 12 files changed, 628 insertions(+), 1 deletion(-) create mode 100644 build-config/tests/player-queue.test.mjs create mode 100644 src/renderer/components/common/PlayQueueBtn.vue create mode 100644 src/renderer/core/player/queue.mjs diff --git a/build-config/tests/player-queue.test.mjs b/build-config/tests/player-queue.test.mjs new file mode 100644 index 0000000000..f39e86428e --- /dev/null +++ b/build-config/tests/player-queue.test.mjs @@ -0,0 +1,71 @@ +import assert from 'node:assert/strict' + +import { + buildPlayQueueSections, + moveTempQueueItem, +} from '../../src/renderer/core/player/queue.mjs' + +const createMusic = (id, name) => ({ + id, + name, + singer: `${name} singer`, + source: 'kw', + interval: '03:00', + meta: { + albumName: `${name} album`, + }, +}) + +const run = (name, fn) => { + try { + fn() + console.log(`PASS ${name}`) + } catch (error) { + console.error(`FAIL ${name}`) + throw error + } +} + +run('buildPlayQueueSections creates temp and base sections with active item metadata', () => { + const tempPlayList = [ + { listId: 'list_a', musicInfo: createMusic('temp_1', 'Temp 1'), isTempPlay: true }, + { listId: 'list_b', musicInfo: createMusic('temp_2', 'Temp 2'), isTempPlay: true }, + ] + const baseList = [ + createMusic('song_1', 'Song 1'), + createMusic('song_2', 'Song 2'), + ] + + const sections = buildPlayQueueSections({ + tempPlayList, + baseList, + baseListId: 'list_a', + playMusicInfo: { + listId: 'list_b', + musicInfo: tempPlayList[1].musicInfo, + isTempPlay: true, + }, + }) + + assert.equal(sections.length, 2) + assert.deepEqual(sections.map(section => section.key), ['temp', 'base']) + assert.equal(sections[0].items[1].isActive, true) + assert.equal(sections[0].items[1].canRemove, true) + assert.equal(sections[0].items[1].canDrag, true) + assert.equal(sections[1].items[0].canRemove, false) + assert.equal(sections[1].items[0].canDrag, false) + assert.equal(sections[1].items[0].isActive, false) +}) + +run('moveTempQueueItem reorders temp queue immutably', () => { + const tempPlayList = [ + { listId: 'list_a', musicInfo: createMusic('temp_1', 'Temp 1'), isTempPlay: true }, + { listId: 'list_b', musicInfo: createMusic('temp_2', 'Temp 2'), isTempPlay: true }, + { listId: 'list_c', musicInfo: createMusic('temp_3', 'Temp 3'), isTempPlay: true }, + ] + + const movedList = moveTempQueueItem(tempPlayList, 2, 0) + + assert.deepEqual(movedList.map(item => item.musicInfo.id), ['temp_3', 'temp_1', 'temp_2']) + assert.deepEqual(tempPlayList.map(item => item.musicInfo.id), ['temp_1', 'temp_2', 'temp_3']) +}) diff --git a/src/lang/en-us.json b/src/lang/en-us.json index f04971a59c..62df12fb17 100644 --- a/src/lang/en-us.json +++ b/src/lang/en-us.json @@ -236,6 +236,7 @@ "player__play_toggle_mode_off": "Disable", "player__play_toggle_mode_random": "Shuffle", "player__play_toggle_mode_single_loop": "Repeat", + "player__play_queue": "Current queue", "player__playback_preserves_pitch": "Pitch compensation", "player__playback_rate": "Current playback rate: ", "player__playback_rate_reset_btn": "Reset", @@ -282,6 +283,10 @@ "player__sound_effect_pitch_shifter_reset_btn": "Reset", "player__sound_effect_pitch_shifter_tip": "This results in additional CPU usage, as raising/lowering the pitch requires real-time processing of audio data.\n\nKnown issues:\nInsufficient CPU resources will cause processing tasks to pile up, and a sound exception will occur.\nAt this time, it is necessary to pause the playback for a period of time and wait for the accumulated tasks to be processed before playing.", "player__stop": "Paused", + "player__queue_clear": "Clear", + "player__queue_current": "Current list", + "player__queue_empty": "There is no playable queue to show right now.", + "player__queue_temp": "Play later", "player__volume": "Volume: ", "player__volume_mute_label": "Mute", "player__volume_muted": "Muted", diff --git a/src/lang/zh-cn.json b/src/lang/zh-cn.json index c5a5242380..bf26daa86d 100644 --- a/src/lang/zh-cn.json +++ b/src/lang/zh-cn.json @@ -236,6 +236,7 @@ "player__play_toggle_mode_off": "禁用歌曲切换", "player__play_toggle_mode_random": "列表随机播放", "player__play_toggle_mode_single_loop": "单曲循环播放", + "player__play_queue": "当前播放列表", "player__playback_preserves_pitch": "音调补偿", "player__playback_rate": "当前播放速率:", "player__playback_rate_reset_btn": "重置", @@ -282,6 +283,10 @@ "player__sound_effect_pitch_shifter_reset_btn": "重置", "player__sound_effect_pitch_shifter_tip": "由于升降调需要实时处理音频数据,这会导致额外的 CPU 占用。\n\n已知问题:\n如果 CPU 资源不够将导致处理任务堆积而出现声音异常。\n这时需要暂停播放一段时间等堆积的任务处理完毕再播放。", "player__stop": "暂停播放", + "player__queue_clear": "清空", + "player__queue_current": "当前歌单", + "player__queue_empty": "当前没有可显示的播放队列", + "player__queue_temp": "稍后播放", "player__volume": "当前音量:", "player__volume_mute_label": "静音", "player__volume_muted": "已静音", diff --git a/src/lang/zh-tw.json b/src/lang/zh-tw.json index 491563fa56..8669f358e5 100644 --- a/src/lang/zh-tw.json +++ b/src/lang/zh-tw.json @@ -236,6 +236,7 @@ "player__play_toggle_mode_off": "停用歌曲切換", "player__play_toggle_mode_random": "隨機播放", "player__play_toggle_mode_single_loop": "重複播放", + "player__play_queue": "目前播放清單", "player__playback_preserves_pitch": "音調補償", "player__playback_rate": "目前播放速率:", "player__playback_rate_reset_btn": "重設", @@ -282,6 +283,10 @@ "player__sound_effect_pitch_shifter_reset_btn": "重設", "player__sound_effect_pitch_shifter_tip": "由於升降調需要即時處理音訊資料,這會導致額外的 CPU 占用。\n\n已知問題:\n如果 CPU 資源不夠將導致處理任務堆積而出現聲音異常。\n這時需要暫停播放一段時間等堆積的任務處理完畢再播放。", "player__stop": "暫停播放", + "player__queue_clear": "清空", + "player__queue_current": "目前歌單", + "player__queue_empty": "目前沒有可顯示的播放佇列", + "player__queue_temp": "稍後播放", "player__volume": "目前音量:", "player__volume_mute_label": "靜音", "player__volume_muted": "已靜音", diff --git a/src/renderer/components/common/PlayQueueBtn.vue b/src/renderer/components/common/PlayQueueBtn.vue new file mode 100644 index 0000000000..c458447b06 --- /dev/null +++ b/src/renderer/components/common/PlayQueueBtn.vue @@ -0,0 +1,449 @@ + + + + + diff --git a/src/renderer/components/layout/PlayBar/ControlBtns.vue b/src/renderer/components/layout/PlayBar/ControlBtns.vue index 4601342fd7..afc64c9f6d 100644 --- a/src/renderer/components/layout/PlayBar/ControlBtns.vue +++ b/src/renderer/components/layout/PlayBar/ControlBtns.vue @@ -14,6 +14,7 @@ + diff --git a/src/renderer/core/player/action.ts b/src/renderer/core/player/action.ts index 88995d0a99..e95d5809a3 100644 --- a/src/renderer/core/player/action.ts +++ b/src/renderer/core/player/action.ts @@ -369,6 +369,13 @@ const handlePlayNext = (playMusicInfo: LX.Player.PlayMusicInfo) => { setPlayMusicInfo(playMusicInfo.listId, playMusicInfo.musicInfo, playMusicInfo.isTempPlay) handlePlay() } + +export const playTempPlayItem = (index: number) => { + const target = tempPlayList[index] + if (!target) return + removeTempPlayList(index) + handlePlayNext(target) +} /** * 下一曲 * @param isAutoToggle 是否自动切换 diff --git a/src/renderer/core/player/queue.mjs b/src/renderer/core/player/queue.mjs new file mode 100644 index 0000000000..828286e548 --- /dev/null +++ b/src/renderer/core/player/queue.mjs @@ -0,0 +1,67 @@ +const isSamePlayItem = (queueItem, playMusicInfo) => { + if (!queueItem || !playMusicInfo?.musicInfo) return false + if (queueItem.musicInfo.id !== playMusicInfo.musicInfo.id) return false + return queueItem.isTempPlay + ? playMusicInfo.isTempPlay + : !playMusicInfo.isTempPlay && queueItem.listId === playMusicInfo.listId +} + +const createQueueItem = (queueItem, index, section) => ({ + key: `${section}_${queueItem.musicInfo.id}_${index}`, + index, + listId: queueItem.listId, + musicInfo: queueItem.musicInfo, + isTempPlay: queueItem.isTempPlay, + isActive: false, + canRemove: section === 'temp', + canDrag: section === 'temp', +}) + +export const buildPlayQueueSections = ({ + tempPlayList = [], + baseList = [], + baseListId = null, + playMusicInfo = null, +}) => { + const sections = [] + + if (tempPlayList.length) { + sections.push({ + key: 'temp', + items: tempPlayList.map((item, index) => { + const queueItem = createQueueItem(item, index, 'temp') + queueItem.isActive = isSamePlayItem(item, playMusicInfo) + return queueItem + }), + }) + } + + if (baseList.length) { + sections.push({ + key: 'base', + items: baseList.map((musicInfo, index) => { + const item = { + listId: baseListId, + musicInfo, + isTempPlay: false, + } + const queueItem = createQueueItem(item, index, 'base') + queueItem.isActive = isSamePlayItem(item, playMusicInfo) + return queueItem + }), + }) + } + + return sections +} + +export const moveTempQueueItem = (list, oldIndex, newIndex) => { + if (oldIndex === newIndex) return [...list] + if (oldIndex < 0 || oldIndex >= list.length) return [...list] + if (newIndex < 0 || newIndex >= list.length) return [...list] + + const nextList = [...list] + const [target] = nextList.splice(oldIndex, 1) + nextList.splice(newIndex, 0, target) + return nextList +} diff --git a/src/renderer/store/player/action.ts b/src/renderer/store/player/action.ts index 87977fc1a1..536e1f1798 100644 --- a/src/renderer/store/player/action.ts +++ b/src/renderer/store/player/action.ts @@ -8,6 +8,7 @@ import { isShowPlayerDetail, isShowPlayComment, isShowLrcSelectContent, + isShowPlayQueue, playInfo, playMusicInfo, playedList, @@ -17,6 +18,7 @@ import { getListMusicsFromCache } from '@renderer/store/list/action' import { downloadList } from '@renderer/store/download/state' import { setProgress } from './playProgress' import { playNext } from '@renderer/core/player' +import { moveTempQueueItem } from '@renderer/core/player/queue.mjs' import { LIST_IDS } from '@common/constants' import { toRaw } from '@common/utils/vueTools' import { arrPush, arrUnshift } from '@common/utils/common' @@ -69,6 +71,10 @@ export const setShowPlayLrcSelectContentLrc = (val: boolean) => { isShowLrcSelectContent.value = val } +export const setShowPlayQueue = (val: boolean) => { + isShowPlayQueue.value = val +} + export const setPlayListId = (listId: string | null) => { playInfo.playerListId = listId } @@ -245,6 +251,11 @@ export const addTempPlayList = (list: LX.Player.TempPlayListItem[]) => { export const removeTempPlayList = (index: number) => { tempPlayList.splice(index, 1) } + +export const moveTempPlayList = (oldIndex: number, newIndex: number) => { + const list = moveTempQueueItem(tempPlayList, oldIndex, newIndex) + tempPlayList.splice(0, tempPlayList.length, ...list) +} /** * 清空稍后播放列表 */ diff --git a/src/renderer/store/player/state.ts b/src/renderer/store/player/state.ts index 48aea238bb..8b0b205465 100644 --- a/src/renderer/store/player/state.ts +++ b/src/renderer/store/player/state.ts @@ -40,6 +40,8 @@ export const isShowPlayComment = ref(false) export const isShowLrcSelectContent = ref(false) +export const isShowPlayQueue = ref(false) + export const playMusicInfo = shallowReactive<{ /** * 当前播放歌曲的列表 id diff --git a/src/renderer/utils/compositions/useDrag.js b/src/renderer/utils/compositions/useDrag.js index 6204e2f395..3f4fb684af 100644 --- a/src/renderer/utils/compositions/useDrag.js +++ b/src/renderer/utils/compositions/useDrag.js @@ -2,7 +2,10 @@ import Sortable, { AutoScroll } from 'sortablejs/modular/sortable.core.esm' import { onMounted } from '@common/utils/vueTools' import { clearDownKeys } from '@renderer/event' -Sortable.mount(new AutoScroll()) +if (!window.__lx_sortableAutoScrollMounted) { + Sortable.mount(new AutoScroll()) + window.__lx_sortableAutoScrollMounted = true +} const noop = () => {} diff --git a/src/renderer/views/songList/List/components/TagList.vue b/src/renderer/views/songList/List/components/TagList.vue index 20c8f6b327..cbd5cbe17f 100644 --- a/src/renderer/views/songList/List/components/TagList.vue +++ b/src/renderer/views/songList/List/components/TagList.vue @@ -86,6 +86,7 @@ const popupStyle = reactive({ const setTagPopupWidth = () => { window.setTimeout(() => { const dom_view = document.getElementById('view') + if (!dom_view) return popupStyle.width = dom_view.clientWidth * 0.96 + 'px' popupStyle.maxHeight = dom_view.clientHeight * 0.65 + 'px' }, 50) From 9a4a32e2b9e288d6b22e78f255a12ece40bc8cf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E6=B3=A2?= Date: Thu, 7 May 2026 16:43:22 +0800 Subject: [PATCH 06/49] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=A8=8D=E5=90=8E?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E6=8B=96=E6=8B=BD=E6=8E=92=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/common/PlayQueueBtn.vue | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/renderer/components/common/PlayQueueBtn.vue b/src/renderer/components/common/PlayQueueBtn.vue index c458447b06..45b472eae8 100644 --- a/src/renderer/components/common/PlayQueueBtn.vue +++ b/src/renderer/components/common/PlayQueueBtn.vue @@ -32,7 +32,7 @@ @click="handlePlayItem(tempSection.key, item.index)" >
-
+
@@ -121,6 +121,7 @@ const styles = useCssModule() const dom_btn = ref(null) const dom_tempList = ref(null) let sortable = null +let suppressPlayUntil = 0 const visible = computed({ get: () => isShowPlayQueue.value, @@ -162,15 +163,20 @@ watch(dom_tempList, (element) => { sortable = Sortable.create(element, { animation: 150, disabled: true, + forceFallback: true, + fallbackOnBody: true, + fallbackTolerance: 4, filter: `.${styles.noDrag}`, + handle: `.${styles.dragHandle}`, ghostClass: styles.dragingItem, - onUpdate(event) { - moveTempPlayList(event.oldIndex, event.newIndex) - }, onStart() { window.app_event.dragStart() }, - onEnd() { + onEnd(event) { + if (event.oldIndex != null && event.newIndex != null && event.oldIndex !== event.newIndex) { + moveTempPlayList(event.oldIndex, event.newIndex) + } + suppressPlayUntil = Date.now() + 200 window.app_event.dragEnd() }, }) @@ -220,6 +226,7 @@ const getSectionTitle = (key) => { } const handlePlayItem = (sectionKey, index) => { + if (Date.now() < suppressPlayUntil) return if (sectionKey === 'temp') { playTempPlayItem(index) } else if (playInfo.playerListId) { @@ -390,6 +397,14 @@ onBeforeUnmount(() => { } } +.dragHandle { + cursor: grab; + + &:active { + cursor: grabbing; + } +} + .itemText { min-width: 0; flex: auto; From 0f0d3f85abc5b5c6c6a711f34c85a18024240f47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E6=B3=A2?= Date: Thu, 7 May 2026 17:57:27 +0800 Subject: [PATCH 07/49] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=96=87=E5=AD=97=E5=8C=BA=E5=9F=9F=E5=8F=B3=E9=94=AE=E8=8F=9C?= =?UTF-8?q?=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build-config/tests/list-context-menu.test.mjs | 34 +++++++++++++++++++ .../components/material/OnlineList/index.vue | 10 ++++-- src/renderer/utils/listContextMenu.mjs | 14 ++++++++ src/renderer/views/List/MusicList/index.vue | 10 ++++-- 4 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 build-config/tests/list-context-menu.test.mjs create mode 100644 src/renderer/utils/listContextMenu.mjs diff --git a/build-config/tests/list-context-menu.test.mjs b/build-config/tests/list-context-menu.test.mjs new file mode 100644 index 0000000000..9e425df688 --- /dev/null +++ b/build-config/tests/list-context-menu.test.mjs @@ -0,0 +1,34 @@ +import assert from 'node:assert/strict' + +import { shouldCopyListTextOnContextMenu } from '../../src/renderer/utils/listContextMenu.mjs' + +const run = (name, fn) => { + try { + fn() + console.log(`PASS ${name}`) + } catch (error) { + console.error(`FAIL ${name}`) + throw error + } +} + +run('does not hijack row menu when right-clicking selectable text without an active selection', () => { + assert.equal(shouldCopyListTextOnContextMenu({ + isSelectTextTarget: true, + selectionText: '', + }), false) +}) + +run('keeps text copy behavior when right-clicking selected text', () => { + assert.equal(shouldCopyListTextOnContextMenu({ + isSelectTextTarget: true, + selectionText: 'Song Name', + }), true) +}) + +run('ignores non-select targets', () => { + assert.equal(shouldCopyListTextOnContextMenu({ + isSelectTextTarget: false, + selectionText: 'Song Name', + }), false) +}) diff --git a/src/renderer/components/material/OnlineList/index.vue b/src/renderer/components/material/OnlineList/index.vue index fcdf52728d..4ffe8f3e78 100644 --- a/src/renderer/components/material/OnlineList/index.vue +++ b/src/renderer/components/material/OnlineList/index.vue @@ -102,6 +102,7 @@ import { clipboardWriteText } from '@common/utils/electron' import { assertApiSupport } from '@renderer/store/utils' import { ref } from '@common/utils/vueTools' +import { shouldCopyListTextOnContextMenu, formatListSelectionText } from '@renderer/utils/listContextMenu.mjs' import useList from './useList' import useMenu from './useMenu' import usePlay from './usePlay' @@ -218,14 +219,17 @@ export default { menuClick(action, index) } const handleListRightClick = (event) => { - if (!event.target.classList.contains('select')) return + const selectionText = window.getSelection().toString() + if (!shouldCopyListTextOnContextMenu({ + isSelectTextTarget: event.target.classList.contains('select'), + selectionText, + })) return event.stopImmediatePropagation() let classList = dom_listContent.value.classList classList.add('copying') window.requestAnimationFrame(() => { - let str = window.getSelection().toString() classList.remove('copying') - str = str.split(/\n\n/).map(s => s.replace(/\n/g, ' ')).join('\n').trim() + let str = formatListSelectionText(window.getSelection().toString()) if (!str.length) return clipboardWriteText(str) }) diff --git a/src/renderer/utils/listContextMenu.mjs b/src/renderer/utils/listContextMenu.mjs new file mode 100644 index 0000000000..e557477ef1 --- /dev/null +++ b/src/renderer/utils/listContextMenu.mjs @@ -0,0 +1,14 @@ +export const shouldCopyListTextOnContextMenu = ({ + isSelectTextTarget, + selectionText, +}) => { + return isSelectTextTarget && !!selectionText.trim() +} + +export const formatListSelectionText = (selectionText) => { + return selectionText + .split(/\n\n/) + .map(text => text.replace(/\n/g, ' ')) + .join('\n') + .trim() +} diff --git a/src/renderer/views/List/MusicList/index.vue b/src/renderer/views/List/MusicList/index.vue index 9681972deb..468a544a8a 100644 --- a/src/renderer/views/List/MusicList/index.vue +++ b/src/renderer/views/List/MusicList/index.vue @@ -106,6 +106,7 @@ + + diff --git a/src/renderer-taskbar-lyric/app.d.ts b/src/renderer-taskbar-lyric/app.d.ts new file mode 100644 index 0000000000..5ce5263422 --- /dev/null +++ b/src/renderer-taskbar-lyric/app.d.ts @@ -0,0 +1,13 @@ +declare global { + interface Window { + ELECTRON_DISABLE_SECURITY_WARNINGS?: string + } +} + +declare module '*.vue' { + import { type Component } from 'vue' + const component: Component + export default component +} + +export {} diff --git a/src/renderer-taskbar-lyric/index.html b/src/renderer-taskbar-lyric/index.html new file mode 100644 index 0000000000..0088d2fd5c --- /dev/null +++ b/src/renderer-taskbar-lyric/index.html @@ -0,0 +1,11 @@ + + + + + + Taskbar Lyric - LX Music + + +
+ + diff --git a/src/renderer-taskbar-lyric/main.ts b/src/renderer-taskbar-lyric/main.ts new file mode 100644 index 0000000000..1f6502bcdc --- /dev/null +++ b/src/renderer-taskbar-lyric/main.ts @@ -0,0 +1,5 @@ +import { createApp } from 'vue' + +import App from './App.vue' + +createApp(App).mount('#root') diff --git a/src/renderer-taskbar-lyric/store/state.ts b/src/renderer-taskbar-lyric/store/state.ts new file mode 100644 index 0000000000..4077396eaa --- /dev/null +++ b/src/renderer-taskbar-lyric/store/state.ts @@ -0,0 +1,27 @@ +import { shallowReactive } from '../../common/utils/vueTools' + +interface TaskbarLyricViewState { + enabled: boolean + isPlaying: boolean + songId: string | null + title: string + artist: string + lyricLine: string + albumCoverUrl: string | null + showCover: boolean + showSongInfo: boolean + showCurrentLine: boolean +} + +export const state = shallowReactive({ + enabled: false, + isPlaying: false, + songId: null, + title: 'LX Music', + artist: 'Taskbar lyric', + lyricLine: 'Renderer target ready for state wiring.', + albumCoverUrl: null, + showCover: true, + showSongInfo: true, + showCurrentLine: true, +}) diff --git a/src/renderer-taskbar-lyric/tsconfig.json b/src/renderer-taskbar-lyric/tsconfig.json new file mode 100644 index 0000000000..89bf2f9fa3 --- /dev/null +++ b/src/renderer-taskbar-lyric/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "isolatedModules": true, + "paths": { + "@common/*": ["../common/*"], + "@renderer/*": ["../renderer/*"], + "@lyric/*": ["../renderer-lyric/*"], + "@taskbar-lyric/*": ["../renderer-taskbar-lyric/*"], + "@static/*": ["../static/*"], + "@root/*": ["../*"] + } + } +} diff --git a/src/renderer-taskbar-lyric/utils/ipc.ts b/src/renderer-taskbar-lyric/utils/ipc.ts new file mode 100644 index 0000000000..fa23c8a285 --- /dev/null +++ b/src/renderer-taskbar-lyric/utils/ipc.ts @@ -0,0 +1,15 @@ +export interface TaskbarLyricStatePayload { + enabled: boolean + isPlaying: boolean + songId: string | null + title: string + artist: string + lyricLine: string + albumCoverUrl: string | null +} + +// Task 3 keeps the renderer static-first. Live IPC wiring will land in a later task. +export const sendTaskbarLyricState = (_state: TaskbarLyricStatePayload) => {} + +// Placeholder for the later refresh handshake once playback state wiring is implemented. +export const requestTaskbarLyricRefresh = () => {} From 8e21e95f06c79d64347f9b159979633261158327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E6=B3=A2?= Date: Thu, 21 May 2026 12:28:48 +0800 Subject: [PATCH 17/49] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E6=A0=8F=E6=AD=8C=E8=AF=8D=E6=B8=B2=E6=9F=93=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../renderer-taskbar-lyric/webpack.config.base.js | 1 - src/renderer-taskbar-lyric/app.d.ts | 13 ------------- src/renderer-taskbar-lyric/store/state.ts | 2 +- src/renderer-taskbar-lyric/tsconfig.json | 12 ++++++++++-- src/renderer-taskbar-lyric/types/app.d.ts | 7 +++++++ src/renderer-taskbar-lyric/types/common.d.ts | 2 ++ 6 files changed, 20 insertions(+), 17 deletions(-) delete mode 100644 src/renderer-taskbar-lyric/app.d.ts create mode 100644 src/renderer-taskbar-lyric/types/app.d.ts create mode 100644 src/renderer-taskbar-lyric/types/common.d.ts diff --git a/build-config/renderer-taskbar-lyric/webpack.config.base.js b/build-config/renderer-taskbar-lyric/webpack.config.base.js index cba6526e01..c07f084cd2 100644 --- a/build-config/renderer-taskbar-lyric/webpack.config.base.js +++ b/build-config/renderer-taskbar-lyric/webpack.config.base.js @@ -44,7 +44,6 @@ module.exports = { options: { appendTsSuffixTo: [/\.vue$/], configFile: path.join(__dirname, '../../src/renderer-taskbar-lyric/tsconfig.json'), - transpileOnly: true, }, }, }, diff --git a/src/renderer-taskbar-lyric/app.d.ts b/src/renderer-taskbar-lyric/app.d.ts deleted file mode 100644 index 5ce5263422..0000000000 --- a/src/renderer-taskbar-lyric/app.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -declare global { - interface Window { - ELECTRON_DISABLE_SECURITY_WARNINGS?: string - } -} - -declare module '*.vue' { - import { type Component } from 'vue' - const component: Component - export default component -} - -export {} diff --git a/src/renderer-taskbar-lyric/store/state.ts b/src/renderer-taskbar-lyric/store/state.ts index 4077396eaa..11c35a7355 100644 --- a/src/renderer-taskbar-lyric/store/state.ts +++ b/src/renderer-taskbar-lyric/store/state.ts @@ -1,4 +1,4 @@ -import { shallowReactive } from '../../common/utils/vueTools' +import { shallowReactive } from '@common/utils/vueTools' interface TaskbarLyricViewState { enabled: boolean diff --git a/src/renderer-taskbar-lyric/tsconfig.json b/src/renderer-taskbar-lyric/tsconfig.json index 89bf2f9fa3..9fdb533b8d 100644 --- a/src/renderer-taskbar-lyric/tsconfig.json +++ b/src/renderer-taskbar-lyric/tsconfig.json @@ -2,13 +2,21 @@ "extends": "../../tsconfig.json", "compilerOptions": { "isolatedModules": true, - "paths": { + "paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */ "@common/*": ["../common/*"], "@renderer/*": ["../renderer/*"], "@lyric/*": ["../renderer-lyric/*"], "@taskbar-lyric/*": ["../renderer-taskbar-lyric/*"], "@static/*": ["../static/*"], "@root/*": ["../*"] - } + }, + "typeRoots": [ /* Specify multiple folders that act like './node_modules/@types'. */ + "./types" + ] + }, + "vueCompilerOptions": { + "plugins": [ + "@vue/language-plugin-pug" + ] } } diff --git a/src/renderer-taskbar-lyric/types/app.d.ts b/src/renderer-taskbar-lyric/types/app.d.ts new file mode 100644 index 0000000000..8870e8ec6d --- /dev/null +++ b/src/renderer-taskbar-lyric/types/app.d.ts @@ -0,0 +1,7 @@ +declare global { + interface Window { + ELECTRON_DISABLE_SECURITY_WARNINGS?: string + } +} + +export {} diff --git a/src/renderer-taskbar-lyric/types/common.d.ts b/src/renderer-taskbar-lyric/types/common.d.ts new file mode 100644 index 0000000000..876534ccb6 --- /dev/null +++ b/src/renderer-taskbar-lyric/types/common.d.ts @@ -0,0 +1,2 @@ +import '@common/types/shims_vue' +import '@common/types/taskbar_lyric' From 503f2519344f50429c42ed371387ea85c6c6c75a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E6=B3=A2?= Date: Thu, 21 May 2026 12:41:03 +0800 Subject: [PATCH 18/49] =?UTF-8?q?=E6=8E=A5=E5=85=A5=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E6=A0=8F=E6=AD=8C=E8=AF=8D=E5=AE=9E=E6=97=B6=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/modules/taskbarLyric/main.ts | 19 +++++++++++ .../modules/winMain/rendererEvent/index.ts | 2 ++ .../winMain/rendererEvent/taskbarLyric.ts | 14 ++++++++ src/renderer-taskbar-lyric/main.ts | 8 +++++ src/renderer-taskbar-lyric/store/state.ts | 4 +++ src/renderer-taskbar-lyric/utils/ipc.ts | 34 ++++++++++++------- src/renderer/core/lyric.ts | 33 ++++++++++++++++-- .../core/useApp/usePlayer/useLyric.ts | 2 ++ 8 files changed, 100 insertions(+), 16 deletions(-) create mode 100644 src/main/modules/winMain/rendererEvent/taskbarLyric.ts diff --git a/src/main/modules/taskbarLyric/main.ts b/src/main/modules/taskbarLyric/main.ts index d535e13992..aadcefab12 100644 --- a/src/main/modules/taskbarLyric/main.ts +++ b/src/main/modules/taskbarLyric/main.ts @@ -1,6 +1,7 @@ import path from 'node:path' import { existsSync } from 'node:fs' import { BrowserWindow, screen } from 'electron' +import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames' import { encodePath } from '@common/utils/electron' import type { TaskbarLyricState } from './types' import { calcTaskbarLyricBounds } from './utils' @@ -10,6 +11,15 @@ const TASKBAR_LYRIC_HEIGHT = 56 let browserWindow: Electron.BrowserWindow | null = null let currentState: TaskbarLyricState | null = null +const sendStateToWindow = (webContents?: Electron.WebContents) => { + if (!currentState) return + + const target = webContents ?? browserWindow?.webContents + if (!target || target.isDestroyed()) return + + target.send(WIN_MAIN_RENDERER_EVENT_NAME.taskbar_lyric_set_state, currentState) +} + const getWindowBounds = () => { const display = screen.getPrimaryDisplay() return calcTaskbarLyricBounds({ @@ -77,6 +87,10 @@ export const createWindow = () => { browserWindow?.showInactive() }) + browserWindow.webContents.on('did-finish-load', () => { + sendStateToWindow() + }) + void browserWindow.loadURL(windowUrl) return browserWindow @@ -94,6 +108,11 @@ export const refreshBounds = () => { export const updateWindowState = (state?: TaskbarLyricState) => { currentState = state ?? currentState + sendStateToWindow() +} + +export const sendCurrentStateToWindow = (webContents?: Electron.WebContents) => { + sendStateToWindow(webContents) } export const isExistWindow = () => { diff --git a/src/main/modules/winMain/rendererEvent/index.ts b/src/main/modules/winMain/rendererEvent/index.ts index f2200ad7b2..e79e9547c7 100644 --- a/src/main/modules/winMain/rendererEvent/index.ts +++ b/src/main/modules/winMain/rendererEvent/index.ts @@ -12,6 +12,7 @@ import music from './music' import download from './download' import soundEffect from './soundEffect' import openAPI from './openAPI' +import taskbarLyric from './taskbarLyric' import { sendEvent } from '../main' export * from './app' @@ -39,6 +40,7 @@ export default () => { download() soundEffect() openAPI() + taskbarLyric() global.lx.event_app.on('updated_config', (keys, setting) => { sendConfigChange(setting) diff --git a/src/main/modules/winMain/rendererEvent/taskbarLyric.ts b/src/main/modules/winMain/rendererEvent/taskbarLyric.ts new file mode 100644 index 0000000000..0e9af893b9 --- /dev/null +++ b/src/main/modules/winMain/rendererEvent/taskbarLyric.ts @@ -0,0 +1,14 @@ +import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames' +import { mainOn } from '@common/mainIpc' +import { sendCurrentStateToWindow, updateWindowState } from '@main/modules/taskbarLyric' +import type { TaskbarLyricState } from '@main/modules/taskbarLyric/types' + +export default () => { + mainOn(WIN_MAIN_RENDERER_EVENT_NAME.taskbar_lyric_set_state, ({ params }) => { + updateWindowState(params) + }) + + mainOn(WIN_MAIN_RENDERER_EVENT_NAME.taskbar_lyric_request_refresh, ({ event }) => { + sendCurrentStateToWindow(event.sender) + }) +} diff --git a/src/renderer-taskbar-lyric/main.ts b/src/renderer-taskbar-lyric/main.ts index 1f6502bcdc..188e76912e 100644 --- a/src/renderer-taskbar-lyric/main.ts +++ b/src/renderer-taskbar-lyric/main.ts @@ -1,5 +1,13 @@ import { createApp } from 'vue' import App from './App.vue' +import { patchState } from './store/state' +import { onTaskbarLyricState, requestTaskbarLyricRefresh } from './utils/ipc' + +onTaskbarLyricState((taskbarLyricState) => { + patchState(taskbarLyricState) +}) + +requestTaskbarLyricRefresh() createApp(App).mount('#root') diff --git a/src/renderer-taskbar-lyric/store/state.ts b/src/renderer-taskbar-lyric/store/state.ts index 11c35a7355..2835d0c097 100644 --- a/src/renderer-taskbar-lyric/store/state.ts +++ b/src/renderer-taskbar-lyric/store/state.ts @@ -25,3 +25,7 @@ export const state = shallowReactive({ showSongInfo: true, showCurrentLine: true, }) + +export const patchState = (payload: Partial) => { + Object.assign(state, payload) +} diff --git a/src/renderer-taskbar-lyric/utils/ipc.ts b/src/renderer-taskbar-lyric/utils/ipc.ts index fa23c8a285..159ebc91d6 100644 --- a/src/renderer-taskbar-lyric/utils/ipc.ts +++ b/src/renderer-taskbar-lyric/utils/ipc.ts @@ -1,15 +1,23 @@ -export interface TaskbarLyricStatePayload { - enabled: boolean - isPlaying: boolean - songId: string | null - title: string - artist: string - lyricLine: string - albumCoverUrl: string | null -} +import { ipcRenderer } from 'electron' +import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames' + +export type TaskbarLyricStatePayload = LX.TaskbarLyric.State + +type TaskbarLyricStateListener = (state: TaskbarLyricStatePayload) => void +type RemoveListener = () => void + +export const onTaskbarLyricState = (listener: TaskbarLyricStateListener): RemoveListener => { + const wrappedListener = (_event: Electron.IpcRendererEvent, state: TaskbarLyricStatePayload) => { + listener(state) + } -// Task 3 keeps the renderer static-first. Live IPC wiring will land in a later task. -export const sendTaskbarLyricState = (_state: TaskbarLyricStatePayload) => {} + ipcRenderer.on(WIN_MAIN_RENDERER_EVENT_NAME.taskbar_lyric_set_state, wrappedListener) -// Placeholder for the later refresh handshake once playback state wiring is implemented. -export const requestTaskbarLyricRefresh = () => {} + return () => { + ipcRenderer.removeListener(WIN_MAIN_RENDERER_EVENT_NAME.taskbar_lyric_set_state, wrappedListener) + } +} + +export const requestTaskbarLyricRefresh = () => { + ipcRenderer.send(WIN_MAIN_RENDERER_EVENT_NAME.taskbar_lyric_request_refresh) +} diff --git a/src/renderer/core/lyric.ts b/src/renderer/core/lyric.ts index fc5840fe21..40bcba35e9 100644 --- a/src/renderer/core/lyric.ts +++ b/src/renderer/core/lyric.ts @@ -5,7 +5,7 @@ import { isPlay, musicInfo } from '@renderer/store/player/state' import { setStatusText } from '@renderer/store/player/action' import { markRawList } from '@common/utils/vueTools' import { appSetting } from '@renderer/store/setting' -import { onNewDesktopLyricProcess } from '@renderer/utils/ipc' +import { onNewDesktopLyricProcess, sendTaskbarLyricState } from '@renderer/utils/ipc' const getCurrentTime = () => { return getPlayerCurrentTime() * 1000 @@ -43,6 +43,23 @@ export const sendDesktopLyricInfo = (info: LX.DesktopLyric.LyricActions, transfe if (transferList) desktopLyricPort.postMessage(info, transferList) else desktopLyricPort.postMessage(info) } + +const getTaskbarLyricState = (): LX.TaskbarLyric.State => { + return { + enabled: appSetting['taskbarLyric.enable'], + isPlaying: isPlay.value, + songId: musicInfo.id, + title: musicInfo.name, + artist: musicInfo.singer, + lyricLine: lyric.text, + albumCoverUrl: musicInfo.pic, + } +} + +const syncTaskbarLyricState = () => { + sendTaskbarLyricState(getTaskbarLyricState()) +} + const handleDesktopLyricMessage = (action: LX.DesktopLyric.WinMainActions) => { switch (action) { case 'get_info': @@ -81,6 +98,7 @@ const handleDesktopLyricMessage = (action: LX.DesktopLyric.WinMainActions) => { break } } + export const init = () => { lrc = new Lyric({ shadowContent: false, @@ -88,18 +106,21 @@ export const init = () => { setText(text, Math.max(line, 0)) setStatusText(text) window.app_event.lyricLinePlay(text, line) + syncTaskbarLyricState() // console.log(line, text) }, onSetLyric(lines, offset) { // listening lyrics seting event // console.log(lines) // lines is array of all lyric text setLines(markRawList([...lines])) setText(lines[0] ?? '', 0) - setOffset(offset) // 歌词延迟 - setTempOffset(0) // 重置临时延迟 + setOffset(offset) // Apply parsed lyric offset + setTempOffset(0) // Reset temporary offset + syncTaskbarLyricState() }, onUpdateLyric(lines) { setLines(markRawList([...lines])) setText(lines[0] ?? '', 0) + syncTaskbarLyricState() }, rate: appSetting['player.playbackRate'], // offset: 80, @@ -186,6 +207,8 @@ export const setLyric = () => { lrc.play(time) }) } + + syncTaskbarLyricState() } export const setDisabledAutoPause = (disabledAutoPause: boolean) => { @@ -208,11 +231,13 @@ export const play = () => { const currentTime = getCurrentTime() lrc.play(currentTime) sendDesktopLyricInfo({ action: 'set_play', data: currentTime }) + syncTaskbarLyricState() } export const pause = () => { lrc.pause() sendDesktopLyricInfo({ action: 'set_pause' }) + syncTaskbarLyricState() } export const stop = () => { @@ -220,6 +245,7 @@ export const stop = () => { sendDesktopLyricInfo({ action: 'set_stop' }) // setLines([]) setText('', 0) + syncTaskbarLyricState() } export const sendInfo = () => { @@ -240,4 +266,5 @@ export const sendInfo = () => { played_time: getCurrentTime(), }, }) + syncTaskbarLyricState() } diff --git a/src/renderer/core/useApp/usePlayer/useLyric.ts b/src/renderer/core/useApp/usePlayer/useLyric.ts index 9c04d5cb7c..7eb72e8767 100644 --- a/src/renderer/core/useApp/usePlayer/useLyric.ts +++ b/src/renderer/core/useApp/usePlayer/useLyric.ts @@ -34,6 +34,7 @@ export default () => { window.app_event.on('error', pause) window.app_event.on('musicToggled', setPlayInfo) window.app_event.on('lyricUpdated', setLyric) + window.app_event.on('picUpdated', sendInfo) window.app_event.on('setPlaybackRate', handleApplyPlaybackRate) onBeforeUnmount(() => { @@ -43,6 +44,7 @@ export default () => { window.app_event.off('error', pause) window.app_event.off('musicToggled', setPlayInfo) window.app_event.off('lyricUpdated', setLyric) + window.app_event.off('picUpdated', sendInfo) window.app_event.off('setPlaybackRate', handleApplyPlaybackRate) }) } From ea21024f359ac856b16444609edbea9748e2f891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E6=B3=A2?= Date: Thu, 21 May 2026 12:49:22 +0800 Subject: [PATCH 19/49] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E6=A0=8F=E6=AD=8C=E8=AF=8D=E7=8A=B6=E6=80=81=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/modules/taskbarLyric/main.ts | 18 ++++++++++++++---- src/renderer/core/useApp/usePlayer/useLyric.ts | 2 ++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/main/modules/taskbarLyric/main.ts b/src/main/modules/taskbarLyric/main.ts index aadcefab12..f31d023f94 100644 --- a/src/main/modules/taskbarLyric/main.ts +++ b/src/main/modules/taskbarLyric/main.ts @@ -11,13 +11,23 @@ const TASKBAR_LYRIC_HEIGHT = 56 let browserWindow: Electron.BrowserWindow | null = null let currentState: TaskbarLyricState | null = null -const sendStateToWindow = (webContents?: Electron.WebContents) => { - if (!currentState) return +const getDefaultState = (): TaskbarLyricState => { + return { + enabled: global.lx.appSetting['taskbarLyric.enable'], + isPlaying: false, + songId: null, + title: 'LX Music', + artist: '', + lyricLine: '', + albumCoverUrl: null, + } +} +const sendStateToWindow = (webContents?: Electron.WebContents) => { const target = webContents ?? browserWindow?.webContents if (!target || target.isDestroyed()) return - target.send(WIN_MAIN_RENDERER_EVENT_NAME.taskbar_lyric_set_state, currentState) + target.send(WIN_MAIN_RENDERER_EVENT_NAME.taskbar_lyric_set_state, currentState ?? getDefaultState()) } const getWindowBounds = () => { @@ -107,7 +117,7 @@ export const refreshBounds = () => { } export const updateWindowState = (state?: TaskbarLyricState) => { - currentState = state ?? currentState + currentState = state ?? currentState ?? getDefaultState() sendStateToWindow() } diff --git a/src/renderer/core/useApp/usePlayer/useLyric.ts b/src/renderer/core/useApp/usePlayer/useLyric.ts index 7eb72e8767..4934f6301c 100644 --- a/src/renderer/core/useApp/usePlayer/useLyric.ts +++ b/src/renderer/core/useApp/usePlayer/useLyric.ts @@ -17,12 +17,14 @@ const handleApplyPlaybackRate = debounce(setPlaybackRate, 300) export default () => { init() + sendInfo() const setPlayInfo = () => { stop() sendInfo() } + watch(() => appSetting['taskbarLyric.enable'], sendInfo) watch(() => appSetting['player.isShowLyricTranslation'], setLyric) watch(() => appSetting['player.isShowLyricRoma'], setLyric) watch(() => appSetting['player.isSwapLyricTranslationAndRoma'], setLyric) From 9a378fb98c5d97f11a6a68675c1386bdc0bd93d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E6=B3=A2?= Date: Thu, 21 May 2026 12:59:21 +0800 Subject: [PATCH 20/49] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E6=A0=8F=E6=AD=8C=E8=AF=8D=E8=AE=BE=E7=BD=AE=E4=B8=8E=E7=AA=97?= =?UTF-8?q?=E5=8F=A3=E5=AE=9A=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/types/taskbar_lyric.d.ts | 3 + src/main/modules/taskbarLyric/index.ts | 12 +++ src/main/modules/taskbarLyric/main.ts | 3 + src/main/modules/taskbarLyric/types.ts | 5 ++ src/main/modules/taskbarLyric/utils.ts | 16 +++- src/renderer/core/lyric.ts | 3 + .../core/useApp/usePlayer/useLyric.ts | 3 + .../components/SettingTaskbarLyric.vue | 81 +++++++++++++++++++ src/renderer/views/Setting/index.vue | 3 + 9 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 src/renderer/views/Setting/components/SettingTaskbarLyric.vue diff --git a/src/common/types/taskbar_lyric.d.ts b/src/common/types/taskbar_lyric.d.ts index 1bf47a0f02..88da1612be 100644 --- a/src/common/types/taskbar_lyric.d.ts +++ b/src/common/types/taskbar_lyric.d.ts @@ -8,6 +8,9 @@ declare namespace LX { artist: string lyricLine: string albumCoverUrl: string | null + showCover: boolean + showSongInfo: boolean + showCurrentLine: boolean } } } diff --git a/src/main/modules/taskbarLyric/index.ts b/src/main/modules/taskbarLyric/index.ts index 9ccd86c217..7697850c18 100644 --- a/src/main/modules/taskbarLyric/index.ts +++ b/src/main/modules/taskbarLyric/index.ts @@ -1,8 +1,14 @@ +import { screen, powerMonitor } from 'electron' import { isWin } from '@common/utils' import { closeWindow, createWindow, refreshBounds } from './main' let isRegistered = false +const refreshBoundsIfEnabled = () => { + if (!global.lx.appSetting['taskbarLyric.enable']) return + refreshBounds() +} + const handleConfigChange = (keys: Array) => { if (!keys.some(key => key.startsWith('taskbarLyric.'))) return @@ -29,6 +35,12 @@ export default () => { global.lx.event_app.on('updated_config', (keys) => { handleConfigChange(keys) }) + + screen.on('display-added', refreshBoundsIfEnabled) + screen.on('display-removed', refreshBoundsIfEnabled) + screen.on('display-metrics-changed', refreshBoundsIfEnabled) + powerMonitor.on('resume', refreshBoundsIfEnabled) + powerMonitor.on('unlock-screen', refreshBoundsIfEnabled) } export * from './main' diff --git a/src/main/modules/taskbarLyric/main.ts b/src/main/modules/taskbarLyric/main.ts index f31d023f94..551a8cc26f 100644 --- a/src/main/modules/taskbarLyric/main.ts +++ b/src/main/modules/taskbarLyric/main.ts @@ -20,6 +20,9 @@ const getDefaultState = (): TaskbarLyricState => { artist: '', lyricLine: '', albumCoverUrl: null, + showCover: global.lx.appSetting['taskbarLyric.showCover'], + showSongInfo: global.lx.appSetting['taskbarLyric.showSongInfo'], + showCurrentLine: global.lx.appSetting['taskbarLyric.showCurrentLine'], } } diff --git a/src/main/modules/taskbarLyric/types.ts b/src/main/modules/taskbarLyric/types.ts index 3c02881456..1d3155e6a2 100644 --- a/src/main/modules/taskbarLyric/types.ts +++ b/src/main/modules/taskbarLyric/types.ts @@ -9,6 +9,8 @@ export interface TaskbarLyricBoundsOptions { position: LX.AppSetting['taskbarLyric.position'] } +export type TaskbarPosition = 'top' | 'right' | 'bottom' | 'left' + export interface TaskbarLyricState { enabled: boolean isPlaying: boolean @@ -17,4 +19,7 @@ export interface TaskbarLyricState { artist: string lyricLine: string albumCoverUrl: string | null + showCover: boolean + showSongInfo: boolean + showCurrentLine: boolean } diff --git a/src/main/modules/taskbarLyric/utils.ts b/src/main/modules/taskbarLyric/utils.ts index 01068a670e..5dc0c89936 100644 --- a/src/main/modules/taskbarLyric/utils.ts +++ b/src/main/modules/taskbarLyric/utils.ts @@ -1,11 +1,11 @@ -import type { TaskbarLyricBoundsOptions } from './types' +import type { TaskbarLyricBoundsOptions, TaskbarPosition } from './types' -const getTaskbarPosition = ({ display }: Pick) => { +const getTaskbarPosition = ({ display }: Pick): TaskbarPosition | null => { if (display.workArea.x > display.x) return 'left' if (display.workArea.y > display.y) return 'top' if (display.workArea.x + display.workArea.width < display.x + display.width) return 'right' if (display.workArea.y + display.workArea.height < display.y + display.height) return 'bottom' - return 'bottom' + return null } export const calcTaskbarLyricBounds = ({ display, width, height, position }: TaskbarLyricBoundsOptions): Electron.Rectangle => { @@ -18,6 +18,16 @@ export const calcTaskbarLyricBounds = ({ display, width, height, position }: Tas const verticalY = position === 'center' ? Math.round(display.workArea.y + (display.workArea.height - safeHeight) / 2) : Math.round(display.workArea.y + display.workArea.height - safeHeight) + + if (taskbarPosition == null) { + return { + x: horizontalX, + y: Math.max(display.workArea.y, display.workArea.y + display.workArea.height - safeHeight), + width: safeWidth, + height: safeHeight, + } + } + let x: number let y: number diff --git a/src/renderer/core/lyric.ts b/src/renderer/core/lyric.ts index 40bcba35e9..6151e8a457 100644 --- a/src/renderer/core/lyric.ts +++ b/src/renderer/core/lyric.ts @@ -53,6 +53,9 @@ const getTaskbarLyricState = (): LX.TaskbarLyric.State => { artist: musicInfo.singer, lyricLine: lyric.text, albumCoverUrl: musicInfo.pic, + showCover: appSetting['taskbarLyric.showCover'], + showSongInfo: appSetting['taskbarLyric.showSongInfo'], + showCurrentLine: appSetting['taskbarLyric.showCurrentLine'], } } diff --git a/src/renderer/core/useApp/usePlayer/useLyric.ts b/src/renderer/core/useApp/usePlayer/useLyric.ts index 4934f6301c..533209b759 100644 --- a/src/renderer/core/useApp/usePlayer/useLyric.ts +++ b/src/renderer/core/useApp/usePlayer/useLyric.ts @@ -25,6 +25,9 @@ export default () => { } watch(() => appSetting['taskbarLyric.enable'], sendInfo) + watch(() => appSetting['taskbarLyric.showCover'], sendInfo) + watch(() => appSetting['taskbarLyric.showSongInfo'], sendInfo) + watch(() => appSetting['taskbarLyric.showCurrentLine'], sendInfo) watch(() => appSetting['player.isShowLyricTranslation'], setLyric) watch(() => appSetting['player.isShowLyricRoma'], setLyric) watch(() => appSetting['player.isSwapLyricTranslationAndRoma'], setLyric) diff --git a/src/renderer/views/Setting/components/SettingTaskbarLyric.vue b/src/renderer/views/Setting/components/SettingTaskbarLyric.vue new file mode 100644 index 0000000000..38e463653a --- /dev/null +++ b/src/renderer/views/Setting/components/SettingTaskbarLyric.vue @@ -0,0 +1,81 @@ + + + diff --git a/src/renderer/views/Setting/index.vue b/src/renderer/views/Setting/index.vue index 49780d7f69..bc78f139dd 100644 --- a/src/renderer/views/Setting/index.vue +++ b/src/renderer/views/Setting/index.vue @@ -56,6 +56,7 @@ import SettingBasic from './components/SettingBasic.vue' import SettingPlay from './components/SettingPlay.vue' import SettingPlayDetail from './components/SettingPlayDetail.vue' import SettingDesktopLyric from './components/SettingDesktopLyric.vue' +import SettingTaskbarLyric from './components/SettingTaskbarLyric.vue' import SettingSearch from './components/SettingSearch.vue' import SettingList from './components/SettingList.vue' import SettingDownload from './components/SettingDownload.vue' @@ -76,6 +77,7 @@ export default { SettingPlay, SettingPlayDetail, SettingDesktopLyric, + SettingTaskbarLyric, SettingSearch, SettingList, SettingDownload, @@ -101,6 +103,7 @@ export default { { id: 'SettingPlay', title: t('setting__play') }, { id: 'SettingPlayDetail', title: t('setting__play_detail') }, { id: 'SettingDesktopLyric', title: t('setting__desktop_lyric') }, + { id: 'SettingTaskbarLyric', title: t('setting__taskbar_lyric') }, { id: 'SettingSearch', title: t('setting__search') }, { id: 'SettingList', title: t('setting__list') }, { id: 'SettingDownload', title: t('setting__download') }, From 351774e2fd20485279e93499f8852b446c21fead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E6=B3=A2?= Date: Thu, 21 May 2026 13:04:09 +0800 Subject: [PATCH 21/49] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E6=A0=8F=E6=AD=8C=E8=AF=8D=E5=9B=9E=E9=80=80=E5=AE=9A=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/modules/taskbarLyric/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/modules/taskbarLyric/utils.ts b/src/main/modules/taskbarLyric/utils.ts index 5dc0c89936..f0686743eb 100644 --- a/src/main/modules/taskbarLyric/utils.ts +++ b/src/main/modules/taskbarLyric/utils.ts @@ -21,7 +21,7 @@ export const calcTaskbarLyricBounds = ({ display, width, height, position }: Tas if (taskbarPosition == null) { return { - x: horizontalX, + x: Math.round(display.workArea.x + display.workArea.width - safeWidth), y: Math.max(display.workArea.y, display.workArea.y + display.workArea.height - safeHeight), width: safeWidth, height: safeHeight, From 67d2a017532d819203398aa7dc08819f59d632f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E6=B3=A2?= Date: Thu, 21 May 2026 17:04:00 +0800 Subject: [PATCH 22/49] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E6=A0=8F=E6=AD=8C=E8=AF=8D=E6=94=B6=E5=B0=BE=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/taskbar-lyric-layout.test.mjs | 29 ++++++++- .../specs/2026-05-20-taskbar-lyric-design.md | 2 - src/common/defaultSetting.ts | 1 - src/common/types/app_setting.d.ts | 5 -- src/lang/en-us.json | 1 - src/lang/zh-cn.json | 1 - src/lang/zh-tw.json | 1 - src/main/modules/taskbarLyric/utils.ts | 62 ++++++++++++++++--- src/renderer/types/common.d.ts | 1 + 9 files changed, 82 insertions(+), 21 deletions(-) diff --git a/build-config/tests/taskbar-lyric-layout.test.mjs b/build-config/tests/taskbar-lyric-layout.test.mjs index a70f8db893..22e5658543 100644 --- a/build-config/tests/taskbar-lyric-layout.test.mjs +++ b/build-config/tests/taskbar-lyric-layout.test.mjs @@ -24,7 +24,7 @@ test('calcTaskbarLyricBounds anchors to bottom-right by default', () => { assert.equal(bounds.x, 1560) assert.equal(bounds.y, 1040) assert.equal(bounds.width, 360) - assert.equal(bounds.height, 56) + assert.equal(bounds.height, 40) }) test('calcTaskbarLyricBounds centers the bar on the bottom taskbar', () => { @@ -73,6 +73,31 @@ test('calcTaskbarLyricBounds anchors to the right edge when the taskbar is verti assert.equal(bounds.x, 1860) assert.equal(bounds.y, 1024) - assert.equal(bounds.width, 360) + assert.equal(bounds.width, 60) + assert.equal(bounds.height, 56) +}) + +test('calcTaskbarLyricBounds keeps the bar inside a vertical taskbar on the left', () => { + const bounds = calcTaskbarLyricBounds({ + display: { + x: 0, + y: 0, + width: 1920, + height: 1080, + workArea: { + x: 72, + y: 0, + width: 1848, + height: 1080, + }, + }, + width: 360, + height: 56, + position: 'right', + }) + + assert.equal(bounds.x, 0) + assert.equal(bounds.y, 1024) + assert.equal(bounds.width, 72) assert.equal(bounds.height, 56) }) diff --git a/docs/superpowers/specs/2026-05-20-taskbar-lyric-design.md b/docs/superpowers/specs/2026-05-20-taskbar-lyric-design.md index c8ec8ae27d..4fe4dc54f2 100644 --- a/docs/superpowers/specs/2026-05-20-taskbar-lyric-design.md +++ b/docs/superpowers/specs/2026-05-20-taskbar-lyric-design.md @@ -288,7 +288,6 @@ Recommended keys: 'taskbarLyric.showCover': boolean 'taskbarLyric.showSongInfo': boolean 'taskbarLyric.showCurrentLine': boolean -'taskbarLyric.followTaskbarAutoHide': boolean ``` ### Defaults @@ -299,7 +298,6 @@ Recommended keys: - show cover: `true` - show song info: `true` - show current line: `true` -- follow auto-hide: `true` ### Settings UI copy diff --git a/src/common/defaultSetting.ts b/src/common/defaultSetting.ts index 86a670230f..ebf06bd60f 100644 --- a/src/common/defaultSetting.ts +++ b/src/common/defaultSetting.ts @@ -109,7 +109,6 @@ const defaultSetting: LX.AppSetting = { 'taskbarLyric.showCover': true, 'taskbarLyric.showSongInfo': true, 'taskbarLyric.showCurrentLine': true, - 'taskbarLyric.followTaskbarAutoHide': true, 'list.isClickPlayList': false, 'list.isShowSource': true, diff --git a/src/common/types/app_setting.d.ts b/src/common/types/app_setting.d.ts index 476fb9a327..199ff3c9a3 100644 --- a/src/common/types/app_setting.d.ts +++ b/src/common/types/app_setting.d.ts @@ -720,11 +720,6 @@ declare global { * Show the current active lyric line */ 'taskbarLyric.showCurrentLine': boolean - - /** - * Track taskbar auto-hide visibility - */ - 'taskbarLyric.followTaskbarAutoHide': boolean } } diff --git a/src/lang/en-us.json b/src/lang/en-us.json index 43767d91e6..31720e227a 100644 --- a/src/lang/en-us.json +++ b/src/lang/en-us.json @@ -412,7 +412,6 @@ "setting__taskbar_lyric_show_cover": "Show cover art", "setting__taskbar_lyric_show_song_info": "Show song info", "setting__taskbar_lyric_show_current_line": "Show current lyric line", - "setting__taskbar_lyric_follow_autohide": "Follow taskbar auto-hide", "setting__taskbar_lyric_position": "Taskbar lyric position", "setting__dislike_list_input_tip": "song_name@artist_name\nsong_name\n@artist_name", "setting__dislike_list_save_btn": "Save", diff --git a/src/lang/zh-cn.json b/src/lang/zh-cn.json index 909bfffa2a..addc79d3c8 100644 --- a/src/lang/zh-cn.json +++ b/src/lang/zh-cn.json @@ -412,7 +412,6 @@ "setting__taskbar_lyric_show_cover": "显示封面", "setting__taskbar_lyric_show_song_info": "显示歌曲信息", "setting__taskbar_lyric_show_current_line": "显示当前歌词", - "setting__taskbar_lyric_follow_autohide": "跟随任务栏自动隐藏", "setting__taskbar_lyric_position": "任务栏歌词位置", "setting__dislike_list_input_tip": "歌曲名@艺术家\n歌曲名\n@艺术家", "setting__dislike_list_save_btn": "保存", diff --git a/src/lang/zh-tw.json b/src/lang/zh-tw.json index bec70503f3..453b43a16a 100644 --- a/src/lang/zh-tw.json +++ b/src/lang/zh-tw.json @@ -756,6 +756,5 @@ "setting__taskbar_lyric_show_cover": "顯示封面", "setting__taskbar_lyric_show_song_info": "顯示歌曲資訊", "setting__taskbar_lyric_show_current_line": "顯示目前歌詞", - "setting__taskbar_lyric_follow_autohide": "跟隨工作列自動隱藏", "setting__taskbar_lyric_position": "工作列歌詞位置" } diff --git a/src/main/modules/taskbarLyric/utils.ts b/src/main/modules/taskbarLyric/utils.ts index f0686743eb..e44b577521 100644 --- a/src/main/modules/taskbarLyric/utils.ts +++ b/src/main/modules/taskbarLyric/utils.ts @@ -1,5 +1,49 @@ import type { TaskbarLyricBoundsOptions, TaskbarPosition } from './types' +const getTaskbarRect = ({ display }: Pick): Electron.Rectangle | null => { + if (display.workArea.x > display.x) { + return { + x: display.x, + y: display.y, + width: display.workArea.x - display.x, + height: display.height, + } + } + + if (display.workArea.y > display.y) { + return { + x: display.x, + y: display.y, + width: display.width, + height: display.workArea.y - display.y, + } + } + + const taskbarRight = display.workArea.x + display.workArea.width + const displayRight = display.x + display.width + if (taskbarRight < displayRight) { + return { + x: taskbarRight, + y: display.y, + width: displayRight - taskbarRight, + height: display.height, + } + } + + const taskbarBottom = display.workArea.y + display.workArea.height + const displayBottom = display.y + display.height + if (taskbarBottom < displayBottom) { + return { + x: display.x, + y: taskbarBottom, + width: display.width, + height: displayBottom - taskbarBottom, + } + } + + return null +} + const getTaskbarPosition = ({ display }: Pick): TaskbarPosition | null => { if (display.workArea.x > display.x) return 'left' if (display.workArea.y > display.y) return 'top' @@ -9,15 +53,17 @@ const getTaskbarPosition = ({ display }: Pick { - const safeWidth = Math.max(0, Math.min(Math.round(width), display.width)) - const safeHeight = Math.max(0, Math.min(Math.round(height), display.height)) const taskbarPosition = getTaskbarPosition({ display }) + const taskbarRect = getTaskbarRect({ display }) + + const safeWidth = Math.max(0, Math.min(Math.round(width), taskbarRect?.width ?? display.width)) + const safeHeight = Math.max(0, Math.min(Math.round(height), taskbarRect?.height ?? display.height)) const horizontalX = position === 'center' - ? Math.round(display.workArea.x + (display.workArea.width - safeWidth) / 2) - : Math.round(display.workArea.x + display.workArea.width - safeWidth) + ? Math.round((taskbarRect?.x ?? display.workArea.x) + ((taskbarRect?.width ?? display.workArea.width) - safeWidth) / 2) + : Math.round((taskbarRect?.x ?? display.workArea.x) + (taskbarRect?.width ?? display.workArea.width) - safeWidth) const verticalY = position === 'center' - ? Math.round(display.workArea.y + (display.workArea.height - safeHeight) / 2) - : Math.round(display.workArea.y + display.workArea.height - safeHeight) + ? Math.round((taskbarRect?.y ?? display.workArea.y) + ((taskbarRect?.height ?? display.workArea.height) - safeHeight) / 2) + : Math.round((taskbarRect?.y ?? display.workArea.y) + (taskbarRect?.height ?? display.workArea.height) - safeHeight) if (taskbarPosition == null) { return { @@ -41,13 +87,13 @@ export const calcTaskbarLyricBounds = ({ display, width, height, position }: Tas y = verticalY break case 'right': - x = display.workArea.x + display.workArea.width + x = (taskbarRect?.x ?? display.workArea.x) + (taskbarRect?.width ?? 0) - safeWidth y = verticalY break case 'bottom': default: x = horizontalX - y = display.workArea.y + display.workArea.height + y = (taskbarRect?.y ?? display.workArea.y) + (taskbarRect?.height ?? 0) - safeHeight break } diff --git a/src/renderer/types/common.d.ts b/src/renderer/types/common.d.ts index 7436b11142..386272bb5c 100644 --- a/src/renderer/types/common.d.ts +++ b/src/renderer/types/common.d.ts @@ -11,6 +11,7 @@ import '@common/types/shims_vue' import '@common/types/utils' import '@common/types/theme' import '@common/types/desktop_lyric' +import '@common/types/taskbar_lyric' import '@common/types/ipc_renderer' import '@common/types/config_files' import '@common/types/music_metadata' From b126ea1a3dc0186c10ec5e25980226d9465ee9d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E6=B3=A2?= Date: Thu, 21 May 2026 17:17:20 +0800 Subject: [PATCH 23/49] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E6=A0=8F=E6=AD=8C=E8=AF=8D=E8=BE=93=E5=85=A5=E7=A9=BF=E9=80=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/taskbar-lyric-window.test.mjs | 21 +++++++++++++++++++ src/main/modules/taskbarLyric/main.ts | 3 ++- src/main/modules/taskbarLyric/utils.ts | 8 +++++++ 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 build-config/tests/taskbar-lyric-window.test.mjs diff --git a/build-config/tests/taskbar-lyric-window.test.mjs b/build-config/tests/taskbar-lyric-window.test.mjs new file mode 100644 index 0000000000..17a9d9da80 --- /dev/null +++ b/build-config/tests/taskbar-lyric-window.test.mjs @@ -0,0 +1,21 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { enableTaskbarLyricIgnoreMouseEvents } from '../../src/main/modules/taskbarLyric/utils.ts' + +test('enableTaskbarLyricIgnoreMouseEvents forwards input while ignoring mouse events', () => { + const calls = [] + const fakeWindow = { + setIgnoreMouseEvents(ignore, options) { + calls.push({ ignore, options }) + }, + } + + enableTaskbarLyricIgnoreMouseEvents(fakeWindow) + + assert.deepEqual(calls, [ + { + ignore: true, + options: { forward: true }, + }, + ]) +}) diff --git a/src/main/modules/taskbarLyric/main.ts b/src/main/modules/taskbarLyric/main.ts index 551a8cc26f..362103abfd 100644 --- a/src/main/modules/taskbarLyric/main.ts +++ b/src/main/modules/taskbarLyric/main.ts @@ -4,7 +4,7 @@ import { BrowserWindow, screen } from 'electron' import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames' import { encodePath } from '@common/utils/electron' import type { TaskbarLyricState } from './types' -import { calcTaskbarLyricBounds } from './utils' +import { calcTaskbarLyricBounds, enableTaskbarLyricIgnoreMouseEvents } from './utils' const TASKBAR_LYRIC_HEIGHT = 56 @@ -97,6 +97,7 @@ export const createWindow = () => { }) browserWindow.once('ready-to-show', () => { + enableTaskbarLyricIgnoreMouseEvents(browserWindow!) browserWindow?.showInactive() }) diff --git a/src/main/modules/taskbarLyric/utils.ts b/src/main/modules/taskbarLyric/utils.ts index e44b577521..6d9ebf10f6 100644 --- a/src/main/modules/taskbarLyric/utils.ts +++ b/src/main/modules/taskbarLyric/utils.ts @@ -1,5 +1,13 @@ import type { TaskbarLyricBoundsOptions, TaskbarPosition } from './types' +interface IgnoreMouseEventsTarget { + setIgnoreMouseEvents: (ignore: boolean, options?: Electron.IgnoreMouseEventsOptions) => void +} + +export const enableTaskbarLyricIgnoreMouseEvents = (target: IgnoreMouseEventsTarget) => { + target.setIgnoreMouseEvents(true, { forward: true }) +} + const getTaskbarRect = ({ display }: Pick): Electron.Rectangle | null => { if (display.workArea.x > display.x) { return { From 011ee3867a333a0d43037d7d9de2e5884c1c7a5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E6=B3=A2?= Date: Thu, 21 May 2026 17:33:44 +0800 Subject: [PATCH 24/49] =?UTF-8?q?=E6=94=B6=E7=B4=A7=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E6=A0=8F=E6=AD=8C=E8=AF=8D=E5=B8=83=E5=B1=80=E8=8C=83=E5=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/taskbar-lyric-layout.test.mjs | 39 ++++++--- .../specs/2026-05-20-taskbar-lyric-design.md | 7 +- src/main/modules/taskbarLyric/index.ts | 5 +- src/main/modules/taskbarLyric/main.ts | 22 +++-- src/main/modules/taskbarLyric/utils.ts | 15 +--- src/renderer-taskbar-lyric/App.vue | 83 ++++++------------- 6 files changed, 82 insertions(+), 89 deletions(-) diff --git a/build-config/tests/taskbar-lyric-layout.test.mjs b/build-config/tests/taskbar-lyric-layout.test.mjs index 22e5658543..de0670a972 100644 --- a/build-config/tests/taskbar-lyric-layout.test.mjs +++ b/build-config/tests/taskbar-lyric-layout.test.mjs @@ -52,7 +52,32 @@ test('calcTaskbarLyricBounds centers the bar on the bottom taskbar', () => { assert.equal(bounds.height, 60) }) -test('calcTaskbarLyricBounds anchors to the right edge when the taskbar is vertical on the right', () => { +test('calcTaskbarLyricBounds keeps the bar inside a top taskbar strip', () => { + const bounds = calcTaskbarLyricBounds({ + display: { + x: 0, + y: 0, + width: 1920, + height: 1080, + workArea: { + x: 0, + y: 40, + width: 1920, + height: 1040, + }, + }, + width: 360, + height: 56, + position: 'center', + }) + + assert.equal(bounds.x, 780) + assert.equal(bounds.y, 0) + assert.equal(bounds.width, 360) + assert.equal(bounds.height, 40) +}) + +test('calcTaskbarLyricBounds returns null for a vertical taskbar on the right', () => { const bounds = calcTaskbarLyricBounds({ display: { x: 0, @@ -71,13 +96,10 @@ test('calcTaskbarLyricBounds anchors to the right edge when the taskbar is verti position: 'right', }) - assert.equal(bounds.x, 1860) - assert.equal(bounds.y, 1024) - assert.equal(bounds.width, 60) - assert.equal(bounds.height, 56) + assert.equal(bounds, null) }) -test('calcTaskbarLyricBounds keeps the bar inside a vertical taskbar on the left', () => { +test('calcTaskbarLyricBounds returns null for a vertical taskbar on the left', () => { const bounds = calcTaskbarLyricBounds({ display: { x: 0, @@ -96,8 +118,5 @@ test('calcTaskbarLyricBounds keeps the bar inside a vertical taskbar on the left position: 'right', }) - assert.equal(bounds.x, 0) - assert.equal(bounds.y, 1024) - assert.equal(bounds.width, 72) - assert.equal(bounds.height, 56) + assert.equal(bounds, null) }) diff --git a/docs/superpowers/specs/2026-05-20-taskbar-lyric-design.md b/docs/superpowers/specs/2026-05-20-taskbar-lyric-design.md index 4fe4dc54f2..a4f4aeae13 100644 --- a/docs/superpowers/specs/2026-05-20-taskbar-lyric-design.md +++ b/docs/superpowers/specs/2026-05-20-taskbar-lyric-design.md @@ -225,12 +225,12 @@ The window should anchor based on: - current display - configured alignment (`right` or `center`) - configured width -- taskbar auto-hide state if available Expected behavior: - if taskbar is at bottom, the bar hugs the bottom edge -- if taskbar is at top/left/right, the bar adapts accordingly +- if taskbar is at top, the bar hugs the top edge +- V1 supports primary-display horizontal taskbars only; left/right vertical taskbars are not supported - if monitor scaling changes, the bar recomputes bounds - if Explorer or display topology changes, the bar repositions @@ -384,7 +384,8 @@ There is no dedicated automated test suite today, so V1 verification will be mos - Windows 11 - 100%, 125%, 150% display scaling - bottom taskbar -- top/left/right taskbar where supported +- top taskbar +- left/right vertical taskbars do not show the overlay in V1 - taskbar auto-hide on/off - Explorer restart recovery diff --git a/src/main/modules/taskbarLyric/index.ts b/src/main/modules/taskbarLyric/index.ts index 7697850c18..8b83ceef3d 100644 --- a/src/main/modules/taskbarLyric/index.ts +++ b/src/main/modules/taskbarLyric/index.ts @@ -1,12 +1,13 @@ import { screen, powerMonitor } from 'electron' import { isWin } from '@common/utils' -import { closeWindow, createWindow, refreshBounds } from './main' +import { closeWindow, createWindow, refreshBounds, isExistWindow } from './main' let isRegistered = false const refreshBoundsIfEnabled = () => { if (!global.lx.appSetting['taskbarLyric.enable']) return - refreshBounds() + if (isExistWindow()) refreshBounds() + else createWindow() } const handleConfigChange = (keys: Array) => { diff --git a/src/main/modules/taskbarLyric/main.ts b/src/main/modules/taskbarLyric/main.ts index 362103abfd..c6afccc8c2 100644 --- a/src/main/modules/taskbarLyric/main.ts +++ b/src/main/modules/taskbarLyric/main.ts @@ -33,7 +33,7 @@ const sendStateToWindow = (webContents?: Electron.WebContents) => { target.send(WIN_MAIN_RENDERER_EVENT_NAME.taskbar_lyric_set_state, currentState ?? getDefaultState()) } -const getWindowBounds = () => { +const getWindowBounds = (): Electron.Rectangle | null => { const display = screen.getPrimaryDisplay() return calcTaskbarLyricBounds({ display: { @@ -57,15 +57,22 @@ const getWindowUrl = () => { export const createWindow = () => { if (browserWindow) { - refreshBounds() + const bounds = getWindowBounds() + if (!bounds) { + closeWindow() + return null + } + + browserWindow.setBounds(bounds) return browserWindow } const windowUrl = getWindowUrl() - if (!windowUrl) return null + const bounds = getWindowBounds() + if (!windowUrl || !bounds) return null browserWindow = new BrowserWindow({ - ...getWindowBounds(), + ...bounds, useContentSize: true, frame: false, transparent: true, @@ -117,7 +124,12 @@ export const closeWindow = () => { export const refreshBounds = () => { if (!browserWindow) return - browserWindow.setBounds(getWindowBounds()) + const bounds = getWindowBounds() + if (!bounds) { + closeWindow() + return + } + browserWindow.setBounds(bounds) } export const updateWindowState = (state?: TaskbarLyricState) => { diff --git a/src/main/modules/taskbarLyric/utils.ts b/src/main/modules/taskbarLyric/utils.ts index 6d9ebf10f6..3758720a5b 100644 --- a/src/main/modules/taskbarLyric/utils.ts +++ b/src/main/modules/taskbarLyric/utils.ts @@ -60,8 +60,10 @@ const getTaskbarPosition = ({ display }: Pick { +export const calcTaskbarLyricBounds = ({ display, width, height, position }: TaskbarLyricBoundsOptions): Electron.Rectangle | null => { const taskbarPosition = getTaskbarPosition({ display }) + if (taskbarPosition === 'left' || taskbarPosition === 'right') return null + const taskbarRect = getTaskbarRect({ display }) const safeWidth = Math.max(0, Math.min(Math.round(width), taskbarRect?.width ?? display.width)) @@ -69,9 +71,6 @@ export const calcTaskbarLyricBounds = ({ display, width, height, position }: Tas const horizontalX = position === 'center' ? Math.round((taskbarRect?.x ?? display.workArea.x) + ((taskbarRect?.width ?? display.workArea.width) - safeWidth) / 2) : Math.round((taskbarRect?.x ?? display.workArea.x) + (taskbarRect?.width ?? display.workArea.width) - safeWidth) - const verticalY = position === 'center' - ? Math.round((taskbarRect?.y ?? display.workArea.y) + ((taskbarRect?.height ?? display.workArea.height) - safeHeight) / 2) - : Math.round((taskbarRect?.y ?? display.workArea.y) + (taskbarRect?.height ?? display.workArea.height) - safeHeight) if (taskbarPosition == null) { return { @@ -90,14 +89,6 @@ export const calcTaskbarLyricBounds = ({ display, width, height, position }: Tas x = horizontalX y = display.y break - case 'left': - x = display.x - y = verticalY - break - case 'right': - x = (taskbarRect?.x ?? display.workArea.x) + (taskbarRect?.width ?? 0) - safeWidth - y = verticalY - break case 'bottom': default: x = horizontalX diff --git a/src/renderer-taskbar-lyric/App.vue b/src/renderer-taskbar-lyric/App.vue index d6a6e4fde2..1099ccaf58 100644 --- a/src/renderer-taskbar-lyric/App.vue +++ b/src/renderer-taskbar-lyric/App.vue @@ -1,19 +1,16 @@ @@ -45,39 +42,30 @@ body { .taskbar-lyric-shell { display: flex; align-items: center; - gap: 12px; + gap: 8px; width: 100%; height: 100%; - padding: 8px 14px; - border-radius: 16px; + padding: 4px 10px; + border-radius: 10px; background: - linear-gradient(135deg, rgba(15, 23, 42, 0.92), rgba(30, 41, 59, 0.72)), - rgba(15, 23, 42, 0.64); - border: 1px solid rgba(148, 163, 184, 0.2); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06); - backdrop-filter: blur(14px); - transition: opacity 0.2s ease, transform 0.2s ease; + linear-gradient(135deg, rgba(15, 23, 42, 0.88), rgba(30, 41, 59, 0.7)), + rgba(15, 23, 42, 0.58); + border: 1px solid rgba(148, 163, 184, 0.16); + backdrop-filter: blur(10px); + transition: opacity 0.2s ease; &.disabled { opacity: 0.78; } - - &.playing { - .status { - color: #34d399; - background-color: rgba(52, 211, 153, 0.14); - } - } } .cover { flex: none; - width: 40px; - height: 40px; - border-radius: 12px; + width: 26px; + height: 26px; + border-radius: 7px; overflow: hidden; background: linear-gradient(135deg, rgba(59, 130, 246, 0.9), rgba(16, 185, 129, 0.86)); - box-shadow: 0 10px 24px rgba(15, 23, 42, 0.32); img { display: block; @@ -92,9 +80,9 @@ body { place-items: center; width: 100%; height: 100%; - font-size: 13px; + font-size: 10px; font-weight: 700; - letter-spacing: 0.14em; + letter-spacing: 0.08em; } .content { @@ -103,23 +91,16 @@ body { min-width: 0; flex-direction: column; justify-content: center; - gap: 5px; -} - -.meta-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - min-width: 0; + gap: 2px; } .song-info { display: flex; align-items: baseline; - gap: 8px; + gap: 5px; min-width: 0; - font-size: 13px; + font-size: 12px; + line-height: 1.1; } .title, @@ -137,25 +118,13 @@ body { .separator, .artist { - color: rgba(226, 232, 240, 0.7); -} - -.status { - flex: none; - padding: 2px 8px; - border-radius: 999px; - color: #fbbf24; - background-color: rgba(251, 191, 36, 0.12); - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.08em; + color: rgba(226, 232, 240, 0.66); } .lyric-line { margin: 0; color: rgba(226, 232, 240, 0.94); - font-size: 12px; - line-height: 1.3; + font-size: 11px; + line-height: 1.1; } From fc59481cf2621032457a9a1d9c0707768843bbab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E6=B3=A2?= Date: Thu, 21 May 2026 17:44:38 +0800 Subject: [PATCH 25/49] =?UTF-8?q?=E8=A1=A5=E5=85=85=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E6=A0=8F=E6=AD=8C=E8=AF=8D=E6=94=AF=E6=8C=81=E8=8C=83=E5=9B=B4?= =?UTF-8?q?=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lang/en-us.json | 2 +- src/lang/zh-cn.json | 2 +- src/lang/zh-tw.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/en-us.json b/src/lang/en-us.json index 31720e227a..3dd3e13e8b 100644 --- a/src/lang/en-us.json +++ b/src/lang/en-us.json @@ -408,7 +408,7 @@ "setting__desktop_lyric_unplay_color": "Not Played", "setting__taskbar_lyric": "Taskbar Lyric", "setting__taskbar_lyric_enable": "Enable taskbar lyric", - "setting__taskbar_lyric_experimental": "Windows-only experimental feature that visually attaches to the taskbar rather than using a true system taskbar extension", + "setting__taskbar_lyric_experimental": "Windows-only experimental feature that visually attaches to the taskbar rather than using a true system taskbar extension; currently supports the primary display top or bottom taskbar only", "setting__taskbar_lyric_show_cover": "Show cover art", "setting__taskbar_lyric_show_song_info": "Show song info", "setting__taskbar_lyric_show_current_line": "Show current lyric line", diff --git a/src/lang/zh-cn.json b/src/lang/zh-cn.json index addc79d3c8..af198148eb 100644 --- a/src/lang/zh-cn.json +++ b/src/lang/zh-cn.json @@ -408,7 +408,7 @@ "setting__desktop_lyric_unplay_color": "未播放颜色", "setting__taskbar_lyric": "任务栏歌词", "setting__taskbar_lyric_enable": "启用任务栏歌词", - "setting__taskbar_lyric_experimental": "仅限 Windows 的实验性功能,视觉上贴附任务栏,并非真正的系统任务栏扩展", + "setting__taskbar_lyric_experimental": "仅限 Windows 的实验性功能,视觉上贴附任务栏,并非真正的系统任务栏扩展;当前仅支持主屏顶部或底部任务栏", "setting__taskbar_lyric_show_cover": "显示封面", "setting__taskbar_lyric_show_song_info": "显示歌曲信息", "setting__taskbar_lyric_show_current_line": "显示当前歌词", diff --git a/src/lang/zh-tw.json b/src/lang/zh-tw.json index 453b43a16a..5350e4eb08 100644 --- a/src/lang/zh-tw.json +++ b/src/lang/zh-tw.json @@ -752,7 +752,7 @@ "user_api_import_online__title": "從線上匯入自訂來源 API" , "setting__taskbar_lyric": "工作列歌詞", "setting__taskbar_lyric_enable": "啟用工作列歌詞", - "setting__taskbar_lyric_experimental": "僅限 Windows 的實驗性功能,會在視覺上貼附工作列,並非真正的系統工作列擴充", + "setting__taskbar_lyric_experimental": "僅限 Windows 的實驗性功能,會在視覺上貼附工作列,並非真正的系統工作列擴充;目前僅支援主螢幕頂部或底部工作列", "setting__taskbar_lyric_show_cover": "顯示封面", "setting__taskbar_lyric_show_song_info": "顯示歌曲資訊", "setting__taskbar_lyric_show_current_line": "顯示目前歌詞", From 5d441892da61d905f43072b6bbb70b38b105ffac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E6=B3=A2?= Date: Thu, 21 May 2026 19:32:05 +0800 Subject: [PATCH 26/49] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E6=A0=8F=E6=AD=8C=E8=AF=8D=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/defaultSetting.ts | 3 +- src/common/ipcNames.ts | 2 + src/common/types/app_setting.d.ts | 5 ++ src/common/types/taskbar_lyric.d.ts | 1 + src/lang/en-us.json | 1 + src/lang/zh-cn.json | 1 + src/lang/zh-tw.json | 3 +- src/main/modules/taskbarLyric/index.ts | 3 +- src/main/modules/taskbarLyric/main.ts | 67 ++++++++++++++++++- src/main/modules/taskbarLyric/types.ts | 6 ++ src/main/modules/taskbarLyric/utils.ts | 31 +++++++-- .../winMain/rendererEvent/taskbarLyric.ts | 12 +++- src/renderer-taskbar-lyric/App.vue | 50 +++++++++++++- src/renderer-taskbar-lyric/main.ts | 6 +- src/renderer-taskbar-lyric/store/state.ts | 2 + src/renderer-taskbar-lyric/utils/ipc.ts | 8 +++ src/renderer/core/lyric.ts | 2 +- .../components/SettingTaskbarLyric.vue | 32 +++++++++ 18 files changed, 220 insertions(+), 15 deletions(-) diff --git a/src/common/defaultSetting.ts b/src/common/defaultSetting.ts index ebf06bd60f..3188d46c00 100644 --- a/src/common/defaultSetting.ts +++ b/src/common/defaultSetting.ts @@ -105,7 +105,8 @@ const defaultSetting: LX.AppSetting = { 'taskbarLyric.enable': false, 'taskbarLyric.position': 'right', - 'taskbarLyric.width': 360, + 'taskbarLyric.width': 230, + 'taskbarLyric.offsetX': 0, 'taskbarLyric.showCover': true, 'taskbarLyric.showSongInfo': true, 'taskbarLyric.showCurrentLine': true, diff --git a/src/common/ipcNames.ts b/src/common/ipcNames.ts index c72e1471d3..f7154826e9 100644 --- a/src/common/ipcNames.ts +++ b/src/common/ipcNames.ts @@ -143,6 +143,8 @@ const modules = { process_new_taskbar_lyric_client: 'process_new_taskbar_lyric_client', taskbar_lyric_set_state: 'taskbar_lyric_set_state', taskbar_lyric_request_refresh: 'taskbar_lyric_request_refresh', + taskbar_lyric_drag_move: 'taskbar_lyric_drag_move', + taskbar_lyric_drag_end: 'taskbar_lyric_drag_end', player_action_set_buttons: 'player_action_set_buttons', // player_action_set_thumbnail_clip: 'player_action_set_thumbnail_clip', diff --git a/src/common/types/app_setting.d.ts b/src/common/types/app_setting.d.ts index 199ff3c9a3..a97c1fae82 100644 --- a/src/common/types/app_setting.d.ts +++ b/src/common/types/app_setting.d.ts @@ -706,6 +706,11 @@ declare global { */ 'taskbarLyric.width': number + /** + * Horizontal offset from the configured taskbar lyric anchor + */ + 'taskbarLyric.offsetX': number + /** * Show album cover art in the bar */ diff --git a/src/common/types/taskbar_lyric.d.ts b/src/common/types/taskbar_lyric.d.ts index 88da1612be..0855b4d643 100644 --- a/src/common/types/taskbar_lyric.d.ts +++ b/src/common/types/taskbar_lyric.d.ts @@ -8,6 +8,7 @@ declare namespace LX { artist: string lyricLine: string albumCoverUrl: string | null + offsetX: number showCover: boolean showSongInfo: boolean showCurrentLine: boolean diff --git a/src/lang/en-us.json b/src/lang/en-us.json index 3dd3e13e8b..aa51cc8366 100644 --- a/src/lang/en-us.json +++ b/src/lang/en-us.json @@ -413,6 +413,7 @@ "setting__taskbar_lyric_show_song_info": "Show song info", "setting__taskbar_lyric_show_current_line": "Show current lyric line", "setting__taskbar_lyric_position": "Taskbar lyric position", + "setting__taskbar_lyric_width": "Taskbar lyric width: {width}px", "setting__dislike_list_input_tip": "song_name@artist_name\nsong_name\n@artist_name", "setting__dislike_list_save_btn": "Save", "setting__dislike_list_tips": "1. One line per entry. If there is an \"@\" symbol in the name of the song or artist, it needs to be replaced with \"#\"\n2. Specify a song by a certain artist: song_name@artist_name\n3. Specify a song: song_name\n4. Specify an artist: @artist_name", diff --git a/src/lang/zh-cn.json b/src/lang/zh-cn.json index af198148eb..b95678b099 100644 --- a/src/lang/zh-cn.json +++ b/src/lang/zh-cn.json @@ -413,6 +413,7 @@ "setting__taskbar_lyric_show_song_info": "显示歌曲信息", "setting__taskbar_lyric_show_current_line": "显示当前歌词", "setting__taskbar_lyric_position": "任务栏歌词位置", + "setting__taskbar_lyric_width": "任务栏歌词宽度:{width}px", "setting__dislike_list_input_tip": "歌曲名@艺术家\n歌曲名\n@艺术家", "setting__dislike_list_save_btn": "保存", "setting__dislike_list_tips": "1. 每条一行,若歌曲或者艺术家名字中存在「@」符号,需要将其替换成「#」\n2. 指定某艺术家的某首歌:歌曲名@艺术家\n3. 指定某首歌:歌曲名\n4. 指定某艺术家:@艺术家", diff --git a/src/lang/zh-tw.json b/src/lang/zh-tw.json index 5350e4eb08..522a2d6fdf 100644 --- a/src/lang/zh-tw.json +++ b/src/lang/zh-tw.json @@ -756,5 +756,6 @@ "setting__taskbar_lyric_show_cover": "顯示封面", "setting__taskbar_lyric_show_song_info": "顯示歌曲資訊", "setting__taskbar_lyric_show_current_line": "顯示目前歌詞", - "setting__taskbar_lyric_position": "工作列歌詞位置" + "setting__taskbar_lyric_position": "工作列歌詞位置", + "setting__taskbar_lyric_width": "工作列歌詞寬度:{width}px" } diff --git a/src/main/modules/taskbarLyric/index.ts b/src/main/modules/taskbarLyric/index.ts index 8b83ceef3d..c15e245672 100644 --- a/src/main/modules/taskbarLyric/index.ts +++ b/src/main/modules/taskbarLyric/index.ts @@ -21,7 +21,8 @@ const handleConfigChange = (keys: Array) => { if (global.lx.appSetting['taskbarLyric.enable'] && ( keys.includes('taskbarLyric.position') || - keys.includes('taskbarLyric.width') + keys.includes('taskbarLyric.width') || + keys.includes('taskbarLyric.offsetX') )) refreshBounds() } diff --git a/src/main/modules/taskbarLyric/main.ts b/src/main/modules/taskbarLyric/main.ts index c6afccc8c2..4c9f5107a6 100644 --- a/src/main/modules/taskbarLyric/main.ts +++ b/src/main/modules/taskbarLyric/main.ts @@ -4,12 +4,36 @@ import { BrowserWindow, screen } from 'electron' import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames' import { encodePath } from '@common/utils/electron' import type { TaskbarLyricState } from './types' -import { calcTaskbarLyricBounds, enableTaskbarLyricIgnoreMouseEvents } from './utils' +import { calcTaskbarLyricBounds, calcTaskbarLyricClampedOffsetX } from './utils' const TASKBAR_LYRIC_HEIGHT = 56 +const TASKBAR_LYRIC_ALWAYS_ON_TOP_LEVEL = 'pop-up-menu' +const TASKBAR_LYRIC_ZORDER_INTERVAL = 1500 let browserWindow: Electron.BrowserWindow | null = null let currentState: TaskbarLyricState | null = null +let dragOffsetX: number | null = null +let zOrderTimer: NodeJS.Timeout | null = null + +const clearZOrderTimer = () => { + if (!zOrderTimer) return + clearInterval(zOrderTimer) + zOrderTimer = null +} + +const refreshWindowZOrder = () => { + if (!browserWindow || browserWindow.isDestroyed()) return + browserWindow.setAlwaysOnTop(true, TASKBAR_LYRIC_ALWAYS_ON_TOP_LEVEL) + browserWindow.moveTop() +} + +const ensureWindowZOrder = () => { + clearZOrderTimer() + refreshWindowZOrder() + zOrderTimer = setInterval(() => { + refreshWindowZOrder() + }, TASKBAR_LYRIC_ZORDER_INTERVAL) +} const getDefaultState = (): TaskbarLyricState => { return { @@ -20,6 +44,7 @@ const getDefaultState = (): TaskbarLyricState => { artist: '', lyricLine: '', albumCoverUrl: null, + offsetX: global.lx.appSetting['taskbarLyric.offsetX'], showCover: global.lx.appSetting['taskbarLyric.showCover'], showSongInfo: global.lx.appSetting['taskbarLyric.showSongInfo'], showCurrentLine: global.lx.appSetting['taskbarLyric.showCurrentLine'], @@ -35,7 +60,8 @@ const sendStateToWindow = (webContents?: Electron.WebContents) => { const getWindowBounds = (): Electron.Rectangle | null => { const display = screen.getPrimaryDisplay() - return calcTaskbarLyricBounds({ + const offsetX = dragOffsetX ?? global.lx.appSetting['taskbarLyric.offsetX'] + const bounds = calcTaskbarLyricBounds({ display: { ...display.bounds, workArea: display.workArea, @@ -43,6 +69,21 @@ const getWindowBounds = (): Electron.Rectangle | null => { width: global.lx.appSetting['taskbarLyric.width'], height: TASKBAR_LYRIC_HEIGHT, position: global.lx.appSetting['taskbarLyric.position'], + offsetX, + }) + return bounds +} + +const getClampedOffsetX = (offsetX: number) => { + const display = screen.getPrimaryDisplay() + return calcTaskbarLyricClampedOffsetX({ + display: { + ...display.bounds, + workArea: display.workArea, + }, + width: global.lx.appSetting['taskbarLyric.width'], + position: global.lx.appSetting['taskbarLyric.position'], + offsetX, }) } @@ -100,11 +141,12 @@ export const createWindow = () => { }) browserWindow.on('closed', () => { + clearZOrderTimer() browserWindow = null }) browserWindow.once('ready-to-show', () => { - enableTaskbarLyricIgnoreMouseEvents(browserWindow!) + ensureWindowZOrder() browserWindow?.showInactive() }) @@ -119,6 +161,7 @@ export const createWindow = () => { export const closeWindow = () => { if (!browserWindow) return + clearZOrderTimer() browserWindow.close() } @@ -134,6 +177,7 @@ export const refreshBounds = () => { export const updateWindowState = (state?: TaskbarLyricState) => { currentState = state ?? currentState ?? getDefaultState() + currentState.offsetX = dragOffsetX ?? global.lx.appSetting['taskbarLyric.offsetX'] sendStateToWindow() } @@ -144,3 +188,20 @@ export const sendCurrentStateToWindow = (webContents?: Electron.WebContents) => export const isExistWindow = () => { return !!browserWindow } + +export const updateDragOffsetX = (offsetX: number) => { + dragOffsetX = getClampedOffsetX(offsetX) + if (currentState) currentState.offsetX = dragOffsetX + refreshBounds() + sendStateToWindow() +} + +export const commitDragOffsetX = () => { + if (dragOffsetX == null) return + const nextOffsetX = getClampedOffsetX(dragOffsetX) + dragOffsetX = null + if (currentState) currentState.offsetX = nextOffsetX + global.lx.event_app.update_config({ + 'taskbarLyric.offsetX': nextOffsetX, + }) +} diff --git a/src/main/modules/taskbarLyric/types.ts b/src/main/modules/taskbarLyric/types.ts index 1d3155e6a2..69c38eb887 100644 --- a/src/main/modules/taskbarLyric/types.ts +++ b/src/main/modules/taskbarLyric/types.ts @@ -7,6 +7,7 @@ export interface TaskbarLyricBoundsOptions { width: number height: number position: LX.AppSetting['taskbarLyric.position'] + offsetX: number } export type TaskbarPosition = 'top' | 'right' | 'bottom' | 'left' @@ -19,7 +20,12 @@ export interface TaskbarLyricState { artist: string lyricLine: string albumCoverUrl: string | null + offsetX: number showCover: boolean showSongInfo: boolean showCurrentLine: boolean } + +export interface TaskbarLyricDragMoveParams { + offsetX: number +} diff --git a/src/main/modules/taskbarLyric/utils.ts b/src/main/modules/taskbarLyric/utils.ts index 3758720a5b..950b883153 100644 --- a/src/main/modules/taskbarLyric/utils.ts +++ b/src/main/modules/taskbarLyric/utils.ts @@ -8,6 +8,10 @@ export const enableTaskbarLyricIgnoreMouseEvents = (target: IgnoreMouseEventsTar target.setIgnoreMouseEvents(true, { forward: true }) } +const clamp = (value: number, min: number, max: number) => { + return Math.min(Math.max(value, min), max) +} + const getTaskbarRect = ({ display }: Pick): Electron.Rectangle | null => { if (display.workArea.x > display.x) { return { @@ -60,7 +64,7 @@ const getTaskbarPosition = ({ display }: Pick { +export const calcTaskbarLyricBounds = ({ display, width, height, position, offsetX }: TaskbarLyricBoundsOptions): Electron.Rectangle | null => { const taskbarPosition = getTaskbarPosition({ display }) if (taskbarPosition === 'left' || taskbarPosition === 'right') return null @@ -68,9 +72,14 @@ export const calcTaskbarLyricBounds = ({ display, width, height, position }: Tas const safeWidth = Math.max(0, Math.min(Math.round(width), taskbarRect?.width ?? display.width)) const safeHeight = Math.max(0, Math.min(Math.round(height), taskbarRect?.height ?? display.height)) - const horizontalX = position === 'center' - ? Math.round((taskbarRect?.x ?? display.workArea.x) + ((taskbarRect?.width ?? display.workArea.width) - safeWidth) / 2) - : Math.round((taskbarRect?.x ?? display.workArea.x) + (taskbarRect?.width ?? display.workArea.width) - safeWidth) + const horizontalAreaX = taskbarRect?.x ?? display.workArea.x + const horizontalAreaWidth = taskbarRect?.width ?? display.workArea.width + const baseHorizontalX = position === 'center' + ? Math.round(horizontalAreaX + (horizontalAreaWidth - safeWidth) / 2) + : Math.round(horizontalAreaX + horizontalAreaWidth - safeWidth) + const minX = Math.round(horizontalAreaX) + const maxX = Math.round(horizontalAreaX + horizontalAreaWidth - safeWidth) + const horizontalX = clamp(Math.round(baseHorizontalX + offsetX), minX, maxX) if (taskbarPosition == null) { return { @@ -103,3 +112,17 @@ export const calcTaskbarLyricBounds = ({ display, width, height, position }: Tas height: safeHeight, } } + +export const calcTaskbarLyricClampedOffsetX = ({ display, width, position, offsetX }: Pick) => { + const taskbarRect = getTaskbarRect({ display }) + const horizontalAreaX = taskbarRect?.x ?? display.workArea.x + const horizontalAreaWidth = taskbarRect?.width ?? display.workArea.width + const safeWidth = Math.max(0, Math.min(Math.round(width), horizontalAreaWidth)) + const baseHorizontalX = position === 'center' + ? Math.round(horizontalAreaX + (horizontalAreaWidth - safeWidth) / 2) + : Math.round(horizontalAreaX + horizontalAreaWidth - safeWidth) + const minX = Math.round(horizontalAreaX) + const maxX = Math.round(horizontalAreaX + horizontalAreaWidth - safeWidth) + const actualX = clamp(Math.round(baseHorizontalX + offsetX), minX, maxX) + return actualX - baseHorizontalX +} diff --git a/src/main/modules/winMain/rendererEvent/taskbarLyric.ts b/src/main/modules/winMain/rendererEvent/taskbarLyric.ts index 0e9af893b9..4be9fd122b 100644 --- a/src/main/modules/winMain/rendererEvent/taskbarLyric.ts +++ b/src/main/modules/winMain/rendererEvent/taskbarLyric.ts @@ -1,7 +1,7 @@ import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames' import { mainOn } from '@common/mainIpc' -import { sendCurrentStateToWindow, updateWindowState } from '@main/modules/taskbarLyric' -import type { TaskbarLyricState } from '@main/modules/taskbarLyric/types' +import { commitDragOffsetX, sendCurrentStateToWindow, updateDragOffsetX, updateWindowState } from '@main/modules/taskbarLyric' +import type { TaskbarLyricDragMoveParams, TaskbarLyricState } from '@main/modules/taskbarLyric/types' export default () => { mainOn(WIN_MAIN_RENDERER_EVENT_NAME.taskbar_lyric_set_state, ({ params }) => { @@ -11,4 +11,12 @@ export default () => { mainOn(WIN_MAIN_RENDERER_EVENT_NAME.taskbar_lyric_request_refresh, ({ event }) => { sendCurrentStateToWindow(event.sender) }) + + mainOn(WIN_MAIN_RENDERER_EVENT_NAME.taskbar_lyric_drag_move, ({ params }) => { + updateDragOffsetX(params.offsetX) + }) + + mainOn(WIN_MAIN_RENDERER_EVENT_NAME.taskbar_lyric_drag_end, () => { + commitDragOffsetX() + }) } diff --git a/src/renderer-taskbar-lyric/App.vue b/src/renderer-taskbar-lyric/App.vue index 1099ccaf58..7c97ecc374 100644 --- a/src/renderer-taskbar-lyric/App.vue +++ b/src/renderer-taskbar-lyric/App.vue @@ -1,5 +1,9 @@