Compare commits

...

12 Commits

Author SHA1 Message Date
Hosted Weblate 062617bb40 Translated using Weblate
Currently translated at 100.0% (1285 of 1285 strings) (Czech)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/

Translated using Weblate

Currently translated at 100.0% (1285 of 1285 strings) (Chinese (Traditional Han script))
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hant/

Translated using Weblate

Currently translated at 100.0% (1285 of 1285 strings) (Spanish)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/

Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Fordas <fordas15@gmail.com>
Co-authored-by: York <goog10216922@gmail.com>
2026-06-28 05:01:23 +00:00
York f8ca8861fc feat: add romaji lyrics display (#2180) 2026-06-26 21:07:58 -07:00
Ryan Kupka 26eea7422d fix: recover mpv playback after the OS resumes from sleep (#2172)
mpv/ffmpeg had no network-level timeout or reconnect options, so a
network stream left open across a system sleep would block forever on
the now-dead TCP connection instead of failing or reconnecting. Since
Node-MPV's IPC commands only resolve when mpv replies, a wedged mpv
process also made quit()/restart hang indefinitely, so the only way
out was to kill the whole app.

- Add --network-timeout and ffmpeg reconnect options to mpv's default
  parameters so a stalled stream fails fast instead of hanging.
- Make the quit() helper resilient to an unresponsive mpv process by
  racing it against a timeout and force-killing as a fallback.
- Listen for Electron's powerMonitor 'resume' event and tell the
  renderer to reload mpv, so playback recovers automatically instead
  of requiring a manual app restart.
2026-06-26 19:18:27 -07:00
Hosted Weblate 21d788226c Translated using Weblate
Currently translated at 100.0% (1283 of 1283 strings) (Czech)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/

Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
2026-06-26 22:01:23 +02:00
Hosted Weblate 9a1bf8f4a9 Translated using Weblate
Currently translated at 48.0% (616 of 1283 strings) (Ukrainian)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/uk/

Co-authored-by: albatrays <weblate.duct925@passmail.net>
2026-06-24 15:27:33 +02:00
Hosted Weblate 0fab3ba318 Translated using Weblate
Currently translated at 100.0% (1283 of 1283 strings) (Estonian)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/et/

Translated using Weblate

Currently translated at 100.0% (1283 of 1283 strings) (Catalan)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/ca/

Translated using Weblate

Currently translated at 100.0% (1283 of 1283 strings) (Polish)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/pl/

Co-authored-by: Ondo <SparkyOndo@proton.me>
Co-authored-by: rimasx <riks_12@hot.ee>
Co-authored-by: skajmer <skajmer@protonmail.com>
2026-06-24 10:01:26 +00:00
Norman 5ddbfcbfee Highlight the playlist in the left panel on play (#2025)
* Fixed bad smart playlist field s

* first try to add playlist highlight

* Simplified calls

* Now works for grids too.

* Derive the playlist highlight from the currently-playing track's origin instead of a stale global field.

* addressed comments
2026-06-24 03:18:02 +00:00
Ilya Shurupov b6519e9839 fix(sharing): copy share link to clipboard from within the user gesture (#2135)
The share URL is only known after the create-share request resolves, so the
previous navigator.clipboard.writeText() in the mutation onSuccess callback ran
outside the originating click's user activation. Firefox and Safari reject such
writes ('Clipboard write was blocked due to lack of user activation'), so the
link was never copied (and the toast still claimed success).

Issue clipboard.write() synchronously inside the submit gesture with a
ClipboardItem whose text/plain value is a promise resolving to the share URL,
which preserves the user activation while the share is created. Falls back to
writeText, and finally to the existing 'click to open' toast when the clipboard
is unavailable or blocked, with the toast text reflecting whether the copy
actually succeeded.

Co-authored-by: ilusha <ilusha.basic@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 22:03:38 -07:00
Hosted Weblate 2103c3b8c6 Translated using Weblate
Currently translated at 100.0% (1283 of 1283 strings) (Chinese (Traditional Han script))
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hant/

Translated using Weblate

Currently translated at 100.0% (1283 of 1283 strings) (Spanish)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/

Translated using Weblate

Currently translated at 48.1% (601 of 1249 strings) (Ukrainian)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/uk/

Translated using Weblate

Currently translated at 100.0% (1249 of 1249 strings) (Chinese (Traditional Han script))
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hant/

Co-authored-by: Fordas <fordas15@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: York <goog10216922@gmail.com>
Co-authored-by: albatrays <weblate.duct925@passmail.net>
2026-06-23 07:03:19 +02:00
York 4a7f084b59 fix: lyrics export issue when furigana is enabled (#2175)
* fix: lyrics export issue when furigana is enabled
2026-06-22 22:03:13 -07:00
jeffvli 5ef4744c44 bump electron-builder to latest, fix alpha build 2026-06-22 21:15:51 -07:00
York ecda4ef8bc fix: add i18n for EQ settings (#2174)
* fix: add i18n for EQ settings

* reuse common reset i18n labels
2026-06-22 14:17:50 +00:00
34 changed files with 951 additions and 520 deletions
+1
View File
@@ -71,4 +71,5 @@ publish:
provider: s3
bucket: feishin-nightly
channel: alpha
region: auto
endpoint: https://065f090c64de2dc707dd70ac72db9669.r2.cloudflarestorage.com
+2 -2
View File
@@ -99,7 +99,7 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"dayjs": "^1.11.21",
"dompurify": "^3.4.8",
"dompurify": "^3.4.11",
"electron-debug": "^3.2.0",
"electron-localshortcut": "^3.2.1",
"electron-log": "^5.4.4",
@@ -162,7 +162,7 @@
"concurrently": "^9.2.1",
"cross-env": "^10.1.0",
"electron": "^41.7.1",
"electron-builder": "^26.15.0",
"electron-builder": "^26.15.5",
"electron-devtools-installer": "^4.0.0",
"electron-vite": "^4.0.1",
"eslint": "^9.39.4",
+86 -376
View File
@@ -94,8 +94,8 @@ importers:
specifier: ^1.11.21
version: 1.11.21
dompurify:
specifier: ^3.4.8
version: 3.4.8
specifier: ^3.4.11
version: 3.4.11
electron-debug:
specifier: ^3.2.0
version: 3.2.0
@@ -278,8 +278,8 @@ importers:
specifier: ^41.7.1
version: 41.7.1
electron-builder:
specifier: ^26.15.0
version: 26.15.0(electron-builder-squirrel-windows@26.15.0)
specifier: ^26.15.5
version: 26.15.5(electron-builder-squirrel-windows@26.15.5)
electron-devtools-installer:
specifier: ^4.0.0
version: 4.0.0
@@ -1011,8 +1011,8 @@ packages:
engines: {node: '>=12.0.0'}
hasBin: true
'@electron/rebuild@4.0.3':
resolution: {integrity: sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA==}
'@electron/rebuild@4.0.4':
resolution: {integrity: sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg==}
engines: {node: '>=22.12.0'}
hasBin: true
@@ -1546,14 +1546,6 @@ packages:
resolution: {integrity: sha512-ugvXJjwF5ldtUpa7D95kruNJ41yFQDEKyF5CW4TgKJnh+W/zmlBzXXeKTyqIgwMFrkePN2JqOBqcF0M0oOunow==}
engines: {node: '>=0.3.0'}
'@npmcli/agent@3.0.0':
resolution: {integrity: sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==}
engines: {node: ^18.17.0 || >=20.5.0}
'@npmcli/fs@4.0.0':
resolution: {integrity: sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==}
engines: {node: ^18.17.0 || >=20.5.0}
'@peculiar/asn1-schema@2.7.0':
resolution: {integrity: sha512-W8ZfWzLmQnrcky+eh3tni4IozMdqBDiHWU0N+vve/UGjMaUs8c0L7A2oEdkBXS8rTpWDpK/aoI3DG/L/hxmxPg==}
@@ -2272,11 +2264,10 @@ packages:
'@xmldom/xmldom@0.8.13':
resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==}
engines: {node: '>=10.0.0'}
deprecated: this version has critical issues, please update to the latest version
abbrev@3.0.1:
resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==}
engines: {node: ^18.17.0 || >=20.5.0}
abbrev@4.0.0:
resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==}
engines: {node: ^20.17.0 || >=22.9.0}
abstract-socket@2.1.1:
resolution: {integrity: sha512-YZJizsvS1aBua5Gd01woe4zuyYBGgSMeqDOB6/ChwdTI904KP6QGtJswXl4hcqWxbz86hQBe++HWV0hF1aGUtA==}
@@ -2336,12 +2327,12 @@ packages:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
app-builder-lib@26.15.0:
resolution: {integrity: sha512-j2+P6Lh+l/VuWfXZWSs7u+OAPqYJQGnZZO30M833XQQaRuyohm4RZk7Gw4nQXfeyQH9GqXaTwR16Y0LaVTlS+g==}
app-builder-lib@26.15.5:
resolution: {integrity: sha512-CJdzqy4YXQQdn+ivw1ssuY4yBTgVaBtniB2Dnjc6JsM9mbXoZ4shbuuysjenZloMOEIKEqkuRxltNQyG/NP/pA==}
engines: {node: '>=14.0.0'}
peerDependencies:
dmg-builder: 26.15.0
electron-builder-squirrel-windows: 26.15.0
dmg-builder: 26.15.5
electron-builder-squirrel-windows: 26.15.5
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
@@ -2486,9 +2477,6 @@ packages:
bindings@1.5.0:
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
bl@5.1.0:
resolution: {integrity: sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==}
@@ -2542,9 +2530,6 @@ packages:
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
buffer@6.0.3:
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
@@ -2552,8 +2537,8 @@ packages:
resolution: {integrity: sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==}
engines: {node: '>=12.0.0'}
builder-util@26.15.0:
resolution: {integrity: sha512-dUx+HxVbiNsNQ4mGe1PyoC/tBmsHwBNDLdBuqWCj+rhHFE9lHgrXiGYKAM1uNlznhAaUSyMlms84VeSSr3gOBA==}
builder-util@26.15.3:
resolution: {integrity: sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw==}
engines: {node: '>=14.0.0'}
butterchurn-presets@3.0.0-beta.4:
@@ -2570,10 +2555,6 @@ packages:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'}
cacache@19.0.1:
resolution: {integrity: sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==}
engines: {node: ^18.17.0 || >=20.5.0}
cacheable-lookup@5.0.4:
resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==}
engines: {node: '>=10.6.0'}
@@ -2640,14 +2621,6 @@ packages:
resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==}
engines: {node: '>=8'}
cli-cursor@3.1.0:
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
engines: {node: '>=8'}
cli-spinners@2.9.2:
resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
engines: {node: '>=6'}
cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
@@ -2655,10 +2628,6 @@ packages:
clone-response@1.0.3:
resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==}
clone@1.0.4:
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
engines: {node: '>=0.8'}
clone@2.1.2:
resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==}
engines: {node: '>=0.8'}
@@ -2850,9 +2819,6 @@ packages:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
defaults@1.0.4:
resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==}
defer-to-connect@2.0.1:
resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==}
engines: {node: '>=10'}
@@ -2873,10 +2839,6 @@ packages:
resolution: {integrity: sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==}
engines: {node: '>=12.20'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
detect-newline@4.0.1:
resolution: {integrity: sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -2897,8 +2859,8 @@ packages:
discord-api-types@0.38.48:
resolution: {integrity: sha512-WFUE/2o0lBlLeCQonQ+Pu2RqHAqbytBJ2RlXR91gzk05InSS6k9ShzzLYoymrA4c2oRgRKGE7/VqQJNNdGWSxQ==}
dmg-builder@26.15.0:
resolution: {integrity: sha512-oS8MWttbpIUF/2v8LOEY+f4ayL84ipMOarZvdRMl/pxlhLxAYjYMklTXHEXIl37Ig+qJv/bVF7HgyIoOoZyMWA==}
dmg-builder@26.15.5:
resolution: {integrity: sha512-Ts58Bs9QVCPhkhvkz9V1JwVoIwmbA06szZTM7W/ihzoDjHlf7KJo1Ci9nFknoFUC8uDeYgtbu5HW8eAeZ5qeSA==}
doctrine@2.1.0:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
@@ -2917,8 +2879,8 @@ packages:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
dompurify@3.4.8:
resolution: {integrity: sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==}
dompurify@3.4.11:
resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==}
domutils@3.2.2:
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
@@ -2962,11 +2924,11 @@ packages:
engines: {node: '>=0.10.0'}
hasBin: true
electron-builder-squirrel-windows@26.15.0:
resolution: {integrity: sha512-WyiGTdAHOk8oncyg/Ik0+NsrPI6y7ftG/eB7uxr9iGjmF9RNuIdeW/BRpI7iLEe718WkBQCbspUAq/4Vi31wXg==}
electron-builder-squirrel-windows@26.15.5:
resolution: {integrity: sha512-+7D6F08V26p8dLLu2rK4MReQR50lA5W6hEgtNmjm6xbLrAGCJSWFbQEca6oNRKnFGlfHGS1TfgHLmOL3HX+6DA==}
electron-builder@26.15.0:
resolution: {integrity: sha512-zd4cfvjHmtyGqMaDudg5rAjNUkwIJDz8ICaCsz77hFKcjMQHcZNNNCs/C4phwN9+gEVwmhvpKMzNFum6fs/n6A==}
electron-builder@26.15.5:
resolution: {integrity: sha512-ii+Befxc8diyoQv9iUchEzBAvFef4vrY/l2NID1wdZL2WCTLe80sYQz7Alc+yswWPpgowUdpsI5HtomE2Lj/Mg==}
engines: {node: '>=14.0.0'}
hasBin: true
@@ -2989,8 +2951,8 @@ packages:
resolution: {integrity: sha512-istWgaXjBfURBSS8LWVW9C3jsc6+ac+tY1lXrQEOTp0lVj+a4OlO1Tmqb36GgnEUDv92DGC9VI1HNXwJinWpgA==}
engines: {node: '>= 14'}
electron-publish@26.15.0:
resolution: {integrity: sha512-pt6K3ol/a+o3HbqmYkL2NYlVH5pd34tL4FPRcgX8E88xQAqQyIsseXe4vWy7Pq2BaYy+iFGJrtInZe11FFAQwQ==}
electron-publish@26.15.3:
resolution: {integrity: sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==}
electron-store@8.2.0:
resolution: {integrity: sha512-ukLL5Bevdil6oieAOXz3CMy+OgaItMiVBg701MNlG6W5RaC0AHN7rvlqTCmeb6O7jP0Qa1KKYTE0xV0xbhF4Hw==}
@@ -3030,9 +2992,6 @@ packages:
encoding-sniffer@0.2.1:
resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==}
encoding@0.1.13:
resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==}
end-of-stream@1.4.5:
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
@@ -3383,10 +3342,6 @@ packages:
fs-merger@3.2.1:
resolution: {integrity: sha512-AN6sX12liy0JE7C2evclwoo0aCG3PFulLjrTLsJpWh/2mM+DinhpSGqYLbHBBbIW1PLRNcFhJG8Axtz8mQW3ug==}
fs-minipass@3.0.3:
resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
fs-mkdirp-stream@2.0.1:
resolution: {integrity: sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw==}
engines: {node: '>=10.13.0'}
@@ -3689,10 +3644,6 @@ packages:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'}
ip-address@10.2.0:
resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==}
engines: {node: '>= 12'}
is-arguments@1.2.0:
resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==}
engines: {node: '>= 0.4'}
@@ -3758,10 +3709,6 @@ packages:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
is-interactive@1.0.0:
resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
engines: {node: '>=8'}
is-map@2.0.3:
resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==}
engines: {node: '>= 0.4'}
@@ -3833,10 +3780,6 @@ packages:
resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==}
engines: {node: '>= 0.4'}
is-unicode-supported@0.1.0:
resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
engines: {node: '>=10'}
is-valid-glob@1.0.0:
resolution: {integrity: sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==}
engines: {node: '>=0.10.0'}
@@ -3874,6 +3817,10 @@ packages:
resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==}
engines: {node: '>=18'}
isexe@4.0.0:
resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==}
engines: {node: '>=20'}
iterator.prototype@1.1.5:
resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
engines: {node: '>= 0.4'}
@@ -4048,10 +3995,6 @@ packages:
lodash@4.18.1:
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
log-symbols@4.1.0:
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
engines: {node: '>=10'}
long@4.0.0:
resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==}
@@ -4086,10 +4029,6 @@ packages:
resolution: {integrity: sha512-S1+hd+dIrC8EZqKyT9DstTH/0Z+f76kmmvZnkfQVmOpDEF9iVgdYif3Q/pIWHmCoo59bQVGW0kVL3e2nl+9+Sw==}
engines: {node: '>=12'}
make-fetch-happen@14.0.3:
resolution: {integrity: sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==}
engines: {node: ^18.17.0 || >=20.5.0}
map-stream@0.1.0:
resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==}
@@ -4176,30 +4115,6 @@ packages:
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
minipass-collect@2.0.1:
resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==}
engines: {node: '>=16 || 14 >=14.17'}
minipass-fetch@4.0.1:
resolution: {integrity: sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==}
engines: {node: ^18.17.0 || >=20.5.0}
minipass-flush@1.0.7:
resolution: {integrity: sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==}
engines: {node: '>= 8'}
minipass-pipeline@1.2.4:
resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==}
engines: {node: '>=8'}
minipass-sized@1.0.3:
resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==}
engines: {node: '>=8'}
minipass@3.3.6:
resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
engines: {node: '>=8'}
minipass@7.1.3:
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -4260,10 +4175,6 @@ packages:
resolution: {integrity: sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg==}
engines: {node: '>=18'}
negotiator@1.0.0:
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
engines: {node: '>= 0.6'}
node-abi@4.31.0:
resolution: {integrity: sha512-Erq5w/t3syw3s4sDsUaX4QttIdBPsGKTT1DTRsCkTonGggczhlDKm/wDX3o+HPJpQ41EjXCbcmXf0tgr5YZJXw==}
engines: {node: '>=22.12.0'}
@@ -4275,9 +4186,9 @@ packages:
resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==}
engines: {node: '>= 0.4'}
node-gyp@11.5.0:
resolution: {integrity: sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==}
engines: {node: ^18.17.0 || >=20.5.0}
node-gyp@12.4.0:
resolution: {integrity: sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==}
engines: {node: ^20.17.0 || >=22.9.0}
hasBin: true
node-int64@0.4.0:
@@ -4291,9 +4202,9 @@ packages:
resolution: {integrity: sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==}
engines: {node: '>=18'}
nopt@8.1.0:
resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==}
engines: {node: ^18.17.0 || >=20.5.0}
nopt@9.0.0:
resolution: {integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==}
engines: {node: ^20.17.0 || >=22.9.0}
hasBin: true
normalize-path@3.0.0:
@@ -4354,10 +4265,6 @@ packages:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
ora@5.4.1:
resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
engines: {node: '>=10'}
overlayscrollbars-react@0.5.6:
resolution: {integrity: sha512-E5To04bL5brn9GVCZ36SnfGanxa2I2MDkWoa4Cjo5wol7l+diAgi4DBc983V7l2nOk/OLJ6Feg4kySspQEGDBw==}
peerDependencies:
@@ -4391,10 +4298,6 @@ packages:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
p-map@7.0.4:
resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==}
engines: {node: '>=18'}
p-try@2.2.0:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'}
@@ -4584,9 +4487,9 @@ packages:
resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==}
engines: {node: ^14.13.1 || >=16.0.0}
proc-log@5.0.0:
resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==}
engines: {node: ^18.17.0 || >=20.5.0}
proc-log@6.1.0:
resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==}
engines: {node: ^20.17.0 || >=22.9.0}
process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
@@ -4868,10 +4771,6 @@ packages:
responselike@2.0.1:
resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==}
restore-cursor@3.1.0:
resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==}
engines: {node: '>=8'}
retry@0.12.0:
resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
engines: {node: '>= 4'}
@@ -5049,22 +4948,10 @@ packages:
resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
engines: {node: '>=10'}
smart-buffer@4.2.0:
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
smob@1.6.2:
resolution: {integrity: sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==}
engines: {node: '>=20.0.0'}
socks-proxy-agent@8.0.5:
resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==}
engines: {node: '>= 14'}
socks@2.8.9:
resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==}
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
sort-keys@5.1.0:
resolution: {integrity: sha512-aSbHV0DaBcr7u0PVHXzM6NbZNAtrr9sF6+Qfs9UUVG7Ll3jQ6hHi8F/xqIIcn2rvIVbr0v/2zyjSdwSV47AgLQ==}
engines: {node: '>=12'}
@@ -5103,10 +4990,6 @@ packages:
sprintf-js@1.1.3:
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
ssri@12.0.0:
resolution: {integrity: sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==}
engines: {node: ^18.17.0 || >=20.5.0}
stat-mode@1.0.0:
resolution: {integrity: sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==}
engines: {node: '>= 6'}
@@ -5411,6 +5294,10 @@ packages:
resolution: {integrity: sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==}
engines: {node: '>=18.17'}
undici@6.27.0:
resolution: {integrity: sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==}
engines: {node: '>=18.17'}
undici@7.27.2:
resolution: {integrity: sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==}
engines: {node: '>=20.18.1'}
@@ -5431,14 +5318,6 @@ packages:
resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==}
engines: {node: '>=4'}
unique-filename@4.0.0:
resolution: {integrity: sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==}
engines: {node: ^18.17.0 || >=20.5.0}
unique-slug@5.0.0:
resolution: {integrity: sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==}
engines: {node: ^18.17.0 || >=20.5.0}
unique-string@2.0.0:
resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==}
engines: {node: '>=8'}
@@ -5595,9 +5474,6 @@ packages:
wavesurfer.js@7.12.7:
resolution: {integrity: sha512-TIe7hB6OCZysNOZ2cn2NR8Qpko22POWel6rauNcqOammFoH65NYQUM35unNLLMIlUMVYvjJ6w/TTl/G/m+w0nA==}
wcwidth@1.0.1:
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
webcrypto-core@1.9.2:
resolution: {integrity: sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==}
@@ -5646,6 +5522,11 @@ packages:
engines: {node: ^18.17.0 || >=20.5.0}
hasBin: true
which@6.0.1:
resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==}
engines: {node: ^20.17.0 || >=22.9.0}
hasBin: true
word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
@@ -6669,21 +6550,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@electron/rebuild@4.0.3':
'@electron/rebuild@4.0.4':
dependencies:
'@malept/cross-spawn-promise': 2.0.0
debug: 4.4.3
detect-libc: 2.1.2
got: 11.8.6
graceful-fs: 4.2.11
node-abi: 4.31.0
node-api-version: 0.2.1
node-gyp: 11.5.0
ora: 5.4.1
node-gyp: 12.4.0
read-binary-file-arch: 1.0.6
semver: 7.8.2
tar: 7.5.16
yargs: 17.7.2
transitivePeerDependencies:
- supports-color
@@ -7094,20 +6968,6 @@ snapshots:
'@nornagon/put@0.0.8': {}
'@npmcli/agent@3.0.0':
dependencies:
agent-base: 7.1.4
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
lru-cache: 10.4.3
socks-proxy-agent: 8.0.5
transitivePeerDependencies:
- supports-color
'@npmcli/fs@4.0.0':
dependencies:
semver: 7.8.2
'@peculiar/asn1-schema@2.7.0':
dependencies:
'@peculiar/utils': 2.0.3
@@ -7793,7 +7653,7 @@ snapshots:
'@xmldom/xmldom@0.8.13': {}
abbrev@3.0.1: {}
abbrev@4.0.0: {}
abstract-socket@2.1.1:
dependencies:
@@ -7848,14 +7708,14 @@ snapshots:
normalize-path: 3.0.0
picomatch: 2.3.2
app-builder-lib@26.15.0(dmg-builder@26.15.0)(electron-builder-squirrel-windows@26.15.0):
app-builder-lib@26.15.5(dmg-builder@26.15.5)(electron-builder-squirrel-windows@26.15.5):
dependencies:
'@electron/asar': 3.4.1
'@electron/fuses': 1.8.0
'@electron/get': 3.1.0
'@electron/notarize': 2.5.0
'@electron/osx-sign': 1.3.3
'@electron/rebuild': 4.0.3
'@electron/rebuild': 4.0.4
'@electron/universal': 2.0.3
'@malept/flatpak-bundler': 0.4.0
'@noble/hashes': 2.2.0
@@ -7864,17 +7724,17 @@ snapshots:
ajv: 8.20.0
asn1js: 3.0.10
async-exit-hook: 2.0.1
builder-util: 26.15.0
builder-util: 26.15.3
builder-util-runtime: 9.7.0
chromium-pickle-js: 0.2.0
ci-info: 4.3.1
debug: 4.4.3
dmg-builder: 26.15.0(electron-builder-squirrel-windows@26.15.0)
dmg-builder: 26.15.5(electron-builder-squirrel-windows@26.15.5)
dotenv: 16.6.1
dotenv-expand: 11.0.7
ejs: 3.1.10
electron-builder-squirrel-windows: 26.15.0(dmg-builder@26.15.0)
electron-publish: 26.15.0
electron-builder-squirrel-windows: 26.15.5(dmg-builder@26.15.5)
electron-publish: 26.15.3
fs-extra: 10.1.0
hosted-git-info: 4.1.0
isbinaryfile: 5.0.7
@@ -8052,12 +7912,6 @@ snapshots:
file-uri-to-path: 1.0.0
optional: true
bl@4.1.0:
dependencies:
buffer: 5.7.1
inherits: 2.0.4
readable-stream: 3.6.2
bl@5.1.0:
dependencies:
buffer: 6.0.3
@@ -8124,11 +7978,6 @@ snapshots:
buffer-from@1.1.2: {}
buffer@5.7.1:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
buffer@6.0.3:
dependencies:
base64-js: 1.5.1
@@ -8141,7 +7990,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
builder-util@26.15.0:
builder-util@26.15.3:
dependencies:
'@types/debug': 4.1.13
builder-util-runtime: 9.7.0
@@ -8174,21 +8023,6 @@ snapshots:
cac@6.7.14: {}
cacache@19.0.1:
dependencies:
'@npmcli/fs': 4.0.0
fs-minipass: 3.0.3
glob: 10.5.0
lru-cache: 10.4.3
minipass: 7.1.3
minipass-collect: 2.0.1
minipass-flush: 1.0.7
minipass-pipeline: 1.2.4
p-map: 7.0.4
ssri: 12.0.0
tar: 7.5.16
unique-filename: 4.0.0
cacheable-lookup@5.0.4: {}
cacheable-request@7.0.4:
@@ -8272,12 +8106,6 @@ snapshots:
ci-info@4.4.0: {}
cli-cursor@3.1.0:
dependencies:
restore-cursor: 3.1.0
cli-spinners@2.9.2: {}
cliui@8.0.1:
dependencies:
string-width: 4.2.3
@@ -8288,8 +8116,6 @@ snapshots:
dependencies:
mimic-response: 1.0.1
clone@1.0.4: {}
clone@2.1.2: {}
clsx@2.1.1: {}
@@ -8480,10 +8306,6 @@ snapshots:
deepmerge@4.3.1: {}
defaults@1.0.4:
dependencies:
clone: 1.0.4
defer-to-connect@2.0.1: {}
define-data-property@1.1.4:
@@ -8502,8 +8324,6 @@ snapshots:
detect-indent@7.0.2: {}
detect-libc@2.1.2: {}
detect-newline@4.0.1: {}
detect-node-es@1.1.0: {}
@@ -8522,10 +8342,10 @@ snapshots:
discord-api-types@0.38.48: {}
dmg-builder@26.15.0(electron-builder-squirrel-windows@26.15.0):
dmg-builder@26.15.5(electron-builder-squirrel-windows@26.15.5):
dependencies:
app-builder-lib: 26.15.0(dmg-builder@26.15.0)(electron-builder-squirrel-windows@26.15.0)
builder-util: 26.15.0
app-builder-lib: 26.15.5(dmg-builder@26.15.5)(electron-builder-squirrel-windows@26.15.5)
builder-util: 26.15.3
fs-extra: 10.1.0
js-yaml: 4.2.0
transitivePeerDependencies:
@@ -8553,7 +8373,7 @@ snapshots:
dependencies:
domelementtype: 2.3.0
dompurify@3.4.8:
dompurify@3.4.11:
optionalDependencies:
'@types/trusted-types': 2.0.7
@@ -8597,23 +8417,23 @@ snapshots:
dependencies:
jake: 10.9.4
electron-builder-squirrel-windows@26.15.0(dmg-builder@26.15.0):
electron-builder-squirrel-windows@26.15.5(dmg-builder@26.15.5):
dependencies:
app-builder-lib: 26.15.0(dmg-builder@26.15.0)(electron-builder-squirrel-windows@26.15.0)
builder-util: 26.15.0
app-builder-lib: 26.15.5(dmg-builder@26.15.5)(electron-builder-squirrel-windows@26.15.5)
builder-util: 26.15.3
electron-winstaller: 5.4.0
transitivePeerDependencies:
- dmg-builder
- supports-color
electron-builder@26.15.0(electron-builder-squirrel-windows@26.15.0):
electron-builder@26.15.5(electron-builder-squirrel-windows@26.15.5):
dependencies:
app-builder-lib: 26.15.0(dmg-builder@26.15.0)(electron-builder-squirrel-windows@26.15.0)
builder-util: 26.15.0
app-builder-lib: 26.15.5(dmg-builder@26.15.5)(electron-builder-squirrel-windows@26.15.5)
builder-util: 26.15.3
builder-util-runtime: 9.7.0
chalk: 4.1.2
ci-info: 4.4.0
dmg-builder: 26.15.0(electron-builder-squirrel-windows@26.15.0)
dmg-builder: 26.15.5(electron-builder-squirrel-windows@26.15.5)
fs-extra: 10.1.0
lazy-val: 1.0.5
simple-update-notifier: 2.0.0
@@ -8648,11 +8468,11 @@ snapshots:
electron-log@5.4.4: {}
electron-publish@26.15.0:
electron-publish@26.15.3:
dependencies:
'@types/fs-extra': 9.0.13
aws4: 1.13.2
builder-util: 26.15.0
builder-util: 26.15.3
builder-util-runtime: 9.7.0
chalk: 4.1.2
form-data: 4.0.5
@@ -8723,11 +8543,6 @@ snapshots:
iconv-lite: 0.6.3
whatwg-encoding: 3.1.1
encoding@0.1.13:
dependencies:
iconv-lite: 0.6.3
optional: true
end-of-stream@1.4.5:
dependencies:
once: 1.4.0
@@ -9245,10 +9060,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
fs-minipass@3.0.3:
dependencies:
minipass: 7.1.3
fs-mkdirp-stream@2.0.1:
dependencies:
graceful-fs: 4.2.11
@@ -9609,8 +9420,6 @@ snapshots:
hasown: 2.0.4
side-channel: 1.1.0
ip-address@10.2.0: {}
is-arguments@1.2.0:
dependencies:
call-bound: 1.0.4
@@ -9682,8 +9491,6 @@ snapshots:
dependencies:
is-extglob: 2.1.1
is-interactive@1.0.0: {}
is-map@2.0.3: {}
is-module@1.0.0: {}
@@ -9739,8 +9546,6 @@ snapshots:
dependencies:
which-typed-array: 1.1.22
is-unicode-supported@0.1.0: {}
is-valid-glob@1.0.0: {}
is-weakmap@2.0.2: {}
@@ -9766,6 +9571,8 @@ snapshots:
isexe@3.1.5: {}
isexe@4.0.0: {}
iterator.prototype@1.1.5:
dependencies:
define-data-property: 1.1.4
@@ -9924,11 +9731,6 @@ snapshots:
lodash@4.18.1: {}
log-symbols@4.1.0:
dependencies:
chalk: 4.1.2
is-unicode-supported: 0.1.0
long@4.0.0: {}
loose-envify@1.4.0:
@@ -9961,22 +9763,6 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
make-fetch-happen@14.0.3:
dependencies:
'@npmcli/agent': 3.0.0
cacache: 19.0.1
http-cache-semantics: 4.2.0
minipass: 7.1.3
minipass-fetch: 4.0.1
minipass-flush: 1.0.7
minipass-pipeline: 1.2.4
negotiator: 1.0.0
proc-log: 5.0.0
promise-retry: 2.0.1
ssri: 12.0.0
transitivePeerDependencies:
- supports-color
map-stream@0.1.0: {}
matcher-collection@2.0.1:
@@ -10046,34 +9832,6 @@ snapshots:
minimist@1.2.8: {}
minipass-collect@2.0.1:
dependencies:
minipass: 7.1.3
minipass-fetch@4.0.1:
dependencies:
minipass: 7.1.3
minipass-sized: 1.0.3
minizlib: 3.1.0
optionalDependencies:
encoding: 0.1.13
minipass-flush@1.0.7:
dependencies:
minipass: 3.3.6
minipass-pipeline@1.2.4:
dependencies:
minipass: 3.3.6
minipass-sized@1.0.3:
dependencies:
minipass: 3.3.6
minipass@3.3.6:
dependencies:
yallist: 4.0.0
minipass@7.1.3: {}
minizlib@3.1.0:
@@ -10119,8 +9877,6 @@ snapshots:
natural-orderby@5.0.0: {}
negotiator@1.0.0: {}
node-abi@4.31.0:
dependencies:
semver: 7.8.2
@@ -10136,20 +9892,18 @@ snapshots:
object.entries: 1.1.9
semver: 6.3.1
node-gyp@11.5.0:
node-gyp@12.4.0:
dependencies:
env-paths: 2.2.1
exponential-backoff: 3.1.3
graceful-fs: 4.2.11
make-fetch-happen: 14.0.3
nopt: 8.1.0
proc-log: 5.0.0
nopt: 9.0.0
proc-log: 6.1.0
semver: 7.8.2
tar: 7.5.16
tinyglobby: 0.2.17
which: 5.0.0
transitivePeerDependencies:
- supports-color
undici: 6.27.0
which: 6.0.1
node-int64@0.4.0: {}
@@ -10157,9 +9911,9 @@ snapshots:
node-releases@2.0.47: {}
nopt@8.1.0:
nopt@9.0.0:
dependencies:
abbrev: 3.0.1
abbrev: 4.0.0
normalize-path@3.0.0: {}
@@ -10231,18 +9985,6 @@ snapshots:
type-check: 0.4.0
word-wrap: 1.2.5
ora@5.4.1:
dependencies:
bl: 4.1.0
chalk: 4.1.2
cli-cursor: 3.1.0
cli-spinners: 2.9.2
is-interactive: 1.0.0
is-unicode-supported: 0.1.0
log-symbols: 4.1.0
strip-ansi: 6.0.1
wcwidth: 1.0.1
overlayscrollbars-react@0.5.6(overlayscrollbars@2.16.0)(react@19.2.7):
dependencies:
overlayscrollbars: 2.16.0
@@ -10274,8 +10016,6 @@ snapshots:
dependencies:
p-limit: 3.1.0
p-map@7.0.4: {}
p-try@2.2.0: {}
package-json-from-dist@1.0.1: {}
@@ -10439,7 +10179,7 @@ snapshots:
pretty-bytes@6.1.1: {}
proc-log@5.0.0: {}
proc-log@6.1.0: {}
process-nextick-args@2.0.1: {}
@@ -10725,11 +10465,6 @@ snapshots:
dependencies:
lowercase-keys: 2.0.0
restore-cursor@3.1.0:
dependencies:
onetime: 5.1.2
signal-exit: 3.0.7
retry@0.12.0: {}
reusify@1.1.0: {}
@@ -10936,23 +10671,8 @@ snapshots:
astral-regex: 2.0.0
is-fullwidth-code-point: 3.0.0
smart-buffer@4.2.0: {}
smob@1.6.2: {}
socks-proxy-agent@8.0.5:
dependencies:
agent-base: 7.1.4
debug: 4.4.3
socks: 2.8.9
transitivePeerDependencies:
- supports-color
socks@2.8.9:
dependencies:
ip-address: 10.2.0
smart-buffer: 4.2.0
sort-keys@5.1.0:
dependencies:
is-plain-obj: 4.1.0
@@ -10990,10 +10710,6 @@ snapshots:
sprintf-js@1.1.3: {}
ssri@12.0.0:
dependencies:
minipass: 7.1.3
stat-mode@1.0.0: {}
stop-iteration-iterator@1.1.0:
@@ -11422,6 +11138,8 @@ snapshots:
undici@6.24.1: {}
undici@6.27.0: {}
undici@7.27.2: {}
unicode-canonical-property-names-ecmascript@2.0.1: {}
@@ -11435,14 +11153,6 @@ snapshots:
unicode-property-aliases-ecmascript@2.2.0: {}
unique-filename@4.0.0:
dependencies:
unique-slug: 5.0.0
unique-slug@5.0.0:
dependencies:
imurmurhash: 0.1.4
unique-string@2.0.0:
dependencies:
crypto-random-string: 2.0.0
@@ -11606,10 +11316,6 @@ snapshots:
wavesurfer.js@7.12.7: {}
wcwidth@1.0.1:
dependencies:
defaults: 1.0.4
webcrypto-core@1.9.2:
dependencies:
'@peculiar/asn1-schema': 2.7.0
@@ -11685,6 +11391,10 @@ snapshots:
dependencies:
isexe: 3.1.5
which@6.0.1:
dependencies:
isexe: 4.0.0
word-wrap@1.2.5: {}
workbox-background-sync@7.3.0:
+40 -2
View File
@@ -973,7 +973,43 @@
"autoDJ_albumStrategy": "Mode de selecció d'àlbum",
"autoDJ_songStrategy": "Mode de selecció de cançó",
"autoDJ_strategy_option_library_random": "A l'atzar",
"autoDJ_strategy_option_similar": "Similar"
"autoDJ_strategy_option_similar": "Similar",
"enableFurigana_description": "Mostra guies de pronunciació (furigana) per les lletres en japonès.",
"enableFurigana": "Activa la generació de furigana",
"equalizer_descriptionMpv": "Equalitzador paramètric amb FFmpeg lavfi (MPV)",
"equalizer_descriptionWebAudio": "Equalitzador paramètric amb l'API de Web Audio",
"equalizer": "Equalitzador",
"equalizerBands_description": "Guany per banda. Arrossegueu-lo amunt o avall, o introduïu-hi un valor. Rang: -12 a +12 dB.",
"equalizerBands": "Bandes",
"equalizerPreamp_description": "Guany d'entrada previ a les bandes de l'equalitzador. Poseu-lo en negatiu quan realceu les bandes per evitar el clipping (MPV).",
"equalizerPreamp": "Preamplificador",
"equalizerPreset_description": "Aplica una corba d'equalitzador personalitzada integrada o desada",
"equalizerPreset": "Preajustament",
"equalizerPresetDeletePlaceholder": "Elimina la personalització...",
"equalizerPresetGroupBuiltIn": "Integrat",
"equalizerPresetGroupCustom": "Personalitzat",
"equalizerPresetNamePlaceholder": "Nom de l'ajust predefinit...",
"equalizerPresetSelectPlaceholder": "Seleccioneu un ajust predefinit",
"equalizerSavePreset_description": "Desa la configuració actual de l'equalitzador com a ajust predefinit amb nom",
"equalizerSavePreset": "Desa l'ajust",
"compressor_descriptionMpv": "Compressor de rang dinàmic amb el compressor de FFmpeg (MPV)",
"compressor_descriptionWebAudio": "Compressor de rang dinàmic amb l'API de Web Audio",
"compressor": "Compressor",
"compressorAttack_description": "La rapidesa amb què el compressor s'activa quan el senyal excedeix el llindar.",
"compressorAttack": "Atac",
"compressorKnee_description": "Amplada de la zona de resposta suau. Com més alt sigui, més gradual serà la transició cap a la compressió.",
"compressorKnee": "Zona de resposta",
"compressorMakeupGain_description": "Guany de sortida aplicat després de la compressió per recuperar volum.",
"compressorMakeupGain": "Guany de compensació",
"compressorPreset_description": "Aplica una configuració personalitzada del compressor integrada o desada",
"compressorRatio_description": "Ràtio de compressio, p. ex. 4 = 4:1.",
"compressorRatio": "Ràtio",
"compressorRelease_description": "Com de ràpid el compressor es desactiva un cop el senyal sigui inferior al llindar.",
"compressorRelease": "Desactivació",
"compressorReset_description": "Restaura tots els paràmetres del compressor als seus valors per defecte",
"compressorSavePreset_description": "Desa la configuració actual del compressor com un ajust predefinit amb nom",
"compressorThreshold_description": "Nivell de senyal a partir del qual comença la compressió.",
"compressorThreshold": "Llindar"
},
"table": {
"column": {
@@ -1266,7 +1302,9 @@
"notContains": "No conté",
"notInPlaylist": "No és a",
"notInTheLast": "No és a l'últim",
"startsWith": "Comença amb"
"startsWith": "Comença amb",
"isMissing": "Falta",
"isPresent": "Està present"
},
"queryBuilder": {
"standardTags": "Etiquetes estàndard",
+39 -1
View File
@@ -457,7 +457,45 @@
"autoDJ_albumStrategy": "Režim výběru alb",
"autoDJ_songStrategy": "Režim výběru skladeb",
"autoDJ_strategy_option_library_random": "Náhodně",
"autoDJ_strategy_option_similar": "Podobné"
"autoDJ_strategy_option_similar": "Podobné",
"enableFurigana_description": "Zobrazit návody na výslovnost (furigana) u japonských kandži textů.",
"enableFurigana": "Povolit generování furigana",
"equalizer_descriptionMpv": "Parametrický ekvalizér skrze FFmpeg lavfi (MPV)",
"equalizer_descriptionWebAudio": "Parametrický ekvalizér skrze Web Audio API",
"equalizer": "Ekvalizér",
"equalizerBands_description": "Zisk na pásmo. Posuňte nahoru/dolů nebo zadejte hodnotu. Rozsah: -12 do +12 dB.",
"equalizerBands": "Pásma",
"equalizerPreamp_description": "Vstupní zisk před pásmy ekvalizéru. Při zvýšení pásem nastavte na negativní hodnotu pro zabránění clippingu (MPV).",
"equalizerPreamp": "Předzesilovač",
"equalizerPreset_description": "Použít vestavěnou nebo uloženou vlastní křivku ekvalizéru",
"equalizerPreset": "Předvolba",
"equalizerPresetDeletePlaceholder": "Odstranit vlastní…",
"equalizerPresetGroupBuiltIn": "Vestavěná",
"equalizerPresetGroupCustom": "Vlastní",
"equalizerPresetNamePlaceholder": "Název předvolby…",
"equalizerPresetSelectPlaceholder": "Vybrat předvolbu",
"equalizerSavePreset_description": "Uložit aktuální nastavení ekvalizéru jako pojmenovanou předvolbu",
"equalizerSavePreset": "Uložit předvolbu",
"compressor_descriptionMpv": "Kompresor dynamického rozsahu skrze FFmpeg acompressor (MPV)",
"compressor_descriptionWebAudio": "Kompresor dynamického rozsahu skrze Web Audio API",
"compressor": "Kompresor",
"compressorAttack_description": "Jak rychle se kompresor spustí, když signál překročí hranici.",
"compressorAttack": "Útok",
"compressorKnee_description": "Měkká šířka. Čím vyšší jsou hodnoty, tím pozvolnější je přechod do komprese.",
"compressorKnee": "Koleno",
"compressorMakeupGain_description": "Výstupní zesílení aplikované po kompresi za účelem obnovení hlasitosti.",
"compressorMakeupGain": "Následný zisk",
"compressorPreset_description": "Použít vestavěné nebo uložené vlastní nastavení kompresoru",
"compressorRatio_description": "Poměr komprese, např. 4 = 4:1.",
"compressorRatio": "Poměr",
"compressorRelease_description": "Jak rychle se kompresor uvolní, když signál spadne pod nastavenou hranici.",
"compressorRelease": "Uvolnění",
"compressorReset_description": "Obnovit všechny parametry kompresoru na jejich výchozí hodnoty",
"compressorSavePreset_description": "Uložit aktuální nastavení kompresoru jako pojmenovanou předvolbu",
"compressorThreshold_description": "Úroveň signálu, nad kterou začne komprese.",
"compressorThreshold": "Hranice",
"enableRomaji_description": "Zobrazit rómadži výslovnost pod japonskými texty.",
"enableRomaji": "Povolit generování rómadži"
},
"action": {
"editPlaylist": "Upravit $t(entity.playlist, {\"count\": 1})",
+36
View File
@@ -845,6 +845,24 @@
"enableAutoTranslation": "Enable auto translation",
"enableFurigana_description": "Display pronunciation guides (furigana) over Japanese kanji lyrics.",
"enableFurigana": "Enable furigana generation",
"enableRomaji_description": "Display a romaji pronunciation line under Japanese lyrics.",
"enableRomaji": "Enable romaji generation",
"equalizer_descriptionMpv": "Parametric equalizer via FFmpeg lavfi (MPV)",
"equalizer_descriptionWebAudio": "Parametric equalizer via Web Audio API",
"equalizer": "Equalizer",
"equalizerBands_description": "Per-band gain. Drag up/down or type a value. Range: -12 to +12 dB.",
"equalizerBands": "Bands",
"equalizerPreamp_description": "Input gain before EQ bands. Set negative when boosting bands to prevent clipping (MPV).",
"equalizerPreamp": "Preamp",
"equalizerPreset_description": "Apply a built-in or saved custom EQ curve",
"equalizerPreset": "Preset",
"equalizerPresetDeletePlaceholder": "Delete custom...",
"equalizerPresetGroupBuiltIn": "Built-in",
"equalizerPresetGroupCustom": "Custom",
"equalizerPresetNamePlaceholder": "Preset name...",
"equalizerPresetSelectPlaceholder": "Select preset",
"equalizerSavePreset_description": "Save the current EQ settings as a named preset",
"equalizerSavePreset": "Save preset",
"enableRemote_description": "Enables the remote control server to allow other devices to control the application",
"enableRemote": "Enable remote control server",
"exitToTray_description": "Exit the application to the system tray",
@@ -1026,6 +1044,24 @@
"showVisualizerInSidebar": "Show visualizer in player sidebar",
"combinedLyricsAndVisualizer_description": "Combine lyrics and visualizer into the same panel",
"combinedLyricsAndVisualizer": "Combine lyrics and visualizer in player sidebar",
"compressor_descriptionMpv": "Dynamic range compressor via FFmpeg acompressor (MPV)",
"compressor_descriptionWebAudio": "Dynamic range compressor via Web Audio API",
"compressor": "Compressor",
"compressorAttack_description": "How quickly the compressor engages after the signal exceeds the threshold.",
"compressorAttack": "Attack",
"compressorKnee_description": "Soft-knee width. Higher values make the transition into compression more gradual.",
"compressorKnee": "Knee",
"compressorMakeupGain_description": "Output gain applied after compression to restore loudness.",
"compressorMakeupGain": "Makeup Gain",
"compressorPreset_description": "Apply a built-in or saved custom compressor setting",
"compressorRatio_description": "Compression ratio, e.g. 4 = 4:1.",
"compressorRatio": "Ratio",
"compressorRelease_description": "How quickly the compressor releases after the signal drops below the threshold.",
"compressorRelease": "Release",
"compressorReset_description": "Restore all compressor parameters to their default values",
"compressorSavePreset_description": "Save the current compressor settings as a named preset",
"compressorThreshold_description": "Signal level above which compression begins.",
"compressorThreshold": "Threshold",
"preservePitch_description": "Preserves pitch when modifying playback speed",
"preservePitch": "Preserve pitch",
"audioFadeOnStatusChange": "Audio fade on status change",
+39 -1
View File
@@ -457,7 +457,45 @@
"autoDJ_songStrategy": "Modo de selección de canción",
"autoDJ_strategy_option_library_random": "Aleatorio",
"autoDJ_strategy_option_similar": "Similar",
"autoDJ_mode_description": "Elegir para añadir canciones o álbumes enteros a la cola"
"autoDJ_mode_description": "Elegir para añadir canciones o álbumes enteros a la cola",
"enableFurigana_description": "Mostrar guías de pronunciación (furigana) sobre letras kanji japonesas.",
"enableFurigana": "Activar generación de furigana",
"equalizer_descriptionMpv": "Ecualizador paramétrico a través de FFmpeg lavfi (MPV)",
"equalizer_descriptionWebAudio": "Ecualizador paramétrico a través de la API de Web Audio",
"equalizer": "Ecualizador",
"equalizerBands": "Bandas",
"equalizerBands_description": "Ganancia por banda. Arrastrar arriba/abajo o escribir un valor. Rango: -12 a +12 dB.",
"equalizerPreamp_description": "Ganancia de entrada antes de las bandas de ecualización. Ajústala en negativo al realzar las bandas para evitar el clipping (MPV).",
"equalizerPreamp": "Preamplificador",
"equalizerPreset_description": "Aplica una curva de ecualizador personalizada integrada o guardada",
"equalizerPreset": "Preajuste",
"equalizerPresetDeletePlaceholder": "Elimianr personalizado...",
"equalizerPresetGroupBuiltIn": "Integrado",
"equalizerPresetGroupCustom": "Personalizado",
"equalizerPresetNamePlaceholder": "Nombre del preajuste...",
"equalizerPresetSelectPlaceholder": "Seleccionar preajuste",
"equalizerSavePreset": "Guardar preajuste",
"equalizerSavePreset_description": "Guarda la configuración del ecualizador actual como un preajuste nombrado",
"compressor_descriptionMpv": "Compresor de rango dinámico a través del compresor de FFmpeg (MPV)",
"compressor_descriptionWebAudio": "Compresor de rango dinámico a través de la API de Web Audio",
"compressor": "Compresor",
"compressorThreshold": "Umbral",
"compressorThreshold_description": "Nivel de señal a partir del cual comienza la compresión.",
"compressorSavePreset_description": "Guarda la configuración actual del compresor como un preajuste nombrado",
"compressorReset_description": "Restaura todos los parámetros del compresor a sus valores predeterminados",
"compressorRelease": "Liberación",
"compressorRelease_description": "La rapidez con la que el compresor se libera una vez que la señal desciende por debajo del umbral.",
"compressorRatio": "Ratio",
"compressorRatio_description": "Ratio de compresión, p. ej. 4 = 4:1.",
"compressorPreset_description": "Aplica una configuración personalizada del compresor integrada o guardada",
"compressorKnee_description": "Ancho de la zona de respuesta suave. Cuanto mayor sea el valor, más gradual será la transición hacia la compresión.",
"compressorKnee": "Zona de respuesta",
"compressorMakeupGain_description": "Ganancia de salida aplicada tras la compresión para recuperar el volumen.",
"compressorMakeupGain": "Ganancia de compensación",
"compressorAttack_description": "La rapidez con la que el compresor entra en acción una vez que la señal supera el umbral.",
"compressorAttack": "Ataque",
"enableRomaji_description": "Muestra una línea de pronunciación en romaji debajo de las letras japonesas.",
"enableRomaji": "Activar generación de romaji"
},
"action": {
"editPlaylist": "Editar $t(entity.playlist, {\"count\": 1})",
+37 -1
View File
@@ -1135,7 +1135,43 @@
"queryBuilderCustomFields_inputLabel": "Nimetus",
"queryBuilderCustomFields_inputTag": "Silt",
"queryBuilderCustomFields": "Kohandatud väljad",
"queryBuilderCustomFields_description": "Lisa kohandatud välju, mida päringukoosturis kasutada"
"queryBuilderCustomFields_description": "Lisa kohandatud välju, mida päringukoosturis kasutada",
"equalizer_descriptionMpv": "Parametriline ekvalaiser FFmpeg lavfi (MPV) kaudu",
"equalizer_descriptionWebAudio": "Parametriline ekvalaiser Web Audio API kaudu",
"equalizer": "Ekvalaiser",
"equalizerBands_description": "Riba põhivõimendus. Lohista üles/alla või sisesta väärtus. Vahemik: -12 kuni +12 dB.",
"equalizerBands": "Ribad",
"equalizerPreamp_description": "Sisendvõimendus enne ekvalaiseri ribasid. Moonutuste vältimiseks määra ribade võimendamisel negatiivne väärtus (MPV).",
"equalizerPreamp": "Eelvõimendus",
"equalizerPreset_description": "Rakenda sisseehitatud või salvestatud kohandatud EQ-häälestus",
"equalizerPreset": "Eelseadistus",
"equalizerPresetDeletePlaceholder": "Kustuta kohandatud...",
"equalizerPresetGroupBuiltIn": "Sisseehitatud",
"equalizerPresetGroupCustom": "Kohandatud",
"equalizerPresetNamePlaceholder": "Eelseadistuse nimi...",
"equalizerPresetSelectPlaceholder": "Vali eelseadistus",
"equalizerSavePreset_description": "Salvesta praegused EQ-seaded nimetatud eelseadistusena",
"equalizerSavePreset": "Salvesta eelseadistus",
"compressor_descriptionMpv": "Dünaamilise vahemiku kompressor FFmpeg acompressori kaudu (MPV)",
"compressor_descriptionWebAudio": "Dünaamilise vahemiku kompressor Web Audio API kaudu",
"enableFurigana_description": "Kuva jaapani kanji-märkide kohal hääldusjuhiseid (furigana).",
"enableFurigana": "Luba furigana kuvamine",
"compressor": "Kompressor",
"compressorAttack_description": "Kui kiiresti kompressor pärast läve ületamist rakendub.",
"compressorAttack": "Rakendumisaeg",
"compressorKnee_description": "Sujuva ülemineku (soft-knee) ulatus. Suuremad väärtused muudavad kompressiooni rakendumise astmelisemaks.",
"compressorKnee": "Üleminek",
"compressorMakeupGain_description": "Väljundvõimendus helitugevuse taastamiseks pärast kompressiooni.",
"compressorMakeupGain": "Väljundvõimendus",
"compressorPreset_description": "Rakenda sisseehitatud või salvestatud kohandatud kompressoriseadistus",
"compressorRatio_description": "Kompressiooniaste, nt 4 = 4:1.",
"compressorRatio": "Kompressiooniaste",
"compressorRelease_description": "Kui kiiresti kompressiooni mõju pärast lävest allapoole langemist lakkab.",
"compressorRelease": "Vabastusaeg",
"compressorReset_description": "Taasta kõigi kompressori parameetrite vaikeväärtused",
"compressorSavePreset_description": "Salvesta praegused kompressori seaded nimetatud eelseadistusena",
"compressorThreshold_description": "Signaali tase, mida ületades kompressioon algab.",
"compressorThreshold": "Lävi"
},
"datetime": {
"minuteShort": "m",
+37 -1
View File
@@ -1108,7 +1108,43 @@
"autoDJ_albumStrategy": "Tryb wyboru albumów",
"autoDJ_songStrategy": "Tryb wyboru piosenek",
"autoDJ_strategy_option_library_random": "Losowo",
"autoDJ_strategy_option_similar": "Podobne"
"autoDJ_strategy_option_similar": "Podobne",
"enableFurigana_description": "Wyświetlaj pomoce wymowy (furigana) nad tekstami Japońskimi kanji.",
"enableFurigana": "Włącz generowanie furigana",
"equalizer_descriptionMpv": "Equalizer parametryczny przez FFmpeg lavfi (MPV)",
"equalizer_descriptionWebAudio": "Parametryczny equalizer przez API Web Audio",
"equalizer": "Equalizer",
"equalizerBands_description": "Wzmocnienie dla poszczególnych pasm. Przesuń w górę/dół lub wpisz wartość. Zakres: -12 do +12 dB.",
"equalizerBands": "Pasma",
"equalizerPreamp_description": "Wzmocnienie sygnału przed pasmami EQ. Ustaw na wartość ujemną podczas wzmacniania pasm, aby zapobiec przesterowaniu (MPV).",
"equalizerPreamp": "Przedwzmacnianie",
"equalizerPreset_description": "Zastosuj wbudowaną lub niestandardową zapisaną krzywą EQ",
"equalizerPreset": "Ustawienia wstępne",
"equalizerPresetDeletePlaceholder": "Usuń niestandardowe...",
"equalizerPresetGroupBuiltIn": "Wbudowane",
"equalizerPresetGroupCustom": "Niestandardowe",
"equalizerPresetNamePlaceholder": "Nazwa ustawień wstępnych...",
"equalizerPresetSelectPlaceholder": "Wybierz ustawienia wstępne",
"equalizerSavePreset_description": "Zapisz aktualne ustawienia EQ jako nazwany zestaw ustawień wstępnych",
"equalizerSavePreset": "Zapisz ustawienia wstępne",
"compressor_descriptionMpv": "Kompresor zakresu dynamicznego przez FFmpeg acompressor (MPV)",
"compressor_descriptionWebAudio": "Kompresor zakresu dynamicznego poprzez API Web Audio",
"compressor": "Kompresor",
"compressorAttack_description": "Jak szybko załączany jest kompresor po przekroczeniu progu przez sygnał.",
"compressorAttack": "Attack",
"compressorKnee_description": "Szerokośc soft-knee. Większe wartości powodują przejście do kompresji bardziej stopniowym.",
"compressorKnee": "Knee",
"compressorMakeupGain_description": "Zwiększenie wyjściowe dodawane po kompresji aby, przywrócić głośność.",
"compressorMakeupGain": "Makeup Gain",
"compressorPreset_description": "Zastosuj wbudowane lub niestandardowe zapisane ustawienie kompresora",
"compressorRatio_description": "Proporcje kompresji, np. 4 = 4:1.",
"compressorRatio": "Proporcje",
"compressorRelease_description": "Jak szybko kompresor odpuszcza po spadnięciu sygnału poniżej progu.",
"compressorRelease": "Odpuszczenie",
"compressorReset_description": "Przywróć wszystkie parametry kompresora do wartości domyślnych",
"compressorSavePreset_description": "Zapisz aktualne ustawienia kompresora jako nazwany zestaw ustawień wstępnych",
"compressorThreshold_description": "Poziom sygnału nad którym rozpoczyna się kompresja.",
"compressorThreshold": "Próg"
},
"table": {
"config": {
+186 -8
View File
@@ -40,7 +40,10 @@
"listenbrainz": "Відкрити у ListenBrainz",
"qobuz": "Відкрити у Qobuz",
"spotify": "Відкрити у Spotify"
}
},
"goToCurrent": "Перейти до поточного елементу",
"collapseAllFolders": "Згорнути всі папки",
"expandAllFolders": "Розгорнути всі папки"
},
"common": {
"countSelected": "Вибрано {{count}}",
@@ -170,7 +173,8 @@
"itemsMore": "{{count}} більше",
"numberOfResults": "{{numberOfResults}} результатів",
"newVersionAvailable": "Доступна нова версія",
"back": "Повернутися"
"back": "Повернутися",
"openFolder": "Відкрити папку"
},
"entity": {
"album_one": "Альбом",
@@ -338,7 +342,9 @@
"notContains": "Не містить",
"notInPlaylist": "Немає в",
"notInTheLast": "Не є в останньому",
"startsWith": "Починається з"
"startsWith": "Починається з",
"isMissing": "Загублений",
"isPresent": "Присутній"
},
"form": {
"addServer": {
@@ -369,7 +375,8 @@
"input_skipDuplicates": "Пропустити дублікати",
"searchOrCreate": "Шукайте $t(entity.playlist, {\"count\": 2}) або пишіть, щоб створити новий",
"success": "Додано $t(entity.trackWithCount, {\"count\": {{message}} }) до $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
"title": "Додати до $t(entity.playlist, {\"count\": 1})"
"title": "Додати до $t(entity.playlist, {\"count\": 1})",
"noneAdded": "Ніяких треків не було додано до $t(entity.playlist, {\"count\": 1}) '{{playlist}}'"
},
"createPlaylist": {
"input_description": "$t(common.description)",
@@ -437,7 +444,12 @@
"input_played": "Відтворити фільтр",
"input_played_optionAll": "Всі треки",
"input_played_optionUnplayed": "Тільки не відтворені треки",
"input_played_optionPlayed": "Тільки відтворені треки"
"input_played_optionPlayed": "Тільки відтворені треки",
"input_kind_albums": "Альбоми",
"input_kind_songs": "Треки",
"input_kind": "Випадкові вибори",
"input_limit_albums": "Скільки альбомів?",
"input_limit_songs": "Скільки треків?"
},
"updateServer": {
"success": "Сервер успішно оновлено",
@@ -453,7 +465,57 @@
}
},
"player": {
"skip": "Пропустити"
"skip": "Пропустити",
"repeat": "Повторювати",
"repeat_all": "Повторювати всі",
"repeat_off": "Повторювання вимкнено",
"restoreQueueFromServer": "Відновити чергу з серверу",
"saveQueueToServer": "Зберегти чергу до серверу",
"shuffle": "Грати (перемішано)",
"shuffle_off": "Перемішування вимкнено",
"addLast": "Останній",
"addNext": "Наступним",
"addLastShuffled": "Останнім (перемішано)",
"addNextShuffled": "Наступним (перемішано)",
"albumRadio": "Радіо альбому",
"artistRadio": "Радіо артиста",
"holdToShuffle": "Утримуйте щоб перемішати",
"favorite": "Додати до вибраних",
"lyrics": "Тексти пісень",
"mute": "Вимкнути звук",
"muted": "Звук вимкнено",
"next": "Наступний",
"play": "Грати",
"playbackFetchCancel": "Будь ласка, трошки почекайте... закрийте повідомлення, щоб скасувати",
"playbackFetchInProgress": "Завантаження треків…",
"playbackFetchNoResults": "Треків не знайдено",
"playbackSpeed": "Швидкість відтворення",
"playRandom": "Грати випадково",
"playSimilarSongs": "Грати схожі треки",
"previous": "Попередній",
"queue_clear": "Очистити чергу",
"queue_moveToBottom": "Пересунути виділені вниз",
"queue_moveToTop": "Пересунути виділені угору",
"queue_remove": "Видалити виділені",
"skip_back": "Перемотати назад",
"skip_forward": "Перемотати вперед",
"stop": "Зупинити",
"toggleFullscreenPlayer": "Перемкнути повноекранний плеєр",
"trackRadio": "Радіо треку",
"unfavorite": "Прибрати з вибраних",
"pause": "Пауза",
"viewQueue": "Переглянути чергу",
"sleepTimer": "Таймер сну",
"sleepTimer_endOfSong": "Кінець поточного треку",
"sleepTimer_endOfAlbum": "Кінець поточного альбому",
"sleepTimer_minutes": "{{count}} хв",
"sleepTimer_hours": "{{count}} г",
"sleepTimer_custom": "Користувацький",
"sleepTimer_off": "Вимкнено",
"sleepTimer_timeRemaining": "{{time}} залишилось",
"sleepTimer_setCustom": "Встановити таймер",
"sleepTimer_cancel": "Скасувати таймер",
"scrobbleForceSubmit": "Змусити скробблинути"
},
"page": {
"albumArtistDetail": {
@@ -570,7 +632,9 @@
"showLyricProvider": "Показувати джерело тексту пісень",
"synchronized": "Синхронізовано",
"unsynchronized": "Несинхронізовано",
"useImageAspectRatio": "Використовувати співвідношення сторін зображення"
"useImageAspectRatio": "Використовувати співвідношення сторін зображення",
"lyricOpacityNonActive": "Непрозорість неактивних слів",
"lyricScaleNonActive": "Масштаб неактивних слів"
},
"lyrics": "Текст пісні",
"related": "Пов'язані",
@@ -599,7 +663,121 @@
"genres": "$t(entity.genre, {\"count\": 2})",
"mostPlayed": "Найбільш відтворені",
"newlyAdded": "Нещодавно додані релізи",
"recentlyPlayed": "Нещодавно відтворені"
"recentlyPlayed": "Нещодавно відтворені",
"recentlyReleased": "Нещодавно випущені",
"title": "$t(common.home)"
},
"itemDetail": {
"copyPath": "Скопіювати шлях до буфера обміну",
"copiedPath": "Шлях успішно скопійовано",
"openFile": "Показати трек у файловому менеджері"
},
"setting": {
"sidebar": "Бічна панель",
"remote": "Віддалений",
"exportImport": "Імпортувати/експортувати",
"scrobble": "Скробблінг",
"audio": "Аудіо",
"lyrics": "Тексти пісень",
"lyricsDisplay": "Відображення текстів пісень",
"transcoding": "Транскодування",
"discord": "Діскорд",
"logger": "Логгер",
"playerFilters": "Фільтри плеєра",
"advanced": "Розширені",
"analytics": "Аналітика",
"generalTab": "Загальні",
"hotkeysTab": "Гарячі клавіші",
"playbackTab": "Відтворення",
"windowTab": "Вікно",
"updates": "Оновлення",
"cache": "Кеш",
"application": "Застосунок",
"queryBuilder": "Конструктор черги",
"theme": "Тема",
"controls": "Керування"
},
"sidebar": {
"albumArtists": "$t(entity.albumArtist, {\"count\": 2})",
"albums": "$t(entity.album, {\"count\": 2})",
"collections": "Колекції",
"artists": "$t(entity.artist, {\"count\": 2})",
"favorites": "$t(entity.favorite, {\"count\": 2})",
"folders": "$t(entity.folder, {\"count\": 2})",
"genres": "$t(entity.genre, {\"count\": 2})",
"home": "$t(common.home)",
"radio": "$t(entity.radioStation, {\"count\": 2})",
"myLibrary": "Моя бібліотека",
"nowPlaying": "Зараз грає",
"playlists": "$t(entity.playlist, {\"count\": 2})",
"search": "$t(common.search)",
"settings": "$t(common.setting, {\"count\": 2})",
"shared": "Поширено $t(entity.playlist, {\"count\": 2})",
"tracks": "$t(entity.track, {\"count\": 2})"
},
"trackList": {
"artistTracks": "Треки {{artist}}",
"genreTracks": "\"{{genre}}\" $t(entity.track, {\"count\": 2})",
"title": "$t(entity.track, {\"count\": 2})"
},
"playlistList": {
"title": "$t(entity.playlist, {\"count\": 2})"
},
"collections": {
"overrideExisting": "Перевизначити існуючі",
"saveAsCollection": "Зберегти як колекцію"
}
},
"queryBuilder": {
"standardTags": "Стандартні теги",
"customTags": "Користувацькі теги"
},
"releaseType": {
"primary": {
"album": "$t(entity.album, {\"count\": 1})",
"broadcast": "Транслювати",
"ep": "Міні-альбом",
"other": "Інші",
"single": "Сінгл"
},
"secondary": {
"audiobook": "Аудіокнига",
"audioDrama": "Радіоп'єса",
"compilation": "Збірка",
"djMix": "DJ мікс",
"demo": "Демо",
"fieldRecording": "Запис поза студією",
"interview": "Інтерв'ю",
"live": "Наживо",
"mixtape": "Мікстейп",
"remix": "Ремікс",
"soundtrack": "Саундтрек",
"spokenWord": "Усне слово"
}
},
"setting": {
"autoDJ": "Авто DJ",
"autoDJ_itemCount": "Кількість елементів",
"autoDJ_itemCount_description": "Кількість елементів, які будуть додані до черги",
"autoDJ_timing": "Таймінг",
"autoDJ_timing_description": "Кількість треків залишившихся в черзі перед тим, як авто DJ запрацює",
"autoDJ_mode": "Режим",
"autoDJ_mode_albums": "Альбоми",
"autoDJ_mode_description": "Оберіть, додавати треки чи цілі альбоми до черги",
"autoDJ_mode_songs": "Треки",
"autoDJ_enabled": "Увімкнути Авто DJ",
"autoDJ_albumStrategy": "Режим вибора альбомів",
"autoDJ_songStrategy": "Режим вибора треків",
"autoDJ_strategy_option_library_random": "Випадково",
"autoDJ_strategy_option_similar": "Схожі",
"autosave": "Автоматично зберігати чергу відтворення",
"autosave_description": "Увімкнути автоматичне збереження черги відтворення до вашого серверу. Це можливо тільки коли використовується Navidrome/Subsonic.Також, ви не можете мати міксовану чергу відтворення.",
"autosaveCount": "Частота автоматичного збереження черги відтворення",
"autosaveCount_description": "Кількість зміни трека перед збереженням черги. 1 (мінімум) означає змінення кожного трека",
"accentColor_description": "Встановлює акцентний колір для застосунка",
"accentColor": "Акцентний колір",
"useThemeAccentColor": "Використовувати акцентний колір теми",
"useThemeAccentColor_description": "Використовувати основний колір визначений у обраній темі замість користувацького акцентного коліру",
"useThemePrimaryShade": "Використовувати основний відтінок теми"
}
}
+39 -1
View File
@@ -828,7 +828,45 @@
"autoDJ_albumStrategy": "專輯選擇模式",
"autoDJ_songStrategy": "歌曲選擇模式",
"autoDJ_strategy_option_library_random": "隨機",
"autoDJ_strategy_option_similar": "相似"
"autoDJ_strategy_option_similar": "相似",
"enableFurigana_description": "在日文歌詞漢字上方顯示發音標記(振假名)。",
"enableFurigana": "啟用振假名顯示",
"equalizer_descriptionMpv": "透過 FFmpeg lavfi (MPV) 使用參數等化器",
"equalizer_descriptionWebAudio": "透過 Web Audio API 使用參數等化器",
"equalizer": "等化器",
"equalizerBands_description": "各頻段增益。可上下拖曳或輸入數值。範圍:-12 至 +12 dB。",
"equalizerBands": "頻段",
"equalizerPreamp_description": "EQ 頻段之前的輸入增益。提升頻段時可設為負值以避免削波 (MPV)。",
"equalizerPreamp": "前級增益",
"equalizerPreset_description": "套用內建或已儲存的自訂 EQ 曲線",
"equalizerPreset": "預設",
"equalizerPresetDeletePlaceholder": "刪除自訂…",
"equalizerPresetGroupBuiltIn": "內建",
"equalizerPresetGroupCustom": "自訂",
"equalizerPresetNamePlaceholder": "預設名稱…",
"equalizerPresetSelectPlaceholder": "選擇預設",
"equalizerSavePreset_description": "將目前 EQ 設定儲存為具名預設",
"equalizerSavePreset": "儲存預設",
"compressor_descriptionMpv": "透過 FFmpeg acompressor (MPV) 使用動態範圍壓縮器",
"compressor_descriptionWebAudio": "透過 Web Audio API 使用動態範圍壓縮器",
"compressor": "壓縮器",
"compressorAttack_description": "訊號超過閾值後,壓縮器開始作用的速度。",
"compressorAttack": "啟動時間",
"compressorKnee_description": "柔性拐點寬度。數值越高,進入壓縮的過渡越平滑。",
"compressorKnee": "拐點",
"compressorMakeupGain_description": "壓縮後套用的輸出增益,用於恢復音量。",
"compressorMakeupGain": "補償增益",
"compressorPreset_description": "套用內建或已儲存的自訂壓縮器設定",
"compressorRatio_description": "壓縮比例,例如 4 表示 4:1。",
"compressorRatio": "比例",
"compressorRelease_description": "訊號低於閾值後,壓縮器解除作用的速度。",
"compressorRelease": "釋放時間",
"compressorReset_description": "將所有壓縮器參數恢復為預設值",
"compressorSavePreset_description": "將目前壓縮器設定儲存為具名預設",
"compressorThreshold_description": "開始進行壓縮的訊號電平。",
"compressorThreshold": "閥值",
"enableRomaji_description": "在日文歌詞下方顯示羅馬拼音。",
"enableRomaji": "啟用羅馬拼音顯示"
},
"table": {
"config": {
+18 -1
View File
@@ -7,16 +7,19 @@ let kuroshiroInstance: any = null;
let initPromise: null | Promise<void> = null;
const getKuroshiro = async () => {
if (kuroshiroInstance) return kuroshiroInstance;
if (initPromise) {
await initPromise;
return kuroshiroInstance;
}
if (kuroshiroInstance) return kuroshiroInstance;
const KuroshiroClass = (Kuroshiro as any).default || Kuroshiro;
kuroshiroInstance = new KuroshiroClass();
initPromise = kuroshiroInstance.init(new KuromojiAnalyzer());
await initPromise;
initPromise = null;
return kuroshiroInstance;
};
@@ -35,3 +38,17 @@ export const convertFurigana = async (text: string): Promise<string> => {
return text;
}
};
export const convertRomaji = async (text: string): Promise<string> => {
const KuroshiroClass = (Kuroshiro as any).default || Kuroshiro;
if (!KuroshiroClass.Util.hasKana(text)) return text;
try {
const kuroshiro = await getKuroshiro();
return await kuroshiro.convert(text, { mode: 'spaced', to: 'romaji' });
} catch (e) {
console.error('Romaji conversion error: ', e);
return text;
}
};
+5 -1
View File
@@ -1,7 +1,7 @@
import { ipcMain } from 'electron';
import { store } from '../settings';
import { convertFurigana } from './furigana';
import { convertFurigana, convertRomaji } from './furigana';
import { getLyricsBySongId as getGenius, getSearchResults as searchGenius } from './genius';
import { getLyricsBySongId as getLrcLib, getSearchResults as searchLrcLib } from './lrclib';
import { getLyricsBySongId as getNetease, getSearchResults as searchNetease } from './netease';
@@ -236,3 +236,7 @@ ipcMain.handle('lyric-by-remote-id', async (_event, params: LyricGetQuery) => {
ipcMain.handle('lyric-convert-furigana', async (_event, text: string) => {
return await convertFurigana(text);
});
ipcMain.handle('lyric-convert-romaji', async (_event, text: string) => {
return await convertRomaji(text);
});
+57 -10
View File
@@ -1,5 +1,5 @@
import console from 'console';
import { app, ipcMain } from 'electron';
import { app, ipcMain, powerMonitor } from 'electron';
import { access, rm } from 'fs/promises';
import uniq from 'lodash/uniq';
import MpvAPI from 'node-mpv';
@@ -85,6 +85,19 @@ const DEFAULT_MPV_PARAMETERS = (extraParameters?: string[]) => {
parameters.push('--prefetch-playlist=yes');
}
// Without these, mpv/ffmpeg will block indefinitely on a dead TCP connection
// instead of failing or reconnecting. This commonly happens when the OS network
// adapter resets after the system wakes from sleep while a stream is open.
if (!extraParameters?.some((param) => param.startsWith('--network-timeout'))) {
parameters.push('--network-timeout=10');
}
if (!extraParameters?.some((param) => param.startsWith('--stream-lavf-o'))) {
parameters.push(
'--stream-lavf-o=reconnect=1,reconnect_streamed=1,reconnect_at_eof=1,reconnect_delay_max=5',
);
}
return parameters;
};
@@ -191,21 +204,44 @@ export const getMpvInstance = () => {
return mpvInstance;
};
const QUIT_TIMEOUT_MS = 3000;
const killMpvProcess = (mpv: MpvAPI) => {
const mpvProcess = (mpv as any).process || (mpv as any).mpvProcess;
if (mpvProcess && typeof mpvProcess.kill === 'function') {
try {
mpvProcess.kill('SIGTERM');
} catch (killErr) {
mpvLog({ action: 'Failed to kill mpv process' }, killErr as NodeMpvError);
}
}
};
const quit = async (instance?: MpvAPI | null) => {
const mpv = instance || getMpvInstance();
if (mpv) {
try {
await mpv.quit();
// mpv.quit() resolves only when mpv replies over IPC. If mpv's command queue
// is wedged (e.g. blocked on a dead network stream after the system resumes
// from sleep), that reply never arrives, so this must not be allowed to hang
// forever - fall back to killing the process directly.
let timedOut = false;
await Promise.race([
mpv.quit(),
new Promise((resolve) => {
setTimeout(() => {
timedOut = true;
resolve(undefined);
}, QUIT_TIMEOUT_MS);
}),
]);
if (timedOut) {
killMpvProcess(mpv);
}
} catch {
// If quit() fails, try to kill the process directly
const mpvProcess = (mpv as any).process || (mpv as any).mpvProcess;
if (mpvProcess && typeof mpvProcess.kill === 'function') {
try {
mpvProcess.kill('SIGTERM');
} catch (killErr) {
mpvLog({ action: 'Failed to kill mpv process' }, killErr as NodeMpvError);
}
}
killMpvProcess(mpv);
}
if (!isWindows()) {
try {
@@ -666,6 +702,17 @@ const cleanupMpv = async (force = false) => {
}
};
// When the OS resumes from sleep, any network stream mpv had open is likely dead
// (the connection silently dropped while the network adapter was suspended). Tell
// the renderer to reload mpv so it reconnects with a fresh stream instead of staying
// stuck on the old, now-dead connection until the app is manually restarted.
powerMonitor.on('resume', () => {
if (getMpvInstance()) {
mpvLog({ action: 'System resumed from sleep, reloading mpv' });
getMainWindow()?.webContents.send('renderer-mpv-reconnect');
}
});
app.on('before-quit', async (event) => {
switch (mpvState) {
case MpvState.DONE:
+5
View File
@@ -30,8 +30,13 @@ const convertFurigana = (text: string): Promise<string> => {
return ipcRenderer.invoke('lyric-convert-furigana', text);
};
const convertRomaji = (text: string): Promise<string> => {
return ipcRenderer.invoke('lyric-convert-romaji', text);
};
export const lyrics = {
convertFurigana,
convertRomaji,
getRemoteLyricsByRemoteId,
getRemoteLyricsBySong,
searchRemoteLyrics,
+5
View File
@@ -174,6 +174,10 @@ const rendererPlayerFallback = (cb: (data: boolean) => void) => {
ipcRenderer.on('renderer-player-fallback', (_, data) => cb(data));
};
const rendererMpvReconnect = (cb: () => void) => {
ipcRenderer.on('renderer-mpv-reconnect', () => cb());
};
export const mpvPlayer = {
autoNext,
cleanup,
@@ -205,6 +209,7 @@ export const mpvPlayerListener = {
rendererAutoNext,
rendererCurrentTime,
rendererError,
rendererMpvReconnect,
rendererNext,
rendererPause,
rendererPlay,
@@ -64,6 +64,7 @@ export const useItemDragDropState = <TElement extends HTMLElement = HTMLDivEleme
return draggedItems;
},
itemType,
metadata: { playlistId },
onDragStart: () => {
if (!item || !isDataRow) {
return;
@@ -248,10 +249,15 @@ export const useItemDragDropState = <TElement extends HTMLElement = HTMLDivEleme
case DragTarget.SONG: {
const sourceItems = (args.source.item || []) as Song[];
if (sourceItems.length > 0) {
playerContext.addToQueueByData(sourceItems, {
edge: args.edge,
uniqueId: droppedOnUniqueId,
});
const sourcePlaylistId = args.source.metadata?.playlistId as
| string
| undefined;
playerContext.addToQueueByData(
sourceItems,
{ edge: args.edge, uniqueId: droppedOnUniqueId },
undefined,
sourcePlaylistId ?? null,
);
}
break;
}
@@ -307,6 +307,20 @@ export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) =>
isHidden: !isElectron(),
title: t('setting.enableFurigana'),
},
{
control: (
<Switch
aria-label="Enable romaji"
defaultChecked={lyricsSettings.enableRomaji}
onChange={(e) => updateLyricsSetting({ enableRomaji: e.currentTarget.checked })}
/>
),
description: t('setting.enableRomaji', {
context: 'description',
}),
isHidden: !isElectron(),
title: t('setting.enableRomaji'),
},
{
control: (
<Switch
@@ -28,3 +28,27 @@ export const useFuriganaLyrics = (lyrics: LyricsResponse | null | undefined, ena
staleTime: Infinity,
});
};
export const useRomajiLyrics = (lyrics: LyricsResponse | null | undefined, enabled: boolean) => {
return useQuery({
enabled: enabled && !!lyrics && !!lyricsApi,
queryFn: async () => {
if (!lyrics || !lyricsApi || !enabled) return lyrics;
if (typeof lyrics === 'string') {
return await lyricsApi.convertRomaji(lyrics);
} else if (Array.isArray(lyrics)) {
const text = lyrics.map(([, line]) => line).join('\n');
const converted = await lyricsApi.convertRomaji(text);
const convertedLines = converted.split('\n');
return lyrics.map(([time], i) => [
time,
convertedLines[i] ?? lyrics[i][1],
]) as SynchronizedLyricsArray;
}
return lyrics;
},
queryKey: ['romaji', lyrics],
staleTime: Infinity,
});
};
@@ -25,3 +25,8 @@
.lyric-line:global(.synchronized) {
cursor: pointer;
}
.romaji-line {
font-size: 0.8em;
font-weight: 600;
}
+20 -1
View File
@@ -10,11 +10,21 @@ import { Stack } from '/@/shared/components/stack/stack';
interface LyricLineProps extends ComponentPropsWithoutRef<'div'> {
alignment: 'center' | 'left' | 'right';
fontSize: number;
romajiText?: null | string;
text: string;
translatedText?: null | string;
}
export const LyricLine = memo(
({ alignment, className, fontSize, text, ...props }: LyricLineProps) => {
({
alignment,
className,
fontSize,
romajiText,
text,
translatedText,
...props
}: LyricLineProps) => {
const lines = useMemo(() => text.split('_BREAK_'), [text]);
const style = useMemo(
@@ -31,6 +41,15 @@ export const LyricLine = memo(
{lines.map((line, index) => (
<span dangerouslySetInnerHTML={{ __html: sanitize(line) }} key={index} />
))}
{romajiText && (
<span
className={styles.romajiLine}
dangerouslySetInnerHTML={{ __html: sanitize(romajiText) }}
/>
)}
{translatedText && (
<span dangerouslySetInnerHTML={{ __html: sanitize(translatedText) }} />
)}
</Stack>
</Box>
);
+19 -4
View File
@@ -14,7 +14,10 @@ import {
type LyricsQueryResult,
} from '/@/renderer/features/lyrics/api/lyrics-api';
import { openLyricsExportModal } from '/@/renderer/features/lyrics/components/lyrics-export-form';
import { useFuriganaLyrics } from '/@/renderer/features/lyrics/hooks/use-furigana-lyrics';
import {
useFuriganaLyrics,
useRomajiLyrics,
} from '/@/renderer/features/lyrics/hooks/use-furigana-lyrics';
import { LyricsActions } from '/@/renderer/features/lyrics/lyrics-actions';
import {
SynchronizedLyrics,
@@ -51,6 +54,7 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
const {
enableAutoTranslation,
enableFurigana,
enableRomaji,
preferLocalLyrics,
translationApiKey,
translationApiProvider,
@@ -119,6 +123,7 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
}, [data, indexToUse, preferLocalLyrics]);
const { data: furiganaConvertedLyrics } = useFuriganaLyrics(lyrics?.lyrics, !!enableFurigana);
const { data: romajiConvertedLyrics } = useRomajiLyrics(lyrics?.lyrics, !!enableRomaji);
const displayLyrics = useMemo(() => {
if (isLyricsDisabled || !lyrics) return null;
@@ -293,10 +298,10 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
}, [isLoadingLyrics, hasNoLyrics, fadeOutNoLyricsMessage]);
const handleExportLyrics = useCallback(() => {
if (displayLyrics) {
openLyricsExportModal({ lyrics: displayLyrics, offsetMs: currentOffsetMs, synced });
if (lyrics && !isLyricsDisabled) {
openLyricsExportModal({ lyrics, offsetMs: currentOffsetMs, synced });
}
}, [currentOffsetMs, displayLyrics, synced]);
}, [currentOffsetMs, isLyricsDisabled, lyrics, synced]);
const handleOpenSettings = () => {
openLyricsSettingsModal(settingsKey);
@@ -344,12 +349,22 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
<SynchronizedLyrics
{...(displayLyrics as SynchronizedLyricsProps)}
offsetMs={displayOffsetMs}
romajiLyrics={
enableRomaji
? (romajiConvertedLyrics as SynchronizedLyricsProps['romajiLyrics'])
: null
}
settingsKey={settingsKey}
translatedLyrics={showTranslation ? translatedLyrics : null}
/>
) : (
<UnsynchronizedLyrics
{...(displayLyrics as UnsynchronizedLyricsProps)}
romajiLyrics={
enableRomaji
? (romajiConvertedLyrics as UnsynchronizedLyricsProps['romajiLyrics'])
: null
}
settingsKey={settingsKey}
translatedLyrics={showTranslation ? translatedLyrics : null}
/>
@@ -23,6 +23,7 @@ const mpris = isElectron() && utils?.isLinux() ? window.api.mpris : null;
export interface SynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
lyrics: SynchronizedLyricsArray;
offsetMs?: number;
romajiLyrics?: null | SynchronizedLyricsArray;
settingsKey?: string;
style?: React.CSSProperties;
translatedLyrics?: null | string;
@@ -34,6 +35,7 @@ export const SynchronizedLyrics = ({
name,
offsetMs,
remote,
romajiLyrics,
settingsKey = 'default',
source,
style,
@@ -368,10 +370,9 @@ export const SynchronizedLyrics = ({
handleSeek(time / 1000);
}
}}
text={
text +
(translatedLyrics ? `_BREAK_${translatedLyrics.split('\n')[idx]}` : '')
}
romajiText={romajiLyrics?.[idx]?.[1]}
text={text}
translatedText={translatedLyrics?.split('\n')[idx]}
/>
))}
</div>
@@ -8,6 +8,7 @@ import { FullLyricsMetadata } from '/@/shared/types/domain-types';
export interface UnsynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
lyrics: string;
romajiLyrics?: null | string;
settingsKey?: string;
translatedLyrics?: null | string;
}
@@ -17,6 +18,7 @@ export const UnsynchronizedLyrics = ({
lyrics,
name,
remote,
romajiLyrics,
settingsKey = 'default',
source,
translatedLyrics,
@@ -42,6 +44,10 @@ export const UnsynchronizedLyrics = ({
return translatedLyrics ? translatedLyrics.split('\n') : [];
}, [translatedLyrics]);
const romajiLines = useMemo(() => {
return romajiLyrics ? romajiLyrics.split('\n') : [];
}, [romajiLyrics]);
return (
<div className={styles.container} style={{ gap: `${settings.gapUnsync}px` }}>
{settings.showProvider && source && (
@@ -67,7 +73,9 @@ export const UnsynchronizedLyrics = ({
fontSize={settings.fontSizeUnsync}
id={`lyric-${idx}`}
key={idx}
text={text + (translatedLines[idx] ? `_BREAK_${translatedLines[idx]}` : '')}
romajiText={romajiLines[idx]}
text={text}
translatedText={translatedLines[idx]}
/>
))}
</div>
@@ -68,9 +68,13 @@ export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
};
eventEmitter.on('MPV_RELOAD', handleMpvReload);
// The main process notifies us after the OS resumes from sleep, since the
// stream mpv had open is likely on a now-dead connection.
mpvPlayerListener?.rendererMpvReconnect(handleMpvReload);
return () => {
eventEmitter.off('MPV_RELOAD', handleMpvReload);
ipc?.removeAllListeners('renderer-mpv-reconnect');
};
}, []);
@@ -39,7 +39,12 @@ import {
import { Play, PlayerRepeat, PlayerShuffle } from '/@/shared/types/types';
export interface PlayerContext {
addToQueueByData: (data: Song[], type: AddToQueueType, playSongId?: string) => void;
addToQueueByData: (
data: Song[],
type: AddToQueueType,
playSongId?: string,
contextPlaylistId?: null | string,
) => void;
addToQueueByFetch: (
serverId: string,
id: string[],
@@ -137,6 +142,23 @@ const getRootQueryKey = (itemType: LibraryItem, serverId: string) => {
}
};
const isReplaceQueueType = (type: AddToQueueType): boolean => {
if (typeof type === 'object') return false;
return type === Play.NOW || type === Play.SHUFFLE;
};
// HashRouter puts the route in location.hash, not pathname.
const inferPlaylistContextFromUrl = (): null | string => {
const route = window.location.hash.replace(/^#/, '');
const match = route.match(/^\/playlists\/([^/]+)/);
return match ? match[1] : null;
};
// Stamps each song with the playlist it was queued from, so the sidebar highlight
// can be derived from whichever song is currently playing (see useCurrentPlaylistContextId).
const tagPlaylistContext = (songs: Song[], contextPlaylistId: string): Song[] =>
songs.map((song) => ({ ...song, _contextPlaylistId: contextPlaylistId }));
export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
const { t } = useTranslation();
const queryClient = useQueryClient();
@@ -187,9 +209,20 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
}, [doNotShowAgain, setDoNotShowAgain, t]);
const addToQueueByData = useCallback(
(data: Song[], type: AddToQueueType, playSongId?: string) => {
(
data: Song[],
type: AddToQueueType,
playSongId?: string,
contextPlaylistId?: null | string,
) => {
const filters = useSettingsStore.getState().playback.filters;
const filteredData = filterSongsByPlayerFilters(data, filters);
let filteredData = filterSongsByPlayerFilters(data, filters);
const resolvedContextId =
contextPlaylistId ??
(isReplaceQueueType(type) ? inferPlaylistContextFromUrl() : null);
if (resolvedContextId) {
filteredData = tagPlaylistContext(filteredData, resolvedContextId);
}
if (typeof type === 'object' && 'edge' in type && type.edge !== null) {
const edge = type.edge === 'top' ? 'top' : 'bottom';
@@ -279,7 +312,21 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
}
const filters = useSettingsStore.getState().playback.filters;
const filteredSongs = filterSongsByPlayerFilters(sortedSongs, filters);
let filteredSongs = filterSongsByPlayerFilters(sortedSongs, filters);
// Songs from multiple playlists are merged together, so there is no single
// playlist to attribute them to: skip tagging (and URL inference) entirely.
const isMultiPlaylist = itemType === LibraryItem.PLAYLIST && id.length > 1;
const explicitId =
itemType === LibraryItem.PLAYLIST && id.length === 1 ? id[0] : null;
const resolvedContextId =
explicitId ??
(!isMultiPlaylist && isReplaceQueueType(type)
? inferPlaylistContextFromUrl()
: null);
if (resolvedContextId) {
filteredSongs = tagPlaylistContext(filteredSongs, resolvedContextId);
}
if (typeof type === 'object' && 'edge' in type && type.edge !== null) {
const edge = type.edge === 'top' ? 'top' : 'bottom';
@@ -107,6 +107,20 @@ export const LyricSettings = memo(() => {
isHidden: !isElectron(),
title: t('setting.enableFurigana'),
},
{
control: (
<Switch
aria-label="Enable romaji generation"
defaultChecked={settings.enableRomaji}
onChange={(e) => updateSetting({ enableRomaji: e.currentTarget.checked })}
/>
),
description: t('setting.enableRomaji', {
context: 'description',
}),
isHidden: !isElectron(),
title: t('setting.enableRomaji'),
},
{
control: (
<Switch
@@ -1,6 +1,7 @@
import { useMove } from '@mantine/hooks';
import isElectron from 'is-electron';
import { memo, useCallback, useContext, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
buildMpvAudioFilters,
@@ -257,6 +258,7 @@ function EqBandSlider({
// ─── Main component ───────────────────────────────────────────────────────────
export const EqSettings = memo(() => {
const { t } = useTranslation();
const settings = usePlaybackSettings();
const { setSettings } = useSettingsStoreActions();
@@ -443,13 +445,13 @@ export const EqSettings = memo(() => {
// ── Preset select data ────────────────────────────────────────────────────
const eqPresetSelectData = [
{
group: 'Built-in',
group: t('setting.equalizerPresetGroupBuiltIn'),
items: Object.keys(EQ_PRESETS).map((name) => ({ label: name, value: name })),
},
...(Object.keys(customEqPresets).length > 0
? [
{
group: 'Custom',
group: t('setting.equalizerPresetGroupCustom'),
items: Object.keys(customEqPresets).map((name) => ({
label: name,
value: name,
@@ -461,13 +463,13 @@ export const EqSettings = memo(() => {
const compPresetSelectData = [
{
group: 'Built-in',
group: t('setting.equalizerPresetGroupBuiltIn'),
items: Object.keys(COMP_PRESETS).map((name) => ({ label: name, value: name })),
},
...(Object.keys(customCompPresets).length > 0
? [
{
group: 'Custom',
group: t('setting.equalizerPresetGroupCustom'),
items: Object.keys(customCompPresets).map((name) => ({
label: name,
value: name,
@@ -488,9 +490,9 @@ export const EqSettings = memo(() => {
),
description:
settings.type === PlayerType.LOCAL
? 'Parametric equalizer via FFmpeg lavfi (MPV)'
: 'Parametric equalizer via Web Audio API',
title: 'Equalizer',
? t('setting.equalizer', { context: 'descriptionMpv' })
: t('setting.equalizer', { context: 'descriptionWebAudio' }),
title: t('setting.equalizer'),
},
...(settings.equalizer.enabled
? ([
@@ -505,7 +507,7 @@ export const EqSettings = memo(() => {
const preset = customEqPresets[name] ?? EQ_PRESETS[name];
if (preset) applyEqPreset(preset);
}}
placeholder="Select preset"
placeholder={t('setting.equalizerPresetSelectPlaceholder')}
searchable
value={null}
w={180}
@@ -521,15 +523,15 @@ export const EqSettings = memo(() => {
if (!name) return;
handleDeleteEqPreset(name);
}}
placeholder="Delete custom..."
placeholder={t('setting.equalizerPresetDeletePlaceholder')}
value={null}
w={160}
/>
)}
</Group>
),
description: 'Apply a built-in or saved custom EQ curve',
title: 'Preset',
description: t('setting.equalizerPreset', { context: 'description' }),
title: t('setting.equalizerPreset'),
},
{
control: (
@@ -539,7 +541,7 @@ export const EqSettings = memo(() => {
onKeyDown={(e) => {
if (e.key === 'Enter') handleSaveEqPreset();
}}
placeholder="Preset name..."
placeholder={t('setting.equalizerPresetNamePlaceholder')}
value={saveEqName}
w={180}
/>
@@ -548,12 +550,12 @@ export const EqSettings = memo(() => {
onClick={handleSaveEqPreset}
variant="subtle"
>
Save
{t('common.save')}
</Button>
</Group>
),
description: 'Save the current EQ settings as a named preset',
title: 'Save preset',
description: t('setting.equalizerSavePreset', { context: 'description' }),
title: t('setting.equalizerSavePreset'),
},
{
control: (
@@ -600,13 +602,12 @@ export const EqSettings = memo(() => {
w={70}
/>
<Button onClick={handleResetEq} variant="subtle">
Reset all
{t('common.reset')}
</Button>
</Group>
),
description:
'Input gain before EQ bands. Set negative when boosting bands to prevent clipping (MPV).',
title: 'Preamp',
description: t('setting.equalizerPreamp', { context: 'description' }),
title: t('setting.equalizerPreamp'),
},
{
control: (
@@ -625,9 +626,8 @@ export const EqSettings = memo(() => {
))}
</Group>
),
description:
'Per-band gain. Drag up/down or type a value. Range: -12 to +12 dB.',
title: 'Bands',
description: t('setting.equalizerBands', { context: 'description' }),
title: t('setting.equalizerBands'),
},
] as SettingOption[])
: []),
@@ -644,60 +644,57 @@ export const EqSettings = memo(() => {
unit: string;
}[] = [
{
description: 'Signal level above which compression begins.',
description: t('setting.compressorThreshold', { context: 'description' }),
key: 'threshold',
max: 0,
min: -60,
step: 1,
title: 'Threshold',
title: t('setting.compressorThreshold'),
unit: 'dB',
},
{
description: 'Compression ratio, e.g. 4 = 4:1.',
description: t('setting.compressorRatio', { context: 'description' }),
key: 'ratio',
max: 20,
min: 1,
step: 0.5,
title: 'Ratio',
title: t('setting.compressorRatio'),
unit: ':1',
},
{
description:
'How quickly the compressor engages after the signal exceeds the threshold.',
description: t('setting.compressorAttack', { context: 'description' }),
key: 'attack',
max: 2000,
min: 0.1,
step: 1,
title: 'Attack',
title: t('setting.compressorAttack'),
unit: 'ms',
},
{
description:
'How quickly the compressor releases after the signal drops below the threshold.',
description: t('setting.compressorRelease', { context: 'description' }),
key: 'release',
max: 9000,
min: 1,
step: 10,
title: 'Release',
title: t('setting.compressorRelease'),
unit: 'ms',
},
{
description: 'Output gain applied after compression to restore loudness.',
description: t('setting.compressorMakeupGain', { context: 'description' }),
key: 'makeup',
max: 30,
min: 0,
step: 0.5,
title: 'Makeup Gain',
title: t('setting.compressorMakeupGain'),
unit: 'dB',
},
{
description:
'Soft-knee width. Higher values make the transition into compression more gradual.',
description: t('setting.compressorKnee', { context: 'description' }),
key: 'knee',
max: 10,
min: 1,
step: 0.5,
title: 'Knee',
title: t('setting.compressorKnee'),
unit: 'dB',
},
];
@@ -713,9 +710,9 @@ export const EqSettings = memo(() => {
),
description:
settings.type === PlayerType.LOCAL
? 'Dynamic range compressor via FFmpeg acompressor (MPV)'
: 'Dynamic range compressor via Web Audio API',
title: 'Compressor',
? t('setting.compressor', { context: 'descriptionMpv' })
: t('setting.compressor', { context: 'descriptionWebAudio' }),
title: t('setting.compressor'),
},
...(settings.compressor.enabled
? ([
@@ -730,7 +727,7 @@ export const EqSettings = memo(() => {
const preset = customCompPresets[name] ?? COMP_PRESETS[name];
if (preset) applyCompPreset(preset);
}}
placeholder="Select preset"
placeholder={t('setting.equalizerPresetSelectPlaceholder')}
searchable
value={null}
w={180}
@@ -746,15 +743,15 @@ export const EqSettings = memo(() => {
if (!name) return;
handleDeleteCompPreset(name);
}}
placeholder="Delete custom..."
placeholder={t('setting.equalizerPresetDeletePlaceholder')}
value={null}
w={160}
/>
)}
</Group>
),
description: 'Apply a built-in or saved custom compressor setting',
title: 'Preset',
description: t('setting.compressorPreset', { context: 'description' }),
title: t('setting.equalizerPreset'),
},
{
control: (
@@ -764,7 +761,7 @@ export const EqSettings = memo(() => {
onKeyDown={(e) => {
if (e.key === 'Enter') handleSaveCompPreset();
}}
placeholder="Preset name..."
placeholder={t('setting.equalizerPresetNamePlaceholder')}
value={saveCompName}
w={180}
/>
@@ -773,12 +770,12 @@ export const EqSettings = memo(() => {
onClick={handleSaveCompPreset}
variant="subtle"
>
Save
{t('common.save')}
</Button>
</Group>
),
description: 'Save the current compressor settings as a named preset',
title: 'Save preset',
description: t('setting.compressorSavePreset', { context: 'description' }),
title: t('setting.equalizerSavePreset'),
},
// One SettingOption per compressor parameter — Slider + NumberInput
...compParams.map(({ description, key, max, min, step, title, unit }) => ({
@@ -834,11 +831,11 @@ export const EqSettings = memo(() => {
{
control: (
<Button onClick={handleResetComp} variant="subtle">
Reset to defaults
{t('common.resetToDefault')}
</Button>
),
description: 'Restore all compressor parameters to their default values',
title: 'Reset',
description: t('setting.compressorReset', { context: 'description' }),
title: t('common.reset'),
},
] as SettingOption[])
: []),
@@ -43,8 +43,18 @@ export const ShareItemContextModal = ({
});
const handleSubmit = form.onSubmit(async (values) => {
shareItemMutation.mutate(
{
const canUseClipboard = Boolean(navigator.clipboard) && window.isSecureContext;
// The share URL only exists once the create request resolves. Calling
// navigator.clipboard.writeText() from that async callback runs outside
// the click's user activation, so Firefox/Safari reject it ("Clipboard
// write was blocked due to lack of user activation") and nothing is
// copied. Instead, call clipboard.write() synchronously within this
// gesture with a ClipboardItem whose value is a promise that resolves to
// the URL — this preserves the activation while the share is created.
// Falls back to writeText, then to the "click to open" toast.
const shareUrlPromise = shareItemMutation
.mutateAsync({
apiClientProps: { serverId: server?.id || '' },
body: {
description: values.description,
@@ -53,51 +63,63 @@ export const ShareItemContextModal = ({
resourceIds: itemIds.join(),
resourceType,
},
})
.then((data) => {
if (!server) throw new Error('Server not found');
if (!data?.id) throw new Error('Failed to share item');
const serverUrl = getServerUrl(server, true);
if (!serverUrl) throw new Error('Server URL not found');
return `${serverUrl}/share/${data.id}`;
});
let copied = false;
if (canUseClipboard) {
try {
if (typeof ClipboardItem !== 'undefined') {
await navigator.clipboard.write([
new ClipboardItem({
'text/plain': shareUrlPromise.then(
(url) => new Blob([url], { type: 'text/plain' }),
),
}),
]);
} else {
await navigator.clipboard.writeText(await shareUrlPromise);
}
copied = true;
} catch {
copied = false;
}
}
let shareUrl: string;
try {
shareUrl = await shareUrlPromise;
} catch {
toast.error({
message: t('form.shareItem.createFailed'),
});
closeModal(id);
return;
}
toast.success({
autoClose: copied ? 5000 : 15000,
id: 'share-item-toast',
message: t(copied ? 'form.shareItem.success' : 'form.shareItem.successMustClick', {}),
onClick: (a) => {
if (!(a.target instanceof HTMLElement)) return;
// Make sure we weren't clicking close (otherwise clicking close /also/ opens the url)
if (a.target.nodeName !== 'svg') {
window.open(shareUrl);
toast.hide('share-item-toast');
}
},
{
onError: () => {
toast.error({
message: t('form.shareItem.createFailed'),
});
},
onSuccess: (_data) => {
if (!server) throw new Error('Server not found');
if (!_data?.id) throw new Error('Failed to share item');
const serverUrl = getServerUrl(server, true);
if (!serverUrl) throw new Error('Server URL not found');
const shareUrl = `${serverUrl}/share/${_data.id}`;
const canUseClipboard = navigator.clipboard && window.isSecureContext;
if (canUseClipboard) {
navigator.clipboard.writeText(shareUrl);
}
toast.success({
autoClose: canUseClipboard ? 5000 : 15000,
id: 'share-item-toast',
message: t(
canUseClipboard
? 'form.shareItem.success'
: 'form.shareItem.successMustClick',
{},
),
onClick: (a) => {
if (!(a.target instanceof HTMLElement)) return;
// Make sure we weren't clicking close (otherwise clicking close /also/ opens the url)
if (a.target.nodeName !== 'svg') {
window.open(shareUrl);
toast.hide('share-item-toast');
}
},
});
},
},
);
});
closeModal(id);
return null;
});
return (
@@ -136,6 +136,10 @@
white-space: nowrap;
}
.name-active {
color: var(--theme-colors-primary);
}
.image-container {
flex-shrink: 0;
width: 3rem;
@@ -28,6 +28,7 @@ import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
import { useDragMonitor } from '/@/renderer/hooks/use-drag-monitor';
import { AppRoute } from '/@/renderer/router/routes';
import {
useCurrentPlaylistContextId,
useCurrentServer,
useCurrentServerId,
usePermissions,
@@ -116,6 +117,8 @@ export const PlaylistRowButton = memo(
const sidebarPlaylistSorting = useSidebarPlaylistSorting();
const sidebarPlaylistMode = useSidebarPlaylistMode();
const isCompact = sidebarPlaylistMode === 'compact';
const activePlaylistId = useCurrentPlaylistContextId();
const isActive = activePlaylistId === item.id;
const [isHovered, setIsHovered] = useState(false);
const isSmartPlaylist = Boolean(item.rules);
@@ -292,7 +295,13 @@ export const PlaylistRowButton = memo(
>
{isCompact ? (
<>
<Text className={styles.compactName} fw={500} size="md">
<Text
className={clsx(styles.compactName, {
[styles.nameActive]: isActive,
})}
fw={500}
size="md"
>
{name}
</Text>
{isHovered && (
@@ -307,7 +316,13 @@ export const PlaylistRowButton = memo(
<div className={styles.rowGroup}>
<Image containerClassName={styles.imageContainer} src={imageUrl} />
<div className={styles.metadata}>
<Text className={styles.name} fw={500} size="md">
<Text
className={clsx(styles.name, {
[styles.nameActive]: isActive,
})}
fw={500}
size="md"
>
{name}
</Text>
<div className={styles.metadataGroup}>
+6
View File
@@ -1640,6 +1640,7 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
const excludedPlayerKeys = ['playerNum', 'seekToTimestamp', 'status'];
// If we're not restoring the play queue, we don't need the index property
// (it is meaningless without the queue)
if (!shouldRestorePlayQueue) {
excludedPlayerKeys.push('index');
}
@@ -2076,6 +2077,7 @@ export const updateQueueSong = (songId: string, updatedSong: Song) => {
const uniqueId = song._uniqueId;
state.queue.songs[song._uniqueId] = {
...updatedSong,
_contextPlaylistId: song._contextPlaylistId,
_uniqueId: uniqueId,
};
}
@@ -2083,6 +2085,10 @@ export const updateQueueSong = (songId: string, updatedSong: Song) => {
});
};
export const useCurrentPlaylistContextId = () => {
return usePlayerStoreBase((state) => state.getCurrentSong()?._contextPlaylistId ?? null);
};
export const usePlayerMuted = () => {
return usePlayerStoreBase((state) => state.player.muted);
};
+2
View File
@@ -578,6 +578,7 @@ const LyricsSettingsSchema = z.object({
enableAutoTranslation: z.boolean(),
enableFurigana: z.boolean().optional(),
enableNeteaseTranslation: z.boolean(),
enableRomaji: z.boolean().optional(),
fetch: z.boolean(),
follow: z.boolean(),
preferLocalLyrics: z.boolean(),
@@ -1848,6 +1849,7 @@ const initialState: SettingsState = {
enableAutoTranslation: false,
enableFurigana: false,
enableNeteaseTranslation: false,
enableRomaji: false,
fetch: true,
follow: true,
preferLocalLyrics: true,
+1
View File
@@ -73,6 +73,7 @@ export interface QueueData {
}
export type QueueSong = Song & {
_contextPlaylistId?: null | string;
_uniqueId: string;
};