mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
Compare commits
45 Commits
v1.10.0
...
development
| Author | SHA1 | Date | |
|---|---|---|---|
| 324936e0c8 | |||
| 868ec15b16 | |||
| 775c4e68fa | |||
| 34e0c4bd4a | |||
| 323130a877 | |||
| 3b2aab74ac | |||
| bc7ef0624b | |||
| 304ce8b881 | |||
| 01011a49a2 | |||
| d24ca04878 | |||
| 640d38e5a9 | |||
| ac0c074d4b | |||
| 6be5818493 | |||
| 03edd5a639 | |||
| f5eb3f1488 | |||
| 8eab9edb15 | |||
| fcc69980e4 | |||
| 053b78a3fd | |||
| 42ded966e4 | |||
| ea9119431c | |||
| add0345f10 | |||
| e5a8324a79 | |||
| cc4e933c07 | |||
| 382d279dad | |||
| b99899f128 | |||
| f5839bf39c | |||
| 914ed5b8f3 | |||
| ca0a1569f8 | |||
| 9f10fe398a | |||
| 8869278898 | |||
| 16c9e6cc1b | |||
| 2a6e9b6ad3 | |||
| 167b42df2b | |||
| e6a2bc3acf | |||
| ca3c7015c6 | |||
| c7c15d917a | |||
| 6adb29bc38 | |||
| 2c3cd7af24 | |||
| 3c442a2d40 | |||
| d67c185c93 | |||
| ff96a5f121 | |||
| 6fc7b6b271 | |||
| 918f453066 | |||
| 4a986069f8 | |||
| 11d26af893 |
@@ -121,7 +121,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [windows-latest, macos-latest, ubuntu-latest]
|
os: [windows-latest, macos-26, ubuntu-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout git repo
|
- name: Checkout git repo
|
||||||
@@ -156,7 +156,7 @@ jobs:
|
|||||||
on_retry_command: pnpm cache delete
|
on_retry_command: pnpm cache delete
|
||||||
|
|
||||||
- name: Build and Publish to R2 (macOS)
|
- name: Build and Publish to R2 (macOS)
|
||||||
if: matrix.os == 'macos-latest'
|
if: matrix.os == 'macos-26'
|
||||||
uses: nick-invision/retry@v3.0.2
|
uses: nick-invision/retry@v3.0.2
|
||||||
with:
|
with:
|
||||||
timeout_minutes: 30
|
timeout_minutes: 30
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [windows-latest, macos-latest, ubuntu-latest]
|
os: [windows-latest, macos-26, ubuntu-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout git repo
|
- name: Checkout git repo
|
||||||
@@ -156,7 +156,7 @@ jobs:
|
|||||||
on_retry_command: pnpm cache delete
|
on_retry_command: pnpm cache delete
|
||||||
|
|
||||||
- name: Build and Publish releases (macOS)
|
- name: Build and Publish releases (macOS)
|
||||||
if: matrix.os == 'macos-latest'
|
if: matrix.os == 'macos-26'
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
uses: nick-invision/retry@v3.0.2
|
uses: nick-invision/retry@v3.0.2
|
||||||
|
|||||||
@@ -51,5 +51,4 @@ jobs:
|
|||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
platforms: |
|
platforms: |
|
||||||
linux/amd64
|
linux/amd64
|
||||||
linux/arm/v7
|
|
||||||
linux/arm64/v8
|
linux/arm64/v8
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest]
|
os: [macos-26]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout git repo
|
- name: Checkout git repo
|
||||||
@@ -24,6 +24,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and Publish releases
|
- name: Build and Publish releases
|
||||||
env:
|
env:
|
||||||
|
NODE_OPTIONS: --max-old-space-size=4096
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
uses: nick-invision/retry@v3.0.2
|
uses: nick-invision/retry@v3.0.2
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
os: [macos-26, ubuntu-latest, windows-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout git repo
|
- name: Checkout git repo
|
||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
pnpm run package:linux:pr
|
pnpm run package:linux:pr
|
||||||
|
|
||||||
- name: Build for MacOS
|
- name: Build for MacOS
|
||||||
if: ${{ matrix.os == 'macos-latest' }}
|
if: ${{ matrix.os == 'macos-26' }}
|
||||||
uses: nick-invision/retry@v3.0.2
|
uses: nick-invision/retry@v3.0.2
|
||||||
with:
|
with:
|
||||||
timeout_minutes: 30
|
timeout_minutes: 30
|
||||||
@@ -86,7 +86,7 @@ jobs:
|
|||||||
zip -r dist/linux-binaries.zip dist/*.{AppImage,deb,rpm}
|
zip -r dist/linux-binaries.zip dist/*.{AppImage,deb,rpm}
|
||||||
|
|
||||||
- name: Zip MacOS Binaries
|
- name: Zip MacOS Binaries
|
||||||
if: ${{ matrix.os == 'macos-latest' }}
|
if: ${{ matrix.os == 'macos-26' }}
|
||||||
run: |
|
run: |
|
||||||
zip -r dist/macos-binaries.zip dist/*.dmg
|
zip -r dist/macos-binaries.zip dist/*.dmg
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ jobs:
|
|||||||
path: dist/linux-binaries.zip
|
path: dist/linux-binaries.zip
|
||||||
|
|
||||||
- name: Upload MacOS Binaries
|
- name: Upload MacOS Binaries
|
||||||
if: ${{ matrix.os == 'macos-latest' }}
|
if: ${{ matrix.os == 'macos-26' }}
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: macos-binaries
|
name: macos-binaries
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [windows-latest, macos-latest, ubuntu-latest]
|
os: [windows-latest, macos-26, ubuntu-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout git repo
|
- name: Checkout git repo
|
||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
on_retry_command: pnpm cache delete
|
on_retry_command: pnpm cache delete
|
||||||
|
|
||||||
- name: Build and Publish releases (macOS)
|
- name: Build and Publish releases (macOS)
|
||||||
if: matrix.os == 'macos-latest'
|
if: matrix.os == 'macos-26'
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
uses: nick-invision/retry@v3.0.2
|
uses: nick-invision/retry@v3.0.2
|
||||||
|
|||||||
Binary file not shown.
@@ -40,13 +40,15 @@ mac:
|
|||||||
arch:
|
arch:
|
||||||
- arm64
|
- arm64
|
||||||
- x64
|
- x64
|
||||||
icon: assets/icons/icon.icns
|
icon: media/feishin.icon
|
||||||
type: distribution
|
type: distribution
|
||||||
hardenedRuntime: false
|
hardenedRuntime: false
|
||||||
identity: "-"
|
identity: '-'
|
||||||
gatekeeperAssess: false
|
gatekeeperAssess: false
|
||||||
notarize: 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:
|
dmg:
|
||||||
contents: [{ x: 130, y: 220 }, { x: 410, y: 220, type: link, path: /Applications }]
|
contents: [{ x: 130, y: 220 }, { x: 410, y: 220, type: link, path: /Applications }]
|
||||||
@@ -61,7 +63,7 @@ linux:
|
|||||||
artifactName: ${productName}-${os}-${arch}.${ext}
|
artifactName: ${productName}-${os}-${arch}.${ext}
|
||||||
|
|
||||||
toolsets:
|
toolsets:
|
||||||
appimage: "1.0.2"
|
appimage: '1.0.2'
|
||||||
|
|
||||||
npmRebuild: false
|
npmRebuild: false
|
||||||
|
|
||||||
|
|||||||
@@ -40,12 +40,15 @@ mac:
|
|||||||
arch:
|
arch:
|
||||||
- arm64
|
- arm64
|
||||||
- x64
|
- x64
|
||||||
icon: assets/icons/icon.icns
|
icon: media/feishin.icon
|
||||||
type: distribution
|
type: distribution
|
||||||
hardenedRuntime: false
|
hardenedRuntime: false
|
||||||
identity: "-"
|
identity: '-'
|
||||||
gatekeeperAssess: false
|
gatekeeperAssess: false
|
||||||
notarize: 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:
|
dmg:
|
||||||
contents: [{ x: 130, y: 220 }, { x: 410, y: 220, type: link, path: /Applications }]
|
contents: [{ x: 130, y: 220 }, { x: 410, y: 220, type: link, path: /Applications }]
|
||||||
@@ -60,7 +63,7 @@ linux:
|
|||||||
artifactName: ${productName}-${os}-${arch}.${ext}
|
artifactName: ${productName}-${os}-${arch}.${ext}
|
||||||
|
|
||||||
toolsets:
|
toolsets:
|
||||||
appimage: "1.0.2"
|
appimage: '1.0.2'
|
||||||
|
|
||||||
npmRebuild: false
|
npmRebuild: false
|
||||||
publish:
|
publish:
|
||||||
|
|||||||
@@ -40,13 +40,14 @@ mac:
|
|||||||
arch:
|
arch:
|
||||||
- arm64
|
- arm64
|
||||||
- x64
|
- x64
|
||||||
icon: assets/icons/icon.icns
|
icon: media/feishin.icon
|
||||||
type: distribution
|
type: distribution
|
||||||
hardenedRuntime: false
|
hardenedRuntime: false
|
||||||
identity: '-'
|
identity: '-'
|
||||||
gatekeeperAssess: false
|
gatekeeperAssess: false
|
||||||
notarize: false
|
notarize: false
|
||||||
extendInfo:
|
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'
|
NSLocalNetworkUsageDescription: 'Local network is necessary for accessing servers hosted on the same system as Feishin'
|
||||||
|
|
||||||
dmg:
|
dmg:
|
||||||
|
|||||||
@@ -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 |
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-8
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"version": "1.10.0",
|
"version": "1.11.0",
|
||||||
"description": "A modern self-hosted music player.",
|
"description": "A modern self-hosted music player.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"subsonic",
|
"subsonic",
|
||||||
@@ -78,13 +78,13 @@
|
|||||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
||||||
"@electron-toolkit/preload": "^3.0.2",
|
"@electron-toolkit/preload": "^3.0.2",
|
||||||
"@electron-toolkit/utils": "^4.0.0",
|
"@electron-toolkit/utils": "^4.0.0",
|
||||||
"@mantine/colors-generator": "^8.3.18",
|
"@mantine/colors-generator": "^9.1.1",
|
||||||
"@mantine/core": "^8.3.18",
|
"@mantine/core": "^9.1.1",
|
||||||
"@mantine/dates": "^8.3.18",
|
"@mantine/dates": "^9.1.1",
|
||||||
"@mantine/form": "^8.3.18",
|
"@mantine/form": "^9.1.1",
|
||||||
"@mantine/hooks": "^8.3.18",
|
"@mantine/hooks": "^9.1.1",
|
||||||
"@mantine/modals": "^8.3.18",
|
"@mantine/modals": "^9.1.1",
|
||||||
"@mantine/notifications": "^8.3.18",
|
"@mantine/notifications": "^9.1.1",
|
||||||
"@radix-ui/react-context-menu": "^2.2.16",
|
"@radix-ui/react-context-menu": "^2.2.16",
|
||||||
"@tanstack/react-query": "^5.96.2",
|
"@tanstack/react-query": "^5.96.2",
|
||||||
"@tanstack/react-query-devtools": "^5.96.2",
|
"@tanstack/react-query-devtools": "^5.96.2",
|
||||||
|
|||||||
Generated
+96
-131
@@ -28,26 +28,26 @@ importers:
|
|||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.0.0(electron@39.8.6)
|
version: 4.0.0(electron@39.8.6)
|
||||||
'@mantine/colors-generator':
|
'@mantine/colors-generator':
|
||||||
specifier: ^8.3.18
|
specifier: ^9.1.1
|
||||||
version: 8.3.18(chroma-js@3.1.2)
|
version: 9.1.1(chroma-js@3.1.2)
|
||||||
'@mantine/core':
|
'@mantine/core':
|
||||||
specifier: ^8.3.18
|
specifier: ^9.1.1
|
||||||
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)
|
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':
|
'@mantine/dates':
|
||||||
specifier: ^8.3.18
|
specifier: ^9.1.1
|
||||||
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)
|
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':
|
'@mantine/form':
|
||||||
specifier: ^8.3.18
|
specifier: ^9.1.1
|
||||||
version: 8.3.18(react@19.2.4)
|
version: 9.1.1(react@19.2.4)
|
||||||
'@mantine/hooks':
|
'@mantine/hooks':
|
||||||
specifier: ^8.3.18
|
specifier: ^9.1.1
|
||||||
version: 8.3.18(react@19.2.4)
|
version: 9.1.1(react@19.2.4)
|
||||||
'@mantine/modals':
|
'@mantine/modals':
|
||||||
specifier: ^8.3.18
|
specifier: ^9.1.1
|
||||||
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)
|
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':
|
'@mantine/notifications':
|
||||||
specifier: ^8.3.18
|
specifier: ^9.1.1
|
||||||
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)
|
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':
|
'@radix-ui/react-context-menu':
|
||||||
specifier: ^2.2.16
|
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)
|
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==}
|
resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
|
|
||||||
'@mantine/colors-generator@8.3.18':
|
'@mantine/colors-generator@9.1.1':
|
||||||
resolution: {integrity: sha512-u7gNAuVD/WPvB49uNszfkn7lQr85OXTI0Ijkbutcymhv0/utqlapQvZlQAYHwdrrWpQgqPNLsDBt3VSheVe9jw==}
|
resolution: {integrity: sha512-mzEkW9oBrtvnq3zHW1jISa5hNWGi2wRgo4/N0W96DKxTqS0E1IMQ13KWJM0I9er8MK9xZo69jlpR7SyztRYFaw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
chroma-js: '>=2.4.2'
|
chroma-js: '>=2.4.2'
|
||||||
|
|
||||||
'@mantine/core@8.3.18':
|
'@mantine/core@9.1.1':
|
||||||
resolution: {integrity: sha512-9tph1lTVogKPjTx02eUxDUOdXacPzK62UuSqb4TdGliI54/Xgxftq0Dfqu6XuhCxn9J5MDJaNiLDvL/1KRkYqA==}
|
resolution: {integrity: sha512-vClOZdCeZ4oLYuA/3jAOgKGQ6dXbF6ZkzpYz09Gied9nZpB7HcQeb3dcMh8UPBE4f+EM7KlYWk6dch7GoASeaA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@mantine/hooks': 8.3.18
|
'@mantine/hooks': 9.1.1
|
||||||
react: ^18.x || ^19.x
|
react: ^19.2.0
|
||||||
react-dom: ^18.x || ^19.x
|
react-dom: ^19.2.0
|
||||||
|
|
||||||
'@mantine/dates@8.3.18':
|
'@mantine/dates@9.1.1':
|
||||||
resolution: {integrity: sha512-FHx5teJOhupI0gO2o5evtVYQEdqOjayOkLRhEQfB5Nc5DvcysfPfmNILGkc1Nrp9ZQeQWKLT9qr+CkcCXwHOaw==}
|
resolution: {integrity: sha512-P1tr/Hr+EVxppbOVpTLvaZZnM1W/r0TNpqNNMeM81xfyuKYzd7zt2/SQYb6BuudgEQfRJnAee+7bIJLEsrb0uA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@mantine/core': 8.3.18
|
'@mantine/core': 9.1.1
|
||||||
'@mantine/hooks': 8.3.18
|
'@mantine/hooks': 9.1.1
|
||||||
dayjs: '>=1.0.0'
|
dayjs: '>=1.0.0'
|
||||||
react: ^18.x || ^19.x
|
react: ^19.2.0
|
||||||
react-dom: ^18.x || ^19.x
|
react-dom: ^19.2.0
|
||||||
|
|
||||||
'@mantine/form@8.3.18':
|
'@mantine/form@9.1.1':
|
||||||
resolution: {integrity: sha512-r5OGLJWTkmIruFjRZRZy9oA7maNYlyt50jB4Pmd2X5360WOmJLd4KH8MFhHZQC7vN+z8/rmBl3t3XGAR2I8xig==}
|
resolution: {integrity: sha512-xmebZ3s8GGMrCOPOaOwA+gQkdgNVfT2F9kBtkjAbRoZrMoY+vYFbiPWbIvWFl8pU1jBslYZrj+M0PIawJmFOdQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.x || ^19.x
|
react: ^19.2.0
|
||||||
|
|
||||||
'@mantine/hooks@8.3.18':
|
'@mantine/hooks@9.1.1':
|
||||||
resolution: {integrity: sha512-QoWr9+S8gg5050TQ06aTSxtlpGjYOpIllRbjYYXlRvZeTsUqiTbVfvQROLexu4rEaK+yy9Wwriwl9PMRgbLqPw==}
|
resolution: {integrity: sha512-tTJK73nGFyy1v214TLdvBq0be7QCoc6osfbXVuJgOH3YG85lWk9Mvvor6k+w6hC6HXSqKMqLKePyiGm83xGcMg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.x || ^19.x
|
react: ^19.2.0
|
||||||
|
|
||||||
'@mantine/modals@8.3.18':
|
'@mantine/modals@9.1.1':
|
||||||
resolution: {integrity: sha512-JfPDS4549L314SxFPC1x6CbKwzh82OdnIzwgMxPCVNsWLKV2vEHHUH/fzUYj4Wli6IBrsW4cufjMj9BTj3hm3Q==}
|
resolution: {integrity: sha512-SjJ2kIheJaWoKMsSuYQlLFvuJTxCQTOl3gr+wDj/bLmGBgfUykLStRNm9s1H7vFxMIWtN20N8mwtcZV2dGeYBg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@mantine/core': 8.3.18
|
'@mantine/core': 9.1.1
|
||||||
'@mantine/hooks': 8.3.18
|
'@mantine/hooks': 9.1.1
|
||||||
react: ^18.x || ^19.x
|
react: ^19.2.0
|
||||||
react-dom: ^18.x || ^19.x
|
react-dom: ^19.2.0
|
||||||
|
|
||||||
'@mantine/notifications@8.3.18':
|
'@mantine/notifications@9.1.1':
|
||||||
resolution: {integrity: sha512-IpQ0lmwbigTBbZCR6iSYWqIOKEx1tlcd7PcEJ5M5X1qeVSY/N3mmDQt1eJmObvcyDeL5cTJMbSA9UPqhRqo9jw==}
|
resolution: {integrity: sha512-ZfcEMMDp0BQ+yKmVp8ifPXLKej8pv9TcaRnmy2CZ07USD61E9LH5ClRAP/hxQuCyf/qLb5BPHsI7+f3K8uhj4Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@mantine/core': 8.3.18
|
'@mantine/core': 9.1.1
|
||||||
'@mantine/hooks': 8.3.18
|
'@mantine/hooks': 9.1.1
|
||||||
react: ^18.x || ^19.x
|
react: ^19.2.0
|
||||||
react-dom: ^18.x || ^19.x
|
react-dom: ^19.2.0
|
||||||
|
|
||||||
'@mantine/store@8.3.18':
|
'@mantine/store@9.1.1':
|
||||||
resolution: {integrity: sha512-i+QRTLmZzLldea0egtUVnGALd6UMIu8jd44nrNWBSNIXJU/8B6rMlC6gyX+l4szopZSuOaaNJIXkqRdC1gQsVg==}
|
resolution: {integrity: sha512-kbxEU8wVGbobHlmQmk0lu9M+xCILKjuAPcMAshgzPznGLfXeE9zrB0gNT2cbk11Ik8dlV9J6Vsn9cuACyOSpfQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.x || ^19.x
|
react: ^19.2.0
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||||
@@ -1912,66 +1912,79 @@ packages:
|
|||||||
resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==}
|
resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm-musleabihf@4.60.1':
|
'@rollup/rollup-linux-arm-musleabihf@4.60.1':
|
||||||
resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==}
|
resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-gnu@4.60.1':
|
'@rollup/rollup-linux-arm64-gnu@4.60.1':
|
||||||
resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==}
|
resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-musl@4.60.1':
|
'@rollup/rollup-linux-arm64-musl@4.60.1':
|
||||||
resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==}
|
resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-linux-loong64-gnu@4.60.1':
|
'@rollup/rollup-linux-loong64-gnu@4.60.1':
|
||||||
resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==}
|
resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==}
|
||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-loong64-musl@4.60.1':
|
'@rollup/rollup-linux-loong64-musl@4.60.1':
|
||||||
resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==}
|
resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==}
|
||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-linux-ppc64-gnu@4.60.1':
|
'@rollup/rollup-linux-ppc64-gnu@4.60.1':
|
||||||
resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==}
|
resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-ppc64-musl@4.60.1':
|
'@rollup/rollup-linux-ppc64-musl@4.60.1':
|
||||||
resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==}
|
resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-gnu@4.60.1':
|
'@rollup/rollup-linux-riscv64-gnu@4.60.1':
|
||||||
resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==}
|
resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-musl@4.60.1':
|
'@rollup/rollup-linux-riscv64-musl@4.60.1':
|
||||||
resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==}
|
resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-linux-s390x-gnu@4.60.1':
|
'@rollup/rollup-linux-s390x-gnu@4.60.1':
|
||||||
resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==}
|
resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-gnu@4.60.1':
|
'@rollup/rollup-linux-x64-gnu@4.60.1':
|
||||||
resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==}
|
resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-musl@4.60.1':
|
'@rollup/rollup-linux-x64-musl@4.60.1':
|
||||||
resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==}
|
resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-openbsd-x64@4.60.1':
|
'@rollup/rollup-openbsd-x64@4.60.1':
|
||||||
resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==}
|
resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==}
|
||||||
@@ -2015,6 +2028,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
|
resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
|
||||||
engines: {node: '>=10'}
|
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':
|
'@surma/rollup-plugin-off-main-thread@2.2.3':
|
||||||
resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==}
|
resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==}
|
||||||
|
|
||||||
@@ -4710,12 +4726,6 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
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:
|
react-transition-group@4.4.5:
|
||||||
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
|
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -5358,6 +5368,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
|
resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
|
|
||||||
|
tagged-tag@1.0.0:
|
||||||
|
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
tar@7.5.13:
|
tar@7.5.13:
|
||||||
resolution: {integrity: sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==}
|
resolution: {integrity: sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -5454,9 +5468,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
|
resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
|
||||||
engines: {node: '>=12.20'}
|
engines: {node: '>=12.20'}
|
||||||
|
|
||||||
type-fest@4.41.0:
|
type-fest@5.6.0:
|
||||||
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
|
resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
typed-array-buffer@1.0.3:
|
typed-array-buffer@1.0.3:
|
||||||
resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
|
resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
|
||||||
@@ -5569,33 +5583,6 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
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:
|
use-sidecar@1.1.3:
|
||||||
resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
|
resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -7111,60 +7098,60 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
chroma-js: 3.1.2
|
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:
|
dependencies:
|
||||||
'@floating-ui/react': 0.27.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
'@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
|
clsx: 2.1.1
|
||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
react-dom: 19.2.4(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-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-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: 5.6.0
|
||||||
type-fest: 4.41.0
|
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@types/react'
|
- '@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:
|
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/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': 8.3.18(react@19.2.4)
|
'@mantine/hooks': 9.1.1(react@19.2.4)
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
dayjs: 1.11.20
|
dayjs: 1.11.20
|
||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
react-dom: 19.2.4(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:
|
dependencies:
|
||||||
|
'@standard-schema/spec': 1.1.0
|
||||||
fast-deep-equal: 3.1.3
|
fast-deep-equal: 3.1.3
|
||||||
klona: 2.0.6
|
klona: 2.0.6
|
||||||
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)':
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.4
|
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:
|
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/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': 8.3.18(react@19.2.4)
|
'@mantine/hooks': 9.1.1(react@19.2.4)
|
||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
react-dom: 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:
|
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/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': 8.3.18(react@19.2.4)
|
'@mantine/hooks': 9.1.1(react@19.2.4)
|
||||||
'@mantine/store': 8.3.18(react@19.2.4)
|
'@mantine/store': 9.1.1(react@19.2.4)
|
||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
react-dom: 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)
|
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:
|
dependencies:
|
||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
|
|
||||||
@@ -7611,6 +7598,8 @@ snapshots:
|
|||||||
|
|
||||||
'@sindresorhus/is@4.6.0': {}
|
'@sindresorhus/is@4.6.0': {}
|
||||||
|
|
||||||
|
'@standard-schema/spec@1.1.0': {}
|
||||||
|
|
||||||
'@surma/rollup-plugin-off-main-thread@2.2.3':
|
'@surma/rollup-plugin-off-main-thread@2.2.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
ejs: 3.1.10
|
ejs: 3.1.10
|
||||||
@@ -10672,15 +10661,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.14
|
'@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):
|
react-transition-group@4.4.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.29.2
|
'@babel/runtime': 7.29.2
|
||||||
@@ -11435,6 +11415,8 @@ snapshots:
|
|||||||
string-width: 4.2.3
|
string-width: 4.2.3
|
||||||
strip-ansi: 6.0.1
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
|
tagged-tag@1.0.0: {}
|
||||||
|
|
||||||
tar@7.5.13:
|
tar@7.5.13:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@isaacs/fs-minipass': 4.0.1
|
'@isaacs/fs-minipass': 4.0.1
|
||||||
@@ -11544,7 +11526,9 @@ snapshots:
|
|||||||
|
|
||||||
type-fest@2.19.0: {}
|
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:
|
typed-array-buffer@1.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -11664,25 +11648,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.14
|
'@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):
|
use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
detect-node-es: 1.1.0
|
detect-node-es: 1.1.0
|
||||||
|
|||||||
@@ -139,7 +139,9 @@
|
|||||||
"lyricOffset": "demora de la lletra (ms)",
|
"lyricOffset": "demora de la lletra (ms)",
|
||||||
"showLyricMatch": "mosta coincidències de lletres",
|
"showLyricMatch": "mosta coincidències de lletres",
|
||||||
"showLyricProvider": "mostra el proveïdor de la lletra",
|
"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",
|
"lyrics": "lletres",
|
||||||
"visualizer": "visualitzador",
|
"visualizer": "visualitzador",
|
||||||
@@ -337,7 +339,8 @@
|
|||||||
"filter_single": "senzill",
|
"filter_single": "senzill",
|
||||||
"filter_multiple": "multi",
|
"filter_multiple": "multi",
|
||||||
"rename": "reanomena",
|
"rename": "reanomena",
|
||||||
"newVersionAvailable": "hi ha una nova versió disponible"
|
"newVersionAvailable": "hi ha una nova versió disponible",
|
||||||
|
"numberOfResults": "{{numberOfResults}} resultats"
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"album_one": "àlbum",
|
"album_one": "àlbum",
|
||||||
@@ -406,7 +409,8 @@
|
|||||||
"input_skipDuplicates": "salta't els duplicats",
|
"input_skipDuplicates": "salta't els duplicats",
|
||||||
"success": "s'ha afegit $t(entity.trackWithCount, {\"count\": {{message}} }) a $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
"success": "s'ha afegit $t(entity.trackWithCount, {\"count\": {{message}} }) a $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
||||||
"create": "crea $t(entity.playlist, {\"count\": 1}) {{playlist}}",
|
"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": {
|
"createPlaylist": {
|
||||||
"input_description": "$t(common.description)",
|
"input_description": "$t(common.description)",
|
||||||
@@ -506,6 +510,9 @@
|
|||||||
"export": "exporta la lletra",
|
"export": "exporta la lletra",
|
||||||
"input_synced": "exporta la lletra sincronitzada",
|
"input_synced": "exporta la lletra sincronitzada",
|
||||||
"input_offset": "$t(setting.lyricOffset)"
|
"input_offset": "$t(setting.lyricOffset)"
|
||||||
|
},
|
||||||
|
"editRadioStation": {
|
||||||
|
"success": "Emissora de ràdio actualitzada amb èxit"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"action": {
|
"action": {
|
||||||
@@ -522,7 +529,10 @@
|
|||||||
"goToPage": "anar a la pàgina",
|
"goToPage": "anar a la pàgina",
|
||||||
"openIn": {
|
"openIn": {
|
||||||
"lastfm": "Obrir a Last.fm",
|
"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",
|
"deselectAll": "deselecciona-ho tot",
|
||||||
"viewPlaylists": "veure $t(entity.playlist, {\"count\": 2})",
|
"viewPlaylists": "veure $t(entity.playlist, {\"count\": 2})",
|
||||||
@@ -597,7 +607,7 @@
|
|||||||
"artistConfiguration": "configuració de la pàgina de l'artista de l'àlbum",
|
"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",
|
"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": "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": "mida dels botons de la barra de reproducció",
|
||||||
"buttonSize_description": "la 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",
|
"clearCache": "neteja la memòria del navegador",
|
||||||
@@ -910,7 +920,25 @@
|
|||||||
"primaryShade": "ombra primària",
|
"primaryShade": "ombra primària",
|
||||||
"primaryShade_description": "substitueix el to primari (0–9) utilitzat per a botons, enllaços i altres elements de color primari",
|
"primaryShade_description": "substitueix el to primari (0–9) 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_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": {
|
"table": {
|
||||||
"column": {
|
"column": {
|
||||||
@@ -1360,6 +1388,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pasteGradient": "enganxa degradat",
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@
|
|||||||
"hotkey_zoomIn": "přiblížení",
|
"hotkey_zoomIn": "přiblížení",
|
||||||
"scrobble_description": "scrobblovat přehrání na váš multimediální server",
|
"scrobble_description": "scrobblovat přehrání na váš multimediální server",
|
||||||
"hotkey_browserForward": "vpřed v prohlížeči",
|
"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",
|
"discordUpdateInterval": "interval aktualizací {{discord}} rich presence",
|
||||||
"themeLight": "motiv (světlý)",
|
"themeLight": "motiv (světlý)",
|
||||||
"fontType_optionBuiltIn": "vestavěné písmo",
|
"fontType_optionBuiltIn": "vestavěné písmo",
|
||||||
@@ -425,7 +425,11 @@
|
|||||||
"sidePlayQueueLayout_optionHorizontal": "na šířku",
|
"sidePlayQueueLayout_optionHorizontal": "na šířku",
|
||||||
"sidePlayQueueLayout_optionVertical": "na výšku",
|
"sidePlayQueueLayout_optionVertical": "na výšku",
|
||||||
"waveformLoadingDelay": "zpoždění načítání vlnové křivky",
|
"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": {
|
"action": {
|
||||||
"editPlaylist": "upravit $t(entity.playlist, {\"count\": 1})",
|
"editPlaylist": "upravit $t(entity.playlist, {\"count\": 1})",
|
||||||
@@ -825,7 +829,9 @@
|
|||||||
"lyricGap": "mezera textů",
|
"lyricGap": "mezera textů",
|
||||||
"dynamicImageBlur": "velikost rozostření obrázku",
|
"dynamicImageBlur": "velikost rozostření obrázku",
|
||||||
"dynamicIsImage": "povolit obrázek na pozadí",
|
"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ší",
|
"upNext": "další",
|
||||||
"lyrics": "texty",
|
"lyrics": "texty",
|
||||||
@@ -1041,7 +1047,8 @@
|
|||||||
"input_skipDuplicates": "přeskočit duplicity",
|
"input_skipDuplicates": "přeskočit duplicity",
|
||||||
"input_playlists": "$t(entity.playlist, {\"count\": 2})",
|
"input_playlists": "$t(entity.playlist, {\"count\": 2})",
|
||||||
"create": "vytvořit $t(entity.playlist, {\"count\": 1}) {{playlist}}",
|
"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": {
|
"updateServer": {
|
||||||
"title": "upravit server",
|
"title": "upravit server",
|
||||||
@@ -1381,6 +1388,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pasteGradient": "Vložit přechod",
|
"pasteGradient": "Vložit přechod",
|
||||||
"pasteGradientPlaceholder": "Sem vložte JSON přechodu…"
|
"pasteGradientPlaceholder": "Sem vložte JSON přechodu…",
|
||||||
|
"systemAudioConsentAllow": "Povolit",
|
||||||
|
"systemAudioConsentBody": "Vizualizér potřebuje pro svou činnost přístup k systémovému zvuku",
|
||||||
|
"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",
|
||||||
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-27
@@ -242,7 +242,7 @@
|
|||||||
"criticRating": "Kritikerbewertung",
|
"criticRating": "Kritikerbewertung",
|
||||||
"album": "$t(entity.album, {\"count\": 1})",
|
"album": "$t(entity.album, {\"count\": 1})",
|
||||||
"trackNumber": "Track",
|
"trackNumber": "Track",
|
||||||
"channels": "$t(common.channel_other)",
|
"channels": "$t(common.channel,{\"count\":2})",
|
||||||
"owner": "$t(common.owner)",
|
"owner": "$t(common.owner)",
|
||||||
"genre": "$t(entity.genre, {\"count\": 1})",
|
"genre": "$t(entity.genre, {\"count\": 1})",
|
||||||
"artist": "$t(entity.artist, {\"count\": 1})",
|
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||||
@@ -273,13 +273,13 @@
|
|||||||
"input_name": "Servername",
|
"input_name": "Servername",
|
||||||
"success": "Server erfolgreich hinzugefügt",
|
"success": "Server erfolgreich hinzugefügt",
|
||||||
"input_savePassword": "Passwort speichern",
|
"input_savePassword": "Passwort speichern",
|
||||||
"ignoreSsl": "SSL ignorieren $t(common.restartRequired)",
|
"ignoreSsl": "SSL ignorieren ($t(common.restartRequired))",
|
||||||
"ignoreCors": "CORS ignorieren $t(common.restartRequired)",
|
"ignoreCors": "CORS ignorieren ($t(common.restartRequired))",
|
||||||
"error_savePassword": "Beim Speichern des Passworts ist ein Fehler aufgetreten",
|
"error_savePassword": "Beim Speichern des Passworts ist ein Fehler aufgetreten",
|
||||||
"input_preferInstantMix": "Instant-Mix bevorzugen",
|
"input_preferInstantMix": "Instant-Mix bevorzugen",
|
||||||
"input_preferInstantMixDescription": "nur Instant-Mix verwenden, um ähnliche Songs zu erhalten. Nützlich bei Verwendung von Plugins, die in dieses Verhalten eingreifen",
|
"input_preferInstantMixDescription": "nur Instant-Mix verwenden, um ähnliche Songs zu erhalten. Nützlich bei Verwendung von Plugins, die in dieses Verhalten eingreifen",
|
||||||
"input_preferRemoteUrl": "öffentliche URL bevorzugen",
|
"input_preferRemoteUrl": "öffentliche URL bevorzugen",
|
||||||
"input_remoteUrl": "Öffentliche URL",
|
"input_remoteUrl": "öffentliche URL",
|
||||||
"input_remoteUrlPlaceholder": "Optional: öffentliche URL für externe Funktionen"
|
"input_remoteUrlPlaceholder": "Optional: öffentliche URL für externe Funktionen"
|
||||||
},
|
},
|
||||||
"addToPlaylist": {
|
"addToPlaylist": {
|
||||||
@@ -357,6 +357,9 @@
|
|||||||
"input_offset": "$t(setting.lyricOffset)",
|
"input_offset": "$t(setting.lyricOffset)",
|
||||||
"export": "Songtexte exportieren",
|
"export": "Songtexte exportieren",
|
||||||
"input_synced": "Synchronisierte Songtexte exportieren"
|
"input_synced": "Synchronisierte Songtexte exportieren"
|
||||||
|
},
|
||||||
|
"editRadioStation": {
|
||||||
|
"success": "Radiosender erfolgreich aktualisiert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
@@ -426,8 +429,8 @@
|
|||||||
"pagination": "Seitenzahlen",
|
"pagination": "Seitenzahlen",
|
||||||
"pagination_itemsPerPage": "Elemente pro Seite",
|
"pagination_itemsPerPage": "Elemente pro Seite",
|
||||||
"pagination_infinite": "unendlich",
|
"pagination_infinite": "unendlich",
|
||||||
"moveUp": "Nach oben bewegen",
|
"moveUp": "nach oben",
|
||||||
"moveDown": "Nach unten bewegen",
|
"moveDown": "nach unten",
|
||||||
"pinToLeft": "links anheften",
|
"pinToLeft": "links anheften",
|
||||||
"pinToRight": "rechts anheften",
|
"pinToRight": "rechts anheften",
|
||||||
"itemGap": "Item Abstand (px)",
|
"itemGap": "Item Abstand (px)",
|
||||||
@@ -450,13 +453,13 @@
|
|||||||
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
|
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
|
||||||
"artist": "$t(entity.artist, {\"count\": 1})",
|
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||||
"favorite": "$t(common.favorite)",
|
"favorite": "$t(common.favorite)",
|
||||||
"actions": "$t(common.action_other)",
|
"actions": "$t(common.action,{\"count\":2})",
|
||||||
"genre": "$t(entity.genre, {\"count\": 1})",
|
"genre": "$t(entity.genre, {\"count\": 1})",
|
||||||
"album": "$t(entity.album, {\"count\": 1})",
|
"album": "$t(entity.album, {\"count\": 1})",
|
||||||
"size": "$t(common.size)",
|
"size": "$t(common.size)",
|
||||||
"bpm": "$t(common.bpm)",
|
"bpm": "$t(common.bpm)",
|
||||||
"titleCombined": "$t(common.title) (kombiniert)",
|
"titleCombined": "$t(common.title) (kombiniert)",
|
||||||
"channels": "$t(common.channel_other)",
|
"channels": "$t(common.channel,{\"count\":2})",
|
||||||
"duration": "$t(common.duration)",
|
"duration": "$t(common.duration)",
|
||||||
"note": "$t(common.note)",
|
"note": "$t(common.note)",
|
||||||
"owner": "$t(common.owner)",
|
"owner": "$t(common.owner)",
|
||||||
@@ -493,7 +496,7 @@
|
|||||||
"rating": "Bewertung",
|
"rating": "Bewertung",
|
||||||
"albumCount": "$t(entity.album, {\"count\": 2})",
|
"albumCount": "$t(entity.album, {\"count\": 2})",
|
||||||
"artist": "$t(entity.artist, {\"count\": 1})",
|
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||||
"channels": "$t(common.channel_other)",
|
"channels": "$t(common.channel,{\"count\":2})",
|
||||||
"comment": "Kommentar",
|
"comment": "Kommentar",
|
||||||
"dateAdded": "hinzugefügt am",
|
"dateAdded": "hinzugefügt am",
|
||||||
"playCount": "Abgespielt",
|
"playCount": "Abgespielt",
|
||||||
@@ -716,7 +719,8 @@
|
|||||||
},
|
},
|
||||||
"releasenotes": {
|
"releasenotes": {
|
||||||
"commitsSinceStable": "Commits seit {{stable}}",
|
"commitsSinceStable": "Commits seit {{stable}}",
|
||||||
"noStableReleaseToCompare": "Kein stable Relase zum vergleichen verfügbar"
|
"noStableReleaseToCompare": "Kein stable Relase zum vergleichen verfügbar",
|
||||||
|
"noNewCommits": "keine neuen Beiträge in diesem Bereich"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
@@ -766,12 +770,12 @@
|
|||||||
"sleepTimer": "Sleep Timer",
|
"sleepTimer": "Sleep Timer",
|
||||||
"sleepTimer_custom": "Benutzerdefiniert",
|
"sleepTimer_custom": "Benutzerdefiniert",
|
||||||
"sleepTimer_hours": "{{count}} std",
|
"sleepTimer_hours": "{{count}} std",
|
||||||
"sleepTimer_minutes": "{{count}} min",
|
"sleepTimer_minutes": "{{count}} Min",
|
||||||
"trackRadio": "Song Radio",
|
"trackRadio": "Song Radio",
|
||||||
"albumRadio": "Album Radio"
|
"albumRadio": "Album Radio"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"audioDevice_description": "wählen Sie das Audiogerät aus, das für die Wiedergabe verwendet werden soll (nur Webplayer)",
|
"audioDevice_description": "das für die Wiedergabe zu verwendende Audiogerät auswählen",
|
||||||
"audioExclusiveMode": "Audio-Exklusivmodus",
|
"audioExclusiveMode": "Audio-Exklusivmodus",
|
||||||
"audioDevice": "Audiogerät",
|
"audioDevice": "Audiogerät",
|
||||||
"accentColor": "Akzentfarbe",
|
"accentColor": "Akzentfarbe",
|
||||||
@@ -789,7 +793,7 @@
|
|||||||
"crossfadeDuration": "Dauer der Überblendung",
|
"crossfadeDuration": "Dauer der Überblendung",
|
||||||
"discordIdleStatus": "rich presence status im Leerlauf",
|
"discordIdleStatus": "rich presence status im Leerlauf",
|
||||||
"audioPlayer": "Audio-Player",
|
"audioPlayer": "Audio-Player",
|
||||||
"discordApplicationId": "{{discord}} Anwendungs ID",
|
"discordApplicationId": "{{discord}} Anwendungs-ID",
|
||||||
"customFontPath_description": "Legt den Pfad zur benutzerdefinierten Schriftart fest, welche für die Anwendung verwendet werden soll",
|
"customFontPath_description": "Legt den Pfad zur benutzerdefinierten Schriftart fest, welche für die Anwendung verwendet werden soll",
|
||||||
"remotePort_description": "Legt den Port des Fernsteuerungsserver fest",
|
"remotePort_description": "Legt den Port des Fernsteuerungsserver fest",
|
||||||
"hotkey_skipBackward": "rückwärts springen",
|
"hotkey_skipBackward": "rückwärts springen",
|
||||||
@@ -1009,7 +1013,7 @@
|
|||||||
"transcodeFormat": "Format für Umwandlung",
|
"transcodeFormat": "Format für Umwandlung",
|
||||||
"startMinimized_description": "Startet die Anwendung im Info-Bereich",
|
"startMinimized_description": "Startet die Anwendung im Info-Bereich",
|
||||||
"startMinimized": "Im Info-Bereich starten",
|
"startMinimized": "Im Info-Bereich starten",
|
||||||
"mediaSession_description": "Aktiviert die Windows Media Session-Integration, zeigt Mediensteuerelemente und Metadaten im Systemlautstärke-Overlay und auf dem Sperrbildschirm an (nur Windows)",
|
"mediaSession_description": "aktiviert die Media Session Integration. Dies ermöglicht die Steuerung und Anzeige der Medien in der Systemlautstärkeoption und auf dem Sperrbildschirm",
|
||||||
"mediaSession": "Media Session aktivieren",
|
"mediaSession": "Media Session aktivieren",
|
||||||
"artistBackgroundBlur": "Unschärfegrad für Künstlerhintergründe",
|
"artistBackgroundBlur": "Unschärfegrad für Künstlerhintergründe",
|
||||||
"artistBackgroundBlur_description": "Legt den Grad der Unschärfe fest, der auf das Hintergrundbild des Künstlers angewendet wird",
|
"artistBackgroundBlur_description": "Legt den Grad der Unschärfe fest, der auf das Hintergrundbild des Künstlers angewendet wird",
|
||||||
@@ -1017,11 +1021,11 @@
|
|||||||
"contextMenu_description": "Legt die Einträge fest, die im Rechtsklick-Menü angezeigt werden sollen. Abgewählte Einträge werden ausgeblendet",
|
"contextMenu_description": "Legt die Einträge fest, die im Rechtsklick-Menü angezeigt werden sollen. Abgewählte Einträge werden ausgeblendet",
|
||||||
"crossfadeStyle": "Art der Überblende",
|
"crossfadeStyle": "Art der Überblende",
|
||||||
"customCss_description": "Benutzerdefinierter CSS-Inhalt. Hinweis: Inhalte und Remote-URLs sind nicht zulässige Eigenschaften. Unten siehst du eine Vorschau deines Inhalts. Aufgrund von Bereinigung werden womöglich zusätzliche, nicht von dir definierte Felder angezeigt",
|
"customCss_description": "Benutzerdefinierter CSS-Inhalt. Hinweis: Inhalte und Remote-URLs sind nicht zulässige Eigenschaften. Unten siehst du eine Vorschau deines Inhalts. Aufgrund von Bereinigung werden womöglich zusätzliche, nicht von dir definierte Felder angezeigt",
|
||||||
"customCssNotice": "Warnung: Obwohl eine gewisse Bereinigung erfolgt (url() und content: sind nicht zulässig), kann die Verwendung von benutzerdefiniertem CSS dennoch Risiken mit sich bringen, da dadurch die Benutzeroberfläche verändert wird",
|
"customCssNotice": "Warnung: Obwohl eine gewisse Bereinigung erfolgt (nicht zulässig sind z. B. \"url()\" und \"content:\"), kann ein benutzerdefiniertes CSS Risiken mit sich bringen, da die Benutzeroberfläche dadurch verändert wird",
|
||||||
"releaseChannel_optionBeta": "Beta",
|
"releaseChannel_optionBeta": "Beta",
|
||||||
"releaseChannel_optionLatest": "Stabil",
|
"releaseChannel_optionLatest": "Stabil",
|
||||||
"releaseChannel": "Veröffentlichungskanal",
|
"releaseChannel": "Veröffentlichungskanal",
|
||||||
"releaseChannel_description": "Zwischen stabilen und beta Veröffentlichungen für automatische Aktualisierungen wählen",
|
"releaseChannel_description": "zwischen stabilen, Beta- oder Alpha-Versionen (Nightly) für automatische Updates wählen",
|
||||||
"discordDisplayType_artistname": "Künstlername(n)",
|
"discordDisplayType_artistname": "Künstlername(n)",
|
||||||
"discordDisplayType_description": "Ändert den aktuellen Titel im Zuhör-Status",
|
"discordDisplayType_description": "Ändert den aktuellen Titel im Zuhör-Status",
|
||||||
"discordDisplayType_songname": "Songtitel",
|
"discordDisplayType_songname": "Songtitel",
|
||||||
@@ -1121,7 +1125,13 @@
|
|||||||
"nativeSpotify": "Spotify App benutzen",
|
"nativeSpotify": "Spotify App benutzen",
|
||||||
"qobuz_description": "Zeige Links zu Qobuz auf den Interpreten/Alben Seiten",
|
"qobuz_description": "Zeige Links zu Qobuz auf den Interpreten/Alben Seiten",
|
||||||
"spotify_description": "Zeige Links zu Spotify auf den Interpreten/Alben Seiten",
|
"spotify_description": "Zeige Links zu Spotify auf den Interpreten/Alben Seiten",
|
||||||
"artistReleaseTypeConfiguration": "Interpreten Release Typ Einstellung"
|
"artistReleaseTypeConfiguration": "Interpreten Release Typ Einstellung",
|
||||||
|
"discordStateIcon": "Play Icon anzeigen",
|
||||||
|
"homeFeatureStyle_optionSingle": "Einzeln",
|
||||||
|
"nativeSpotify_description": "in der Spotify App statt im Browser öffnen",
|
||||||
|
"imageResolution_optionFullScreenPlayer": "Wiedergabe im Vollbildmodus",
|
||||||
|
"sidePlayQueueLayout_optionHorizontal": "horizontal",
|
||||||
|
"sidePlayQueueLayout_optionVertical": "vertikal"
|
||||||
},
|
},
|
||||||
"dragDropZone": {
|
"dragDropZone": {
|
||||||
"error_oneFileOnly": "Bitte wähle nur 1 Datei",
|
"error_oneFileOnly": "Bitte wähle nur 1 Datei",
|
||||||
@@ -1211,7 +1221,7 @@
|
|||||||
"dualVertical": "Dual-Vertikal"
|
"dualVertical": "Dual-Vertikal"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"minimumFrequency": "Mindestfrequenz",
|
"minimumFrequency": "Minimale Frequenz",
|
||||||
"minimumDecibels": "Minimale Dezibel",
|
"minimumDecibels": "Minimale Dezibel",
|
||||||
"visualizerType": "Visualizer Art",
|
"visualizerType": "Visualizer Art",
|
||||||
"cyclePresets": "Vorlagen durchrotieren",
|
"cyclePresets": "Vorlagen durchrotieren",
|
||||||
@@ -1246,10 +1256,10 @@
|
|||||||
"channelLayout": "Kanallayout",
|
"channelLayout": "Kanallayout",
|
||||||
"maxFPS": "Max FPS",
|
"maxFPS": "Max FPS",
|
||||||
"opacity": "Deckkraft",
|
"opacity": "Deckkraft",
|
||||||
"customGradients": "Benutzerdefinierte Gradienten",
|
"customGradients": "Benutzerdefinierter Farbverlauf",
|
||||||
"addCustomGradient": "Benutzerdefinierten Gradienten hinzufügen",
|
"addCustomGradient": "Benutzerdefinierten Gradienten hinzufügen",
|
||||||
"gradientName": "Gradientenname",
|
"gradientName": "Name Farbverlauf",
|
||||||
"gradientNamePlaceholder": "Gradientenname",
|
"gradientNamePlaceholder": "Name Farbverlauf",
|
||||||
"vertical": "Vertikal",
|
"vertical": "Vertikal",
|
||||||
"horizontal": "Horizontal",
|
"horizontal": "Horizontal",
|
||||||
"addColor": "Farbe hinzufügen",
|
"addColor": "Farbe hinzufügen",
|
||||||
@@ -1262,9 +1272,9 @@
|
|||||||
"builtIn": "Eingebaut",
|
"builtIn": "Eingebaut",
|
||||||
"colors": "Farben",
|
"colors": "Farben",
|
||||||
"colorMode": "Farbmodus",
|
"colorMode": "Farbmodus",
|
||||||
"gradient": "Gradienten",
|
"gradient": "Farbverlauf",
|
||||||
"gradientLeft": "Gradienten links",
|
"gradientLeft": "Farberverlauf links",
|
||||||
"gradientRight": "Gradienten rechts",
|
"gradientRight": "Farbverlauf rechts",
|
||||||
"fft": "FFT",
|
"fft": "FFT",
|
||||||
"fftSize": "FFT Größe",
|
"fftSize": "FFT Größe",
|
||||||
"smoothing": "Glätten",
|
"smoothing": "Glätten",
|
||||||
@@ -1273,17 +1283,20 @@
|
|||||||
"sensitivity": "Empfindlichkeit",
|
"sensitivity": "Empfindlichkeit",
|
||||||
"weightingFilter": "Gewichtungsfilter",
|
"weightingFilter": "Gewichtungsfilter",
|
||||||
"maximumDecibels": "Maximale Dezibel",
|
"maximumDecibels": "Maximale Dezibel",
|
||||||
"linearAmplitude": "Lineare Amplitude",
|
"linearAmplitude": "Linearer Ausschlag",
|
||||||
"linearBoost": "Linearer Boost",
|
"linearBoost": "Linearer Boost",
|
||||||
"radialSpectrum": "Radiales Spektrum",
|
"radialSpectrum": "Radiales Spektrum",
|
||||||
"radial": "Radial",
|
"radial": "Radial",
|
||||||
"radialInvert": "Radial invertiert",
|
"radialInvert": "Radial invertiert",
|
||||||
"radius": "Radius",
|
"radius": "Radius",
|
||||||
"miscellaneousSettings": "Verschiedenes Einstellungen",
|
"miscellaneousSettings": "Verschiedene Einstellungen",
|
||||||
"ansiBands": "ANSI Bänder",
|
"ansiBands": "ANSI Bänder",
|
||||||
"lowResolution": "Niedrige Auflösung",
|
"lowResolution": "Niedrige Auflösung",
|
||||||
"showFPS": "FPS anzeigen",
|
"showFPS": "FPS anzeigen",
|
||||||
"fadePeaks": "Spitzen abblenden",
|
"fadePeaks": "Spitzen abblenden",
|
||||||
"showPeaks": "Spitzen anzeigen"
|
"showPeaks": "Spitzen anzeigen",
|
||||||
|
"systemAudioConsentAllow": "Erlauben",
|
||||||
|
"systemAudioConsentDecline": "Ablehnen",
|
||||||
|
"frequencyScale": "Frequenzskala"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -347,6 +347,7 @@
|
|||||||
"input_skipDuplicates": "skip duplicates",
|
"input_skipDuplicates": "skip duplicates",
|
||||||
"searchOrCreate": "search $t(entity.playlist, {\"count\": 2}) or type to create a new one",
|
"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}} })",
|
"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})"
|
"title": "add to $t(entity.playlist, {\"count\": 1})"
|
||||||
},
|
},
|
||||||
"createPlaylist": {
|
"createPlaylist": {
|
||||||
@@ -540,6 +541,8 @@
|
|||||||
"lyricOffset": "lyrics offset (ms)",
|
"lyricOffset": "lyrics offset (ms)",
|
||||||
"lyricGap": "lyric gap",
|
"lyricGap": "lyric gap",
|
||||||
"lyricSize": "lyric size",
|
"lyricSize": "lyric size",
|
||||||
|
"lyricOpacityNonActive": "non-active lyric opacity",
|
||||||
|
"lyricScaleNonActive": "non-active lyric scale",
|
||||||
"opacity": "opacity",
|
"opacity": "opacity",
|
||||||
"showLyricMatch": "show lyric match",
|
"showLyricMatch": "show lyric match",
|
||||||
"showLyricProvider": "show lyric provider",
|
"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",
|
"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_description": "select the audio device to use for playback",
|
||||||
"audioDevice": "audio device",
|
"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",
|
"audioExclusiveMode": "audio exclusive mode",
|
||||||
"audioPlayer_description": "select the audio player to use for playback",
|
"audioPlayer_description": "select the audio player to use for playback",
|
||||||
"audioPlayer": "audio player",
|
"audioPlayer": "audio player",
|
||||||
@@ -983,6 +986,8 @@
|
|||||||
"playerbarWaveformBarWidth": "waveform bar width",
|
"playerbarWaveformBarWidth": "waveform bar width",
|
||||||
"playerbarWaveformGap": "waveform gap",
|
"playerbarWaveformGap": "waveform gap",
|
||||||
"playerbarWaveformRadius": "waveform radius",
|
"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_description": "prefer local lyrics over remote lyrics when available",
|
||||||
"preferLocalLyrics": "prefer local lyrics",
|
"preferLocalLyrics": "prefer local lyrics",
|
||||||
"showLyricsInSidebar_description": "a panel will be added to the attached play queue that displays the 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",
|
"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_description": "prevent the display from sleeping while music is playing",
|
||||||
"preventSleepOnPlayback": "prevent sleep on playback",
|
"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_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",
|
"remotePassword": "remote control server password",
|
||||||
"remotePort_description": "sets the port for the remote control server",
|
"remotePort_description": "sets the port for the remote control server",
|
||||||
@@ -1220,6 +1227,7 @@
|
|||||||
"systemAudioConsentTitle": "Allow access to system audio?",
|
"systemAudioConsentTitle": "Allow access to system audio?",
|
||||||
"systemAudioCaptureFailed": "Could not start capture: {{message}}",
|
"systemAudioCaptureFailed": "Could not start capture: {{message}}",
|
||||||
"systemAudioNoAudioTrack": "No audio track was returned. Ensure audio capture is enabled when prompted.",
|
"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",
|
"visualizerType": "Visualizer Type",
|
||||||
"cyclePresets": "Cycle Presets",
|
"cyclePresets": "Cycle Presets",
|
||||||
"cycleTime": "Cycle Time (seconds)",
|
"cycleTime": "Cycle Time (seconds)",
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
"replayGainClipping": "recortar {{ReplayGain}}",
|
"replayGainClipping": "recortar {{ReplayGain}}",
|
||||||
"hotkey_zoomIn": "ampliar",
|
"hotkey_zoomIn": "ampliar",
|
||||||
"scrobble_description": "hace scrobble de las reproducciones en tu servidor de medios",
|
"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}}",
|
"discordUpdateInterval": "intervalo de actualización del estado de actividad de {{discord}}",
|
||||||
"themeLight": "tema (claro)",
|
"themeLight": "tema (claro)",
|
||||||
"fontType_optionBuiltIn": "fuente incorporada",
|
"fontType_optionBuiltIn": "fuente incorporada",
|
||||||
@@ -425,7 +425,11 @@
|
|||||||
"sidePlayQueueLayout": "Diseño de la cola de reproducción lateral",
|
"sidePlayQueueLayout": "Diseño de la cola de reproducción lateral",
|
||||||
"sidePlayQueueLayout_description": "Establece el diseño de la cola de reproducción lateral adjunta",
|
"sidePlayQueueLayout_description": "Establece el diseño de la cola de reproducción lateral adjunta",
|
||||||
"waveformLoadingDelay": "Retraso de carga de la forma de onda",
|
"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": {
|
"action": {
|
||||||
"editPlaylist": "editar $t(entity.playlist, {\"count\": 1})",
|
"editPlaylist": "editar $t(entity.playlist, {\"count\": 1})",
|
||||||
@@ -772,7 +776,9 @@
|
|||||||
"lyricGap": "desfase de letra",
|
"lyricGap": "desfase de letra",
|
||||||
"dynamicImageBlur": "tamaño de desenfoque de imagen",
|
"dynamicImageBlur": "tamaño de desenfoque de imagen",
|
||||||
"dynamicIsImage": "habilitar imagen de fondo",
|
"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",
|
"lyrics": "letras",
|
||||||
"related": "relacionado",
|
"related": "relacionado",
|
||||||
@@ -932,7 +938,8 @@
|
|||||||
"input_skipDuplicates": "saltar duplicados",
|
"input_skipDuplicates": "saltar duplicados",
|
||||||
"input_playlists": "$t(entity.playlist, {\"count\": 2})",
|
"input_playlists": "$t(entity.playlist, {\"count\": 2})",
|
||||||
"create": "Crear $t(entity.playlist, {\"count\": 1}) {{playlist}}",
|
"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": {
|
"updateServer": {
|
||||||
"title": "actualizar servidor",
|
"title": "actualizar servidor",
|
||||||
@@ -1001,6 +1008,9 @@
|
|||||||
"export": "Exportar letras",
|
"export": "Exportar letras",
|
||||||
"input_synced": "Exportar letras sincronizadas",
|
"input_synced": "Exportar letras sincronizadas",
|
||||||
"input_offset": "$t(setting.lyricOffset)"
|
"input_offset": "$t(setting.lyricOffset)"
|
||||||
|
},
|
||||||
|
"editRadioStation": {
|
||||||
|
"success": "Estación de radio actualizada con éxito"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
@@ -1378,6 +1388,13 @@
|
|||||||
"lowResolution": "Baja resolución",
|
"lowResolution": "Baja resolución",
|
||||||
"splitGradient": "Dividir degradado",
|
"splitGradient": "Dividir degradado",
|
||||||
"noteLabels": "Etiquetas de notas",
|
"noteLabels": "Etiquetas de notas",
|
||||||
"lumiBars": "Barras luminiscentes"
|
"lumiBars": "Barras luminiscentes",
|
||||||
|
"systemAudioConsentAllow": "Permitir",
|
||||||
|
"systemAudioConsentDecline": "Denegar",
|
||||||
|
"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.",
|
||||||
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,7 +157,8 @@
|
|||||||
"doNotShowAgain": "ez erakutsi hau berriro",
|
"doNotShowAgain": "ez erakutsi hau berriro",
|
||||||
"numberOfResults": "{{numberOfResults}} emaitza",
|
"numberOfResults": "{{numberOfResults}} emaitza",
|
||||||
"rename": "berrizendatu",
|
"rename": "berrizendatu",
|
||||||
"newVersionAvailable": "bertsio berri bat eskuragarri dago"
|
"newVersionAvailable": "bertsio berri bat eskuragarri dago",
|
||||||
|
"gridRows": "sareta-errenkadak"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
"repeat": "errepikatu",
|
"repeat": "errepikatu",
|
||||||
@@ -409,7 +410,8 @@
|
|||||||
"fromYear": "urtetik aurrera",
|
"fromYear": "urtetik aurrera",
|
||||||
"explicitStatus": "$t(common.explicitStatus)",
|
"explicitStatus": "$t(common.explicitStatus)",
|
||||||
"matchAnd": "eta",
|
"matchAnd": "eta",
|
||||||
"matchOr": "edo"
|
"matchOr": "edo",
|
||||||
|
"sortName": "izenaren arabera ordenatu"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"hotkey_playbackPause": "pausatu",
|
"hotkey_playbackPause": "pausatu",
|
||||||
@@ -643,7 +645,7 @@
|
|||||||
"passwordStore_description": "zein pasahitz/sekretu biltegi erabili. aldatu hau pasahitzak gordetzeko arazoak badituzu",
|
"passwordStore_description": "zein pasahitz/sekretu biltegi erabili. aldatu hau pasahitzak gordetzeko arazoak badituzu",
|
||||||
"playerFilters": "Iragazi ilarako abestiak",
|
"playerFilters": "Iragazi ilarako abestiak",
|
||||||
"sidePlayQueueStyle_description": "alboko erreprodukzio-ilararen estiloa ezartzen du",
|
"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",
|
"sidePlayQueueStyle": "alboko erreprodukzio-ilarako estiloa",
|
||||||
"skipPlaylistPage": "saltatu erreprodukzio-zerrenda orria",
|
"skipPlaylistPage": "saltatu erreprodukzio-zerrenda orria",
|
||||||
"startMinimized_description": "abiarazi aplikazioa sistemaren erretiluan",
|
"startMinimized_description": "abiarazi aplikazioa sistemaren erretiluan",
|
||||||
@@ -714,7 +716,19 @@
|
|||||||
"preservePitch": "mantendu tonua",
|
"preservePitch": "mantendu tonua",
|
||||||
"preventSleepOnPlayback": "erreprodukzioan loa saihestu",
|
"preventSleepOnPlayback": "erreprodukzioan loa saihestu",
|
||||||
"replayGainClipping_description": "Saihestu {{ReplayGain}}-k eragindako mozketa irabazpena automatikoki jaitsiz",
|
"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": {
|
"form": {
|
||||||
"addServer": {
|
"addServer": {
|
||||||
@@ -731,7 +745,9 @@
|
|||||||
"success": "zerbitzaria behar bezala gehitu da",
|
"success": "zerbitzaria behar bezala gehitu da",
|
||||||
"input_preferInstantMix": "nahiago izan berehalako nahasketa",
|
"input_preferInstantMix": "nahiago izan berehalako nahasketa",
|
||||||
"input_preferInstantMixDescription": "erabili berehalako nahasketa soilik antzeko abestiak lortzeko. erabilgarria portaera hau aldatzen duten pluginak badituzu",
|
"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": {
|
"addToPlaylist": {
|
||||||
"input_playlists": "$t(entity.playlist, {\"count\": 2})",
|
"input_playlists": "$t(entity.playlist, {\"count\": 2})",
|
||||||
@@ -739,7 +755,8 @@
|
|||||||
"input_skipDuplicates": "saltatu bikoiztuak",
|
"input_skipDuplicates": "saltatu bikoiztuak",
|
||||||
"title": "gehitu $t(entity.playlist, {\"count\": 1})-(a)ri",
|
"title": "gehitu $t(entity.playlist, {\"count\": 1})-(a)ri",
|
||||||
"create": "sortu $t(entity.playlist, {\"count\": 1}) {{playlist}}",
|
"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": {
|
"createPlaylist": {
|
||||||
"input_description": "$t(common.description)",
|
"input_description": "$t(common.description)",
|
||||||
@@ -760,7 +777,9 @@
|
|||||||
"success": "partekatzeko esteka arbelera kopiatu da (edo egin klik hemen irekitzeko)",
|
"success": "partekatzeko esteka arbelera kopiatu da (edo egin klik hemen irekitzeko)",
|
||||||
"expireInvalid": "iraungitze-data etorkizunean izan behar da",
|
"expireInvalid": "iraungitze-data etorkizunean izan behar da",
|
||||||
"allowDownloading": "baimendu deskargatzea",
|
"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": {
|
"deletePlaylist": {
|
||||||
"success": "$t(entity.playlist, {\"count\": 1}) behar bezala ezabatu da",
|
"success": "$t(entity.playlist, {\"count\": 1}) behar bezala ezabatu da",
|
||||||
@@ -988,7 +1007,10 @@
|
|||||||
"recentReleases": "azken argitalpenak",
|
"recentReleases": "azken argitalpenak",
|
||||||
"viewDiscography": "ikusi diskografia",
|
"viewDiscography": "ikusi diskografia",
|
||||||
"groupingTypeAll": "argitalpen mota guztiak",
|
"groupingTypeAll": "argitalpen mota guztiak",
|
||||||
"groupingTypePrimary": "argitalpen mota nagusiak"
|
"groupingTypePrimary": "argitalpen mota nagusiak",
|
||||||
|
"favoriteSongs": "abesti gogokoenak",
|
||||||
|
"topSongsCommunity": "komunitatea",
|
||||||
|
"topSongsPersonal": "pertsonala"
|
||||||
},
|
},
|
||||||
"itemDetail": {
|
"itemDetail": {
|
||||||
"copyPath": "kopiatu bidea arbelean",
|
"copyPath": "kopiatu bidea arbelean",
|
||||||
@@ -1006,6 +1028,13 @@
|
|||||||
},
|
},
|
||||||
"radioList": {
|
"radioList": {
|
||||||
"title": "irrati-kateak"
|
"title": "irrati-kateak"
|
||||||
|
},
|
||||||
|
"releasenotes": {
|
||||||
|
"noStableReleaseToCompare": "ez dago bertsio egonkorrik alderatzeko"
|
||||||
|
},
|
||||||
|
"windowBar": {
|
||||||
|
"paused": "(Pausatuta) ",
|
||||||
|
"privateMode": "(Modu pribatua)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"releaseType": {
|
"releaseType": {
|
||||||
@@ -1038,7 +1067,19 @@
|
|||||||
"notContains": "ez dauka",
|
"notContains": "ez dauka",
|
||||||
"startsWith": "honekin hasten da",
|
"startsWith": "honekin hasten da",
|
||||||
"endsWith": "honekin amaitzen 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": {
|
"visualizer": {
|
||||||
"general": "Orokorra",
|
"general": "Orokorra",
|
||||||
|
|||||||
@@ -246,10 +246,10 @@
|
|||||||
"loginRateError": "trop de tentatives de connexion, merci de réessayer dans quelques secondes",
|
"loginRateError": "trop de tentatives de connexion, merci de réessayer dans quelques secondes",
|
||||||
"openError": "impossible d'ouvrir le fichier",
|
"openError": "impossible d'ouvrir le fichier",
|
||||||
"networkError": "une erreur de réseau est survenue",
|
"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",
|
"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",
|
"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",
|
"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",
|
"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",
|
"noNetwork": "serveur indisponible",
|
||||||
@@ -340,7 +340,9 @@
|
|||||||
"lyricGap": "espacement des lettres",
|
"lyricGap": "espacement des lettres",
|
||||||
"dynamicIsImage": "activer l'image d'arrière-plan",
|
"dynamicIsImage": "activer l'image d'arrière-plan",
|
||||||
"dynamicImageBlur": "intensité du flou sur 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",
|
"upNext": "à suivre",
|
||||||
"lyrics": "paroles",
|
"lyrics": "paroles",
|
||||||
@@ -477,7 +479,7 @@
|
|||||||
"groupingTypePrimary": "types de parution principale",
|
"groupingTypePrimary": "types de parution principale",
|
||||||
"topSongsCommunity": "communauté",
|
"topSongsCommunity": "communauté",
|
||||||
"topSongsPersonal": "personnel",
|
"topSongsPersonal": "personnel",
|
||||||
"favoriteSongsFrom": "titres favori de {{title}}"
|
"favoriteSongsFrom": "titres favoris de {{title}}"
|
||||||
},
|
},
|
||||||
"itemDetail": {
|
"itemDetail": {
|
||||||
"copyPath": "copier le chemin dans le presse-papiers",
|
"copyPath": "copier le chemin dans le presse-papiers",
|
||||||
@@ -520,7 +522,7 @@
|
|||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"audioDevice_description": "sélectionnez le périphérique audio à utiliser pour la lecture",
|
"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",
|
"audioPlayer_description": "sélectionnez le lecteur audio à utiliser pour la lecture",
|
||||||
"crossfadeDuration_description": "définit la durée du fondu enchaîné",
|
"crossfadeDuration_description": "définit la durée du fondu enchaîné",
|
||||||
"audioDevice": "périphérique audio",
|
"audioDevice": "périphérique audio",
|
||||||
@@ -914,7 +916,7 @@
|
|||||||
"ignoreCors": "ignorer cors $t(common.restartRequired)",
|
"ignoreCors": "ignorer cors $t(common.restartRequired)",
|
||||||
"error_savePassword": "une erreur s’est produite lors de la tentative de sauvegarde du mot de passe",
|
"error_savePassword": "une erreur s’est produite lors de la tentative de sauvegarde du mot de passe",
|
||||||
"input_preferInstantMix": "préférer le mix instantané",
|
"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_preferRemoteUrl": "préférer une URL publique",
|
||||||
"input_remoteUrl": "URL publique",
|
"input_remoteUrl": "URL publique",
|
||||||
"input_remoteUrlPlaceholder": "optionnel : URL publique pour les fonctionnalités externes"
|
"input_remoteUrlPlaceholder": "optionnel : URL publique pour les fonctionnalités externes"
|
||||||
@@ -1002,6 +1004,9 @@
|
|||||||
"export": "exporter les paroles",
|
"export": "exporter les paroles",
|
||||||
"input_synced": "exporter les paroles synchronisées",
|
"input_synced": "exporter les paroles synchronisées",
|
||||||
"input_offset": "$t(setting.lyricOffset)"
|
"input_offset": "$t(setting.lyricOffset)"
|
||||||
|
},
|
||||||
|
"editRadioStation": {
|
||||||
|
"success": "station de radio a été mise à jour avec succès"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
@@ -1379,6 +1384,13 @@
|
|||||||
},
|
},
|
||||||
"lumiBars": "Lumi Bars",
|
"lumiBars": "Lumi Bars",
|
||||||
"outlineBars": "Outline Bars",
|
"outlineBars": "Outline Bars",
|
||||||
"splitGradient": "Split Gradient"
|
"splitGradient": "Split Gradient",
|
||||||
|
"systemAudioNoAudioTrack": "Aucune piste audio n'a été renvoyée. Assurez-vous que la capture audio est activée lorsque vous y êtes invité.",
|
||||||
|
"systemAudioConsentAllow": "Permettre",
|
||||||
|
"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}}",
|
||||||
|
"systemAudioExclusiveModeNotSupported": "Le visualiseur est indisponible lorsque le mode audio exclusif est activé. Désactivez ce mode dans les paramètres MPV et réessayez."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1037,7 +1037,7 @@
|
|||||||
},
|
},
|
||||||
"updateServer": {
|
"updateServer": {
|
||||||
"title": "サーバーをアップデート",
|
"title": "サーバーをアップデート",
|
||||||
"success": "サーバーがアップデートされました"
|
"success": "サーバーの更新に成功しました"
|
||||||
},
|
},
|
||||||
"queryEditor": {
|
"queryEditor": {
|
||||||
"input_optionMatchAll": "すべて一致",
|
"input_optionMatchAll": "すべて一致",
|
||||||
@@ -1102,6 +1102,9 @@
|
|||||||
},
|
},
|
||||||
"saveQueue": {
|
"saveQueue": {
|
||||||
"success": "プレイキューをサーバーに保存しました"
|
"success": "プレイキューをサーバーに保存しました"
|
||||||
|
},
|
||||||
|
"editRadioStation": {
|
||||||
|
"success": "ラジオ局の更新に成功しました"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
@@ -1332,6 +1335,12 @@
|
|||||||
"d": "D",
|
"d": "D",
|
||||||
"z": "Z"
|
"z": "Z"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"systemAudioConsentAllow": "許可",
|
||||||
|
"systemAudioConsentBody": "ビジュアライザーを機能させるためには、システムオーディオへのアクセスが必要です",
|
||||||
|
"systemAudioConsentDecline": "拒否",
|
||||||
|
"systemAudioConsentTitle": "システムオーディオへのアクセスを許可しますか?",
|
||||||
|
"systemAudioCaptureFailed": "キャプチャを開始できませんでした: {{message}}",
|
||||||
|
"systemAudioNoAudioTrack": "音声トラックが返されませんでした。プロンプトが表示されたら、音声キャプチャが有効になっていることを確認してください。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,7 +143,7 @@
|
|||||||
"doNotShowAgain": "niet opnieuw tonen",
|
"doNotShowAgain": "niet opnieuw tonen",
|
||||||
"externalLinks": "externe links",
|
"externalLinks": "externe links",
|
||||||
"faster": "sneller",
|
"faster": "sneller",
|
||||||
"preview": "voorvertoning",
|
"preview": "voorbeeld",
|
||||||
"private": "privé",
|
"private": "privé",
|
||||||
"public": "publiekelijk",
|
"public": "publiekelijk",
|
||||||
"recordLabel": "platenlabel",
|
"recordLabel": "platenlabel",
|
||||||
@@ -152,7 +152,7 @@
|
|||||||
"sort": "sorteer",
|
"sort": "sorteer",
|
||||||
"trackGain": "trackvolume",
|
"trackGain": "trackvolume",
|
||||||
"trackPeak": "piekniveau",
|
"trackPeak": "piekniveau",
|
||||||
"clean": "schoon",
|
"clean": "opschonen",
|
||||||
"gridRows": "rasterrijen",
|
"gridRows": "rasterrijen",
|
||||||
"tableColumns": "tabelkolommen",
|
"tableColumns": "tabelkolommen",
|
||||||
"itemsMore": "{{count}} meer",
|
"itemsMore": "{{count}} meer",
|
||||||
@@ -283,7 +283,9 @@
|
|||||||
"unsynchronized": "niet gesynchronizeerd",
|
"unsynchronized": "niet gesynchronizeerd",
|
||||||
"useImageAspectRatio": "gebruik aspect ratio van de afbeelding",
|
"useImageAspectRatio": "gebruik aspect ratio van de afbeelding",
|
||||||
"lyricOffset": "songtekst-vertraging (ms)",
|
"lyricOffset": "songtekst-vertraging (ms)",
|
||||||
"showLyricProvider": "toon songtekstaanbieder"
|
"showLyricProvider": "toon songtekstaanbieder",
|
||||||
|
"lyricOpacityNonActive": "opaciteit van niet-actieve songtekst",
|
||||||
|
"lyricScaleNonActive": "schaal niet-actieve songtekst"
|
||||||
},
|
},
|
||||||
"lyrics": "liedtekst",
|
"lyrics": "liedtekst",
|
||||||
"related": "gerelateerd",
|
"related": "gerelateerd",
|
||||||
@@ -680,7 +682,7 @@
|
|||||||
"artistReleaseTypeConfiguration_description": "configureer welke uitgavesoorten worden getoond op de albumartiestpagina en in welke volgorde",
|
"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_description": "kies het audioapparaat dat wordt gebruikt om af te spelen",
|
||||||
"audioDevice": "audioapparaat",
|
"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",
|
"audioExclusiveMode": "audio-exclusieve modus",
|
||||||
"audioPlayer_description": "kies de audiospeler om te gebruiken bij het afspelen",
|
"audioPlayer_description": "kies de audiospeler om te gebruiken bij het afspelen",
|
||||||
"audioPlayer": "audiospeler",
|
"audioPlayer": "audiospeler",
|
||||||
@@ -985,7 +987,11 @@
|
|||||||
"sidePlayQueueLayout": "uitlijning afspeelwachtrij aan zijkant",
|
"sidePlayQueueLayout": "uitlijning afspeelwachtrij aan zijkant",
|
||||||
"sidePlayQueueLayout_description": "stel de uitlijning in voor de afspeelwachtrij die aan de zijkant is gekoppeld",
|
"sidePlayQueueLayout_description": "stel de uitlijning in voor de afspeelwachtrij die aan de zijkant is gekoppeld",
|
||||||
"sidePlayQueueLayout_optionHorizontal": "horizontaal",
|
"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": {
|
"form": {
|
||||||
"addServer": {
|
"addServer": {
|
||||||
@@ -1094,6 +1100,9 @@
|
|||||||
},
|
},
|
||||||
"saveQueue": {
|
"saveQueue": {
|
||||||
"success": "wachtrij opgeslagen op server"
|
"success": "wachtrij opgeslagen op server"
|
||||||
|
},
|
||||||
|
"editRadioStation": {
|
||||||
|
"success": "radiozender succesvol bijgewerkt"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
@@ -1353,6 +1362,13 @@
|
|||||||
"d": "D",
|
"d": "D",
|
||||||
"z": "Z"
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -352,7 +352,8 @@
|
|||||||
"input_skipDuplicates": "pomiń duplikaty",
|
"input_skipDuplicates": "pomiń duplikaty",
|
||||||
"input_playlists": "$t(entity.playlist, {\"count\": 2})",
|
"input_playlists": "$t(entity.playlist, {\"count\": 2})",
|
||||||
"create": "utwórz $t(entity.playlist, {\"count\": 1}) {{playlist}}",
|
"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": {
|
"updateServer": {
|
||||||
"title": "uaktualnij serwer",
|
"title": "uaktualnij serwer",
|
||||||
@@ -442,7 +443,9 @@
|
|||||||
"lyricGap": "odstępy tekstu",
|
"lyricGap": "odstępy tekstu",
|
||||||
"dynamicImageBlur": "rozmiar rozmycia obrazu",
|
"dynamicImageBlur": "rozmiar rozmycia obrazu",
|
||||||
"dynamicIsImage": "włącz obraz w tle",
|
"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",
|
"upNext": "następne",
|
||||||
"lyrics": "tekst",
|
"lyrics": "tekst",
|
||||||
@@ -702,7 +705,7 @@
|
|||||||
"hotkey_favoriteCurrentSong": "ulubiona $t(common.currentSong)",
|
"hotkey_favoriteCurrentSong": "ulubiona $t(common.currentSong)",
|
||||||
"hotkey_zoomIn": "przybliż",
|
"hotkey_zoomIn": "przybliż",
|
||||||
"hotkey_browserForward": "przeglądarka w przód",
|
"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",
|
"discordUpdateInterval": "{{discord}} interwał aktualizacji rich presence",
|
||||||
"fontType_optionBuiltIn": "wbudowana czcionka",
|
"fontType_optionBuiltIn": "wbudowana czcionka",
|
||||||
"hotkey_playbackPlayPause": "odtwarzaj / wstrzymaj",
|
"hotkey_playbackPlayPause": "odtwarzaj / wstrzymaj",
|
||||||
@@ -1064,7 +1067,11 @@
|
|||||||
"sidePlayQueueLayout_optionHorizontal": "poziomy",
|
"sidePlayQueueLayout_optionHorizontal": "poziomy",
|
||||||
"sidePlayQueueLayout_optionVertical": "pionowy",
|
"sidePlayQueueLayout_optionVertical": "pionowy",
|
||||||
"waveformLoadingDelay": "opóźnienie załadowania fali",
|
"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": {
|
"table": {
|
||||||
"config": {
|
"config": {
|
||||||
@@ -1381,6 +1388,13 @@
|
|||||||
},
|
},
|
||||||
"pasteGradient": "Wklej Gradient",
|
"pasteGradient": "Wklej Gradient",
|
||||||
"pasteGradientPlaceholder": "Wklej tutaj JSON gradientu...",
|
"pasteGradientPlaceholder": "Wklej tutaj JSON gradientu...",
|
||||||
"ansiBands": "Paski ANSI"
|
"ansiBands": "Paski ANSI",
|
||||||
|
"systemAudioConsentAllow": "Zezwól",
|
||||||
|
"systemAudioConsentBody": "Wizualizer wymaga dostępu do audio systemu do działania",
|
||||||
|
"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.",
|
||||||
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,7 @@
|
|||||||
"share": "compartilhar",
|
"share": "compartilhar",
|
||||||
"close": "fechar",
|
"close": "fechar",
|
||||||
"translation": "tradução",
|
"translation": "tradução",
|
||||||
"albumGain": "ganho do album",
|
"albumGain": "ganho do álbum",
|
||||||
"trackPeak": "peak da faixa",
|
"trackPeak": "peak da faixa",
|
||||||
"albumPeak": "pico do álbum",
|
"albumPeak": "pico do álbum",
|
||||||
"trackGain": "ganho da faixa",
|
"trackGain": "ganho da faixa",
|
||||||
@@ -121,10 +121,21 @@
|
|||||||
"removeFromFavorites": "remover de $t(entity.favorite, {\"count\": 2})",
|
"removeFromFavorites": "remover de $t(entity.favorite, {\"count\": 2})",
|
||||||
"openIn": {
|
"openIn": {
|
||||||
"lastfm": "Abrir em Last.fm",
|
"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)",
|
"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": {
|
"form": {
|
||||||
"deletePlaylist": {
|
"deletePlaylist": {
|
||||||
|
|||||||
+47
-13
@@ -18,8 +18,11 @@
|
|||||||
"toggleSmartPlaylistEditor": "вкл./откл. редактор $t(entity.smartPlaylist)",
|
"toggleSmartPlaylistEditor": "вкл./откл. редактор $t(entity.smartPlaylist)",
|
||||||
"removeFromFavorites": "удалить из $t(entity.favorite, {\"count\": 2})",
|
"removeFromFavorites": "удалить из $t(entity.favorite, {\"count\": 2})",
|
||||||
"openIn": {
|
"openIn": {
|
||||||
"lastfm": "открыть на Last.fm",
|
"lastfm": "Открыть на Last.fm",
|
||||||
"musicbrainz": "открыть на MusicBrainz"
|
"musicbrainz": "открыть на MusicBrainz",
|
||||||
|
"listenbrainz": "Открыть на ListenBrainz",
|
||||||
|
"qobuz": "Открыть в Qobuz",
|
||||||
|
"spotify": "Открыть в Spotify"
|
||||||
},
|
},
|
||||||
"moveToNext": "следующий",
|
"moveToNext": "следующий",
|
||||||
"addOrRemoveFromSelection": "добавить или удалить из выделения",
|
"addOrRemoveFromSelection": "добавить или удалить из выделения",
|
||||||
@@ -167,7 +170,8 @@
|
|||||||
"explicit": "нецензурная лексика",
|
"explicit": "нецензурная лексика",
|
||||||
"externalLinks": "внешние ссылки",
|
"externalLinks": "внешние ссылки",
|
||||||
"explicitStatus": "признак нецензурного контента",
|
"explicitStatus": "признак нецензурного контента",
|
||||||
"newVersionAvailable": "доступна новая версия"
|
"newVersionAvailable": "доступна новая версия",
|
||||||
|
"numberOfResults": "{{numberOfResults}} результатов"
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"album_one": "альбом",
|
"album_one": "альбом",
|
||||||
@@ -435,7 +439,17 @@
|
|||||||
"home": "$t(common.home)",
|
"home": "$t(common.home)",
|
||||||
"myLibrary": "Моя библиотека",
|
"myLibrary": "Моя библиотека",
|
||||||
"shared": "Публичные плейлисты $t(entity.playlist, {\"count\": 2})",
|
"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": {
|
"fullscreenPlayer": {
|
||||||
"config": {
|
"config": {
|
||||||
@@ -452,7 +466,9 @@
|
|||||||
"useImageAspectRatio": "использовать соотношение сторон изображения",
|
"useImageAspectRatio": "использовать соотношение сторон изображения",
|
||||||
"lyricGap": "пробел между словами",
|
"lyricGap": "пробел между словами",
|
||||||
"dynamicIsImage": "включить фоновое изображение",
|
"dynamicIsImage": "включить фоновое изображение",
|
||||||
"dynamicImageBlur": "сила размытия изображения"
|
"dynamicImageBlur": "сила размытия изображения",
|
||||||
|
"lyricOpacityNonActive": "непрозрачность неактивных слов",
|
||||||
|
"lyricScaleNonActive": "размер неактивных слов"
|
||||||
},
|
},
|
||||||
"upNext": "играет",
|
"upNext": "играет",
|
||||||
"lyrics": "слова",
|
"lyrics": "слова",
|
||||||
@@ -475,7 +491,8 @@
|
|||||||
"selectMusicFolder": "выбрать папку с музыкой",
|
"selectMusicFolder": "выбрать папку с музыкой",
|
||||||
"noMusicFolder": "папка с музыкой не выбрана",
|
"noMusicFolder": "папка с музыкой не выбрана",
|
||||||
"multipleMusicFolders": "{{count}} выбрано музыкальных папок",
|
"multipleMusicFolders": "{{count}} выбрано музыкальных папок",
|
||||||
"commandPalette": "открыть командную строку"
|
"commandPalette": "открыть командную строку",
|
||||||
|
"settings": "$t(common.setting, {\"count\": 2})"
|
||||||
},
|
},
|
||||||
"manageServers": {
|
"manageServers": {
|
||||||
"title": "сервера",
|
"title": "сервера",
|
||||||
@@ -510,7 +527,8 @@
|
|||||||
"goTo": "перейти в",
|
"goTo": "перейти в",
|
||||||
"moveToNext": "$t(action.moveToNext)",
|
"moveToNext": "$t(action.moveToNext)",
|
||||||
"playShuffled": "$t(player.shuffle)",
|
"playShuffled": "$t(player.shuffle)",
|
||||||
"moveItems": "$t(action.moveItems)"
|
"moveItems": "$t(action.moveItems)",
|
||||||
|
"playSimilarSongs": "$t(player.playSimilarSongs)"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"mostPlayed": "слушают чаще всего",
|
"mostPlayed": "слушают чаще всего",
|
||||||
@@ -518,7 +536,8 @@
|
|||||||
"title": "$t(common.home)",
|
"title": "$t(common.home)",
|
||||||
"explore": "откройте новое",
|
"explore": "откройте новое",
|
||||||
"recentlyPlayed": "игралось недавно",
|
"recentlyPlayed": "игралось недавно",
|
||||||
"recentlyReleased": "Новинки"
|
"recentlyReleased": "Новинки",
|
||||||
|
"genres": "$t(entity.genre, {\"count\": 2})"
|
||||||
},
|
},
|
||||||
"albumDetail": {
|
"albumDetail": {
|
||||||
"moreFromArtist": "больше от $t(entity.artist, {\"count\": 1})",
|
"moreFromArtist": "больше от $t(entity.artist, {\"count\": 1})",
|
||||||
@@ -552,10 +571,13 @@
|
|||||||
},
|
},
|
||||||
"genreList": {
|
"genreList": {
|
||||||
"showAlbums": "показать $t(entity.genre, {\"count\": 1}) $t(entity.album, {\"count\": 2})",
|
"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": {
|
"trackList": {
|
||||||
"artistTracks": "Треки {{artist}}"
|
"artistTracks": "Треки {{artist}}",
|
||||||
|
"genreTracks": "\"{{genre}}\" $t(entity.track, {\"count\": 2})",
|
||||||
|
"title": "$t(entity.track, {\"count\": 2})"
|
||||||
},
|
},
|
||||||
"globalSearch": {
|
"globalSearch": {
|
||||||
"commands": {
|
"commands": {
|
||||||
@@ -616,6 +638,12 @@
|
|||||||
},
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
"title": "$t(entity.favorite, {\"count\": 2})"
|
"title": "$t(entity.favorite, {\"count\": 2})"
|
||||||
|
},
|
||||||
|
"folderList": {
|
||||||
|
"title": "$t(entity.folder, {\"count\": 2})"
|
||||||
|
},
|
||||||
|
"playlistList": {
|
||||||
|
"title": "$t(entity.playlist, {\"count\": 2})"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
@@ -656,7 +684,8 @@
|
|||||||
"input_skipDuplicates": "не добавлять дубликаты",
|
"input_skipDuplicates": "не добавлять дубликаты",
|
||||||
"create": "создать $t(entity.playlist, {\"count\": 1}) {{playlist}}",
|
"create": "создать $t(entity.playlist, {\"count\": 1}) {{playlist}}",
|
||||||
"searchOrCreate": "для создания нового списка выполните поиск по $t(entity.playlist, {\"count\": 2}) или введите соответствующий текст",
|
"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": {
|
"updateServer": {
|
||||||
"title": "обновление сервера",
|
"title": "обновление сервера",
|
||||||
@@ -725,6 +754,9 @@
|
|||||||
"input_played_optionUnplayed": "только не игранные треки",
|
"input_played_optionUnplayed": "только не игранные треки",
|
||||||
"input_played_optionPlayed": "только воспроизведённые треки",
|
"input_played_optionPlayed": "только воспроизведённые треки",
|
||||||
"input_genre": "$t(entity.genre, {\"count\": 1})"
|
"input_genre": "$t(entity.genre, {\"count\": 1})"
|
||||||
|
},
|
||||||
|
"editRadioStation": {
|
||||||
|
"success": "радиостанция успешно обновлена"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
@@ -1053,7 +1085,8 @@
|
|||||||
"audioFadeOnStatusChange": "плавное изменение звука",
|
"audioFadeOnStatusChange": "плавное изменение звука",
|
||||||
"audioFadeOnStatusChange_description": "включает эффекты затухания и появления звука при изменении статуса (пауза/проигрывание)",
|
"audioFadeOnStatusChange_description": "включает эффекты затухания и появления звука при изменении статуса (пауза/проигрывание)",
|
||||||
"preventSleepOnPlayback_description": "запрещает спящий режим экрана, пока играет музыка",
|
"preventSleepOnPlayback_description": "запрещает спящий режим экрана, пока играет музыка",
|
||||||
"preventSleepOnPlayback": "не переходить в спящий режим"
|
"preventSleepOnPlayback": "не переходить в спящий режим",
|
||||||
|
"discordLinkType_none": "$t(common.none)"
|
||||||
},
|
},
|
||||||
"releaseType": {
|
"releaseType": {
|
||||||
"secondary": {
|
"secondary": {
|
||||||
@@ -1074,7 +1107,8 @@
|
|||||||
"other": "другие",
|
"other": "другие",
|
||||||
"broadcast": "транслировать",
|
"broadcast": "транслировать",
|
||||||
"ep": "эп",
|
"ep": "эп",
|
||||||
"single": "сингл"
|
"single": "сингл",
|
||||||
|
"album": "$t(entity.album, {\"count\": 1})"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"datetime": {
|
"datetime": {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -36,7 +36,10 @@
|
|||||||
"openApplicationDirectory": "відкрити каталог додатків",
|
"openApplicationDirectory": "відкрити каталог додатків",
|
||||||
"openIn": {
|
"openIn": {
|
||||||
"lastfm": "Відкрити в Last.fm",
|
"lastfm": "Відкрити в Last.fm",
|
||||||
"musicbrainz": "Відкрити в MusicBrainz"
|
"musicbrainz": "Відкрити в MusicBrainz",
|
||||||
|
"listenbrainz": "Відкрити у ListenBrainz",
|
||||||
|
"qobuz": "Відкрити у Qobuz",
|
||||||
|
"spotify": "Відкрити у Spotify"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -164,7 +167,9 @@
|
|||||||
"explicit": "Експліцитний зміст",
|
"explicit": "Експліцитний зміст",
|
||||||
"gridRows": "рядки сітки",
|
"gridRows": "рядки сітки",
|
||||||
"tableColumns": "стовпці таблиці",
|
"tableColumns": "стовпці таблиці",
|
||||||
"itemsMore": "{{count}} більше"
|
"itemsMore": "{{count}} більше",
|
||||||
|
"numberOfResults": "{{numberOfResults}} результатів",
|
||||||
|
"newVersionAvailable": "доступна нова версія"
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"album_one": "альбом",
|
"album_one": "альбом",
|
||||||
@@ -255,7 +260,9 @@
|
|||||||
"serverRequired": "потрібен сервер",
|
"serverRequired": "потрібен сервер",
|
||||||
"sessionExpiredError": "ваша сесія закінчилася",
|
"sessionExpiredError": "ваша сесія закінчилася",
|
||||||
"systemFontError": "сталася помилка під час спроби отримати системні шрифти",
|
"systemFontError": "сталася помилка під час спроби отримати системні шрифти",
|
||||||
"settingsSyncError": "виявлено розбіжності між налаштуваннями в рендерері та основним процесом. перезапустіть програму, щоб застосувати зміни"
|
"settingsSyncError": "виявлено розбіжності між налаштуваннями в рендерері та основним процесом. перезапустіть програму, щоб застосувати зміни",
|
||||||
|
"invalidJson": "недійсний JSON",
|
||||||
|
"playbackPausedDueToError": "відтворення було призупинено через помилку"
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"album": "$t(entity.album, {\"count\": 1})",
|
"album": "$t(entity.album, {\"count\": 1})",
|
||||||
@@ -281,7 +288,7 @@
|
|||||||
"isPublic": "є публічним",
|
"isPublic": "є публічним",
|
||||||
"isRated": "є оціненим",
|
"isRated": "є оціненим",
|
||||||
"isRecentlyPlayed": "нещодавно відтворено",
|
"isRecentlyPlayed": "нещодавно відтворено",
|
||||||
"lastPlayed": "нещодавно відтворені",
|
"lastPlayed": "останнє відтворене",
|
||||||
"mostPlayed": "найбільш відтворювані",
|
"mostPlayed": "найбільш відтворювані",
|
||||||
"name": "назва",
|
"name": "назва",
|
||||||
"note": "примітка",
|
"note": "примітка",
|
||||||
@@ -301,7 +308,9 @@
|
|||||||
"title": "назва",
|
"title": "назва",
|
||||||
"toYear": "до року",
|
"toYear": "до року",
|
||||||
"trackNumber": "трек",
|
"trackNumber": "трек",
|
||||||
"explicitStatus": "$t(common.explicitStatus)"
|
"explicitStatus": "$t(common.explicitStatus)",
|
||||||
|
"matchAnd": "і",
|
||||||
|
"matchOr": "або"
|
||||||
},
|
},
|
||||||
"datetime": {
|
"datetime": {
|
||||||
"minuteShort": "хв.",
|
"minuteShort": "хв.",
|
||||||
@@ -414,7 +423,9 @@
|
|||||||
"setExpiration": "встановити термін дії",
|
"setExpiration": "встановити термін дії",
|
||||||
"success": "посилання для спільного використання скопійовано в буфер обміну (натисніть тут, щоб відкрити)",
|
"success": "посилання для спільного використання скопійовано в буфер обміну (натисніть тут, щоб відкрити)",
|
||||||
"expireInvalid": "термін дії повинен бути в майбутньому",
|
"expireInvalid": "термін дії повинен бути в майбутньому",
|
||||||
"createFailed": "не вдалося створити спільний доступ (чи ввімкнено спільний доступ?)"
|
"createFailed": "не вдалося створити спільний доступ (чи ввімкнено спільний доступ?)",
|
||||||
|
"copyToClipboard": "Скопіювати до буфера обміну: Ctrl+C, Enter",
|
||||||
|
"successMustClick": "посилання успішно створено, натисніть сюди, щоб відкрити"
|
||||||
},
|
},
|
||||||
"shuffleAll": {
|
"shuffleAll": {
|
||||||
"title": "відтворити випадково",
|
"title": "відтворити випадково",
|
||||||
@@ -435,6 +446,9 @@
|
|||||||
"enabled": "приватний режим увімкнено, стан відтворення тепер приховано від зовнішніх інтеграцій",
|
"enabled": "приватний режим увімкнено, стан відтворення тепер приховано від зовнішніх інтеграцій",
|
||||||
"disabled": "приватний режим вимкнено, стан відтворення тепер видно для увімкнених зовнішніх інтеграцій",
|
"disabled": "приватний режим вимкнено, стан відтворення тепер видно для увімкнених зовнішніх інтеграцій",
|
||||||
"title": "приватний режим"
|
"title": "приватний режим"
|
||||||
|
},
|
||||||
|
"editRadioStation": {
|
||||||
|
"success": "радіо станція успішно оновлена"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
@@ -539,6 +553,52 @@
|
|||||||
"goToAlbum": "перейти до $t(entity.album, {\"count\": 1})",
|
"goToAlbum": "перейти до $t(entity.album, {\"count\": 1})",
|
||||||
"goToAlbumArtist": "перейти до $t(entity.albumArtist, {\"count\": 1})",
|
"goToAlbumArtist": "перейти до $t(entity.albumArtist, {\"count\": 1})",
|
||||||
"showDetails": "отримати інформацію"
|
"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": "нещодавно відтворені"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1015,6 +1015,9 @@
|
|||||||
"input_played_optionPlayed": "仅已播放的曲目",
|
"input_played_optionPlayed": "仅已播放的曲目",
|
||||||
"input_limit": "有多少首歌?",
|
"input_limit": "有多少首歌?",
|
||||||
"input_played": "播放筛选器"
|
"input_played": "播放筛选器"
|
||||||
|
},
|
||||||
|
"editRadioStation": {
|
||||||
|
"success": "电台更新成功"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
|
|||||||
@@ -233,7 +233,9 @@
|
|||||||
"showLyricMatch": "顯示匹配的歌詞",
|
"showLyricMatch": "顯示匹配的歌詞",
|
||||||
"dynamicImageBlur": "圖片模糊大小",
|
"dynamicImageBlur": "圖片模糊大小",
|
||||||
"dynamicIsImage": "啟用背景圖片",
|
"dynamicIsImage": "啟用背景圖片",
|
||||||
"lyricOffset": "歌詞偏移時間 (ms)"
|
"lyricOffset": "歌詞偏移時間 (ms)",
|
||||||
|
"lyricOpacityNonActive": "非活躍歌詞的不透明度",
|
||||||
|
"lyricScaleNonActive": "非活躍歌詞的比例"
|
||||||
},
|
},
|
||||||
"lyrics": "歌詞",
|
"lyrics": "歌詞",
|
||||||
"related": "相關",
|
"related": "相關",
|
||||||
@@ -433,7 +435,7 @@
|
|||||||
"audioDevice": "音訊設備",
|
"audioDevice": "音訊設備",
|
||||||
"audioDevice_description": "選擇用於播放的音訊設備",
|
"audioDevice_description": "選擇用於播放的音訊設備",
|
||||||
"audioExclusiveMode": "音訊獨佔模式",
|
"audioExclusiveMode": "音訊獨佔模式",
|
||||||
"audioExclusiveMode_description": "啟用獨佔輸出模式。在此模式下,系統通常被鎖定,只有 mpv 能夠輸出音訊",
|
"audioExclusiveMode_description": "啟用獨佔輸出模式。在此模式下,系統通常被鎖定,只有 mpv 能夠輸出音訊。視覺化音訊截取在此選項啟用時不會作用",
|
||||||
"audioPlayer": "音訊播放器",
|
"audioPlayer": "音訊播放器",
|
||||||
"crossfadeDuration": "淡入淡出持續時間",
|
"crossfadeDuration": "淡入淡出持續時間",
|
||||||
"crossfadeDuration_description": "設定淡入淡出持續時間",
|
"crossfadeDuration_description": "設定淡入淡出持續時間",
|
||||||
@@ -792,7 +794,11 @@
|
|||||||
"qobuz_description": "在藝術家/專輯頁面上顯示 Qobuz 的連結",
|
"qobuz_description": "在藝術家/專輯頁面上顯示 Qobuz 的連結",
|
||||||
"qobuz": "顯示 Qobuz 連結",
|
"qobuz": "顯示 Qobuz 連結",
|
||||||
"waveformLoadingDelay": "波形載入延遲",
|
"waveformLoadingDelay": "波形載入延遲",
|
||||||
"waveformLoadingDelay_description": "載入波形前的延遲(以秒為單位)。如果您在使用網頁播放器時遇到卡頓,請增加此值。"
|
"waveformLoadingDelay_description": "載入波形前的延遲(以秒為單位)。如果您在使用網頁播放器時遇到卡頓,請增加此值。",
|
||||||
|
"playerbarWaveformStretch": "波形拉伸",
|
||||||
|
"playerbarWaveformStretch_description": "拉伸波形來填補可用空間",
|
||||||
|
"preventSuspendOnPlayback_description": "音樂播放時防止應用程式進入休眠",
|
||||||
|
"preventSuspendOnPlayback": "在播放時防止應用程式暫停"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"config": {
|
"config": {
|
||||||
@@ -1042,7 +1048,8 @@
|
|||||||
"success": "新增 $t(entity.trackWithCount, {\"count\": {{message}} }) 到 $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
"success": "新增 $t(entity.trackWithCount, {\"count\": {{message}} }) 到 $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
||||||
"title": "新增到$t(entity.playlist, {\"count\": 1})",
|
"title": "新增到$t(entity.playlist, {\"count\": 1})",
|
||||||
"create": "建立 $t(entity.playlist, {\"count\": 1}) {{playlist}}",
|
"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": {
|
"createPlaylist": {
|
||||||
"input_description": "$t(common.description)",
|
"input_description": "$t(common.description)",
|
||||||
@@ -1337,6 +1344,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"systemAudioCaptureFailed": "無法開始擷取:{{message}}",
|
"systemAudioCaptureFailed": "無法開始擷取:{{message}}",
|
||||||
"systemAudioNoAudioTrack": "沒有回傳任何音軌。確保在提示時啟用音訊擷取。"
|
"systemAudioNoAudioTrack": "沒有回傳任何音軌。確保在提示時啟用音訊擷取。",
|
||||||
|
"systemAudioConsentAllow": "允許",
|
||||||
|
"systemAudioConsentBody": "此視覺化器需要存取系統音訊才能運作",
|
||||||
|
"systemAudioConsentDecline": "拒絕",
|
||||||
|
"systemAudioConsentTitle": "允許存取系統音訊?",
|
||||||
|
"systemAudioExclusiveModeNotSupported": "啟用音訊獨佔模式時,視覺化不可用。 在MPV設定中停用音訊獨佔模式,然後再試一次。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import console from 'console';
|
import console from 'console';
|
||||||
import { app, ipcMain } from 'electron';
|
import { app, ipcMain } from 'electron';
|
||||||
import { rm } from 'fs/promises';
|
import { access, rm } from 'fs/promises';
|
||||||
import uniq from 'lodash/uniq';
|
import uniq from 'lodash/uniq';
|
||||||
import MpvAPI from 'node-mpv';
|
import MpvAPI from 'node-mpv';
|
||||||
import { pid } from 'node:process';
|
import { pid } from 'node:process';
|
||||||
import process from 'process';
|
import process from 'process';
|
||||||
|
|
||||||
import { getMainWindow, sendToastToRenderer } from '../../../index';
|
import { getMainWindow, sendToastToRenderer } from '../../../index';
|
||||||
import { createLog, isWindows } from '../../../utils';
|
import { createLog, isMacOS, isWindows } from '../../../utils';
|
||||||
import { store } from '../settings';
|
import { store } from '../settings';
|
||||||
|
|
||||||
import { PlayerData } from '/@/shared/types/domain-types';
|
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 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 = [
|
const prefetchPlaylistParams = [
|
||||||
'--prefetch-playlist=no',
|
'--prefetch-playlist=no',
|
||||||
@@ -86,12 +87,38 @@ const DEFAULT_MPV_PARAMETERS = (extraParameters?: string[]) => {
|
|||||||
return parameters;
|
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: {
|
const createMpv = async (data: {
|
||||||
binaryPath?: string;
|
binaryPath?: string;
|
||||||
extraParameters?: string[];
|
extraParameters?: string[];
|
||||||
properties?: Record<string, any>;
|
properties?: Record<string, any>;
|
||||||
}): Promise<MpvAPI> => {
|
}): Promise<MpvAPI> => {
|
||||||
const { binaryPath, extraParameters, properties } = data;
|
const { binaryPath, extraParameters, properties } = data;
|
||||||
|
const resolvedBinaryPath = await resolveMpvBinaryPath(binaryPath);
|
||||||
|
|
||||||
const params = uniq([...DEFAULT_MPV_PARAMETERS(extraParameters), ...(extraParameters || [])]);
|
const params = uniq([...DEFAULT_MPV_PARAMETERS(extraParameters), ...(extraParameters || [])]);
|
||||||
|
|
||||||
@@ -99,7 +126,7 @@ const createMpv = async (data: {
|
|||||||
{
|
{
|
||||||
audio_only: true,
|
audio_only: true,
|
||||||
auto_restart: false,
|
auto_restart: false,
|
||||||
binary: binaryPath || MPV_BINARY_PATH || undefined,
|
binary: resolvedBinaryPath,
|
||||||
socket: socketPath,
|
socket: socketPath,
|
||||||
time_update: 1,
|
time_update: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,2 +1,9 @@
|
|||||||
import './core';
|
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');
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
|
|||||||
+18
-9
@@ -734,14 +734,21 @@ async function createWindow(first = true): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.webContents.session.setDisplayMediaRequestHandler((_request, callback) => {
|
mainWindow.webContents.session.setDisplayMediaRequestHandler((_request, callback) => {
|
||||||
desktopCapturer
|
if (!isMacOS()) {
|
||||||
.getSources({ types: ['screen'] })
|
callback({ audio: 'loopback' });
|
||||||
.then((sources) => {
|
return;
|
||||||
if (sources.length > 0) {
|
|
||||||
callback({ audio: 'loopback', video: sources[0] });
|
|
||||||
} else {
|
|
||||||
callback({});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) => {
|
.catch((err) => {
|
||||||
log.warn('desktopCapturer.getSources failed', err);
|
log.warn('desktopCapturer.getSources failed', err);
|
||||||
@@ -924,12 +931,14 @@ ipcMain.on(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('power-save-blocker-start', () => {
|
ipcMain.handle('power-save-blocker-start', (_event, { full }: { full: boolean }) => {
|
||||||
if (powerSaveBlockerId !== null) {
|
if (powerSaveBlockerId !== null) {
|
||||||
return powerSaveBlockerId;
|
return powerSaveBlockerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
powerSaveBlockerId = powerSaveBlocker.start('prevent-display-sleep');
|
powerSaveBlockerId = powerSaveBlocker.start(
|
||||||
|
full ? 'prevent-display-sleep' : 'prevent-app-suspension',
|
||||||
|
);
|
||||||
return powerSaveBlockerId;
|
return powerSaveBlockerId;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { SetActivity } from '@xhayper/discord-rpc';
|
import type { SetActivity } from '@xhayper/discord-rpc';
|
||||||
|
|
||||||
import { ipcRenderer } from 'electron';
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
const initialize = (clientId: string) => {
|
const initialize = (clientId: string) => {
|
||||||
|
|||||||
@@ -147,6 +147,20 @@ export const controller: GeneralController = {
|
|||||||
server.type,
|
server.type,
|
||||||
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||||
},
|
},
|
||||||
|
deleteArtistImage(args) {
|
||||||
|
const server = getServerById(args.apiClientProps.serverId);
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
throw new Error(
|
||||||
|
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: deleteArtistImage`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiController(
|
||||||
|
'deleteArtistImage',
|
||||||
|
server.type,
|
||||||
|
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||||
|
},
|
||||||
deleteFavorite(args) {
|
deleteFavorite(args) {
|
||||||
const server = getServerById(args.apiClientProps.serverId);
|
const server = getServerById(args.apiClientProps.serverId);
|
||||||
|
|
||||||
@@ -988,6 +1002,20 @@ export const controller: GeneralController = {
|
|||||||
server.type,
|
server.type,
|
||||||
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||||
},
|
},
|
||||||
|
uploadArtistImage(args) {
|
||||||
|
const server = getServerById(args.apiClientProps.serverId);
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
throw new Error(
|
||||||
|
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: uploadArtistImage`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiController(
|
||||||
|
'uploadArtistImage',
|
||||||
|
server.type,
|
||||||
|
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||||
|
},
|
||||||
uploadInternetRadioStationImage(args) {
|
uploadInternetRadioStationImage(args) {
|
||||||
const server = getServerById(args.apiClientProps.serverId);
|
const server = getServerById(args.apiClientProps.serverId);
|
||||||
|
|
||||||
|
|||||||
@@ -409,6 +409,8 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
return jfNormalize.album(
|
return jfNormalize.album(
|
||||||
{ ...res.body, Songs: songsRes.body.Items },
|
{ ...res.body, Songs: songsRes.body.Items },
|
||||||
apiClientProps.server,
|
apiClientProps.server,
|
||||||
|
args.context?.pathReplace,
|
||||||
|
args.context?.pathReplaceWith,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
getAlbumList: async (args) => {
|
getAlbumList: async (args) => {
|
||||||
@@ -580,7 +582,8 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
|
|
||||||
return `${apiClientProps.server?.url}/items/${query.id}/download?apiKey=${apiClientProps.server?.credential}`;
|
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;
|
const userId = apiClientProps.server?.userId;
|
||||||
|
|
||||||
if (!userId) throw new Error('No userId found');
|
if (!userId) throw new Error('No userId found');
|
||||||
@@ -742,6 +745,8 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
jfNormalize.song(
|
jfNormalize.song(
|
||||||
item as unknown as z.infer<typeof jfType._response.song>,
|
item as unknown as z.infer<typeof jfType._response.song>,
|
||||||
apiClientProps.server,
|
apiClientProps.server,
|
||||||
|
args.context?.pathReplace,
|
||||||
|
args.context?.pathReplaceWith,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,15 @@ export const contract = c.router({
|
|||||||
500: resultWithHeaders(ndType._response.error),
|
500: resultWithHeaders(ndType._response.error),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
deleteArtistImage: {
|
||||||
|
body: null,
|
||||||
|
method: 'DELETE',
|
||||||
|
path: 'artist/:id/image',
|
||||||
|
responses: {
|
||||||
|
200: resultWithHeaders(ndType._response.deleteArtistImage),
|
||||||
|
500: resultWithHeaders(ndType._response.error),
|
||||||
|
},
|
||||||
|
},
|
||||||
deleteInternetRadioStation: {
|
deleteInternetRadioStation: {
|
||||||
body: null,
|
body: null,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
@@ -259,6 +268,15 @@ export const contract = c.router({
|
|||||||
500: resultWithHeaders(ndType._response.error),
|
500: resultWithHeaders(ndType._response.error),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
uploadArtistImage: {
|
||||||
|
body: ndType._parameters.uploadArtistImage,
|
||||||
|
method: 'POST',
|
||||||
|
path: 'artist/:id/image',
|
||||||
|
responses: {
|
||||||
|
200: resultWithHeaders(ndType._response.uploadArtistImage),
|
||||||
|
500: resultWithHeaders(ndType._response.error),
|
||||||
|
},
|
||||||
|
},
|
||||||
uploadInternetRadioStationImage: {
|
uploadInternetRadioStationImage: {
|
||||||
body: ndType._parameters.uploadInternetRadioStationImage,
|
body: ndType._parameters.uploadInternetRadioStationImage,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
albumArtistListSortMap,
|
albumArtistListSortMap,
|
||||||
albumListSortMap,
|
albumListSortMap,
|
||||||
AuthenticationResponse,
|
AuthenticationResponse,
|
||||||
|
DeleteArtistImageArgs,
|
||||||
|
DeleteArtistImageResponse,
|
||||||
DeleteInternetRadioStationImageArgs,
|
DeleteInternetRadioStationImageArgs,
|
||||||
DeleteInternetRadioStationImageResponse,
|
DeleteInternetRadioStationImageResponse,
|
||||||
DeletePlaylistImageArgs,
|
DeletePlaylistImageArgs,
|
||||||
@@ -28,6 +30,8 @@ import {
|
|||||||
SortOrder,
|
SortOrder,
|
||||||
sortOrderMap,
|
sortOrderMap,
|
||||||
tagListSortMap,
|
tagListSortMap,
|
||||||
|
UploadArtistImageArgs,
|
||||||
|
UploadArtistImageResponse,
|
||||||
UploadInternetRadioStationImageArgs,
|
UploadInternetRadioStationImageArgs,
|
||||||
UploadInternetRadioStationImageResponse,
|
UploadInternetRadioStationImageResponse,
|
||||||
UploadPlaylistImageArgs,
|
UploadPlaylistImageArgs,
|
||||||
@@ -42,6 +46,7 @@ const VERSION_INFO: VersionInfo = [
|
|||||||
[
|
[
|
||||||
'0.61.0',
|
'0.61.0',
|
||||||
{
|
{
|
||||||
|
[ServerFeature.ARTIST_IMAGE_UPLOAD]: [1],
|
||||||
[ServerFeature.INTERNET_RADIO_IMAGE_UPLOAD]: [1],
|
[ServerFeature.INTERNET_RADIO_IMAGE_UPLOAD]: [1],
|
||||||
[ServerFeature.PLAYLIST_IMAGE_UPLOAD]: [1],
|
[ServerFeature.PLAYLIST_IMAGE_UPLOAD]: [1],
|
||||||
},
|
},
|
||||||
@@ -186,6 +191,21 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
id: res.body.data.id,
|
id: res.body.data.id,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
deleteArtistImage: async (args: DeleteArtistImageArgs): Promise<DeleteArtistImageResponse> => {
|
||||||
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
|
const res = await ndApiClient(apiClientProps as any).deleteArtistImage({
|
||||||
|
params: {
|
||||||
|
id: query.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to delete artist image');
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.body.data.status === 'ok';
|
||||||
|
},
|
||||||
deleteFavorite: SubsonicController.deleteFavorite,
|
deleteFavorite: SubsonicController.deleteFavorite,
|
||||||
deleteInternetRadioStation: async (args) => {
|
deleteInternetRadioStation: async (args) => {
|
||||||
const { apiClientProps, query } = args;
|
const { apiClientProps, query } = args;
|
||||||
@@ -476,7 +496,12 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return res.body.similarSongs.song.map((song) =>
|
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) => {
|
getArtistList: async (args) => {
|
||||||
@@ -546,7 +571,12 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return res.body.similarSongs2.song.map((song) =>
|
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,
|
getDownloadUrl: SubsonicController.getDownloadUrl,
|
||||||
@@ -803,7 +833,14 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
return (
|
return (
|
||||||
(res.body.similarSongs?.song || [])
|
(res.body.similarSongs?.song || [])
|
||||||
.filter((song) => song.id !== query.songId)
|
.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) => {
|
getSongDetail: async (args) => {
|
||||||
@@ -1002,6 +1039,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
|
|
||||||
const res = await NavidromeController.getSongList({
|
const res = await NavidromeController.getSongList({
|
||||||
apiClientProps,
|
apiClientProps,
|
||||||
|
context: args.context,
|
||||||
query: {
|
query: {
|
||||||
artistIds: [query.artistId],
|
artistIds: [query.artistId],
|
||||||
sortBy: SongListSort.PLAY_COUNT,
|
sortBy: SongListSort.PLAY_COUNT,
|
||||||
@@ -1270,6 +1308,40 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
uploadArtistImage: async (args: UploadArtistImageArgs): Promise<UploadArtistImageResponse> => {
|
||||||
|
const { apiClientProps, body, query } = args;
|
||||||
|
|
||||||
|
const server = apiClientProps.server;
|
||||||
|
const serverUrl = server?.url?.replace(/\/$/, '');
|
||||||
|
|
||||||
|
if (!serverUrl) {
|
||||||
|
throw new Error('Server is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = new FormData();
|
||||||
|
const bytes = body.image as Uint8Array<ArrayBuffer>;
|
||||||
|
const fileLike =
|
||||||
|
typeof File !== 'undefined'
|
||||||
|
? new File([bytes], 'image', { type: 'application/octet-stream' })
|
||||||
|
: new Blob([bytes], { type: 'application/octet-stream' });
|
||||||
|
form.append('image', fileLike as any);
|
||||||
|
|
||||||
|
const res = await axios.post(`${serverUrl}/api/artist/${query.id}/image`, form, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
...(server?.ndCredential && {
|
||||||
|
'x-nd-authorization': `Bearer ${server.ndCredential}`,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
signal: apiClientProps.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to upload artist image');
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.data?.status === 'ok';
|
||||||
|
},
|
||||||
uploadInternetRadioStationImage: async (
|
uploadInternetRadioStationImage: async (
|
||||||
args: UploadInternetRadioStationImageArgs,
|
args: UploadInternetRadioStationImageArgs,
|
||||||
): Promise<UploadInternetRadioStationImageResponse> => {
|
): Promise<UploadInternetRadioStationImageResponse> => {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { initClient, initContract } from '@ts-rest/core';
|
import { initClient, initContract } from '@ts-rest/core';
|
||||||
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse, isAxiosError } from 'axios';
|
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse, isAxiosError } from 'axios';
|
||||||
import omitBy from 'lodash/omitBy';
|
|
||||||
import qs from 'qs';
|
import qs from 'qs';
|
||||||
import { z } from 'zod';
|
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 parsePath = (fullPath: string) => {
|
||||||
const [path, params] = fullPath.split('?');
|
const [path, params] = fullPath.split('?');
|
||||||
|
|
||||||
const parsedParams = qs.parse(params, { arrayLimit: 99999, parameterLimit: 99999 });
|
const url = new URLSearchParams(params);
|
||||||
const notNilParams = omitBy(parsedParams, (value) => value === 'undefined' || value === 'null');
|
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 {
|
return {
|
||||||
params: notNilParams,
|
params: notNilParams,
|
||||||
|
|||||||
@@ -2015,8 +2015,12 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If the server returns an error for transcodeDecision, fall back to direct stream so that we don't break the player
|
||||||
if (transcodeDecision.status !== 200) {
|
if (transcodeDecision.status !== 200) {
|
||||||
throw new Error('Failed to get transcode decision');
|
logFn.error(
|
||||||
|
`Failed to get transcode decision for song ${id}, falling back to direct stream`,
|
||||||
|
);
|
||||||
|
return streamUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
const td = transcodeDecision.body.transcodeDecision;
|
const td = transcodeDecision.body.transcodeDecision;
|
||||||
@@ -2121,6 +2125,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
|
|
||||||
const res = await SubsonicController.getSongList({
|
const res = await SubsonicController.getSongList({
|
||||||
apiClientProps,
|
apiClientProps,
|
||||||
|
context,
|
||||||
query: {
|
query: {
|
||||||
artistIds: [query.artistId],
|
artistIds: [query.artistId],
|
||||||
sortBy: SongListSort.PLAY_COUNT,
|
sortBy: SongListSort.PLAY_COUNT,
|
||||||
|
|||||||
@@ -22,12 +22,7 @@ import { WebAudio } from '/@/shared/types/types';
|
|||||||
import '/@/shared/styles/global.css';
|
import '/@/shared/styles/global.css';
|
||||||
import { PlayerProvider } from '/@/renderer/features/player/context/player-context';
|
import { PlayerProvider } from '/@/renderer/features/player/context/player-context';
|
||||||
import { AudioPlayers } from '/@/renderer/features/player/components/audio-players';
|
import { AudioPlayers } from '/@/renderer/features/player/components/audio-players';
|
||||||
|
import { ReleaseNotesModal } from '/@/renderer/release-notes-modal';
|
||||||
const ReleaseNotesModal = lazy(() =>
|
|
||||||
import('./release-notes-modal').then((module) => ({
|
|
||||||
default: module.ReleaseNotesModal,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
const UpdateAvailableDialog = lazy(() =>
|
const UpdateAvailableDialog = lazy(() =>
|
||||||
import('./update-available-dialog').then((module) => ({
|
import('./update-available-dialog').then((module) => ({
|
||||||
@@ -82,8 +77,8 @@ const AppShell = memo(function AppShell() {
|
|||||||
<AppRouter />
|
<AppRouter />
|
||||||
</PlayerProvider>
|
</PlayerProvider>
|
||||||
</WebAudioContext.Provider>
|
</WebAudioContext.Provider>
|
||||||
<Suspense fallback={null}>
|
|
||||||
<ReleaseNotesModal />
|
<ReleaseNotesModal />
|
||||||
|
<Suspense fallback={null}>
|
||||||
<UpdateAvailableDialog />
|
<UpdateAvailableDialog />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -169,6 +169,292 @@ export interface ItemCardDerivativeProps extends Omit<ItemCardProps, 'type'> {
|
|||||||
showRating: boolean;
|
showRating: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ItemCardData = NonNullable<ItemCardProps['data']>;
|
||||||
|
|
||||||
|
const ItemCardStandardImageArea = memo(function ItemCardStandardImageArea({
|
||||||
|
controls,
|
||||||
|
data,
|
||||||
|
enableExpansion,
|
||||||
|
enableImageViewport = true,
|
||||||
|
enableNavigation,
|
||||||
|
handleContextMenu,
|
||||||
|
handleImageClick,
|
||||||
|
handleLinkDragStart,
|
||||||
|
imageAsLink,
|
||||||
|
imageFetchPriority,
|
||||||
|
internalState,
|
||||||
|
isRound,
|
||||||
|
itemType,
|
||||||
|
navigationPath,
|
||||||
|
showRating,
|
||||||
|
variant,
|
||||||
|
withControls,
|
||||||
|
}: {
|
||||||
|
controls?: ItemControls;
|
||||||
|
data: ItemCardData;
|
||||||
|
enableExpansion?: boolean;
|
||||||
|
enableImageViewport?: boolean;
|
||||||
|
enableNavigation?: boolean;
|
||||||
|
handleContextMenu: (e: React.MouseEvent<HTMLElement>) => void;
|
||||||
|
handleImageClick: (e: React.MouseEvent<HTMLElement>) => void;
|
||||||
|
handleLinkDragStart: (e: React.DragEvent<HTMLAnchorElement>) => void;
|
||||||
|
imageAsLink?: boolean;
|
||||||
|
imageFetchPriority?: 'auto' | 'high' | 'low';
|
||||||
|
internalState?: ItemListStateActions;
|
||||||
|
isRound?: boolean;
|
||||||
|
itemType: LibraryItem;
|
||||||
|
navigationPath: null | string;
|
||||||
|
showRating: boolean;
|
||||||
|
variant: 'default' | 'poster';
|
||||||
|
withControls?: boolean;
|
||||||
|
}) {
|
||||||
|
const [showControls, setShowControls] = useState(false);
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
if (withControls) {
|
||||||
|
setShowControls(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
if (withControls) {
|
||||||
|
setShowControls(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const imageContainerClassName = clsx(styles.imageContainer, {
|
||||||
|
[styles.isRound]: isRound,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isFavorite = 'userFavorite' in data && (data as { userFavorite: boolean }).userFavorite;
|
||||||
|
const userRating =
|
||||||
|
'userRating' in data &&
|
||||||
|
typeof (data as { userRating: null | number }).userRating === 'number'
|
||||||
|
? (data as { userRating: null | number }).userRating
|
||||||
|
: null;
|
||||||
|
const hasRating = showRating && userRating !== null && userRating > 0;
|
||||||
|
|
||||||
|
const imageContainerContent = (
|
||||||
|
<>
|
||||||
|
{itemType === LibraryItem.GENRE &&
|
||||||
|
data &&
|
||||||
|
'name' in data &&
|
||||||
|
typeof (data as Genre).name === 'string' ? (
|
||||||
|
<GenreImagePlaceholder
|
||||||
|
className={clsx(styles.image, styles.genrePlaceholder, {
|
||||||
|
[styles.isRound]: isRound,
|
||||||
|
})}
|
||||||
|
name={(data as Genre).name}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ItemImage
|
||||||
|
className={clsx(styles.image, { [styles.isRound]: isRound })}
|
||||||
|
enableDebounce={false}
|
||||||
|
{...(variant === 'poster' ? { enableViewport: enableImageViewport } : {})}
|
||||||
|
explicitStatus={'explicitStatus' in data && data ? data.explicitStatus : null}
|
||||||
|
fetchPriority={imageFetchPriority}
|
||||||
|
id={(data as { imageId?: string })?.imageId}
|
||||||
|
itemType={itemType}
|
||||||
|
src={(data as { imageUrl?: string })?.imageUrl}
|
||||||
|
type="itemCard"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isFavorite && <div className={styles.favoriteBadge} />}
|
||||||
|
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
|
||||||
|
<AnimatePresence>
|
||||||
|
{withControls && showControls && (
|
||||||
|
<ItemCardControls
|
||||||
|
controls={controls}
|
||||||
|
enableExpansion={enableExpansion}
|
||||||
|
{...(variant === 'poster' ? { internalState } : {})}
|
||||||
|
item={data}
|
||||||
|
itemType={itemType}
|
||||||
|
showRating={showRating}
|
||||||
|
type={variant}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return enableNavigation && navigationPath && (imageAsLink ?? !internalState) ? (
|
||||||
|
<Link
|
||||||
|
className={imageContainerClassName}
|
||||||
|
draggable={false}
|
||||||
|
onClick={handleImageClick}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
|
onDragStart={handleLinkDragStart}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
state={{ item: data }}
|
||||||
|
to={navigationPath}
|
||||||
|
>
|
||||||
|
{imageContainerContent}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={imageContainerClassName}
|
||||||
|
onClick={handleImageClick}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
>
|
||||||
|
{imageContainerContent}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ItemCardStandardImageArea.displayName = 'ItemCardStandardImageArea';
|
||||||
|
|
||||||
|
const CompactItemCardImageArea = memo(function CompactItemCardImageArea({
|
||||||
|
controls,
|
||||||
|
data,
|
||||||
|
enableExpansion,
|
||||||
|
enableNavigation,
|
||||||
|
handleContextMenu,
|
||||||
|
handleImageClick,
|
||||||
|
handleLinkDragStart,
|
||||||
|
imageAsLink,
|
||||||
|
imageFetchPriority,
|
||||||
|
internalState,
|
||||||
|
isRound,
|
||||||
|
itemType,
|
||||||
|
navigationPath,
|
||||||
|
rows,
|
||||||
|
showRating,
|
||||||
|
withControls,
|
||||||
|
}: {
|
||||||
|
controls?: ItemControls;
|
||||||
|
data: ItemCardData;
|
||||||
|
enableExpansion?: boolean;
|
||||||
|
enableNavigation?: boolean;
|
||||||
|
handleContextMenu: (e: React.MouseEvent<HTMLElement>) => void;
|
||||||
|
handleImageClick: (e: React.MouseEvent<HTMLElement>) => void;
|
||||||
|
handleLinkDragStart: (e: React.DragEvent<HTMLAnchorElement>) => void;
|
||||||
|
imageAsLink?: boolean;
|
||||||
|
imageFetchPriority?: 'auto' | 'high' | 'low';
|
||||||
|
internalState?: ItemListStateActions;
|
||||||
|
isRound?: boolean;
|
||||||
|
itemType: LibraryItem;
|
||||||
|
navigationPath: null | string;
|
||||||
|
rows: DataRow[];
|
||||||
|
showRating: boolean;
|
||||||
|
withControls?: boolean;
|
||||||
|
}) {
|
||||||
|
const [showControls, setShowControls] = useState(false);
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
if (withControls) {
|
||||||
|
setShowControls(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
if (withControls) {
|
||||||
|
setShowControls(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const imageContainerClassName = clsx(styles.imageContainer, {
|
||||||
|
[styles.isRound]: isRound,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isFavorite = 'userFavorite' in data && (data as { userFavorite: boolean }).userFavorite;
|
||||||
|
const userRating =
|
||||||
|
'userRating' in data &&
|
||||||
|
typeof (data as { userRating: null | number }).userRating === 'number'
|
||||||
|
? (data as { userRating: null | number }).userRating
|
||||||
|
: null;
|
||||||
|
const hasRating = showRating && userRating !== null && userRating > 0;
|
||||||
|
|
||||||
|
const imageContainerContent = (
|
||||||
|
<>
|
||||||
|
{itemType === LibraryItem.GENRE &&
|
||||||
|
data &&
|
||||||
|
'name' in data &&
|
||||||
|
typeof (data as Genre).name === 'string' ? (
|
||||||
|
<GenreImagePlaceholder
|
||||||
|
className={clsx(styles.image, styles.genrePlaceholder, {
|
||||||
|
[styles.isRound]: isRound,
|
||||||
|
})}
|
||||||
|
name={(data as Genre).name}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ItemImage
|
||||||
|
className={clsx(styles.image, {
|
||||||
|
[styles.isRound]: isRound,
|
||||||
|
})}
|
||||||
|
enableDebounce={false}
|
||||||
|
explicitStatus={'explicitStatus' in data && data ? data.explicitStatus : null}
|
||||||
|
fetchPriority={imageFetchPriority}
|
||||||
|
id={data?.imageId}
|
||||||
|
itemType={itemType}
|
||||||
|
src={(data as Album | AlbumArtist | Playlist | Song)?.imageUrl}
|
||||||
|
type="itemCard"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isFavorite && <div className={styles.favoriteBadge} />}
|
||||||
|
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
|
||||||
|
<AnimatePresence>
|
||||||
|
{withControls && showControls && data && (
|
||||||
|
<ItemCardControls
|
||||||
|
controls={controls}
|
||||||
|
enableExpansion={enableExpansion}
|
||||||
|
internalState={internalState}
|
||||||
|
item={data}
|
||||||
|
itemType={itemType}
|
||||||
|
showRating={showRating}
|
||||||
|
type="compact"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
<div className={clsx(styles.detailContainer, styles.compact)}>
|
||||||
|
{rows
|
||||||
|
.filter(
|
||||||
|
(row): row is NonNullable<typeof row> => row !== null && row !== undefined,
|
||||||
|
)
|
||||||
|
.map((row, index) => (
|
||||||
|
<ItemCardRow
|
||||||
|
data={data!}
|
||||||
|
index={index}
|
||||||
|
key={row.id}
|
||||||
|
row={row}
|
||||||
|
type="compact"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return enableNavigation && navigationPath && (imageAsLink ?? !internalState) ? (
|
||||||
|
<Link
|
||||||
|
className={imageContainerClassName}
|
||||||
|
draggable={false}
|
||||||
|
onClick={handleImageClick}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
|
onDragStart={handleLinkDragStart}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
state={{ item: data }}
|
||||||
|
to={navigationPath}
|
||||||
|
>
|
||||||
|
{imageContainerContent}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={imageContainerClassName}
|
||||||
|
onClick={handleImageClick}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
>
|
||||||
|
{imageContainerContent}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
CompactItemCardImageArea.displayName = 'CompactItemCardImageArea';
|
||||||
|
|
||||||
const CompactItemCard = ({
|
const CompactItemCard = ({
|
||||||
controls,
|
controls,
|
||||||
data,
|
data,
|
||||||
@@ -185,7 +471,6 @@ const CompactItemCard = ({
|
|||||||
showRating,
|
showRating,
|
||||||
withControls,
|
withControls,
|
||||||
}: ItemCardDerivativeProps) => {
|
}: ItemCardDerivativeProps) => {
|
||||||
const [showControls, setShowControls] = useState(false);
|
|
||||||
const itemRowId =
|
const itemRowId =
|
||||||
data && internalState && typeof data === 'object' && 'id' in data
|
data && internalState && typeof data === 'object' && 'id' in data
|
||||||
? internalState.extractRowId(data)
|
? internalState.extractRowId(data)
|
||||||
@@ -297,18 +582,6 @@ const CompactItemCard = ({
|
|||||||
if (data) {
|
if (data) {
|
||||||
const navigationPath = getItemNavigationPath(data, itemType);
|
const navigationPath = getItemNavigationPath(data, itemType);
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
|
||||||
if (withControls) {
|
|
||||||
setShowControls(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
|
||||||
if (withControls) {
|
|
||||||
setShowControls(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleContextMenu = (e: React.MouseEvent<HTMLElement>) => {
|
const handleContextMenu = (e: React.MouseEvent<HTMLElement>) => {
|
||||||
if (!data || !controls) {
|
if (!data || !controls) {
|
||||||
return;
|
return;
|
||||||
@@ -338,81 +611,6 @@ const CompactItemCard = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
const isFavorite =
|
|
||||||
'userFavorite' in data && (data as { userFavorite: boolean }).userFavorite;
|
|
||||||
const userRating =
|
|
||||||
'userRating' in data &&
|
|
||||||
typeof (data as { userRating: null | number }).userRating === 'number'
|
|
||||||
? (data as { userRating: null | number }).userRating
|
|
||||||
: null;
|
|
||||||
const hasRating = showRating && userRating !== null && userRating > 0;
|
|
||||||
|
|
||||||
const imageContainerClassName = clsx(styles.imageContainer, {
|
|
||||||
[styles.isRound]: isRound,
|
|
||||||
});
|
|
||||||
|
|
||||||
const imageContainerContent = (
|
|
||||||
<>
|
|
||||||
{itemType === LibraryItem.GENRE &&
|
|
||||||
data &&
|
|
||||||
'name' in data &&
|
|
||||||
typeof (data as Genre).name === 'string' ? (
|
|
||||||
<GenreImagePlaceholder
|
|
||||||
className={clsx(styles.image, styles.genrePlaceholder, {
|
|
||||||
[styles.isRound]: isRound,
|
|
||||||
})}
|
|
||||||
name={(data as Genre).name}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ItemImage
|
|
||||||
className={clsx(styles.image, {
|
|
||||||
[styles.isRound]: isRound,
|
|
||||||
})}
|
|
||||||
enableDebounce={false}
|
|
||||||
explicitStatus={
|
|
||||||
'explicitStatus' in data && data ? data.explicitStatus : null
|
|
||||||
}
|
|
||||||
fetchPriority={imageFetchPriority}
|
|
||||||
id={data?.imageId}
|
|
||||||
itemType={itemType}
|
|
||||||
src={(data as Album | AlbumArtist | Playlist | Song)?.imageUrl}
|
|
||||||
type="itemCard"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isFavorite && <div className={styles.favoriteBadge} />}
|
|
||||||
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
|
|
||||||
<AnimatePresence>
|
|
||||||
{withControls && showControls && data && (
|
|
||||||
<ItemCardControls
|
|
||||||
controls={controls}
|
|
||||||
enableExpansion={enableExpansion}
|
|
||||||
internalState={internalState}
|
|
||||||
item={data}
|
|
||||||
itemType={itemType}
|
|
||||||
showRating={showRating}
|
|
||||||
type="compact"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
<div className={clsx(styles.detailContainer, styles.compact)}>
|
|
||||||
{rows
|
|
||||||
.filter(
|
|
||||||
(row): row is NonNullable<typeof row> =>
|
|
||||||
row !== null && row !== undefined,
|
|
||||||
)
|
|
||||||
.map((row, index) => (
|
|
||||||
<ItemCardRow
|
|
||||||
data={data!}
|
|
||||||
index={index}
|
|
||||||
key={row.id}
|
|
||||||
row={row}
|
|
||||||
type="compact"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(styles.container, styles.compact, {
|
className={clsx(styles.container, styles.compact, {
|
||||||
@@ -421,31 +619,24 @@ const CompactItemCard = ({
|
|||||||
})}
|
})}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
{enableNavigation && navigationPath && (imageAsLink ?? !internalState) ? (
|
<CompactItemCardImageArea
|
||||||
<Link
|
controls={controls}
|
||||||
className={imageContainerClassName}
|
data={data}
|
||||||
draggable={false}
|
enableExpansion={enableExpansion}
|
||||||
onClick={handleImageClick}
|
enableNavigation={enableNavigation}
|
||||||
onContextMenu={handleContextMenu}
|
handleContextMenu={handleContextMenu}
|
||||||
onDragStart={handleLinkDragStart}
|
handleImageClick={handleImageClick}
|
||||||
onMouseEnter={handleMouseEnter}
|
handleLinkDragStart={handleLinkDragStart}
|
||||||
onMouseLeave={handleMouseLeave}
|
imageAsLink={imageAsLink}
|
||||||
state={{ item: data }}
|
imageFetchPriority={imageFetchPriority}
|
||||||
to={navigationPath}
|
internalState={internalState}
|
||||||
>
|
isRound={isRound}
|
||||||
{imageContainerContent}
|
itemType={itemType}
|
||||||
</Link>
|
navigationPath={navigationPath}
|
||||||
) : (
|
rows={rows}
|
||||||
<div
|
showRating={showRating}
|
||||||
className={imageContainerClassName}
|
withControls={withControls}
|
||||||
onClick={handleImageClick}
|
/>
|
||||||
onContextMenu={handleContextMenu}
|
|
||||||
onMouseEnter={handleMouseEnter}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
>
|
|
||||||
{imageContainerContent}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -491,7 +682,6 @@ const DefaultItemCard = ({
|
|||||||
showRating,
|
showRating,
|
||||||
withControls,
|
withControls,
|
||||||
}: ItemCardDerivativeProps) => {
|
}: ItemCardDerivativeProps) => {
|
||||||
const [showControls, setShowControls] = useState(false);
|
|
||||||
const itemRowId =
|
const itemRowId =
|
||||||
data && internalState && typeof data === 'object' && 'id' in data
|
data && internalState && typeof data === 'object' && 'id' in data
|
||||||
? internalState.extractRowId(data)
|
? internalState.extractRowId(data)
|
||||||
@@ -538,18 +728,6 @@ const DefaultItemCard = ({
|
|||||||
if (data) {
|
if (data) {
|
||||||
const navigationPath = getItemNavigationPath(data, itemType);
|
const navigationPath = getItemNavigationPath(data, itemType);
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
|
||||||
if (withControls) {
|
|
||||||
setShowControls(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
|
||||||
if (withControls) {
|
|
||||||
setShowControls(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleContextMenu = (e: React.MouseEvent<HTMLElement>) => {
|
const handleContextMenu = (e: React.MouseEvent<HTMLElement>) => {
|
||||||
if (!data || !controls) {
|
if (!data || !controls) {
|
||||||
return;
|
return;
|
||||||
@@ -579,93 +757,30 @@ const DefaultItemCard = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
const imageContainerClassName = clsx(styles.imageContainer, {
|
|
||||||
[styles.isRound]: isRound,
|
|
||||||
});
|
|
||||||
|
|
||||||
const isFavorite =
|
|
||||||
'userFavorite' in data && (data as { userFavorite: boolean }).userFavorite;
|
|
||||||
const userRating =
|
|
||||||
'userRating' in data &&
|
|
||||||
typeof (data as { userRating: null | number }).userRating === 'number'
|
|
||||||
? (data as { userRating: null | number }).userRating
|
|
||||||
: null;
|
|
||||||
const hasRating = showRating && userRating !== null && userRating > 0;
|
|
||||||
|
|
||||||
const imageContainerContent = (
|
|
||||||
<>
|
|
||||||
{itemType === LibraryItem.GENRE &&
|
|
||||||
data &&
|
|
||||||
'name' in data &&
|
|
||||||
typeof (data as Genre).name === 'string' ? (
|
|
||||||
<GenreImagePlaceholder
|
|
||||||
className={clsx(styles.image, styles.genrePlaceholder, {
|
|
||||||
[styles.isRound]: isRound,
|
|
||||||
})}
|
|
||||||
name={(data as Genre).name}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ItemImage
|
|
||||||
className={clsx(styles.image, { [styles.isRound]: isRound })}
|
|
||||||
enableDebounce={false}
|
|
||||||
explicitStatus={
|
|
||||||
'explicitStatus' in data && data ? data.explicitStatus : null
|
|
||||||
}
|
|
||||||
fetchPriority={imageFetchPriority}
|
|
||||||
id={data?.imageId}
|
|
||||||
itemType={itemType}
|
|
||||||
src={(data as Album | AlbumArtist | Playlist | Song)?.imageUrl}
|
|
||||||
type="itemCard"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isFavorite && <div className={styles.favoriteBadge} />}
|
|
||||||
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
|
|
||||||
<AnimatePresence>
|
|
||||||
{withControls && showControls && (
|
|
||||||
<ItemCardControls
|
|
||||||
controls={controls}
|
|
||||||
enableExpansion={enableExpansion}
|
|
||||||
item={data}
|
|
||||||
itemType={itemType}
|
|
||||||
showRating={showRating}
|
|
||||||
type="default"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(styles.container, {
|
className={clsx(styles.container, {
|
||||||
[styles.selected]: isSelected,
|
[styles.selected]: isSelected,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{enableNavigation && navigationPath && (imageAsLink ?? !internalState) ? (
|
<ItemCardStandardImageArea
|
||||||
<Link
|
controls={controls}
|
||||||
className={imageContainerClassName}
|
data={data}
|
||||||
draggable={false}
|
enableExpansion={enableExpansion}
|
||||||
onClick={handleImageClick}
|
enableNavigation={enableNavigation}
|
||||||
onContextMenu={handleContextMenu}
|
handleContextMenu={handleContextMenu}
|
||||||
onDragStart={handleLinkDragStart}
|
handleImageClick={handleImageClick}
|
||||||
onMouseEnter={handleMouseEnter}
|
handleLinkDragStart={handleLinkDragStart}
|
||||||
onMouseLeave={handleMouseLeave}
|
imageAsLink={imageAsLink}
|
||||||
state={{ item: data }}
|
imageFetchPriority={imageFetchPriority}
|
||||||
to={navigationPath}
|
internalState={internalState}
|
||||||
>
|
isRound={isRound}
|
||||||
{imageContainerContent}
|
itemType={itemType}
|
||||||
</Link>
|
navigationPath={navigationPath}
|
||||||
) : (
|
showRating={showRating}
|
||||||
<div
|
variant="default"
|
||||||
className={imageContainerClassName}
|
withControls={withControls}
|
||||||
onClick={handleImageClick}
|
/>
|
||||||
onContextMenu={handleContextMenu}
|
|
||||||
onMouseEnter={handleMouseEnter}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
>
|
|
||||||
{imageContainerContent}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={styles.detailContainer}>
|
<div className={styles.detailContainer}>
|
||||||
{rows
|
{rows
|
||||||
.filter(
|
.filter(
|
||||||
@@ -728,7 +843,6 @@ const PosterItemCard = ({
|
|||||||
showRating,
|
showRating,
|
||||||
withControls,
|
withControls,
|
||||||
}: ItemCardDerivativeProps) => {
|
}: ItemCardDerivativeProps) => {
|
||||||
const [showControls, setShowControls] = useState(false);
|
|
||||||
const itemRowId =
|
const itemRowId =
|
||||||
data && internalState && typeof data === 'object' && 'id' in data
|
data && internalState && typeof data === 'object' && 'id' in data
|
||||||
? internalState.extractRowId(data)
|
? internalState.extractRowId(data)
|
||||||
@@ -840,18 +954,6 @@ const PosterItemCard = ({
|
|||||||
if (data) {
|
if (data) {
|
||||||
const navigationPath = getItemNavigationPath(data, itemType);
|
const navigationPath = getItemNavigationPath(data, itemType);
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
|
||||||
if (withControls) {
|
|
||||||
setShowControls(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
|
||||||
if (withControls) {
|
|
||||||
setShowControls(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleContextMenu = (e: React.MouseEvent<HTMLElement>) => {
|
const handleContextMenu = (e: React.MouseEvent<HTMLElement>) => {
|
||||||
if (!data || !controls) {
|
if (!data || !controls) {
|
||||||
return;
|
return;
|
||||||
@@ -881,63 +983,6 @@ const PosterItemCard = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
const imageContainerClassName = clsx(styles.imageContainer, {
|
|
||||||
[styles.isRound]: isRound,
|
|
||||||
});
|
|
||||||
|
|
||||||
const isFavorite =
|
|
||||||
'userFavorite' in data && (data as { userFavorite: boolean }).userFavorite;
|
|
||||||
const userRating =
|
|
||||||
'userRating' in data &&
|
|
||||||
typeof (data as { userRating: null | number }).userRating === 'number'
|
|
||||||
? (data as { userRating: null | number }).userRating
|
|
||||||
: null;
|
|
||||||
const hasRating = showRating && userRating !== null && userRating > 0;
|
|
||||||
|
|
||||||
const imageContainerContent = (
|
|
||||||
<>
|
|
||||||
{itemType === LibraryItem.GENRE &&
|
|
||||||
data &&
|
|
||||||
'name' in data &&
|
|
||||||
typeof (data as Genre).name === 'string' ? (
|
|
||||||
<GenreImagePlaceholder
|
|
||||||
className={clsx(styles.image, styles.genrePlaceholder, {
|
|
||||||
[styles.isRound]: isRound,
|
|
||||||
})}
|
|
||||||
name={(data as Genre).name}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ItemImage
|
|
||||||
className={clsx(styles.image, { [styles.isRound]: isRound })}
|
|
||||||
enableDebounce={false}
|
|
||||||
explicitStatus={
|
|
||||||
'explicitStatus' in data && data ? data.explicitStatus : null
|
|
||||||
}
|
|
||||||
fetchPriority={imageFetchPriority}
|
|
||||||
id={(data as { imageId: string })?.imageId}
|
|
||||||
itemType={itemType}
|
|
||||||
src={(data as { imageUrl: string })?.imageUrl}
|
|
||||||
type="itemCard"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isFavorite && <div className={styles.favoriteBadge} />}
|
|
||||||
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
|
|
||||||
<AnimatePresence>
|
|
||||||
{withControls && showControls && data && (
|
|
||||||
<ItemCardControls
|
|
||||||
controls={controls}
|
|
||||||
enableExpansion={enableExpansion}
|
|
||||||
internalState={internalState}
|
|
||||||
item={data}
|
|
||||||
itemType={itemType}
|
|
||||||
showRating={showRating}
|
|
||||||
type="poster"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(styles.container, styles.poster, {
|
className={clsx(styles.container, styles.poster, {
|
||||||
@@ -946,31 +991,24 @@ const PosterItemCard = ({
|
|||||||
})}
|
})}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
{enableNavigation && navigationPath && (imageAsLink ?? !internalState) ? (
|
<ItemCardStandardImageArea
|
||||||
<Link
|
controls={controls}
|
||||||
className={imageContainerClassName}
|
data={data}
|
||||||
draggable={false}
|
enableExpansion={enableExpansion}
|
||||||
onClick={handleImageClick}
|
enableNavigation={enableNavigation}
|
||||||
onContextMenu={handleContextMenu}
|
handleContextMenu={handleContextMenu}
|
||||||
onDragStart={handleLinkDragStart}
|
handleImageClick={handleImageClick}
|
||||||
onMouseEnter={handleMouseEnter}
|
handleLinkDragStart={handleLinkDragStart}
|
||||||
onMouseLeave={handleMouseLeave}
|
imageAsLink={imageAsLink}
|
||||||
state={{ item: data }}
|
imageFetchPriority={imageFetchPriority}
|
||||||
to={navigationPath}
|
internalState={internalState}
|
||||||
>
|
isRound={isRound}
|
||||||
{imageContainerContent}
|
itemType={itemType}
|
||||||
</Link>
|
navigationPath={navigationPath}
|
||||||
) : (
|
showRating={showRating}
|
||||||
<div
|
variant="poster"
|
||||||
className={imageContainerClassName}
|
withControls={withControls}
|
||||||
onClick={handleImageClick}
|
/>
|
||||||
onContextMenu={handleContextMenu}
|
|
||||||
onMouseEnter={handleMouseEnter}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
>
|
|
||||||
{imageContainerContent}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{data && (
|
{data && (
|
||||||
<div className={styles.detailContainer}>
|
<div className={styles.detailContainer}>
|
||||||
{rows
|
{rows
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import {
|
|||||||
ItemListStateItemWithRequiredProperties,
|
ItemListStateItemWithRequiredProperties,
|
||||||
} from '/@/renderer/components/item-list/helpers/item-list-state';
|
} from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||||
import { ItemControls } from '/@/renderer/components/item-list/types';
|
import { ItemControls } from '/@/renderer/components/item-list/types';
|
||||||
|
import { useHotkeys } from '/@/renderer/hooks/use-hotkeys';
|
||||||
import { useHotkeySettings, usePlayButtonBehavior } from '/@/renderer/store';
|
import { useHotkeySettings, usePlayButtonBehavior } from '/@/renderer/store';
|
||||||
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
|
||||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
import { Play } from '/@/shared/types/types';
|
import { Play } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
|||||||
@@ -385,8 +385,8 @@ const BaseItemGridList = ({
|
|||||||
rows,
|
rows,
|
||||||
size = 'default',
|
size = 'default',
|
||||||
}: ItemGridListProps) => {
|
}: ItemGridListProps) => {
|
||||||
const rootRef = useRef(null);
|
const rootRef = useRef<HTMLDivElement | null>(null);
|
||||||
const outerRef = useRef(null);
|
const outerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const listRef = useRef<FixedSizeList<GridItemProps>>(null);
|
const listRef = useRef<FixedSizeList<GridItemProps>>(null);
|
||||||
const { ref: containerRef, width: containerWidth } = useElementSize();
|
const { ref: containerRef, width: containerWidth } = useElementSize();
|
||||||
const { focused, ref: containerFocusRef } = useFocusWithin();
|
const { focused, ref: containerFocusRef } = useFocusWithin();
|
||||||
@@ -486,7 +486,7 @@ const BaseItemGridList = ({
|
|||||||
}, [itemsPerRow, rows?.length, size]);
|
}, [itemsPerRow, rows?.length, size]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const { current: container } = containerRef;
|
const container = rootRef.current;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
throttledSetTableMeta(containerWidth, resolvedItemCount, (meta) => {
|
throttledSetTableMeta(containerWidth, resolvedItemCount, (meta) => {
|
||||||
@@ -500,13 +500,15 @@ const BaseItemGridList = ({
|
|||||||
current.rowCount !== meta.rowCount
|
current.rowCount !== meta.rowCount
|
||||||
) {
|
) {
|
||||||
tableMetaRef.current = meta;
|
tableMetaRef.current = meta;
|
||||||
container.style.setProperty('--grid-column-count', String(meta.columnCount));
|
const el = rootRef.current;
|
||||||
container.style.setProperty('--grid-item-height', `${meta.itemHeight}px`);
|
if (!el) return;
|
||||||
container.style.setProperty('--grid-row-count', String(meta.rowCount));
|
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);
|
setTableMetaVersion((v) => v + 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [containerWidth, resolvedItemCount, throttledSetTableMeta, containerRef]);
|
}, [containerWidth, resolvedItemCount, throttledSetTableMeta]);
|
||||||
|
|
||||||
const controls = useDefaultItemListControls({
|
const controls = useDefaultItemListControls({
|
||||||
enableMultiSelect,
|
enableMultiSelect,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
} from '/@/renderer/features/shared/components/list-sort-by-dropdown';
|
} from '/@/renderer/features/shared/components/list-sort-by-dropdown';
|
||||||
import { ListSortOrderToggleButtonControlled } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
|
import { ListSortOrderToggleButtonControlled } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
|
||||||
import { FILTER_KEYS, searchLibraryItems } from '/@/renderer/features/shared/utils';
|
import { FILTER_KEYS, searchLibraryItems } from '/@/renderer/features/shared/utils';
|
||||||
|
import { useHotkeys } from '/@/renderer/hooks/use-hotkeys';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { useCurrentServer, usePlayerSong } from '/@/renderer/store';
|
import { useCurrentServer, usePlayerSong } from '/@/renderer/store';
|
||||||
import { useExternalLinks, useSettingsStore } from '/@/renderer/store/settings.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 { TextInput } from '/@/shared/components/text-input/text-input';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { useDebouncedValue } from '/@/shared/hooks/use-debounced-value';
|
import { useDebouncedValue } from '/@/shared/hooks/use-debounced-value';
|
||||||
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
|
||||||
import {
|
import {
|
||||||
Album,
|
Album,
|
||||||
AlbumListSort,
|
AlbumListSort,
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import { searchLibraryItems } from '/@/renderer/features/shared/utils';
|
|||||||
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
|
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
|
||||||
import { useContainerQuery } from '/@/renderer/hooks';
|
import { useContainerQuery } from '/@/renderer/hooks';
|
||||||
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
|
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
|
||||||
|
import { useHotkeys } from '/@/renderer/hooks/use-hotkeys';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import {
|
import {
|
||||||
ArtistItem,
|
ArtistItem,
|
||||||
@@ -75,7 +76,6 @@ import { TextInput } from '/@/shared/components/text-input/text-input';
|
|||||||
import { TextTitle } from '/@/shared/components/text-title/text-title';
|
import { TextTitle } from '/@/shared/components/text-title/text-title';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { useDebouncedValue } from '/@/shared/hooks/use-debounced-value';
|
import { useDebouncedValue } from '/@/shared/hooks/use-debounced-value';
|
||||||
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
|
||||||
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
|
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
|
||||||
import {
|
import {
|
||||||
Album,
|
Album,
|
||||||
@@ -1214,7 +1214,7 @@ export const AlbumArtistDetailContent = ({
|
|||||||
artistSongsLink={artistSongsLink}
|
artistSongsLink={artistSongsLink}
|
||||||
onArtistRadio={handleArtistRadio}
|
onArtistRadio={handleArtistRadio}
|
||||||
/>
|
/>
|
||||||
<Grid gutter="2xl">
|
<Grid gap="2xl">
|
||||||
<AlbumArtistMetadataGenres
|
<AlbumArtistMetadataGenres
|
||||||
genres={detailQuery.data?.genres}
|
genres={detailQuery.data?.genres}
|
||||||
order={genresOrder}
|
order={genresOrder}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useQuery, useSuspenseQuery, UseSuspenseQueryResult } from '@tanstack/react-query';
|
import { useSuspenseQuery, UseSuspenseQueryResult } from '@tanstack/react-query';
|
||||||
import { forwardRef, Fragment, useCallback, useMemo } from 'react';
|
import { forwardRef, Fragment, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
|
|
||||||
@@ -8,6 +8,8 @@ import styles from './album-artist-detail-header.module.css';
|
|||||||
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
|
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
|
||||||
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
||||||
import { getArtistAlbumsGrouped } from '/@/renderer/features/artists/hooks/use-artist-albums-grouped';
|
import { getArtistAlbumsGrouped } from '/@/renderer/features/artists/hooks/use-artist-albums-grouped';
|
||||||
|
import { useDeleteArtistImage } from '/@/renderer/features/artists/mutations/delete-artist-image-mutation';
|
||||||
|
import { useUploadArtistImage } from '/@/renderer/features/artists/mutations/upload-artist-image-mutation';
|
||||||
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
|
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
|
||||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||||
import {
|
import {
|
||||||
@@ -20,17 +22,80 @@ import { AppRoute } from '/@/renderer/router/routes';
|
|||||||
import { useAppStore, useCurrentServer, useShowRatings } from '/@/renderer/store';
|
import { useAppStore, useCurrentServer, useShowRatings } from '/@/renderer/store';
|
||||||
import { useArtistReleaseTypeItems, usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
import { useArtistReleaseTypeItems, usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||||
import { formatDurationString } from '/@/renderer/utils';
|
import { formatDurationString } from '/@/renderer/utils';
|
||||||
import { SEPARATOR_STRING, sortAlbumList } from '/@/shared/api/utils';
|
import { hasFeature, SEPARATOR_STRING, sortAlbumList } from '/@/shared/api/utils';
|
||||||
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
|
import { FileButton } from '/@/shared/components/file-button/file-button';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { AlbumListResponse, LibraryItem, ServerType } from '/@/shared/types/domain-types';
|
import {
|
||||||
|
AlbumArtistDetailResponse,
|
||||||
|
AlbumListResponse,
|
||||||
|
LibraryItem,
|
||||||
|
ServerType,
|
||||||
|
} from '/@/shared/types/domain-types';
|
||||||
|
import { ServerFeature } from '/@/shared/types/features-types';
|
||||||
import { Play } from '/@/shared/types/types';
|
import { Play } from '/@/shared/types/types';
|
||||||
|
|
||||||
interface AlbumArtistDetailHeaderProps {
|
interface AlbumArtistDetailHeaderProps {
|
||||||
albumsQuery: UseSuspenseQueryResult<AlbumListResponse, Error>;
|
albumsQuery: UseSuspenseQueryResult<AlbumListResponse, Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ArtistImageUploadOverlay({
|
||||||
|
data,
|
||||||
|
onUploadFile,
|
||||||
|
}: {
|
||||||
|
data?: AlbumArtistDetailResponse;
|
||||||
|
onUploadFile: (file: File) => Promise<void>;
|
||||||
|
}) {
|
||||||
|
const deleteArtistImageMutation = useDeleteArtistImage({});
|
||||||
|
const server = useCurrentServer();
|
||||||
|
|
||||||
|
if (!data) return null;
|
||||||
|
if (!hasFeature(server, ServerFeature.ARTIST_IMAGE_UPLOAD)) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group gap="xs">
|
||||||
|
<FileButton
|
||||||
|
accept="image/*"
|
||||||
|
onChange={async (file) => {
|
||||||
|
if (!file) return;
|
||||||
|
await onUploadFile(file);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(props) => (
|
||||||
|
<ActionIcon
|
||||||
|
icon="uploadImage"
|
||||||
|
iconProps={{ size: 'lg' }}
|
||||||
|
radius="xl"
|
||||||
|
size="xs"
|
||||||
|
variant="default"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FileButton>
|
||||||
|
<ActionIcon
|
||||||
|
disabled={!data?.uploadedImage}
|
||||||
|
icon="delete"
|
||||||
|
iconProps={{ size: 'lg' }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!data?._serverId) return;
|
||||||
|
deleteArtistImageMutation.mutate({
|
||||||
|
apiClientProps: {
|
||||||
|
serverId: data._serverId,
|
||||||
|
},
|
||||||
|
query: { id: data.id },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
radius="xl"
|
||||||
|
size="xs"
|
||||||
|
variant="default"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const AlbumArtistDetailHeader = forwardRef<HTMLDivElement, AlbumArtistDetailHeaderProps>(
|
export const AlbumArtistDetailHeader = forwardRef<HTMLDivElement, AlbumArtistDetailHeaderProps>(
|
||||||
({ albumsQuery }, ref) => {
|
({ albumsQuery }, ref) => {
|
||||||
const { albumArtistId, artistId } = useParams() as {
|
const { albumArtistId, artistId } = useParams() as {
|
||||||
@@ -78,6 +143,7 @@ export const AlbumArtistDetailHeader = forwardRef<HTMLDivElement, AlbumArtistDet
|
|||||||
const playButtonBehavior = usePlayButtonBehavior();
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
const setFavorite = useSetFavorite();
|
const setFavorite = useSetFavorite();
|
||||||
const setRating = useSetRating();
|
const setRating = useSetRating();
|
||||||
|
const uploadArtistImageMutation = useUploadArtistImage({});
|
||||||
|
|
||||||
const albumArtistDetailSort = useAppStore((state) => state.albumArtistDetailSort);
|
const albumArtistDetailSort = useAppStore((state) => state.albumArtistDetailSort);
|
||||||
const sortBy = albumArtistDetailSort.sortBy;
|
const sortBy = albumArtistDetailSort.sortBy;
|
||||||
@@ -167,40 +233,52 @@ export const AlbumArtistDetailHeader = forwardRef<HTMLDivElement, AlbumArtistDet
|
|||||||
[detailQuery.data],
|
[detailQuery.data],
|
||||||
);
|
);
|
||||||
|
|
||||||
const imageUrl = useItemImageUrl({
|
const headerImageUrl = useItemImageUrl({
|
||||||
id: detailQuery.data?.imageId || undefined,
|
id: detailQuery.data?.imageId || undefined,
|
||||||
imageUrl: detailQuery.data?.imageUrl,
|
imageUrl: detailQuery.data?.imageUrl,
|
||||||
itemType: LibraryItem.ALBUM_ARTIST,
|
itemType: LibraryItem.ALBUM_ARTIST,
|
||||||
type: 'itemCard',
|
type: 'header',
|
||||||
});
|
|
||||||
|
|
||||||
const artistInfoQuery = useQuery({
|
|
||||||
...artistsQueries.albumArtistInfo({
|
|
||||||
query: { id: routeId, limit: 10 },
|
|
||||||
serverId: server?.id,
|
|
||||||
}),
|
|
||||||
enabled: Boolean(server?.id && routeId),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const showRating = showRatings && detailQuery?.data?._serverType === ServerType.NAVIDROME;
|
const showRating = showRatings && detailQuery?.data?._serverType === ServerType.NAVIDROME;
|
||||||
|
|
||||||
const selectedImageUrl = useMemo(() => {
|
const canUploadArtistImage =
|
||||||
return detailQuery.data?.imageUrl || imageUrl;
|
hasFeature(server, ServerFeature.ARTIST_IMAGE_UPLOAD) &&
|
||||||
}, [detailQuery.data?.imageUrl, imageUrl]);
|
Boolean(detailQuery.data?._serverId);
|
||||||
|
|
||||||
const alternateImageUrl = artistInfoQuery.data?.imageUrl;
|
const handleArtistImageUpload = useCallback(
|
||||||
const hasImageId = Boolean(detailQuery.data?.imageId);
|
async (file: File) => {
|
||||||
const fallbackHeaderImageUrl = alternateImageUrl || selectedImageUrl;
|
const artist = detailQuery.data;
|
||||||
|
if (!artist?._serverId) return;
|
||||||
|
|
||||||
|
const buffer = await file.arrayBuffer();
|
||||||
|
uploadArtistImageMutation.mutate({
|
||||||
|
apiClientProps: {
|
||||||
|
serverId: artist._serverId,
|
||||||
|
},
|
||||||
|
body: { image: new Uint8Array(buffer) },
|
||||||
|
query: { id: artist.id },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[detailQuery.data, uploadArtistImageMutation],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LibraryHeader
|
<LibraryHeader
|
||||||
imageUrl={hasImageId ? undefined : fallbackHeaderImageUrl}
|
imageOverlay={
|
||||||
|
<ArtistImageUploadOverlay
|
||||||
|
data={detailQuery.data}
|
||||||
|
onUploadFile={handleArtistImageUpload}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
imageUrl={headerImageUrl}
|
||||||
item={{
|
item={{
|
||||||
imageId: detailQuery.data?.imageId,
|
imageId: detailQuery.data?.imageId,
|
||||||
imageUrl: hasImageId ? undefined : fallbackHeaderImageUrl,
|
imageUrl: detailQuery.data?.imageUrl,
|
||||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS,
|
route: AppRoute.LIBRARY_ALBUM_ARTISTS,
|
||||||
type: LibraryItem.ALBUM_ARTIST,
|
type: LibraryItem.ALBUM_ARTIST,
|
||||||
}}
|
}}
|
||||||
|
onImageFileDrop={canUploadArtistImage ? handleArtistImageUpload : undefined}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
title={detailQuery.data?.name || ''}
|
title={detailQuery.data?.name || ''}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -13,6 +13,15 @@ export type GroupingType = 'all' | 'primary';
|
|||||||
|
|
||||||
const PRIMARY_RELEASE_TYPES = ['album', 'broadcast', 'ep', 'other', 'single'];
|
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 = (
|
export const groupAlbumsByReleaseType = (
|
||||||
albums: Album[],
|
albums: Album[],
|
||||||
routeId: string,
|
routeId: string,
|
||||||
@@ -44,10 +53,9 @@ export const groupAlbumsByReleaseType = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Group by all release types
|
// Group by all release types
|
||||||
const releaseTypes = album.releaseTypes || [];
|
const normalizedTypes = getNormalizedReleaseTypes(album);
|
||||||
if (releaseTypes.length > 0) {
|
if (normalizedTypes.length > 0) {
|
||||||
// Sort release types: primaries first (alphabetically), then secondaries (alphabetically)
|
// Sort release types: primaries first (alphabetically), then secondaries (alphabetically)
|
||||||
const normalizedTypes = releaseTypes.map((type) => type.toLowerCase());
|
|
||||||
const primaryTypes = normalizedTypes
|
const primaryTypes = normalizedTypes
|
||||||
.filter((type) => PRIMARY_RELEASE_TYPES.includes(type))
|
.filter((type) => PRIMARY_RELEASE_TYPES.includes(type))
|
||||||
.sort();
|
.sort();
|
||||||
@@ -92,8 +100,7 @@ export const groupAlbumsByReleaseType = (
|
|||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
const releaseTypes = album.releaseTypes || [];
|
const normalizedTypes = getNormalizedReleaseTypes(album);
|
||||||
const normalizedTypes = releaseTypes.map((type) => type.toLowerCase());
|
|
||||||
|
|
||||||
let matchedType: null | string = null;
|
let matchedType: null | string = null;
|
||||||
|
|
||||||
@@ -107,6 +114,8 @@ export const groupAlbumsByReleaseType = (
|
|||||||
matchedType = 'broadcast';
|
matchedType = 'broadcast';
|
||||||
} else if (normalizedTypes.includes('other')) {
|
} else if (normalizedTypes.includes('other')) {
|
||||||
matchedType = 'other';
|
matchedType = 'other';
|
||||||
|
} else if (normalizedTypes.length > 0) {
|
||||||
|
matchedType = normalizedTypes[0];
|
||||||
} else {
|
} else {
|
||||||
matchedType = 'album';
|
matchedType = 'album';
|
||||||
}
|
}
|
||||||
@@ -292,11 +301,11 @@ export const getArtistAlbumsGrouped = (
|
|||||||
const types = releaseType.split('/');
|
const types = releaseType.split('/');
|
||||||
return types.some((type) => {
|
return types.some((type) => {
|
||||||
const enumValue = releaseTypeToEnumMap[type];
|
const enumValue = releaseTypeToEnumMap[type];
|
||||||
return enumValue ? enabledReleaseTypeEnums.has(enumValue) : false;
|
return enumValue ? enabledReleaseTypeEnums.has(enumValue) : true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const enumValue = releaseTypeToEnumMap[releaseType];
|
const enumValue = releaseTypeToEnumMap[releaseType];
|
||||||
return enumValue ? enabledReleaseTypeEnums.has(enumValue) : false;
|
return enumValue ? enabledReleaseTypeEnums.has(enumValue) : true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const releaseTypeEntries = Object.entries(albumsByReleaseType)
|
const releaseTypeEntries = Object.entries(albumsByReleaseType)
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { MutationHookArgs } from '/@/renderer/lib/react-query';
|
||||||
|
import { DeleteArtistImageArgs, DeleteArtistImageResponse } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
export const useDeleteArtistImage = (args: MutationHookArgs) => {
|
||||||
|
const { options } = args || {};
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<DeleteArtistImageResponse, AxiosError, DeleteArtistImageArgs, null>({
|
||||||
|
mutationFn: (args) => {
|
||||||
|
return api.controller.deleteArtistImage({
|
||||||
|
...args,
|
||||||
|
apiClientProps: { serverId: args.apiClientProps.serverId },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: (_data, variables) => {
|
||||||
|
const { apiClientProps, query } = variables;
|
||||||
|
const serverId = apiClientProps.serverId;
|
||||||
|
|
||||||
|
if (!serverId) return;
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: queryKeys.albumArtists.list(serverId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (query?.id) {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: queryKeys.albumArtists.detail(serverId, { id: query.id }),
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: queryKeys.albumArtists.info(serverId, { id: query.id }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { MutationHookArgs } from '/@/renderer/lib/react-query';
|
||||||
|
import { UploadArtistImageArgs, UploadArtistImageResponse } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
export const useUploadArtistImage = (args: MutationHookArgs) => {
|
||||||
|
const { options } = args || {};
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<UploadArtistImageResponse, AxiosError, UploadArtistImageArgs, null>({
|
||||||
|
mutationFn: (args) => {
|
||||||
|
return api.controller.uploadArtistImage({
|
||||||
|
...args,
|
||||||
|
apiClientProps: { serverId: args.apiClientProps.serverId },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: (_data, variables) => {
|
||||||
|
const { apiClientProps, query } = variables;
|
||||||
|
const serverId = apiClientProps.serverId;
|
||||||
|
|
||||||
|
if (!serverId) return;
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: queryKeys.albumArtists.list(serverId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (query?.id) {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: queryKeys.albumArtists.detail(serverId, { id: query.id }),
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: queryKeys.albumArtists.info(serverId, { id: query.id }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -165,7 +165,7 @@ export const AddToPlaylistAction = ({ items, itemType }: AddToPlaylistActionProp
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleAddToPlaylist = useCallback(
|
const handleAddToPlaylist = useCallback(
|
||||||
async (playlistId: string) => {
|
async (playlistId: string, playlistName: string) => {
|
||||||
if (items.length === 0 || !serverId) return;
|
if (items.length === 0 || !serverId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -202,10 +202,9 @@ export const AddToPlaylistAction = ({ items, itemType }: AddToPlaylistActionProp
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (allSongIds.length === 0) {
|
if (allSongIds.length === 0) {
|
||||||
toast.success({
|
toast.info({
|
||||||
message: t('form.addToPlaylist.success', {
|
message: t('form.addToPlaylist.noneAdded', {
|
||||||
message: 0,
|
playlist: playlistName,
|
||||||
numOfPlaylists: 1,
|
|
||||||
postProcess: 'sentenceCase',
|
postProcess: 'sentenceCase',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -245,10 +244,9 @@ export const AddToPlaylistAction = ({ items, itemType }: AddToPlaylistActionProp
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (songsToAdd.length === 0) {
|
if (songsToAdd.length === 0) {
|
||||||
toast.success({
|
toast.info({
|
||||||
message: t('form.addToPlaylist.success', {
|
message: t('form.addToPlaylist.noneAdded', {
|
||||||
message: 0,
|
playlist: playlistName,
|
||||||
numOfPlaylists: 1,
|
|
||||||
postProcess: 'sentenceCase',
|
postProcess: 'sentenceCase',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -413,7 +411,9 @@ export const AddToPlaylistAction = ({ items, itemType }: AddToPlaylistActionProp
|
|||||||
<>
|
<>
|
||||||
<ContextMenu.Item
|
<ContextMenu.Item
|
||||||
key={recentPlaylist.id}
|
key={recentPlaylist.id}
|
||||||
onSelect={() => handleAddToPlaylist(recentPlaylist.id)}
|
onSelect={() =>
|
||||||
|
handleAddToPlaylist(recentPlaylist.id, recentPlaylist.name)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{recentPlaylist.name}
|
{recentPlaylist.name}
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
@@ -428,7 +428,7 @@ export const AddToPlaylistAction = ({ items, itemType }: AddToPlaylistActionProp
|
|||||||
{filteredPlaylists.map((playlist) => (
|
{filteredPlaylists.map((playlist) => (
|
||||||
<ContextMenu.Item
|
<ContextMenu.Item
|
||||||
key={playlist.id}
|
key={playlist.id}
|
||||||
onSelect={() => handleAddToPlaylist(playlist.id)}
|
onSelect={() => handleAddToPlaylist(playlist.id, playlist.name)}
|
||||||
>
|
>
|
||||||
{playlist.name}
|
{playlist.name}
|
||||||
</ContextMenu.Item>
|
</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 isElectron from 'is-electron';
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
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';
|
import { PlayerStatus } from '/@/shared/types/types';
|
||||||
|
|
||||||
const discordRpc = isElectron() ? window.api.discordRpc : null;
|
const discordRpc = isElectron() ? window.api.discordRpc : null;
|
||||||
|
|
||||||
|
const DiscordStatusDisplayType = {
|
||||||
|
DETAILS: 2,
|
||||||
|
NAME: 0,
|
||||||
|
STATE: 1,
|
||||||
|
} as const;
|
||||||
|
|
||||||
type ActivityState = [QueueSong | undefined, number, PlayerStatus];
|
type ActivityState = [QueueSong | undefined, number, PlayerStatus];
|
||||||
|
|
||||||
const MAX_FIELD_LENGTH = 127;
|
const MAX_FIELD_LENGTH = 127;
|
||||||
@@ -122,7 +130,7 @@ export const useDiscordRpc = () => {
|
|||||||
: undefined
|
: undefined
|
||||||
: sentenceCase(current[2]),
|
: sentenceCase(current[2]),
|
||||||
state: truncate(artist),
|
state: truncate(artist),
|
||||||
statusDisplayType: StatusDisplayType.STATE,
|
statusDisplayType: DiscordStatusDisplayType.STATE,
|
||||||
type: discordSettings.showAsListening ? 2 : 0,
|
type: discordSettings.showAsListening ? 2 : 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -196,9 +204,9 @@ export const useDiscordRpc = () => {
|
|||||||
const artists = song?.artists.map((artist) => artist.name).join(', ');
|
const artists = song?.artists.map((artist) => artist.name).join(', ');
|
||||||
|
|
||||||
const statusDisplayMap = {
|
const statusDisplayMap = {
|
||||||
[DiscordDisplayType.ARTIST_NAME]: StatusDisplayType.STATE,
|
[DiscordDisplayType.ARTIST_NAME]: DiscordStatusDisplayType.STATE,
|
||||||
[DiscordDisplayType.FEISHIN]: StatusDisplayType.NAME,
|
[DiscordDisplayType.FEISHIN]: DiscordStatusDisplayType.NAME,
|
||||||
[DiscordDisplayType.SONG_NAME]: StatusDisplayType.DETAILS,
|
[DiscordDisplayType.SONG_NAME]: DiscordStatusDisplayType.DETAILS,
|
||||||
};
|
};
|
||||||
|
|
||||||
const activity: SetActivity = {
|
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 { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||||
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
|
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
|
||||||
import { Select } from '/@/shared/components/select/select';
|
import { Select } from '/@/shared/components/select/select';
|
||||||
|
import { Slider } from '/@/shared/components/slider/slider';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { Switch } from '/@/shared/components/switch/switch';
|
import { Switch } from '/@/shared/components/switch/switch';
|
||||||
import { TextInput } from '/@/shared/components/text-input/text-input';
|
import { TextInput } from '/@/shared/components/text-input/text-input';
|
||||||
@@ -185,6 +186,48 @@ export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) =>
|
|||||||
postProcess: 'sentenceCase',
|
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: (
|
control: (
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -4,14 +4,18 @@
|
|||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
color: var(--theme-colors-foreground);
|
color: var(--theme-colors-foreground);
|
||||||
word-break: normal;
|
word-break: normal;
|
||||||
opacity: 0.35;
|
opacity: var(--lyric-opacity);
|
||||||
|
transform: scale(var(--lyric-scale));
|
||||||
|
transform-origin: var(--lyric-scale-origin) center;
|
||||||
transition:
|
transition:
|
||||||
opacity 0.3s ease-in-out,
|
opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1),
|
||||||
transform 0.3s ease-in-out;
|
transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lyric-line:global(.active) {
|
.lyric-line:global(.active) {
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.lyric-line:global(.unsynchronized) {
|
.lyric-line:global(.unsynchronized) {
|
||||||
|
|||||||
@@ -131,7 +131,9 @@ export const LyricsActions = ({
|
|||||||
uppercase
|
uppercase
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
>
|
>
|
||||||
{t('common.clear', { postProcess: 'sentenceCase' })}
|
{hasLyrics
|
||||||
|
? t('common.clear', { postProcess: 'sentenceCase' })
|
||||||
|
: t('common.refresh', { postProcess: 'sentenceCase' })}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ export const SynchronizedLyrics = ({
|
|||||||
? displaySettings.fontSize
|
? displaySettings.fontSize
|
||||||
: 24,
|
: 24,
|
||||||
gap: displaySettings.gap && displaySettings.gap !== 0 ? displaySettings.gap : 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 { mediaSeekToTimestamp } = usePlayerActions();
|
||||||
const status = usePlayerStatus();
|
const status = usePlayerStatus();
|
||||||
@@ -90,18 +95,20 @@ export const SynchronizedLyrics = ({
|
|||||||
const programmaticScrollRef = useRef(false);
|
const programmaticScrollRef = useRef(false);
|
||||||
|
|
||||||
const getCurrentLyric = (timeInMs: number) => {
|
const getCurrentLyric = (timeInMs: number) => {
|
||||||
if (lyricRef.current) {
|
|
||||||
const activeLyrics = lyricRef.current;
|
const activeLyrics = lyricRef.current;
|
||||||
for (let idx = 0; idx < activeLyrics.length; idx += 1) {
|
if (!activeLyrics?.length) {
|
||||||
if (timeInMs <= activeLyrics[idx][0]) {
|
|
||||||
return idx === 0 ? idx : idx - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return activeLyrics.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
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<
|
const setCurrentLyricRef = useRef<
|
||||||
@@ -136,7 +143,20 @@ export const SynchronizedLyrics = ({
|
|||||||
.forEach((node) => node.classList.remove('active'));
|
.forEach((node) => node.classList.remove('active'));
|
||||||
|
|
||||||
if (index === -1) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +168,6 @@ export const SynchronizedLyrics = ({
|
|||||||
const offsetTop = currentLyric?.offsetTop - doc?.clientHeight / 2 || 0;
|
const offsetTop = currentLyric?.offsetTop - doc?.clientHeight / 2 || 0;
|
||||||
|
|
||||||
if (currentLyric === null) {
|
if (currentLyric === null) {
|
||||||
lyricRef.current = null;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +327,18 @@ export const SynchronizedLyrics = ({
|
|||||||
onMouseEnter={showScrollbar}
|
onMouseEnter={showScrollbar}
|
||||||
onMouseLeave={hideScrollbar}
|
onMouseLeave={hideScrollbar}
|
||||||
ref={containerRef}
|
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 && (
|
{settings.showProvider && source && (
|
||||||
<LyricLine
|
<LyricLine
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { eventEmitter } from '/@/renderer/events/event-emitter';
|
|||||||
import { useIsPlayerFetching, usePlayer } from '/@/renderer/features/player/context/player-context';
|
import { useIsPlayerFetching, usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||||
import { searchLibraryItems } from '/@/renderer/features/shared/utils';
|
import { searchLibraryItems } from '/@/renderer/features/shared/utils';
|
||||||
import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
|
import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
|
||||||
|
import { useHotkeys } from '/@/renderer/hooks/use-hotkeys';
|
||||||
import {
|
import {
|
||||||
isShuffleEnabled,
|
isShuffleEnabled,
|
||||||
mapShuffledToQueueIndex,
|
mapShuffledToQueueIndex,
|
||||||
@@ -30,7 +31,6 @@ import { Flex } from '/@/shared/components/flex/flex';
|
|||||||
import { LoadingOverlay } from '/@/shared/components/loading-overlay/loading-overlay';
|
import { LoadingOverlay } from '/@/shared/components/loading-overlay/loading-overlay';
|
||||||
import { useDebouncedValue } from '/@/shared/hooks/use-debounced-value';
|
import { useDebouncedValue } from '/@/shared/hooks/use-debounced-value';
|
||||||
import { useFocusWithin } from '/@/shared/hooks/use-focus-within';
|
import { useFocusWithin } from '/@/shared/hooks/use-focus-within';
|
||||||
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
|
||||||
import { useMergedRef } from '/@/shared/hooks/use-merged-ref';
|
import { useMergedRef } from '/@/shared/hooks/use-merged-ref';
|
||||||
import { Folder, LibraryItem, QueueSong, Song } from '/@/shared/types/domain-types';
|
import { Folder, LibraryItem, QueueSong, Song } from '/@/shared/types/domain-types';
|
||||||
import { DragTarget } from '/@/shared/types/drag-and-drop';
|
import { DragTarget } from '/@/shared/types/drag-and-drop';
|
||||||
|
|||||||
@@ -140,9 +140,15 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
seekTo(seekTo: number) {
|
seekTo(seekTo: number) {
|
||||||
|
let type: 'fraction' | 'seconds' | undefined = undefined;
|
||||||
|
|
||||||
|
if (seekTo < 1) {
|
||||||
|
type = 'seconds';
|
||||||
|
}
|
||||||
|
|
||||||
playerNum === 1
|
playerNum === 1
|
||||||
? player1Ref.current?.seekTo(seekTo)
|
? player1Ref.current?.seekTo(seekTo, type)
|
||||||
: player2Ref.current?.seekTo(seekTo);
|
: player2Ref.current?.seekTo(seekTo, type);
|
||||||
},
|
},
|
||||||
setVolume(volume: number) {
|
setVolume(volume: number) {
|
||||||
setInternalVolume1(volume / 100 || 0);
|
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) {
|
if (num === 1) {
|
||||||
playerRef.current?.player1()?.ref?.seekTo(timestamp);
|
playerRef.current?.player1()?.ref?.seekTo(timestamp, type);
|
||||||
} else {
|
} else {
|
||||||
playerRef.current?.player2()?.ref?.seekTo(timestamp);
|
playerRef.current?.player2()?.ref?.seekTo(timestamp, type);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPlayerStatus: async (properties) => {
|
onPlayerStatus: async (properties) => {
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ const CODEC_PROBES = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const DEFAULT_TRANSCODING_PROFILES = [
|
const DEFAULT_TRANSCODING_PROFILES = [
|
||||||
|
{ audioCodec: 'flac', container: 'flac', protocol: 'http' },
|
||||||
{ audioCodec: 'opus', container: 'ogg', protocol: 'http' },
|
{ audioCodec: 'opus', container: 'ogg', protocol: 'http' },
|
||||||
{ audioCodec: 'mp3', container: 'mp3', protocol: 'http' },
|
{ audioCodec: 'mp3', container: 'mp3', protocol: 'http' },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
SONG_DISPLAY_TYPES,
|
SONG_DISPLAY_TYPES,
|
||||||
} from '/@/renderer/features/shared/components/list-config-menu';
|
} from '/@/renderer/features/shared/components/list-config-menu';
|
||||||
import { useFastAverageColor } from '/@/renderer/hooks';
|
import { useFastAverageColor } from '/@/renderer/hooks';
|
||||||
|
import { useHotkeys } from '/@/renderer/hooks/use-hotkeys';
|
||||||
import {
|
import {
|
||||||
useFullScreenPlayerStore,
|
useFullScreenPlayerStore,
|
||||||
useFullScreenPlayerStoreActions,
|
useFullScreenPlayerStoreActions,
|
||||||
@@ -46,7 +47,6 @@ import { Popover } from '/@/shared/components/popover/popover';
|
|||||||
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
|
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
|
||||||
import { Slider } from '/@/shared/components/slider/slider';
|
import { Slider } from '/@/shared/components/slider/slider';
|
||||||
import { Switch } from '/@/shared/components/switch/switch';
|
import { Switch } from '/@/shared/components/switch/switch';
|
||||||
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
|
||||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey, ListDisplayType, Platform } from '/@/shared/types/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 styles from './full-screen-visualizer.module.css';
|
||||||
|
|
||||||
import { FullScreenVisualizerSongInfo } from '/@/renderer/features/player/components/full-screen-visualizer-song-info';
|
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 { useIsMobile } from '/@/renderer/hooks/use-is-mobile';
|
||||||
import { useFullScreenPlayerStoreActions } from '/@/renderer/store/full-screen-player.store';
|
import { useFullScreenPlayerStoreActions } from '/@/renderer/store/full-screen-player.store';
|
||||||
import {
|
import {
|
||||||
@@ -12,7 +13,6 @@ import {
|
|||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
useWindowSettings,
|
useWindowSettings,
|
||||||
} from '/@/renderer/store/settings.store';
|
} from '/@/renderer/store/settings.store';
|
||||||
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
|
||||||
import { Platform } from '/@/shared/types/types';
|
import { Platform } from '/@/shared/types/types';
|
||||||
|
|
||||||
const AudioMotionAnalyzerVisualizer = lazy(() =>
|
const AudioMotionAnalyzerVisualizer = lazy(() =>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
useIsRadioActive,
|
useIsRadioActive,
|
||||||
useRadioPlayer,
|
useRadioPlayer,
|
||||||
} from '/@/renderer/features/radio/hooks/use-radio-player';
|
} from '/@/renderer/features/radio/hooks/use-radio-player';
|
||||||
|
import { useHotkeys } from '/@/renderer/hooks/use-hotkeys';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import {
|
import {
|
||||||
useAppStore,
|
useAppStore,
|
||||||
@@ -34,7 +35,6 @@ import { Icon } from '/@/shared/components/icon/icon';
|
|||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
|
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
|
||||||
import { PlaybackSelectors } from '/@/shared/constants/playback-selectors';
|
import { PlaybackSelectors } from '/@/shared/constants/playback-selectors';
|
||||||
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
|
||||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
export const LeftControls = () => {
|
export const LeftControls = () => {
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export const PlayerbarWaveform = () => {
|
|||||||
height: 18,
|
height: 18,
|
||||||
interact: false,
|
interact: false,
|
||||||
media: audioElementRef.current,
|
media: audioElementRef.current,
|
||||||
normalize: false,
|
normalize: playerbarSlider?.stretched ?? false,
|
||||||
progressColor: primaryColor,
|
progressColor: primaryColor,
|
||||||
waveColor,
|
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 { useSetRating } from '/@/renderer/features/shared/hooks/use-set-rating';
|
||||||
import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
|
import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
|
||||||
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
|
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
|
||||||
|
import { useHotkeys } from '/@/renderer/hooks/use-hotkeys';
|
||||||
import {
|
import {
|
||||||
useAppStoreActions,
|
useAppStoreActions,
|
||||||
useAutoDJSettings,
|
useAutoDJSettings,
|
||||||
@@ -34,7 +35,6 @@ import { Button } from '/@/shared/components/button/button';
|
|||||||
import { Flex } from '/@/shared/components/flex/flex';
|
import { Flex } from '/@/shared/components/flex/flex';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Rating } from '/@/shared/components/rating/rating';
|
import { Rating } from '/@/shared/components/rating/rating';
|
||||||
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
|
||||||
import { useMediaQuery } from '/@/shared/hooks/use-media-query';
|
import { useMediaQuery } from '/@/shared/hooks/use-media-query';
|
||||||
import { useThrottledCallback } from '/@/shared/hooks/use-throttled-callback';
|
import { useThrottledCallback } from '/@/shared/hooks/use-throttled-callback';
|
||||||
import { useThrottledValue } from '/@/shared/hooks/use-throttled-value';
|
import { useThrottledValue } from '/@/shared/hooks/use-throttled-value';
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ export const SleepTimerButton = () => {
|
|||||||
|
|
||||||
<Divider my="md" />
|
<Divider my="md" />
|
||||||
|
|
||||||
<Grid gutter="xs">
|
<Grid gap="xs">
|
||||||
{PRESET_OPTIONS.filter((option) => option.mode === 'timed').map(
|
{PRESET_OPTIONS.filter((option) => option.mode === 'timed').map(
|
||||||
(option, index) => (
|
(option, index) => (
|
||||||
<Grid.Col key={index} span={4}>
|
<Grid.Col key={index} span={4}>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { HotkeyItem, useHotkeys } from '/@/renderer/hooks/use-hotkeys';
|
||||||
import { useHotkeySettings, usePlayerStore } from '/@/renderer/store';
|
import { useHotkeySettings, usePlayerStore } from '/@/renderer/store';
|
||||||
import { HotkeyItem, useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
|
||||||
|
|
||||||
export const usePlaybackHotkeys = () => {
|
export const usePlaybackHotkeys = () => {
|
||||||
const { bindings } = useHotkeySettings();
|
const { bindings } = useHotkeySettings();
|
||||||
|
|||||||
@@ -8,17 +8,17 @@ const ipc = isElectron() ? window.api.ipc : null;
|
|||||||
|
|
||||||
export const usePowerSaveBlocker = () => {
|
export const usePowerSaveBlocker = () => {
|
||||||
const status = usePlayerStatus();
|
const status = usePlayerStatus();
|
||||||
const { preventSleepOnPlayback } = useWindowSettings();
|
const { preventSleepOnPlayback, preventSuspendOnPlayback } = useWindowSettings();
|
||||||
|
|
||||||
const startPowerSaveBlocker = useCallback(async () => {
|
const startPowerSaveBlocker = useCallback(async () => {
|
||||||
if (!ipc) return;
|
if (!ipc) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ipc.invoke('power-save-blocker-start');
|
await ipc.invoke('power-save-blocker-start', { full: preventSleepOnPlayback });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to start power save blocker:', error);
|
console.error('Failed to start power save blocker:', error);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [preventSleepOnPlayback]);
|
||||||
|
|
||||||
const stopPowerSaveBlocker = useCallback(async () => {
|
const stopPowerSaveBlocker = useCallback(async () => {
|
||||||
if (!ipc) return;
|
if (!ipc) return;
|
||||||
@@ -31,16 +31,21 @@ export const usePowerSaveBlocker = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!preventSleepOnPlayback) return;
|
if (!preventSleepOnPlayback || !preventSuspendOnPlayback) return;
|
||||||
|
|
||||||
if (status === PlayerStatus.PLAYING) {
|
if (status === PlayerStatus.PLAYING) {
|
||||||
startPowerSaveBlocker();
|
startPowerSaveBlocker();
|
||||||
} else {
|
} else {
|
||||||
stopPowerSaveBlocker();
|
stopPowerSaveBlocker();
|
||||||
}
|
}
|
||||||
}, [status, preventSleepOnPlayback, startPowerSaveBlocker, stopPowerSaveBlocker]);
|
}, [
|
||||||
|
status,
|
||||||
|
preventSleepOnPlayback,
|
||||||
|
startPowerSaveBlocker,
|
||||||
|
stopPowerSaveBlocker,
|
||||||
|
preventSuspendOnPlayback,
|
||||||
|
]);
|
||||||
|
|
||||||
// Clean up on unmount
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
stopPowerSaveBlocker();
|
stopPowerSaveBlocker();
|
||||||
@@ -56,8 +61,11 @@ const PowerSaveBlockerHookInner = () => {
|
|||||||
export const PowerSaveBlockerHook = () => {
|
export const PowerSaveBlockerHook = () => {
|
||||||
const isElectronEnv = isElectron();
|
const isElectronEnv = isElectron();
|
||||||
const preventSleepOnPlayback = useSettingsStore((state) => state.window.preventSleepOnPlayback);
|
const preventSleepOnPlayback = useSettingsStore((state) => state.window.preventSleepOnPlayback);
|
||||||
|
const preventSuspendOnPlayback = useSettingsStore(
|
||||||
|
(state) => state.window.preventSuspendOnPlayback,
|
||||||
|
);
|
||||||
|
|
||||||
if (!isElectronEnv || !preventSleepOnPlayback) {
|
if (!isElectronEnv || !preventSleepOnPlayback || !preventSuspendOnPlayback) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export function useVisualizerSystemAudio(options: {
|
|||||||
onDeniedRef.current = onSystemAudioCaptureDenied;
|
onDeniedRef.current = onSystemAudioCaptureDenied;
|
||||||
onSuccessRef.current = onSystemAudioCaptureSuccess;
|
onSuccessRef.current = onSystemAudioCaptureSuccess;
|
||||||
const playbackType = usePlaybackType();
|
const playbackType = usePlaybackType();
|
||||||
|
const isMacOS = Boolean(window.api?.utils?.isMacOS?.());
|
||||||
const { setWebAudio, webAudio } = useWebAudio();
|
const { setWebAudio, webAudio } = useWebAudio();
|
||||||
const webAudioRef = useRef(webAudio);
|
const webAudioRef = useRef(webAudio);
|
||||||
const streamRef = useRef<MediaStream | null>(null);
|
const streamRef = useRef<MediaStream | null>(null);
|
||||||
@@ -80,7 +81,7 @@ export function useVisualizerSystemAudio(options: {
|
|||||||
try {
|
try {
|
||||||
const stream = await navigator.mediaDevices.getDisplayMedia({
|
const stream = await navigator.mediaDevices.getDisplayMedia({
|
||||||
audio: true,
|
audio: true,
|
||||||
video: false,
|
video: isMacOS, // On macOS, getDisplayMedia requires video to be requested in order to capture system audio
|
||||||
});
|
});
|
||||||
|
|
||||||
const audioTracks = stream.getAudioTracks();
|
const audioTracks = stream.getAudioTracks();
|
||||||
@@ -124,7 +125,7 @@ export function useVisualizerSystemAudio(options: {
|
|||||||
} finally {
|
} finally {
|
||||||
connectInFlightRef.current = false;
|
connectInFlightRef.current = false;
|
||||||
}
|
}
|
||||||
}, [disconnect, setWebAudio]);
|
}, [disconnect, isMacOS, setWebAudio]);
|
||||||
|
|
||||||
const connectRef = useRef(connect);
|
const connectRef = useRef(connect);
|
||||||
connectRef.current = connect;
|
connectRef.current = connect;
|
||||||
|
|||||||
@@ -552,7 +552,7 @@ const PlaylistTableItem = memo(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={styles.container} w="100%">
|
<Box className={styles.container} w="100%">
|
||||||
<Grid align="center" gutter="xs" w="100%">
|
<Grid align="center" gap="xs" w="100%">
|
||||||
<Grid.Col span="content">
|
<Grid.Col span="content">
|
||||||
<Flex align="center" justify="center" px="sm">
|
<Flex align="center" justify="center" px="sm">
|
||||||
<ItemImage
|
<ItemImage
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useLocation, useParams } from 'react-router';
|
import { useLocation, useParams } from 'react-router';
|
||||||
|
|
||||||
@@ -40,8 +41,13 @@ interface PlaylistDetailSongListHeaderProps {
|
|||||||
onToggleQueryBuilder?: () => void;
|
onToggleQueryBuilder?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ImageUploadOverlay({ data }: { data?: Playlist }) {
|
function ImageUploadOverlay({
|
||||||
const uploadPlaylistImageMutation = useUploadPlaylistImage({});
|
data,
|
||||||
|
onUploadFile,
|
||||||
|
}: {
|
||||||
|
data?: Playlist;
|
||||||
|
onUploadFile: (file: File) => Promise<void>;
|
||||||
|
}) {
|
||||||
const deletePlaylistImageMutation = useDeletePlaylistImage({});
|
const deletePlaylistImageMutation = useDeletePlaylistImage({});
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
|
|
||||||
@@ -53,16 +59,8 @@ function ImageUploadOverlay({ data }: { data?: Playlist }) {
|
|||||||
<FileButton
|
<FileButton
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
onChange={async (file) => {
|
onChange={async (file) => {
|
||||||
if (!file || !data?._serverId) return;
|
if (!file) return;
|
||||||
|
await onUploadFile(file);
|
||||||
const buffer = await file.arrayBuffer();
|
|
||||||
uploadPlaylistImageMutation.mutate({
|
|
||||||
apiClientProps: {
|
|
||||||
serverId: data._serverId,
|
|
||||||
},
|
|
||||||
body: { image: new Uint8Array(buffer) },
|
|
||||||
query: { id: data.id },
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(props) => (
|
{(props) => (
|
||||||
@@ -121,11 +119,33 @@ export const PlaylistDetailSongListHeader = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const player = usePlayer();
|
const player = usePlayer();
|
||||||
|
const uploadPlaylistImageMutation = useUploadPlaylistImage({});
|
||||||
|
|
||||||
const handlePlay = (type?: Play) => {
|
const handlePlay = (type?: Play) => {
|
||||||
player.addToQueueByData(listData as Song[], type || Play.NOW);
|
player.addToQueueByData(listData as Song[], type || Play.NOW);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const canUploadPlaylistImage =
|
||||||
|
hasFeature(server, ServerFeature.PLAYLIST_IMAGE_UPLOAD) &&
|
||||||
|
Boolean(detailQuery?.data?._serverId);
|
||||||
|
|
||||||
|
const handlePlaylistImageUpload = useCallback(
|
||||||
|
async (file: File) => {
|
||||||
|
const playlist = detailQuery?.data;
|
||||||
|
if (!playlist?._serverId) return;
|
||||||
|
|
||||||
|
const buffer = await file.arrayBuffer();
|
||||||
|
uploadPlaylistImageMutation.mutate({
|
||||||
|
apiClientProps: {
|
||||||
|
serverId: playlist._serverId,
|
||||||
|
},
|
||||||
|
body: { image: new Uint8Array(buffer) },
|
||||||
|
query: { id: playlist.id },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[detailQuery?.data, uploadPlaylistImageMutation],
|
||||||
|
);
|
||||||
|
|
||||||
const imageUrl = useItemImageUrl({
|
const imageUrl = useItemImageUrl({
|
||||||
id: detailQuery?.data?.imageId || undefined,
|
id: detailQuery?.data?.imageId || undefined,
|
||||||
itemType: LibraryItem.PLAYLIST,
|
itemType: LibraryItem.PLAYLIST,
|
||||||
@@ -163,7 +183,12 @@ export const PlaylistDetailSongListHeader = ({
|
|||||||
) : (
|
) : (
|
||||||
<LibraryHeader
|
<LibraryHeader
|
||||||
compact
|
compact
|
||||||
imageOverlay={<ImageUploadOverlay data={detailQuery?.data} />}
|
imageOverlay={
|
||||||
|
<ImageUploadOverlay
|
||||||
|
data={detailQuery?.data}
|
||||||
|
onUploadFile={handlePlaylistImageUpload}
|
||||||
|
/>
|
||||||
|
}
|
||||||
imageUrl={imageUrl}
|
imageUrl={imageUrl}
|
||||||
item={{
|
item={{
|
||||||
imageId: detailQuery?.data?.imageId,
|
imageId: detailQuery?.data?.imageId,
|
||||||
@@ -171,6 +196,7 @@ export const PlaylistDetailSongListHeader = ({
|
|||||||
route: AppRoute.PLAYLISTS,
|
route: AppRoute.PLAYLISTS,
|
||||||
type: LibraryItem.PLAYLIST,
|
type: LibraryItem.PLAYLIST,
|
||||||
}}
|
}}
|
||||||
|
onImageFileDrop={canUploadPlaylistImage ? handlePlaylistImageUpload : undefined}
|
||||||
title={detailQuery?.data?.name || ''}
|
title={detailQuery?.data?.name || ''}
|
||||||
topRight={<ListSearchInput />}
|
topRight={<ListSearchInput />}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { useCurrentServer, useCurrentServerId, usePermissions } from '/@/rendere
|
|||||||
import { hasFeature } from '/@/shared/api/utils';
|
import { hasFeature } from '/@/shared/api/utils';
|
||||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { Box } from '/@/shared/components/box/box';
|
import { Box } from '/@/shared/components/box/box';
|
||||||
|
import { DragDropZone } from '/@/shared/components/drag-drop-zone/drag-drop-zone';
|
||||||
import { FileButton } from '/@/shared/components/file-button/file-button';
|
import { FileButton } from '/@/shared/components/file-button/file-button';
|
||||||
import { Flex } from '/@/shared/components/flex/flex';
|
import { Flex } from '/@/shared/components/flex/flex';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
@@ -270,16 +271,20 @@ function PlaylistCoverField({
|
|||||||
const iconControls = (
|
const iconControls = (
|
||||||
<>
|
<>
|
||||||
<FileButton accept="image/*" onChange={onFileSelect}>
|
<FileButton accept="image/*" onChange={onFileSelect}>
|
||||||
{(props) => (
|
{(props) => {
|
||||||
|
const { ...triggerRest } = props;
|
||||||
|
return (
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
icon="uploadImage"
|
icon="uploadImage"
|
||||||
iconProps={{ size: 'lg' }}
|
iconProps={{ size: 'lg' }}
|
||||||
radius="xl"
|
radius="xl"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="default"
|
variant="default"
|
||||||
{...props}
|
{...triggerRest}
|
||||||
|
style={{ pointerEvents: 'auto' }}
|
||||||
/>
|
/>
|
||||||
)}
|
);
|
||||||
|
}}
|
||||||
</FileButton>
|
</FileButton>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
disabled={secondaryDisabled}
|
disabled={secondaryDisabled}
|
||||||
@@ -288,22 +293,12 @@ function PlaylistCoverField({
|
|||||||
onClick={secondaryAction}
|
onClick={secondaryAction}
|
||||||
radius="xl"
|
radius="xl"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
style={{ pointerEvents: 'auto' }}
|
||||||
variant="default"
|
variant="default"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const coverArt = (
|
|
||||||
<ItemImage
|
|
||||||
enableViewport={false}
|
|
||||||
id={previewId}
|
|
||||||
itemType={LibraryItem.PLAYLIST}
|
|
||||||
serverId={server?.id}
|
|
||||||
src={previewSrc}
|
|
||||||
type="header"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
@@ -315,21 +310,41 @@ function PlaylistCoverField({
|
|||||||
width: COVER_SIZE,
|
width: COVER_SIZE,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{coverArt}
|
<DragDropZone
|
||||||
|
accept="image/*"
|
||||||
|
mode="file"
|
||||||
|
onFileSelected={(file) => onFileSelect(file)}
|
||||||
|
style={{
|
||||||
|
height: '100%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ItemImage
|
||||||
|
enableViewport={false}
|
||||||
|
id={previewId}
|
||||||
|
itemType={LibraryItem.PLAYLIST}
|
||||||
|
serverId={server?.id}
|
||||||
|
src={previewSrc}
|
||||||
|
type="header"
|
||||||
|
/>
|
||||||
<Group
|
<Group
|
||||||
gap={4}
|
gap={4}
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(0, 0, 0, 0.55)',
|
background: 'rgba(0, 0, 0, 0.55)',
|
||||||
borderRadius: 'var(--mantine-radius-md)',
|
|
||||||
bottom: 6,
|
bottom: 6,
|
||||||
padding: 4,
|
padding: 4,
|
||||||
|
pointerEvents: 'none',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
right: 6,
|
right: 6,
|
||||||
|
zIndex: 2,
|
||||||
}}
|
}}
|
||||||
wrap="nowrap"
|
wrap="nowrap"
|
||||||
>
|
>
|
||||||
{iconControls}
|
{iconControls}
|
||||||
</Group>
|
</Group>
|
||||||
|
</DragDropZone>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { logMsg } from '/@/renderer/utils/logger-message';
|
|||||||
import { hasFeature } from '/@/shared/api/utils';
|
import { hasFeature } from '/@/shared/api/utils';
|
||||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { Box } from '/@/shared/components/box/box';
|
import { Box } from '/@/shared/components/box/box';
|
||||||
|
import { DragDropZone } from '/@/shared/components/drag-drop-zone/drag-drop-zone';
|
||||||
import { FileButton } from '/@/shared/components/file-button/file-button';
|
import { FileButton } from '/@/shared/components/file-button/file-button';
|
||||||
import { Flex } from '/@/shared/components/flex/flex';
|
import { Flex } from '/@/shared/components/flex/flex';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
@@ -241,16 +242,20 @@ function RadioStationCoverField({
|
|||||||
const iconControls = (
|
const iconControls = (
|
||||||
<>
|
<>
|
||||||
<FileButton accept="image/*" onChange={onFileSelect}>
|
<FileButton accept="image/*" onChange={onFileSelect}>
|
||||||
{(props) => (
|
{(props) => {
|
||||||
|
const { ...triggerRest } = props;
|
||||||
|
return (
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
icon="uploadImage"
|
icon="uploadImage"
|
||||||
iconProps={{ size: 'lg' }}
|
iconProps={{ size: 'lg' }}
|
||||||
radius="xl"
|
radius="xl"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="default"
|
variant="default"
|
||||||
{...props}
|
{...triggerRest}
|
||||||
|
style={{ pointerEvents: 'auto' }}
|
||||||
/>
|
/>
|
||||||
)}
|
);
|
||||||
|
}}
|
||||||
</FileButton>
|
</FileButton>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
disabled={secondaryDisabled}
|
disabled={secondaryDisabled}
|
||||||
@@ -259,22 +264,12 @@ function RadioStationCoverField({
|
|||||||
onClick={secondaryAction}
|
onClick={secondaryAction}
|
||||||
radius="xl"
|
radius="xl"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
style={{ pointerEvents: 'auto' }}
|
||||||
variant="default"
|
variant="default"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const coverArt = (
|
|
||||||
<ItemImage
|
|
||||||
enableViewport={false}
|
|
||||||
id={previewId}
|
|
||||||
itemType={LibraryItem.RADIO_STATION}
|
|
||||||
serverId={server?.id}
|
|
||||||
src={previewSrc}
|
|
||||||
type="header"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
@@ -286,21 +281,41 @@ function RadioStationCoverField({
|
|||||||
width: COVER_SIZE,
|
width: COVER_SIZE,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{coverArt}
|
<DragDropZone
|
||||||
|
accept="image/*"
|
||||||
|
mode="file"
|
||||||
|
onFileSelected={(file) => onFileSelect(file)}
|
||||||
|
style={{
|
||||||
|
height: '100%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ItemImage
|
||||||
|
enableViewport={false}
|
||||||
|
id={previewId}
|
||||||
|
itemType={LibraryItem.RADIO_STATION}
|
||||||
|
serverId={server?.id}
|
||||||
|
src={previewSrc}
|
||||||
|
type="header"
|
||||||
|
/>
|
||||||
<Group
|
<Group
|
||||||
gap={4}
|
gap={4}
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(0, 0, 0, 0.55)',
|
background: 'rgba(0, 0, 0, 0.55)',
|
||||||
borderRadius: 'var(--mantine-radius-md)',
|
|
||||||
bottom: 6,
|
bottom: 6,
|
||||||
padding: 4,
|
padding: 4,
|
||||||
|
pointerEvents: 'none',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
right: 6,
|
right: 6,
|
||||||
|
zIndex: 2,
|
||||||
}}
|
}}
|
||||||
wrap="nowrap"
|
wrap="nowrap"
|
||||||
>
|
>
|
||||||
{iconControls}
|
{iconControls}
|
||||||
</Group>
|
</Group>
|
||||||
|
</DragDropZone>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -477,6 +477,32 @@ export const ControlSettings = memo(() => {
|
|||||||
postProcess: 'sentenceCase',
|
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: (
|
control: (
|
||||||
<NumberInput
|
<NumberInput
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ export const ThemeSettings = memo(() => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
renderOption={renderThemeOption}
|
renderOption={renderThemeOption}
|
||||||
|
searchable
|
||||||
width={240}
|
width={240}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ export const WindowSettings = memo(() => {
|
|||||||
<Switch
|
<Switch
|
||||||
aria-label="Toggle prevent sleep on playback"
|
aria-label="Toggle prevent sleep on playback"
|
||||||
defaultChecked={settings.preventSleepOnPlayback}
|
defaultChecked={settings.preventSleepOnPlayback}
|
||||||
disabled={!isElectron()}
|
disabled={!isElectron() || settings.preventSuspendOnPlayback}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (!e) return;
|
if (!e) return;
|
||||||
localSettings?.set(
|
localSettings?.set(
|
||||||
@@ -206,6 +206,33 @@ export const WindowSettings = memo(() => {
|
|||||||
isHidden: !isElectron(),
|
isHidden: !isElectron(),
|
||||||
title: t('setting.preventSleepOnPlayback', { postProcess: 'sentenceCase' }),
|
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 (
|
return (
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
background-image: var(--theme-overlay-subheader);
|
background-image: var(--theme-overlay-subheader);
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
backface-visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.background-overlay {
|
.background-overlay {
|
||||||
@@ -30,6 +32,18 @@
|
|||||||
var(--dither);
|
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 {
|
.background-image {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -37,6 +51,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
background-position: center !important;
|
background-position: center !important;
|
||||||
background-size: cover !important;
|
background-size: cover !important;
|
||||||
|
isolation: isolate;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export const LibraryBackgroundImage = ({ blur, headerRef, imageUrl }: LibraryBac
|
|||||||
const url = imageUrl ? `url(${imageUrl})` : undefined;
|
const url = imageUrl ? `url(${imageUrl})` : undefined;
|
||||||
const height = useHeaderHeight(headerRef);
|
const height = useHeaderHeight(headerRef);
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={styles.backgroundImageStack}>
|
||||||
<div
|
<div
|
||||||
className={styles.backgroundImage}
|
className={styles.backgroundImage}
|
||||||
style={{
|
style={{
|
||||||
@@ -91,7 +91,7 @@ export const LibraryBackgroundImage = ({ blur, headerRef, imageUrl }: LibraryBac
|
|||||||
height: height ? `${height + 64}px` : undefined,
|
height: height ? `${height + 64}px` : undefined,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { KeyboardEvent } from 'react';
|
||||||
|
|
||||||
import { closeAllModals, openModal } from '@mantine/modals';
|
import { closeAllModals, openModal } from '@mantine/modals';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { forwardRef, ReactNode, Ref, useCallback } from 'react';
|
import { forwardRef, ReactNode, Ref, useCallback } from 'react';
|
||||||
@@ -22,6 +24,7 @@ import { useGeneralSettings } from '/@/renderer/store';
|
|||||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { Button } from '/@/shared/components/button/button';
|
import { Button } from '/@/shared/components/button/button';
|
||||||
import { Center } from '/@/shared/components/center/center';
|
import { Center } from '/@/shared/components/center/center';
|
||||||
|
import { DragDropZone } from '/@/shared/components/drag-drop-zone/drag-drop-zone';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
import { BaseImage } from '/@/shared/components/image/image';
|
import { BaseImage } from '/@/shared/components/image/image';
|
||||||
@@ -47,6 +50,7 @@ interface LibraryHeaderProps {
|
|||||||
type?: LibraryItem;
|
type?: LibraryItem;
|
||||||
};
|
};
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
onImageFileDrop?: (file: File) => Promise<void> | void;
|
||||||
title: string;
|
title: string;
|
||||||
topRight?: ReactNode;
|
topRight?: ReactNode;
|
||||||
}
|
}
|
||||||
@@ -60,6 +64,7 @@ export const LibraryHeader = forwardRef(
|
|||||||
imageOverlay,
|
imageOverlay,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
item,
|
item,
|
||||||
|
onImageFileDrop,
|
||||||
title,
|
title,
|
||||||
topRight,
|
topRight,
|
||||||
}: LibraryHeaderProps,
|
}: LibraryHeaderProps,
|
||||||
@@ -136,6 +141,17 @@ export const LibraryHeader = forwardRef(
|
|||||||
});
|
});
|
||||||
}, [blurExplicitImages, item.explicitStatus, item.imageId, item.type]);
|
}, [blurExplicitImages, item.explicitStatus, item.imageId, item.type]);
|
||||||
|
|
||||||
|
const imageSectionSharedProps = {
|
||||||
|
onClick: () => {
|
||||||
|
openImage();
|
||||||
|
},
|
||||||
|
onKeyDown: (event: KeyboardEvent) =>
|
||||||
|
[' ', 'Enter', 'Spacebar'].includes(event.key) && openImage(),
|
||||||
|
role: 'button' as const,
|
||||||
|
style: { cursor: 'pointer' as const },
|
||||||
|
tabIndex: 0,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@@ -146,17 +162,13 @@ export const LibraryHeader = forwardRef(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
{topRight && <div className={styles.topRight}>{topRight}</div>}
|
{topRight && <div className={styles.topRight}>{topRight}</div>}
|
||||||
<div
|
{onImageFileDrop ? (
|
||||||
|
<DragDropZone
|
||||||
|
accept="image/*"
|
||||||
className={styles.imageSection}
|
className={styles.imageSection}
|
||||||
onClick={() => {
|
mode="file"
|
||||||
openImage();
|
onFileSelected={(file) => void onImageFileDrop(file)}
|
||||||
}}
|
{...imageSectionSharedProps}
|
||||||
onKeyDown={(event) =>
|
|
||||||
[' ', 'Enter', 'Spacebar'].includes(event.key) && openImage()
|
|
||||||
}
|
|
||||||
role="button"
|
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
<ItemImage
|
<ItemImage
|
||||||
className={styles.image}
|
className={styles.image}
|
||||||
@@ -180,7 +192,33 @@ export const LibraryHeader = forwardRef(
|
|||||||
{imageOverlay}
|
{imageOverlay}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</DragDropZone>
|
||||||
|
) : (
|
||||||
|
<div className={styles.imageSection} {...imageSectionSharedProps}>
|
||||||
|
<ItemImage
|
||||||
|
className={styles.image}
|
||||||
|
containerClassName={styles.image}
|
||||||
|
enableDebounce={false}
|
||||||
|
enableViewport={false}
|
||||||
|
explicitStatus={item.explicitStatus ?? null}
|
||||||
|
fetchPriority="high"
|
||||||
|
id={item.imageId}
|
||||||
|
itemType={item.type as LibraryItem}
|
||||||
|
src={imageUrl || ''}
|
||||||
|
type="header"
|
||||||
|
/>
|
||||||
|
{imageOverlay && (
|
||||||
|
<div
|
||||||
|
className={styles.imageOverlay}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onKeyDown={(e) => e.stopPropagation()}
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
{imageOverlay}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{title && (
|
{title && (
|
||||||
<div className={styles.metadataSection}>
|
<div className={styles.metadataSection}>
|
||||||
{item.children ? (
|
{item.children ? (
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import { shallow } from 'zustand/shallow';
|
import { shallow } from 'zustand/shallow';
|
||||||
|
|
||||||
|
import { useHotkeys } from '/@/renderer/hooks/use-hotkeys';
|
||||||
import { useSettingsStore } from '/@/renderer/store';
|
import { useSettingsStore } from '/@/renderer/store';
|
||||||
import { ActionIcon, ActionIconProps } from '/@/shared/components/action-icon/action-icon';
|
import { ActionIcon, ActionIconProps } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { Box } from '/@/shared/components/box/box';
|
import { Box } from '/@/shared/components/box/box';
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
import { TextInput, TextInputProps } from '/@/shared/components/text-input/text-input';
|
import { TextInput, TextInputProps } from '/@/shared/components/text-input/text-input';
|
||||||
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
|
||||||
|
|
||||||
interface SearchInputProps extends TextInputProps {
|
interface SearchInputProps extends TextInputProps {
|
||||||
buttonProps?: Partial<ActionIconProps>;
|
buttonProps?: Partial<ActionIconProps>;
|
||||||
|
|||||||
@@ -18,12 +18,7 @@ export const ActionBar = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Grid
|
<Grid display="flex" gap="sm" style={{ padding: '0 var(--theme-spacing-md)' }} w="100%">
|
||||||
display="flex"
|
|
||||||
gutter="sm"
|
|
||||||
style={{ padding: '0 var(--theme-spacing-md)' }}
|
|
||||||
w="100%"
|
|
||||||
>
|
|
||||||
<Grid.Col span={7}>
|
<Grid.Col span={7}>
|
||||||
<TextInput
|
<TextInput
|
||||||
leftSection={<Icon icon="search" />}
|
leftSection={<Icon icon="search" />}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import styles from './sidebar-item.module.css';
|
|||||||
|
|
||||||
import { Button, ButtonProps } from '/@/shared/components/button/button';
|
import { Button, ButtonProps } from '/@/shared/components/button/button';
|
||||||
|
|
||||||
interface SidebarItemProps extends ButtonProps {
|
interface SidebarItemProps extends Omit<ButtonProps, 'component' | 'ref'> {
|
||||||
to: LinkProps['to'];
|
to: LinkProps['to'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,7 @@
|
|||||||
.image-container {
|
.image-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: var(--sidebar-image-height);
|
min-height: 0;
|
||||||
height: var(--sidebar-image-height);
|
|
||||||
padding: var(--theme-spacing-md);
|
padding: var(--theme-spacing-md);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
animation: fade-in 0.2s ease-in-out;
|
animation: fade-in 0.2s ease-in-out;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { AnimatePresence, motion } from 'motion/react';
|
import { AnimatePresence, motion } from 'motion/react';
|
||||||
import { CSSProperties, MouseEvent, useMemo } from 'react';
|
import { MouseEvent, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import styles from './sidebar.module.css';
|
import styles from './sidebar.module.css';
|
||||||
@@ -159,7 +159,6 @@ export const Sidebar = () => {
|
|||||||
|
|
||||||
const SidebarImage = () => {
|
const SidebarImage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const leftWidth = useAppStore((state) => state.sidebar.leftWidth);
|
|
||||||
const { setSideBar } = useAppStoreActions();
|
const { setSideBar } = useAppStoreActions();
|
||||||
const currentSong = usePlayerSong();
|
const currentSong = usePlayerSong();
|
||||||
const isRadioActive = useIsRadioActive();
|
const isRadioActive = useIsRadioActive();
|
||||||
@@ -216,11 +215,7 @@ const SidebarImage = () => {
|
|||||||
onClick={expandFullScreenPlayer}
|
onClick={expandFullScreenPlayer}
|
||||||
onContextMenu={handleToggleContextMenu}
|
onContextMenu={handleToggleContextMenu}
|
||||||
role="button"
|
role="button"
|
||||||
style={
|
style={{ aspectRatio: 1 }}
|
||||||
{
|
|
||||||
'--sidebar-image-height': leftWidth,
|
|
||||||
} as CSSProperties
|
|
||||||
}
|
|
||||||
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||||||
>
|
>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { useIsLocalVisualizerSurfaceVisible } from '/@/renderer/features/player/hooks/use-is-local-visualizer-surface-visible';
|
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 { useVisualizerSystemAudio } from '/@/renderer/features/player/hooks/use-visualizer-system-audio';
|
||||||
import { closeLocalVisualizerSurfaces } from '/@/renderer/features/player/utils/close-local-visualizer-surfaces';
|
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 { Button } from '/@/shared/components/button/button';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Modal } from '/@/shared/components/modal/modal';
|
import { Modal } from '/@/shared/components/modal/modal';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
import { useDisclosure } from '/@/shared/hooks/use-disclosure';
|
import { useDisclosure } from '/@/shared/hooks/use-disclosure';
|
||||||
import { PlayerType } from '/@/shared/types/types';
|
import { PlayerType } from '/@/shared/types/types';
|
||||||
|
|
||||||
@@ -31,12 +32,21 @@ export function VisualizerSystemAudioBridgeHook() {
|
|||||||
function VisualizerSystemAudioBridge() {
|
function VisualizerSystemAudioBridge() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const playbackType = usePlaybackType();
|
const playbackType = usePlaybackType();
|
||||||
|
const { audioExclusiveMode } = useMpvSettings();
|
||||||
const isVisualizerSurfaceVisible = useIsLocalVisualizerSurfaceVisible();
|
const isVisualizerSurfaceVisible = useIsLocalVisualizerSurfaceVisible();
|
||||||
const [promptState, setPromptState] = useState<PromptState>('loading');
|
const [promptState, setPromptState] = useState<PromptState>('loading');
|
||||||
const [sessionAllowCapture, setSessionAllowCapture] = useState(false);
|
const [sessionAllowCapture, setSessionAllowCapture] = useState(false);
|
||||||
|
const wasBlockedByExclusiveModeRef = useRef(false);
|
||||||
const [isPromptOpen, { close: closePrompt, open: openPrompt, toggle: togglePrompt }] =
|
const [isPromptOpen, { close: closePrompt, open: openPrompt, toggle: togglePrompt }] =
|
||||||
useDisclosure(false);
|
useDisclosure(false);
|
||||||
|
|
||||||
|
const isExclusiveModeEnabled = audioExclusiveMode === 'yes';
|
||||||
|
const isVisualizerBlockedByExclusiveMode =
|
||||||
|
isElectron() &&
|
||||||
|
playbackType === PlayerType.LOCAL &&
|
||||||
|
isVisualizerSurfaceVisible &&
|
||||||
|
isExclusiveModeEnabled;
|
||||||
|
|
||||||
const persistConsent = useCallback((granted: boolean) => {
|
const persistConsent = useCallback((granted: boolean) => {
|
||||||
if (!isElectron() || !window.api.localSettings) {
|
if (!isElectron() || !window.api.localSettings) {
|
||||||
return;
|
return;
|
||||||
@@ -67,6 +77,7 @@ function VisualizerSystemAudioBridge() {
|
|||||||
const eligibleForPrompt =
|
const eligibleForPrompt =
|
||||||
isElectron() &&
|
isElectron() &&
|
||||||
playbackType === PlayerType.LOCAL &&
|
playbackType === PlayerType.LOCAL &&
|
||||||
|
!isExclusiveModeEnabled &&
|
||||||
isVisualizerSurfaceVisible &&
|
isVisualizerSurfaceVisible &&
|
||||||
promptState !== 'loading' &&
|
promptState !== 'loading' &&
|
||||||
!promptState.consent &&
|
!promptState.consent &&
|
||||||
@@ -80,9 +91,25 @@ function VisualizerSystemAudioBridge() {
|
|||||||
}
|
}
|
||||||
}, [eligibleForPrompt, closePrompt, openPrompt]);
|
}, [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 =
|
const shouldAttemptConnection =
|
||||||
isElectron() &&
|
isElectron() &&
|
||||||
playbackType === PlayerType.LOCAL &&
|
playbackType === PlayerType.LOCAL &&
|
||||||
|
!isExclusiveModeEnabled &&
|
||||||
isVisualizerSurfaceVisible &&
|
isVisualizerSurfaceVisible &&
|
||||||
promptState !== 'loading' &&
|
promptState !== 'loading' &&
|
||||||
(promptState.consent || sessionAllowCapture);
|
(promptState.consent || sessionAllowCapture);
|
||||||
|
|||||||
@@ -3,5 +3,6 @@ export * from './use-check-for-updates';
|
|||||||
export * from './use-container-query';
|
export * from './use-container-query';
|
||||||
export * from './use-fast-average-color';
|
export * from './use-fast-average-color';
|
||||||
export * from './use-hide-scrollbar';
|
export * from './use-hide-scrollbar';
|
||||||
|
export * from './use-hotkeys';
|
||||||
export * from './use-is-mounted';
|
export * from './use-is-mounted';
|
||||||
export * from './use-should-pad-titlebar';
|
export * from './use-should-pad-titlebar';
|
||||||
|
|||||||
@@ -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 clsx from 'clsx';
|
||||||
import { AnimatePresence } from 'motion/react';
|
import { AnimatePresence } from 'motion/react';
|
||||||
import { lazy, Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
import { Outlet } from 'react-router';
|
import { Outlet } from 'react-router';
|
||||||
|
|
||||||
import styles from './mobile-layout.module.css';
|
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 { MobileFullscreenPlayer } from '/@/renderer/features/player/components/mobile-fullscreen-player';
|
||||||
import { MobileSidebar } from '/@/renderer/features/sidebar/components/mobile-sidebar';
|
import { MobileSidebar } from '/@/renderer/features/sidebar/components/mobile-sidebar';
|
||||||
import { PlayerBar } from '/@/renderer/layouts/default-layout/player-bar';
|
import { PlayerBar } from '/@/renderer/layouts/default-layout/player-bar';
|
||||||
|
import { WindowBar } from '/@/renderer/layouts/window-bar';
|
||||||
import { useFullScreenPlayerOverlayState, useWindowBarStyle } from '/@/renderer/store';
|
import { useFullScreenPlayerOverlayState, useWindowBarStyle } from '/@/renderer/store';
|
||||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { Drawer } from '/@/shared/components/drawer/drawer';
|
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 { useDisclosure } from '/@/shared/hooks/use-disclosure';
|
||||||
import { Platform } from '/@/shared/types/types';
|
import { Platform } from '/@/shared/types/types';
|
||||||
|
|
||||||
const WindowBar = lazy(() =>
|
|
||||||
import('/@/renderer/layouts/window-bar').then((module) => ({
|
|
||||||
default: module.WindowBar,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
interface MobileLayoutProps {
|
interface MobileLayoutProps {
|
||||||
shell?: boolean;
|
shell?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useNavigate } from 'react-router';
|
|||||||
import { useAppTracker } from '/@/renderer/features/analytics/hooks/use-app-tracker';
|
import { useAppTracker } from '/@/renderer/features/analytics/hooks/use-app-tracker';
|
||||||
import { CommandPalette } from '/@/renderer/features/search/components/command-palette';
|
import { CommandPalette } from '/@/renderer/features/search/components/command-palette';
|
||||||
import { useGarbageCollection } from '/@/renderer/hooks/use-garbage-collection';
|
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 { useIsMobile } from '/@/renderer/hooks/use-is-mobile';
|
||||||
import { DefaultLayout } from '/@/renderer/layouts/default-layout';
|
import { DefaultLayout } from '/@/renderer/layouts/default-layout';
|
||||||
import { MobileLayout } from '/@/renderer/layouts/mobile-layout/mobile-layout';
|
import { MobileLayout } from '/@/renderer/layouts/mobile-layout/mobile-layout';
|
||||||
@@ -15,7 +16,6 @@ import {
|
|||||||
useSettingsStoreActions,
|
useSettingsStoreActions,
|
||||||
useZoomFactor,
|
useZoomFactor,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { HotkeyItem, useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
|
||||||
|
|
||||||
interface ResponsiveLayoutProps {
|
interface ResponsiveLayoutProps {
|
||||||
shell?: boolean;
|
shell?: boolean;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { lazy, Suspense } from 'react';
|
import { lazy, Suspense } from 'react';
|
||||||
import { HashRouter, Route, Routes } from 'react-router';
|
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 { RouterErrorBoundary } from '/@/renderer/features/shared/components/router-error-boundary';
|
||||||
import { AuthenticationOutlet } from '/@/renderer/layouts/authentication-outlet';
|
import { AuthenticationOutlet } from '/@/renderer/layouts/authentication-outlet';
|
||||||
import { ResponsiveLayout } from '/@/renderer/layouts/responsive-layout';
|
import { ResponsiveLayout } from '/@/renderer/layouts/responsive-layout';
|
||||||
@@ -96,18 +97,6 @@ const LyricsSettingsContextModal = (props: any) => (
|
|||||||
</Suspense>
|
</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(() =>
|
const LazyAddToPlaylistContextModal = lazy(() =>
|
||||||
import('/@/renderer/features/playlists/components/add-to-playlist-context-modal').then(
|
import('/@/renderer/features/playlists/components/add-to-playlist-context-modal').then(
|
||||||
(module) => ({
|
(module) => ({
|
||||||
@@ -200,7 +189,7 @@ const appRouterModals = {
|
|||||||
|
|
||||||
export const AppRouter = () => {
|
export const AppRouter = () => {
|
||||||
const router = (
|
const router = (
|
||||||
<HashRouter unstable_useTransitions>
|
<HashRouter unstable_useTransitions={false}>
|
||||||
<ModalsProvider modals={appRouterModals}>
|
<ModalsProvider modals={appRouterModals}>
|
||||||
<RouterErrorBoundary>
|
<RouterErrorBoundary>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ const APP_THEMES = new Set([
|
|||||||
'defaultDark',
|
'defaultDark',
|
||||||
'defaultLight',
|
'defaultLight',
|
||||||
'dracula',
|
'dracula',
|
||||||
|
'everforestDark',
|
||||||
|
'everforestLight',
|
||||||
'githubDark',
|
'githubDark',
|
||||||
'githubLight',
|
'githubLight',
|
||||||
'glassyDark',
|
'glassyDark',
|
||||||
|
|||||||
@@ -776,7 +776,7 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
|||||||
|
|
||||||
// For nextSong calculation, we need to consider the shuffled order
|
// For nextSong calculation, we need to consider the shuffled order
|
||||||
let nextSong: QueueSong | undefined;
|
let nextSong: QueueSong | undefined;
|
||||||
if (isShuffleEnabled(state)) {
|
if (isShuffleEnabled(state) && repeat !== PlayerRepeat.ONE) {
|
||||||
// Calculate next in shuffled order
|
// Calculate next in shuffled order
|
||||||
const nextShuffledIndex = index + 1;
|
const nextShuffledIndex = index + 1;
|
||||||
if (nextShuffledIndex < state.queue.shuffled.length) {
|
if (nextShuffledIndex < state.queue.shuffled.length) {
|
||||||
@@ -925,7 +925,7 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
|||||||
const currentSong = queue.items[currentQueueIndex];
|
const currentSong = queue.items[currentQueueIndex];
|
||||||
|
|
||||||
let nextSong: QueueSong | undefined;
|
let nextSong: QueueSong | undefined;
|
||||||
if (isShuffle) {
|
if (isShuffle && repeat !== PlayerRepeat.ONE) {
|
||||||
const nextShuffledIndex = nextPlaybackIndex + 1;
|
const nextShuffledIndex = nextPlaybackIndex + 1;
|
||||||
if (nextShuffledIndex < stateSnapshot.queue.shuffled.length) {
|
if (nextShuffledIndex < stateSnapshot.queue.shuffled.length) {
|
||||||
const nextQueueIndex = stateSnapshot.queue.shuffled[nextShuffledIndex];
|
const nextQueueIndex = stateSnapshot.queue.shuffled[nextShuffledIndex];
|
||||||
@@ -933,8 +933,6 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
|||||||
} else if (repeat === PlayerRepeat.ALL) {
|
} else if (repeat === PlayerRepeat.ALL) {
|
||||||
const firstQueueIndex = stateSnapshot.queue.shuffled[0];
|
const firstQueueIndex = stateSnapshot.queue.shuffled[0];
|
||||||
nextSong = queue.items[firstQueueIndex];
|
nextSong = queue.items[firstQueueIndex];
|
||||||
} else if (repeat === PlayerRepeat.ONE) {
|
|
||||||
nextSong = currentSong;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
nextSong = calculateNextSong(currentQueueIndex, queue.items, repeat);
|
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
|
// Calculate next song based on shuffle and repeat settings
|
||||||
let nextSong: QueueSong | undefined;
|
let nextSong: QueueSong | undefined;
|
||||||
if (isShuffleEnabled(state)) {
|
if (isShuffleEnabled(state) && repeat !== PlayerRepeat.ONE) {
|
||||||
// Calculate next in shuffled order
|
// Calculate next in shuffled order
|
||||||
const nextShuffledIndex = state.player.index + 1;
|
const nextShuffledIndex = state.player.index + 1;
|
||||||
if (nextShuffledIndex < state.queue.shuffled.length) {
|
if (nextShuffledIndex < state.queue.shuffled.length) {
|
||||||
@@ -1940,7 +1938,7 @@ export const usePlayerData = (): PlayerData => {
|
|||||||
|
|
||||||
// For nextSong calculation, we need to consider the shuffled order
|
// For nextSong calculation, we need to consider the shuffled order
|
||||||
let nextSong: QueueSong | undefined;
|
let nextSong: QueueSong | undefined;
|
||||||
if (isShuffleEnabled(state)) {
|
if (isShuffleEnabled(state) && repeat !== PlayerRepeat.ONE) {
|
||||||
// Calculate next in shuffled order
|
// Calculate next in shuffled order
|
||||||
const nextShuffledIndex = index + 1;
|
const nextShuffledIndex = index + 1;
|
||||||
if (nextShuffledIndex < state.queue.shuffled.length) {
|
if (nextShuffledIndex < state.queue.shuffled.length) {
|
||||||
|
|||||||
@@ -306,6 +306,7 @@ const PlayerbarSliderSchema = z.object({
|
|||||||
barRadius: z.number(),
|
barRadius: z.number(),
|
||||||
barWidth: z.number(),
|
barWidth: z.number(),
|
||||||
loadingDelay: z.number(),
|
loadingDelay: z.number(),
|
||||||
|
stretched: z.boolean(),
|
||||||
type: PlayerbarSliderTypeSchema,
|
type: PlayerbarSliderTypeSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -536,6 +537,8 @@ const LyricsDisplaySettingsSchema = z.object({
|
|||||||
fontSizeUnsync: z.number(),
|
fontSizeUnsync: z.number(),
|
||||||
gap: z.number(),
|
gap: z.number(),
|
||||||
gapUnsync: z.number(),
|
gapUnsync: z.number(),
|
||||||
|
opacityNonActive: z.number(),
|
||||||
|
scaleNonActive: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const LyricsSettingsSchema = z.object({
|
const LyricsSettingsSchema = z.object({
|
||||||
@@ -635,6 +638,7 @@ const WindowSettingsSchema = z.object({
|
|||||||
exitToTray: z.boolean(),
|
exitToTray: z.boolean(),
|
||||||
minimizeToTray: z.boolean(),
|
minimizeToTray: z.boolean(),
|
||||||
preventSleepOnPlayback: z.boolean(),
|
preventSleepOnPlayback: z.boolean(),
|
||||||
|
preventSuspendOnPlayback: z.boolean(),
|
||||||
releaseChannel: z.enum(['alpha', 'beta', 'latest']),
|
releaseChannel: z.enum(['alpha', 'beta', 'latest']),
|
||||||
startMinimized: z.boolean(),
|
startMinimized: z.boolean(),
|
||||||
tray: z.boolean(),
|
tray: z.boolean(),
|
||||||
@@ -1150,6 +1154,7 @@ const initialState: SettingsState = {
|
|||||||
barRadius: 4,
|
barRadius: 4,
|
||||||
barWidth: 2,
|
barWidth: 2,
|
||||||
loadingDelay: 2,
|
loadingDelay: 2,
|
||||||
|
stretched: false,
|
||||||
type: PlayerbarSliderType.SLIDER,
|
type: PlayerbarSliderType.SLIDER,
|
||||||
},
|
},
|
||||||
playerItems,
|
playerItems,
|
||||||
@@ -1794,6 +1799,8 @@ const initialState: SettingsState = {
|
|||||||
fontSizeUnsync: 24,
|
fontSizeUnsync: 24,
|
||||||
gap: 24,
|
gap: 24,
|
||||||
gapUnsync: 24,
|
gapUnsync: 24,
|
||||||
|
opacityNonActive: 0.2,
|
||||||
|
scaleNonActive: 0.95,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
playback: {
|
playback: {
|
||||||
@@ -1908,6 +1915,7 @@ const initialState: SettingsState = {
|
|||||||
exitToTray: false,
|
exitToTray: false,
|
||||||
minimizeToTray: false,
|
minimizeToTray: false,
|
||||||
preventSleepOnPlayback: false,
|
preventSleepOnPlayback: false,
|
||||||
|
preventSuspendOnPlayback: false,
|
||||||
releaseChannel: 'latest',
|
releaseChannel: 'latest',
|
||||||
startMinimized: false,
|
startMinimized: false,
|
||||||
tray: true,
|
tray: true,
|
||||||
@@ -2211,7 +2219,10 @@ export const useSettingsStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
|
|
||||||
state.lyrics = mainSettings;
|
state.lyrics = mainSettings;
|
||||||
state.lyricsDisplay = {
|
state.lyricsDisplay = {
|
||||||
default: displaySettings,
|
default: {
|
||||||
|
...state.lyricsDisplay.default,
|
||||||
|
...displaySettings,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ export const THEME_DATA = [
|
|||||||
{ label: 'One Dark', type: 'dark', value: AppTheme.ONE_DARK },
|
{ label: 'One Dark', type: 'dark', value: AppTheme.ONE_DARK },
|
||||||
{ label: 'Solarized Dark', type: 'dark', value: AppTheme.SOLARIZED_DARK },
|
{ label: 'Solarized Dark', type: 'dark', value: AppTheme.SOLARIZED_DARK },
|
||||||
{ label: 'Solarized Light', type: 'light', value: AppTheme.SOLARIZED_LIGHT },
|
{ 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 Dark', type: 'dark', value: AppTheme.GITHUB_DARK },
|
||||||
{ label: 'GitHub Light', type: 'light', value: AppTheme.GITHUB_LIGHT },
|
{ label: 'GitHub Light', type: 'light', value: AppTheme.GITHUB_LIGHT },
|
||||||
{ label: 'Glassy Dark', type: 'dark', value: AppTheme.GLASSY_DARK },
|
{ label: 'Glassy Dark', type: 'dark', value: AppTheme.GLASSY_DARK },
|
||||||
|
|||||||
@@ -278,6 +278,8 @@ const normalizeSong = (
|
|||||||
const normalizeAlbum = (
|
const normalizeAlbum = (
|
||||||
item: z.infer<typeof jfType._response.album>,
|
item: z.infer<typeof jfType._response.album>,
|
||||||
server: null | ServerListItem,
|
server: null | ServerListItem,
|
||||||
|
pathReplace?: string,
|
||||||
|
pathReplaceWith?: string,
|
||||||
): Album => {
|
): Album => {
|
||||||
const { originalYear, releaseDate, releaseYear } = jellyfinPremiereFields(item);
|
const { originalYear, releaseDate, releaseYear } = jellyfinPremiereFields(item);
|
||||||
|
|
||||||
@@ -340,7 +342,7 @@ const normalizeAlbum = (
|
|||||||
releaseYear,
|
releaseYear,
|
||||||
size: null,
|
size: null,
|
||||||
songCount: item?.ChildCount || 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,
|
sortName: item.SortName || item.Name,
|
||||||
tags: getTags(item),
|
tags: getTags(item),
|
||||||
updatedAt: item?.DateLastMediaAdded || item.DateCreated,
|
updatedAt: item?.DateLastMediaAdded || item.DateCreated,
|
||||||
|
|||||||
@@ -18,14 +18,20 @@ import {
|
|||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
import { ServerListItem, ServerType } from '/@/shared/types/types';
|
import { ServerListItem, ServerType } from '/@/shared/types/types';
|
||||||
|
|
||||||
const getImageUrl = (args: { url: null | string }) => {
|
// const getImageUrl = (args: { url: null | string }) => {
|
||||||
const { url } = args;
|
// const { url } = args;
|
||||||
if (url === '/app/artist-placeholder.webp') {
|
// if (url === '/app/artist-placeholder.webp') {
|
||||||
return null;
|
// return null;
|
||||||
}
|
// }
|
||||||
|
|
||||||
return url;
|
// return url;
|
||||||
};
|
// };
|
||||||
|
|
||||||
|
const navidromeImageIdWithCacheBust = (
|
||||||
|
id: string,
|
||||||
|
uploadedImage: string | undefined,
|
||||||
|
updatedAt: string | undefined,
|
||||||
|
): string => (!uploadedImage ? id : `${id}&_=${updatedAt ?? ''}`);
|
||||||
|
|
||||||
interface WithDate {
|
interface WithDate {
|
||||||
playDate?: string;
|
playDate?: string;
|
||||||
@@ -397,7 +403,7 @@ const normalizeAlbumArtist = (
|
|||||||
},
|
},
|
||||||
server?: null | ServerListItem,
|
server?: null | ServerListItem,
|
||||||
): AlbumArtist => {
|
): AlbumArtist => {
|
||||||
const imageUrl = getImageUrl({ url: item?.largeImageUrl?.replace(/\?size=\d+/, '') || null });
|
// const imageUrl = getImageUrl({ url: item?.largeImageUrl?.replace(/\?size=\d+/, '') || null });
|
||||||
|
|
||||||
let albumCount: number;
|
let albumCount: number;
|
||||||
let songCount: number;
|
let songCount: number;
|
||||||
@@ -416,6 +422,12 @@ const normalizeAlbumArtist = (
|
|||||||
songCount = item.songCount;
|
songCount = item.songCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const imageId = navidromeImageIdWithCacheBust(
|
||||||
|
item.id,
|
||||||
|
item.uploadedImage,
|
||||||
|
item.updatedAt ?? item.externalInfoUpdatedAt,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_itemType: LibraryItem.ALBUM_ARTIST,
|
_itemType: LibraryItem.ALBUM_ARTIST,
|
||||||
_serverId: server?.id || 'unknown',
|
_serverId: server?.id || 'unknown',
|
||||||
@@ -435,8 +447,8 @@ const normalizeAlbumArtist = (
|
|||||||
songCount: null,
|
songCount: null,
|
||||||
})),
|
})),
|
||||||
id: item.id,
|
id: item.id,
|
||||||
imageId: item.id,
|
imageId,
|
||||||
imageUrl: imageUrl || null,
|
imageUrl: null,
|
||||||
lastPlayedAt: normalizePlayDate(item),
|
lastPlayedAt: normalizePlayDate(item),
|
||||||
mbz: item.mbzArtistId || null,
|
mbz: item.mbzArtistId || null,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
@@ -451,6 +463,7 @@ const normalizeAlbumArtist = (
|
|||||||
userRating: artist.userRating || null,
|
userRating: artist.userRating || null,
|
||||||
})) || [],
|
})) || [],
|
||||||
songCount,
|
songCount,
|
||||||
|
uploadedImage: item.uploadedImage,
|
||||||
userFavorite: item.starred || false,
|
userFavorite: item.starred || false,
|
||||||
userRating: item.rating || null,
|
userRating: item.rating || null,
|
||||||
};
|
};
|
||||||
@@ -460,7 +473,7 @@ const normalizePlaylist = (
|
|||||||
item: z.infer<typeof ndType._response.playlist>,
|
item: z.infer<typeof ndType._response.playlist>,
|
||||||
server?: null | ServerListItem,
|
server?: null | ServerListItem,
|
||||||
): Playlist => {
|
): Playlist => {
|
||||||
const imageId = !item.uploadedImage ? item.id : `${item.id}&_=${item.updatedAt}`;
|
const imageId = navidromeImageIdWithCacheBust(item.id, item.uploadedImage, item.updatedAt);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_itemType: LibraryItem.PLAYLIST,
|
_itemType: LibraryItem.PLAYLIST,
|
||||||
@@ -517,7 +530,7 @@ const normalizeInternetRadioStation = (
|
|||||||
item: z.infer<typeof ndType._response.radioStation>,
|
item: z.infer<typeof ndType._response.radioStation>,
|
||||||
): InternetRadioStation => {
|
): InternetRadioStation => {
|
||||||
const homepageUrl = item.homePageUrl?.trim() ? item.homePageUrl : null;
|
const homepageUrl = item.homePageUrl?.trim() ? item.homePageUrl : null;
|
||||||
const imageId = item.uploadedImage ? `${item.id}&_=${item.updatedAt}` : item.id;
|
const imageId = navidromeImageIdWithCacheBust(item.id, item.uploadedImage, item.updatedAt);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
homepageUrl,
|
homepageUrl,
|
||||||
|
|||||||
@@ -428,6 +428,7 @@ const albumArtist = z.object({
|
|||||||
starredAt: z.string(),
|
starredAt: z.string(),
|
||||||
stats: z.record(z.string(), stats).optional(),
|
stats: z.record(z.string(), stats).optional(),
|
||||||
updatedAt: z.string().optional(),
|
updatedAt: z.string().optional(),
|
||||||
|
uploadedImage: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const albumArtistList = z.array(albumArtist);
|
const albumArtistList = z.array(albumArtist);
|
||||||
@@ -683,6 +684,9 @@ const deletePlaylistImage = z.object({
|
|||||||
|
|
||||||
const uploadInternetRadioStationImage = uploadPlaylistImage;
|
const uploadInternetRadioStationImage = uploadPlaylistImage;
|
||||||
const uploadInternetRadioStationImageParameters = uploadPlaylistImageParameters;
|
const uploadInternetRadioStationImageParameters = uploadPlaylistImageParameters;
|
||||||
|
const uploadArtistImage = uploadPlaylistImage;
|
||||||
|
const uploadArtistImageParameters = uploadPlaylistImageParameters;
|
||||||
|
const deleteArtistImage = deletePlaylistImage;
|
||||||
const deleteInternetRadioStationImage = deletePlaylistImage;
|
const deleteInternetRadioStationImage = deletePlaylistImage;
|
||||||
|
|
||||||
const deletePlaylist = z.null();
|
const deletePlaylist = z.null();
|
||||||
@@ -813,6 +817,7 @@ export const ndType = {
|
|||||||
tagList: tagListParameters,
|
tagList: tagListParameters,
|
||||||
updateInternetRadioStation: updateInternetRadioStationParameters,
|
updateInternetRadioStation: updateInternetRadioStationParameters,
|
||||||
updatePlaylist: updatePlaylistParameters,
|
updatePlaylist: updatePlaylistParameters,
|
||||||
|
uploadArtistImage: uploadArtistImageParameters,
|
||||||
uploadInternetRadioStationImage: uploadInternetRadioStationImageParameters,
|
uploadInternetRadioStationImage: uploadInternetRadioStationImageParameters,
|
||||||
uploadPlaylistImage: uploadPlaylistImageParameters,
|
uploadPlaylistImage: uploadPlaylistImageParameters,
|
||||||
userList: userListParameters,
|
userList: userListParameters,
|
||||||
@@ -825,6 +830,7 @@ export const ndType = {
|
|||||||
albumList,
|
albumList,
|
||||||
authenticate,
|
authenticate,
|
||||||
createPlaylist,
|
createPlaylist,
|
||||||
|
deleteArtistImage,
|
||||||
deleteInternetRadioStation,
|
deleteInternetRadioStation,
|
||||||
deleteInternetRadioStationImage,
|
deleteInternetRadioStationImage,
|
||||||
deletePlaylist,
|
deletePlaylist,
|
||||||
@@ -848,6 +854,7 @@ export const ndType = {
|
|||||||
tagList,
|
tagList,
|
||||||
updateInternetRadioStation,
|
updateInternetRadioStation,
|
||||||
updatePlaylist,
|
updatePlaylist,
|
||||||
|
uploadArtistImage,
|
||||||
uploadInternetRadioStationImage,
|
uploadInternetRadioStationImage,
|
||||||
uploadPlaylistImage,
|
uploadPlaylistImage,
|
||||||
user,
|
user,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* Inset outline on the root is hidden behind a full-bleed ItemImage; a ::after layer paints
|
||||||
|
* above the image. Keep z-index below overlay controls (e.g. z-index: 2).
|
||||||
|
* Avoid positive outline-offset so ancestors with overflow:hidden do not clip the indicator.
|
||||||
|
*/
|
||||||
|
.file-target-drag-over {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-target-drag-over::after {
|
||||||
|
position: absolute;
|
||||||
|
inset: calc(var(--theme-spacing-sm) * -1);
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
content: '';
|
||||||
|
border-radius: var(--theme-radius-md);
|
||||||
|
box-shadow: inset 0 0 0 3px var(--theme-colors-primary);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user