Compare commits

...

31 Commits

Author SHA1 Message Date
Hosted Weblate 324936e0c8 Translated using Weblate
Currently translated at 100.0% (1208 of 1208 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% (1208 of 1208 strings) (Catalan)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/ca/

Co-authored-by: Ondo <SparkyOndo@proton.me>
Co-authored-by: York <goog10216922@gmail.com>
2026-05-05 05:09:50 +00:00
Hosted Weblate 868ec15b16 Added translation using Weblate (Thai)
Translated using Weblate

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

Translated using Weblate

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

Translated using Weblate

Currently translated at 100.0% (1208 of 1208 strings) (Czech)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/

Co-authored-by: Anucha Hlownonkor <anucha.initdz@gmail.com>
Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Fordas <fordas15@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: skajmer <skajmer@protonmail.com>
2026-05-03 12:06:39 +02:00
Hosted Weblate 775c4e68fa Translated using Weblate
Currently translated at 81.3% (981 of 1206 strings) (Russian)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/ru/

Translated using Weblate

Currently translated at 100.0% (1206 of 1206 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 85.6% (1033 of 1206 strings) (Basque)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/eu/

Co-authored-by: Aitor Astorga <a.astorga.sdv@protonmail.com>
Co-authored-by: York <goog10216922@gmail.com>
Co-authored-by: spoon <monkeyfart907@gmail.com>
2026-05-02 04:34:18 +00:00
jeffvli 34e0c4bd4a prevent first lyric line highlight before timestamp (#1965) 2026-05-01 21:33:40 -07:00
jeffvli 323130a877 add toggle for app-suspension for powersave block (#1992) 2026-05-01 21:24:45 -07:00
jeffvli 3b2aab74ac enforce web player seek by seconds when less than 1 (#1993) 2026-05-01 21:12:20 -07:00
Hosted Weblate bc7ef0624b Translated using Weblate
Currently translated at 100.0% (1205 of 1205 strings) (Dutch)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/nl/

Translated using Weblate

Currently translated at 100.0% (1205 of 1205 strings) (Dutch)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/nl/

Co-authored-by: Joren Vansteenkiste <vansteenkiste.joren@telenet.be>
Co-authored-by: bokse <weblate@bokse.nl>
2026-05-01 21:32:24 +02:00
Pedro Daniel Reis 304ce8b881 [UI] Made sidebar image just use flex (#1975)
* made sidebar image just use flex

* force aspect ratio to be square

* prevent image container from expanding

---------

Co-authored-by: jeffvli <jeffvictorli@gmail.com>
2026-05-01 12:32:18 -07:00
Jonne Saloranta 01011a49a2 Replace success toast with info when no songs are added (#1994) 2026-05-01 11:44:15 -07:00
York d24ca04878 fix: detect Homebrew mpv on macOS (#1989) 2026-05-01 11:43:07 -07:00
Hosted Weblate 640d38e5a9 Translated using Weblate
Currently translated at 100.0% (1205 of 1205 strings) (Catalan)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/ca/

Co-authored-by: Ondo <SparkyOndo@proton.me>
2026-04-30 15:09:57 +00:00
jeffvli ac0c074d4b fix undefined / null parameter string for Subsonic (#1978) 2026-04-28 21:17:44 -07:00
jeffvli 6be5818493 migrate to mantine v9 2026-04-28 21:02:27 -07:00
Hosted Weblate 03edd5a639 Translated using Weblate
Currently translated at 100.0% (1205 of 1205 strings) (Spanish)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/

Translated using Weblate

Currently translated at 100.0% (1205 of 1205 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% (1205 of 1205 strings) (Czech)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/

Translated using Weblate

Currently translated at 99.8% (1201 of 1203 strings) (French)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fr/

Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Fordas <fordas15@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: KosmoMoustache <kosmomoustache@users.noreply.hosted.weblate.org>
Co-authored-by: York <goog10216922@gmail.com>
2026-04-29 04:45:44 +02:00
jeffvli f5eb3f1488 wrap useHotkeys to disable on command palette open (#1925) 2026-04-28 19:31:41 -07:00
jeffvli 8eab9edb15 fix performance issue related to blurred library header 2026-04-28 19:05:43 -07:00
Mitch Ray fcc69980e4 Stretch the wavesurfer waveform to the full height (#1962)
* Stretch the wavesurfer waveform to the full height

* Add waveform stretch setting
2026-04-27 20:28:03 -07:00
Hosted Weblate 053b78a3fd Translated using Weblate
Currently translated at 100.0% (1203 of 1203 strings) (Spanish)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/

Translated using Weblate

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

Translated using Weblate

Currently translated at 100.0% (1203 of 1203 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% (1203 of 1203 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>
Co-authored-by: Fordas <fordas15@gmail.com>
Co-authored-by: linger <linger0517@gmail.com>
Co-authored-by: skajmer <skajmer@protonmail.com>
2026-04-22 11:09:49 +02:00
mathew4 42ded966e4 fix: proper selection of next song when using shuffle and repeat-one (#1932) 2026-04-21 00:10:21 -07:00
Kendall Garner ea9119431c use urlsearchparams instead of qs (#1970) 2026-04-21 00:09:23 -07:00
vimae add0345f10 feat(lyrics): non-active lyric settings (#1954)
* feat: non-active lyric settings
2026-04-21 00:09:03 -07:00
Hosted Weblate e5a8324a79 Translated using Weblate
Currently translated at 99.3% (1193 of 1201 strings) (Chinese (Simplified Han script))
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hans/

Co-authored-by: yidaduizuoye <yidaduizuoye@outlook.com>
2026-04-19 06:09:51 +02:00
jeffvli cc4e933c07 fix missing path replacement transformations 2026-04-16 00:29:13 -07:00
Hosted Weblate 382d279dad Translated using Weblate
Currently translated at 100.0% (1201 of 1201 strings) (Spanish)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/

Translated using Weblate

Currently translated at 100.0% (1201 of 1201 strings) (French)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fr/

Translated using Weblate

Currently translated at 100.0% (1201 of 1201 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% (1201 of 1201 strings) (Polish)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/pl/

Translated using Weblate

Currently translated at 100.0% (1201 of 1201 strings) (Czech)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/

Translated using Weblate

Currently translated at 79.9% (959 of 1200 strings) (Russian)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/ru/

Co-authored-by: Dylan MONTIGAUD <dylanmontigaud17@gmail.com>
Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Fordas <fordas15@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Nick <n.grakhov08@gmail.com>
Co-authored-by: York <goog10216922@gmail.com>
Co-authored-by: skajmer <skajmer@protonmail.com>
2026-04-15 06:10:05 +02:00
York b99899f128 fix MPV visualizer on macOS and handle exclusive mode UX (#1930) 2026-04-13 20:47:03 -07:00
korpseluv f5839bf39c normalize album release types and improve grouping logic (#1892) 2026-04-13 20:40:11 -07:00
Damien Erambert 914ed5b8f3 macOS 26-friendly icon (#1941) 2026-04-13 20:32:21 -07:00
Ross ca0a1569f8 Add everfrost dark and light themes (#1934)
Co-authored-by: Ross <ro@noirlab.edu>
2026-04-13 20:22:37 -07:00
Hosted Weblate 9f10fe398a Translated using Weblate
Currently translated at 58.1% (698 of 1200 strings) (Portuguese (Brazil))
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/pt_BR/

Translated using Weblate

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

Co-authored-by: Yuri Shumyatsky <shumy260404@gmail.com>
Co-authored-by: albatrays <weblate.duct925@passmail.net>
2026-04-12 12:09:54 +00:00
Kendall Garner 8869278898 make theme selector serachable 2026-04-10 20:03:50 -07:00
Jeff 16c9e6cc1b Fix various build issues (#1942)
* remove dynamic import for platform features

* increase node memory limit on macOS build

* fix invalid dynamic imports in renderer

* remove discord-rpc import in renderer
2026-04-10 01:54:11 -07:00
86 changed files with 1176 additions and 369 deletions
+2 -2
View File
@@ -121,7 +121,7 @@ jobs:
strategy:
matrix:
os: [windows-latest, macos-latest, ubuntu-latest]
os: [windows-latest, macos-26, ubuntu-latest]
steps:
- name: Checkout git repo
@@ -156,7 +156,7 @@ jobs:
on_retry_command: pnpm cache delete
- name: Build and Publish to R2 (macOS)
if: matrix.os == 'macos-latest'
if: matrix.os == 'macos-26'
uses: nick-invision/retry@v3.0.2
with:
timeout_minutes: 30
+2 -2
View File
@@ -115,7 +115,7 @@ jobs:
strategy:
matrix:
os: [windows-latest, macos-latest, ubuntu-latest]
os: [windows-latest, macos-26, ubuntu-latest]
steps:
- name: Checkout git repo
@@ -156,7 +156,7 @@ jobs:
on_retry_command: pnpm cache delete
- name: Build and Publish releases (macOS)
if: matrix.os == 'macos-latest'
if: matrix.os == 'macos-26'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: nick-invision/retry@v3.0.2
+2 -1
View File
@@ -8,7 +8,7 @@ jobs:
strategy:
matrix:
os: [macos-latest]
os: [macos-26]
steps:
- name: Checkout git repo
@@ -24,6 +24,7 @@ jobs:
- name: Build and Publish releases
env:
NODE_OPTIONS: --max-old-space-size=4096
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: nick-invision/retry@v3.0.2
with:
+4 -4
View File
@@ -30,7 +30,7 @@ jobs:
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
os: [macos-26, ubuntu-latest, windows-latest]
steps:
- name: Checkout git repo
@@ -65,7 +65,7 @@ jobs:
pnpm run package:linux:pr
- name: Build for MacOS
if: ${{ matrix.os == 'macos-latest' }}
if: ${{ matrix.os == 'macos-26' }}
uses: nick-invision/retry@v3.0.2
with:
timeout_minutes: 30
@@ -86,7 +86,7 @@ jobs:
zip -r dist/linux-binaries.zip dist/*.{AppImage,deb,rpm}
- name: Zip MacOS Binaries
if: ${{ matrix.os == 'macos-latest' }}
if: ${{ matrix.os == 'macos-26' }}
run: |
zip -r dist/macos-binaries.zip dist/*.dmg
@@ -105,7 +105,7 @@ jobs:
path: dist/linux-binaries.zip
- name: Upload MacOS Binaries
if: ${{ matrix.os == 'macos-latest' }}
if: ${{ matrix.os == 'macos-26' }}
uses: actions/upload-artifact@v7
with:
name: macos-binaries
+2 -2
View File
@@ -8,7 +8,7 @@ jobs:
strategy:
matrix:
os: [windows-latest, macos-latest, ubuntu-latest]
os: [windows-latest, macos-26, ubuntu-latest]
steps:
- name: Checkout git repo
@@ -36,7 +36,7 @@ jobs:
on_retry_command: pnpm cache delete
- name: Build and Publish releases (macOS)
if: matrix.os == 'macos-latest'
if: matrix.os == 'macos-26'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: nick-invision/retry@v3.0.2
Binary file not shown.
+2 -1
View File
@@ -40,13 +40,14 @@ mac:
arch:
- arm64
- x64
icon: assets/icons/icon.icns
icon: media/feishin.icon
type: distribution
hardenedRuntime: false
identity: '-'
gatekeeperAssess: false
notarize: false
extendInfo:
NSAudioCaptureUsageDescription: "System audio access is required for mpv visualizer capture in Feishin"
NSLocalNetworkUsageDescription: 'Local network is necessary for accessing servers hosted on the same system as Feishin'
dmg:
+2 -1
View File
@@ -40,13 +40,14 @@ mac:
arch:
- arm64
- x64
icon: assets/icons/icon.icns
icon: media/feishin.icon
type: distribution
hardenedRuntime: false
identity: '-'
gatekeeperAssess: false
notarize: false
extendInfo:
NSAudioCaptureUsageDescription: "System audio access is required for mpv visualizer capture in Feishin"
NSLocalNetworkUsageDescription: 'Local network is necessary for accessing servers hosted on the same system as Feishin'
dmg:
+2 -1
View File
@@ -40,13 +40,14 @@ mac:
arch:
- arm64
- x64
icon: assets/icons/icon.icns
icon: media/feishin.icon
type: distribution
hardenedRuntime: false
identity: '-'
gatekeeperAssess: false
notarize: false
extendInfo:
NSAudioCaptureUsageDescription: 'System audio access is required for mpv visualizer capture in Feishin'
NSLocalNetworkUsageDescription: 'Local network is necessary for accessing servers hosted on the same system as Feishin'
dmg:
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512"><g style="display:inline" transform="translate(-53.452 -43.352)scale(1.11813)"><circle cx="256" cy="240.312" r="21.5" style="opacity:1;fill:#000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.19597;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke;filter:url(#filter249)"/><path d="M220.85 277.951 183.5 315.6l36 36.1 20-19.7s5.856-6.2 16.5-6.2 16.5 6.2 16.5 6.2l20 19.7 36-36.1-37.35-37.649A51.5 51.5 0 0 1 256 291.812a51.5 51.5 0 0 1-35.15-13.86" style="opacity:1;fill:#000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter249)"/><path d="M256 145.4a25.7 25.7 0 0 0-18.229 7.551L66.97 323.47A25.42 25.42 0 0 0 59.5 341.5c0 14.083 11.417 25.5 25.5 25.5a25.42 25.42 0 0 0 18.031-7.469l103.895-103.597a51.5 51.5 0 0 1-2.426-15.621 51.5 51.5 0 0 1 51.5-51.5 51.5 51.5 0 0 1 51.5 51.5 51.5 51.5 0 0 1-2.426 15.62L408.97 359.532A25.42 25.42 0 0 0 427 367c14.083 0 25.5-11.417 25.5-25.5a25.42 25.42 0 0 0-7.469-18.031L274.23 152.95a25.7 25.7 0 0 0-18.229-7.55" style="display:inline;opacity:1;fill:#000;fill-opacity:1;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round;paint-order:markers fill stroke;filter:url(#filter249)"/></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+202
View File
@@ -0,0 +1,202 @@
{
"fill-specializations" : [
{
"value" : {
"linear-gradient" : [
"display-p3:0.87416,0.87416,0.87416,1.00000",
"display-p3:0.99575,0.99575,0.99575,1.00000"
],
"orientation" : {
"start" : {
"x" : 0.5,
"y" : 1
},
"stop" : {
"x" : 0.5,
"y" : 0.3
}
}
}
},
{
"appearance" : "dark",
"value" : "system-dark"
}
],
"groups" : [
{
"blend-mode-specializations" : [
{
"appearance" : "tinted",
"value" : "normal"
}
],
"blur-material-specializations" : [
{
"value" : 0.7
},
{
"appearance" : "dark",
"value" : 0.7
},
{
"appearance" : "tinted",
"value" : null
}
],
"hidden" : false,
"layers" : [
{
"blend-mode-specializations" : [
{
"appearance" : "tinted",
"value" : "normal"
}
],
"fill-specializations" : [
{
"value" : {
"solid" : "extended-gray:0.00000,1.00000"
}
},
{
"appearance" : "dark",
"value" : {
"linear-gradient" : [
"display-p3:0.78674,0.78674,0.78674,1.00000",
"display-p3:0.87416,0.87416,0.87416,1.00000"
],
"orientation" : {
"start" : {
"x" : 0.5,
"y" : 1
},
"stop" : {
"x" : 0.5,
"y" : 0
}
}
}
},
{
"appearance" : "tinted",
"value" : {
"solid" : "gray:1.00000,1.00000"
}
}
],
"glass-specializations" : [
{
"value" : true
},
{
"appearance" : "dark",
"value" : true
},
{
"appearance" : "tinted",
"value" : true
}
],
"hidden" : false,
"image-name" : "feishin.svg",
"name" : "feishin",
"opacity-specializations" : [
{
"value" : 1
},
{
"appearance" : "tinted",
"value" : 1
}
],
"position" : {
"scale" : 0.79,
"translation-in-points" : [
18,
-2
]
}
}
],
"lighting-specializations" : [
{
"value" : "individual"
},
{
"appearance" : "tinted",
"value" : "combined"
}
],
"position" : {
"scale" : 2.2,
"translation-in-points" : [
0,
0
]
},
"shadow-specializations" : [
{
"value" : {
"kind" : "neutral",
"opacity" : 1
}
},
{
"appearance" : "dark",
"value" : {
"kind" : "layer-color",
"opacity" : 0.5
}
},
{
"appearance" : "tinted",
"value" : {
"kind" : "neutral",
"opacity" : 1
}
}
],
"specular-specializations" : [
{
"value" : false
},
{
"appearance" : "dark",
"value" : false
},
{
"appearance" : "tinted",
"value" : true
}
],
"translucency-specializations" : [
{
"value" : {
"enabled" : true,
"value" : 0.29
}
},
{
"appearance" : "dark",
"value" : {
"enabled" : false,
"value" : 0.29
}
},
{
"appearance" : "tinted",
"value" : {
"enabled" : true,
"value" : 0.5
}
}
]
}
],
"supported-platforms" : {
"squares" : [
"macOS"
]
}
}
+7 -7
View File
@@ -78,13 +78,13 @@
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
"@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0",
"@mantine/colors-generator": "^8.3.18",
"@mantine/core": "^8.3.18",
"@mantine/dates": "^8.3.18",
"@mantine/form": "^8.3.18",
"@mantine/hooks": "^8.3.18",
"@mantine/modals": "^8.3.18",
"@mantine/notifications": "^8.3.18",
"@mantine/colors-generator": "^9.1.1",
"@mantine/core": "^9.1.1",
"@mantine/dates": "^9.1.1",
"@mantine/form": "^9.1.1",
"@mantine/hooks": "^9.1.1",
"@mantine/modals": "^9.1.1",
"@mantine/notifications": "^9.1.1",
"@radix-ui/react-context-menu": "^2.2.16",
"@tanstack/react-query": "^5.96.2",
"@tanstack/react-query-devtools": "^5.96.2",
+96 -131
View File
@@ -28,26 +28,26 @@ importers:
specifier: ^4.0.0
version: 4.0.0(electron@39.8.6)
'@mantine/colors-generator':
specifier: ^8.3.18
version: 8.3.18(chroma-js@3.1.2)
specifier: ^9.1.1
version: 9.1.1(chroma-js@3.1.2)
'@mantine/core':
specifier: ^8.3.18
version: 8.3.18(@mantine/hooks@8.3.18(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
specifier: ^9.1.1
version: 9.1.1(@mantine/hooks@9.1.1(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@mantine/dates':
specifier: ^8.3.18
version: 8.3.18(@mantine/core@8.3.18(@mantine/hooks@8.3.18(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@mantine/hooks@8.3.18(react@19.2.4))(dayjs@1.11.20)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
specifier: ^9.1.1
version: 9.1.1(@mantine/core@9.1.1(@mantine/hooks@9.1.1(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@mantine/hooks@9.1.1(react@19.2.4))(dayjs@1.11.20)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@mantine/form':
specifier: ^8.3.18
version: 8.3.18(react@19.2.4)
specifier: ^9.1.1
version: 9.1.1(react@19.2.4)
'@mantine/hooks':
specifier: ^8.3.18
version: 8.3.18(react@19.2.4)
specifier: ^9.1.1
version: 9.1.1(react@19.2.4)
'@mantine/modals':
specifier: ^8.3.18
version: 8.3.18(@mantine/core@8.3.18(@mantine/hooks@8.3.18(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@mantine/hooks@8.3.18(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
specifier: ^9.1.1
version: 9.1.1(@mantine/core@9.1.1(@mantine/hooks@9.1.1(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@mantine/hooks@9.1.1(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@mantine/notifications':
specifier: ^8.3.18
version: 8.3.18(@mantine/core@8.3.18(@mantine/hooks@8.3.18(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@mantine/hooks@8.3.18(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
specifier: ^9.1.1
version: 9.1.1(@mantine/core@9.1.1(@mantine/hooks@9.1.1(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@mantine/hooks@9.1.1(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-context-menu':
specifier: ^2.2.16
version: 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -1441,57 +1441,57 @@ packages:
resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==}
engines: {node: '>= 10.0.0'}
'@mantine/colors-generator@8.3.18':
resolution: {integrity: sha512-u7gNAuVD/WPvB49uNszfkn7lQr85OXTI0Ijkbutcymhv0/utqlapQvZlQAYHwdrrWpQgqPNLsDBt3VSheVe9jw==}
'@mantine/colors-generator@9.1.1':
resolution: {integrity: sha512-mzEkW9oBrtvnq3zHW1jISa5hNWGi2wRgo4/N0W96DKxTqS0E1IMQ13KWJM0I9er8MK9xZo69jlpR7SyztRYFaw==}
peerDependencies:
chroma-js: '>=2.4.2'
'@mantine/core@8.3.18':
resolution: {integrity: sha512-9tph1lTVogKPjTx02eUxDUOdXacPzK62UuSqb4TdGliI54/Xgxftq0Dfqu6XuhCxn9J5MDJaNiLDvL/1KRkYqA==}
'@mantine/core@9.1.1':
resolution: {integrity: sha512-vClOZdCeZ4oLYuA/3jAOgKGQ6dXbF6ZkzpYz09Gied9nZpB7HcQeb3dcMh8UPBE4f+EM7KlYWk6dch7GoASeaA==}
peerDependencies:
'@mantine/hooks': 8.3.18
react: ^18.x || ^19.x
react-dom: ^18.x || ^19.x
'@mantine/hooks': 9.1.1
react: ^19.2.0
react-dom: ^19.2.0
'@mantine/dates@8.3.18':
resolution: {integrity: sha512-FHx5teJOhupI0gO2o5evtVYQEdqOjayOkLRhEQfB5Nc5DvcysfPfmNILGkc1Nrp9ZQeQWKLT9qr+CkcCXwHOaw==}
'@mantine/dates@9.1.1':
resolution: {integrity: sha512-P1tr/Hr+EVxppbOVpTLvaZZnM1W/r0TNpqNNMeM81xfyuKYzd7zt2/SQYb6BuudgEQfRJnAee+7bIJLEsrb0uA==}
peerDependencies:
'@mantine/core': 8.3.18
'@mantine/hooks': 8.3.18
'@mantine/core': 9.1.1
'@mantine/hooks': 9.1.1
dayjs: '>=1.0.0'
react: ^18.x || ^19.x
react-dom: ^18.x || ^19.x
react: ^19.2.0
react-dom: ^19.2.0
'@mantine/form@8.3.18':
resolution: {integrity: sha512-r5OGLJWTkmIruFjRZRZy9oA7maNYlyt50jB4Pmd2X5360WOmJLd4KH8MFhHZQC7vN+z8/rmBl3t3XGAR2I8xig==}
'@mantine/form@9.1.1':
resolution: {integrity: sha512-xmebZ3s8GGMrCOPOaOwA+gQkdgNVfT2F9kBtkjAbRoZrMoY+vYFbiPWbIvWFl8pU1jBslYZrj+M0PIawJmFOdQ==}
peerDependencies:
react: ^18.x || ^19.x
react: ^19.2.0
'@mantine/hooks@8.3.18':
resolution: {integrity: sha512-QoWr9+S8gg5050TQ06aTSxtlpGjYOpIllRbjYYXlRvZeTsUqiTbVfvQROLexu4rEaK+yy9Wwriwl9PMRgbLqPw==}
'@mantine/hooks@9.1.1':
resolution: {integrity: sha512-tTJK73nGFyy1v214TLdvBq0be7QCoc6osfbXVuJgOH3YG85lWk9Mvvor6k+w6hC6HXSqKMqLKePyiGm83xGcMg==}
peerDependencies:
react: ^18.x || ^19.x
react: ^19.2.0
'@mantine/modals@8.3.18':
resolution: {integrity: sha512-JfPDS4549L314SxFPC1x6CbKwzh82OdnIzwgMxPCVNsWLKV2vEHHUH/fzUYj4Wli6IBrsW4cufjMj9BTj3hm3Q==}
'@mantine/modals@9.1.1':
resolution: {integrity: sha512-SjJ2kIheJaWoKMsSuYQlLFvuJTxCQTOl3gr+wDj/bLmGBgfUykLStRNm9s1H7vFxMIWtN20N8mwtcZV2dGeYBg==}
peerDependencies:
'@mantine/core': 8.3.18
'@mantine/hooks': 8.3.18
react: ^18.x || ^19.x
react-dom: ^18.x || ^19.x
'@mantine/core': 9.1.1
'@mantine/hooks': 9.1.1
react: ^19.2.0
react-dom: ^19.2.0
'@mantine/notifications@8.3.18':
resolution: {integrity: sha512-IpQ0lmwbigTBbZCR6iSYWqIOKEx1tlcd7PcEJ5M5X1qeVSY/N3mmDQt1eJmObvcyDeL5cTJMbSA9UPqhRqo9jw==}
'@mantine/notifications@9.1.1':
resolution: {integrity: sha512-ZfcEMMDp0BQ+yKmVp8ifPXLKej8pv9TcaRnmy2CZ07USD61E9LH5ClRAP/hxQuCyf/qLb5BPHsI7+f3K8uhj4Q==}
peerDependencies:
'@mantine/core': 8.3.18
'@mantine/hooks': 8.3.18
react: ^18.x || ^19.x
react-dom: ^18.x || ^19.x
'@mantine/core': 9.1.1
'@mantine/hooks': 9.1.1
react: ^19.2.0
react-dom: ^19.2.0
'@mantine/store@8.3.18':
resolution: {integrity: sha512-i+QRTLmZzLldea0egtUVnGALd6UMIu8jd44nrNWBSNIXJU/8B6rMlC6gyX+l4szopZSuOaaNJIXkqRdC1gQsVg==}
'@mantine/store@9.1.1':
resolution: {integrity: sha512-kbxEU8wVGbobHlmQmk0lu9M+xCILKjuAPcMAshgzPznGLfXeE9zrB0gNT2cbk11Ik8dlV9J6Vsn9cuACyOSpfQ==}
peerDependencies:
react: ^18.x || ^19.x
react: ^19.2.0
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
@@ -1912,66 +1912,79 @@ packages:
resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.60.1':
resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.60.1':
resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.60.1':
resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.60.1':
resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.60.1':
resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==}
cpu: [loong64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.60.1':
resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.60.1':
resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==}
cpu: [ppc64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.60.1':
resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.60.1':
resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.60.1':
resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.60.1':
resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.60.1':
resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.60.1':
resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==}
@@ -2015,6 +2028,9 @@ packages:
resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
engines: {node: '>=10'}
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
'@surma/rollup-plugin-off-main-thread@2.2.3':
resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==}
@@ -4710,12 +4726,6 @@ packages:
'@types/react':
optional: true
react-textarea-autosize@8.5.9:
resolution: {integrity: sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==}
engines: {node: '>=10'}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-transition-group@4.4.5:
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
peerDependencies:
@@ -5358,6 +5368,10 @@ packages:
resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
engines: {node: '>=10.0.0'}
tagged-tag@1.0.0:
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
engines: {node: '>=20'}
tar@7.5.13:
resolution: {integrity: sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==}
engines: {node: '>=18'}
@@ -5454,9 +5468,9 @@ packages:
resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
engines: {node: '>=12.20'}
type-fest@4.41.0:
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
engines: {node: '>=16'}
type-fest@5.6.0:
resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==}
engines: {node: '>=20'}
typed-array-buffer@1.0.3:
resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
@@ -5569,33 +5583,6 @@ packages:
'@types/react':
optional: true
use-composed-ref@1.4.0:
resolution: {integrity: sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
use-isomorphic-layout-effect@1.2.1:
resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
use-latest@1.3.0:
resolution: {integrity: sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
use-sidecar@1.1.3:
resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
engines: {node: '>=10'}
@@ -7111,60 +7098,60 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@mantine/colors-generator@8.3.18(chroma-js@3.1.2)':
'@mantine/colors-generator@9.1.1(chroma-js@3.1.2)':
dependencies:
chroma-js: 3.1.2
'@mantine/core@8.3.18(@mantine/hooks@8.3.18(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@mantine/core@9.1.1(@mantine/hooks@9.1.1(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@floating-ui/react': 0.27.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@mantine/hooks': 8.3.18(react@19.2.4)
'@mantine/hooks': 9.1.1(react@19.2.4)
clsx: 2.1.1
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
react-number-format: 5.4.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4)
react-textarea-autosize: 8.5.9(@types/react@19.2.14)(react@19.2.4)
type-fest: 4.41.0
type-fest: 5.6.0
transitivePeerDependencies:
- '@types/react'
'@mantine/dates@8.3.18(@mantine/core@8.3.18(@mantine/hooks@8.3.18(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@mantine/hooks@8.3.18(react@19.2.4))(dayjs@1.11.20)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@mantine/dates@9.1.1(@mantine/core@9.1.1(@mantine/hooks@9.1.1(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@mantine/hooks@9.1.1(react@19.2.4))(dayjs@1.11.20)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@mantine/core': 8.3.18(@mantine/hooks@8.3.18(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@mantine/hooks': 8.3.18(react@19.2.4)
'@mantine/core': 9.1.1(@mantine/hooks@9.1.1(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@mantine/hooks': 9.1.1(react@19.2.4)
clsx: 2.1.1
dayjs: 1.11.20
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@mantine/form@8.3.18(react@19.2.4)':
'@mantine/form@9.1.1(react@19.2.4)':
dependencies:
'@standard-schema/spec': 1.1.0
fast-deep-equal: 3.1.3
klona: 2.0.6
react: 19.2.4
'@mantine/hooks@8.3.18(react@19.2.4)':
'@mantine/hooks@9.1.1(react@19.2.4)':
dependencies:
react: 19.2.4
'@mantine/modals@8.3.18(@mantine/core@8.3.18(@mantine/hooks@8.3.18(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@mantine/hooks@8.3.18(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@mantine/modals@9.1.1(@mantine/core@9.1.1(@mantine/hooks@9.1.1(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@mantine/hooks@9.1.1(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@mantine/core': 8.3.18(@mantine/hooks@8.3.18(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@mantine/hooks': 8.3.18(react@19.2.4)
'@mantine/core': 9.1.1(@mantine/hooks@9.1.1(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@mantine/hooks': 9.1.1(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@mantine/notifications@8.3.18(@mantine/core@8.3.18(@mantine/hooks@8.3.18(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@mantine/hooks@8.3.18(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@mantine/notifications@9.1.1(@mantine/core@9.1.1(@mantine/hooks@9.1.1(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@mantine/hooks@9.1.1(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@mantine/core': 8.3.18(@mantine/hooks@8.3.18(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@mantine/hooks': 8.3.18(react@19.2.4)
'@mantine/store': 8.3.18(react@19.2.4)
'@mantine/core': 9.1.1(@mantine/hooks@9.1.1(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@mantine/hooks': 9.1.1(react@19.2.4)
'@mantine/store': 9.1.1(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
react-transition-group: 4.4.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@mantine/store@8.3.18(react@19.2.4)':
'@mantine/store@9.1.1(react@19.2.4)':
dependencies:
react: 19.2.4
@@ -7611,6 +7598,8 @@ snapshots:
'@sindresorhus/is@4.6.0': {}
'@standard-schema/spec@1.1.0': {}
'@surma/rollup-plugin-off-main-thread@2.2.3':
dependencies:
ejs: 3.1.10
@@ -10672,15 +10661,6 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
react-textarea-autosize@8.5.9(@types/react@19.2.14)(react@19.2.4):
dependencies:
'@babel/runtime': 7.29.2
react: 19.2.4
use-composed-ref: 1.4.0(@types/react@19.2.14)(react@19.2.4)
use-latest: 1.3.0(@types/react@19.2.14)(react@19.2.4)
transitivePeerDependencies:
- '@types/react'
react-transition-group@4.4.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
'@babel/runtime': 7.29.2
@@ -11435,6 +11415,8 @@ snapshots:
string-width: 4.2.3
strip-ansi: 6.0.1
tagged-tag@1.0.0: {}
tar@7.5.13:
dependencies:
'@isaacs/fs-minipass': 4.0.1
@@ -11544,7 +11526,9 @@ snapshots:
type-fest@2.19.0: {}
type-fest@4.41.0: {}
type-fest@5.6.0:
dependencies:
tagged-tag: 1.0.0
typed-array-buffer@1.0.3:
dependencies:
@@ -11664,25 +11648,6 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
use-composed-ref@1.4.0(@types/react@19.2.14)(react@19.2.4):
dependencies:
react: 19.2.4
optionalDependencies:
'@types/react': 19.2.14
use-isomorphic-layout-effect@1.2.1(@types/react@19.2.14)(react@19.2.4):
dependencies:
react: 19.2.4
optionalDependencies:
'@types/react': 19.2.14
use-latest@1.3.0(@types/react@19.2.14)(react@19.2.4):
dependencies:
react: 19.2.4
use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.14)(react@19.2.4)
optionalDependencies:
'@types/react': 19.2.14
use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.4):
dependencies:
detect-node-es: 1.1.0
+42 -7
View File
@@ -139,7 +139,9 @@
"lyricOffset": "demora de la lletra (ms)",
"showLyricMatch": "mosta coincidències de lletres",
"showLyricProvider": "mostra el proveïdor de la lletra",
"lyricGap": "espera entre lletres"
"lyricGap": "espera entre lletres",
"lyricOpacityNonActive": "Opacitat de la lletra inactiva",
"lyricScaleNonActive": "Escala de la lletra inactiva"
},
"lyrics": "lletres",
"visualizer": "visualitzador",
@@ -337,7 +339,8 @@
"filter_single": "senzill",
"filter_multiple": "multi",
"rename": "reanomena",
"newVersionAvailable": "hi ha una nova versió disponible"
"newVersionAvailable": "hi ha una nova versió disponible",
"numberOfResults": "{{numberOfResults}} resultats"
},
"entity": {
"album_one": "àlbum",
@@ -406,7 +409,8 @@
"input_skipDuplicates": "salta't els duplicats",
"success": "s'ha afegit $t(entity.trackWithCount, {\"count\": {{message}} }) a $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
"create": "crea $t(entity.playlist, {\"count\": 1}) {{playlist}}",
"searchOrCreate": "cerca $t(entity.playlist, {\"count\": 2}) o escriu per crear-ne una de nova"
"searchOrCreate": "cerca $t(entity.playlist, {\"count\": 2}) o escriu per crear-ne una de nova",
"noneAdded": "no s'han afegit pistes a la $t(entity.playlist, {\"count\": 1}) '{{playlist}}'"
},
"createPlaylist": {
"input_description": "$t(common.description)",
@@ -506,6 +510,9 @@
"export": "exporta la lletra",
"input_synced": "exporta la lletra sincronitzada",
"input_offset": "$t(setting.lyricOffset)"
},
"editRadioStation": {
"success": "Emissora de ràdio actualitzada amb èxit"
}
},
"action": {
@@ -522,7 +529,10 @@
"goToPage": "anar a la pàgina",
"openIn": {
"lastfm": "Obrir a Last.fm",
"musicbrainz": "Obrir a MusicBrainz"
"musicbrainz": "Obrir a MusicBrainz",
"listenbrainz": "Obre a ListenBrainz",
"qobuz": "Obre a Qobuz",
"spotify": "Obre a Spotify"
},
"deselectAll": "deselecciona-ho tot",
"viewPlaylists": "veure $t(entity.playlist, {\"count\": 2})",
@@ -597,7 +607,7 @@
"artistConfiguration": "configuració de la pàgina de l'artista de l'àlbum",
"artistConfiguration_description": "configura quins elements es mostren i el seu ordre de la pàgina de l'artista de l'àlbum",
"audioExclusiveMode": "mode d'àudio exclusiu",
"audioExclusiveMode_description": "activa el mode d'àudio exclusiu. En aquest mode, el sistema normalment estarà bloquejat i només mpv podrà emetre àudio",
"audioExclusiveMode_description": "activa el mode d'àudio exclusiu. En aquest mode, el sistema normalment estarà bloquejat i només mpv podrà emetre àudio. El sistema visualitzador de captura d'àudio no funcionarà mentre això estigui activat",
"buttonSize": "mida dels botons de la barra de reproducció",
"buttonSize_description": "la mida dels botons de la barra de reproducció",
"clearCache": "neteja la memòria del navegador",
@@ -910,7 +920,25 @@
"primaryShade": "ombra primària",
"primaryShade_description": "substitueix el to primari (09) utilitzat per a botons, enllaços i altres elements de color primari",
"playerItemConfiguration_description": "configurar quins elements es mostren i en quin ordre al reproductor de pantalla completa",
"playerItemConfiguration": "configuració d'elements del jugador"
"playerItemConfiguration": "configuració d'elements del jugador",
"listenbrainz_description": "mostra enllaços a ListenBrainz a les pàgines d'artista/àlbum",
"listenbrainz": "mostra enllaços a ListenBrainz",
"qobuz_description": "mostra enllaços a Qobuz a les pàgines d'artista/àlbum",
"qobuz": "mostra enllaços a Qobuz",
"spotify_description": "mostra enllaços a Spotify a les pàgines d'artista/àlbum",
"spotify": "mostra enllaços a Spotify",
"nativeSpotify_description": "obre amb Spotify en lloc del vostre navegador",
"nativeSpotify": "fes servir Spotify",
"playerbarWaveformStretch": "extensió de la forma d'ona",
"playerbarWaveformStretch_description": "estén la forma d'ona per omplir l'espai disponible",
"sidePlayQueueLayout": "disposició de la cua de reproducció lateral",
"sidePlayQueueLayout_description": "estableix la disposició de la cua de reproducció lateral adjunta",
"sidePlayQueueLayout_optionHorizontal": "horitzontal",
"sidePlayQueueLayout_optionVertical": "vertical",
"waveformLoadingDelay": "demora de càrrega de la forma d'ona",
"waveformLoadingDelay_description": "demora en segons abans de carregar la forma d'ona. incrementeu aquest valor si patiu interrupcions en fer servir el reproductor web.",
"preventSuspendOnPlayback_description": "evita que l'aplicació quedi suspesa mentre es reprodueix música",
"preventSuspendOnPlayback": "evita la suspensió durant la reproducció"
},
"table": {
"column": {
@@ -1360,6 +1388,13 @@
}
},
"pasteGradient": "enganxa degradat",
"pasteGradientPlaceholder": "enganxa el degradat JSON aquí..."
"pasteGradientPlaceholder": "enganxa el degradat JSON aquí...",
"systemAudioConsentAllow": "permetre",
"systemAudioConsentBody": "el visualitzador necessita accés a l'àudio del sistema per funcionar",
"systemAudioConsentDecline": "denega",
"systemAudioConsentTitle": "Voleu permetre accés al sistema d'àudio?",
"systemAudioCaptureFailed": "No s'ha pogut iniciar la captura: {{message}}",
"systemAudioNoAudioTrack": "No s'ha tornat cap pista d'àudio. Comproveu que la captura d'àudio estigui activada quan se sol·liciti.",
"systemAudioExclusiveModeNotSupported": "El visualitzador no està disponible amb el mode d'àudio exclusiu activat. Desactiveu-lo a la configuració d'MPV i torneu-ho a intentar."
}
}
+13 -5
View File
@@ -85,7 +85,7 @@
"hotkey_zoomIn": "přiblížení",
"scrobble_description": "scrobblovat přehrání na váš multimediální server",
"hotkey_browserForward": "vpřed v prohlížeči",
"audioExclusiveMode_description": "zapnout režim výhradního výstupu. V tomto režimu bude obvykle v systému schopný přehrávat zvuk pouze přehrávač mpv",
"audioExclusiveMode_description": "zapnout režim výhradního výstupu. v tomto režimu bude obvykle v systému schopný přehrávat zvuk pouze přehrávač mpv. záznam systémového zvuku ve vizualizéru nebude fungovat",
"discordUpdateInterval": "interval aktualizací {{discord}} rich presence",
"themeLight": "motiv (světlý)",
"fontType_optionBuiltIn": "vestavěné písmo",
@@ -425,7 +425,11 @@
"sidePlayQueueLayout_optionHorizontal": "na šířku",
"sidePlayQueueLayout_optionVertical": "na výšku",
"waveformLoadingDelay": "zpoždění načítání vlnové křivky",
"waveformLoadingDelay_description": "zpoždění v sekundách před načtením vlnové křivky. zvyšte, pokud jste během používání webového přehrávače zaznamenali záseky."
"waveformLoadingDelay_description": "zpoždění v sekundách před načtením vlnové křivky. zvyšte, pokud jste během používání webového přehrávače zaznamenali záseky.",
"playerbarWaveformStretch": "natáhnutí vlnové křivky",
"playerbarWaveformStretch_description": "natáhně vlnovou křivku tak, aby vyplnila dostupný prostor",
"preventSuspendOnPlayback_description": "zabránit aplikaci v uspání během přehrávání hudby",
"preventSuspendOnPlayback": "zabránit uspání při přehrávání"
},
"action": {
"editPlaylist": "upravit $t(entity.playlist, {\"count\": 1})",
@@ -825,7 +829,9 @@
"lyricGap": "mezera textů",
"dynamicImageBlur": "velikost rozostření obrázku",
"dynamicIsImage": "povolit obrázek na pozadí",
"lyricOffset": "posunutí textů (ms)"
"lyricOffset": "posunutí textů (ms)",
"lyricOpacityNonActive": "neprůhlednost neaktivních textů",
"lyricScaleNonActive": "velikost neaktivních textů"
},
"upNext": "další",
"lyrics": "texty",
@@ -1041,7 +1047,8 @@
"input_skipDuplicates": "přeskočit duplicity",
"input_playlists": "$t(entity.playlist, {\"count\": 2})",
"create": "vytvořit $t(entity.playlist, {\"count\": 1}) {{playlist}}",
"searchOrCreate": "vyhledejte $t(entity.playlist, {\"count\": 2}) nebo pište pro vytvoření nového"
"searchOrCreate": "vyhledejte $t(entity.playlist, {\"count\": 2}) nebo pište pro vytvoření nového",
"noneAdded": "do $t(entity.playlist, {\"count\": 1}) '{{playlist}}' nebyly přidány žádné skladby"
},
"updateServer": {
"title": "upravit server",
@@ -1387,6 +1394,7 @@
"systemAudioConsentTitle": "Povolit přístup k systémovému zvuku?",
"systemAudioCaptureFailed": "Nepodařilo se spustit zachytávání: {{message}}",
"systemAudioNoAudioTrack": "Nebyla zachycena žádná zvuková stopa. Ujistěte se, že jste při výzvě povolili zachytávání zvuku.",
"systemAudioConsentDecline": "Zamítnout"
"systemAudioConsentDecline": "Zamítnout",
"systemAudioExclusiveModeNotSupported": "Vizualizér není dostupný při zapnutém režimu výhradního výstupu zvuku. Zakažte Režim výhradního výstupu zvuku v nastavení MPV a zkuste to znovu."
}
}
+9 -1
View File
@@ -347,6 +347,7 @@
"input_skipDuplicates": "skip duplicates",
"searchOrCreate": "search $t(entity.playlist, {\"count\": 2}) or type to create a new one",
"success": "added $t(entity.trackWithCount, {\"count\": {{message}} }) to $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
"noneAdded": "no tracks were added to $t(entity.playlist, {\"count\": 1}) '{{playlist}}'",
"title": "add to $t(entity.playlist, {\"count\": 1})"
},
"createPlaylist": {
@@ -540,6 +541,8 @@
"lyricOffset": "lyrics offset (ms)",
"lyricGap": "lyric gap",
"lyricSize": "lyric size",
"lyricOpacityNonActive": "non-active lyric opacity",
"lyricScaleNonActive": "non-active lyric scale",
"opacity": "opacity",
"showLyricMatch": "show lyric match",
"showLyricProvider": "show lyric provider",
@@ -761,7 +764,7 @@
"artistReleaseTypeConfiguration_description": "configure what release types are shown, and in what order, on the album artist page",
"audioDevice_description": "select the audio device to use for playback",
"audioDevice": "audio device",
"audioExclusiveMode_description": "enable exclusive output mode. In this mode, the system is usually locked out, and only mpv will be able to output audio",
"audioExclusiveMode_description": "enable exclusive output mode. in this mode, the system is usually locked out, and only mpv will be able to output audio. visualizer system audio capture will not work while this is enabled",
"audioExclusiveMode": "audio exclusive mode",
"audioPlayer_description": "select the audio player to use for playback",
"audioPlayer": "audio player",
@@ -983,6 +986,8 @@
"playerbarWaveformBarWidth": "waveform bar width",
"playerbarWaveformGap": "waveform gap",
"playerbarWaveformRadius": "waveform radius",
"playerbarWaveformStretch": "waveform stretch",
"playerbarWaveformStretch_description": "stretches the waveform to fill the available space",
"preferLocalLyrics_description": "prefer local lyrics over remote lyrics when available",
"preferLocalLyrics": "prefer local lyrics",
"showLyricsInSidebar_description": "a panel will be added to the attached play queue that displays the lyrics",
@@ -1003,6 +1008,8 @@
"audioFadeOnStatusChange_description": "enables fade out and fade in when play/pause status changes",
"preventSleepOnPlayback_description": "prevent the display from sleeping while music is playing",
"preventSleepOnPlayback": "prevent sleep on playback",
"preventSuspendOnPlayback_description": "prevent the application from suspending while music is playing",
"preventSuspendOnPlayback": "prevent suspend on playback",
"remotePassword_description": "sets the password for the remote control server. These credentials are by default transferred insecurely, so you should use a unique password that you do not care about",
"remotePassword": "remote control server password",
"remotePort_description": "sets the port for the remote control server",
@@ -1220,6 +1227,7 @@
"systemAudioConsentTitle": "Allow access to system audio?",
"systemAudioCaptureFailed": "Could not start capture: {{message}}",
"systemAudioNoAudioTrack": "No audio track was returned. Ensure audio capture is enabled when prompted.",
"systemAudioExclusiveModeNotSupported": "Visualizer is unavailable while audio exclusive mode is enabled. Disable Audio Exclusive Mode in MPV settings and try again.",
"visualizerType": "Visualizer Type",
"cyclePresets": "Cycle Presets",
"cycleTime": "Cycle Time (seconds)",
+13 -5
View File
@@ -83,7 +83,7 @@
"replayGainClipping": "recortar {{ReplayGain}}",
"hotkey_zoomIn": "ampliar",
"scrobble_description": "hace scrobble de las reproducciones en tu servidor de medios",
"audioExclusiveMode_description": "activa el modo de audio exclusivo. En este modo, el sistema es normalmente bloqueado, y solo se permitirá mpv en la salida de audio",
"audioExclusiveMode_description": "activa el modo de audio exclusivo. En este modo, el sistema es normalmente bloqueado, y solo se permitirá mpv en la salida de audio. El sistema visualizador de captura de audio no funcionará mientras esto esté activado",
"discordUpdateInterval": "intervalo de actualización del estado de actividad de {{discord}}",
"themeLight": "tema (claro)",
"fontType_optionBuiltIn": "fuente incorporada",
@@ -425,7 +425,11 @@
"sidePlayQueueLayout": "Diseño de la cola de reproducción lateral",
"sidePlayQueueLayout_description": "Establece el diseño de la cola de reproducción lateral adjunta",
"waveformLoadingDelay": "Retraso de carga de la forma de onda",
"waveformLoadingDelay_description": "Retraso en segundos antes de cargar la forma de onda. Incrementa este valor si estás experimentando tartamudeos al usar el reproductor web."
"waveformLoadingDelay_description": "Retraso en segundos antes de cargar la forma de onda. Incrementa este valor si estás experimentando tartamudeos al usar el reproductor web.",
"playerbarWaveformStretch": "Estiramiento de la forma de onda",
"playerbarWaveformStretch_description": "Estira la forma de onda para rellenar el espacio disponible",
"preventSuspendOnPlayback": "Evitar la suspensión durante la reproducción",
"preventSuspendOnPlayback_description": "Evita que la aplicación se suspenda mientras se reproduce música"
},
"action": {
"editPlaylist": "editar $t(entity.playlist, {\"count\": 1})",
@@ -772,7 +776,9 @@
"lyricGap": "desfase de letra",
"dynamicImageBlur": "tamaño de desenfoque de imagen",
"dynamicIsImage": "habilitar imagen de fondo",
"lyricOffset": "desplazamiento de letras (ms)"
"lyricOffset": "desplazamiento de letras (ms)",
"lyricScaleNonActive": "Escala de letra no activa",
"lyricOpacityNonActive": "Opacidad de letra no activa"
},
"lyrics": "letras",
"related": "relacionado",
@@ -932,7 +938,8 @@
"input_skipDuplicates": "saltar duplicados",
"input_playlists": "$t(entity.playlist, {\"count\": 2})",
"create": "Crear $t(entity.playlist, {\"count\": 1}) {{playlist}}",
"searchOrCreate": "Buscar $t(entity.playlist, {\"count\": 2}) o escribir para crear uno nuevo"
"searchOrCreate": "Buscar $t(entity.playlist, {\"count\": 2}) o escribir para crear uno nuevo",
"noneAdded": "Ninguna pista fue añadida a $t(entity.playlist, {\"count\": 1}) '{{playlist}}'"
},
"updateServer": {
"title": "actualizar servidor",
@@ -1387,6 +1394,7 @@
"systemAudioConsentTitle": "¿Permitir acceso al sistema de audio?",
"systemAudioConsentBody": "El visualizador requiere acceso al sistema de audio para funcionar",
"systemAudioCaptureFailed": "No se pudo iniciar la captura: {{message}}",
"systemAudioNoAudioTrack": "Ninguna pista de audio devuelta. Asegúrate de que la captura de audio está activada cuando se solicite."
"systemAudioNoAudioTrack": "Ninguna pista de audio devuelta. Asegúrate de que la captura de audio está activada cuando se solicite.",
"systemAudioExclusiveModeNotSupported": "El visualizador no está disponible mientras el modo de audio exclusivo esté activado. Desactiva el modo de audio exclusivo en la configuración de MPV e inténtalo de nuevo."
}
}
+50 -9
View File
@@ -157,7 +157,8 @@
"doNotShowAgain": "ez erakutsi hau berriro",
"numberOfResults": "{{numberOfResults}} emaitza",
"rename": "berrizendatu",
"newVersionAvailable": "bertsio berri bat eskuragarri dago"
"newVersionAvailable": "bertsio berri bat eskuragarri dago",
"gridRows": "sareta-errenkadak"
},
"player": {
"repeat": "errepikatu",
@@ -409,7 +410,8 @@
"fromYear": "urtetik aurrera",
"explicitStatus": "$t(common.explicitStatus)",
"matchAnd": "eta",
"matchOr": "edo"
"matchOr": "edo",
"sortName": "izenaren arabera ordenatu"
},
"setting": {
"hotkey_playbackPause": "pausatu",
@@ -643,7 +645,7 @@
"passwordStore_description": "zein pasahitz/sekretu biltegi erabili. aldatu hau pasahitzak gordetzeko arazoak badituzu",
"playerFilters": "Iragazi ilarako abestiak",
"sidePlayQueueStyle_description": "alboko erreprodukzio-ilararen estiloa ezartzen du",
"mediaSession_description": "Windows Media Session integrazioa gaitzen du, multimedia kontrolak eta metadatuak sistemaren bolumenaren gainjartzean eta blokeo pantailan bistaratuz (Windows bakarrik)",
"mediaSession_description": "Media Session integrazioa gaitzen du, multimedia kontrolak eta metadatuak sistemaren bolumenaren gainjartzean eta blokeo pantailan bistaratuz",
"sidePlayQueueStyle": "alboko erreprodukzio-ilarako estiloa",
"skipPlaylistPage": "saltatu erreprodukzio-zerrenda orria",
"startMinimized_description": "abiarazi aplikazioa sistemaren erretiluan",
@@ -714,7 +716,19 @@
"preservePitch": "mantendu tonua",
"preventSleepOnPlayback": "erreprodukzioan loa saihestu",
"replayGainClipping_description": "Saihestu {{ReplayGain}}-k eragindako mozketa irabazpena automatikoki jaitsiz",
"replayGainMode_description": "doitu bolumenaren irabazia fitxategiaren metadatuetan gordetako {{ReplayGain}} balioen arabera"
"replayGainMode_description": "doitu bolumenaren irabazia fitxategiaren metadatuetan gordetako {{ReplayGain}} balioen arabera",
"listenbrainz": "erakutsi ListenBrainz estekak",
"sidebarPlaylistSorting": "alboko barrako erreprodukzio-zerrenda ordenatzea",
"sidebarPlaylistListFilterRegex_description": "ezkutatu alboko barran adierazpen erregular honekin bat datozen erreprodukzio-zerrendak",
"sidebarPlaylistListFilterRegex_placeholder": "adib. ^Eguneroko Nahasketa.*",
"sidebarPlaylistListFilterRegex": "erreprodukzio-zerrenda iragazteko adierazpen erregularra",
"sidePlayQueueLayout": "alboko erreprodukzio-ilararen diseinua",
"sidePlayQueueLayout_description": "erantsitako alboko erreprodukzio-ilararen diseinua ezartzen du",
"sidePlayQueueLayout_optionHorizontal": "horizontala",
"sidePlayQueueLayout_optionVertical": "bertikala",
"skipDuration_description": "erreproduzitzailearen barran saltatzeko botoiak erabiltzean saltatzeko iraupena ezartzen du",
"skipPlaylistPage_description": "erreprodukzio-zerrenda batera nabigatzean, joan erreprodukzio-zerrendako abestien zerrendara lehenetsitako orrialdera joan beharrean",
"translationApiKey_description": "itzulpenerako api gakoa (zerbitzu globalaren amaiera-puntua soilik)"
},
"form": {
"addServer": {
@@ -731,7 +745,9 @@
"success": "zerbitzaria behar bezala gehitu da",
"input_preferInstantMix": "nahiago izan berehalako nahasketa",
"input_preferInstantMixDescription": "erabili berehalako nahasketa soilik antzeko abestiak lortzeko. erabilgarria portaera hau aldatzen duten pluginak badituzu",
"input_remoteUrl": "URL publikoa"
"input_remoteUrl": "URL publikoa",
"input_preferRemoteUrl": "url publikoa nahiago izan",
"input_remoteUrlPlaceholder": "aukerakoa: kanpoko funtzioetarako url publikoa"
},
"addToPlaylist": {
"input_playlists": "$t(entity.playlist, {\"count\": 2})",
@@ -739,7 +755,8 @@
"input_skipDuplicates": "saltatu bikoiztuak",
"title": "gehitu $t(entity.playlist, {\"count\": 1})-(a)ri",
"create": "sortu $t(entity.playlist, {\"count\": 1}) {{playlist}}",
"searchOrCreate": "bilatu $t(entity.playlist, {\"count\": 2}) edo idatzi berri bat sortzeko"
"searchOrCreate": "bilatu $t(entity.playlist, {\"count\": 2}) edo idatzi berri bat sortzeko",
"noneAdded": "ez da pistarik gehitu honi $t(entity.playlist, {\"count\": 1}) '{{playlist}}'"
},
"createPlaylist": {
"input_description": "$t(common.description)",
@@ -760,7 +777,9 @@
"success": "partekatzeko esteka arbelera kopiatu da (edo egin klik hemen irekitzeko)",
"expireInvalid": "iraungitze-data etorkizunean izan behar da",
"allowDownloading": "baimendu deskargatzea",
"createFailed": "partekatzea sortzeak huts egin du (partekatzea gaituta al dago?)"
"createFailed": "partekatzea sortzeak huts egin du (partekatzea gaituta al dago?)",
"copyToClipboard": "Kopiatu arbelean: Ctrl+C, Enter",
"successMustClick": "partekatzea behar bezala sortu da. egin klik hemen irekitzeko"
},
"deletePlaylist": {
"success": "$t(entity.playlist, {\"count\": 1}) behar bezala ezabatu da",
@@ -988,7 +1007,10 @@
"recentReleases": "azken argitalpenak",
"viewDiscography": "ikusi diskografia",
"groupingTypeAll": "argitalpen mota guztiak",
"groupingTypePrimary": "argitalpen mota nagusiak"
"groupingTypePrimary": "argitalpen mota nagusiak",
"favoriteSongs": "abesti gogokoenak",
"topSongsCommunity": "komunitatea",
"topSongsPersonal": "pertsonala"
},
"itemDetail": {
"copyPath": "kopiatu bidea arbelean",
@@ -1006,6 +1028,13 @@
},
"radioList": {
"title": "irrati-kateak"
},
"releasenotes": {
"noStableReleaseToCompare": "ez dago bertsio egonkorrik alderatzeko"
},
"windowBar": {
"paused": "(Pausatuta) ",
"privateMode": "(Modu pribatua)"
}
},
"releaseType": {
@@ -1038,7 +1067,19 @@
"notContains": "ez dauka",
"startsWith": "honekin hasten da",
"endsWith": "honekin amaitzen da",
"isNot": "ez da"
"isNot": "ez da",
"afterDate": "(data) ondoren da",
"after": "ondoren da",
"before": "lehenago da",
"beforeDate": "(data) baino lehenagokoa da",
"inTheLast": "azkenengoan dago",
"inPlaylist": "bertan dago",
"inTheRange": "tartean dago",
"inTheRangeDate": "(data) tartean dago",
"isGreaterThan": "hau baino handiagoa da",
"isLessThan": "hau baino gutxiago da",
"notInPlaylist": "ez dago barruan",
"notInTheLast": "ez dago azkenengoan"
},
"visualizer": {
"general": "Orokorra",
+10 -7
View File
@@ -246,10 +246,10 @@
"loginRateError": "trop de tentatives de connexion, merci de réessayer dans quelques secondes",
"openError": "impossible d'ouvrir le fichier",
"networkError": "une erreur de réseau est survenue",
"badAlbum": "vous voyez cette page parce que ce titre ne fait pas parti d'un album. vous rencontrez probablement cette erreur si vous avez un titre à la racine de votre dossier musique. Jellyfin gère les chansons uniquement si elles sont dans un sous-dossier, qui est lui-même dans un dossier \"Musique(s)\"",
"badAlbum": "vous voyez cette page parce que ce titre ne fait pas parti d'un album. vous rencontrez probablement cette erreur si vous avez un titre à la racine de votre dossier musique. Jellyfin gère les pistes uniquement si elles sont dans un sous-dossier, qui est lui-même dans un dossier \"Musique(s)\"",
"badValue": "option {{value}} invalide. cette valeur n'existe plus",
"notificationDenied": "les autorisations pour les notifications ont été refusées. ce paramètre n'a aucun effet",
"multipleServerSaveQueueError": "la file d'attente contient un ou plusieurs morceaux qui ne proviennent pas du serveur actuel. Ceci n'est pas prise en charge",
"multipleServerSaveQueueError": "la file d'attente contient un ou plusieurs titres qui ne proviennent pas du serveur actuel. Ceci n'est pas prise en charge",
"saveQueueFailed": "échec de l'enregistrement de la file d'attente",
"settingsSyncError": "des incohérences ont été détectées entre les paramètres du moteur de rendu et ceux du processus principal. redémarrez l'application pour appliquer les modifications",
"noNetwork": "serveur indisponible",
@@ -340,7 +340,9 @@
"lyricGap": "espacement des lettres",
"dynamicIsImage": "activer l'image d'arrière-plan",
"dynamicImageBlur": "intensité du flou sur l'image d'arrière-plan",
"lyricOffset": "décalage des paroles (ms)"
"lyricOffset": "décalage des paroles (ms)",
"lyricScaleNonActive": "mise à l'échelle des paroles inactives",
"lyricOpacityNonActive": "opacité des paroles inactive"
},
"upNext": "à suivre",
"lyrics": "paroles",
@@ -477,7 +479,7 @@
"groupingTypePrimary": "types de parution principale",
"topSongsCommunity": "communauté",
"topSongsPersonal": "personnel",
"favoriteSongsFrom": "titres favori de {{title}}"
"favoriteSongsFrom": "titres favoris de {{title}}"
},
"itemDetail": {
"copyPath": "copier le chemin dans le presse-papiers",
@@ -520,7 +522,7 @@
},
"setting": {
"audioDevice_description": "sélectionnez le périphérique audio à utiliser pour la lecture",
"audioExclusiveMode_description": "activer le mode de sortie exclusif. Dans ce mode, le système est généralement verrouillé et seul mpv pourra émettre de l'audio",
"audioExclusiveMode_description": "activer le mode de sortie exclusif. Dans ce mode, le système est généralement verrouillé et seul mpv pourra émettre de l'audio. La capture audio du système de visualisation ne fonctionnera pas lorsque cette option est activée",
"audioPlayer_description": "sélectionnez le lecteur audio à utiliser pour la lecture",
"crossfadeDuration_description": "définit la durée du fondu enchaîné",
"audioDevice": "périphérique audio",
@@ -914,7 +916,7 @@
"ignoreCors": "ignorer cors $t(common.restartRequired)",
"error_savePassword": "une erreur sest produite lors de la tentative de sauvegarde du mot de passe",
"input_preferInstantMix": "préférer le mix instantané",
"input_preferInstantMixDescription": "utiliser uniquement le mix instantané pour jouer des pistes similaires. utile si vous avez des plugins qui modifient ce comportement",
"input_preferInstantMixDescription": "utiliser uniquement le mix instantané pour jouer des titres similaires. utile si vous avez des plugins qui modifient ce comportement",
"input_preferRemoteUrl": "préférer une URL publique",
"input_remoteUrl": "URL publique",
"input_remoteUrlPlaceholder": "optionnel : URL publique pour les fonctionnalités externes"
@@ -1388,6 +1390,7 @@
"systemAudioConsentBody": "Le visualiseur nécessite un accès au système audio pour fonctionner",
"systemAudioConsentDecline": "Refuser",
"systemAudioConsentTitle": "Permettre l'accès au système audio?",
"systemAudioCaptureFailed": "Impossible de démarrer la capture : {{message}}"
"systemAudioCaptureFailed": "Impossible de démarrer la capture : {{message}}",
"systemAudioExclusiveModeNotSupported": "Le visualiseur est indisponible lorsque le mode audio exclusif est activé. Désactivez ce mode dans les paramètres MPV et réessayez."
}
}
+22 -6
View File
@@ -143,7 +143,7 @@
"doNotShowAgain": "niet opnieuw tonen",
"externalLinks": "externe links",
"faster": "sneller",
"preview": "voorvertoning",
"preview": "voorbeeld",
"private": "privé",
"public": "publiekelijk",
"recordLabel": "platenlabel",
@@ -152,7 +152,7 @@
"sort": "sorteer",
"trackGain": "trackvolume",
"trackPeak": "piekniveau",
"clean": "schoon",
"clean": "opschonen",
"gridRows": "rasterrijen",
"tableColumns": "tabelkolommen",
"itemsMore": "{{count}} meer",
@@ -283,7 +283,9 @@
"unsynchronized": "niet gesynchronizeerd",
"useImageAspectRatio": "gebruik aspect ratio van de afbeelding",
"lyricOffset": "songtekst-vertraging (ms)",
"showLyricProvider": "toon songtekstaanbieder"
"showLyricProvider": "toon songtekstaanbieder",
"lyricOpacityNonActive": "opaciteit van niet-actieve songtekst",
"lyricScaleNonActive": "schaal niet-actieve songtekst"
},
"lyrics": "liedtekst",
"related": "gerelateerd",
@@ -680,7 +682,7 @@
"artistReleaseTypeConfiguration_description": "configureer welke uitgavesoorten worden getoond op de albumartiestpagina en in welke volgorde",
"audioDevice_description": "kies het audioapparaat dat wordt gebruikt om af te spelen",
"audioDevice": "audioapparaat",
"audioExclusiveMode_description": "schakel exclusieve uitvoermodus in. In deze modus wordt het systeem normaliter uitgesloten en zal enkel mpv audio kunnen uitvoeren",
"audioExclusiveMode_description": "schakel exclusieve uitvoermodus in. in deze modus wordt het systeem normaliter uitgesloten en zal enkel mpv audio kunnen uitvoeren. geluidsopname in de visualiseerder zal niet werken als dit is ingeschakeld",
"audioExclusiveMode": "audio-exclusieve modus",
"audioPlayer_description": "kies de audiospeler om te gebruiken bij het afspelen",
"audioPlayer": "audiospeler",
@@ -985,7 +987,11 @@
"sidePlayQueueLayout": "uitlijning afspeelwachtrij aan zijkant",
"sidePlayQueueLayout_description": "stel de uitlijning in voor de afspeelwachtrij die aan de zijkant is gekoppeld",
"sidePlayQueueLayout_optionHorizontal": "horizontaal",
"sidePlayQueueLayout_optionVertical": "verticaal"
"sidePlayQueueLayout_optionVertical": "verticaal",
"playerbarWaveformStretch": "rekking van golfvorm",
"playerbarWaveformStretch_description": "rekt de golfvorm om de beschikbare ruimte op te vullen",
"waveformLoadingDelay": "laadvertraging van golfvorm",
"waveformLoadingDelay_description": "vertraging in seconden voordat de golfvorm wordt geladen. vergroot deze waarde als je stotteringen ervaart bij gebruik van de webspeler."
},
"form": {
"addServer": {
@@ -1094,6 +1100,9 @@
},
"saveQueue": {
"success": "wachtrij opgeslagen op server"
},
"editRadioStation": {
"success": "radiozender succesvol bijgewerkt"
}
},
"player": {
@@ -1353,6 +1362,13 @@
"d": "D",
"z": "Z"
}
}
},
"systemAudioConsentAllow": "Toestaan",
"systemAudioConsentBody": "De visualiseerder heeft toegang tot het audiosysteem nodig om te werken",
"systemAudioConsentDecline": "Weigeren",
"systemAudioConsentTitle": "Toegang tot audiosysteem toestaan?",
"systemAudioCaptureFailed": "Kon opname niet starten: {{message}}",
"systemAudioNoAudioTrack": "Geen geluidsspoor ontvangen. Verifieer dat audio-opname is ingeschakeld als daar om wordt gevraagd.",
"systemAudioExclusiveModeNotSupported": "De visualiseerder is niet beschikbaar als audio-exclusieve modus is ingeschakeld. Schakel audio-exclusieve modus in de MPV-instellingen uit en probeer het opnieuw."
}
}
+13 -5
View File
@@ -352,7 +352,8 @@
"input_skipDuplicates": "pomiń duplikaty",
"input_playlists": "$t(entity.playlist, {\"count\": 2})",
"create": "utwórz $t(entity.playlist, {\"count\": 1}) {{playlist}}",
"searchOrCreate": "wyszukaj $t(entity.playlist, {\"count\": 2}) lub wpisz, aby utworzyć nową"
"searchOrCreate": "wyszukaj $t(entity.playlist, {\"count\": 2}) lub wpisz, aby utworzyć nową",
"noneAdded": "nie dodano utworów do $t(entity.playlist, {\"count\": 1}) '{{playlist}}'"
},
"updateServer": {
"title": "uaktualnij serwer",
@@ -442,7 +443,9 @@
"lyricGap": "odstępy tekstu",
"dynamicImageBlur": "rozmiar rozmycia obrazu",
"dynamicIsImage": "włącz obraz w tle",
"lyricOffset": "opóźnienie tekstów (ms)"
"lyricOffset": "opóźnienie tekstów (ms)",
"lyricOpacityNonActive": "przezroczystość nieaktywnego tekstu",
"lyricScaleNonActive": "skala nieaktywnego tekstu"
},
"upNext": "następne",
"lyrics": "tekst",
@@ -702,7 +705,7 @@
"hotkey_favoriteCurrentSong": "ulubiona $t(common.currentSong)",
"hotkey_zoomIn": "przybliż",
"hotkey_browserForward": "przeglądarka w przód",
"audioExclusiveMode_description": "włącz wyłączny tryb wyjścia. W tym trybie, system zwykle jest zablokowany i może odtwarzać tylko poprzez mpv",
"audioExclusiveMode_description": "włącz wyłączny tryb wyjścia. W tym trybie, system zwykle jest zablokowany i może odtwarzać tylko poprzez mpv. wizualizer i przechwytywanie audio przez sysytem nie będzie działać gdy jest to włączone",
"discordUpdateInterval": "{{discord}} interwał aktualizacji rich presence",
"fontType_optionBuiltIn": "wbudowana czcionka",
"hotkey_playbackPlayPause": "odtwarzaj / wstrzymaj",
@@ -1064,7 +1067,11 @@
"sidePlayQueueLayout_optionHorizontal": "poziomy",
"sidePlayQueueLayout_optionVertical": "pionowy",
"waveformLoadingDelay": "opóźnienie załadowania fali",
"waveformLoadingDelay_description": "opóźnienie w sekundach przed załadowaniem fali. zwiększ tą wartość jeżeli doświadczasz zawieszania się odtwarzacza przeglądarkowego."
"waveformLoadingDelay_description": "opóźnienie w sekundach przed załadowaniem fali. zwiększ tą wartość jeżeli doświadczasz zawieszania się odtwarzacza przeglądarkowego.",
"playerbarWaveformStretch": "rozciąganie przebiegu",
"playerbarWaveformStretch_description": "rozciąga przebieg aby wypełnić dostępną przestrzeń",
"preventSuspendOnPlayback_description": "powstrzymuj wstrzymanie podczas odtwarzania muzyki",
"preventSuspendOnPlayback": "powstrzymuje wstrzymanie przy odtwarzaniu"
},
"table": {
"config": {
@@ -1387,6 +1394,7 @@
"systemAudioConsentDecline": "Odmów",
"systemAudioConsentTitle": "Przyznać dostęp do audio systemu?",
"systemAudioCaptureFailed": "Nie udało się rozpocząć przechwytywania: {{message}}",
"systemAudioNoAudioTrack": "Nie została zwrócona żadna ścieżka audio. Sprawdź czy przechwytywanie audio będzie włączone, gdy będzie o to prośba."
"systemAudioNoAudioTrack": "Nie została zwrócona żadna ścieżka audio. Sprawdź czy przechwytywanie audio będzie włączone, gdy będzie o to prośba.",
"systemAudioExclusiveModeNotSupported": "Wizualizer jest niedostępny w gdy tryb wyłącznie z dźwiękiek jest włączony. Wyłącz tryb wyłącznie audio w ustawieniach MPV i spróbuj ponownie."
}
}
+14 -3
View File
@@ -91,7 +91,7 @@
"share": "compartilhar",
"close": "fechar",
"translation": "tradução",
"albumGain": "ganho do album",
"albumGain": "ganho do álbum",
"trackPeak": "peak da faixa",
"albumPeak": "pico do álbum",
"trackGain": "ganho da faixa",
@@ -121,10 +121,21 @@
"removeFromFavorites": "remover de $t(entity.favorite, {\"count\": 2})",
"openIn": {
"lastfm": "Abrir em Last.fm",
"musicbrainz": "Abrir em MusicBrainz"
"musicbrainz": "Abrir em MusicBrainz",
"listenbrainz": "Abrir no ListenBrainz",
"qobuz": "Abrir no Qobuz"
},
"toggleSmartPlaylistEditor": "alternar editor $t(entity.smartPlaylist)",
"moveToNext": "mover para o próximo"
"moveToNext": "mover para o próximo",
"addOrRemoveFromSelection": "adicionar ou remover da seleção",
"goToCurrent": "ir para o item atual",
"createRadioStation": "criar $t(entity.radioStation, {\"count\": 1})",
"deleteRadioStation": "deletar $t(entity.radioStation, {\"count\": 1})",
"selectAll": "selecionar tudo",
"holdToMoveToTop": "segure para ir ao topo",
"moveItems": "mover itens",
"viewMore": "ver mais",
"openApplicationDirectory": "abrir a pasta do aplicativo"
},
"form": {
"deletePlaylist": {
+47 -13
View File
@@ -18,8 +18,11 @@
"toggleSmartPlaylistEditor": "вкл./откл. редактор $t(entity.smartPlaylist)",
"removeFromFavorites": "удалить из $t(entity.favorite, {\"count\": 2})",
"openIn": {
"lastfm": "открыть на Last.fm",
"musicbrainz": "открыть на MusicBrainz"
"lastfm": "Открыть на Last.fm",
"musicbrainz": "открыть на MusicBrainz",
"listenbrainz": "Открыть на ListenBrainz",
"qobuz": "Открыть в Qobuz",
"spotify": "Открыть в Spotify"
},
"moveToNext": "следующий",
"addOrRemoveFromSelection": "добавить или удалить из выделения",
@@ -167,7 +170,8 @@
"explicit": "нецензурная лексика",
"externalLinks": "внешние ссылки",
"explicitStatus": "признак нецензурного контента",
"newVersionAvailable": "доступна новая версия"
"newVersionAvailable": "доступна новая версия",
"numberOfResults": "{{numberOfResults}} результатов"
},
"entity": {
"album_one": "альбом",
@@ -435,7 +439,17 @@
"home": "$t(common.home)",
"myLibrary": "Моя библиотека",
"shared": "Публичные плейлисты $t(entity.playlist, {\"count\": 2})",
"collections": "коллекции"
"collections": "коллекции",
"albumArtists": "$t(entity.albumArtist, {\"count\": 2})",
"albums": "$t(entity.album, {\"count\": 2})",
"artists": "$t(entity.artist, {\"count\": 2})",
"favorites": "$t(entity.favorite, {\"count\": 2})",
"folders": "$t(entity.folder, {\"count\": 2})",
"genres": "$t(entity.genre, {\"count\": 2})",
"radio": "$t(entity.radioStation, {\"count\": 2})",
"playlists": "$t(entity.playlist, {\"count\": 2})",
"settings": "$t(common.setting, {\"count\": 2})",
"tracks": "$t(entity.track, {\"count\": 2})"
},
"fullscreenPlayer": {
"config": {
@@ -452,7 +466,9 @@
"useImageAspectRatio": "использовать соотношение сторон изображения",
"lyricGap": "пробел между словами",
"dynamicIsImage": "включить фоновое изображение",
"dynamicImageBlur": "сила размытия изображения"
"dynamicImageBlur": "сила размытия изображения",
"lyricOpacityNonActive": "непрозрачность неактивных слов",
"lyricScaleNonActive": "размер неактивных слов"
},
"upNext": "играет",
"lyrics": "слова",
@@ -475,7 +491,8 @@
"selectMusicFolder": "выбрать папку с музыкой",
"noMusicFolder": "папка с музыкой не выбрана",
"multipleMusicFolders": "{{count}} выбрано музыкальных папок",
"commandPalette": "открыть командную строку"
"commandPalette": "открыть командную строку",
"settings": "$t(common.setting, {\"count\": 2})"
},
"manageServers": {
"title": "сервера",
@@ -510,7 +527,8 @@
"goTo": "перейти в",
"moveToNext": "$t(action.moveToNext)",
"playShuffled": "$t(player.shuffle)",
"moveItems": "$t(action.moveItems)"
"moveItems": "$t(action.moveItems)",
"playSimilarSongs": "$t(player.playSimilarSongs)"
},
"home": {
"mostPlayed": "слушают чаще всего",
@@ -518,7 +536,8 @@
"title": "$t(common.home)",
"explore": "откройте новое",
"recentlyPlayed": "игралось недавно",
"recentlyReleased": "Новинки"
"recentlyReleased": "Новинки",
"genres": "$t(entity.genre, {\"count\": 2})"
},
"albumDetail": {
"moreFromArtist": "больше от $t(entity.artist, {\"count\": 1})",
@@ -552,10 +571,13 @@
},
"genreList": {
"showAlbums": "показать $t(entity.genre, {\"count\": 1}) $t(entity.album, {\"count\": 2})",
"showTracks": "показать $t(entity.genre, {\"count\": 1}) $t(entity.track, {\"count\": 2})"
"showTracks": "показать $t(entity.genre, {\"count\": 1}) $t(entity.track, {\"count\": 2})",
"title": "$t(entity.genre, {\"count\": 2})"
},
"trackList": {
"artistTracks": "Треки {{artist}}"
"artistTracks": "Треки {{artist}}",
"genreTracks": "\"{{genre}}\" $t(entity.track, {\"count\": 2})",
"title": "$t(entity.track, {\"count\": 2})"
},
"globalSearch": {
"commands": {
@@ -616,6 +638,12 @@
},
"favorites": {
"title": "$t(entity.favorite, {\"count\": 2})"
},
"folderList": {
"title": "$t(entity.folder, {\"count\": 2})"
},
"playlistList": {
"title": "$t(entity.playlist, {\"count\": 2})"
}
},
"form": {
@@ -656,7 +684,8 @@
"input_skipDuplicates": "не добавлять дубликаты",
"create": "создать $t(entity.playlist, {\"count\": 1}) {{playlist}}",
"searchOrCreate": "для создания нового списка выполните поиск по $t(entity.playlist, {\"count\": 2}) или введите соответствующий текст",
"input_playlists": "$t(entity.playlist, {\"count\": 2})"
"input_playlists": "$t(entity.playlist, {\"count\": 2})",
"noneAdded": "в плейлист $t(entity.playlist, {\"count\": 1}) '{{playlist}}' не было добавлено ни одного трека"
},
"updateServer": {
"title": "обновление сервера",
@@ -725,6 +754,9 @@
"input_played_optionUnplayed": "только не игранные треки",
"input_played_optionPlayed": "только воспроизведённые треки",
"input_genre": "$t(entity.genre, {\"count\": 1})"
},
"editRadioStation": {
"success": "радиостанция успешно обновлена"
}
},
"setting": {
@@ -1053,7 +1085,8 @@
"audioFadeOnStatusChange": "плавное изменение звука",
"audioFadeOnStatusChange_description": "включает эффекты затухания и появления звука при изменении статуса (пауза/проигрывание)",
"preventSleepOnPlayback_description": "запрещает спящий режим экрана, пока играет музыка",
"preventSleepOnPlayback": "не переходить в спящий режим"
"preventSleepOnPlayback": "не переходить в спящий режим",
"discordLinkType_none": "$t(common.none)"
},
"releaseType": {
"secondary": {
@@ -1074,7 +1107,8 @@
"other": "другие",
"broadcast": "транслировать",
"ep": "эп",
"single": "сингл"
"single": "сингл",
"album": "$t(entity.album, {\"count\": 1})"
}
},
"datetime": {
+1
View File
@@ -0,0 +1 @@
{}
+66 -6
View File
@@ -36,7 +36,10 @@
"openApplicationDirectory": "відкрити каталог додатків",
"openIn": {
"lastfm": "Відкрити в Last.fm",
"musicbrainz": "Відкрити в MusicBrainz"
"musicbrainz": "Відкрити в MusicBrainz",
"listenbrainz": "Відкрити у ListenBrainz",
"qobuz": "Відкрити у Qobuz",
"spotify": "Відкрити у Spotify"
}
},
"common": {
@@ -164,7 +167,9 @@
"explicit": "Експліцитний зміст",
"gridRows": "рядки сітки",
"tableColumns": "стовпці таблиці",
"itemsMore": "{{count}} більше"
"itemsMore": "{{count}} більше",
"numberOfResults": "{{numberOfResults}} результатів",
"newVersionAvailable": "доступна нова версія"
},
"entity": {
"album_one": "альбом",
@@ -255,7 +260,9 @@
"serverRequired": "потрібен сервер",
"sessionExpiredError": "ваша сесія закінчилася",
"systemFontError": "сталася помилка під час спроби отримати системні шрифти",
"settingsSyncError": "виявлено розбіжності між налаштуваннями в рендерері та основним процесом. перезапустіть програму, щоб застосувати зміни"
"settingsSyncError": "виявлено розбіжності між налаштуваннями в рендерері та основним процесом. перезапустіть програму, щоб застосувати зміни",
"invalidJson": "недійсний JSON",
"playbackPausedDueToError": "відтворення було призупинено через помилку"
},
"filter": {
"album": "$t(entity.album, {\"count\": 1})",
@@ -281,7 +288,7 @@
"isPublic": "є публічним",
"isRated": "є оціненим",
"isRecentlyPlayed": "нещодавно відтворено",
"lastPlayed": "нещодавно відтворені",
"lastPlayed": "останнє відтворене",
"mostPlayed": "найбільш відтворювані",
"name": "назва",
"note": "примітка",
@@ -301,7 +308,9 @@
"title": "назва",
"toYear": "до року",
"trackNumber": "трек",
"explicitStatus": "$t(common.explicitStatus)"
"explicitStatus": "$t(common.explicitStatus)",
"matchAnd": "і",
"matchOr": "або"
},
"datetime": {
"minuteShort": "хв.",
@@ -414,7 +423,9 @@
"setExpiration": "встановити термін дії",
"success": "посилання для спільного використання скопійовано в буфер обміну (натисніть тут, щоб відкрити)",
"expireInvalid": "термін дії повинен бути в майбутньому",
"createFailed": "не вдалося створити спільний доступ (чи ввімкнено спільний доступ?)"
"createFailed": "не вдалося створити спільний доступ (чи ввімкнено спільний доступ?)",
"copyToClipboard": "Скопіювати до буфера обміну: Ctrl+C, Enter",
"successMustClick": "посилання успішно створено, натисніть сюди, щоб відкрити"
},
"shuffleAll": {
"title": "відтворити випадково",
@@ -435,6 +446,9 @@
"enabled": "приватний режим увімкнено, стан відтворення тепер приховано від зовнішніх інтеграцій",
"disabled": "приватний режим вимкнено, стан відтворення тепер видно для увімкнених зовнішніх інтеграцій",
"title": "приватний режим"
},
"editRadioStation": {
"success": "радіо станція успішно оновлена"
}
},
"player": {
@@ -539,6 +553,52 @@
"goToAlbum": "перейти до $t(entity.album, {\"count\": 1})",
"goToAlbumArtist": "перейти до $t(entity.albumArtist, {\"count\": 1})",
"showDetails": "отримати інформацію"
},
"fullscreenPlayer": {
"config": {
"dynamicBackground": "динамічний фон",
"dynamicImageBlur": "розмір розмиття зображення",
"dynamicIsImage": "включити фонове зображення",
"followCurrentLyric": "слідкувати за поточним рядком",
"lyricAlignment": "вирівнювання тексту",
"lyricOffset": "затримка тексту (мс)",
"lyricGap": "розмір між рядками",
"lyricSize": "розмір тексту",
"opacity": "непрозорість",
"showLyricMatch": "показувати збіг тексту пісень",
"showLyricProvider": "показувати джерело тексту пісень",
"synchronized": "синхронізовано",
"unsynchronized": "несинхронізовано",
"useImageAspectRatio": "використовувати співвідношення сторін зображення"
},
"lyrics": "текст пісні",
"related": "пов'язані",
"upNext": "далі",
"visualizer": "візуалізатор",
"noLyrics": "текст пісні не знайдено"
},
"genreList": {
"showAlbums": "показати $t(entity.genre, {\"count\": 1}) $t(entity.album, {\"count\": 2})",
"showTracks": "показати $t(entity.genre, {\"count\": 1}) $t(entity.track, {\"count\": 2})",
"title": "$t(entity.genre, {\"count\": 2})"
},
"folderList": {
"title": "$t(entity.folder, {\"count\": 2})"
},
"globalSearch": {
"commands": {
"goToPage": "перейти до сторінки",
"searchFor": "шукати на {{query}}",
"serverCommands": "команди сервера"
},
"title": "команди"
},
"home": {
"explore": "дослідити з вашої бібліотеки",
"genres": "$t(entity.genre, {\"count\": 2})",
"mostPlayed": "найбільш відтворені",
"newlyAdded": "нещодавно додані релізи",
"recentlyPlayed": "нещодавно відтворені"
}
}
}
+3
View File
@@ -1015,6 +1015,9 @@
"input_played_optionPlayed": "仅已播放的曲目",
"input_limit": "有多少首歌?",
"input_played": "播放筛选器"
},
"editRadioStation": {
"success": "电台更新成功"
}
},
"table": {
+13 -5
View File
@@ -233,7 +233,9 @@
"showLyricMatch": "顯示匹配的歌詞",
"dynamicImageBlur": "圖片模糊大小",
"dynamicIsImage": "啟用背景圖片",
"lyricOffset": "歌詞偏移時間 (ms)"
"lyricOffset": "歌詞偏移時間 (ms)",
"lyricOpacityNonActive": "非活躍歌詞的不透明度",
"lyricScaleNonActive": "非活躍歌詞的比例"
},
"lyrics": "歌詞",
"related": "相關",
@@ -433,7 +435,7 @@
"audioDevice": "音訊設備",
"audioDevice_description": "選擇用於播放的音訊設備",
"audioExclusiveMode": "音訊獨佔模式",
"audioExclusiveMode_description": "啟用獨佔輸出模式。在此模式下,系統通常被鎖定,只有 mpv 能夠輸出音訊",
"audioExclusiveMode_description": "啟用獨佔輸出模式。在此模式下,系統通常被鎖定,只有 mpv 能夠輸出音訊。視覺化音訊截取在此選項啟用時不會作用",
"audioPlayer": "音訊播放器",
"crossfadeDuration": "淡入淡出持續時間",
"crossfadeDuration_description": "設定淡入淡出持續時間",
@@ -792,7 +794,11 @@
"qobuz_description": "在藝術家/專輯頁面上顯示 Qobuz 的連結",
"qobuz": "顯示 Qobuz 連結",
"waveformLoadingDelay": "波形載入延遲",
"waveformLoadingDelay_description": "載入波形前的延遲(以秒為單位)。如果您在使用網頁播放器時遇到卡頓,請增加此值。"
"waveformLoadingDelay_description": "載入波形前的延遲(以秒為單位)。如果您在使用網頁播放器時遇到卡頓,請增加此值。",
"playerbarWaveformStretch": "波形拉伸",
"playerbarWaveformStretch_description": "拉伸波形來填補可用空間",
"preventSuspendOnPlayback_description": "音樂播放時防止應用程式進入休眠",
"preventSuspendOnPlayback": "在播放時防止應用程式暫停"
},
"table": {
"config": {
@@ -1042,7 +1048,8 @@
"success": "新增 $t(entity.trackWithCount, {\"count\": {{message}} }) 到 $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
"title": "新增到$t(entity.playlist, {\"count\": 1})",
"create": "建立 $t(entity.playlist, {\"count\": 1}) {{playlist}}",
"searchOrCreate": "搜尋$t(entity.playlist, {\"count\": 2}) 或輸入內容以建立新項目"
"searchOrCreate": "搜尋$t(entity.playlist, {\"count\": 2}) 或輸入內容以建立新項目",
"noneAdded": "沒有曲目新增到 $t(entity.playlist, {\"count\": 1}) '{{playlist}}'"
},
"createPlaylist": {
"input_description": "$t(common.description)",
@@ -1341,6 +1348,7 @@
"systemAudioConsentAllow": "允許",
"systemAudioConsentBody": "此視覺化器需要存取系統音訊才能運作",
"systemAudioConsentDecline": "拒絕",
"systemAudioConsentTitle": "允許存取系統音訊?"
"systemAudioConsentTitle": "允許存取系統音訊?",
"systemAudioExclusiveModeNotSupported": "啟用音訊獨佔模式時,視覺化不可用。 在MPV設定中停用音訊獨佔模式,然後再試一次。"
}
}
+30 -3
View File
@@ -1,13 +1,13 @@
import console from 'console';
import { app, ipcMain } from 'electron';
import { rm } from 'fs/promises';
import { access, rm } from 'fs/promises';
import uniq from 'lodash/uniq';
import MpvAPI from 'node-mpv';
import { pid } from 'node:process';
import process from 'process';
import { getMainWindow, sendToastToRenderer } from '../../../index';
import { createLog, isWindows } from '../../../utils';
import { createLog, isMacOS, isWindows } from '../../../utils';
import { store } from '../settings';
import { PlayerData } from '/@/shared/types/domain-types';
@@ -69,6 +69,7 @@ const mpvLog = (
};
const MPV_BINARY_PATH = store.get('mpv_path') as string | undefined;
const MACOS_MPV_BINARY_PATHS = ['/opt/homebrew/bin/mpv', '/usr/local/bin/mpv'];
const prefetchPlaylistParams = [
'--prefetch-playlist=no',
@@ -86,12 +87,38 @@ const DEFAULT_MPV_PARAMETERS = (extraParameters?: string[]) => {
return parameters;
};
const resolveMpvBinaryPath = async (binaryPath?: string) => {
if (binaryPath) {
return binaryPath;
}
if (MPV_BINARY_PATH) {
return MPV_BINARY_PATH;
}
if (!isMacOS()) {
return undefined;
}
for (const candidate of MACOS_MPV_BINARY_PATHS) {
try {
await access(candidate);
return candidate;
} catch {
// Try the next common Homebrew location.
}
}
return undefined;
};
const createMpv = async (data: {
binaryPath?: string;
extraParameters?: string[];
properties?: Record<string, any>;
}): Promise<MpvAPI> => {
const { binaryPath, extraParameters, properties } = data;
const resolvedBinaryPath = await resolveMpvBinaryPath(binaryPath);
const params = uniq([...DEFAULT_MPV_PARAMETERS(extraParameters), ...(extraParameters || [])]);
@@ -99,7 +126,7 @@ const createMpv = async (data: {
{
audio_only: true,
auto_restart: false,
binary: binaryPath || MPV_BINARY_PATH || undefined,
binary: resolvedBinaryPath,
socket: socketPath,
time_update: 1,
},
+8 -1
View File
@@ -1,2 +1,9 @@
import './core';
import(`./${process.platform}`);
if (process.platform === 'linux') {
import('./linux');
} else if (process.platform === 'darwin') {
import('./darwin');
} else if (process.platform === 'win32') {
import('./win32');
}
+1
View File
@@ -0,0 +1 @@
export {};
+25 -3
View File
@@ -5,6 +5,7 @@ import {
app,
BrowserWindow,
BrowserWindowConstructorOptions,
desktopCapturer,
globalShortcut,
ipcMain,
Menu,
@@ -733,7 +734,26 @@ async function createWindow(first = true): Promise<void> {
});
mainWindow.webContents.session.setDisplayMediaRequestHandler((_request, callback) => {
callback({ audio: 'loopback' });
if (!isMacOS()) {
callback({ audio: 'loopback' });
return;
}
desktopCapturer
.getSources({ thumbnailSize: { height: 0, width: 0 }, types: ['screen'] })
.then((sources) => {
const source = sources[0];
if (!source) {
callback({});
return;
}
callback({ audio: 'loopback', video: source });
})
.catch((err) => {
log.warn('desktopCapturer.getSources failed', err);
callback({});
});
});
if (!disableAutoUpdates() && store.get('disable_auto_updates') !== true) {
@@ -911,12 +931,14 @@ ipcMain.on(
},
);
ipcMain.handle('power-save-blocker-start', () => {
ipcMain.handle('power-save-blocker-start', (_event, { full }: { full: boolean }) => {
if (powerSaveBlockerId !== null) {
return powerSaveBlockerId;
}
powerSaveBlockerId = powerSaveBlocker.start('prevent-display-sleep');
powerSaveBlockerId = powerSaveBlocker.start(
full ? 'prevent-display-sleep' : 'prevent-app-suspension',
);
return powerSaveBlockerId;
});
+2 -1
View File
@@ -1,4 +1,5 @@
import { SetActivity } from '@xhayper/discord-rpc';
import type { SetActivity } from '@xhayper/discord-rpc';
import { ipcRenderer } from 'electron';
const initialize = (clientId: string) => {
@@ -409,6 +409,8 @@ export const JellyfinController: InternalControllerEndpoint = {
return jfNormalize.album(
{ ...res.body, Songs: songsRes.body.Items },
apiClientProps.server,
args.context?.pathReplace,
args.context?.pathReplaceWith,
);
},
getAlbumList: async (args) => {
@@ -580,7 +582,8 @@ export const JellyfinController: InternalControllerEndpoint = {
return `${apiClientProps.server?.url}/items/${query.id}/download?apiKey=${apiClientProps.server?.credential}`;
},
getFolder: async ({ apiClientProps, query }) => {
getFolder: async (args) => {
const { apiClientProps, query } = args;
const userId = apiClientProps.server?.userId;
if (!userId) throw new Error('No userId found');
@@ -742,6 +745,8 @@ export const JellyfinController: InternalControllerEndpoint = {
jfNormalize.song(
item as unknown as z.infer<typeof jfType._response.song>,
apiClientProps.server,
args.context?.pathReplace,
args.context?.pathReplaceWith,
),
);
@@ -496,7 +496,12 @@ export const NavidromeController: InternalControllerEndpoint = {
}
return res.body.similarSongs.song.map((song) =>
ssNormalize.song(song, apiClientProps.server),
ssNormalize.song(
song,
apiClientProps.server,
args.context?.pathReplace,
args.context?.pathReplaceWith,
),
);
},
getArtistList: async (args) => {
@@ -566,7 +571,12 @@ export const NavidromeController: InternalControllerEndpoint = {
}
return res.body.similarSongs2.song.map((song) =>
ssNormalize.song(song, apiClientProps.server),
ssNormalize.song(
song,
apiClientProps.server,
args.context?.pathReplace,
args.context?.pathReplaceWith,
),
);
},
getDownloadUrl: SubsonicController.getDownloadUrl,
@@ -823,7 +833,14 @@ export const NavidromeController: InternalControllerEndpoint = {
return (
(res.body.similarSongs?.song || [])
.filter((song) => song.id !== query.songId)
.map((song) => ssNormalize.song(song, apiClientProps.server)) || []
.map((song) =>
ssNormalize.song(
song,
apiClientProps.server,
args.context?.pathReplace,
args.context?.pathReplaceWith,
),
) || []
);
},
getSongDetail: async (args) => {
@@ -1022,6 +1039,7 @@ export const NavidromeController: InternalControllerEndpoint = {
const res = await NavidromeController.getSongList({
apiClientProps,
context: args.context,
query: {
artistIds: [query.artistId],
sortBy: SongListSort.PLAY_COUNT,
+30 -3
View File
@@ -1,6 +1,5 @@
import { initClient, initContract } from '@ts-rest/core';
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse, isAxiosError } from 'axios';
import omitBy from 'lodash/omitBy';
import qs from 'qs';
import { z } from 'zod';
@@ -377,11 +376,39 @@ axiosClient.interceptors.response.use(
},
);
const keysToSkipEmptyCheck = new Set([
'artist',
'comment',
'genre',
'name',
'query',
'u',
'username',
]);
const parsePath = (fullPath: string) => {
const [path, params] = fullPath.split('?');
const parsedParams = qs.parse(params, { arrayLimit: 99999, parameterLimit: 99999 });
const notNilParams = omitBy(parsedParams, (value) => value === 'undefined' || value === 'null');
const url = new URLSearchParams(params);
const notNilParams: Record<string, string[]> = {};
for (const [key, value] of url) {
if (!keysToSkipEmptyCheck.has(key) && (value === 'undefined' || value === 'null')) {
continue;
}
let realKey = key;
if (key.includes('[') && key.includes(']')) {
realKey = key.split('[')[0];
}
if (realKey in notNilParams) {
notNilParams[realKey].push(value);
} else {
notNilParams[realKey] = [value];
}
}
return {
params: notNilParams,
@@ -2125,6 +2125,7 @@ export const SubsonicController: InternalControllerEndpoint = {
const res = await SubsonicController.getSongList({
apiClientProps,
context,
query: {
artistIds: [query.artistId],
sortBy: SongListSort.PLAY_COUNT,
+2 -7
View File
@@ -22,12 +22,7 @@ import { WebAudio } from '/@/shared/types/types';
import '/@/shared/styles/global.css';
import { PlayerProvider } from '/@/renderer/features/player/context/player-context';
import { AudioPlayers } from '/@/renderer/features/player/components/audio-players';
const ReleaseNotesModal = lazy(() =>
import('./release-notes-modal').then((module) => ({
default: module.ReleaseNotesModal,
})),
);
import { ReleaseNotesModal } from '/@/renderer/release-notes-modal';
const UpdateAvailableDialog = lazy(() =>
import('./update-available-dialog').then((module) => ({
@@ -82,8 +77,8 @@ const AppShell = memo(function AppShell() {
<AppRouter />
</PlayerProvider>
</WebAudioContext.Provider>
<ReleaseNotesModal />
<Suspense fallback={null}>
<ReleaseNotesModal />
<UpdateAvailableDialog />
</Suspense>
</>
@@ -6,8 +6,8 @@ import {
ItemListStateItemWithRequiredProperties,
} from '/@/renderer/components/item-list/helpers/item-list-state';
import { ItemControls } from '/@/renderer/components/item-list/types';
import { useHotkeys } from '/@/renderer/hooks/use-hotkeys';
import { useHotkeySettings, usePlayButtonBehavior } from '/@/renderer/store';
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
import { LibraryItem } from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types';
@@ -385,8 +385,8 @@ const BaseItemGridList = ({
rows,
size = 'default',
}: ItemGridListProps) => {
const rootRef = useRef(null);
const outerRef = useRef(null);
const rootRef = useRef<HTMLDivElement | null>(null);
const outerRef = useRef<HTMLDivElement | null>(null);
const listRef = useRef<FixedSizeList<GridItemProps>>(null);
const { ref: containerRef, width: containerWidth } = useElementSize();
const { focused, ref: containerFocusRef } = useFocusWithin();
@@ -486,7 +486,7 @@ const BaseItemGridList = ({
}, [itemsPerRow, rows?.length, size]);
useLayoutEffect(() => {
const { current: container } = containerRef;
const container = rootRef.current;
if (!container) return;
throttledSetTableMeta(containerWidth, resolvedItemCount, (meta) => {
@@ -500,13 +500,15 @@ const BaseItemGridList = ({
current.rowCount !== meta.rowCount
) {
tableMetaRef.current = meta;
container.style.setProperty('--grid-column-count', String(meta.columnCount));
container.style.setProperty('--grid-item-height', `${meta.itemHeight}px`);
container.style.setProperty('--grid-row-count', String(meta.rowCount));
const el = rootRef.current;
if (!el) return;
el.style.setProperty('--grid-column-count', String(meta.columnCount));
el.style.setProperty('--grid-item-height', `${meta.itemHeight}px`);
el.style.setProperty('--grid-row-count', String(meta.rowCount));
setTableMetaVersion((v) => v + 1);
}
});
}, [containerWidth, resolvedItemCount, throttledSetTableMeta, containerRef]);
}, [containerWidth, resolvedItemCount, throttledSetTableMeta]);
const controls = useDefaultItemListControls({
enableMultiSelect,
@@ -31,6 +31,7 @@ import {
} from '/@/renderer/features/shared/components/list-sort-by-dropdown';
import { ListSortOrderToggleButtonControlled } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
import { FILTER_KEYS, searchLibraryItems } from '/@/renderer/features/shared/utils';
import { useHotkeys } from '/@/renderer/hooks/use-hotkeys';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer, usePlayerSong } from '/@/renderer/store';
import { useExternalLinks, useSettingsStore } from '/@/renderer/store/settings.store';
@@ -49,7 +50,6 @@ import { Stack } from '/@/shared/components/stack/stack';
import { TextInput } from '/@/shared/components/text-input/text-input';
import { Text } from '/@/shared/components/text/text';
import { useDebouncedValue } from '/@/shared/hooks/use-debounced-value';
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
import {
Album,
AlbumListSort,
@@ -43,6 +43,7 @@ import { searchLibraryItems } from '/@/renderer/features/shared/utils';
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
import { useContainerQuery } from '/@/renderer/hooks';
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
import { useHotkeys } from '/@/renderer/hooks/use-hotkeys';
import { AppRoute } from '/@/renderer/router/routes';
import {
ArtistItem,
@@ -75,7 +76,6 @@ import { TextInput } from '/@/shared/components/text-input/text-input';
import { TextTitle } from '/@/shared/components/text-title/text-title';
import { Text } from '/@/shared/components/text/text';
import { useDebouncedValue } from '/@/shared/hooks/use-debounced-value';
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
import {
Album,
@@ -1214,7 +1214,7 @@ export const AlbumArtistDetailContent = ({
artistSongsLink={artistSongsLink}
onArtistRadio={handleArtistRadio}
/>
<Grid gutter="2xl">
<Grid gap="2xl">
<AlbumArtistMetadataGenres
genres={detailQuery.data?.genres}
order={genresOrder}
@@ -13,6 +13,15 @@ export type GroupingType = 'all' | 'primary';
const PRIMARY_RELEASE_TYPES = ['album', 'broadcast', 'ep', 'other', 'single'];
const getNormalizedReleaseTypes = (album: Album): string[] => {
const rawReleaseTypes = [...(album.releaseTypes || []), album.releaseType || ''];
const normalizedReleaseTypes = rawReleaseTypes
.map((type) => type.trim().toLowerCase())
.filter(Boolean);
return [...new Set(normalizedReleaseTypes)];
};
export const groupAlbumsByReleaseType = (
albums: Album[],
routeId: string,
@@ -44,10 +53,9 @@ export const groupAlbumsByReleaseType = (
}
// Group by all release types
const releaseTypes = album.releaseTypes || [];
if (releaseTypes.length > 0) {
const normalizedTypes = getNormalizedReleaseTypes(album);
if (normalizedTypes.length > 0) {
// Sort release types: primaries first (alphabetically), then secondaries (alphabetically)
const normalizedTypes = releaseTypes.map((type) => type.toLowerCase());
const primaryTypes = normalizedTypes
.filter((type) => PRIMARY_RELEASE_TYPES.includes(type))
.sort();
@@ -92,8 +100,7 @@ export const groupAlbumsByReleaseType = (
return acc;
}
const releaseTypes = album.releaseTypes || [];
const normalizedTypes = releaseTypes.map((type) => type.toLowerCase());
const normalizedTypes = getNormalizedReleaseTypes(album);
let matchedType: null | string = null;
@@ -107,6 +114,8 @@ export const groupAlbumsByReleaseType = (
matchedType = 'broadcast';
} else if (normalizedTypes.includes('other')) {
matchedType = 'other';
} else if (normalizedTypes.length > 0) {
matchedType = normalizedTypes[0];
} else {
matchedType = 'album';
}
@@ -292,11 +301,11 @@ export const getArtistAlbumsGrouped = (
const types = releaseType.split('/');
return types.some((type) => {
const enumValue = releaseTypeToEnumMap[type];
return enumValue ? enabledReleaseTypeEnums.has(enumValue) : false;
return enumValue ? enabledReleaseTypeEnums.has(enumValue) : true;
});
}
const enumValue = releaseTypeToEnumMap[releaseType];
return enumValue ? enabledReleaseTypeEnums.has(enumValue) : false;
return enumValue ? enabledReleaseTypeEnums.has(enumValue) : true;
};
const releaseTypeEntries = Object.entries(albumsByReleaseType)
@@ -165,7 +165,7 @@ export const AddToPlaylistAction = ({ items, itemType }: AddToPlaylistActionProp
);
const handleAddToPlaylist = useCallback(
async (playlistId: string) => {
async (playlistId: string, playlistName: string) => {
if (items.length === 0 || !serverId) return;
try {
@@ -202,10 +202,9 @@ export const AddToPlaylistAction = ({ items, itemType }: AddToPlaylistActionProp
}
if (allSongIds.length === 0) {
toast.success({
message: t('form.addToPlaylist.success', {
message: 0,
numOfPlaylists: 1,
toast.info({
message: t('form.addToPlaylist.noneAdded', {
playlist: playlistName,
postProcess: 'sentenceCase',
}),
});
@@ -245,10 +244,9 @@ export const AddToPlaylistAction = ({ items, itemType }: AddToPlaylistActionProp
}
if (songsToAdd.length === 0) {
toast.success({
message: t('form.addToPlaylist.success', {
message: 0,
numOfPlaylists: 1,
toast.info({
message: t('form.addToPlaylist.noneAdded', {
playlist: playlistName,
postProcess: 'sentenceCase',
}),
});
@@ -413,7 +411,9 @@ export const AddToPlaylistAction = ({ items, itemType }: AddToPlaylistActionProp
<>
<ContextMenu.Item
key={recentPlaylist.id}
onSelect={() => handleAddToPlaylist(recentPlaylist.id)}
onSelect={() =>
handleAddToPlaylist(recentPlaylist.id, recentPlaylist.name)
}
>
{recentPlaylist.name}
</ContextMenu.Item>
@@ -428,7 +428,7 @@ export const AddToPlaylistAction = ({ items, itemType }: AddToPlaylistActionProp
{filteredPlaylists.map((playlist) => (
<ContextMenu.Item
key={playlist.id}
onSelect={() => handleAddToPlaylist(playlist.id)}
onSelect={() => handleAddToPlaylist(playlist.id, playlist.name)}
>
{playlist.name}
</ContextMenu.Item>
@@ -1,4 +1,5 @@
import { SetActivity, StatusDisplayType } from '@xhayper/discord-rpc';
import type { SetActivity } from '@xhayper/discord-rpc';
import isElectron from 'is-electron';
import React, { useCallback, useEffect, useRef, useState } from 'react';
@@ -27,6 +28,13 @@ import { LibraryItem, QueueSong, ServerType } from '/@/shared/types/domain-types
import { PlayerStatus } from '/@/shared/types/types';
const discordRpc = isElectron() ? window.api.discordRpc : null;
const DiscordStatusDisplayType = {
DETAILS: 2,
NAME: 0,
STATE: 1,
} as const;
type ActivityState = [QueueSong | undefined, number, PlayerStatus];
const MAX_FIELD_LENGTH = 127;
@@ -122,7 +130,7 @@ export const useDiscordRpc = () => {
: undefined
: sentenceCase(current[2]),
state: truncate(artist),
statusDisplayType: StatusDisplayType.STATE,
statusDisplayType: DiscordStatusDisplayType.STATE,
type: discordSettings.showAsListening ? 2 : 0,
};
@@ -196,9 +204,9 @@ export const useDiscordRpc = () => {
const artists = song?.artists.map((artist) => artist.name).join(', ');
const statusDisplayMap = {
[DiscordDisplayType.ARTIST_NAME]: StatusDisplayType.STATE,
[DiscordDisplayType.FEISHIN]: StatusDisplayType.NAME,
[DiscordDisplayType.SONG_NAME]: StatusDisplayType.DETAILS,
[DiscordDisplayType.ARTIST_NAME]: DiscordStatusDisplayType.STATE,
[DiscordDisplayType.FEISHIN]: DiscordStatusDisplayType.NAME,
[DiscordDisplayType.SONG_NAME]: DiscordStatusDisplayType.DETAILS,
};
const activity: SetActivity = {
@@ -17,6 +17,7 @@ import { MultiSelect } from '/@/shared/components/multi-select/multi-select';
import { NumberInput } from '/@/shared/components/number-input/number-input';
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
import { Select } from '/@/shared/components/select/select';
import { Slider } from '/@/shared/components/slider/slider';
import { Stack } from '/@/shared/components/stack/stack';
import { Switch } from '/@/shared/components/switch/switch';
import { TextInput } from '/@/shared/components/text-input/text-input';
@@ -185,6 +186,48 @@ export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) =>
postProcess: 'sentenceCase',
}),
},
{
control: (
<Slider
defaultValue={displaySettings.opacityNonActive}
label={(e) => (e * 100).toFixed(0) + '%'}
max={1.0}
min={0.0}
onChangeEnd={(e) => {
updateDisplaySetting({
opacityNonActive: e,
});
}}
step={0.01}
w={100}
/>
),
description: '',
title: t(`${t('page.fullscreenPlayer.config.lyricOpacityNonActive')}`, {
postProcess: 'sentenceCase',
}),
},
{
control: (
<Slider
defaultValue={displaySettings.scaleNonActive}
label={(e) => (e * 100).toFixed(0) + '%'}
max={1.0}
min={0.5}
onChangeEnd={(e) => {
updateDisplaySetting({
scaleNonActive: e,
});
}}
step={0.01}
w={100}
/>
),
description: '',
title: t(`${t('page.fullscreenPlayer.config.lyricScaleNonActive')}`, {
postProcess: 'sentenceCase',
}),
},
{
control: (
<Switch
@@ -4,14 +4,18 @@
line-height: 1.2;
color: var(--theme-colors-foreground);
word-break: normal;
opacity: 0.35;
opacity: var(--lyric-opacity);
transform: scale(var(--lyric-scale));
transform-origin: var(--lyric-scale-origin) center;
transition:
opacity 0.3s ease-in-out,
transform 0.3s ease-in-out;
opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1),
transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
will-change: transform;
}
.lyric-line:global(.active) {
opacity: 1 !important;
transform: scale(1);
}
.lyric-line:global(.unsynchronized) {
@@ -49,6 +49,11 @@ export const SynchronizedLyrics = ({
? displaySettings.fontSize
: 24,
gap: displaySettings.gap && displaySettings.gap !== 0 ? displaySettings.gap : 24,
opacityNonActive: displaySettings.opacityNonActive,
scaleNonActive:
displaySettings.scaleNonActive && displaySettings.scaleNonActive !== 0
? displaySettings.scaleNonActive
: 0.95,
};
const { mediaSeekToTimestamp } = usePlayerActions();
const status = usePlayerStatus();
@@ -90,18 +95,20 @@ export const SynchronizedLyrics = ({
const programmaticScrollRef = useRef(false);
const getCurrentLyric = (timeInMs: number) => {
if (lyricRef.current) {
const activeLyrics = lyricRef.current;
for (let idx = 0; idx < activeLyrics.length; idx += 1) {
if (timeInMs <= activeLyrics[idx][0]) {
return idx === 0 ? idx : idx - 1;
}
}
return activeLyrics.length - 1;
const activeLyrics = lyricRef.current;
if (!activeLyrics?.length) {
return -1;
}
return -1;
let index = -1;
for (let idx = 0; idx < activeLyrics.length; idx += 1) {
if (timeInMs < activeLyrics[idx][0]) {
break;
}
index = idx;
}
return index;
};
const setCurrentLyricRef = useRef<
@@ -136,7 +143,20 @@ export const SynchronizedLyrics = ({
.forEach((node) => node.classList.remove('active'));
if (index === -1) {
lyricRef.current = null;
const activeLyrics = lyricRef.current;
if (!activeLyrics?.length) {
return;
}
const firstTime = activeLyrics[0][0];
if (timeInMs < firstTime) {
const elapsed = performance.now() - start;
const delay = Math.max(0, firstTime - timeInMs - elapsed);
lyricTimer.current = setTimeout(() => {
setCurrentLyricRef.current(firstTime, nextEpoch, 0);
}, delay);
}
return;
}
@@ -148,7 +168,6 @@ export const SynchronizedLyrics = ({
const offsetTop = currentLyric?.offsetTop - doc?.clientHeight / 2 || 0;
if (currentLyric === null) {
lyricRef.current = null;
return;
}
@@ -308,7 +327,18 @@ export const SynchronizedLyrics = ({
onMouseEnter={showScrollbar}
onMouseLeave={hideScrollbar}
ref={containerRef}
style={{ gap: `${settings.gap}px`, ...style }}
style={
{
// opacity/scale is set here for every lyric,
// and then overwritten by CSS for active lyrics
// to prevent expensive rerenders each lyric
'--lyric-opacity': settings.opacityNonActive,
'--lyric-scale': settings.scaleNonActive,
'--lyric-scale-origin': settings.alignment,
gap: `${settings.gap}px`,
...style,
} as React.CSSProperties
}
>
{settings.showProvider && source && (
<LyricLine
@@ -15,6 +15,7 @@ import { eventEmitter } from '/@/renderer/events/event-emitter';
import { useIsPlayerFetching, usePlayer } from '/@/renderer/features/player/context/player-context';
import { searchLibraryItems } from '/@/renderer/features/shared/utils';
import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
import { useHotkeys } from '/@/renderer/hooks/use-hotkeys';
import {
isShuffleEnabled,
mapShuffledToQueueIndex,
@@ -30,7 +31,6 @@ import { Flex } from '/@/shared/components/flex/flex';
import { LoadingOverlay } from '/@/shared/components/loading-overlay/loading-overlay';
import { useDebouncedValue } from '/@/shared/hooks/use-debounced-value';
import { useFocusWithin } from '/@/shared/hooks/use-focus-within';
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
import { useMergedRef } from '/@/shared/hooks/use-merged-ref';
import { Folder, LibraryItem, QueueSong, Song } from '/@/shared/types/domain-types';
import { DragTarget } from '/@/shared/types/drag-and-drop';
@@ -140,9 +140,15 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
};
},
seekTo(seekTo: number) {
let type: 'fraction' | 'seconds' | undefined = undefined;
if (seekTo < 1) {
type = 'seconds';
}
playerNum === 1
? player1Ref.current?.seekTo(seekTo)
: player2Ref.current?.seekTo(seekTo);
? player1Ref.current?.seekTo(seekTo, type)
: player2Ref.current?.seekTo(seekTo, type);
},
setVolume(volume: number) {
setInternalVolume1(volume / 100 || 0);
@@ -240,10 +240,16 @@ export function WebPlayer() {
}
}
let type: 'fraction' | 'seconds' | undefined = undefined;
if (timestamp < 1) {
type = 'seconds';
}
if (num === 1) {
playerRef.current?.player1()?.ref?.seekTo(timestamp);
playerRef.current?.player1()?.ref?.seekTo(timestamp, type);
} else {
playerRef.current?.player2()?.ref?.seekTo(timestamp);
playerRef.current?.player2()?.ref?.seekTo(timestamp, type);
}
},
onPlayerStatus: async (properties) => {
@@ -26,6 +26,7 @@ import {
SONG_DISPLAY_TYPES,
} from '/@/renderer/features/shared/components/list-config-menu';
import { useFastAverageColor } from '/@/renderer/hooks';
import { useHotkeys } from '/@/renderer/hooks/use-hotkeys';
import {
useFullScreenPlayerStore,
useFullScreenPlayerStoreActions,
@@ -46,7 +47,6 @@ import { Popover } from '/@/shared/components/popover/popover';
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
import { Slider } from '/@/shared/components/slider/slider';
import { Switch } from '/@/shared/components/switch/switch';
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
import { LibraryItem } from '/@/shared/types/domain-types';
import { ItemListKey, ListDisplayType, Platform } from '/@/shared/types/types';
@@ -5,6 +5,7 @@ import { useLocation } from 'react-router';
import styles from './full-screen-visualizer.module.css';
import { FullScreenVisualizerSongInfo } from '/@/renderer/features/player/components/full-screen-visualizer-song-info';
import { useHotkeys } from '/@/renderer/hooks/use-hotkeys';
import { useIsMobile } from '/@/renderer/hooks/use-is-mobile';
import { useFullScreenPlayerStoreActions } from '/@/renderer/store/full-screen-player.store';
import {
@@ -12,7 +13,6 @@ import {
useSettingsStore,
useWindowSettings,
} from '/@/renderer/store/settings.store';
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
import { Platform } from '/@/shared/types/types';
const AudioMotionAnalyzerVisualizer = lazy(() =>
@@ -18,6 +18,7 @@ import {
useIsRadioActive,
useRadioPlayer,
} from '/@/renderer/features/radio/hooks/use-radio-player';
import { useHotkeys } from '/@/renderer/hooks/use-hotkeys';
import { AppRoute } from '/@/renderer/router/routes';
import {
useAppStore,
@@ -34,7 +35,6 @@ import { Icon } from '/@/shared/components/icon/icon';
import { Text } from '/@/shared/components/text/text';
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
import { PlaybackSelectors } from '/@/shared/constants/playback-selectors';
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
import { LibraryItem } from '/@/shared/types/domain-types';
export const LeftControls = () => {
@@ -58,7 +58,7 @@ export const PlayerbarWaveform = () => {
height: 18,
interact: false,
media: audioElementRef.current,
normalize: false,
normalize: playerbarSlider?.stretched ?? false,
progressColor: primaryColor,
waveColor,
});
@@ -10,6 +10,7 @@ import { usePlayer } from '/@/renderer/features/player/context/player-context';
import { useSetRating } from '/@/renderer/features/shared/hooks/use-set-rating';
import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
import { useHotkeys } from '/@/renderer/hooks/use-hotkeys';
import {
useAppStoreActions,
useAutoDJSettings,
@@ -34,7 +35,6 @@ import { Button } from '/@/shared/components/button/button';
import { Flex } from '/@/shared/components/flex/flex';
import { Group } from '/@/shared/components/group/group';
import { Rating } from '/@/shared/components/rating/rating';
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
import { useMediaQuery } from '/@/shared/hooks/use-media-query';
import { useThrottledCallback } from '/@/shared/hooks/use-throttled-callback';
import { useThrottledValue } from '/@/shared/hooks/use-throttled-value';
@@ -273,7 +273,7 @@ export const SleepTimerButton = () => {
<Divider my="md" />
<Grid gutter="xs">
<Grid gap="xs">
{PRESET_OPTIONS.filter((option) => option.mode === 'timed').map(
(option, index) => (
<Grid.Col key={index} span={4}>
@@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { HotkeyItem, useHotkeys } from '/@/renderer/hooks/use-hotkeys';
import { useHotkeySettings, usePlayerStore } from '/@/renderer/store';
import { HotkeyItem, useHotkeys } from '/@/shared/hooks/use-hotkeys';
export const usePlaybackHotkeys = () => {
const { bindings } = useHotkeySettings();
@@ -8,17 +8,17 @@ const ipc = isElectron() ? window.api.ipc : null;
export const usePowerSaveBlocker = () => {
const status = usePlayerStatus();
const { preventSleepOnPlayback } = useWindowSettings();
const { preventSleepOnPlayback, preventSuspendOnPlayback } = useWindowSettings();
const startPowerSaveBlocker = useCallback(async () => {
if (!ipc) return;
try {
await ipc.invoke('power-save-blocker-start');
await ipc.invoke('power-save-blocker-start', { full: preventSleepOnPlayback });
} catch (error) {
console.error('Failed to start power save blocker:', error);
}
}, []);
}, [preventSleepOnPlayback]);
const stopPowerSaveBlocker = useCallback(async () => {
if (!ipc) return;
@@ -31,16 +31,21 @@ export const usePowerSaveBlocker = () => {
}, []);
useEffect(() => {
if (!preventSleepOnPlayback) return;
if (!preventSleepOnPlayback || !preventSuspendOnPlayback) return;
if (status === PlayerStatus.PLAYING) {
startPowerSaveBlocker();
} else {
stopPowerSaveBlocker();
}
}, [status, preventSleepOnPlayback, startPowerSaveBlocker, stopPowerSaveBlocker]);
}, [
status,
preventSleepOnPlayback,
startPowerSaveBlocker,
stopPowerSaveBlocker,
preventSuspendOnPlayback,
]);
// Clean up on unmount
useEffect(() => {
return () => {
stopPowerSaveBlocker();
@@ -56,8 +61,11 @@ const PowerSaveBlockerHookInner = () => {
export const PowerSaveBlockerHook = () => {
const isElectronEnv = isElectron();
const preventSleepOnPlayback = useSettingsStore((state) => state.window.preventSleepOnPlayback);
const preventSuspendOnPlayback = useSettingsStore(
(state) => state.window.preventSuspendOnPlayback,
);
if (!isElectronEnv || !preventSleepOnPlayback) {
if (!isElectronEnv || !preventSleepOnPlayback || !preventSuspendOnPlayback) {
return null;
}
@@ -19,6 +19,7 @@ export function useVisualizerSystemAudio(options: {
onDeniedRef.current = onSystemAudioCaptureDenied;
onSuccessRef.current = onSystemAudioCaptureSuccess;
const playbackType = usePlaybackType();
const isMacOS = Boolean(window.api?.utils?.isMacOS?.());
const { setWebAudio, webAudio } = useWebAudio();
const webAudioRef = useRef(webAudio);
const streamRef = useRef<MediaStream | null>(null);
@@ -80,7 +81,7 @@ export function useVisualizerSystemAudio(options: {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
audio: true,
video: false,
video: isMacOS, // On macOS, getDisplayMedia requires video to be requested in order to capture system audio
});
const audioTracks = stream.getAudioTracks();
@@ -124,7 +125,7 @@ export function useVisualizerSystemAudio(options: {
} finally {
connectInFlightRef.current = false;
}
}, [disconnect, setWebAudio]);
}, [disconnect, isMacOS, setWebAudio]);
const connectRef = useRef(connect);
connectRef.current = connect;
@@ -552,7 +552,7 @@ const PlaylistTableItem = memo(
return (
<Box className={styles.container} w="100%">
<Grid align="center" gutter="xs" w="100%">
<Grid align="center" gap="xs" w="100%">
<Grid.Col span="content">
<Flex align="center" justify="center" px="sm">
<ItemImage
@@ -477,6 +477,32 @@ export const ControlSettings = memo(() => {
postProcess: 'sentenceCase',
}),
},
{
control: (
<Switch
defaultChecked={playerbarSlider?.stretched ?? false}
onChange={(e) =>
setSettings({
general: {
...settings,
playerbarSlider: {
...playerbarSlider,
stretched: e.currentTarget.checked,
},
},
})
}
/>
),
description: t('setting.playerbarWaveformStretch', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: false,
title: t('setting.playerbarWaveformStretch', {
postProcess: 'sentenceCase',
}),
},
{
control: (
<NumberInput
@@ -146,6 +146,7 @@ export const ThemeSettings = memo(() => {
}
}}
renderOption={renderThemeOption}
searchable
width={240}
/>
),
@@ -184,7 +184,7 @@ export const WindowSettings = memo(() => {
<Switch
aria-label="Toggle prevent sleep on playback"
defaultChecked={settings.preventSleepOnPlayback}
disabled={!isElectron()}
disabled={!isElectron() || settings.preventSuspendOnPlayback}
onChange={(e) => {
if (!e) return;
localSettings?.set(
@@ -206,6 +206,33 @@ export const WindowSettings = memo(() => {
isHidden: !isElectron(),
title: t('setting.preventSleepOnPlayback', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
aria-label="Toggle prevent suspend on playback"
defaultChecked={settings.preventSuspendOnPlayback}
disabled={!isElectron() || settings.preventSleepOnPlayback}
onChange={(e) => {
if (!e) return;
localSettings?.set(
'window_prevent_suspend_on_playback',
e.currentTarget.checked,
);
setSettings({
window: {
preventSuspendOnPlayback: e.currentTarget.checked,
},
});
}}
/>
),
description: t('setting.preventSuspendOnPlayback', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !isElectron(),
title: t('setting.preventSuspendOnPlayback', { postProcess: 'sentenceCase' }),
},
];
return (
@@ -6,6 +6,8 @@
pointer-events: none;
user-select: none;
background-image: var(--theme-overlay-subheader);
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
}
.background-overlay {
@@ -30,6 +32,18 @@
var(--dither);
}
.background-image-stack {
position: absolute;
top: 0;
left: 0;
z-index: 0;
width: 100%;
pointer-events: none;
isolation: isolate;
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
}
.background-image {
position: absolute;
top: 0;
@@ -37,6 +51,7 @@
width: 100%;
background-position: center !important;
background-size: cover !important;
isolation: isolate;
opacity: 0.7;
}
@@ -76,7 +76,7 @@ export const LibraryBackgroundImage = ({ blur, headerRef, imageUrl }: LibraryBac
const url = imageUrl ? `url(${imageUrl})` : undefined;
const height = useHeaderHeight(headerRef);
return (
<>
<div className={styles.backgroundImageStack}>
<div
className={styles.backgroundImage}
style={{
@@ -91,7 +91,7 @@ export const LibraryBackgroundImage = ({ blur, headerRef, imageUrl }: LibraryBac
height: height ? `${height + 64}px` : undefined,
}}
/>
</>
</div>
);
};
@@ -9,12 +9,12 @@ import {
} from 'react';
import { shallow } from 'zustand/shallow';
import { useHotkeys } from '/@/renderer/hooks/use-hotkeys';
import { useSettingsStore } from '/@/renderer/store';
import { ActionIcon, ActionIconProps } from '/@/shared/components/action-icon/action-icon';
import { Box } from '/@/shared/components/box/box';
import { Icon } from '/@/shared/components/icon/icon';
import { TextInput, TextInputProps } from '/@/shared/components/text-input/text-input';
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
interface SearchInputProps extends TextInputProps {
buttonProps?: Partial<ActionIconProps>;
@@ -18,12 +18,7 @@ export const ActionBar = () => {
return (
<div className={styles.container}>
<Grid
display="flex"
gutter="sm"
style={{ padding: '0 var(--theme-spacing-md)' }}
w="100%"
>
<Grid display="flex" gap="sm" style={{ padding: '0 var(--theme-spacing-md)' }} w="100%">
<Grid.Col span={7}>
<TextInput
leftSection={<Icon icon="search" />}
@@ -6,7 +6,7 @@ import styles from './sidebar-item.module.css';
import { Button, ButtonProps } from '/@/shared/components/button/button';
interface SidebarItemProps extends ButtonProps {
interface SidebarItemProps extends Omit<ButtonProps, 'component' | 'ref'> {
to: LinkProps['to'];
}
@@ -31,8 +31,7 @@
.image-container {
position: relative;
flex-shrink: 0;
width: var(--sidebar-image-height);
height: var(--sidebar-image-height);
min-height: 0;
padding: var(--theme-spacing-md);
cursor: pointer;
animation: fade-in 0.2s ease-in-out;
@@ -1,6 +1,6 @@
import clsx from 'clsx';
import { AnimatePresence, motion } from 'motion/react';
import { CSSProperties, MouseEvent, useMemo } from 'react';
import { MouseEvent, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import styles from './sidebar.module.css';
@@ -159,7 +159,6 @@ export const Sidebar = () => {
const SidebarImage = () => {
const { t } = useTranslation();
const leftWidth = useAppStore((state) => state.sidebar.leftWidth);
const { setSideBar } = useAppStoreActions();
const currentSong = usePlayerSong();
const isRadioActive = useIsRadioActive();
@@ -216,11 +215,7 @@ const SidebarImage = () => {
onClick={expandFullScreenPlayer}
onContextMenu={handleToggleContextMenu}
role="button"
style={
{
'--sidebar-image-height': leftWidth,
} as CSSProperties
}
style={{ aspectRatio: 1 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
>
<Tooltip
@@ -1,16 +1,17 @@
import isElectron from 'is-electron';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useIsLocalVisualizerSurfaceVisible } from '/@/renderer/features/player/hooks/use-is-local-visualizer-surface-visible';
import { useVisualizerSystemAudio } from '/@/renderer/features/player/hooks/use-visualizer-system-audio';
import { closeLocalVisualizerSurfaces } from '/@/renderer/features/player/utils/close-local-visualizer-surfaces';
import { usePlaybackType } from '/@/renderer/store';
import { useMpvSettings, usePlaybackType } from '/@/renderer/store';
import { Button } from '/@/shared/components/button/button';
import { Group } from '/@/shared/components/group/group';
import { Modal } from '/@/shared/components/modal/modal';
import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text';
import { toast } from '/@/shared/components/toast/toast';
import { useDisclosure } from '/@/shared/hooks/use-disclosure';
import { PlayerType } from '/@/shared/types/types';
@@ -31,12 +32,21 @@ export function VisualizerSystemAudioBridgeHook() {
function VisualizerSystemAudioBridge() {
const { t } = useTranslation();
const playbackType = usePlaybackType();
const { audioExclusiveMode } = useMpvSettings();
const isVisualizerSurfaceVisible = useIsLocalVisualizerSurfaceVisible();
const [promptState, setPromptState] = useState<PromptState>('loading');
const [sessionAllowCapture, setSessionAllowCapture] = useState(false);
const wasBlockedByExclusiveModeRef = useRef(false);
const [isPromptOpen, { close: closePrompt, open: openPrompt, toggle: togglePrompt }] =
useDisclosure(false);
const isExclusiveModeEnabled = audioExclusiveMode === 'yes';
const isVisualizerBlockedByExclusiveMode =
isElectron() &&
playbackType === PlayerType.LOCAL &&
isVisualizerSurfaceVisible &&
isExclusiveModeEnabled;
const persistConsent = useCallback((granted: boolean) => {
if (!isElectron() || !window.api.localSettings) {
return;
@@ -67,6 +77,7 @@ function VisualizerSystemAudioBridge() {
const eligibleForPrompt =
isElectron() &&
playbackType === PlayerType.LOCAL &&
!isExclusiveModeEnabled &&
isVisualizerSurfaceVisible &&
promptState !== 'loading' &&
!promptState.consent &&
@@ -80,9 +91,25 @@ function VisualizerSystemAudioBridge() {
}
}, [eligibleForPrompt, closePrompt, openPrompt]);
useEffect(() => {
if (isVisualizerBlockedByExclusiveMode && !wasBlockedByExclusiveModeRef.current) {
toast.error({
message: t('visualizer.systemAudioExclusiveModeNotSupported', {
postProcess: 'sentenceCase',
}),
});
setSessionAllowCapture(false);
closePrompt();
closeLocalVisualizerSurfaces();
}
wasBlockedByExclusiveModeRef.current = isVisualizerBlockedByExclusiveMode;
}, [closePrompt, isVisualizerBlockedByExclusiveMode, t]);
const shouldAttemptConnection =
isElectron() &&
playbackType === PlayerType.LOCAL &&
!isExclusiveModeEnabled &&
isVisualizerSurfaceVisible &&
promptState !== 'loading' &&
(promptState.consent || sessionAllowCapture);
+1
View File
@@ -3,5 +3,6 @@ export * from './use-check-for-updates';
export * from './use-container-query';
export * from './use-fast-average-color';
export * from './use-hide-scrollbar';
export * from './use-hotkeys';
export * from './use-is-mounted';
export * from './use-should-pad-titlebar';
+23
View File
@@ -0,0 +1,23 @@
import {
type HotkeyItem as MantineHotkeyItem,
useHotkeys as useMantineHotkeys,
} from '@mantine/hooks';
import { useAppStore } from '/@/renderer/store';
const EMPTY_HOTKEYS: MantineHotkeyItem[] = [];
export const useHotkeys = (
hotkeys: MantineHotkeyItem[],
tagsToIgnore?: string[],
triggerOnContentEditable?: boolean,
) => {
const commandPaletteOpened = useAppStore((state) => state.commandPalette.opened);
useMantineHotkeys(
commandPaletteOpened ? EMPTY_HOTKEYS : hotkeys,
tagsToIgnore,
triggerOnContentEditable,
);
};
export type HotkeyItem = MantineHotkeyItem;
@@ -1,6 +1,6 @@
import clsx from 'clsx';
import { AnimatePresence } from 'motion/react';
import { lazy, Suspense } from 'react';
import { Suspense } from 'react';
import { Outlet } from 'react-router';
import styles from './mobile-layout.module.css';
@@ -10,6 +10,7 @@ import { FullScreenVisualizer } from '/@/renderer/features/player/components/ful
import { MobileFullscreenPlayer } from '/@/renderer/features/player/components/mobile-fullscreen-player';
import { MobileSidebar } from '/@/renderer/features/sidebar/components/mobile-sidebar';
import { PlayerBar } from '/@/renderer/layouts/default-layout/player-bar';
import { WindowBar } from '/@/renderer/layouts/window-bar';
import { useFullScreenPlayerOverlayState, useWindowBarStyle } from '/@/renderer/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Drawer } from '/@/shared/components/drawer/drawer';
@@ -17,12 +18,6 @@ import { Spinner } from '/@/shared/components/spinner/spinner';
import { useDisclosure } from '/@/shared/hooks/use-disclosure';
import { Platform } from '/@/shared/types/types';
const WindowBar = lazy(() =>
import('/@/renderer/layouts/window-bar').then((module) => ({
default: module.WindowBar,
})),
);
interface MobileLayoutProps {
shell?: boolean;
}
+1 -1
View File
@@ -5,6 +5,7 @@ import { useNavigate } from 'react-router';
import { useAppTracker } from '/@/renderer/features/analytics/hooks/use-app-tracker';
import { CommandPalette } from '/@/renderer/features/search/components/command-palette';
import { useGarbageCollection } from '/@/renderer/hooks/use-garbage-collection';
import { HotkeyItem, useHotkeys } from '/@/renderer/hooks/use-hotkeys';
import { useIsMobile } from '/@/renderer/hooks/use-is-mobile';
import { DefaultLayout } from '/@/renderer/layouts/default-layout';
import { MobileLayout } from '/@/renderer/layouts/mobile-layout/mobile-layout';
@@ -15,7 +16,6 @@ import {
useSettingsStoreActions,
useZoomFactor,
} from '/@/renderer/store';
import { HotkeyItem, useHotkeys } from '/@/shared/hooks/use-hotkeys';
interface ResponsiveLayoutProps {
shell?: boolean;
+1 -12
View File
@@ -1,6 +1,7 @@
import { lazy, Suspense } from 'react';
import { HashRouter, Route, Routes } from 'react-router';
import { ShuffleAllContextModal } from '/@/renderer/features/player/components/shuffle-all-modal';
import { RouterErrorBoundary } from '/@/renderer/features/shared/components/router-error-boundary';
import { AuthenticationOutlet } from '/@/renderer/layouts/authentication-outlet';
import { ResponsiveLayout } from '/@/renderer/layouts/responsive-layout';
@@ -96,18 +97,6 @@ const LyricsSettingsContextModal = (props: any) => (
</Suspense>
);
const LazyShuffleAllContextModal = lazy(() =>
import('/@/renderer/features/player/components/shuffle-all-modal').then((module) => ({
default: module.ShuffleAllContextModal,
})),
);
const ShuffleAllContextModal = (props: any) => (
<Suspense fallback={<Spinner container />}>
<LazyShuffleAllContextModal {...props} />
</Suspense>
);
const LazyAddToPlaylistContextModal = lazy(() =>
import('/@/renderer/features/playlists/components/add-to-playlist-context-modal').then(
(module) => ({
@@ -10,6 +10,8 @@ const APP_THEMES = new Set([
'defaultDark',
'defaultLight',
'dracula',
'everforestDark',
'everforestLight',
'githubDark',
'githubLight',
'glassyDark',
+4 -6
View File
@@ -776,7 +776,7 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
// For nextSong calculation, we need to consider the shuffled order
let nextSong: QueueSong | undefined;
if (isShuffleEnabled(state)) {
if (isShuffleEnabled(state) && repeat !== PlayerRepeat.ONE) {
// Calculate next in shuffled order
const nextShuffledIndex = index + 1;
if (nextShuffledIndex < state.queue.shuffled.length) {
@@ -925,7 +925,7 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
const currentSong = queue.items[currentQueueIndex];
let nextSong: QueueSong | undefined;
if (isShuffle) {
if (isShuffle && repeat !== PlayerRepeat.ONE) {
const nextShuffledIndex = nextPlaybackIndex + 1;
if (nextShuffledIndex < stateSnapshot.queue.shuffled.length) {
const nextQueueIndex = stateSnapshot.queue.shuffled[nextShuffledIndex];
@@ -933,8 +933,6 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
} else if (repeat === PlayerRepeat.ALL) {
const firstQueueIndex = stateSnapshot.queue.shuffled[0];
nextSong = queue.items[firstQueueIndex];
} else if (repeat === PlayerRepeat.ONE) {
nextSong = currentSong;
}
} else {
nextSong = calculateNextSong(currentQueueIndex, queue.items, repeat);
@@ -1733,7 +1731,7 @@ export const subscribeNextSongInsertion = (onChange: (song: QueueSong | undefine
// Calculate next song based on shuffle and repeat settings
let nextSong: QueueSong | undefined;
if (isShuffleEnabled(state)) {
if (isShuffleEnabled(state) && repeat !== PlayerRepeat.ONE) {
// Calculate next in shuffled order
const nextShuffledIndex = state.player.index + 1;
if (nextShuffledIndex < state.queue.shuffled.length) {
@@ -1940,7 +1938,7 @@ export const usePlayerData = (): PlayerData => {
// For nextSong calculation, we need to consider the shuffled order
let nextSong: QueueSong | undefined;
if (isShuffleEnabled(state)) {
if (isShuffleEnabled(state) && repeat !== PlayerRepeat.ONE) {
// Calculate next in shuffled order
const nextShuffledIndex = index + 1;
if (nextShuffledIndex < state.queue.shuffled.length) {
+12 -1
View File
@@ -306,6 +306,7 @@ const PlayerbarSliderSchema = z.object({
barRadius: z.number(),
barWidth: z.number(),
loadingDelay: z.number(),
stretched: z.boolean(),
type: PlayerbarSliderTypeSchema,
});
@@ -536,6 +537,8 @@ const LyricsDisplaySettingsSchema = z.object({
fontSizeUnsync: z.number(),
gap: z.number(),
gapUnsync: z.number(),
opacityNonActive: z.number(),
scaleNonActive: z.number(),
});
const LyricsSettingsSchema = z.object({
@@ -635,6 +638,7 @@ const WindowSettingsSchema = z.object({
exitToTray: z.boolean(),
minimizeToTray: z.boolean(),
preventSleepOnPlayback: z.boolean(),
preventSuspendOnPlayback: z.boolean(),
releaseChannel: z.enum(['alpha', 'beta', 'latest']),
startMinimized: z.boolean(),
tray: z.boolean(),
@@ -1150,6 +1154,7 @@ const initialState: SettingsState = {
barRadius: 4,
barWidth: 2,
loadingDelay: 2,
stretched: false,
type: PlayerbarSliderType.SLIDER,
},
playerItems,
@@ -1794,6 +1799,8 @@ const initialState: SettingsState = {
fontSizeUnsync: 24,
gap: 24,
gapUnsync: 24,
opacityNonActive: 0.2,
scaleNonActive: 0.95,
},
},
playback: {
@@ -1908,6 +1915,7 @@ const initialState: SettingsState = {
exitToTray: false,
minimizeToTray: false,
preventSleepOnPlayback: false,
preventSuspendOnPlayback: false,
releaseChannel: 'latest',
startMinimized: false,
tray: true,
@@ -2211,7 +2219,10 @@ export const useSettingsStore = createWithEqualityFn<SettingsSlice>()(
state.lyrics = mainSettings;
state.lyricsDisplay = {
default: displaySettings,
default: {
...state.lyricsDisplay.default,
...displaySettings,
},
};
}
}
+2
View File
@@ -23,6 +23,8 @@ export const THEME_DATA = [
{ label: 'One Dark', type: 'dark', value: AppTheme.ONE_DARK },
{ label: 'Solarized Dark', type: 'dark', value: AppTheme.SOLARIZED_DARK },
{ label: 'Solarized Light', type: 'light', value: AppTheme.SOLARIZED_LIGHT },
{ label: 'Everforest Dark', type: 'dark', value: AppTheme.EVERFOREST_DARK },
{ label: 'Everforest Light', type: 'light', value: AppTheme.EVERFOREST_LIGHT },
{ label: 'GitHub Dark', type: 'dark', value: AppTheme.GITHUB_DARK },
{ label: 'GitHub Light', type: 'light', value: AppTheme.GITHUB_LIGHT },
{ label: 'Glassy Dark', type: 'dark', value: AppTheme.GLASSY_DARK },
@@ -278,6 +278,8 @@ const normalizeSong = (
const normalizeAlbum = (
item: z.infer<typeof jfType._response.album>,
server: null | ServerListItem,
pathReplace?: string,
pathReplaceWith?: string,
): Album => {
const { originalYear, releaseDate, releaseYear } = jellyfinPremiereFields(item);
@@ -340,7 +342,7 @@ const normalizeAlbum = (
releaseYear,
size: null,
songCount: item?.ChildCount || null,
songs: item.Songs?.map((song) => normalizeSong(song, server)),
songs: item.Songs?.map((song) => normalizeSong(song, server, pathReplace, pathReplaceWith)),
sortName: item.SortName || item.Name,
tags: getTags(item),
updatedAt: item?.DateLastMediaAdded || item.DateCreated,
+1 -1
View File
@@ -28,4 +28,4 @@ const BaseBadge = ({ children, classNames, variant = 'default', ...props }: Badg
);
};
export const Badge = createPolymorphicComponent<'button', BadgeProps>(BaseBadge);
export const Badge = createPolymorphicComponent<'div', BadgeProps>(BaseBadge);
+2
View File
@@ -10,6 +10,8 @@ export enum AppTheme {
DEFAULT_DARK = 'defaultDark',
DEFAULT_LIGHT = 'defaultLight',
DRACULA = 'dracula',
EVERFOREST_DARK = 'everforestDark',
EVERFOREST_LIGHT = 'everforestLight',
GITHUB_DARK = 'githubDark',
GITHUB_LIGHT = 'githubLight',
GLASSY_DARK = 'glassyDark',
+4
View File
@@ -11,6 +11,8 @@ import { defaultTheme } from '/@/shared/themes/default';
import { defaultDark } from '/@/shared/themes/default-dark/default-dark';
import { defaultLight } from '/@/shared/themes/default-light/default-light';
import { dracula } from '/@/shared/themes/dracula/dracula';
import { everforestDark } from '/@/shared/themes/everforest-dark/everforest-dark';
import { everforestLight } from '/@/shared/themes/everforest-light/everforest-light';
import { githubDark } from '/@/shared/themes/github-dark/github-dark';
import { githubLight } from '/@/shared/themes/github-light/github-light';
import { glassyDark } from '/@/shared/themes/glassy-dark/glassy-dark';
@@ -42,6 +44,8 @@ export const appTheme: Record<AppTheme, AppThemeConfiguration> = {
[AppTheme.DEFAULT_DARK]: defaultDark,
[AppTheme.DEFAULT_LIGHT]: defaultLight,
[AppTheme.DRACULA]: dracula,
[AppTheme.EVERFOREST_DARK]: everforestDark,
[AppTheme.EVERFOREST_LIGHT]: everforestLight,
[AppTheme.GITHUB_DARK]: githubDark,
[AppTheme.GITHUB_LIGHT]: githubLight,
[AppTheme.GLASSY_DARK]: glassyDark,
@@ -0,0 +1,28 @@
import { AppThemeConfiguration } from '/@/shared/themes/app-theme-types';
export const everforestDark: AppThemeConfiguration = {
app: {
'overlay-header':
'linear-gradient(transparent 0%, rgb(39 46 41 / 85%) 100%), var(--theme-background-noise)',
'overlay-subheader':
'linear-gradient(180deg, rgb(39 46 41 / 5%) 0%, var(--theme-colors-background) 100%), var(--theme-background-noise)',
'scrollbar-handle-background': 'rgba(160, 160, 160, 20%)',
'scrollbar-handle-hover-background': 'rgba(160, 160, 160, 40%)',
},
colors: {
background: 'rgb(35, 42, 46)',
'background-alternate': 'rgb(35, 42, 46)',
black: 'rgb(0, 0, 0)',
foreground: 'rgb(211, 198, 170)',
'foreground-muted': 'rgb(211, 198, 170)',
primary: 'rgb(167, 192, 128)',
'state-error': 'rgb(230, 126, 128)',
'state-info': 'rgb(127, 187, 179)',
'state-success': 'rgb(167, 192, 128)',
'state-warning': 'rgb(219, 188, 127)',
surface: 'rgb(52, 63, 56)',
'surface-foreground': 'rgb(211, 198, 170)',
white: 'rgb(255, 255, 255)',
},
mode: 'dark',
};
@@ -0,0 +1,28 @@
import { AppThemeConfiguration } from '/@/shared/themes/app-theme-types';
export const everforestLight: AppThemeConfiguration = {
app: {
'overlay-header':
'linear-gradient(transparent 0%, rgb(253 246 227 / 85%) 100%), var(--theme-background-noise)',
'overlay-subheader':
'linear-gradient(180deg, rgb(253 246 227 / 5%) 0%, var(--theme-colors-background) 100%), var(--theme-background-noise)',
'scrollbar-handle-background': 'rgba(90, 107, 78, 20%)',
'scrollbar-handle-hover-background': 'rgba(90, 107, 78, 40%)',
},
colors: {
background: 'rgb(253, 246, 227)',
'background-alternate': 'rgb(237, 230, 211)',
black: 'rgb(0, 0, 0)',
foreground: 'rgb(92, 103, 76)',
'foreground-muted': 'rgb(131, 145, 112)',
primary: 'rgb(141, 165, 96)',
'state-error': 'rgb(241, 103, 98)',
'state-info': 'rgb(59, 145, 152)',
'state-success': 'rgb(141, 165, 96)',
'state-warning': 'rgb(223, 163, 59)',
surface: 'rgb(237, 230, 211)',
'surface-foreground': 'rgb(92, 103, 76)',
white: 'rgb(255, 255, 255)',
},
mode: 'light',
};