diff --git a/components.d.ts b/components.d.ts
index 50895f2..8c59879 100644
--- a/components.d.ts
+++ b/components.d.ts
@@ -14,9 +14,11 @@ declare module 'vue' {
AbLoopDialog: typeof import('./src/components/modals/AbLoopDialog.vue')['default']
AboutSettings: typeof import('./src/components/settings/custom/AboutSettings.vue')['default']
AmllDbServerConfig: typeof import('./src/components/settings/custom/AmllDbServerConfig.vue')['default']
+ AMLLLyrics: typeof import('./src/components/player/Lyrics/AMLLLyrics.vue')['default']
AppBackground: typeof import('./src/components/AppBackground.vue')['default']
AutoCloseDialog: typeof import('./src/components/modals/AutoCloseDialog.vue')['default']
BackgroundImagePicker: typeof import('./src/components/settings/custom/BackgroundImagePicker.vue')['default']
+ BackgroundRender: typeof import('./src/components/player/FullPlayer/BackgroundRender.vue')['default']
BottomSpectrum: typeof import('./src/components/player/FullPlayer/BottomSpectrum.vue')['default']
ComboboxAnchor: typeof import('reka-ui')['ComboboxAnchor']
ComboboxContent: typeof import('reka-ui')['ComboboxContent']
diff --git a/package.json b/package.json
index 4281078..99d8269 100644
--- a/package.json
+++ b/package.json
@@ -48,8 +48,17 @@
"docs:preview": "vitepress preview docs"
},
"dependencies": {
+ "@applemusic-like-lyrics/core": "^0.5.1",
+ "@applemusic-like-lyrics/lyric": "^1.0.1",
"@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0",
+ "@pixi/app": "^7.4.3",
+ "@pixi/core": "^7.4.3",
+ "@pixi/display": "^7.4.3",
+ "@pixi/filter-blur": "^7.4.3",
+ "@pixi/filter-bulge-pinch": "^5.1.1",
+ "@pixi/filter-color-matrix": "^7.4.3",
+ "@pixi/sprite": "^7.4.3",
"@hono/node-server": "^2.0.2",
"@material/material-color-utilities": "^0.4.0",
"@vueuse/core": "^14.2.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d6a0af1..a6283d5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,6 +8,12 @@ importers:
.:
dependencies:
+ '@applemusic-like-lyrics/core':
+ specifier: ^0.5.1
+ version: 0.5.1(@pixi/app@7.4.3(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3)))(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3))(@pixi/filter-blur@7.4.3(@pixi/core@7.4.3))(@pixi/filter-bulge-pinch@5.1.1(@pixi/core@7.4.3))(@pixi/filter-color-matrix@7.4.3(@pixi/core@7.4.3))(@pixi/sprite@7.4.3(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3)))
+ '@applemusic-like-lyrics/lyric':
+ specifier: ^1.0.1
+ version: 1.0.1
'@electron-toolkit/preload':
specifier: ^3.0.2
version: 3.0.2(electron@41.6.1)
@@ -20,6 +26,27 @@ importers:
'@material/material-color-utilities':
specifier: ^0.4.0
version: 0.4.0
+ '@pixi/app':
+ specifier: ^7.4.3
+ version: 7.4.3(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3))
+ '@pixi/core':
+ specifier: ^7.4.3
+ version: 7.4.3
+ '@pixi/display':
+ specifier: ^7.4.3
+ version: 7.4.3(@pixi/core@7.4.3)
+ '@pixi/filter-blur':
+ specifier: ^7.4.3
+ version: 7.4.3(@pixi/core@7.4.3)
+ '@pixi/filter-bulge-pinch':
+ specifier: ^5.1.1
+ version: 5.1.1(@pixi/core@7.4.3)
+ '@pixi/filter-color-matrix':
+ specifier: ^7.4.3
+ version: 7.4.3(@pixi/core@7.4.3)
+ '@pixi/sprite':
+ specifier: ^7.4.3
+ version: 7.4.3(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3))
'@vueuse/core':
specifier: ^14.2.1
version: 14.2.1(vue@3.5.32(typescript@5.9.3))
@@ -174,6 +201,23 @@ packages:
'@antfu/install-pkg@1.1.0':
resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==}
+ '@applemusic-like-lyrics/core@0.5.1':
+ resolution: {integrity: sha512-DAEHmAe2USj/9qH4GKRFr/TuIKCOsNlcjgB2J2Lh4Be65RLU/pWpgnRhpIbV9ppinJzs+dWEsueWZiFTgLvMYQ==}
+ peerDependencies:
+ '@pixi/app': '*'
+ '@pixi/core': '*'
+ '@pixi/display': '*'
+ '@pixi/filter-blur': '*'
+ '@pixi/filter-bulge-pinch': '*'
+ '@pixi/filter-color-matrix': '*'
+ '@pixi/sprite': '*'
+
+ '@applemusic-like-lyrics/lyric@1.0.1':
+ resolution: {integrity: sha512-b4/9MUTdp9AuW59JTBpOxb8P+4SF8NjaoZYpLD/nkVHSM/4kfbnYpo9HldSMOjRLwlKVv8bTF6Vr9K3OLHL85Q==}
+
+ '@applemusic-like-lyrics/ttml@1.0.1':
+ resolution: {integrity: sha512-xhLajMI9Jm+Etv99GbK5Fx8wqa5m9wnS3kCqOTBUy9imjsah2PrV1XyCc6LBssPhEFcQt2SrFNQlwT6GZwpY7A==}
+
'@babel/code-frame@7.29.0':
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
engines: {node: '>=6.9.0'}
@@ -1564,6 +1608,68 @@ packages:
resolution: {integrity: sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==}
engines: {node: '>=14.18.0'}
+ '@pixi/app@7.4.3':
+ resolution: {integrity: sha512-opyWMuO0Ir8pf1DYUR++wAA6ZfNU+nIX2z95R2OD172HbcdhB4/HD7leLIIAny/LciEdMqlWEBhXK7N93YWbdg==}
+ peerDependencies:
+ '@pixi/core': 7.4.3
+ '@pixi/display': 7.4.3
+
+ '@pixi/color@7.4.3':
+ resolution: {integrity: sha512-a6R+bXKeXMDcRmjYQoBIK+v2EYqxSX49wcjAY579EYM/WrFKS98nSees6lqVUcLKrcQh2DT9srJHX7XMny3voQ==}
+
+ '@pixi/colord@2.9.6':
+ resolution: {integrity: sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==}
+
+ '@pixi/constants@7.4.3':
+ resolution: {integrity: sha512-QGmwJUNQy/vVEHzL6VGQvnwawLZ1wceZMI8HwJAT4/I2uAzbBeFDdmCS8WsTpSWLZjF/DszDc1D8BFp4pVJ5UQ==}
+
+ '@pixi/core@7.4.3':
+ resolution: {integrity: sha512-5YDs11faWgVVTL8VZtLU05/Fl47vaP5Tnsbf+y/WRR0VSW3KhRRGTBU1J3Gdc2xEWbJhUK07KGP7eSZpvtPVgA==}
+
+ '@pixi/display@7.4.3':
+ resolution: {integrity: sha512-b5m2dAaoNAVdxz1oDaxl3XZ059NEOcNtGkxTOZ4EYCw/jcp9sZXkgSROHRzsGn4k+NugH7+9MP4Id2Z0kkdUhw==}
+ peerDependencies:
+ '@pixi/core': 7.4.3
+
+ '@pixi/extensions@7.4.3':
+ resolution: {integrity: sha512-FhoiYkHQEDYHUE7wXhqfsTRz6KxLXjuMbSiAwnLb9uG1vAgp6q6qd6HEsf4X30YaZbLFY8a4KY6hFZWjF+4Fdw==}
+
+ '@pixi/filter-blur@7.4.3':
+ resolution: {integrity: sha512-ZFzS9L/whdRbs5A/EUgF3yQaBcxNarmbuwaMgrfnpQ84mRczkGByqDLGToadiufyals07ufTrXBGRle9lbtEDA==}
+ peerDependencies:
+ '@pixi/core': 7.4.3
+
+ '@pixi/filter-bulge-pinch@5.1.1':
+ resolution: {integrity: sha512-80I3g813td7Fnzi7IJSiR3z8gZlKblk6WN+5z6WnscQROcNEpck6lgWS/Lf/IdeHB/FtUKJCbx7RzxkUhiRTvA==}
+ peerDependencies:
+ '@pixi/core': ^7.0.0-X
+
+ '@pixi/filter-color-matrix@7.4.3':
+ resolution: {integrity: sha512-TNu0h20SrzjUWIb5v19dAp1vPpqtG0w2XF9kIHN91bMNaf3R1jzhpWG6TtaVO9eo1IolWcEJLw38jIohyC+KNw==}
+ peerDependencies:
+ '@pixi/core': 7.4.3
+
+ '@pixi/math@7.4.3':
+ resolution: {integrity: sha512-/uJOVhR2DOZ+zgdI6Bs/CwcXT4bNRKsS+TqX3ekRIxPCwaLra+Qdm7aDxT5cTToDzdxbKL5+rwiLu3Y1egILDw==}
+
+ '@pixi/runner@7.4.3':
+ resolution: {integrity: sha512-TJyfp7y23u5vvRAyYhVSa7ytq0PdKSvPLXu4G3meoFh1oxTLHH6g/RIzLuxUAThPG2z7ftthuW3qWq6dRV+dhw==}
+
+ '@pixi/settings@7.4.3':
+ resolution: {integrity: sha512-SmGK8smc0PxRB9nr0UJioEtE9hl4gvj9OedCvZx3bxBwA3omA5BmP3CyhQfN8XJ29+o2OUL01r3zAPVol4l4lA==}
+
+ '@pixi/sprite@7.4.3':
+ resolution: {integrity: sha512-iNBrpOFF9nXDT6m2jcyYy6l/sRzklLDDck1eFHprHZwvNquY2nzRfh+RGBCecxhBcijiLJ3fsZN33fP0LDXkvw==}
+ peerDependencies:
+ '@pixi/core': 7.4.3
+ '@pixi/display': 7.4.3
+
+ '@pixi/ticker@7.4.3':
+ resolution: {integrity: sha512-tHsAD0iOUb6QSGGw+c8cyRBvxsq/NlfzIFBZLEHhWZ+Bx4a0MmXup6I/yJDGmyPCYE+ctCcAfY13wKAzdiVFgQ==}
+
+ '@pixi/utils@7.4.3':
+ resolution: {integrity: sha512-NO3Y9HAn2UKS1YdxffqsPp+kDpVm8XWvkZcS/E+rBzY9VTLnNOI7cawSRm+dacdET3a8Jad3aDKEDZ0HmAqAFA==}
+
'@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
@@ -1763,9 +1869,15 @@ packages:
'@types/cacheable-request@6.0.3':
resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==}
+ '@types/css-font-loading-module@0.0.12':
+ resolution: {integrity: sha512-x2tZZYkSxXqWvTDgveSynfjq/T2HyiZHXb00j/+gy19yp70PHCizM48XFdjBCWH7eHBD0R5i/pw9yMBP/BH5uA==}
+
'@types/debug@4.1.13':
resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==}
+ '@types/earcut@2.1.4':
+ resolution: {integrity: sha512-qp3m9PPz4gULB9MhjGID7wpo3gJ4bTGXm7ltNDsmOvsPduTeHp8wSW9YckBj3mljeOh4F0m2z/0JKAALRKbmLQ==}
+
'@types/esrecurse@4.3.1':
resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==}
@@ -2198,6 +2310,9 @@ packages:
resolution: {integrity: sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==}
engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x}
+ bezier-easing@3.0.0:
+ resolution: {integrity: sha512-lE85voPXiK99T8NHOfhaUqCZpJdP1gBbbTEvdBDdPB+phyvPZPNWalBe42eb6lKOYchP0qZrtBiRCARtT4edRQ==}
+
bindings@1.5.0:
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
@@ -2277,6 +2392,10 @@ packages:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
+ call-bound@1.0.4:
+ resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
+ engines: {node: '>= 0.4'}
+
callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
@@ -2443,6 +2562,9 @@ packages:
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
engines: {node: '>=4.0.0'}
+ deep-freeze@0.0.1:
+ resolution: {integrity: sha512-Z+z8HiAvsGwmjqlphnHW5oz6yWlOwu6EQfFTjmeTWlDeda3FS2yv3jhq35TX/ewmsnqB+RX2IdsIOyjJCQN5tg==}
+
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
@@ -2509,6 +2631,9 @@ packages:
duplexer@0.1.2:
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
+ earcut@2.2.4:
+ resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==}
+
ejs@3.1.10:
resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==}
engines: {node: '>=0.10.0'}
@@ -2704,6 +2829,9 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
+ eventemitter3@4.0.7:
+ resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
+
expand-template@2.0.3:
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
engines: {node: '>=6'}
@@ -2850,6 +2978,9 @@ packages:
github-from-package@0.0.0:
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
+ gl-matrix@4.0.0-beta.2:
+ resolution: {integrity: sha512-OF6IkQpMkF8p2CZF9EtzYZPlPaW3M41KMsgZGlTKmMv/nWaP6GMJi9V5tI+oPn8FG0io85Q5ZtKpCXP4u6YmDA==}
+
glob-parent@6.0.2:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
@@ -3024,6 +3155,9 @@ packages:
resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==}
engines: {node: '>=20'}
+ ismobilejs@1.1.1:
+ resolution: {integrity: sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==}
+
jake@10.9.4:
resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==}
engines: {node: '>=10'}
@@ -3308,6 +3442,10 @@ packages:
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
+ object-inspect@1.13.4:
+ resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
+ engines: {node: '>= 0.4'}
+
object-keys@1.1.1:
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
engines: {node: '>= 0.4'}
@@ -3373,6 +3511,9 @@ packages:
package-manager-detector@1.6.0:
resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
+ pako@2.1.0:
+ resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
+
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
@@ -3512,6 +3653,9 @@ packages:
pump@3.0.4:
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
+ punycode@1.4.1:
+ resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==}
+
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@@ -3528,6 +3672,10 @@ packages:
engines: {node: '>=10.13.0'}
hasBin: true
+ qs@6.15.2:
+ resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==}
+ engines: {node: '>=0.6'}
+
quansync@0.2.11:
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
@@ -3807,6 +3955,22 @@ packages:
shiki@3.23.0:
resolution: {integrity: sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==}
+ side-channel-list@1.0.1:
+ resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-map@1.0.1:
+ resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-weakmap@1.0.2:
+ resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
+ engines: {node: '>= 0.4'}
+
+ side-channel@1.1.1:
+ resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==}
+ engines: {node: '>= 0.4'}
+
signal-exit@3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
@@ -4139,6 +4303,10 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+ url@0.11.4:
+ resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==}
+ engines: {node: '>= 0.4'}
+
utf8-byte-length@1.0.5:
resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==}
@@ -4381,6 +4549,27 @@ snapshots:
package-manager-detector: 1.6.0
tinyexec: 1.1.1
+ '@applemusic-like-lyrics/core@0.5.1(@pixi/app@7.4.3(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3)))(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3))(@pixi/filter-blur@7.4.3(@pixi/core@7.4.3))(@pixi/filter-bulge-pinch@5.1.1(@pixi/core@7.4.3))(@pixi/filter-color-matrix@7.4.3(@pixi/core@7.4.3))(@pixi/sprite@7.4.3(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3)))':
+ dependencies:
+ '@pixi/app': 7.4.3(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3))
+ '@pixi/core': 7.4.3
+ '@pixi/display': 7.4.3(@pixi/core@7.4.3)
+ '@pixi/filter-blur': 7.4.3(@pixi/core@7.4.3)
+ '@pixi/filter-bulge-pinch': 5.1.1(@pixi/core@7.4.3)
+ '@pixi/filter-color-matrix': 7.4.3(@pixi/core@7.4.3)
+ '@pixi/sprite': 7.4.3(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3))
+ '@ungap/structured-clone': 1.3.1
+ bezier-easing: 3.0.0
+ deep-freeze: 0.0.1
+ gl-matrix: 4.0.0-beta.2
+
+ '@applemusic-like-lyrics/lyric@1.0.1':
+ dependencies:
+ '@applemusic-like-lyrics/ttml': 1.0.1
+ pako: 2.1.0
+
+ '@applemusic-like-lyrics/ttml@1.0.1': {}
+
'@babel/code-frame@7.29.0':
dependencies:
'@babel/helper-validator-identifier': 7.28.5
@@ -5550,6 +5739,79 @@ snapshots:
tslib: 2.8.1
webcrypto-core: 1.9.2
+ '@pixi/app@7.4.3(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3))':
+ dependencies:
+ '@pixi/core': 7.4.3
+ '@pixi/display': 7.4.3(@pixi/core@7.4.3)
+
+ '@pixi/color@7.4.3':
+ dependencies:
+ '@pixi/colord': 2.9.6
+
+ '@pixi/colord@2.9.6': {}
+
+ '@pixi/constants@7.4.3': {}
+
+ '@pixi/core@7.4.3':
+ dependencies:
+ '@pixi/color': 7.4.3
+ '@pixi/constants': 7.4.3
+ '@pixi/extensions': 7.4.3
+ '@pixi/math': 7.4.3
+ '@pixi/runner': 7.4.3
+ '@pixi/settings': 7.4.3
+ '@pixi/ticker': 7.4.3
+ '@pixi/utils': 7.4.3
+
+ '@pixi/display@7.4.3(@pixi/core@7.4.3)':
+ dependencies:
+ '@pixi/core': 7.4.3
+
+ '@pixi/extensions@7.4.3': {}
+
+ '@pixi/filter-blur@7.4.3(@pixi/core@7.4.3)':
+ dependencies:
+ '@pixi/core': 7.4.3
+
+ '@pixi/filter-bulge-pinch@5.1.1(@pixi/core@7.4.3)':
+ dependencies:
+ '@pixi/core': 7.4.3
+
+ '@pixi/filter-color-matrix@7.4.3(@pixi/core@7.4.3)':
+ dependencies:
+ '@pixi/core': 7.4.3
+
+ '@pixi/math@7.4.3': {}
+
+ '@pixi/runner@7.4.3': {}
+
+ '@pixi/settings@7.4.3':
+ dependencies:
+ '@pixi/constants': 7.4.3
+ '@types/css-font-loading-module': 0.0.12
+ ismobilejs: 1.1.1
+
+ '@pixi/sprite@7.4.3(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3))':
+ dependencies:
+ '@pixi/core': 7.4.3
+ '@pixi/display': 7.4.3(@pixi/core@7.4.3)
+
+ '@pixi/ticker@7.4.3':
+ dependencies:
+ '@pixi/extensions': 7.4.3
+ '@pixi/settings': 7.4.3
+ '@pixi/utils': 7.4.3
+
+ '@pixi/utils@7.4.3':
+ dependencies:
+ '@pixi/color': 7.4.3
+ '@pixi/constants': 7.4.3
+ '@pixi/settings': 7.4.3
+ '@types/earcut': 2.1.4
+ earcut: 2.2.4
+ eventemitter3: 4.0.7
+ url: 0.11.4
+
'@polka/url@1.0.0-next.29': {}
'@quansync/fs@1.0.0':
@@ -5704,10 +5966,14 @@ snapshots:
'@types/node': 22.19.17
'@types/responselike': 1.0.3
+ '@types/css-font-loading-module@0.0.12': {}
+
'@types/debug@4.1.13':
dependencies:
'@types/ms': 2.1.0
+ '@types/earcut@2.1.4': {}
+
'@types/esrecurse@4.3.1': {}
'@types/estree@1.0.8': {}
@@ -6277,6 +6543,8 @@ snapshots:
bindings: 1.5.0
prebuild-install: 7.1.3
+ bezier-easing@3.0.0: {}
+
bindings@1.5.0:
dependencies:
file-uri-to-path: 1.0.0
@@ -6382,6 +6650,11 @@ snapshots:
es-errors: 1.3.0
function-bind: 1.1.2
+ call-bound@1.0.4:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ get-intrinsic: 1.3.0
+
callsites@3.1.0: {}
camelcase@5.3.1:
@@ -6512,6 +6785,8 @@ snapshots:
deep-extend@0.6.0: {}
+ deep-freeze@0.0.1: {}
+
deep-is@0.1.4: {}
defer-to-connect@2.0.1: {}
@@ -6583,6 +6858,8 @@ snapshots:
duplexer@0.1.2: {}
+ earcut@2.2.4: {}
+
ejs@3.1.10:
dependencies:
jake: 10.9.4
@@ -6881,6 +7158,8 @@ snapshots:
esutils@2.0.3: {}
+ eventemitter3@4.0.7: {}
+
expand-template@2.0.3: {}
exponential-backoff@3.1.3: {}
@@ -7039,6 +7318,8 @@ snapshots:
github-from-package@0.0.0: {}
+ gl-matrix@4.0.0-beta.2: {}
+
glob-parent@6.0.2:
dependencies:
is-glob: 4.0.3
@@ -7219,6 +7500,8 @@ snapshots:
isexe@4.0.0: {}
+ ismobilejs@1.1.1: {}
+
jake@10.9.4:
dependencies:
async: 3.2.6
@@ -7487,6 +7770,8 @@ snapshots:
dependencies:
boolbase: 1.0.0
+ object-inspect@1.13.4: {}
+
object-keys@1.1.1:
optional: true
@@ -7581,6 +7866,8 @@ snapshots:
package-manager-detector@1.6.0: {}
+ pako@2.1.0: {}
+
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
@@ -7714,6 +8001,8 @@ snapshots:
end-of-stream: 1.4.5
once: 1.4.0
+ punycode@1.4.1: {}
+
punycode@2.3.1: {}
pvtsutils@1.3.6:
@@ -7729,6 +8018,10 @@ snapshots:
yargs: 15.4.1
optional: true
+ qs@6.15.2:
+ dependencies:
+ side-channel: 1.1.1
+
quansync@0.2.11: {}
quansync@1.0.0: {}
@@ -8019,6 +8312,34 @@ snapshots:
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
+ side-channel-list@1.0.1:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-map@1.0.1:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-weakmap@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-map: 1.0.1
+
+ side-channel@1.1.1:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-list: 1.0.1
+ side-channel-map: 1.0.1
+ side-channel-weakmap: 1.0.2
+
signal-exit@3.0.7: {}
signal-exit@4.1.0: {}
@@ -8391,6 +8712,11 @@ snapshots:
dependencies:
punycode: 2.3.1
+ url@0.11.4:
+ dependencies:
+ punycode: 1.4.1
+ qs: 6.15.2
+
utf8-byte-length@1.0.5: {}
util-deprecate@1.0.2: {}
diff --git a/src/components/player/FullPlayer/BackgroundRender.vue b/src/components/player/FullPlayer/BackgroundRender.vue
new file mode 100644
index 0000000..c56d00e
--- /dev/null
+++ b/src/components/player/FullPlayer/BackgroundRender.vue
@@ -0,0 +1,229 @@
+
+
+
+
+
+
+
diff --git a/src/components/player/FullPlayer/PlayerBackground.vue b/src/components/player/FullPlayer/PlayerBackground.vue
index 5e1f832..9d15d89 100644
--- a/src/components/player/FullPlayer/PlayerBackground.vue
+++ b/src/components/player/FullPlayer/PlayerBackground.vue
@@ -4,13 +4,14 @@ import { useThemeStore } from "@/stores/theme";
import { useMediaStore } from "@/stores/media";
import { useStatusStore } from "@/stores/status";
import DEFAULT_COVER from "@/assets/images/song.jpg";
+import BackgroundRender from "./BackgroundRender.vue";
const media = useMediaStore();
const settings = useSettingsStore();
const theme = useThemeStore();
const status = useStatusStore();
-const bgType = computed(() => settings.player.playerBgType);
+const bgType = computed(() => settings.player.playerBgType as string);
// 封面颜色(纯色模式)
const coverColor = computed(() => {
@@ -95,6 +96,17 @@ onBeforeUnmount(() => {
/>
+
+
+
+
+
@@ -110,11 +122,13 @@ onBeforeUnmount(() => {
diff --git a/src/components/player/Lyrics/renderer.css b/src/components/player/Lyrics/renderer.css
index 81538fd..f3219c7 100644
--- a/src/components/player/Lyrics/renderer.css
+++ b/src/components/player/Lyrics/renderer.css
@@ -120,6 +120,22 @@
.lp-credit {
opacity: var(--lp-credit-opacity, 0.3);
+ pointer-events: auto !important; /* 启用交互并阻止点击穿透到下方的普通歌词行上 */
+ z-index: 10 !important; /* 提升层级,防止被普通歌词行(尤其是最后一行)遮挡和阻断点击 */
+ background: none !important;
+ background-color: transparent !important;
+ box-shadow: none !important;
+ border: none !important;
+ outline: none !important;
+}
+
+.lp-credit:hover,
+.lp-credit:active {
+ background: none !important;
+ background-color: transparent !important;
+ box-shadow: none !important;
+ border: none !important;
+ outline: none !important;
}
.lp-credit:empty {
diff --git a/src/components/settings/SettingsItem.vue b/src/components/settings/SettingsItem.vue
index 09b80c2..0dfeed1 100644
--- a/src/components/settings/SettingsItem.vue
+++ b/src/components/settings/SettingsItem.vue
@@ -19,6 +19,7 @@ const selectOptions = computed(() =>
);
const isChildrenActive = computed(() => {
+ if (props.item.type === "title") return true;
if (props.item.childrenCondition) return props.item.childrenCondition();
return model.value === true;
});
@@ -39,6 +40,14 @@ const descriptionText = computed(() =>
class="transition-all duration-300"
:class="highlighted ? 'animate-highlight-pulse' : ''"
/>
+
+
+
+ {{ t(`settings.${item.key}.label`) }}
+
diff --git a/src/components/settings/SettingsSearch.vue b/src/components/settings/SettingsSearch.vue
index c74ee00..d98a3e9 100644
--- a/src/components/settings/SettingsSearch.vue
+++ b/src/components/settings/SettingsSearch.vue
@@ -29,6 +29,7 @@ const results = computed
(() => {
for (const cat of settingsSchema) {
for (const sec of cat.sections ?? []) {
for (const item of sec.items) {
+ if (item.visible && !item.visible()) continue;
const label = t(`settings.${item.key}.label`);
const desc = item.hideDescription ? "" : t(`settings.${item.key}.description`);
const kw = item.keywords?.map((k) => t(k)).join(" ") ?? "";
diff --git a/src/components/settings/SettingsSection.vue b/src/components/settings/SettingsSection.vue
index 916ff4c..0b10711 100644
--- a/src/components/settings/SettingsSection.vue
+++ b/src/components/settings/SettingsSection.vue
@@ -13,7 +13,9 @@ const props = withDefaults(
const { t } = useI18n();
-const visibleItems = computed(() => props.section.items);
+const visibleItems = computed(() =>
+ props.section.items.filter((item) => !item.visible || item.visible()),
+);
const itemStyle = (i: number) => {
const d = props.highlightKey ? "0s" : `${Math.min(props.startIndex + i, 15) * 0.03}s`;
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json
index fd5ecb2..757d96a 100644
--- a/src/i18n/locales/en-US.json
+++ b/src/i18n/locales/en-US.json
@@ -619,6 +619,7 @@
"general": "General",
"player": "Player",
"lyric": "Lyrics",
+ "fullScreenLyric": "Full-screen Lyrics",
"externalLyric": "External Lyrics",
"appearance": "Appearance",
"hotkeys": "Hotkey Settings",
@@ -639,6 +640,7 @@
"language": "Language",
"playback": "Player",
"playControl": "Playback Control",
+ "lyricEngine": "Lyric Engine",
"lyricContent": "Lyric Content",
"lyricTTML": "TTML Lyrics",
"lyricExclude": "Metadata Stripping",
@@ -646,6 +648,7 @@
"lyricDisplay": "Display Effects",
"lyricSpring": "Spring Animation",
"lyricLayout": "Layout & Opacity",
+ "amllLyricSpring": "Physics Spring & Scale",
"desktopLyric": "Desktop Lyric",
"dynamicIsland": "Dynamic Island Lyric",
"taskbarLyric": "Taskbar Lyric",
@@ -837,6 +840,56 @@
"openSearch": "Open search"
}
},
+ "engine": {
+ "label": "Lyric Engine",
+ "description": "Choose the engine type used for lyric rendering and scrolling"
+ },
+ "lyricEngine": {
+ "physics": "Default",
+ "amll": "AMLL"
+ },
+ "useAMSpring": {
+ "label": "Spring Animation Debug Switch",
+ "description": "Enable spring bounce and active line scaling effects for applemusic-like-lyrics"
+ },
+ "amllVerticalSpringHeader": {
+ "label": "Vertical Translation Spring Parameters"
+ },
+ "amllScaleSpringHeader": {
+ "label": "Scale Spring Parameters"
+ },
+ "amllVerticalSpringMass": {
+ "label": "Vertical Spring Mass",
+ "description": "Mass of the vertical scrolling physics spring. Larger mass increases spring inertia."
+ },
+ "amllVerticalSpringDamping": {
+ "label": "Vertical Spring Damping",
+ "description": "Damping of the vertical scrolling physics spring. Larger damping reduces oscillation and settles faster."
+ },
+ "amllVerticalSpringStiffness": {
+ "label": "Vertical Spring Stiffness",
+ "description": "Stiffness of the vertical scrolling physics spring. Larger stiffness pulls harder and bounces quicker."
+ },
+ "amllVerticalSpringSoft": {
+ "label": "Vertical Soft Spring",
+ "description": "Forces the vertical scrolling to use a softer spring profile."
+ },
+ "amllScaleSpringMass": {
+ "label": "Scale Spring Mass",
+ "description": "Mass of the active line scale animation. Larger mass increases scale bounce inertia."
+ },
+ "amllScaleSpringDamping": {
+ "label": "Scale Spring Damping",
+ "description": "Damping of the active line scale animation. Larger damping reduces scale jitter and settles faster."
+ },
+ "amllScaleSpringStiffness": {
+ "label": "Scale Spring Stiffness",
+ "description": "Stiffness of the active line scale animation. Larger stiffness responds and scales quicker."
+ },
+ "amllScaleSpringSoft": {
+ "label": "Scale Soft Spring",
+ "description": "Forces the active line scale animation to use a softer spring profile."
+ },
"autoCenterCover": {
"label": "Auto Center Cover",
"description": "Center cover and hide lyric area when no lyrics available"
@@ -869,6 +922,14 @@
"label": "Show Romanization",
"description": "Display romanized lyrics"
},
+ "amllShowLineRomanization": {
+ "label": "Show Line Romanization",
+ "description": "Display romanized lyrics for each line"
+ },
+ "amllShowWordRomanization": {
+ "label": "Show Word Romanization",
+ "description": "Display romanized lyrics for each word in word-by-word lyrics"
+ },
"enableWordHighlight": {
"label": "Word Highlight",
"description": "Show word-by-word lyric highlight progress"
diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json
index 507addd..0780b7b 100644
--- a/src/i18n/locales/zh-CN.json
+++ b/src/i18n/locales/zh-CN.json
@@ -607,6 +607,7 @@
"general": "常规设置",
"player": "播放设置",
"lyric": "歌词设置",
+ "fullScreenLyric": "全屏歌词",
"externalLyric": "外部歌词",
"appearance": "外观设置",
"hotkeys": "快捷键配置",
@@ -627,6 +628,7 @@
"language": "语言",
"playback": "播放器",
"playControl": "播放控制",
+ "lyricEngine": "歌词引擎",
"lyricContent": "歌词内容",
"lyricTTML": "TTML 歌词",
"lyricExclude": "歌词排除",
@@ -634,6 +636,7 @@
"lyricDisplay": "显示效果",
"lyricSpring": "弹簧动画",
"lyricLayout": "布局与透明度",
+ "amllLyricSpring": "物理回弹与缩放",
"desktopLyric": "桌面歌词",
"dynamicIsland": "灵动岛歌词",
"taskbarLyric": "任务栏歌词",
@@ -825,6 +828,56 @@
"openSearch": "打开搜索"
}
},
+ "engine": {
+ "label": "歌词渲染引擎",
+ "description": "选择用于歌词渲染与滚动的动效引擎"
+ },
+ "lyricEngine": {
+ "physics": "默认",
+ "amll": "AMLL"
+ },
+ "useAMSpring": {
+ "label": "弹簧动画调试开关",
+ "description": "为 applemusic-like-lyrics 启用物理弹性回弹与焦点行缩放效果"
+ },
+ "amllVerticalSpringHeader": {
+ "label": "垂直位移弹簧参数"
+ },
+ "amllScaleSpringHeader": {
+ "label": "缩放弹簧参数"
+ },
+ "amllVerticalSpringMass": {
+ "label": "垂直回弹质量",
+ "description": "垂直弹性滚动的惯性质量。质量越大,回弹惯性越大"
+ },
+ "amllVerticalSpringDamping": {
+ "label": "垂直回弹阻尼",
+ "description": "垂直弹性滚动的阻尼阻力。阻力越大,回弹振幅越小且越快静止"
+ },
+ "amllVerticalSpringStiffness": {
+ "label": "垂直回弹刚度",
+ "description": "垂直弹性滚动的弹簧刚度。刚度越大,拉力越强,回弹速度越快"
+ },
+ "amllVerticalSpringSoft": {
+ "label": "垂直强制软弹簧",
+ "description": "启用后将强制使用较软的垂直滚动弹簧动效"
+ },
+ "amllScaleSpringMass": {
+ "label": "缩放回弹质量",
+ "description": "焦点行缩放动画的质量。值越大,缩放回弹的惯性越大"
+ },
+ "amllScaleSpringDamping": {
+ "label": "缩放回弹阻尼",
+ "description": "焦点行缩放动画的阻尼阻力。值越大,缩放震荡越少且越快静止"
+ },
+ "amllScaleSpringStiffness": {
+ "label": "缩放回弹刚度",
+ "description": "焦点行缩放动画的弹簧刚度。值越大,缩放回弹越迅速"
+ },
+ "amllScaleSpringSoft": {
+ "label": "缩放强制软弹簧",
+ "description": "启用后将强制使用较软的焦点行缩放弹簧动效"
+ },
"autoCenterCover": {
"label": "自动居中封面",
"description": "无歌词时自动居中封面并隐藏歌词区域"
@@ -857,6 +910,14 @@
"label": "显示音译",
"description": "显示歌词的音译文本"
},
+ "amllShowLineRomanization": {
+ "label": "显示逐行音译",
+ "description": "显示歌词行的音译文本"
+ },
+ "amllShowWordRomanization": {
+ "label": "显示逐词音译",
+ "description": "在逐字歌词中,显示每个字词的音译文本"
+ },
"enableWordHighlight": {
"label": "逐字高亮",
"description": "逐字显示歌词高亮进度"
diff --git a/src/settings/categories/fullScreenLyric.ts b/src/settings/categories/fullScreenLyric.ts
new file mode 100644
index 0000000..6eb1e09
--- /dev/null
+++ b/src/settings/categories/fullScreenLyric.ts
@@ -0,0 +1,316 @@
+import type { SettingCategory } from "@/types/settings-schema";
+import { useSettingsStore } from "@/stores/settings";
+import IconLucideTv from "~icons/lucide/tv";
+
+const fullScreenLyricCategory: SettingCategory = {
+ id: "fullScreenLyric",
+ icon: IconLucideTv,
+ sections: [
+ {
+ id: "lyricEngine",
+ items: [
+ {
+ key: "engine",
+ type: "select",
+ binding: { store: "settings", path: "lyric.engine" },
+ options: [
+ { value: "physics", labelKey: "settings.lyricEngine.physics" },
+ { value: "amll", labelKey: "settings.lyricEngine.amll" },
+ ],
+ defaultValue: "physics",
+ },
+ ],
+ },
+ {
+ id: "lyricGeneral",
+ items: [
+ {
+ key: "adaptiveFontSize",
+ type: "switch",
+ binding: { store: "settings", path: "lyric.adaptiveFontSize" },
+ defaultValue: true,
+ },
+ {
+ key: "fontSize",
+ type: "slider",
+ binding: { store: "settings", path: "lyric.fontSize" },
+ min: 30,
+ max: 64,
+ step: 1,
+ defaultValue: 48,
+ marks: { 30: "30", 48: "48", 64: "64" },
+ },
+ {
+ key: "fontWeight",
+ type: "slider",
+ binding: { store: "settings", path: "lyric.fontWeight" },
+ min: 100,
+ max: 900,
+ step: 100,
+ defaultValue: 700,
+ marks: { 100: "100", 400: "400", 700: "700", 900: "900" },
+ },
+ {
+ key: "showTranslation",
+ type: "switch",
+ binding: { store: "settings", path: "lyric.showTranslation" },
+ defaultValue: true,
+ },
+ {
+ key: "showRomanization",
+ type: "switch",
+ binding: { store: "settings", path: "lyric.showRomanization" },
+ defaultValue: true,
+ visible: () => useSettingsStore().lyric.engine === "physics",
+ },
+ {
+ key: "amllShowLineRomanization",
+ type: "switch",
+ binding: { store: "settings", path: "lyric.amllShowLineRomanization" },
+ defaultValue: true,
+ visible: () => useSettingsStore().lyric.engine === "amll",
+ },
+ {
+ key: "amllShowWordRomanization",
+ type: "switch",
+ binding: { store: "settings", path: "lyric.amllShowWordRomanization" },
+ defaultValue: true,
+ visible: () => useSettingsStore().lyric.engine === "amll",
+ },
+ ],
+ },
+ {
+ id: "lyricDisplay",
+ items: [
+ {
+ key: "enableWordHighlight",
+ type: "switch",
+ binding: { store: "settings", path: "lyric.enableWordHighlight" },
+ defaultValue: true,
+ visible: () => useSettingsStore().lyric.engine === "physics",
+ },
+ {
+ key: "enableFloatAnimation",
+ type: "switch",
+ binding: { store: "settings", path: "lyric.enableFloatAnimation" },
+ defaultValue: false,
+ visible: () => useSettingsStore().lyric.engine === "physics",
+ },
+ {
+ key: "enableEmphasizeEffect",
+ type: "switch",
+ binding: { store: "settings", path: "lyric.enableEmphasizeEffect" },
+ defaultValue: false,
+ visible: () => useSettingsStore().lyric.engine === "physics",
+ },
+ {
+ key: "enableBlur",
+ type: "switch",
+ binding: { store: "settings", path: "lyric.enableBlur" },
+ defaultValue: false,
+ },
+ {
+ key: "hidePassedLines",
+ type: "switch",
+ binding: { store: "settings", path: "lyric.hidePassedLines" },
+ defaultValue: false,
+ },
+ ],
+ },
+ {
+ id: "lyricSpring",
+ items: [
+ {
+ key: "springPreset",
+ type: "select",
+ binding: { store: "settings", path: "lyric.springPreset" },
+ options: [
+ { value: "default", labelKey: "settings.springPreset.default" },
+ { value: "smooth", labelKey: "settings.springPreset.smooth" },
+ { value: "responsive", labelKey: "settings.springPreset.responsive" },
+ { value: "jello", labelKey: "settings.springPreset.jello" },
+ { value: "heavy", labelKey: "settings.springPreset.heavy" },
+ { value: "noBounce", labelKey: "settings.springPreset.noBounce" },
+ { value: "custom", labelKey: "settings.springPreset.custom" },
+ ],
+ defaultValue: "default",
+ visible: () => useSettingsStore().lyric.engine === "physics",
+ indentChildren: false,
+ childrenCondition: () =>
+ useSettingsStore().lyric.engine !== "amll" &&
+ useSettingsStore().lyric.springPreset === "custom",
+ children: [
+ {
+ key: "springMass",
+ type: "slider",
+ binding: { store: "settings", path: "lyric.springMass" },
+ min: 0.1,
+ max: 5,
+ step: 0.1,
+ defaultValue: 0.9,
+ marks: { 0.1: "0.1", 0.9: "0.9", 5: "5" },
+ },
+ {
+ key: "springDamping",
+ type: "slider",
+ binding: { store: "settings", path: "lyric.springDamping" },
+ min: 1,
+ max: 50,
+ step: 0.5,
+ defaultValue: 15,
+ marks: { 1: "1", 15: "15", 50: "50" },
+ },
+ {
+ key: "springStiffness",
+ type: "slider",
+ binding: { store: "settings", path: "lyric.springStiffness" },
+ min: 10,
+ max: 300,
+ step: 5,
+ defaultValue: 90,
+ marks: { 10: "10", 90: "90", 300: "300" },
+ },
+ ],
+ },
+ ],
+ },
+ {
+ id: "lyricLayout",
+ items: [
+ {
+ key: "alignPosition",
+ type: "slider",
+ binding: { store: "settings", path: "lyric.alignPosition" },
+ min: 0.1,
+ max: 0.9,
+ step: 0.05,
+ defaultValue: 0.35,
+ marks: { 0.1: "0.1", 0.35: "0.35", 0.9: "0.9" },
+ },
+ {
+ key: "wordFadeWidth",
+ type: "slider",
+ binding: { store: "settings", path: "lyric.wordFadeWidth" },
+ min: 0.1,
+ max: 1,
+ step: 0.1,
+ defaultValue: 0.5,
+ marks: { 0.1: "0.1", 0.5: "0.5", 1: "1" },
+ },
+ {
+ key: "inactiveAlpha",
+ type: "slider",
+ binding: { store: "settings", path: "lyric.inactiveAlpha" },
+ min: 0,
+ max: 1,
+ step: 0.05,
+ defaultValue: 0.2,
+ marks: { 0: "0", 0.2: "0.2", 1: "1" },
+ visible: () => useSettingsStore().lyric.engine === "physics",
+ },
+ ],
+ },
+ {
+ id: "amllLyricSpring",
+ items: [
+ {
+ key: "useAMSpring",
+ type: "switch",
+ binding: { store: "settings", path: "lyric.useAMSpring" },
+ defaultValue: true,
+ visible: () => useSettingsStore().lyric.engine === "amll",
+ hideChildren: true,
+ childrenCondition: () => useSettingsStore().lyric.useAMSpring,
+ children: [
+ {
+ key: "amllVerticalSpringHeader",
+ type: "title",
+ children: [
+ {
+ key: "amllVerticalSpringMass",
+ type: "slider",
+ binding: { store: "settings", path: "lyric.amllVerticalSpringMass" },
+ min: 0.1,
+ max: 5,
+ step: 0.1,
+ defaultValue: 1,
+ marks: { 0.1: "0.1", 1: "1", 5: "5" },
+ },
+ {
+ key: "amllVerticalSpringDamping",
+ type: "slider",
+ binding: { store: "settings", path: "lyric.amllVerticalSpringDamping" },
+ min: 0,
+ max: 40,
+ step: 0.5,
+ defaultValue: 15,
+ marks: { 0: "0", 15: "15", 40: "40" },
+ },
+ {
+ key: "amllVerticalSpringStiffness",
+ type: "slider",
+ binding: { store: "settings", path: "lyric.amllVerticalSpringStiffness" },
+ min: 1,
+ max: 300,
+ step: 1,
+ defaultValue: 100,
+ marks: { 1: "1", 100: "100", 300: "300" },
+ },
+ {
+ key: "amllVerticalSpringSoft",
+ type: "switch",
+ binding: { store: "settings", path: "lyric.amllVerticalSpringSoft" },
+ defaultValue: false,
+ },
+ ],
+ },
+ {
+ key: "amllScaleSpringHeader",
+ type: "title",
+ children: [
+ {
+ key: "amllScaleSpringMass",
+ type: "slider",
+ binding: { store: "settings", path: "lyric.amllScaleSpringMass" },
+ min: 0.1,
+ max: 5,
+ step: 0.1,
+ defaultValue: 1,
+ marks: { 0.1: "0.1", 1: "1", 5: "5" },
+ },
+ {
+ key: "amllScaleSpringDamping",
+ type: "slider",
+ binding: { store: "settings", path: "lyric.amllScaleSpringDamping" },
+ min: 0,
+ max: 40,
+ step: 0.5,
+ defaultValue: 20,
+ marks: { 0: "0", 20: "20", 40: "40" },
+ },
+ {
+ key: "amllScaleSpringStiffness",
+ type: "slider",
+ binding: { store: "settings", path: "lyric.amllScaleSpringStiffness" },
+ min: 1,
+ max: 300,
+ step: 1,
+ defaultValue: 100,
+ marks: { 1: "1", 100: "100", 300: "300" },
+ },
+ {
+ key: "amllScaleSpringSoft",
+ type: "switch",
+ binding: { store: "settings", path: "lyric.amllScaleSpringSoft" },
+ defaultValue: false,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+};
+
+export default fullScreenLyricCategory;
diff --git a/src/settings/categories/lyric.ts b/src/settings/categories/lyric.ts
index 1148095..34dbaac 100644
--- a/src/settings/categories/lyric.ts
+++ b/src/settings/categories/lyric.ts
@@ -106,172 +106,6 @@ const lyricCategory: SettingCategory = {
},
],
},
- {
- id: "lyricGeneral",
- items: [
- {
- key: "adaptiveFontSize",
- type: "switch",
- binding: { store: "settings", path: "lyric.adaptiveFontSize" },
- defaultValue: true,
- },
- {
- key: "fontSize",
- type: "slider",
- binding: { store: "settings", path: "lyric.fontSize" },
- min: 30,
- max: 64,
- step: 1,
- defaultValue: 48,
- marks: { 30: "30", 48: "48", 64: "64" },
- },
- {
- key: "fontWeight",
- type: "slider",
- binding: { store: "settings", path: "lyric.fontWeight" },
- min: 100,
- max: 900,
- step: 100,
- defaultValue: 700,
- marks: { 100: "100", 400: "400", 700: "700", 900: "900" },
- },
- {
- key: "showTranslation",
- type: "switch",
- binding: { store: "settings", path: "lyric.showTranslation" },
- defaultValue: true,
- },
- {
- key: "showRomanization",
- type: "switch",
- binding: { store: "settings", path: "lyric.showRomanization" },
- defaultValue: true,
- },
- ],
- },
- {
- id: "lyricDisplay",
- items: [
- {
- key: "enableWordHighlight",
- type: "switch",
- binding: { store: "settings", path: "lyric.enableWordHighlight" },
- defaultValue: true,
- },
- {
- key: "enableFloatAnimation",
- type: "switch",
- binding: { store: "settings", path: "lyric.enableFloatAnimation" },
- defaultValue: false,
- },
- {
- key: "enableEmphasizeEffect",
- type: "switch",
- binding: { store: "settings", path: "lyric.enableEmphasizeEffect" },
- defaultValue: false,
- },
- {
- key: "enableBlur",
- type: "switch",
- binding: { store: "settings", path: "lyric.enableBlur" },
- defaultValue: false,
- },
- {
- key: "hidePassedLines",
- type: "switch",
- binding: { store: "settings", path: "lyric.hidePassedLines" },
- defaultValue: false,
- },
- ],
- },
- {
- id: "lyricSpring",
- items: [
- {
- key: "springPreset",
- type: "select",
- binding: { store: "settings", path: "lyric.springPreset" },
- options: [
- { value: "default", labelKey: "settings.springPreset.default" },
- { value: "smooth", labelKey: "settings.springPreset.smooth" },
- { value: "responsive", labelKey: "settings.springPreset.responsive" },
- { value: "jello", labelKey: "settings.springPreset.jello" },
- { value: "heavy", labelKey: "settings.springPreset.heavy" },
- { value: "noBounce", labelKey: "settings.springPreset.noBounce" },
- { value: "custom", labelKey: "settings.springPreset.custom" },
- ],
- defaultValue: "default",
- childrenCondition: () => useSettingsStore().lyric.springPreset === "custom",
- children: [
- {
- key: "springMass",
- type: "slider",
- binding: { store: "settings", path: "lyric.springMass" },
- min: 0.1,
- max: 5,
- step: 0.1,
- defaultValue: 0.9,
- marks: { 0.1: "0.1", 0.9: "0.9", 5: "5" },
- },
- {
- key: "springDamping",
- type: "slider",
- binding: { store: "settings", path: "lyric.springDamping" },
- min: 1,
- max: 50,
- step: 0.5,
- defaultValue: 15,
- marks: { 1: "1", 15: "15", 50: "50" },
- },
- {
- key: "springStiffness",
- type: "slider",
- binding: { store: "settings", path: "lyric.springStiffness" },
- min: 10,
- max: 300,
- step: 5,
- defaultValue: 90,
- marks: { 10: "10", 90: "90", 300: "300" },
- },
- ],
- },
- ],
- },
- {
- id: "lyricLayout",
- items: [
- {
- key: "alignPosition",
- type: "slider",
- binding: { store: "settings", path: "lyric.alignPosition" },
- min: 0.1,
- max: 0.9,
- step: 0.05,
- defaultValue: 0.35,
- marks: { 0.1: "0.1", 0.35: "0.35", 0.9: "0.9" },
- },
- {
- key: "wordFadeWidth",
- type: "slider",
- binding: { store: "settings", path: "lyric.wordFadeWidth" },
- min: 0.1,
- max: 1,
- step: 0.1,
- defaultValue: 0.5,
- marks: { 0.1: "0.1", 0.5: "0.5", 1: "1" },
- },
- {
- key: "inactiveAlpha",
- type: "slider",
- binding: { store: "settings", path: "lyric.inactiveAlpha" },
- min: 0,
- max: 1,
- step: 0.05,
- defaultValue: 0.2,
- marks: { 0: "0", 0.2: "0.2", 1: "1" },
- },
- ],
- },
],
};
diff --git a/src/settings/schema.ts b/src/settings/schema.ts
index 5ba42e4..b4a416c 100644
--- a/src/settings/schema.ts
+++ b/src/settings/schema.ts
@@ -3,6 +3,7 @@ import generalCategory from "./categories/general";
import appearanceCategory from "./categories/appearance";
import playerCategory from "./categories/player";
import lyricCategory from "./categories/lyric";
+import fullScreenLyricCategory from "./categories/fullScreenLyric";
import externalLyricCategory from "./categories/externalLyric";
import hotkeysCategory from "./categories/hotkeys";
import servicesCategory from "./categories/services";
@@ -18,6 +19,7 @@ export const settingsSchema: SettingCategory[] = [
appearanceCategory,
playerCategory,
lyricCategory,
+ fullScreenLyricCategory,
externalLyricCategory,
hotkeysCategory,
servicesCategory,
diff --git a/src/stores/media.ts b/src/stores/media.ts
index 3419739..edadbd0 100644
--- a/src/stores/media.ts
+++ b/src/stores/media.ts
@@ -4,7 +4,7 @@ import { findLyricIndex } from "@shared/utils/lyric";
import { useSettingsStore } from "@/stores/settings";
import { watchLyricPreference } from "@/services/lyricLoader";
import { parseLyric } from "@/utils/lyric/parse";
-import { extractLyricAuthor } from "@/utils/lyric/author";
+import { extractLyricAuthor, extractLyricAuthors } from "@/utils/lyric/author";
import { applyLyricExclude } from "@/utils/lyric/lyricStripper";
import { normalizeLyricLines } from "@/utils/lyric/normalize";
@@ -38,6 +38,9 @@ export const useMediaStore = defineStore("media", () => {
/** 当前歌词文件制作者 */
const lyricAuthor = ref(null);
+ /** 当前歌词文件制作者列表 */
+ const lyricAuthors = ref([]);
+
/** 同步当前歌词源到主进程 */
const syncToMain = (): void => {
try {
@@ -86,6 +89,7 @@ export const useMediaStore = defineStore("media", () => {
lyricContent.value = null;
parsedLyric.value = [];
lyricAuthor.value = null;
+ lyricAuthors.value = [];
lyricIndex.value = -1;
lyricLoading.value = true;
syncToMain();
@@ -115,6 +119,8 @@ export const useMediaStore = defineStore("media", () => {
parsedLyric.value = nextLines;
lyricAuthor.value =
hasContent && source && input ? extractLyricAuthor(input.content, source.format) : null;
+ lyricAuthors.value =
+ hasContent && source && input ? extractLyricAuthors(input.content, source.format) : [];
lyricIndex.value = -1;
lyricLoading.value = false;
syncToMain();
@@ -136,6 +142,7 @@ export const useMediaStore = defineStore("media", () => {
lyricContent.value = null;
parsedLyric.value = [];
lyricAuthor.value = null;
+ lyricAuthors.value = [];
lyricLoading.value = false;
lyricIndex.value = -1;
syncToMain();
@@ -149,6 +156,7 @@ export const useMediaStore = defineStore("media", () => {
lyricFormat,
parsedLyric,
lyricAuthor,
+ lyricAuthors,
lyricLoading,
lyricIndex,
setTrack,
diff --git a/src/stores/settings.ts b/src/stores/settings.ts
index 93b01fb..8de9de2 100644
--- a/src/stores/settings.ts
+++ b/src/stores/settings.ts
@@ -73,6 +73,8 @@ export const useSettingsStore = defineStore(
fontFamily: "",
showTranslation: true,
showRomanization: true,
+ amllShowLineRomanization: true,
+ amllShowWordRomanization: true,
enableWordHighlight: true,
enableFloatAnimation: false,
enableEmphasizeEffect: false,
@@ -88,6 +90,16 @@ export const useSettingsStore = defineStore(
enableExcludeLyrics: true,
excludeLyricsUserKeywords: [],
excludeLyricsUserRegexes: [],
+ engine: "physics",
+ useAMSpring: true,
+ amllVerticalSpringMass: 1,
+ amllVerticalSpringDamping: 15,
+ amllVerticalSpringStiffness: 100,
+ amllVerticalSpringSoft: false,
+ amllScaleSpringMass: 1,
+ amllScaleSpringDamping: 20,
+ amllScaleSpringStiffness: 100,
+ amllScaleSpringSoft: false,
});
/** 系统配置 - 传递主进程 */
diff --git a/src/types/settings-schema.ts b/src/types/settings-schema.ts
index 1099e77..c3d3f57 100644
--- a/src/types/settings-schema.ts
+++ b/src/types/settings-schema.ts
@@ -8,7 +8,8 @@ export type SettingWidgetType =
| "color"
| "button"
| "custom"
- | "number";
+ | "number"
+ | "title";
/** 选择项 */
export interface SettingOption {
@@ -51,6 +52,8 @@ export interface SettingItem {
hideDescription?: boolean;
/** 条件禁用 */
disabled?: () => boolean;
+ /** 条件隐藏 */
+ visible?: () => boolean;
/** button 类型的点击回调 */
action?: () => void;
/** custom 类型的组件 */
@@ -65,6 +68,8 @@ export interface SettingItem {
childrenCondition?: () => boolean;
/** 是否完全隐藏子项 */
hideChildren?: boolean;
+ /** 是否对子项进行左侧边线和缩进(默认 true) */
+ indentChildren?: boolean;
/** 标题旁的徽标 */
tag?: SettingTag;
}
diff --git a/src/types/settings.ts b/src/types/settings.ts
index aedc492..c86dd3b 100644
--- a/src/types/settings.ts
+++ b/src/types/settings.ts
@@ -5,7 +5,7 @@ import { ALL_PLATFORMS } from "@shared/types/platform";
import type { QualityLevel } from "@/utils/quality";
/** 播放器背景类型 */
-export type PlayerBgType = "blur" | "solid";
+export type PlayerBgType = "blur" | "solid" | "animation";
export type CoverLayout = "default" | "fullscreen";
/**
@@ -79,6 +79,10 @@ export interface LyricSettings {
showTranslation: boolean;
/** 是否显示音译歌词 */
showRomanization: boolean;
+ /** AMLL 是否显示逐行音译 */
+ amllShowLineRomanization: boolean;
+ /** AMLL 是否显示逐词音译 */
+ amllShowWordRomanization: boolean;
/** 逐字高亮效果 */
enableWordHighlight: boolean;
/** 逐字上浮动画 */
@@ -109,6 +113,20 @@ export interface LyricSettings {
excludeLyricsUserKeywords: string[];
/** 用户自定义正则 */
excludeLyricsUserRegexes: string[];
+ /** 歌词引擎类型 */
+ engine: "physics" | "amll";
+ /** AM 歌词是否启用物理回弹与缩放 */
+ useAMSpring: boolean;
+ /** AMLL 垂直位移弹簧参数 */
+ amllVerticalSpringMass: number;
+ amllVerticalSpringDamping: number;
+ amllVerticalSpringStiffness: number;
+ amllVerticalSpringSoft: boolean;
+ /** AMLL 缩放弹簧参数 */
+ amllScaleSpringMass: number;
+ amllScaleSpringDamping: number;
+ amllScaleSpringStiffness: number;
+ amllScaleSpringSoft: boolean;
}
/** 播放器设置 */
diff --git a/src/utils/lyric/author.ts b/src/utils/lyric/author.ts
index d3cd4e5..a2b54e0 100644
--- a/src/utils/lyric/author.ts
+++ b/src/utils/lyric/author.ts
@@ -1,19 +1,43 @@
import type { LyricFormat } from "@shared/types/lyrics";
/**
- * 从歌词原始内容中提取「歌词文件制作者」
+ * 从歌词原始内容中提取「歌词文件制作者」列表
* @param content - 歌词原始文本
* @param format - 歌词格式
+ * @returns 作者账号/名称的数组
*/
-export const extractLyricAuthor = (content: string, format: LyricFormat): string | null => {
+export const extractLyricAuthors = (content: string, format: LyricFormat): string[] => {
if (format === "ttml") {
- // 优先 GitHub 登录名
- const login = content.match(/key="ttmlAuthorGithubLogin"\s+value="([^"]*)"/)?.[1];
- const base = content.match(/key="ttmlAuthorGithub"\s+value="([^"]*)"/)?.[1];
- return (login || base || "").trim() || null;
+ // 优先提取 ttmlAuthorGithubLogin,作为可以直接用于跳转 GitHub 的账号
+ const logins = [...content.matchAll(/key="ttmlAuthorGithubLogin"\s+value="([^"]*)"/g)]
+ .map((m) => m[1].trim())
+ .filter(Boolean);
+ if (logins.length > 0) {
+ return Array.from(new Set(logins));
+ }
+ // 如果无 login 标识,从 ttmlAuthorGithub 主页链接中截取最后的用户名
+ const bases = [...content.matchAll(/key="ttmlAuthorGithub"\s+value="([^"]*)"/g)]
+ .map((m) => {
+ const val = m[1].trim();
+ const parts = val.split("/");
+ return parts[parts.length - 1] || val;
+ })
+ .filter(Boolean);
+ return Array.from(new Set(bases));
}
if (format === "lrc") {
- return content.match(/\[by:([^\]]+)\]/i)?.[1]?.trim() || null;
+ const match = content.match(/\[by:([^\]]+)\]/i)?.[1]?.trim();
+ return match ? [match] : [];
}
- return null;
+ return [];
+};
+
+/**
+ * 从歌词原始内容中提取首个「歌词文件制作者」
+ * @param content - 歌词原始文本
+ * @param format - 歌词格式
+ * @returns 首个作者名称或 null
+ */
+export const extractLyricAuthor = (content: string, format: LyricFormat): string | null => {
+ return extractLyricAuthors(content, format)[0] || null;
};
diff --git a/src/utils/lyric/parse.ts b/src/utils/lyric/parse.ts
index 98d7b1b..8cf868d 100644
--- a/src/utils/lyric/parse.ts
+++ b/src/utils/lyric/parse.ts
@@ -4,7 +4,11 @@ import { parseLRC } from "./parseLRC";
import { parseQRC } from "./parseQRC";
import { parseYRC } from "./parseYRC";
import { parseKRC } from "./parseKRC";
-import { parseTTML } from "./parseTTML";
+import { parseTTML, cleanTTMLTranslations } from "./parseTTML";
+import {
+ parseTTML as parseAMLLTtml,
+ parseYrc as parseAMLLYrc,
+} from "@applemusic-like-lyrics/lyric";
import { parseLyS } from "./parseLyS";
import { parseSRT } from "./parseSRT";
import { parseASS } from "./parseASS";
@@ -70,14 +74,26 @@ export const detectFormat = (text: string): LyricFormat => {
*/
const parseContent = (text: string, format: LyricFormat, preferredLang = ""): LyricLine[] => {
switch (format) {
- case "ttml":
- return parseTTML(text, preferredLang);
+ case "ttml": {
+ const cleaned = cleanTTMLTranslations(text, preferredLang);
+ try {
+ return parseAMLLTtml(cleaned).lines || [];
+ } catch (err) {
+ console.error("AMLL TTML parse failed, fallback to local parseTTML:", err);
+ return parseTTML(text, preferredLang);
+ }
+ }
case "qrc":
return parseQRC(text);
case "krc":
return parseKRC(text);
case "yrc":
- return parseYRC(text);
+ try {
+ return parseAMLLYrc(text) || [];
+ } catch (err) {
+ console.error("AMLL YRC parse failed, fallback to local parseYRC:", err);
+ return parseYRC(text);
+ }
case "lrc":
return parseLRC(text);
case "lys":
diff --git a/src/utils/lyric/parseTTML.ts b/src/utils/lyric/parseTTML.ts
index 2fd625f..f84b991 100644
--- a/src/utils/lyric/parseTTML.ts
+++ b/src/utils/lyric/parseTTML.ts
@@ -424,3 +424,79 @@ export const parseTTML = (text: string, preferredLang = ""): LyricLine[] => {
return lines;
};
+
+/**
+ * 清洗 TTML 中不需要的翻译,过滤非首选语言节点,避免上游解析器混淆
+ * @param ttmlContent 原始 TTML 内容
+ * @param preferredLang 偏好的语言(如 zh-CN)
+ * @returns 清洗后的 TTML 内容
+ */
+export const cleanTTMLTranslations = (ttmlContent: string, preferredLang = ""): string => {
+ /**
+ * 统计 TTML 中的语言
+ */
+ const langCounter = (ttml_text: string) => {
+ const langRegex = /(?<=<(span|translation)[^<>]+)xml:lang="([^"]+)"/g;
+ const matches = ttml_text.matchAll(langRegex);
+ const langSet = new Set();
+ for (const match of matches) {
+ if (match[2]) langSet.add(match[2]);
+ }
+ return Array.from(langSet);
+ };
+
+ /**
+ * 过滤语言并选择最佳匹配
+ */
+ const langFilter = (langs: string[]): string | null => {
+ if (langs.length <= 1) return null;
+
+ const langMatcher = (target: string) => {
+ return langs.find((lang) => {
+ try {
+ return new Intl.Locale(lang).maximize().script === target;
+ } catch {
+ return false;
+ }
+ });
+ };
+
+ // 优先匹配用户的偏好语言
+ if (preferredLang) {
+ const preferred = preferredLang.toLowerCase().replace(/_/g, "-");
+ const matched = langs.find((lang) => lang.toLowerCase().replace(/_/g, "-") === preferred);
+ if (matched) return matched;
+
+ const prefBase = preferred.split("-")[0];
+ const matchedBase = langs.find(
+ (lang) => lang.toLowerCase().replace(/_/g, "-").split("-")[0] === prefBase,
+ );
+ if (matchedBase) return matchedBase;
+ }
+
+ // 备选的中文脚本匹配优先级
+ const hans_matched = langMatcher("Hans");
+ if (hans_matched) return hans_matched;
+ const hant_matched = langMatcher("Hant");
+ if (hant_matched) return hant_matched;
+ const major = langs.find((key) => key.startsWith("zh"));
+ if (major) return major;
+ return langs[0];
+ };
+
+ /**
+ * 替换清洗标签
+ */
+ const ttmlCleaner = (ttml_text: string, major_lang: string | null): string => {
+ if (major_lang === null) return ttml_text;
+ const replacer = (match: string, lang: string) => (lang === major_lang ? match : "");
+ const translationRegex = /]+xml:lang="([^"]+)"[^>]*>[\s\S]*?<\/translation>/g;
+ const spanRegex = /]+xml:lang="([^" ]+)"[^>]*>[\s\S]*?<\/span>/g;
+ return ttml_text.replace(translationRegex, replacer).replace(spanRegex, replacer);
+ };
+
+ const context_lang = langCounter(ttmlContent);
+ const major = langFilter(context_lang);
+ const cleaned_ttml = ttmlCleaner(ttmlContent, major);
+ return cleaned_ttml.replace(/\n\s*/g, "");
+};