mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-06 20:10:12 +02:00
Compare commits
178 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5bab80b89b | |||
| 066e5188f2 | |||
| 456f4d7f65 | |||
| 02f5a1bd94 | |||
| 4424e9ae33 | |||
| 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 | |||
| ad13fea033 | |||
| 8a75ec2558 | |||
| 895cbb4d16 | |||
| 3f300c40cc | |||
| c8e8f58cce | |||
| 56cd50e0ed | |||
| 1b2a6dfc1f | |||
| 356f5487b0 | |||
| 37501f2983 | |||
| d61587b16f | |||
| 06b7b53dc9 | |||
| 6c2cd1c274 | |||
| ef129e4638 | |||
| a01b4e664d | |||
| 0b45ab7f36 | |||
| 031d365262 | |||
| 4fd56281d5 | |||
| 08ce8a4028 | |||
| e06877af76 | |||
| 84395ce5b4 | |||
| 94886a2d5a | |||
| 25bb7f7069 | |||
| 573fe5ee35 | |||
| a868d4d539 | |||
| 564ee721c4 | |||
| a8d990db23 | |||
| e21515f7fb | |||
| 3e5a8ac78d | |||
| 6c73d06dcf | |||
| a8954bfa2a | |||
| 19a1617a8d | |||
| 1abae986f8 | |||
| 43fa574dab | |||
| 99530c670e | |||
| 3a0dfe59ce | |||
| d60ed0a793 | |||
| a32fed3bcf | |||
| 132ac92984 | |||
| 141a20f042 | |||
| 1592204515 | |||
| b9f5459725 | |||
| d4e9b9b7a6 | |||
| ec9e4b1339 | |||
| f09109b887 | |||
| 1494c8e044 | |||
| f3a6027e6d | |||
| 3c42355c1e | |||
| feda1bb06f | |||
| 72f1d2f9f9 | |||
| ad11a9303c | |||
| db06e7f601 | |||
| fbf82c1ef0 | |||
| 92cea5dfda | |||
| 7442f9d3ca | |||
| 68dacea228 | |||
| 51425b5e86 | |||
| c60610cb42 | |||
| d3881ee3be | |||
| de403ea6ac | |||
| a30b1ec90b | |||
| 7982c0e1bd | |||
| baf4e7bc0b | |||
| 74c44558fe | |||
| 4033619421 | |||
| 5d206bbb1f | |||
| 3db801f2de | |||
| 0d3cf912d3 | |||
| d81f30a8b5 | |||
| a5c3b454f4 | |||
| 68e6e3cf65 | |||
| 86e6b88555 | |||
| 5cdc45836f | |||
| d438c802a4 | |||
| a838bdebb7 | |||
| 8ff2f4dfb4 | |||
| ede47fbf8f | |||
| 9eb64079f7 | |||
| 3b955bb319 | |||
| 816adfa6c7 | |||
| f91dcc6af6 | |||
| 6dc58a3ff8 | |||
| 09fa10a4e9 | |||
| 6f45e1a814 | |||
| 62ba721f26 | |||
| 67231753e4 | |||
| c16eccaecb | |||
| 0bdf1dcb75 | |||
| 598e9ca5c2 | |||
| 615f9c3515 | |||
| b7cbdb4d6c | |||
| 3c562c1398 | |||
| 3eafa73217 | |||
| 74864d9621 | |||
| cb5562d32e | |||
| e40a175e12 | |||
| f996b111b9 | |||
| 0cb5c49924 | |||
| c636029003 | |||
| db88a6bc22 | |||
| 8ccd97b574 | |||
| 3f99acf473 | |||
| 0cd37ce8ec | |||
| ee04878580 | |||
| e987049f20 | |||
| 122552287a | |||
| d318e6d341 | |||
| d96b282cae | |||
| f2ab01199f | |||
| 04b22431f4 | |||
| 31fce705ab | |||
| a28fab0ff3 | |||
| 0a1d4788ee | |||
| fafb9d4f56 | |||
| 4fdc38caee | |||
| 799cdb44d3 | |||
| 372892199f | |||
| d16184fb25 | |||
| b8564f6d41 | |||
| d474e60c51 | |||
| dfdac28f53 | |||
| 16b713bc85 | |||
| 81cd0722b1 | |||
| 1526f9b8d6 | |||
| 5b4da3bc29 | |||
| d78ea440cc | |||
| 1595805b83 | |||
| 00fa45f15d | |||
| ab05be30c0 | |||
| 3d407e5f24 | |||
| 60776b5f02 | |||
| 8699b1ffea | |||
| 16ac536f93 | |||
| f51d3d5711 | |||
| 17a4a14a4e |
@@ -40,12 +40,12 @@ jobs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node and PNPM
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
@@ -121,16 +121,16 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, macos-latest, ubuntu-latest]
|
||||
os: [windows-latest, macos-26, ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node and PNPM
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
@@ -146,7 +146,7 @@ jobs:
|
||||
|
||||
- name: Build and Publish to R2 (Windows)
|
||||
if: matrix.os == 'windows-latest'
|
||||
uses: nick-invision/retry@v2.8.2
|
||||
uses: nick-invision/retry@v3.0.2
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
@@ -156,8 +156,8 @@ jobs:
|
||||
on_retry_command: pnpm cache delete
|
||||
|
||||
- name: Build and Publish to R2 (macOS)
|
||||
if: matrix.os == 'macos-latest'
|
||||
uses: nick-invision/retry@v2.8.2
|
||||
if: matrix.os == 'macos-26'
|
||||
uses: nick-invision/retry@v3.0.2
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
@@ -168,7 +168,7 @@ jobs:
|
||||
|
||||
- name: Build and Publish to R2 (Linux)
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
uses: nick-invision/retry@v2.8.2
|
||||
uses: nick-invision/retry@v3.0.2
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
@@ -179,7 +179,7 @@ jobs:
|
||||
|
||||
- name: Build and Publish to R2 (Linux ARM64)
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
uses: nick-invision/retry@v2.8.2
|
||||
uses: nick-invision/retry@v3.0.2
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
|
||||
@@ -15,12 +15,12 @@ jobs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node and PNPM
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
@@ -115,16 +115,16 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, macos-latest, ubuntu-latest]
|
||||
os: [windows-latest, macos-26, ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node and PNPM
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
@@ -146,7 +146,7 @@ jobs:
|
||||
if: matrix.os == 'windows-latest'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: nick-invision/retry@v2.8.2
|
||||
uses: nick-invision/retry@v3.0.2
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
@@ -156,10 +156,10 @@ jobs:
|
||||
on_retry_command: pnpm cache delete
|
||||
|
||||
- name: Build and Publish releases (macOS)
|
||||
if: matrix.os == 'macos-latest'
|
||||
if: matrix.os == 'macos-26'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: nick-invision/retry@v2.8.2
|
||||
uses: nick-invision/retry@v3.0.2
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
@@ -172,7 +172,7 @@ jobs:
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: nick-invision/retry@v2.8.2
|
||||
uses: nick-invision/retry@v3.0.2
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
@@ -185,7 +185,7 @@ jobs:
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: nick-invision/retry@v2.8.2
|
||||
uses: nick-invision/retry@v3.0.2
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
@@ -199,7 +199,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Edit release with commits and title
|
||||
shell: pwsh
|
||||
@@ -346,7 +346,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Delete existing prereleases
|
||||
shell: pwsh
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
|
||||
with:
|
||||
@@ -51,5 +51,4 @@ jobs:
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm/v7
|
||||
linux/arm64/v8
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
|
||||
with:
|
||||
|
||||
@@ -12,12 +12,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node and PNPM
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
- name: Build and Publish releases
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: nick-invision/retry@v2.8.2
|
||||
uses: nick-invision/retry@v3.0.2
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Build and Publish releases (arm64)
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: nick-invision/retry@v2.8.2
|
||||
uses: nick-invision/retry@v3.0.2
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
|
||||
@@ -8,24 +8,25 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest]
|
||||
os: [macos-26]
|
||||
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node and PNPM
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build and Publish releases
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: nick-invision/retry@v2.8.2
|
||||
uses: nick-invision/retry@v3.0.2
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
|
||||
@@ -11,6 +11,7 @@ on:
|
||||
|
||||
jobs:
|
||||
wait-for-lint:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Wait for Test workflow to complete
|
||||
@@ -24,27 +25,28 @@ jobs:
|
||||
|
||||
publish:
|
||||
needs: wait-for-lint
|
||||
if: always() && (needs.wait-for-lint.result == 'success' || needs.wait-for-lint.result == 'skipped')
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
os: [macos-26, ubuntu-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node and PNPM
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build for Windows
|
||||
if: ${{ matrix.os == 'windows-latest' }}
|
||||
uses: nick-invision/retry@v2.8.2
|
||||
uses: nick-invision/retry@v3.0.2
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
@@ -54,7 +56,7 @@ jobs:
|
||||
|
||||
- name: Build for Linux
|
||||
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||
uses: nick-invision/retry@v2.8.2
|
||||
uses: nick-invision/retry@v3.0.2
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
@@ -63,8 +65,8 @@ jobs:
|
||||
pnpm run package:linux:pr
|
||||
|
||||
- name: Build for MacOS
|
||||
if: ${{ matrix.os == 'macos-latest' }}
|
||||
uses: nick-invision/retry@v2.8.2
|
||||
if: ${{ matrix.os == 'macos-26' }}
|
||||
uses: nick-invision/retry@v3.0.2
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
@@ -84,27 +86,27 @@ jobs:
|
||||
zip -r dist/linux-binaries.zip dist/*.{AppImage,deb,rpm}
|
||||
|
||||
- name: Zip MacOS Binaries
|
||||
if: ${{ matrix.os == 'macos-latest' }}
|
||||
if: ${{ matrix.os == 'macos-26' }}
|
||||
run: |
|
||||
zip -r dist/macos-binaries.zip dist/*.dmg
|
||||
|
||||
- name: Upload Windows Binaries
|
||||
if: ${{ matrix.os == 'windows-latest' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: windows-binaries
|
||||
path: dist/windows-binaries.zip
|
||||
|
||||
- name: Upload Linux Binaries
|
||||
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: linux-binaries
|
||||
path: dist/linux-binaries.zip
|
||||
|
||||
- name: Upload MacOS Binaries
|
||||
if: ${{ matrix.os == 'macos-latest' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ matrix.os == 'macos-26' }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: macos-binaries
|
||||
path: dist/macos-binaries.zip
|
||||
|
||||
@@ -12,12 +12,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node and PNPM
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
- name: Build and Publish releases
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: nick-invision/retry@v2.8.2
|
||||
uses: nick-invision/retry@v3.0.2
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
|
||||
@@ -8,16 +8,16 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, macos-latest, ubuntu-latest]
|
||||
os: [windows-latest, macos-26, ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node and PNPM
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
if: matrix.os == 'windows-latest'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: nick-invision/retry@v2.8.2
|
||||
uses: nick-invision/retry@v3.0.2
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
@@ -36,10 +36,10 @@ jobs:
|
||||
on_retry_command: pnpm cache delete
|
||||
|
||||
- name: Build and Publish releases (macOS)
|
||||
if: matrix.os == 'macos-latest'
|
||||
if: matrix.os == 'macos-26'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: nick-invision/retry@v2.8.2
|
||||
uses: nick-invision/retry@v3.0.2
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: nick-invision/retry@v2.8.2
|
||||
uses: nick-invision/retry@v3.0.2
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: nick-invision/retry@v2.8.2
|
||||
uses: nick-invision/retry@v3.0.2
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
|
||||
@@ -8,12 +8,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node.js and PNPM
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
# --- Builder stage
|
||||
FROM node:23-alpine as builder
|
||||
FROM node:23-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package.json first to cache node_modules
|
||||
@@ -14,11 +14,11 @@ COPY . .
|
||||
RUN pnpm run build:web
|
||||
|
||||
# --- Production stage
|
||||
FROM nginx:alpine-slim
|
||||
FROM nginxinc/nginx-unprivileged:alpine-slim
|
||||
|
||||
COPY --chown=nginx:nginx --from=builder /app/out/web /usr/share/nginx/html
|
||||
COPY ./settings.js.template /etc/nginx/templates/settings.js.template
|
||||
COPY ng.conf.template /etc/nginx/templates/default.conf.template
|
||||
COPY --chown=nginx:nginx ./settings.js.template /etc/nginx/templates/settings.js.template
|
||||
COPY --chown=nginx:nginx ng.conf.template /etc/nginx/templates/default.conf.template
|
||||
|
||||
ENV SERVER_LOCK=false SERVER_NAME="" SERVER_TYPE="" SERVER_URL="" REMOTE_URL=""
|
||||
ENV LEGACY_AUTHENTICATION="" ANALYTICS_DISABLED="" PUBLIC_PATH="/"
|
||||
|
||||
@@ -59,7 +59,11 @@ For media keys to work, you will be prompted to allow Feishin to be a Trusted Ac
|
||||
|
||||
#### Linux Notes
|
||||
|
||||
We provide a small install script to download the latest `.AppImage`, make it executable, and also download the icons required by Desktop Environments. Finally, it generates a `.desktop` file to add Feishin to your Application Launcher.
|
||||
Feishin is available in [Flathub](https://flathub.org/en/apps/org.jeffvli.feishin).
|
||||
|
||||
Alternatively, you can install it as an Appimage.
|
||||
We provide a small install script to download the latest `.AppImage`, make it executable, and also download the icons required by Desktop Environments.
|
||||
Finally, it generates a `.desktop` file to add Feishin to your Application Launcher.
|
||||
|
||||
Simply run the installer like this:
|
||||
|
||||
@@ -114,7 +118,7 @@ services:
|
||||
- SERVER_LOCK=true # When true AND name/type/url are set, only username/password can be toggled
|
||||
- SERVER_TYPE=jellyfin # the allowed types are: jellyfin, navidrome, subsonic. These values are case insensitive
|
||||
- SERVER_URL= # http://address:port or https://address:port
|
||||
= REMOTE_URL= # http://address or https://address
|
||||
- REMOTE_URL= # http://address or https://address
|
||||
- LEGACY_AUTHENTICATION=false # When SERVER_LOCK is true, sets the legacy (plaintext) authentication flag for Subsonic/OpenSubsonic servers
|
||||
- ANALYTICS_DISABLED=true # Set to true to disable Umami analytics tracking
|
||||
ports:
|
||||
@@ -136,7 +140,7 @@ services:
|
||||
4. _Optional_ - To hard code the server url, pass the following environment variables: `SERVER_NAME`, `SERVER_TYPE` (one of `jellyfin` or `navidrome` or `subsonic`), `SERVER_URL`. To prevent users from changing these settings, pass `SERVER_LOCK=true`. This can only be set if all three of the previous values are set. When `SERVER_LOCK=true`, you can also set `LEGACY_AUTHENTICATION=true` or `LEGACY_AUTHENTICATION=false` to configure the legacy authentication flag for the server (only applicable for Subsonic/OpenSubsonic servers).
|
||||
|
||||
5. _Optional_ - If your server uses a separate public-facing URL than what integrating applications use internally to communicate with your server, such as a separate Navidrome `ShareURL`, set `REMOTE_URL` to said public-facing URL.
|
||||
|
||||
|
||||
6. _Optional_ - To disable Umami analytics tracking in the Docker/web version, set the environment variable `ANALYTICS_DISABLED=true`. When enabled, the analytics script will not be loaded and all tracking will be disabled.
|
||||
|
||||
7. _Optional_ - App settings (theme, language, sidebar options, etc.) can be overridden with environment variables on first run. The variables use the `FS_` prefix (e.g. `FS_GENERAL_THEME=defaultDark`, `FS_GENERAL_LANGUAGE=de`). See [the settings environment variable documentation](docs/ENV_SETTINGS.md) for the full list.
|
||||
@@ -165,6 +169,10 @@ Feishin supports any music server that implements a [Navidrome](https://www.navi
|
||||
- [Qm-Music](https://github.com/chenqimiao/qm-music)
|
||||
- More (?)
|
||||
|
||||
- [Plex](https://www.plex.tv/media-server-downloads)
|
||||
- [Feishin fork by lux032](https://github.com/lux032/feishin) - Plex is not natively supported. Use the fork by lux032 to use Plex with Feishin.
|
||||
|
||||
|
||||
### I have the issue "The SUID sandbox helper binary was found, but is not configured correctly" on Linux
|
||||
|
||||
This happens when you have user (unprivileged) namespaces disabled (`sysctl kernel.unprivileged_userns_clone` returns 0). You can fix this by either enabling unprivileged namespaces, or by making the `chrome-sandbox` Setuid.
|
||||
|
||||
Binary file not shown.
@@ -29,12 +29,14 @@ These variables override app settings **on first run** when no persisted setting
|
||||
| `general.themeLight` | `defaultLight` | `FS_GENERAL_THEME_LIGHT` | Same as theme (used when system is light). |
|
||||
| `general.lastfmApiKey` | *(empty)* | `FS_GENERAL_LASTFM_API_KEY` | Last.fm API key. |
|
||||
| `general.lastFM` | `true` | `FS_GENERAL_LAST_FM` | `true` / `false` — Enable Last.fm. |
|
||||
| `general.listenBrainz` | `true` | `FS_GENERAL_LISTEN_BRAINZ` | `true` / `false` — ListenBrainz links. |
|
||||
| `general.musicBrainz` | `true` | `FS_GENERAL_MUSIC_BRAINZ` | `true` / `false` — MusicBrainz links. |
|
||||
| `general.nativeAspectRatio` | `false` | `FS_GENERAL_NATIVE_ASPECT_RATIO` | `true` / `false` — Use native cover art aspect ratio. |
|
||||
| `general.pathReplace` | *(empty)* | `FS_GENERAL_PATH_REPLACE` | Path pattern to replace (e.g. server path in Docker). |
|
||||
| `general.pathReplaceWith` | *(empty)* | `FS_GENERAL_PATH_REPLACE_WITH` | Replacement path. |
|
||||
| `general.playerbarOpenDrawer` | `false` | `FS_GENERAL_PLAYERBAR_OPEN_DRAWER` | `true` / `false` — Open queue/lyrics as drawer from player bar. |
|
||||
| `general.primaryShade` | `6` | `FS_GENERAL_PRIMARY_SHADE` | Mantine primary shade 0–9 (number). |
|
||||
| `general.qobuz` | `true` | `FS_GENERAL_QOBUZ` | `true` / `false` — Qobuz links. |
|
||||
| `general.resume` | `true` | `FS_GENERAL_RESUME` | `true` / `false` — Resume playback on load. |
|
||||
| `general.showLyricsInSidebar` | `true` | `FS_GENERAL_SHOW_LYRICS_IN_SIDEBAR` | `true` / `false` — Show lyrics in sidebar. |
|
||||
| `general.showRatings` | `true` | `FS_GENERAL_SHOW_RATINGS` | `true` / `false` — Show star ratings. |
|
||||
@@ -44,6 +46,7 @@ These variables override app settings **on first run** when no persisted setting
|
||||
| `general.sidebarPlaylistList` | `true` | `FS_GENERAL_SIDEBAR_PLAYLIST_LIST` | `true` / `false` — Show playlist list in sidebar. |
|
||||
| `general.sidebarPlaylistSorting` | `false` | `FS_GENERAL_SIDEBAR_PLAYLIST_SORTING` | `true` / `false` — Enable playlist sorting in sidebar. |
|
||||
| `general.sideQueueType` | `sideQueue` | `FS_GENERAL_SIDE_QUEUE_TYPE` | `sideDrawerQueue` / `sideQueue` — Side play queue style. |
|
||||
| `general.sideQueueLayout` | `horizontal` | `FS_GENERAL_SIDE_QUEUE_LAYOUT` | `horizontal` / `vertical` — Attached side queue layout orientation. |
|
||||
| `general.useThemeAccentColor` | `false` | `FS_GENERAL_USE_THEME_ACCENT_COLOR` | `true` / `false` — Use theme’s accent color instead of custom. |
|
||||
| `general.useThemePrimaryShade` | `true` | `FS_GENERAL_USE_THEME_PRIMARY_SHADE` | `true` / `false` — Use theme’s primary shade. |
|
||||
| `general.zoomFactor` | `100` | `FS_GENERAL_ZOOM_FACTOR` | UI zoom percentage (number). |
|
||||
|
||||
@@ -40,13 +40,15 @@ mac:
|
||||
arch:
|
||||
- arm64
|
||||
- x64
|
||||
icon: assets/icons/icon.icns
|
||||
icon: media/feishin.icon
|
||||
type: distribution
|
||||
hardenedRuntime: false
|
||||
identity: "-"
|
||||
identity: '-'
|
||||
gatekeeperAssess: false
|
||||
notarize: false
|
||||
|
||||
extendInfo:
|
||||
NSAudioCaptureUsageDescription: "System audio access is required for mpv visualizer capture in Feishin"
|
||||
NSLocalNetworkUsageDescription: 'Local network is necessary for accessing servers hosted on the same system as Feishin'
|
||||
|
||||
dmg:
|
||||
contents: [{ x: 130, y: 220 }, { x: 410, y: 220, type: link, path: /Applications }]
|
||||
@@ -61,7 +63,7 @@ linux:
|
||||
artifactName: ${productName}-${os}-${arch}.${ext}
|
||||
|
||||
toolsets:
|
||||
appimage: "1.0.2"
|
||||
appimage: '1.0.2'
|
||||
|
||||
npmRebuild: false
|
||||
|
||||
|
||||
@@ -40,12 +40,15 @@ mac:
|
||||
arch:
|
||||
- arm64
|
||||
- x64
|
||||
icon: assets/icons/icon.icns
|
||||
icon: media/feishin.icon
|
||||
type: distribution
|
||||
hardenedRuntime: false
|
||||
identity: "-"
|
||||
identity: '-'
|
||||
gatekeeperAssess: false
|
||||
notarize: false
|
||||
extendInfo:
|
||||
NSAudioCaptureUsageDescription: "System audio access is required for mpv visualizer capture in Feishin"
|
||||
NSLocalNetworkUsageDescription: 'Local network is necessary for accessing servers hosted on the same system as Feishin'
|
||||
|
||||
dmg:
|
||||
contents: [{ x: 130, y: 220 }, { x: 410, y: 220, type: link, path: /Applications }]
|
||||
@@ -60,7 +63,7 @@ linux:
|
||||
artifactName: ${productName}-${os}-${arch}.${ext}
|
||||
|
||||
toolsets:
|
||||
appimage: "1.0.2"
|
||||
appimage: '1.0.2'
|
||||
|
||||
npmRebuild: false
|
||||
publish:
|
||||
|
||||
@@ -40,12 +40,15 @@ mac:
|
||||
arch:
|
||||
- arm64
|
||||
- x64
|
||||
icon: assets/icons/icon.icns
|
||||
icon: media/feishin.icon
|
||||
type: distribution
|
||||
hardenedRuntime: false
|
||||
identity: "-"
|
||||
identity: '-'
|
||||
gatekeeperAssess: false
|
||||
notarize: false
|
||||
extendInfo:
|
||||
NSAudioCaptureUsageDescription: 'System audio access is required for mpv visualizer capture in Feishin'
|
||||
NSLocalNetworkUsageDescription: 'Local network is necessary for accessing servers hosted on the same system as Feishin'
|
||||
|
||||
dmg:
|
||||
contents: [{ x: 130, y: 220 }, { x: 410, y: 220, type: link, path: /Applications }]
|
||||
@@ -60,7 +63,7 @@ linux:
|
||||
artifactName: ${productName}-${os}-${arch}.${ext}
|
||||
|
||||
toolsets:
|
||||
appimage: "1.0.2"
|
||||
appimage: '1.0.2'
|
||||
|
||||
npmRebuild: false
|
||||
afterAllArtifactBuild: scripts/after-all-artifact-build.mjs
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { externalizeDepsPlugin, UserConfig } from 'electron-vite';
|
||||
import { resolve } from 'path';
|
||||
import conditionalImportPlugin from 'vite-plugin-conditional-import';
|
||||
import dynamicImportPlugin from 'vite-plugin-dynamic-import';
|
||||
import { ViteEjsPlugin } from 'vite-plugin-ejs';
|
||||
|
||||
import { createReactPlugin } from './vite.react-plugin';
|
||||
|
||||
const currentOSEnv = process.platform;
|
||||
const electronRendererTarget = 'chrome87';
|
||||
|
||||
@@ -64,7 +65,7 @@ const config: UserConfig = {
|
||||
localsConvention: 'camelCase',
|
||||
},
|
||||
},
|
||||
plugins: [react(), ViteEjsPlugin({ web: false })],
|
||||
plugins: [createReactPlugin(), ViteEjsPlugin({ web: false })],
|
||||
resolve: {
|
||||
alias: {
|
||||
'/@/i18n': resolve('src/i18n'),
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ export default tseslint.config(
|
||||
'react-refresh': eslintPluginReactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...eslintPluginReactHooks.configs.recommended.rules,
|
||||
...eslintPluginReactHooks.configs['recommended-latest'].rules,
|
||||
...eslintPluginReactRefresh.configs.vite.rules,
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/no-duplicate-enum-values': 'off',
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
+78
-74
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "feishin",
|
||||
"version": "1.8.0",
|
||||
"version": "1.11.0",
|
||||
"description": "A modern self-hosted music player.",
|
||||
"keywords": [
|
||||
"subsonic",
|
||||
@@ -31,36 +31,36 @@
|
||||
"i18next": "i18next -c src/i18n/i18next-parser.config.js",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"lint": "pnpm run typecheck && pnpm run lint-code && pnpm run lint-styles",
|
||||
"lint:fix": "pnpm run lint-code:fix && pnpm run lint-styles:fix",
|
||||
"lint-code": "eslint --max-warnings=0 --cache .",
|
||||
"lint-code:fix": "eslint --cache --fix .",
|
||||
"lint-styles": "stylelint --max-warnings=0 'src/**/*.{css,scss}'",
|
||||
"lint-styles:fix": "stylelint 'src/**/*.{css,scss}' --fix",
|
||||
"lint:fix": "pnpm run lint-code:fix && pnpm run lint-styles:fix",
|
||||
"package": "pnpm run build && electron-builder",
|
||||
"package:dev": "pnpm run build && electron-builder --dir",
|
||||
"package:linux": "pnpm run build && electron-builder --linux",
|
||||
"package:linux-arm64:pr": "pnpm run build && electron-builder --linux --arm64 --publish never",
|
||||
"package:linux:pr": "pnpm run build && electron-builder --linux --publish never",
|
||||
"package:linux-arm64:pr": "pnpm run build && electron-builder --linux --arm64 --publish never",
|
||||
"package:mac": "pnpm run build && electron-builder --mac",
|
||||
"package:mac:pr": "pnpm run build && electron-builder --mac --publish never",
|
||||
"package:win": "pnpm run build && electron-builder --win",
|
||||
"package:win-arm64:pr": "pnpm run build && electron-builder --win --arm64 --publish never",
|
||||
"package:win:pr": "pnpm run build && electron-builder --win --publish never",
|
||||
"package:win-arm64:pr": "pnpm run build && electron-builder --win --arm64 --publish never",
|
||||
"publish:linux": "pnpm run build && electron-builder --publish always --linux",
|
||||
"publish:linux:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --linux",
|
||||
"publish:linux:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --linux",
|
||||
"publish:linux-arm64": "pnpm run build && electron-builder --publish always --linux --arm64",
|
||||
"publish:linux-arm64:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --linux --arm64",
|
||||
"publish:linux-arm64:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --linux --arm64",
|
||||
"publish:linux:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --linux",
|
||||
"publish:linux:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --linux",
|
||||
"publish:mac": "pnpm run build && electron-builder --publish always --mac",
|
||||
"publish:mac:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --mac",
|
||||
"publish:mac:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --mac",
|
||||
"publish:win": "pnpm run build && electron-builder --publish always --win",
|
||||
"publish:win:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --win",
|
||||
"publish:win:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --win",
|
||||
"publish:win-arm64": "pnpm run build && electron-builder --publish always --win --arm64",
|
||||
"publish:win-arm64:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --win --arm64",
|
||||
"publish:win-arm64:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --win --arm64",
|
||||
"publish:win:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --win",
|
||||
"publish:win:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --win",
|
||||
"start": "electron-vite preview",
|
||||
"typecheck": "pnpm run typecheck:node && pnpm run typecheck:web",
|
||||
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||
@@ -68,119 +68,123 @@
|
||||
"version": "pnpm version --no-git-tag-version",
|
||||
"postversion": "node ./scripts/update-app-stream.mjs"
|
||||
},
|
||||
"resolutions": {
|
||||
"react-router": "7.14.0",
|
||||
"xml2js": "0.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "1.7.7",
|
||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.2",
|
||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.5",
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
||||
"@electron-toolkit/preload": "^3.0.1",
|
||||
"@electron-toolkit/preload": "^3.0.2",
|
||||
"@electron-toolkit/utils": "^4.0.0",
|
||||
"@mantine/colors-generator": "^8.3.8",
|
||||
"@mantine/core": "^8.3.8",
|
||||
"@mantine/dates": "^8.3.8",
|
||||
"@mantine/form": "^8.3.8",
|
||||
"@mantine/hooks": "^8.3.8",
|
||||
"@mantine/modals": "^8.3.8",
|
||||
"@mantine/notifications": "^8.3.8",
|
||||
"@mantine/colors-generator": "^9.1.1",
|
||||
"@mantine/core": "^9.1.1",
|
||||
"@mantine/dates": "^9.1.1",
|
||||
"@mantine/form": "^9.1.1",
|
||||
"@mantine/hooks": "^9.1.1",
|
||||
"@mantine/modals": "^9.1.1",
|
||||
"@mantine/notifications": "^9.1.1",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
"@tanstack/react-query": "^5.90.9",
|
||||
"@tanstack/react-query-devtools": "^5.90.2",
|
||||
"@tanstack/react-query-persist-client": "^5.90.11",
|
||||
"@tanstack/react-query": "^5.96.2",
|
||||
"@tanstack/react-query-devtools": "^5.96.2",
|
||||
"@tanstack/react-query-persist-client": "^5.96.2",
|
||||
"@ts-rest/core": "^3.52.1",
|
||||
"@wavesurfer/react": "^1.0.11",
|
||||
"@xhayper/discord-rpc": "^1.3.0",
|
||||
"audiomotion-analyzer": "^4.5.1",
|
||||
"axios": "^1.13.5",
|
||||
"butterchurn": "^3.0.0-beta.5",
|
||||
"butterchurn-presets": "^3.0.0-beta.4",
|
||||
"cheerio": "^1.1.2",
|
||||
"@wavesurfer/react": "^1.0.12",
|
||||
"@xhayper/discord-rpc": "^1.3.3",
|
||||
"audiomotion-analyzer": "^4.5.4",
|
||||
"axios": "^1.14.0",
|
||||
"butterchurn": "3.0.0-beta.5",
|
||||
"butterchurn-presets": "3.0.0-beta.4",
|
||||
"cheerio": "^1.2.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"dayjs": "^1.11.19",
|
||||
"dompurify": "^3.3.0",
|
||||
"dayjs": "^1.11.20",
|
||||
"dompurify": "^3.3.3",
|
||||
"electron-debug": "^3.2.0",
|
||||
"electron-localshortcut": "^3.2.1",
|
||||
"electron-log": "^5.4.3",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "^6.6.2",
|
||||
"fast-average-color": "^9.5.0",
|
||||
"fast-xml-parser": "^5.3.6",
|
||||
"electron-updater": "^6.8.3",
|
||||
"fast-average-color": "9.5.0",
|
||||
"fast-xml-parser": "^5.5.10",
|
||||
"format-duration": "^3.0.2",
|
||||
"fuse.js": "^7.1.0",
|
||||
"i18next": "^25.6.2",
|
||||
"fuse.js": "^7.2.0",
|
||||
"i18next": "^25.10.10",
|
||||
"icecast-metadata-stats": "^0.1.12",
|
||||
"idb-keyval": "^6.2.2",
|
||||
"immer": "^10.2.0",
|
||||
"is-electron": "^2.2.2",
|
||||
"lodash": "^4.17.23",
|
||||
"lodash": "^4.18.1",
|
||||
"md5": "^2.3.0",
|
||||
"motion": "^12.23.24",
|
||||
"motion": "^12.38.0",
|
||||
"mpris-service": "^2.1.2",
|
||||
"nanoid": "^3.3.11",
|
||||
"node-mpv": "github:jeffvli/Node-MPV#32b4d64395289ad710c41d481d2707a7acfc228f",
|
||||
"nuqs": "^2.7.1",
|
||||
"overlayscrollbars": "^2.11.1",
|
||||
"overlayscrollbars": "^2.14.0",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
"qs": "^6.14.2",
|
||||
"react": "^19.1.0",
|
||||
"react-call": "^1.8.1",
|
||||
"react-dom": "^19.1.0",
|
||||
"qs": "^6.15.0",
|
||||
"react": "^19.2.4",
|
||||
"react-call": "^1.8.2",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-error-boundary": "^5.0.0",
|
||||
"react-i18next": "^16.3.3",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-player": "^2.16.0",
|
||||
"react-router": "^7.13.1",
|
||||
"react-split-pane": "^3.0.4",
|
||||
"react-i18next": "^16.6.6",
|
||||
"react-icons": "^5.6.0",
|
||||
"react-player": "^2.16.1",
|
||||
"react-router": "^7.14.0",
|
||||
"react-split-pane": "^3.2.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.26",
|
||||
"react-window": "1.8.11",
|
||||
"react-window-v2": "npm:react-window@^2.2.3",
|
||||
"semver": "^7.5.4",
|
||||
"react-window-v2": "npm:react-window@^2.2.7",
|
||||
"semver": "^7.7.4",
|
||||
"string-to-color": "^2.2.2",
|
||||
"wavesurfer.js": "^7.11.1",
|
||||
"ws": "^8.18.2",
|
||||
"zod": "^3.22.3",
|
||||
"zustand": "^5.0.5"
|
||||
"wavesurfer.js": "^7.12.5",
|
||||
"ws": "^8.20.0",
|
||||
"zod": "^3.25.76",
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||
"@electron-toolkit/eslint-config-ts": "^3.0.0",
|
||||
"@electron-toolkit/eslint-config-ts": "^3.1.0",
|
||||
"@electron-toolkit/tsconfig": "^2.0.0",
|
||||
"@types/electron-localshortcut": "^3.1.0",
|
||||
"@types/lodash": "^4.17.18",
|
||||
"@types/md5": "^2.3.5",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.5",
|
||||
"@types/electron-localshortcut": "^3.1.3",
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@types/md5": "^2.3.6",
|
||||
"@types/node": "^24.12.2",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@types/source-map-support": "^0.5.10",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"@vitejs/plugin-react": "^5.2.0",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"concurrently": "^9.2.1",
|
||||
"cross-env": "^10.1.0",
|
||||
"electron": "^39.4.0",
|
||||
"electron": "^39.8.6",
|
||||
"electron-builder": "^26.8.2",
|
||||
"electron-devtools-installer": "^4.0.0",
|
||||
"electron-vite": "^4.0.1",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-plugin-perfectionist": "^4.13.0",
|
||||
"eslint-plugin-prettier": "^5.4.0",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-perfectionist": "^4.15.1",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"i18next-parser": "^9.3.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.26",
|
||||
"i18next-parser": "^9.4.0",
|
||||
"postcss-preset-mantine": "^1.18.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-packagejson": "^2.5.19",
|
||||
"stylelint": "^16.25.0",
|
||||
"stylelint-config-css-modules": "^4.5.1",
|
||||
"stylelint-config-recess-order": "^7.4.0",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-packagejson": "^2.5.22",
|
||||
"stylelint": "^16.26.1",
|
||||
"stylelint-config-css-modules": "^4.6.0",
|
||||
"stylelint-config-recess-order": "^7.7.0",
|
||||
"stylelint-config-standard": "^39.0.1",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^7.2.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-conditional-import": "^0.1.7",
|
||||
"vite-plugin-dynamic-import": "^1.6.0",
|
||||
"vite-plugin-ejs": "^1.7.0",
|
||||
"vite-plugin-pwa": "^1.1.0"
|
||||
"vite-plugin-pwa": "^1.2.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
|
||||
Generated
+2458
-2461
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
import { defineConfig, normalizePath } from 'vite';
|
||||
import { ViteEjsPlugin } from 'vite-plugin-ejs';
|
||||
|
||||
import { version } from './package.json';
|
||||
import { createReactPlugin } from './vite.react-plugin';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
@@ -35,7 +35,7 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
react(),
|
||||
createReactPlugin(),
|
||||
ViteEjsPlugin({
|
||||
prod: process.env.NODE_ENV === 'production',
|
||||
root: normalizePath(path.resolve(__dirname, './src/remote')),
|
||||
|
||||
@@ -24,12 +24,14 @@ window.FS_GENERAL_HOME_FEATURE_STYLE = "${FS_GENERAL_HOME_FEATURE_STYLE}";
|
||||
window.FS_GENERAL_LANGUAGE = "${FS_GENERAL_LANGUAGE}";
|
||||
window.FS_GENERAL_LAST_FM = "${FS_GENERAL_LAST_FM}";
|
||||
window.FS_GENERAL_LASTFM_API_KEY = "${FS_GENERAL_LASTFM_API_KEY}";
|
||||
window.FS_GENERAL_LISTEN_BRAINZ = "${FS_GENERAL_LISTEN_BRAINZ}";
|
||||
window.FS_GENERAL_MUSIC_BRAINZ = "${FS_GENERAL_MUSIC_BRAINZ}";
|
||||
window.FS_GENERAL_NATIVE_ASPECT_RATIO = "${FS_GENERAL_NATIVE_ASPECT_RATIO}";
|
||||
window.FS_GENERAL_PATH_REPLACE = "${FS_GENERAL_PATH_REPLACE}";
|
||||
window.FS_GENERAL_PATH_REPLACE_WITH = "${FS_GENERAL_PATH_REPLACE_WITH}";
|
||||
window.FS_GENERAL_PLAYERBAR_OPEN_DRAWER = "${FS_GENERAL_PLAYERBAR_OPEN_DRAWER}";
|
||||
window.FS_GENERAL_PRIMARY_SHADE = "${FS_GENERAL_PRIMARY_SHADE}";
|
||||
window.FS_GENERAL_QOBUZ = "${FS_GENERAL_QOBUZ}";
|
||||
window.FS_GENERAL_RESUME = "${FS_GENERAL_RESUME}";
|
||||
window.FS_GENERAL_SHOW_LYRICS_IN_SIDEBAR = "${FS_GENERAL_SHOW_LYRICS_IN_SIDEBAR}";
|
||||
window.FS_GENERAL_SHOW_RATINGS = "${FS_GENERAL_SHOW_RATINGS}";
|
||||
@@ -39,6 +41,7 @@ window.FS_GENERAL_SIDEBAR_COLLAPSE_SHARED = "${FS_GENERAL_SIDEBAR_COLLAPSE_SHARE
|
||||
window.FS_GENERAL_SIDEBAR_PLAYLIST_LIST = "${FS_GENERAL_SIDEBAR_PLAYLIST_LIST}";
|
||||
window.FS_GENERAL_SIDEBAR_PLAYLIST_SORTING = "${FS_GENERAL_SIDEBAR_PLAYLIST_SORTING}";
|
||||
window.FS_GENERAL_SIDE_QUEUE_TYPE = "${FS_GENERAL_SIDE_QUEUE_TYPE}";
|
||||
window.FS_GENERAL_SIDE_QUEUE_LAYOUT = "${FS_GENERAL_SIDE_QUEUE_LAYOUT}";
|
||||
window.FS_GENERAL_THEME = "${FS_GENERAL_THEME}";
|
||||
window.FS_GENERAL_THEME_DARK = "${FS_GENERAL_THEME_DARK}";
|
||||
window.FS_GENERAL_THEME_LIGHT = "${FS_GENERAL_THEME_LIGHT}";
|
||||
|
||||
+4
-12
@@ -1,4 +1,4 @@
|
||||
import { PostProcessorModule, TOptions } from 'i18next';
|
||||
import { PostProcessorModule } from 'i18next';
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
@@ -203,25 +203,17 @@ const titleCasePostProcessor: PostProcessorModule = {
|
||||
type: 'postProcessor',
|
||||
};
|
||||
|
||||
const ignoreSentenceCaseLanguages = ['de'];
|
||||
// const ignoreSentenceCaseLanguages = ['de'];
|
||||
|
||||
const sentenceCasePostProcessor: PostProcessorModule = {
|
||||
name: 'sentenceCase',
|
||||
process: (
|
||||
value: string,
|
||||
_key: string,
|
||||
_options: TOptions<Record<string, string>>,
|
||||
translator: any,
|
||||
) => {
|
||||
process: (value: string) => {
|
||||
const sentences = value.split('. ');
|
||||
|
||||
return sentences
|
||||
.map((sentence) => {
|
||||
return (
|
||||
sentence.charAt(0).toLocaleUpperCase() +
|
||||
(!ignoreSentenceCaseLanguages.includes(translator.language)
|
||||
? sentence.slice(1).toLocaleLowerCase()
|
||||
: sentence.slice(1))
|
||||
sentence.charAt(0).toLocaleUpperCase() + sentence.slice(1).toLocaleLowerCase()
|
||||
);
|
||||
})
|
||||
.join('. ');
|
||||
|
||||
@@ -21,7 +21,13 @@
|
||||
"openIn": {
|
||||
"lastfm": "فتح في Last.fm",
|
||||
"musicbrainz": "فتح في MusicBrainz"
|
||||
}
|
||||
},
|
||||
"addOrRemoveFromSelection": "إضافة أو إزالة من الإختيارات",
|
||||
"selectRangeOfItems": "اختر مجموعة من العناصر",
|
||||
"goToCurrent": "الانتقال إلى العنصر الحالي",
|
||||
"createRadioStation": "يخلق $t(entity.radioStation, {\"count\": 1})",
|
||||
"deleteRadioStation": "يمسح $t(entity.radioStation, {\"count\": 1})",
|
||||
"selectAll": "تحديد الكل"
|
||||
},
|
||||
"common": {
|
||||
"action_zero": "عملية",
|
||||
|
||||
+959
-912
File diff suppressed because it is too large
Load Diff
+945
-913
File diff suppressed because it is too large
Load Diff
+871
-872
File diff suppressed because it is too large
Load Diff
+453
-411
File diff suppressed because it is too large
Load Diff
+937
-904
File diff suppressed because it is too large
Load Diff
+542
-510
File diff suppressed because it is too large
Load Diff
+775
-713
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@
|
||||
"unfavorite": "حذف از موردعلاقهها",
|
||||
"shuffle_off": "پخش تصادفی غیر فعال",
|
||||
"skip_forward": "برو جلو",
|
||||
"queue_moveToTop": "جابجا کردن انتخاب شده به پایین",
|
||||
"queue_moveToTop": "جابجا کردن انتخاب شده به بالا",
|
||||
"queue_clear": "خالی کردن صف",
|
||||
"queue_remove": "حذف انتخاب شده",
|
||||
"addLast": "افزودن به پایان",
|
||||
@@ -24,7 +24,7 @@
|
||||
"mute": "بیصدا کردن",
|
||||
"playbackFetchCancel": "دارد طول میکشد... برای لفو کردن اعلان را ببندید",
|
||||
"playbackFetchInProgress": "بارگذاری قطعهها…",
|
||||
"queue_moveToBottom": "جابجا کردن انتخاب شده به بالا",
|
||||
"queue_moveToBottom": "جابجا کردن انتخاب شده به پایین",
|
||||
"addNext": "افزودن به پسین",
|
||||
"favorite": "مورد علاقه",
|
||||
"playSimilarSongs": "پخش آهنگهای همگون",
|
||||
@@ -70,7 +70,7 @@
|
||||
"hotkey_rate1": "امتیاز ۱ ستاره",
|
||||
"hotkey_skipForward": "برو جلو",
|
||||
"disableLibraryUpdateOnStartup": "غیرفعال کردن بررسی آخرین نسخه در آغاز به کار برنامه",
|
||||
"discordApplicationId_description": "the application id for {{discord}} rich presence (defaults to {{defaultId}})",
|
||||
"discordApplicationId_description": "the application ID for {{discord}} Rich Presence (defaults to {{defaultId}})",
|
||||
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
|
||||
"hotkey_playbackPlay": "پخش",
|
||||
"hotkey_volumeDown": "کم کردن صدا",
|
||||
@@ -109,7 +109,7 @@
|
||||
"customFontPath": "مسیر قلم سفارشی",
|
||||
"audioPlayer": "پخشکنندهٔ صدا",
|
||||
"hotkey_rate0": "حذف امتیاز",
|
||||
"discordApplicationId": "{{discord}} application id",
|
||||
"discordApplicationId": "{{discord}} application ID",
|
||||
"hotkey_volumeMute": "بستن صدا",
|
||||
"showSkipButton": "نمایش دکمهٔ رد کردن",
|
||||
"customFontPath_description": "مسیر قلم سفارشی را برای استفاده در اپلیکیشن مشخص کنید",
|
||||
@@ -132,7 +132,7 @@
|
||||
"buttonSize": "اندازهی دکمهی پخش نوار",
|
||||
"contextMenu": "پیکربندی فهرست زمینه (کلیک راست)",
|
||||
"buttonSize_description": "اندازهی دکمههای پخش نوار",
|
||||
"audioExclusiveMode_description": "حالت اختصاصی خروجی را فعال میکند. در این حالت، سامانه معمولاً قفل است و فقط mpv میتواند خروجی صدا دهد",
|
||||
"audioExclusiveMode_description": "حالت اختصاصی خروجی را فعال میکند. در این حالت، سامانه معمولاً قفل است و فقط MPV میتواند خروجی صدا دهد",
|
||||
"clearQueryCache_description": "یک 'پاکسازی نرم' از فیشین. این فهرستهای پخش و فرادادهی قطعهها را تازه میکند و متن شعرهای ذخیره شده را بازنشانی میکند. پیکربندیها، اعتبارنامههای سرویسدهنده و نگارههای کَش شده حفظ میشوند",
|
||||
"clearCache_description": "یک 'پاکسازی سخت' فیشین. افزون بر پاکسازی کَش فیشین، کَش مرورگر هم تهی میشود (نگارههای ذخیره شده و باقی داراییها). اعتبارنامهها و پیکربندیها حفظ میشوند",
|
||||
"contextMenu_description": "به شما اجازه میدهد که آیتمهای نمایش داده شده در فهرستی که وقتی روی یک آیتم کلیک راست میکنید پدیدار میشود، را پنهان کنید. آیتمهایی که منتخب نیستند پنهان میشوند",
|
||||
@@ -176,7 +176,7 @@
|
||||
"backward": "به عقب",
|
||||
"increase": "افزایش",
|
||||
"rating": "امتیاز",
|
||||
"bpm": "bpm",
|
||||
"bpm": "BPM",
|
||||
"refresh": "تازهسازی",
|
||||
"unknown": "ناشناخته",
|
||||
"areYouSure": "مطمئنید؟",
|
||||
@@ -313,9 +313,9 @@
|
||||
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
|
||||
"isRecentlyPlayed": "به تازگی پخش شده است",
|
||||
"isFavorited": "موردعلاقه است",
|
||||
"bpm": "bpm",
|
||||
"bpm": "BPM",
|
||||
"releaseYear": "سال انتشار",
|
||||
"id": "id",
|
||||
"id": "ID",
|
||||
"disc": "دیسک",
|
||||
"biography": "زندگینامه",
|
||||
"songCount": "تعداد ترانه",
|
||||
|
||||
+754
-755
File diff suppressed because it is too large
Load Diff
+999
-950
File diff suppressed because it is too large
Load Diff
+728
-729
File diff suppressed because it is too large
Load Diff
+821
-822
File diff suppressed because it is too large
Load Diff
+734
-733
File diff suppressed because it is too large
Load Diff
+293
-62
@@ -7,7 +7,7 @@
|
||||
"playRandom": "ランダム再生",
|
||||
"skip": "スキップ",
|
||||
"previous": "前へ",
|
||||
"toggleFullscreenPlayer": "フルスクリーンプレーヤーの切り替え",
|
||||
"toggleFullscreenPlayer": "全画面プレーヤーに切り替える",
|
||||
"skip_back": "前へスキップ",
|
||||
"favorite": "お気に入り",
|
||||
"next": "次へ",
|
||||
@@ -22,8 +22,8 @@
|
||||
"queue_clear": "キューをクリア",
|
||||
"muted": "ミュート中",
|
||||
"unfavorite": "お気に入り解除",
|
||||
"queue_moveToTop": "選択項目を一番下に移動",
|
||||
"queue_moveToBottom": "選択項目を先頭に移動",
|
||||
"queue_moveToTop": "選択項目を先頭に移動",
|
||||
"queue_moveToBottom": "選択項目を一番下に移動",
|
||||
"shuffle_off": "シャッフル無効",
|
||||
"addLast": "最後",
|
||||
"mute": "ミュート",
|
||||
@@ -44,7 +44,11 @@
|
||||
"sleepTimer_off": "オフ",
|
||||
"sleepTimer_timeRemaining": "残り {{time}}",
|
||||
"sleepTimer_setCustom": "タイマーを設定",
|
||||
"sleepTimer_cancel": "タイマーをキャンセル"
|
||||
"sleepTimer_cancel": "タイマーをキャンセル",
|
||||
"holdToShuffle": "長押しでシャッフル",
|
||||
"albumRadio": "アルバム・ラジオ",
|
||||
"artistRadio": "アーティストラジオ",
|
||||
"trackRadio": "ラジオを追跡する"
|
||||
},
|
||||
"setting": {
|
||||
"crossfadeStyle_description": "オーディオプレーヤーが使用するクロスフェードのスタイルを選択します",
|
||||
@@ -56,7 +60,7 @@
|
||||
"theme_description": "アプリケーションに使用するテーマを設定します",
|
||||
"hotkey_playbackPause": "一時停止",
|
||||
"replayGainFallback": "{{ReplayGain}} フォールバック",
|
||||
"sidebarCollapsedNavigation_description": "折りたたみサイドバーのナビゲーションを表示/非表示にします",
|
||||
"sidebarCollapsedNavigation_description": "折りたたまれたサイドバーのナビゲーションを表示または非表示にします",
|
||||
"hotkey_volumeUp": "音量を上げる",
|
||||
"skipDuration": "スキップの長さ",
|
||||
"discordIdleStatus_description": "有効にすると、プレーヤーがアイドル状態でもステータスを更新します",
|
||||
@@ -71,7 +75,7 @@
|
||||
"mpvExecutablePath_description": "MPV を実行するファイルパスを設定します。空のままにすると、デフォルトのパスが使用されます",
|
||||
"replayGainClipping_description": "自動的にゲインを下げて {{ReplayGain}} によるクリッピングを防ぎます",
|
||||
"replayGainPreamp": "{{ReplayGain}} プリアンプ (dB)",
|
||||
"hotkey_favoriteCurrentSong": "$t(common.currentSong) をお気に入り",
|
||||
"hotkey_favoriteCurrentSong": "$t(common.currentSong) をお気に入りに登録",
|
||||
"sampleRate": "サンプルレート",
|
||||
"sidePlayQueueStyle_optionAttached": "結合",
|
||||
"sidebarConfiguration": "サイドバー設定",
|
||||
@@ -86,19 +90,19 @@
|
||||
"themeLight": "テーマ (ライト)",
|
||||
"fontType_optionBuiltIn": "組み込みフォント",
|
||||
"hotkey_playbackPlayPause": "再生 / 一時停止",
|
||||
"hotkey_rate1": "1つ星で評価",
|
||||
"hotkey_rate1": "1 つ星で評価",
|
||||
"hotkey_skipForward": "次へスキップ",
|
||||
"disableLibraryUpdateOnStartup": "起動時の新バージョンチェックを無効にします",
|
||||
"discordApplicationId_description": "{{discord}} に Rich Presence ステータスを表示するためのアプリケーション ID (デフォルトは {{defaultId}} です)",
|
||||
"sidePlayQueueStyle": "サイド再生キュースタイル",
|
||||
"sidePlayQueueStyle": "サイド再生キューの形式",
|
||||
"gaplessAudio": "ギャップレス再生",
|
||||
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
|
||||
"zoom": "ズーム率",
|
||||
"minimizeToTray_description": "最小化ボタンが押された際、システムトレイに格納します",
|
||||
"hotkey_playbackPlay": "再生",
|
||||
"hotkey_togglePreviousSongFavorite": "$t(common.previousSong) をお気に入り登録/解除",
|
||||
"hotkey_togglePreviousSongFavorite": "$t(common.previousSong) のお気に入りを切り替え",
|
||||
"hotkey_volumeDown": "音量を下げる",
|
||||
"hotkey_unfavoritePreviousSong": "$t(common.previousSong) をお気に入り解除",
|
||||
"hotkey_unfavoritePreviousSong": "$t(common.previousSong) をお気に入りから解除",
|
||||
"audioPlayer_description": "再生に使用するオーディオプレーヤーを選択します",
|
||||
"globalMediaHotkeys": "グローバルメディアホットキー",
|
||||
"hotkey_globalSearch": "グローバル検索",
|
||||
@@ -106,7 +110,7 @@
|
||||
"remoteUsername_description": "リモートコントロール サーバーのユーザ名を設定します。 ユーザー名とパスワードの両方が空の場合、認証は無効になります",
|
||||
"exitToTray_description": "アプリケーション終了ボタンが押された際、システムトレイに格納します",
|
||||
"followLyric_description": "現在の再生位置に歌詞をスクロールします",
|
||||
"hotkey_favoritePreviousSong": "$t(common.previousSong) をお気に入り",
|
||||
"hotkey_favoritePreviousSong": "$t(common.previousSong) をお気に入りに登録",
|
||||
"replayGainMode_optionAlbum": "$t(entity.album, {\"count\": 1})",
|
||||
"lyricOffset": "歌詞のオフセット (ミリ秒)",
|
||||
"discordUpdateInterval_description": "更新間隔 (秒単位、最小 15 秒)",
|
||||
@@ -117,16 +121,16 @@
|
||||
"lyricFetchProvider": "歌詞取得先",
|
||||
"language_description": "アプリケーションの言語を設定します ($t(common.restartRequired))",
|
||||
"playbackStyle_optionCrossFade": "クロスフェード",
|
||||
"hotkey_rate3": "3つ星で評価",
|
||||
"hotkey_rate3": "3 つ星で評価",
|
||||
"font": "フォント",
|
||||
"replayGainMode_optionTrack": "$t(entity.track, {\"count\": 1})",
|
||||
"themeLight_description": "アプリケーションに使用するライトテーマを設定します",
|
||||
"hotkey_toggleFullScreenPlayer": "フルスクリーンプレーヤーの切り替え",
|
||||
"hotkey_toggleFullScreenPlayer": "全画面プレーヤーに切り替え",
|
||||
"hotkey_localSearch": "ページ内検索",
|
||||
"hotkey_toggleQueue": "キューの切り替え",
|
||||
"zoom_description": "アプリケーションのズーム率を設定します",
|
||||
"remotePassword_description": "リモートコントロール サーバーのパスワードを設定します。 ログイン情報はデフォルトでセキュアな通信がされないため、個人情報と関係ないランダムなパスワードを利用してください",
|
||||
"hotkey_rate5": "5つ星で評価",
|
||||
"hotkey_rate5": "5 つ星で評価",
|
||||
"hotkey_playbackPrevious": "前のトラック",
|
||||
"showSkipButtons_description": "プレーヤーバーのスキップボタンを表示または非表示にします",
|
||||
"crossfadeDuration_description": "クロスフェード効果の時間を設定します",
|
||||
@@ -137,11 +141,11 @@
|
||||
"discordRichPresence_description": "{{discord}} Rich Presence で再生ステータスを有効にします。画像キー: {{icon}}, {{playing}}, {{paused}}",
|
||||
"mpvExecutablePath": "MPV 実行ファイルパス",
|
||||
"audioDevice": "オーディオデバイス",
|
||||
"hotkey_rate2": "2つ星で評価",
|
||||
"hotkey_rate2": "2 つ星で評価",
|
||||
"playButtonBehavior_description": "キューに曲を追加するときの再生ボタンのデフォルトの動作を設定します",
|
||||
"minimumScrobblePercentage_description": "Scrobble されるために必要な最短の再生時間 (%)",
|
||||
"exitToTray": "終了時にシステムトレイに格納",
|
||||
"hotkey_rate4": "4つ星で評価",
|
||||
"hotkey_rate4": "4 つ星で評価",
|
||||
"enableRemote": "リモートコントロール サーバーを有効化",
|
||||
"showSkipButton_description": "プレーヤーバーのスキップボタンを表示または非表示にします",
|
||||
"savePlayQueue": "再生キューを保存",
|
||||
@@ -150,9 +154,9 @@
|
||||
"fontType_description": "組み込みフォントの場合、Feishin が提供するフォントの中から 1 つ選択します。 システムフォントの場合、OS が提供する任意のフォントを選択できます。 カスタムフォントの場合、フォントファイルを自身で選択できます",
|
||||
"playButtonBehavior": "再生ボタンの動作",
|
||||
"volumeWheelStep": "音量ホイールステップ",
|
||||
"sidebarPlaylistList_description": "サイドバーでプレイリストのリストを表示/非表示にします",
|
||||
"sidebarPlaylistList_description": "サイドバーのプレイリストを表示または非表示にします",
|
||||
"accentColor": "アクセントカラー",
|
||||
"sidePlayQueueStyle_description": "サイド再生キューのスタイルを設定します",
|
||||
"sidePlayQueueStyle_description": "サイド再生キューの形式を設定します",
|
||||
"accentColor_description": "アプリケーションが利用するアクセントカラーを設定します",
|
||||
"replayGainMode": "{{ReplayGain}} モード",
|
||||
"playbackStyle_optionNormal": "通常",
|
||||
@@ -161,7 +165,7 @@
|
||||
"replayGainPreamp_description": "{{ReplayGain}} の値に適用されるプリアンプゲインを調整します",
|
||||
"hotkey_toggleRepeat": "リピートの切り替え",
|
||||
"lyricOffset_description": "歌詞のオフセットをミリ秒単位で指定します",
|
||||
"sidebarConfiguration_description": "サイドバーに表示されるアイテムと並び順を選択します",
|
||||
"sidebarConfiguration_description": "サイドバーに表示する項目と順序を選択します",
|
||||
"fontType": "フォントタイプ",
|
||||
"remotePort": "リモートコントロールサーバーのポート",
|
||||
"applicationHotkeys": "アプリケーションホットキー",
|
||||
@@ -178,16 +182,16 @@
|
||||
"sidePlayQueueStyle_optionDetached": "分離",
|
||||
"audioPlayer": "オーディオプレーヤー",
|
||||
"hotkey_zoomOut": "縮小",
|
||||
"hotkey_unfavoriteCurrentSong": "$t(common.currentSong) をお気に入り解除",
|
||||
"hotkey_unfavoriteCurrentSong": "$t(common.currentSong) をお気に入りから解除",
|
||||
"hotkey_rate0": "評価をクリア",
|
||||
"discordApplicationId": "{{discord}} アプリケーション ID",
|
||||
"applicationHotkeys_description": "アプリケーションのホットキーを設定します。チェックボックスを切り替えて、グローバルホットキーとして設定します (デスクトップのみ)",
|
||||
"hotkey_volumeMute": "音量をミュート",
|
||||
"hotkey_toggleCurrentSongFavorite": "$t(common.currentSong) をお気に入り登録/解除",
|
||||
"hotkey_toggleCurrentSongFavorite": "$t(common.currentSong) のお気に入りを切り替え",
|
||||
"remoteUsername": "リモートコントロールサーバーのユーザー名",
|
||||
"hotkey_browserBack": "ブラウザ 戻る",
|
||||
"showSkipButton": "スキップボタンを表示",
|
||||
"sidebarPlaylistList": "サイドバー プレイリスト リスト",
|
||||
"sidebarPlaylistList": "サイドバーのプレイリスト",
|
||||
"minimizeToTray": "最小化時にシステムトレイに格納",
|
||||
"skipPlaylistPage": "プレイリストページをスキップ",
|
||||
"themeDark": "テーマ (ダーク)",
|
||||
@@ -215,10 +219,10 @@
|
||||
"trayEnabled": "トレイを表示する",
|
||||
"volumeWidth_description": "音量スライダーの幅",
|
||||
"volumeWidth": "音量スライダーの幅",
|
||||
"webAudio_description": "Web Audio を使用します。これにより、リプレイゲインなどの高度な機能が有効になります。それ以外の場合は無効にしてください",
|
||||
"webAudio_description": "Web Audio を使用します。これにより、リプレイゲインなどの高度な機能が有効になります。問題が発生した場合は無効にしてください",
|
||||
"mpvExtraParameters_help": "1 行に 1 つずつ",
|
||||
"musicbrainz_description": "MusicBrainz ID が存在するアーティスト/アルバムページに MusicBrainz へのリンクを表示します",
|
||||
"musicbrainz": "MusicBrainz リンクを表示する",
|
||||
"musicbrainz_description": "MusicBrainz ID が存在するアーティストとアルバムページに MusicBrainz へのリンクを表示します",
|
||||
"musicbrainz": "MusicBrainz のリンクを表示",
|
||||
"neteaseTranslation_description": "有効にすると、利用可能な場合は NetEase から翻訳された歌詞を取得して表示します",
|
||||
"neteaseTranslation": "NetEase 翻訳歌詞を有効にする",
|
||||
"passwordStore_description": "使用するパスワード / シークレットストア。パスワードの保存に問題がある場合はこれを変更してください",
|
||||
@@ -234,8 +238,8 @@
|
||||
"imageAspectRatio": "ネイティブのカバーアートの縦横比を使用する",
|
||||
"language": "言語",
|
||||
"imageAspectRatio_description": "有効にすると、カバーアートはネイティブの縦横比で表示されます。縦横比が 1:1 でない場合、残りのスペースは空白になります",
|
||||
"lastfm_description": "アーティスト/アルバムページに Last.fm へのリンクを表示します",
|
||||
"lastfm": "Last.fm リンクを表示する",
|
||||
"lastfm_description": "アーティストとアルバムページに Last.fm へのリンクを表示します",
|
||||
"lastfm": "Last.fm のリンクを表示",
|
||||
"lastfmApiKey": "{{lastfm}} API キー",
|
||||
"homeConfiguration_description": "ホーム画面に表示する項目と表示順序を設定します",
|
||||
"homeConfiguration": "ホーム画面の設定",
|
||||
@@ -267,7 +271,7 @@
|
||||
"customCss": "カスタム CSS",
|
||||
"customCssEnable_description": "カスタム CSS の記述を許可します",
|
||||
"customCssEnable": "カスタム CSS を有効にする",
|
||||
"customCssNotice": "警告: ある程度のサニタイズ (url() と content: の禁止) はありますが、カスタム CSS を使用するとインターフェースの変更によりリスクが生じる可能性があります",
|
||||
"customCssNotice": "警告: ある程度のサニタイズ (URL() と content: の禁止) はありますが、カスタム CSS を使用するとインターフェースの変更によりリスクが生じる可能性があります",
|
||||
"releaseChannel_optionBeta": "ベータ",
|
||||
"releaseChannel_optionLatest": "最新",
|
||||
"releaseChannel": "リリースチャンネル",
|
||||
@@ -283,7 +287,7 @@
|
||||
"exportImportSettings_control_title": "設定をインポート/エクスポート",
|
||||
"exportImportSettings_control_description": "JSON 経由で設定をエクスポートおよびインポートする",
|
||||
"exportImportSettings_destructiveWarning": "設定のインポートは破壊的です。下の「インポート」をクリックする前に、上記の内容を必ずご確認ください!",
|
||||
"hotkey_navigateHome": "ホームに移動",
|
||||
"hotkey_navigateHome": "ホーム画面へ移動",
|
||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||
"playerbarOpenDrawer": "プレーヤーバーの全画面表示切り替え",
|
||||
"transcode": "トランスコーディングを有効にする",
|
||||
@@ -325,11 +329,11 @@
|
||||
"followCurrentSong": "現在の曲をフォロー",
|
||||
"followCurrentSong_description": "再生キューを現在再生中の曲まで自動的にスクロールします",
|
||||
"logLevel": "ログレベル",
|
||||
"logLevel_description": "表示するログの最小レベルを設定します。debug はすべてのログを表示し、error はエラーのみを表示します",
|
||||
"logLevel_optionDebug": "debug",
|
||||
"logLevel_optionError": "error",
|
||||
"logLevel_optionInfo": "info",
|
||||
"logLevel_optionWarn": "warn",
|
||||
"logLevel_description": "表示するログの最小レベルを設定します。Debug はすべてのログを表示し、Error はエラーのみを表示します",
|
||||
"logLevel_optionDebug": "Debug",
|
||||
"logLevel_optionError": "Error",
|
||||
"logLevel_optionInfo": "Info",
|
||||
"logLevel_optionWarn": "Warn",
|
||||
"playerFilters": "キューから曲をフィルタリング",
|
||||
"playerFilters_description": "以下の基準に基づいて曲をキューに追加しないようにします",
|
||||
"artistRadioCount": "アーティスト / トラックのラジオカウント",
|
||||
@@ -337,7 +341,7 @@
|
||||
"imageResolution": "画像の解像度",
|
||||
"imageResolution_description": "アプリ内で使用される画像の解像度。値を 0 に設定すると、デフォルトでネイティブ画像解像度が適用されます",
|
||||
"showLyricsInSidebar_description": "添付の再生キューに歌詞を表示するパネルが追加されます",
|
||||
"showLyricsInSidebar": "プレーヤーのサイドバーに歌詞を表示する",
|
||||
"showLyricsInSidebar": "サイドバーのプレーヤーに歌詞を表示",
|
||||
"showRatings": "星評価を表示する",
|
||||
"imageResolution_optionSidebar": "サイドバー",
|
||||
"imageResolution_optionHeader": "ヘッダー",
|
||||
@@ -348,12 +352,12 @@
|
||||
"playerbarSliderType_optionWaveform": "波形",
|
||||
"playerbarWaveformAlign": "波形アライメント",
|
||||
"showRatings_description": "インターフェースに星評価機能を表示するかどうかを制御します",
|
||||
"showVisualizerInSidebar": "プレーヤーのサイドバーにビジュアライザーを表示する",
|
||||
"combinedLyricsAndVisualizer": "プレーヤーのサイドバーに歌詞とビジュアライザーを統合する",
|
||||
"showVisualizerInSidebar": "サイドバーのプレーヤーにビジュアライザーを表示",
|
||||
"combinedLyricsAndVisualizer": "サイドバーのプレーヤーに歌詞とビジュアライザーを統合",
|
||||
"audioFadeOnStatusChange_description": "再生 / 一時停止の状態が変わったときにフェードアウトとフェードインを有効にします",
|
||||
"audioFadeOnStatusChange": "ステータス変更時の音声フェード",
|
||||
"combinedLyricsAndVisualizer_description": "歌詞とビジュアライザーを同じパネルに統合します",
|
||||
"showVisualizerInSidebar_description": "プレーヤーのサイドバーにビジュアライザーを表示するパネルが追加されます",
|
||||
"showVisualizerInSidebar_description": "サイドバーのプレーヤーにビジュアライザーを表示するパネルが追加されます",
|
||||
"queryBuilderCustomFields": "カスタムフィールド",
|
||||
"queryBuilderCustomFields_inputLabel": "ラベル",
|
||||
"queryBuilderCustomFields_inputTag": "タグ",
|
||||
@@ -375,7 +379,53 @@
|
||||
"automaticUpdates_description": "更新を自動的に確認してインストールします",
|
||||
"releaseChannel_optionAlpha": "アルファ (nightly)",
|
||||
"discordStateIcon": "再生中アイコンを表示",
|
||||
"discordStateIcon_description": "Rich Presence ステータスに小さな再生アイコンを表示します。「一時停止時に Rich Presence を表示」が有効になっている場合は、常に一時停止アイコンが表示されます"
|
||||
"discordStateIcon_description": "Rich Presence ステータスに小さな再生アイコンを表示します。「一時停止時に Rich Presence を表示」が有効になっている場合は、常に一時停止アイコンが表示されます",
|
||||
"sidebarPlaylistListFilterRegex_description": "この正規表現に一致するプレイリストをサイドバーから非表示にします",
|
||||
"sidebarPlaylistListFilterRegex_placeholder": "例: ^Daily Mix.*",
|
||||
"sidebarPlaylistListFilterRegex": "プレイリストフィルターの正規表現",
|
||||
"sidebarPlaylistSorting": "サイドバーでプレイリストを並べ替え",
|
||||
"sidebarPlaylistSorting_description": "デフォルトのサーバー順ではなく、ドラッグアンドドロップを使用してサイドバーでプレイリストを手動で並べ替えることができます",
|
||||
"playerItemConfiguration_description": "全画面プレーヤーに表示する項目と順序を設定します",
|
||||
"playerItemConfiguration": "プレーヤーの項目設定",
|
||||
"autosave": "再生キューを自動的に保存",
|
||||
"autosave_description": "再生キューをサーバーに自動的に保存できるようにします。これは Navidrome/Subsonic を使用している場合にのみ可能であり、再生キューを混在させることはできません。",
|
||||
"autosaveCount": "自動再生キューの保存頻度",
|
||||
"autosaveCount_description": "キューが保存されるまでにトラックが変更される回数を設定します。1 (最小値) は曲が変わるたびに保存されることを意味します",
|
||||
"useThemePrimaryShade_description": "選択したテーマで定義されたプライマリシェードをプライマリカラーのバリアントに使用します",
|
||||
"useThemePrimaryShade": "テーマのプライマリシェードを使用",
|
||||
"primaryShade": "プライマリシェード",
|
||||
"primaryShade_description": "ボタン、リンク、およびその他の主要色要素に使用されるプライマリシェード (0–9) を上書きします",
|
||||
"playerbarWaveformAlign_optionTop": "上部",
|
||||
"playerbarWaveformAlign_optionCenter": "中央",
|
||||
"playerbarWaveformAlign_optionBottom": "下部",
|
||||
"imageResolution_optionTable": "表",
|
||||
"imageResolution_optionItemCard": "アイテムカード",
|
||||
"blurExplicitImages": "露骨な画像をぼかす",
|
||||
"blurExplicitImages_description": "露骨な表現を含むタグが付けられたアルバムおよび楽曲のアートワークをぼかします",
|
||||
"enableGridMultiSelect": "グリッドの複数選択を有効にする",
|
||||
"enableGridMultiSelect_description": "有効にすると、グリッドビューで複数のアイテムを選択できます。無効にすると、グリッドアイテムの画像をクリックするとアイテムページに移動します",
|
||||
"playerbarWaveformBarWidth": "波形バーの幅",
|
||||
"playerbarWaveformGap": "波形ギャップ",
|
||||
"playerbarWaveformRadius": "波形半径",
|
||||
"hotkey_listNavigateToPage": "項目の詳細ページへ移動",
|
||||
"hotkey_listPlayDefault": "リストを再生 (デフォルト)",
|
||||
"hotkey_listPlayLast": "最後に再生",
|
||||
"hotkey_listPlayNext": "次に再生",
|
||||
"hotkey_listPlayNow": "今すぐ再生",
|
||||
"spotify_description": "アーティストとアルバムページに Spotify へのリンクを表示します",
|
||||
"spotify": "Spotify のリンクを表示",
|
||||
"nativeSpotify_description": "ブラウザーの代わりに Spotify アプリで開きます",
|
||||
"nativeSpotify": "Spotify アプリを使用",
|
||||
"listenbrainz_description": "アーティストとアルバムページに ListenBrainz へのリンクを表示します",
|
||||
"listenbrainz": "ListenBrainz のリンクを表示",
|
||||
"qobuz_description": "アーティストとアルバムページに Qobuz へのリンクを表示します",
|
||||
"qobuz": "Qobuz のリンクを表示",
|
||||
"sidePlayQueueLayout": "サイド再生キューのレイアウト",
|
||||
"sidePlayQueueLayout_description": "結合されたサイド再生キューのレイアウトを設定します",
|
||||
"sidePlayQueueLayout_optionHorizontal": "水平",
|
||||
"sidePlayQueueLayout_optionVertical": "垂直",
|
||||
"waveformLoadingDelay": "波形読み込みの遅延",
|
||||
"waveformLoadingDelay_description": "波形を読み込むまでの遅延時間(秒単位)を設定します。Web プレーヤー使用時にカクつきが発生する場合は、この値を増やしてください。"
|
||||
},
|
||||
"action": {
|
||||
"editPlaylist": "$t(entity.playlist, {\"count\": 1}) を編集",
|
||||
@@ -397,7 +447,10 @@
|
||||
"removeFromFavorites": "$t(entity.favorite, {\"count\": 2}) から削除",
|
||||
"openIn": {
|
||||
"lastfm": "Last.fm で開く",
|
||||
"musicbrainz": "MusicBrainz で開く"
|
||||
"musicbrainz": "MusicBrainz で開く",
|
||||
"spotify": "Spotify で開く",
|
||||
"listenbrainz": "ListenBrainz で開く",
|
||||
"qobuz": "Qobuz で開く"
|
||||
},
|
||||
"moveToNext": "次",
|
||||
"downloadStarted": "{{count}} 曲のダウンロードを開始しました",
|
||||
@@ -415,7 +468,8 @@
|
||||
"holdToMoveToBottom": "押し続けると一番下に移動します",
|
||||
"openApplicationDirectory": "アプリケーションディレクトリを開く",
|
||||
"selectRangeOfItems": "項目の範囲を選択",
|
||||
"addOrRemoveFromSelection": "選択に追加または削除"
|
||||
"addOrRemoveFromSelection": "選択に追加または削除",
|
||||
"goToCurrent": "現在の項目へ移動"
|
||||
},
|
||||
"common": {
|
||||
"backward": "戻る",
|
||||
@@ -447,7 +501,7 @@
|
||||
"name": "名前",
|
||||
"maximize": "最大化",
|
||||
"decrease": "減少",
|
||||
"ok": "OK",
|
||||
"ok": "Ok",
|
||||
"description": "説明",
|
||||
"configure": "設定",
|
||||
"path": "パス",
|
||||
@@ -463,8 +517,8 @@
|
||||
"setting_other": "設定",
|
||||
"version": "バージョン",
|
||||
"title": "タイトル",
|
||||
"filter_other": "フィルタ",
|
||||
"filters": "フィルタ",
|
||||
"filter_other": "フィルター",
|
||||
"filters": "フィルター",
|
||||
"create": "作成",
|
||||
"bitrate": "ビットレート",
|
||||
"saveAndReplace": "保存して変更",
|
||||
@@ -533,19 +587,23 @@
|
||||
"clean": "クリーン",
|
||||
"filter_single": "シングル",
|
||||
"filter_multiple": "複数枚組",
|
||||
"rename": "名前を変更"
|
||||
"rename": "名前を変更",
|
||||
"newVersionAvailable": "新しいバージョンが利用可能です",
|
||||
"numberOfResults": "{{numberOfResults}} 件の結果",
|
||||
"grouping": "グループ化"
|
||||
},
|
||||
"table": {
|
||||
"config": {
|
||||
"view": {
|
||||
"table": "テーブル",
|
||||
"table": "表",
|
||||
"grid": "グリッド",
|
||||
"list": "リスト"
|
||||
"list": "リスト",
|
||||
"detail": "詳細"
|
||||
},
|
||||
"general": {
|
||||
"displayType": "表示タイプ",
|
||||
"gap": "$t(common.gap)",
|
||||
"tableColumns": "テーブル カラム",
|
||||
"tableColumns": "テーブル列",
|
||||
"autoFitColumns": "カラム長を自動調整",
|
||||
"size": "$t(common.size)",
|
||||
"itemSize": "項目のサイズ (px)",
|
||||
@@ -565,7 +623,14 @@
|
||||
"size_compact": "コンパクト",
|
||||
"size_large": "大きい",
|
||||
"pagination_itemsPerPage": "ページあたりの項目数",
|
||||
"pagination_infinite": "無限"
|
||||
"pagination_infinite": "無限",
|
||||
"pagination": "ページネーション",
|
||||
"pagination_paginate": "ページ分割",
|
||||
"showHeader": "ヘッダーを表示",
|
||||
"verticalBorders": "列の境界線",
|
||||
"rowHoverHighlight": "行ホバーハイライト",
|
||||
"alternateRowColors": "交互の行の色",
|
||||
"horizontalBorders": "行の境界線"
|
||||
},
|
||||
"label": {
|
||||
"releaseDate": "発売日",
|
||||
@@ -574,7 +639,7 @@
|
||||
"titleCombined": "$t(common.title) (結合)",
|
||||
"dateAdded": "追加日",
|
||||
"size": "$t(common.size)",
|
||||
"bpm": "$t(common.bpm)",
|
||||
"bpm": "$t(common.BPM)",
|
||||
"lastPlayed": "最後に再生",
|
||||
"trackNumber": "トラック番号",
|
||||
"rowIndex": "行インデックス",
|
||||
@@ -602,7 +667,8 @@
|
||||
"image": "画像",
|
||||
"sampleRate": "$t(common.sampleRate)",
|
||||
"composer": "作曲家",
|
||||
"titleArtist": "$t(common.title) (アーティスト)"
|
||||
"titleArtist": "$t(common.title) (アーティスト)",
|
||||
"albumGroup": "アルバムグループ"
|
||||
}
|
||||
},
|
||||
"column": {
|
||||
@@ -666,7 +732,8 @@
|
||||
"saveQueueFailed": "キューを保存できませんでした",
|
||||
"settingsSyncError": "レンダラーとメインプロセスの設定に矛盾が見つかりました。変更を適用するにはアプリケーションを再起動してください",
|
||||
"invalidJson": "無効な JSON",
|
||||
"serverLockSingleServer": "サーバーがロックされている場合、1 つのサーバーのみが許可されます"
|
||||
"serverLockSingleServer": "サーバーがロックされている場合、1 つのサーバーのみが許可されます",
|
||||
"playbackPausedDueToError": "エラーのため再生が一時停止されました"
|
||||
},
|
||||
"filter": {
|
||||
"mostPlayed": "最も多く再生",
|
||||
@@ -712,7 +779,9 @@
|
||||
"id": "ID",
|
||||
"album": "$t(entity.album, {\"count\": 1})",
|
||||
"explicitStatus": "$t(common.explicitStatus)",
|
||||
"sortName": "ソート名"
|
||||
"sortName": "ソート名",
|
||||
"matchAnd": "すべて",
|
||||
"matchOr": "いずれか"
|
||||
},
|
||||
"page": {
|
||||
"sidebar": {
|
||||
@@ -884,7 +953,8 @@
|
||||
"groupingTypePrimary": "主なリリースタイプ",
|
||||
"favoriteSongs": "お気に入りの曲",
|
||||
"topSongsCommunity": "コミュニティ",
|
||||
"favoriteSongsFrom": "{{title}} のお気に入りの曲"
|
||||
"favoriteSongsFrom": "{{title}} のお気に入りの曲",
|
||||
"topSongsPersonal": "個人的"
|
||||
},
|
||||
"manageServers": {
|
||||
"title": "サーバーの管理",
|
||||
@@ -967,7 +1037,7 @@
|
||||
},
|
||||
"updateServer": {
|
||||
"title": "サーバーをアップデート",
|
||||
"success": "サーバーがアップデートされました"
|
||||
"success": "サーバーの更新に成功しました"
|
||||
},
|
||||
"queryEditor": {
|
||||
"input_optionMatchAll": "すべて一致",
|
||||
@@ -986,8 +1056,7 @@
|
||||
"editPlaylist": {
|
||||
"title": "$t(entity.playlist, {\"count\": 1}) を編集",
|
||||
"publicJellyfinNote": "Jellyfin では、何らかの理由でプレイリストが公開されているかどうかが表示されません。公開されたままにしたい場合は、以下の項目を選択してください",
|
||||
"success": "$t(entity.playlist, {\"count\": 1}) が正常に更新されました",
|
||||
"editNote": "大規模なプレイリストの場合、手動編集は推奨されません。既存のプレイリストを上書きすることでデータ損失が発生するリスクを許容しますか?"
|
||||
"success": "$t(entity.playlist, {\"count\": 1}) が正常に更新されました"
|
||||
},
|
||||
"shareItem": {
|
||||
"allowDownloading": "ダウンロードを許可",
|
||||
@@ -995,7 +1064,9 @@
|
||||
"setExpiration": "有効期限を設定",
|
||||
"success": "共有リンクがクリップボードにコピーされました (またはここをクリックして開きます)",
|
||||
"expireInvalid": "有効期限は将来の日時である必要があります",
|
||||
"createFailed": "共有リンクを作成できませんでした (共有は有効になっていますか?)"
|
||||
"createFailed": "共有リンクを作成できませんでした (共有は有効になっていますか?)",
|
||||
"copyToClipboard": "クリップボードにコピー: Ctrl+C、Enter",
|
||||
"successMustClick": "共有リンクが正常に作成されました。開くにはここをクリックしてください"
|
||||
},
|
||||
"privateMode": {
|
||||
"enabled": "プライベートモードが有効になりました。再生ステータスは外部連携から非表示になっています",
|
||||
@@ -1011,7 +1082,7 @@
|
||||
"title": "ラジオ局を作成",
|
||||
"input_homepageUrl": "ホームページ URL",
|
||||
"input_name": "名前",
|
||||
"input_streamUrl": "Stream URL"
|
||||
"input_streamUrl": "ストリーム URL"
|
||||
},
|
||||
"lyricsExport": {
|
||||
"export": "歌詞をエクスポート",
|
||||
@@ -1031,6 +1102,9 @@
|
||||
},
|
||||
"saveQueue": {
|
||||
"success": "プレイキューをサーバーに保存しました"
|
||||
},
|
||||
"editRadioStation": {
|
||||
"success": "ラジオ局の更新に成功しました"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@@ -1072,9 +1146,15 @@
|
||||
"audiobook": "オーディオブック",
|
||||
"audioDrama": "オーディオドラマ",
|
||||
"compilation": "コンピレーション",
|
||||
"djMix": "DJ Mix",
|
||||
"djMix": "DJ ミックス",
|
||||
"demo": "デモ",
|
||||
"soundtrack": "サウンドトラック"
|
||||
"soundtrack": "サウンドトラック",
|
||||
"fieldRecording": "フィールドレコーディング",
|
||||
"interview": "インタビュー",
|
||||
"live": "ライブ",
|
||||
"mixtape": "ミックステープ",
|
||||
"remix": "リミックス",
|
||||
"spokenWord": "スポークン・ワード"
|
||||
}
|
||||
},
|
||||
"datetime": {
|
||||
@@ -1110,6 +1190,157 @@
|
||||
},
|
||||
"visualizer": {
|
||||
"visualizerType": "ビジュアライザーの種類",
|
||||
"colors": "色"
|
||||
"colors": "色",
|
||||
"cyclePresets": "サイクルプリセット",
|
||||
"cycleTime": "サイクルタイム(秒)",
|
||||
"includeAllPresets": "すべてのプリセットを含める",
|
||||
"ignoredPresets": "無視されたプリセット",
|
||||
"selectedPresets": "選択されたプリセット",
|
||||
"randomizeNextPreset": "次のプリセットをランダム化",
|
||||
"blendTime": "ブレンド時間",
|
||||
"presets": "プリセット",
|
||||
"selectPreset": "プリセットを選択",
|
||||
"applyPreset": "プリセットを適用",
|
||||
"saveAsPreset": "プリセットとして保存",
|
||||
"updatePreset": "プリセットを更新",
|
||||
"copyConfiguration": "設定をコピーする",
|
||||
"pasteConfiguration": "設定を貼り付け",
|
||||
"pasteConfigurationPlaceholder": "ここに JSON 設定を貼り付けてください...",
|
||||
"pasteFromClipboard": "クリップボードから貼り付け",
|
||||
"applyConfiguration": "設定を適用",
|
||||
"configCopied": "設定をクリップボードにコピーしました",
|
||||
"configCopyFailed": "設定のコピーに失敗しました",
|
||||
"configPasted": "設定が正常に適用されました",
|
||||
"configPasteFailed": "設定の適用に失敗しました。形式を確認してください。",
|
||||
"configPasteReadFailed": "クリップボードからの読み取りに失敗しました",
|
||||
"presetName": "プリセット名",
|
||||
"presetNamePlaceholder": "プリセット名を入力",
|
||||
"general": "全般",
|
||||
"mode": "モード",
|
||||
"mode1To8": "モード 1 - 8",
|
||||
"mode10": "モード 10",
|
||||
"barSpace": "バースペース",
|
||||
"lineWidth": "線幅",
|
||||
"fillAlpha": "アルファ塗りつぶしを設定",
|
||||
"channelLayout": "チャンネルレイアウト",
|
||||
"maxFPS": "最大フレームレート",
|
||||
"opacity": "不透明度",
|
||||
"customGradients": "カスタムグラデーション",
|
||||
"addCustomGradient": "カスタムグラデーションを追加",
|
||||
"gradientName": "グラデーション名",
|
||||
"gradientNamePlaceholder": "グラデーション名",
|
||||
"vertical": "垂直",
|
||||
"horizontal": "水平",
|
||||
"colorStops": "カラー停止点の数",
|
||||
"addColor": "色を加える",
|
||||
"position": "位置",
|
||||
"level": "レベル",
|
||||
"remove": "取り除く",
|
||||
"pasteGradient": "グラデーションを貼り付け",
|
||||
"pasteGradientPlaceholder": "グラデーションの JSON をここに貼り付けてください...",
|
||||
"custom": "カスタム",
|
||||
"builtIn": "組み込み",
|
||||
"colorMode": "カラーモード",
|
||||
"gradient": "勾配",
|
||||
"gradientLeft": "左へのグラデーション",
|
||||
"gradientRight": "右へのグラデーション",
|
||||
"fft": "高速フーリエ変換",
|
||||
"fftSize": "FFT サイズ",
|
||||
"smoothing": "平滑化",
|
||||
"frequencyRangeAndScaling": "周波数範囲とスケーリング",
|
||||
"minimumFrequency": "最小周波数",
|
||||
"maximumFrequency": "最大周波数",
|
||||
"frequencyScale": "周波数スケール",
|
||||
"sensitivity": "感度",
|
||||
"weightingFilter": "重み付けフィルタ",
|
||||
"minimumDecibels": "最小デシベル",
|
||||
"maximumDecibels": "最大デシベル",
|
||||
"linearAmplitude": "線形振幅",
|
||||
"linearBoost": "リニアブースト",
|
||||
"peakBehavior": "ピーク時の振る舞い",
|
||||
"showPeaks": "ピークスを表示",
|
||||
"fadePeaks": "フェードピークス",
|
||||
"peakLine": "ピークライン",
|
||||
"gravity": "重力",
|
||||
"peakFadeTime": "ピークフェード時間(ミリ秒)",
|
||||
"peakHoldTime": "ピークホールド時間(ミリ秒)",
|
||||
"radialSpectrum": "放射状スペクトル",
|
||||
"radial": "ラジアル",
|
||||
"radialInvert": "放射状インバート",
|
||||
"spinSpeed": "回転速度",
|
||||
"radius": "半径",
|
||||
"reflexMirror": "反射鏡",
|
||||
"reflexFit": "リフレックス・フィット",
|
||||
"reflexRatio": "反射比",
|
||||
"reflexAlpha": "リフレックス・アルファ",
|
||||
"reflexBrightness": "反射輝度",
|
||||
"mirror": "鏡",
|
||||
"miscellaneousSettings": "その他の設定",
|
||||
"alphaBars": "アルファバー",
|
||||
"ansiBands": "ANSI バンド",
|
||||
"ledBars": "LED バー",
|
||||
"trueLeds": "真の LED",
|
||||
"lumiBars": "ルミ・バー",
|
||||
"outlineBars": "アウトラインバー",
|
||||
"roundBars": "丸棒",
|
||||
"lowResolution": "低解像度",
|
||||
"splitGradient": "分割グラデーション",
|
||||
"showFPS": "FPS を表示",
|
||||
"showScaleX": "X 軸スケールを表示",
|
||||
"noteLabels": "注釈ラベル",
|
||||
"showScaleY": "Y 軸スケールを表示",
|
||||
"options": {
|
||||
"mode": {
|
||||
"0": "[0] 離散周波数",
|
||||
"1": "[1] 1/24 オクターブ / 240 バンド",
|
||||
"2": "[2] 1/12 オクターブ / 120 バンド",
|
||||
"3": "[3] 1/8 オクターブ / 80 バンド",
|
||||
"4": "[4] 1/6 オクターブ / 60 バンド",
|
||||
"5": "[5] 1/4 オクターブ / 40 バンド",
|
||||
"6": "[6] 1/3 オクターブ / 30 バンド",
|
||||
"7": "[7] 半オクターブ / 20 バンド",
|
||||
"8": "[8] フルオクターブ / 10 バンド",
|
||||
"10": "[10] 折れ線グラフ / 面グラフ"
|
||||
},
|
||||
"colorMode": {
|
||||
"gradient": "勾配",
|
||||
"barIndex": "バー・インデックス",
|
||||
"barLevel": "バーレベル"
|
||||
},
|
||||
"gradient": {
|
||||
"classic": "クラシック",
|
||||
"prism": "プリズム",
|
||||
"rainbow": "虹",
|
||||
"steelblue": "スチールブルー",
|
||||
"orangered": "オレンジレッド"
|
||||
},
|
||||
"channelLayout": {
|
||||
"single": "シングル",
|
||||
"dualCombined": "デュアルコンバインド",
|
||||
"dualHorizontal": "デュアル水平",
|
||||
"dualVertical": "デュアルバーティカル"
|
||||
},
|
||||
"frequencyScale": {
|
||||
"none": "なし",
|
||||
"bark": "樹皮スケール",
|
||||
"linear": "線形スケール",
|
||||
"log": "対数スケール",
|
||||
"mel": "メル尺度"
|
||||
},
|
||||
"weightingFilter": {
|
||||
"none": "なし",
|
||||
"a": "A",
|
||||
"b": "B",
|
||||
"c": "C",
|
||||
"d": "D",
|
||||
"z": "Z"
|
||||
}
|
||||
},
|
||||
"systemAudioConsentAllow": "許可",
|
||||
"systemAudioConsentBody": "ビジュアライザーを機能させるためには、システムオーディオへのアクセスが必要です",
|
||||
"systemAudioConsentDecline": "拒否",
|
||||
"systemAudioConsentTitle": "システムオーディオへのアクセスを許可しますか?",
|
||||
"systemAudioCaptureFailed": "キャプチャを開始できませんでした: {{message}}",
|
||||
"systemAudioNoAudioTrack": "音声トラックが返されませんでした。プロンプトが表示されたら、音声キャプチャが有効になっていることを確認してください。"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"ascending": "오름차순",
|
||||
"areYouSure": "확실한가요?",
|
||||
"bitrate": "비트 전송률",
|
||||
"bpm": "bpm",
|
||||
"bpm": "BPM",
|
||||
"biography": "바이오그래피",
|
||||
"center": "중앙",
|
||||
"channel_other": "채널",
|
||||
@@ -127,7 +127,7 @@
|
||||
"filters": "필터",
|
||||
"noResultsFromQuery": "쿼리 결과가 없습니다",
|
||||
"note": "노트",
|
||||
"ok": "OK",
|
||||
"ok": "Ok",
|
||||
"owner": "소유자",
|
||||
"sampleRate": "샘플레이트",
|
||||
"tags": "태그",
|
||||
@@ -224,7 +224,7 @@
|
||||
"biography": "바이오그래피",
|
||||
"channels": "$t(common.channel_other)",
|
||||
"duration": "길이",
|
||||
"bpm": "bpm",
|
||||
"bpm": "BPM",
|
||||
"albumCount": "$t(entity.album, {\"count\": 2}) 앨범수",
|
||||
"comment": "코멘트",
|
||||
"favorited": "즐겨찾기",
|
||||
@@ -251,7 +251,7 @@
|
||||
"input_name": "서버 이름",
|
||||
"input_password": "비밀번호",
|
||||
"input_savePassword": "비밀번호 저장하기",
|
||||
"input_url": "url",
|
||||
"input_url": "URL",
|
||||
"error_savePassword": "비밀번호를 저장하는 도중 오류가 발생했습니다",
|
||||
"ignoreCors": "CORS 무시 ($t(common.restartRequired))",
|
||||
"ignoreSsl": "SSL 무시 ($t(common.restartRequired))",
|
||||
@@ -458,8 +458,8 @@
|
||||
"playSimilarSongs": "비슷한 곡 재생",
|
||||
"previous": "이전",
|
||||
"queue_clear": "재생 대기열 지우기",
|
||||
"queue_moveToBottom": "선택한 곡을 가장 위로 이동",
|
||||
"queue_moveToTop": "선택한 곡을 가장 아래로 이동",
|
||||
"queue_moveToBottom": "선택한 곡을 가장 아래로 이동",
|
||||
"queue_moveToTop": "선택한 곡을 가장 위로 이동",
|
||||
"queue_remove": "선택한 항목 삭제",
|
||||
"repeat": "반복",
|
||||
"repeat_all": "모두 반복하기",
|
||||
|
||||
+480
-460
File diff suppressed because it is too large
Load Diff
+920
-879
File diff suppressed because it is too large
Load Diff
+927
-892
File diff suppressed because it is too large
Load Diff
+408
-396
File diff suppressed because it is too large
Load Diff
+352
-351
@@ -1,165 +1,166 @@
|
||||
{
|
||||
"action": {
|
||||
"addToFavorites": "adicionar a $t(entity.favorite, {\"count\": 2})",
|
||||
"addToPlaylist": "adicionar a $t(entity.playlist, {\"count\": 1})",
|
||||
"clearQueue": "limpar fila",
|
||||
"createPlaylist": "criar $t(entity.playlist, {\"count\": 1})",
|
||||
"deletePlaylist": "apagar $t(entity.playlist, {\"count\": 1})",
|
||||
"deselectAll": "desmarcar todos",
|
||||
"editPlaylist": "editar $t(entity.playlist, {\"count\": 1})",
|
||||
"goToPage": "vá para página",
|
||||
"moveToNext": "mover para o próximo",
|
||||
"moveToBottom": "mover para baixo",
|
||||
"moveToTop": "mover para o topo",
|
||||
"addToFavorites": "Adicionar a $t(entity.favorite, {\"count\": 2})",
|
||||
"addToPlaylist": "Adicionar a $t(entity.playlist, {\"count\": 1})",
|
||||
"clearQueue": "Limpar fila",
|
||||
"createPlaylist": "Criar $t(entity.playlist, {\"count\": 1})",
|
||||
"deletePlaylist": "Apagar $t(entity.playlist, {\"count\": 1})",
|
||||
"deselectAll": "Desmarcar todos",
|
||||
"editPlaylist": "Editar $t(entity.playlist, {\"count\": 1})",
|
||||
"goToPage": "Vá para página",
|
||||
"moveToNext": "Mover para o próximo",
|
||||
"moveToBottom": "Mover para baixo",
|
||||
"moveToTop": "Mover para o topo",
|
||||
"refresh": "$t(common.refresh)",
|
||||
"removeFromFavorites": "remover de $t(entity.favorite, {\"count\": 2})",
|
||||
"removeFromPlaylist": "remover da $t(entity.playlist, {\"count\": 1})",
|
||||
"removeFromQueue": "remover da fila",
|
||||
"setRating": "definir classificação",
|
||||
"toggleSmartPlaylistEditor": "alternar editor $t(entity.smartPlaylist)",
|
||||
"viewPlaylists": "ver $t(entity.playlist, {\"count\": 2})",
|
||||
"removeFromFavorites": "Remover de $t(entity.favorite, {\"count\": 2})",
|
||||
"removeFromPlaylist": "Remover da $t(entity.playlist, {\"count\": 1})",
|
||||
"removeFromQueue": "Remover da fila",
|
||||
"setRating": "Definir classificação",
|
||||
"toggleSmartPlaylistEditor": "Alternar editor $t(entity.smartPlaylist)",
|
||||
"viewPlaylists": "Ver $t(entity.playlist, {\"count\": 2})",
|
||||
"openIn": {
|
||||
"lastfm": "Abrir em Last.fm",
|
||||
"musicbrainz": "Abrir em MusicBrainz"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"action_one": "ação",
|
||||
"action_one": "Ação",
|
||||
"action_many": "ações",
|
||||
"action_other": "ações",
|
||||
"add": "adicionar",
|
||||
"additionalParticipants": "participantes adicionais",
|
||||
"newVersion": "uma nova versão foi instalada ({{version}})",
|
||||
"viewReleaseNotes": "ver notas de lançamento",
|
||||
"albumGain": "ganho do álbum",
|
||||
"albumPeak": "pico do álbum",
|
||||
"areYouSure": "tem certeza?",
|
||||
"ascending": "ascendente",
|
||||
"backward": "para trás",
|
||||
"biography": "biografia",
|
||||
"bitrate": "taxa de bits",
|
||||
"bpm": "bpm",
|
||||
"cancel": "cancelar",
|
||||
"center": "centro",
|
||||
"channel_one": "canal",
|
||||
"action_other": "Ações",
|
||||
"add": "Adicionar",
|
||||
"additionalParticipants": "Participantes adicionais",
|
||||
"newVersion": "Uma nova versão foi instalada ({{version}})",
|
||||
"viewReleaseNotes": "Ver notas de lançamento",
|
||||
"albumGain": "Ganho do álbum",
|
||||
"albumPeak": "Pico do álbum",
|
||||
"areYouSure": "Tem certeza?",
|
||||
"ascending": "Ascendente",
|
||||
"backward": "Para trás",
|
||||
"biography": "Biografia",
|
||||
"bitrate": "Taxa de bits",
|
||||
"bpm": "Bpm",
|
||||
"cancel": "Cancelar",
|
||||
"center": "Centro",
|
||||
"channel_one": "Canal",
|
||||
"channel_many": "canais",
|
||||
"channel_other": "canais",
|
||||
"clear": "limpar",
|
||||
"close": "fechar",
|
||||
"codec": "codec",
|
||||
"collapse": "minimizar",
|
||||
"comingSoon": "em breve…",
|
||||
"configure": "configurar",
|
||||
"confirm": "confirmar",
|
||||
"create": "criar",
|
||||
"channel_other": "Canais",
|
||||
"clear": "Limpar",
|
||||
"close": "Fechar",
|
||||
"codec": "Codec",
|
||||
"collapse": "Minimizar",
|
||||
"comingSoon": "Em breve…",
|
||||
"configure": "Configurar",
|
||||
"confirm": "Confirmar",
|
||||
"create": "Criar",
|
||||
"currentSong": "$t(entity.track, {\"count\": 1}) atual",
|
||||
"decrease": "diminuir",
|
||||
"delete": "apagar",
|
||||
"descending": "abaixar",
|
||||
"description": "descrição",
|
||||
"disable": "desativar",
|
||||
"disc": "disco",
|
||||
"dismiss": "liberar",
|
||||
"duration": "duração",
|
||||
"edit": "editar",
|
||||
"enable": "ativar",
|
||||
"expand": "expandir",
|
||||
"favorite": "favorito",
|
||||
"filter_one": "filtro",
|
||||
"decrease": "Diminuir",
|
||||
"delete": "Apagar",
|
||||
"descending": "Abaixar",
|
||||
"description": "Descrição",
|
||||
"disable": "Desativar",
|
||||
"disc": "Disco",
|
||||
"dismiss": "Liberar",
|
||||
"duration": "Duração",
|
||||
"edit": "Editar",
|
||||
"enable": "Ativar",
|
||||
"expand": "Expandir",
|
||||
"favorite": "Favorito",
|
||||
"filter_one": "Filtro",
|
||||
"filter_many": "filtros",
|
||||
"filter_other": "filtros",
|
||||
"filters": "filtros",
|
||||
"forceRestartRequired": "reinicie para aplicar as alterações… feche a notificação para reiniciar",
|
||||
"forward": "para frente",
|
||||
"gap": "intervalo",
|
||||
"home": "início",
|
||||
"increase": "incrementar",
|
||||
"left": "esquerda",
|
||||
"limit": "limite",
|
||||
"manage": "gerir",
|
||||
"maximize": "maximizar",
|
||||
"menu": "menu",
|
||||
"minimize": "minimizar",
|
||||
"modified": "modificado",
|
||||
"filter_other": "Filtros",
|
||||
"filters": "Filtros",
|
||||
"forceRestartRequired": "Reinicie para aplicar as alterações… feche a notificação para reiniciar",
|
||||
"forward": "Para frente",
|
||||
"gap": "Intervalo",
|
||||
"grouping": "Agrupamento",
|
||||
"home": "Início",
|
||||
"increase": "Incrementar",
|
||||
"left": "Esquerda",
|
||||
"limit": "Limite",
|
||||
"manage": "Gerir",
|
||||
"maximize": "Maximizar",
|
||||
"menu": "Menu",
|
||||
"minimize": "Minimizar",
|
||||
"modified": "Modificado",
|
||||
"mbid": "ID no MusicBrainz",
|
||||
"name": "nome",
|
||||
"no": "não",
|
||||
"none": "nenhum",
|
||||
"noResultsFromQuery": "a consulta não retornou resultados",
|
||||
"note": "observação",
|
||||
"ok": "ok",
|
||||
"owner": "dono",
|
||||
"path": "caminho",
|
||||
"playerMustBePaused": "o player deve estar pausado",
|
||||
"preview": "pré-visualizar",
|
||||
"previousSong": "anterior $t(entity.track, {\"count\": 1})",
|
||||
"quit": "sair",
|
||||
"random": "aleatório",
|
||||
"rating": "classificação",
|
||||
"refresh": "atualizar",
|
||||
"reload": "recarregar",
|
||||
"reset": "reiniciar",
|
||||
"resetToDefault": "restaurar ao padrão",
|
||||
"restartRequired": "é necessário reiniciar",
|
||||
"right": "direita",
|
||||
"save": "gravar",
|
||||
"saveAndReplace": "gravar e substituir",
|
||||
"saveAs": "gravar como",
|
||||
"search": "procurar",
|
||||
"setting_one": "configuração",
|
||||
"name": "Nome",
|
||||
"no": "Não",
|
||||
"none": "Nenhum",
|
||||
"noResultsFromQuery": "A consulta não retornou resultados",
|
||||
"note": "Observação",
|
||||
"ok": "Ok",
|
||||
"owner": "Dono",
|
||||
"path": "Caminho",
|
||||
"playerMustBePaused": "O player deve estar pausado",
|
||||
"preview": "Pré-visualizar",
|
||||
"previousSong": "Anterior $t(entity.track, {\"count\": 1})",
|
||||
"quit": "Sair",
|
||||
"random": "Aleatório",
|
||||
"rating": "Classificação",
|
||||
"refresh": "Atualizar",
|
||||
"reload": "Recarregar",
|
||||
"reset": "Reiniciar",
|
||||
"resetToDefault": "Restaurar ao padrão",
|
||||
"restartRequired": "É necessário reiniciar",
|
||||
"right": "Direita",
|
||||
"save": "Gravar",
|
||||
"saveAndReplace": "Gravar e substituir",
|
||||
"saveAs": "Gravar como",
|
||||
"search": "Procurar",
|
||||
"setting_one": "Configuração",
|
||||
"setting_many": "",
|
||||
"setting_other": "",
|
||||
"share": "partilhar",
|
||||
"size": "tamanho",
|
||||
"sortOrder": "ordem",
|
||||
"tags": "tags",
|
||||
"title": "titulo",
|
||||
"trackNumber": "faixa",
|
||||
"trackGain": "ganho da faixa",
|
||||
"trackPeak": "pico da faixa",
|
||||
"translation": "tradução",
|
||||
"unknown": "desconhecido",
|
||||
"version": "versão",
|
||||
"year": "ano",
|
||||
"yes": "sim"
|
||||
"share": "Partilhar",
|
||||
"size": "Tamanho",
|
||||
"sortOrder": "Ordem",
|
||||
"tags": "Tags",
|
||||
"title": "Titulo",
|
||||
"trackNumber": "Faixa",
|
||||
"trackGain": "Ganho da faixa",
|
||||
"trackPeak": "Pico da faixa",
|
||||
"translation": "Tradução",
|
||||
"unknown": "Desconhecido",
|
||||
"version": "Versão",
|
||||
"year": "Ano",
|
||||
"yes": "Sim"
|
||||
},
|
||||
"entity": {
|
||||
"album_one": "álbum",
|
||||
"album_one": "Álbum",
|
||||
"album_many": "álbuns",
|
||||
"album_other": "álbuns",
|
||||
"albumArtist_one": "artista do álbum",
|
||||
"album_other": "Álbuns",
|
||||
"albumArtist_one": "Artista do álbum",
|
||||
"albumArtist_many": "artistas do álbum",
|
||||
"albumArtist_other": "artistas do álbum",
|
||||
"albumArtist_other": "Artistas do álbum",
|
||||
"albumArtistCount_one": "{{count}} artista do álbum",
|
||||
"albumArtistCount_many": "{{count}} artistas do álbum",
|
||||
"albumArtistCount_other": "{{count}} artistas do álbum",
|
||||
"albumWithCount_one": "{{count}} álbum",
|
||||
"albumWithCount_many": "{{count}} álbuns",
|
||||
"albumWithCount_other": "{{count}} álbuns",
|
||||
"artist_one": "artista",
|
||||
"artist_one": "Artista",
|
||||
"artist_many": "artistas",
|
||||
"artist_other": "artistas",
|
||||
"artist_other": "Artistas",
|
||||
"artistWithCount_one": "{{count}} artista",
|
||||
"artistWithCount_many": "{{count}} artistas",
|
||||
"artistWithCount_other": "{{count}} artistas",
|
||||
"favorite_one": "favorito",
|
||||
"favorite_one": "Favorito",
|
||||
"favorite_many": "favoritos",
|
||||
"favorite_other": "favoritos",
|
||||
"folder_one": "pasta",
|
||||
"favorite_other": "Favoritos",
|
||||
"folder_one": "Pasta",
|
||||
"folder_many": "pastas",
|
||||
"folder_other": "pastas",
|
||||
"folder_other": "Pastas",
|
||||
"folderWithCount_one": "{{count}} pasta",
|
||||
"folderWithCount_many": "{{count}} pastas",
|
||||
"folderWithCount_other": "{{count}} pastas",
|
||||
"genre_one": "gênero",
|
||||
"genre_one": "Gênero",
|
||||
"genre_many": "gêneros",
|
||||
"genre_other": "gêneros",
|
||||
"genre_other": "Gêneros",
|
||||
"genreWithCount_one": "{{count}} gênero",
|
||||
"genreWithCount_many": "{{count}} gêneros",
|
||||
"genreWithCount_other": "{{count}} gêneros",
|
||||
"playlist_one": "playlist",
|
||||
"playlist_one": "Playlist",
|
||||
"playlist_many": "playlists",
|
||||
"playlist_other": "playlists",
|
||||
"playlist_other": "Playlists",
|
||||
"play_one": "{{count}} reprodução",
|
||||
"play_many": "{{count}} reproduções",
|
||||
"play_other": "{{count}} reproduções",
|
||||
@@ -167,189 +168,189 @@
|
||||
"playlistWithCount_many": "{{count}} playlists",
|
||||
"playlistWithCount_other": "{{count}} playlists",
|
||||
"smartPlaylist": "$t(entity.playlist, {\"count\": 1}) inteligente",
|
||||
"track_one": "faixa",
|
||||
"track_one": "Faixa",
|
||||
"track_many": "faixas",
|
||||
"track_other": "faixas",
|
||||
"song_one": "música",
|
||||
"track_other": "Faixas",
|
||||
"song_one": "Música",
|
||||
"song_many": "músicas",
|
||||
"song_other": "músicas",
|
||||
"song_other": "Músicas",
|
||||
"trackWithCount_one": "{{count}} faixa",
|
||||
"trackWithCount_many": "{{count}} faixas",
|
||||
"trackWithCount_other": "{{count}} faixas"
|
||||
},
|
||||
"error": {
|
||||
"apiRouteError": "não é possível encaminhar a solicitação",
|
||||
"audioDeviceFetchError": "ocorreu um erro ao tentar obter dispositivos de áudio",
|
||||
"authenticationFailed": "falha na autenticação",
|
||||
"badAlbum": "está a ver este erro por que está música não é parte de algum album. um motivo comum para si estar a ver este erro é se a sua música estiver na raiz da sua pasta de músicas. o Jellyfin apenas agrupa as músicas se elas estiveram na mesma pasta",
|
||||
"badValue": "opção inválida \"{{value}}\". este valor não existe no momento",
|
||||
"credentialsRequired": "credenciais necessárias",
|
||||
"endpointNotImplementedError": "endpoint {{endpoint}} não está implementado para {{serverType}}",
|
||||
"genericError": "um erro ocorreu",
|
||||
"invalidServer": "servidor inválido",
|
||||
"localFontAccessDenied": "acesso a fontes locais rejeitado",
|
||||
"loginRateError": "muitas tentativas de login, tente novamente em alguns segundos",
|
||||
"apiRouteError": "Não é possível encaminhar a solicitação",
|
||||
"audioDeviceFetchError": "Ocorreu um erro ao tentar obter dispositivos de áudio",
|
||||
"authenticationFailed": "Falha na autenticação",
|
||||
"badAlbum": "Está a ver este erro por que está música não é parte de algum album. um motivo comum para si estar a ver este erro é se a sua música estiver na raiz da sua pasta de músicas. o Jellyfin apenas agrupa as músicas se elas estiveram na mesma pasta",
|
||||
"badValue": "Opção inválida \"{{value}}\". este valor não existe no momento",
|
||||
"credentialsRequired": "Credenciais necessárias",
|
||||
"endpointNotImplementedError": "Endpoint {{endpoint}} não está implementado para {{serverType}}",
|
||||
"genericError": "Um erro ocorreu",
|
||||
"invalidServer": "Servidor inválido",
|
||||
"localFontAccessDenied": "Acesso a fontes locais rejeitado",
|
||||
"loginRateError": "Muitas tentativas de login, tente novamente em alguns segundos",
|
||||
"mpvRequired": "MPV necessário",
|
||||
"networkError": "ocorreu um erro na internet",
|
||||
"openError": "não foi possível abrir o ficheiro",
|
||||
"playbackError": "ocorreu um erro ao tentar reproduzir a média",
|
||||
"remoteDisableError": "ocorreu um erro ao tentar $t(common.disable) o servidor remoto",
|
||||
"remoteEnableError": "ocorreu um erro ao tentar $t(common.enable) o servidor remoto",
|
||||
"remotePortError": "ocorreu um erro ao tentar definir a porta do servidor remoto",
|
||||
"remotePortWarning": "reinicie o servidor para aplicar a nova porta",
|
||||
"serverNotSelectedError": "nenhum servidor selecionado",
|
||||
"serverRequired": "servidor necessário",
|
||||
"sessionExpiredError": "a sua sessão expirou",
|
||||
"systemFontError": "ocorreu um erro ao tentar obter fontes do sistema"
|
||||
"networkError": "Ocorreu um erro na internet",
|
||||
"openError": "Não foi possível abrir o ficheiro",
|
||||
"playbackError": "Ocorreu um erro ao tentar reproduzir a média",
|
||||
"remoteDisableError": "Ocorreu um erro ao tentar $t(common.disable) o servidor remoto",
|
||||
"remoteEnableError": "Ocorreu um erro ao tentar $t(common.enable) o servidor remoto",
|
||||
"remotePortError": "Ocorreu um erro ao tentar definir a porta do servidor remoto",
|
||||
"remotePortWarning": "Reinicie o servidor para aplicar a nova porta",
|
||||
"serverNotSelectedError": "Nenhum servidor selecionado",
|
||||
"serverRequired": "Servidor necessário",
|
||||
"sessionExpiredError": "A sua sessão expirou",
|
||||
"systemFontError": "Ocorreu um erro ao tentar obter fontes do sistema"
|
||||
},
|
||||
"filter": {
|
||||
"album": "$t(entity.album, {\"count\": 1})",
|
||||
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
|
||||
"albumCount": "número de $t(entity.album, {\"count\": 2})",
|
||||
"albumCount": "Número de $t(entity.album, {\"count\": 2})",
|
||||
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||
"biography": "bibliografia",
|
||||
"bitrate": "bitrate",
|
||||
"bpm": "bpm",
|
||||
"biography": "Bibliografia",
|
||||
"bitrate": "Bitrate",
|
||||
"bpm": "Bpm",
|
||||
"channels": "$t(common.channel_other)",
|
||||
"comment": "comentário",
|
||||
"comment": "Comentário",
|
||||
"communityRating": "Nota da comunidade",
|
||||
"criticRating": "avaliação da crítica",
|
||||
"dateAdded": "data de adição",
|
||||
"disc": "disco",
|
||||
"duration": "duração",
|
||||
"favorited": "favoritado",
|
||||
"fromYear": "a partir do ano",
|
||||
"criticRating": "Avaliação da crítica",
|
||||
"dateAdded": "Data de adição",
|
||||
"disc": "Disco",
|
||||
"duration": "Duração",
|
||||
"favorited": "Favoritado",
|
||||
"fromYear": "A partir do ano",
|
||||
"genre": "$t(entity.genre, {\"count\": 1})",
|
||||
"id": "id",
|
||||
"isCompilation": "é compilação",
|
||||
"isFavorited": "é favoritado",
|
||||
"isPublic": "é público",
|
||||
"isRated": "possui avaliação",
|
||||
"isRecentlyPlayed": "foi tocado recentemente",
|
||||
"lastPlayed": "última tocada",
|
||||
"mostPlayed": "mais tocado",
|
||||
"name": "nome",
|
||||
"note": "nota",
|
||||
"id": "Id",
|
||||
"isCompilation": "É compilação",
|
||||
"isFavorited": "É favoritado",
|
||||
"isPublic": "É público",
|
||||
"isRated": "Possui avaliação",
|
||||
"isRecentlyPlayed": "Foi tocado recentemente",
|
||||
"lastPlayed": "Última tocada",
|
||||
"mostPlayed": "Mais tocado",
|
||||
"name": "Nome",
|
||||
"note": "Nota",
|
||||
"owner": "$t(common.owner)",
|
||||
"path": "caminho",
|
||||
"playCount": "contador de reproduções",
|
||||
"random": "aleatório",
|
||||
"rating": "avaliação",
|
||||
"recentlyAdded": "adicionado recentemente",
|
||||
"recentlyPlayed": "tocado recentemente",
|
||||
"recentlyUpdated": "atualizado recentemente",
|
||||
"releaseDate": "data de lançamento",
|
||||
"releaseYear": "ano de lançamento",
|
||||
"search": "buscar",
|
||||
"songCount": "contador de músicas",
|
||||
"title": "titulo",
|
||||
"toYear": "até o ano",
|
||||
"trackNumber": "faixa"
|
||||
"path": "Caminho",
|
||||
"playCount": "Contador de reproduções",
|
||||
"random": "Aleatório",
|
||||
"rating": "Avaliação",
|
||||
"recentlyAdded": "Adicionado recentemente",
|
||||
"recentlyPlayed": "Tocado recentemente",
|
||||
"recentlyUpdated": "Atualizado recentemente",
|
||||
"releaseDate": "Data de lançamento",
|
||||
"releaseYear": "Ano de lançamento",
|
||||
"search": "Buscar",
|
||||
"songCount": "Contador de músicas",
|
||||
"title": "Titulo",
|
||||
"toYear": "Até o ano",
|
||||
"trackNumber": "Faixa"
|
||||
},
|
||||
"form": {
|
||||
"addServer": {
|
||||
"error_savePassword": "um erro ocorreu ao tentar gravar a palavra-passe",
|
||||
"ignoreCors": "ignorar CORS ($t(common.restartRequired))",
|
||||
"ignoreSsl": "ignorar ssl ($t(common.restartRequired))",
|
||||
"input_legacyAuthentication": "ativar autenticação legada",
|
||||
"input_name": "nome do servidor",
|
||||
"input_password": "palavra-passe",
|
||||
"input_savePassword": "gravar palavra-passe",
|
||||
"input_url": "url",
|
||||
"input_username": "nome de utilizador",
|
||||
"success": "servidor adicionado com sucesso",
|
||||
"title": "adicionar servidor"
|
||||
"error_savePassword": "Um erro ocorreu ao tentar gravar a palavra-passe",
|
||||
"ignoreCors": "Ignorar CORS ($t(common.restartRequired))",
|
||||
"ignoreSsl": "Ignorar ssl ($t(common.restartRequired))",
|
||||
"input_legacyAuthentication": "Ativar autenticação legada",
|
||||
"input_name": "Nome do servidor",
|
||||
"input_password": "Palavra-passe",
|
||||
"input_savePassword": "Gravar palavra-passe",
|
||||
"input_url": "Url",
|
||||
"input_username": "Nome de utilizador",
|
||||
"success": "Servidor adicionado com sucesso",
|
||||
"title": "Adicionar servidor"
|
||||
},
|
||||
"addToPlaylist": {
|
||||
"input_playlists": "$t(entity.playlist, {\"count\": 2})",
|
||||
"input_skipDuplicates": "pular duplicadas",
|
||||
"success": "adicionado $t(entity.trackWithCount, {\"count\": {{message}} }) para $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
||||
"title": "adicionar à $t(entity.playlist, {\"count\": 1})"
|
||||
"input_skipDuplicates": "Pular duplicadas",
|
||||
"success": "Adicionado $t(entity.trackWithCount, {\"count\": {{message}} }) para $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
||||
"title": "Adicionar à $t(entity.playlist, {\"count\": 1})"
|
||||
},
|
||||
"createPlaylist": {
|
||||
"input_description": "$t(common.description)",
|
||||
"input_name": "$t(common.name)",
|
||||
"input_owner": "$t(common.owner)",
|
||||
"input_public": "público",
|
||||
"input_public": "Público",
|
||||
"success": "$t(entity.playlist, {\"count\": 1}) criada com sucesso",
|
||||
"title": "criar $t(entity.playlist, {\"count\": 1})"
|
||||
"title": "Criar $t(entity.playlist, {\"count\": 1})"
|
||||
},
|
||||
"deletePlaylist": {
|
||||
"input_confirm": "escreva o nome da $t(entity.playlist, {\"count\": 1}) para confirmar",
|
||||
"input_confirm": "Escreva o nome da $t(entity.playlist, {\"count\": 1}) para confirmar",
|
||||
"success": "$t(entity.playlist, {\"count\": 1}) apagada com sucesso",
|
||||
"title": "apagar $t(entity.playlist, {\"count\": 1})"
|
||||
"title": "Apagar $t(entity.playlist, {\"count\": 1})"
|
||||
},
|
||||
"editPlaylist": {
|
||||
"publicJellyfinNote": "O Jellyfin por algum motivo não expõe se uma playlist é pública ou não. Se deseja que ela permaneça pública, por favor selecione a seguinte entrada",
|
||||
"success": "$t(entity.playlist, {\"count\": 1}) atualizada com sucesso",
|
||||
"title": "editar $t(entity.playlist, {\"count\": 1})"
|
||||
"title": "Editar $t(entity.playlist, {\"count\": 1})"
|
||||
},
|
||||
"lyricSearch": {
|
||||
"input_artist": "$t(entity.artist, {\"count\": 1})",
|
||||
"input_name": "$t(common.name)",
|
||||
"title": "pesquisa de letras"
|
||||
"title": "Pesquisa de letras"
|
||||
},
|
||||
"queryEditor": {
|
||||
"input_optionMatchAll": "corresponder todos",
|
||||
"input_optionMatchAny": "corresponder qualquer um"
|
||||
"input_optionMatchAll": "Corresponder todos",
|
||||
"input_optionMatchAny": "Corresponder qualquer um"
|
||||
},
|
||||
"shareItem": {
|
||||
"allowDownloading": "permitir descargas",
|
||||
"description": "descrição",
|
||||
"setExpiration": "definir expiração",
|
||||
"success": "ligação de compartilhamento copiado para a área de transferência (ou clique aqui para abrir)",
|
||||
"expireInvalid": "a expiração deve ser uma data futura",
|
||||
"createFailed": "falha ao criar compartilhamento (o compartilhamento está ativado?)"
|
||||
"allowDownloading": "Permitir descargas",
|
||||
"description": "Descrição",
|
||||
"setExpiration": "Definir expiração",
|
||||
"success": "Ligação de compartilhamento copiado para a área de transferência (ou clique aqui para abrir)",
|
||||
"expireInvalid": "A expiração deve ser uma data futura",
|
||||
"createFailed": "Falha ao criar compartilhamento (o compartilhamento está ativado?)"
|
||||
},
|
||||
"updateServer": {
|
||||
"success": "servidor atualizado com sucesso",
|
||||
"title": "atualizar servidor"
|
||||
"success": "Servidor atualizado com sucesso",
|
||||
"title": "Atualizar servidor"
|
||||
}
|
||||
},
|
||||
"page": {
|
||||
"albumArtistDetail": {
|
||||
"about": "Sobre {{artist}}",
|
||||
"appearsOn": "aparece em",
|
||||
"recentReleases": "lançamentos recentes",
|
||||
"viewDiscography": "ver discografia",
|
||||
"appearsOn": "Aparece em",
|
||||
"recentReleases": "Lançamentos recentes",
|
||||
"viewDiscography": "Ver discografia",
|
||||
"relatedArtists": "$t(entity.artist, {\"count\": 2}) relacionados",
|
||||
"topSongs": "músicas mais tocadas",
|
||||
"topSongsFrom": "músicas mais tocadas de {{title}}",
|
||||
"viewAll": "ver tudo",
|
||||
"viewAllTracks": "ver todas as $t(entity.track, {\"count\": 2})"
|
||||
"topSongs": "Músicas mais tocadas",
|
||||
"topSongsFrom": "Músicas mais tocadas de {{title}}",
|
||||
"viewAll": "Ver tudo",
|
||||
"viewAllTracks": "Ver todas as $t(entity.track, {\"count\": 2})"
|
||||
},
|
||||
"albumArtistList": {
|
||||
"title": "$t(entity.albumArtist, {\"count\": 2})"
|
||||
},
|
||||
"albumDetail": {
|
||||
"moreFromArtist": "mais deste $t(entity.artist, {\"count\": 1})",
|
||||
"moreFromGeneric": "mais que {{item}}",
|
||||
"released": "lançado"
|
||||
"moreFromArtist": "Mais deste $t(entity.artist, {\"count\": 1})",
|
||||
"moreFromGeneric": "Mais que {{item}}",
|
||||
"released": "Lançado"
|
||||
},
|
||||
"albumList": {
|
||||
"artistAlbums": "álbuns de {{artist}}",
|
||||
"artistAlbums": "Álbuns de {{artist}}",
|
||||
"genreAlbums": "\"{{genre}}\" $t(entity.album, {\"count\": 2})",
|
||||
"title": "$t(entity.album, {\"count\": 2})"
|
||||
},
|
||||
"appMenu": {
|
||||
"collapseSidebar": "recolher barra lateral",
|
||||
"expandSidebar": "expandir barra lateral",
|
||||
"goBack": "voltar",
|
||||
"goForward": "avançar",
|
||||
"manageServers": "gerir servidores",
|
||||
"openBrowserDevtools": "abrir ferramentas do programador",
|
||||
"collapseSidebar": "Recolher barra lateral",
|
||||
"expandSidebar": "Expandir barra lateral",
|
||||
"goBack": "Voltar",
|
||||
"goForward": "Avançar",
|
||||
"manageServers": "Gerir servidores",
|
||||
"openBrowserDevtools": "Abrir ferramentas do programador",
|
||||
"quit": "$t(common.quit)",
|
||||
"selectServer": "selecionar servidor",
|
||||
"selectServer": "Selecionar servidor",
|
||||
"settings": "$t(common.setting, {\"count\": 2})",
|
||||
"version": "versão {{version}}"
|
||||
"version": "Versão {{version}}"
|
||||
},
|
||||
"manageServers": {
|
||||
"title": "gerir servidores",
|
||||
"serverDetails": "pormenores do servidor",
|
||||
"title": "Gerir servidores",
|
||||
"serverDetails": "Pormenores do servidor",
|
||||
"url": "URL",
|
||||
"username": "nome de utilizador",
|
||||
"editServerDetailsTooltip": "editar pormenores do servidor",
|
||||
"removeServer": "remover servidor"
|
||||
"username": "Nome de utilizador",
|
||||
"editServerDetailsTooltip": "Editar pormenores do servidor",
|
||||
"removeServer": "Remover servidor"
|
||||
},
|
||||
"contextMenu": {
|
||||
"addFavorite": "$t(action.addToFavorites)",
|
||||
@@ -360,7 +361,7 @@
|
||||
"createPlaylist": "$t(action.createPlaylist)",
|
||||
"deletePlaylist": "$t(action.deletePlaylist)",
|
||||
"deselectAll": "$t(action.deselectAll)",
|
||||
"download": "descarregar",
|
||||
"download": "Descarregar",
|
||||
"moveToNext": "$t(action.moveToNext)",
|
||||
"moveToBottom": "$t(action.moveToBottom)",
|
||||
"moveToTop": "$t(action.moveToTop)",
|
||||
@@ -372,69 +373,69 @@
|
||||
"removeFromQueue": "$t(action.removeFromQueue)",
|
||||
"setRating": "$t(action.setRating)",
|
||||
"playShuffled": "$t(player.shuffle)",
|
||||
"shareItem": "partilhar elemento",
|
||||
"showDetails": "obter informações"
|
||||
"shareItem": "Partilhar elemento",
|
||||
"showDetails": "Obter informações"
|
||||
},
|
||||
"fullscreenPlayer": {
|
||||
"config": {
|
||||
"dynamicBackground": "fundo dinâmico",
|
||||
"dynamicImageBlur": "tamanho do desfoque da imagem",
|
||||
"dynamicIsImage": "ativar imagem de fundo",
|
||||
"followCurrentLyric": "acompanhar letra",
|
||||
"lyricAlignment": "alinhamento da letra",
|
||||
"lyricOffset": "deslocamento da letra (ms)",
|
||||
"lyricGap": "espaçamento da letra",
|
||||
"lyricSize": "tamanho da letra",
|
||||
"opacity": "opacidade",
|
||||
"showLyricMatch": "exibir correspondência da letra",
|
||||
"showLyricProvider": "exibir origem da letra",
|
||||
"synchronized": "sincronizado",
|
||||
"unsynchronized": "não sincronizado",
|
||||
"useImageAspectRatio": "usar proporção da imagem"
|
||||
"dynamicBackground": "Fundo dinâmico",
|
||||
"dynamicImageBlur": "Tamanho do desfoque da imagem",
|
||||
"dynamicIsImage": "Ativar imagem de fundo",
|
||||
"followCurrentLyric": "Acompanhar letra",
|
||||
"lyricAlignment": "Alinhamento da letra",
|
||||
"lyricOffset": "Deslocamento da letra (ms)",
|
||||
"lyricGap": "Espaçamento da letra",
|
||||
"lyricSize": "Tamanho da letra",
|
||||
"opacity": "Opacidade",
|
||||
"showLyricMatch": "Exibir correspondência da letra",
|
||||
"showLyricProvider": "Exibir origem da letra",
|
||||
"synchronized": "Sincronizado",
|
||||
"unsynchronized": "Não sincronizado",
|
||||
"useImageAspectRatio": "Usar proporção da imagem"
|
||||
},
|
||||
"lyrics": "letra",
|
||||
"related": "relacionado",
|
||||
"upNext": "a seguir",
|
||||
"visualizer": "visualizador",
|
||||
"noLyrics": "nenhuma letra encontrada"
|
||||
"lyrics": "Letra",
|
||||
"related": "Relacionado",
|
||||
"upNext": "A seguir",
|
||||
"visualizer": "Visualizador",
|
||||
"noLyrics": "Nenhuma letra encontrada"
|
||||
},
|
||||
"genreList": {
|
||||
"showAlbums": "mostrar $t(entity.genre, {\"count\": 1}) $t(entity.album, {\"count\": 2})",
|
||||
"showTracks": "mostrar $t(entity.genre, {\"count\": 1}) $t(entity.track, {\"count\": 2})",
|
||||
"showAlbums": "Mostrar $t(entity.genre, {\"count\": 1}) $t(entity.album, {\"count\": 2})",
|
||||
"showTracks": "Mostrar $t(entity.genre, {\"count\": 1}) $t(entity.track, {\"count\": 2})",
|
||||
"title": "$t(entity.genre, {\"count\": 2})"
|
||||
},
|
||||
"globalSearch": {
|
||||
"commands": {
|
||||
"goToPage": "ir à página",
|
||||
"searchFor": "procurar {{query}}",
|
||||
"serverCommands": "comandos do servidor"
|
||||
"goToPage": "Ir à página",
|
||||
"searchFor": "Procurar {{query}}",
|
||||
"serverCommands": "Comandos do servidor"
|
||||
},
|
||||
"title": "comandos"
|
||||
"title": "Comandos"
|
||||
},
|
||||
"home": {
|
||||
"explore": "explore a sua biblioteca",
|
||||
"mostPlayed": "mais tocado",
|
||||
"newlyAdded": "lançamentos recém-adicionados",
|
||||
"recentlyPlayed": "tocado recentemente",
|
||||
"explore": "Explore a sua biblioteca",
|
||||
"mostPlayed": "Mais tocado",
|
||||
"newlyAdded": "Lançamentos recém-adicionados",
|
||||
"recentlyPlayed": "Tocado recentemente",
|
||||
"title": "$t(common.home)"
|
||||
},
|
||||
"itemDetail": {
|
||||
"copyPath": "copiar caminho para a área de transferência",
|
||||
"copiedPath": "caminho copiado com sucesso",
|
||||
"openFile": "mostrar faixa no gestor de ficheiros"
|
||||
"copyPath": "Copiar caminho para a área de transferência",
|
||||
"copiedPath": "Caminho copiado com sucesso",
|
||||
"openFile": "Mostrar faixa no gestor de ficheiros"
|
||||
},
|
||||
"playlist": {
|
||||
"reorder": "reordenar apenas disponível quando ordenado pelo id"
|
||||
"reorder": "Reordenar apenas disponível quando ordenado pelo ID"
|
||||
},
|
||||
"playlistList": {
|
||||
"title": "$t(entity.playlist, {\"count\": 2})"
|
||||
},
|
||||
"setting": {
|
||||
"advanced": "avançado",
|
||||
"generalTab": "geral",
|
||||
"hotkeysTab": "teclas de atalho",
|
||||
"playbackTab": "reprodução",
|
||||
"windowTab": "janela"
|
||||
"advanced": "Avançado",
|
||||
"generalTab": "Geral",
|
||||
"hotkeysTab": "Teclas de atalho",
|
||||
"playbackTab": "Reprodução",
|
||||
"windowTab": "Janela"
|
||||
},
|
||||
"sidebar": {
|
||||
"albumArtists": "$t(entity.albumArtist, {\"count\": 2})",
|
||||
@@ -443,8 +444,8 @@
|
||||
"folders": "$t(entity.folder, {\"count\": 2})",
|
||||
"genres": "$t(entity.genre, {\"count\": 2})",
|
||||
"home": "$t(common.home)",
|
||||
"myLibrary": "a minha biblioteca",
|
||||
"nowPlaying": "agora a tocar",
|
||||
"myLibrary": "A minha biblioteca",
|
||||
"nowPlaying": "Agora a tocar",
|
||||
"playlists": "$t(entity.playlist, {\"count\": 2})",
|
||||
"search": "$t(common.search)",
|
||||
"settings": "$t(common.setting, {\"count\": 2})",
|
||||
@@ -452,92 +453,92 @@
|
||||
"tracks": "$t(entity.track, {\"count\": 2})"
|
||||
},
|
||||
"trackList": {
|
||||
"artistTracks": "faixas de {{artist}}",
|
||||
"artistTracks": "Faixas de {{artist}}",
|
||||
"genreTracks": "\"{{genre}}\" $t(entity.track, {\"count\": 2})",
|
||||
"title": "$t(entity.track, {\"count\": 2})"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"addLast": "adicionar no final",
|
||||
"addNext": "adicionar a seguir",
|
||||
"favorite": "favorito",
|
||||
"mute": "mudo",
|
||||
"muted": "mudo",
|
||||
"next": "próximo",
|
||||
"play": "tocar",
|
||||
"playbackFetchCancel": "isto demora um pouco... feche a notificação para cancelar",
|
||||
"playbackFetchInProgress": "a carregar músicas…",
|
||||
"playbackFetchNoResults": "nenhuma música encontrada",
|
||||
"playbackSpeed": "velocidade de reprodução",
|
||||
"playRandom": "tocar aleatório",
|
||||
"playSimilarSongs": "tocar músicas similares",
|
||||
"previous": "anterior",
|
||||
"queue_clear": "limpar fila",
|
||||
"queue_moveToBottom": "mover selecionados para o topo",
|
||||
"queue_moveToTop": "mover selecionados para o fim",
|
||||
"queue_remove": "remover selecionados",
|
||||
"repeat": "repetir",
|
||||
"repeat_all": "repetir tudo",
|
||||
"repeat_off": "repetição desativada",
|
||||
"shuffle": "tocar aleatório",
|
||||
"shuffle_off": "aleatório desativado",
|
||||
"skip": "pular",
|
||||
"skip_back": "retroceder",
|
||||
"skip_forward": "avançar",
|
||||
"stop": "parar",
|
||||
"toggleFullscreenPlayer": "alternar player de ecrã cheio",
|
||||
"unfavorite": "remover favorito",
|
||||
"pause": "pausar",
|
||||
"viewQueue": "ver fila"
|
||||
"addLast": "Adicionar no final",
|
||||
"addNext": "Adicionar a seguir",
|
||||
"favorite": "Favorito",
|
||||
"mute": "Mudo",
|
||||
"muted": "Mudo",
|
||||
"next": "Próximo",
|
||||
"play": "Tocar",
|
||||
"playbackFetchCancel": "Isto demora um pouco... feche a notificação para cancelar",
|
||||
"playbackFetchInProgress": "A carregar músicas…",
|
||||
"playbackFetchNoResults": "Nenhuma música encontrada",
|
||||
"playbackSpeed": "Velocidade de reprodução",
|
||||
"playRandom": "Tocar aleatório",
|
||||
"playSimilarSongs": "Tocar músicas similares",
|
||||
"previous": "Anterior",
|
||||
"queue_clear": "Limpar fila",
|
||||
"queue_moveToBottom": "Mover selecionados para o fim",
|
||||
"queue_moveToTop": "Mover selecionados para o topo",
|
||||
"queue_remove": "Remover selecionados",
|
||||
"repeat": "Repetir",
|
||||
"repeat_all": "Repetir tudo",
|
||||
"repeat_off": "Repetição desativada",
|
||||
"shuffle": "Tocar aleatório",
|
||||
"shuffle_off": "Aleatório desativado",
|
||||
"skip": "Pular",
|
||||
"skip_back": "Retroceder",
|
||||
"skip_forward": "Avançar",
|
||||
"stop": "Parar",
|
||||
"toggleFullscreenPlayer": "Alternar player de ecrã cheio",
|
||||
"unfavorite": "Remover favorito",
|
||||
"pause": "Pausar",
|
||||
"viewQueue": "Ver fila"
|
||||
},
|
||||
"setting": {
|
||||
"accentColor": "cor de realce",
|
||||
"accentColor_description": "define a cor de realce para a aplicação",
|
||||
"albumBackground": "imagem de fundo do álbum",
|
||||
"albumBackground_description": "adiciona uma imagem de fundo contendo a arte do álbum para a página de álbum",
|
||||
"albumBackgroundBlur": "tamanho de desfoque da imagem de fundo do álbum",
|
||||
"albumBackgroundBlur_description": "ajusta a quantidade de desfoque aplicada para a imagem de fundo do álbum",
|
||||
"applicationHotkeys": "teclas de atalho da aplicação",
|
||||
"applicationHotkeys_description": "configure as teclas de atalho da aplicação. clique na caixa de seleção para definir como tecla de atalho global (somente desktop)",
|
||||
"artistConfiguration": "configuração da página de artista de álbum",
|
||||
"artistConfiguration_description": "configure quais elementos serão mostrados, e em qual ordem, na página de artista de álbum",
|
||||
"audioDevice": "dispositivo de áudio",
|
||||
"audioDevice_description": "selecione o dispositivo de áudio usado para reprodução (somente player web)",
|
||||
"audioExclusiveMode": "modo de áudio exclusivo",
|
||||
"audioExclusiveMode_description": "ativar modo de saída exclusiva. Neste modo, o sistema é geralmente bloqueado, e apenas mpv terá saída de áudio",
|
||||
"audioPlayer": "player de áudio",
|
||||
"audioPlayer_description": "selecione o player de áudio usado para reprodução",
|
||||
"buttonSize": "tamanho do botão da barra de reprodução",
|
||||
"buttonSize_description": "o tamanho dos botões da barra de reprodução",
|
||||
"clearCache": "limpar cache do navegador",
|
||||
"clearCache_description": "uma 'limpeza geral' do feishin. em adição a limpar o cache do feishin, limpa o cache do navegador (imagens gravadas e outros recursos). as credenciais de servidor e as configurações serão mantidas",
|
||||
"clearQueryCache": "limpar cache do feishin",
|
||||
"clearQueryCache_description": "uma 'limpeza leve' do feishin. isto irá renovar playlists, metadados de faixas, e resetar letras gravadas. as configurações, as credenciais de servidor e o cache de imagens serão mantidos",
|
||||
"clearCacheSuccess": "cache limpo com sucesso",
|
||||
"contextMenu": "configuração do menu de contexto (clique do botão direito do rato)",
|
||||
"contextMenu_description": "permite esconder elementos exibidos no menu quando clica num elemento com o botão direito. elementos não selecionados serão escondidos",
|
||||
"crossfadeDuration": "duraçao de crossfade",
|
||||
"crossfadeDuration_description": "define a duração do efeito crossfade",
|
||||
"crossfadeStyle_description": "seleciona qual estilo de crossfade usado no player de áudio",
|
||||
"customCssEnable": "ativar css customizado",
|
||||
"customCssEnable_description": "permite escrever css customizado",
|
||||
"customCssNotice": "Aviso: apesar de existir alguma higienização (url() e content: não são permitidas), o uso de css personalizado ainda pode representar riscos ao alterar a interface",
|
||||
"customCss": "css customizado",
|
||||
"disableLibraryUpdateOnStartup": "desativar a verificação de novas versões na inicialização",
|
||||
"accentColor": "Cor de realce",
|
||||
"accentColor_description": "Define a cor de realce para a aplicação",
|
||||
"albumBackground": "Imagem de fundo do álbum",
|
||||
"albumBackground_description": "Adiciona uma imagem de fundo contendo a arte do álbum para a página de álbum",
|
||||
"albumBackgroundBlur": "Tamanho de desfoque da imagem de fundo do álbum",
|
||||
"albumBackgroundBlur_description": "Ajusta a quantidade de desfoque aplicada para a imagem de fundo do álbum",
|
||||
"applicationHotkeys": "Teclas de atalho da aplicação",
|
||||
"applicationHotkeys_description": "Configure as teclas de atalho da aplicação. clique na caixa de seleção para definir como tecla de atalho global (somente desktop)",
|
||||
"artistConfiguration": "Configuração da página de artista de álbum",
|
||||
"artistConfiguration_description": "Configure quais elementos serão mostrados, e em qual ordem, na página de artista de álbum",
|
||||
"audioDevice": "Dispositivo de áudio",
|
||||
"audioDevice_description": "Selecione o dispositivo de áudio usado para reprodução (somente player web)",
|
||||
"audioExclusiveMode": "Modo de áudio exclusivo",
|
||||
"audioExclusiveMode_description": "Ativar modo de saída exclusiva. Neste modo, o sistema é geralmente bloqueado, e apenas mpv terá saída de áudio",
|
||||
"audioPlayer": "Player de áudio",
|
||||
"audioPlayer_description": "Selecione o player de áudio usado para reprodução",
|
||||
"buttonSize": "Tamanho do botão da barra de reprodução",
|
||||
"buttonSize_description": "O tamanho dos botões da barra de reprodução",
|
||||
"clearCache": "Limpar cache do navegador",
|
||||
"clearCache_description": "Uma 'limpeza geral' do Feishin. Em adição a limpar o cache do Feishin, limpa o cache do navegador (imagens gravadas e outros recursos). As credenciais de servidor e as configurações serão mantidas",
|
||||
"clearQueryCache": "Limpar cache do Feishin",
|
||||
"clearQueryCache_description": "Uma 'limpeza leve' do Feishin. Isto irá renovar playlists, metadados de faixas, e resetar letras gravadas. As configurações, as credenciais de servidor e o cache de imagens serão mantidos",
|
||||
"clearCacheSuccess": "Cache limpo com sucesso",
|
||||
"contextMenu": "Configuração do menu de contexto (clique do botão direito do rato)",
|
||||
"contextMenu_description": "Permite esconder elementos exibidos no menu quando clica num elemento com o botão direito. elementos não selecionados serão escondidos",
|
||||
"crossfadeDuration": "Duraçao de crossfade",
|
||||
"crossfadeDuration_description": "Define a duração do efeito crossfade",
|
||||
"crossfadeStyle_description": "Seleciona qual estilo de crossfade usado no player de áudio",
|
||||
"customCssEnable": "Ativar CSS customizado",
|
||||
"customCssEnable_description": "Permite escrever CSS customizado",
|
||||
"customCssNotice": "Aviso: apesar de existir alguma higienização (URL() e content: não são permitidas), o uso de CSS personalizado ainda pode representar riscos ao alterar a interface",
|
||||
"customCss": "Css customizado",
|
||||
"disableLibraryUpdateOnStartup": "Desativar a verificação de novas versões na inicialização",
|
||||
"discordApplicationId": "{{discord}} ID da aplicação",
|
||||
"discordIdleStatus_description": "quando ativado, atualiza o estado enquanto o player está ocioso",
|
||||
"discordUpdateInterval_description": "o tempo em segundos entre cada atualização (mínimo 15 segundos)",
|
||||
"playButtonBehavior_description": "define o comportamento padrão do botão play ao adicionar músicas à fila"
|
||||
"discordIdleStatus_description": "Quando ativado, atualiza o estado enquanto o player está ocioso",
|
||||
"discordUpdateInterval_description": "O tempo em segundos entre cada atualização (mínimo 15 segundos)",
|
||||
"playButtonBehavior_description": "Define o comportamento padrão do botão play ao adicionar músicas à fila"
|
||||
},
|
||||
"table": {
|
||||
"column": {
|
||||
"discNumber": "disco",
|
||||
"discNumber": "Disco",
|
||||
"size": "$t(common.size)",
|
||||
"title": "titulo"
|
||||
"title": "Titulo"
|
||||
},
|
||||
"config": {
|
||||
"label": {
|
||||
"discNumber": "numero do disco",
|
||||
"discNumber": "Numero do disco",
|
||||
"titleCombined": "$t(common.title) (combinado)"
|
||||
}
|
||||
}
|
||||
|
||||
+13
-13
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"common": {
|
||||
"confirm": "confirmă",
|
||||
"create": "creează",
|
||||
"biography": "biografie",
|
||||
"areYouSure": "ești sigur?",
|
||||
"no": "nu",
|
||||
"name": "nume",
|
||||
"ok": "ok",
|
||||
"note": "notă",
|
||||
"yes": "da",
|
||||
"explicit": "explicit",
|
||||
"year": "an",
|
||||
"menu": "meniu"
|
||||
"confirm": "Confirmă",
|
||||
"create": "Creează",
|
||||
"biography": "Biografie",
|
||||
"areYouSure": "Ești sigur?",
|
||||
"no": "Nu",
|
||||
"name": "Nume",
|
||||
"ok": "Ok",
|
||||
"note": "Notă",
|
||||
"yes": "Da",
|
||||
"explicit": "Explicit",
|
||||
"year": "An",
|
||||
"menu": "Meniu"
|
||||
},
|
||||
"filter": {
|
||||
"biography": "biografie"
|
||||
"biography": "Biografie"
|
||||
}
|
||||
}
|
||||
|
||||
+823
-727
File diff suppressed because it is too large
Load Diff
+579
-579
File diff suppressed because it is too large
Load Diff
+438
-438
File diff suppressed because it is too large
Load Diff
+421
-421
File diff suppressed because it is too large
Load Diff
+341
-341
@@ -1,323 +1,323 @@
|
||||
{
|
||||
"action": {
|
||||
"editPlaylist": "redigera $t(entity.playlist, {\"count\": 1})",
|
||||
"goToPage": "gå till sida",
|
||||
"moveToTop": "flytta till toppen",
|
||||
"clearQueue": "rensa kö",
|
||||
"addToFavorites": "lägg till $t(entity.favorite, {\"count\": 2})",
|
||||
"addToPlaylist": "lägg till $t(entity.playlist, {\"count\": 1})",
|
||||
"createPlaylist": "skapa $t(entity.playlist, {\"count\": 1})",
|
||||
"removeFromPlaylist": "ta bort från $t(entity.playlist, {\"count\": 1})",
|
||||
"viewPlaylists": "visa $t(entity.playlist, {\"count\": 2})",
|
||||
"editPlaylist": "Redigera $t(entity.playlist, {\"count\": 1})",
|
||||
"goToPage": "Gå till sida",
|
||||
"moveToTop": "Flytta till toppen",
|
||||
"clearQueue": "Rensa kö",
|
||||
"addToFavorites": "Lägg till $t(entity.favorite, {\"count\": 2})",
|
||||
"addToPlaylist": "Lägg till $t(entity.playlist, {\"count\": 1})",
|
||||
"createPlaylist": "Skapa $t(entity.playlist, {\"count\": 1})",
|
||||
"removeFromPlaylist": "Ta bort från $t(entity.playlist, {\"count\": 1})",
|
||||
"viewPlaylists": "Visa $t(entity.playlist, {\"count\": 2})",
|
||||
"refresh": "$t(common.refresh)",
|
||||
"deletePlaylist": "ta bort $t(entity.playlist, {\"count\": 1})",
|
||||
"removeFromQueue": "ta bort från kö",
|
||||
"deselectAll": "avmarkera alla",
|
||||
"moveToBottom": "flytta till botten",
|
||||
"setRating": "sätt betyg",
|
||||
"toggleSmartPlaylistEditor": "växla $t(entity.smartPlaylist) redigerare",
|
||||
"removeFromFavorites": "ta bort från $t(entity.favorite, {\"count\": 2})",
|
||||
"downloadStarted": "startade nedladdning av {{count}} objekt",
|
||||
"moveToNext": "flytta till nästa",
|
||||
"moveUp": "flytta upp",
|
||||
"moveDown": "flytta ner",
|
||||
"holdToMoveToTop": "håll för att flytta till toppen",
|
||||
"holdToMoveToBottom": "håll för att flytta till botten",
|
||||
"moveItems": "flytta objekt",
|
||||
"shuffle": "slumpa",
|
||||
"shuffleAll": "slumpa alla",
|
||||
"shuffleSelected": "slumpa valda",
|
||||
"viewMore": "visa mer",
|
||||
"deletePlaylist": "Ta bort $t(entity.playlist, {\"count\": 1})",
|
||||
"removeFromQueue": "Ta bort från kö",
|
||||
"deselectAll": "Avmarkera alla",
|
||||
"moveToBottom": "Flytta till botten",
|
||||
"setRating": "Sätt betyg",
|
||||
"toggleSmartPlaylistEditor": "Växla $t(entity.smartPlaylist) redigerare",
|
||||
"removeFromFavorites": "Ta bort från $t(entity.favorite, {\"count\": 2})",
|
||||
"downloadStarted": "Startade nedladdning av {{count}} objekt",
|
||||
"moveToNext": "Flytta till nästa",
|
||||
"moveUp": "Flytta upp",
|
||||
"moveDown": "Flytta ner",
|
||||
"holdToMoveToTop": "Håll för att flytta till toppen",
|
||||
"holdToMoveToBottom": "Håll för att flytta till botten",
|
||||
"moveItems": "Flytta objekt",
|
||||
"shuffle": "Slumpa",
|
||||
"shuffleAll": "Slumpa alla",
|
||||
"shuffleSelected": "Slumpa valda",
|
||||
"viewMore": "Visa mer",
|
||||
"openIn": {
|
||||
"lastfm": "Öppna i Last.fm",
|
||||
"musicbrainz": "Öppna i MusicBrainz"
|
||||
},
|
||||
"createRadioStation": "skapa $t(entity.radioStation, {\"count\": 1})",
|
||||
"deleteRadioStation": "ta bort $t(entity.radioStation, {\"count\": 1})",
|
||||
"addOrRemoveFromSelection": "lägg till eller ta bort från markerade",
|
||||
"selectRangeOfItems": "välj en mängd objekt",
|
||||
"selectAll": "markera alla",
|
||||
"openApplicationDirectory": "öppna applikationskatalog"
|
||||
"createRadioStation": "Skapa $t(entity.radioStation, {\"count\": 1})",
|
||||
"deleteRadioStation": "Ta bort $t(entity.radioStation, {\"count\": 1})",
|
||||
"addOrRemoveFromSelection": "Lägg till eller ta bort från markerade",
|
||||
"selectRangeOfItems": "Välj en mängd objekt",
|
||||
"selectAll": "Markera alla",
|
||||
"openApplicationDirectory": "Öppna applikationskatalog"
|
||||
},
|
||||
"common": {
|
||||
"backward": "bakåt",
|
||||
"increase": "öka",
|
||||
"rating": "betyg",
|
||||
"bpm": "bpm",
|
||||
"refresh": "laddaom",
|
||||
"unknown": "okänd",
|
||||
"areYouSure": "är du säker?",
|
||||
"edit": "redigera",
|
||||
"favorite": "favorit",
|
||||
"left": "vänster",
|
||||
"save": "spara",
|
||||
"right": "höger",
|
||||
"currentSong": "aktuell $t(entity.track, {\"count\": 1})",
|
||||
"collapse": "kollaps",
|
||||
"trackNumber": "spår",
|
||||
"descending": "fallande",
|
||||
"add": "lägg till",
|
||||
"gap": "avstånd",
|
||||
"ascending": "stigande",
|
||||
"dismiss": "avskeda",
|
||||
"year": "år",
|
||||
"manage": "hantera",
|
||||
"limit": "gräns",
|
||||
"minimize": "minimera",
|
||||
"modified": "modifierad",
|
||||
"duration": "längd",
|
||||
"name": "namn",
|
||||
"maximize": "maximera",
|
||||
"decrease": "minska",
|
||||
"ok": "ok",
|
||||
"description": "beskrivning",
|
||||
"configure": "konfigurera",
|
||||
"path": "sökväg",
|
||||
"no": "nej",
|
||||
"owner": "ägare",
|
||||
"enable": "aktivera",
|
||||
"clear": "töm",
|
||||
"forward": "framåt",
|
||||
"delete": "ta bort",
|
||||
"cancel": "avbryt",
|
||||
"forceRestartRequired": "starta om för att tillämpa ändringar... Stäng meddelandet för att starta om",
|
||||
"setting_one": "inställning",
|
||||
"backward": "Bakåt",
|
||||
"increase": "Öka",
|
||||
"rating": "Betyg",
|
||||
"bpm": "Bpm",
|
||||
"refresh": "Laddaom",
|
||||
"unknown": "Okänd",
|
||||
"areYouSure": "Är du säker?",
|
||||
"edit": "Redigera",
|
||||
"favorite": "Favorit",
|
||||
"left": "Vänster",
|
||||
"save": "Spara",
|
||||
"right": "Höger",
|
||||
"currentSong": "Aktuell $t(entity.track, {\"count\": 1})",
|
||||
"collapse": "Kollaps",
|
||||
"trackNumber": "Spår",
|
||||
"descending": "Fallande",
|
||||
"add": "Lägg till",
|
||||
"gap": "Avstånd",
|
||||
"ascending": "Stigande",
|
||||
"dismiss": "Avskeda",
|
||||
"year": "År",
|
||||
"manage": "Hantera",
|
||||
"limit": "Gräns",
|
||||
"minimize": "Minimera",
|
||||
"modified": "Modifierad",
|
||||
"duration": "Längd",
|
||||
"name": "Namn",
|
||||
"maximize": "Maximera",
|
||||
"decrease": "Minska",
|
||||
"ok": "Ok",
|
||||
"description": "Beskrivning",
|
||||
"configure": "Konfigurera",
|
||||
"path": "Sökväg",
|
||||
"no": "Nej",
|
||||
"owner": "Ägare",
|
||||
"enable": "Aktivera",
|
||||
"clear": "Töm",
|
||||
"forward": "Framåt",
|
||||
"delete": "Ta bort",
|
||||
"cancel": "Avbryt",
|
||||
"forceRestartRequired": "Starta om för att tillämpa ändringar... Stäng meddelandet för att starta om",
|
||||
"setting_one": "Inställning",
|
||||
"setting_other": "",
|
||||
"version": "version",
|
||||
"title": "titel",
|
||||
"filter_one": "filter",
|
||||
"filter_other": "filter",
|
||||
"filters": "filter",
|
||||
"create": "skapa",
|
||||
"bitrate": "bithastighet",
|
||||
"saveAndReplace": "spara och skrivöver",
|
||||
"action_one": "handling",
|
||||
"action_other": "handlingar",
|
||||
"playerMustBePaused": "spelaren måste pausas",
|
||||
"confirm": "bekräfta",
|
||||
"resetToDefault": "återställ till standard",
|
||||
"home": "hem",
|
||||
"comingSoon": "kommer snart…",
|
||||
"reset": "nollställ",
|
||||
"channel_one": "kanal",
|
||||
"channel_other": "kanaler",
|
||||
"disable": "inaktivera",
|
||||
"sortOrder": "ordning",
|
||||
"none": "ingen",
|
||||
"menu": "meny",
|
||||
"restartRequired": "omstart krävs",
|
||||
"previousSong": "föregående $t(entity.track, {\"count\": 1})",
|
||||
"noResultsFromQuery": "frågan returnerade inga resultat",
|
||||
"quit": "avsluta",
|
||||
"expand": "expandera",
|
||||
"search": "sök",
|
||||
"saveAs": "spara som",
|
||||
"disc": "skiva",
|
||||
"yes": "ja",
|
||||
"random": "slumpmässig",
|
||||
"size": "storlek",
|
||||
"biography": "biografi",
|
||||
"note": "anteckning",
|
||||
"center": "center",
|
||||
"explicitStatus": "olämplig status",
|
||||
"additionalParticipants": "ytterligare medverkare",
|
||||
"newVersion": "en ny version har installerats {{version}}",
|
||||
"viewReleaseNotes": "se utgåveinformation",
|
||||
"bitDepth": "bitdjup",
|
||||
"close": "stäng",
|
||||
"codec": "kodek",
|
||||
"doNotShowAgain": "visa inte detta igen",
|
||||
"view": "visa",
|
||||
"externalLinks": "externa länkar",
|
||||
"faster": "snabbare",
|
||||
"version": "Version",
|
||||
"title": "Titel",
|
||||
"filter_one": "Filter",
|
||||
"filter_other": "Filter",
|
||||
"filters": "Filter",
|
||||
"create": "Skapa",
|
||||
"bitrate": "Bithastighet",
|
||||
"saveAndReplace": "Spara och skrivöver",
|
||||
"action_one": "Handling",
|
||||
"action_other": "Handlingar",
|
||||
"playerMustBePaused": "Spelaren måste pausas",
|
||||
"confirm": "Bekräfta",
|
||||
"resetToDefault": "Återställ till standard",
|
||||
"home": "Hem",
|
||||
"comingSoon": "Kommer snart…",
|
||||
"reset": "Nollställ",
|
||||
"channel_one": "Kanal",
|
||||
"channel_other": "Kanaler",
|
||||
"disable": "Inaktivera",
|
||||
"sortOrder": "Ordning",
|
||||
"none": "Ingen",
|
||||
"menu": "Meny",
|
||||
"restartRequired": "Omstart krävs",
|
||||
"previousSong": "Föregående $t(entity.track, {\"count\": 1})",
|
||||
"noResultsFromQuery": "Frågan returnerade inga resultat",
|
||||
"quit": "Avsluta",
|
||||
"expand": "Expandera",
|
||||
"search": "Sök",
|
||||
"saveAs": "Spara som",
|
||||
"disc": "Skiva",
|
||||
"yes": "Ja",
|
||||
"random": "Slumpmässig",
|
||||
"size": "Storlek",
|
||||
"biography": "Biografi",
|
||||
"note": "Anteckning",
|
||||
"center": "Center",
|
||||
"explicitStatus": "Olämplig status",
|
||||
"additionalParticipants": "Ytterligare medverkare",
|
||||
"newVersion": "En ny version har installerats {{version}}",
|
||||
"viewReleaseNotes": "Se utgåveinformation",
|
||||
"bitDepth": "Bitdjup",
|
||||
"close": "Stäng",
|
||||
"codec": "Kodek",
|
||||
"doNotShowAgain": "Visa inte detta igen",
|
||||
"view": "Visa",
|
||||
"externalLinks": "Externa länkar",
|
||||
"faster": "Snabbare",
|
||||
"mbid": "MusicBrainz ID",
|
||||
"noFilters": "inga filter konfigurerade",
|
||||
"preview": "förhandsvisa",
|
||||
"private": "privat",
|
||||
"public": "allmän",
|
||||
"recordLabel": "skivbolag",
|
||||
"releaseType": "utgåvetyp",
|
||||
"reload": "ladda om",
|
||||
"sampleRate": "samplingstakt",
|
||||
"slower": "långsammare",
|
||||
"share": "dela",
|
||||
"sort": "sortera",
|
||||
"tags": "taggar",
|
||||
"translation": "översättning",
|
||||
"explicit": "olämplig",
|
||||
"clean": "städad",
|
||||
"gridRows": "rutnätsrader",
|
||||
"tableColumns": "tabellkolumner",
|
||||
"noFilters": "Inga filter konfigurerade",
|
||||
"preview": "Förhandsvisa",
|
||||
"private": "Privat",
|
||||
"public": "Allmän",
|
||||
"recordLabel": "Skivbolag",
|
||||
"releaseType": "Utgåvetyp",
|
||||
"reload": "Ladda om",
|
||||
"sampleRate": "Samplingstakt",
|
||||
"slower": "Långsammare",
|
||||
"share": "Dela",
|
||||
"sort": "Sortera",
|
||||
"tags": "Taggar",
|
||||
"translation": "Översättning",
|
||||
"explicit": "Olämplig",
|
||||
"clean": "Städad",
|
||||
"gridRows": "Rutnätsrader",
|
||||
"tableColumns": "Tabellkolumner",
|
||||
"itemsMore": "{{count}} fler",
|
||||
"countSelected": "{{count}} markerade"
|
||||
},
|
||||
"error": {
|
||||
"remotePortWarning": "starta om servern för att tillämpa den nya porten",
|
||||
"systemFontError": "ett fel uppstod vid försök att hämta systemteckensnitt",
|
||||
"playbackError": "ett fel uppstod vid försök att spela upp media",
|
||||
"endpointNotImplementedError": "endpoint {{endpoint}} är inte implementerad för {{serverType}}",
|
||||
"remotePortError": "ett fel uppstod vid försök att ange serverporten",
|
||||
"serverRequired": "server krävs",
|
||||
"authenticationFailed": "autentiseringen misslyckades",
|
||||
"apiRouteError": "det går inte att dirigera begäran",
|
||||
"genericError": "ett fel uppstod",
|
||||
"credentialsRequired": "autentiseringsuppgifter som krävs",
|
||||
"sessionExpiredError": "din session har löpt ut",
|
||||
"remotePortWarning": "Starta om servern för att tillämpa den nya porten",
|
||||
"systemFontError": "Ett fel uppstod vid försök att hämta systemteckensnitt",
|
||||
"playbackError": "Ett fel uppstod vid försök att spela upp media",
|
||||
"endpointNotImplementedError": "Endpoint {{endpoint}} är inte implementerad för {{serverType}}",
|
||||
"remotePortError": "Ett fel uppstod vid försök att ange serverporten",
|
||||
"serverRequired": "Server krävs",
|
||||
"authenticationFailed": "Autentiseringen misslyckades",
|
||||
"apiRouteError": "Det går inte att dirigera begäran",
|
||||
"genericError": "Ett fel uppstod",
|
||||
"credentialsRequired": "Autentiseringsuppgifter som krävs",
|
||||
"sessionExpiredError": "Din session har löpt ut",
|
||||
"remoteEnableError": "Ett fel uppstod vid försök att $t(common.enable) servern",
|
||||
"localFontAccessDenied": "åtkomst nekad till lokala teckensnitt",
|
||||
"serverNotSelectedError": "ingen server vald",
|
||||
"remoteDisableError": "ett fel uppstod vid försök av $t(common.disable) servern",
|
||||
"localFontAccessDenied": "Åtkomst nekad till lokala teckensnitt",
|
||||
"serverNotSelectedError": "Ingen server vald",
|
||||
"remoteDisableError": "Ett fel uppstod vid försök av $t(common.disable) servern",
|
||||
"mpvRequired": "MPV krävs",
|
||||
"audioDeviceFetchError": "ett fel uppstod vid hämtning av ljudenheter",
|
||||
"invalidServer": "ogiltig server",
|
||||
"loginRateError": "för många inloggningsförsök, försök igen om några sekunder",
|
||||
"badAlbum": "du ser denna sidan eftersom denna låten inte är en del av ett album. du ser troligtvis detta problemet för att du har en låt på toppnivån i din musikmapp. Jellyfin grupperar bara låtar om de finns i en mapp",
|
||||
"badValue": "felaktigt alternativ \"{{value}}\". detta värde existerar inte längre",
|
||||
"multipleServerSaveQueueError": "spelningskön har en eller flera låtar som inte är från den nuvarande valda servern. detta är inte stöttat",
|
||||
"networkError": "en nätverksfel uppstod",
|
||||
"notificationDenied": "åtkomst till notifieringarna var nekad. inställningen har ingen verkan",
|
||||
"openError": "kunde inte öppna filen",
|
||||
"settingsSyncError": "diskrepans hittades mellan inställningarna för renderingsprocessen och huvudprocessen. starta om applikationen för att ändringarna ska tillämpas"
|
||||
"audioDeviceFetchError": "Ett fel uppstod vid hämtning av ljudenheter",
|
||||
"invalidServer": "Ogiltig server",
|
||||
"loginRateError": "För många inloggningsförsök, försök igen om några sekunder",
|
||||
"badAlbum": "Du ser denna sidan eftersom denna låten inte är en del av ett album. du ser troligtvis detta problemet för att du har en låt på toppnivån i din musikmapp. Jellyfin grupperar bara låtar om de finns i en mapp",
|
||||
"badValue": "Felaktigt alternativ \"{{value}}\". detta värde existerar inte längre",
|
||||
"multipleServerSaveQueueError": "Spelningskön har en eller flera låtar som inte är från den nuvarande valda servern. detta är inte stöttat",
|
||||
"networkError": "En nätverksfel uppstod",
|
||||
"notificationDenied": "Åtkomst till notifieringarna var nekad. inställningen har ingen verkan",
|
||||
"openError": "Kunde inte öppna filen",
|
||||
"settingsSyncError": "Diskrepans hittades mellan inställningarna för renderingsprocessen och huvudprocessen. starta om applikationen för att ändringarna ska tillämpas"
|
||||
},
|
||||
"filter": {
|
||||
"mostPlayed": "mest spelade",
|
||||
"comment": "kommentar",
|
||||
"playCount": "antal spelningar",
|
||||
"recentlyUpdated": "nyligen uppdaterad",
|
||||
"mostPlayed": "Mest spelade",
|
||||
"comment": "Kommentar",
|
||||
"playCount": "Antal spelningar",
|
||||
"recentlyUpdated": "Nyligen uppdaterad",
|
||||
"channels": "$t(common.channel_other)",
|
||||
"isCompilation": "är kompilering",
|
||||
"recentlyPlayed": "nyligen spelad",
|
||||
"isRated": "är betygsatt",
|
||||
"isCompilation": "Är kompilering",
|
||||
"recentlyPlayed": "Nyligen spelad",
|
||||
"isRated": "Är betygsatt",
|
||||
"owner": "$t(common.owner)",
|
||||
"title": "titel",
|
||||
"rating": "betyg",
|
||||
"search": "sök",
|
||||
"bitrate": "bithastighet",
|
||||
"title": "Titel",
|
||||
"rating": "Betyg",
|
||||
"search": "Sök",
|
||||
"bitrate": "Bithastighet",
|
||||
"genre": "$t(entity.genre, {\"count\": 1})",
|
||||
"recentlyAdded": "nyligen tillagda",
|
||||
"note": "anteckning",
|
||||
"name": "namn",
|
||||
"dateAdded": "datum tillagt",
|
||||
"releaseDate": "utgivningsdag",
|
||||
"communityRating": "betyg från communityn",
|
||||
"path": "sökväg",
|
||||
"favorited": "favoritmärkt",
|
||||
"recentlyAdded": "Nyligen tillagda",
|
||||
"note": "Anteckning",
|
||||
"name": "Namn",
|
||||
"dateAdded": "Datum tillagt",
|
||||
"releaseDate": "Utgivningsdag",
|
||||
"communityRating": "Betyg från communityn",
|
||||
"path": "Sökväg",
|
||||
"favorited": "Favoritmärkt",
|
||||
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
|
||||
"isRecentlyPlayed": "spelas nyligen",
|
||||
"isFavorited": "är favoritmärkt",
|
||||
"bpm": "bpm",
|
||||
"releaseYear": "utgivningsår",
|
||||
"id": "id",
|
||||
"disc": "skiva",
|
||||
"biography": "biografi",
|
||||
"isRecentlyPlayed": "Spelas nyligen",
|
||||
"isFavorited": "Är favoritmärkt",
|
||||
"bpm": "Bpm",
|
||||
"releaseYear": "Utgivningsår",
|
||||
"id": "Id",
|
||||
"disc": "Skiva",
|
||||
"biography": "Biografi",
|
||||
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||
"duration": "längd",
|
||||
"isPublic": "är offentlig",
|
||||
"random": "slumpmässig",
|
||||
"lastPlayed": "senast spelad",
|
||||
"toYear": "till år",
|
||||
"fromYear": "från år",
|
||||
"duration": "Längd",
|
||||
"isPublic": "Är offentlig",
|
||||
"random": "Slumpmässig",
|
||||
"lastPlayed": "Senast spelad",
|
||||
"toYear": "Till år",
|
||||
"fromYear": "Från år",
|
||||
"album": "$t(entity.album, {\"count\": 1})",
|
||||
"trackNumber": "spår",
|
||||
"songCount": "sångräkning",
|
||||
"criticRating": "kritikerbetyg",
|
||||
"trackNumber": "Spår",
|
||||
"songCount": "Sångräkning",
|
||||
"criticRating": "Kritikerbetyg",
|
||||
"albumCount": "$t(entity.album, {\"count\": 2}) antal",
|
||||
"explicitStatus": "$t(common.explicitStatus)"
|
||||
},
|
||||
"form": {
|
||||
"deletePlaylist": {
|
||||
"title": "ta bort $t(entity.playlist, {\"count\": 1})",
|
||||
"title": "Ta bort $t(entity.playlist, {\"count\": 1})",
|
||||
"success": "$t(entity.playlist, {\"count\": 1}) har tagits bort",
|
||||
"input_confirm": "Skriv namnet på $t(entity.playlist, {\"count\": 1}) för att bekräfta"
|
||||
},
|
||||
"createPlaylist": {
|
||||
"input_description": "$t(common.description)",
|
||||
"title": "skapa $t(entity.playlist, {\"count\": 1})",
|
||||
"input_public": "offentlig",
|
||||
"title": "Skapa $t(entity.playlist, {\"count\": 1})",
|
||||
"input_public": "Offentlig",
|
||||
"input_name": "$t(common.name)",
|
||||
"success": "$t(entity.playlist, {\"count\": 1}) skapad",
|
||||
"input_owner": "$t(common.owner)"
|
||||
},
|
||||
"addServer": {
|
||||
"title": "lägg till server",
|
||||
"input_username": "användarnamn",
|
||||
"input_url": "länk",
|
||||
"input_password": "lösenord",
|
||||
"input_legacyAuthentication": "aktivera äldre autentisering",
|
||||
"input_name": "server namn",
|
||||
"success": "servern har lagts till",
|
||||
"input_savePassword": "spara lösenord",
|
||||
"ignoreSsl": "ignorera ssl ($t(common.restartRequired))",
|
||||
"ignoreCors": "ignorera cors ($t(common.restartRequired))",
|
||||
"error_savePassword": "ett fel uppstod när lösenordet skulle sparas",
|
||||
"input_preferInstantMix": "föredra instant mixning",
|
||||
"input_preferInstantMixDescription": "använd bara instant mixning för att få liknande låtar. användbar om du har plugin för att förändra detta beteendet"
|
||||
"title": "Lägg till server",
|
||||
"input_username": "Användarnamn",
|
||||
"input_url": "Länk",
|
||||
"input_password": "Lösenord",
|
||||
"input_legacyAuthentication": "Aktivera äldre autentisering",
|
||||
"input_name": "Server namn",
|
||||
"success": "Servern har lagts till",
|
||||
"input_savePassword": "Spara lösenord",
|
||||
"ignoreSsl": "Ignorera ssl ($t(common.restartRequired))",
|
||||
"ignoreCors": "Ignorera cors ($t(common.restartRequired))",
|
||||
"error_savePassword": "Ett fel uppstod när lösenordet skulle sparas",
|
||||
"input_preferInstantMix": "Föredra instant mixning",
|
||||
"input_preferInstantMixDescription": "Använd bara instant mixning för att få liknande låtar. användbar om du har plugin för att förändra detta beteendet"
|
||||
},
|
||||
"addToPlaylist": {
|
||||
"success": "lade till $t(entity.trackWithCount, {\"count\": {{message}} }) till $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
||||
"title": "lägg till i $t(entity.playlist, {\"count\": 1})",
|
||||
"input_skipDuplicates": "hoppa över dubbletter",
|
||||
"success": "Lade till $t(entity.trackWithCount, {\"count\": {{message}} }) till $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
||||
"title": "Lägg till i $t(entity.playlist, {\"count\": 1})",
|
||||
"input_skipDuplicates": "Hoppa över dubbletter",
|
||||
"input_playlists": "$t(entity.playlist, {\"count\": 2})",
|
||||
"create": "skapa $t(entity.playlist, {\"count\": 1}) {{playlist}}",
|
||||
"searchOrCreate": "sök $t(entity.playlist, {\"count\": 2}) eller skriv för att skapa en ny"
|
||||
"create": "Skapa $t(entity.playlist, {\"count\": 1}) {{playlist}}",
|
||||
"searchOrCreate": "Sök $t(entity.playlist, {\"count\": 2}) eller skriv för att skapa en ny"
|
||||
},
|
||||
"updateServer": {
|
||||
"title": "uppdatera server",
|
||||
"success": "servern har uppdaterats"
|
||||
"title": "Uppdatera server",
|
||||
"success": "Servern har uppdaterats"
|
||||
},
|
||||
"queryEditor": {
|
||||
"input_optionMatchAll": "matcha alla",
|
||||
"input_optionMatchAny": "matcha något"
|
||||
"input_optionMatchAll": "Matcha alla",
|
||||
"input_optionMatchAny": "Matcha något"
|
||||
},
|
||||
"lyricSearch": {
|
||||
"input_name": "$t(common.name)",
|
||||
"input_artist": "$t(entity.artist, {\"count\": 1})",
|
||||
"title": "sångtext sök"
|
||||
"title": "Sångtext sök"
|
||||
},
|
||||
"editPlaylist": {
|
||||
"title": "redigera $t(entity.playlist, {\"count\": 1})",
|
||||
"title": "Redigera $t(entity.playlist, {\"count\": 1})",
|
||||
"publicJellyfinNote": "Jellyfin visar av någon anledning inte om en spellista är publik eller inte. Om du önskar att denna ska förbli publik, så får du ha följande indata markerade"
|
||||
},
|
||||
"largeFetchConfirmation": {
|
||||
"title": "lägg till objekt till kön",
|
||||
"title": "Lägg till objekt till kön",
|
||||
"description": "Åtgärden kommer att lägga till alla objekt till den nuvarande filtrerade vyn"
|
||||
},
|
||||
"createRadioStation": {
|
||||
"success": "radiostation skapades",
|
||||
"title": "skapa radiostation",
|
||||
"input_homepageUrl": "hemside-URL",
|
||||
"input_name": "namn",
|
||||
"input_streamUrl": "stream url"
|
||||
"success": "Radiostation skapades",
|
||||
"title": "Skapa radiostation",
|
||||
"input_homepageUrl": "Hemside-URL",
|
||||
"input_name": "Namn",
|
||||
"input_streamUrl": "Stream url"
|
||||
}
|
||||
},
|
||||
"page": {
|
||||
"fullscreenPlayer": {
|
||||
"config": {
|
||||
"showLyricMatch": "Visa låttext matchning",
|
||||
"dynamicBackground": "dynamisk bakgrund",
|
||||
"followCurrentLyric": "följ aktuell låttext",
|
||||
"opacity": "ogenomskinlighet",
|
||||
"lyricSize": "låttext storlek",
|
||||
"lyricAlignment": "låttext justering",
|
||||
"lyricGap": "låttext mellanrum",
|
||||
"synchronized": "synkroniserad",
|
||||
"showLyricProvider": "visa sångtextleverantör",
|
||||
"unsynchronized": "osynkroniserad"
|
||||
"dynamicBackground": "Dynamisk bakgrund",
|
||||
"followCurrentLyric": "Följ aktuell låttext",
|
||||
"opacity": "Ogenomskinlighet",
|
||||
"lyricSize": "Låttext storlek",
|
||||
"lyricAlignment": "Låttext justering",
|
||||
"lyricGap": "Låttext mellanrum",
|
||||
"synchronized": "Synkroniserad",
|
||||
"showLyricProvider": "Visa sångtextleverantör",
|
||||
"unsynchronized": "Osynkroniserad"
|
||||
},
|
||||
"lyrics": "sångtext",
|
||||
"related": "relaterad"
|
||||
"lyrics": "Sångtext",
|
||||
"related": "Relaterad"
|
||||
},
|
||||
"appMenu": {
|
||||
"selectServer": "välj server",
|
||||
"version": "version {{version}}",
|
||||
"selectServer": "Välj server",
|
||||
"version": "Version {{version}}",
|
||||
"settings": "$t(common.setting, {\"count\": 2})",
|
||||
"manageServers": "hantera servrar",
|
||||
"expandSidebar": "expandera sidofältet",
|
||||
"openBrowserDevtools": "öppna webbläsarens utvecklingsverktyg",
|
||||
"manageServers": "Hantera servrar",
|
||||
"expandSidebar": "Expandera sidofältet",
|
||||
"openBrowserDevtools": "Öppna webbläsarens utvecklingsverktyg",
|
||||
"quit": "$t(common.quit)",
|
||||
"goBack": "gå tillbaka",
|
||||
"goForward": "gå framåt",
|
||||
"collapseSidebar": "växla sidofältet"
|
||||
"goBack": "Gå tillbaka",
|
||||
"goForward": "Gå framåt",
|
||||
"collapseSidebar": "Växla sidofältet"
|
||||
},
|
||||
"contextMenu": {
|
||||
"addToPlaylist": "$t(action.addToPlaylist)",
|
||||
@@ -336,20 +336,20 @@
|
||||
"play": "$t(player.play)",
|
||||
"numberSelected": "{{count}} vald",
|
||||
"removeFromQueue": "$t(action.removeFromQueue)",
|
||||
"download": "ladda ner",
|
||||
"download": "Ladda ner",
|
||||
"moveItems": "$t(action.moveItems)",
|
||||
"moveToNext": "$t(action.moveToNext)",
|
||||
"playSimilarSongs": "$t(player.playSimilarSongs)",
|
||||
"playShuffled": "$t(player.shuffle)",
|
||||
"shareItem": "dela objekt",
|
||||
"goTo": "gå till",
|
||||
"goToAlbum": "gå till $t(entity.album, {\"count\": 1})",
|
||||
"goToAlbumArtist": "gå till $t(entity.albumArtist, {\"count\": 1})",
|
||||
"showDetails": "hämta information"
|
||||
"shareItem": "Dela objekt",
|
||||
"goTo": "Gå till",
|
||||
"goToAlbum": "Gå till $t(entity.album, {\"count\": 1})",
|
||||
"goToAlbumArtist": "Gå till $t(entity.albumArtist, {\"count\": 1})",
|
||||
"showDetails": "Hämta information"
|
||||
},
|
||||
"albumDetail": {
|
||||
"moreFromArtist": "mer från $t(entity.artist, {\"count\": 1})",
|
||||
"moreFromGeneric": "mer från {{item}}"
|
||||
"moreFromArtist": "Mer från $t(entity.artist, {\"count\": 1})",
|
||||
"moreFromGeneric": "Mer från {{item}}"
|
||||
},
|
||||
"albumArtistList": {
|
||||
"title": "$t(entity.albumArtist, {\"count\": 2})"
|
||||
@@ -358,124 +358,124 @@
|
||||
"title": "$t(entity.album, {\"count\": 2})"
|
||||
},
|
||||
"sidebar": {
|
||||
"nowPlaying": "nu spelas"
|
||||
"nowPlaying": "Nu spelas"
|
||||
},
|
||||
"home": {
|
||||
"mostPlayed": "mest spelade",
|
||||
"newlyAdded": "nytillkomna utgåvor",
|
||||
"explore": "utforska från ditt bibliotek",
|
||||
"recentlyPlayed": "nyligen spelat"
|
||||
"mostPlayed": "Mest spelade",
|
||||
"newlyAdded": "Nytillkomna utgåvor",
|
||||
"explore": "Utforska från ditt bibliotek",
|
||||
"recentlyPlayed": "Nyligen spelat"
|
||||
},
|
||||
"setting": {
|
||||
"playbackTab": "uppspelning",
|
||||
"generalTab": "allmänt",
|
||||
"hotkeysTab": "snabbtangenter",
|
||||
"windowTab": "fönster"
|
||||
"playbackTab": "Uppspelning",
|
||||
"generalTab": "Allmänt",
|
||||
"hotkeysTab": "Snabbtangenter",
|
||||
"windowTab": "Fönster"
|
||||
},
|
||||
"globalSearch": {
|
||||
"commands": {
|
||||
"serverCommands": "serverkommandon",
|
||||
"goToPage": "gå till sidan",
|
||||
"searchFor": "sök efter {{query}}"
|
||||
"serverCommands": "Serverkommandon",
|
||||
"goToPage": "Gå till sidan",
|
||||
"searchFor": "Sök efter {{query}}"
|
||||
},
|
||||
"title": "kommandon"
|
||||
"title": "Kommandon"
|
||||
},
|
||||
"manageServers": {
|
||||
"url": "URL",
|
||||
"username": "användarnamn",
|
||||
"editServerDetailsTooltip": "redigera serverinställningar",
|
||||
"removeServer": "ta bort server"
|
||||
"username": "Användarnamn",
|
||||
"editServerDetailsTooltip": "Redigera serverinställningar",
|
||||
"removeServer": "Ta bort server"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"playlist_one": "spellista",
|
||||
"playlist_other": "spellistor",
|
||||
"artist_one": "artist",
|
||||
"artist_other": "artister",
|
||||
"albumArtist_one": "albumartist",
|
||||
"albumArtist_other": "albumartister",
|
||||
"albumArtistCount_one": "{{count}} Albumartist",
|
||||
"albumArtistCount_other": "{{count}} Albumartister",
|
||||
"playlist_one": "Spellista",
|
||||
"playlist_other": "Spellistor",
|
||||
"artist_one": "Artist",
|
||||
"artist_other": "Artister",
|
||||
"albumArtist_one": "Albumartist",
|
||||
"albumArtist_other": "Albumartister",
|
||||
"albumArtistCount_one": "{{count}} albumartist",
|
||||
"albumArtistCount_other": "{{count}} albumartister",
|
||||
"albumWithCount_one": "{{count}} album",
|
||||
"albumWithCount_other": "{{count}} album",
|
||||
"favorite_one": "favorit",
|
||||
"favorite_other": "favoriter",
|
||||
"folder_one": "mapp",
|
||||
"folder_other": "mappar",
|
||||
"album_one": "album",
|
||||
"album_other": "album",
|
||||
"favorite_one": "Favorit",
|
||||
"favorite_other": "Favoriter",
|
||||
"folder_one": "Mapp",
|
||||
"folder_other": "Mappar",
|
||||
"album_one": "Album",
|
||||
"album_other": "Album",
|
||||
"playlistWithCount_one": "{{count}} spellista",
|
||||
"playlistWithCount_other": "{{count}} spellistor",
|
||||
"folderWithCount_one": "{{count}} mapp",
|
||||
"folderWithCount_other": "{{count}} mappar",
|
||||
"track_one": "spår",
|
||||
"track_other": "spår",
|
||||
"track_one": "Spår",
|
||||
"track_other": "Spår",
|
||||
"trackWithCount_one": "{{count}} spår",
|
||||
"trackWithCount_other": "{{count}} spår",
|
||||
"artistWithCount_one": "{{count}} artist",
|
||||
"artistWithCount_other": "{{count}} artister",
|
||||
"genre_one": "genre",
|
||||
"genre_other": "genrer",
|
||||
"genre_one": "Genre",
|
||||
"genre_other": "Genrer",
|
||||
"genreWithCount_one": "{{count}} genre",
|
||||
"genreWithCount_other": "{{count}} genrer",
|
||||
"play_one": "{{count}} spelning",
|
||||
"play_other": "{{count}} spelningar",
|
||||
"smartPlaylist": "smart $t(entity.playlist, {\"count\": 1})",
|
||||
"song_one": "låt",
|
||||
"song_other": "låtar",
|
||||
"radioStation_one": "radiostation",
|
||||
"radioStation_other": "radiostationer",
|
||||
"smartPlaylist": "Smart $t(entity.playlist, {\"count\": 1})",
|
||||
"song_one": "Låt",
|
||||
"song_other": "Låtar",
|
||||
"radioStation_one": "Radiostation",
|
||||
"radioStation_other": "Radiostationer",
|
||||
"radioStationWithCount_one": "{{count}} radiostation",
|
||||
"radioStationWithCount_other": "{{count}} radiostationer"
|
||||
},
|
||||
"player": {
|
||||
"repeat_all": "repetera alla",
|
||||
"repeat": "repetera",
|
||||
"queue_remove": "ta bort markerad",
|
||||
"playRandom": "spela slumpmässigt",
|
||||
"previous": "föregående",
|
||||
"favorite": "favorit",
|
||||
"next": "nästa",
|
||||
"shuffle": "blanda",
|
||||
"playbackFetchNoResults": "inga låtar hittades",
|
||||
"playbackFetchInProgress": "laddar låtar…",
|
||||
"addNext": "lägg till nästa",
|
||||
"playbackSpeed": "uppspelningshastighet",
|
||||
"playbackFetchCancel": "det här tar ett tag... stäng aviseringen för att avbryta",
|
||||
"play": "spela",
|
||||
"repeat_off": "repetera inaktiverad",
|
||||
"queue_clear": "rensa kö",
|
||||
"muted": "mutad",
|
||||
"queue_moveToTop": "flytta markerad till botten",
|
||||
"queue_moveToBottom": "flytta markerad till toppen",
|
||||
"addLast": "lägg till sist",
|
||||
"mute": "muta"
|
||||
"repeat_all": "Repetera alla",
|
||||
"repeat": "Repetera",
|
||||
"queue_remove": "Ta bort markerad",
|
||||
"playRandom": "Spela slumpmässigt",
|
||||
"previous": "Föregående",
|
||||
"favorite": "Favorit",
|
||||
"next": "Nästa",
|
||||
"shuffle": "Blanda",
|
||||
"playbackFetchNoResults": "Inga låtar hittades",
|
||||
"playbackFetchInProgress": "Laddar låtar…",
|
||||
"addNext": "Lägg till nästa",
|
||||
"playbackSpeed": "Uppspelningshastighet",
|
||||
"playbackFetchCancel": "Det här tar ett tag... stäng aviseringen för att avbryta",
|
||||
"play": "Spela",
|
||||
"repeat_off": "Repetera inaktiverad",
|
||||
"queue_clear": "Rensa kö",
|
||||
"muted": "Mutad",
|
||||
"queue_moveToTop": "Flytta markerad till toppen",
|
||||
"queue_moveToBottom": "Flytta markerad till botten",
|
||||
"addLast": "Lägg till sist",
|
||||
"mute": "Muta"
|
||||
},
|
||||
"datetime": {
|
||||
"minuteShort": "min",
|
||||
"secondShort": "sek",
|
||||
"hourShort": "h",
|
||||
"dayShort": "dag"
|
||||
"minuteShort": "Min",
|
||||
"secondShort": "Sek",
|
||||
"hourShort": "H",
|
||||
"dayShort": "Dag"
|
||||
},
|
||||
"filterOperator": {
|
||||
"after": "är efter",
|
||||
"afterDate": "är efter (datum)",
|
||||
"before": "är före",
|
||||
"beforeDate": "är före (datum)",
|
||||
"contains": "innehåller",
|
||||
"endsWith": "slutar med",
|
||||
"inPlaylist": "är inom",
|
||||
"inTheLast": "är i den sista",
|
||||
"inTheRange": "är i spannet",
|
||||
"inTheRangeDate": "är i spannet (datum)",
|
||||
"is": "är",
|
||||
"isNot": "är inte",
|
||||
"isGreaterThan": "är större än",
|
||||
"isLessThan": "är mindre än",
|
||||
"matchesRegex": "matchar regex",
|
||||
"notContains": "innehåller inte",
|
||||
"notInPlaylist": "är inte inom",
|
||||
"notInTheLast": "är inte inom den sista",
|
||||
"startsWith": "startar med"
|
||||
"after": "Är efter",
|
||||
"afterDate": "Är efter (datum)",
|
||||
"before": "Är före",
|
||||
"beforeDate": "Är före (datum)",
|
||||
"contains": "Innehåller",
|
||||
"endsWith": "Slutar med",
|
||||
"inPlaylist": "Är inom",
|
||||
"inTheLast": "Är i den sista",
|
||||
"inTheRange": "Är i spannet",
|
||||
"inTheRangeDate": "Är i spannet (datum)",
|
||||
"is": "Är",
|
||||
"isNot": "Är inte",
|
||||
"isGreaterThan": "Är större än",
|
||||
"isLessThan": "Är mindre än",
|
||||
"matchesRegex": "Matchar regex",
|
||||
"notContains": "Innehåller inte",
|
||||
"notInPlaylist": "Är inte inom",
|
||||
"notInTheLast": "Är inte inom den sista",
|
||||
"startsWith": "Startar med"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,8 +310,7 @@
|
||||
"editPlaylist": {
|
||||
"title": "திருத்து $t(entity.playlist, {\"count\": 1})",
|
||||
"publicJellyfinNote": "சில காரணங்களால் செல்லிஃபின் ஒரு பிளேலிச்ட் பொதுவில் இல்லையா என்பதை அம்பலப்படுத்தவில்லை. இது பொதுவில் இருக்க விரும்பினால், தயவுசெய்து பின்வரும் உள்ளீட்டைத் தேர்ந்தெடுக்கவும்",
|
||||
"success": "$t(entity.playlist, {\"count\": 1}) வெற்றிகரமாகப் புதுப்பிக்கப்பட்டது",
|
||||
"editNote": "பெரிய பிளேலிச்ட்களுக்கு கைமுறை திருத்தங்கள் பரிந்துரைக்கப்படவில்லை. ஏற்கனவே உள்ள பிளேலிச்ட்டை மேலெழுதுவதால் ஏற்படும் தரவு இழப்பின் அபாயத்தை நிச்சயமாக ஏற்றுக்கொள்கிறீர்களா?"
|
||||
"success": "$t(entity.playlist, {\"count\": 1}) வெற்றிகரமாகப் புதுப்பிக்கப்பட்டது"
|
||||
},
|
||||
"lyricSearch": {
|
||||
"input_artist": "$t(entity.artist, {\"count\": 1})",
|
||||
@@ -628,8 +627,8 @@
|
||||
"playbackFetchNoResults": "பாடல்கள் எதுவும் கிடைக்கவில்லை",
|
||||
"playbackSpeed": "பிளேபேக் விரைவு",
|
||||
"playRandom": "சீரற்ற முறையில் விளையாடுங்கள்",
|
||||
"queue_moveToBottom": "மேலே தேர்ந்தெடுக்கப்பட்ட நகர்த்து",
|
||||
"queue_moveToTop": "தேர்ந்தெடுக்கப்பட்டதை கீழே நகர்த்தவும்",
|
||||
"queue_moveToBottom": "தேர்ந்தெடுக்கப்பட்டதை கீழே நகர்த்தவும்",
|
||||
"queue_moveToTop": "மேலே தேர்ந்தெடுக்கப்பட்ட நகர்த்து",
|
||||
"skip_back": "பின்னோக்கி தவிர்க்கவும்",
|
||||
"skip_forward": "முன்னோக்கி தவிர்க்கவும்",
|
||||
"stop": "நிறுத்து",
|
||||
@@ -801,7 +800,7 @@
|
||||
"enableRemote": "ரிமோட் கண்ட்ரோல் சேவையகத்தை இயக்கவும்",
|
||||
"enableRemote_description": "பயன்பாட்டைக் கட்டுப்படுத்த மற்ற சாதனங்களை அனுமதிக்க ரிமோட் கண்ட்ரோல் சேவையகத்தை இயக்குகிறது",
|
||||
"externalLinks": "வெளிப்புற இணைப்புகளைக் காட்டு",
|
||||
"externalLinks_description": "கலைஞர்/ஆல்பம் பக்கங்களில் வெளிப்புற இணைப்புகளை (last.fm, மியூசிக் ப்ரெய்ன்ச்) காண்பிக்க உதவுகிறது",
|
||||
"externalLinks_description": "கலைஞர்/ஆல்பம் பக்கங்களில் வெளிப்புற இணைப்புகளை (Last.fm, மியூசிக் ப்ரெய்ன்ச்) காண்பிக்க உதவுகிறது",
|
||||
"exitToTray": "தட்டில் வெளியேறவும்",
|
||||
"globalMediaHotkeys": "உலகளாவிய மீடியா ஆட்கீச்",
|
||||
"discordUpdateInterval": "{{discord}} பணக்கார இருப்பு புதுப்பிப்பு இடைவெளி",
|
||||
@@ -873,7 +872,7 @@
|
||||
"discordServeImage_description": "சேவையகத்திலிருந்தே {{discord}} சிறந்த இருப்புக்கான கவர் ஆர்ட்டைப் பகிரவும், செல்லிஃபின் மற்றும் நவிட்ரோமுக்கு மட்டுமே கிடைக்கும். படங்களைப் பெற {{discord}} ஒரு போட்டைப் பயன்படுத்துகிறது, எனவே உங்கள் சர்வர் பொது இணையத்திலிருந்து அணுகக்கூடியதாக இருக்க வேண்டும்",
|
||||
"preferLocalLyrics": "உள்ளக பாடல்களை விரும்புங்கள்",
|
||||
"preferLocalLyrics_description": "கிடைக்கும்போது தொலைநிலை பாடல்களை விட உள்ளக பாடல்களை விரும்புங்கள்",
|
||||
"lastfm": "last.fm இணைப்புகளைக் காட்டு",
|
||||
"lastfm": "Last.fm இணைப்புகளைக் காட்டு",
|
||||
"lastfm_description": "கலைஞர்/ஆல்பம் பக்கங்களில் Last.fm க்கான இணைப்புகளைக் காட்டு",
|
||||
"musicbrainz": "மியூசிக் பிரேன்ச் இணைப்புகளைக் காட்டு",
|
||||
"musicbrainz_description": "கலைஞர்/ஆல்பம் பக்கங்களில் மியூசிக் பிரைன்ச் இணைப்புகளைக் காட்டு, அங்கு மியூசிக் பிரைன்ச் ID உள்ளது",
|
||||
@@ -926,7 +925,7 @@
|
||||
"exportImportSettings_control_title": "இறக்குமதி / ஏற்றுமதி அமைப்புகள்",
|
||||
"exportImportSettings_destructiveWarning": "அமைப்புகளை இறக்குமதி செய்வது அழிவுகரமானது, கீழே உள்ள \"இறக்குமதி\" என்பதைக் சொடுக்கு செய்வதற்கு முன் மேலே உள்ளவற்றை மதிப்பாய்வு செய்யவும்!",
|
||||
"exportImportSettings_importBtn": "இறக்குமதி அமைப்புகள்",
|
||||
"exportImportSettings_importModalTitle": "feishin அமைப்புகளை இறக்குமதி செய்யவும்",
|
||||
"exportImportSettings_importModalTitle": "Feishin அமைப்புகளை இறக்குமதி செய்யவும்",
|
||||
"exportImportSettings_importSuccess": "அமைப்புகள் வெற்றிகரமாக இறக்குமதி செய்யப்பட்டன!",
|
||||
"exportImportSettings_notValidJSON": "அனுப்பப்பட்ட கோப்பு சாதொபொகு செல்லுபடியாகாது",
|
||||
"exportImportSettings_offendingKeyError": "\"{{offendingKey}}\" தவறானது - {{reason}}",
|
||||
@@ -949,8 +948,8 @@
|
||||
"logLevel_optionError": "பிழை",
|
||||
"logLevel_optionInfo": "தகவல்",
|
||||
"logLevel_optionWarn": "முன்னறிவிப்பு",
|
||||
"mpvExtraParameters": "mpv கூடுதல் அளவுருக்கள்",
|
||||
"mpvExtraParameters_description": "mpv க்கு அனுப்ப கூடுதல் வாதங்கள்",
|
||||
"mpvExtraParameters": "MPV கூடுதல் அளவுருக்கள்",
|
||||
"mpvExtraParameters_description": "MPV க்கு அனுப்ப கூடுதல் வாதங்கள்",
|
||||
"notify": "பாடல் அறிவிப்புகளை இயக்கவும்",
|
||||
"notify_description": "தற்போதைய பாடலை மாற்றும்போது அறிவிப்புகளைக் காட்டு",
|
||||
"pathReplace": "கோப்பு பாதை மாற்று",
|
||||
@@ -1016,7 +1015,7 @@
|
||||
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||
"biography": "$t(common.biography)",
|
||||
"bitrate": "$t(common.bitrate)",
|
||||
"bpm": "$t(common.bpm)",
|
||||
"bpm": "$t(common.BPM)",
|
||||
"channels": "$t(common.channel, {\"count\": 2})",
|
||||
"codec": "$t(common.codec)",
|
||||
"dateAdded": "தேதி சேர்க்கப்பட்டது",
|
||||
|
||||
+554
-554
File diff suppressed because it is too large
Load Diff
+415
-355
@@ -1,175 +1,181 @@
|
||||
{
|
||||
"action": {
|
||||
"addToFavorites": "додати до $t(entity.favorite, {\"count\": 2})",
|
||||
"addOrRemoveFromSelection": "додати або видалити з вибору",
|
||||
"selectRangeOfItems": "вибрати діапазон елементів",
|
||||
"addToPlaylist": "додати до $t(entity.playlist, {\"count\": 1})",
|
||||
"clearQueue": "очистити чергу",
|
||||
"createPlaylist": "створити $t(entity.playlist, {\"count\": 1})",
|
||||
"createRadioStation": "створити $t(entity.radioStation, {\"count\": 1})",
|
||||
"deletePlaylist": "видалити $t(entity.playlist, {\"count\": 1})",
|
||||
"deleteRadioStation": "видалити $t(entity.radioStation, {\"count\": 1})",
|
||||
"selectAll": "вибрати все",
|
||||
"deselectAll": "скасувати вибір усього",
|
||||
"downloadStarted": "почато завантаження {{count}} елементів",
|
||||
"editPlaylist": "редагувати $t(entity.playlist, {\"count\": 1})",
|
||||
"goToPage": "перейти на сторінку",
|
||||
"moveToNext": "перейти до наступного",
|
||||
"moveToBottom": "перемістити вниз",
|
||||
"moveToTop": "перемістити вгору",
|
||||
"moveUp": "перемістити вище",
|
||||
"moveDown": "перемістити нижче",
|
||||
"holdToMoveToTop": "утримуйте, щоб перемістити вгору",
|
||||
"holdToMoveToBottom": "утримувати, щоб перемістити вниз",
|
||||
"moveItems": "перемістити елементи",
|
||||
"shuffle": "перемішати",
|
||||
"shuffleAll": "все випадково",
|
||||
"shuffleSelected": "вибране випадково",
|
||||
"addToFavorites": "Додати до $t(entity.favorite, {\"count\": 2})",
|
||||
"addOrRemoveFromSelection": "Додати або видалити з вибору",
|
||||
"selectRangeOfItems": "Вибрати діапазон елементів",
|
||||
"addToPlaylist": "Додати до $t(entity.playlist, {\"count\": 1})",
|
||||
"clearQueue": "Очистити чергу",
|
||||
"createPlaylist": "Створити $t(entity.playlist, {\"count\": 1})",
|
||||
"createRadioStation": "Створити $t(entity.radioStation, {\"count\": 1})",
|
||||
"deletePlaylist": "Видалити $t(entity.playlist, {\"count\": 1})",
|
||||
"deleteRadioStation": "Видалити $t(entity.radioStation, {\"count\": 1})",
|
||||
"selectAll": "Вибрати все",
|
||||
"deselectAll": "Скасувати вибір усього",
|
||||
"downloadStarted": "Почато завантаження {{count}} елементів",
|
||||
"editPlaylist": "Редагувати $t(entity.playlist, {\"count\": 1})",
|
||||
"goToPage": "Перейти на сторінку",
|
||||
"moveToNext": "Перейти до наступного",
|
||||
"moveToBottom": "Перемістити вниз",
|
||||
"moveToTop": "Перемістити вгору",
|
||||
"moveUp": "Перемістити вище",
|
||||
"moveDown": "Перемістити нижче",
|
||||
"holdToMoveToTop": "Утримуйте, щоб перемістити вгору",
|
||||
"holdToMoveToBottom": "Утримувати, щоб перемістити вниз",
|
||||
"moveItems": "Перемістити елементи",
|
||||
"shuffle": "Перемішати",
|
||||
"shuffleAll": "Все випадково",
|
||||
"shuffleSelected": "Вибране випадково",
|
||||
"refresh": "$t(common.refresh)",
|
||||
"removeFromFavorites": "видалити з $t(entity.favorite, {\"count\": 2})",
|
||||
"removeFromPlaylist": "видалити з $t(entity.playlist, {\"count\": 1})",
|
||||
"removeFromQueue": "видалити з черги",
|
||||
"setRating": "встановити рейтинг",
|
||||
"toggleSmartPlaylistEditor": "перемикати редактор $t(entity.smartPlaylist)",
|
||||
"viewPlaylists": "показати $t(entity.playlist, {\"count\": 2})",
|
||||
"viewMore": "переглянути більше",
|
||||
"openApplicationDirectory": "відкрити каталог додатків",
|
||||
"removeFromFavorites": "Видалити з $t(entity.favorite, {\"count\": 2})",
|
||||
"removeFromPlaylist": "Видалити з $t(entity.playlist, {\"count\": 1})",
|
||||
"removeFromQueue": "Видалити з черги",
|
||||
"setRating": "Встановити рейтинг",
|
||||
"toggleSmartPlaylistEditor": "Перемикати редактор $t(entity.smartPlaylist)",
|
||||
"viewPlaylists": "Показати $t(entity.playlist, {\"count\": 2})",
|
||||
"viewMore": "Переглянути більше",
|
||||
"openApplicationDirectory": "Відкрити каталог додатків",
|
||||
"openIn": {
|
||||
"lastfm": "Відкрити в Last.fm",
|
||||
"musicbrainz": "Відкрити в MusicBrainz"
|
||||
"musicbrainz": "Відкрити в MusicBrainz",
|
||||
"listenbrainz": "Відкрити у ListenBrainz",
|
||||
"qobuz": "Відкрити у Qobuz",
|
||||
"spotify": "Відкрити у Spotify"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"countSelected": "вибрано {{count}}",
|
||||
"explicitStatus": "явний статус",
|
||||
"action_one": "дія",
|
||||
"countSelected": "Вибрано {{count}}",
|
||||
"explicitStatus": "Явний статус",
|
||||
"action_one": "Дія",
|
||||
"action_few": "дії",
|
||||
"action_many": "дій",
|
||||
"add": "додати",
|
||||
"additionalParticipants": "додаткові учасники",
|
||||
"newVersion": "встановлено нову версію ({{version}})",
|
||||
"viewReleaseNotes": "переглянути список змін",
|
||||
"albumGain": "підсилення альбому",
|
||||
"albumPeak": "піковий рівень альбому",
|
||||
"areYouSure": "ви впевнені?",
|
||||
"ascending": "зростаючи",
|
||||
"backward": "назад",
|
||||
"biography": "біографія",
|
||||
"bitDepth": "розрядність",
|
||||
"bitrate": "бітрейт",
|
||||
"bpm": "уд/хв",
|
||||
"cancel": "скасувати",
|
||||
"center": "посередині",
|
||||
"channel_one": "канал",
|
||||
"add": "Додати",
|
||||
"additionalParticipants": "Додаткові учасники",
|
||||
"newVersion": "Встановлено нову версію ({{version}})",
|
||||
"viewReleaseNotes": "Переглянути список змін",
|
||||
"albumGain": "Підсилення альбому",
|
||||
"albumPeak": "Піковий рівень альбому",
|
||||
"areYouSure": "Ви впевнені?",
|
||||
"ascending": "Зростаючи",
|
||||
"backward": "Назад",
|
||||
"biography": "Біографія",
|
||||
"bitDepth": "Розрядність",
|
||||
"bitrate": "Бітрейт",
|
||||
"bpm": "Уд/хв",
|
||||
"cancel": "Скасувати",
|
||||
"center": "Посередині",
|
||||
"channel_one": "Канал",
|
||||
"channel_few": "канали",
|
||||
"channel_many": "каналів",
|
||||
"clear": "очистити",
|
||||
"close": "закрити",
|
||||
"codec": "кодек",
|
||||
"collapse": "згорнути",
|
||||
"comingSoon": "скоро…",
|
||||
"configure": "налаштувати",
|
||||
"confirm": "підтвердити",
|
||||
"create": "створити",
|
||||
"currentSong": "поточний $t(entity.track, {\"count\": 1})",
|
||||
"decrease": "знизити",
|
||||
"delete": "видалити",
|
||||
"descending": "за спаданням",
|
||||
"description": "опис",
|
||||
"disable": "вимкнути",
|
||||
"disc": "диск",
|
||||
"dismiss": "відхилити",
|
||||
"doNotShowAgain": "не показувати це знову",
|
||||
"duration": "тривалість",
|
||||
"view": "показати",
|
||||
"edit": "змінити",
|
||||
"enable": "увімкнути",
|
||||
"expand": "розширити",
|
||||
"example": "приклад",
|
||||
"externalLinks": "зовнішні посилання",
|
||||
"faster": "швидше",
|
||||
"favorite": "улюблений",
|
||||
"filter_one": "фільтр",
|
||||
"clear": "Очистити",
|
||||
"close": "Закрити",
|
||||
"codec": "Кодек",
|
||||
"collapse": "Згорнути",
|
||||
"comingSoon": "Скоро…",
|
||||
"configure": "Налаштувати",
|
||||
"confirm": "Підтвердити",
|
||||
"create": "Створити",
|
||||
"currentSong": "Поточний $t(entity.track, {\"count\": 1})",
|
||||
"decrease": "Знизити",
|
||||
"delete": "Видалити",
|
||||
"descending": "За спаданням",
|
||||
"description": "Опис",
|
||||
"disable": "Вимкнути",
|
||||
"disc": "Диск",
|
||||
"dismiss": "Відхилити",
|
||||
"doNotShowAgain": "Не показувати це знову",
|
||||
"duration": "Тривалість",
|
||||
"view": "Показати",
|
||||
"edit": "Змінити",
|
||||
"enable": "Увімкнути",
|
||||
"expand": "Розширити",
|
||||
"example": "Приклад",
|
||||
"externalLinks": "Зовнішні посилання",
|
||||
"faster": "Швидше",
|
||||
"favorite": "Улюблений",
|
||||
"filter_one": "Фільтр",
|
||||
"filter_few": "фільтри",
|
||||
"filter_many": "фільтрів",
|
||||
"filters": "фільтри",
|
||||
"filter_single": "одиночний",
|
||||
"filter_multiple": "кілька",
|
||||
"forceRestartRequired": "перезапустіть, щоб застосувати зміни… закрийте повідомлення, щоб перезапустити",
|
||||
"forward": "уперед",
|
||||
"gap": "прогалина",
|
||||
"home": "додому",
|
||||
"increase": "збільшити",
|
||||
"left": "ліво",
|
||||
"limit": "ліміт",
|
||||
"manage": "управління",
|
||||
"maximize": "максимізувати",
|
||||
"menu": "меню",
|
||||
"minimize": "мінімізувати",
|
||||
"modified": "відредаговано",
|
||||
"filters": "Фільтри",
|
||||
"filter_single": "Одиночний",
|
||||
"filter_multiple": "Кілька",
|
||||
"forceRestartRequired": "Перезапустіть, щоб застосувати зміни… закрийте повідомлення, щоб перезапустити",
|
||||
"forward": "Уперед",
|
||||
"gap": "Прогалина",
|
||||
"grouping": "Групування",
|
||||
"home": "Додому",
|
||||
"increase": "Збільшити",
|
||||
"left": "Ліво",
|
||||
"limit": "Ліміт",
|
||||
"manage": "Управління",
|
||||
"maximize": "Максимізувати",
|
||||
"menu": "Меню",
|
||||
"minimize": "Мінімізувати",
|
||||
"modified": "Відредаговано",
|
||||
"mbid": "MusicBrainz ID",
|
||||
"mood": "настрій",
|
||||
"name": "назва",
|
||||
"no": "ні",
|
||||
"none": "жоден",
|
||||
"noResultsFromQuery": "запит не дав результатів",
|
||||
"noFilters": "фільтри не налаштовані",
|
||||
"note": "примітка",
|
||||
"ok": "ок",
|
||||
"owner": "власник",
|
||||
"path": "шлях",
|
||||
"playerMustBePaused": "плеєр повинен бути призупинений",
|
||||
"preview": "перегляд",
|
||||
"previousSong": "минулий $t(entity.track, {\"count\": 1})",
|
||||
"private": "приватний",
|
||||
"public": "публічний",
|
||||
"quit": "вийти",
|
||||
"random": "випадково",
|
||||
"rating": "рейтинг",
|
||||
"retry": "повторити спробу",
|
||||
"recordLabel": "лейбл звукозапису",
|
||||
"releaseType": "тип випуску",
|
||||
"refresh": "оновити",
|
||||
"reload": "перезавантажити",
|
||||
"rename": "перейменувати",
|
||||
"reset": "скинути",
|
||||
"resetToDefault": "скинути до заводських налаштувань",
|
||||
"restartRequired": "необхідний перезапуск",
|
||||
"right": "право",
|
||||
"clean": "чистo",
|
||||
"sampleRate": "частота дискретизації",
|
||||
"save": "зберегти",
|
||||
"saveAndReplace": "зберегти та замінити",
|
||||
"saveAs": "зберегти як",
|
||||
"search": "пошук",
|
||||
"setting_one": "налаштування",
|
||||
"mood": "Настрій",
|
||||
"name": "Назва",
|
||||
"no": "Ні",
|
||||
"none": "Жоден",
|
||||
"noResultsFromQuery": "Запит не дав результатів",
|
||||
"noFilters": "Фільтри не налаштовані",
|
||||
"note": "Примітка",
|
||||
"ok": "Ок",
|
||||
"owner": "Власник",
|
||||
"path": "Шлях",
|
||||
"playerMustBePaused": "Плеєр повинен бути призупинений",
|
||||
"preview": "Перегляд",
|
||||
"previousSong": "Минулий $t(entity.track, {\"count\": 1})",
|
||||
"private": "Приватний",
|
||||
"public": "Публічний",
|
||||
"quit": "Вийти",
|
||||
"random": "Випадково",
|
||||
"rating": "Рейтинг",
|
||||
"retry": "Повторити спробу",
|
||||
"recordLabel": "Лейбл звукозапису",
|
||||
"releaseType": "Тип випуску",
|
||||
"refresh": "Оновити",
|
||||
"reload": "Перезавантажити",
|
||||
"rename": "Перейменувати",
|
||||
"reset": "Скинути",
|
||||
"resetToDefault": "Скинути до заводських налаштувань",
|
||||
"restartRequired": "Необхідний перезапуск",
|
||||
"right": "Право",
|
||||
"clean": "Чистo",
|
||||
"sampleRate": "Частота дискретизації",
|
||||
"save": "Зберегти",
|
||||
"saveAndReplace": "Зберегти та замінити",
|
||||
"saveAs": "Зберегти як",
|
||||
"search": "Пошук",
|
||||
"setting_one": "Налаштування",
|
||||
"setting_few": "налаштування",
|
||||
"setting_many": "налаштувань",
|
||||
"slower": "повільніше",
|
||||
"share": "поділитися",
|
||||
"size": "розмір",
|
||||
"sort": "впорядкувати",
|
||||
"sortOrder": "порядок",
|
||||
"tags": "теги",
|
||||
"title": "назва",
|
||||
"trackNumber": "трек",
|
||||
"trackGain": "підсилення треку",
|
||||
"trackPeak": "піковий рівень треку",
|
||||
"translation": "переклад",
|
||||
"unknown": "невідомий",
|
||||
"version": "версія",
|
||||
"year": "рік",
|
||||
"yes": "так",
|
||||
"slower": "Повільніше",
|
||||
"share": "Поділитися",
|
||||
"size": "Розмір",
|
||||
"sort": "Впорядкувати",
|
||||
"sortOrder": "Порядок",
|
||||
"tags": "Теги",
|
||||
"title": "Назва",
|
||||
"trackNumber": "Трек",
|
||||
"trackGain": "Підсилення треку",
|
||||
"trackPeak": "Піковий рівень треку",
|
||||
"translation": "Переклад",
|
||||
"unknown": "Невідомий",
|
||||
"version": "Версія",
|
||||
"year": "Рік",
|
||||
"yes": "Так",
|
||||
"explicit": "Експліцитний зміст",
|
||||
"gridRows": "рядки сітки",
|
||||
"tableColumns": "стовпці таблиці",
|
||||
"itemsMore": "{{count}} більше"
|
||||
"gridRows": "Рядки сітки",
|
||||
"tableColumns": "Стовпці таблиці",
|
||||
"itemsMore": "{{count}} більше",
|
||||
"numberOfResults": "{{numberOfResults}} результатів",
|
||||
"newVersionAvailable": "Доступна нова версія"
|
||||
},
|
||||
"entity": {
|
||||
"album_one": "альбом",
|
||||
"album_one": "Альбом",
|
||||
"album_few": "альбоми",
|
||||
"album_many": "альбомів",
|
||||
"albumArtist_one": "виконавець альбому",
|
||||
"albumArtist_one": "Виконавець альбому",
|
||||
"albumArtist_few": "виконавці альбому",
|
||||
"albumArtist_many": "виконавців альбому",
|
||||
"albumArtistCount_one": "{{count}} виконавець альбому",
|
||||
@@ -178,34 +184,34 @@
|
||||
"albumWithCount_one": "{{count}} альбом",
|
||||
"albumWithCount_few": "{{count}} альбоми",
|
||||
"albumWithCount_many": "{{count}} альбомів",
|
||||
"radioStation_one": "радіостанція",
|
||||
"radioStation_one": "Радіостанція",
|
||||
"radioStation_few": "радіостанції",
|
||||
"radioStation_many": "радіостанцій",
|
||||
"radioStationWithCount_one": "{{count}} радіостанція",
|
||||
"radioStationWithCount_few": "{{count}} радіостанції",
|
||||
"radioStationWithCount_many": "{{count}} радіостанцій",
|
||||
"artist_one": "виконавець",
|
||||
"artist_one": "Виконавець",
|
||||
"artist_few": "виконавці",
|
||||
"artist_many": "виконавців",
|
||||
"artistWithCount_one": "{{count}} виконавець",
|
||||
"artistWithCount_few": "{{count}} виконавці",
|
||||
"artistWithCount_many": "{{count}} виконавців",
|
||||
"favorite_one": "улюблений",
|
||||
"favorite_one": "Улюблений",
|
||||
"favorite_few": "улюблені",
|
||||
"favorite_many": "улюблених",
|
||||
"folder_one": "папка",
|
||||
"folder_one": "Папка",
|
||||
"folder_few": "папки",
|
||||
"folder_many": "папок",
|
||||
"folderWithCount_one": "{{count}} папка",
|
||||
"folderWithCount_few": "{{count}} папки",
|
||||
"folderWithCount_many": "{{count}} папок",
|
||||
"genre_one": "жанр",
|
||||
"genre_one": "Жанр",
|
||||
"genre_few": "жанри",
|
||||
"genre_many": "жанрів",
|
||||
"genreWithCount_one": "{{count}} жанр",
|
||||
"genreWithCount_few": "{{count}} жанри",
|
||||
"genreWithCount_many": "{{count}} жанрів",
|
||||
"playlist_one": "плейлист",
|
||||
"playlist_one": "Плейлист",
|
||||
"playlist_few": "плейлисти",
|
||||
"playlist_many": "плейлистів",
|
||||
"play_one": "{{count}} відтворення",
|
||||
@@ -214,11 +220,11 @@
|
||||
"playlistWithCount_one": "{{count}} плейлист",
|
||||
"playlistWithCount_few": "{{count}} плейлисти",
|
||||
"playlistWithCount_many": "{{count}} плейлистів",
|
||||
"smartPlaylist": "розумний $t(entity.playlist, {\"count\": 1})",
|
||||
"track_one": "трек",
|
||||
"smartPlaylist": "Розумний $t(entity.playlist, {\"count\": 1})",
|
||||
"track_one": "Трек",
|
||||
"track_few": "треки",
|
||||
"track_many": "треків",
|
||||
"song_one": "пісня",
|
||||
"song_one": "Пісня",
|
||||
"song_few": "пісні",
|
||||
"song_many": "пісень",
|
||||
"trackWithCount_one": "{{count}} трек",
|
||||
@@ -226,258 +232,266 @@
|
||||
"trackWithCount_many": "{{count}} треків"
|
||||
},
|
||||
"error": {
|
||||
"apiRouteError": "неможливо виконати запит",
|
||||
"audioDeviceFetchError": "сталася помилка під час спроби отримати аудіопристрої",
|
||||
"authenticationFailed": "аутентифікація не вдалася",
|
||||
"badAlbum": "ви бачите цю сторінку, тому що ця пісня не входить до альбому. найімовірніше, ця проблема виникає, якщо у верхньому рівні вашої музичної папки знаходиться пісня. Jellyfin групує треки тільки в тому випадку, якщо вони знаходяться в папці",
|
||||
"badValue": "недійсний параметр \"{{value}}\". це значення більше не існує",
|
||||
"credentialsRequired": "необхідні дані для входу",
|
||||
"endpointNotImplementedError": "кінцева точка {{endpoint}} не реалізована для {{serverType}}",
|
||||
"genericError": "сталася помилка",
|
||||
"invalidServer": "недійсний сервер",
|
||||
"localFontAccessDenied": "відмова в доступі до локальних шрифтів",
|
||||
"loginRateError": "занадто багато спроб входу, спробуйте ще раз через кілька секунд",
|
||||
"mpvRequired": "необхідний MPV",
|
||||
"multipleServerSaveQueueError": "у черзі відтворення є одна або кілька пісень, які не належать до поточного сервера. це не підтримується",
|
||||
"networkError": "сталася мережева помилка",
|
||||
"noNetwork": "сервер недоступний",
|
||||
"noNetworkDescription": "не вдалося підключитися до цього сервера",
|
||||
"notificationDenied": "дозвіл на сповіщення було відхилено. це налаштування не має впливу",
|
||||
"openError": "не вдалося відкрити файл",
|
||||
"playbackError": "сталася помилка під час спроби відтворити медіафайл",
|
||||
"remoteDisableError": "сталася помилка під час спроби $t(common.disable) віддаленого сервера",
|
||||
"remoteEnableError": "сталася помилка під час спроби $t(common.enable) віддаленого сервера",
|
||||
"remotePortError": "сталася помилка під час спроби налаштувати порт віддаленого сервера",
|
||||
"remotePortWarning": "перезапустіть сервер щоб застосувати новий порт",
|
||||
"saveQueueFailed": "не вдалося зберегти чергу",
|
||||
"serverNotSelectedError": "не вибрано жодного сервера",
|
||||
"serverRequired": "потрібен сервер",
|
||||
"sessionExpiredError": "ваша сесія закінчилася",
|
||||
"systemFontError": "сталася помилка під час спроби отримати системні шрифти",
|
||||
"settingsSyncError": "виявлено розбіжності між налаштуваннями в рендерері та основним процесом. перезапустіть програму, щоб застосувати зміни"
|
||||
"apiRouteError": "Неможливо виконати запит",
|
||||
"audioDeviceFetchError": "Сталася помилка під час спроби отримати аудіопристрої",
|
||||
"authenticationFailed": "Аутентифікація не вдалася",
|
||||
"badAlbum": "Ви бачите цю сторінку, тому що ця пісня не входить до альбому. найімовірніше, ця проблема виникає, якщо у верхньому рівні вашої музичної папки знаходиться пісня. Jellyfin групує треки тільки в тому випадку, якщо вони знаходяться в папці",
|
||||
"badValue": "Недійсний параметр \"{{value}}\". це значення більше не існує",
|
||||
"credentialsRequired": "Необхідні дані для входу",
|
||||
"endpointNotImplementedError": "Кінцева точка {{endpoint}} не реалізована для {{serverType}}",
|
||||
"genericError": "Сталася помилка",
|
||||
"invalidServer": "Недійсний сервер",
|
||||
"localFontAccessDenied": "Відмова в доступі до локальних шрифтів",
|
||||
"loginRateError": "Занадто багато спроб входу, спробуйте ще раз через кілька секунд",
|
||||
"mpvRequired": "Необхідний MPV",
|
||||
"multipleServerSaveQueueError": "У черзі відтворення є одна або кілька пісень, які не належать до поточного сервера. це не підтримується",
|
||||
"networkError": "Сталася мережева помилка",
|
||||
"noNetwork": "Сервер недоступний",
|
||||
"noNetworkDescription": "Не вдалося підключитися до цього сервера",
|
||||
"notificationDenied": "Дозвіл на сповіщення було відхилено. це налаштування не має впливу",
|
||||
"openError": "Не вдалося відкрити файл",
|
||||
"playbackError": "Сталася помилка під час спроби відтворити медіафайл",
|
||||
"remoteDisableError": "Сталася помилка під час спроби $t(common.disable) віддаленого сервера",
|
||||
"remoteEnableError": "Сталася помилка під час спроби $t(common.enable) віддаленого сервера",
|
||||
"remotePortError": "Сталася помилка під час спроби налаштувати порт віддаленого сервера",
|
||||
"remotePortWarning": "Перезапустіть сервер щоб застосувати новий порт",
|
||||
"saveQueueFailed": "Не вдалося зберегти чергу",
|
||||
"serverNotSelectedError": "Не вибрано жодного сервера",
|
||||
"serverRequired": "Потрібен сервер",
|
||||
"sessionExpiredError": "Ваша сесія закінчилася",
|
||||
"systemFontError": "Сталася помилка під час спроби отримати системні шрифти",
|
||||
"settingsSyncError": "Виявлено розбіжності між налаштуваннями в рендерері та основним процесом. перезапустіть програму, щоб застосувати зміни",
|
||||
"invalidJson": "Недійсний JSON",
|
||||
"playbackPausedDueToError": "Відтворення було призупинено через помилку"
|
||||
},
|
||||
"filter": {
|
||||
"album": "$t(entity.album, {\"count\": 1})",
|
||||
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
|
||||
"albumCount": "кількість $t(entity.album, {\"count\": 2})",
|
||||
"albumCount": "Кількість $t(entity.album, {\"count\": 2})",
|
||||
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||
"biography": "біографія",
|
||||
"bitrate": "бітрейт",
|
||||
"bpm": "уд/хв",
|
||||
"biography": "Біографія",
|
||||
"bitrate": "Бітрейт",
|
||||
"bpm": "Уд/хв",
|
||||
"channels": "$t(common.channel, {\"count\": 2})",
|
||||
"comment": "коментар",
|
||||
"communityRating": "рейтинг спільноти",
|
||||
"criticRating": "рейтинг критиків",
|
||||
"dateAdded": "дата додавання",
|
||||
"disc": "диск",
|
||||
"duration": "тривалість",
|
||||
"favorited": "улюблене",
|
||||
"fromYear": "з року",
|
||||
"comment": "Коментар",
|
||||
"communityRating": "Рейтинг спільноти",
|
||||
"criticRating": "Рейтинг критиків",
|
||||
"dateAdded": "Дата додавання",
|
||||
"disc": "Диск",
|
||||
"duration": "Тривалість",
|
||||
"favorited": "Улюблене",
|
||||
"fromYear": "З року",
|
||||
"genre": "$t(entity.genre, {\"count\": 1})",
|
||||
"id": "id",
|
||||
"isCompilation": "є компіляцією",
|
||||
"isFavorited": "є улюбленим",
|
||||
"isPublic": "є публічним",
|
||||
"isRated": "є оціненим",
|
||||
"isRecentlyPlayed": "нещодавно відтворено",
|
||||
"lastPlayed": "нещодавно відтворені",
|
||||
"mostPlayed": "найбільш відтворювані",
|
||||
"name": "назва",
|
||||
"note": "примітка",
|
||||
"id": "Id",
|
||||
"isCompilation": "Є компіляцією",
|
||||
"isFavorited": "Є улюбленим",
|
||||
"isPublic": "Є публічним",
|
||||
"isRated": "Є оціненим",
|
||||
"isRecentlyPlayed": "Нещодавно відтворено",
|
||||
"lastPlayed": "Останнє відтворене",
|
||||
"mostPlayed": "Найбільш відтворювані",
|
||||
"name": "Назва",
|
||||
"note": "Примітка",
|
||||
"owner": "$t(common.owner)",
|
||||
"path": "шлях",
|
||||
"playCount": "кількість відтворень",
|
||||
"random": "випадково",
|
||||
"rating": "рейтинг",
|
||||
"recentlyAdded": "нещодавно додано",
|
||||
"recentlyPlayed": "нещодавно відтворено",
|
||||
"recentlyUpdated": "нещодавно оновлено",
|
||||
"releaseDate": "дата випуску",
|
||||
"releaseYear": "рік випуску",
|
||||
"search": "шукати",
|
||||
"songCount": "кількість пісень",
|
||||
"sortName": "сортування за назвою",
|
||||
"title": "назва",
|
||||
"toYear": "до року",
|
||||
"trackNumber": "трек",
|
||||
"explicitStatus": "$t(common.explicitStatus)"
|
||||
"path": "Шлях",
|
||||
"playCount": "Кількість відтворень",
|
||||
"random": "Випадково",
|
||||
"rating": "Рейтинг",
|
||||
"recentlyAdded": "Нещодавно додано",
|
||||
"recentlyPlayed": "Нещодавно відтворено",
|
||||
"recentlyUpdated": "Нещодавно оновлено",
|
||||
"releaseDate": "Дата випуску",
|
||||
"releaseYear": "Рік випуску",
|
||||
"search": "Шукати",
|
||||
"songCount": "Кількість пісень",
|
||||
"sortName": "Сортування за назвою",
|
||||
"title": "Назва",
|
||||
"toYear": "До року",
|
||||
"trackNumber": "Трек",
|
||||
"explicitStatus": "$t(common.explicitStatus)",
|
||||
"matchAnd": "І",
|
||||
"matchOr": "Або"
|
||||
},
|
||||
"datetime": {
|
||||
"minuteShort": "хв.",
|
||||
"secondShort": "сек.",
|
||||
"hourShort": "год",
|
||||
"dayShort": "дн."
|
||||
"minuteShort": "Хв.",
|
||||
"secondShort": "Сек.",
|
||||
"hourShort": "Год",
|
||||
"dayShort": "Дн."
|
||||
},
|
||||
"filterOperator": {
|
||||
"after": "є після",
|
||||
"afterDate": "після (дата)",
|
||||
"before": "є перед",
|
||||
"beforeDate": "є перед (дата)",
|
||||
"contains": "містить",
|
||||
"endsWith": "закінчується на",
|
||||
"inPlaylist": "є в",
|
||||
"inTheLast": "є в останньому",
|
||||
"inTheRange": "є в межах",
|
||||
"inTheRangeDate": "є в межах (дата)",
|
||||
"is": "є",
|
||||
"isNot": "не є",
|
||||
"isGreaterThan": "більше ніж",
|
||||
"isLessThan": "менше ніж",
|
||||
"matchesRegex": "відповідає регулярному виразу",
|
||||
"notContains": "не містить",
|
||||
"notInPlaylist": "немає в",
|
||||
"notInTheLast": "не є в останньому",
|
||||
"startsWith": "починається з"
|
||||
"after": "Є після",
|
||||
"afterDate": "Після (дата)",
|
||||
"before": "Є перед",
|
||||
"beforeDate": "Є перед (дата)",
|
||||
"contains": "Містить",
|
||||
"endsWith": "Закінчується на",
|
||||
"inPlaylist": "Є в",
|
||||
"inTheLast": "Є в останньому",
|
||||
"inTheRange": "Є в межах",
|
||||
"inTheRangeDate": "Є в межах (дата)",
|
||||
"is": "Є",
|
||||
"isNot": "Не є",
|
||||
"isGreaterThan": "Більше ніж",
|
||||
"isLessThan": "Менше ніж",
|
||||
"matchesRegex": "Відповідає регулярному виразу",
|
||||
"notContains": "Не містить",
|
||||
"notInPlaylist": "Немає в",
|
||||
"notInTheLast": "Не є в останньому",
|
||||
"startsWith": "Починається з"
|
||||
},
|
||||
"form": {
|
||||
"addServer": {
|
||||
"error_savePassword": "сталася помилка під час спроби зберегти пароль",
|
||||
"ignoreCors": "ігнорувати cors ($t(common.restartRequired))",
|
||||
"ignoreSsl": "ігнорувати ssl ($t(common.restartRequired)}",
|
||||
"input_legacyAuthentication": "увімкнути застарілу автентифікацію",
|
||||
"input_name": "назва сервера",
|
||||
"input_password": "пароль",
|
||||
"input_preferInstantMix": "віддавати перевагу миттєвому міксу",
|
||||
"input_preferInstantMixDescription": "використовувати тільки миттєвий мікс щоб отримати подібні пісні. корисно, коли у вас є плагіни, які змінюють цю поведінку",
|
||||
"input_preferRemoteUrl": "віддавати перевагу публічній URL-адресі",
|
||||
"input_remoteUrl": "публічна URL-адреса",
|
||||
"input_remoteUrlPlaceholder": "опціонально: публічна URL-адреса для зовнішніх функцій",
|
||||
"input_savePassword": "зберегти пароль",
|
||||
"error_savePassword": "Сталася помилка під час спроби зберегти пароль",
|
||||
"ignoreCors": "Ігнорувати cors ($t(common.restartRequired))",
|
||||
"ignoreSsl": "Ігнорувати ssl ($t(common.restartRequired)}",
|
||||
"input_legacyAuthentication": "Увімкнути застарілу автентифікацію",
|
||||
"input_name": "Назва сервера",
|
||||
"input_password": "Пароль",
|
||||
"input_preferInstantMix": "Віддавати перевагу миттєвому міксу",
|
||||
"input_preferInstantMixDescription": "Використовувати тільки миттєвий мікс щоб отримати подібні пісні. корисно, коли у вас є плагіни, які змінюють цю поведінку",
|
||||
"input_preferRemoteUrl": "Віддавати перевагу публічній URL-адресі",
|
||||
"input_remoteUrl": "Публічна URL-адреса",
|
||||
"input_remoteUrlPlaceholder": "Опціонально: публічна URL-адреса для зовнішніх функцій",
|
||||
"input_savePassword": "Зберегти пароль",
|
||||
"input_url": "URL-адреса",
|
||||
"input_username": "Ім'я користувача",
|
||||
"success": "сервер додано успішно",
|
||||
"title": "додати сервер"
|
||||
"success": "Сервер додано успішно",
|
||||
"title": "Додати сервер"
|
||||
},
|
||||
"largeFetchConfirmation": {
|
||||
"title": "додати елементи до черги",
|
||||
"title": "Додати елементи до черги",
|
||||
"description": "Ця дія додасть усі елементи в поточний відфільтрований перегляд"
|
||||
},
|
||||
"addToPlaylist": {
|
||||
"create": "створити $t(entity.playlist, {\"count\": 1}) {{playlist}}",
|
||||
"create": "Створити $t(entity.playlist, {\"count\": 1}) {{playlist}}",
|
||||
"input_playlists": "$t(entity.playlist, {\"count\": 2})",
|
||||
"input_skipDuplicates": "пропустити дублікати",
|
||||
"searchOrCreate": "шукайте $t(entity.playlist, {\"count\": 2}) або пишіть, щоб створити новий",
|
||||
"success": "додано $t(entity.trackWithCount, {\"count\": {{message}} }) до $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
||||
"title": "додати до $t(entity.playlist, {\"count\": 1})"
|
||||
"input_skipDuplicates": "Пропустити дублікати",
|
||||
"searchOrCreate": "Шукайте $t(entity.playlist, {\"count\": 2}) або пишіть, щоб створити новий",
|
||||
"success": "Додано $t(entity.trackWithCount, {\"count\": {{message}} }) до $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
||||
"title": "Додати до $t(entity.playlist, {\"count\": 1})"
|
||||
},
|
||||
"createPlaylist": {
|
||||
"input_description": "$t(common.description)",
|
||||
"input_name": "$t(common.name)",
|
||||
"input_owner": "$t(common.owner)",
|
||||
"input_public": "публічний",
|
||||
"input_public": "Публічний",
|
||||
"success": "$t(entity.playlist, {\"count\": 1}) стрворено успішно",
|
||||
"title": "створити $t(entity.playlist, {\"count\": 1})"
|
||||
"title": "Створити $t(entity.playlist, {\"count\": 1})"
|
||||
},
|
||||
"createRadioStation": {
|
||||
"success": "радіостанція створена успішно",
|
||||
"title": "створити радіостанцію",
|
||||
"input_homepageUrl": "адреса домашньої сторінки",
|
||||
"input_name": "назва",
|
||||
"success": "Радіостанція створена успішно",
|
||||
"title": "Створити радіостанцію",
|
||||
"input_homepageUrl": "Адреса домашньої сторінки",
|
||||
"input_name": "Назва",
|
||||
"input_streamUrl": "URL-адреса потоку"
|
||||
},
|
||||
"deletePlaylist": {
|
||||
"input_confirm": "введіть ім'я $t(entity.playlist, {\"count\": 1}) для підтвердження",
|
||||
"input_confirm": "Введіть ім'я $t(entity.playlist, {\"count\": 1}) для підтвердження",
|
||||
"success": "$t(entity.playlist, {\"count\": 1}) успішно видалено",
|
||||
"title": "видалити $t(entity.playlist, {\"count\": 1})"
|
||||
"title": "Видалити $t(entity.playlist, {\"count\": 1})"
|
||||
},
|
||||
"editPlaylist": {
|
||||
"publicJellyfinNote": "Jellyfin з якоїсь причини не показує, чи є плейлист публічним чи ні. Якщо ви хочете, щоб він залишався публічним, виберіть варіант нижче",
|
||||
"editNote": "ручне редагування не рекомендується для великих плейлистів. ви впевнені, що готові прийняти ризик втрати даних, який виникає при перезапису існуючого плейлисту?",
|
||||
"success": "$t(entity.playlist, {\"count\": 1}) успішно оновлено",
|
||||
"title": "змінити $t(entity.playlist, {\"count\": 1})"
|
||||
"title": "Змінити $t(entity.playlist, {\"count\": 1})"
|
||||
},
|
||||
"lyricsExport": {
|
||||
"export": "експортувати тексти пісень",
|
||||
"input_synced": "експортувати синхронізовані тексти пісень",
|
||||
"export": "Експортувати тексти пісень",
|
||||
"input_synced": "Експортувати синхронізовані тексти пісень",
|
||||
"input_offset": "$t(setting.lyricOffset)"
|
||||
},
|
||||
"lyricSearch": {
|
||||
"input_artist": "$t(entity.artist, {\"count\": 1})",
|
||||
"input_name": "$t(common.name)",
|
||||
"title": "шукати тексти пісень"
|
||||
"title": "Шукати тексти пісень"
|
||||
},
|
||||
"queryEditor": {
|
||||
"title": "редактор запитів",
|
||||
"input_optionMatchAll": "збіг за всіма",
|
||||
"input_optionMatchAny": "збіг за будь-яким",
|
||||
"addRuleGroup": "додати групу правил",
|
||||
"removeRuleGroup": "видалити групу правил",
|
||||
"resetToDefault": "скинути до заводських налаштувань",
|
||||
"clearFilters": "очистити фільтри"
|
||||
"title": "Редактор запитів",
|
||||
"input_optionMatchAll": "Збіг за всіма",
|
||||
"input_optionMatchAny": "Збіг за будь-яким",
|
||||
"addRuleGroup": "Додати групу правил",
|
||||
"removeRuleGroup": "Видалити групу правил",
|
||||
"resetToDefault": "Скинути до заводських налаштувань",
|
||||
"clearFilters": "Очистити фільтри"
|
||||
},
|
||||
"saveQueue": {
|
||||
"success": "черга відтворення збережена на сервері"
|
||||
"success": "Черга відтворення збережена на сервері"
|
||||
},
|
||||
"shareItem": {
|
||||
"allowDownloading": "дозволити завантаження",
|
||||
"description": "опис",
|
||||
"setExpiration": "встановити термін дії",
|
||||
"success": "посилання для спільного використання скопійовано в буфер обміну (натисніть тут, щоб відкрити)",
|
||||
"expireInvalid": "термін дії повинен бути в майбутньому",
|
||||
"createFailed": "не вдалося створити спільний доступ (чи ввімкнено спільний доступ?)"
|
||||
"allowDownloading": "Дозволити завантаження",
|
||||
"description": "Опис",
|
||||
"setExpiration": "Встановити термін дії",
|
||||
"success": "Посилання для спільного використання скопійовано в буфер обміну (натисніть тут, щоб відкрити)",
|
||||
"expireInvalid": "Термін дії повинен бути в майбутньому",
|
||||
"createFailed": "Не вдалося створити спільний доступ (чи ввімкнено спільний доступ?)",
|
||||
"copyToClipboard": "Скопіювати до буфера обміну: Ctrl+C, enter",
|
||||
"successMustClick": "Посилання успішно створено, натисніть сюди, щоб відкрити"
|
||||
},
|
||||
"shuffleAll": {
|
||||
"title": "відтворити випадково",
|
||||
"title": "Відтворити випадково",
|
||||
"input_genre": "$t(entity.genre, {\"count\": 1})",
|
||||
"input_limit": "скільки пісень?",
|
||||
"input_minYear": "від року",
|
||||
"input_maxYear": "до року",
|
||||
"input_played": "відтворити фільтр",
|
||||
"input_played_optionAll": "всі треки",
|
||||
"input_played_optionUnplayed": "тільки не відтворені треки",
|
||||
"input_played_optionPlayed": "тільки відтворені треки"
|
||||
"input_limit": "Скільки пісень?",
|
||||
"input_minYear": "Від року",
|
||||
"input_maxYear": "До року",
|
||||
"input_played": "Відтворити фільтр",
|
||||
"input_played_optionAll": "Всі треки",
|
||||
"input_played_optionUnplayed": "Тільки не відтворені треки",
|
||||
"input_played_optionPlayed": "Тільки відтворені треки"
|
||||
},
|
||||
"updateServer": {
|
||||
"success": "сервер успішно оновлено",
|
||||
"title": "оновити сервер"
|
||||
"success": "Сервер успішно оновлено",
|
||||
"title": "Оновити сервер"
|
||||
},
|
||||
"privateMode": {
|
||||
"enabled": "приватний режим увімкнено, стан відтворення тепер приховано від зовнішніх інтеграцій",
|
||||
"disabled": "приватний режим вимкнено, стан відтворення тепер видно для увімкнених зовнішніх інтеграцій",
|
||||
"title": "приватний режим"
|
||||
"enabled": "Приватний режим увімкнено, стан відтворення тепер приховано від зовнішніх інтеграцій",
|
||||
"disabled": "Приватний режим вимкнено, стан відтворення тепер видно для увімкнених зовнішніх інтеграцій",
|
||||
"title": "Приватний режим"
|
||||
},
|
||||
"editRadioStation": {
|
||||
"success": "Радіо станція успішно оновлена"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"skip": "пропустити"
|
||||
"skip": "Пропустити"
|
||||
},
|
||||
"page": {
|
||||
"albumArtistDetail": {
|
||||
"about": "Про {{artist}}",
|
||||
"appearsOn": "з'являється на",
|
||||
"favoriteSongs": "улюблені пісні",
|
||||
"groupingTypeAll": "всі типи випуску",
|
||||
"groupingTypePrimary": "основні типи випуску",
|
||||
"recentReleases": "останні випуски",
|
||||
"viewDiscography": "переглянути дискографію",
|
||||
"relatedArtists": "подібні $t(entity.artist, {\"count\": 2})",
|
||||
"topSongs": "найкращі пісні",
|
||||
"topSongsCommunity": "спільнота",
|
||||
"topSongsFrom": "найкращі пісні від {{title}}",
|
||||
"topSongsPersonal": "особисте",
|
||||
"favoriteSongsFrom": "улюблені пісні від {{title}}",
|
||||
"viewAll": "показати все",
|
||||
"viewAllTracks": "показати усі $t(entity.track, {\"count\": 2})"
|
||||
"appearsOn": "З'являється на",
|
||||
"favoriteSongs": "Улюблені пісні",
|
||||
"groupingTypeAll": "Всі типи випуску",
|
||||
"groupingTypePrimary": "Основні типи випуску",
|
||||
"recentReleases": "Останні випуски",
|
||||
"viewDiscography": "Переглянути дискографію",
|
||||
"relatedArtists": "Подібні $t(entity.artist, {\"count\": 2})",
|
||||
"topSongs": "Найкращі пісні",
|
||||
"topSongsCommunity": "Спільнота",
|
||||
"topSongsFrom": "Найкращі пісні від {{title}}",
|
||||
"topSongsPersonal": "Особисте",
|
||||
"favoriteSongsFrom": "Улюблені пісні від {{title}}",
|
||||
"viewAll": "Показати все",
|
||||
"viewAllTracks": "Показати усі $t(entity.track, {\"count\": 2})"
|
||||
},
|
||||
"albumArtistList": {
|
||||
"title": "$t(entity.albumArtist, {\"count\": 2})"
|
||||
},
|
||||
"albumDetail": {
|
||||
"moreFromArtist": "більше від цього $t(entity.artist, {\"count\": 1})",
|
||||
"moreFromGeneric": "більше від {{item}}",
|
||||
"released": "видано"
|
||||
"moreFromArtist": "Більше від цього $t(entity.artist, {\"count\": 1})",
|
||||
"moreFromGeneric": "Більше від {{item}}",
|
||||
"released": "Видано"
|
||||
},
|
||||
"albumList": {
|
||||
"artistAlbums": "альбоми виконавця {{artist}}",
|
||||
"artistAlbums": "Альбоми виконавця {{artist}}",
|
||||
"genreAlbums": "\"{{genre}}\" $t(entity.album, {\"count\": 2})",
|
||||
"title": "$t(entity.album, {\"count\": 2})"
|
||||
},
|
||||
"radioList": {
|
||||
"title": "радіостанції"
|
||||
"title": "Радіостанції"
|
||||
},
|
||||
"releasenotes": {
|
||||
"commitsSinceStable": "комміти від {{stable}}",
|
||||
"noNewCommits": "немає нових коммітів у цьому періоді",
|
||||
"noStableReleaseToCompare": "немає доступної стабільної версії для порівняння"
|
||||
"commitsSinceStable": "Комміти від {{stable}}",
|
||||
"noNewCommits": "Немає нових коммітів у цьому періоді",
|
||||
"noStableReleaseToCompare": "Немає доступної стабільної версії для порівняння"
|
||||
},
|
||||
"favorites": {
|
||||
"title": "$t(entity.favorite, {\"count\": 2})"
|
||||
@@ -487,30 +501,30 @@
|
||||
"privateMode": "(Приватний режим)"
|
||||
},
|
||||
"appMenu": {
|
||||
"collapseSidebar": "згорнути бічну панель",
|
||||
"commandPalette": "відкрити палітру команд",
|
||||
"expandSidebar": "розгорнути бічну панель",
|
||||
"goBack": "повернутися назад",
|
||||
"goForward": "перейти вперед",
|
||||
"manageServers": "управління серверами",
|
||||
"privateModeOff": "вимкнути приватний режим",
|
||||
"privateModeOn": "увімкнути приватний режим",
|
||||
"openBrowserDevtools": "відкрити інструменти розробника",
|
||||
"collapseSidebar": "Згорнути бічну панель",
|
||||
"commandPalette": "Відкрити палітру команд",
|
||||
"expandSidebar": "Розгорнути бічну панель",
|
||||
"goBack": "Повернутися назад",
|
||||
"goForward": "Перейти вперед",
|
||||
"manageServers": "Управління серверами",
|
||||
"privateModeOff": "Вимкнути приватний режим",
|
||||
"privateModeOn": "Увімкнути приватний режим",
|
||||
"openBrowserDevtools": "Відкрити інструменти розробника",
|
||||
"quit": "$t(common.quit)",
|
||||
"selectServer": "вибрати сервер",
|
||||
"selectMusicFolder": "вибрати папку з музикою",
|
||||
"noMusicFolder": "не вибрано папку з музикою",
|
||||
"selectServer": "Вибрати сервер",
|
||||
"selectMusicFolder": "Вибрати папку з музикою",
|
||||
"noMusicFolder": "Не вибрано папку з музикою",
|
||||
"multipleMusicFolders": "Вибрано {{count}} папок з музикою",
|
||||
"settings": "$t(common.setting, {\"count\": 2})",
|
||||
"version": "версія {{version}}"
|
||||
"version": "Версія {{version}}"
|
||||
},
|
||||
"manageServers": {
|
||||
"title": "управління серверами",
|
||||
"serverDetails": "інформація про сервер",
|
||||
"title": "Управління серверами",
|
||||
"serverDetails": "Інформація про сервер",
|
||||
"url": "URL-адреса",
|
||||
"username": "Ім'я користувача",
|
||||
"editServerDetailsTooltip": "редагувати дані сервера",
|
||||
"removeServer": "видалити сервер"
|
||||
"editServerDetailsTooltip": "Редагувати дані сервера",
|
||||
"removeServer": "Видалити сервер"
|
||||
},
|
||||
"contextMenu": {
|
||||
"addFavorite": "$t(action.addToFavorites)",
|
||||
@@ -521,7 +535,7 @@
|
||||
"createPlaylist": "$t(action.createPlaylist)",
|
||||
"deletePlaylist": "$t(action.deletePlaylist)",
|
||||
"deselectAll": "$t(action.deselectAll)",
|
||||
"download": "завантажити",
|
||||
"download": "Завантажити",
|
||||
"moveItems": "$t(action.moveItems)",
|
||||
"moveToNext": "$t(action.moveToNext)",
|
||||
"moveToBottom": "$t(action.moveToBottom)",
|
||||
@@ -534,11 +548,57 @@
|
||||
"removeFromQueue": "$t(action.removeFromQueue)",
|
||||
"setRating": "$t(action.setRating)",
|
||||
"playShuffled": "$t(player.shuffle)",
|
||||
"shareItem": "поділитися елементом",
|
||||
"goTo": "перейти до",
|
||||
"goToAlbum": "перейти до $t(entity.album, {\"count\": 1})",
|
||||
"goToAlbumArtist": "перейти до $t(entity.albumArtist, {\"count\": 1})",
|
||||
"showDetails": "отримати інформацію"
|
||||
"shareItem": "Поділитися елементом",
|
||||
"goTo": "Перейти до",
|
||||
"goToAlbum": "Перейти до $t(entity.album, {\"count\": 1})",
|
||||
"goToAlbumArtist": "Перейти до $t(entity.albumArtist, {\"count\": 1})",
|
||||
"showDetails": "Отримати інформацію"
|
||||
},
|
||||
"fullscreenPlayer": {
|
||||
"config": {
|
||||
"dynamicBackground": "Динамічний фон",
|
||||
"dynamicImageBlur": "Розмір розмиття зображення",
|
||||
"dynamicIsImage": "Включити фонове зображення",
|
||||
"followCurrentLyric": "Слідкувати за поточним рядком",
|
||||
"lyricAlignment": "Вирівнювання тексту",
|
||||
"lyricOffset": "Затримка тексту (мс)",
|
||||
"lyricGap": "Розмір між рядками",
|
||||
"lyricSize": "Розмір тексту",
|
||||
"opacity": "Непрозорість",
|
||||
"showLyricMatch": "Показувати збіг тексту пісень",
|
||||
"showLyricProvider": "Показувати джерело тексту пісень",
|
||||
"synchronized": "Синхронізовано",
|
||||
"unsynchronized": "Несинхронізовано",
|
||||
"useImageAspectRatio": "Використовувати співвідношення сторін зображення"
|
||||
},
|
||||
"lyrics": "Текст пісні",
|
||||
"related": "Пов'язані",
|
||||
"upNext": "Далі",
|
||||
"visualizer": "Візуалізатор",
|
||||
"noLyrics": "Текст пісні не знайдено"
|
||||
},
|
||||
"genreList": {
|
||||
"showAlbums": "Показати $t(entity.genre, {\"count\": 1}) $t(entity.album, {\"count\": 2})",
|
||||
"showTracks": "Показати $t(entity.genre, {\"count\": 1}) $t(entity.track, {\"count\": 2})",
|
||||
"title": "$t(entity.genre, {\"count\": 2})"
|
||||
},
|
||||
"folderList": {
|
||||
"title": "$t(entity.folder, {\"count\": 2})"
|
||||
},
|
||||
"globalSearch": {
|
||||
"commands": {
|
||||
"goToPage": "Перейти до сторінки",
|
||||
"searchFor": "Шукати на {{query}}",
|
||||
"serverCommands": "Команди сервера"
|
||||
},
|
||||
"title": "Команди"
|
||||
},
|
||||
"home": {
|
||||
"explore": "Дослідити з вашої бібліотеки",
|
||||
"genres": "$t(entity.genre, {\"count\": 2})",
|
||||
"mostPlayed": "Найбільш відтворені",
|
||||
"newlyAdded": "Нещодавно додані релізи",
|
||||
"recentlyPlayed": "Нещодавно відтворені"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,10 @@
|
||||
"goToPage": "前往页面",
|
||||
"openIn": {
|
||||
"lastfm": "在 Last.fm 中打开",
|
||||
"musicbrainz": "在 MusicBrainz 中打开"
|
||||
"musicbrainz": "在 MusicBrainz 中打开",
|
||||
"listenbrainz": "在 ListenBrainz 中打开",
|
||||
"qobuz": "在 Qobuz 中打开",
|
||||
"spotify": "在 Spotify 中打开"
|
||||
},
|
||||
"moveToNext": "移至下一首",
|
||||
"downloadStarted": "开始下载 {{count}} 个项目",
|
||||
@@ -43,7 +46,7 @@
|
||||
"common": {
|
||||
"increase": "增高",
|
||||
"rating": "评分",
|
||||
"bpm": "bpm",
|
||||
"bpm": "BPM",
|
||||
"refresh": "刷新",
|
||||
"unknown": "未知",
|
||||
"edit": "编辑",
|
||||
@@ -157,7 +160,9 @@
|
||||
"mood": "氛围",
|
||||
"rename": "重命名",
|
||||
"filter_multiple": "多项",
|
||||
"newVersionAvailable": "a new version is available"
|
||||
"newVersionAvailable": "新版本现已可用",
|
||||
"numberOfResults": "{{numberOfResults}} 结果",
|
||||
"grouping": "分组"
|
||||
},
|
||||
"entity": {
|
||||
"albumArtist_other": "专辑艺术家",
|
||||
@@ -203,8 +208,8 @@
|
||||
"queue_clear": "清空播放队列",
|
||||
"muted": "已静音",
|
||||
"unfavorite": "取消收藏",
|
||||
"queue_moveToTop": "将所选项移至底部",
|
||||
"queue_moveToBottom": "将所选项移至顶部",
|
||||
"queue_moveToTop": "将所选项移至顶部",
|
||||
"queue_moveToBottom": "将所选项移至底部",
|
||||
"shuffle_off": "禁用随机播放",
|
||||
"addLast": "最后",
|
||||
"mute": "静音",
|
||||
@@ -235,12 +240,12 @@
|
||||
"setting": {
|
||||
"crossfadeStyle_description": "选择用于音频播放器的淡入淡出风格",
|
||||
"hotkey_favoriteCurrentSong": "收藏$t(common.currentSong)",
|
||||
"audioExclusiveMode_description": "启用独占输出模式。在此模式下,系统通常被锁定为只有 mpv 能够输出音频",
|
||||
"audioExclusiveMode_description": "启用独占输出模式。在此模式下,系统通常被锁定为只有 MPV 能够输出音频",
|
||||
"disableLibraryUpdateOnStartup": "禁用启动时查询新版本",
|
||||
"gaplessAudio": "无缝音频",
|
||||
"audioPlayer_description": "选择用于播放的音频播放器",
|
||||
"globalMediaHotkeys": "全局媒体快捷键",
|
||||
"gaplessAudio_description": "调整 mpv 无缝音频设置",
|
||||
"gaplessAudio_description": "调整 MPV 无缝音频设置",
|
||||
"followLyric_description": "滚动歌词到当前播放位置",
|
||||
"audioExclusiveMode": "音频独占模式",
|
||||
"font": "字体",
|
||||
@@ -256,7 +261,7 @@
|
||||
"followLyric": "跟随当前歌词",
|
||||
"crossfadeDuration": "淡入淡出持续时间",
|
||||
"audioPlayer": "音频播放器",
|
||||
"discordApplicationId": "{{discord}} 应用 id",
|
||||
"discordApplicationId": "{{discord}} 应用 ID",
|
||||
"applicationHotkeys_description": "配置应用快捷键。勾选设为全局快捷键(仅桌面端)",
|
||||
"customFontPath_description": "设置应用使用的自定义字体路径",
|
||||
"gaplessAudio_optionWeak": "弱(推荐)",
|
||||
@@ -280,7 +285,7 @@
|
||||
"scrobble": "记录播放信息",
|
||||
"skipDuration_description": "设置每次按下跳过按钮将会跳过的时长",
|
||||
"fontType_optionSystem": "系统字体",
|
||||
"mpvExecutablePath_description": "设置 mpv 可执行文件的路径。如果留空,则使用默认路径",
|
||||
"mpvExecutablePath_description": "设置 MPV 可执行文件的路径。如果留空,则使用默认路径",
|
||||
"sampleRate": "采样率",
|
||||
"sidePlayQueueStyle_optionAttached": "吸附",
|
||||
"sidebarConfiguration": "侧边栏设定",
|
||||
@@ -329,7 +334,7 @@
|
||||
"hotkey_toggleShuffle": "切换随机",
|
||||
"theme": "主题",
|
||||
"playbackStyle_description": "选择音频播放器的播放风格",
|
||||
"mpvExecutablePath": "mpv 可执行文件路径",
|
||||
"mpvExecutablePath": "MPV 可执行文件路径",
|
||||
"hotkey_rate2": "评为 2 星",
|
||||
"playButtonBehavior_description": "设置将歌曲添加到播放队列时播放按钮的默认行为",
|
||||
"minimumScrobblePercentage_description": "歌曲被记录为已播放所需的最小播放百分比",
|
||||
@@ -339,7 +344,7 @@
|
||||
"savePlayQueue": "保存播放队列",
|
||||
"minimumScrobbleSeconds_description": "歌曲被记录为已播放所需的最小播放时间",
|
||||
"skipPlaylistPage_description": "打开歌单时,直接查看歌曲列表而非查看默认页面",
|
||||
"fontType_description": "内置字体可以选择 feishin 提供的字体之一。系统字体允许您选择操作系统提供的任何字体。自定义选项允许您使用自己的字体",
|
||||
"fontType_description": "内置字体可以选择 Feishin 提供的字体之一。系统字体允许您选择操作系统提供的任何字体。自定义选项允许您使用自己的字体",
|
||||
"playButtonBehavior": "播放按钮行为",
|
||||
"volumeWheelStep": "音量滚轮分度",
|
||||
"sidebarPlaylistList_description": "显示或隐藏侧边栏歌单列表",
|
||||
@@ -380,20 +385,20 @@
|
||||
"replayGainClipping_description": "自动降低增益以防止{{ReplayGain}}造成削波",
|
||||
"replayGainPreamp": "{{ReplayGain}}前置放大(分贝)",
|
||||
"replayGainClipping": "{{ReplayGain}}削波",
|
||||
"discordUpdateInterval": "{{discord}} rich presence 更新间隔",
|
||||
"discordApplicationId_description": "{{discord}} rich presence 应用 id(默认为 {{defaultId}})",
|
||||
"discordUpdateInterval": "{{discord}} Rich Presence 更新间隔",
|
||||
"discordApplicationId_description": "{{discord}} Rich Presence 应用 ID(默认为 {{defaultId}})",
|
||||
"discordUpdateInterval_description": "更新间隔秒数(至少 15 秒)",
|
||||
"discordRichPresence_description": "在 {{discord}} rich presence 中显示播放状态。图片键为:{{icon}}、{{playing}} 和 {{paused}}",
|
||||
"discordRichPresence_description": "在 {{discord}} Rich Presence 中显示播放状态。图片键为:{{icon}}、{{playing}} 和 {{paused}}",
|
||||
"accentColor": "强调色",
|
||||
"accentColor_description": "设置应用的强调色",
|
||||
"replayGainPreamp_description": "调整应用在{{ReplayGain}}值上的前置放大增益",
|
||||
"discordIdleStatus": "显示 rich presence 闲置状态",
|
||||
"discordIdleStatus": "显示 Rich Presence 闲置状态",
|
||||
"clearCache": "清除浏览器缓存",
|
||||
"buttonSize": "播放器栏按钮大小",
|
||||
"buttonSize_description": "播放器栏按钮大小",
|
||||
"clearCache_description": "feishin的“硬清除”。除了清除feishin的缓存,清空浏览器缓存(保存的图像和其他资源)。服务器凭据和设置会被保留",
|
||||
"clearQueryCache_description": "feishin的“软清除”。这将会刷新播放列表、元数据并重置保存的歌词。设置、服务器凭据和缓存图像会被保留",
|
||||
"clearQueryCache": "清除feishin缓存",
|
||||
"clearCache_description": "Feishin的“硬清除”。除了清除Feishin的缓存,清空浏览器缓存(保存的图像和其他资源)。服务器凭据和设置会被保留",
|
||||
"clearQueryCache_description": "Feishin的“软清除”。这将会刷新播放列表、元数据并重置保存的歌词。设置、服务器凭据和缓存图像会被保留",
|
||||
"clearQueryCache": "清除Feishin缓存",
|
||||
"externalLinks": "显示外部链接",
|
||||
"externalLinks_description": "允许在艺术家/专辑页面上显示外部链接(Last.fm、MusicBrainz)",
|
||||
"mpvExtraParameters_help": "每行一个",
|
||||
@@ -415,10 +420,10 @@
|
||||
"contextMenu_description": "允许您隐藏右键单击项目时显示在菜单中的项目。未选中的项目将被隐藏",
|
||||
"customCssEnable_description": "允许编写自定义 css",
|
||||
"customCss": "自定义css",
|
||||
"customCss_description": "自定义css内容。注意:内容和远程url是不允许的属性。内容预览展示如下。出于安全考虑,您未设置的其它字段也会显示",
|
||||
"customCss_description": "自定义css内容。注意:内容和远程URL是不允许的属性。内容预览展示如下。出于安全考虑,您未设置的其它字段也会显示",
|
||||
"contextMenu": "上下文菜单(右键单击)配置",
|
||||
"customCssEnable": "启用自定义 css",
|
||||
"customCssNotice": "警告:虽然预设了一些安全限制(不允许 url() 和 content:),但使用自定义 css 仍然会因更改界面而带来风险",
|
||||
"customCssNotice": "警告:虽然预设了一些安全限制(不允许 URL() 和 content:),但使用自定义 css 仍然会因更改界面而带来风险",
|
||||
"transcode_description": "可以转码为不同的格式",
|
||||
"transcodeBitrate": "转码比特率",
|
||||
"albumBackground": "专辑背景图片",
|
||||
@@ -446,14 +451,14 @@
|
||||
"lastfmApiKey": "{{lastfm}} API 密钥",
|
||||
"lastfmApiKey_description": "{{lastfm}} 的 API 密钥。封面艺术图所需",
|
||||
"discordServeImage": "从服务器提供 {{discord}} 图像",
|
||||
"discordServeImage_description": "从服务器本身分享 {{discord}} rich presence 的封面艺术,仅适用于 Jellyfin 和 Navidrome。 {{discord}} 使用机器人来获取图像,因此您的服务器必须可通过公共互联网访问",
|
||||
"discordServeImage_description": "从服务器本身分享 {{discord}} Rich Presence 的封面艺术,仅适用于 Jellyfin 和 Navidrome。 {{discord}} 使用机器人来获取图像,因此您的服务器必须可通过公共互联网访问",
|
||||
"musicbrainz": "显示 MusicBrainz 链接",
|
||||
"musicbrainz_description": "在存在 MusicBrainz ID 的艺术家/专辑页面上显示 MusicBrainz 的链接",
|
||||
"lastfm": "显示 last.fm 链接",
|
||||
"musicbrainz_description": "在艺术家/专辑页面上显示 MusicBrainz 链接(如果存在 MusicBrainz ID)",
|
||||
"lastfm": "显示 Last.fm 链接",
|
||||
"lastfm_description": "在艺术家/专辑页面上显示 Last.fm 的链接",
|
||||
"preferLocalLyrics_description": "优先选择本地歌词(如有),而不是远程歌词",
|
||||
"preferLocalLyrics": "首选本地歌词",
|
||||
"discordPausedStatus": "暂停时显示rich presence",
|
||||
"discordPausedStatus": "暂停时显示Rich Presence",
|
||||
"discordPausedStatus_description": "启用后将在播放器暂停时显示状态",
|
||||
"preservePitch": "保持音高",
|
||||
"preservePitch_description": "在调整播放速度时保持音高",
|
||||
@@ -484,7 +489,7 @@
|
||||
"exportImportSettings_control_title": "导入/导出设置",
|
||||
"exportImportSettings_destructiveWarning": "导入设置会破坏现有设置,请在点击下方“导入”按钮前仔细阅读以上内容!",
|
||||
"exportImportSettings_importBtn": "导入设置",
|
||||
"exportImportSettings_importModalTitle": "导入 feishin 设置",
|
||||
"exportImportSettings_importModalTitle": "导入 Feishin 设置",
|
||||
"exportImportSettings_importSuccess": "设置已成功导入!",
|
||||
"exportImportSettings_notValidJSON": "传递的文件不是有效的 JSON 文件",
|
||||
"exportImportSettings_offendingKeyError": "\"{{offendingKey}}\" 不正确 - {{reason}}",
|
||||
@@ -505,14 +510,14 @@
|
||||
"combinedLyricsAndVisualizer": "在播放器侧边栏合并歌词和可视化界面",
|
||||
"autoDJ_description": "自动添加相似歌曲到队列中",
|
||||
"notify_description": "歌曲变更时显示通知",
|
||||
"mpvExtraParameters_description": "向mpv传递额外参数",
|
||||
"mpvExtraParameters_description": "向MPV传递额外参数",
|
||||
"audioFadeOnStatusChange": "音频改变时淡入淡出",
|
||||
"showVisualizerInSidebar": "在播放器侧边栏显示可视化效果",
|
||||
"showLyricsInSidebar": "在播放器侧边栏显示歌词",
|
||||
"analyticsDisable": "退出使用情况的分析",
|
||||
"artistReleaseTypeConfiguration": "艺术家发行类型设置",
|
||||
"useThemeAccentColor": "使用主题强调色",
|
||||
"mpvExtraParameters": "mpv额外参数",
|
||||
"mpvExtraParameters": "MPV额外参数",
|
||||
"showRatings": "显示星级评分",
|
||||
"followCurrentSong": "跟随当前歌曲",
|
||||
"logLevel": "日志等级",
|
||||
@@ -547,7 +552,7 @@
|
||||
"autoDJ_timing": "定时",
|
||||
"autoDJ_timing_description": "自动 DJ 触发前队列中剩余的歌曲数量",
|
||||
"crossfadeStyle": "交叉渐变风格",
|
||||
"discordRichPresence": "{{discord}} rich presence",
|
||||
"discordRichPresence": "{{discord}} Rich Presence",
|
||||
"homeFeatureStyle_description": "控制首页特色轮播图的样式",
|
||||
"homeFeatureStyle": "首页特色旋转样式",
|
||||
"homeFeatureStyle_optionMultiple": "多样",
|
||||
@@ -581,7 +586,7 @@
|
||||
"automaticUpdates_description": "自动检查并安装更新",
|
||||
"releaseChannel_optionAlpha": "alpha(每日构建版)",
|
||||
"discordStateIcon": "显示播放图标",
|
||||
"discordStateIcon_description": "在 rich presence 状态中显示一个小的播放图标。启用“暂停时显示 rich presence 在线状态”后,暂停图标始终显示",
|
||||
"discordStateIcon_description": "在 Rich Presence 状态中显示一个小的播放图标。启用“暂停时显示 Rich Presence 在线状态”后,暂停图标始终显示",
|
||||
"blurExplicitImages": "模糊显式图片",
|
||||
"blurExplicitImages_description": "专辑和歌曲封面若被标记为不雅内容,将会进行模糊处理",
|
||||
"autosave": "自动保存播放队列",
|
||||
@@ -593,7 +598,21 @@
|
||||
"primaryShade": "主色调",
|
||||
"primaryShade_description": "覆盖按钮、链接和其他主色元素使用的主色调(0-9)",
|
||||
"playerItemConfiguration_description": "配置全屏播放器上显示的项目及其显示顺序",
|
||||
"playerItemConfiguration": "播放器项目配置"
|
||||
"playerItemConfiguration": "播放器项目配置",
|
||||
"listenbrainz_description": "在艺术家/专辑页面上显示 ListenBrainz 链接",
|
||||
"listenbrainz": "显示 ListenBrainz 链接",
|
||||
"qobuz_description": "在艺术家/专辑页面上显示 Qobuz 链接",
|
||||
"qobuz": "显示 Qobuz 链接",
|
||||
"spotify_description": "在艺术家/专辑页面上显示 Spotify 链接",
|
||||
"spotify": "显示 Spotify 链接",
|
||||
"nativeSpotify_description": "在 Spotify 应用中打开,而不是在浏览器中打开",
|
||||
"nativeSpotify": "使用 Spotify 应用",
|
||||
"sidePlayQueueLayout": "侧边播放队列布局",
|
||||
"sidePlayQueueLayout_description": "设置附加侧边播放队列的布局",
|
||||
"sidePlayQueueLayout_optionHorizontal": "水平",
|
||||
"sidePlayQueueLayout_optionVertical": "垂直",
|
||||
"waveformLoadingDelay": "波形加载延迟",
|
||||
"waveformLoadingDelay_description": "加载波形前的延迟时间(秒)。如果在使用网页播放器时遇到卡顿现象,请增加此值。"
|
||||
},
|
||||
"error": {
|
||||
"remotePortWarning": "重启服务器使新端口生效",
|
||||
@@ -654,7 +673,7 @@
|
||||
"fromYear": "起始年份",
|
||||
"criticRating": "评论家评分",
|
||||
"trackNumber": "曲目",
|
||||
"bpm": "bpm",
|
||||
"bpm": "BPM",
|
||||
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||
"comment": "评论",
|
||||
"isCompilation": "为合辑",
|
||||
@@ -668,7 +687,7 @@
|
||||
"genre": "$t(entity.genre, {\"count\": 1})",
|
||||
"note": "注释",
|
||||
"albumCount": "$t(entity.album, {\"count\": 2})数",
|
||||
"id": "id",
|
||||
"id": "ID",
|
||||
"disc": "碟片",
|
||||
"duration": "时长",
|
||||
"album": "$t(entity.album, {\"count\": 1})",
|
||||
@@ -906,12 +925,12 @@
|
||||
"ignoreSsl": "忽略 ssl $t(common.restartRequired)",
|
||||
"ignoreCors": "忽略 cors $t(common.restartRequired)",
|
||||
"error_savePassword": "保存密码时出现错误",
|
||||
"input_url": "url",
|
||||
"input_url": "URL",
|
||||
"input_preferInstantMixDescription": "仅使用即时混音来获取类似的歌曲。如果您有修改此行为的插件,则很有用",
|
||||
"input_preferInstantMix": "首选即时混音",
|
||||
"input_preferRemoteUrl": "首选公共 url",
|
||||
"input_remoteUrl": "公共 url",
|
||||
"input_remoteUrlPlaceholder": "可选:对外功能的公共 url"
|
||||
"input_preferRemoteUrl": "首选公共 URL",
|
||||
"input_remoteUrl": "公共 URL",
|
||||
"input_remoteUrlPlaceholder": "可选:对外功能的公共 URL"
|
||||
},
|
||||
"addToPlaylist": {
|
||||
"success": "添加$t(entity.trackWithCount, {\"count\": {{message}} })到$t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
||||
@@ -945,8 +964,7 @@
|
||||
"editPlaylist": {
|
||||
"title": "编辑$t(entity.playlist, {\"count\": 1})",
|
||||
"publicJellyfinNote": "Jellyfin 出于某种原因不会显示播放列表是否公开。如果您希望保持公开,请选择以下输入",
|
||||
"success": "$t(entity.playlist, {\"count\": 1})更新成功",
|
||||
"editNote": "不建议对大型播放列表进行手动编辑,你确定接受新播放列表覆盖已有播放列表可能导致的数据丢失风险吗?"
|
||||
"success": "$t(entity.playlist, {\"count\": 1})更新成功"
|
||||
},
|
||||
"lyricSearch": {
|
||||
"title": "搜索歌词",
|
||||
@@ -997,6 +1015,9 @@
|
||||
"input_played_optionPlayed": "仅已播放的曲目",
|
||||
"input_limit": "有多少首歌?",
|
||||
"input_played": "播放筛选器"
|
||||
},
|
||||
"editRadioStation": {
|
||||
"success": "电台更新成功"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
@@ -1045,7 +1066,7 @@
|
||||
"duration": "$t(common.duration)",
|
||||
"dateAdded": "添加日期",
|
||||
"size": "$t(common.size)",
|
||||
"bpm": "$t(common.bpm)",
|
||||
"bpm": "$t(common.BPM)",
|
||||
"lastPlayed": "最后播放",
|
||||
"trackNumber": "音轨编号",
|
||||
"rowIndex": "行索引",
|
||||
@@ -1091,7 +1112,7 @@
|
||||
"releaseDate": "发布日期",
|
||||
"bitrate": "比特率",
|
||||
"title": "标题",
|
||||
"bpm": "bpm",
|
||||
"bpm": "BPM",
|
||||
"dateAdded": "添加日期",
|
||||
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||
"songCount": "$t(entity.track, {\"count\": 2})",
|
||||
@@ -1212,21 +1233,21 @@
|
||||
"options": {
|
||||
"channelLayout": {
|
||||
"single": "单项",
|
||||
"dualCombined": "Dual-Combined",
|
||||
"dualHorizontal": "Dual-Horizontal",
|
||||
"dualVertical": "Dual-Vertical"
|
||||
"dualCombined": "双重组合",
|
||||
"dualHorizontal": "双水平",
|
||||
"dualVertical": "双垂直"
|
||||
},
|
||||
"mode": {
|
||||
"0": "[0] 离散频率",
|
||||
"1": "[1] 1/24th octave / 240 bands",
|
||||
"2": "[2] 1/12th octave / 120 bands",
|
||||
"3": "[3] 1/8th octave / 80 bands",
|
||||
"4": "[4] 1/6th octave / 60 bands",
|
||||
"5": "[5] 1/4th octave / 40 bands",
|
||||
"6": "[6] 1/3rd octave / 30 bands",
|
||||
"7": "[7] Half octave / 20 bands",
|
||||
"8": "[8] Full octave / 10 bands",
|
||||
"10": "[10] Line / Area graph"
|
||||
"1": "[1] 1/24倍频程 / 240频段",
|
||||
"2": "[2] 1/12 倍频程 / 120 频段",
|
||||
"3": "[3] 1/8倍频程 / 80频段",
|
||||
"4": "[4] 1/6倍频程 / 60频段",
|
||||
"5": "[5] 1/4倍频程 / 40频段",
|
||||
"6": "[6] 1/3倍频程 / 30频段",
|
||||
"7": "[7] 半倍频程 / 20 频段",
|
||||
"8": "[8] 全倍频程 / 10 频段",
|
||||
"10": "[10] 折线图 / 面积图"
|
||||
},
|
||||
"colorMode": {
|
||||
"gradient": "渐变",
|
||||
@@ -1242,10 +1263,10 @@
|
||||
},
|
||||
"frequencyScale": {
|
||||
"none": "无",
|
||||
"bark": "Bark Scale",
|
||||
"linear": "Linear Scale",
|
||||
"log": "Log Scale",
|
||||
"mel": "Mel Scale"
|
||||
"bark": "树皮鳞片",
|
||||
"linear": "线性刻度",
|
||||
"log": "对数刻度",
|
||||
"mel": "梅尔刻度"
|
||||
},
|
||||
"weightingFilter": {
|
||||
"none": "无",
|
||||
@@ -1304,13 +1325,13 @@
|
||||
"showScaleX": "显示比例尺 X",
|
||||
"noteLabels": "笔记标签",
|
||||
"showScaleY": "显示比例尺 Y",
|
||||
"alphaBars": "Alpha Bars",
|
||||
"ansiBands": "ANSI Bands",
|
||||
"ledBars": "LED Bars",
|
||||
"trueLeds": "True LEDs",
|
||||
"lumiBars": "Lumi Bars",
|
||||
"outlineBars": "Outline Bars",
|
||||
"roundBars": "Round Bars"
|
||||
"alphaBars": "Alpha 条",
|
||||
"ansiBands": "ANSI 频段",
|
||||
"ledBars": "LED 灯条",
|
||||
"trueLeds": "真正的LED",
|
||||
"lumiBars": "Lumi 条",
|
||||
"outlineBars": "轮廓栏",
|
||||
"roundBars": "圆条"
|
||||
},
|
||||
"queryBuilder": {
|
||||
"standardTags": "标准标签",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"backward": "返回",
|
||||
"biography": "簡介",
|
||||
"bitrate": "位元率",
|
||||
"bpm": "bpm",
|
||||
"bpm": "BPM",
|
||||
"clear": "清空",
|
||||
"collapse": "折疊",
|
||||
"comingSoon": "即將推出…",
|
||||
@@ -116,7 +116,9 @@
|
||||
"itemsMore": "{{count}} 更多",
|
||||
"filter_single": "單選",
|
||||
"filter_multiple": "複選",
|
||||
"newVersionAvailable": "有新的版本可供使用"
|
||||
"newVersionAvailable": "有新的版本可供使用",
|
||||
"numberOfResults": "{{numberOfResults}} 項結果",
|
||||
"grouping": "分組"
|
||||
},
|
||||
"error": {
|
||||
"endpointNotImplementedError": "{{serverType}} 尚未實現端點 {{endpoint}}",
|
||||
@@ -231,7 +233,9 @@
|
||||
"showLyricMatch": "顯示匹配的歌詞",
|
||||
"dynamicImageBlur": "圖片模糊大小",
|
||||
"dynamicIsImage": "啟用背景圖片",
|
||||
"lyricOffset": "歌詞偏移時間 (ms)"
|
||||
"lyricOffset": "歌詞偏移時間 (ms)",
|
||||
"lyricOpacityNonActive": "非活躍歌詞的不透明度",
|
||||
"lyricScaleNonActive": "非活躍歌詞的比例"
|
||||
},
|
||||
"lyrics": "歌詞",
|
||||
"related": "相關",
|
||||
@@ -392,8 +396,8 @@
|
||||
"next": "下一首",
|
||||
"play": "播放",
|
||||
"playbackFetchCancel": "請稍等…關閉通知以取消",
|
||||
"queue_moveToBottom": "使所選置頂",
|
||||
"queue_moveToTop": "使所選置底",
|
||||
"queue_moveToBottom": "使所選置底",
|
||||
"queue_moveToTop": "使所選置頂",
|
||||
"playSimilarSongs": "播放相似歌曲",
|
||||
"viewQueue": "檢視佇列",
|
||||
"addLastShuffled": "新增至尾端 (隨機)",
|
||||
@@ -422,7 +426,7 @@
|
||||
"hotkey_volumeDown": "音量降低",
|
||||
"hotkey_volumeMute": "靜音",
|
||||
"minimumScrobblePercentage": "最小紀錄時長(百分比)",
|
||||
"minimumScrobblePercentage_description": "歌曲被記錄為已播放(scrobble)所需的最小播放百分比",
|
||||
"minimumScrobblePercentage_description": "歌曲被記錄為已播放(Scrobble)所需的最小播放百分比",
|
||||
"theme_description": "設定應用程式的主題",
|
||||
"accentColor": "強調色",
|
||||
"accentColor_description": "設定應用程式的強調色",
|
||||
@@ -431,7 +435,7 @@
|
||||
"audioDevice": "音訊設備",
|
||||
"audioDevice_description": "選擇用於播放的音訊設備",
|
||||
"audioExclusiveMode": "音訊獨佔模式",
|
||||
"audioExclusiveMode_description": "啟用獨佔輸出模式。在此模式下,系統通常被鎖定,只有 mpv 能夠輸出音訊",
|
||||
"audioExclusiveMode_description": "啟用獨佔輸出模式。在此模式下,系統通常被鎖定,只有 MPV 能夠輸出音訊。視覺化音訊截取在此選項啟用時不會作用",
|
||||
"audioPlayer": "音訊播放器",
|
||||
"crossfadeDuration": "淡入淡出持續時間",
|
||||
"crossfadeDuration_description": "設定淡入淡出持續時間",
|
||||
@@ -439,12 +443,12 @@
|
||||
"customFontPath": "自定字體路徑",
|
||||
"customFontPath_description": "設定應用程式使用的自定字體路徑",
|
||||
"disableLibraryUpdateOnStartup": "禁用啟動時檢查新版本",
|
||||
"discordApplicationId": "{{discord}} 應用程式 id",
|
||||
"discordApplicationId_description": "{{discord}} rich presence 應用程式 id(預設為 {{defaultId}})",
|
||||
"discordIdleStatus": "顯示 rich presence 閒置狀態",
|
||||
"discordApplicationId": "{{discord}} 應用程式 ID",
|
||||
"discordApplicationId_description": "{{discord}} Rich Presence 應用程式 ID(預設為 {{defaultId}})",
|
||||
"discordIdleStatus": "顯示 Rich Presence 閒置狀態",
|
||||
"discordIdleStatus_description": "啟用後將會在播放器閒置時更新狀態",
|
||||
"discordRichPresence_description": "在 {{discord}} rich presence 中顯示播放狀態。圖片鍵為:{{icon}}、{{playing}} 和 {{paused}}",
|
||||
"discordUpdateInterval": "{{discord}} rich presence 更新間隔",
|
||||
"discordRichPresence_description": "在 {{discord}} Rich Presence 中顯示播放狀態。圖片鍵為:{{icon}}、{{playing}} 和 {{paused}}",
|
||||
"discordUpdateInterval": "{{discord}} Rich Presence 更新間隔",
|
||||
"discordUpdateInterval_description": "更新間隔秒數(至少 15 秒)",
|
||||
"enableRemote": "啟用遠端控制伺服器",
|
||||
"enableRemote_description": "啟用遠端控制伺服器,以允許其他設備控制此應用程式",
|
||||
@@ -452,12 +456,12 @@
|
||||
"followLyric": "跟隨目前歌詞",
|
||||
"font_description": "設定應用程式使用的字體",
|
||||
"fontType": "字體類型",
|
||||
"fontType_description": "內建字體可以選擇 feishin 提供的字體之一。系統字體允許您選擇作業系統提供的任何字體。自定選項允許您使用自己的字體",
|
||||
"fontType_description": "內建字體可以選擇 Feishin 提供的字體之一。系統字體允許您選擇作業系統提供的任何字體。自定選項允許您使用自己的字體",
|
||||
"fontType_optionBuiltIn": "內建字體",
|
||||
"fontType_optionCustom": "自定字體",
|
||||
"fontType_optionSystem": "系統字體",
|
||||
"gaplessAudio": "無間隔音訊",
|
||||
"gaplessAudio_description": "調整 mpv 無間隔音訊設定",
|
||||
"gaplessAudio_description": "調整 MPV 無間隔音訊設定",
|
||||
"gaplessAudio_optionWeak": "弱(建議)",
|
||||
"globalMediaHotkeys": "全域媒體快捷鍵",
|
||||
"hotkey_browserForward": "瀏覽器往前",
|
||||
@@ -496,8 +500,8 @@
|
||||
"minimizeToTray": "最小化到系統匣",
|
||||
"minimizeToTray_description": "將應用程式最小化到系統匣",
|
||||
"minimumScrobbleSeconds": "最小紀錄時間(秒)",
|
||||
"minimumScrobbleSeconds_description": "歌曲被記錄為已播放(scrobble)所需的最小播放時間",
|
||||
"mpvExecutablePath": "mpv 執行檔路徑",
|
||||
"minimumScrobbleSeconds_description": "歌曲被記錄為已播放(Scrobble)所需的最小播放時間",
|
||||
"mpvExecutablePath": "MPV 執行檔路徑",
|
||||
"playbackStyle_optionCrossFade": "淡入淡出",
|
||||
"playbackStyle_optionNormal": "一般",
|
||||
"playButtonBehavior": "播放按鈕動作",
|
||||
@@ -559,7 +563,7 @@
|
||||
"hotkey_favoriteCurrentSong": "收藏 $t(common.currentSong)",
|
||||
"hotkey_playbackStop": "停止",
|
||||
"hotkey_rate0": "清除評分",
|
||||
"mpvExecutablePath_description": "設定 mpv 執行檔的路徑。如果留空,則使用預設路徑",
|
||||
"mpvExecutablePath_description": "設定 MPV 執行檔的路徑。如果留空,則使用預設路徑",
|
||||
"playbackStyle_description": "選擇播放器的播放風格",
|
||||
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||
"remotePassword": "遠端控制伺服器密碼",
|
||||
@@ -586,10 +590,10 @@
|
||||
"contextMenu_description": "允許您隱藏在右鍵選單項目時顯示的項目。未選取的項目將被隱藏",
|
||||
"customCssEnable": "啟用自訂CSS",
|
||||
"customCssEnable_description": "允許撰寫自訂CSS",
|
||||
"customCssNotice": "警告:即使已限制某些用法(不允許 url() 和 content:),但使用自訂 CSS 仍然會透過更改介面帶來風險",
|
||||
"customCssNotice": "警告:即使已限制某些用法(不允許 URL() 和 content:),但使用自訂 CSS 仍然會透過更改介面帶來風險",
|
||||
"customCss": "自訂CSS",
|
||||
"customCss_description": "自訂 CSS 內容。注意:內容和遠端 URL 是不允許使用的屬性。您的內容預覽如下所示。由於需要進行清理,因此存在一些您未設定的其他欄位",
|
||||
"discordPausedStatus": "暫停時顯示 rich presence",
|
||||
"discordPausedStatus": "暫停時顯示 Rich Presence",
|
||||
"discordPausedStatus_description": "啟用後,播放器暫停時將顯示狀態",
|
||||
"discordListening": "將狀態設為\"正在聽\"",
|
||||
"discordListening_description": "將狀態顯示為\"正在聽\"而不是\"正在玩\"",
|
||||
@@ -605,7 +609,7 @@
|
||||
"homeFeature_description": "控制是否在首頁上顯示大型特色輪播",
|
||||
"imageAspectRatio": "使用原生封面照長寬比",
|
||||
"imageAspectRatio_description": "如果啟用,封面照將使用其原始長寬比顯示。對於非 1:1 的封面,剩餘空間將為空",
|
||||
"lastfm": "顯示 last.fm 連結",
|
||||
"lastfm": "顯示 Last.fm 連結",
|
||||
"lastfm_description": "在藝人/專輯頁面顯示 Last.fm 連結",
|
||||
"lastfmApiKey": "{{lastfm}} API金鑰",
|
||||
"lastfmApiKey_description": "{{lastfm}}的API金鑰。用於封面照",
|
||||
@@ -766,7 +770,7 @@
|
||||
"automaticUpdates": "自動更新",
|
||||
"automaticUpdates_description": "自動檢查並安裝更新",
|
||||
"discordStateIcon": "顯示播放中圖示",
|
||||
"discordStateIcon_description": "在 rich presence 狀態中顯示一個小的播放圖示。啟用「暫停時顯示 rich presence」時,會始終顯示暫停的圖示",
|
||||
"discordStateIcon_description": "在 Rich Presence 狀態中顯示一個小的播放圖示。啟用「暫停時顯示 Rich Presence」時,會始終顯示暫停的圖示",
|
||||
"useThemePrimaryShade": "套用主題主色調",
|
||||
"useThemePrimaryShade_description": "使用所選主題中定義的主色調作為主色變體",
|
||||
"primaryShade": "主要色調",
|
||||
@@ -776,7 +780,23 @@
|
||||
"autosave": "自動儲存播放佇列",
|
||||
"autosave_description": "啟用自動將播放佇列儲存到您的伺服器。這只有在使用Navidrome/Subsonic時才可使用,並且您不能有混合播放佇列。",
|
||||
"autosaveCount": "自動播放佇列儲存頻率",
|
||||
"autosaveCount_description": "在儲存佇列之前,有多少曲目更改。1(最小)表示每次歌曲更改"
|
||||
"autosaveCount_description": "在儲存佇列之前,有多少曲目更改。1(最小)表示每次歌曲更改",
|
||||
"spotify_description": "在藝人與專輯頁面顯示 Spotify 的連結",
|
||||
"spotify": "顯示 Spotify 的連結",
|
||||
"nativeSpotify_description": "在 Spotify 應用程式中開啟,而非在瀏覽器中開啟",
|
||||
"nativeSpotify": "使用 Spotify 應用程式",
|
||||
"sidePlayQueueLayout": "側邊播放佇列佈局",
|
||||
"sidePlayQueueLayout_description": "設定吸附側邊播放佇列的佈局",
|
||||
"sidePlayQueueLayout_optionHorizontal": "水平",
|
||||
"sidePlayQueueLayout_optionVertical": "垂直",
|
||||
"listenbrainz_description": "在藝術家/專輯頁面上顯示 ListenBrainz 的連結",
|
||||
"listenbrainz": "顯示 ListenBrainz 連結",
|
||||
"qobuz_description": "在藝術家/專輯頁面上顯示 Qobuz 的連結",
|
||||
"qobuz": "顯示 Qobuz 連結",
|
||||
"waveformLoadingDelay": "波形載入延遲",
|
||||
"waveformLoadingDelay_description": "載入波形前的延遲(以秒為單位)。如果您在使用網頁播放器時遇到卡頓,請增加此值。",
|
||||
"playerbarWaveformStretch": "波形拉伸",
|
||||
"playerbarWaveformStretch_description": "拉伸波形來填補可用空間"
|
||||
},
|
||||
"table": {
|
||||
"config": {
|
||||
@@ -817,7 +837,7 @@
|
||||
"album": "$t(entity.album, {\"count\": 1})",
|
||||
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
|
||||
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||
"bpm": "$t(common.bpm)",
|
||||
"bpm": "$t(common.BPM)",
|
||||
"biography": "$t(common.biography)",
|
||||
"bitrate": "$t(common.bitrate)",
|
||||
"channels": "$t(common.channel, {\"count\": 2})",
|
||||
@@ -876,7 +896,7 @@
|
||||
"releaseDate": "發布日期",
|
||||
"releaseYear": "年份",
|
||||
"genre": "$t(entity.genre, {\"count\": 1})",
|
||||
"bpm": "bpm",
|
||||
"bpm": "BPM",
|
||||
"songCount": "$t(entity.track, {\"count\": 2})",
|
||||
"title": "標題",
|
||||
"trackNumber": "曲目編號",
|
||||
@@ -908,7 +928,10 @@
|
||||
"moveToNext": "移至下一項",
|
||||
"openIn": {
|
||||
"lastfm": "在Last.fm開啟",
|
||||
"musicbrainz": "在MusicBrainz開啟"
|
||||
"musicbrainz": "在MusicBrainz開啟",
|
||||
"spotify": "在 Spotify 中開啟",
|
||||
"listenbrainz": "在 ListenBrainz 中開啟",
|
||||
"qobuz": "在 Qobuz 中開啟"
|
||||
},
|
||||
"downloadStarted": "已開始下載 {{count}} 項內容",
|
||||
"moveItems": "移動項目",
|
||||
@@ -955,7 +978,7 @@
|
||||
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||
"biography": "個人簡介",
|
||||
"bitrate": "位元率",
|
||||
"bpm": "bpm",
|
||||
"bpm": "BPM",
|
||||
"channels": "$t(common.channel, {\"count\": 2})",
|
||||
"comment": "評論",
|
||||
"communityRating": "社群評分",
|
||||
@@ -963,7 +986,7 @@
|
||||
"dateAdded": "已新增日期",
|
||||
"disc": "光碟",
|
||||
"duration": "時長",
|
||||
"id": "id",
|
||||
"id": "ID",
|
||||
"fromYear": "從年份",
|
||||
"genre": "$t(entity.genre, {\"count\": 1})",
|
||||
"isCompilation": "為合輯",
|
||||
@@ -1004,7 +1027,7 @@
|
||||
"input_name": "伺服器名稱",
|
||||
"input_password": "密碼",
|
||||
"input_savePassword": "儲存密碼",
|
||||
"input_url": "url",
|
||||
"input_url": "URL",
|
||||
"input_username": "使用者名稱",
|
||||
"success": "伺服器新增成功",
|
||||
"title": "新增伺服器",
|
||||
@@ -1059,8 +1082,7 @@
|
||||
"editPlaylist": {
|
||||
"title": "編輯$t(entity.playlist, {\"count\": 1})",
|
||||
"publicJellyfinNote": "Jellyfin 出於某種原因,不會顯示播放清單是否公開。如果您希望保持公開狀態,請選擇以下輸入",
|
||||
"success": "$t(entity.playlist, {\"count\": 1}) 更新成功",
|
||||
"editNote": "不建議手動編輯大型播放清單,你確定要承擔覆寫現有播放清單可能造成的資料遺失風險嗎?"
|
||||
"success": "$t(entity.playlist, {\"count\": 1}) 更新成功"
|
||||
},
|
||||
"shareItem": {
|
||||
"allowDownloading": "允許下載",
|
||||
@@ -1106,6 +1128,9 @@
|
||||
"export": "匯出歌詞",
|
||||
"input_synced": "匯出同步歌詞",
|
||||
"input_offset": "$t(setting.lyricOffset)"
|
||||
},
|
||||
"editRadioStation": {
|
||||
"success": "電臺更新成功"
|
||||
}
|
||||
},
|
||||
"releaseType": {
|
||||
@@ -1314,6 +1339,13 @@
|
||||
"d": "D",
|
||||
"z": "Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"systemAudioCaptureFailed": "無法開始擷取:{{message}}",
|
||||
"systemAudioNoAudioTrack": "沒有回傳任何音軌。確保在提示時啟用音訊擷取。",
|
||||
"systemAudioConsentAllow": "允許",
|
||||
"systemAudioConsentBody": "此視覺化器需要存取系統音訊才能運作",
|
||||
"systemAudioConsentDecline": "拒絕",
|
||||
"systemAudioConsentTitle": "允許存取系統音訊?",
|
||||
"systemAudioExclusiveModeNotSupported": "啟用音訊獨佔模式時,視覺化不可用。 在MPV設定中停用音訊獨佔模式,然後再試一次。"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@ import { getLyricsBySongId as getGenius, getSearchResults as searchGenius } from
|
||||
import { getLyricsBySongId as getLrcLib, getSearchResults as searchLrcLib } from './lrclib';
|
||||
import { getLyricsBySongId as getNetease, getSearchResults as searchNetease } from './netease';
|
||||
import { orderSearchResults } from './shared';
|
||||
import {
|
||||
getLyricsBySongId as getSimpMusic,
|
||||
getSearchResults as searchSimpMusic,
|
||||
} from './simpmusic';
|
||||
|
||||
import { Song } from '/@/shared/types/domain-types';
|
||||
|
||||
@@ -12,6 +16,7 @@ export enum LyricSource {
|
||||
GENIUS = 'Genius',
|
||||
LRCLIB = 'lrclib.net',
|
||||
NETEASE = 'NetEase',
|
||||
SIMPMUSIC = 'SimpMusic',
|
||||
}
|
||||
|
||||
export type FullLyricsMetadata = Omit<InternetProviderLyricResponse, 'id' | 'lyrics' | 'source'> & {
|
||||
@@ -66,12 +71,14 @@ const SEARCH_FETCHERS: Record<LyricSource, SearchFetcher> = {
|
||||
[LyricSource.GENIUS]: searchGenius,
|
||||
[LyricSource.LRCLIB]: searchLrcLib,
|
||||
[LyricSource.NETEASE]: searchNetease,
|
||||
[LyricSource.SIMPMUSIC]: searchSimpMusic,
|
||||
};
|
||||
|
||||
const GET_FETCHERS: Record<LyricSource, GetFetcher> = {
|
||||
[LyricSource.GENIUS]: getGenius,
|
||||
[LyricSource.LRCLIB]: getLrcLib,
|
||||
[LyricSource.NETEASE]: getNetease,
|
||||
[LyricSource.SIMPMUSIC]: getSimpMusic,
|
||||
};
|
||||
|
||||
const MAX_CACHED_ITEMS = 10;
|
||||
@@ -191,6 +198,7 @@ const searchRemoteLyrics = async (params: LyricSearchQuery) => {
|
||||
[LyricSource.GENIUS]: [],
|
||||
[LyricSource.LRCLIB]: [],
|
||||
[LyricSource.NETEASE]: [],
|
||||
[LyricSource.SIMPMUSIC]: [],
|
||||
};
|
||||
for (const item of allSearchResults) {
|
||||
results[item.source].push(item);
|
||||
|
||||
@@ -58,14 +58,16 @@ export async function getSearchResults(
|
||||
): Promise<InternetProviderLyricSearchResponse[] | null> {
|
||||
let result: AxiosResponse<LrcLibSearchResponse[]>;
|
||||
|
||||
if (!params.name) {
|
||||
if (!params.name && !params.artist) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const searchQuery = [params.name, params.artist].join(' ');
|
||||
|
||||
try {
|
||||
result = await axios.get<LrcLibSearchResponse[]>(SEEARCH_URL, {
|
||||
params: {
|
||||
q: params.name,
|
||||
q: searchQuery,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
|
||||
import {
|
||||
InternetProviderLyricResponse,
|
||||
InternetProviderLyricSearchResponse,
|
||||
LyricSearchQuery,
|
||||
LyricSource,
|
||||
} from '.';
|
||||
import { orderSearchResults } from './shared';
|
||||
|
||||
const API_URL = 'https://api-lyrics.simpmusic.org/v1';
|
||||
|
||||
const TIMEOUT_MS = 5000;
|
||||
|
||||
export interface SimpMusicLyric {
|
||||
albumName?: string;
|
||||
artistName: string;
|
||||
durationSeconds?: number;
|
||||
id: string;
|
||||
plainLyric?: string;
|
||||
richSyncLyrics?: string;
|
||||
songTitle: string;
|
||||
syncedLyrics?: string;
|
||||
videoId: string;
|
||||
vote?: number;
|
||||
}
|
||||
|
||||
export interface SimpMusicSearchResponse {
|
||||
data: SimpMusicLyric[];
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export async function getLyricsBySongId(songId: string): Promise<null | string> {
|
||||
let result: AxiosResponse;
|
||||
|
||||
try {
|
||||
result = await axios.get(`${API_URL}/${songId}`, {
|
||||
timeout: TIMEOUT_MS,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('SimpMusic lyrics request errored:', (e as Error)?.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
const firstLyric = (result.data.data?.[0] ?? null) as null | SimpMusicLyric;
|
||||
if (!firstLyric) return null;
|
||||
|
||||
return firstLyric.syncedLyrics || firstLyric.plainLyric || null;
|
||||
}
|
||||
|
||||
export async function getSearchResults(
|
||||
params: LyricSearchQuery,
|
||||
): Promise<InternetProviderLyricSearchResponse[] | null> {
|
||||
let result: AxiosResponse<SimpMusicSearchResponse>;
|
||||
|
||||
if (!params.name) return null;
|
||||
|
||||
try {
|
||||
result = await axios.get<SimpMusicSearchResponse>(`${API_URL}/search`, {
|
||||
params: {
|
||||
q: params.name,
|
||||
},
|
||||
timeout: TIMEOUT_MS,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('SimpMusic search errored:', (e as Error)?.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!result.data?.data) return null;
|
||||
|
||||
const songResults: InternetProviderLyricSearchResponse[] = result.data.data.map((song) => ({
|
||||
artist: song.artistName,
|
||||
id: song.videoId,
|
||||
isSync: song.syncedLyrics ? true : false,
|
||||
name: song.songTitle,
|
||||
source: LyricSource.SIMPMUSIC,
|
||||
}));
|
||||
|
||||
return orderSearchResults({ params, results: songResults });
|
||||
}
|
||||
|
||||
export async function query(
|
||||
params: LyricSearchQuery,
|
||||
): Promise<InternetProviderLyricResponse | null> {
|
||||
if (!params.name) return null;
|
||||
|
||||
let search: AxiosResponse<SimpMusicSearchResponse>;
|
||||
|
||||
try {
|
||||
search = await axios.get<SimpMusicSearchResponse>(`${API_URL}/search`, {
|
||||
params: {
|
||||
q: params.name,
|
||||
},
|
||||
timeout: TIMEOUT_MS,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('SimpMusic search errored:', (e as Error).message);
|
||||
return null;
|
||||
}
|
||||
|
||||
const first = search.data?.data?.[0];
|
||||
if (!first) return null;
|
||||
|
||||
let lyric: AxiosResponse<SimpMusicLyric>;
|
||||
|
||||
try {
|
||||
lyric = await axios.get<SimpMusicLyric>(`${API_URL}/${first.videoId}`, {
|
||||
timeout: TIMEOUT_MS,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('SimpMusic lyrics fetch errored:', (e as Error).message);
|
||||
return null;
|
||||
}
|
||||
|
||||
const lyrics = lyric.data.syncedLyrics || lyric.data.plainLyric || null;
|
||||
if (!lyrics) return null;
|
||||
|
||||
return {
|
||||
artist: lyric.data.artistName,
|
||||
id: lyric.data.videoId,
|
||||
lyrics,
|
||||
name: lyric.data.songTitle,
|
||||
source: LyricSource.SIMPMUSIC,
|
||||
};
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import console from 'console';
|
||||
import { app, ipcMain } from 'electron';
|
||||
import { rm } from 'fs/promises';
|
||||
import { access, rm } from 'fs/promises';
|
||||
import uniq from 'lodash/uniq';
|
||||
import MpvAPI from 'node-mpv';
|
||||
import { pid } from 'node:process';
|
||||
import process from 'process';
|
||||
|
||||
import { getMainWindow, sendToastToRenderer } from '../../../index';
|
||||
import { createLog, isWindows } from '../../../utils';
|
||||
import { createLog, isMacOS, isWindows } from '../../../utils';
|
||||
import { store } from '../settings';
|
||||
|
||||
import { PlayerData } from '/@/shared/types/domain-types';
|
||||
@@ -69,6 +69,7 @@ const mpvLog = (
|
||||
};
|
||||
|
||||
const MPV_BINARY_PATH = store.get('mpv_path') as string | undefined;
|
||||
const MACOS_MPV_BINARY_PATHS = ['/opt/homebrew/bin/mpv', '/usr/local/bin/mpv'];
|
||||
|
||||
const prefetchPlaylistParams = [
|
||||
'--prefetch-playlist=no',
|
||||
@@ -86,12 +87,38 @@ const DEFAULT_MPV_PARAMETERS = (extraParameters?: string[]) => {
|
||||
return parameters;
|
||||
};
|
||||
|
||||
const resolveMpvBinaryPath = async (binaryPath?: string) => {
|
||||
if (binaryPath) {
|
||||
return binaryPath;
|
||||
}
|
||||
|
||||
if (MPV_BINARY_PATH) {
|
||||
return MPV_BINARY_PATH;
|
||||
}
|
||||
|
||||
if (!isMacOS()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const candidate of MACOS_MPV_BINARY_PATHS) {
|
||||
try {
|
||||
await access(candidate);
|
||||
return candidate;
|
||||
} catch {
|
||||
// Try the next common Homebrew location.
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const createMpv = async (data: {
|
||||
binaryPath?: string;
|
||||
extraParameters?: string[];
|
||||
properties?: Record<string, any>;
|
||||
}): Promise<MpvAPI> => {
|
||||
const { binaryPath, extraParameters, properties } = data;
|
||||
const resolvedBinaryPath = await resolveMpvBinaryPath(binaryPath);
|
||||
|
||||
const params = uniq([...DEFAULT_MPV_PARAMETERS(extraParameters), ...(extraParameters || [])]);
|
||||
|
||||
@@ -99,7 +126,7 @@ const createMpv = async (data: {
|
||||
{
|
||||
audio_only: true,
|
||||
auto_restart: false,
|
||||
binary: binaryPath || MPV_BINARY_PATH || undefined,
|
||||
binary: resolvedBinaryPath,
|
||||
socket: socketPath,
|
||||
time_update: 1,
|
||||
},
|
||||
@@ -437,10 +464,18 @@ ipcMain.on('player-mute', async (_event, mute: boolean) => {
|
||||
|
||||
ipcMain.handle('player-get-time', async (): Promise<number | undefined> => {
|
||||
try {
|
||||
return getMpvInstance()?.getTimePosition();
|
||||
const mpv = getMpvInstance();
|
||||
if (!mpv) {
|
||||
return undefined;
|
||||
}
|
||||
return await mpv.getTimePosition();
|
||||
} catch (err: any | NodeMpvError) {
|
||||
// Err 3: IPC command invalid — e.g. time-pos unavailable when idle / between tracks
|
||||
if (err?.errcode === 3) {
|
||||
return undefined;
|
||||
}
|
||||
mpvLog({ action: `Failed to get current time` }, err);
|
||||
return 0;
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ export const store = new Store<any>({
|
||||
playbackType: 'web',
|
||||
should_prompt_accessibility: true,
|
||||
shown_accessibility_warning: false,
|
||||
visualizer_system_audio_consent_granted: false,
|
||||
window_enable_tray: true,
|
||||
window_exit_to_tray: false,
|
||||
window_minimize_to_tray: false,
|
||||
|
||||
@@ -1,2 +1,9 @@
|
||||
import './core';
|
||||
import(`./${process.platform}`);
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
import('./linux');
|
||||
} else if (process.platform === 'darwin') {
|
||||
import('./darwin');
|
||||
} else if (process.platform === 'win32') {
|
||||
import('./win32');
|
||||
}
|
||||
|
||||
@@ -150,6 +150,23 @@ ipcMain.on(
|
||||
return;
|
||||
}
|
||||
|
||||
// If the served id is an empty string, this is a radio
|
||||
// Use a limited subset of the fields
|
||||
if (song._serverId === '') {
|
||||
// The id as passed in from use-mpris is radio- plus the radio ID
|
||||
// If there are spaces or some other characters, this causes MPRIS to error and
|
||||
// disconnect the bus. To prevent this, just use a fake track/radio
|
||||
mprisPlayer.metadata = {
|
||||
'mpris:trackid': mprisPlayer.objectPath(`track/radio`),
|
||||
'xesam:album': song.album || null,
|
||||
'xesam:artist': song.artists?.length
|
||||
? song.artists.map((artist) => artist.name)
|
||||
: null,
|
||||
'xesam:title': song.name || null,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
mprisPlayer.metadata = {
|
||||
'mpris:artUrl': imageUrl || null,
|
||||
'mpris:length': song.duration ? Math.round((song.duration || 0) * 1e3) : null,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export {};
|
||||
|
||||
+144
-21
@@ -5,6 +5,7 @@ import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
BrowserWindowConstructorOptions,
|
||||
desktopCapturer,
|
||||
globalShortcut,
|
||||
ipcMain,
|
||||
Menu,
|
||||
@@ -29,7 +30,7 @@ import packageJson from '../../package.json';
|
||||
import { disableMediaKeys, enableMediaKeys } from './features/core/player/media-keys';
|
||||
import { shutdownServer } from './features/core/remote';
|
||||
import { store } from './features/core/settings';
|
||||
import MenuBuilder from './menu';
|
||||
import MenuBuilder, { MenuPlaybackState } from './menu';
|
||||
import {
|
||||
autoUpdaterLogInterface,
|
||||
createLog,
|
||||
@@ -41,7 +42,7 @@ import {
|
||||
} from './utils';
|
||||
import './features';
|
||||
|
||||
import { PlayerType, TitleTheme } from '/@/shared/types/types';
|
||||
import { PlayerRepeat, PlayerStatus, PlayerType, TitleTheme } from '/@/shared/types/types';
|
||||
|
||||
const ALPHA_UPDATER_CONFIG: {
|
||||
bucket: string;
|
||||
@@ -277,6 +278,13 @@ let tray: null | Tray = null;
|
||||
let exitFromTray = false;
|
||||
let forceQuit = false;
|
||||
let powerSaveBlockerId: null | number = null;
|
||||
let menuBuilder: MenuBuilder | null = null;
|
||||
let currentPlaybackStatus: PlayerStatus = PlayerStatus.PAUSED;
|
||||
let currentPrivateMode = false;
|
||||
let currentRepeatMode: PlayerRepeat = PlayerRepeat.NONE;
|
||||
let currentSidebarCollapsed = false;
|
||||
let currentShuffleEnabled = false;
|
||||
let playbackMenuAccelerators: MenuPlaybackState['accelerators'] = {};
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
import('source-map-support').then((sourceMapSupport) => {
|
||||
@@ -333,6 +341,23 @@ export const getMainWindow = () => {
|
||||
return mainWindow;
|
||||
};
|
||||
|
||||
const rebuildMainMenu = () => {
|
||||
if (!menuBuilder || !mainWindow) return;
|
||||
|
||||
menuBuilder.buildMenu({
|
||||
accelerators: playbackMenuAccelerators,
|
||||
playbackStatus: currentPlaybackStatus,
|
||||
privateMode: currentPrivateMode,
|
||||
repeatMode: currentRepeatMode,
|
||||
shuffleEnabled: currentShuffleEnabled,
|
||||
sidebarCollapsed: currentSidebarCollapsed,
|
||||
});
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
Menu.setApplicationMenu(null);
|
||||
}
|
||||
};
|
||||
|
||||
export const sendToastToRenderer = ({
|
||||
message,
|
||||
type,
|
||||
@@ -431,19 +456,21 @@ const createTray = () => {
|
||||
},
|
||||
]);
|
||||
|
||||
tray.on('click', () => {
|
||||
if (store.get('window_minimize_to_tray')) {
|
||||
if (mainWindow?.isVisible()) {
|
||||
mainWindow?.hide();
|
||||
if (!isMacOS()) {
|
||||
tray.on('click', () => {
|
||||
if (store.get('window_minimize_to_tray')) {
|
||||
if (mainWindow?.isVisible()) {
|
||||
mainWindow?.hide();
|
||||
} else {
|
||||
mainWindow?.show();
|
||||
createWinThumbarButtons();
|
||||
}
|
||||
} else {
|
||||
mainWindow?.show();
|
||||
createWinThumbarButtons();
|
||||
}
|
||||
} else {
|
||||
mainWindow?.show();
|
||||
createWinThumbarButtons();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
tray.setToolTip('Feishin');
|
||||
tray.setContextMenu(contextMenu);
|
||||
@@ -697,12 +724,8 @@ async function createWindow(first = true): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
const menuBuilder = new MenuBuilder(mainWindow);
|
||||
menuBuilder.buildMenu();
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
Menu.setApplicationMenu(null);
|
||||
}
|
||||
menuBuilder = new MenuBuilder(mainWindow);
|
||||
rebuildMainMenu();
|
||||
|
||||
// Open URLs in the user's browser
|
||||
mainWindow.webContents.setWindowOpenHandler((edata) => {
|
||||
@@ -710,6 +733,29 @@ async function createWindow(first = true): Promise<void> {
|
||||
return { action: 'deny' };
|
||||
});
|
||||
|
||||
mainWindow.webContents.session.setDisplayMediaRequestHandler((_request, callback) => {
|
||||
if (!isMacOS()) {
|
||||
callback({ audio: 'loopback' });
|
||||
return;
|
||||
}
|
||||
|
||||
desktopCapturer
|
||||
.getSources({ thumbnailSize: { height: 0, width: 0 }, types: ['screen'] })
|
||||
.then((sources) => {
|
||||
const source = sources[0];
|
||||
if (!source) {
|
||||
callback({});
|
||||
return;
|
||||
}
|
||||
|
||||
callback({ audio: 'loopback', video: source });
|
||||
})
|
||||
.catch((err) => {
|
||||
log.warn('desktopCapturer.getSources failed', err);
|
||||
callback({});
|
||||
});
|
||||
});
|
||||
|
||||
if (!disableAutoUpdates() && store.get('disable_auto_updates') !== true) {
|
||||
new AppUpdater();
|
||||
}
|
||||
@@ -740,11 +786,17 @@ const playbackType = store.get('playbackType', PlayerType.WEB) as PlayerType;
|
||||
const shouldDisableMediaFeatures =
|
||||
isLinux() || !enableMediaSession || playbackType !== PlayerType.WEB;
|
||||
|
||||
const chromiumDisabledFeatures: string[] = [];
|
||||
// Fractional scaling on Wayland: https://github.com/jeffvli/feishin/issues/1271#issuecomment-4063326712
|
||||
if (isLinux()) {
|
||||
chromiumDisabledFeatures.push('WaylandFractionalScaleV1');
|
||||
}
|
||||
if (shouldDisableMediaFeatures) {
|
||||
app.commandLine.appendSwitch(
|
||||
'disable-features',
|
||||
'HardwareMediaKeyHandling,MediaSessionService',
|
||||
);
|
||||
chromiumDisabledFeatures.push('HardwareMediaKeyHandling', 'MediaSessionService');
|
||||
}
|
||||
|
||||
if (chromiumDisabledFeatures.length > 0) {
|
||||
app.commandLine.appendSwitch('disable-features', chromiumDisabledFeatures.join(','));
|
||||
}
|
||||
|
||||
// https://github.com/electron/electron/issues/46538#issuecomment-2808806722
|
||||
@@ -774,6 +826,17 @@ enum BindingActions {
|
||||
VOLUME_UP = 'volumeUp',
|
||||
}
|
||||
|
||||
const getMenuAccelerator = (
|
||||
data: Record<BindingActions, { allowGlobal: boolean; hotkey: string; isGlobal: boolean }>,
|
||||
action: BindingActions,
|
||||
) => {
|
||||
const hotkey = data[action]?.hotkey;
|
||||
|
||||
if (!hotkey) return undefined;
|
||||
|
||||
return hotkeyToElectronAccelerator(hotkey);
|
||||
};
|
||||
|
||||
const HOTKEY_ACTIONS: Record<BindingActions, () => void> = {
|
||||
[BindingActions.GLOBAL_SEARCH]: () => {},
|
||||
[BindingActions.LOCAL_SEARCH]: () => {},
|
||||
@@ -827,6 +890,26 @@ ipcMain.on(
|
||||
}
|
||||
}
|
||||
|
||||
playbackMenuAccelerators = {
|
||||
next: getMenuAccelerator(data, BindingActions.NEXT),
|
||||
playPause:
|
||||
getMenuAccelerator(data, BindingActions.PLAY_PAUSE) ||
|
||||
getMenuAccelerator(data, BindingActions.PLAY) ||
|
||||
getMenuAccelerator(data, BindingActions.PAUSE),
|
||||
previous: getMenuAccelerator(data, BindingActions.PREVIOUS),
|
||||
repeat: getMenuAccelerator(data, BindingActions.TOGGLE_REPEAT),
|
||||
seekBackward: getMenuAccelerator(data, BindingActions.SKIP_BACKWARD),
|
||||
seekForward: getMenuAccelerator(data, BindingActions.SKIP_FORWARD),
|
||||
shuffle: getMenuAccelerator(data, BindingActions.SHUFFLE),
|
||||
stop: getMenuAccelerator(data, BindingActions.STOP),
|
||||
volumeDown: getMenuAccelerator(data, BindingActions.VOLUME_DOWN),
|
||||
volumeUp: getMenuAccelerator(data, BindingActions.VOLUME_UP),
|
||||
};
|
||||
|
||||
if (isMacOS()) {
|
||||
rebuildMainMenu();
|
||||
}
|
||||
|
||||
const globalMediaKeysEnabled = store.get('global_media_hotkeys', true) as boolean;
|
||||
|
||||
if (globalMediaKeysEnabled) {
|
||||
@@ -967,3 +1050,43 @@ if (!ipcMain.eventNames().includes('open-application-directory')) {
|
||||
shell.openPath(userDataPath);
|
||||
});
|
||||
}
|
||||
|
||||
ipcMain.on('update-playback', (_event, status: PlayerStatus) => {
|
||||
currentPlaybackStatus = status;
|
||||
|
||||
if (!isMacOS()) return;
|
||||
|
||||
rebuildMainMenu();
|
||||
});
|
||||
|
||||
ipcMain.on('update-repeat', (_event, repeat: PlayerRepeat) => {
|
||||
currentRepeatMode = repeat;
|
||||
|
||||
if (!isMacOS()) return;
|
||||
|
||||
rebuildMainMenu();
|
||||
});
|
||||
|
||||
ipcMain.on('update-shuffle', (_event, shuffle: boolean) => {
|
||||
currentShuffleEnabled = shuffle;
|
||||
|
||||
if (!isMacOS()) return;
|
||||
|
||||
rebuildMainMenu();
|
||||
});
|
||||
|
||||
ipcMain.on('update-private-mode', (_event, privateMode: boolean) => {
|
||||
currentPrivateMode = privateMode;
|
||||
|
||||
if (!isMacOS()) return;
|
||||
|
||||
rebuildMainMenu();
|
||||
});
|
||||
|
||||
ipcMain.on('update-sidebar-collapsed', (_event, collapsedSidebar: boolean) => {
|
||||
currentSidebarCollapsed = collapsedSidebar;
|
||||
|
||||
if (!isMacOS()) return;
|
||||
|
||||
rebuildMainMenu();
|
||||
});
|
||||
|
||||
+190
-4
@@ -1,18 +1,53 @@
|
||||
import { app, BrowserWindow, Menu, MenuItemConstructorOptions, shell } from 'electron';
|
||||
|
||||
import packageJson from '../../package.json';
|
||||
|
||||
import { PlayerRepeat, PlayerStatus } from '/@/shared/types/types';
|
||||
|
||||
export type MenuPlaybackState = {
|
||||
accelerators?: {
|
||||
next?: string;
|
||||
playPause?: string;
|
||||
previous?: string;
|
||||
repeat?: string;
|
||||
seekBackward?: string;
|
||||
seekForward?: string;
|
||||
shuffle?: string;
|
||||
stop?: string;
|
||||
volumeDown?: string;
|
||||
volumeUp?: string;
|
||||
};
|
||||
playbackStatus?: PlayerStatus;
|
||||
privateMode?: boolean;
|
||||
repeatMode?: PlayerRepeat;
|
||||
shuffleEnabled?: boolean;
|
||||
sidebarCollapsed?: boolean;
|
||||
};
|
||||
|
||||
interface DarwinMenuItemConstructorOptions extends MenuItemConstructorOptions {
|
||||
selector?: string;
|
||||
submenu?: DarwinMenuItemConstructorOptions[] | Menu;
|
||||
}
|
||||
|
||||
export default class MenuBuilder {
|
||||
developmentEnvironmentSetup = false;
|
||||
mainWindow: BrowserWindow;
|
||||
|
||||
constructor(mainWindow: BrowserWindow) {
|
||||
this.mainWindow = mainWindow;
|
||||
}
|
||||
|
||||
buildDarwinTemplate(): MenuItemConstructorOptions[] {
|
||||
buildDarwinTemplate({
|
||||
accelerators,
|
||||
playbackStatus = PlayerStatus.PAUSED,
|
||||
privateMode = false,
|
||||
repeatMode = PlayerRepeat.NONE,
|
||||
shuffleEnabled = false,
|
||||
sidebarCollapsed = false,
|
||||
}: MenuPlaybackState = {}): MenuItemConstructorOptions[] {
|
||||
const isPlaying = playbackStatus === PlayerStatus.PLAYING;
|
||||
const isRepeatEnabled = repeatMode !== PlayerRepeat.NONE;
|
||||
|
||||
const subMenuAbout: DarwinMenuItemConstructorOptions = {
|
||||
label: 'Electron',
|
||||
submenu: [
|
||||
@@ -29,6 +64,21 @@ export default class MenuBuilder {
|
||||
label: 'Settings',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-open-manage-servers');
|
||||
},
|
||||
label: 'Manage servers',
|
||||
},
|
||||
{
|
||||
checked: privateMode,
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-toggle-private-mode');
|
||||
},
|
||||
label: 'Private session',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ label: 'Services', submenu: [] },
|
||||
{ type: 'separator' },
|
||||
{
|
||||
@@ -71,6 +121,22 @@ export default class MenuBuilder {
|
||||
const subMenuViewDev: MenuItemConstructorOptions = {
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{
|
||||
accelerator: 'Command+K',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-open-command-palette');
|
||||
},
|
||||
label: 'Command Palette...',
|
||||
},
|
||||
{
|
||||
checked: sidebarCollapsed,
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-toggle-sidebar');
|
||||
},
|
||||
label: 'Collapse sidebar',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
accelerator: 'Command+R',
|
||||
click: () => {
|
||||
@@ -97,6 +163,22 @@ export default class MenuBuilder {
|
||||
const subMenuViewProd: MenuItemConstructorOptions = {
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{
|
||||
accelerator: 'Command+K',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-open-command-palette');
|
||||
},
|
||||
label: 'Command Palette...',
|
||||
},
|
||||
{
|
||||
checked: sidebarCollapsed,
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-toggle-sidebar');
|
||||
},
|
||||
label: 'Collapse sidebar',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
accelerator: 'Ctrl+Command+F',
|
||||
click: () => {
|
||||
@@ -119,6 +201,89 @@ export default class MenuBuilder {
|
||||
{ label: 'Bring All to Front', selector: 'arrangeInFront:' },
|
||||
],
|
||||
};
|
||||
const subMenuPlayback: MenuItemConstructorOptions = {
|
||||
label: 'Playback',
|
||||
submenu: [
|
||||
{
|
||||
accelerator: accelerators?.playPause,
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-player-play-pause');
|
||||
},
|
||||
label: isPlaying ? 'Pause' : 'Play',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
accelerator: accelerators?.next,
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-player-next');
|
||||
},
|
||||
label: 'Next',
|
||||
},
|
||||
{
|
||||
accelerator: accelerators?.previous,
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-player-previous');
|
||||
},
|
||||
label: 'Previous',
|
||||
},
|
||||
{
|
||||
accelerator: accelerators?.seekForward,
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-player-skip-forward');
|
||||
},
|
||||
label: 'Seek Forward',
|
||||
},
|
||||
{
|
||||
accelerator: accelerators?.seekBackward,
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-player-skip-backward');
|
||||
},
|
||||
label: 'Seek Backforward',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
accelerator: accelerators?.shuffle,
|
||||
checked: shuffleEnabled,
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-player-toggle-shuffle');
|
||||
},
|
||||
label: 'Shuffle',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
accelerator: accelerators?.repeat,
|
||||
checked: isRepeatEnabled,
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-player-toggle-repeat');
|
||||
},
|
||||
label: 'Repeat',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
accelerator: accelerators?.stop,
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-player-stop');
|
||||
},
|
||||
label: 'Stop',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
accelerator: accelerators?.volumeUp,
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-player-volume-up');
|
||||
},
|
||||
label: 'Volume Up',
|
||||
},
|
||||
{
|
||||
accelerator: accelerators?.volumeDown,
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-player-volume-down');
|
||||
},
|
||||
label: 'Volume Down',
|
||||
},
|
||||
],
|
||||
};
|
||||
const subMenuHelp: MenuItemConstructorOptions = {
|
||||
label: 'Help',
|
||||
submenu: [
|
||||
@@ -148,6 +313,13 @@ export default class MenuBuilder {
|
||||
},
|
||||
label: 'Search Issues',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-open-release-notes');
|
||||
},
|
||||
label: 'Version ' + packageJson.version,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -156,7 +328,14 @@ export default class MenuBuilder {
|
||||
? subMenuViewDev
|
||||
: subMenuViewProd;
|
||||
|
||||
return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp];
|
||||
return [
|
||||
subMenuAbout,
|
||||
subMenuEdit,
|
||||
subMenuView,
|
||||
subMenuPlayback,
|
||||
subMenuWindow,
|
||||
subMenuHelp,
|
||||
];
|
||||
}
|
||||
|
||||
buildDefaultTemplate(): MenuItemConstructorOptions[] {
|
||||
@@ -262,14 +441,14 @@ export default class MenuBuilder {
|
||||
return templateDefault;
|
||||
}
|
||||
|
||||
buildMenu(): Menu {
|
||||
buildMenu(playbackState: MenuPlaybackState = {}): Menu {
|
||||
if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') {
|
||||
this.setupDevelopmentEnvironment();
|
||||
}
|
||||
|
||||
const template =
|
||||
process.platform === 'darwin'
|
||||
? this.buildDarwinTemplate()
|
||||
? this.buildDarwinTemplate(playbackState)
|
||||
: this.buildDefaultTemplate();
|
||||
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
@@ -279,6 +458,13 @@ export default class MenuBuilder {
|
||||
}
|
||||
|
||||
setupDevelopmentEnvironment(): void {
|
||||
// buildMenu can run multiple times as menu state updates; attach this once.
|
||||
if (this.developmentEnvironmentSetup) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.developmentEnvironmentSetup = true;
|
||||
|
||||
this.mainWindow.webContents.on('context-menu', (_, props) => {
|
||||
const { x, y } = props;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SetActivity } from '@xhayper/discord-rpc';
|
||||
import type { SetActivity } from '@xhayper/discord-rpc';
|
||||
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
const initialize = (clientId: string) => {
|
||||
|
||||
@@ -65,6 +65,26 @@ const rendererOpenSettings = (cb: (event: IpcRendererEvent) => void) => {
|
||||
ipcRenderer.on('renderer-open-settings', cb);
|
||||
};
|
||||
|
||||
const rendererOpenCommandPalette = (cb: (event: IpcRendererEvent) => void) => {
|
||||
ipcRenderer.on('renderer-open-command-palette', cb);
|
||||
};
|
||||
|
||||
const rendererOpenManageServers = (cb: (event: IpcRendererEvent) => void) => {
|
||||
ipcRenderer.on('renderer-open-manage-servers', cb);
|
||||
};
|
||||
|
||||
const rendererTogglePrivateMode = (cb: (event: IpcRendererEvent) => void) => {
|
||||
ipcRenderer.on('renderer-toggle-private-mode', cb);
|
||||
};
|
||||
|
||||
const rendererToggleSidebar = (cb: (event: IpcRendererEvent) => void) => {
|
||||
ipcRenderer.on('renderer-toggle-sidebar', cb);
|
||||
};
|
||||
|
||||
const rendererOpenReleaseNotes = (cb: (event: IpcRendererEvent) => void) => {
|
||||
ipcRenderer.on('renderer-open-release-notes', cb);
|
||||
};
|
||||
|
||||
export const utils = {
|
||||
checkForUpdates,
|
||||
disableAutoUpdates,
|
||||
@@ -78,7 +98,12 @@ export const utils = {
|
||||
openApplicationDirectory,
|
||||
openItem,
|
||||
playerErrorListener,
|
||||
rendererOpenCommandPalette,
|
||||
rendererOpenManageServers,
|
||||
rendererOpenReleaseNotes,
|
||||
rendererOpenSettings,
|
||||
rendererTogglePrivateMode,
|
||||
rendererToggleSidebar,
|
||||
};
|
||||
|
||||
export type Utils = typeof utils;
|
||||
|
||||
+143
-160
@@ -10,6 +10,8 @@ import {
|
||||
ControllerEndpoint,
|
||||
InternalControllerEndpoint,
|
||||
ServerType,
|
||||
SetPlaylistSongsArgs,
|
||||
SetPlaylistSongsResponse,
|
||||
} from '/@/shared/types/domain-types';
|
||||
|
||||
type ApiController = {
|
||||
@@ -32,10 +34,8 @@ const apiController = <K extends keyof ControllerEndpoint>(
|
||||
|
||||
if (!serverType) {
|
||||
toast.error({
|
||||
message: i18n.t('error.serverNotSelectedError', {
|
||||
postProcess: 'sentenceCase',
|
||||
}) as string,
|
||||
title: i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' }) as string,
|
||||
message: i18n.t('error.serverNotSelectedError') as string,
|
||||
title: i18n.t('error.apiRouteError') as string,
|
||||
});
|
||||
throw new Error(`No server selected`);
|
||||
}
|
||||
@@ -45,13 +45,13 @@ const apiController = <K extends keyof ControllerEndpoint>(
|
||||
if (typeof controllerFn !== 'function') {
|
||||
toast.error({
|
||||
message: `Endpoint ${endpoint} is not implemented for ${serverType}`,
|
||||
title: i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' }) as string,
|
||||
title: i18n.t('error.apiRouteError') as string,
|
||||
});
|
||||
|
||||
throw new Error(
|
||||
i18n.t('error.endpointNotImplementedError', {
|
||||
endpoint,
|
||||
postProcess: 'sentenceCase',
|
||||
|
||||
serverType,
|
||||
}) as string,
|
||||
);
|
||||
@@ -67,6 +67,7 @@ const getPathReplaceSettings = () => {
|
||||
|
||||
const addContext = <T extends { apiClientProps: any; context?: any }>(args: T): T => {
|
||||
const pathSettings = getPathReplaceSettings();
|
||||
|
||||
return {
|
||||
...args,
|
||||
context: {
|
||||
@@ -89,9 +90,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: addToPlaylist`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: addToPlaylist`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -106,9 +105,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: createFavorite`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: createFavorite`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -120,9 +117,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: createInternetRadioStation`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: createInternetRadioStation`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -134,9 +129,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: createPlaylist`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: createPlaylist`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -144,13 +137,23 @@ export const controller: GeneralController = {
|
||||
server.type,
|
||||
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||
},
|
||||
deleteArtistImage(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: deleteArtistImage`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
'deleteArtistImage',
|
||||
server.type,
|
||||
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||
},
|
||||
deleteFavorite(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: deleteFavorite`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: deleteFavorite`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -162,9 +165,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: deleteInternetRadioStation`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: deleteInternetRadioStation`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -172,13 +173,23 @@ export const controller: GeneralController = {
|
||||
server.type,
|
||||
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||
},
|
||||
deleteInternetRadioStationImage(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: deleteInternetRadioStationImage`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
'deleteInternetRadioStationImage',
|
||||
server.type,
|
||||
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||
},
|
||||
deletePlaylist(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: deletePlaylist`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: deletePlaylist`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -186,13 +197,23 @@ export const controller: GeneralController = {
|
||||
server.type,
|
||||
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||
},
|
||||
deletePlaylistImage(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: deletePlaylistImage`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
'deletePlaylistImage',
|
||||
server.type,
|
||||
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||
},
|
||||
getAlbumArtistDetail(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getAlbumArtistDetail`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getAlbumArtistDetail`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -216,9 +237,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getAlbumArtistList`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getAlbumArtistList`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -236,9 +255,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getAlbumArtistListCount`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getAlbumArtistListCount`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -256,9 +273,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getAlbumDetail`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getAlbumDetail`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -270,9 +285,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getAlbumInfo`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getAlbumInfo`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -284,9 +297,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getAlbumList`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getAlbumList`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -304,9 +315,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getAlbumListCount`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getAlbumListCount`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -324,9 +333,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getAlbumRadio`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getAlbumRadio`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -338,9 +345,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getArtistList`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getArtistList`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -358,9 +363,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getArtistListCount`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getArtistListCount`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -378,9 +381,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getArtistRadio`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getArtistRadio`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -392,9 +393,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getDownloadUrl`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getDownloadUrl`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -406,9 +405,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getFolder`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getFolder`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -426,9 +423,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getGenreList`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getGenreList`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -484,9 +479,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getInternetRadioStations`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getInternetRadioStations`);
|
||||
}
|
||||
return apiController(
|
||||
'getInternetRadioStations',
|
||||
@@ -497,9 +490,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getLyrics`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getLyrics`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -511,9 +502,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getMusicFolderList`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getMusicFolderList`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -525,9 +514,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getPlaylistDetail`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getPlaylistDetail`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -539,9 +526,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getPlaylistList`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getPlaylistList`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -553,9 +538,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getPlaylistListCount`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getPlaylistListCount`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -567,9 +550,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getPlaylistSongList`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getPlaylistSongList`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -581,9 +562,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getPlayQueue`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getPlayQueue`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -595,9 +574,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getRandomSongList`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getRandomSongList`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -615,9 +592,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getRoles`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getRoles`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -629,9 +604,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getServerInfo`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getServerInfo`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -643,9 +616,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getSimilarSongs`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getSimilarSongs`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -663,9 +634,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getSongDetail`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getSongDetail`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -677,9 +646,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getSongList`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getSongList`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -697,9 +664,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getSongListCount`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getSongListCount`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -717,7 +682,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
return '';
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getStreamUrl`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -729,9 +694,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getStructuredLyrics`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getStructuredLyrics`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -743,9 +706,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getTags`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getTags`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -757,9 +718,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getTopSongs`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getTopSongs`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -771,9 +730,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getUserInfo`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getUserInfo`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -785,9 +742,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getUserList`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getUserList`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -799,9 +754,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: movePlaylistItem`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: movePlaylistItem`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -813,9 +766,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: removeFromPlaylist`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: removeFromPlaylist`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -827,9 +778,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: replacePlaylist`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: replacePlaylist`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -841,9 +790,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: savePlayQueue`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: savePlayQueue`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -855,9 +802,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: scrobble`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: scrobble`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -869,9 +814,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: search`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: search`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -885,13 +828,23 @@ export const controller: GeneralController = {
|
||||
}),
|
||||
);
|
||||
},
|
||||
setPlaylistSongs: function (args: SetPlaylistSongsArgs): Promise<SetPlaylistSongsResponse> {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: setPlaylistSongs`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
'setPlaylistSongs',
|
||||
server.type,
|
||||
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||
},
|
||||
setRating(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: setRating`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: setRating`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -903,9 +856,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: shareItem`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: shareItem`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -917,9 +868,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: updateInternetRadioStation`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: updateInternetRadioStation`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -931,9 +880,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: updatePlaylist`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: updatePlaylist`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -941,4 +888,40 @@ export const controller: GeneralController = {
|
||||
server.type,
|
||||
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||
},
|
||||
uploadArtistImage(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: uploadArtistImage`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
'uploadArtistImage',
|
||||
server.type,
|
||||
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||
},
|
||||
uploadInternetRadioStationImage(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: uploadInternetRadioStationImage`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
'uploadInternetRadioStationImage',
|
||||
server.type,
|
||||
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||
},
|
||||
uploadPlaylistImage(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: uploadPlaylistImage`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
'uploadPlaylistImage',
|
||||
server.type,
|
||||
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -447,11 +447,7 @@ export const jfApiClient = (args: {
|
||||
} catch (e: any | AxiosError | Error) {
|
||||
if (isAxiosError(e)) {
|
||||
if (e.code === 'ERR_NETWORK') {
|
||||
throw new Error(
|
||||
i18n.t('error.networkError', {
|
||||
postProcess: 'sentenceCase',
|
||||
}) as string,
|
||||
);
|
||||
throw new Error(i18n.t('error.networkError') as string);
|
||||
}
|
||||
|
||||
const error = e as AxiosError;
|
||||
|
||||
@@ -409,6 +409,8 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
return jfNormalize.album(
|
||||
{ ...res.body, Songs: songsRes.body.Items },
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
);
|
||||
},
|
||||
getAlbumList: async (args) => {
|
||||
@@ -580,7 +582,8 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
|
||||
return `${apiClientProps.server?.url}/items/${query.id}/download?apiKey=${apiClientProps.server?.credential}`;
|
||||
},
|
||||
getFolder: async ({ apiClientProps, query }) => {
|
||||
getFolder: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
const userId = apiClientProps.server?.userId;
|
||||
|
||||
if (!userId) throw new Error('No userId found');
|
||||
@@ -742,6 +745,8 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
jfNormalize.song(
|
||||
item as unknown as z.infer<typeof jfType._response.song>,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1283,7 +1288,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
apiClientProps,
|
||||
query: { ...query, limit: 1, startIndex: 0 },
|
||||
}).then((result) => result!.totalRecordCount!),
|
||||
getStreamUrl: ({ apiClientProps: { server }, query }) => {
|
||||
getStreamUrl: async ({ apiClientProps: { server }, query }) => {
|
||||
const { bitrate, format, id, transcode } = query;
|
||||
const deviceId = '';
|
||||
|
||||
@@ -1769,6 +1774,24 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
),
|
||||
};
|
||||
},
|
||||
setPlaylistSongs: async (args) => {
|
||||
const { apiClientProps, body } = args;
|
||||
|
||||
const res = await jfApiClient(apiClientProps).updatePlaylist({
|
||||
body: {
|
||||
Ids: body.songIds,
|
||||
},
|
||||
params: {
|
||||
id: body.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 204) {
|
||||
throw new Error('Failed to update playlist songs');
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
updateInternetRadioStation: async (args) => {
|
||||
const { apiClientProps, body, query } = args;
|
||||
|
||||
@@ -1798,14 +1821,8 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
|
||||
const res = await jfApiClient(apiClientProps).updatePlaylist({
|
||||
body: {
|
||||
Genres: body.genres?.map((item) => ({ Id: item.id, Name: item.name })) || [],
|
||||
IsPublic: body.public,
|
||||
MediaType: 'Audio',
|
||||
Name: body.name,
|
||||
PremiereDate: null,
|
||||
ProviderIds: {},
|
||||
Tags: [],
|
||||
UserId: apiClientProps.server?.userId, // Required
|
||||
},
|
||||
params: {
|
||||
id: query.id,
|
||||
@@ -1820,31 +1837,6 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
},
|
||||
};
|
||||
|
||||
// const getArtistList = async (args: ArtistListArgs): Promise<AlbumArtistListResponse> => {
|
||||
// const { query, apiClientProps } = args;
|
||||
|
||||
// const res = await jfApiClient(apiClientProps).getAlbumArtistList({
|
||||
// query: {
|
||||
// Limit: query.limit,
|
||||
// ParentId: query.musicFolderId,
|
||||
// Recursive: true,
|
||||
// SortBy: artistListSortMap.jellyfin[query.sortBy] || 'SortName,Name',
|
||||
// SortOrder: sortOrderMap.jellyfin[query.sortOrder],
|
||||
// StartIndex: query.startIndex,
|
||||
// },
|
||||
// });
|
||||
|
||||
// if (res.status !== 200) {
|
||||
// throw new Error('Failed to get artist list');
|
||||
// }
|
||||
|
||||
// return {
|
||||
// items: res.body.Items.map((item) => jfNormalize.albumArtist(item, apiClientProps.server)),
|
||||
// startIndex: query.startIndex,
|
||||
// totalRecordCount: res.body.TotalRecordCount,
|
||||
// };
|
||||
// };
|
||||
|
||||
function getLibraryId(musicFolderId?: string | string[]) {
|
||||
return Array.isArray(musicFolderId) ? musicFolderId[0] : musicFolderId;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,33 @@ export const contract = c.router({
|
||||
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: {
|
||||
body: null,
|
||||
method: 'DELETE',
|
||||
path: 'radio/:id',
|
||||
responses: {
|
||||
200: resultWithHeaders(ndType._response.deleteInternetRadioStation),
|
||||
500: resultWithHeaders(ndType._response.error),
|
||||
},
|
||||
},
|
||||
deleteInternetRadioStationImage: {
|
||||
body: null,
|
||||
method: 'DELETE',
|
||||
path: 'radio/:id/image',
|
||||
responses: {
|
||||
200: resultWithHeaders(ndType._response.deleteInternetRadioStationImage),
|
||||
500: resultWithHeaders(ndType._response.error),
|
||||
},
|
||||
},
|
||||
deletePlaylist: {
|
||||
body: null,
|
||||
method: 'DELETE',
|
||||
@@ -55,6 +82,15 @@ export const contract = c.router({
|
||||
500: resultWithHeaders(ndType._response.error),
|
||||
},
|
||||
},
|
||||
deletePlaylistImage: {
|
||||
body: null,
|
||||
method: 'DELETE',
|
||||
path: 'playlist/:id/image',
|
||||
responses: {
|
||||
200: resultWithHeaders(ndType._response.deletePlaylistImage),
|
||||
500: resultWithHeaders(ndType._response.error),
|
||||
},
|
||||
},
|
||||
getAlbumArtistDetail: {
|
||||
method: 'GET',
|
||||
path: 'artist/:id',
|
||||
@@ -132,6 +168,15 @@ export const contract = c.router({
|
||||
500: resultWithHeaders(ndType._response.error),
|
||||
},
|
||||
},
|
||||
getRadioList: {
|
||||
method: 'GET',
|
||||
path: 'radio',
|
||||
query: ndType._parameters.radioList,
|
||||
responses: {
|
||||
200: resultWithHeaders(ndType._response.radioList),
|
||||
500: resultWithHeaders(ndType._response.error),
|
||||
},
|
||||
},
|
||||
getSongDetail: {
|
||||
method: 'GET',
|
||||
path: 'song/:id',
|
||||
@@ -205,6 +250,15 @@ export const contract = c.router({
|
||||
500: resultWithHeaders(ndType._response.error),
|
||||
},
|
||||
},
|
||||
updateInternetRadioStation: {
|
||||
body: ndType._parameters.updateInternetRadioStation,
|
||||
method: 'PUT',
|
||||
path: 'radio/:id',
|
||||
responses: {
|
||||
200: resultWithHeaders(ndType._response.updateInternetRadioStation),
|
||||
500: resultWithHeaders(ndType._response.error),
|
||||
},
|
||||
},
|
||||
updatePlaylist: {
|
||||
body: ndType._parameters.updatePlaylist,
|
||||
method: 'PUT',
|
||||
@@ -214,6 +268,33 @@ export const contract = c.router({
|
||||
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: {
|
||||
body: ndType._parameters.uploadInternetRadioStationImage,
|
||||
method: 'POST',
|
||||
path: 'radio/:id/image',
|
||||
responses: {
|
||||
200: resultWithHeaders(ndType._response.uploadInternetRadioStationImage),
|
||||
500: resultWithHeaders(ndType._response.error),
|
||||
},
|
||||
},
|
||||
uploadPlaylistImage: {
|
||||
body: ndType._parameters.uploadPlaylistImage,
|
||||
method: 'POST',
|
||||
path: 'playlist/:id/image',
|
||||
responses: {
|
||||
200: resultWithHeaders(ndType._response.uploadPlaylistImage),
|
||||
500: resultWithHeaders(ndType._response.error),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const axiosClient = axios.create({});
|
||||
@@ -324,12 +405,8 @@ axiosClient.interceptors.response.use(
|
||||
|
||||
if (res.status === 429) {
|
||||
toast.error({
|
||||
message: i18n.t('error.loginRateError', {
|
||||
postProcess: 'sentenceCase',
|
||||
}) as string,
|
||||
title: i18n.t('error.sessionExpiredError', {
|
||||
postProcess: 'sentenceCase',
|
||||
}) as string,
|
||||
message: i18n.t('error.loginRateError') as string,
|
||||
title: i18n.t('error.sessionExpiredError') as string,
|
||||
});
|
||||
|
||||
const serverId = currentServer.id;
|
||||
@@ -344,11 +421,7 @@ axiosClient.interceptors.response.use(
|
||||
throw TIMEOUT_ERROR;
|
||||
}
|
||||
if (res.status !== 200) {
|
||||
throw new Error(
|
||||
i18n.t('error.authenticatedFailed', {
|
||||
postProcess: 'sentenceCase',
|
||||
}) as string,
|
||||
);
|
||||
throw new Error(i18n.t('error.authenticatedFailed') as string);
|
||||
}
|
||||
|
||||
const newCredential = res.data.token;
|
||||
@@ -441,11 +514,7 @@ export const ndApiClient = (args: {
|
||||
} catch (e: any | AxiosError | Error) {
|
||||
if (isAxiosError(e)) {
|
||||
if (e.code === 'ERR_NETWORK') {
|
||||
throw new Error(
|
||||
i18n.t('error.networkError', {
|
||||
postProcess: 'sentenceCase',
|
||||
}) as string,
|
||||
);
|
||||
throw new Error(i18n.t('error.networkError') as string);
|
||||
}
|
||||
|
||||
const error = e as AxiosError;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import axios from 'axios';
|
||||
import { set } from 'idb-keyval';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
|
||||
@@ -5,13 +6,19 @@ import { ndApiClient } from '/@/renderer/api/navidrome/navidrome-api';
|
||||
import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
||||
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
|
||||
import { ndNormalize } from '/@/shared/api/navidrome/navidrome-normalize';
|
||||
import { NDSongListSort } from '/@/shared/api/navidrome/navidrome-types';
|
||||
import { NDRadioListSort, NDSongListSort } from '/@/shared/api/navidrome/navidrome-types';
|
||||
import { ssNormalize } from '/@/shared/api/subsonic/subsonic-normalize';
|
||||
import { getFeatures, hasFeature, hasFeatureWithVersion, VersionInfo } from '/@/shared/api/utils';
|
||||
import {
|
||||
albumArtistListSortMap,
|
||||
albumListSortMap,
|
||||
AuthenticationResponse,
|
||||
DeleteArtistImageArgs,
|
||||
DeleteArtistImageResponse,
|
||||
DeleteInternetRadioStationImageArgs,
|
||||
DeleteInternetRadioStationImageResponse,
|
||||
DeletePlaylistImageArgs,
|
||||
DeletePlaylistImageResponse,
|
||||
genreListSortMap,
|
||||
InternalControllerEndpoint,
|
||||
playlistListSortMap,
|
||||
@@ -23,6 +30,12 @@ import {
|
||||
SortOrder,
|
||||
sortOrderMap,
|
||||
tagListSortMap,
|
||||
UploadArtistImageArgs,
|
||||
UploadArtistImageResponse,
|
||||
UploadInternetRadioStationImageArgs,
|
||||
UploadInternetRadioStationImageResponse,
|
||||
UploadPlaylistImageArgs,
|
||||
UploadPlaylistImageResponse,
|
||||
userListSortMap,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { ServerFeature } from '/@/shared/types/features-types';
|
||||
@@ -30,6 +43,14 @@ import { ServerFeature } from '/@/shared/types/features-types';
|
||||
const VERSION_INFO: VersionInfo = [
|
||||
// Why 2? Subsonic controller will return 1 for its own implementation
|
||||
// Use 2 to denote that Navidrome's own API has a different endpoint
|
||||
[
|
||||
'0.61.0',
|
||||
{
|
||||
[ServerFeature.ARTIST_IMAGE_UPLOAD]: [1],
|
||||
[ServerFeature.INTERNET_RADIO_IMAGE_UPLOAD]: [1],
|
||||
[ServerFeature.PLAYLIST_IMAGE_UPLOAD]: [1],
|
||||
},
|
||||
],
|
||||
['0.60.4', { [ServerFeature.TRACK_YES_NO_RATING_FILTER]: [1] }],
|
||||
['0.57.0', { [ServerFeature.SERVER_PLAY_QUEUE]: [2] }],
|
||||
['0.56.0', { [ServerFeature.TRACK_ALBUM_ARTIST_SEARCH]: [1] }],
|
||||
@@ -170,8 +191,54 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
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,
|
||||
deleteInternetRadioStation: SubsonicController.deleteInternetRadioStation,
|
||||
deleteInternetRadioStation: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
const res = await ndApiClient(apiClientProps).deleteInternetRadioStation({
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to delete internet radio station');
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
deleteInternetRadioStationImage: async (
|
||||
args: DeleteInternetRadioStationImageArgs,
|
||||
): Promise<DeleteInternetRadioStationImageResponse> => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
const res = await ndApiClient(apiClientProps as any).deleteInternetRadioStationImage({
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to delete internet radio station image');
|
||||
}
|
||||
|
||||
return res.body.data.status === 'ok';
|
||||
},
|
||||
deletePlaylist: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
@@ -187,6 +254,23 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
|
||||
return null;
|
||||
},
|
||||
deletePlaylistImage: async (
|
||||
args: DeletePlaylistImageArgs,
|
||||
): Promise<DeletePlaylistImageResponse> => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
const res = await ndApiClient(apiClientProps as any).deletePlaylistImage({
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to delete playlist image');
|
||||
}
|
||||
|
||||
return res.body.data.status === 'ok';
|
||||
},
|
||||
getAlbumArtistDetail: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
@@ -233,8 +317,8 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
similarArtists:
|
||||
artistInfo?.similarArtist?.map((artist) => ({
|
||||
id: artist.id,
|
||||
imageId: null,
|
||||
imageUrl: artist?.artistImageUrl?.replace(/\?size=\d+/, '') ?? null,
|
||||
imageId: artist.id,
|
||||
imageUrl: null,
|
||||
name: artist.name,
|
||||
userFavorite: Boolean(artist.starred) || false,
|
||||
userRating: artist.userRating ?? null,
|
||||
@@ -412,7 +496,12 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
return res.body.similarSongs.song.map((song) =>
|
||||
ssNormalize.song(song, apiClientProps.server),
|
||||
ssNormalize.song(
|
||||
song,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
);
|
||||
},
|
||||
getArtistList: async (args) => {
|
||||
@@ -482,7 +571,12 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
return res.body.similarSongs2.song.map((song) =>
|
||||
ssNormalize.song(song, apiClientProps.server),
|
||||
ssNormalize.song(
|
||||
song,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
);
|
||||
},
|
||||
getDownloadUrl: SubsonicController.getDownloadUrl,
|
||||
@@ -547,7 +641,24 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
},
|
||||
getImageRequest: SubsonicController.getImageRequest,
|
||||
getImageUrl: SubsonicController.getImageUrl,
|
||||
getInternetRadioStations: SubsonicController.getInternetRadioStations,
|
||||
getInternetRadioStations: async (args) => {
|
||||
const { apiClientProps } = args;
|
||||
|
||||
const res = await ndApiClient(apiClientProps).getRadioList({
|
||||
query: {
|
||||
_end: -1,
|
||||
_order: 'ASC',
|
||||
_sort: NDRadioListSort.NAME,
|
||||
_start: 0,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get internet radio stations');
|
||||
}
|
||||
|
||||
return res.body.data.map((station) => ndNormalize.internetRadioStation(station));
|
||||
},
|
||||
getLyrics: SubsonicController.getLyrics,
|
||||
getMusicFolderList: SubsonicController.getMusicFolderList,
|
||||
getPlaylistDetail: async (args) => {
|
||||
@@ -604,6 +715,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
query: {
|
||||
_end: -1,
|
||||
_order: 'ASC',
|
||||
_sort: NDSongListSort.ID,
|
||||
_start: 0,
|
||||
...excludeMissing(apiClientProps.server),
|
||||
},
|
||||
@@ -636,7 +748,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
throw new Error('Failed to get play queue');
|
||||
}
|
||||
|
||||
const { changedBy, current, items, position, updatedAt } = res.body.data;
|
||||
const { changedBy, current, items = [], position, updatedAt } = res.body.data; // if there is no queue saved, items is undefined
|
||||
|
||||
const entries = items.map((song) =>
|
||||
ndNormalize.song(
|
||||
@@ -721,7 +833,14 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
return (
|
||||
(res.body.similarSongs?.song || [])
|
||||
.filter((song) => song.id !== query.songId)
|
||||
.map((song) => ssNormalize.song(song, apiClientProps.server)) || []
|
||||
.map((song) =>
|
||||
ssNormalize.song(
|
||||
song,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
) || []
|
||||
);
|
||||
},
|
||||
getSongDetail: async (args) => {
|
||||
@@ -747,45 +866,77 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
getSongList: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
const res = await ndApiClient(apiClientProps).getSongList({
|
||||
query: {
|
||||
_end: query.startIndex + (query.limit || -1),
|
||||
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||
_sort: songListSortMap.navidrome[query.sortBy],
|
||||
_start: query.startIndex,
|
||||
album_id: query.albumIds,
|
||||
genre_id: query.genreIds,
|
||||
[getArtistSongKey(apiClientProps.server)]: query.artistIds ?? query.albumArtistIds,
|
||||
...(hasFeature(apiClientProps.server, ServerFeature.TRACK_YES_NO_RATING_FILTER) &&
|
||||
query.hasRating !== undefined
|
||||
? { has_rating: query.hasRating }
|
||||
: {}),
|
||||
library_id: getLibraryId(query.musicFolderId),
|
||||
starred: query.favorite,
|
||||
title: query.searchTerm,
|
||||
year: query.maxYear || query.minYear,
|
||||
...query._custom,
|
||||
...excludeMissing(apiClientProps.server),
|
||||
},
|
||||
});
|
||||
const ALBUM_IDS_BATCH_SIZE = 500;
|
||||
const albumIds = query.albumIds;
|
||||
const shouldBatch = albumIds && albumIds.length > ALBUM_IDS_BATCH_SIZE;
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get song list');
|
||||
const fetchAlbums = async (albumIdBatch: string[] | undefined) => {
|
||||
const res = await ndApiClient(apiClientProps).getSongList({
|
||||
query: {
|
||||
_end: query.startIndex + (query.limit || -1),
|
||||
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||
_sort: songListSortMap.navidrome[query.sortBy],
|
||||
_start: query.startIndex,
|
||||
album_id: albumIdBatch ?? query.albumIds,
|
||||
genre_id: query.genreIds,
|
||||
[getArtistSongKey(apiClientProps.server)]:
|
||||
query.artistIds ?? query.albumArtistIds,
|
||||
...(hasFeature(
|
||||
apiClientProps.server,
|
||||
ServerFeature.TRACK_YES_NO_RATING_FILTER,
|
||||
) && query.hasRating !== undefined
|
||||
? { has_rating: query.hasRating }
|
||||
: {}),
|
||||
library_id: getLibraryId(query.musicFolderId),
|
||||
starred: query.favorite,
|
||||
title: query.searchTerm,
|
||||
year: query.maxYear || query.minYear,
|
||||
...query._custom,
|
||||
...excludeMissing(apiClientProps.server),
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get song list');
|
||||
}
|
||||
|
||||
return {
|
||||
items: res.body.data.map((song) =>
|
||||
ndNormalize.song(
|
||||
song,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
),
|
||||
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||
};
|
||||
};
|
||||
|
||||
if (shouldBatch && albumIds) {
|
||||
const batches: string[][] = [];
|
||||
for (let i = 0; i < albumIds.length; i += ALBUM_IDS_BATCH_SIZE) {
|
||||
batches.push(albumIds.slice(i, i + ALBUM_IDS_BATCH_SIZE));
|
||||
}
|
||||
|
||||
const results = await Promise.all(batches.map((batch) => fetchAlbums(batch)));
|
||||
|
||||
return {
|
||||
items: results.flatMap((r) => r.items),
|
||||
startIndex: query?.startIndex ?? 0,
|
||||
totalRecordCount: results.reduce((sum, r) => sum + r.totalRecordCount, 0),
|
||||
};
|
||||
}
|
||||
|
||||
const albums = await fetchAlbums(undefined);
|
||||
|
||||
return {
|
||||
items: res.body.data.map((song) =>
|
||||
ndNormalize.song(
|
||||
song,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
),
|
||||
startIndex: query?.startIndex || 0,
|
||||
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||
items: albums.items,
|
||||
startIndex: query?.startIndex ?? 0,
|
||||
totalRecordCount: albums.totalRecordCount,
|
||||
};
|
||||
},
|
||||
|
||||
getSongListCount: async ({ apiClientProps, query }) =>
|
||||
NavidromeController.getSongList({
|
||||
apiClientProps,
|
||||
@@ -888,6 +1039,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
|
||||
const res = await NavidromeController.getSongList({
|
||||
apiClientProps,
|
||||
context: args.context,
|
||||
query: {
|
||||
artistIds: [query.artistId],
|
||||
sortBy: SongListSort.PLAY_COUNT,
|
||||
@@ -978,6 +1130,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
query: {
|
||||
_end: -1,
|
||||
_order: 'ASC',
|
||||
_sort: NDSongListSort.ID,
|
||||
_start: 0,
|
||||
...excludeMissing(apiClientProps.server),
|
||||
},
|
||||
@@ -1088,6 +1241,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
},
|
||||
scrobble: SubsonicController.scrobble,
|
||||
search: SubsonicController.search,
|
||||
setPlaylistSongs: SubsonicController.setPlaylistSongs,
|
||||
setRating: SubsonicController.setRating,
|
||||
shareItem: async (args) => {
|
||||
const { apiClientProps, body } = args;
|
||||
@@ -1110,7 +1264,26 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
id: res.body.data.id,
|
||||
};
|
||||
},
|
||||
updateInternetRadioStation: SubsonicController.updateInternetRadioStation,
|
||||
updateInternetRadioStation: async (args) => {
|
||||
const { apiClientProps, body, query } = args;
|
||||
|
||||
const res = await ndApiClient(apiClientProps).updateInternetRadioStation({
|
||||
body: {
|
||||
homePageUrl: body.homepageUrl ?? '',
|
||||
name: body.name,
|
||||
streamUrl: body.streamUrl,
|
||||
},
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to update internet radio station');
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
updatePlaylist: async (args) => {
|
||||
const { apiClientProps, body, query } = args;
|
||||
|
||||
@@ -1135,4 +1308,110 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
|
||||
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 (
|
||||
args: UploadInternetRadioStationImageArgs,
|
||||
): Promise<UploadInternetRadioStationImageResponse> => {
|
||||
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/radio/${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 internet radio station image');
|
||||
}
|
||||
|
||||
return res.data?.status === 'ok';
|
||||
},
|
||||
uploadPlaylistImage: async (
|
||||
args: UploadPlaylistImageArgs,
|
||||
): Promise<UploadPlaylistImageResponse> => {
|
||||
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/playlist/${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 playlist image');
|
||||
}
|
||||
|
||||
return res.data?.status === 'ok';
|
||||
},
|
||||
};
|
||||
|
||||
@@ -347,6 +347,11 @@ export const queryKeys: Record<
|
||||
list: (serverId: string) => [serverId, 'roles'] as const,
|
||||
},
|
||||
search: {
|
||||
infiniteList: (
|
||||
serverId: string,
|
||||
type: 'albumArtists' | 'albums' | 'songs',
|
||||
searchTerm: string,
|
||||
) => [serverId, 'search', 'infiniteList', type, searchTerm] as const,
|
||||
list: (serverId: string, query?: SearchQuery) => {
|
||||
if (query) return [serverId, 'search', 'list', query] as const;
|
||||
return [serverId, 'search', 'list'] as const;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { initClient, initContract } from '@ts-rest/core';
|
||||
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse, isAxiosError } from 'axios';
|
||||
import omitBy from 'lodash/omitBy';
|
||||
import qs from 'qs';
|
||||
import { z } from 'zod';
|
||||
|
||||
@@ -250,6 +249,23 @@ export const contract = c.router({
|
||||
200: ssType._response.topSongsList,
|
||||
},
|
||||
},
|
||||
getTranscodeDecision: {
|
||||
body: ssType._body.getTranscodeDecision,
|
||||
method: 'POST',
|
||||
path: 'getTranscodeDecision.view',
|
||||
query: ssType._parameters.getTranscodeDecision,
|
||||
responses: {
|
||||
200: ssType._response.getTranscodeDecision,
|
||||
},
|
||||
},
|
||||
getTranscodeStream: {
|
||||
method: 'GET',
|
||||
path: 'getTranscodeStream.view',
|
||||
query: ssType._parameters.getTranscodeStream,
|
||||
responses: {
|
||||
200: z.string(),
|
||||
},
|
||||
},
|
||||
getUser: {
|
||||
method: 'GET',
|
||||
path: 'getUser.view',
|
||||
@@ -345,7 +361,7 @@ axiosClient.interceptors.response.use(
|
||||
if (data['subsonic-response'].error.code !== 0) {
|
||||
toast.error({
|
||||
message: data['subsonic-response'].error.message,
|
||||
title: i18n.t('error.genericError', { postProcess: 'sentenceCase' }) as string,
|
||||
title: i18n.t('error.genericError') as string,
|
||||
});
|
||||
|
||||
// Since we do status === 200, override this value with the error code
|
||||
@@ -360,11 +376,39 @@ axiosClient.interceptors.response.use(
|
||||
},
|
||||
);
|
||||
|
||||
const keysToSkipEmptyCheck = new Set([
|
||||
'artist',
|
||||
'comment',
|
||||
'genre',
|
||||
'name',
|
||||
'query',
|
||||
'u',
|
||||
'username',
|
||||
]);
|
||||
|
||||
const parsePath = (fullPath: string) => {
|
||||
const [path, params] = fullPath.split('?');
|
||||
|
||||
const parsedParams = qs.parse(params, { arrayLimit: 99999, parameterLimit: 99999 });
|
||||
const notNilParams = omitBy(parsedParams, (value) => value === 'undefined' || value === 'null');
|
||||
const url = new URLSearchParams(params);
|
||||
const notNilParams: Record<string, string[]> = {};
|
||||
|
||||
for (const [key, value] of url) {
|
||||
if (!keysToSkipEmptyCheck.has(key) && (value === 'undefined' || value === 'null')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let realKey = key;
|
||||
|
||||
if (key.includes('[') && key.includes(']')) {
|
||||
realKey = key.split('[')[0];
|
||||
}
|
||||
|
||||
if (realKey in notNilParams) {
|
||||
notNilParams[realKey].push(value);
|
||||
} else {
|
||||
notNilParams[realKey] = [value];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
params: notNilParams,
|
||||
@@ -392,7 +436,7 @@ export const ssApiClient = (args: {
|
||||
const { server, signal, silent, url } = args;
|
||||
|
||||
return initClient(contract, {
|
||||
api: async ({ headers, method, path }) => {
|
||||
api: async ({ body, headers, method, path, rawQuery }) => {
|
||||
let baseUrl: string | undefined;
|
||||
const authParams: Record<string, any> = {};
|
||||
|
||||
@@ -423,19 +467,44 @@ export const ssApiClient = (args: {
|
||||
url: `${baseUrl}/${api}`,
|
||||
};
|
||||
|
||||
const data = {
|
||||
c: 'Feishin',
|
||||
f: 'json',
|
||||
v: '1.13.0',
|
||||
...authParams,
|
||||
...params,
|
||||
};
|
||||
const isGetTranscodeDecisionPost =
|
||||
method === 'POST' && api === 'getTranscodeDecision.view';
|
||||
|
||||
if (hasFeature(server, ServerFeature.OS_FORM_POST)) {
|
||||
if (isGetTranscodeDecisionPost && body != null) {
|
||||
request.method = 'POST';
|
||||
request.headers = {
|
||||
...headers,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
request.data = body;
|
||||
request.params = {
|
||||
c: 'Feishin',
|
||||
f: 'json',
|
||||
v: '1.13.0',
|
||||
...authParams,
|
||||
...(typeof rawQuery === 'object' && rawQuery !== null
|
||||
? (rawQuery as Record<string, unknown>)
|
||||
: {}),
|
||||
};
|
||||
} else if (hasFeature(server, ServerFeature.OS_FORM_POST)) {
|
||||
headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||
request.method = 'POST';
|
||||
const data = {
|
||||
c: 'Feishin',
|
||||
f: 'json',
|
||||
v: '1.13.0',
|
||||
...authParams,
|
||||
...params,
|
||||
};
|
||||
request.data = qs.stringify(data, { arrayFormat: 'repeat' });
|
||||
} else {
|
||||
const data = {
|
||||
c: 'Feishin',
|
||||
f: 'json',
|
||||
v: '1.13.0',
|
||||
...authParams,
|
||||
...params,
|
||||
};
|
||||
request.method = method;
|
||||
request.params = data;
|
||||
}
|
||||
@@ -454,11 +523,7 @@ export const ssApiClient = (args: {
|
||||
} catch (e: any | AxiosError | Error) {
|
||||
if (isAxiosError(e)) {
|
||||
if (e.code === 'ERR_NETWORK') {
|
||||
throw new Error(
|
||||
i18n.t('error.networkError', {
|
||||
postProcess: 'sentenceCase',
|
||||
}) as string,
|
||||
);
|
||||
throw new Error(i18n.t('error.networkError') as string);
|
||||
}
|
||||
|
||||
const error = e as AxiosError;
|
||||
|
||||
@@ -8,7 +8,12 @@ import md5 from 'md5';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { contract, ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
||||
import {
|
||||
getDefaultTranscodingProfiles,
|
||||
getDirectPlayProfiles,
|
||||
} from '/@/renderer/features/player/components/audio-players';
|
||||
import { randomString } from '/@/renderer/utils';
|
||||
import { logFn } from '/@/renderer/utils/logger';
|
||||
import { getServerUrl } from '/@/renderer/utils/normalize-server-url';
|
||||
import { ssNormalize } from '/@/shared/api/subsonic/subsonic-normalize';
|
||||
import {
|
||||
@@ -87,6 +92,172 @@ const ALBUM_LIST_SORT_MAPPING: Record<AlbumListSort, AlbumListSortType | undefin
|
||||
const MAX_SUBSONIC_ITEMS = 500;
|
||||
const SUBSONIC_FAST_BATCH_SIZE = MAX_SUBSONIC_ITEMS * 10;
|
||||
|
||||
// const TRANSCODE_DIRECT_PLAY_PROFILES = [
|
||||
// {
|
||||
// audioCodecs: ['mp3'],
|
||||
// containers: ['mp3'],
|
||||
// maxAudioChannels: 2,
|
||||
// protocols: ['http'],
|
||||
// },
|
||||
// {
|
||||
// audioCodecs: ['aac'],
|
||||
// containers: ['m4a', 'mp4'],
|
||||
// maxAudioChannels: 2,
|
||||
// protocols: ['http'],
|
||||
// },
|
||||
// {
|
||||
// audioCodecs: ['vorbis'],
|
||||
// containers: ['ogg'],
|
||||
// maxAudioChannels: 2,
|
||||
// protocols: ['http'],
|
||||
// },
|
||||
// {
|
||||
// audioCodecs: ['opus'],
|
||||
// containers: ['ogg', 'webm'],
|
||||
// maxAudioChannels: 2,
|
||||
// protocols: ['http'],
|
||||
// },
|
||||
// {
|
||||
// audioCodecs: ['pcm'],
|
||||
// containers: ['wav'],
|
||||
// maxAudioChannels: 2,
|
||||
// protocols: ['http'],
|
||||
// },
|
||||
// {
|
||||
// audioCodecs: ['flac'],
|
||||
// containers: ['flac'],
|
||||
// maxAudioChannels: 2,
|
||||
// protocols: ['http'],
|
||||
// },
|
||||
// ];
|
||||
|
||||
// const TRANSCODE_UNSUPPORTED_DIRECT_PLAY_PROFILES = [
|
||||
// {
|
||||
// containers: ["m4a", "mp4"],
|
||||
// audioCodecs: ["alac"],
|
||||
// protocols: ["http"],
|
||||
// maxAudioChannels: 2
|
||||
// },
|
||||
// {
|
||||
// containers: ["m4a", "mp4"],
|
||||
// audioCodecs: ["ac3", "eac3"],
|
||||
// protocols: ["http"],
|
||||
// maxAudioChannels: 6
|
||||
// },
|
||||
// {
|
||||
// containers: ["ogg"],
|
||||
// audioCodecs: ["flac", "speex"],
|
||||
// protocols: ["http"],
|
||||
// maxAudioChannels: 2
|
||||
// },
|
||||
// {
|
||||
// containers: ["wav"],
|
||||
// audioCodecs: ["adpcm", "gsm", "aac", "mp3"],
|
||||
// protocols: ["http"],
|
||||
// maxAudioChannels: 2
|
||||
// },
|
||||
// {
|
||||
// containers: ["mkv"],
|
||||
// audioCodecs: ["aac", "mp3", "flac", "opus", "vorbis", "ac3", "eac3", "dts"],
|
||||
// protocols: ["http"],
|
||||
// maxAudioChannels: 8
|
||||
// },
|
||||
// {
|
||||
// containers: ["avi"],
|
||||
// audioCodecs: ["mp3", "ac3", "pcm", "aac"],
|
||||
// protocols: ["http"],
|
||||
// maxAudioChannels: 6
|
||||
// },
|
||||
// {
|
||||
// containers: ["asf", "wma"],
|
||||
// audioCodecs: ["wma", "pcm", "mp3"],
|
||||
// protocols: ["http"],
|
||||
// maxAudioChannels: 2
|
||||
// },
|
||||
// {
|
||||
// containers: ["caf"],
|
||||
// audioCodecs: ["pcm", "aac", "alac", "mp3"],
|
||||
// protocols: ["http"],
|
||||
// maxAudioChannels: 8
|
||||
// },
|
||||
// {
|
||||
// containers: ["3gp"],
|
||||
// audioCodecs: ["aac", "amr"],
|
||||
// protocols: ["http"],
|
||||
// maxAudioChannels: 2
|
||||
// },
|
||||
// {
|
||||
// containers: ["amr"],
|
||||
// audioCodecs: ["amr"],
|
||||
// protocols: ["http"],
|
||||
// maxAudioChannels: 1
|
||||
// },
|
||||
// {
|
||||
// containers: ["ape"],
|
||||
// audioCodecs: ["ape"],
|
||||
// protocols: ["http"],
|
||||
// maxAudioChannels: 2
|
||||
// },
|
||||
// {
|
||||
// containers: ["wv"],
|
||||
// audioCodecs: ["wavpack"],
|
||||
// protocols: ["http"],
|
||||
// maxAudioChannels: 2
|
||||
// },
|
||||
// {
|
||||
// containers: ["ac3"],
|
||||
// audioCodecs: ["ac3"],
|
||||
// protocols: ["http"],
|
||||
// maxAudioChannels: 6
|
||||
// },
|
||||
// {
|
||||
// containers: ["eac3"],
|
||||
// audioCodecs: ["eac3"],
|
||||
// protocols: ["http"],
|
||||
// maxAudioChannels: 8
|
||||
// },
|
||||
// {
|
||||
// containers: ["dts"],
|
||||
// audioCodecs: ["dts"],
|
||||
// protocols: ["http"],
|
||||
// maxAudioChannels: 8
|
||||
// }
|
||||
// ];
|
||||
|
||||
function appendTranscodeParams(url: string, format?: string, bitrate?: number) {
|
||||
let streamUrl = url;
|
||||
|
||||
if (format) {
|
||||
streamUrl += `&format=${format}`;
|
||||
}
|
||||
if (bitrate !== undefined) {
|
||||
streamUrl += `&maxBitRate=${bitrate}`;
|
||||
}
|
||||
|
||||
return streamUrl;
|
||||
}
|
||||
|
||||
function buildGetTranscodeStreamUrl(
|
||||
server: null | undefined | { credential?: string; url?: string },
|
||||
args: {
|
||||
mediaId: string;
|
||||
mediaType: 'podcast' | 'song';
|
||||
offset: number;
|
||||
transcodeParams: string;
|
||||
},
|
||||
): string {
|
||||
const params = new URLSearchParams({
|
||||
c: 'Feishin',
|
||||
mediaId: args.mediaId,
|
||||
mediaType: args.mediaType,
|
||||
offset: String(args.offset),
|
||||
transcodeParams: args.transcodeParams,
|
||||
v: '1.13.0',
|
||||
});
|
||||
|
||||
return `${server?.url}/rest/getTranscodeStream.view?${params.toString()}&${server?.credential}`;
|
||||
}
|
||||
|
||||
function sortAndPaginate<T>(
|
||||
items: T[],
|
||||
options: {
|
||||
@@ -337,7 +508,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
similarArtists:
|
||||
artistInfo?.similarArtist?.map((artist) => ({
|
||||
id: artist.id,
|
||||
imageId: null,
|
||||
imageId: artist.coverArt ?? artist.id,
|
||||
imageUrl: null,
|
||||
name: artist.name,
|
||||
userFavorite: Boolean(artist.starred) || false,
|
||||
@@ -1035,7 +1206,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
return ssNormalize.playlist(res.body.playlist, apiClientProps.server);
|
||||
},
|
||||
getPlaylistList: async ({ apiClientProps, query }) => {
|
||||
const sortOrder = query.sortOrder.toLowerCase() as 'asc' | 'desc';
|
||||
const sortOrder = (query.sortOrder || SortOrder.ASC).toLowerCase() as 'asc' | 'desc';
|
||||
|
||||
const res = await ssApiClient(apiClientProps).getPlaylists({});
|
||||
|
||||
@@ -1137,15 +1308,15 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
const res = await ssApiClient(apiClientProps).getPlayQueueByIndex();
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get random songs');
|
||||
throw new Error('Failed to get play queue');
|
||||
}
|
||||
|
||||
const { changed, changedBy, currentIndex, entry, position, username } =
|
||||
res.body.playQueueByIndex;
|
||||
res.body.playQueueByIndex || {}; // if there is no queue saved, playQueueByIndex may be undefined from a bug in Navidrome
|
||||
|
||||
return {
|
||||
changed,
|
||||
changedBy,
|
||||
changed: changed ?? '',
|
||||
changedBy: changedBy ?? '',
|
||||
currentIndex: currentIndex ?? 0,
|
||||
entry:
|
||||
entry?.map((song) =>
|
||||
@@ -1157,13 +1328,13 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
),
|
||||
) || [],
|
||||
positionMs: position ?? 0,
|
||||
username,
|
||||
username: username ?? '',
|
||||
};
|
||||
} else {
|
||||
const res = await ssApiClient(apiClientProps).getPlayQueue();
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get random songs');
|
||||
throw new Error('Failed to get play queue');
|
||||
}
|
||||
|
||||
const { changed, changedBy, current, entry, position, username } = res.body.playQueue;
|
||||
@@ -1273,6 +1444,10 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
}
|
||||
}
|
||||
|
||||
if (subsonicFeatures[SubsonicExtensions.TRANSCODING]) {
|
||||
features.osTranscodeDecision = [1];
|
||||
}
|
||||
|
||||
if (subsonicFeatures[SubsonicExtensions.SONG_LYRICS]) {
|
||||
features.lyricsMultipleStructured = [1];
|
||||
}
|
||||
@@ -1801,20 +1976,79 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
|
||||
return totalRecordCount;
|
||||
},
|
||||
getStreamUrl: ({ apiClientProps: { server }, query }) => {
|
||||
const { bitrate, format, id, transcode } = query;
|
||||
let url = `${server?.url}/rest/stream.view?id=${id}&v=1.13.0&c=Feishin&${server?.credential}`;
|
||||
getStreamUrl: async ({ apiClientProps, query }) => {
|
||||
const { server } = apiClientProps;
|
||||
const { bitrate, format, id, mediaType = 'song', skipAutoTranscode, transcode } = query;
|
||||
|
||||
const streamUrl = `${server?.url}/rest/stream.view?id=${id}&v=1.13.0&c=Feishin&${server?.credential}`;
|
||||
|
||||
// If transcoding is explicitly enabled, just return the direct transcoded stream URL
|
||||
if (transcode) {
|
||||
if (format) {
|
||||
url += `&format=${format}`;
|
||||
}
|
||||
if (bitrate !== undefined) {
|
||||
url += `&maxBitRate=${bitrate}`;
|
||||
}
|
||||
return appendTranscodeParams(streamUrl, format, bitrate);
|
||||
}
|
||||
|
||||
return url;
|
||||
// Used in cases where MPV is the default player, since mpv handles basically every audio format
|
||||
if (skipAutoTranscode) {
|
||||
return streamUrl;
|
||||
}
|
||||
|
||||
// If the server supports transcoding decision, always use it to determine if we need to transcode
|
||||
if (hasFeature(server, ServerFeature.OS_TRANSCODE_DECISION)) {
|
||||
const maxTranscodingAudioBitrate = 0;
|
||||
|
||||
const directPlayProfiles = getDirectPlayProfiles();
|
||||
const transcodingProfiles = getDefaultTranscodingProfiles();
|
||||
|
||||
const transcodeDecision = await ssApiClient(apiClientProps).getTranscodeDecision({
|
||||
body: {
|
||||
codecProfiles: [],
|
||||
directPlayProfiles,
|
||||
maxAudioBitrate: 0,
|
||||
maxTranscodingAudioBitrate,
|
||||
name: 'Feishin',
|
||||
platform: navigator.userAgent,
|
||||
transcodingProfiles,
|
||||
},
|
||||
query: {
|
||||
mediaId: id,
|
||||
mediaType,
|
||||
},
|
||||
});
|
||||
|
||||
// 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) {
|
||||
logFn.error(
|
||||
`Failed to get transcode decision for song ${id}, falling back to direct stream`,
|
||||
);
|
||||
return streamUrl;
|
||||
}
|
||||
|
||||
const td = transcodeDecision.body.transcodeDecision;
|
||||
const requiresTranscoding = !td?.canDirectPlay;
|
||||
|
||||
// If the server does not require transcoding, just return the direct stream URL
|
||||
if (!requiresTranscoding) {
|
||||
return streamUrl;
|
||||
}
|
||||
|
||||
logFn.info(`Song ${id} requires transcoding: ${[td.transcodeReason].join(', ')}`);
|
||||
|
||||
// If the server does not return transcode params, manually create the transcode params
|
||||
if (!td.transcodeParams) {
|
||||
return appendTranscodeParams(streamUrl, format, bitrate);
|
||||
}
|
||||
|
||||
const transcodeStreamUrl = buildGetTranscodeStreamUrl(server, {
|
||||
mediaId: String(id),
|
||||
mediaType: (mediaType ?? 'song') as 'podcast' | 'song',
|
||||
offset: 0,
|
||||
transcodeParams: td.transcodeParams,
|
||||
});
|
||||
|
||||
return transcodeStreamUrl;
|
||||
}
|
||||
|
||||
return streamUrl;
|
||||
},
|
||||
getStructuredLyrics: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
@@ -1891,6 +2125,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
|
||||
const res = await SubsonicController.getSongList({
|
||||
apiClientProps,
|
||||
context,
|
||||
query: {
|
||||
artistIds: [query.artistId],
|
||||
sortBy: SongListSort.PLAY_COUNT,
|
||||
@@ -2118,6 +2353,22 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
),
|
||||
};
|
||||
},
|
||||
setPlaylistSongs: async (args) => {
|
||||
const { apiClientProps, body } = args;
|
||||
|
||||
const res = await ssApiClient(apiClientProps).createPlaylist({
|
||||
query: {
|
||||
playlistId: body.id,
|
||||
songId: body.songIds,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to update playlist songs');
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
setRating: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
|
||||
+104
-63
@@ -7,12 +7,12 @@ import '@mantine/core/styles.css';
|
||||
import '@mantine/dates/styles.css';
|
||||
import '@mantine/notifications/styles.css';
|
||||
import isElectron from 'is-electron';
|
||||
import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { lazy, memo, Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { openSettingsModal } from '/@/renderer/features/settings/utils/open-settings-modal';
|
||||
import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context';
|
||||
import { useCheckForUpdates } from '/@/renderer/hooks/use-check-for-updates';
|
||||
import { useNativeMenuSync } from '/@/renderer/hooks/use-native-menu-sync';
|
||||
import { useSyncSettingsToMain } from '/@/renderer/hooks/use-sync-settings-to-main';
|
||||
import { AppRouter } from '/@/renderer/router/app-router';
|
||||
import { useCssSettings, useHotkeySettings, useLanguage } from '/@/renderer/store';
|
||||
@@ -22,12 +22,7 @@ import { WebAudio } from '/@/shared/types/types';
|
||||
import '/@/shared/styles/global.css';
|
||||
import { PlayerProvider } from '/@/renderer/features/player/context/player-context';
|
||||
import { AudioPlayers } from '/@/renderer/features/player/components/audio-players';
|
||||
|
||||
const ReleaseNotesModal = lazy(() =>
|
||||
import('./release-notes-modal').then((module) => ({
|
||||
default: module.ReleaseNotesModal,
|
||||
})),
|
||||
);
|
||||
import { ReleaseNotesModal } from '/@/renderer/release-notes-modal';
|
||||
|
||||
const UpdateAvailableDialog = lazy(() =>
|
||||
import('./update-available-dialog').then((module) => ({
|
||||
@@ -38,67 +33,26 @@ const UpdateAvailableDialog = lazy(() =>
|
||||
const ipc = isElectron() ? window.api.ipc : null;
|
||||
|
||||
export const App = () => {
|
||||
return <ThemedApp />;
|
||||
};
|
||||
|
||||
const ThemedApp = () => {
|
||||
const { mode, theme } = useAppTheme();
|
||||
const language = useLanguage();
|
||||
|
||||
const { content, enabled } = useCssSettings();
|
||||
const { bindings } = useHotkeySettings();
|
||||
const cssRef = useRef<HTMLStyleElement | null>(null);
|
||||
|
||||
useSyncSettingsToMain();
|
||||
useCheckForUpdates();
|
||||
return (
|
||||
<MantineProvider forceColorScheme={mode} theme={theme}>
|
||||
<AppShell />
|
||||
</MantineProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const AppShell = memo(function AppShell() {
|
||||
const [webAudio, setWebAudio] = useState<WebAudio>();
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled && content) {
|
||||
// Yes, CSS is sanitized here as well. Prevent a suer from changing the
|
||||
// localStorage to bypass sanitizing.
|
||||
const sanitized = sanitizeCss(content);
|
||||
if (!cssRef.current) {
|
||||
cssRef.current = document.createElement('style');
|
||||
document.body.appendChild(cssRef.current);
|
||||
}
|
||||
|
||||
cssRef.current.textContent = sanitized;
|
||||
|
||||
return () => {
|
||||
cssRef.current!.textContent = '';
|
||||
};
|
||||
}
|
||||
|
||||
return () => {};
|
||||
}, [content, enabled]);
|
||||
|
||||
const webAudioProvider = useMemo(() => {
|
||||
return { setWebAudio, webAudio };
|
||||
}, [webAudio]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isElectron()) {
|
||||
ipc?.send('set-global-shortcuts', bindings);
|
||||
}
|
||||
}, [bindings]);
|
||||
|
||||
useEffect(() => {
|
||||
if (language) {
|
||||
i18n.changeLanguage(language);
|
||||
}
|
||||
}, [language]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isElectron()) {
|
||||
window.api.utils.rendererOpenSettings(() => {
|
||||
openSettingsModal();
|
||||
});
|
||||
|
||||
return () => {
|
||||
ipc?.removeAllListeners('renderer-open-settings');
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}, []);
|
||||
|
||||
const notificationStyles = useMemo(
|
||||
() => ({
|
||||
root: {
|
||||
@@ -109,7 +63,8 @@ export const App = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<MantineProvider forceColorScheme={mode} theme={theme}>
|
||||
<>
|
||||
<AppEffects />
|
||||
<Notifications
|
||||
containerWidth="300px"
|
||||
position="bottom-center"
|
||||
@@ -122,10 +77,96 @@ export const App = () => {
|
||||
<AppRouter />
|
||||
</PlayerProvider>
|
||||
</WebAudioContext.Provider>
|
||||
<ReleaseNotesModal />
|
||||
<Suspense fallback={null}>
|
||||
<ReleaseNotesModal />
|
||||
<UpdateAvailableDialog />
|
||||
</Suspense>
|
||||
</MantineProvider>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const AppEffects = () => (
|
||||
<>
|
||||
<SyncSettingsEffect />
|
||||
<UpdateCheckEffect />
|
||||
<CssSettingsEffect />
|
||||
<GlobalShortcutsEffect />
|
||||
<LanguageEffect />
|
||||
<NativeMenuSyncEffect />
|
||||
</>
|
||||
);
|
||||
|
||||
const SyncSettingsEffect = () => {
|
||||
useSyncSettingsToMain();
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const UpdateCheckEffect = () => {
|
||||
useCheckForUpdates();
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const CssSettingsEffect = () => {
|
||||
const { content, enabled } = useCssSettings();
|
||||
const cssRef = useRef<HTMLStyleElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled || !content) {
|
||||
if (cssRef.current) {
|
||||
cssRef.current.textContent = '';
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Yes, CSS is sanitized here as well. Prevent a user from changing the
|
||||
// localStorage to bypass sanitizing.
|
||||
const sanitized = sanitizeCss(content);
|
||||
if (!cssRef.current) {
|
||||
cssRef.current = document.createElement('style');
|
||||
document.body.appendChild(cssRef.current);
|
||||
}
|
||||
|
||||
cssRef.current.textContent = sanitized;
|
||||
|
||||
return () => {
|
||||
if (cssRef.current) {
|
||||
cssRef.current.textContent = '';
|
||||
}
|
||||
};
|
||||
}, [content, enabled]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const GlobalShortcutsEffect = () => {
|
||||
const { bindings } = useHotkeySettings();
|
||||
|
||||
useEffect(() => {
|
||||
if (isElectron()) {
|
||||
ipc?.send('set-global-shortcuts', bindings);
|
||||
}
|
||||
}, [bindings]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const LanguageEffect = () => {
|
||||
const language = useLanguage();
|
||||
|
||||
useEffect(() => {
|
||||
if (language) {
|
||||
i18n.changeLanguage(language);
|
||||
}
|
||||
}, [language]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const NativeMenuSyncEffect = () => {
|
||||
useNativeMenuSync();
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -67,10 +67,19 @@
|
||||
padding: var(--theme-spacing-md);
|
||||
}
|
||||
|
||||
.single-carousel-container .carousel {
|
||||
min-height: 240px;
|
||||
}
|
||||
|
||||
.single-carousel-container .carousel-item {
|
||||
min-height: 240px;
|
||||
}
|
||||
|
||||
.single-carousel-container .carousel-item .content {
|
||||
flex-direction: row;
|
||||
gap: var(--theme-spacing-lg);
|
||||
gap: var(--theme-spacing-md);
|
||||
align-items: flex-end;
|
||||
min-height: 240px;
|
||||
padding: var(--theme-spacing-xl);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,11 +36,22 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.grid-carousel-viewport {
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--cards-to-show, 2), minmax(0, 1fr));
|
||||
gap: var(--theme-spacing-md);
|
||||
contain: layout paint;
|
||||
overflow: hidden;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.card {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.page-indicator {
|
||||
|
||||
@@ -22,9 +22,9 @@ import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { useShowRatings } from '/@/renderer/store';
|
||||
import {
|
||||
formatDateAbsolute,
|
||||
formatDateAbsoluteUTC,
|
||||
formatDateRelative,
|
||||
formatDurationString,
|
||||
formatPartialIsoDateUTC,
|
||||
formatRating,
|
||||
} from '/@/renderer/utils/format';
|
||||
import { SEPARATOR_STRING } from '/@/shared/api/utils';
|
||||
@@ -64,6 +64,7 @@ export interface ItemCardProps {
|
||||
enableMultiSelect?: boolean;
|
||||
enableNavigation?: boolean;
|
||||
imageAsLink?: boolean;
|
||||
imageFetchPriority?: 'auto' | 'high' | 'low';
|
||||
internalState?: ItemListStateActions;
|
||||
isRound?: boolean;
|
||||
itemType: LibraryItem;
|
||||
@@ -80,6 +81,7 @@ export const ItemCard = ({
|
||||
enableMultiSelect,
|
||||
enableNavigation = true,
|
||||
imageAsLink,
|
||||
imageFetchPriority,
|
||||
internalState,
|
||||
isRound,
|
||||
itemType,
|
||||
@@ -102,6 +104,7 @@ export const ItemCard = ({
|
||||
enableMultiSelect={enableMultiSelect}
|
||||
enableNavigation={enableNavigation}
|
||||
imageAsLink={imageAsLink}
|
||||
imageFetchPriority={imageFetchPriority}
|
||||
imageUrl={imageUrl}
|
||||
internalState={internalState}
|
||||
isRound={isRound}
|
||||
@@ -121,6 +124,7 @@ export const ItemCard = ({
|
||||
enableMultiSelect={enableMultiSelect}
|
||||
enableNavigation={enableNavigation}
|
||||
imageAsLink={imageAsLink}
|
||||
imageFetchPriority={imageFetchPriority}
|
||||
imageUrl={imageUrl}
|
||||
internalState={internalState}
|
||||
isRound={isRound}
|
||||
@@ -140,6 +144,7 @@ export const ItemCard = ({
|
||||
enableExpansion={enableExpansion}
|
||||
enableNavigation={enableNavigation}
|
||||
imageAsLink={imageAsLink}
|
||||
imageFetchPriority={imageFetchPriority}
|
||||
imageUrl={imageUrl}
|
||||
internalState={internalState}
|
||||
isRound={isRound}
|
||||
@@ -157,12 +162,299 @@ export interface ItemCardDerivativeProps extends Omit<ItemCardProps, 'type'> {
|
||||
enableExpansion?: boolean;
|
||||
enableNavigation?: boolean;
|
||||
imageAsLink?: boolean;
|
||||
imageFetchPriority?: 'auto' | 'high' | 'low';
|
||||
imageUrl: string | undefined;
|
||||
internalState?: ItemListStateActions;
|
||||
rows: DataRow[];
|
||||
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 = ({
|
||||
controls,
|
||||
data,
|
||||
@@ -171,6 +463,7 @@ const CompactItemCard = ({
|
||||
enableMultiSelect,
|
||||
enableNavigation,
|
||||
imageAsLink,
|
||||
imageFetchPriority,
|
||||
internalState,
|
||||
isRound,
|
||||
itemType,
|
||||
@@ -178,7 +471,6 @@ const CompactItemCard = ({
|
||||
showRating,
|
||||
withControls,
|
||||
}: ItemCardDerivativeProps) => {
|
||||
const [showControls, setShowControls] = useState(false);
|
||||
const itemRowId =
|
||||
data && internalState && typeof data === 'object' && 'id' in data
|
||||
? internalState.extractRowId(data)
|
||||
@@ -290,18 +582,6 @@ const CompactItemCard = ({
|
||||
if (data) {
|
||||
const navigationPath = getItemNavigationPath(data, itemType);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (withControls) {
|
||||
setShowControls(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (withControls) {
|
||||
setShowControls(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleContextMenu = (e: React.MouseEvent<HTMLElement>) => {
|
||||
if (!data || !controls) {
|
||||
return;
|
||||
@@ -331,80 +611,6 @@ const CompactItemCard = ({
|
||||
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
|
||||
}
|
||||
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 (
|
||||
<div
|
||||
className={clsx(styles.container, styles.compact, {
|
||||
@@ -413,31 +619,24 @@ const CompactItemCard = ({
|
||||
})}
|
||||
ref={ref}
|
||||
>
|
||||
{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
|
||||
controls={controls}
|
||||
data={data}
|
||||
enableExpansion={enableExpansion}
|
||||
enableNavigation={enableNavigation}
|
||||
handleContextMenu={handleContextMenu}
|
||||
handleImageClick={handleImageClick}
|
||||
handleLinkDragStart={handleLinkDragStart}
|
||||
imageAsLink={imageAsLink}
|
||||
imageFetchPriority={imageFetchPriority}
|
||||
internalState={internalState}
|
||||
isRound={isRound}
|
||||
itemType={itemType}
|
||||
navigationPath={navigationPath}
|
||||
rows={rows}
|
||||
showRating={showRating}
|
||||
withControls={withControls}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -475,6 +674,7 @@ const DefaultItemCard = ({
|
||||
enableExpansion,
|
||||
enableNavigation,
|
||||
imageAsLink,
|
||||
imageFetchPriority,
|
||||
internalState,
|
||||
isRound,
|
||||
itemType,
|
||||
@@ -482,7 +682,6 @@ const DefaultItemCard = ({
|
||||
showRating,
|
||||
withControls,
|
||||
}: ItemCardDerivativeProps) => {
|
||||
const [showControls, setShowControls] = useState(false);
|
||||
const itemRowId =
|
||||
data && internalState && typeof data === 'object' && 'id' in data
|
||||
? internalState.extractRowId(data)
|
||||
@@ -529,18 +728,6 @@ const DefaultItemCard = ({
|
||||
if (data) {
|
||||
const navigationPath = getItemNavigationPath(data, itemType);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (withControls) {
|
||||
setShowControls(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (withControls) {
|
||||
setShowControls(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleContextMenu = (e: React.MouseEvent<HTMLElement>) => {
|
||||
if (!data || !controls) {
|
||||
return;
|
||||
@@ -570,92 +757,30 @@ const DefaultItemCard = ({
|
||||
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
|
||||
}
|
||||
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 (
|
||||
<div
|
||||
className={clsx(styles.container, {
|
||||
[styles.selected]: isSelected,
|
||||
})}
|
||||
>
|
||||
{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
|
||||
controls={controls}
|
||||
data={data}
|
||||
enableExpansion={enableExpansion}
|
||||
enableNavigation={enableNavigation}
|
||||
handleContextMenu={handleContextMenu}
|
||||
handleImageClick={handleImageClick}
|
||||
handleLinkDragStart={handleLinkDragStart}
|
||||
imageAsLink={imageAsLink}
|
||||
imageFetchPriority={imageFetchPriority}
|
||||
internalState={internalState}
|
||||
isRound={isRound}
|
||||
itemType={itemType}
|
||||
navigationPath={navigationPath}
|
||||
showRating={showRating}
|
||||
variant="default"
|
||||
withControls={withControls}
|
||||
/>
|
||||
<div className={styles.detailContainer}>
|
||||
{rows
|
||||
.filter(
|
||||
@@ -710,6 +835,7 @@ const PosterItemCard = ({
|
||||
enableMultiSelect,
|
||||
enableNavigation,
|
||||
imageAsLink,
|
||||
imageFetchPriority,
|
||||
internalState,
|
||||
isRound,
|
||||
itemType,
|
||||
@@ -717,7 +843,6 @@ const PosterItemCard = ({
|
||||
showRating,
|
||||
withControls,
|
||||
}: ItemCardDerivativeProps) => {
|
||||
const [showControls, setShowControls] = useState(false);
|
||||
const itemRowId =
|
||||
data && internalState && typeof data === 'object' && 'id' in data
|
||||
? internalState.extractRowId(data)
|
||||
@@ -829,18 +954,6 @@ const PosterItemCard = ({
|
||||
if (data) {
|
||||
const navigationPath = getItemNavigationPath(data, itemType);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (withControls) {
|
||||
setShowControls(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (withControls) {
|
||||
setShowControls(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleContextMenu = (e: React.MouseEvent<HTMLElement>) => {
|
||||
if (!data || !controls) {
|
||||
return;
|
||||
@@ -870,62 +983,6 @@ const PosterItemCard = ({
|
||||
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
|
||||
}
|
||||
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 (
|
||||
<div
|
||||
className={clsx(styles.container, styles.poster, {
|
||||
@@ -934,31 +991,24 @@ const PosterItemCard = ({
|
||||
})}
|
||||
ref={ref}
|
||||
>
|
||||
{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
|
||||
controls={controls}
|
||||
data={data}
|
||||
enableExpansion={enableExpansion}
|
||||
enableNavigation={enableNavigation}
|
||||
handleContextMenu={handleContextMenu}
|
||||
handleImageClick={handleImageClick}
|
||||
handleLinkDragStart={handleLinkDragStart}
|
||||
imageAsLink={imageAsLink}
|
||||
imageFetchPriority={imageFetchPriority}
|
||||
internalState={internalState}
|
||||
isRound={isRound}
|
||||
itemType={itemType}
|
||||
navigationPath={navigationPath}
|
||||
showRating={showRating}
|
||||
variant="poster"
|
||||
withControls={withControls}
|
||||
/>
|
||||
{data && (
|
||||
<div className={styles.detailContainer}>
|
||||
{rows
|
||||
@@ -1149,12 +1199,10 @@ export const getDataRows = (type?: 'compact' | 'default' | 'poster'): DataRow[]
|
||||
},
|
||||
{
|
||||
format: (data) => {
|
||||
if ('releaseYear' in data && data.releaseYear !== null) {
|
||||
if ('releaseYear' in data && data.releaseYear != null) {
|
||||
const releaseYear = data.releaseYear;
|
||||
const originalYear =
|
||||
'originalYear' in data && data.originalYear !== null
|
||||
? data.originalYear
|
||||
: null;
|
||||
'originalYear' in data && data.originalYear > 0 ? data.originalYear : null;
|
||||
|
||||
if (originalYear !== null && originalYear !== releaseYear) {
|
||||
return `${originalYear}${SEPARATOR_STRING}${releaseYear}`;
|
||||
@@ -1174,10 +1222,10 @@ export const getDataRows = (type?: 'compact' | 'default' | 'poster'): DataRow[]
|
||||
data.originalDate &&
|
||||
data.originalDate !== data.releaseDate
|
||||
) {
|
||||
return `${formatDateAbsoluteUTC(data.originalDate)}${SEPARATOR_STRING}${formatDateAbsoluteUTC(data.releaseDate)}`;
|
||||
return `${formatPartialIsoDateUTC(data.originalDate)}${SEPARATOR_STRING}${formatPartialIsoDateUTC(data.releaseDate)}`;
|
||||
}
|
||||
|
||||
return `${formatDateAbsoluteUTC(data.releaseDate)}`;
|
||||
return `${formatPartialIsoDateUTC(data.releaseDate)}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ItemTableListColumnConfig } from '/@/renderer/components/item-list/types';
|
||||
import { TableColumn } from '/@/shared/types/types';
|
||||
|
||||
const LAYOUT_FILL_COLUMN: ItemTableListColumnConfig = {
|
||||
align: 'start',
|
||||
autoSize: true,
|
||||
id: TableColumn.LAYOUT_FILL,
|
||||
isEnabled: true,
|
||||
pinned: null,
|
||||
width: 0,
|
||||
};
|
||||
|
||||
export const appendLayoutFillColumn = (
|
||||
columns: ItemTableListColumnConfig[],
|
||||
autoFitColumns: boolean,
|
||||
): ItemTableListColumnConfig[] => {
|
||||
if (autoFitColumns || columns.length === 0) {
|
||||
return columns;
|
||||
}
|
||||
|
||||
const unpinnedEnabled = columns.filter((c) => c.pinned === null && c.isEnabled !== false);
|
||||
if (unpinnedEnabled.length === 0) {
|
||||
return columns;
|
||||
}
|
||||
if (unpinnedEnabled.some((c) => c.autoSize === true)) {
|
||||
return columns;
|
||||
}
|
||||
|
||||
return [...columns, LAYOUT_FILL_COLUMN];
|
||||
};
|
||||
@@ -40,6 +40,13 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
|
||||
const setFavorite = useSetFavorite();
|
||||
const setRating = useSetRating();
|
||||
|
||||
const playerRef = useRef(player);
|
||||
const setFavoriteRef = useRef(setFavorite);
|
||||
const setRatingRef = useRef(setRating);
|
||||
playerRef.current = player;
|
||||
setFavoriteRef.current = setFavorite;
|
||||
setRatingRef.current = setRating;
|
||||
|
||||
useEffect(() => {
|
||||
navigateRef.current = navigate;
|
||||
}, [navigate]);
|
||||
@@ -266,14 +273,14 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
|
||||
return;
|
||||
}
|
||||
|
||||
player.addToQueueByData(songsToAdd, playType, item.id);
|
||||
playerRef.current.addToQueueByData(songsToAdd, playType, item.id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (itemType === LibraryItem.QUEUE_SONG) {
|
||||
const queueSong = item as QueueSong;
|
||||
if (queueSong._uniqueId) {
|
||||
player.mediaPlay(queueSong._uniqueId);
|
||||
playerRef.current.mediaPlay(queueSong._uniqueId);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -316,7 +323,7 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
|
||||
return;
|
||||
}
|
||||
|
||||
setFavorite(item._serverId, [item.id], apiItemType, favorite);
|
||||
setFavoriteRef.current(item._serverId, [item.id], apiItemType, favorite);
|
||||
},
|
||||
|
||||
onMore: ({ event, internalState, item, itemType }: DefaultItemControlProps) => {
|
||||
@@ -394,7 +401,7 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
|
||||
return;
|
||||
}
|
||||
|
||||
player.addToQueueByFetch(item._serverId, [item.id], itemType, playType);
|
||||
playerRef.current.addToQueueByFetch(item._serverId, [item.id], itemType, playType);
|
||||
},
|
||||
|
||||
onRating: ({
|
||||
@@ -420,20 +427,12 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
|
||||
newRating = 0;
|
||||
}
|
||||
|
||||
setRating(item._serverId, [item.id], apiItemType, newRating);
|
||||
setRatingRef.current(item._serverId, [item.id], apiItemType, newRating);
|
||||
},
|
||||
|
||||
...overrides,
|
||||
};
|
||||
}, [
|
||||
enableMultiSelect,
|
||||
overrides,
|
||||
onColumnReordered,
|
||||
onColumnResized,
|
||||
player,
|
||||
setFavorite,
|
||||
setRating,
|
||||
]);
|
||||
}, [enableMultiSelect, overrides, onColumnReordered, onColumnResized]);
|
||||
|
||||
return controls;
|
||||
};
|
||||
|
||||
@@ -349,9 +349,12 @@ export const useItemListInfiniteLoader = ({
|
||||
mutationKey: getListRefreshMutationKey(eventKey),
|
||||
});
|
||||
|
||||
const refreshMutationRef = useRef(refreshMutation);
|
||||
refreshMutationRef.current = refreshMutation;
|
||||
|
||||
const refresh = useCallback(
|
||||
async (force?: boolean) => refreshMutation.mutateAsync(force),
|
||||
[refreshMutation],
|
||||
async (force?: boolean) => refreshMutationRef.current.mutateAsync(force),
|
||||
[],
|
||||
);
|
||||
|
||||
const updateItems = useCallback(
|
||||
@@ -383,7 +386,7 @@ export const useItemListInfiniteLoader = ({
|
||||
return;
|
||||
}
|
||||
|
||||
refreshMutation.mutate(true);
|
||||
refreshMutationRef.current.mutate(true);
|
||||
};
|
||||
|
||||
eventEmitter.on('ITEM_LIST_REFRESH', handleRefresh);
|
||||
@@ -391,7 +394,7 @@ export const useItemListInfiniteLoader = ({
|
||||
return () => {
|
||||
eventEmitter.off('ITEM_LIST_REFRESH', handleRefresh);
|
||||
};
|
||||
}, [eventKey, refreshMutation]);
|
||||
}, [eventKey]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleFavorite = (payload: UserFavoriteEventPayload) => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
useSuspenseQuery,
|
||||
UseSuspenseQueryOptions,
|
||||
} from '@tanstack/react-query';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { useListContext } from '/@/renderer/context/list-context';
|
||||
@@ -115,6 +115,9 @@ export const useItemListPaginatedLoader = ({
|
||||
mutationKey: getListRefreshMutationKey(eventKey ?? 'paginated'),
|
||||
});
|
||||
|
||||
const refreshMutationRef = useRef(refreshMutation);
|
||||
refreshMutationRef.current = refreshMutation;
|
||||
|
||||
const updateItems = useCallback(
|
||||
(indexes: number[], value: object) => {
|
||||
return queryClient.setQueryData(
|
||||
@@ -153,7 +156,7 @@ export const useItemListPaginatedLoader = ({
|
||||
return;
|
||||
}
|
||||
|
||||
refreshMutation.mutate(true);
|
||||
refreshMutationRef.current.mutate(true);
|
||||
};
|
||||
|
||||
const handleFavorite = (payload: UserFavoriteEventPayload) => {
|
||||
@@ -220,7 +223,7 @@ export const useItemListPaginatedLoader = ({
|
||||
eventEmitter.off('USER_FAVORITE', handleFavorite);
|
||||
eventEmitter.off('USER_RATING', handleRating);
|
||||
};
|
||||
}, [data, eventKey, itemType, refreshMutation, serverId, updateItems]);
|
||||
}, [data, eventKey, itemType, serverId, updateItems]);
|
||||
|
||||
return { data: data?.items || [], pageCount, totalItemCount };
|
||||
};
|
||||
|
||||
@@ -67,6 +67,7 @@ const getRowIdFromTableColumn = (tableColumn: TableColumn): null | string => {
|
||||
[TableColumn.ID]: null,
|
||||
[TableColumn.IMAGE]: null,
|
||||
[TableColumn.LAST_PLAYED]: 'lastPlayedAt',
|
||||
[TableColumn.LAYOUT_FILL]: null,
|
||||
[TableColumn.OWNER]: null,
|
||||
[TableColumn.PATH]: null,
|
||||
[TableColumn.PLAY_COUNT]: 'playCount',
|
||||
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
ItemListStateItemWithRequiredProperties,
|
||||
} from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||
import { ItemControls } from '/@/renderer/components/item-list/types';
|
||||
import { useHotkeys } from '/@/renderer/hooks/use-hotkeys';
|
||||
import { useHotkeySettings, usePlayButtonBehavior } from '/@/renderer/store';
|
||||
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { Play } from '/@/shared/types/types';
|
||||
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
import { formatDateAbsoluteUTC } from '/@/renderer/utils/format';
|
||||
import { formatPartialIsoDateUTC } from '/@/renderer/utils/format';
|
||||
import { SEPARATOR_STRING } from '/@/shared/api/utils';
|
||||
|
||||
export const ReleaseDateColumn = ({ song }: ItemDetailListCellProps) =>
|
||||
song.releaseDate ? formatDateAbsoluteUTC(song.releaseDate) : <> </>;
|
||||
export const ReleaseDateColumn = ({ song }: ItemDetailListCellProps) => {
|
||||
const row = song as typeof song & { originalDate?: null | string };
|
||||
const releaseDate = row.releaseDate;
|
||||
if (!releaseDate) {
|
||||
return <> </>;
|
||||
}
|
||||
|
||||
const originalDate =
|
||||
row.originalDate && row.originalDate !== releaseDate ? row.originalDate : null;
|
||||
|
||||
if (originalDate) {
|
||||
return `${formatPartialIsoDateUTC(originalDate)}${SEPARATOR_STRING}${formatPartialIsoDateUTC(releaseDate)}`;
|
||||
}
|
||||
|
||||
return formatPartialIsoDateUTC(releaseDate);
|
||||
};
|
||||
|
||||
@@ -179,6 +179,14 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.resize-handle.resize-handle-disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.track-header-cell:hover .resize-handle.resize-handle-disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.resize-handle:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
|
||||
import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { useSettingsStore, useShowRatings } from '/@/renderer/store';
|
||||
import { formatDateAbsoluteUTC, formatDurationString } from '/@/renderer/utils';
|
||||
import { formatDurationString, formatPartialIsoDateUTC } from '/@/renderer/utils';
|
||||
import { SEPARATOR_STRING } from '/@/shared/api/utils';
|
||||
import { ExplicitIndicator } from '/@/shared/components/explicit-indicator/explicit-indicator';
|
||||
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
||||
@@ -489,9 +489,9 @@ const MetadataSection = memo(
|
||||
let releaseStr = '';
|
||||
if (item.releaseDate) {
|
||||
if (item.originalDate && item.originalDate !== item.releaseDate) {
|
||||
releaseStr = `${formatDateAbsoluteUTC(item.originalDate)}${SEPARATOR_STRING}${formatDateAbsoluteUTC(item.releaseDate)}`;
|
||||
releaseStr = `${formatPartialIsoDateUTC(item.originalDate)}${SEPARATOR_STRING}${formatPartialIsoDateUTC(item.releaseDate)}`;
|
||||
} else {
|
||||
releaseStr = formatDateAbsoluteUTC(item.releaseDate);
|
||||
releaseStr = formatPartialIsoDateUTC(item.releaseDate);
|
||||
}
|
||||
} else if (item.releaseYear != null) {
|
||||
releaseStr = String(item.releaseYear);
|
||||
@@ -911,8 +911,7 @@ const DetailListHeaderCell = memo(
|
||||
const percent = col ? (columnWidthPercents[colIndex] ?? 0) : 0;
|
||||
const { fixedWidth, isFixedColumn } = getTrackColumnFixed(columnId);
|
||||
const currentWidth = col?.width ?? (fixedWidth || 100);
|
||||
const showResizeHandle =
|
||||
enableColumnResize && !isFixedColumn && !col?.autoSize && onColumnResized;
|
||||
const showResizeHandle = enableColumnResize && !isFixedColumn;
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || !onColumnReordered) {
|
||||
@@ -1026,6 +1025,7 @@ const DetailListHeaderCell = memo(
|
||||
{showResizeHandle && (
|
||||
<DetailListColumnResizeHandle
|
||||
columnId={columnId}
|
||||
disabled={!!col?.autoSize}
|
||||
initialWidth={currentWidth}
|
||||
onResize={handleResize}
|
||||
side="right"
|
||||
@@ -1040,6 +1040,7 @@ DetailListHeaderCell.displayName = 'DetailListHeaderCell';
|
||||
|
||||
interface DetailListColumnResizeHandleProps {
|
||||
columnId: TableColumn;
|
||||
disabled?: boolean;
|
||||
initialWidth: number;
|
||||
onResize: (columnId: TableColumn, width: number) => void;
|
||||
side: 'left' | 'right';
|
||||
@@ -1047,6 +1048,7 @@ interface DetailListColumnResizeHandleProps {
|
||||
|
||||
const DetailListColumnResizeHandle = ({
|
||||
columnId,
|
||||
disabled = false,
|
||||
initialWidth,
|
||||
onResize,
|
||||
side,
|
||||
@@ -1091,6 +1093,11 @@ const DetailListColumnResizeHandle = ({
|
||||
}, [isDragging, columnId, onResize]);
|
||||
|
||||
const handleMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (disabled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setIsDragging(true);
|
||||
@@ -1103,6 +1110,7 @@ const DetailListColumnResizeHandle = ({
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.resizeHandle, {
|
||||
[styles.resizeHandleDisabled]: disabled,
|
||||
[styles.resizeHandleDragging]: isDragging,
|
||||
[styles.resizeHandleLeft]: side === 'left',
|
||||
[styles.resizeHandleRight]: side === 'right',
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
flex-direction: column !important;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-block: var(--theme-spacing-xs);
|
||||
padding-right: var(--theme-spacing-md);
|
||||
outline: none;
|
||||
border: none;
|
||||
|
||||
@@ -385,8 +385,8 @@ const BaseItemGridList = ({
|
||||
rows,
|
||||
size = 'default',
|
||||
}: ItemGridListProps) => {
|
||||
const rootRef = useRef(null);
|
||||
const outerRef = useRef(null);
|
||||
const rootRef = useRef<HTMLDivElement | null>(null);
|
||||
const outerRef = useRef<HTMLDivElement | null>(null);
|
||||
const listRef = useRef<FixedSizeList<GridItemProps>>(null);
|
||||
const { ref: containerRef, width: containerWidth } = useElementSize();
|
||||
const { focused, ref: containerFocusRef } = useFocusWithin();
|
||||
@@ -486,7 +486,7 @@ const BaseItemGridList = ({
|
||||
}, [itemsPerRow, rows?.length, size]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const { current: container } = containerRef;
|
||||
const container = rootRef.current;
|
||||
if (!container) return;
|
||||
|
||||
throttledSetTableMeta(containerWidth, resolvedItemCount, (meta) => {
|
||||
@@ -500,13 +500,15 @@ const BaseItemGridList = ({
|
||||
current.rowCount !== meta.rowCount
|
||||
) {
|
||||
tableMetaRef.current = meta;
|
||||
container.style.setProperty('--grid-column-count', String(meta.columnCount));
|
||||
container.style.setProperty('--grid-item-height', `${meta.itemHeight}px`);
|
||||
container.style.setProperty('--grid-row-count', String(meta.rowCount));
|
||||
const el = rootRef.current;
|
||||
if (!el) return;
|
||||
el.style.setProperty('--grid-column-count', String(meta.columnCount));
|
||||
el.style.setProperty('--grid-item-height', `${meta.itemHeight}px`);
|
||||
el.style.setProperty('--grid-row-count', String(meta.rowCount));
|
||||
setTableMetaVersion((v) => v + 1);
|
||||
}
|
||||
});
|
||||
}, [containerWidth, resolvedItemCount, throttledSetTableMeta, containerRef]);
|
||||
}, [containerWidth, resolvedItemCount, throttledSetTableMeta]);
|
||||
|
||||
const controls = useDefaultItemListControls({
|
||||
enableMultiSelect,
|
||||
|
||||
@@ -20,7 +20,8 @@ export const createColumnCellComponent = (
|
||||
prevProps.columnIndex === nextProps.columnIndex &&
|
||||
prevProps.data === nextProps.data &&
|
||||
prevProps.style === nextProps.style &&
|
||||
prevProps.columns === nextProps.columns
|
||||
prevProps.columns === nextProps.columns &&
|
||||
prevProps.playlistId === nextProps.playlistId
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -8,49 +8,25 @@ import {
|
||||
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
||||
import {
|
||||
formatDateAbsolute,
|
||||
formatDateAbsoluteUTC,
|
||||
formatDateRelative,
|
||||
formatHrDateTime,
|
||||
formatPartialIsoDateUTC,
|
||||
} from '/@/renderer/utils/format';
|
||||
import { SEPARATOR_STRING } from '/@/shared/api/utils';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
|
||||
import { TableColumn } from '/@/shared/types/types';
|
||||
|
||||
const getDateTooltipLabel = (utcString: string) => {
|
||||
return (
|
||||
<Stack gap="xs" justify="center">
|
||||
<Text size="md" ta="center">
|
||||
{formatHrDateTime(utcString)}
|
||||
</Text>
|
||||
<Text isMuted size="sm" ta="center">
|
||||
{utcString}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const DateColumnBase = (props: ItemTableListInnerColumn) => {
|
||||
const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex];
|
||||
const row: string | undefined = (rowItem as any)?.[props.columns[props.columnIndex].id];
|
||||
|
||||
const { formattedDate, tooltipLabel } = useMemo(() => {
|
||||
if (typeof row === 'string' && row) {
|
||||
return {
|
||||
formattedDate: formatDateAbsolute(row),
|
||||
tooltipLabel: getDateTooltipLabel(row),
|
||||
};
|
||||
}
|
||||
return { formattedDate: null, tooltipLabel: null };
|
||||
}, [row]);
|
||||
const formattedAbsolute = useMemo(
|
||||
() => (typeof row === 'string' && row ? formatDateAbsolute(row) : null),
|
||||
[row],
|
||||
);
|
||||
|
||||
if (typeof row === 'string' && row) {
|
||||
if (formattedAbsolute) {
|
||||
return (
|
||||
<TableColumnTextContainer {...props}>
|
||||
<Tooltip label={tooltipLabel} multiline={false}>
|
||||
<span>{formattedDate}</span>
|
||||
</Tooltip>
|
||||
<span>{formattedAbsolute}</span>
|
||||
</TableColumnTextContainer>
|
||||
);
|
||||
}
|
||||
@@ -79,44 +55,37 @@ const AbsoluteDateColumnBase = (props: ItemTableListInnerColumn) => {
|
||||
: null;
|
||||
|
||||
if (originalDate) {
|
||||
const formattedOriginalDate = formatDateAbsoluteUTC(originalDate);
|
||||
const formattedReleaseDate = formatDateAbsoluteUTC(releaseDate);
|
||||
const displayText = `${formattedOriginalDate}${SEPARATOR_STRING}${formattedReleaseDate}`;
|
||||
|
||||
return {
|
||||
displayText,
|
||||
tooltipLabel: getDateTooltipLabel(releaseDate),
|
||||
};
|
||||
const formattedOriginalDate = formatPartialIsoDateUTC(originalDate);
|
||||
const formattedReleaseDate = formatPartialIsoDateUTC(releaseDate);
|
||||
return `${formattedOriginalDate}${SEPARATOR_STRING}${formattedReleaseDate}`;
|
||||
}
|
||||
|
||||
if (typeof releaseDate === 'string' && releaseDate) {
|
||||
return {
|
||||
displayText: formatDateAbsoluteUTC(releaseDate),
|
||||
tooltipLabel: getDateTooltipLabel(releaseDate),
|
||||
};
|
||||
return formatPartialIsoDateUTC(releaseDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, [props.type, rowItem]);
|
||||
|
||||
const { formattedDate, tooltipLabel } = useMemo(() => {
|
||||
if (typeof row === 'string' && row) {
|
||||
return {
|
||||
formattedDate: formatDateAbsoluteUTC(row),
|
||||
tooltipLabel: getDateTooltipLabel(row),
|
||||
};
|
||||
}
|
||||
return { formattedDate: null, tooltipLabel: null };
|
||||
}, [row]);
|
||||
const formattedIsoFallback = useMemo(
|
||||
() => (typeof row === 'string' && row ? formatPartialIsoDateUTC(row) : null),
|
||||
[row],
|
||||
);
|
||||
|
||||
if (props.type === TableColumn.RELEASE_DATE) {
|
||||
if (releaseDateContent) {
|
||||
return (
|
||||
<TableColumnTextContainer {...props}>
|
||||
<Tooltip label={releaseDateContent.tooltipLabel} multiline={false}>
|
||||
<span>{releaseDateContent.displayText}</span>
|
||||
</Tooltip>
|
||||
<span>{releaseDateContent}</span>
|
||||
</TableColumnTextContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (formattedIsoFallback) {
|
||||
return (
|
||||
<TableColumnTextContainer {...props}>
|
||||
<span>{formattedIsoFallback}</span>
|
||||
</TableColumnTextContainer>
|
||||
);
|
||||
}
|
||||
@@ -128,20 +97,6 @@ const AbsoluteDateColumnBase = (props: ItemTableListInnerColumn) => {
|
||||
return <ColumnSkeletonFixed {...props} />;
|
||||
}
|
||||
|
||||
if (typeof row === 'string' && row) {
|
||||
return (
|
||||
<TableColumnTextContainer {...props}>
|
||||
<Tooltip label={tooltipLabel} multiline={false}>
|
||||
<span>{formattedDate}</span>
|
||||
</Tooltip>
|
||||
</TableColumnTextContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (row === null) {
|
||||
return <ColumnNullFallback {...props} />;
|
||||
}
|
||||
|
||||
return <ColumnSkeletonFixed {...props} />;
|
||||
};
|
||||
|
||||
@@ -151,22 +106,15 @@ const RelativeDateColumnBase = (props: ItemTableListInnerColumn) => {
|
||||
const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex];
|
||||
const row: string | undefined = (rowItem as any)?.[props.columns[props.columnIndex].id];
|
||||
|
||||
const { formattedDate, tooltipLabel } = useMemo(() => {
|
||||
if (typeof row === 'string') {
|
||||
return {
|
||||
formattedDate: formatDateRelative(row),
|
||||
tooltipLabel: getDateTooltipLabel(row),
|
||||
};
|
||||
}
|
||||
return { formattedDate: null, tooltipLabel: null };
|
||||
const formattedRelative = useMemo(() => {
|
||||
if (typeof row !== 'string') return null;
|
||||
return formatDateRelative(row);
|
||||
}, [row]);
|
||||
|
||||
if (typeof row === 'string') {
|
||||
if (formattedRelative !== null) {
|
||||
return (
|
||||
<TableColumnTextContainer {...props}>
|
||||
<Tooltip label={tooltipLabel} multiline={false}>
|
||||
<span>{formattedDate}</span>
|
||||
</Tooltip>
|
||||
<span>{formattedRelative}</span>
|
||||
</TableColumnTextContainer>
|
||||
);
|
||||
}
|
||||
|
||||
+4
-8
@@ -313,12 +313,10 @@ const PlaylistReorderColumnBase = (props: ItemTableListInnerColumn) => {
|
||||
<>
|
||||
<Stack gap="xs" justify="center">
|
||||
<Text fw={500} ta="center">
|
||||
{t('action.moveUp', { postProcess: 'sentenceCase' })}
|
||||
{t('action.moveUp')}
|
||||
</Text>
|
||||
<Text fw={500} isMuted size="xs" ta="center">
|
||||
{t('action.holdToMoveToTop', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
{t('action.holdToMoveToTop')}
|
||||
</Text>
|
||||
</Stack>
|
||||
</>
|
||||
@@ -336,12 +334,10 @@ const PlaylistReorderColumnBase = (props: ItemTableListInnerColumn) => {
|
||||
<>
|
||||
<Stack gap="xs" justify="center">
|
||||
<Text fw={500} ta="center">
|
||||
{t('action.moveDown', { postProcess: 'sentenceCase' })}
|
||||
{t('action.moveDown')}
|
||||
</Text>
|
||||
<Text fw={500} isMuted size="xs" ta="center">
|
||||
{t('action.holdToMoveToBottom', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
{t('action.holdToMoveToBottom')}
|
||||
</Text>
|
||||
</Stack>
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import { useMemo } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import styles from './title-column.module.css';
|
||||
@@ -35,8 +36,12 @@ function DefaultTitleColumn(props: ItemTableListInnerColumn) {
|
||||
const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex];
|
||||
const row: string | undefined = rowItem?.[props.columns[props.columnIndex].id];
|
||||
|
||||
const path = useMemo(() => {
|
||||
if (typeof row !== 'string' || !rowItem || !(rowItem as any).id) return undefined;
|
||||
return getTitlePath(props.itemType, (rowItem as any).id as string);
|
||||
}, [props.itemType, row, rowItem]);
|
||||
|
||||
if (typeof row === 'string') {
|
||||
const path = getTitlePath(props.itemType, (rowItem as any).id as string);
|
||||
const item = rowItem as any;
|
||||
|
||||
const titleLinkProps = path
|
||||
@@ -80,8 +85,12 @@ function QueueSongTitleColumn(props: ItemTableListInnerColumn) {
|
||||
const song = rowItem as QueueSong;
|
||||
const isActive = useIsActiveRow(song?.id, song?._uniqueId);
|
||||
|
||||
const path = useMemo(() => {
|
||||
if (typeof row !== 'string' || !rowItem || !(rowItem as any).id) return undefined;
|
||||
return getTitlePath(props.itemType, (rowItem as any).id as string);
|
||||
}, [props.itemType, row, rowItem]);
|
||||
|
||||
if (typeof row === 'string') {
|
||||
const path = getTitlePath(props.itemType, (rowItem as any).id as string);
|
||||
const item = rowItem as any;
|
||||
|
||||
const titleLinkProps = path
|
||||
|
||||
@@ -13,10 +13,10 @@ const YearColumnBase = (props: ItemTableListInnerColumn) => {
|
||||
const item = rowItem as any;
|
||||
|
||||
const yearDisplay = useMemo(() => {
|
||||
if (item && 'releaseYear' in item && item.releaseYear !== null) {
|
||||
if (item && 'releaseYear' in item && item.releaseYear != null) {
|
||||
const releaseYear = item.releaseYear;
|
||||
const originalYear =
|
||||
'originalYear' in item && item.originalYear !== null ? item.originalYear : null;
|
||||
'originalYear' in item && item.originalYear > 0 ? item.originalYear : null;
|
||||
|
||||
if (originalYear !== null && originalYear !== releaseYear) {
|
||||
return `${originalYear}${SEPARATOR_STRING}${releaseYear}`;
|
||||
|
||||
@@ -17,7 +17,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.albumGroup', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.albumGroup'),
|
||||
pinned: 'left',
|
||||
value: TableColumn.ALBUM_GROUP,
|
||||
width: 200,
|
||||
@@ -26,7 +26,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.rowIndex'),
|
||||
pinned: null,
|
||||
value: TableColumn.ROW_INDEX,
|
||||
width: 60,
|
||||
@@ -35,7 +35,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.image', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.image'),
|
||||
pinned: null,
|
||||
value: TableColumn.IMAGE,
|
||||
width: 70,
|
||||
@@ -44,7 +44,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.title'),
|
||||
pinned: null,
|
||||
value: TableColumn.TITLE,
|
||||
width: 300,
|
||||
@@ -53,7 +53,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.titleCombined', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.titleCombined'),
|
||||
pinned: null,
|
||||
value: TableColumn.TITLE_COMBINED,
|
||||
width: 300,
|
||||
@@ -62,7 +62,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.titleArtist', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.titleArtist'),
|
||||
pinned: null,
|
||||
value: TableColumn.TITLE_ARTIST,
|
||||
width: 300,
|
||||
@@ -71,7 +71,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.duration', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.duration'),
|
||||
pinned: null,
|
||||
value: TableColumn.DURATION,
|
||||
width: 100,
|
||||
@@ -80,7 +80,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.album', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.album'),
|
||||
pinned: null,
|
||||
value: TableColumn.ALBUM,
|
||||
width: 300,
|
||||
@@ -89,7 +89,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'start',
|
||||
autoSize: true,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.albumArtist', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.albumArtist'),
|
||||
pinned: null,
|
||||
value: TableColumn.ALBUM_ARTIST,
|
||||
width: 300,
|
||||
@@ -98,7 +98,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.artist', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.artist'),
|
||||
pinned: null,
|
||||
value: TableColumn.ARTIST,
|
||||
width: 300,
|
||||
@@ -107,7 +107,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.composer', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.composer'),
|
||||
pinned: null,
|
||||
value: TableColumn.COMPOSER,
|
||||
width: 300,
|
||||
@@ -116,7 +116,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.genre', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.genre'),
|
||||
pinned: null,
|
||||
value: TableColumn.GENRE,
|
||||
width: 300,
|
||||
@@ -125,7 +125,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.genreBadge', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.genreBadge'),
|
||||
pinned: null,
|
||||
value: TableColumn.GENRE_BADGE,
|
||||
width: 300,
|
||||
@@ -134,7 +134,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.year', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.year'),
|
||||
pinned: null,
|
||||
value: TableColumn.YEAR,
|
||||
width: 200,
|
||||
@@ -143,7 +143,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.releaseDate', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.releaseDate'),
|
||||
pinned: null,
|
||||
value: TableColumn.RELEASE_DATE,
|
||||
width: 240,
|
||||
@@ -152,7 +152,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.discNumber', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.discNumber'),
|
||||
pinned: null,
|
||||
value: TableColumn.DISC_NUMBER,
|
||||
width: 100,
|
||||
@@ -161,7 +161,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.trackNumber', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.trackNumber'),
|
||||
pinned: null,
|
||||
value: TableColumn.TRACK_NUMBER,
|
||||
width: 100,
|
||||
@@ -170,7 +170,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.bitDepth', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.bitDepth'),
|
||||
pinned: null,
|
||||
value: TableColumn.BIT_DEPTH,
|
||||
width: 100,
|
||||
@@ -179,7 +179,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.bitrate', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.bitrate'),
|
||||
pinned: null,
|
||||
value: TableColumn.BIT_RATE,
|
||||
width: 100,
|
||||
@@ -188,7 +188,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.codec', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.codec'),
|
||||
pinned: null,
|
||||
value: TableColumn.CODEC,
|
||||
width: 100,
|
||||
@@ -197,7 +197,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.sampleRate', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.sampleRate'),
|
||||
pinned: null,
|
||||
value: TableColumn.SAMPLE_RATE,
|
||||
width: 100,
|
||||
@@ -206,7 +206,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.lastPlayed', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.lastPlayed'),
|
||||
pinned: null,
|
||||
value: TableColumn.LAST_PLAYED,
|
||||
width: 150,
|
||||
@@ -215,7 +215,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.note', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.note'),
|
||||
pinned: null,
|
||||
value: TableColumn.COMMENT,
|
||||
width: 300,
|
||||
@@ -224,7 +224,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.channels', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.channels'),
|
||||
pinned: null,
|
||||
value: TableColumn.CHANNELS,
|
||||
width: 100,
|
||||
@@ -233,7 +233,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.bpm', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.bpm'),
|
||||
pinned: null,
|
||||
value: TableColumn.BPM,
|
||||
width: 100,
|
||||
@@ -242,7 +242,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.dateAdded', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.dateAdded'),
|
||||
pinned: null,
|
||||
value: TableColumn.DATE_ADDED,
|
||||
width: 120,
|
||||
@@ -251,7 +251,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.path', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.path'),
|
||||
pinned: null,
|
||||
value: TableColumn.PATH,
|
||||
width: 300,
|
||||
@@ -260,7 +260,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.playCount', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.playCount'),
|
||||
pinned: null,
|
||||
value: TableColumn.PLAY_COUNT,
|
||||
width: 100,
|
||||
@@ -269,7 +269,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.size', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.size'),
|
||||
pinned: null,
|
||||
value: TableColumn.SIZE,
|
||||
width: 100,
|
||||
@@ -278,7 +278,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.favorite', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.favorite'),
|
||||
pinned: null,
|
||||
value: TableColumn.USER_FAVORITE,
|
||||
width: 60,
|
||||
@@ -287,7 +287,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.rating', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.rating'),
|
||||
pinned: null,
|
||||
value: TableColumn.USER_RATING,
|
||||
width: 100,
|
||||
@@ -296,7 +296,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.actions'),
|
||||
pinned: null,
|
||||
value: TableColumn.ACTIONS,
|
||||
width: 60,
|
||||
@@ -310,7 +310,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.rowIndex'),
|
||||
pinned: null,
|
||||
value: TableColumn.ROW_INDEX,
|
||||
width: 60,
|
||||
@@ -319,7 +319,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.image', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.image'),
|
||||
pinned: null,
|
||||
value: TableColumn.IMAGE,
|
||||
width: 70,
|
||||
@@ -328,7 +328,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.title'),
|
||||
pinned: null,
|
||||
value: TableColumn.TITLE,
|
||||
width: 300,
|
||||
@@ -337,7 +337,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.titleCombined', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.titleCombined'),
|
||||
pinned: null,
|
||||
value: TableColumn.TITLE_COMBINED,
|
||||
width: 300,
|
||||
@@ -346,7 +346,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.titleArtist', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.titleArtist'),
|
||||
pinned: null,
|
||||
value: TableColumn.TITLE_ARTIST,
|
||||
width: 300,
|
||||
@@ -355,7 +355,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.duration', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.duration'),
|
||||
pinned: null,
|
||||
value: TableColumn.DURATION,
|
||||
width: 100,
|
||||
@@ -364,7 +364,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'start',
|
||||
autoSize: true,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.albumArtist', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.albumArtist'),
|
||||
pinned: null,
|
||||
value: TableColumn.ALBUM_ARTIST,
|
||||
width: 300,
|
||||
@@ -373,7 +373,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.artist', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.artist'),
|
||||
pinned: null,
|
||||
value: TableColumn.ARTIST,
|
||||
width: 300,
|
||||
@@ -382,7 +382,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.composer', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.composer'),
|
||||
pinned: null,
|
||||
value: TableColumn.COMPOSER,
|
||||
width: 300,
|
||||
@@ -391,7 +391,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.songCount', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.songCount'),
|
||||
pinned: null,
|
||||
value: TableColumn.SONG_COUNT,
|
||||
width: 100,
|
||||
@@ -400,7 +400,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.genre', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.genre'),
|
||||
pinned: null,
|
||||
value: TableColumn.GENRE,
|
||||
width: 300,
|
||||
@@ -409,7 +409,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.genreBadge', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.genreBadge'),
|
||||
pinned: null,
|
||||
value: TableColumn.GENRE_BADGE,
|
||||
width: 300,
|
||||
@@ -418,7 +418,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.year', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.year'),
|
||||
pinned: null,
|
||||
value: TableColumn.YEAR,
|
||||
width: 200,
|
||||
@@ -427,7 +427,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.releaseDate', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.releaseDate'),
|
||||
pinned: null,
|
||||
value: TableColumn.RELEASE_DATE,
|
||||
width: 240,
|
||||
@@ -436,7 +436,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.lastPlayed', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.lastPlayed'),
|
||||
pinned: null,
|
||||
value: TableColumn.LAST_PLAYED,
|
||||
width: 150,
|
||||
@@ -445,7 +445,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.dateAdded', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.dateAdded'),
|
||||
pinned: null,
|
||||
value: TableColumn.DATE_ADDED,
|
||||
width: 120,
|
||||
@@ -454,7 +454,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.playCount', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.playCount'),
|
||||
pinned: null,
|
||||
value: TableColumn.PLAY_COUNT,
|
||||
width: 100,
|
||||
@@ -463,7 +463,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.favorite', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.favorite'),
|
||||
pinned: null,
|
||||
value: TableColumn.USER_FAVORITE,
|
||||
width: 60,
|
||||
@@ -472,7 +472,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.rating', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.rating'),
|
||||
pinned: null,
|
||||
value: TableColumn.USER_RATING,
|
||||
width: 100,
|
||||
@@ -481,7 +481,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.actions'),
|
||||
pinned: null,
|
||||
value: TableColumn.ACTIONS,
|
||||
width: 60,
|
||||
@@ -493,7 +493,7 @@ export const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.rowIndex'),
|
||||
pinned: null,
|
||||
value: TableColumn.ROW_INDEX,
|
||||
width: 60,
|
||||
@@ -502,7 +502,7 @@ export const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.image', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.image'),
|
||||
pinned: null,
|
||||
value: TableColumn.IMAGE,
|
||||
width: 70,
|
||||
@@ -511,7 +511,7 @@ export const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.title'),
|
||||
pinned: null,
|
||||
value: TableColumn.TITLE,
|
||||
width: 300,
|
||||
@@ -520,7 +520,7 @@ export const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.duration', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.duration'),
|
||||
pinned: null,
|
||||
value: TableColumn.DURATION,
|
||||
width: 100,
|
||||
@@ -529,7 +529,7 @@ export const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.biography', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.biography'),
|
||||
pinned: null,
|
||||
value: TableColumn.BIOGRAPHY,
|
||||
width: 300,
|
||||
@@ -538,7 +538,7 @@ export const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.genre', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.genre'),
|
||||
pinned: null,
|
||||
value: TableColumn.GENRE,
|
||||
width: 300,
|
||||
@@ -547,7 +547,7 @@ export const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.lastPlayed', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.lastPlayed'),
|
||||
pinned: null,
|
||||
value: TableColumn.LAST_PLAYED,
|
||||
width: 150,
|
||||
@@ -556,7 +556,7 @@ export const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.playCount', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.playCount'),
|
||||
pinned: null,
|
||||
value: TableColumn.PLAY_COUNT,
|
||||
width: 100,
|
||||
@@ -565,7 +565,7 @@ export const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('filter.albumCount', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('filter.albumCount'),
|
||||
pinned: null,
|
||||
value: TableColumn.ALBUM_COUNT,
|
||||
width: 100,
|
||||
@@ -574,7 +574,7 @@ export const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.songCount', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.songCount'),
|
||||
pinned: null,
|
||||
value: TableColumn.SONG_COUNT,
|
||||
width: 100,
|
||||
@@ -583,7 +583,7 @@ export const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.favorite', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.favorite'),
|
||||
pinned: null,
|
||||
value: TableColumn.USER_FAVORITE,
|
||||
width: 60,
|
||||
@@ -592,7 +592,7 @@ export const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.rating', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.rating'),
|
||||
pinned: null,
|
||||
value: TableColumn.USER_RATING,
|
||||
width: 100,
|
||||
@@ -601,7 +601,7 @@ export const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.actions'),
|
||||
pinned: null,
|
||||
value: TableColumn.ACTIONS,
|
||||
width: 60,
|
||||
@@ -613,7 +613,7 @@ export const PLAYLIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.rowIndex'),
|
||||
pinned: null,
|
||||
value: TableColumn.ROW_INDEX,
|
||||
width: 60,
|
||||
@@ -622,7 +622,7 @@ export const PLAYLIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.image', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.image'),
|
||||
pinned: null,
|
||||
value: TableColumn.IMAGE,
|
||||
width: 70,
|
||||
@@ -631,7 +631,7 @@ export const PLAYLIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.title'),
|
||||
pinned: null,
|
||||
value: TableColumn.TITLE,
|
||||
width: 300,
|
||||
@@ -640,7 +640,7 @@ export const PLAYLIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.titleCombined', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.titleCombined'),
|
||||
pinned: null,
|
||||
value: TableColumn.TITLE_COMBINED,
|
||||
width: 300,
|
||||
@@ -649,7 +649,7 @@ export const PLAYLIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.duration', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.duration'),
|
||||
pinned: null,
|
||||
value: TableColumn.DURATION,
|
||||
width: 100,
|
||||
@@ -658,7 +658,7 @@ export const PLAYLIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.owner', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.owner'),
|
||||
pinned: null,
|
||||
value: TableColumn.OWNER,
|
||||
width: 150,
|
||||
@@ -667,7 +667,7 @@ export const PLAYLIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.songCount', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.songCount'),
|
||||
pinned: null,
|
||||
value: TableColumn.SONG_COUNT,
|
||||
width: 100,
|
||||
@@ -676,7 +676,7 @@ export const PLAYLIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.actions'),
|
||||
pinned: null,
|
||||
value: TableColumn.ACTIONS,
|
||||
width: 60,
|
||||
@@ -688,7 +688,7 @@ export const GENRE_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.rowIndex'),
|
||||
pinned: null,
|
||||
value: TableColumn.ROW_INDEX,
|
||||
width: 60,
|
||||
@@ -697,7 +697,7 @@ export const GENRE_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'start',
|
||||
autoSize: true,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.title'),
|
||||
pinned: null,
|
||||
value: TableColumn.TITLE,
|
||||
width: 300,
|
||||
@@ -706,7 +706,7 @@ export const GENRE_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.songCount', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.songCount'),
|
||||
pinned: null,
|
||||
value: TableColumn.SONG_COUNT,
|
||||
width: 100,
|
||||
@@ -715,7 +715,7 @@ export const GENRE_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.albumCount', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.albumCount'),
|
||||
pinned: null,
|
||||
value: TableColumn.ALBUM_COUNT,
|
||||
width: 100,
|
||||
@@ -724,7 +724,7 @@ export const GENRE_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
|
||||
label: i18n.t('table.config.label.actions'),
|
||||
pinned: null,
|
||||
value: TableColumn.ACTIONS,
|
||||
width: 60,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user