mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-08 13:00:13 +02:00
Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 31bcc70498 | |||
| 41c21b94c1 | |||
| bca14176fb | |||
| 02a5395453 | |||
| 7ba2f6b827 | |||
| f1b5dc8ef3 | |||
| 78875572e9 | |||
| f487560ec5 | |||
| f752090c78 | |||
| 96f5b2b82a | |||
| 80292ae579 | |||
| 1d156ac506 | |||
| dc5586f859 | |||
| 1a9f36ce9e | |||
| 203c8a6588 | |||
| 2e6cf8d869 | |||
| b1827dd352 | |||
| 0d2dddddbc | |||
| 1d8e1957ba | |||
| dc957cb3cc | |||
| c314fa0bf3 | |||
| c5ebfac647 | |||
| 4adea11a93 | |||
| e6f49b9f1f | |||
| 9cde569c7d | |||
| 91e7c7434c | |||
| ffef5dfdee | |||
| 409dd69fcb | |||
| 064cf5103a | |||
| 7e3a613a93 | |||
| e7c49f6d67 | |||
| 022b83ab32 | |||
| 551d705ee1 | |||
| 83f73c7fa9 | |||
| cc8cb4f4f1 | |||
| 496eab7d09 | |||
| 5197c967c2 | |||
| 74b615dba7 | |||
| b67ee797cb | |||
| 4228084810 | |||
| b514c7972d | |||
| 83d9042a47 | |||
| a28c403093 | |||
| 2927fa5ff7 | |||
| f39a7f8d6f | |||
| 397610d8ab | |||
| fb170bb7c4 | |||
| d93f6e8720 | |||
| 668de93829 | |||
| 7cecd859ae | |||
| fea2966f62 | |||
| 6efa308e85 | |||
| 82b50a60bc | |||
| f52c4f7900 | |||
| 2fb621993d | |||
| cf663de2fc | |||
| 65c215fa9c | |||
| 8af972c20b | |||
| 027e4046a2 | |||
| 4c256348fc | |||
| 6e3275c05c | |||
| 3518a3f3b6 | |||
| 2b6b0cb38b | |||
| f56a836ffd | |||
| 2d963a9d23 | |||
| 4423b06807 | |||
| 1f9223b476 | |||
| b4ecf5d257 | |||
| 0dd13cbab1 | |||
| 48e50430fe | |||
| ac5611fdca | |||
| 50c3dbc0a0 | |||
| ddd840d2df | |||
| c0c9878fad | |||
| c4fc8a8aef | |||
| 0620b096db | |||
| f998491beb | |||
| 55a6ea4fca | |||
| 72fc5beb98 | |||
| a45b607fe7 | |||
| adfdf04240 | |||
| faa7281993 | |||
| 2d0f4e7881 | |||
| ce9183ffd6 | |||
| 3a5508653b | |||
| 7e4e28037c | |||
| d2d8ea8249 |
@@ -0,0 +1,189 @@
|
|||||||
|
# Alpha builds published to Cloudflare R2 with date versioning (e.g. 1.0.0-alpha-20260205).
|
||||||
|
# Required repo secrets: R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY (from R2 API token in Cloudflare dashboard).
|
||||||
|
name: Publish Alpha
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Semantic version number (e.g., 1.0.0) - alpha suffix will be added automatically'
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
schedule:
|
||||||
|
# Run at 3:00 AM PST daily (11:00 UTC; PST = UTC-8)
|
||||||
|
- cron: '0 11 * * *'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-new-commits:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
has_new_commits: ${{ steps.manual.outputs.has_new_commits || steps.check.outputs['has-new-commits'] }}
|
||||||
|
steps:
|
||||||
|
- name: Set has new commits (manual trigger)
|
||||||
|
id: manual
|
||||||
|
if: github.event_name == 'workflow_dispatch'
|
||||||
|
run: echo "has_new_commits=true" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Check for new commits (24 hr interval)
|
||||||
|
id: check
|
||||||
|
if: github.event_name != 'workflow_dispatch'
|
||||||
|
uses: adriangl/check-new-commits-action@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
seconds: 86400
|
||||||
|
|
||||||
|
prepare:
|
||||||
|
needs: check-new-commits
|
||||||
|
if: needs.check-new-commits.outputs.has_new_commits == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.version.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout git repo
|
||||||
|
uses: actions/checkout@v1
|
||||||
|
|
||||||
|
- name: Install Node and PNPM
|
||||||
|
uses: pnpm/action-setup@v4.1.0
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Set date-based alpha version
|
||||||
|
id: version
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$inputVersion = "${{ github.event.inputs.version }}"
|
||||||
|
Write-Host "Input version: $inputVersion"
|
||||||
|
|
||||||
|
if ($inputVersion -eq "" -or $inputVersion -eq "null") {
|
||||||
|
# No input version provided (scheduled run or manual without input), auto-increment patch version
|
||||||
|
Write-Host "No version provided, auto-incrementing patch version..."
|
||||||
|
|
||||||
|
$currentVersion = (Get-Content package.json | ConvertFrom-Json).version
|
||||||
|
Write-Host "Current version: $currentVersion"
|
||||||
|
|
||||||
|
$cleanVersion = $currentVersion -replace '-.*$', ''
|
||||||
|
$versionParts = $cleanVersion.Split('.')
|
||||||
|
if ($versionParts.Length -ne 3) {
|
||||||
|
Write-Error "Current version format is invalid: $cleanVersion"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$major = [int]$versionParts[0]
|
||||||
|
$minor = [int]$versionParts[1]
|
||||||
|
$patch = [int]$versionParts[2]
|
||||||
|
$newPatch = $patch + 1
|
||||||
|
$inputVersion = "$major.$minor.$newPatch"
|
||||||
|
Write-Host "Auto-generated version: $inputVersion"
|
||||||
|
} else {
|
||||||
|
# Validate semantic version format (major.minor.patch)
|
||||||
|
$versionPattern = '^\d+\.\d+\.\d+$'
|
||||||
|
if ($inputVersion -notmatch $versionPattern) {
|
||||||
|
Write-Error "Invalid version format. Expected semantic version (e.g., 1.0.0), got: $inputVersion"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Date in YYYYMMDD (PST / America/Los_Angeles)
|
||||||
|
$pst = [TimeZoneInfo]::FindSystemTimeZoneById('America/Los_Angeles')
|
||||||
|
$dateInPst = [TimeZoneInfo]::ConvertTimeFromUtc([DateTime]::UtcNow, $pst)
|
||||||
|
$dateStr = $dateInPst.ToString("yyyyMMdd")
|
||||||
|
$alphaVersion = "$inputVersion-alpha-$dateStr"
|
||||||
|
Write-Host "Alpha version: $alphaVersion"
|
||||||
|
|
||||||
|
# Update package.json
|
||||||
|
$packageJson = Get-Content package.json | ConvertFrom-Json
|
||||||
|
$packageJson.version = $alphaVersion
|
||||||
|
$packageJson | ConvertTo-Json -Depth 10 | Set-Content package.json
|
||||||
|
|
||||||
|
echo "version=$alphaVersion" >> $env:GITHUB_OUTPUT
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
needs: prepare
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||||
|
R2_ENDPOINT_URL: ${{ secrets.R2_ENDPOINT_URL }}
|
||||||
|
steps:
|
||||||
|
- name: Delete all objects in R2 bucket
|
||||||
|
run: |
|
||||||
|
aws s3 rm s3://feishin-nightly --recursive --endpoint-url $R2_ENDPOINT_URL
|
||||||
|
|
||||||
|
publish:
|
||||||
|
needs: [prepare, cleanup]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [windows-latest, macos-latest, ubuntu-latest]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout git repo
|
||||||
|
uses: actions/checkout@v1
|
||||||
|
|
||||||
|
- name: Install Node and PNPM
|
||||||
|
uses: pnpm/action-setup@v4.1.0
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Set version from prepare job
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$version = "${{ needs.prepare.outputs.version }}"
|
||||||
|
Write-Host "Setting version: $version"
|
||||||
|
$packageJson = Get-Content package.json | ConvertFrom-Json
|
||||||
|
$packageJson.version = $version
|
||||||
|
$packageJson | ConvertTo-Json -Depth 10 | Set-Content package.json
|
||||||
|
|
||||||
|
- name: Build and Publish to R2 (Windows)
|
||||||
|
if: matrix.os == 'windows-latest'
|
||||||
|
uses: nick-invision/retry@v2.8.2
|
||||||
|
with:
|
||||||
|
timeout_minutes: 30
|
||||||
|
max_attempts: 3
|
||||||
|
retry_on: error
|
||||||
|
command: |
|
||||||
|
pnpm run publish:win:alpha
|
||||||
|
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
|
||||||
|
with:
|
||||||
|
timeout_minutes: 30
|
||||||
|
max_attempts: 3
|
||||||
|
retry_on: error
|
||||||
|
command: |
|
||||||
|
pnpm run publish:mac:alpha
|
||||||
|
on_retry_command: pnpm cache delete
|
||||||
|
|
||||||
|
- name: Build and Publish to R2 (Linux)
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
uses: nick-invision/retry@v2.8.2
|
||||||
|
with:
|
||||||
|
timeout_minutes: 30
|
||||||
|
max_attempts: 3
|
||||||
|
retry_on: error
|
||||||
|
command: |
|
||||||
|
pnpm run publish:linux:alpha
|
||||||
|
on_retry_command: pnpm cache delete
|
||||||
|
|
||||||
|
- name: Build and Publish to R2 (Linux ARM64)
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
uses: nick-invision/retry@v2.8.2
|
||||||
|
with:
|
||||||
|
timeout_minutes: 30
|
||||||
|
max_attempts: 3
|
||||||
|
retry_on: error
|
||||||
|
command: |
|
||||||
|
pnpm run publish:linux-arm64:alpha
|
||||||
|
on_retry_command: pnpm cache delete
|
||||||
@@ -16,6 +16,5 @@ jobs:
|
|||||||
- uses: vedantmgoyal9/winget-releaser@main
|
- uses: vedantmgoyal9/winget-releaser@main
|
||||||
with:
|
with:
|
||||||
identifier: jeffvli.Feishin
|
identifier: jeffvli.Feishin
|
||||||
installers-regex: 'Feishin-*-win-x64\.exe'
|
installers-regex: 'Feishin-*-win-(x64|arm64)\.exe'
|
||||||
token: ${{ secrets.WINGET_ACC_TOKEN }}
|
token: ${{ secrets.WINGET_ACC_TOKEN }}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,65 @@
|
|||||||
|
appId: org.jeffvli.feishin
|
||||||
|
productName: Feishin
|
||||||
|
artifactName: ${productName}-${version}-${os}-${arch}.${ext}
|
||||||
|
electronVersion: 39.4.0
|
||||||
|
directories:
|
||||||
|
buildResources: assets
|
||||||
|
files:
|
||||||
|
- 'out/**/*'
|
||||||
|
- 'package.json'
|
||||||
|
extraResources:
|
||||||
|
- assets/**
|
||||||
|
asarUnpack:
|
||||||
|
- resources/**
|
||||||
|
win:
|
||||||
|
target:
|
||||||
|
- target: zip
|
||||||
|
arch:
|
||||||
|
- x64
|
||||||
|
- arm64
|
||||||
|
- target: nsis
|
||||||
|
arch:
|
||||||
|
- x64
|
||||||
|
- arm64
|
||||||
|
icon: assets/icons/icon.ico
|
||||||
|
|
||||||
|
nsis:
|
||||||
|
allowToChangeInstallationDirectory: true
|
||||||
|
oneClick: false
|
||||||
|
shortcutName: ${productName}
|
||||||
|
uninstallDisplayName: ${productName}
|
||||||
|
createDesktopShortcut: always
|
||||||
|
|
||||||
|
mac:
|
||||||
|
target:
|
||||||
|
target: default
|
||||||
|
arch:
|
||||||
|
- arm64
|
||||||
|
- x64
|
||||||
|
icon: assets/icons/icon.icns
|
||||||
|
type: distribution
|
||||||
|
hardenedRuntime: true
|
||||||
|
entitlements: assets/entitlements.mac.plist
|
||||||
|
entitlementsInherit: assets/entitlements.mac.plist
|
||||||
|
gatekeeperAssess: false
|
||||||
|
notarize: false
|
||||||
|
|
||||||
|
dmg:
|
||||||
|
contents: [{ x: 130, y: 220 }, { x: 410, y: 220, type: link, path: /Applications }]
|
||||||
|
|
||||||
|
linux:
|
||||||
|
target:
|
||||||
|
- AppImage
|
||||||
|
- deb
|
||||||
|
- tar.xz
|
||||||
|
category: AudioVideo;Audio;Player
|
||||||
|
icon: assets/icons/icon.png
|
||||||
|
artifactName: ${productName}-${os}-${arch}.${ext}
|
||||||
|
|
||||||
|
npmRebuild: false
|
||||||
|
|
||||||
|
publish:
|
||||||
|
provider: s3
|
||||||
|
bucket: feishin-nightly
|
||||||
|
channel: alpha
|
||||||
|
endpoint: https://065f090c64de2dc707dd70ac72db9669.r2.cloudflarestorage.com
|
||||||
@@ -13,9 +13,15 @@ asarUnpack:
|
|||||||
- resources/**
|
- resources/**
|
||||||
win:
|
win:
|
||||||
target:
|
target:
|
||||||
- zip
|
- target: zip
|
||||||
- nsis
|
arch:
|
||||||
icon: assets/icons/icon.png
|
- x64
|
||||||
|
- arm64
|
||||||
|
- target: nsis
|
||||||
|
arch:
|
||||||
|
- x64
|
||||||
|
- arm64
|
||||||
|
icon: assets/icons/icon.ico
|
||||||
|
|
||||||
nsis:
|
nsis:
|
||||||
allowToChangeInstallationDirectory: true
|
allowToChangeInstallationDirectory: true
|
||||||
|
|||||||
@@ -13,8 +13,14 @@ asarUnpack:
|
|||||||
- resources/**
|
- resources/**
|
||||||
win:
|
win:
|
||||||
target:
|
target:
|
||||||
- zip
|
- target: zip
|
||||||
- nsis
|
arch:
|
||||||
|
- x64
|
||||||
|
- arm64
|
||||||
|
- target: nsis
|
||||||
|
arch:
|
||||||
|
- x64
|
||||||
|
- arm64
|
||||||
icon: assets/icons/icon.ico
|
icon: assets/icons/icon.ico
|
||||||
|
|
||||||
nsis:
|
nsis:
|
||||||
|
|||||||
+10
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"version": "1.4.1",
|
"version": "1.5.0",
|
||||||
"description": "A modern self-hosted music player.",
|
"description": "A modern self-hosted music player.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"subsonic",
|
"subsonic",
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"dev:watch": "electron-vite dev --watch",
|
"dev:watch": "electron-vite dev --watch",
|
||||||
"i18next": "i18next -c src/i18n/i18next-parser.config.js",
|
"i18next": "i18next -c src/i18n/i18next-parser.config.js",
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"lint": "pnpm run lint-code && pnpm run lint-styles",
|
"lint": "pnpm run typecheck && pnpm run lint-code && pnpm run lint-styles",
|
||||||
"lint-code": "eslint --max-warnings=0 --cache .",
|
"lint-code": "eslint --max-warnings=0 --cache .",
|
||||||
"lint-code:fix": "eslint --cache --fix .",
|
"lint-code:fix": "eslint --cache --fix .",
|
||||||
"lint-styles": "stylelint --max-warnings=0 'src/**/*.{css,scss}'",
|
"lint-styles": "stylelint --max-warnings=0 'src/**/*.{css,scss}'",
|
||||||
@@ -44,14 +44,22 @@
|
|||||||
"package:mac": "pnpm run build && electron-builder --mac",
|
"package:mac": "pnpm run build && electron-builder --mac",
|
||||||
"package:mac:pr": "pnpm run build && electron-builder --mac --publish never",
|
"package:mac:pr": "pnpm run build && electron-builder --mac --publish never",
|
||||||
"package:win": "pnpm run build && electron-builder --win",
|
"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:pr": "pnpm run build && electron-builder --win --publish never",
|
||||||
"publish:linux": "pnpm run build && electron-builder --publish always --linux",
|
"publish:linux": "pnpm run build && electron-builder --publish always --linux",
|
||||||
"publish:linux-arm64": "pnpm run build && electron-builder --publish always --linux --arm64",
|
"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-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: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": "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: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": "pnpm run build && electron-builder --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",
|
"publish:win:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --win",
|
||||||
"start": "electron-vite preview",
|
"start": "electron-vite preview",
|
||||||
"typecheck": "pnpm run typecheck:node && pnpm run typecheck:web",
|
"typecheck": "pnpm run typecheck:node && pnpm run typecheck:web",
|
||||||
|
|||||||
+56
-12
@@ -15,7 +15,8 @@
|
|||||||
"nowPlaying": "ara sona",
|
"nowPlaying": "ara sona",
|
||||||
"shared": "$t(entity.playlist, {\"count\": 2}) compartides",
|
"shared": "$t(entity.playlist, {\"count\": 2}) compartides",
|
||||||
"favorites": "$t(entity.favorite, {\"count\": 2})",
|
"favorites": "$t(entity.favorite, {\"count\": 2})",
|
||||||
"radio": "$t(entity.radioStation, {\"count\": 2})"
|
"radio": "$t(entity.radioStation, {\"count\": 2})",
|
||||||
|
"collections": "col·leccions"
|
||||||
},
|
},
|
||||||
"albumArtistDetail": {
|
"albumArtistDetail": {
|
||||||
"relatedArtists": "$t(entity.artist, {\"count\": 2}) similars",
|
"relatedArtists": "$t(entity.artist, {\"count\": 2}) similars",
|
||||||
@@ -28,7 +29,11 @@
|
|||||||
"topSongsFrom": "les millors cançons de {{title}}",
|
"topSongsFrom": "les millors cançons de {{title}}",
|
||||||
"viewAll": "mostra-ho tot",
|
"viewAll": "mostra-ho tot",
|
||||||
"groupingTypeAll": "tots els tipus de llançaments",
|
"groupingTypeAll": "tots els tipus de llançaments",
|
||||||
"groupingTypePrimary": "tipus principals de llançament"
|
"groupingTypePrimary": "tipus principals de llançament",
|
||||||
|
"favoriteSongs": "Cançons preferides",
|
||||||
|
"topSongsCommunity": "comunitat",
|
||||||
|
"topSongsPersonal": "personal",
|
||||||
|
"favoriteSongsFrom": "cançons preferides de {{title}}"
|
||||||
},
|
},
|
||||||
"albumArtistList": {
|
"albumArtistList": {
|
||||||
"title": "$t(entity.albumArtist, {\"count\": 2})"
|
"title": "$t(entity.albumArtist, {\"count\": 2})"
|
||||||
@@ -191,6 +196,19 @@
|
|||||||
},
|
},
|
||||||
"radioList": {
|
"radioList": {
|
||||||
"title": "emissores de ràdio"
|
"title": "emissores de ràdio"
|
||||||
|
},
|
||||||
|
"windowBar": {
|
||||||
|
"paused": "(en pausa) ",
|
||||||
|
"privateMode": "(mode privat)"
|
||||||
|
},
|
||||||
|
"collections": {
|
||||||
|
"overrideExisting": "sobreescriu existents",
|
||||||
|
"saveAsCollection": "desa com a col·lecció"
|
||||||
|
},
|
||||||
|
"releasenotes": {
|
||||||
|
"commitsSinceStable": "commits des de {{stable}}",
|
||||||
|
"noNewCommits": "no hi ha hagut commits en aquest període",
|
||||||
|
"noStableReleaseToCompare": "no hi ha actualitzacions disponibles amb les quals comparar"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -289,8 +307,8 @@
|
|||||||
"restartRequired": "cal reiniciar",
|
"restartRequired": "cal reiniciar",
|
||||||
"sampleRate": "freqüència de mostreig",
|
"sampleRate": "freqüència de mostreig",
|
||||||
"setting_one": "configuració",
|
"setting_one": "configuració",
|
||||||
"setting_many": "",
|
"setting_many": "configuracions",
|
||||||
"setting_other": "",
|
"setting_other": "configuracions",
|
||||||
"trackGain": "guany de pista",
|
"trackGain": "guany de pista",
|
||||||
"trackPeak": "pic de pista",
|
"trackPeak": "pic de pista",
|
||||||
"gap": "espera",
|
"gap": "espera",
|
||||||
@@ -316,7 +334,8 @@
|
|||||||
"example": "exemple",
|
"example": "exemple",
|
||||||
"mood": "estat d'ànim",
|
"mood": "estat d'ànim",
|
||||||
"filter_single": "senzill",
|
"filter_single": "senzill",
|
||||||
"filter_multiple": "multi"
|
"filter_multiple": "multi",
|
||||||
|
"rename": "reanomena"
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"album_one": "àlbum",
|
"album_one": "àlbum",
|
||||||
@@ -540,7 +559,6 @@
|
|||||||
"fontType_optionBuiltIn": "tipus de lletra integrats",
|
"fontType_optionBuiltIn": "tipus de lletra integrats",
|
||||||
"fontType_optionCustom": "tipus de lletra personalitzats",
|
"fontType_optionCustom": "tipus de lletra personalitzats",
|
||||||
"fontType_optionSystem": "tipus de lletra del sistema",
|
"fontType_optionSystem": "tipus de lletra del sistema",
|
||||||
"disableAutomaticUpdates": "desactivar les actualitzacions automàtiques",
|
|
||||||
"disableLibraryUpdateOnStartup": "desactiva la comprovació de noves versions a l'inici",
|
"disableLibraryUpdateOnStartup": "desactiva la comprovació de noves versions a l'inici",
|
||||||
"homeConfiguration": "configuració de la pàgina d'inici",
|
"homeConfiguration": "configuració de la pàgina d'inici",
|
||||||
"sidebarConfiguration": "configuració de la barra lateral",
|
"sidebarConfiguration": "configuració de la barra lateral",
|
||||||
@@ -770,7 +788,7 @@
|
|||||||
"releaseChannel_optionLatest": "última versió",
|
"releaseChannel_optionLatest": "última versió",
|
||||||
"releaseChannel_optionBeta": "beta",
|
"releaseChannel_optionBeta": "beta",
|
||||||
"releaseChannel": "canal de versions",
|
"releaseChannel": "canal de versions",
|
||||||
"releaseChannel_description": "tria entre versions estables i versions beta per les actualitzacions automàtiques",
|
"releaseChannel_description": "trieu entre versions estables i beta o alfa (diàries) per les actualitzacions automàtiques",
|
||||||
"mediaSession": "activa Media Session",
|
"mediaSession": "activa Media Session",
|
||||||
"mediaSession_description": "activa la integració amb Media Session per mostrar els controls multimèdia i les metadades a l'indicador de volum del sistema i la pantalla de bloqueig",
|
"mediaSession_description": "activa la integració amb Media Session per mostrar els controls multimèdia i les metadades a l'indicador de volum del sistema i la pantalla de bloqueig",
|
||||||
"crossfadeStyle": "estil de fosa encadenada",
|
"crossfadeStyle": "estil de fosa encadenada",
|
||||||
@@ -862,7 +880,21 @@
|
|||||||
"homeFeatureStyle_description": "controla l'estil del carrusel de destacats de l'inici",
|
"homeFeatureStyle_description": "controla l'estil del carrusel de destacats de l'inici",
|
||||||
"homeFeatureStyle": "estil del carrusel de destacats de l'inici",
|
"homeFeatureStyle": "estil del carrusel de destacats de l'inici",
|
||||||
"homeFeatureStyle_optionMultiple": "múltiple",
|
"homeFeatureStyle_optionMultiple": "múltiple",
|
||||||
"homeFeatureStyle_optionSingle": "simple"
|
"homeFeatureStyle_optionSingle": "simple",
|
||||||
|
"enableGridMultiSelect": "activa la selecció múltiple de quadrícula",
|
||||||
|
"enableGridMultiSelect_description": "quan està activada, podeu seleccionar més d'un element en la vista de quadrícula; si feu clic en la imatge d'un element de la quadrícula, accedireu a la pàgina de l'element",
|
||||||
|
"sidebarPlaylistSorting_description": "permet ordenar manualment les llistes de reproducció a la barra lateral arrossegant amb el ratolí en comptes de seguir l'ordre predeterminat del servidor",
|
||||||
|
"sidebarPlaylistSorting": "ordenació de llistes de reproducció de la barra lateral",
|
||||||
|
"sidebarPlaylistListFilterRegex_description": "amaga les llistes de reproducció de la barra lateral que coincideixin amb aquesta expressió regular",
|
||||||
|
"sidebarPlaylistListFilterRegex_placeholder": "ex. ^Mescla diària.*",
|
||||||
|
"sidebarPlaylistListFilterRegex": "regex pel filtre de llistes",
|
||||||
|
"analyticsEnable": "envia analítiques basades en l'ús",
|
||||||
|
"analyticsEnable_description": "s'envien dades d'ús anonimitzades al desenvolupar per ajudar a millorar l'aplicació",
|
||||||
|
"automaticUpdates": "actualitzacions automàtiques",
|
||||||
|
"automaticUpdates_description": "cerca i instal·la actualitzacions automàticament",
|
||||||
|
"releaseChannel_optionAlpha": "alfa (diària)",
|
||||||
|
"blurExplicitImages": "desenfoca imatges explícites",
|
||||||
|
"blurExplicitImages_description": "les caràtules d'àlbums i cançons marcades com a explícites quedaran desenfocades"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"column": {
|
"column": {
|
||||||
@@ -924,7 +956,8 @@
|
|||||||
"alternateRowColors": "colors de fila alternants",
|
"alternateRowColors": "colors de fila alternants",
|
||||||
"horizontalBorders": "vores de fila",
|
"horizontalBorders": "vores de fila",
|
||||||
"rowHoverHighlight": "ressalta en passar el cursor per la fila",
|
"rowHoverHighlight": "ressalta en passar el cursor per la fila",
|
||||||
"verticalBorders": "vores de columna"
|
"verticalBorders": "vores de columna",
|
||||||
|
"showHeader": "mostra l'encapçalament"
|
||||||
},
|
},
|
||||||
"label": {
|
"label": {
|
||||||
"actions": "$t(common.action, {\"count\": 2})",
|
"actions": "$t(common.action, {\"count\": 2})",
|
||||||
@@ -966,7 +999,8 @@
|
|||||||
"view": {
|
"view": {
|
||||||
"table": "taula",
|
"table": "taula",
|
||||||
"grid": "quadrícula",
|
"grid": "quadrícula",
|
||||||
"list": "llista"
|
"list": "llista",
|
||||||
|
"detail": "detall"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1013,7 +1047,8 @@
|
|||||||
"lastPlayed": "última reproducció",
|
"lastPlayed": "última reproducció",
|
||||||
"path": "ruta",
|
"path": "ruta",
|
||||||
"songCount": "nombre de cançons",
|
"songCount": "nombre de cançons",
|
||||||
"explicitStatus": "$t(common.explicitStatus)"
|
"explicitStatus": "$t(common.explicitStatus)",
|
||||||
|
"sortName": "ordena per nom"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
"muted": "silenciat",
|
"muted": "silenciat",
|
||||||
@@ -1054,7 +1089,16 @@
|
|||||||
"restoreQueueFromServer": "restaura la cua del servidor",
|
"restoreQueueFromServer": "restaura la cua del servidor",
|
||||||
"saveQueueToServer": "desa la cua al servidor",
|
"saveQueueToServer": "desa la cua al servidor",
|
||||||
"artistRadio": "ràdio de l'artista",
|
"artistRadio": "ràdio de l'artista",
|
||||||
"trackRadio": "ràdio de la pista"
|
"trackRadio": "ràdio de la pista",
|
||||||
|
"sleepTimer": "temporitzador d'adormir",
|
||||||
|
"sleepTimer_endOfSong": "final de la cançó actual",
|
||||||
|
"sleepTimer_minutes": "{{count}} min",
|
||||||
|
"sleepTimer_hours": "{{count}} h",
|
||||||
|
"sleepTimer_custom": "personalitzat",
|
||||||
|
"sleepTimer_off": "apagat",
|
||||||
|
"sleepTimer_timeRemaining": "queden {{time}}",
|
||||||
|
"sleepTimer_setCustom": "configura el temporitzador",
|
||||||
|
"sleepTimer_cancel": "cancel·la el temporitzador"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"credentialsRequired": "credencials requerides",
|
"credentialsRequired": "credencials requerides",
|
||||||
|
|||||||
@@ -38,7 +38,16 @@
|
|||||||
"restoreQueueFromServer": "obnovit frontu ze serveru",
|
"restoreQueueFromServer": "obnovit frontu ze serveru",
|
||||||
"saveQueueToServer": "uložit frontu na server",
|
"saveQueueToServer": "uložit frontu na server",
|
||||||
"artistRadio": "rádio umělce",
|
"artistRadio": "rádio umělce",
|
||||||
"trackRadio": "rádio skladby"
|
"trackRadio": "rádio skladby",
|
||||||
|
"sleepTimer": "časovač spánku",
|
||||||
|
"sleepTimer_endOfSong": "konec aktuální skladby",
|
||||||
|
"sleepTimer_minutes": "{{count}} min.",
|
||||||
|
"sleepTimer_hours": "{{count}} hod.",
|
||||||
|
"sleepTimer_custom": "vlastní",
|
||||||
|
"sleepTimer_off": "vypnuto",
|
||||||
|
"sleepTimer_timeRemaining": "zbývá {{time}}",
|
||||||
|
"sleepTimer_setCustom": "nastavit časovač",
|
||||||
|
"sleepTimer_cancel": "zrušit časovač"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"crossfadeStyle_description": "vyberte způsob prolnutí u přehrávače zvuku",
|
"crossfadeStyle_description": "vyberte způsob prolnutí u přehrávače zvuku",
|
||||||
@@ -46,7 +55,7 @@
|
|||||||
"hotkey_skipBackward": "přeskočení zpět",
|
"hotkey_skipBackward": "přeskočení zpět",
|
||||||
"replayGainMode_description": "úprava zesílení hlasitosti podle hodnot {{ReplayGain}} uložených v metadatech souborů",
|
"replayGainMode_description": "úprava zesílení hlasitosti podle hodnot {{ReplayGain}} uložených v metadatech souborů",
|
||||||
"volumeWheelStep_description": "počet procent, o které má být hlasitost posunuta při přejetí kolečkem myši na posuvníku hlasitosti",
|
"volumeWheelStep_description": "počet procent, o které má být hlasitost posunuta při přejetí kolečkem myši na posuvníku hlasitosti",
|
||||||
"audioDevice_description": "vyberte zvukové zařízení k přehrávání (pouze webový přehrávač)",
|
"audioDevice_description": "vyberte zvukové zařízení k přehrávání",
|
||||||
"theme_description": "nastavení motivu použitého v aplikaci",
|
"theme_description": "nastavení motivu použitého v aplikaci",
|
||||||
"hotkey_playbackPause": "pozastavení",
|
"hotkey_playbackPause": "pozastavení",
|
||||||
"replayGainFallback": "fallback {{ReplayGain}}",
|
"replayGainFallback": "fallback {{ReplayGain}}",
|
||||||
@@ -98,7 +107,6 @@
|
|||||||
"hotkey_globalSearch": "globální vyhledávání",
|
"hotkey_globalSearch": "globální vyhledávání",
|
||||||
"gaplessAudio_description": "nastavení přehrávače mpv pro přehrávání bez mezer",
|
"gaplessAudio_description": "nastavení přehrávače mpv pro přehrávání bez mezer",
|
||||||
"remoteUsername_description": "nastavení uživatelského jména pro server vzdáleného ovládání. pokud je jméno i heslo prázdné, bude autentifikace zakázána",
|
"remoteUsername_description": "nastavení uživatelského jména pro server vzdáleného ovládání. pokud je jméno i heslo prázdné, bude autentifikace zakázána",
|
||||||
"disableAutomaticUpdates": "vypnout automatické aktualizace",
|
|
||||||
"exitToTray_description": "ukončit aplikaci do systémové lišty",
|
"exitToTray_description": "ukončit aplikaci do systémové lišty",
|
||||||
"followLyric_description": "přesouvat texty s aktuální pozicí přehrávání",
|
"followLyric_description": "přesouvat texty s aktuální pozicí přehrávání",
|
||||||
"hotkey_favoritePreviousSong": "oblíbit $t(common.previousSong)",
|
"hotkey_favoritePreviousSong": "oblíbit $t(common.previousSong)",
|
||||||
@@ -262,7 +270,7 @@
|
|||||||
"neteaseTranslation_description": "Pokud je povoleno, načte a zobrazí přeložené texty ze služby NetEase, pokud jsou dostupné",
|
"neteaseTranslation_description": "Pokud je povoleno, načte a zobrazí přeložené texty ze služby NetEase, pokud jsou dostupné",
|
||||||
"preferLocalLyrics": "preferovat místní texty",
|
"preferLocalLyrics": "preferovat místní texty",
|
||||||
"preferLocalLyrics_description": "preferovat místní texty před vzdálenými, pokud jsou dostupné",
|
"preferLocalLyrics_description": "preferovat místní texty před vzdálenými, pokud jsou dostupné",
|
||||||
"discordPausedStatus": "zobrazit rich presence při pozastavení",
|
"discordPausedStatus": "zobrazit stav při pozastavení",
|
||||||
"discordPausedStatus_description": "pokud je povoleno, bude při pozastavení přehrávače zobrazen stav",
|
"discordPausedStatus_description": "pokud je povoleno, bude při pozastavení přehrávače zobrazen stav",
|
||||||
"preservePitch": "zachovat výšku",
|
"preservePitch": "zachovat výšku",
|
||||||
"preservePitch_description": "zachová výšku při úpravě rychlosti přehrávání",
|
"preservePitch_description": "zachová výšku při úpravě rychlosti přehrávání",
|
||||||
@@ -284,7 +292,7 @@
|
|||||||
"releaseChannel_optionLatest": "nejnovější",
|
"releaseChannel_optionLatest": "nejnovější",
|
||||||
"releaseChannel_optionBeta": "beta",
|
"releaseChannel_optionBeta": "beta",
|
||||||
"releaseChannel": "kanál vydání",
|
"releaseChannel": "kanál vydání",
|
||||||
"releaseChannel_description": "vyberte si mezi stabilními vydáními nebo beta vydáními pro automatické aktualizace",
|
"releaseChannel_description": "vyberte si mezi stabilními, beta nebo alpha (nočními) vydáními pro automatické aktualizace",
|
||||||
"mediaSession": "povolit relaci médií",
|
"mediaSession": "povolit relaci médií",
|
||||||
"mediaSession_description": "povolí integraci do služby Media Session, což zobrazí ovládání a metadata médií v překrytí systémové hlasitosti a na zamykací obrazovce",
|
"mediaSession_description": "povolí integraci do služby Media Session, což zobrazí ovládání a metadata médií v překrytí systémové hlasitosti a na zamykací obrazovce",
|
||||||
"exportImportSettings_control_description": "exportovat a importovat nastavení pomocí souboru JSON",
|
"exportImportSettings_control_description": "exportovat a importovat nastavení pomocí souboru JSON",
|
||||||
@@ -380,7 +388,19 @@
|
|||||||
"enableGridMultiSelect": "povolit vícenásobný výběr v mřížce",
|
"enableGridMultiSelect": "povolit vícenásobný výběr v mřížce",
|
||||||
"enableGridMultiSelect_description": "pokud je povoleno, umožňuje vybrat několik položek v zobrazení mřížky. pokud je zakázáno, kliknutím na obrázek položky mřížky přejdete na stránku položky",
|
"enableGridMultiSelect_description": "pokud je povoleno, umožňuje vybrat několik položek v zobrazení mřížky. pokud je zakázáno, kliknutím na obrázek položky mřížky přejdete na stránku položky",
|
||||||
"sidebarPlaylistSorting_description": "umožňuje ruční řazení seznamů skladeb v postranní liště pomocí přetažení namísto výchozího pořadí serveru",
|
"sidebarPlaylistSorting_description": "umožňuje ruční řazení seznamů skladeb v postranní liště pomocí přetažení namísto výchozího pořadí serveru",
|
||||||
"sidebarPlaylistSorting": "řazení seznamů skladeb v postranní liště"
|
"sidebarPlaylistSorting": "řazení seznamů skladeb v postranní liště",
|
||||||
|
"blurExplicitImages": "rozostřit explicitní obrázky",
|
||||||
|
"blurExplicitImages_description": "obaly alb a skladeb označené jako explicitní budou rozostřeny",
|
||||||
|
"sidebarPlaylistListFilterRegex_description": "v postranní liště skrýt seznamy skladeb, které odpovídají tomuto regulárnímu výrazu",
|
||||||
|
"sidebarPlaylistListFilterRegex_placeholder": "např. ^Denní mix.*",
|
||||||
|
"sidebarPlaylistListFilterRegex": "regulární výraz filtru seznamů skladeb",
|
||||||
|
"releaseChannel_optionAlpha": "alpha (noční)",
|
||||||
|
"analyticsEnable": "Posílat analytiku založenou na využití",
|
||||||
|
"analyticsEnable_description": "Anonymizovaná data o používání jsou odesílána vývojáři za účelem zlepšení aplikace",
|
||||||
|
"automaticUpdates": "Automatické aktualizace",
|
||||||
|
"automaticUpdates_description": "Kontrolovat a automaticky instalovat aktualizace",
|
||||||
|
"discordStateIcon": "zobrazit ikonu přehrávání",
|
||||||
|
"discordStateIcon_description": "zobrazit malou ikonu přehrávání ve stavu na Discordu. ikona pozastavení bude zobrazena vždy, když je povolena možnost „Zobrazit stav při pozastavení“"
|
||||||
},
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"editPlaylist": "upravit $t(entity.playlist, {\"count\": 1})",
|
"editPlaylist": "upravit $t(entity.playlist, {\"count\": 1})",
|
||||||
@@ -553,7 +573,8 @@
|
|||||||
"view": {
|
"view": {
|
||||||
"table": "tabulka",
|
"table": "tabulka",
|
||||||
"list": "seznam",
|
"list": "seznam",
|
||||||
"grid": "mřížka"
|
"grid": "mřížka",
|
||||||
|
"detail": "podrobnosti"
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"displayType": "typ zobrazení",
|
"displayType": "typ zobrazení",
|
||||||
@@ -899,7 +920,11 @@
|
|||||||
"viewAllTracks": "zobrazit všechny $t(entity.track, {\"count\": 2})",
|
"viewAllTracks": "zobrazit všechny $t(entity.track, {\"count\": 2})",
|
||||||
"viewAll": "zobrazit vše",
|
"viewAll": "zobrazit vše",
|
||||||
"groupingTypeAll": "všechny typy vydání",
|
"groupingTypeAll": "všechny typy vydání",
|
||||||
"groupingTypePrimary": "primární typy vydání"
|
"groupingTypePrimary": "primární typy vydání",
|
||||||
|
"favoriteSongs": "oblíbené skladby",
|
||||||
|
"topSongsCommunity": "komunita",
|
||||||
|
"topSongsPersonal": "osobní",
|
||||||
|
"favoriteSongsFrom": "oblíbené skladby od umělce {{title}}"
|
||||||
},
|
},
|
||||||
"itemDetail": {
|
"itemDetail": {
|
||||||
"copiedPath": "cesta úspěšně zkopírována",
|
"copiedPath": "cesta úspěšně zkopírována",
|
||||||
@@ -933,6 +958,11 @@
|
|||||||
"collections": {
|
"collections": {
|
||||||
"overrideExisting": "nahradit existující",
|
"overrideExisting": "nahradit existující",
|
||||||
"saveAsCollection": "uložit jako sbírku"
|
"saveAsCollection": "uložit jako sbírku"
|
||||||
|
},
|
||||||
|
"releasenotes": {
|
||||||
|
"commitsSinceStable": "revize od {{stable}}",
|
||||||
|
"noNewCommits": "žádné nové revize v tomto období",
|
||||||
|
"noStableReleaseToCompare": "není dostupné žádné stabilní vydání k porovnání"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
|
|||||||
+1299
-2
File diff suppressed because it is too large
Load Diff
@@ -33,7 +33,8 @@
|
|||||||
"createRadioStation": "$t(entity.radioStation, {\"count\": 1}) erstellen",
|
"createRadioStation": "$t(entity.radioStation, {\"count\": 1}) erstellen",
|
||||||
"deleteRadioStation": "$t(entity.radioStation, {\"count\": 1}) löschen",
|
"deleteRadioStation": "$t(entity.radioStation, {\"count\": 1}) löschen",
|
||||||
"selectAll": "alle auswählen",
|
"selectAll": "alle auswählen",
|
||||||
"openApplicationDirectory": "Anwendungsverzeichnis öffnen"
|
"openApplicationDirectory": "Anwendungsverzeichnis öffnen",
|
||||||
|
"addOrRemoveFromSelection": "Zur Auswahl hinzufügen oder entfernen"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"backward": "zurück",
|
"backward": "zurück",
|
||||||
@@ -571,7 +572,8 @@
|
|||||||
"shared": "$t(entity.playlist, {\"count\": 2}) geteilt",
|
"shared": "$t(entity.playlist, {\"count\": 2}) geteilt",
|
||||||
"myLibrary": "meine bibliothek",
|
"myLibrary": "meine bibliothek",
|
||||||
"favorites": "$t(entity.favorite, {\"count\": 2})",
|
"favorites": "$t(entity.favorite, {\"count\": 2})",
|
||||||
"radio": "$t(entity.radioStation, {\"count\": 2})"
|
"radio": "$t(entity.radioStation, {\"count\": 2})",
|
||||||
|
"collections": "Sammlungen"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"playbackTab": "Wiedergabe",
|
"playbackTab": "Wiedergabe",
|
||||||
@@ -629,7 +631,9 @@
|
|||||||
"topSongs": "Toplieder",
|
"topSongs": "Toplieder",
|
||||||
"relatedArtists": "ähnliche $t(entity.artist, {\"count\": 2})",
|
"relatedArtists": "ähnliche $t(entity.artist, {\"count\": 2})",
|
||||||
"groupingTypeAll": "alle Veröffentlichungsformate",
|
"groupingTypeAll": "alle Veröffentlichungsformate",
|
||||||
"groupingTypePrimary": "primäre Veröffentlichungsformate"
|
"groupingTypePrimary": "primäre Veröffentlichungsformate",
|
||||||
|
"favoriteSongs": "Lieblingssongs",
|
||||||
|
"favoriteSongsFrom": "Liebslingssongs von {{title}}"
|
||||||
},
|
},
|
||||||
"manageServers": {
|
"manageServers": {
|
||||||
"title": "Servers verwalten",
|
"title": "Servers verwalten",
|
||||||
@@ -655,6 +659,13 @@
|
|||||||
},
|
},
|
||||||
"radioList": {
|
"radioList": {
|
||||||
"title": "Radiosender"
|
"title": "Radiosender"
|
||||||
|
},
|
||||||
|
"windowBar": {
|
||||||
|
"paused": "(Pausiert) ",
|
||||||
|
"privateMode": "(Privater Modus)"
|
||||||
|
},
|
||||||
|
"collections": {
|
||||||
|
"saveAsCollection": "Als Sammlung speichern"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
@@ -693,7 +704,8 @@
|
|||||||
"addNextShuffled": "als Nächstes (zufällige Wiedergabe)",
|
"addNextShuffled": "als Nächstes (zufällige Wiedergabe)",
|
||||||
"holdToShuffle": "Halten für Zufallswiedergabe",
|
"holdToShuffle": "Halten für Zufallswiedergabe",
|
||||||
"restoreQueueFromServer": "Wiedergabeliste von Server wiederherstellen",
|
"restoreQueueFromServer": "Wiedergabeliste von Server wiederherstellen",
|
||||||
"saveQueueToServer": "Wiedergabeliste auf Server speichern"
|
"saveQueueToServer": "Wiedergabeliste auf Server speichern",
|
||||||
|
"lyrics": "Songtexte"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"audioDevice_description": "wählen Sie das Audiogerät aus, das für die Wiedergabe verwendet werden soll (nur Webplayer)",
|
"audioDevice_description": "wählen Sie das Audiogerät aus, das für die Wiedergabe verwendet werden soll (nur Webplayer)",
|
||||||
@@ -709,7 +721,6 @@
|
|||||||
"disableLibraryUpdateOnStartup": "beim Start nicht nach neuen Versionen suchen",
|
"disableLibraryUpdateOnStartup": "beim Start nicht nach neuen Versionen suchen",
|
||||||
"discordApplicationId_description": "die Application-ID für {{discord}} Rich Presence (Standard: {{defaultId}})",
|
"discordApplicationId_description": "die Application-ID für {{discord}} Rich Presence (Standard: {{defaultId}})",
|
||||||
"audioPlayer_description": "Wählen Sie den Audioplayer aus, der für die Wiedergabe verwendet werden soll",
|
"audioPlayer_description": "Wählen Sie den Audioplayer aus, der für die Wiedergabe verwendet werden soll",
|
||||||
"disableAutomaticUpdates": "Automatische Updates deaktivieren",
|
|
||||||
"crossfadeDuration_description": "Legt die Dauer der Überblendung fest",
|
"crossfadeDuration_description": "Legt die Dauer der Überblendung fest",
|
||||||
"customFontPath": "Benutzerdefinierter Pfad für Schriftarten",
|
"customFontPath": "Benutzerdefinierter Pfad für Schriftarten",
|
||||||
"crossfadeDuration": "Dauer der Überblendung",
|
"crossfadeDuration": "Dauer der Überblendung",
|
||||||
@@ -974,7 +985,12 @@
|
|||||||
"translationTargetLanguage_description": "die gewünschte Sprache der Übersetzung",
|
"translationTargetLanguage_description": "die gewünschte Sprache der Übersetzung",
|
||||||
"translationTargetLanguage": "Zielsprache der Übersetzung",
|
"translationTargetLanguage": "Zielsprache der Übersetzung",
|
||||||
"queryBuilderCustomFields": "benutzerdefiniertes Feld",
|
"queryBuilderCustomFields": "benutzerdefiniertes Feld",
|
||||||
"queryBuilderCustomFields_inputTag": "Tag"
|
"queryBuilderCustomFields_inputTag": "Tag",
|
||||||
|
"homeFeatureStyle_optionMultiple": "mehrere",
|
||||||
|
"imageResolution": "Bildauflösung",
|
||||||
|
"imageResolution_optionTable": "Tabelle",
|
||||||
|
"imageResolution_optionSidebar": "Seitenleiste",
|
||||||
|
"preservePitch": "Tonhöhe erhalten"
|
||||||
},
|
},
|
||||||
"dragDropZone": {
|
"dragDropZone": {
|
||||||
"error_oneFileOnly": "Bitte wähle nur 1 Datei",
|
"error_oneFileOnly": "Bitte wähle nur 1 Datei",
|
||||||
|
|||||||
Regular → Executable
+33
-4
@@ -236,6 +236,8 @@
|
|||||||
"filter": {
|
"filter": {
|
||||||
"album": "$t(entity.album, {\"count\": 1})",
|
"album": "$t(entity.album, {\"count\": 1})",
|
||||||
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
|
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
|
||||||
|
"matchAnd": "and",
|
||||||
|
"matchOr": "or",
|
||||||
"albumCount": "$t(entity.album, {\"count\": 2}) count",
|
"albumCount": "$t(entity.album, {\"count\": 2}) count",
|
||||||
"artist": "$t(entity.artist, {\"count\": 1})",
|
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||||
"biography": "biography",
|
"biography": "biography",
|
||||||
@@ -418,13 +420,17 @@
|
|||||||
"albumArtistDetail": {
|
"albumArtistDetail": {
|
||||||
"about": "About {{artist}}",
|
"about": "About {{artist}}",
|
||||||
"appearsOn": "appears on",
|
"appearsOn": "appears on",
|
||||||
|
"favoriteSongs": "favorite songs",
|
||||||
"groupingTypeAll": "all release types",
|
"groupingTypeAll": "all release types",
|
||||||
"groupingTypePrimary": "primary release types",
|
"groupingTypePrimary": "primary release types",
|
||||||
"recentReleases": "recent releases",
|
"recentReleases": "recent releases",
|
||||||
"viewDiscography": "view discography",
|
"viewDiscography": "view discography",
|
||||||
"relatedArtists": "related $t(entity.artist, {\"count\": 2})",
|
"relatedArtists": "related $t(entity.artist, {\"count\": 2})",
|
||||||
"topSongs": "top songs",
|
"topSongs": "top songs",
|
||||||
|
"topSongsCommunity": "community",
|
||||||
"topSongsFrom": "top songs from {{title}}",
|
"topSongsFrom": "top songs from {{title}}",
|
||||||
|
"topSongsPersonal": "personal",
|
||||||
|
"favoriteSongsFrom": "favorite songs from {{title}}",
|
||||||
"viewAll": "view all",
|
"viewAll": "view all",
|
||||||
"viewAllTracks": "view all $t(entity.track, {\"count\": 2})"
|
"viewAllTracks": "view all $t(entity.track, {\"count\": 2})"
|
||||||
},
|
},
|
||||||
@@ -444,6 +450,11 @@
|
|||||||
"radioList": {
|
"radioList": {
|
||||||
"title": "radio stations"
|
"title": "radio stations"
|
||||||
},
|
},
|
||||||
|
"releasenotes": {
|
||||||
|
"commitsSinceStable": "commits since {{stable}}",
|
||||||
|
"noNewCommits": "no new commits in this range",
|
||||||
|
"noStableReleaseToCompare": "no stable release available to compare with"
|
||||||
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
"title": "$t(entity.favorite, {\"count\": 2})"
|
"title": "$t(entity.favorite, {\"count\": 2})"
|
||||||
},
|
},
|
||||||
@@ -658,7 +669,16 @@
|
|||||||
"trackRadio": "track radio",
|
"trackRadio": "track radio",
|
||||||
"unfavorite": "unfavorite",
|
"unfavorite": "unfavorite",
|
||||||
"pause": "pause",
|
"pause": "pause",
|
||||||
"viewQueue": "view queue"
|
"viewQueue": "view queue",
|
||||||
|
"sleepTimer": "sleep timer",
|
||||||
|
"sleepTimer_endOfSong": "end of current song",
|
||||||
|
"sleepTimer_minutes": "{{count}} min",
|
||||||
|
"sleepTimer_hours": "{{count}} hr",
|
||||||
|
"sleepTimer_custom": "custom",
|
||||||
|
"sleepTimer_off": "off",
|
||||||
|
"sleepTimer_timeRemaining": "{{time}} remaining",
|
||||||
|
"sleepTimer_setCustom": "set timer",
|
||||||
|
"sleepTimer_cancel": "cancel timer"
|
||||||
},
|
},
|
||||||
"queryBuilder": {
|
"queryBuilder": {
|
||||||
"standardTags": "standard tags",
|
"standardTags": "standard tags",
|
||||||
@@ -704,6 +724,8 @@
|
|||||||
"albumBackgroundBlur": "album background image blur size",
|
"albumBackgroundBlur": "album background image blur size",
|
||||||
"analyticsDisable": "Opt-out of usage based analytics",
|
"analyticsDisable": "Opt-out of usage based analytics",
|
||||||
"analyticsDisable_description": "Anonymized usage data is sent to the developer to help improve the application",
|
"analyticsDisable_description": "Anonymized usage data is sent to the developer to help improve the application",
|
||||||
|
"analyticsEnable": "Send usage-based analytics",
|
||||||
|
"analyticsEnable_description": "Anonymized usage data is sent to the developer to help improve the application",
|
||||||
"applicationHotkeys_description": "configure application hotkeys. toggle the checkbox to set as a global hotkey (desktop only)",
|
"applicationHotkeys_description": "configure application hotkeys. toggle the checkbox to set as a global hotkey (desktop only)",
|
||||||
"applicationHotkeys": "application hotkeys",
|
"applicationHotkeys": "application hotkeys",
|
||||||
"artistBackground": "artist background image",
|
"artistBackground": "artist background image",
|
||||||
@@ -714,7 +736,7 @@
|
|||||||
"artistConfiguration_description": "configure what items are shown, and in what order, on the album artist page",
|
"artistConfiguration_description": "configure what items are shown, and in what order, on the album artist page",
|
||||||
"artistReleaseTypeConfiguration": "artist release type configuration",
|
"artistReleaseTypeConfiguration": "artist release type configuration",
|
||||||
"artistReleaseTypeConfiguration_description": "configure what release types are shown, and in what order, on the album artist page",
|
"artistReleaseTypeConfiguration_description": "configure what release types are shown, and in what order, on the album artist page",
|
||||||
"audioDevice_description": "select the audio device to use for playback (web player only)",
|
"audioDevice_description": "select the audio device to use for playback",
|
||||||
"audioDevice": "audio device",
|
"audioDevice": "audio device",
|
||||||
"audioExclusiveMode_description": "enable exclusive output mode. In this mode, the system is usually locked out, and only mpv will be able to output audio",
|
"audioExclusiveMode_description": "enable exclusive output mode. In this mode, the system is usually locked out, and only mpv will be able to output audio",
|
||||||
"audioExclusiveMode": "audio exclusive mode",
|
"audioExclusiveMode": "audio exclusive mode",
|
||||||
@@ -740,11 +762,13 @@
|
|||||||
"customCssNotice": "Warning: while there is some sanitization (disallowing url() and content:), using custom css can still pose risks by changing the interface",
|
"customCssNotice": "Warning: while there is some sanitization (disallowing url() and content:), using custom css can still pose risks by changing the interface",
|
||||||
"customFontPath_description": "sets the path to the custom font to use for the application",
|
"customFontPath_description": "sets the path to the custom font to use for the application",
|
||||||
"customFontPath": "custom font path",
|
"customFontPath": "custom font path",
|
||||||
"disableAutomaticUpdates": "disable automatic updates",
|
"automaticUpdates": "Automatic updates",
|
||||||
|
"automaticUpdates_description": "Check for and install updates automatically",
|
||||||
|
"releaseChannel_optionAlpha": "alpha (nightly)",
|
||||||
"releaseChannel_optionBeta": "beta",
|
"releaseChannel_optionBeta": "beta",
|
||||||
"releaseChannel_optionLatest": "latest",
|
"releaseChannel_optionLatest": "latest",
|
||||||
"releaseChannel": "release channel",
|
"releaseChannel": "release channel",
|
||||||
"releaseChannel_description": "choose between stable releases or beta releases for automatic updates",
|
"releaseChannel_description": "choose between stable, beta, or alpha (nightly) releases for automatic updates",
|
||||||
"disableLibraryUpdateOnStartup": "disable checking for new versions on startup",
|
"disableLibraryUpdateOnStartup": "disable checking for new versions on startup",
|
||||||
"discordApplicationId_description": "the application id for {{discord}} rich presence (defaults to {{defaultId}})",
|
"discordApplicationId_description": "the application id for {{discord}} rich presence (defaults to {{defaultId}})",
|
||||||
"discordApplicationId": "{{discord}} application id",
|
"discordApplicationId": "{{discord}} application id",
|
||||||
@@ -766,6 +790,8 @@
|
|||||||
"discordRichPresence_description": "enable playback status in {{discord}} rich presence. Image keys are: {{icon}}, {{playing}}, and {{paused}}",
|
"discordRichPresence_description": "enable playback status in {{discord}} rich presence. Image keys are: {{icon}}, {{playing}}, and {{paused}}",
|
||||||
"discordServeImage": "serve {{discord}} images from server",
|
"discordServeImage": "serve {{discord}} images from server",
|
||||||
"discordServeImage_description": "share cover art for {{discord}} rich presence from server itself, only available for Jellyfin and Navidrome. {{discord}} uses a bot to fetch images, so your server must be reachable from the public internet",
|
"discordServeImage_description": "share cover art for {{discord}} rich presence from server itself, only available for Jellyfin and Navidrome. {{discord}} uses a bot to fetch images, so your server must be reachable from the public internet",
|
||||||
|
"discordStateIcon": "show playing icon",
|
||||||
|
"discordStateIcon_description": "show a small playing icon in the rich presence status. the paused icon is always shown when \"Show rich presence when paused\" is enabled",
|
||||||
"discordUpdateInterval": "{{discord}} rich presence update interval",
|
"discordUpdateInterval": "{{discord}} rich presence update interval",
|
||||||
"discordUpdateInterval_description": "the time in seconds between each update (minimum 15 seconds)",
|
"discordUpdateInterval_description": "the time in seconds between each update (minimum 15 seconds)",
|
||||||
"enableAutoTranslation_description": "enable translation automatically when lyrics are loaded",
|
"enableAutoTranslation_description": "enable translation automatically when lyrics are loaded",
|
||||||
@@ -932,6 +958,8 @@
|
|||||||
"showLyricsInSidebar": "show lyrics in player sidebar",
|
"showLyricsInSidebar": "show lyrics in player sidebar",
|
||||||
"showRatings_description": "controls if the star ratings feature shows up in the interface",
|
"showRatings_description": "controls if the star ratings feature shows up in the interface",
|
||||||
"showRatings": "show star ratings",
|
"showRatings": "show star ratings",
|
||||||
|
"blurExplicitImages": "blur explicit images",
|
||||||
|
"blurExplicitImages_description": "album and song artwork tagged as explicit will be blurred",
|
||||||
"enableGridMultiSelect": "enable grid multi-select",
|
"enableGridMultiSelect": "enable grid multi-select",
|
||||||
"enableGridMultiSelect_description": "when enabled, allows selecting multiple items in grid views. when disabled, clicking grid item images will navigate to the item page",
|
"enableGridMultiSelect_description": "when enabled, allows selecting multiple items in grid views. when disabled, clicking grid item images will navigate to the item page",
|
||||||
"showVisualizerInSidebar_description": "a panel will be added to the player sidebar that displays the visualizer",
|
"showVisualizerInSidebar_description": "a panel will be added to the player sidebar that displays the visualizer",
|
||||||
@@ -1133,6 +1161,7 @@
|
|||||||
"year": "$t(common.year)"
|
"year": "$t(common.year)"
|
||||||
},
|
},
|
||||||
"view": {
|
"view": {
|
||||||
|
"detail": "detail",
|
||||||
"grid": "grid",
|
"grid": "grid",
|
||||||
"list": "list",
|
"list": "list",
|
||||||
"table": "table"
|
"table": "table"
|
||||||
|
|||||||
+54
-24
@@ -32,13 +32,22 @@
|
|||||||
"playSimilarSongs": "Reproducir canciones similares",
|
"playSimilarSongs": "Reproducir canciones similares",
|
||||||
"viewQueue": "ver cola",
|
"viewQueue": "ver cola",
|
||||||
"addLastShuffled": "Al final (mezclado)",
|
"addLastShuffled": "Al final (mezclado)",
|
||||||
"addNextShuffled": "Al siguiente (mezclado)",
|
"addNextShuffled": "Siguiente (mezclado)",
|
||||||
"holdToShuffle": "Mantener para mezclar",
|
"holdToShuffle": "Mantener para mezclar",
|
||||||
"lyrics": "Letras",
|
"lyrics": "Letras",
|
||||||
"restoreQueueFromServer": "Restaurar cola del servidor",
|
"restoreQueueFromServer": "Restaurar cola del servidor",
|
||||||
"saveQueueToServer": "Guardar cola en el servidor",
|
"saveQueueToServer": "Guardar cola en el servidor",
|
||||||
"artistRadio": "Radio de artista",
|
"artistRadio": "Radio de artista",
|
||||||
"trackRadio": "Radio de pista"
|
"trackRadio": "Radio de pista",
|
||||||
|
"sleepTimer_minutes": "{{count}} min",
|
||||||
|
"sleepTimer_hours": "{{count}} h",
|
||||||
|
"sleepTimer_custom": "Personalizado",
|
||||||
|
"sleepTimer_setCustom": "Configurar temporizador",
|
||||||
|
"sleepTimer_cancel": "Cancelar temporizador",
|
||||||
|
"sleepTimer_timeRemaining": "{{time}} restante",
|
||||||
|
"sleepTimer_off": "Apagado",
|
||||||
|
"sleepTimer_endOfSong": "Fin de la canción actual",
|
||||||
|
"sleepTimer": "Temporizador de apagado"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"crossfadeStyle_description": "selecciona el estilo de crossfade a usar por el reproductor de audio",
|
"crossfadeStyle_description": "selecciona el estilo de crossfade a usar por el reproductor de audio",
|
||||||
@@ -95,7 +104,6 @@
|
|||||||
"hotkey_globalSearch": "búsqueda global",
|
"hotkey_globalSearch": "búsqueda global",
|
||||||
"gaplessAudio_description": "establece la configuración de audio sin pausas para mpv",
|
"gaplessAudio_description": "establece la configuración de audio sin pausas para mpv",
|
||||||
"remoteUsername_description": "establece el nombre de usuario para el control remoto del servidor. si el usuario y la contraseña están vacíos, la autenticación será deshabilitada",
|
"remoteUsername_description": "establece el nombre de usuario para el control remoto del servidor. si el usuario y la contraseña están vacíos, la autenticación será deshabilitada",
|
||||||
"disableAutomaticUpdates": "desactiva las actualizaciones automáticas",
|
|
||||||
"exitToTray_description": "sale de la aplicación a la bandeja del sistema",
|
"exitToTray_description": "sale de la aplicación a la bandeja del sistema",
|
||||||
"followLyric_description": "desplaza la letra a la posición de reproducción actual",
|
"followLyric_description": "desplaza la letra a la posición de reproducción actual",
|
||||||
"hotkey_favoritePreviousSong": "$t(common.previousSong) favorita",
|
"hotkey_favoritePreviousSong": "$t(common.previousSong) favorita",
|
||||||
@@ -160,7 +168,7 @@
|
|||||||
"customFontPath": "ruta de fuente personalizada",
|
"customFontPath": "ruta de fuente personalizada",
|
||||||
"followLyric": "seguir la letra actual",
|
"followLyric": "seguir la letra actual",
|
||||||
"crossfadeDuration": "duración del crossfade",
|
"crossfadeDuration": "duración del crossfade",
|
||||||
"discordIdleStatus": "mostrar el estado inactivo en el estado de actividad",
|
"discordIdleStatus": "mostrar estado inactivo en el estado de actividad",
|
||||||
"sidePlayQueueStyle_optionDetached": "separada",
|
"sidePlayQueueStyle_optionDetached": "separada",
|
||||||
"audioPlayer": "reproductor de audio",
|
"audioPlayer": "reproductor de audio",
|
||||||
"hotkey_zoomOut": "reducir",
|
"hotkey_zoomOut": "reducir",
|
||||||
@@ -283,7 +291,7 @@
|
|||||||
"releaseChannel_optionLatest": "Última versión",
|
"releaseChannel_optionLatest": "Última versión",
|
||||||
"releaseChannel_optionBeta": "Beta",
|
"releaseChannel_optionBeta": "Beta",
|
||||||
"releaseChannel": "Canal de lanzamiento",
|
"releaseChannel": "Canal de lanzamiento",
|
||||||
"releaseChannel_description": "Elige entre lanzamientos estables o beta para las actualizaciones automáticas",
|
"releaseChannel_description": "Elige entre lanzamientos estables, beta, o alpha (nightly) para las actualizaciones automáticas",
|
||||||
"artistBackground_description": "Añade una imagen de fondo para las páginas de artista que contienen el arte del artista",
|
"artistBackground_description": "Añade una imagen de fondo para las páginas de artista que contienen el arte del artista",
|
||||||
"mediaSession": "Activar sesión de medios",
|
"mediaSession": "Activar sesión de medios",
|
||||||
"mediaSession_description": "Activa la integración de la sesión de medios, mostrando los controles de medios y los metadatos en la superposición del volumen del sistema y en la pantalla de bloqueo",
|
"mediaSession_description": "Activa la integración de la sesión de medios, mostrando los controles de medios y los metadatos en la superposición del volumen del sistema y en la pantalla de bloqueo",
|
||||||
@@ -319,8 +327,8 @@
|
|||||||
"playerbarWaveformRadius": "Radio de la forma de onda",
|
"playerbarWaveformRadius": "Radio de la forma de onda",
|
||||||
"showLyricsInSidebar_description": "Se añadirá un panel a la cola de reproducción acoplada que muestra las letras",
|
"showLyricsInSidebar_description": "Se añadirá un panel a la cola de reproducción acoplada que muestra las letras",
|
||||||
"showLyricsInSidebar": "Mostrar letras en la barra lateral del reproductor",
|
"showLyricsInSidebar": "Mostrar letras en la barra lateral del reproductor",
|
||||||
"showVisualizerInSidebar_description": "Se añadirá un panel a la barra lateral de reproducción que muestra el visualizador",
|
"showVisualizerInSidebar_description": "Se añadirá un panel a la barra lateral del reproductor que muestra el visualizador",
|
||||||
"showVisualizerInSidebar": "Mostrar visualizador en la barra lateral de reproducción",
|
"showVisualizerInSidebar": "Mostrar visualizador en la barra lateral del reproductor",
|
||||||
"queryBuilder": "Generador de consultas",
|
"queryBuilder": "Generador de consultas",
|
||||||
"queryBuilderCustomFields_inputTag": "Etiqueta",
|
"queryBuilderCustomFields_inputTag": "Etiqueta",
|
||||||
"queryBuilderCustomFields": "Campos personalizados",
|
"queryBuilderCustomFields": "Campos personalizados",
|
||||||
@@ -380,7 +388,19 @@
|
|||||||
"enableGridMultiSelect": "Activar selección múltiple de rejilla",
|
"enableGridMultiSelect": "Activar selección múltiple de rejilla",
|
||||||
"enableGridMultiSelect_description": "Cuando está activo, permite seleccionar múltiples elementos en las vistas de rejilla. Cuando está desactivado, hacer clic en las imágenes de los elementos de la rejilla navegará a la página del elemento",
|
"enableGridMultiSelect_description": "Cuando está activo, permite seleccionar múltiples elementos en las vistas de rejilla. Cuando está desactivado, hacer clic en las imágenes de los elementos de la rejilla navegará a la página del elemento",
|
||||||
"sidebarPlaylistSorting": "Ordenación de la lista de reproducción de la barra lateral",
|
"sidebarPlaylistSorting": "Ordenación de la lista de reproducción de la barra lateral",
|
||||||
"sidebarPlaylistSorting_description": "Permite la ordenación manual de la lista de reproducción en la barra lateral usando arrastrar y soltar en lugar del orden predeterminado del servidor"
|
"sidebarPlaylistSorting_description": "Permite la ordenación manual de la lista de reproducción en la barra lateral usando arrastrar y soltar en lugar del orden predeterminado del servidor",
|
||||||
|
"sidebarPlaylistListFilterRegex": "Expresión regular de filtrado de listas de reproducción",
|
||||||
|
"sidebarPlaylistListFilterRegex_description": "Esconde las listas de reproducción en la barra lateral que coincidan con esta expresión regular",
|
||||||
|
"sidebarPlaylistListFilterRegex_placeholder": "p. ej. ^Mezcla diaria.*",
|
||||||
|
"blurExplicitImages": "Desenfocar imágenes explícitas",
|
||||||
|
"blurExplicitImages_description": "El álbum y la carátula de la canción etiquetados como explícitos serán desenfocados",
|
||||||
|
"releaseChannel_optionAlpha": "Alpha (nightly)",
|
||||||
|
"analyticsEnable": "Enviar analíticas basadas en el uso",
|
||||||
|
"analyticsEnable_description": "Se envían datos de uso anonimizados al desarrollador para ayudar a mejorar la aplicación",
|
||||||
|
"automaticUpdates": "Actualizaciones automáticas",
|
||||||
|
"automaticUpdates_description": "Busca e instala actualizaciones automáticamente",
|
||||||
|
"discordStateIcon": "Mostrar icono de reproducción",
|
||||||
|
"discordStateIcon_description": "Muestra un icono pequeño de reproducción en el estado de actividad. El icono de pausa se muestra siempre cuando \"Mostrar estado de actividad cuando esté en pausa\" esté activado"
|
||||||
},
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"editPlaylist": "editar $t(entity.playlist, {\"count\": 1})",
|
"editPlaylist": "editar $t(entity.playlist, {\"count\": 1})",
|
||||||
@@ -426,7 +446,7 @@
|
|||||||
"backward": "hacia atrás",
|
"backward": "hacia atrás",
|
||||||
"increase": "aumentar",
|
"increase": "aumentar",
|
||||||
"rating": "calificación",
|
"rating": "calificación",
|
||||||
"bpm": "lpm",
|
"bpm": "bpm",
|
||||||
"refresh": "actualizar",
|
"refresh": "actualizar",
|
||||||
"unknown": "desconocido",
|
"unknown": "desconocido",
|
||||||
"areYouSure": "seguro?",
|
"areYouSure": "seguro?",
|
||||||
@@ -438,7 +458,7 @@
|
|||||||
"currentSong": "$t(entity.track, {\"count\": 1}) actual",
|
"currentSong": "$t(entity.track, {\"count\": 1}) actual",
|
||||||
"collapse": "contraer",
|
"collapse": "contraer",
|
||||||
"trackNumber": "pista",
|
"trackNumber": "pista",
|
||||||
"descending": "descendiente",
|
"descending": "descendente",
|
||||||
"add": "añadir",
|
"add": "añadir",
|
||||||
"ascending": "ascendente",
|
"ascending": "ascendente",
|
||||||
"dismiss": "descartar",
|
"dismiss": "descartar",
|
||||||
@@ -465,8 +485,8 @@
|
|||||||
"cancel": "cancelar",
|
"cancel": "cancelar",
|
||||||
"forceRestartRequired": "reiniciar para aplicar cambios... cerrar la notificación para reiniciar",
|
"forceRestartRequired": "reiniciar para aplicar cambios... cerrar la notificación para reiniciar",
|
||||||
"setting_one": "configuración",
|
"setting_one": "configuración",
|
||||||
"setting_many": "configuraciones",
|
"setting_many": "configuración",
|
||||||
"setting_other": "configuraciones",
|
"setting_other": "configuración",
|
||||||
"version": "versión",
|
"version": "versión",
|
||||||
"title": "título",
|
"title": "título",
|
||||||
"filters": "filtros",
|
"filters": "filtros",
|
||||||
@@ -580,10 +600,10 @@
|
|||||||
"noNetworkDescription": "No se pudo conectar a este servidor"
|
"noNetworkDescription": "No se pudo conectar a este servidor"
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"mostPlayed": "más reproducido",
|
"mostPlayed": "más reproducidos",
|
||||||
"isCompilation": "es una compilación",
|
"isCompilation": "es una compilación",
|
||||||
"recentlyPlayed": "recientemente reproducido",
|
"recentlyPlayed": "recientemente reproducido",
|
||||||
"isRated": "es clasificado",
|
"isRated": "Está calificado",
|
||||||
"title": "título",
|
"title": "título",
|
||||||
"rating": "calificación",
|
"rating": "calificación",
|
||||||
"search": "buscar",
|
"search": "buscar",
|
||||||
@@ -599,7 +619,7 @@
|
|||||||
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
|
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
|
||||||
"isRecentlyPlayed": "reproducido recientemente",
|
"isRecentlyPlayed": "reproducido recientemente",
|
||||||
"isFavorited": "es favorito",
|
"isFavorited": "es favorito",
|
||||||
"bpm": "lpm",
|
"bpm": "bpm",
|
||||||
"releaseYear": "año de lanzamiento",
|
"releaseYear": "año de lanzamiento",
|
||||||
"disc": "disco",
|
"disc": "disco",
|
||||||
"biography": "biografía",
|
"biography": "biografía",
|
||||||
@@ -618,10 +638,10 @@
|
|||||||
"owner": "$t(common.owner)",
|
"owner": "$t(common.owner)",
|
||||||
"genre": "$t(entity.genre, {\"count\": 1})",
|
"genre": "$t(entity.genre, {\"count\": 1})",
|
||||||
"id": "id",
|
"id": "id",
|
||||||
"songCount": "número de canción",
|
"songCount": "número de canciones",
|
||||||
"isPublic": "es público",
|
"isPublic": "es público",
|
||||||
"album": "$t(entity.album, {\"count\": 1})",
|
"album": "$t(entity.album, {\"count\": 1})",
|
||||||
"albumCount": "Contar $t(entity.album, {\"count\": 2})",
|
"albumCount": "Número de $t(entity.album, {\"count\": 2})",
|
||||||
"explicitStatus": "$t(common.explicitStatus)",
|
"explicitStatus": "$t(common.explicitStatus)",
|
||||||
"sortName": "Ordenar por nombre"
|
"sortName": "Ordenar por nombre"
|
||||||
},
|
},
|
||||||
@@ -792,7 +812,11 @@
|
|||||||
"about": "Sobre {{artist}}",
|
"about": "Sobre {{artist}}",
|
||||||
"appearsOn": "Aparece en",
|
"appearsOn": "Aparece en",
|
||||||
"groupingTypeAll": "Todos los tipos de lanzamiento",
|
"groupingTypeAll": "Todos los tipos de lanzamiento",
|
||||||
"groupingTypePrimary": "Tipos de lanzamiento principales"
|
"groupingTypePrimary": "Tipos de lanzamiento principales",
|
||||||
|
"favoriteSongs": "Canciones favoritas",
|
||||||
|
"favoriteSongsFrom": "Canciones favoritas de {{title}}",
|
||||||
|
"topSongsPersonal": "Personal",
|
||||||
|
"topSongsCommunity": "Comunidad"
|
||||||
},
|
},
|
||||||
"itemDetail": {
|
"itemDetail": {
|
||||||
"copiedPath": "Ruta copiada correctamente",
|
"copiedPath": "Ruta copiada correctamente",
|
||||||
@@ -826,6 +850,11 @@
|
|||||||
"collections": {
|
"collections": {
|
||||||
"overrideExisting": "Sobreescribir existente",
|
"overrideExisting": "Sobreescribir existente",
|
||||||
"saveAsCollection": "Guardar como colección"
|
"saveAsCollection": "Guardar como colección"
|
||||||
|
},
|
||||||
|
"releasenotes": {
|
||||||
|
"commitsSinceStable": "Actualizaciones desde {{stable}}",
|
||||||
|
"noNewCommits": "Ninguna nueva actualización en este rango",
|
||||||
|
"noStableReleaseToCompare": "Ningún lanzamiento estable disponible con el que comparar"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
@@ -851,8 +880,8 @@
|
|||||||
"input_name": "nombre del servidor",
|
"input_name": "nombre del servidor",
|
||||||
"success": "servidor añadido correctamente",
|
"success": "servidor añadido correctamente",
|
||||||
"input_savePassword": "guardar contraseña",
|
"input_savePassword": "guardar contraseña",
|
||||||
"ignoreSsl": "ignorar ssl ($t(common.restartRequired))",
|
"ignoreSsl": "Ignorar SSL ($t(common.restartRequired))",
|
||||||
"ignoreCors": "ignorar cors ($t(common.restartRequired))",
|
"ignoreCors": "Ignorar CORS ($t(common.restartRequired))",
|
||||||
"error_savePassword": "un error ocurrió cuando se intentó guardar la contraseña",
|
"error_savePassword": "un error ocurrió cuando se intentó guardar la contraseña",
|
||||||
"input_preferInstantMix": "Preferir mix instantáneo",
|
"input_preferInstantMix": "Preferir mix instantáneo",
|
||||||
"input_preferInstantMixDescription": "Usa solo el mix instantáneo para obtener canciones similares. Útil si tienes complementos que modifican este comportamiento",
|
"input_preferInstantMixDescription": "Usa solo el mix instantáneo para obtener canciones similares. Útil si tienes complementos que modifican este comportamiento",
|
||||||
@@ -950,7 +979,7 @@
|
|||||||
"releaseDate": "fecha de lanzamiento",
|
"releaseDate": "fecha de lanzamiento",
|
||||||
"bitrate": "tasa de bits",
|
"bitrate": "tasa de bits",
|
||||||
"title": "título",
|
"title": "título",
|
||||||
"bpm": "lpm",
|
"bpm": "bpm",
|
||||||
"dateAdded": "fecha de adición",
|
"dateAdded": "fecha de adición",
|
||||||
"artist": "$t(entity.artist, {\"count\": 1})",
|
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||||
"songCount": "$t(entity.track, {\"count\": 2})",
|
"songCount": "$t(entity.track, {\"count\": 2})",
|
||||||
@@ -1015,8 +1044,8 @@
|
|||||||
"followCurrentSong": "seguir la canción actual",
|
"followCurrentSong": "seguir la canción actual",
|
||||||
"advancedSettings": "Opciones avanzadas",
|
"advancedSettings": "Opciones avanzadas",
|
||||||
"autosize": "Autodimensionar",
|
"autosize": "Autodimensionar",
|
||||||
"moveUp": "Ascender",
|
"moveUp": "Subir",
|
||||||
"moveDown": "Descender",
|
"moveDown": "Bajar",
|
||||||
"pinToLeft": "Anclar a la izquierda",
|
"pinToLeft": "Anclar a la izquierda",
|
||||||
"pinToRight": "Anclar a la derecha",
|
"pinToRight": "Anclar a la derecha",
|
||||||
"alignLeft": "Alinear a la izquierda",
|
"alignLeft": "Alinear a la izquierda",
|
||||||
@@ -1039,7 +1068,8 @@
|
|||||||
"view": {
|
"view": {
|
||||||
"table": "tabla",
|
"table": "tabla",
|
||||||
"list": "Lista",
|
"list": "Lista",
|
||||||
"grid": "Cuadrícula"
|
"grid": "Cuadrícula",
|
||||||
|
"detail": "Detalle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -419,7 +419,6 @@
|
|||||||
"customCss": "css pertsonalizatua",
|
"customCss": "css pertsonalizatua",
|
||||||
"customFontPath": "letra-tipo pertsonalizatuaren bidea",
|
"customFontPath": "letra-tipo pertsonalizatuaren bidea",
|
||||||
"customFontPath_description": "aplikazioan erabiliko den letra-tipo pertsonalizatuaren bidea ezartzen du",
|
"customFontPath_description": "aplikazioan erabiliko den letra-tipo pertsonalizatuaren bidea ezartzen du",
|
||||||
"disableAutomaticUpdates": "desgaitu eguneratze automatikoak",
|
|
||||||
"discordApplicationId": "{{discord}} aplikazioaren IDa",
|
"discordApplicationId": "{{discord}} aplikazioaren IDa",
|
||||||
"followLyric": "jarraitu uneko letra",
|
"followLyric": "jarraitu uneko letra",
|
||||||
"font_description": "aplikazioan erabiliko den letra-tipoa ezartzen du",
|
"font_description": "aplikazioan erabiliko den letra-tipoa ezartzen du",
|
||||||
|
|||||||
@@ -76,7 +76,6 @@
|
|||||||
"hotkey_volumeDown": "کم کردن صدا",
|
"hotkey_volumeDown": "کم کردن صدا",
|
||||||
"audioPlayer_description": "پخشکنندهٔ صدا را برای پخش انتخاب کنید",
|
"audioPlayer_description": "پخشکنندهٔ صدا را برای پخش انتخاب کنید",
|
||||||
"hotkey_globalSearch": "جست و جوی سراسری",
|
"hotkey_globalSearch": "جست و جوی سراسری",
|
||||||
"disableAutomaticUpdates": "غیرفعال کردن بهروزرسانی خودکار",
|
|
||||||
"exitToTray_description": "خروج از اپلیکیشن به system tray",
|
"exitToTray_description": "خروج از اپلیکیشن به system tray",
|
||||||
"replayGainMode_optionAlbum": "$t(entity.album, {\"count\": 1})",
|
"replayGainMode_optionAlbum": "$t(entity.album, {\"count\": 1})",
|
||||||
"discordUpdateInterval_description": "فاصلهٔ بین هر به روزرسانی به ثانیه (حداقل ۱۵ ثانیه)",
|
"discordUpdateInterval_description": "فاصلهٔ بین هر به روزرسانی به ثانیه (حداقل ۱۵ ثانیه)",
|
||||||
|
|||||||
@@ -375,7 +375,6 @@
|
|||||||
"customCss_description": "mukautettu CSS-sisältö. Huomautus: content- ja etä-URL-osoitteet ovat estettyjä ominaisuuksia. Esikatselu sisällöstäsi on alla. Lisäkenttiä, joita et ole määrittänyt, on näkyvissä puhdistuksen vuoksi",
|
"customCss_description": "mukautettu CSS-sisältö. Huomautus: content- ja etä-URL-osoitteet ovat estettyjä ominaisuuksia. Esikatselu sisällöstäsi on alla. Lisäkenttiä, joita et ole määrittänyt, on näkyvissä puhdistuksen vuoksi",
|
||||||
"customCssNotice": "Varoitus: vaikka jonkinlainen puhdistus onkin tehty (url()- ja content:-komentojen estäminen), mukautetun css:n käyttäminen voi silti aiheuttaa riskejä muuttamalla käyttöliittymää",
|
"customCssNotice": "Varoitus: vaikka jonkinlainen puhdistus onkin tehty (url()- ja content:-komentojen estäminen), mukautetun css:n käyttäminen voi silti aiheuttaa riskejä muuttamalla käyttöliittymää",
|
||||||
"disableLibraryUpdateOnStartup": "poista uusimman version tarkistus käynnistyksen yhteydessä käytöstä",
|
"disableLibraryUpdateOnStartup": "poista uusimman version tarkistus käynnistyksen yhteydessä käytöstä",
|
||||||
"disableAutomaticUpdates": "poista automaattiset päivitykset käytöstä",
|
|
||||||
"discordIdleStatus": "näytä rich presencen käyttämätön tila",
|
"discordIdleStatus": "näytä rich presencen käyttämätön tila",
|
||||||
"discordIdleStatus_description": "kun käytössä, päivitä tila kun soitin on käyttämättömänä",
|
"discordIdleStatus_description": "kun käytössä, päivitä tila kun soitin on käyttämättömänä",
|
||||||
"discordUpdateInterval_description": "päivitysväli sekunnteina (vähintään 15 sekunttia)",
|
"discordUpdateInterval_description": "päivitysväli sekunnteina (vähintään 15 sekunttia)",
|
||||||
|
|||||||
+27
-11
@@ -125,7 +125,7 @@
|
|||||||
"forceRestartRequired": "redémarrer pour appliquer les changements… fermer la notification pour redémarrer",
|
"forceRestartRequired": "redémarrer pour appliquer les changements… fermer la notification pour redémarrer",
|
||||||
"setting": "paramètre",
|
"setting": "paramètre",
|
||||||
"setting_one": "paramètre",
|
"setting_one": "paramètre",
|
||||||
"setting_many": "",
|
"setting_many": "paramètres",
|
||||||
"setting_other": "paramètres",
|
"setting_other": "paramètres",
|
||||||
"version": "version",
|
"version": "version",
|
||||||
"title": "titre",
|
"title": "titre",
|
||||||
@@ -202,7 +202,10 @@
|
|||||||
"countSelected": "{{count}} sélectionnée",
|
"countSelected": "{{count}} sélectionnée",
|
||||||
"example": "exemple",
|
"example": "exemple",
|
||||||
"mood": "humeur",
|
"mood": "humeur",
|
||||||
"retry": "réessayer"
|
"retry": "réessayer",
|
||||||
|
"filter_single": "unique",
|
||||||
|
"filter_multiple": "multiple",
|
||||||
|
"rename": "renommer"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"remotePortWarning": "redémarrer le serveur pour appliquer le nouveau port",
|
"remotePortWarning": "redémarrer le serveur pour appliquer le nouveau port",
|
||||||
@@ -270,7 +273,7 @@
|
|||||||
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
|
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
|
||||||
"comment": "commentaire",
|
"comment": "commentaire",
|
||||||
"recentlyUpdated": "mis à jour récemment",
|
"recentlyUpdated": "mis à jour récemment",
|
||||||
"channels": "$t(common.channel_other)",
|
"channels": "$t(common.channel, {\"count\": 2})",
|
||||||
"owner": "$t(common.owner)",
|
"owner": "$t(common.owner)",
|
||||||
"genre": "$t(entity.genre, {\"count\": 1})",
|
"genre": "$t(entity.genre, {\"count\": 1})",
|
||||||
"albumCount": "$t(entity.album, {\"count\": 2}) total",
|
"albumCount": "$t(entity.album, {\"count\": 2}) total",
|
||||||
@@ -278,7 +281,8 @@
|
|||||||
"artist": "$t(entity.artist, {\"count\": 1})",
|
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||||
"isPublic": "est public",
|
"isPublic": "est public",
|
||||||
"album": "$t(entity.album, {\"count\": 1})",
|
"album": "$t(entity.album, {\"count\": 1})",
|
||||||
"explicitStatus": "$t(common.explicitStatus)"
|
"explicitStatus": "$t(common.explicitStatus)",
|
||||||
|
"sortName": "tri par nom"
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
@@ -425,7 +429,7 @@
|
|||||||
"trackList": {
|
"trackList": {
|
||||||
"title": "$t(entity.track, {\"count\": 2})",
|
"title": "$t(entity.track, {\"count\": 2})",
|
||||||
"artistTracks": "pistes par {{artist}}",
|
"artistTracks": "pistes par {{artist}}",
|
||||||
"genreTracks": "'{{genre}}' $t(entity.track, {\"count\": 2})"
|
"genreTracks": "\"{{genre}}\" $t(entity.track, {\"count\": 2})"
|
||||||
},
|
},
|
||||||
"playlistList": {
|
"playlistList": {
|
||||||
"title": "$t(entity.playlist, {\"count\": 2})"
|
"title": "$t(entity.playlist, {\"count\": 2})"
|
||||||
@@ -445,7 +449,12 @@
|
|||||||
"viewDiscography": "voir la discographie",
|
"viewDiscography": "voir la discographie",
|
||||||
"relatedArtists": "$t(entity.artist, {\"count\": 2}) similaires",
|
"relatedArtists": "$t(entity.artist, {\"count\": 2}) similaires",
|
||||||
"topSongs": "meilleurs titres",
|
"topSongs": "meilleurs titres",
|
||||||
"groupingTypeAll": "toutes les types de sortie"
|
"groupingTypeAll": "toutes les types de sortie",
|
||||||
|
"favoriteSongs": "titres préférées",
|
||||||
|
"groupingTypePrimary": "types de parution principale",
|
||||||
|
"topSongsCommunity": "communauté",
|
||||||
|
"topSongsPersonal": "personnel",
|
||||||
|
"favoriteSongsFrom": "meilleurs titres de {{title}}"
|
||||||
},
|
},
|
||||||
"itemDetail": {
|
"itemDetail": {
|
||||||
"copyPath": "copier le chemin dans le presse-papiers",
|
"copyPath": "copier le chemin dans le presse-papiers",
|
||||||
@@ -471,6 +480,14 @@
|
|||||||
},
|
},
|
||||||
"radioList": {
|
"radioList": {
|
||||||
"title": "stations radio"
|
"title": "stations radio"
|
||||||
|
},
|
||||||
|
"releasenotes": {
|
||||||
|
"commitsSinceStable": "commits depuis {{stable}}",
|
||||||
|
"noNewCommits": "pas de nouveaux commits dans cette plage"
|
||||||
|
},
|
||||||
|
"windowBar": {
|
||||||
|
"paused": "(Pause) ",
|
||||||
|
"privateMode": "(Mode Privé)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
@@ -487,7 +504,6 @@
|
|||||||
"applicationHotkeys_description": "configurer les raccourcis clavier d’application. activer la case à cocher pour définir comme raccourci clavier global (bureau uniquement)",
|
"applicationHotkeys_description": "configurer les raccourcis clavier d’application. activer la case à cocher pour définir comme raccourci clavier global (bureau uniquement)",
|
||||||
"crossfadeStyle_description": "sélectionnez le style du fondu enchaîné à utiliser pour le lecteur audio",
|
"crossfadeStyle_description": "sélectionnez le style du fondu enchaîné à utiliser pour le lecteur audio",
|
||||||
"customFontPath": "chemin de police personnalisé",
|
"customFontPath": "chemin de police personnalisé",
|
||||||
"disableAutomaticUpdates": "désactiver les mises à jour automatiques",
|
|
||||||
"customFontPath_description": "définit le chemin de police personnalisé pour l'application",
|
"customFontPath_description": "définit le chemin de police personnalisé pour l'application",
|
||||||
"remotePort_description": "définit le port du serveur de contrôle à distance",
|
"remotePort_description": "définit le port du serveur de contrôle à distance",
|
||||||
"hotkey_skipBackward": "reculer",
|
"hotkey_skipBackward": "reculer",
|
||||||
@@ -717,7 +733,7 @@
|
|||||||
"releaseChannel_optionLatest": "dernière",
|
"releaseChannel_optionLatest": "dernière",
|
||||||
"releaseChannel_optionBeta": "bêta",
|
"releaseChannel_optionBeta": "bêta",
|
||||||
"releaseChannel": "canal de diffusion",
|
"releaseChannel": "canal de diffusion",
|
||||||
"releaseChannel_description": "choisissez entre les versions stables ou les versions bêta pour les mises à jour automatiques",
|
"releaseChannel_description": "choisissez entre les versions stables, bêta, ou alpha (nightly) pour les mises à jour automatiques",
|
||||||
"mediaSession": "activer media session",
|
"mediaSession": "activer media session",
|
||||||
"mediaSession_description": "active l'intégration Media Session, affichant les commandes multimédias et les métadonnées dans la superposition du volume du système et l'écran de verrouillage",
|
"mediaSession_description": "active l'intégration Media Session, affichant les commandes multimédias et les métadonnées dans la superposition du volume du système et l'écran de verrouillage",
|
||||||
"enableAutoTranslation_description": "activer la traduction automatiquement lorsque les paroles sont chargées",
|
"enableAutoTranslation_description": "activer la traduction automatiquement lorsque les paroles sont chargées",
|
||||||
@@ -1022,9 +1038,9 @@
|
|||||||
"artist": "$t(entity.artist, {\"count\": 1})",
|
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||||
"album": "$t(entity.album, {\"count\": 1})",
|
"album": "$t(entity.album, {\"count\": 1})",
|
||||||
"biography": "$t(common.biography)",
|
"biography": "$t(common.biography)",
|
||||||
"channels": "$t(common.channel_other)",
|
"channels": "$t(common.channel, {\"count\": 2})",
|
||||||
"bitrate": "$t(common.bitrate)",
|
"bitrate": "$t(common.bitrate)",
|
||||||
"actions": "$t(common.action_other)",
|
"actions": "$t(common.action, {\"count\": 2})",
|
||||||
"favorite": "$t(common.favorite)",
|
"favorite": "$t(common.favorite)",
|
||||||
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
|
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
|
||||||
"rating": "$t(common.rating)",
|
"rating": "$t(common.rating)",
|
||||||
@@ -1066,7 +1082,7 @@
|
|||||||
"artist": "$t(entity.artist, {\"count\": 1})",
|
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||||
"genre": "$t(entity.genre, {\"count\": 1})",
|
"genre": "$t(entity.genre, {\"count\": 1})",
|
||||||
"songCount": "$t(entity.track, {\"count\": 2})",
|
"songCount": "$t(entity.track, {\"count\": 2})",
|
||||||
"channels": "$t(common.channel_other)",
|
"channels": "$t(common.channel, {\"count\": 2})",
|
||||||
"size": "$t(common.size)",
|
"size": "$t(common.size)",
|
||||||
"codec": "$t(common.codec)",
|
"codec": "$t(common.codec)",
|
||||||
"owner": "propriétaire",
|
"owner": "propriétaire",
|
||||||
|
|||||||
@@ -666,7 +666,6 @@
|
|||||||
"customCss": "egyéni css",
|
"customCss": "egyéni css",
|
||||||
"customCssEnable_description": "lehetővé teszi az egyéni css írását",
|
"customCssEnable_description": "lehetővé teszi az egyéni css írását",
|
||||||
"customCssEnable": "egyéni css engedélyezése",
|
"customCssEnable": "egyéni css engedélyezése",
|
||||||
"disableAutomaticUpdates": "automatikus frissítés kikapcsolása",
|
|
||||||
"customFontPath": "egyéni betűtípus elérési út",
|
"customFontPath": "egyéni betűtípus elérési út",
|
||||||
"customCss_description": "egyéni css tartalom. Megjegyzés: a tartalom és a távoli URL-ek nem megengedett tulajdonságok. A tartalom előnézete az alábbiakban látható. A tisztítás miatt további mezők is megjelennek, amelyeket te nem állítottál be",
|
"customCss_description": "egyéni css tartalom. Megjegyzés: a tartalom és a távoli URL-ek nem megengedett tulajdonságok. A tartalom előnézete az alábbiakban látható. A tisztítás miatt további mezők is megjelennek, amelyeket te nem állítottál be",
|
||||||
"customCssNotice": "Figyelem: bár van némi tisztítás (az url() és a content: használata nem engedélyezett), az egyéni css használata továbbra is kockázatot jelenthet, mivel megváltoztatja a felületet",
|
"customCssNotice": "Figyelem: bár van némi tisztítás (az url() és a content: használata nem engedélyezett), az egyéni css használata továbbra is kockázatot jelenthet, mivel megváltoztatja a felületet",
|
||||||
|
|||||||
@@ -648,7 +648,6 @@
|
|||||||
"customCss_description": "konten CSS kustom. Catatan: properti content dan URL jarak jauh tidak diizinkan. Pratinjau konten Anda ditampilkan di bawah. Kolom tambahan yang tidak Anda atur ada karena sanitasi",
|
"customCss_description": "konten CSS kustom. Catatan: properti content dan URL jarak jauh tidak diizinkan. Pratinjau konten Anda ditampilkan di bawah. Kolom tambahan yang tidak Anda atur ada karena sanitasi",
|
||||||
"customFontPath": "jalur font kustom",
|
"customFontPath": "jalur font kustom",
|
||||||
"customFontPath_description": "tentukan jalur font kustom yang akan digunakan aplikasi",
|
"customFontPath_description": "tentukan jalur font kustom yang akan digunakan aplikasi",
|
||||||
"disableAutomaticUpdates": "nonaktifkan pembaruan otomatis",
|
|
||||||
"discordApplicationId": "ID aplikasi {{discord}}",
|
"discordApplicationId": "ID aplikasi {{discord}}",
|
||||||
"discordApplicationId_description": "id aplikasi untuk rich presence {{discord}} (default: {{defaultId}})",
|
"discordApplicationId_description": "id aplikasi untuk rich presence {{discord}} (default: {{defaultId}})",
|
||||||
"discordIdleStatus": "tampilkan status tidak aktif dalam status aktivitas",
|
"discordIdleStatus": "tampilkan status tidak aktif dalam status aktivitas",
|
||||||
|
|||||||
@@ -202,7 +202,6 @@
|
|||||||
"hotkey_globalSearch": "ricerca globale",
|
"hotkey_globalSearch": "ricerca globale",
|
||||||
"gaplessAudio_description": "imposta l'audio gapless per mpv",
|
"gaplessAudio_description": "imposta l'audio gapless per mpv",
|
||||||
"remoteUsername_description": "imposta l'username del server di controllo remoto. Se username e password sono vuoti, l'autenticazione sarà disattivata",
|
"remoteUsername_description": "imposta l'username del server di controllo remoto. Se username e password sono vuoti, l'autenticazione sarà disattivata",
|
||||||
"disableAutomaticUpdates": "disabilita aggiornamenti automatici",
|
|
||||||
"exitToTray_description": "riduce a icona nella barra di sistema all'uscita",
|
"exitToTray_description": "riduce a icona nella barra di sistema all'uscita",
|
||||||
"followLyric_description": "scorre il testo alla posizione di riproduzione corrente",
|
"followLyric_description": "scorre il testo alla posizione di riproduzione corrente",
|
||||||
"hotkey_favoritePreviousSong": "$t(common.previousSong) preferita",
|
"hotkey_favoritePreviousSong": "$t(common.previousSong) preferita",
|
||||||
|
|||||||
@@ -95,7 +95,6 @@
|
|||||||
"hotkey_globalSearch": "グローバル検索",
|
"hotkey_globalSearch": "グローバル検索",
|
||||||
"gaplessAudio_description": "MPV 向けのギャップレス再生を設定します",
|
"gaplessAudio_description": "MPV 向けのギャップレス再生を設定します",
|
||||||
"remoteUsername_description": "リモートコントロール サーバーのユーザ名を設定します。 ユーザー名とパスワードの両方が空の場合、認証は無効になります",
|
"remoteUsername_description": "リモートコントロール サーバーのユーザ名を設定します。 ユーザー名とパスワードの両方が空の場合、認証は無効になります",
|
||||||
"disableAutomaticUpdates": "自動更新を無効化",
|
|
||||||
"exitToTray_description": "アプリケーション終了ボタンが押された際、システムトレイに格納します",
|
"exitToTray_description": "アプリケーション終了ボタンが押された際、システムトレイに格納します",
|
||||||
"followLyric_description": "現在の再生位置に歌詞をスクロールします",
|
"followLyric_description": "現在の再生位置に歌詞をスクロールします",
|
||||||
"hotkey_favoritePreviousSong": "$t(common.previousSong) をお気に入り",
|
"hotkey_favoritePreviousSong": "$t(common.previousSong) をお気に入り",
|
||||||
@@ -631,10 +630,10 @@
|
|||||||
"genericError": "エラーが発生しました",
|
"genericError": "エラーが発生しました",
|
||||||
"credentialsRequired": "ログイン情報が必要です",
|
"credentialsRequired": "ログイン情報が必要です",
|
||||||
"sessionExpiredError": "セッションの有効期限が切れました",
|
"sessionExpiredError": "セッションの有効期限が切れました",
|
||||||
"remoteEnableError": "リモートサーバーを $t(common.enable) にする際にエラーが発生しました",
|
"remoteEnableError": "リモートサーバーを$t(common.enable)にする際にエラーが発生しました",
|
||||||
"localFontAccessDenied": "ローカルフォントへのアクセスが拒否されました",
|
"localFontAccessDenied": "ローカルフォントへのアクセスが拒否されました",
|
||||||
"serverNotSelectedError": "サーバーが選択されていません",
|
"serverNotSelectedError": "サーバーが選択されていません",
|
||||||
"remoteDisableError": "リモートサーバーを $t(common.disable) にする際にエラーが発生しました",
|
"remoteDisableError": "リモートサーバーを$t(common.disable)にする際にエラーが発生しました",
|
||||||
"mpvRequired": "MPV が必要です",
|
"mpvRequired": "MPV が必要です",
|
||||||
"audioDeviceFetchError": "オーディオデバイスの取得時にエラーが発生しました",
|
"audioDeviceFetchError": "オーディオデバイスの取得時にエラーが発生しました",
|
||||||
"invalidServer": "無効なサーバー",
|
"invalidServer": "無効なサーバー",
|
||||||
@@ -923,8 +922,8 @@
|
|||||||
"input_name": "サーバー名",
|
"input_name": "サーバー名",
|
||||||
"success": "サーバーが追加されました",
|
"success": "サーバーが追加されました",
|
||||||
"input_savePassword": "パスワードを保存",
|
"input_savePassword": "パスワードを保存",
|
||||||
"ignoreSsl": "SSL を無視 ($t(common.restartRequired))",
|
"ignoreSsl": "SSL を無視します ($t(common.restartRequired))",
|
||||||
"ignoreCors": "CORSを無視 ($t(common.restartRequired))",
|
"ignoreCors": "CORS を無視します ($t(common.restartRequired))",
|
||||||
"error_savePassword": "パスワードを保存する際にエラーが発生しました",
|
"error_savePassword": "パスワードを保存する際にエラーが発生しました",
|
||||||
"input_preferInstantMixDescription": "類似曲を取得するにはインスタントミックスのみを使用してください。この動作を変更するプラグインがある場合に役立ちます",
|
"input_preferInstantMixDescription": "類似曲を取得するにはインスタントミックスのみを使用してください。この動作を変更するプラグインがある場合に役立ちます",
|
||||||
"input_preferInstantMix": "インスタントミックスを優先する",
|
"input_preferInstantMix": "インスタントミックスを優先する",
|
||||||
|
|||||||
+348
-17
@@ -119,7 +119,7 @@
|
|||||||
"size": "grootte",
|
"size": "grootte",
|
||||||
"reload": "herlaad",
|
"reload": "herlaad",
|
||||||
"setting_one": "instelling",
|
"setting_one": "instelling",
|
||||||
"setting_other": "",
|
"setting_other": "instellingen",
|
||||||
"close": "sluiten",
|
"close": "sluiten",
|
||||||
"additionalParticipants": "andere deelnemers",
|
"additionalParticipants": "andere deelnemers",
|
||||||
"newVersion": "een nieuwe versie is geïnstalleerd ({{version}})",
|
"newVersion": "een nieuwe versie is geïnstalleerd ({{version}})",
|
||||||
@@ -157,7 +157,9 @@
|
|||||||
"example": "voorbeeld",
|
"example": "voorbeeld",
|
||||||
"mood": "stemming",
|
"mood": "stemming",
|
||||||
"retry": "opnieuw proberen",
|
"retry": "opnieuw proberen",
|
||||||
"filter_single": "single"
|
"filter_single": "single",
|
||||||
|
"rename": "hernoemen",
|
||||||
|
"filter_multiple": "meerdere"
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"rating": "rating",
|
"rating": "rating",
|
||||||
@@ -202,7 +204,8 @@
|
|||||||
"songCount": "aantal nummers",
|
"songCount": "aantal nummers",
|
||||||
"toYear": "tot jaar",
|
"toYear": "tot jaar",
|
||||||
"trackNumber": "track",
|
"trackNumber": "track",
|
||||||
"explicitStatus": "$t(common.explicitStatus)"
|
"explicitStatus": "$t(common.explicitStatus)",
|
||||||
|
"sortName": "sorteernaam"
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
@@ -238,8 +241,8 @@
|
|||||||
"version": "versie {{version}}",
|
"version": "versie {{version}}",
|
||||||
"settings": "$t(common.setting, {\"count\": 2})",
|
"settings": "$t(common.setting, {\"count\": 2})",
|
||||||
"manageServers": "beheer servers",
|
"manageServers": "beheer servers",
|
||||||
"expandSidebar": "sidebar uitklappen",
|
"expandSidebar": "zijbalk uitklappen",
|
||||||
"collapseSidebar": "sidebar inklappen",
|
"collapseSidebar": "zijbalk inklappen",
|
||||||
"openBrowserDevtools": "open browser devtools",
|
"openBrowserDevtools": "open browser devtools",
|
||||||
"quit": "$t(common.quit)",
|
"quit": "$t(common.quit)",
|
||||||
"goBack": "terug",
|
"goBack": "terug",
|
||||||
@@ -298,7 +301,11 @@
|
|||||||
"viewAllTracks": "bekijk alle $t(entity.track, {\"count\": 2})",
|
"viewAllTracks": "bekijk alle $t(entity.track, {\"count\": 2})",
|
||||||
"recentReleases": "recente uitgaven",
|
"recentReleases": "recente uitgaven",
|
||||||
"groupingTypeAll": "alle soorten publicaties",
|
"groupingTypeAll": "alle soorten publicaties",
|
||||||
"groupingTypePrimary": "primaire publicatiesoorten"
|
"groupingTypePrimary": "primaire publicatiesoorten",
|
||||||
|
"favoriteSongs": "favoriete nummers",
|
||||||
|
"topSongsCommunity": "community",
|
||||||
|
"topSongsPersonal": "persoonlijk",
|
||||||
|
"favoriteSongsFrom": "favoriete nummers van {{title}}"
|
||||||
},
|
},
|
||||||
"manageServers": {
|
"manageServers": {
|
||||||
"title": "beheer servers",
|
"title": "beheer servers",
|
||||||
@@ -384,7 +391,8 @@
|
|||||||
"settings": "$t(common.setting, {\"count\": 2})",
|
"settings": "$t(common.setting, {\"count\": 2})",
|
||||||
"shared": "$t(entity.playlist, {\"count\": 2}) gedeeld",
|
"shared": "$t(entity.playlist, {\"count\": 2}) gedeeld",
|
||||||
"tracks": "$t(entity.track, {\"count\": 2})",
|
"tracks": "$t(entity.track, {\"count\": 2})",
|
||||||
"radio": "$t(entity.radioStation, {\"count\": 2})"
|
"radio": "$t(entity.radioStation, {\"count\": 2})",
|
||||||
|
"collections": "verzamelingen"
|
||||||
},
|
},
|
||||||
"trackList": {
|
"trackList": {
|
||||||
"artistTracks": "nummers van {{artist}}",
|
"artistTracks": "nummers van {{artist}}",
|
||||||
@@ -396,6 +404,14 @@
|
|||||||
},
|
},
|
||||||
"folderList": {
|
"folderList": {
|
||||||
"title": "$t(entity.folder, {\"count\": 2})"
|
"title": "$t(entity.folder, {\"count\": 2})"
|
||||||
|
},
|
||||||
|
"windowBar": {
|
||||||
|
"paused": "(Gepauzeerd) ",
|
||||||
|
"privateMode": "(Privémodus)"
|
||||||
|
},
|
||||||
|
"collections": {
|
||||||
|
"overrideExisting": "bestaande overschrijven",
|
||||||
|
"saveAsCollection": "sla op als verzameling"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
@@ -440,12 +456,12 @@
|
|||||||
"artist_other": "artiesten",
|
"artist_other": "artiesten",
|
||||||
"folderWithCount_one": "{{count}} folder",
|
"folderWithCount_one": "{{count}} folder",
|
||||||
"folderWithCount_other": "{{count}} folders",
|
"folderWithCount_other": "{{count}} folders",
|
||||||
"albumArtist_one": "album artiest",
|
"albumArtist_one": "albumartiest",
|
||||||
"albumArtist_other": "album artiesten",
|
"albumArtist_other": "albumartiesten",
|
||||||
"track_one": "track",
|
"track_one": "track",
|
||||||
"track_other": "tracks",
|
"track_other": "tracks",
|
||||||
"albumArtistCount_one": "{{count}} album artiest",
|
"albumArtistCount_one": "{{count}} albumartiest",
|
||||||
"albumArtistCount_other": "{{count}} album artiesten",
|
"albumArtistCount_other": "{{count}} albumartiesten",
|
||||||
"albumWithCount_one": "{{count}} album",
|
"albumWithCount_one": "{{count}} album",
|
||||||
"albumWithCount_other": "{{count}} albums",
|
"albumWithCount_other": "{{count}} albums",
|
||||||
"favorite_one": "favoriet",
|
"favorite_one": "favoriet",
|
||||||
@@ -473,11 +489,107 @@
|
|||||||
"table": {
|
"table": {
|
||||||
"column": {
|
"column": {
|
||||||
"rating": "rating",
|
"rating": "rating",
|
||||||
"size": "$t(common.size)"
|
"size": "$t(common.size)",
|
||||||
|
"albumArtist": "albumartiest",
|
||||||
|
"biography": "biografie",
|
||||||
|
"bitrate": "bitsnelheid",
|
||||||
|
"comment": "opmerking",
|
||||||
|
"dateAdded": "datum toegevoegd",
|
||||||
|
"favorite": "favoriet",
|
||||||
|
"discNumber": "disc",
|
||||||
|
"bpm": "bpm",
|
||||||
|
"album": "album",
|
||||||
|
"lastPlayed": "laatst gespeeld",
|
||||||
|
"path": "pad",
|
||||||
|
"playCount": "keren gespeeld",
|
||||||
|
"releaseDate": "uitgavedatum",
|
||||||
|
"releaseYear": "jaar",
|
||||||
|
"title": "titel",
|
||||||
|
"trackNumber": "nummer",
|
||||||
|
"owner": "eigenaar",
|
||||||
|
"albumCount": "$t(entity.album, {\"count\": 2})",
|
||||||
|
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||||
|
"bitDepth": "$t(common.bitDepth)",
|
||||||
|
"channels": "$t(common.channel, {\"count\": 2})",
|
||||||
|
"codec": "$t(common.codec)",
|
||||||
|
"genre": "$t(entity.genre, {\"count\": 1})",
|
||||||
|
"sampleRate": "$t(common.sampleRate)",
|
||||||
|
"songCount": "$t(entity.track, {\"count\": 2})"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"label": {
|
"label": {
|
||||||
"rating": "$t(common.rating)"
|
"rating": "$t(common.rating)",
|
||||||
|
"composer": "componist",
|
||||||
|
"dateAdded": "datum toegevoegd",
|
||||||
|
"discNumber": "discnummer",
|
||||||
|
"image": "afbeelding",
|
||||||
|
"lastPlayed": "laatst gespeeld",
|
||||||
|
"playCount": "keren afgespeeld",
|
||||||
|
"releaseDate": "uitgavedatum",
|
||||||
|
"rowIndex": "rij-index",
|
||||||
|
"trackNumber": "nummer",
|
||||||
|
"actions": "$t(common.action, {\"count\": 2})",
|
||||||
|
"album": "$t(entity.album, {\"count\": 1})",
|
||||||
|
"albumCount": "$t(entity.album, {\"count\": 2})",
|
||||||
|
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
|
||||||
|
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||||
|
"biography": "$t(common.biography)",
|
||||||
|
"bitDepth": "$t(common.bitDepth)",
|
||||||
|
"bitrate": "$t(common.bitrate)",
|
||||||
|
"bpm": "$t(common.bpm)",
|
||||||
|
"channels": "$t(common.channel, {\"count\": 2})",
|
||||||
|
"codec": "$t(common.codec)",
|
||||||
|
"duration": "$t(common.duration)",
|
||||||
|
"favorite": "$t(common.favorite)",
|
||||||
|
"genre": "$t(entity.genre, {\"count\": 1})",
|
||||||
|
"genreBadge": "$t(entity.genre, {\"count\": 1}) (badges)",
|
||||||
|
"note": "$t(common.note)",
|
||||||
|
"owner": "$t(common.owner)",
|
||||||
|
"path": "$t(common.path)",
|
||||||
|
"sampleRate": "$t(common.sampleRate)",
|
||||||
|
"size": "$t(common.size)",
|
||||||
|
"songCount": "$t(entity.track, {\"count\": 2})",
|
||||||
|
"title": "$t(common.title)",
|
||||||
|
"titleArtist": "$t(common.title) (artiest)",
|
||||||
|
"titleCombined": "$t(common.title) (gecombineerd)",
|
||||||
|
"year": "$t(common.year)"
|
||||||
|
},
|
||||||
|
"general": {
|
||||||
|
"advancedSettings": "geavanceerde instellingen",
|
||||||
|
"autoFitColumns": "kolommen automatisch passend maken",
|
||||||
|
"autosize": "automatische afmetingen",
|
||||||
|
"moveUp": "omhoog verplaatsen",
|
||||||
|
"moveDown": "omlaag verplaatsen",
|
||||||
|
"pinToLeft": "links vastpinnen",
|
||||||
|
"pinToRight": "rechts vastpinnen",
|
||||||
|
"alignLeft": "links uitlijnen",
|
||||||
|
"alignCenter": "centreren",
|
||||||
|
"alignRight": "rechts uitlijnen",
|
||||||
|
"followCurrentSong": "huidige nummer volgen",
|
||||||
|
"displayType": "weergavesoort",
|
||||||
|
"itemGap": "ruimte tussen items (px)",
|
||||||
|
"itemSize": "grootte item (px)",
|
||||||
|
"itemsPerRow": "items per rij",
|
||||||
|
"size_default": "standaard",
|
||||||
|
"size_compact": "compact",
|
||||||
|
"size_large": "groot",
|
||||||
|
"tableColumns": "kolommen",
|
||||||
|
"pagination": "paginering",
|
||||||
|
"pagination_itemsPerPage": "items per pagina",
|
||||||
|
"pagination_infinite": "oneindig",
|
||||||
|
"pagination_paginate": "gepagineerd",
|
||||||
|
"alternateRowColors": "afwisselende rijkleuren",
|
||||||
|
"horizontalBorders": "randen om rijen",
|
||||||
|
"rowHoverHighlight": "oplichtende rijen bij zweven met de muis",
|
||||||
|
"showHeader": "toon kop",
|
||||||
|
"verticalBorders": "randen om kolommen",
|
||||||
|
"gap": "$t(common.gap)",
|
||||||
|
"size": "$t(common.size)"
|
||||||
|
},
|
||||||
|
"view": {
|
||||||
|
"grid": "grid",
|
||||||
|
"list": "lijst",
|
||||||
|
"table": "tabel"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -503,8 +615,8 @@
|
|||||||
"exportImportSettings_importBtn": "importeer instellingen",
|
"exportImportSettings_importBtn": "importeer instellingen",
|
||||||
"exportImportSettings_importModalTitle": "importeer feishing-instellingen",
|
"exportImportSettings_importModalTitle": "importeer feishing-instellingen",
|
||||||
"exportImportSettings_importSuccess": "instellingen zijn succesvol geïmporteerd!",
|
"exportImportSettings_importSuccess": "instellingen zijn succesvol geïmporteerd!",
|
||||||
"exportImportSettings_notValidJSON": "het ingevoerde bestand is geen valide JSON",
|
"exportImportSettings_notValidJSON": "het ingevoerde bestand is geen geldige JSON",
|
||||||
"exportImportSettings_offendingKeyError": "\"{{offendingKey}}\" is incorrect - {{reason}}",
|
"exportImportSettings_offendingKeyError": "\"{{offendingKey}}\" is onjuist - {{reason}}",
|
||||||
"externalLinks_description": "maakt het mogelijk om externe links (Last.fm, MusicBrainz) te tonen op artiesten-/albumpagina's",
|
"externalLinks_description": "maakt het mogelijk om externe links (Last.fm, MusicBrainz) te tonen op artiesten-/albumpagina's",
|
||||||
"externalLinks": "toon externe links",
|
"externalLinks": "toon externe links",
|
||||||
"followLyric_description": "scroll de songtekst naar de huidige positie",
|
"followLyric_description": "scroll de songtekst naar de huidige positie",
|
||||||
@@ -573,7 +685,6 @@
|
|||||||
"customCssNotice": "Waarschuwing: ondanks sanering (het niet toestaan van url() en content:) brengt aangepaste css nog steeds risico's met zich mee omdat de interface wordt gewijzigd",
|
"customCssNotice": "Waarschuwing: ondanks sanering (het niet toestaan van url() en content:) brengt aangepaste css nog steeds risico's met zich mee omdat de interface wordt gewijzigd",
|
||||||
"customFontPath_description": "bepaal het pad naar het aangepaste lettertype voor gebruik in de applicatie",
|
"customFontPath_description": "bepaal het pad naar het aangepaste lettertype voor gebruik in de applicatie",
|
||||||
"customFontPath": "aangepaste lettertypelocatie",
|
"customFontPath": "aangepaste lettertypelocatie",
|
||||||
"disableAutomaticUpdates": "automatische updates uitschakelen",
|
|
||||||
"releaseChannel_optionBeta": "beta",
|
"releaseChannel_optionBeta": "beta",
|
||||||
"releaseChannel_optionLatest": "meest recente",
|
"releaseChannel_optionLatest": "meest recente",
|
||||||
"releaseChannel": "releasekanaal",
|
"releaseChannel": "releasekanaal",
|
||||||
@@ -760,7 +871,73 @@
|
|||||||
"sidebarPlaylistList": "afspeellijsten zijbalk",
|
"sidebarPlaylistList": "afspeellijsten zijbalk",
|
||||||
"sidePlayQueueStyle_description": "de stijl van de wachtrij aan de zijkant",
|
"sidePlayQueueStyle_description": "de stijl van de wachtrij aan de zijkant",
|
||||||
"sidePlayQueueStyle_optionAttached": "aangekoppeld",
|
"sidePlayQueueStyle_optionAttached": "aangekoppeld",
|
||||||
"sidePlayQueueStyle_optionDetached": "afgekoppeld"
|
"sidePlayQueueStyle_optionDetached": "afgekoppeld",
|
||||||
|
"homeFeatureStyle_description": "bepaalt de stijl van de uitgelicht-carrousel op de homepagina",
|
||||||
|
"homeFeatureStyle": "stijl uitgelicht-carrousel",
|
||||||
|
"homeFeatureStyle_optionMultiple": "meervoudig",
|
||||||
|
"homeFeatureStyle_optionSingle": "enkelvoudig",
|
||||||
|
"blurExplicitImages": "vervaag expliciete afbeeldingen",
|
||||||
|
"blurExplicitImages_description": "hoezen van albums en nummers die getagd zijn als expliciet zullen worden vervaagd",
|
||||||
|
"mediaSession": "mediasessie inschakelen",
|
||||||
|
"sidePlayQueueStyle": "stijl van zijwachtrij",
|
||||||
|
"skipDuration_description": "de tijdsduur die wordt doorgespoeld bij gebruik van de spoelknoppen in de afspeelbalk",
|
||||||
|
"skipDuration": "doorspoelduur",
|
||||||
|
"startMinimized_description": "start de applicatie in het systeemvak",
|
||||||
|
"startMinimized": "start geminimaliseerd",
|
||||||
|
"theme": "thema",
|
||||||
|
"theme_description": "het visuele thema dat de applicatie gebruikt",
|
||||||
|
"themeDark_description": "het donkere thema dat de applicatie gebruikt",
|
||||||
|
"themeDark": "thema (donker)",
|
||||||
|
"themeLight_description": "het lichte thema dat de applicatie gebruikt",
|
||||||
|
"themeLight": "thema (licht)",
|
||||||
|
"transcode": "transcoderen inschakelen",
|
||||||
|
"transcode_description": "schakel transcoderen naar andere formaten in",
|
||||||
|
"transcodeBitrate_description": "de bitsnelheid waarnaar wordt getranscodeerd. bij 0 bepaalt de server de waarde",
|
||||||
|
"transcodeBitrate": "transcodeerbitsnelheid",
|
||||||
|
"transcodeFormat_description": "het formaat waarnaar wordt getranscodeerd. laat leeg om de server te laten bepalen",
|
||||||
|
"transcodeFormat": "transcodeerformaat",
|
||||||
|
"translationApiKey_description": "api-sleutel voor vertaling (enkel globaal service-eindpunt)",
|
||||||
|
"translationApiKey": "vertalings-api-sleutel",
|
||||||
|
"translationApiProvider_description": "api-provider voor vertalingen",
|
||||||
|
"translationApiProvider": "vertalings-api-provider",
|
||||||
|
"translationTargetLanguage_description": "doeltaal voor vertalingen",
|
||||||
|
"translationTargetLanguage": "doeltaal vertaling",
|
||||||
|
"trayEnabled_description": "toon/verstop het systeemvakicoon/-menu. indien uitgeschakeld wordt het minimaliseren/sluiten naar het systeemvak ook uitgeschakeld",
|
||||||
|
"trayEnabled": "toon systeemvak",
|
||||||
|
"useSystemTheme_description": "volg de systeemvoorkeur voor licht of donker thema",
|
||||||
|
"useSystemTheme": "gebruik systeemthema",
|
||||||
|
"volumeWheelStep_description": "de hoeveelheid volume die gewijzigd wordt bij het scrollen met het muiswiel op de volumebalk",
|
||||||
|
"volumeWheelStep": "volumestap muiswiel",
|
||||||
|
"volumeWidth_description": "de breedte van de volumebalk",
|
||||||
|
"volumeWidth": "volumebalkbreedte",
|
||||||
|
"webAudio_description": "gebruik web-audio. dit schakeld geavanceerde mogelijkheden als replaygain in. schakel uit als dit niet werkt",
|
||||||
|
"webAudio": "gebruik web-audio",
|
||||||
|
"windowBarStyle": "vensterbalkstijl",
|
||||||
|
"windowBarStyle_description": "kies de stijl van de vensterbalk",
|
||||||
|
"zoom": "zoompercentage",
|
||||||
|
"zoom_description": "het zoompercentage van de applicatie",
|
||||||
|
"queryBuilder": "opdrachtbouwer",
|
||||||
|
"queryBuilderCustomFields": "aangepaste velden",
|
||||||
|
"queryBuilderCustomFields_description": "voeg aangepaste velden voor gebruik in opdrachtbouwers toe",
|
||||||
|
"enableGridMultiSelect": "meervoudig selecteren in grid",
|
||||||
|
"enableGridMultiSelect_description": "staat toe meerdere items in gridweergaven te selecteren. indien uitgeschakeld zal het klikken op een item in een gridweergave naar diens pagina navigeren",
|
||||||
|
"sidebarPlaylistSorting": "afspeellijstsortering in zijbalk",
|
||||||
|
"sidebarPlaylistSorting_description": "activeert het handmatig sorteren van de afspeellijst in de zijbalk door middel van slepen in plaats van het gebruiken van de servervolgorde",
|
||||||
|
"sidebarPlaylistListFilterRegex_description": "verberg afspeellijsten in de zijbalk die overeenkomen met deze reguliere expressie",
|
||||||
|
"sidebarPlaylistListFilterRegex_placeholder": "bijv. ^Daily Mix.*",
|
||||||
|
"sidebarPlaylistListFilterRegex": "regex afspeellijstfilter",
|
||||||
|
"mediaSession_description": "schakelt mediasessie-integratie in, waarbij mediabesturing en metadata in het volumeweergave en het lock-scherm worden weergegeven",
|
||||||
|
"skipPlaylistPage": "sla afspeellijstpagina over",
|
||||||
|
"skipPlaylistPage_description": "ga naar de nummerlijst in plaats van de standaard pagina bij het navigeren naar een afspeellijst",
|
||||||
|
"queryBuilderCustomFields_inputLabel": "label",
|
||||||
|
"queryBuilderCustomFields_inputTag": "tag",
|
||||||
|
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
|
||||||
|
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||||
|
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||||
|
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||||
|
"replayGainMode_optionAlbum": "$t(entity.album, {\"count\": 1})",
|
||||||
|
"replayGainMode_optionNone": "$t(common.none)",
|
||||||
|
"replayGainMode_optionTrack": "$t(entity.track, {\"count\": 1})"
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"addServer": {
|
"addServer": {
|
||||||
@@ -964,5 +1141,159 @@
|
|||||||
"soundtrack": "soundtrack",
|
"soundtrack": "soundtrack",
|
||||||
"spokenWord": "gesproken woord"
|
"spokenWord": "gesproken woord"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"dragDropZone": {
|
||||||
|
"error_oneFileOnly": "Kies één bestand",
|
||||||
|
"error_readingFile": "probleem opgetreden bij het lezen van het bestand: {{errorMessage}}",
|
||||||
|
"mainText": "sleep hier een bestand naartoe"
|
||||||
|
},
|
||||||
|
"visualizer": {
|
||||||
|
"visualizerType": "Type Visualiseerder",
|
||||||
|
"cyclePresets": "Doorloop Voorinstellingen",
|
||||||
|
"cycleTime": "Cyclustijd (seconden)",
|
||||||
|
"ignoredPresets": "Genegeerde Voorinstellingen",
|
||||||
|
"selectedPresets": "Gekozen Voorinstellingen",
|
||||||
|
"includeAllPresets": "Alle Voorinstellingen Opnemen",
|
||||||
|
"randomizeNextPreset": "Willekeurige Volgende Voorinstelling",
|
||||||
|
"blendTime": "Mengtijd",
|
||||||
|
"presets": "Voorinstellingen",
|
||||||
|
"selectPreset": "Kies Voorinstelling",
|
||||||
|
"applyPreset": "Voorinstelling Toepassen",
|
||||||
|
"saveAsPreset": "Opslaan als Voorinstelling",
|
||||||
|
"updatePreset": "Voorinstelling Bijwerken",
|
||||||
|
"copyConfiguration": "Kopieer Configuratie",
|
||||||
|
"pasteConfiguration": "Plak Configuratie",
|
||||||
|
"pasteConfigurationPlaceholder": "Plak JSON-configuratie hier...",
|
||||||
|
"pasteFromClipboard": "Plakken vanaf Klembord",
|
||||||
|
"applyConfiguration": "Configuratie Toepassen",
|
||||||
|
"configCopied": "Configuratie gekopieerd naar het klembord",
|
||||||
|
"configCopyFailed": "Kopiëren van configuratie is mislukt",
|
||||||
|
"configPasted": "Configuratie succesvol toegepast",
|
||||||
|
"configPasteFailed": "Toepassen configuratie mislukt. Controleer het formaat.",
|
||||||
|
"configPasteReadFailed": "Lezen van het klembord mislukt",
|
||||||
|
"presetName": "Naam Voorinstelling",
|
||||||
|
"presetNamePlaceholder": "Voer de naam van de voorinstelling in",
|
||||||
|
"general": "Algemeen",
|
||||||
|
"mode": "Modus",
|
||||||
|
"mode1To8": "Modus 1-8",
|
||||||
|
"mode10": "Modus 10",
|
||||||
|
"barSpace": "Balkruimte",
|
||||||
|
"lineWidth": "Lijnbreedte",
|
||||||
|
"fillAlpha": "Alfavulling",
|
||||||
|
"channelLayout": "Kanaalindeling",
|
||||||
|
"maxFPS": "Max FPS",
|
||||||
|
"opacity": "Opaciteit",
|
||||||
|
"customGradients": "Aangepaste Kleurverlopen",
|
||||||
|
"addCustomGradient": "Voeg Aangepast Kleurverloop Toe",
|
||||||
|
"gradientName": "Naam Kleurverloop",
|
||||||
|
"gradientNamePlaceholder": "Naam Kleurverloop",
|
||||||
|
"vertical": "Verticaal",
|
||||||
|
"horizontal": "Horizontaal",
|
||||||
|
"colorStops": "Kleurstop",
|
||||||
|
"addColor": "Voeg Kleur Toe",
|
||||||
|
"position": "Positie",
|
||||||
|
"level": "Niveau",
|
||||||
|
"remove": "Verwijder",
|
||||||
|
"pasteGradient": "Plak Kleurverloop",
|
||||||
|
"pasteGradientPlaceholder": "Plak JSON van kleurverloop hier...",
|
||||||
|
"custom": "Aangepast",
|
||||||
|
"builtIn": "Ingebouwd",
|
||||||
|
"colors": "Kleuren",
|
||||||
|
"colorMode": "Kleurmodus",
|
||||||
|
"gradient": "Kleurverloop",
|
||||||
|
"gradientLeft": "Kleurverloop Links",
|
||||||
|
"gradientRight": "Kleurverloop Rechts",
|
||||||
|
"fft": "FFT",
|
||||||
|
"fftSize": "FFT-grootte",
|
||||||
|
"smoothing": "Gladstrijken",
|
||||||
|
"frequencyRangeAndScaling": "Frequentiebereik en -schaling",
|
||||||
|
"minimumFrequency": "Minimumfrequentie",
|
||||||
|
"maximumFrequency": "Maximumfrequentie",
|
||||||
|
"frequencyScale": "Frequentieschaal",
|
||||||
|
"sensitivity": "Gevoeligheid",
|
||||||
|
"weightingFilter": "Gewichtsfilter",
|
||||||
|
"minimumDecibels": "Minimum aantal decibel",
|
||||||
|
"maximumDecibels": "Maximum aantal decibel",
|
||||||
|
"linearAmplitude": "Lineaire Amplitude",
|
||||||
|
"linearBoost": "Lineaire Versterking",
|
||||||
|
"peakBehavior": "Piekgedrag",
|
||||||
|
"showPeaks": "Toon Pieken",
|
||||||
|
"fadePeaks": "Vervaag Pieken",
|
||||||
|
"peakLine": "Pieklijn",
|
||||||
|
"gravity": "Zwaartekracht",
|
||||||
|
"peakFadeTime": "Piekvervagingstijd (ms)",
|
||||||
|
"peakHoldTime": "Piekvasthoudtijd (ms)",
|
||||||
|
"radialSpectrum": "Radiaal Spectrum",
|
||||||
|
"radial": "Radiaal",
|
||||||
|
"radialInvert": "Geïnverteerde Radiaal",
|
||||||
|
"spinSpeed": "Draaisnelheid",
|
||||||
|
"radius": "Radius",
|
||||||
|
"reflexMirror": "Reflexspiegel",
|
||||||
|
"reflexRatio": "Reflexverhouding",
|
||||||
|
"reflexAlpha": "Reflex-alfa",
|
||||||
|
"reflexFit": "Reflex-inpassing",
|
||||||
|
"reflexBrightness": "Reflex-helderheid",
|
||||||
|
"mirror": "Spiegel",
|
||||||
|
"miscellaneousSettings": "Diverse Instellingen",
|
||||||
|
"alphaBars": "Alfabalken",
|
||||||
|
"ansiBands": "ANSI-banden",
|
||||||
|
"ledBars": "LED-balken",
|
||||||
|
"trueLeds": "Ware LEDs",
|
||||||
|
"lumiBars": "Lumi-balken",
|
||||||
|
"outlineBars": "Uitgelijnde balken",
|
||||||
|
"roundBars": "Ronde Balken",
|
||||||
|
"lowResolution": "Lage Resolutie",
|
||||||
|
"splitGradient": "Gescheiden Kleurverloop",
|
||||||
|
"showFPS": "Toon FPS",
|
||||||
|
"showScaleX": "Toon X-schaal",
|
||||||
|
"showScaleY": "Toon Y-schaal",
|
||||||
|
"noteLabels": "Nootlabels",
|
||||||
|
"options": {
|
||||||
|
"mode": {
|
||||||
|
"0": "[0] Discrete Frequenties",
|
||||||
|
"1": "[1] 1/24e octaaf / 240 bands",
|
||||||
|
"2": "[2] 1/12e octaaf / 120 bands",
|
||||||
|
"3": "[3] 1/8e octaaf / 80 bands",
|
||||||
|
"4": "[4] 1/6e octaaf / 60 bands",
|
||||||
|
"5": "[5] 1/4e octaaf / 40 bands",
|
||||||
|
"6": "[6] 1/3e octaaf / 30 bands",
|
||||||
|
"7": "[7] Half octaaf / 20 bands",
|
||||||
|
"8": "[8] Volledig octaaf / 10 bands",
|
||||||
|
"10": "[10] Lijn- / Gebiedsgrafiek"
|
||||||
|
},
|
||||||
|
"colorMode": {
|
||||||
|
"gradient": "Kleurverloop",
|
||||||
|
"barIndex": "Balk-index",
|
||||||
|
"barLevel": "Balkniveau"
|
||||||
|
},
|
||||||
|
"gradient": {
|
||||||
|
"classic": "Klassiek",
|
||||||
|
"prism": "Prisma",
|
||||||
|
"rainbow": "Regenboog",
|
||||||
|
"steelblue": "Staalblauw",
|
||||||
|
"orangered": "Oranjerood"
|
||||||
|
},
|
||||||
|
"channelLayout": {
|
||||||
|
"single": "Enkelvoudig",
|
||||||
|
"dualCombined": "Duaalgecombineerd",
|
||||||
|
"dualHorizontal": "Duaalhorizontaal",
|
||||||
|
"dualVertical": "Duaalvertikaal"
|
||||||
|
},
|
||||||
|
"frequencyScale": {
|
||||||
|
"none": "Geen",
|
||||||
|
"bark": "Bark-schaal",
|
||||||
|
"linear": "Lineaire Schaal",
|
||||||
|
"log": "Log-schaal",
|
||||||
|
"mel": "Mel-schaal"
|
||||||
|
},
|
||||||
|
"weightingFilter": {
|
||||||
|
"none": "Geen",
|
||||||
|
"a": "A",
|
||||||
|
"b": "B",
|
||||||
|
"c": "C",
|
||||||
|
"d": "D",
|
||||||
|
"z": "Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+55
-15
@@ -73,9 +73,9 @@
|
|||||||
"delete": "usuń",
|
"delete": "usuń",
|
||||||
"cancel": "anuluj",
|
"cancel": "anuluj",
|
||||||
"forceRestartRequired": "zrestartuj aby zastosować zmiany... zamknij powiadomienie aby zrestartować",
|
"forceRestartRequired": "zrestartuj aby zastosować zmiany... zamknij powiadomienie aby zrestartować",
|
||||||
"setting_one": "ustawienia",
|
"setting_one": "ustawienie",
|
||||||
"setting_few": "",
|
"setting_few": "ustawienia",
|
||||||
"setting_many": "",
|
"setting_many": "ustawień",
|
||||||
"version": "wersja",
|
"version": "wersja",
|
||||||
"title": "tytuł",
|
"title": "tytuł",
|
||||||
"filter_one": "filtr",
|
"filter_one": "filtr",
|
||||||
@@ -162,7 +162,8 @@
|
|||||||
"mood": "nastrój",
|
"mood": "nastrój",
|
||||||
"example": "przykład",
|
"example": "przykład",
|
||||||
"filter_multiple": "multi",
|
"filter_multiple": "multi",
|
||||||
"filter_single": "single"
|
"filter_single": "single",
|
||||||
|
"rename": "zmień nazwę"
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"genre_one": "gatunek",
|
"genre_one": "gatunek",
|
||||||
@@ -175,14 +176,14 @@
|
|||||||
"playlist_few": "playlisty",
|
"playlist_few": "playlisty",
|
||||||
"playlist_many": "playlist",
|
"playlist_many": "playlist",
|
||||||
"artist_one": "wykonawca",
|
"artist_one": "wykonawca",
|
||||||
"artist_few": "wykonawców",
|
"artist_few": "wykonawcy",
|
||||||
"artist_many": "wykonawców",
|
"artist_many": "wykonawców",
|
||||||
"folderWithCount_one": "{{count}} katalog",
|
"folderWithCount_one": "{{count}} katalog",
|
||||||
"folderWithCount_few": "{{count}} katalogi",
|
"folderWithCount_few": "{{count}} katalogi",
|
||||||
"folderWithCount_many": "{{count}} katalogów",
|
"folderWithCount_many": "{{count}} katalogów",
|
||||||
"albumArtist_one": "wykonawca albumu",
|
"albumArtist_one": "wykonawca albumu",
|
||||||
"albumArtist_few": "wykonawcy albumu",
|
"albumArtist_few": "wykonawcy albumu",
|
||||||
"albumArtist_many": "wykonawców albumu",
|
"albumArtist_many": "wykonawcy albumów",
|
||||||
"track_one": "utwór",
|
"track_one": "utwór",
|
||||||
"track_few": "utwory",
|
"track_few": "utwory",
|
||||||
"track_many": "utworów",
|
"track_many": "utworów",
|
||||||
@@ -427,7 +428,7 @@
|
|||||||
"dynamicIsImage": "włącz obraz w tle",
|
"dynamicIsImage": "włącz obraz w tle",
|
||||||
"lyricOffset": "opóźnienie tekstów (ms)"
|
"lyricOffset": "opóźnienie tekstów (ms)"
|
||||||
},
|
},
|
||||||
"upNext": "następny",
|
"upNext": "następne",
|
||||||
"lyrics": "tekst",
|
"lyrics": "tekst",
|
||||||
"related": "powiązane",
|
"related": "powiązane",
|
||||||
"visualizer": "wizualizer",
|
"visualizer": "wizualizer",
|
||||||
@@ -512,7 +513,8 @@
|
|||||||
"shared": "udostępniono $t(entity.playlist, {\"count\": 2})",
|
"shared": "udostępniono $t(entity.playlist, {\"count\": 2})",
|
||||||
"myLibrary": "Moja biblioteka",
|
"myLibrary": "Moja biblioteka",
|
||||||
"favorites": "$t(entity.favorite, {\"count\": 2})",
|
"favorites": "$t(entity.favorite, {\"count\": 2})",
|
||||||
"radio": "$t(entity.radioStation, {\"count\": 2})"
|
"radio": "$t(entity.radioStation, {\"count\": 2})",
|
||||||
|
"collections": "kolekcje"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"mostPlayed": "najczęściej odtwarzane",
|
"mostPlayed": "najczęściej odtwarzane",
|
||||||
@@ -575,7 +577,11 @@
|
|||||||
"appearsOn": "pojawia się na",
|
"appearsOn": "pojawia się na",
|
||||||
"viewAllTracks": "zobacz wszystko $t(entity.track, {\"count\": 2})",
|
"viewAllTracks": "zobacz wszystko $t(entity.track, {\"count\": 2})",
|
||||||
"groupingTypeAll": "wszystkie typy wydań",
|
"groupingTypeAll": "wszystkie typy wydań",
|
||||||
"groupingTypePrimary": "główne typy wydań"
|
"groupingTypePrimary": "główne typy wydań",
|
||||||
|
"favoriteSongs": "ulubione piosenki",
|
||||||
|
"topSongsCommunity": "społeczność",
|
||||||
|
"topSongsPersonal": "osobiste",
|
||||||
|
"favoriteSongsFrom": "ulubione piosenki z {{title}}"
|
||||||
},
|
},
|
||||||
"itemDetail": {
|
"itemDetail": {
|
||||||
"copyPath": "kopiuj ścieżkę do schowka",
|
"copyPath": "kopiuj ścieżkę do schowka",
|
||||||
@@ -601,6 +607,19 @@
|
|||||||
},
|
},
|
||||||
"radioList": {
|
"radioList": {
|
||||||
"title": "stacje radiowe"
|
"title": "stacje radiowe"
|
||||||
|
},
|
||||||
|
"windowBar": {
|
||||||
|
"paused": "(Wstrzymane) ",
|
||||||
|
"privateMode": "(Tryb prywatny)"
|
||||||
|
},
|
||||||
|
"collections": {
|
||||||
|
"overrideExisting": "nadpisz istniejące",
|
||||||
|
"saveAsCollection": "zapisz jako kolekcję"
|
||||||
|
},
|
||||||
|
"releasenotes": {
|
||||||
|
"commitsSinceStable": "commity od {{stable}}",
|
||||||
|
"noNewCommits": "brak nowych commitów w tym zakresie",
|
||||||
|
"noStableReleaseToCompare": "brak dostępnego stabilnego wydania do porównania"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
@@ -642,7 +661,16 @@
|
|||||||
"restoreQueueFromServer": "przywróć kolejkę z serwera",
|
"restoreQueueFromServer": "przywróć kolejkę z serwera",
|
||||||
"saveQueueToServer": "zapisz kolejkę na serwerze",
|
"saveQueueToServer": "zapisz kolejkę na serwerze",
|
||||||
"artistRadio": "radio wykonawcy",
|
"artistRadio": "radio wykonawcy",
|
||||||
"trackRadio": "radio utworu"
|
"trackRadio": "radio utworu",
|
||||||
|
"sleepTimer": "wyłącznik czasowy",
|
||||||
|
"sleepTimer_endOfSong": "do końca aktualnej piosenki",
|
||||||
|
"sleepTimer_minutes": "{{count}} min",
|
||||||
|
"sleepTimer_hours": "{{count}} godz",
|
||||||
|
"sleepTimer_custom": "niestandardowy",
|
||||||
|
"sleepTimer_off": "wyłączony",
|
||||||
|
"sleepTimer_timeRemaining": "pozostało {{time}}",
|
||||||
|
"sleepTimer_setCustom": "ustaw wyłącznik",
|
||||||
|
"sleepTimer_cancel": "anuluj wyłączanie"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"crossfadeStyle_description": "wybierz styl przenikania, który ma być używany do odtwarzania dźwięku",
|
"crossfadeStyle_description": "wybierz styl przenikania, który ma być używany do odtwarzania dźwięku",
|
||||||
@@ -674,7 +702,6 @@
|
|||||||
"globalMediaHotkeys": "globalne skróty klawiszowe multimediów",
|
"globalMediaHotkeys": "globalne skróty klawiszowe multimediów",
|
||||||
"hotkey_globalSearch": "globalne wyszukiwanie",
|
"hotkey_globalSearch": "globalne wyszukiwanie",
|
||||||
"gaplessAudio_description": "ustaw dźwięk bez przerw dla mpv",
|
"gaplessAudio_description": "ustaw dźwięk bez przerw dla mpv",
|
||||||
"disableAutomaticUpdates": "wyłącz automatyczne aktualizacje",
|
|
||||||
"exitToTray_description": "zamknij aplikację do zasobnika systemowego",
|
"exitToTray_description": "zamknij aplikację do zasobnika systemowego",
|
||||||
"followLyric_description": "przewiń tekst do obecnego momentu",
|
"followLyric_description": "przewiń tekst do obecnego momentu",
|
||||||
"hotkey_favoritePreviousSong": "ulubiona $t(common.previousSong)",
|
"hotkey_favoritePreviousSong": "ulubiona $t(common.previousSong)",
|
||||||
@@ -718,7 +745,7 @@
|
|||||||
"hotkey_zoomOut": "oddal",
|
"hotkey_zoomOut": "oddal",
|
||||||
"hotkey_unfavoriteCurrentSong": "usuń $t(common.currentSong) z ulubionych",
|
"hotkey_unfavoriteCurrentSong": "usuń $t(common.currentSong) z ulubionych",
|
||||||
"hotkey_rate0": "wyczyść oceny",
|
"hotkey_rate0": "wyczyść oceny",
|
||||||
"discordApplicationId": "ID aplikacji {{discord}}",
|
"discordApplicationId": "id aplikacji {{discord}}",
|
||||||
"applicationHotkeys_description": "ustaw skróty klawiszowe aplikacji. przełącz pole wyboru aby ustawić skrót globalny (tylko komputery)",
|
"applicationHotkeys_description": "ustaw skróty klawiszowe aplikacji. przełącz pole wyboru aby ustawić skrót globalny (tylko komputery)",
|
||||||
"hotkey_volumeMute": "wycisz",
|
"hotkey_volumeMute": "wycisz",
|
||||||
"hotkey_toggleCurrentSongFavorite": "dodaj $t(common.currentSong) do ulubionych",
|
"hotkey_toggleCurrentSongFavorite": "dodaj $t(common.currentSong) do ulubionych",
|
||||||
@@ -876,7 +903,7 @@
|
|||||||
"releaseChannel_optionBeta": "beta",
|
"releaseChannel_optionBeta": "beta",
|
||||||
"releaseChannel_optionLatest": "najnowsza",
|
"releaseChannel_optionLatest": "najnowsza",
|
||||||
"releaseChannel": "kanał wydań",
|
"releaseChannel": "kanał wydań",
|
||||||
"releaseChannel_description": "wybieraj pomiędzy stabilnymi wydaniami a wydaniami beta dla automatycznych aktualizacji",
|
"releaseChannel_description": "wybieraj pomiędzy wydaniami stabilnymi, beta lub alpha (nightly) dla automatycznych aktualizacji",
|
||||||
"discordDisplayType_artistname": "nazwa(y) wykonawców",
|
"discordDisplayType_artistname": "nazwa(y) wykonawców",
|
||||||
"discordDisplayType_description": "zmienia co jest pokazywane jako słuchane w twoim statusie",
|
"discordDisplayType_description": "zmienia co jest pokazywane jako słuchane w twoim statusie",
|
||||||
"discordDisplayType_songname": "nazwa piosenki",
|
"discordDisplayType_songname": "nazwa piosenki",
|
||||||
@@ -984,14 +1011,27 @@
|
|||||||
"enableGridMultiSelect_description": "gdy włączone, pozwala na wybieranie wielu elementów w widokach siatki, gdy wyłączone, klikanie obrazów elementów siatki będzie przenosić na stronę elementu",
|
"enableGridMultiSelect_description": "gdy włączone, pozwala na wybieranie wielu elementów w widokach siatki, gdy wyłączone, klikanie obrazów elementów siatki będzie przenosić na stronę elementu",
|
||||||
"enableGridMultiSelect": "wybieranie wielu w siatce",
|
"enableGridMultiSelect": "wybieranie wielu w siatce",
|
||||||
"sidebarPlaylistSorting_description": "pozwala na ręczne sortowanie playlist w bocznym pasku używając przeciągania i upuszczania zamiast używania domyślnej kolejności serwera",
|
"sidebarPlaylistSorting_description": "pozwala na ręczne sortowanie playlist w bocznym pasku używając przeciągania i upuszczania zamiast używania domyślnej kolejności serwera",
|
||||||
"sidebarPlaylistSorting": "sortowanie playlist w bocznym pasku"
|
"sidebarPlaylistSorting": "sortowanie playlist w bocznym pasku",
|
||||||
|
"sidebarPlaylistListFilterRegex_description": "ukryj playlisty w pasku bocznym pasujące do wyrażenia regularnego",
|
||||||
|
"sidebarPlaylistListFilterRegex_placeholder": "np. ^Miks codzienny.^",
|
||||||
|
"sidebarPlaylistListFilterRegex": "filtr playlist regex",
|
||||||
|
"blurExplicitImages": "rozmazuj nieodpowiednie obrazy",
|
||||||
|
"blurExplicitImages_description": "obrazy piosenek oraz albumów oznaczone jako nieodpowiednie będą rozmazywane",
|
||||||
|
"releaseChannel_optionAlpha": "alpha (nightly)",
|
||||||
|
"analyticsEnable": "Wysyłaj statystyki na podstawie użytkowania",
|
||||||
|
"analyticsEnable_description": "Zanonimizowane statystki użytkowania będą wysyłane do twórcy, aby pomóc w poprawie aplikacji",
|
||||||
|
"automaticUpdates": "Aktualizacje automatyczne",
|
||||||
|
"automaticUpdates_description": "Sprawdzaj i instaluj aktualizacje automatycznie",
|
||||||
|
"discordStateIcon": "pokaż ikonę odtwarzania",
|
||||||
|
"discordStateIcon_description": "pokazuje małą ikonę odtwarzania w statusie. ikona pauzy jest zawsze pokazywana gdy \"Pokaż status podczas pauzy\" jest włączone"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"config": {
|
"config": {
|
||||||
"view": {
|
"view": {
|
||||||
"table": "tabela",
|
"table": "tabela",
|
||||||
"grid": "siatka",
|
"grid": "siatka",
|
||||||
"list": "lista"
|
"list": "lista",
|
||||||
|
"detail": "szczegół"
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"displayType": "typ wyświetlania",
|
"displayType": "typ wyświetlania",
|
||||||
|
|||||||
@@ -230,7 +230,6 @@
|
|||||||
"crossfadeDuration_description": "define a duração do efeito crossfade",
|
"crossfadeDuration_description": "define a duração do efeito crossfade",
|
||||||
"customCssNotice": "Atenção: embora haja alguma sanitização (proibindo url() e content:), usar CSS personalizado ainda pode representar riscos ao alterar a interface",
|
"customCssNotice": "Atenção: embora haja alguma sanitização (proibindo url() e content:), usar CSS personalizado ainda pode representar riscos ao alterar a interface",
|
||||||
"crossfadeStyle_description": "seleciona qual estilo de crossfade usado no player de áudio",
|
"crossfadeStyle_description": "seleciona qual estilo de crossfade usado no player de áudio",
|
||||||
"disableAutomaticUpdates": "desabilitar atualizações automáticas",
|
|
||||||
"disableLibraryUpdateOnStartup": "desabilitar a verificação de novas versões na inicialização",
|
"disableLibraryUpdateOnStartup": "desabilitar a verificação de novas versões na inicialização",
|
||||||
"artistBackground": "Imagem de fundo do artista",
|
"artistBackground": "Imagem de fundo do artista",
|
||||||
"artistBackground_description": "Adiciona uma imagem de fundo às páginas do artista contendo a arte do artista",
|
"artistBackground_description": "Adiciona uma imagem de fundo às páginas do artista contendo a arte do artista",
|
||||||
|
|||||||
@@ -523,7 +523,6 @@
|
|||||||
"customCssEnable_description": "permite escrever 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",
|
"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",
|
"customCss": "css customizado",
|
||||||
"disableAutomaticUpdates": "desativar atualizações automáticas",
|
|
||||||
"disableLibraryUpdateOnStartup": "desativar a verificação de novas versões na inicialização",
|
"disableLibraryUpdateOnStartup": "desativar a verificação de novas versões na inicialização",
|
||||||
"discordApplicationId": "{{discord}} ID da aplicação",
|
"discordApplicationId": "{{discord}} ID da aplicação",
|
||||||
"discordIdleStatus_description": "quando ativado, atualiza o estado enquanto o player está ocioso",
|
"discordIdleStatus_description": "quando ativado, atualiza o estado enquanto o player está ocioso",
|
||||||
|
|||||||
@@ -727,7 +727,6 @@
|
|||||||
"disableLibraryUpdateOnStartup": "отключить проверку новых версий при запуске приложения",
|
"disableLibraryUpdateOnStartup": "отключить проверку новых версий при запуске приложения",
|
||||||
"minimizeToTray_description": "сворачивать приложение в панель уведомлений",
|
"minimizeToTray_description": "сворачивать приложение в панель уведомлений",
|
||||||
"audioPlayer_description": "укажите, какой аудиоплеер использовать для воспроизведения",
|
"audioPlayer_description": "укажите, какой аудиоплеер использовать для воспроизведения",
|
||||||
"disableAutomaticUpdates": "отключить проверку обновлений",
|
|
||||||
"exitToTray_description": "При закрытии приложения - оно останется в панели уведомлений",
|
"exitToTray_description": "При закрытии приложения - оно останется в панели уведомлений",
|
||||||
"fontType_optionCustom": "пользовательский",
|
"fontType_optionCustom": "пользовательский",
|
||||||
"remotePassword": "пароль к серверу удалённого управления",
|
"remotePassword": "пароль к серверу удалённого управления",
|
||||||
|
|||||||
@@ -541,7 +541,6 @@
|
|||||||
"customCss_description": "vlastný css obsah. Poznámka: obsah a vzdialené url linky sú defaultne deaktivované.Náhľad vášho obsahu je zobrazený nižšie. Pridané polia, ktoré ste nenastavovali boli pridané pri sanitizácii",
|
"customCss_description": "vlastný css obsah. Poznámka: obsah a vzdialené url linky sú defaultne deaktivované.Náhľad vášho obsahu je zobrazený nižšie. Pridané polia, ktoré ste nenastavovali boli pridané pri sanitizácii",
|
||||||
"customFontPath": "cesta k vlastným fontom",
|
"customFontPath": "cesta k vlastným fontom",
|
||||||
"customFontPath_description": "Nastaví cestu k vlastným fontom na použitie aplikáciou",
|
"customFontPath_description": "Nastaví cestu k vlastným fontom na použitie aplikáciou",
|
||||||
"disableAutomaticUpdates": "vypnúť automatické aktualizácie",
|
|
||||||
"disableLibraryUpdateOnStartup": "vypnúť kontrolu nových verzií pri štarte",
|
"disableLibraryUpdateOnStartup": "vypnúť kontrolu nových verzií pri štarte",
|
||||||
"discordApplicationId": "id aplikácie {{discord}}",
|
"discordApplicationId": "id aplikácie {{discord}}",
|
||||||
"discordApplicationId_description": "aplikačné id pre plnohodnotné prepojenie s {{discord}} (predvolená hodnota {{defaultId}})",
|
"discordApplicationId_description": "aplikačné id pre plnohodnotné prepojenie s {{discord}} (predvolená hodnota {{defaultId}})",
|
||||||
|
|||||||
@@ -548,7 +548,6 @@
|
|||||||
"customCss_description": "vsebina css po meri. Opomba: vsebina in oddaljeni url-ji so prepovedane lastnosti. Spodaj je prikazan predogled vaše vsebine. Dodatna polja, ki jih niste nastavili, so prisotna zaradi prečiščevanja",
|
"customCss_description": "vsebina css po meri. Opomba: vsebina in oddaljeni url-ji so prepovedane lastnosti. Spodaj je prikazan predogled vaše vsebine. Dodatna polja, ki jih niste nastavili, so prisotna zaradi prečiščevanja",
|
||||||
"customFontPath": "pot za pisavo po meri",
|
"customFontPath": "pot za pisavo po meri",
|
||||||
"customFontPath_description": "nastavi pot do pisave po meri",
|
"customFontPath_description": "nastavi pot do pisave po meri",
|
||||||
"disableAutomaticUpdates": "onemogoči samodejne posodobitve",
|
|
||||||
"disableLibraryUpdateOnStartup": "onemogoči prevejranje novih verzij ob zagonu",
|
"disableLibraryUpdateOnStartup": "onemogoči prevejranje novih verzij ob zagonu",
|
||||||
"discordApplicationId": "{{discord}} identifikator aplikacije",
|
"discordApplicationId": "{{discord}} identifikator aplikacije",
|
||||||
"discordApplicationId_description": "identifikator aplikacije za {{discord}} bogato prezenco (privzeto {{defaultId}})",
|
"discordApplicationId_description": "identifikator aplikacije za {{discord}} bogato prezenco (privzeto {{defaultId}})",
|
||||||
|
|||||||
@@ -88,7 +88,6 @@
|
|||||||
"hotkey_globalSearch": "globalno pretraživanje",
|
"hotkey_globalSearch": "globalno pretraživanje",
|
||||||
"gaplessAudio_description": "postavlja opciju bez pauze zvuka za mpv (preporučeno: slabo)",
|
"gaplessAudio_description": "postavlja opciju bez pauze zvuka za mpv (preporučeno: slabo)",
|
||||||
"remoteUsername_description": "postavlja korisničko ime za daljinsku kontrolu servera. Ako su i korisničko ime i lozinka prazni, autentifikacija će biti onemogućena",
|
"remoteUsername_description": "postavlja korisničko ime za daljinsku kontrolu servera. Ako su i korisničko ime i lozinka prazni, autentifikacija će biti onemogućena",
|
||||||
"disableAutomaticUpdates": "onemogući automatsko ažuriranje",
|
|
||||||
"exitToTray_description": "izlazak aplikacije u sistemsku traku",
|
"exitToTray_description": "izlazak aplikacije u sistemsku traku",
|
||||||
"followLyric_description": "pomera tekst pesme na trenutnu poziciju reprodukcije",
|
"followLyric_description": "pomera tekst pesme na trenutnu poziciju reprodukcije",
|
||||||
"hotkey_favoritePreviousSong": "omiljena $t(common.previousSong)",
|
"hotkey_favoritePreviousSong": "omiljena $t(common.previousSong)",
|
||||||
|
|||||||
@@ -609,7 +609,6 @@
|
|||||||
"customCssEnable": "தனிப்பயன் சிஎச்எச் ஐ இயக்கவும்",
|
"customCssEnable": "தனிப்பயன் சிஎச்எச் ஐ இயக்கவும்",
|
||||||
"customCssNotice": "எச்சரிக்கை: சில சுத்திகரிப்பு (URL () மற்றும் உள்ளடக்கத்தை அனுமதிக்காதது :) இருக்கும்போது, தனிப்பயன் சிஎச்எச் ஐப் பயன்படுத்துவது இடைமுகத்தை மாற்றுவதன் மூலம் ஆபத்துக்களை ஏற்படுத்தக்கூடும்",
|
"customCssNotice": "எச்சரிக்கை: சில சுத்திகரிப்பு (URL () மற்றும் உள்ளடக்கத்தை அனுமதிக்காதது :) இருக்கும்போது, தனிப்பயன் சிஎச்எச் ஐப் பயன்படுத்துவது இடைமுகத்தை மாற்றுவதன் மூலம் ஆபத்துக்களை ஏற்படுத்தக்கூடும்",
|
||||||
"contextMenu_description": "நீங்கள் ஒரு உருப்படியை வலது சொடுக்கு செய்யும் போது பட்டியலில் காட்டப்பட்டுள்ள உருப்படிகளை மறைக்க உங்களை அனுமதிக்கிறது. சரிபார்க்கப்படாத உருப்படிகள் மறைக்கப்படும்",
|
"contextMenu_description": "நீங்கள் ஒரு உருப்படியை வலது சொடுக்கு செய்யும் போது பட்டியலில் காட்டப்பட்டுள்ள உருப்படிகளை மறைக்க உங்களை அனுமதிக்கிறது. சரிபார்க்கப்படாத உருப்படிகள் மறைக்கப்படும்",
|
||||||
"disableAutomaticUpdates": "தானியங்கி புதுப்பிப்புகளை முடக்கு",
|
|
||||||
"discordApplicationId_description": "{{discord}} பணக்கார இருப்புக்கான பயன்பாட்டு ஐடி (இயல்புநிலை {{defaultId}})",
|
"discordApplicationId_description": "{{discord}} பணக்கார இருப்புக்கான பயன்பாட்டு ஐடி (இயல்புநிலை {{defaultId}})",
|
||||||
"discordIdleStatus": "பணக்கார இருப்பு செயலற்ற நிலையைக் காட்டுங்கள்",
|
"discordIdleStatus": "பணக்கார இருப்பு செயலற்ற நிலையைக் காட்டுங்கள்",
|
||||||
"discordIdleStatus_description": "இயக்கப்பட்டால், பிளேயர் சும்மா இருக்கும்போது நிலையைப் புதுப்பிக்கவும்",
|
"discordIdleStatus_description": "இயக்கப்பட்டால், பிளேயர் சும்மா இருக்கும்போது நிலையைப் புதுப்பிக்கவும்",
|
||||||
|
|||||||
@@ -534,7 +534,6 @@
|
|||||||
"customCss_description": "özel css içeriği. Not: içerik ve uzaktan URL'ler izin verilmeyen özelliklerdir. İçeriğinizin önizlemesi aşağıda gösterilmektedir. Ayarlamadığınız ek alanlar sterilleme nedeniyle mevcuttur",
|
"customCss_description": "özel css içeriği. Not: içerik ve uzaktan URL'ler izin verilmeyen özelliklerdir. İçeriğinizin önizlemesi aşağıda gösterilmektedir. Ayarlamadığınız ek alanlar sterilleme nedeniyle mevcuttur",
|
||||||
"customFontPath": "özel yazı tipi yolu",
|
"customFontPath": "özel yazı tipi yolu",
|
||||||
"customFontPath_description": "uygulama için kullanılacak özel yazı tipinin yolunu ayarlar",
|
"customFontPath_description": "uygulama için kullanılacak özel yazı tipinin yolunu ayarlar",
|
||||||
"disableAutomaticUpdates": "otomatik güncellemeleri devre dışı bırak",
|
|
||||||
"disableLibraryUpdateOnStartup": "başlangıçta yeni sürümler için denetimi devre dışı bırak",
|
"disableLibraryUpdateOnStartup": "başlangıçta yeni sürümler için denetimi devre dışı bırak",
|
||||||
"discordApplicationId": "{{discord}} uygulama kimliği",
|
"discordApplicationId": "{{discord}} uygulama kimliği",
|
||||||
"discordApplicationId_description": "{{discord}} \"Rich Presence\" için uygulama kimliği (varsayılan olarak {{defaultId}})",
|
"discordApplicationId_description": "{{discord}} \"Rich Presence\" için uygulama kimliği (varsayılan olarak {{defaultId}})",
|
||||||
|
|||||||
+540
-1
@@ -1,5 +1,544 @@
|
|||||||
{
|
{
|
||||||
"action": {
|
"action": {
|
||||||
"addToFavorites": "додати до $t(entity.favorite, {\"count\": 2})"
|
"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": "відкрити каталог додатків",
|
||||||
|
"openIn": {
|
||||||
|
"lastfm": "Відкрити в Last.fm",
|
||||||
|
"musicbrainz": "Відкрити в MusicBrainz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"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": "канал",
|
||||||
|
"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": "фільтр",
|
||||||
|
"filter_few": "фільтри",
|
||||||
|
"filter_many": "фільтрів",
|
||||||
|
"filters": "фільтри",
|
||||||
|
"filter_single": "одиночний",
|
||||||
|
"filter_multiple": "кілька",
|
||||||
|
"forceRestartRequired": "перезапустіть, щоб застосувати зміни… закрийте повідомлення, щоб перезапустити",
|
||||||
|
"forward": "уперед",
|
||||||
|
"gap": "прогалина",
|
||||||
|
"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": "налаштування",
|
||||||
|
"setting_few": "налаштування",
|
||||||
|
"setting_many": "налаштувань",
|
||||||
|
"slower": "повільніше",
|
||||||
|
"share": "поділитися",
|
||||||
|
"size": "розмір",
|
||||||
|
"sort": "впорядкувати",
|
||||||
|
"sortOrder": "порядок",
|
||||||
|
"tags": "теги",
|
||||||
|
"title": "назва",
|
||||||
|
"trackNumber": "трек",
|
||||||
|
"trackGain": "підсилення треку",
|
||||||
|
"trackPeak": "піковий рівень треку",
|
||||||
|
"translation": "переклад",
|
||||||
|
"unknown": "невідомий",
|
||||||
|
"version": "версія",
|
||||||
|
"year": "рік",
|
||||||
|
"yes": "так",
|
||||||
|
"explicit": "Експліцитний зміст",
|
||||||
|
"gridRows": "рядки сітки",
|
||||||
|
"tableColumns": "стовпці таблиці",
|
||||||
|
"itemsMore": "{{count}} більше"
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"album_one": "альбом",
|
||||||
|
"album_few": "альбоми",
|
||||||
|
"album_many": "альбомів",
|
||||||
|
"albumArtist_one": "виконавець альбому",
|
||||||
|
"albumArtist_few": "виконавці альбому",
|
||||||
|
"albumArtist_many": "виконавців альбому",
|
||||||
|
"albumArtistCount_one": "{{count}} виконавець альбому",
|
||||||
|
"albumArtistCount_few": "{{count}} виконавці альбому",
|
||||||
|
"albumArtistCount_many": "{{count}} виконавців альбому",
|
||||||
|
"albumWithCount_one": "{{count}} альбом",
|
||||||
|
"albumWithCount_few": "{{count}} альбоми",
|
||||||
|
"albumWithCount_many": "{{count}} альбомів",
|
||||||
|
"radioStation_one": "радіостанція",
|
||||||
|
"radioStation_few": "радіостанції",
|
||||||
|
"radioStation_many": "радіостанцій",
|
||||||
|
"radioStationWithCount_one": "{{count}} радіостанція",
|
||||||
|
"radioStationWithCount_few": "{{count}} радіостанції",
|
||||||
|
"radioStationWithCount_many": "{{count}} радіостанцій",
|
||||||
|
"artist_one": "виконавець",
|
||||||
|
"artist_few": "виконавці",
|
||||||
|
"artist_many": "виконавців",
|
||||||
|
"artistWithCount_one": "{{count}} виконавець",
|
||||||
|
"artistWithCount_few": "{{count}} виконавці",
|
||||||
|
"artistWithCount_many": "{{count}} виконавців",
|
||||||
|
"favorite_one": "улюблений",
|
||||||
|
"favorite_few": "улюблені",
|
||||||
|
"favorite_many": "улюблених",
|
||||||
|
"folder_one": "папка",
|
||||||
|
"folder_few": "папки",
|
||||||
|
"folder_many": "папок",
|
||||||
|
"folderWithCount_one": "{{count}} папка",
|
||||||
|
"folderWithCount_few": "{{count}} папки",
|
||||||
|
"folderWithCount_many": "{{count}} папок",
|
||||||
|
"genre_one": "жанр",
|
||||||
|
"genre_few": "жанри",
|
||||||
|
"genre_many": "жанрів",
|
||||||
|
"genreWithCount_one": "{{count}} жанр",
|
||||||
|
"genreWithCount_few": "{{count}} жанри",
|
||||||
|
"genreWithCount_many": "{{count}} жанрів",
|
||||||
|
"playlist_one": "плейлист",
|
||||||
|
"playlist_few": "плейлисти",
|
||||||
|
"playlist_many": "плейлистів",
|
||||||
|
"play_one": "{{count}} відтворення",
|
||||||
|
"play_few": "{{count}} відтворення",
|
||||||
|
"play_many": "{{count}} відтворень",
|
||||||
|
"playlistWithCount_one": "{{count}} плейлист",
|
||||||
|
"playlistWithCount_few": "{{count}} плейлисти",
|
||||||
|
"playlistWithCount_many": "{{count}} плейлистів",
|
||||||
|
"smartPlaylist": "розумний $t(entity.playlist, {\"count\": 1})",
|
||||||
|
"track_one": "трек",
|
||||||
|
"track_few": "треки",
|
||||||
|
"track_many": "треків",
|
||||||
|
"song_one": "пісня",
|
||||||
|
"song_few": "пісні",
|
||||||
|
"song_many": "пісень",
|
||||||
|
"trackWithCount_one": "{{count}} трек",
|
||||||
|
"trackWithCount_few": "{{count}} треки",
|
||||||
|
"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": "виявлено розбіжності між налаштуваннями в рендерері та основним процесом. перезапустіть програму, щоб застосувати зміни"
|
||||||
|
},
|
||||||
|
"filter": {
|
||||||
|
"album": "$t(entity.album, {\"count\": 1})",
|
||||||
|
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
|
||||||
|
"albumCount": "кількість $t(entity.album, {\"count\": 2})",
|
||||||
|
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||||
|
"biography": "біографія",
|
||||||
|
"bitrate": "бітрейт",
|
||||||
|
"bpm": "уд/хв",
|
||||||
|
"channels": "$t(common.channel, {\"count\": 2})",
|
||||||
|
"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": "примітка",
|
||||||
|
"owner": "$t(common.owner)",
|
||||||
|
"path": "шлях",
|
||||||
|
"playCount": "кількість відтворень",
|
||||||
|
"random": "випадково",
|
||||||
|
"rating": "рейтинг",
|
||||||
|
"recentlyAdded": "нещодавно додано",
|
||||||
|
"recentlyPlayed": "нещодавно відтворено",
|
||||||
|
"recentlyUpdated": "нещодавно оновлено",
|
||||||
|
"releaseDate": "дата випуску",
|
||||||
|
"releaseYear": "рік випуску",
|
||||||
|
"search": "шукати",
|
||||||
|
"songCount": "кількість пісень",
|
||||||
|
"sortName": "сортування за назвою",
|
||||||
|
"title": "назва",
|
||||||
|
"toYear": "до року",
|
||||||
|
"trackNumber": "трек",
|
||||||
|
"explicitStatus": "$t(common.explicitStatus)"
|
||||||
|
},
|
||||||
|
"datetime": {
|
||||||
|
"minuteShort": "хв.",
|
||||||
|
"secondShort": "сек.",
|
||||||
|
"hourShort": "год",
|
||||||
|
"dayShort": "дн."
|
||||||
|
},
|
||||||
|
"filterOperator": {
|
||||||
|
"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": "зберегти пароль",
|
||||||
|
"input_url": "URL-адреса",
|
||||||
|
"input_username": "Ім'я користувача",
|
||||||
|
"success": "сервер додано успішно",
|
||||||
|
"title": "додати сервер"
|
||||||
|
},
|
||||||
|
"largeFetchConfirmation": {
|
||||||
|
"title": "додати елементи до черги",
|
||||||
|
"description": "Ця дія додасть усі елементи в поточний відфільтрований перегляд"
|
||||||
|
},
|
||||||
|
"addToPlaylist": {
|
||||||
|
"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})"
|
||||||
|
},
|
||||||
|
"createPlaylist": {
|
||||||
|
"input_description": "$t(common.description)",
|
||||||
|
"input_name": "$t(common.name)",
|
||||||
|
"input_owner": "$t(common.owner)",
|
||||||
|
"input_public": "публічний",
|
||||||
|
"success": "$t(entity.playlist, {\"count\": 1}) стрворено успішно",
|
||||||
|
"title": "створити $t(entity.playlist, {\"count\": 1})"
|
||||||
|
},
|
||||||
|
"createRadioStation": {
|
||||||
|
"success": "радіостанція створена успішно",
|
||||||
|
"title": "створити радіостанцію",
|
||||||
|
"input_homepageUrl": "адреса домашньої сторінки",
|
||||||
|
"input_name": "назва",
|
||||||
|
"input_streamUrl": "URL-адреса потоку"
|
||||||
|
},
|
||||||
|
"deletePlaylist": {
|
||||||
|
"input_confirm": "введіть ім'я $t(entity.playlist, {\"count\": 1}) для підтвердження",
|
||||||
|
"success": "$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})"
|
||||||
|
},
|
||||||
|
"lyricsExport": {
|
||||||
|
"export": "експортувати тексти пісень",
|
||||||
|
"input_synced": "експортувати синхронізовані тексти пісень",
|
||||||
|
"input_offset": "$t(setting.lyricOffset)"
|
||||||
|
},
|
||||||
|
"lyricSearch": {
|
||||||
|
"input_artist": "$t(entity.artist, {\"count\": 1})",
|
||||||
|
"input_name": "$t(common.name)",
|
||||||
|
"title": "шукати тексти пісень"
|
||||||
|
},
|
||||||
|
"queryEditor": {
|
||||||
|
"title": "редактор запитів",
|
||||||
|
"input_optionMatchAll": "збіг за всіма",
|
||||||
|
"input_optionMatchAny": "збіг за будь-яким",
|
||||||
|
"addRuleGroup": "додати групу правил",
|
||||||
|
"removeRuleGroup": "видалити групу правил",
|
||||||
|
"resetToDefault": "скинути до заводських налаштувань",
|
||||||
|
"clearFilters": "очистити фільтри"
|
||||||
|
},
|
||||||
|
"saveQueue": {
|
||||||
|
"success": "черга відтворення збережена на сервері"
|
||||||
|
},
|
||||||
|
"shareItem": {
|
||||||
|
"allowDownloading": "дозволити завантаження",
|
||||||
|
"description": "опис",
|
||||||
|
"setExpiration": "встановити термін дії",
|
||||||
|
"success": "посилання для спільного використання скопійовано в буфер обміну (натисніть тут, щоб відкрити)",
|
||||||
|
"expireInvalid": "термін дії повинен бути в майбутньому",
|
||||||
|
"createFailed": "не вдалося створити спільний доступ (чи ввімкнено спільний доступ?)"
|
||||||
|
},
|
||||||
|
"shuffleAll": {
|
||||||
|
"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": "тільки відтворені треки"
|
||||||
|
},
|
||||||
|
"updateServer": {
|
||||||
|
"success": "сервер успішно оновлено",
|
||||||
|
"title": "оновити сервер"
|
||||||
|
},
|
||||||
|
"privateMode": {
|
||||||
|
"enabled": "приватний режим увімкнено, стан відтворення тепер приховано від зовнішніх інтеграцій",
|
||||||
|
"disabled": "приватний режим вимкнено, стан відтворення тепер видно для увімкнених зовнішніх інтеграцій",
|
||||||
|
"title": "приватний режим"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"player": {
|
||||||
|
"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})"
|
||||||
|
},
|
||||||
|
"albumArtistList": {
|
||||||
|
"title": "$t(entity.albumArtist, {\"count\": 2})"
|
||||||
|
},
|
||||||
|
"albumDetail": {
|
||||||
|
"moreFromArtist": "більше від цього $t(entity.artist, {\"count\": 1})",
|
||||||
|
"moreFromGeneric": "більше від {{item}}",
|
||||||
|
"released": "видано"
|
||||||
|
},
|
||||||
|
"albumList": {
|
||||||
|
"artistAlbums": "альбоми виконавця {{artist}}",
|
||||||
|
"genreAlbums": "\"{{genre}}\" $t(entity.album, {\"count\": 2})",
|
||||||
|
"title": "$t(entity.album, {\"count\": 2})"
|
||||||
|
},
|
||||||
|
"radioList": {
|
||||||
|
"title": "радіостанції"
|
||||||
|
},
|
||||||
|
"releasenotes": {
|
||||||
|
"commitsSinceStable": "комміти від {{stable}}",
|
||||||
|
"noNewCommits": "немає нових коммітів у цьому періоді",
|
||||||
|
"noStableReleaseToCompare": "немає доступної стабільної версії для порівняння"
|
||||||
|
},
|
||||||
|
"favorites": {
|
||||||
|
"title": "$t(entity.favorite, {\"count\": 2})"
|
||||||
|
},
|
||||||
|
"windowBar": {
|
||||||
|
"paused": "(Призупинено) ",
|
||||||
|
"privateMode": "(Приватний режим)"
|
||||||
|
},
|
||||||
|
"appMenu": {
|
||||||
|
"collapseSidebar": "згорнути бічну панель",
|
||||||
|
"commandPalette": "відкрити палітру команд",
|
||||||
|
"expandSidebar": "розгорнути бічну панель",
|
||||||
|
"goBack": "повернутися назад",
|
||||||
|
"goForward": "перейти вперед",
|
||||||
|
"manageServers": "управління серверами",
|
||||||
|
"privateModeOff": "вимкнути приватний режим",
|
||||||
|
"privateModeOn": "увімкнути приватний режим",
|
||||||
|
"openBrowserDevtools": "відкрити інструменти розробника",
|
||||||
|
"quit": "$t(common.quit)",
|
||||||
|
"selectServer": "вибрати сервер",
|
||||||
|
"selectMusicFolder": "вибрати папку з музикою",
|
||||||
|
"noMusicFolder": "не вибрано папку з музикою",
|
||||||
|
"multipleMusicFolders": "Вибрано {{count}} папок з музикою",
|
||||||
|
"settings": "$t(common.setting, {\"count\": 2})",
|
||||||
|
"version": "версія {{version}}"
|
||||||
|
},
|
||||||
|
"manageServers": {
|
||||||
|
"title": "управління серверами",
|
||||||
|
"serverDetails": "інформація про сервер",
|
||||||
|
"url": "URL-адреса",
|
||||||
|
"username": "Ім'я користувача",
|
||||||
|
"editServerDetailsTooltip": "редагувати дані сервера",
|
||||||
|
"removeServer": "видалити сервер"
|
||||||
|
},
|
||||||
|
"contextMenu": {
|
||||||
|
"addFavorite": "$t(action.addToFavorites)",
|
||||||
|
"addLast": "$t(player.addLast)",
|
||||||
|
"addNext": "$t(player.addNext)",
|
||||||
|
"addToFavorites": "$t(action.addToFavorites)",
|
||||||
|
"addToPlaylist": "$t(action.addToPlaylist)",
|
||||||
|
"createPlaylist": "$t(action.createPlaylist)",
|
||||||
|
"deletePlaylist": "$t(action.deletePlaylist)",
|
||||||
|
"deselectAll": "$t(action.deselectAll)",
|
||||||
|
"download": "завантажити",
|
||||||
|
"moveItems": "$t(action.moveItems)",
|
||||||
|
"moveToNext": "$t(action.moveToNext)",
|
||||||
|
"moveToBottom": "$t(action.moveToBottom)",
|
||||||
|
"moveToTop": "$t(action.moveToTop)",
|
||||||
|
"numberSelected": "{{count}} вибрано",
|
||||||
|
"play": "$t(player.play)",
|
||||||
|
"playSimilarSongs": "$t(player.playSimilarSongs)",
|
||||||
|
"removeFromFavorites": "$t(action.removeFromFavorites)",
|
||||||
|
"removeFromPlaylist": "$t(action.removeFromPlaylist)",
|
||||||
|
"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": "отримати інформацію"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+183
-30
@@ -150,7 +150,12 @@
|
|||||||
"tableColumns": "表格列",
|
"tableColumns": "表格列",
|
||||||
"itemsMore": "{{count}} 更多",
|
"itemsMore": "{{count}} 更多",
|
||||||
"countSelected": "已选择{{count}}项",
|
"countSelected": "已选择{{count}}项",
|
||||||
"retry": "重试"
|
"retry": "重试",
|
||||||
|
"example": "示例",
|
||||||
|
"filter_single": "单项",
|
||||||
|
"mood": "氛围",
|
||||||
|
"rename": "重命名",
|
||||||
|
"filter_multiple": "多项"
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"albumArtist_other": "专辑艺术家",
|
"albumArtist_other": "专辑艺术家",
|
||||||
@@ -170,7 +175,9 @@
|
|||||||
"genreWithCount_other": "{{count}} 种流派",
|
"genreWithCount_other": "{{count}} 种流派",
|
||||||
"trackWithCount_other": "{{count}} 首曲目",
|
"trackWithCount_other": "{{count}} 首曲目",
|
||||||
"play_other": "{{count}} 次播放",
|
"play_other": "{{count}} 次播放",
|
||||||
"song_other": "歌曲"
|
"song_other": "歌曲",
|
||||||
|
"radioStation_other": "广播电台",
|
||||||
|
"radioStationWithCount_other": "{{count}} 个广播电台"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
"repeat_all": "循环全部",
|
"repeat_all": "循环全部",
|
||||||
@@ -187,7 +194,7 @@
|
|||||||
"shuffle": "播放(随机)",
|
"shuffle": "播放(随机)",
|
||||||
"playbackFetchNoResults": "未找到歌曲",
|
"playbackFetchNoResults": "未找到歌曲",
|
||||||
"playbackFetchInProgress": "正在加载歌曲…",
|
"playbackFetchInProgress": "正在加载歌曲…",
|
||||||
"addNext": "下一首播放",
|
"addNext": "下一个",
|
||||||
"playbackFetchCancel": "请稍等…关闭通知以取消操作",
|
"playbackFetchCancel": "请稍等…关闭通知以取消操作",
|
||||||
"play": "播放",
|
"play": "播放",
|
||||||
"repeat_off": "循环关闭",
|
"repeat_off": "循环关闭",
|
||||||
@@ -197,7 +204,7 @@
|
|||||||
"queue_moveToTop": "将所选项移至底部",
|
"queue_moveToTop": "将所选项移至底部",
|
||||||
"queue_moveToBottom": "将所选项移至顶部",
|
"queue_moveToBottom": "将所选项移至顶部",
|
||||||
"shuffle_off": "禁用随机播放",
|
"shuffle_off": "禁用随机播放",
|
||||||
"addLast": "上一曲",
|
"addLast": "最后",
|
||||||
"mute": "静音",
|
"mute": "静音",
|
||||||
"skip_forward": "向前跳过",
|
"skip_forward": "向前跳过",
|
||||||
"playbackSpeed": "播放速度",
|
"playbackSpeed": "播放速度",
|
||||||
@@ -206,7 +213,12 @@
|
|||||||
"viewQueue": "查看播放队列",
|
"viewQueue": "查看播放队列",
|
||||||
"saveQueueToServer": "将播放队列保存到服务器",
|
"saveQueueToServer": "将播放队列保存到服务器",
|
||||||
"restoreQueueFromServer": "从服务器恢复播放队列",
|
"restoreQueueFromServer": "从服务器恢复播放队列",
|
||||||
"lyrics": "歌词"
|
"lyrics": "歌词",
|
||||||
|
"addLastShuffled": "最后(随机)",
|
||||||
|
"addNextShuffled": "下一个(随机)",
|
||||||
|
"artistRadio": "艺术家电台",
|
||||||
|
"holdToShuffle": "按住即可随机",
|
||||||
|
"trackRadio": "追踪广播"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"crossfadeStyle_description": "选择用于音频播放器的淡入淡出风格",
|
"crossfadeStyle_description": "选择用于音频播放器的淡入淡出风格",
|
||||||
@@ -217,7 +229,6 @@
|
|||||||
"audioPlayer_description": "选择用于播放的音频播放器",
|
"audioPlayer_description": "选择用于播放的音频播放器",
|
||||||
"globalMediaHotkeys": "全局媒体快捷键",
|
"globalMediaHotkeys": "全局媒体快捷键",
|
||||||
"gaplessAudio_description": "调整 mpv 无缝音频设置",
|
"gaplessAudio_description": "调整 mpv 无缝音频设置",
|
||||||
"disableAutomaticUpdates": "禁用自动更新",
|
|
||||||
"followLyric_description": "滚动歌词到当前播放位置",
|
"followLyric_description": "滚动歌词到当前播放位置",
|
||||||
"audioExclusiveMode": "音频独占模式",
|
"audioExclusiveMode": "音频独占模式",
|
||||||
"font": "字体",
|
"font": "字体",
|
||||||
@@ -333,7 +344,7 @@
|
|||||||
"useSystemTheme_description": "使用系统定义的浅色或深色主题",
|
"useSystemTheme_description": "使用系统定义的浅色或深色主题",
|
||||||
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||||
"lyricFetch_description": "从多个互联网源获取歌词",
|
"lyricFetch_description": "从多个互联网源获取歌词",
|
||||||
"lyricFetchProvider_description": "选择歌词源。 歌词源顺序与查询顺序一致",
|
"lyricFetchProvider_description": "选择要从中获取歌词的提供商",
|
||||||
"sidePlayQueueStyle_optionDetached": "不吸附",
|
"sidePlayQueueStyle_optionDetached": "不吸附",
|
||||||
"hotkey_zoomOut": "缩小",
|
"hotkey_zoomOut": "缩小",
|
||||||
"hotkey_unfavoriteCurrentSong": "取消收藏$t(common.currentSong)",
|
"hotkey_unfavoriteCurrentSong": "取消收藏$t(common.currentSong)",
|
||||||
@@ -452,9 +463,9 @@
|
|||||||
"releaseChannel_optionLatest": "最新的",
|
"releaseChannel_optionLatest": "最新的",
|
||||||
"releaseChannel_optionBeta": "测试版",
|
"releaseChannel_optionBeta": "测试版",
|
||||||
"releaseChannel": "发布通道",
|
"releaseChannel": "发布通道",
|
||||||
"releaseChannel_description": "选择稳定版本或测试版以进行自动更新",
|
"releaseChannel_description": "选择稳定版、测试版或 Alpha(夜间构建版)以启用自动更新。",
|
||||||
"mediaSession": "启用媒体会话",
|
"mediaSession": "启用媒体会话",
|
||||||
"mediaSession_description": "启用 Windows 媒体会话集成,在系统音量覆盖和锁定屏幕中显示媒体控件和元数据(仅限 Windows)",
|
"mediaSession_description": "启用媒体会话集成,在系统音量叠加层和锁屏界面显示媒体控件和元数据",
|
||||||
"exportImportSettings_control_description": "通过 JSON 导出和导入设置",
|
"exportImportSettings_control_description": "通过 JSON 导出和导入设置",
|
||||||
"exportImportSettings_control_exportText": "导出设置",
|
"exportImportSettings_control_exportText": "导出设置",
|
||||||
"exportImportSettings_control_importText": "导入设置",
|
"exportImportSettings_control_importText": "导入设置",
|
||||||
@@ -515,7 +526,43 @@
|
|||||||
"playerbarWaveformAlign": "波形对齐方式",
|
"playerbarWaveformAlign": "波形对齐方式",
|
||||||
"playerbarWaveformBarWidth": "波形宽度",
|
"playerbarWaveformBarWidth": "波形宽度",
|
||||||
"playerbarWaveformGap": "波形间距",
|
"playerbarWaveformGap": "波形间距",
|
||||||
"transcode": "启用转码功能"
|
"transcode": "启用转码功能",
|
||||||
|
"useThemeAccentColor_description": "使用所选主题中定义的主色,而不是自定义强调色",
|
||||||
|
"homeFeatureStyle_optionSingle": "单项",
|
||||||
|
"autoDJ": "自动DJ",
|
||||||
|
"autoDJ_itemCount": "项目数量",
|
||||||
|
"autoDJ_itemCount_description": "启用自动 DJ 功能后,尝试添加到队列中的项目数",
|
||||||
|
"autoDJ_timing": "定时",
|
||||||
|
"autoDJ_timing_description": "自动 DJ 触发前队列中剩余的歌曲数量",
|
||||||
|
"crossfadeStyle": "交叉渐变风格",
|
||||||
|
"discordRichPresence": "{{discord}} rich presence",
|
||||||
|
"homeFeatureStyle_description": "控制首页特色轮播图的样式",
|
||||||
|
"homeFeatureStyle": "首页特色旋转样式",
|
||||||
|
"homeFeatureStyle_optionMultiple": "多样",
|
||||||
|
"hotkey_listNavigateToPage": "列表导航至项目页面",
|
||||||
|
"hotkey_listPlayDefault": "播放列表",
|
||||||
|
"hotkey_listPlayLast": "播放列表最后",
|
||||||
|
"hotkey_listPlayNext": "播放列表下一个",
|
||||||
|
"hotkey_listPlayNow": "播放列表现在",
|
||||||
|
"pathReplace": "文件路径替换",
|
||||||
|
"pathReplace_description": "替换服务器的默认文件路径",
|
||||||
|
"pathReplace_optionRemovePrefix": "移除前缀",
|
||||||
|
"pathReplace_optionAddPrefix": "添加前缀",
|
||||||
|
"playerFilters": "从队列中筛选歌曲",
|
||||||
|
"playerFilters_description": "根据以下条件,忽略添加到队列中的歌曲",
|
||||||
|
"artistRadioCount_description": "设置艺术家电台和曲目电台要获取的歌曲数量",
|
||||||
|
"artistRadioCount": "艺术家/曲目电台数量",
|
||||||
|
"imageResolution_optionItemCard": "项目卡",
|
||||||
|
"playerbarWaveformRadius": "波形半径",
|
||||||
|
"enableGridMultiSelect": "启用网格多选",
|
||||||
|
"enableGridMultiSelect_description": "启用后,允许在网格视图中选择多个项目。禁用后,点击网格项目图像将跳转到项目页面",
|
||||||
|
"sidebarPlaylistSorting_description": "允许在侧边栏中使用拖放操作手动对播放列表进行排序,而不是使用默认的服务器顺序",
|
||||||
|
"sidebarPlaylistSorting": "侧边栏播放列表排序",
|
||||||
|
"sidebarPlaylistListFilterRegex_description": "隐藏侧边栏中与此正则表达式匹配的播放列表",
|
||||||
|
"sidebarPlaylistListFilterRegex_placeholder": "例如:^每日精选*",
|
||||||
|
"sidebarPlaylistListFilterRegex": "播放列表筛选正则表达式",
|
||||||
|
"queryBuilder": "查询构建器",
|
||||||
|
"queryBuilderCustomFields": "自定义字段"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"remotePortWarning": "重启服务器使新端口生效",
|
"remotePortWarning": "重启服务器使新端口生效",
|
||||||
@@ -568,7 +615,7 @@
|
|||||||
"biography": "个人简介",
|
"biography": "个人简介",
|
||||||
"songCount": "歌曲数量",
|
"songCount": "歌曲数量",
|
||||||
"random": "随机",
|
"random": "随机",
|
||||||
"lastPlayed": "上次播放过",
|
"lastPlayed": "最后播放",
|
||||||
"toYear": "截止年份",
|
"toYear": "截止年份",
|
||||||
"fromYear": "起始年份",
|
"fromYear": "起始年份",
|
||||||
"criticRating": "评论家评分",
|
"criticRating": "评论家评分",
|
||||||
@@ -582,7 +629,7 @@
|
|||||||
"recentlyUpdated": "最近更新",
|
"recentlyUpdated": "最近更新",
|
||||||
"isRated": "已评分",
|
"isRated": "已评分",
|
||||||
"isRecentlyPlayed": "最近播放过",
|
"isRecentlyPlayed": "最近播放过",
|
||||||
"channels": "$t(common.channel_other)",
|
"channels": "$t(common.channel, {\"count\": 2})",
|
||||||
"owner": "$t(common.owner)",
|
"owner": "$t(common.owner)",
|
||||||
"genre": "$t(entity.genre, {\"count\": 1})",
|
"genre": "$t(entity.genre, {\"count\": 1})",
|
||||||
"note": "注释",
|
"note": "注释",
|
||||||
@@ -591,7 +638,8 @@
|
|||||||
"disc": "碟片",
|
"disc": "碟片",
|
||||||
"duration": "时长",
|
"duration": "时长",
|
||||||
"album": "$t(entity.album, {\"count\": 1})",
|
"album": "$t(entity.album, {\"count\": 1})",
|
||||||
"explicitStatus": "$t(common.explicitStatus)"
|
"explicitStatus": "$t(common.explicitStatus)",
|
||||||
|
"sortName": "排序名称"
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
@@ -609,7 +657,8 @@
|
|||||||
"shared": "共享$t(entity.playlist, {\"count\": 2})",
|
"shared": "共享$t(entity.playlist, {\"count\": 2})",
|
||||||
"myLibrary": "我的媒体库",
|
"myLibrary": "我的媒体库",
|
||||||
"favorites": "$t(entity.favorite, {\"count\": 2})",
|
"favorites": "$t(entity.favorite, {\"count\": 2})",
|
||||||
"radio": "$t(entity.radioStation, {\"count\": 2})"
|
"radio": "$t(entity.radioStation, {\"count\": 2})",
|
||||||
|
"collections": "合集"
|
||||||
},
|
},
|
||||||
"fullscreenPlayer": {
|
"fullscreenPlayer": {
|
||||||
"config": {
|
"config": {
|
||||||
@@ -649,7 +698,8 @@
|
|||||||
"privateModeOn": "开启私人模式",
|
"privateModeOn": "开启私人模式",
|
||||||
"multipleMusicFolders": "已选择{{count}}个媒体库",
|
"multipleMusicFolders": "已选择{{count}}个媒体库",
|
||||||
"noMusicFolder": "未选择任何音乐库",
|
"noMusicFolder": "未选择任何音乐库",
|
||||||
"selectMusicFolder": "选择媒体库"
|
"selectMusicFolder": "选择媒体库",
|
||||||
|
"commandPalette": "打开命令面板"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"mostPlayed": "最多播放",
|
"mostPlayed": "最多播放",
|
||||||
@@ -687,7 +737,8 @@
|
|||||||
"discord": "Discord",
|
"discord": "Discord",
|
||||||
"logger": "日志记录器",
|
"logger": "日志记录器",
|
||||||
"queryBuilder": "查询构建器",
|
"queryBuilder": "查询构建器",
|
||||||
"lyricsDisplay": "歌词显示"
|
"lyricsDisplay": "歌词显示",
|
||||||
|
"playerFilters": "播放筛选器"
|
||||||
},
|
},
|
||||||
"globalSearch": {
|
"globalSearch": {
|
||||||
"commands": {
|
"commands": {
|
||||||
@@ -756,7 +807,8 @@
|
|||||||
"about": "关于{{artist}}",
|
"about": "关于{{artist}}",
|
||||||
"appearsOn": "出现在",
|
"appearsOn": "出现在",
|
||||||
"viewAll": "查看全部",
|
"viewAll": "查看全部",
|
||||||
"groupingTypeAll": "所有发行类型"
|
"groupingTypeAll": "所有发行类型",
|
||||||
|
"groupingTypePrimary": "首选发布类型"
|
||||||
},
|
},
|
||||||
"itemDetail": {
|
"itemDetail": {
|
||||||
"copyPath": "将路径复制到剪贴板",
|
"copyPath": "将路径复制到剪贴板",
|
||||||
@@ -779,6 +831,17 @@
|
|||||||
},
|
},
|
||||||
"folderList": {
|
"folderList": {
|
||||||
"title": "$t(entity.folder, {\"count\": 2})"
|
"title": "$t(entity.folder, {\"count\": 2})"
|
||||||
|
},
|
||||||
|
"radioList": {
|
||||||
|
"title": "广播电台"
|
||||||
|
},
|
||||||
|
"windowBar": {
|
||||||
|
"paused": "(暂停) ",
|
||||||
|
"privateMode": "(私人模式)"
|
||||||
|
},
|
||||||
|
"collections": {
|
||||||
|
"overrideExisting": "覆盖现有",
|
||||||
|
"saveAsCollection": "保存为集合"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
@@ -801,9 +864,9 @@
|
|||||||
"input_url": "url",
|
"input_url": "url",
|
||||||
"input_preferInstantMixDescription": "仅使用即时混音来获取类似的歌曲。如果您有修改此行为的插件,则很有用",
|
"input_preferInstantMixDescription": "仅使用即时混音来获取类似的歌曲。如果您有修改此行为的插件,则很有用",
|
||||||
"input_preferInstantMix": "首选即时混音",
|
"input_preferInstantMix": "首选即时混音",
|
||||||
"input_preferRemoteUrl": "首选公共 URL",
|
"input_preferRemoteUrl": "首选公共 url",
|
||||||
"input_remoteUrl": "公共 URL",
|
"input_remoteUrl": "公共 url",
|
||||||
"input_remoteUrlPlaceholder": "可选:对外功能的公共 URL"
|
"input_remoteUrlPlaceholder": "可选:对外功能的公共 url"
|
||||||
},
|
},
|
||||||
"addToPlaylist": {
|
"addToPlaylist": {
|
||||||
"success": "添加$t(entity.trackWithCount, {\"count\": {{message}} })到$t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
"success": "添加$t(entity.trackWithCount, {\"count\": {{message}} })到$t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
||||||
@@ -865,7 +928,9 @@
|
|||||||
"createRadioStation": {
|
"createRadioStation": {
|
||||||
"input_homepageUrl": "首页地址",
|
"input_homepageUrl": "首页地址",
|
||||||
"input_name": "名称",
|
"input_name": "名称",
|
||||||
"input_streamUrl": "串流地址"
|
"input_streamUrl": "串流地址",
|
||||||
|
"success": "电台创建成功",
|
||||||
|
"title": "创建广播电台"
|
||||||
},
|
},
|
||||||
"lyricsExport": {
|
"lyricsExport": {
|
||||||
"export": "导出歌词",
|
"export": "导出歌词",
|
||||||
@@ -882,7 +947,9 @@
|
|||||||
"input_maxYear": "截止年份",
|
"input_maxYear": "截止年份",
|
||||||
"input_minYear": "起始年份",
|
"input_minYear": "起始年份",
|
||||||
"input_played_optionUnplayed": "仅未播放的曲目",
|
"input_played_optionUnplayed": "仅未播放的曲目",
|
||||||
"input_played_optionPlayed": "仅已播放的曲目"
|
"input_played_optionPlayed": "仅已播放的曲目",
|
||||||
|
"input_limit": "有多少首歌?",
|
||||||
|
"input_played": "播放筛选器"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
@@ -916,7 +983,8 @@
|
|||||||
"advancedSettings": "高级设置",
|
"advancedSettings": "高级设置",
|
||||||
"autosize": "自动调整大小",
|
"autosize": "自动调整大小",
|
||||||
"horizontalBorders": "行边框",
|
"horizontalBorders": "行边框",
|
||||||
"verticalBorders": "列边框"
|
"verticalBorders": "列边框",
|
||||||
|
"showHeader": "显示标题"
|
||||||
},
|
},
|
||||||
"view": {
|
"view": {
|
||||||
"table": "表格",
|
"table": "表格",
|
||||||
@@ -940,10 +1008,10 @@
|
|||||||
"biography": "$t(common.biography)",
|
"biography": "$t(common.biography)",
|
||||||
"owner": "$t(common.owner)",
|
"owner": "$t(common.owner)",
|
||||||
"path": "$t(common.path)",
|
"path": "$t(common.path)",
|
||||||
"channels": "$t(common.channel_other)",
|
"channels": "$t(common.channel, {\"count\": 2})",
|
||||||
"playCount": "播放次数",
|
"playCount": "播放次数",
|
||||||
"bitrate": "$t(common.bitrate)",
|
"bitrate": "$t(common.bitrate)",
|
||||||
"actions": "$t(common.action_other)",
|
"actions": "$t(common.action, {\"count\": 2})",
|
||||||
"genre": "$t(entity.genre, {\"count\": 1})",
|
"genre": "$t(entity.genre, {\"count\": 1})",
|
||||||
"discNumber": "碟片编号",
|
"discNumber": "碟片编号",
|
||||||
"favorite": "$t(common.favorite)",
|
"favorite": "$t(common.favorite)",
|
||||||
@@ -956,7 +1024,9 @@
|
|||||||
"image": "图片",
|
"image": "图片",
|
||||||
"bitDepth": "$t(common.bitDepth)",
|
"bitDepth": "$t(common.bitDepth)",
|
||||||
"sampleRate": "$t(common.sampleRate)",
|
"sampleRate": "$t(common.sampleRate)",
|
||||||
"genreBadge": "$t(entity.genre, {\"count\": 1})(徽章)"
|
"genreBadge": "$t(entity.genre, {\"count\": 1})(徽章)",
|
||||||
|
"composer": "作曲家",
|
||||||
|
"titleArtist": "$t(common.title) (艺术家)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"column": {
|
"column": {
|
||||||
@@ -980,7 +1050,7 @@
|
|||||||
"genre": "$t(entity.genre, {\"count\": 1})",
|
"genre": "$t(entity.genre, {\"count\": 1})",
|
||||||
"albumArtist": "专辑艺术家",
|
"albumArtist": "专辑艺术家",
|
||||||
"path": "路径",
|
"path": "路径",
|
||||||
"channels": "$t(common.channel_other)",
|
"channels": "$t(common.channel, {\"count\": 2})",
|
||||||
"discNumber": "碟片",
|
"discNumber": "碟片",
|
||||||
"size": "$t(common.size)",
|
"size": "$t(common.size)",
|
||||||
"codec": "$t(common.codec)",
|
"codec": "$t(common.codec)",
|
||||||
@@ -1011,7 +1081,10 @@
|
|||||||
"mixtape": "混音专辑",
|
"mixtape": "混音专辑",
|
||||||
"remix": "再混音(Remix)",
|
"remix": "再混音(Remix)",
|
||||||
"soundtrack": "原声带",
|
"soundtrack": "原声带",
|
||||||
"audioDrama": "广播剧"
|
"audioDrama": "广播剧",
|
||||||
|
"djMix": "DJ混音",
|
||||||
|
"fieldRecording": "现场录制",
|
||||||
|
"spokenWord": "访谈"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"filterOperator": {
|
"filterOperator": {
|
||||||
@@ -1032,7 +1105,8 @@
|
|||||||
"notContains": "不包含",
|
"notContains": "不包含",
|
||||||
"startsWith": "以…开头",
|
"startsWith": "以…开头",
|
||||||
"inTheRangeDate": "在(日期)范围内",
|
"inTheRangeDate": "在(日期)范围内",
|
||||||
"notInPlaylist": "不在…中"
|
"notInPlaylist": "不在…中",
|
||||||
|
"notInTheLast": "不在最后"
|
||||||
},
|
},
|
||||||
"datetime": {
|
"datetime": {
|
||||||
"minuteShort": "分",
|
"minuteShort": "分",
|
||||||
@@ -1085,6 +1159,85 @@
|
|||||||
"builtIn": "内置",
|
"builtIn": "内置",
|
||||||
"colors": "颜色",
|
"colors": "颜色",
|
||||||
"gradient": "渐变",
|
"gradient": "渐变",
|
||||||
"miscellaneousSettings": "杂项设置"
|
"miscellaneousSettings": "杂项设置",
|
||||||
|
"options": {
|
||||||
|
"channelLayout": {
|
||||||
|
"single": "单项"
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"0": "[0] 离散频率"
|
||||||
|
},
|
||||||
|
"colorMode": {
|
||||||
|
"gradient": "渐变"
|
||||||
|
},
|
||||||
|
"gradient": {
|
||||||
|
"classic": "经典",
|
||||||
|
"prism": "棱镜",
|
||||||
|
"rainbow": "彩虹"
|
||||||
|
},
|
||||||
|
"frequencyScale": {
|
||||||
|
"none": "无"
|
||||||
|
},
|
||||||
|
"weightingFilter": {
|
||||||
|
"none": "无",
|
||||||
|
"a": "A",
|
||||||
|
"b": "B",
|
||||||
|
"c": "C",
|
||||||
|
"d": "D",
|
||||||
|
"z": "Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cyclePresets": "循环预设",
|
||||||
|
"includeAllPresets": "包含所有预设",
|
||||||
|
"ignoredPresets": "忽略预设",
|
||||||
|
"selectedPresets": "已选预设",
|
||||||
|
"randomizeNextPreset": "随机化下一个预设",
|
||||||
|
"blendTime": "混合时间",
|
||||||
|
"barSpace": "住间距",
|
||||||
|
"colorStops": "颜色停止",
|
||||||
|
"level": "等级",
|
||||||
|
"colorMode": "颜色模式",
|
||||||
|
"gradientLeft": "左侧渐变",
|
||||||
|
"gradientRight": "右侧渐变",
|
||||||
|
"fft": "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": "反射Alpha",
|
||||||
|
"reflexBrightness": "反射亮度",
|
||||||
|
"mirror": "镜像",
|
||||||
|
"lowResolution": "低分辨率",
|
||||||
|
"splitGradient": "渐变分割",
|
||||||
|
"showScaleX": "显示比例尺 X",
|
||||||
|
"noteLabels": "笔记标签",
|
||||||
|
"showScaleY": "显示比例尺 Y"
|
||||||
|
},
|
||||||
|
"queryBuilder": {
|
||||||
|
"standardTags": "标准标签",
|
||||||
|
"customTags": "自定义标签"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+153
-21
@@ -20,7 +20,7 @@
|
|||||||
"ascending": "升冪",
|
"ascending": "升冪",
|
||||||
"disable": "禁用",
|
"disable": "禁用",
|
||||||
"disc": "光碟",
|
"disc": "光碟",
|
||||||
"dismiss": "忽略",
|
"dismiss": "不再顯示",
|
||||||
"duration": "時長",
|
"duration": "時長",
|
||||||
"edit": "編輯",
|
"edit": "編輯",
|
||||||
"enable": "啟用",
|
"enable": "啟用",
|
||||||
@@ -113,7 +113,9 @@
|
|||||||
"mood": "情緒",
|
"mood": "情緒",
|
||||||
"view": "查看",
|
"view": "查看",
|
||||||
"rename": "重新命名",
|
"rename": "重新命名",
|
||||||
"itemsMore": "{{count}} 更多"
|
"itemsMore": "{{count}} 更多",
|
||||||
|
"filter_single": "單選",
|
||||||
|
"filter_multiple": "複選"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"endpointNotImplementedError": "{{serverType}} 尚未實現端點 {{endpoint}}",
|
"endpointNotImplementedError": "{{serverType}} 尚未實現端點 {{endpoint}}",
|
||||||
@@ -294,7 +296,8 @@
|
|||||||
"myLibrary": "我的媒體庫",
|
"myLibrary": "我的媒體庫",
|
||||||
"shared": "已分享 $t(entity.playlist, {\"count\": 2})",
|
"shared": "已分享 $t(entity.playlist, {\"count\": 2})",
|
||||||
"favorites": "$t(entity.favorite, {\"count\": 2})",
|
"favorites": "$t(entity.favorite, {\"count\": 2})",
|
||||||
"radio": "$t(entity.radioStation, {\"count\": 2})"
|
"radio": "$t(entity.radioStation, {\"count\": 2})",
|
||||||
|
"collections": "收藏"
|
||||||
},
|
},
|
||||||
"trackList": {
|
"trackList": {
|
||||||
"title": "$t(entity.track, {\"count\": 2})",
|
"title": "$t(entity.track, {\"count\": 2})",
|
||||||
@@ -312,7 +315,11 @@
|
|||||||
"viewAll": "檢視所有",
|
"viewAll": "檢視所有",
|
||||||
"viewAllTracks": "檢視所有$t(entity.track, {\"count\": 2})",
|
"viewAllTracks": "檢視所有$t(entity.track, {\"count\": 2})",
|
||||||
"groupingTypeAll": "所有發佈類型",
|
"groupingTypeAll": "所有發佈類型",
|
||||||
"groupingTypePrimary": "主要發佈類型"
|
"groupingTypePrimary": "主要發佈類型",
|
||||||
|
"favoriteSongs": "最愛歌曲",
|
||||||
|
"favoriteSongsFrom": "{{title}} 的最愛歌曲",
|
||||||
|
"topSongsCommunity": "社群",
|
||||||
|
"topSongsPersonal": "個人"
|
||||||
},
|
},
|
||||||
"manageServers": {
|
"manageServers": {
|
||||||
"title": "管理伺服器",
|
"title": "管理伺服器",
|
||||||
@@ -338,6 +345,19 @@
|
|||||||
},
|
},
|
||||||
"radioList": {
|
"radioList": {
|
||||||
"title": "電台"
|
"title": "電台"
|
||||||
|
},
|
||||||
|
"windowBar": {
|
||||||
|
"paused": "(暫停) ",
|
||||||
|
"privateMode": "(私人模式)"
|
||||||
|
},
|
||||||
|
"collections": {
|
||||||
|
"overrideExisting": "複寫現有的",
|
||||||
|
"saveAsCollection": "儲存為收藏"
|
||||||
|
},
|
||||||
|
"releasenotes": {
|
||||||
|
"commitsSinceStable": "提交自 {{stable}}",
|
||||||
|
"noNewCommits": "在此區間內沒有新的提交",
|
||||||
|
"noStableReleaseToCompare": "沒有穩定的發行可供比較"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
@@ -379,7 +399,16 @@
|
|||||||
"restoreQueueFromServer": "從伺服器還原播放佇列",
|
"restoreQueueFromServer": "從伺服器還原播放佇列",
|
||||||
"saveQueueToServer": "將播放佇列儲存至伺服器",
|
"saveQueueToServer": "將播放佇列儲存至伺服器",
|
||||||
"artistRadio": "藝人電台",
|
"artistRadio": "藝人電台",
|
||||||
"trackRadio": "曲目電台"
|
"trackRadio": "曲目電台",
|
||||||
|
"sleepTimer": "睡眠定時器",
|
||||||
|
"sleepTimer_endOfSong": "歌曲播完時",
|
||||||
|
"sleepTimer_minutes": "{{count}} 分鐘",
|
||||||
|
"sleepTimer_hours": "{{count}} 小時",
|
||||||
|
"sleepTimer_custom": "自訂",
|
||||||
|
"sleepTimer_off": "關閉",
|
||||||
|
"sleepTimer_timeRemaining": "剩餘 {{time}}",
|
||||||
|
"sleepTimer_setCustom": "設定定時器",
|
||||||
|
"sleepTimer_cancel": "取消定時器"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"audioPlayer_description": "選擇用於播放的音訊播放器",
|
"audioPlayer_description": "選擇用於播放的音訊播放器",
|
||||||
@@ -393,7 +422,7 @@
|
|||||||
"accentColor": "強調色",
|
"accentColor": "強調色",
|
||||||
"accentColor_description": "設定應用程式的強調色",
|
"accentColor_description": "設定應用程式的強調色",
|
||||||
"applicationHotkeys": "應用程式快捷鍵",
|
"applicationHotkeys": "應用程式快捷鍵",
|
||||||
"applicationHotkeys_description": "設定應用程式快捷鍵。切換勾選框來設為全局快捷鍵(僅桌面端)",
|
"applicationHotkeys_description": "設定應用程式快捷鍵。切換勾選框來設為全域快捷鍵(僅桌面端)",
|
||||||
"audioDevice": "音訊設備",
|
"audioDevice": "音訊設備",
|
||||||
"audioDevice_description": "選擇用於播放的音訊設備(僅 web 播放器)",
|
"audioDevice_description": "選擇用於播放的音訊設備(僅 web 播放器)",
|
||||||
"audioExclusiveMode": "音訊獨占模式",
|
"audioExclusiveMode": "音訊獨占模式",
|
||||||
@@ -404,7 +433,6 @@
|
|||||||
"crossfadeStyle_description": "選擇用於音訊播放器的淡入淡出風格",
|
"crossfadeStyle_description": "選擇用於音訊播放器的淡入淡出風格",
|
||||||
"customFontPath": "自定字體路徑",
|
"customFontPath": "自定字體路徑",
|
||||||
"customFontPath_description": "設定應用程式使用的自定字體路徑",
|
"customFontPath_description": "設定應用程式使用的自定字體路徑",
|
||||||
"disableAutomaticUpdates": "禁用自動更新",
|
|
||||||
"disableLibraryUpdateOnStartup": "禁用啟動時檢查新版本",
|
"disableLibraryUpdateOnStartup": "禁用啟動時檢查新版本",
|
||||||
"discordApplicationId": "{{discord}} 應用程式 id",
|
"discordApplicationId": "{{discord}} 應用程式 id",
|
||||||
"discordApplicationId_description": "{{discord}} rich presence 應用程式 id(預設為 {{defaultId}})",
|
"discordApplicationId_description": "{{discord}} rich presence 應用程式 id(預設為 {{defaultId}})",
|
||||||
@@ -426,10 +454,10 @@
|
|||||||
"gaplessAudio": "無間隔音訊",
|
"gaplessAudio": "無間隔音訊",
|
||||||
"gaplessAudio_description": "調整 mpv 無間隔音訊設定",
|
"gaplessAudio_description": "調整 mpv 無間隔音訊設定",
|
||||||
"gaplessAudio_optionWeak": "弱(建議)",
|
"gaplessAudio_optionWeak": "弱(建議)",
|
||||||
"globalMediaHotkeys": "全局媒體快捷鍵",
|
"globalMediaHotkeys": "全域媒體快捷鍵",
|
||||||
"hotkey_browserForward": "瀏覽器往前",
|
"hotkey_browserForward": "瀏覽器往前",
|
||||||
"hotkey_favoritePreviousSong": "收藏 $t(common.previousSong)",
|
"hotkey_favoritePreviousSong": "收藏 $t(common.previousSong)",
|
||||||
"hotkey_globalSearch": "全局搜尋",
|
"hotkey_globalSearch": "全域搜尋",
|
||||||
"hotkey_localSearch": "頁面內搜尋",
|
"hotkey_localSearch": "頁面內搜尋",
|
||||||
"hotkey_playbackNext": "下一首",
|
"hotkey_playbackNext": "下一首",
|
||||||
"hotkey_playbackPause": "暫停",
|
"hotkey_playbackPause": "暫停",
|
||||||
@@ -490,7 +518,7 @@
|
|||||||
"scrobble_description": "在你的媒體伺服器中記錄播放資訊",
|
"scrobble_description": "在你的媒體伺服器中記錄播放資訊",
|
||||||
"showSkipButton": "顯示跳過按鈕",
|
"showSkipButton": "顯示跳過按鈕",
|
||||||
"showSkipButton_description": "在播放條上顯示/隱藏跳過按鈕",
|
"showSkipButton_description": "在播放條上顯示/隱藏跳過按鈕",
|
||||||
"sidebarPlaylistList": "側邊欄歌單清單",
|
"sidebarPlaylistList": "側邊欄播放清單列表",
|
||||||
"sidebarCollapsedNavigation": "側邊欄(已折疊)導航",
|
"sidebarCollapsedNavigation": "側邊欄(已折疊)導航",
|
||||||
"sidebarCollapsedNavigation_description": "在折疊的側邊欄中顯示或隱藏導航",
|
"sidebarCollapsedNavigation_description": "在折疊的側邊欄中顯示或隱藏導航",
|
||||||
"sidebarConfiguration": "側邊欄設定",
|
"sidebarConfiguration": "側邊欄設定",
|
||||||
@@ -553,7 +581,7 @@
|
|||||||
"contextMenu_description": "允許您隱藏在右鍵選單項目時顯示的項目。未選取的項目將被隱藏",
|
"contextMenu_description": "允許您隱藏在右鍵選單項目時顯示的項目。未選取的項目將被隱藏",
|
||||||
"customCssEnable": "啟用自訂CSS",
|
"customCssEnable": "啟用自訂CSS",
|
||||||
"customCssEnable_description": "允許撰寫自訂CSS",
|
"customCssEnable_description": "允許撰寫自訂CSS",
|
||||||
"customCssNotice": "警告:雖然有一些清理措施(不允許 url() 和 content:),但使用自訂 CSS 仍然會透過更改介面帶來風險",
|
"customCssNotice": "警告:即使已限制某些用法(不允許 url() 和 content:),但使用自訂 CSS 仍然會透過更改介面帶來風險",
|
||||||
"customCss": "自訂CSS",
|
"customCss": "自訂CSS",
|
||||||
"customCss_description": "自訂 CSS 內容。注意:內容和遠端 URL 是不允許使用的屬性。您的內容預覽如下所示。由於需要進行清理,因此存在一些您未設定的其他欄位",
|
"customCss_description": "自訂 CSS 內容。注意:內容和遠端 URL 是不允許使用的屬性。您的內容預覽如下所示。由於需要進行清理,因此存在一些您未設定的其他欄位",
|
||||||
"discordPausedStatus": "暫停時顯示 rich presence",
|
"discordPausedStatus": "暫停時顯示 rich presence",
|
||||||
@@ -613,7 +641,7 @@
|
|||||||
"artistBackgroundBlur_description": "調整套用至藝人背景圖片的模糊程度",
|
"artistBackgroundBlur_description": "調整套用至藝人背景圖片的模糊程度",
|
||||||
"releaseChannel_optionLatest": "最新版本",
|
"releaseChannel_optionLatest": "最新版本",
|
||||||
"releaseChannel_optionBeta": "測試版",
|
"releaseChannel_optionBeta": "測試版",
|
||||||
"releaseChannel_description": "選擇自動更新時要使用穩定版本或是測試版本",
|
"releaseChannel_description": "選擇自動更新時要使用穩定、測試或是 alpha (每日建構版) 版本",
|
||||||
"discordDisplayType": "{{discord}} presence 顯示類型",
|
"discordDisplayType": "{{discord}} presence 顯示類型",
|
||||||
"discordDisplayType_description": "變更您在狀態中正在聆聽的內容",
|
"discordDisplayType_description": "變更您在狀態中正在聆聽的內容",
|
||||||
"discordDisplayType_songname": "歌曲名稱",
|
"discordDisplayType_songname": "歌曲名稱",
|
||||||
@@ -625,8 +653,8 @@
|
|||||||
"hotkey_navigateHome": "導航至首頁",
|
"hotkey_navigateHome": "導航至首頁",
|
||||||
"preventSleepOnPlayback": "防止播放時進入睡眠狀態",
|
"preventSleepOnPlayback": "防止播放時進入睡眠狀態",
|
||||||
"preventSleepOnPlayback_description": "在音樂播放時防止螢幕進入睡眠狀態",
|
"preventSleepOnPlayback_description": "在音樂播放時防止螢幕進入睡眠狀態",
|
||||||
"mediaSession": "啟用Media Session",
|
"mediaSession": "啟用 Media Session",
|
||||||
"mediaSession_description": "啟用 Media Session 整合功能,於系統音量Overlay和鎖定畫面中顯示媒體資料與控制面板",
|
"mediaSession_description": "啟用 Media Session 整合功能,於系統音量 Overlay 和鎖定畫面中顯示媒體資料與控制面板",
|
||||||
"releaseChannel": "發佈通道",
|
"releaseChannel": "發佈通道",
|
||||||
"analyticsDisable": "選擇退出使用情況分析",
|
"analyticsDisable": "選擇退出使用情況分析",
|
||||||
"analyticsDisable_description": "經過匿名處理的使用情況資料將傳送給開發者,以協助改進應用程式",
|
"analyticsDisable_description": "經過匿名處理的使用情況資料將傳送給開發者,以協助改進應用程式",
|
||||||
@@ -709,7 +737,31 @@
|
|||||||
"pathReplace": "檔案路徑替換",
|
"pathReplace": "檔案路徑替換",
|
||||||
"pathReplace_description": "替換您伺服器的預設檔案路徑",
|
"pathReplace_description": "替換您伺服器的預設檔案路徑",
|
||||||
"pathReplace_optionRemovePrefix": "移除前綴",
|
"pathReplace_optionRemovePrefix": "移除前綴",
|
||||||
"pathReplace_optionAddPrefix": "增加前綴"
|
"pathReplace_optionAddPrefix": "增加前綴",
|
||||||
|
"sidebarPlaylistSorting": "側邊欄播放清單排序",
|
||||||
|
"homeFeatureStyle_description": "控制首頁輪播的樣式",
|
||||||
|
"homeFeatureStyle": "首頁特色輪播樣式",
|
||||||
|
"homeFeatureStyle_optionMultiple": "多重",
|
||||||
|
"homeFeatureStyle_optionSingle": "單一",
|
||||||
|
"hotkey_listPlayDefault": "清單播放",
|
||||||
|
"hotkey_listPlayLast": "清單尾端播放",
|
||||||
|
"hotkey_listPlayNext": "清單下一項播放",
|
||||||
|
"hotkey_listPlayNow": "清單立即播放",
|
||||||
|
"enableGridMultiSelect": "啟用網格多選",
|
||||||
|
"enableGridMultiSelect_description": "啟用時,允許在網格檢視中選擇多項。停用時,單擊網格項目圖片將導航到項目頁面",
|
||||||
|
"sidebarPlaylistSorting_description": "允許在側邊欄中使用拖放手動對播放清單進行排序,而不是預設的伺服器排序",
|
||||||
|
"sidebarPlaylistListFilterRegex_description": "在側邊欄中隱藏與此正規表達式匹配的播放清單",
|
||||||
|
"sidebarPlaylistListFilterRegex_placeholder": "範例: ^Daily Mix.*",
|
||||||
|
"sidebarPlaylistListFilterRegex": "播放清單過濾器正規表達式",
|
||||||
|
"blurExplicitImages": "模糊露骨圖片",
|
||||||
|
"blurExplicitImages_description": "標記為露骨的專輯和歌曲封面將被模糊",
|
||||||
|
"releaseChannel_optionAlpha": "alpha (每日建構版)",
|
||||||
|
"analyticsEnable": "傳送基於使用情況的分析報告",
|
||||||
|
"analyticsEnable_description": "匿名化的使用情況資料會傳送給開發者,以協助改進應用程式",
|
||||||
|
"automaticUpdates": "自動更新",
|
||||||
|
"automaticUpdates_description": "自動檢查並安裝更新",
|
||||||
|
"discordStateIcon": "顯示播放中圖示",
|
||||||
|
"discordStateIcon_description": "在 rich presence 狀態中顯示一個小的播放圖示。啟用「暫停時顯示 rich presence」時,會始終顯示暫停的圖示"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"config": {
|
"config": {
|
||||||
@@ -742,7 +794,8 @@
|
|||||||
"alternateRowColors": "隔行上色",
|
"alternateRowColors": "隔行上色",
|
||||||
"horizontalBorders": "行邊框線",
|
"horizontalBorders": "行邊框線",
|
||||||
"rowHoverHighlight": "滑鼠懸停Highlight",
|
"rowHoverHighlight": "滑鼠懸停Highlight",
|
||||||
"verticalBorders": "列邊框線"
|
"verticalBorders": "列邊框線",
|
||||||
|
"showHeader": "顯示標題"
|
||||||
},
|
},
|
||||||
"label": {
|
"label": {
|
||||||
"actions": "$t(common.action, {\"count\": 2})",
|
"actions": "$t(common.action, {\"count\": 2})",
|
||||||
@@ -777,12 +830,15 @@
|
|||||||
"genreBadge": "$t(entity.genre, {\"count\": 1}) (徽章)",
|
"genreBadge": "$t(entity.genre, {\"count\": 1}) (徽章)",
|
||||||
"image": "圖片",
|
"image": "圖片",
|
||||||
"bitDepth": "$t(common.bitDepth)",
|
"bitDepth": "$t(common.bitDepth)",
|
||||||
"sampleRate": "$t(common.sampleRate)"
|
"sampleRate": "$t(common.sampleRate)",
|
||||||
|
"composer": "作曲者",
|
||||||
|
"titleArtist": "$t(common.title) (藝人)"
|
||||||
},
|
},
|
||||||
"view": {
|
"view": {
|
||||||
"table": "表格",
|
"table": "表格",
|
||||||
"grid": "網格",
|
"grid": "網格",
|
||||||
"list": "列表"
|
"list": "列表",
|
||||||
|
"detail": "詳情"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"column": {
|
"column": {
|
||||||
@@ -920,7 +976,8 @@
|
|||||||
"title": "標題",
|
"title": "標題",
|
||||||
"toYear": "從年份",
|
"toYear": "從年份",
|
||||||
"trackNumber": "曲目",
|
"trackNumber": "曲目",
|
||||||
"explicitStatus": "$t(common.explicitStatus)"
|
"explicitStatus": "$t(common.explicitStatus)",
|
||||||
|
"sortName": "排序名稱"
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"addServer": {
|
"addServer": {
|
||||||
@@ -1049,7 +1106,8 @@
|
|||||||
"live": "Live",
|
"live": "Live",
|
||||||
"mixtape": "混音帶",
|
"mixtape": "混音帶",
|
||||||
"remix": "Remix",
|
"remix": "Remix",
|
||||||
"soundtrack": "原聲帶"
|
"soundtrack": "原聲帶",
|
||||||
|
"spokenWord": "訪談"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dragDropZone": {
|
"dragDropZone": {
|
||||||
@@ -1161,6 +1219,80 @@
|
|||||||
"gravity": "重力",
|
"gravity": "重力",
|
||||||
"peakFadeTime": "峰值淡出時間 (毫秒)",
|
"peakFadeTime": "峰值淡出時間 (毫秒)",
|
||||||
"peakHoldTime": "峰值停留時間 (毫秒)",
|
"peakHoldTime": "峰值停留時間 (毫秒)",
|
||||||
"radialSpectrum": "圓形頻譜"
|
"radialSpectrum": "圓形頻譜",
|
||||||
|
"level": "層級",
|
||||||
|
"pasteGradient": "貼上漸層",
|
||||||
|
"pasteGradientPlaceholder": "在這裡貼上漸層JSON...",
|
||||||
|
"radial": "放射",
|
||||||
|
"radialInvert": "反轉放射",
|
||||||
|
"spinSpeed": "旋轉速度",
|
||||||
|
"radius": "半徑",
|
||||||
|
"reflexMirror": "反射鏡像",
|
||||||
|
"reflexFit": "反射貼齊",
|
||||||
|
"reflexRatio": "反射比例",
|
||||||
|
"reflexAlpha": "反射 Alpha",
|
||||||
|
"reflexBrightness": "反射亮度",
|
||||||
|
"mirror": "鏡像",
|
||||||
|
"miscellaneousSettings": "雜項設定",
|
||||||
|
"alphaBars": "Alpha 條",
|
||||||
|
"ansiBands": "ASNI 波段",
|
||||||
|
"ledBars": "LED 條",
|
||||||
|
"trueLeds": "真實 LED",
|
||||||
|
"lumiBars": "輝光條",
|
||||||
|
"outlineBars": "外框條",
|
||||||
|
"roundBars": "圓角條",
|
||||||
|
"lowResolution": "低解析",
|
||||||
|
"splitGradient": "分割漸層",
|
||||||
|
"showFPS": "顯示 FPS",
|
||||||
|
"showScaleX": "顯示 X 軸比例",
|
||||||
|
"noteLabels": "音符標籤",
|
||||||
|
"showScaleY": "顯示Y軸比例",
|
||||||
|
"options": {
|
||||||
|
"mode": {
|
||||||
|
"0": "[0] 離散頻率",
|
||||||
|
"1": "[1] 1/24th 八度音 / 240 頻段",
|
||||||
|
"2": "[2] 1/12th 八度音 / 120 頻段",
|
||||||
|
"3": "[3] 1/8th 八度音 / 80 頻段",
|
||||||
|
"4": "[4] 1/6th 八度音 / 60 頻段",
|
||||||
|
"5": "[5] 1/4th 八度音 / 40 頻段",
|
||||||
|
"6": "[6] 1/3rd 八度音 / 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": "Log 比例",
|
||||||
|
"mel": "Mel 比例"
|
||||||
|
},
|
||||||
|
"weightingFilter": {
|
||||||
|
"none": "無",
|
||||||
|
"a": "A",
|
||||||
|
"b": "B",
|
||||||
|
"c": "C",
|
||||||
|
"d": "D",
|
||||||
|
"z": "Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createSocket } from 'dgram';
|
import { createSocket } from 'dgram';
|
||||||
import { ipcMain } from 'electron';
|
import { ipcMain } from 'electron';
|
||||||
|
|
||||||
|
import { mainLogger } from '/@/main/logger';
|
||||||
import { DiscoveredServerItem, ServerType } from '/@/shared/types/types';
|
import { DiscoveredServerItem, ServerType } from '/@/shared/types/types';
|
||||||
|
|
||||||
type JellyfinResponse = {
|
type JellyfinResponse = {
|
||||||
@@ -26,7 +27,7 @@ function discoverJellyfin(reply: (server: DiscoveredServerItem) => void) {
|
|||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Got a spurious response, ignore?
|
// Got a spurious response, ignore?
|
||||||
console.error(e);
|
mainLogger.error('Autodiscover Jellyfin parse error', e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -51,5 +52,5 @@ ipcMain.on('autodiscover-ping', (ev) => {
|
|||||||
|
|
||||||
discoverAll((result) => port.postMessage(result))
|
discoverAll((result) => port.postMessage(result))
|
||||||
.then(() => port.close())
|
.then(() => port.close())
|
||||||
.catch((err) => console.error(err));
|
.catch((err) => mainLogger.error('Autodiscover failed', err));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
LyricSearchQuery,
|
LyricSearchQuery,
|
||||||
LyricSource,
|
LyricSource,
|
||||||
} from '.';
|
} from '.';
|
||||||
|
import { mainLogger } from '../../../logger';
|
||||||
import { orderSearchResults } from './shared';
|
import { orderSearchResults } from './shared';
|
||||||
|
|
||||||
const SEARCH_URL = 'https://genius.com/api/search/song';
|
const SEARCH_URL = 'https://genius.com/api/search/song';
|
||||||
@@ -100,7 +101,7 @@ export async function getLyricsBySongId(url: string): Promise<null | string> {
|
|||||||
try {
|
try {
|
||||||
result = await axios.get<string>(url, { responseType: 'text' });
|
result = await axios.get<string>(url, { responseType: 'text' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Genius lyrics request got an error!', (e as Error)?.message);
|
mainLogger.error('Genius lyrics request failed', (e as Error)?.message);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +139,7 @@ export async function getSearchResults(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Genius search request got an error!', (e as Error)?.message);
|
mainLogger.error('Genius search request failed', (e as Error)?.message);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +194,7 @@ async function getSongId(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Genius search request got an error!', (e as Error)?.message);
|
mainLogger.error('Genius search request failed', (e as Error)?.message);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ipcMain } from 'electron';
|
import { ipcMain } from 'electron';
|
||||||
|
|
||||||
|
import { mainLogger } from '../../../logger';
|
||||||
import { store } from '../settings';
|
import { store } from '../settings';
|
||||||
import { getLyricsBySongId as getGenius, getSearchResults as searchGenius } from './genius';
|
import { getLyricsBySongId as getGenius, getSearchResults as searchGenius } from './genius';
|
||||||
import { getLyricsBySongId as getLrcLib, getSearchResults as searchLrcLib } from './lrclib';
|
import { getLyricsBySongId as getLrcLib, getSearchResults as searchLrcLib } from './lrclib';
|
||||||
@@ -96,7 +97,7 @@ const searchAllSources = async (
|
|||||||
allSearchResults.push(...result.value.searchResults);
|
allSearchResults.push(...result.value.searchResults);
|
||||||
} else if (result.status === 'rejected') {
|
} else if (result.status === 'rejected') {
|
||||||
const index = settled.indexOf(result);
|
const index = settled.indexOf(result);
|
||||||
console.error(`Error searching ${sources[index]} for lyrics:`, result.reason);
|
mainLogger.error(`Error searching ${sources[index]} for lyrics`, result.reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return allSearchResults;
|
return allSearchResults;
|
||||||
@@ -160,7 +161,7 @@ const getRemoteLyrics = async (song: Song) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching lyrics from ${bestMatch.source}:`, error);
|
mainLogger.error(`Error fetching lyrics from ${bestMatch.source}`, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lyricsFromSource) {
|
if (lyricsFromSource) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
LyricSearchQuery,
|
LyricSearchQuery,
|
||||||
LyricSource,
|
LyricSource,
|
||||||
} from '.';
|
} from '.';
|
||||||
|
import { mainLogger } from '../../../logger';
|
||||||
import { orderSearchResults } from './shared';
|
import { orderSearchResults } from './shared';
|
||||||
|
|
||||||
const FETCH_URL = 'https://lrclib.net/api/get';
|
const FETCH_URL = 'https://lrclib.net/api/get';
|
||||||
@@ -46,7 +47,7 @@ export async function getLyricsBySongId(songId: string): Promise<null | string>
|
|||||||
try {
|
try {
|
||||||
result = await axios.get<LrcLibTrackResponse>(`${FETCH_URL}/${songId}`);
|
result = await axios.get<LrcLibTrackResponse>(`${FETCH_URL}/${songId}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('LrcLib lyrics request got an error!', (e as Error)?.message);
|
mainLogger.error('LrcLib lyrics request failed', (e as Error)?.message);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +70,7 @@ export async function getSearchResults(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('LrcLib search request got an error!', (e as Error)?.message);
|
mainLogger.error('LrcLib search request failed', (e as Error)?.message);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +108,7 @@ export async function query(
|
|||||||
timeout: TIMEOUT_MS,
|
timeout: TIMEOUT_MS,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('LrcLib search request got an error!', (e as Error).message);
|
mainLogger.error('LrcLib search request failed', (e as Error).message);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
LyricSearchQuery,
|
LyricSearchQuery,
|
||||||
LyricSource,
|
LyricSource,
|
||||||
} from '.';
|
} from '.';
|
||||||
|
import { mainLogger } from '../../../logger';
|
||||||
import { store } from '../settings';
|
import { store } from '../settings';
|
||||||
import { orderSearchResults } from './shared';
|
import { orderSearchResults } from './shared';
|
||||||
|
|
||||||
@@ -81,7 +82,7 @@ export async function getLyricsBySongId(songId: string): Promise<null | string>
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('NetEase lyrics request got an error!', e);
|
mainLogger.error('NetEase lyrics request failed', e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const enableTranslation = store.get('enableNeteaseTranslation', false) as boolean;
|
const enableTranslation = store.get('enableNeteaseTranslation', false) as boolean;
|
||||||
@@ -114,7 +115,7 @@ export async function getSearchResults(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('NetEase search request got an error!', e);
|
mainLogger.error('NetEase search request failed', e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import console from 'console';
|
|
||||||
import { app, ipcMain } from 'electron';
|
import { app, ipcMain } from 'electron';
|
||||||
import { rm } from 'fs/promises';
|
import { rm } from 'fs/promises';
|
||||||
import uniq from 'lodash/uniq';
|
import uniq from 'lodash/uniq';
|
||||||
@@ -7,6 +6,7 @@ import { pid } from 'node:process';
|
|||||||
import process from 'process';
|
import process from 'process';
|
||||||
|
|
||||||
import { getMainWindow, sendToastToRenderer } from '../../../index';
|
import { getMainWindow, sendToastToRenderer } from '../../../index';
|
||||||
|
import { mainLogger } from '../../../logger';
|
||||||
import { createLog, isWindows } from '../../../utils';
|
import { createLog, isWindows } from '../../../utils';
|
||||||
import { store } from '../settings';
|
import { store } from '../settings';
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ const createMpv = async (data: {
|
|||||||
try {
|
try {
|
||||||
await mpv.start();
|
await mpv.start();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('mpv failed to start', error);
|
mainLogger.error('mpv failed to start', error);
|
||||||
} finally {
|
} finally {
|
||||||
await mpv.setMultipleProperties(properties || {});
|
await mpv.setMultipleProperties(properties || {});
|
||||||
}
|
}
|
||||||
@@ -672,7 +672,7 @@ process.on('SIGTERM', async () => {
|
|||||||
|
|
||||||
// Handle uncaught exceptions - cleanup mpv before crashing
|
// Handle uncaught exceptions - cleanup mpv before crashing
|
||||||
process.on('uncaughtException', async (error) => {
|
process.on('uncaughtException', async (error) => {
|
||||||
console.error('Uncaught exception:', error);
|
mainLogger.error('Uncaught exception', error);
|
||||||
await cleanupMpv(true).catch(() => {
|
await cleanupMpv(true).catch(() => {
|
||||||
// Ignore cleanup errors during crash
|
// Ignore cleanup errors during crash
|
||||||
});
|
});
|
||||||
@@ -680,7 +680,7 @@ process.on('uncaughtException', async (error) => {
|
|||||||
|
|
||||||
// Handle unhandled rejections - cleanup mpv
|
// Handle unhandled rejections - cleanup mpv
|
||||||
process.on('unhandledRejection', async (reason) => {
|
process.on('unhandledRejection', async (reason) => {
|
||||||
console.error('Unhandled rejection:', reason);
|
mainLogger.error('Unhandled rejection', reason);
|
||||||
await cleanupMpv(true).catch(() => {
|
await cleanupMpv(true).catch(() => {
|
||||||
// Ignore cleanup errors
|
// Ignore cleanup errors
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { deflate, gzip } from 'zlib';
|
|||||||
import manifest from './manifest.json';
|
import manifest from './manifest.json';
|
||||||
|
|
||||||
import { getMainWindow } from '/@/main/index';
|
import { getMainWindow } from '/@/main/index';
|
||||||
|
import { mainLogger } from '/@/main/logger';
|
||||||
import { isLinux } from '/@/main/utils';
|
import { isLinux } from '/@/main/utils';
|
||||||
import { QueueSong } from '/@/shared/types/domain-types';
|
import { QueueSong } from '/@/shared/types/domain-types';
|
||||||
import { ClientEvent, ServerEvent } from '/@/shared/types/remote-types';
|
import { ClientEvent, ServerEvent } from '/@/shared/types/remote-types';
|
||||||
@@ -349,7 +350,7 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
|
|||||||
}, 10000) as unknown as number;
|
}, 10000) as unknown as number;
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.on('error', console.error);
|
ws.on('error', (err) => mainLogger.error('Remote WebSocket error', err));
|
||||||
|
|
||||||
ws.on('message', (data) => {
|
ws.on('message', (data) => {
|
||||||
try {
|
try {
|
||||||
@@ -488,7 +489,7 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
mainLogger.error('Remote message handler error', error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+229
-29
@@ -1,3 +1,5 @@
|
|||||||
|
import type { UpdateCheckResult } from 'electron-updater';
|
||||||
|
|
||||||
import { is } from '@electron-toolkit/utils';
|
import { is } from '@electron-toolkit/utils';
|
||||||
import {
|
import {
|
||||||
app,
|
app,
|
||||||
@@ -18,14 +20,16 @@ import {
|
|||||||
} from 'electron';
|
} from 'electron';
|
||||||
import electronLocalShortcut from 'electron-localshortcut';
|
import electronLocalShortcut from 'electron-localshortcut';
|
||||||
import log from 'electron-log/main';
|
import log from 'electron-log/main';
|
||||||
import { autoUpdater } from 'electron-updater';
|
import { AppImageUpdater, autoUpdater, MacUpdater, NsisUpdater } from 'electron-updater';
|
||||||
import { access, constants } from 'fs';
|
import { access, constants } from 'fs';
|
||||||
import path, { join } from 'path';
|
import path, { join } from 'path';
|
||||||
|
import semver from 'semver';
|
||||||
|
|
||||||
import packageJson from '../../package.json';
|
import packageJson from '../../package.json';
|
||||||
import { disableMediaKeys, enableMediaKeys } from './features/core/player/media-keys';
|
import { disableMediaKeys, enableMediaKeys } from './features/core/player/media-keys';
|
||||||
import { shutdownServer } from './features/core/remote';
|
import { shutdownServer } from './features/core/remote';
|
||||||
import { store } from './features/core/settings';
|
import { store } from './features/core/settings';
|
||||||
|
import { mainLogger } from './logger';
|
||||||
import MenuBuilder from './menu';
|
import MenuBuilder from './menu';
|
||||||
import {
|
import {
|
||||||
autoUpdaterLogInterface,
|
autoUpdaterLogInterface,
|
||||||
@@ -40,44 +44,199 @@ import './features';
|
|||||||
|
|
||||||
import { PlayerType, TitleTheme } from '/@/shared/types/types';
|
import { PlayerType, TitleTheme } from '/@/shared/types/types';
|
||||||
|
|
||||||
export default class AppUpdater {
|
const ALPHA_UPDATER_CONFIG: {
|
||||||
|
bucket: string;
|
||||||
|
channel: string;
|
||||||
|
endpoint: string;
|
||||||
|
provider: 's3';
|
||||||
|
} = {
|
||||||
|
bucket: '',
|
||||||
|
channel: 'alpha',
|
||||||
|
endpoint: 'https://feishin-nightly-bucket.jeffvli.org',
|
||||||
|
provider: 's3',
|
||||||
|
};
|
||||||
|
|
||||||
|
const GITHUB_UPDATER_CONFIG = {
|
||||||
|
owner: 'jeffvli',
|
||||||
|
provider: 'github' as const,
|
||||||
|
repo: 'feishin',
|
||||||
|
};
|
||||||
|
|
||||||
|
type UpdaterInstance = AppImageUpdater | MacUpdater | NsisUpdater | typeof autoUpdater;
|
||||||
|
|
||||||
|
class AppUpdater {
|
||||||
constructor() {
|
constructor() {
|
||||||
log.transports.file.level = 'info';
|
const effectiveChannel = store.get('release_channel') as string;
|
||||||
autoUpdater.logger = autoUpdaterLogInterface;
|
mainLogger.info('Effective update channel:', effectiveChannel);
|
||||||
|
if (effectiveChannel === 'alpha') {
|
||||||
const isBetaVersion = packageJson.version.includes('-beta');
|
checkAllChannelsAndGetBest().then(({ updater: updaterInstance }) => {
|
||||||
const releaseChannel = store.get('release_channel');
|
updaterInstance.autoInstallOnAppQuit = true;
|
||||||
const isNotConfigured = !releaseChannel;
|
updaterInstance.autoRunAppAfterInstall = true;
|
||||||
|
updaterInstance.checkForUpdatesAndNotify();
|
||||||
console.log('Release channel: ', releaseChannel);
|
});
|
||||||
console.log('Is beta version: ', isBetaVersion);
|
return;
|
||||||
|
|
||||||
if (isNotConfigured) {
|
|
||||||
console.log(
|
|
||||||
'Release channel not configured, setting to ',
|
|
||||||
isBetaVersion ? 'beta' : 'latest',
|
|
||||||
);
|
|
||||||
store.set('release_channel', isBetaVersion ? 'beta' : 'latest');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (releaseChannel === 'beta') {
|
|
||||||
autoUpdater.channel = 'beta';
|
|
||||||
autoUpdater.allowPrerelease = true;
|
|
||||||
autoUpdater.disableDifferentialDownload = true;
|
|
||||||
} else if (releaseChannel === 'latest') {
|
|
||||||
autoUpdater.channel = 'latest';
|
|
||||||
autoUpdater.allowDowngrade = true;
|
|
||||||
autoUpdater.allowPrerelease = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configureAndGetUpdater();
|
||||||
autoUpdater.checkForUpdatesAndNotify();
|
autoUpdater.checkForUpdatesAndNotify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When release channel is alpha, check alpha and latest for updates and return
|
||||||
|
// the updater + result for the newest version found (so alpha users can receive
|
||||||
|
// latest updates when they are newer than the current alpha).
|
||||||
|
async function checkAllChannelsAndGetBest(): Promise<{
|
||||||
|
result: null | UpdateCheckResult;
|
||||||
|
updater: UpdaterInstance;
|
||||||
|
}> {
|
||||||
|
const currentVersion = packageJson.version;
|
||||||
|
const candidates: Array<{
|
||||||
|
channel: 'alpha' | 'beta' | 'latest';
|
||||||
|
result: UpdateCheckResult;
|
||||||
|
updater: UpdaterInstance;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
const alphaUpdater = createAlphaUpdaterInstance();
|
||||||
|
alphaUpdater.logger = autoUpdaterLogInterface;
|
||||||
|
alphaUpdater.channel = ALPHA_UPDATER_CONFIG.channel;
|
||||||
|
alphaUpdater.allowPrerelease = true;
|
||||||
|
alphaUpdater.disableDifferentialDownload = true;
|
||||||
|
alphaUpdater.allowDowngrade = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
mainLogger.info('Checking for updates on alpha channel');
|
||||||
|
const alphaResult = await alphaUpdater.checkForUpdates();
|
||||||
|
if (
|
||||||
|
alphaResult?.updateInfo?.version &&
|
||||||
|
alphaResult.isUpdateAvailable &&
|
||||||
|
semver.valid(alphaResult.updateInfo.version) &&
|
||||||
|
semver.gt(alphaResult.updateInfo.version, currentVersion)
|
||||||
|
) {
|
||||||
|
candidates.push({ channel: 'alpha', result: alphaResult, updater: alphaUpdater });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log.warn('Alpha channel check failed', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
autoUpdater.setFeedURL(GITHUB_UPDATER_CONFIG);
|
||||||
|
configureAutoUpdaterForChannel('latest');
|
||||||
|
mainLogger.info('Checking for updates on latest channel (GitHub)');
|
||||||
|
const latestResult = await autoUpdater.checkForUpdates();
|
||||||
|
if (
|
||||||
|
latestResult?.updateInfo?.version &&
|
||||||
|
latestResult.isUpdateAvailable &&
|
||||||
|
semver.valid(latestResult.updateInfo.version) &&
|
||||||
|
semver.gt(latestResult.updateInfo.version, currentVersion)
|
||||||
|
) {
|
||||||
|
candidates.push({ channel: 'latest', result: latestResult, updater: autoUpdater });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log.warn('Latest channel check failed', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidates.length === 0) {
|
||||||
|
return { result: null, updater: alphaUpdater };
|
||||||
|
}
|
||||||
|
|
||||||
|
const best = candidates.reduce((a, b) =>
|
||||||
|
semver.gt(a.result.updateInfo.version, b.result.updateInfo.version) ? a : b,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (best.channel === 'latest') {
|
||||||
|
configureAutoUpdaterForChannel('latest');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { result: best.result, updater: best.updater };
|
||||||
|
}
|
||||||
|
|
||||||
|
function configureAndGetUpdater(): UpdaterInstance {
|
||||||
|
const isBetaVersion = packageJson.version.includes('-beta');
|
||||||
|
const isAlphaVersion = packageJson.version.includes('-alpha');
|
||||||
|
let releaseChannel = store.get('release_channel');
|
||||||
|
const isNotConfigured = !releaseChannel;
|
||||||
|
|
||||||
|
mainLogger.info('Release channel:', releaseChannel);
|
||||||
|
mainLogger.info('Is beta version:', isBetaVersion);
|
||||||
|
mainLogger.info('Is alpha version:', isAlphaVersion);
|
||||||
|
mainLogger.info('Is not configured:', isNotConfigured);
|
||||||
|
|
||||||
|
if (isNotConfigured) {
|
||||||
|
mainLogger.info('Release channel not configured, setting default channel');
|
||||||
|
const defaultChannel = isAlphaVersion ? 'alpha' : isBetaVersion ? 'beta' : 'latest';
|
||||||
|
store.set('release_channel', defaultChannel);
|
||||||
|
releaseChannel = defaultChannel;
|
||||||
|
}
|
||||||
|
|
||||||
|
const effectiveChannel = store.get('release_channel') as string;
|
||||||
|
|
||||||
|
if (effectiveChannel === 'alpha') {
|
||||||
|
const updater = createAlphaUpdaterInstance();
|
||||||
|
log.transports.file.level = 'info';
|
||||||
|
updater.logger = autoUpdaterLogInterface;
|
||||||
|
updater.channel = ALPHA_UPDATER_CONFIG.channel;
|
||||||
|
updater.allowPrerelease = true;
|
||||||
|
updater.disableDifferentialDownload = true;
|
||||||
|
updater.allowDowngrade = true;
|
||||||
|
updater.autoInstallOnAppQuit = true;
|
||||||
|
updater.autoRunAppAfterInstall = true;
|
||||||
|
return updater;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.transports.file.level = 'info';
|
||||||
|
autoUpdater.logger = autoUpdaterLogInterface;
|
||||||
|
autoUpdater.autoInstallOnAppQuit = true;
|
||||||
|
autoUpdater.autoRunAppAfterInstall = true;
|
||||||
|
|
||||||
|
if (effectiveChannel === 'beta') {
|
||||||
|
autoUpdater.channel = 'beta';
|
||||||
|
autoUpdater.allowDowngrade = true;
|
||||||
|
autoUpdater.allowPrerelease = true;
|
||||||
|
autoUpdater.disableDifferentialDownload = true;
|
||||||
|
} else {
|
||||||
|
autoUpdater.channel = 'latest';
|
||||||
|
autoUpdater.allowPrerelease = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return autoUpdater;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the global autoUpdater for a specific GitHub channel (beta or latest).
|
||||||
|
* Used when checking multiple channels or when the winning channel is beta/latest.
|
||||||
|
*/
|
||||||
|
function configureAutoUpdaterForChannel(channel: 'beta' | 'latest'): void {
|
||||||
|
log.transports.file.level = 'info';
|
||||||
|
autoUpdater.logger = autoUpdaterLogInterface;
|
||||||
|
autoUpdater.autoInstallOnAppQuit = true;
|
||||||
|
autoUpdater.autoRunAppAfterInstall = true;
|
||||||
|
if (channel === 'beta') {
|
||||||
|
autoUpdater.channel = 'beta';
|
||||||
|
autoUpdater.allowDowngrade = true;
|
||||||
|
autoUpdater.allowPrerelease = true;
|
||||||
|
autoUpdater.disableDifferentialDownload = true;
|
||||||
|
} else {
|
||||||
|
autoUpdater.channel = 'latest';
|
||||||
|
autoUpdater.allowPrerelease = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAlphaUpdaterInstance(): AppImageUpdater | MacUpdater | NsisUpdater {
|
||||||
|
if (isMacOS()) {
|
||||||
|
return new MacUpdater(ALPHA_UPDATER_CONFIG);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLinux()) {
|
||||||
|
return new AppImageUpdater(ALPHA_UPDATER_CONFIG);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new NsisUpdater(ALPHA_UPDATER_CONFIG);
|
||||||
|
}
|
||||||
|
|
||||||
protocol.registerSchemesAsPrivileged([{ privileges: { bypassCSP: true }, scheme: 'feishin' }]);
|
protocol.registerSchemesAsPrivileged([{ privileges: { bypassCSP: true }, scheme: 'feishin' }]);
|
||||||
|
|
||||||
process.on('uncaughtException', (error: any) => {
|
process.on('uncaughtException', (error: any) => {
|
||||||
console.error('Error in main process', error);
|
mainLogger.error('Uncaught exception in main process', error);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (store.get('ignore_ssl')) {
|
if (store.get('ignore_ssl')) {
|
||||||
@@ -359,6 +518,47 @@ async function createWindow(first = true): Promise<void> {
|
|||||||
return mainWindow?.webContents.session.clearCache();
|
return mainWindow?.webContents.session.clearCache();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle(
|
||||||
|
'app-check-for-updates',
|
||||||
|
async (): Promise<{ updateAvailable: boolean; version?: string }> => {
|
||||||
|
if (disableAutoUpdates()) {
|
||||||
|
mainLogger.info('Auto updates are disabled');
|
||||||
|
return { updateAvailable: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
mainLogger.info('Checking for updates');
|
||||||
|
const effectiveChannel = store.get('release_channel') as string;
|
||||||
|
let result: null | UpdateCheckResult;
|
||||||
|
let updater: UpdaterInstance;
|
||||||
|
|
||||||
|
if (effectiveChannel === 'alpha') {
|
||||||
|
const best = await checkAllChannelsAndGetBest();
|
||||||
|
result = best.result;
|
||||||
|
updater = best.updater;
|
||||||
|
} else {
|
||||||
|
updater = configureAndGetUpdater();
|
||||||
|
result = await updater.checkForUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateAvailable = result?.isUpdateAvailable ?? false;
|
||||||
|
mainLogger.info('Update available:', updateAvailable);
|
||||||
|
if (updateAvailable && store.get('disable_auto_updates') !== true) {
|
||||||
|
mainLogger.info('Downloading update');
|
||||||
|
updater.downloadUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateAvailable,
|
||||||
|
version: result?.updateInfo?.version,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
mainLogger.error('Error checking for updates');
|
||||||
|
return { updateAvailable: false };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
ipcMain.on('app-restart', () => {
|
ipcMain.on('app-restart', () => {
|
||||||
// Fix for .AppImage
|
// Fix for .AppImage
|
||||||
if (process.env.APPIMAGE) {
|
if (process.env.APPIMAGE) {
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
const pad = (n: number) => String(n).padStart(2, '0');
|
||||||
|
|
||||||
|
const timestamp = () => {
|
||||||
|
const d = new Date();
|
||||||
|
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const format = (level: string, message: string, ...args: unknown[]) => {
|
||||||
|
const prefix = `[${timestamp()}] [${level}] ${message}`;
|
||||||
|
if (args.length > 0) {
|
||||||
|
console.log(prefix, ...args);
|
||||||
|
} else {
|
||||||
|
console.log(prefix);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mainLogger = {
|
||||||
|
debug: (message: string, ...args: unknown[]) => format('DEBUG', message, ...args),
|
||||||
|
error: (message: string, ...args: unknown[]) => {
|
||||||
|
const prefix = `[${timestamp()}] [ERROR] ${message}`;
|
||||||
|
if (args.length > 0) {
|
||||||
|
console.error(prefix, ...args);
|
||||||
|
} else {
|
||||||
|
console.error(prefix);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
info: (message: string, ...args: unknown[]) => format('INFO', message, ...args),
|
||||||
|
warn: (message: string, ...args: unknown[]) => {
|
||||||
|
const prefix = `[${timestamp()}] [WARN] ${message}`;
|
||||||
|
if (args.length > 0) {
|
||||||
|
console.warn(prefix, ...args);
|
||||||
|
} else {
|
||||||
|
console.warn(prefix);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
+18
-2
@@ -21,6 +21,14 @@ export default class MenuBuilder {
|
|||||||
selector: 'orderFrontStandardAboutPanel:',
|
selector: 'orderFrontStandardAboutPanel:',
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
accelerator: 'Command+,',
|
||||||
|
click: () => {
|
||||||
|
this.mainWindow.webContents.send('renderer-open-settings');
|
||||||
|
},
|
||||||
|
label: 'Settings',
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
{ label: 'Services', submenu: [] },
|
{ label: 'Services', submenu: [] },
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
@@ -151,8 +159,8 @@ export default class MenuBuilder {
|
|||||||
return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp];
|
return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp];
|
||||||
}
|
}
|
||||||
|
|
||||||
buildDefaultTemplate() {
|
buildDefaultTemplate(): MenuItemConstructorOptions[] {
|
||||||
const templateDefault = [
|
const templateDefault: MenuItemConstructorOptions[] = [
|
||||||
{
|
{
|
||||||
label: '&File',
|
label: '&File',
|
||||||
submenu: [
|
submenu: [
|
||||||
@@ -160,6 +168,14 @@ export default class MenuBuilder {
|
|||||||
accelerator: 'Ctrl+O',
|
accelerator: 'Ctrl+O',
|
||||||
label: '&Open',
|
label: '&Open',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accelerator: 'Ctrl+,',
|
||||||
|
click: () => {
|
||||||
|
this.mainWindow.webContents.send('renderer-open-settings');
|
||||||
|
},
|
||||||
|
label: '&Settings...',
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
accelerator: 'Ctrl+W',
|
accelerator: 'Ctrl+W',
|
||||||
click: () => {
|
click: () => {
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ const download = (url: string) => {
|
|||||||
ipcRenderer.send('download-url', url);
|
ipcRenderer.send('download-url', url);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkForUpdates = (): Promise<{ updateAvailable: boolean; version?: string }> => {
|
||||||
|
return ipcRenderer.invoke('app-check-for-updates');
|
||||||
|
};
|
||||||
|
|
||||||
const forceGarbageCollection = (): boolean => {
|
const forceGarbageCollection = (): boolean => {
|
||||||
try {
|
try {
|
||||||
if (typeof global.gc === 'function') {
|
if (typeof global.gc === 'function') {
|
||||||
@@ -57,7 +61,12 @@ const forceGarbageCollection = (): boolean => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rendererOpenSettings = (cb: (event: IpcRendererEvent) => void) => {
|
||||||
|
ipcRenderer.on('renderer-open-settings', cb);
|
||||||
|
};
|
||||||
|
|
||||||
export const utils = {
|
export const utils = {
|
||||||
|
checkForUpdates,
|
||||||
disableAutoUpdates,
|
disableAutoUpdates,
|
||||||
download,
|
download,
|
||||||
forceGarbageCollection,
|
forceGarbageCollection,
|
||||||
@@ -69,6 +78,7 @@ export const utils = {
|
|||||||
openApplicationDirectory,
|
openApplicationDirectory,
|
||||||
openItem,
|
openItem,
|
||||||
playerErrorListener,
|
playerErrorListener,
|
||||||
|
rendererOpenSettings,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Utils = typeof utils;
|
export type Utils = typeof utils;
|
||||||
|
|||||||
+59
-87
@@ -4,7 +4,6 @@ import { immer } from 'zustand/middleware/immer';
|
|||||||
import { createWithEqualityFn } from 'zustand/traditional';
|
import { createWithEqualityFn } from 'zustand/traditional';
|
||||||
|
|
||||||
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||||
import { logMsg } from '/@/renderer/utils/logger-message';
|
|
||||||
import { toast } from '/@/shared/components/toast/toast';
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
import { ClientEvent, ServerEvent, SongUpdateSocket } from '/@/shared/types/remote-types';
|
import { ClientEvent, ServerEvent, SongUpdateSocket } from '/@/shared/types/remote-types';
|
||||||
|
|
||||||
@@ -42,7 +41,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
immer((set, get) => ({
|
immer((set, get) => ({
|
||||||
actions: {
|
actions: {
|
||||||
reconnect: async () => {
|
reconnect: async () => {
|
||||||
logFn.debug(logMsg[LogCategory.REMOTE].reconnectInitiated, {
|
logFn.debug('Reconnect initiated', {
|
||||||
category: LogCategory.REMOTE,
|
category: LogCategory.REMOTE,
|
||||||
});
|
});
|
||||||
const existing = get().socket;
|
const existing = get().socket;
|
||||||
@@ -52,7 +51,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
existing.readyState === WebSocket.OPEN ||
|
existing.readyState === WebSocket.OPEN ||
|
||||||
existing.readyState === WebSocket.CONNECTING
|
existing.readyState === WebSocket.CONNECTING
|
||||||
) {
|
) {
|
||||||
logFn.debug(logMsg[LogCategory.REMOTE].closingExistingSocket, {
|
logFn.debug('Closing existing socket', {
|
||||||
category: LogCategory.REMOTE,
|
category: LogCategory.REMOTE,
|
||||||
meta: { readyState: existing.readyState },
|
meta: { readyState: existing.readyState },
|
||||||
});
|
});
|
||||||
@@ -64,17 +63,17 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
let authHeader: string | undefined;
|
let authHeader: string | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logFn.debug(logMsg[LogCategory.REMOTE].fetchingCredentials, {
|
logFn.debug('Fetching credentials', {
|
||||||
category: LogCategory.REMOTE,
|
category: LogCategory.REMOTE,
|
||||||
});
|
});
|
||||||
const credentials = await fetch('/credentials');
|
const credentials = await fetch('/credentials');
|
||||||
authHeader = await credentials.text();
|
authHeader = await credentials.text();
|
||||||
logFn.debug(logMsg[LogCategory.REMOTE].credentialsFetched, {
|
logFn.debug('Credentials fetched', {
|
||||||
category: LogCategory.REMOTE,
|
category: LogCategory.REMOTE,
|
||||||
meta: { hasAuthHeader: !!authHeader },
|
meta: { hasAuthHeader: !!authHeader },
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logFn.error(logMsg[LogCategory.REMOTE].failedToGetCredentials, {
|
logFn.error('Failed to get credentials', {
|
||||||
category: LogCategory.REMOTE,
|
category: LogCategory.REMOTE,
|
||||||
meta: { error },
|
meta: { error },
|
||||||
});
|
});
|
||||||
@@ -82,7 +81,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const wsUrl = location.href.replace('http', 'ws');
|
const wsUrl = location.href.replace('http', 'ws');
|
||||||
logFn.debug(logMsg[LogCategory.REMOTE].creatingWebSocket, {
|
logFn.debug('Creating new WebSocket', {
|
||||||
category: LogCategory.REMOTE,
|
category: LogCategory.REMOTE,
|
||||||
meta: { url: wsUrl },
|
meta: { url: wsUrl },
|
||||||
});
|
});
|
||||||
@@ -93,34 +92,28 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
socket.addEventListener('message', (message) => {
|
socket.addEventListener('message', (message) => {
|
||||||
const { data, event } = JSON.parse(message.data) as ServerEvent;
|
const { data, event } = JSON.parse(message.data) as ServerEvent;
|
||||||
|
|
||||||
logFn.debug(logMsg[LogCategory.REMOTE].webSocketMessageReceived, {
|
logFn.debug('WebSocket message received', {
|
||||||
category: LogCategory.REMOTE,
|
category: LogCategory.REMOTE,
|
||||||
meta: { data, event },
|
meta: { data, event },
|
||||||
});
|
});
|
||||||
|
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case 'error': {
|
case 'error': {
|
||||||
logFn.error(
|
logFn.error('WebSocket error event', {
|
||||||
logMsg[LogCategory.REMOTE].webSocketErrorEvent,
|
category: LogCategory.REMOTE,
|
||||||
{
|
meta: { data },
|
||||||
category: LogCategory.REMOTE,
|
});
|
||||||
meta: { data },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
toast.error({ message: data, title: 'Socket error' });
|
toast.error({ message: data, title: 'Socket error' });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'favorite': {
|
case 'favorite': {
|
||||||
logFn.debug(
|
logFn.debug('Favorite event received', {
|
||||||
logMsg[LogCategory.REMOTE].favoriteEventReceived,
|
category: LogCategory.REMOTE,
|
||||||
{
|
meta: {
|
||||||
category: LogCategory.REMOTE,
|
favorite: data.favorite,
|
||||||
meta: {
|
id: data.id,
|
||||||
favorite: data.favorite,
|
|
||||||
id: data.id,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
set((state) => {
|
set((state) => {
|
||||||
if (state.info.song?.id === data.id) {
|
if (state.info.song?.id === data.id) {
|
||||||
state.info.song.userFavorite = data.favorite;
|
state.info.song.userFavorite = data.favorite;
|
||||||
@@ -129,33 +122,27 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'playback': {
|
case 'playback': {
|
||||||
logFn.debug(
|
logFn.debug('Playback event received', {
|
||||||
logMsg[LogCategory.REMOTE].playbackEventReceived,
|
category: LogCategory.REMOTE,
|
||||||
{
|
meta: { status: data },
|
||||||
category: LogCategory.REMOTE,
|
});
|
||||||
meta: { status: data },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.info.status = data;
|
state.info.status = data;
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'position': {
|
case 'position': {
|
||||||
logFn.debug(
|
logFn.debug('Position event received', {
|
||||||
logMsg[LogCategory.REMOTE].positionEventReceived,
|
category: LogCategory.REMOTE,
|
||||||
{
|
meta: { position: data },
|
||||||
category: LogCategory.REMOTE,
|
});
|
||||||
meta: { position: data },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.info.position = data;
|
state.info.position = data;
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'proxy': {
|
case 'proxy': {
|
||||||
logFn.debug(logMsg[LogCategory.REMOTE].proxyEventReceived, {
|
logFn.debug('Proxy event received (image update)', {
|
||||||
category: LogCategory.REMOTE,
|
category: LogCategory.REMOTE,
|
||||||
meta: {
|
meta: {
|
||||||
dataLength: data?.length,
|
dataLength: data?.length,
|
||||||
@@ -170,16 +157,13 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'rating': {
|
case 'rating': {
|
||||||
logFn.debug(
|
logFn.debug('Rating event received', {
|
||||||
logMsg[LogCategory.REMOTE].ratingEventReceived,
|
category: LogCategory.REMOTE,
|
||||||
{
|
meta: {
|
||||||
category: LogCategory.REMOTE,
|
id: data.id,
|
||||||
meta: {
|
rating: data.rating,
|
||||||
id: data.id,
|
|
||||||
rating: data.rating,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
set((state) => {
|
set((state) => {
|
||||||
if (state.info.song?.id === data.id) {
|
if (state.info.song?.id === data.id) {
|
||||||
state.info.song.userRating = data.rating;
|
state.info.song.userRating = data.rating;
|
||||||
@@ -188,33 +172,27 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'repeat': {
|
case 'repeat': {
|
||||||
logFn.debug(
|
logFn.debug('Repeat event received', {
|
||||||
logMsg[LogCategory.REMOTE].repeatEventReceived,
|
category: LogCategory.REMOTE,
|
||||||
{
|
meta: { repeat: data },
|
||||||
category: LogCategory.REMOTE,
|
});
|
||||||
meta: { repeat: data },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.info.repeat = data;
|
state.info.repeat = data;
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'shuffle': {
|
case 'shuffle': {
|
||||||
logFn.debug(
|
logFn.debug('Shuffle event received', {
|
||||||
logMsg[LogCategory.REMOTE].shuffleEventReceived,
|
category: LogCategory.REMOTE,
|
||||||
{
|
meta: { shuffle: data },
|
||||||
category: LogCategory.REMOTE,
|
});
|
||||||
meta: { shuffle: data },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.info.shuffle = data;
|
state.info.shuffle = data;
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'song': {
|
case 'song': {
|
||||||
logFn.debug(logMsg[LogCategory.REMOTE].songEventReceived, {
|
logFn.debug('Song event received', {
|
||||||
category: LogCategory.REMOTE,
|
category: LogCategory.REMOTE,
|
||||||
meta: {
|
meta: {
|
||||||
artistName: data?.artistName,
|
artistName: data?.artistName,
|
||||||
@@ -228,7 +206,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'state': {
|
case 'state': {
|
||||||
logFn.debug(logMsg[LogCategory.REMOTE].stateEventReceived, {
|
logFn.debug('State event received (full state update)', {
|
||||||
category: LogCategory.REMOTE,
|
category: LogCategory.REMOTE,
|
||||||
meta: {
|
meta: {
|
||||||
hasSong: !!data.song,
|
hasSong: !!data.song,
|
||||||
@@ -243,13 +221,10 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'volume': {
|
case 'volume': {
|
||||||
logFn.debug(
|
logFn.debug('Volume event received', {
|
||||||
logMsg[LogCategory.REMOTE].volumeEventReceived,
|
category: LogCategory.REMOTE,
|
||||||
{
|
meta: { volume: data },
|
||||||
category: LogCategory.REMOTE,
|
});
|
||||||
meta: { volume: data },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.info.volume = data;
|
state.info.volume = data;
|
||||||
});
|
});
|
||||||
@@ -258,7 +233,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.addEventListener('open', () => {
|
socket.addEventListener('open', () => {
|
||||||
logFn.debug(logMsg[LogCategory.REMOTE].webSocketOpened, {
|
logFn.debug('WebSocket opened', {
|
||||||
category: LogCategory.REMOTE,
|
category: LogCategory.REMOTE,
|
||||||
meta: {
|
meta: {
|
||||||
hasAuthHeader: !!authHeader,
|
hasAuthHeader: !!authHeader,
|
||||||
@@ -266,7 +241,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (authHeader) {
|
if (authHeader) {
|
||||||
logFn.debug(logMsg[LogCategory.REMOTE].sendingAuthentication, {
|
logFn.debug('Sending authentication', {
|
||||||
category: LogCategory.REMOTE,
|
category: LogCategory.REMOTE,
|
||||||
});
|
});
|
||||||
socket.send(
|
socket.send(
|
||||||
@@ -280,7 +255,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.addEventListener('close', (reason) => {
|
socket.addEventListener('close', (reason) => {
|
||||||
logFn.debug(logMsg[LogCategory.REMOTE].webSocketClosed, {
|
logFn.debug('WebSocket closed', {
|
||||||
category: LogCategory.REMOTE,
|
category: LogCategory.REMOTE,
|
||||||
meta: {
|
meta: {
|
||||||
code: reason.code,
|
code: reason.code,
|
||||||
@@ -290,13 +265,13 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (reason.code === 4002 || reason.code === 4003) {
|
if (reason.code === 4002 || reason.code === 4003) {
|
||||||
logFn.debug(logMsg[LogCategory.REMOTE].reloadingPage, {
|
logFn.debug('Reloading page due to close code', {
|
||||||
category: LogCategory.REMOTE,
|
category: LogCategory.REMOTE,
|
||||||
meta: { code: reason.code },
|
meta: { code: reason.code },
|
||||||
});
|
});
|
||||||
location.reload();
|
location.reload();
|
||||||
} else if (reason.code === 4000) {
|
} else if (reason.code === 4000) {
|
||||||
logFn.warn(logMsg[LogCategory.REMOTE].serverIsDown, {
|
logFn.warn('Server is down', {
|
||||||
category: LogCategory.REMOTE,
|
category: LogCategory.REMOTE,
|
||||||
});
|
});
|
||||||
toast.warn({
|
toast.warn({
|
||||||
@@ -304,16 +279,13 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
title: 'Connection closed',
|
title: 'Connection closed',
|
||||||
});
|
});
|
||||||
} else if (reason.code !== 4001 && !socket.natural) {
|
} else if (reason.code !== 4001 && !socket.natural) {
|
||||||
logFn.error(
|
logFn.error('Socket closed unexpectedly', {
|
||||||
logMsg[LogCategory.REMOTE].socketClosedUnexpectedly,
|
category: LogCategory.REMOTE,
|
||||||
{
|
meta: {
|
||||||
category: LogCategory.REMOTE,
|
code: reason.code,
|
||||||
meta: {
|
reason: reason.reason,
|
||||||
code: reason.code,
|
|
||||||
reason: reason.reason,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
toast.error({
|
toast.error({
|
||||||
message: 'Socket closed for unexpected reason',
|
message: 'Socket closed for unexpected reason',
|
||||||
title: 'Connection closed',
|
title: 'Connection closed',
|
||||||
@@ -331,7 +303,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
send: (data: ClientEvent) => {
|
send: (data: ClientEvent) => {
|
||||||
const socket = get().socket;
|
const socket = get().socket;
|
||||||
if (socket) {
|
if (socket) {
|
||||||
logFn.debug(logMsg[LogCategory.REMOTE].sendingEventToServer, {
|
logFn.debug('Sending event to server', {
|
||||||
category: LogCategory.REMOTE,
|
category: LogCategory.REMOTE,
|
||||||
meta: {
|
meta: {
|
||||||
data: data,
|
data: data,
|
||||||
@@ -341,7 +313,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
});
|
});
|
||||||
socket.send(JSON.stringify(data));
|
socket.send(JSON.stringify(data));
|
||||||
} else {
|
} else {
|
||||||
logFn.warn(logMsg[LogCategory.REMOTE].cannotSendEvent, {
|
logFn.warn('Cannot send event - socket not available', {
|
||||||
category: LogCategory.REMOTE,
|
category: LogCategory.REMOTE,
|
||||||
meta: { event: data.event },
|
meta: { event: data.event },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { NavidromeController } from '/@/renderer/api/navidrome/navidrome-control
|
|||||||
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
|
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
|
||||||
import { mergeMusicFolderId } from '/@/renderer/api/utils-music-folder';
|
import { mergeMusicFolderId } from '/@/renderer/api/utils-music-folder';
|
||||||
import { getServerById, useAuthStore, useSettingsStore } from '/@/renderer/store';
|
import { getServerById, useAuthStore, useSettingsStore } from '/@/renderer/store';
|
||||||
|
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||||
import { toast } from '/@/shared/components/toast/toast';
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
import {
|
import {
|
||||||
AuthenticationResponse,
|
AuthenticationResponse,
|
||||||
@@ -31,6 +32,7 @@ const apiController = <K extends keyof ControllerEndpoint>(
|
|||||||
const serverType = type || useAuthStore.getState().currentServer?.type;
|
const serverType = type || useAuthStore.getState().currentServer?.type;
|
||||||
|
|
||||||
if (!serverType) {
|
if (!serverType) {
|
||||||
|
logFn.warn('No server selected', { category: LogCategory.API });
|
||||||
toast.error({
|
toast.error({
|
||||||
message: i18n.t('error.serverNotSelectedError', {
|
message: i18n.t('error.serverNotSelectedError', {
|
||||||
postProcess: 'sentenceCase',
|
postProcess: 'sentenceCase',
|
||||||
@@ -43,6 +45,10 @@ const apiController = <K extends keyof ControllerEndpoint>(
|
|||||||
const controllerFn = endpoints?.[serverType]?.[endpoint];
|
const controllerFn = endpoints?.[serverType]?.[endpoint];
|
||||||
|
|
||||||
if (typeof controllerFn !== 'function') {
|
if (typeof controllerFn !== 'function') {
|
||||||
|
logFn.warn('Endpoint not implemented', {
|
||||||
|
category: LogCategory.API,
|
||||||
|
meta: { endpoint, serverType },
|
||||||
|
});
|
||||||
toast.error({
|
toast.error({
|
||||||
message: `Endpoint ${endpoint} is not implemented for ${serverType}`,
|
message: `Endpoint ${endpoint} is not implemented for ${serverType}`,
|
||||||
title: i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' }) as string,
|
title: i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' }) as string,
|
||||||
@@ -57,6 +63,10 @@ const apiController = <K extends keyof ControllerEndpoint>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logFn.debug('API controller call', {
|
||||||
|
category: LogCategory.API,
|
||||||
|
meta: { endpoint, serverType },
|
||||||
|
});
|
||||||
return controllerFn;
|
return controllerFn;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ export const contract = c.router({
|
|||||||
},
|
},
|
||||||
getInstantMix: {
|
getInstantMix: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: 'songs/:itemId/InstantMix',
|
path: 'items/:itemId/InstantMix',
|
||||||
query: jfType._parameters.similarSongs,
|
query: jfType._parameters.similarSongs,
|
||||||
responses: {
|
responses: {
|
||||||
200: jfType._response.songList,
|
200: jfType._response.songList,
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ const formatCommaDelimitedString = (value: string[]) => {
|
|||||||
// not the POST body
|
// not the POST body
|
||||||
const MAX_ITEMS_PER_PLAYLIST_ADD = 50;
|
const MAX_ITEMS_PER_PLAYLIST_ADD = 50;
|
||||||
|
|
||||||
|
// Defining a re-usable Collator instance for performance reasons.
|
||||||
|
const numericSortCollator = new Intl.Collator(undefined, { numeric: true });
|
||||||
|
const collator = new Intl.Collator();
|
||||||
|
|
||||||
const VERSION_INFO: VersionInfo = [
|
const VERSION_INFO: VersionInfo = [
|
||||||
[
|
[
|
||||||
'10.9.0',
|
'10.9.0',
|
||||||
@@ -49,6 +53,18 @@ const VERSION_INFO: VersionInfo = [
|
|||||||
['10.0.0', { [ServerFeature.TAGS]: [1] }],
|
['10.0.0', { [ServerFeature.TAGS]: [1] }],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const JF_FIELDS = {
|
||||||
|
ALBUM_ARTIST_DETAIL: 'Genres, Overview, SortName, ProviderIds',
|
||||||
|
ALBUM_ARTIST_LIST: 'Genres, DateCreated, ExternalUrls, Overview, SortName, ProviderIds',
|
||||||
|
ALBUM_DETAIL: 'Genres, DateCreated, ChildCount, People, Tags, ProviderIds',
|
||||||
|
ALBUM_LIST: 'People, Tags, Studios, SortName, UserData, ProviderIds, ChildCount',
|
||||||
|
FOLDER: 'Genres, DateCreated, MediaSources, UserData, ParentId',
|
||||||
|
GENRE: 'ItemCounts',
|
||||||
|
PLAYLIST_DETAIL: 'Genres, DateCreated, MediaSources, ChildCount, ParentId, SortName',
|
||||||
|
PLAYLIST_LIST: 'ChildCount, Genres, DateCreated, ParentId, Overview',
|
||||||
|
SONG: 'Genres, DateCreated, MediaSources, ParentId, People, Tags, SortName, UserData, ProviderIds',
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const JellyfinController: InternalControllerEndpoint = {
|
export const JellyfinController: InternalControllerEndpoint = {
|
||||||
addToPlaylist: async (args) => {
|
addToPlaylist: async (args) => {
|
||||||
const { apiClientProps, body, query } = args;
|
const { apiClientProps, body, query } = args;
|
||||||
@@ -226,7 +242,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
userId: apiClientProps.server?.userId,
|
userId: apiClientProps.server?.userId,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
Fields: 'Genres, Overview, SortName',
|
Fields: JF_FIELDS.ALBUM_ARTIST_DETAIL,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
jfApiClient(apiClientProps).getSimilarArtistList({
|
jfApiClient(apiClientProps).getSimilarArtistList({
|
||||||
@@ -253,7 +269,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
|
|
||||||
const res = await jfApiClient(apiClientProps).getAlbumArtistList({
|
const res = await jfApiClient(apiClientProps).getAlbumArtistList({
|
||||||
query: {
|
query: {
|
||||||
Fields: 'Genres, DateCreated, ExternalUrls, Overview, SortName',
|
Fields: JF_FIELDS.ALBUM_ARTIST_LIST,
|
||||||
ImageTypeLimit: 1,
|
ImageTypeLimit: 1,
|
||||||
Limit: query.limit,
|
Limit: query.limit,
|
||||||
ParentId: getLibraryId(query.musicFolderId),
|
ParentId: getLibraryId(query.musicFolderId),
|
||||||
@@ -296,7 +312,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
userId: apiClientProps.server.userId,
|
userId: apiClientProps.server.userId,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
Fields: 'Genres, DateCreated, ChildCount, People, Tags',
|
Fields: JF_FIELDS.ALBUM_DETAIL,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -305,7 +321,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
userId: apiClientProps.server.userId,
|
userId: apiClientProps.server.userId,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags, SortName',
|
Fields: JF_FIELDS.SONG,
|
||||||
IncludeItemTypes: 'Audio',
|
IncludeItemTypes: 'Audio',
|
||||||
ParentId: query.id,
|
ParentId: query.id,
|
||||||
SortBy: 'ParentIndexNumber,IndexNumber,SortName',
|
SortBy: 'ParentIndexNumber,IndexNumber,SortName',
|
||||||
@@ -363,7 +379,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
...artistQuery,
|
...artistQuery,
|
||||||
Fields: 'People, Tags, Studios, SortName',
|
Fields: JF_FIELDS.ALBUM_LIST,
|
||||||
GenreIds: query.genreIds ? query.genreIds.join(',') : undefined,
|
GenreIds: query.genreIds ? query.genreIds.join(',') : undefined,
|
||||||
IncludeItemTypes: 'MusicAlbum',
|
IncludeItemTypes: 'MusicAlbum',
|
||||||
IsFavorite: query.favorite,
|
IsFavorite: query.favorite,
|
||||||
@@ -399,7 +415,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
|
|
||||||
const res = await jfApiClient(apiClientProps).getArtistList({
|
const res = await jfApiClient(apiClientProps).getArtistList({
|
||||||
query: {
|
query: {
|
||||||
Fields: 'Genres, DateCreated, ExternalUrls, Overview, SortName',
|
Fields: JF_FIELDS.ALBUM_ARTIST_LIST,
|
||||||
ImageTypeLimit: 1,
|
ImageTypeLimit: 1,
|
||||||
Limit: query.limit,
|
Limit: query.limit,
|
||||||
ParentId: getLibraryId(query.musicFolderId),
|
ParentId: getLibraryId(query.musicFolderId),
|
||||||
@@ -438,7 +454,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
itemId: query.artistId,
|
itemId: query.artistId,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
Fields: 'Genres, DateCreated, MediaSources, ParentId, SortName',
|
Fields: JF_FIELDS.SONG,
|
||||||
Limit: query.count,
|
Limit: query.count,
|
||||||
UserId: apiClientProps.server?.userId || undefined,
|
UserId: apiClientProps.server?.userId || undefined,
|
||||||
},
|
},
|
||||||
@@ -563,7 +579,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
Fields: 'Genres, DateCreated, MediaSources, UserData, ParentId',
|
Fields: JF_FIELDS.FOLDER,
|
||||||
ParentId: query.id,
|
ParentId: query.id,
|
||||||
SortBy: query.sortBy
|
SortBy: query.sortBy
|
||||||
? (songListSortMap.jellyfin[query.sortBy] as string) || 'SortName'
|
? (songListSortMap.jellyfin[query.sortBy] as string) || 'SortName'
|
||||||
@@ -592,7 +608,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
Fields: 'Genres, DateCreated, MediaSources, UserData, ParentId',
|
Fields: JF_FIELDS.FOLDER,
|
||||||
ParentId: parentId,
|
ParentId: parentId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -679,7 +695,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
const res = await jfApiClient(apiClientProps).getGenreList({
|
const res = await jfApiClient(apiClientProps).getGenreList({
|
||||||
query: {
|
query: {
|
||||||
EnableTotalRecordCount: true,
|
EnableTotalRecordCount: true,
|
||||||
Fields: 'ItemCounts',
|
Fields: JF_FIELDS.GENRE,
|
||||||
Limit: query.limit === -1 ? undefined : query.limit,
|
Limit: query.limit === -1 ? undefined : query.limit,
|
||||||
ParentId: getLibraryId(query.musicFolderId),
|
ParentId: getLibraryId(query.musicFolderId),
|
||||||
Recursive: true,
|
Recursive: true,
|
||||||
@@ -794,7 +810,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
userId: apiClientProps.server?.userId,
|
userId: apiClientProps.server?.userId,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
Fields: 'Genres, DateCreated, MediaSources, ChildCount, ParentId, SortName',
|
Fields: JF_FIELDS.PLAYLIST_DETAIL,
|
||||||
Ids: query.id,
|
Ids: query.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -817,7 +833,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
userId: apiClientProps.server?.userId,
|
userId: apiClientProps.server?.userId,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
Fields: 'ChildCount, Genres, DateCreated, ParentId, Overview',
|
Fields: JF_FIELDS.PLAYLIST_LIST,
|
||||||
IncludeItemTypes: 'Playlist',
|
IncludeItemTypes: 'Playlist',
|
||||||
Limit: query.limit,
|
Limit: query.limit,
|
||||||
Recursive: true,
|
Recursive: true,
|
||||||
@@ -855,7 +871,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
id: query.id,
|
id: query.id,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
Fields: 'Genres, DateCreated, MediaSources, UserData, ParentId, People, Tags, SortName',
|
Fields: JF_FIELDS.SONG,
|
||||||
IncludeItemTypes: 'Audio',
|
IncludeItemTypes: 'Audio',
|
||||||
UserId: apiClientProps.server?.userId,
|
UserId: apiClientProps.server?.userId,
|
||||||
},
|
},
|
||||||
@@ -902,7 +918,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
userId: apiClientProps.server?.userId,
|
userId: apiClientProps.server?.userId,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags, SortName',
|
Fields: JF_FIELDS.SONG,
|
||||||
GenreIds: query.genre ? query.genre : undefined,
|
GenreIds: query.genre ? query.genre : undefined,
|
||||||
IncludeItemTypes: 'Audio',
|
IncludeItemTypes: 'Audio',
|
||||||
IsPlayed:
|
IsPlayed:
|
||||||
@@ -974,7 +990,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
itemId: query.songId,
|
itemId: query.songId,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
Fields: 'Genres, DateCreated, MediaSources, ParentId, SortName',
|
Fields: JF_FIELDS.SONG,
|
||||||
Limit: query.count,
|
Limit: query.count,
|
||||||
UserId: apiClientProps.server?.userId || undefined,
|
UserId: apiClientProps.server?.userId || undefined,
|
||||||
},
|
},
|
||||||
@@ -1007,7 +1023,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
itemId: query.songId,
|
itemId: query.songId,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
Fields: 'Genres, DateCreated, MediaSources, ParentId, SortName',
|
Fields: JF_FIELDS.SONG,
|
||||||
Limit: query.count,
|
Limit: query.count,
|
||||||
UserId: apiClientProps.server?.userId || undefined,
|
UserId: apiClientProps.server?.userId || undefined,
|
||||||
},
|
},
|
||||||
@@ -1092,11 +1108,11 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
query: {
|
query: {
|
||||||
AlbumIds: albumIdsFilter,
|
AlbumIds: albumIdsFilter,
|
||||||
ArtistIds: artistIdsFilter,
|
ArtistIds: artistIdsFilter,
|
||||||
Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags, SortName',
|
Fields: JF_FIELDS.SONG,
|
||||||
GenreIds: query.genreIds?.join(','),
|
GenreIds: query.genreIds?.join(','),
|
||||||
IncludeItemTypes: 'Audio',
|
IncludeItemTypes: 'Audio',
|
||||||
IsFavorite: query.favorite,
|
IsFavorite: query.favorite,
|
||||||
Limit: query.limit,
|
Limit: query.limit === -1 ? undefined : query.limit,
|
||||||
ParentId: getLibraryId(query.musicFolderId),
|
ParentId: getLibraryId(query.musicFolderId),
|
||||||
Recursive: true,
|
Recursive: true,
|
||||||
SearchTerm: query.searchTerm,
|
SearchTerm: query.searchTerm,
|
||||||
@@ -1127,11 +1143,11 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
query: {
|
query: {
|
||||||
AlbumIds: albumIdsFilter,
|
AlbumIds: albumIdsFilter,
|
||||||
ArtistIds: artistIdsFilter,
|
ArtistIds: artistIdsFilter,
|
||||||
Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags, SortName',
|
Fields: JF_FIELDS.SONG,
|
||||||
GenreIds: query.genreIds?.join(','),
|
GenreIds: query.genreIds?.join(','),
|
||||||
IncludeItemTypes: 'Audio',
|
IncludeItemTypes: 'Audio',
|
||||||
IsFavorite: query.favorite,
|
IsFavorite: query.favorite,
|
||||||
Limit: query.limit,
|
Limit: query.limit === -1 ? undefined : query.limit,
|
||||||
ParentId: getLibraryId(query.musicFolderId),
|
ParentId: getLibraryId(query.musicFolderId),
|
||||||
Recursive: true,
|
Recursive: true,
|
||||||
SearchTerm: query.searchTerm,
|
SearchTerm: query.searchTerm,
|
||||||
@@ -1250,11 +1266,12 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
if (res.body.Tags?.length) {
|
if (res.body.Tags?.length) {
|
||||||
tags.push({
|
tags.push({
|
||||||
name: 'Tags',
|
name: 'Tags',
|
||||||
options: res.body.Tags.sort((a, b) =>
|
options: res.body.Tags.sort((a, b) => {
|
||||||
a
|
return numericSortCollator.compare(
|
||||||
.toLocaleLowerCase()
|
a.toLocaleLowerCase(),
|
||||||
.localeCompare(b.toLocaleLowerCase(), undefined, { numeric: true }),
|
b.toLocaleLowerCase(),
|
||||||
).map((tag) => ({ id: tag, name: tag })),
|
);
|
||||||
|
}).map((tag) => ({ id: tag, name: tag })),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1262,7 +1279,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
tags.push({
|
tags.push({
|
||||||
name: 'Studios',
|
name: 'Studios',
|
||||||
options: studioRes.body.Items.sort((a, b) =>
|
options: studioRes.body.Items.sort((a, b) =>
|
||||||
a.Name.toLocaleLowerCase().localeCompare(b.Name.toLocaleLowerCase()),
|
collator.compare(a.Name.toLocaleLowerCase(), b.Name.toLocaleLowerCase()),
|
||||||
).map((option) => ({ id: option.Name, name: option.Name })),
|
).map((option) => ({ id: option.Name, name: option.Name })),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1276,17 +1293,22 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
throw new Error('No userId found');
|
throw new Error('No userId found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const type = query.type === 'personal' ? 'personal' : 'community';
|
||||||
|
|
||||||
const res = await jfApiClient(apiClientProps).getTopSongsList({
|
const res = await jfApiClient(apiClientProps).getTopSongsList({
|
||||||
params: {
|
params: {
|
||||||
userId: apiClientProps.server?.userId,
|
userId: apiClientProps.server?.userId,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
ArtistIds: query.artistId,
|
ArtistIds: query.artistId,
|
||||||
Fields: 'Genres, DateCreated, MediaSources, ParentId, SortName',
|
Fields: JF_FIELDS.SONG,
|
||||||
IncludeItemTypes: 'Audio',
|
IncludeItemTypes: 'Audio',
|
||||||
Limit: query.limit,
|
Limit: query.limit,
|
||||||
Recursive: true,
|
Recursive: true,
|
||||||
SortBy: 'CommunityRating,SortName',
|
SortBy:
|
||||||
|
type === 'personal'
|
||||||
|
? JFSongListSort.PLAY_COUNT
|
||||||
|
: JFSongListSort.COMMUNITY_RATING,
|
||||||
SortOrder: 'Descending',
|
SortOrder: 'Descending',
|
||||||
UserId: apiClientProps.server?.userId,
|
UserId: apiClientProps.server?.userId,
|
||||||
},
|
},
|
||||||
@@ -1296,15 +1318,31 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
throw new Error('Failed to get top song list');
|
throw new Error('Failed to get top song list');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const items = res.body.Items.map((item) =>
|
||||||
items: res.body.Items.map((item) =>
|
jfNormalize.song(
|
||||||
jfNormalize.song(
|
item,
|
||||||
item,
|
apiClientProps.server,
|
||||||
apiClientProps.server,
|
args.context?.pathReplace,
|
||||||
args.context?.pathReplace,
|
args.context?.pathReplaceWith,
|
||||||
args.context?.pathReplaceWith,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (type === 'personal') {
|
||||||
|
const sorted = orderBy(
|
||||||
|
items,
|
||||||
|
['playCount', 'albumId', 'trackNumber'],
|
||||||
|
['desc', 'asc', 'asc'],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: sorted,
|
||||||
|
startIndex: 0,
|
||||||
|
totalRecordCount: res.body.TotalRecordCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items,
|
||||||
startIndex: 0,
|
startIndex: 0,
|
||||||
totalRecordCount: res.body.TotalRecordCount,
|
totalRecordCount: res.body.TotalRecordCount,
|
||||||
};
|
};
|
||||||
@@ -1378,7 +1416,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
id: query.id,
|
id: query.id,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
Fields: 'Genres, DateCreated, MediaSources, UserData, ParentId, People, Tags, SortName',
|
Fields: JF_FIELDS.SONG,
|
||||||
IncludeItemTypes: 'Audio',
|
IncludeItemTypes: 'Audio',
|
||||||
UserId: apiClientProps.server?.userId,
|
UserId: apiClientProps.server?.userId,
|
||||||
},
|
},
|
||||||
@@ -1404,7 +1442,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
userId: apiClientProps.server?.userId,
|
userId: apiClientProps.server?.userId,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
Fields: 'Genres, DateCreated, MediaSources, ChildCount, ParentId, SortName',
|
Fields: JF_FIELDS.PLAYLIST_DETAIL,
|
||||||
Ids: query.id,
|
Ids: query.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1562,7 +1600,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
EnableTotalRecordCount: true,
|
EnableTotalRecordCount: true,
|
||||||
Fields: 'People, Tags, SortName',
|
Fields: JF_FIELDS.ALBUM_LIST,
|
||||||
ImageTypeLimit: 1,
|
ImageTypeLimit: 1,
|
||||||
IncludeItemTypes: 'MusicAlbum',
|
IncludeItemTypes: 'MusicAlbum',
|
||||||
Limit: query.albumLimit,
|
Limit: query.albumLimit,
|
||||||
@@ -1585,7 +1623,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
const res = await jfApiClient(apiClientProps).getAlbumArtistList({
|
const res = await jfApiClient(apiClientProps).getAlbumArtistList({
|
||||||
query: {
|
query: {
|
||||||
EnableTotalRecordCount: true,
|
EnableTotalRecordCount: true,
|
||||||
Fields: 'Genres, DateCreated, ExternalUrls, Overview',
|
Fields: JF_FIELDS.ALBUM_ARTIST_LIST,
|
||||||
ImageTypeLimit: 1,
|
ImageTypeLimit: 1,
|
||||||
IncludeArtists: true,
|
IncludeArtists: true,
|
||||||
Limit: query.albumArtistLimit,
|
Limit: query.albumArtistLimit,
|
||||||
@@ -1610,7 +1648,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
EnableTotalRecordCount: true,
|
EnableTotalRecordCount: true,
|
||||||
Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags, SortName',
|
Fields: JF_FIELDS.SONG,
|
||||||
IncludeItemTypes: 'Audio',
|
IncludeItemTypes: 'Audio',
|
||||||
Limit: query.songLimit,
|
Limit: query.songLimit,
|
||||||
Recursive: true,
|
Recursive: true,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import qs from 'qs';
|
|||||||
import i18n from '/@/i18n/i18n';
|
import i18n from '/@/i18n/i18n';
|
||||||
import { authenticationFailure } from '/@/renderer/api/utils';
|
import { authenticationFailure } from '/@/renderer/api/utils';
|
||||||
import { useAuthStore } from '/@/renderer/store';
|
import { useAuthStore } from '/@/renderer/store';
|
||||||
|
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||||
import { getServerUrl } from '/@/renderer/utils/normalize-server-url';
|
import { getServerUrl } from '/@/renderer/utils/normalize-server-url';
|
||||||
import { ndType } from '/@/shared/api/navidrome/navidrome-types';
|
import { ndType } from '/@/shared/api/navidrome/navidrome-types';
|
||||||
import { resultWithHeaders } from '/@/shared/api/utils';
|
import { resultWithHeaders } from '/@/shared/api/utils';
|
||||||
@@ -367,11 +368,21 @@ axiosClient.interceptors.response.use(
|
|||||||
})
|
})
|
||||||
.catch((newError: any) => {
|
.catch((newError: any) => {
|
||||||
if (newError !== TIMEOUT_ERROR) {
|
if (newError !== TIMEOUT_ERROR) {
|
||||||
console.error('Error when trying to reauthenticate: ', newError);
|
logFn.error('Reauthentication failed', {
|
||||||
|
category: LogCategory.API,
|
||||||
|
meta: {
|
||||||
|
message: (newError as Error)?.message,
|
||||||
|
serverId: currentServer.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (isAxiosError(newError) && newError.code === 'ERR_NETWORK') {
|
if (isAxiosError(newError) && newError.code === 'ERR_NETWORK') {
|
||||||
console.log(
|
logFn.info(
|
||||||
'Network error during reauthentication - preserving credentials',
|
'Network error during reauthentication - preserving credentials',
|
||||||
|
{
|
||||||
|
category: LogCategory.API,
|
||||||
|
meta: { serverId: currentServer.id },
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
limitedFail(currentServer);
|
limitedFail(currentServer);
|
||||||
@@ -387,7 +398,10 @@ axiosClient.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isAxiosError(error) && error.code === 'ERR_NETWORK') {
|
if (isAxiosError(error) && error.code === 'ERR_NETWORK') {
|
||||||
console.log('Network error during authentication - preserving credentials');
|
logFn.info('Network error during authentication - preserving credentials', {
|
||||||
|
category: LogCategory.API,
|
||||||
|
meta: { serverId: useAuthStore.getState().currentServer?.id },
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
limitedFail(currentServer);
|
limitedFail(currentServer);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { set } from 'idb-keyval';
|
import { set } from 'idb-keyval';
|
||||||
|
import orderBy from 'lodash/orderBy';
|
||||||
|
|
||||||
import { ndApiClient } from '/@/renderer/api/navidrome/navidrome-api';
|
import { ndApiClient } from '/@/renderer/api/navidrome/navidrome-api';
|
||||||
import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
||||||
@@ -17,7 +18,9 @@ import {
|
|||||||
PlaylistSongListArgs,
|
PlaylistSongListArgs,
|
||||||
PlaylistSongListResponse,
|
PlaylistSongListResponse,
|
||||||
ServerListItemWithCredential,
|
ServerListItemWithCredential,
|
||||||
|
SongListSort,
|
||||||
songListSortMap,
|
songListSortMap,
|
||||||
|
SortOrder,
|
||||||
sortOrderMap,
|
sortOrderMap,
|
||||||
tagListSortMap,
|
tagListSortMap,
|
||||||
userListSortMap,
|
userListSortMap,
|
||||||
@@ -71,6 +74,10 @@ const EXCLUDED_ALBUM_TAGS = new Set<string>([
|
|||||||
|
|
||||||
const EXCLUDED_SONG_TAGS = new Set<string>(['disctotal', 'tracktotal']);
|
const EXCLUDED_SONG_TAGS = new Set<string>(['disctotal', 'tracktotal']);
|
||||||
|
|
||||||
|
// Defining a re-usable Collator instance for performance reasons.
|
||||||
|
const numericSortCollator = new Intl.Collator(undefined, { numeric: true });
|
||||||
|
const collator = new Intl.Collator();
|
||||||
|
|
||||||
// Tags that use IDs as values as opposed to the tag value
|
// Tags that use IDs as values as opposed to the tag value
|
||||||
const ID_TAGS = new Set<string>(['albumversion', 'mood']);
|
const ID_TAGS = new Set<string>(['albumversion', 'mood']);
|
||||||
|
|
||||||
@@ -780,16 +787,17 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
.map((data) => ({
|
.map((data) => ({
|
||||||
name: data[0],
|
name: data[0],
|
||||||
options: data[1]
|
options: data[1]
|
||||||
.sort((a, b) =>
|
.sort((a, b) => {
|
||||||
a.name
|
return numericSortCollator.compare(
|
||||||
.toLocaleLowerCase()
|
a.name.toLocaleLowerCase(),
|
||||||
.localeCompare(b.name.toLocaleLowerCase(), undefined, {
|
b.name.toLocaleLowerCase(),
|
||||||
numeric: true,
|
);
|
||||||
}),
|
})
|
||||||
)
|
|
||||||
.map((option) => ({ id: option.id, name: option.name })),
|
.map((option) => ({ id: option.id, name: option.name })),
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase()));
|
.sort((a, b) =>
|
||||||
|
collator.compare(a.name.toLocaleLowerCase(), b.name.toLocaleLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
const excludedAlbumTags = Array.from(EXCLUDED_ALBUM_TAGS.values());
|
const excludedAlbumTags = Array.from(EXCLUDED_ALBUM_TAGS.values());
|
||||||
const excludedSongTags = Array.from(EXCLUDED_SONG_TAGS.values());
|
const excludedSongTags = Array.from(EXCLUDED_SONG_TAGS.values());
|
||||||
@@ -802,7 +810,59 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
tags,
|
tags,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getTopSongs: SubsonicController.getTopSongs,
|
getTopSongs: async (args) => {
|
||||||
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
|
const type = query.type === 'personal' ? 'personal' : 'community';
|
||||||
|
|
||||||
|
if (type === 'community') {
|
||||||
|
const res = await ssApiClient(apiClientProps).getTopSongsList({
|
||||||
|
query: {
|
||||||
|
artist: query.artist,
|
||||||
|
count: query.limit,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get top songs');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: (res.body.topSongs?.song || []).map((song) =>
|
||||||
|
ssNormalize.song(
|
||||||
|
song,
|
||||||
|
apiClientProps.server,
|
||||||
|
args.context?.pathReplace,
|
||||||
|
args.context?.pathReplaceWith,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
startIndex: 0,
|
||||||
|
totalRecordCount: res.body.topSongs?.song?.length || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await NavidromeController.getSongList({
|
||||||
|
apiClientProps,
|
||||||
|
query: {
|
||||||
|
artistIds: [query.artistId],
|
||||||
|
sortBy: SongListSort.PLAY_COUNT,
|
||||||
|
sortOrder: SortOrder.DESC,
|
||||||
|
startIndex: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const songsWithPlayCount = orderBy(
|
||||||
|
res.items.filter((song) => song.playCount > 0),
|
||||||
|
['playCount', 'albumId', 'trackNumber'],
|
||||||
|
['desc', 'asc', 'asc'],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: songsWithPlayCount,
|
||||||
|
startIndex: 0,
|
||||||
|
totalRecordCount: res.totalRecordCount,
|
||||||
|
};
|
||||||
|
},
|
||||||
getUserInfo: SubsonicController.getUserInfo,
|
getUserInfo: SubsonicController.getUserInfo,
|
||||||
getUserList: async (args) => {
|
getUserList: async (args) => {
|
||||||
const { apiClientProps, query } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|||||||
@@ -73,6 +73,13 @@ export const queryKeys: Record<
|
|||||||
|
|
||||||
return [serverId, 'albumArtists', 'detail'] as const;
|
return [serverId, 'albumArtists', 'detail'] as const;
|
||||||
},
|
},
|
||||||
|
favoriteSongs: (serverId: string, artistId?: string) => {
|
||||||
|
if (artistId) {
|
||||||
|
return [serverId, 'albumArtists', 'favoriteSongs', artistId] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [serverId, 'albumArtists', 'favoriteSongs'] as const;
|
||||||
|
},
|
||||||
infiniteList: (serverId: string, query?: AlbumArtistListQuery) => {
|
infiniteList: (serverId: string, query?: AlbumArtistListQuery) => {
|
||||||
const { filter, pagination } = splitPaginatedQuery(query);
|
const { filter, pagination } = splitPaginatedQuery(query);
|
||||||
if (query && pagination) {
|
if (query && pagination) {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const ALBUM_LIST_SORT_MAPPING: Record<AlbumListSort, AlbumListSortType | undefin
|
|||||||
[AlbumListSort.DURATION]: undefined,
|
[AlbumListSort.DURATION]: undefined,
|
||||||
[AlbumListSort.EXPLICIT_STATUS]: undefined,
|
[AlbumListSort.EXPLICIT_STATUS]: undefined,
|
||||||
[AlbumListSort.FAVORITED]: AlbumListSortType.STARRED,
|
[AlbumListSort.FAVORITED]: AlbumListSortType.STARRED,
|
||||||
|
[AlbumListSort.ID]: undefined,
|
||||||
[AlbumListSort.NAME]: AlbumListSortType.ALPHABETICAL_BY_NAME,
|
[AlbumListSort.NAME]: AlbumListSortType.ALPHABETICAL_BY_NAME,
|
||||||
[AlbumListSort.PLAY_COUNT]: AlbumListSortType.FREQUENT,
|
[AlbumListSort.PLAY_COUNT]: AlbumListSortType.FREQUENT,
|
||||||
[AlbumListSort.RANDOM]: AlbumListSortType.RANDOM,
|
[AlbumListSort.RANDOM]: AlbumListSortType.RANDOM,
|
||||||
@@ -1361,7 +1362,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
throw new Error('Failed to get song list');
|
throw new Error('Failed to get song list');
|
||||||
}
|
}
|
||||||
|
|
||||||
const allResults =
|
let allResults =
|
||||||
(res.body.starred?.song || []).map((song) =>
|
(res.body.starred?.song || []).map((song) =>
|
||||||
ssNormalize.song(
|
ssNormalize.song(
|
||||||
song,
|
song,
|
||||||
@@ -1371,6 +1372,15 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
),
|
),
|
||||||
) || [];
|
) || [];
|
||||||
|
|
||||||
|
const filterArtistIds = query.albumArtistIds || query.artistIds;
|
||||||
|
|
||||||
|
if (filterArtistIds?.length) {
|
||||||
|
const idSet = new Set(filterArtistIds);
|
||||||
|
allResults = allResults.filter((song) =>
|
||||||
|
song.albumArtists?.some((aa) => idSet.has(aa.id)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return sortAndPaginate(allResults, {
|
return sortAndPaginate(allResults, {
|
||||||
limit: query.limit,
|
limit: query.limit,
|
||||||
sortBy: query.sortBy,
|
sortBy: query.sortBy,
|
||||||
@@ -1794,29 +1804,54 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
getTopSongs: async (args) => {
|
getTopSongs: async (args) => {
|
||||||
const { apiClientProps, context, query } = args;
|
const { apiClientProps, context, query } = args;
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).getTopSongsList({
|
const type = query.type === 'personal' ? 'personal' : 'community';
|
||||||
query: {
|
|
||||||
artist: query.artist,
|
|
||||||
count: query.limit,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (type === 'community') {
|
||||||
throw new Error('Failed to get top songs');
|
const res = await ssApiClient(apiClientProps).getTopSongsList({
|
||||||
}
|
query: {
|
||||||
|
artist: query.artist,
|
||||||
|
count: query.limit,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
if (res.status !== 200) {
|
||||||
items:
|
throw new Error('Failed to get top songs');
|
||||||
res.body.topSongs?.song?.map((song) =>
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: (res.body.topSongs?.song || []).map((song) =>
|
||||||
ssNormalize.song(
|
ssNormalize.song(
|
||||||
song,
|
song,
|
||||||
apiClientProps.server,
|
apiClientProps.server,
|
||||||
context?.pathReplace,
|
context?.pathReplace,
|
||||||
context?.pathReplaceWith,
|
context?.pathReplaceWith,
|
||||||
),
|
),
|
||||||
) || [],
|
),
|
||||||
|
startIndex: 0,
|
||||||
|
totalRecordCount: res.body.topSongs?.song?.length || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await SubsonicController.getSongList({
|
||||||
|
apiClientProps,
|
||||||
|
query: {
|
||||||
|
artistIds: [query.artistId],
|
||||||
|
sortBy: SongListSort.PLAY_COUNT,
|
||||||
|
sortOrder: SortOrder.DESC,
|
||||||
|
startIndex: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const songsWithPlayCount = orderBy(
|
||||||
|
res.items.filter((song) => song.playCount > 0),
|
||||||
|
['playCount', 'albumId', 'trackNumber'],
|
||||||
|
['desc', 'asc', 'asc'],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: songsWithPlayCount,
|
||||||
startIndex: 0,
|
startIndex: 0,
|
||||||
totalRecordCount: res.body.topSongs?.song?.length || 0,
|
totalRecordCount: res.totalRecordCount,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getUserInfo: async (args) => {
|
getUserInfo: async (args) => {
|
||||||
|
|||||||
@@ -62,7 +62,11 @@ export const getOptimizedListCount = async <
|
|||||||
query: pageQuery,
|
query: pageQuery,
|
||||||
});
|
});
|
||||||
|
|
||||||
client.setQueryData(pageQueryKey, pageResult);
|
const keyContainsRandom = JSON.stringify(pageQueryKey).toLowerCase().includes('random');
|
||||||
|
|
||||||
|
if (!keyContainsRandom) {
|
||||||
|
client.setQueryData(pageQueryKey, pageResult);
|
||||||
|
}
|
||||||
|
|
||||||
return pageResult.totalRecordCount ?? 0;
|
return pageResult.totalRecordCount ?? 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import { useAuthStore } from '/@/renderer/store';
|
import { useAuthStore } from '/@/renderer/store';
|
||||||
|
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||||
import { toast } from '/@/shared/components/toast/toast';
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
import { ServerListItem } from '/@/shared/types/types';
|
import { ServerListItem } from '/@/shared/types/types';
|
||||||
|
|
||||||
export const authenticationFailure = (currentServer: null | ServerListItem) => {
|
export const authenticationFailure = (currentServer: null | ServerListItem) => {
|
||||||
|
logFn.error('Token expired', {
|
||||||
|
category: LogCategory.API,
|
||||||
|
meta: { serverId: currentServer?.id },
|
||||||
|
});
|
||||||
toast.error({
|
toast.error({
|
||||||
message: 'Your session has expired.',
|
message: 'Your session has expired.',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (currentServer) {
|
if (currentServer) {
|
||||||
const serverId = currentServer.id;
|
const serverId = currentServer.id;
|
||||||
const token = currentServer.ndCredential;
|
|
||||||
console.error(`token is expired: ${token}`);
|
|
||||||
useAuthStore.getState().actions.updateServer(serverId, { ndCredential: undefined });
|
useAuthStore.getState().actions.updateServer(serverId, { ndCredential: undefined });
|
||||||
useAuthStore.getState().actions.setCurrentServer(null);
|
useAuthStore.getState().actions.setCurrentServer(null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import isElectron from 'is-electron';
|
|||||||
import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import i18n from '/@/i18n/i18n';
|
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 { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context';
|
||||||
|
import { useCheckForUpdates } from '/@/renderer/hooks/use-check-for-updates';
|
||||||
import { useSyncSettingsToMain } from '/@/renderer/hooks/use-sync-settings-to-main';
|
import { useSyncSettingsToMain } from '/@/renderer/hooks/use-sync-settings-to-main';
|
||||||
import { AppRouter } from '/@/renderer/router/app-router';
|
import { AppRouter } from '/@/renderer/router/app-router';
|
||||||
import { useCssSettings, useHotkeySettings, useLanguage } from '/@/renderer/store';
|
import { useCssSettings, useHotkeySettings, useLanguage } from '/@/renderer/store';
|
||||||
@@ -38,6 +40,7 @@ export const App = () => {
|
|||||||
const cssRef = useRef<HTMLStyleElement | null>(null);
|
const cssRef = useRef<HTMLStyleElement | null>(null);
|
||||||
|
|
||||||
useSyncSettingsToMain();
|
useSyncSettingsToMain();
|
||||||
|
useCheckForUpdates();
|
||||||
|
|
||||||
const [webAudio, setWebAudio] = useState<WebAudio>();
|
const [webAudio, setWebAudio] = useState<WebAudio>();
|
||||||
|
|
||||||
@@ -77,6 +80,19 @@ export const App = () => {
|
|||||||
}
|
}
|
||||||
}, [language]);
|
}, [language]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isElectron()) {
|
||||||
|
window.api.utils.rendererOpenSettings(() => {
|
||||||
|
openSettingsModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ipc?.removeAllListeners('renderer-open-settings');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const notificationStyles = useMemo(
|
const notificationStyles = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
root: {
|
root: {
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ const CarouselItem = ({ album }: CarouselItemProps) => {
|
|||||||
containerClassName={styles.albumImageContainer}
|
containerClassName={styles.albumImageContainer}
|
||||||
enableDebounce={false}
|
enableDebounce={false}
|
||||||
enableViewport={false}
|
enableViewport={false}
|
||||||
|
explicitStatus={album.explicitStatus}
|
||||||
fetchPriority="high"
|
fetchPriority="high"
|
||||||
id={album.imageId}
|
id={album.imageId}
|
||||||
itemType={LibraryItem.ALBUM}
|
itemType={LibraryItem.ALBUM}
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ const CarouselItem = ({ album }: CarouselItemProps) => {
|
|||||||
containerClassName={styles.albumImageContainer}
|
containerClassName={styles.albumImageContainer}
|
||||||
enableDebounce={false}
|
enableDebounce={false}
|
||||||
enableViewport={false}
|
enableViewport={false}
|
||||||
|
explicitStatus={album.explicitStatus}
|
||||||
fetchPriority="high"
|
fetchPriority="high"
|
||||||
id={album.imageId}
|
id={album.imageId}
|
||||||
itemType={LibraryItem.ALBUM}
|
itemType={LibraryItem.ALBUM}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
z-index: 5;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
formatRating,
|
formatRating,
|
||||||
} from '/@/renderer/utils/format';
|
} from '/@/renderer/utils/format';
|
||||||
import { SEPARATOR_STRING } from '/@/shared/api/utils';
|
import { SEPARATOR_STRING } from '/@/shared/api/utils';
|
||||||
|
import { ExplicitIndicator } from '/@/shared/components/explicit-indicator/explicit-indicator';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
import { Separator } from '/@/shared/components/separator/separator';
|
import { Separator } from '/@/shared/components/separator/separator';
|
||||||
@@ -361,6 +362,9 @@ const CompactItemCard = ({
|
|||||||
[styles.isRound]: isRound,
|
[styles.isRound]: isRound,
|
||||||
})}
|
})}
|
||||||
enableDebounce={false}
|
enableDebounce={false}
|
||||||
|
explicitStatus={
|
||||||
|
'explicitStatus' in data && data ? data.explicitStatus : null
|
||||||
|
}
|
||||||
id={data?.imageId}
|
id={data?.imageId}
|
||||||
itemType={itemType}
|
itemType={itemType}
|
||||||
src={(data as Album | AlbumArtist | Playlist | Song)?.imageUrl}
|
src={(data as Album | AlbumArtist | Playlist | Song)?.imageUrl}
|
||||||
@@ -595,6 +599,9 @@ const DefaultItemCard = ({
|
|||||||
<ItemImage
|
<ItemImage
|
||||||
className={clsx(styles.image, { [styles.isRound]: isRound })}
|
className={clsx(styles.image, { [styles.isRound]: isRound })}
|
||||||
enableDebounce={false}
|
enableDebounce={false}
|
||||||
|
explicitStatus={
|
||||||
|
'explicitStatus' in data && data ? data.explicitStatus : null
|
||||||
|
}
|
||||||
id={data?.imageId}
|
id={data?.imageId}
|
||||||
itemType={itemType}
|
itemType={itemType}
|
||||||
src={(data as Album | AlbumArtist | Playlist | Song)?.imageUrl}
|
src={(data as Album | AlbumArtist | Playlist | Song)?.imageUrl}
|
||||||
@@ -892,6 +899,9 @@ const PosterItemCard = ({
|
|||||||
<ItemImage
|
<ItemImage
|
||||||
className={clsx(styles.image, { [styles.isRound]: isRound })}
|
className={clsx(styles.image, { [styles.isRound]: isRound })}
|
||||||
enableDebounce={false}
|
enableDebounce={false}
|
||||||
|
explicitStatus={
|
||||||
|
'explicitStatus' in data && data ? data.explicitStatus : null
|
||||||
|
}
|
||||||
id={(data as { imageId: string })?.imageId}
|
id={(data as { imageId: string })?.imageId}
|
||||||
itemType={itemType}
|
itemType={itemType}
|
||||||
src={(data as { imageUrl: string })?.imageUrl}
|
src={(data as { imageUrl: string })?.imageUrl}
|
||||||
@@ -1010,6 +1020,7 @@ export const getDataRows = (type?: 'compact' | 'default' | 'poster'): DataRow[]
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
format: (data) => {
|
format: (data) => {
|
||||||
|
const explicitStatus = 'explicitStatus' in data ? data.explicitStatus : null;
|
||||||
if ('name' in data && data.name) {
|
if ('name' in data && data.name) {
|
||||||
if ('id' in data && data.id) {
|
if ('id' in data && data.id) {
|
||||||
if ('_itemType' in data) {
|
if ('_itemType' in data) {
|
||||||
@@ -1022,6 +1033,7 @@ export const getDataRows = (type?: 'compact' | 'default' | 'poster'): DataRow[]
|
|||||||
albumId: data.id,
|
albumId: data.id,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
<ExplicitIndicator explicitStatus={explicitStatus} />
|
||||||
{data.name}
|
{data.name}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
@@ -1036,6 +1048,7 @@ export const getDataRows = (type?: 'compact' | 'default' | 'poster'): DataRow[]
|
|||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<ExplicitIndicator explicitStatus={explicitStatus} />
|
||||||
{data.name}
|
{data.name}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
@@ -1062,11 +1075,21 @@ export const getDataRows = (type?: 'compact' | 'default' | 'poster'): DataRow[]
|
|||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return data.name;
|
return (
|
||||||
|
<>
|
||||||
|
<ExplicitIndicator explicitStatus={explicitStatus} />
|
||||||
|
{data.name}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return data.name;
|
return (
|
||||||
|
<>
|
||||||
|
<ExplicitIndicator explicitStatus={explicitStatus} />
|
||||||
|
{data.name}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
.container {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: 1fr;
|
|
||||||
grid-template-columns: auto minmax(0, 1fr);
|
|
||||||
gap: var(--theme-spacing-sm);
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: var(--theme-spacing-sm);
|
|
||||||
container-type: inline-size;
|
|
||||||
background: var(--theme-colors-surface);
|
|
||||||
border-radius: var(--theme-radius-md);
|
|
||||||
|
|
||||||
@container (min-width: 500px) {
|
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-container {
|
|
||||||
position: relative;
|
|
||||||
display: none;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
aspect-ratio: 1/1;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
content: '';
|
|
||||||
background-color: rgb(0 0 0);
|
|
||||||
opacity: 0;
|
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
&::before {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@container (min-width: 500px) {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image {
|
|
||||||
aspect-ratio: 1/1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metadata-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--theme-spacing-sm);
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: var(--theme-spacing-xs) 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metadata-container .header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metadata-container .header .title {
|
|
||||||
max-width: 70%;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metadata-container .content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--theme-spacing-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.metadata-container .content .tags {
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
// import { AnimatePresence } from 'motion/react';
|
|
||||||
// import { MouseEvent, useMemo, useState } from 'react';
|
|
||||||
// import { Link } from 'react-router';
|
|
||||||
|
|
||||||
// import styles from './item-detail.module.css';
|
|
||||||
|
|
||||||
// import { ItemCardControls } from '/@/renderer/components/item-card/item-card-controls';
|
|
||||||
// import { useFastAverageColor } from '/@/renderer/hooks';
|
|
||||||
// import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
|
||||||
// import { Badge } from '/@/shared/components/badge/badge';
|
|
||||||
// import { Divider } from '/@/shared/components/divider/divider';
|
|
||||||
// import { Group } from '/@/shared/components/group/group';
|
|
||||||
// import { Image } from '/@/shared/components/image/image';
|
|
||||||
// import { Rating } from '/@/shared/components/rating/rating';
|
|
||||||
// import { Text } from '/@/shared/components/text/text';
|
|
||||||
// import {
|
|
||||||
// Album,
|
|
||||||
// AlbumArtist,
|
|
||||||
// Artist,
|
|
||||||
// LibraryItem,
|
|
||||||
// Playlist,
|
|
||||||
// Song,
|
|
||||||
// } from '/@/shared/types/domain-types';
|
|
||||||
// import { stringToColor } from '/@/shared/utils/string-to-color';
|
|
||||||
|
|
||||||
// interface ItemDetailProps {
|
|
||||||
// data: Album | AlbumArtist | Artist | Playlist | Song | undefined;
|
|
||||||
// itemHeight: number;
|
|
||||||
// itemType: LibraryItem;
|
|
||||||
// onClick?: (e: MouseEvent<HTMLDivElement>, item: unknown, itemType: LibraryItem) => void;
|
|
||||||
// withControls?: boolean;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export const ItemDetail = ({ data, itemType, onClick, withControls }: ItemDetailProps) => {
|
|
||||||
// const imageUrl = getImageUrl(data);
|
|
||||||
|
|
||||||
// const [showControls, setShowControls] = useState(false);
|
|
||||||
|
|
||||||
// const { background } = useFastAverageColor({
|
|
||||||
// algorithm: 'simple',
|
|
||||||
// src: imageUrl,
|
|
||||||
// srcLoaded: false,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // const tags = [...(data?.genres ?? [])];
|
|
||||||
|
|
||||||
// const tags = useMemo(() => {
|
|
||||||
// if (!data) {
|
|
||||||
// return [];
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const items: {
|
|
||||||
// color?: string;
|
|
||||||
// id: string;
|
|
||||||
// isLight?: boolean;
|
|
||||||
// itemType: LibraryItem;
|
|
||||||
// name: string;
|
|
||||||
// }[] = [];
|
|
||||||
|
|
||||||
// if ('albumArtists' in data && Array.isArray(data.albumArtists)) {
|
|
||||||
// data.albumArtists?.forEach((tag: { id: string; name: string }) => {
|
|
||||||
// items.push({ id: tag.id, itemType: LibraryItem.ALBUM_ARTIST, name: tag.name });
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if ('genres' in data && Array.isArray(data.genres)) {
|
|
||||||
// data.genres?.forEach((tag: { id: string; itemType: LibraryItem; name: string }) => {
|
|
||||||
// const { color, isLight } = stringToColor(tag.name);
|
|
||||||
// items.push({ ...tag, color, isLight });
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // if ('tags' in data && typeof data.tags === 'object') {
|
|
||||||
// // console.log('data.tags :>> ', data.tags);
|
|
||||||
// // Object.entries(data.tags).forEach(([key, value]) => {
|
|
||||||
// // items.push({ id: key, itemType: LibraryItem.TAG, name: value });
|
|
||||||
// // });
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// return items;
|
|
||||||
// }, [data]);
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <div
|
|
||||||
// className={styles.container}
|
|
||||||
// onClick={(e) => onClick?.(e, data, itemType)}
|
|
||||||
// style={{ backgroundColor: background }}
|
|
||||||
// >
|
|
||||||
// <div
|
|
||||||
// className={styles.imageContainer}
|
|
||||||
// onMouseEnter={() => withControls && setShowControls(true)}
|
|
||||||
// onMouseLeave={() => withControls && setShowControls(false)}
|
|
||||||
// >
|
|
||||||
// <Image alt={data?.name} src={imageUrl} />
|
|
||||||
// <AnimatePresence>
|
|
||||||
// {withControls && showControls && <ItemCardControls type="compact" />}
|
|
||||||
// </AnimatePresence>
|
|
||||||
// </div>
|
|
||||||
// <div className={styles.metadataContainer}>
|
|
||||||
// <div className={styles.header}>
|
|
||||||
// <Text className={styles.title} component={Link} isLink size="lg" weight={500}>
|
|
||||||
// {data?.name}
|
|
||||||
// </Text>
|
|
||||||
// <Group>
|
|
||||||
// {data && 'userRating' in data && (
|
|
||||||
// <Rating size="xs" value={data?.userRating ?? 0} />
|
|
||||||
// )}
|
|
||||||
// {data && 'userFavorite' in data && (
|
|
||||||
// <ActionIcon
|
|
||||||
// icon="favorite"
|
|
||||||
// iconProps={{
|
|
||||||
// fill: data?.userFavorite ? 'primary' : 'default',
|
|
||||||
// }}
|
|
||||||
// size="xs"
|
|
||||||
// />
|
|
||||||
// )}
|
|
||||||
// </Group>
|
|
||||||
// </div>
|
|
||||||
// <Divider />
|
|
||||||
// <div className={styles.content}>
|
|
||||||
// <Group className={styles.tags} gap="xs">
|
|
||||||
// {tags.map((tag) => (
|
|
||||||
// <Badge
|
|
||||||
// key={tag.id}
|
|
||||||
// style={{
|
|
||||||
// backgroundColor: tag.color,
|
|
||||||
// color: tag.isLight ? 'black' : 'white',
|
|
||||||
// }}
|
|
||||||
// >
|
|
||||||
// {tag.name}
|
|
||||||
// </Badge>
|
|
||||||
// ))}
|
|
||||||
// </Group>
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// );
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const getImageUrl = (data: Album | AlbumArtist | Artist | Playlist | Song | undefined) => {
|
|
||||||
// if (data && 'imageUrl' in data) {
|
|
||||||
// return data.imageUrl || undefined;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return undefined;
|
|
||||||
// };
|
|
||||||
@@ -7,11 +7,12 @@ import {
|
|||||||
getServerById,
|
getServerById,
|
||||||
useAuthStore,
|
useAuthStore,
|
||||||
useCurrentServerId,
|
useCurrentServerId,
|
||||||
|
useGeneralSettings,
|
||||||
useImageRes,
|
useImageRes,
|
||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { BaseImage, ImageProps } from '/@/shared/components/image/image';
|
import { BaseImage, ImageProps } from '/@/shared/components/image/image';
|
||||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
import { ExplicitStatus, LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
const getUnloaderIcon = (itemType: LibraryItem) => {
|
const getUnloaderIcon = (itemType: LibraryItem) => {
|
||||||
switch (itemType) {
|
switch (itemType) {
|
||||||
@@ -34,6 +35,7 @@ const getUnloaderIcon = (itemType: LibraryItem) => {
|
|||||||
|
|
||||||
const BaseItemImage = (
|
const BaseItemImage = (
|
||||||
props: Omit<ImageProps, 'id' | 'src'> & {
|
props: Omit<ImageProps, 'id' | 'src'> & {
|
||||||
|
explicitStatus?: ExplicitStatus | null;
|
||||||
id?: null | string;
|
id?: null | string;
|
||||||
itemType: LibraryItem;
|
itemType: LibraryItem;
|
||||||
serverId?: null | string;
|
serverId?: null | string;
|
||||||
@@ -41,7 +43,8 @@ const BaseItemImage = (
|
|||||||
type?: keyof z.infer<typeof GeneralSettingsSchema>['imageRes'];
|
type?: keyof z.infer<typeof GeneralSettingsSchema>['imageRes'];
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const { serverId, src, ...rest } = props;
|
const { explicitStatus, serverId, src, ...rest } = props;
|
||||||
|
const { blurExplicitImages } = useGeneralSettings();
|
||||||
|
|
||||||
const imageUrl = useItemImageUrl({
|
const imageUrl = useItemImageUrl({
|
||||||
id: props.id,
|
id: props.id,
|
||||||
@@ -51,8 +54,11 @@ const BaseItemImage = (
|
|||||||
type: props.type,
|
type: props.type,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isExplicit = blurExplicitImages && explicitStatus === ExplicitStatus.EXPLICIT;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseImage
|
<BaseImage
|
||||||
|
isExplicit={isExplicit}
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
unloaderIcon={getUnloaderIcon(props.itemType)}
|
unloaderIcon={getUnloaderIcon(props.itemType)}
|
||||||
{...rest}
|
{...rest}
|
||||||
|
|||||||
@@ -192,9 +192,10 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
|
|||||||
onColumnReordered?.(columnIdFrom, columnIdTo, edge);
|
onColumnReordered?.(columnIdFrom, columnIdTo, edge);
|
||||||
},
|
},
|
||||||
|
|
||||||
onColumnResized: ({ columnId, width }: { columnId: TableColumn; width: number }) => {
|
onColumnResized: onColumnResized
|
||||||
onColumnResized?.(columnId, width);
|
? ({ columnId, width }: { columnId: TableColumn; width: number }) =>
|
||||||
},
|
onColumnResized(columnId, width)
|
||||||
|
: undefined,
|
||||||
|
|
||||||
onDoubleClick: ({ internalState, item, itemType, meta }: DefaultItemControlProps) => {
|
onDoubleClick: ({ internalState, item, itemType, meta }: DefaultItemControlProps) => {
|
||||||
if (!item || !internalState) {
|
if (!item || !internalState) {
|
||||||
@@ -241,11 +242,11 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
|
|||||||
}
|
}
|
||||||
|
|
||||||
const playType = (meta?.playType as Play) || Play.NOW;
|
const playType = (meta?.playType as Play) || Play.NOW;
|
||||||
|
const singleSongOnly = meta?.singleSongOnly === true;
|
||||||
|
|
||||||
// For NEXT, LAST, NEXT_SHUFFLE, and LAST_SHUFFLE, only add the clicked song
|
|
||||||
// For NOW and SHUFFLE, add a range of songs around the clicked song
|
|
||||||
let songsToAdd: Song[];
|
let songsToAdd: Song[];
|
||||||
if (
|
if (
|
||||||
|
singleSongOnly ||
|
||||||
playType === Play.NEXT ||
|
playType === Play.NEXT ||
|
||||||
playType === Play.LAST ||
|
playType === Play.LAST ||
|
||||||
playType === Play.NEXT_SHUFFLE ||
|
playType === Play.NEXT_SHUFFLE ||
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
useMutation,
|
||||||
useQuery,
|
useQuery,
|
||||||
useQueryClient,
|
useQueryClient,
|
||||||
useSuspenseQuery,
|
useSuspenseQuery,
|
||||||
@@ -11,6 +12,7 @@ import { queryKeys } from '/@/renderer/api/query-keys';
|
|||||||
import { useListContext } from '/@/renderer/context/list-context';
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
||||||
import { UserFavoriteEventPayload, UserRatingEventPayload } from '/@/renderer/events/events';
|
import { UserFavoriteEventPayload, UserRatingEventPayload } from '/@/renderer/events/events';
|
||||||
|
import { getListRefreshMutationKey } from '/@/renderer/features/shared/components/list-refresh-button';
|
||||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
export const getListQueryKeyName = (itemType: LibraryItem): string => {
|
export const getListQueryKeyName = (itemType: LibraryItem): string => {
|
||||||
@@ -293,10 +295,10 @@ export const useItemListInfiniteLoader = ({
|
|||||||
[onRangeChangedBase],
|
[onRangeChangedBase],
|
||||||
);
|
);
|
||||||
|
|
||||||
const refresh = useCallback(
|
const refreshMutation = useMutation({
|
||||||
async (force?: boolean) => {
|
mutationFn: async (force?: boolean) => {
|
||||||
// Invalidate all queries to ensure fresh data
|
// Invalidate all queries to ensure fresh data
|
||||||
await queryClient.invalidateQueries();
|
queryClient.invalidateQueries();
|
||||||
|
|
||||||
// Reset the infinite list data
|
// Reset the infinite list data
|
||||||
const currentData = queryClient.getQueryData<{
|
const currentData = queryClient.getQueryData<{
|
||||||
@@ -320,7 +322,7 @@ export const useItemListInfiniteLoader = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add a delay to make the refresh visually clear
|
// Add a delay to make the refresh visually clear
|
||||||
await new Promise((resolve) => setTimeout(resolve, 150));
|
// await new Promise((resolve) => setTimeout(resolve, 150));
|
||||||
|
|
||||||
// Determine which page to refetch based on current visible range
|
// Determine which page to refetch based on current visible range
|
||||||
let pageToFetch = 0;
|
let pageToFetch = 0;
|
||||||
@@ -344,7 +346,12 @@ export const useItemListInfiniteLoader = ({
|
|||||||
stopIndex,
|
stopIndex,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[queryClient, itemsPerPage, onRangeChangedBase, dataQueryKey, totalItemCount, fetchPage],
|
mutationKey: getListRefreshMutationKey(eventKey),
|
||||||
|
});
|
||||||
|
|
||||||
|
const refresh = useCallback(
|
||||||
|
async (force?: boolean) => refreshMutation.mutateAsync(force),
|
||||||
|
[refreshMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateItems = useCallback(
|
const updateItems = useCallback(
|
||||||
@@ -376,7 +383,7 @@ export const useItemListInfiniteLoader = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return refresh(true);
|
refreshMutation.mutate(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
eventEmitter.on('ITEM_LIST_REFRESH', handleRefresh);
|
eventEmitter.on('ITEM_LIST_REFRESH', handleRefresh);
|
||||||
@@ -384,7 +391,7 @@ export const useItemListInfiniteLoader = ({
|
|||||||
return () => {
|
return () => {
|
||||||
eventEmitter.off('ITEM_LIST_REFRESH', handleRefresh);
|
eventEmitter.off('ITEM_LIST_REFRESH', handleRefresh);
|
||||||
};
|
};
|
||||||
}, [eventKey, refresh]);
|
}, [eventKey, refreshMutation]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleFavorite = (payload: UserFavoriteEventPayload) => {
|
const handleFavorite = (payload: UserFavoriteEventPayload) => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
useMutation,
|
||||||
useQuery,
|
useQuery,
|
||||||
useQueryClient,
|
useQueryClient,
|
||||||
useSuspenseQuery,
|
useSuspenseQuery,
|
||||||
@@ -10,6 +11,7 @@ import { queryKeys } from '/@/renderer/api/query-keys';
|
|||||||
import { useListContext } from '/@/renderer/context/list-context';
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
||||||
import { UserFavoriteEventPayload, UserRatingEventPayload } from '/@/renderer/events/events';
|
import { UserFavoriteEventPayload, UserRatingEventPayload } from '/@/renderer/events/events';
|
||||||
|
import { getListRefreshMutationKey } from '/@/renderer/features/shared/components/list-refresh-button';
|
||||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
const getQueryKeyName = (itemType: LibraryItem): string => {
|
const getQueryKeyName = (itemType: LibraryItem): string => {
|
||||||
@@ -83,7 +85,7 @@ export const useItemListPaginatedLoader = ({
|
|||||||
[itemsPerPage, startIndex, query],
|
[itemsPerPage, startIndex, query],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data, refetch: queryRefetch } = useQuery({
|
const { data } = useQuery({
|
||||||
gcTime: 1000 * 15,
|
gcTime: 1000 * 15,
|
||||||
placeholderData: { items: getInitialData(itemsPerPage) },
|
placeholderData: { items: getInitialData(itemsPerPage) },
|
||||||
queryFn: async ({ signal }) => {
|
queryFn: async ({ signal }) => {
|
||||||
@@ -98,22 +100,20 @@ export const useItemListPaginatedLoader = ({
|
|||||||
staleTime: 1000 * 15,
|
staleTime: 1000 * 15,
|
||||||
});
|
});
|
||||||
|
|
||||||
const refresh = useCallback(
|
const refreshMutation = useMutation({
|
||||||
async (force?: boolean) => {
|
mutationFn: async (force?: boolean) => {
|
||||||
const queryKey = queryKeys[getQueryKeyName(itemType)].list(serverId, queryParams);
|
const queryKey = queryKeys[getQueryKeyName(itemType)].list(serverId, queryParams);
|
||||||
|
|
||||||
await queryClient.invalidateQueries();
|
|
||||||
|
|
||||||
if (force) {
|
if (force) {
|
||||||
queryClient.setQueryData(queryKey, {
|
queryClient.setQueryData(queryKey, {
|
||||||
items: getInitialData(itemsPerPage),
|
items: getInitialData(itemsPerPage),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return queryRefetch();
|
await queryClient.invalidateQueries();
|
||||||
},
|
},
|
||||||
[queryClient, queryRefetch, queryParams, serverId, itemType, itemsPerPage],
|
mutationKey: getListRefreshMutationKey(eventKey ?? 'paginated'),
|
||||||
);
|
});
|
||||||
|
|
||||||
const updateItems = useCallback(
|
const updateItems = useCallback(
|
||||||
(indexes: number[], value: object) => {
|
(indexes: number[], value: object) => {
|
||||||
@@ -153,7 +153,7 @@ export const useItemListPaginatedLoader = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return refresh(true);
|
refreshMutation.mutate(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFavorite = (payload: UserFavoriteEventPayload) => {
|
const handleFavorite = (payload: UserFavoriteEventPayload) => {
|
||||||
@@ -220,7 +220,7 @@ export const useItemListPaginatedLoader = ({
|
|||||||
eventEmitter.off('USER_FAVORITE', handleFavorite);
|
eventEmitter.off('USER_FAVORITE', handleFavorite);
|
||||||
eventEmitter.off('USER_RATING', handleRating);
|
eventEmitter.off('USER_RATING', handleRating);
|
||||||
};
|
};
|
||||||
}, [data, eventKey, itemType, serverId, refresh, updateItems]);
|
}, [data, eventKey, itemType, refreshMutation, serverId, updateItems]);
|
||||||
|
|
||||||
return { data: data?.items || [], pageCount, totalItemCount };
|
return { data: data?.items || [], pageCount, totalItemCount };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ export interface ItemListStateItem {
|
|||||||
_itemType: LibraryItem;
|
_itemType: LibraryItem;
|
||||||
_serverId: string;
|
_serverId: string;
|
||||||
id: string;
|
id: string;
|
||||||
|
imageId: null | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ItemListStateItemWithRequiredProperties = Record<string, unknown> & {
|
export type ItemListStateItemWithRequiredProperties = Record<string, unknown> & {
|
||||||
|
|||||||
@@ -7,14 +7,19 @@ import { ItemListKey, TableColumn } from '/@/shared/types/types';
|
|||||||
|
|
||||||
interface UseItemListColumnReorderProps {
|
interface UseItemListColumnReorderProps {
|
||||||
itemListKey: ItemListKey;
|
itemListKey: ItemListKey;
|
||||||
|
tableKey?: 'detail' | 'main';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useItemListColumnReorder = ({ itemListKey }: UseItemListColumnReorderProps) => {
|
export const useItemListColumnReorder = ({
|
||||||
|
itemListKey,
|
||||||
|
tableKey = 'main',
|
||||||
|
}: UseItemListColumnReorderProps) => {
|
||||||
const { setList } = useSettingsStoreActions();
|
const { setList } = useSettingsStoreActions();
|
||||||
|
|
||||||
const handleColumnReordered = useCallback(
|
const handleColumnReordered = useCallback(
|
||||||
(columnIdFrom: TableColumn, columnIdTo: TableColumn, edge: Edge | null) => {
|
(columnIdFrom: TableColumn, columnIdTo: TableColumn, edge: Edge | null) => {
|
||||||
const columns = useSettingsStore.getState().lists[itemListKey]?.table.columns;
|
const list = useSettingsStore.getState().lists[itemListKey];
|
||||||
|
const columns = tableKey === 'detail' ? list?.detail?.columns : list?.table?.columns;
|
||||||
|
|
||||||
if (!columns) {
|
if (!columns) {
|
||||||
return;
|
return;
|
||||||
@@ -83,13 +88,20 @@ export const useItemListColumnReorder = ({ itemListKey }: UseItemListColumnReord
|
|||||||
// Insert the column at the new position
|
// Insert the column at the new position
|
||||||
newColumns.splice(newIndex, 0, updatedMovedColumn);
|
newColumns.splice(newIndex, 0, updatedMovedColumn);
|
||||||
|
|
||||||
setList(itemListKey, {
|
if (tableKey === 'detail') {
|
||||||
table: {
|
type SetListData = Parameters<
|
||||||
columns: newColumns,
|
ReturnType<typeof useSettingsStoreActions>['setList']
|
||||||
},
|
>[1];
|
||||||
});
|
setList(itemListKey, { detail: { columns: newColumns } } as SetListData);
|
||||||
|
} else {
|
||||||
|
setList(itemListKey, {
|
||||||
|
table: {
|
||||||
|
columns: newColumns,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[itemListKey, setList],
|
[itemListKey, setList, tableKey],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { handleColumnReordered };
|
return { handleColumnReordered };
|
||||||
|
|||||||
@@ -5,11 +5,18 @@ import { ItemListKey, TableColumn } from '/@/shared/types/types';
|
|||||||
|
|
||||||
interface UseItemListColumnResizeProps {
|
interface UseItemListColumnResizeProps {
|
||||||
itemListKey: ItemListKey;
|
itemListKey: ItemListKey;
|
||||||
|
tableKey?: 'detail' | 'main';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useItemListColumnResize = ({ itemListKey }: UseItemListColumnResizeProps) => {
|
export const useItemListColumnResize = ({
|
||||||
|
itemListKey,
|
||||||
|
tableKey = 'main',
|
||||||
|
}: UseItemListColumnResizeProps) => {
|
||||||
const { setList } = useSettingsStoreActions();
|
const { setList } = useSettingsStoreActions();
|
||||||
const columns = useSettingsStore((state) => state.lists[itemListKey]?.table.columns);
|
const columns = useSettingsStore((state) => {
|
||||||
|
const list = state.lists[itemListKey];
|
||||||
|
return tableKey === 'detail' ? list?.detail?.columns : list?.table?.columns;
|
||||||
|
});
|
||||||
|
|
||||||
const handleColumnResized = useCallback(
|
const handleColumnResized = useCallback(
|
||||||
(columnId: TableColumn, width: number) => {
|
(columnId: TableColumn, width: number) => {
|
||||||
@@ -19,13 +26,20 @@ export const useItemListColumnResize = ({ itemListKey }: UseItemListColumnResize
|
|||||||
column.id === columnId ? { ...column, width } : column,
|
column.id === columnId ? { ...column, width } : column,
|
||||||
);
|
);
|
||||||
|
|
||||||
setList(itemListKey, {
|
if (tableKey === 'detail') {
|
||||||
table: {
|
type SetListData = Parameters<
|
||||||
columns: updatedColumns,
|
ReturnType<typeof useSettingsStoreActions>['setList']
|
||||||
},
|
>[1];
|
||||||
});
|
setList(itemListKey, { detail: { columns: updatedColumns } } as SetListData);
|
||||||
|
} else {
|
||||||
|
setList(itemListKey, {
|
||||||
|
table: {
|
||||||
|
columns: updatedColumns,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[columns, itemListKey, setList],
|
[columns, itemListKey, setList, tableKey],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { handleColumnResized };
|
return { handleColumnResized };
|
||||||
|
|||||||
@@ -1,26 +1,29 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useSearchParams } from 'react-router';
|
import { useLocation, useNavigationType } from 'react-router';
|
||||||
|
|
||||||
import { parseIntParam, setSearchParam } from '/@/renderer/utils/query-params';
|
import { useScrollStore } from '/@/renderer/store/scroll.store';
|
||||||
|
|
||||||
interface UseItemListScrollPersistProps {
|
interface UseItemListScrollPersistProps {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useItemListScrollPersist = ({ enabled }: UseItemListScrollPersistProps) => {
|
export const useItemListScrollPersist = ({ enabled }: UseItemListScrollPersistProps) => {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const location = useLocation();
|
||||||
|
const navigationType = useNavigationType();
|
||||||
|
const setOffset = useScrollStore((s) => s.setOffset);
|
||||||
|
const getOffset = useScrollStore((s) => s.getOffset);
|
||||||
|
|
||||||
const scrollOffset = useMemo(() => parseIntParam(searchParams, 'scrollOffset'), [searchParams]);
|
const scrollOffset = useMemo(() => {
|
||||||
|
if (navigationType !== 'POP') return undefined;
|
||||||
|
return getOffset(location.key);
|
||||||
|
}, [getOffset, location.key, navigationType]);
|
||||||
|
|
||||||
const handleOnScrollEnd = useCallback(
|
const handleOnScrollEnd = useCallback(
|
||||||
(offset: number) => {
|
(offset: number) => {
|
||||||
if (!enabled) return;
|
if (!enabled) return;
|
||||||
|
setOffset(location.key, offset);
|
||||||
setSearchParams((prev) => setSearchParam(prev, 'scrollOffset', offset), {
|
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[enabled, setSearchParams],
|
[enabled, location.key, setOffset],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { handleOnScrollEnd, scrollOffset };
|
return { handleOnScrollEnd, scrollOffset };
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
export const ActionsColumn = ({ controls, internalState, song }: ItemDetailListCellProps) => {
|
||||||
|
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
const index = internalState?.findItemIndex(song.id) ?? -1;
|
||||||
|
controls?.onMore?.({
|
||||||
|
event,
|
||||||
|
index,
|
||||||
|
internalState: internalState ?? undefined,
|
||||||
|
item: song,
|
||||||
|
itemType: LibraryItem.SONG,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDoubleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionIcon
|
||||||
|
icon="ellipsisHorizontal"
|
||||||
|
iconProps={{
|
||||||
|
color: 'muted',
|
||||||
|
size: 'xs',
|
||||||
|
}}
|
||||||
|
onClick={handleClick}
|
||||||
|
onDoubleClick={handleDoubleClick}
|
||||||
|
size="xs"
|
||||||
|
variant="subtle"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
JOINED_ARTISTS_MUTED_PROPS,
|
||||||
|
JoinedArtists,
|
||||||
|
} from '/@/renderer/features/albums/components/joined-artists';
|
||||||
|
|
||||||
|
export const AlbumArtistColumn = ({ isRowHovered, song }: ItemDetailListCellProps) => {
|
||||||
|
const name = song.albumArtistName?.trim() ?? '';
|
||||||
|
const hasArtists = name.length > 0 || (song.albumArtists?.length ?? 0) > 0;
|
||||||
|
|
||||||
|
if (!hasArtists) return <> </>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<JoinedArtists
|
||||||
|
artistName={song.albumArtistName ?? ''}
|
||||||
|
artists={song.albumArtists ?? []}
|
||||||
|
linkProps={JOINED_ARTISTS_MUTED_PROPS.linkProps}
|
||||||
|
readOnly={!isRowHovered}
|
||||||
|
rootTextProps={JOINED_ARTISTS_MUTED_PROPS.rootTextProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
|
export const AlbumColumn = ({ song }: ItemDetailListCellProps) => song.album ?? <> </>;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
JOINED_ARTISTS_MUTED_PROPS,
|
||||||
|
JoinedArtists,
|
||||||
|
} from '/@/renderer/features/albums/components/joined-artists';
|
||||||
|
|
||||||
|
export const ArtistColumn = ({ isRowHovered, song }: ItemDetailListCellProps) => {
|
||||||
|
const name = song.artistName?.trim() ?? '';
|
||||||
|
const hasArtists = name.length > 0 || (song.artists?.length ?? 0) > 0;
|
||||||
|
|
||||||
|
if (!hasArtists) return <> </>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<JoinedArtists
|
||||||
|
artistName={song.artistName ?? ''}
|
||||||
|
artists={song.artists ?? []}
|
||||||
|
linkProps={JOINED_ARTISTS_MUTED_PROPS.linkProps}
|
||||||
|
readOnly={!isRowHovered}
|
||||||
|
rootTextProps={JOINED_ARTISTS_MUTED_PROPS.rootTextProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
|
export const BitDepthColumn = ({ song }: ItemDetailListCellProps) => song.bitDepth;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
|
export const BitRateColumn = ({ song }: ItemDetailListCellProps) =>
|
||||||
|
song.bitRate != null ? `${song.bitRate} kbps` : <> </>;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
|
export const BpmColumn = ({ song }: ItemDetailListCellProps) => song.bpm ?? <> </>;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
|
export const ChannelsColumn = ({ song }: ItemDetailListCellProps) =>
|
||||||
|
song.channels != null ? String(song.channels) : <> </>;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
|
export const CodecColumn = ({ song }: ItemDetailListCellProps) => song.container ?? <> </>;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
|
export const CommentColumn = ({ song }: ItemDetailListCellProps) => song.comment ?? <> </>;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
|
export const ComposerColumn = ({ song }: ItemDetailListCellProps) => {
|
||||||
|
const composers = song.participants?.composer;
|
||||||
|
if (!composers?.length) return <> </>;
|
||||||
|
return composers.map((a) => a.name).join(', ');
|
||||||
|
};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
|
import { formatDateAbsolute } from '/@/renderer/utils/format';
|
||||||
|
|
||||||
|
export const DateAddedColumn = ({ song }: ItemDetailListCellProps) =>
|
||||||
|
song.createdAt ? formatDateAbsolute(song.createdAt) : <> </>;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
|
interface DefaultColumnProps extends ItemDetailListCellProps {
|
||||||
|
columnId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DefaultColumn = ({ columnId, song }: DefaultColumnProps) => {
|
||||||
|
const raw = (song as Record<string, unknown>)[columnId];
|
||||||
|
if (raw === undefined || raw === null || typeof raw === 'object') return <> </>;
|
||||||
|
return String(raw);
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
|
export const DiscNumberColumn = ({ song }: ItemDetailListCellProps) => String(song.discNumber ?? 1);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import formatDuration from 'format-duration';
|
||||||
|
|
||||||
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
|
export const DurationColumn = ({ song }: ItemDetailListCellProps) => formatDuration(song.duration);
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
|
import { useIsMutatingCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
|
||||||
|
import { useIsMutatingDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
|
||||||
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
export const FavoriteColumn = ({
|
||||||
|
controls,
|
||||||
|
internalState,
|
||||||
|
isMutatingFavorite,
|
||||||
|
onFavoriteClick,
|
||||||
|
song,
|
||||||
|
}: ItemDetailListCellProps) => {
|
||||||
|
const isMutatingCreateFavorite = useIsMutatingCreateFavorite();
|
||||||
|
const isMutatingDeleteFavorite = useIsMutatingDeleteFavorite();
|
||||||
|
const isMutating = isMutatingFavorite ?? (isMutatingCreateFavorite || isMutatingDeleteFavorite);
|
||||||
|
const isFavorite = song.userFavorite ?? false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionIcon
|
||||||
|
disabled={isMutating}
|
||||||
|
icon="favorite"
|
||||||
|
iconProps={{
|
||||||
|
color: isFavorite ? 'primary' : 'muted',
|
||||||
|
fill: isFavorite ? 'primary' : undefined,
|
||||||
|
size: 'xs',
|
||||||
|
}}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
const index = internalState?.findItemIndex(song.id) ?? -1;
|
||||||
|
if (controls?.onFavorite) {
|
||||||
|
controls.onFavorite({
|
||||||
|
event,
|
||||||
|
favorite: !isFavorite,
|
||||||
|
index,
|
||||||
|
internalState: internalState ?? undefined,
|
||||||
|
item: song,
|
||||||
|
itemType: LibraryItem.SONG,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
onFavoriteClick?.(song);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onDoubleClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
}}
|
||||||
|
size="xs"
|
||||||
|
variant="subtle"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
.group {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: var(--theme-spacing-sm) var(--theme-spacing-xs);
|
||||||
|
min-width: 0;
|
||||||
|
padding: var(--theme-spacing-xs) 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group a {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { generatePath, Link } from 'react-router';
|
||||||
|
|
||||||
|
import styles from './genre-badge-column.module.css';
|
||||||
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
|
import { Badge } from '/@/shared/components/badge/badge';
|
||||||
|
import { Group } from '/@/shared/components/group/group';
|
||||||
|
import { stringToColor } from '/@/shared/utils/string-to-color';
|
||||||
|
|
||||||
|
const MAX_GENRES = 4;
|
||||||
|
|
||||||
|
export const GenreBadgeColumn = ({ song }: ItemDetailListCellProps) => {
|
||||||
|
const genres = song.genres;
|
||||||
|
|
||||||
|
const genresWithStyle = useMemo(() => {
|
||||||
|
if (!genres) return [];
|
||||||
|
return genres.slice(0, MAX_GENRES).map((genre) => {
|
||||||
|
const { color, isLight } = stringToColor(genre.name);
|
||||||
|
const path = generatePath(AppRoute.LIBRARY_GENRES_DETAIL, { genreId: genre.id });
|
||||||
|
return { ...genre, color, isLight, path };
|
||||||
|
});
|
||||||
|
}, [genres]);
|
||||||
|
|
||||||
|
if (!genresWithStyle.length) return <> </>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group className={styles.group} wrap="nowrap">
|
||||||
|
{genresWithStyle.map((genre) => (
|
||||||
|
<Badge
|
||||||
|
component={Link}
|
||||||
|
key={genre.id}
|
||||||
|
state={{ item: genre }}
|
||||||
|
style={{
|
||||||
|
backgroundColor: genre.color,
|
||||||
|
color: genre.isLight ? 'black' : 'white',
|
||||||
|
}}
|
||||||
|
to={genre.path}
|
||||||
|
>
|
||||||
|
{genre.name}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Fragment } from 'react';
|
||||||
|
import { generatePath, Link } from 'react-router';
|
||||||
|
|
||||||
|
import { ItemDetailListCellProps } from '/@/renderer/components/item-list/item-detail-list/columns/types';
|
||||||
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
|
import { Text } from '/@/shared/components/text/text';
|
||||||
|
|
||||||
|
const TEXT_PROPS = { isMuted: true, isNoSelect: true, size: 'sm' as const } as const;
|
||||||
|
|
||||||
|
export const GenreColumn = ({ isRowHovered, song }: ItemDetailListCellProps) => {
|
||||||
|
const genres = song.genres ?? [];
|
||||||
|
if (!genres.length) return <> </>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{genres.map((genre, index) => (
|
||||||
|
<Fragment key={genre.id}>
|
||||||
|
{isRowHovered ? (
|
||||||
|
<Text
|
||||||
|
component={Link}
|
||||||
|
isLink
|
||||||
|
state={{ item: genre }}
|
||||||
|
to={generatePath(AppRoute.LIBRARY_GENRES_DETAIL, {
|
||||||
|
genreId: genre.id,
|
||||||
|
})}
|
||||||
|
{...TEXT_PROPS}
|
||||||
|
>
|
||||||
|
{genre.name}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text component="span" {...TEXT_PROPS}>
|
||||||
|
{genre.name}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{index < genres.length - 1 && ', '}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
.image-container {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-container {
|
||||||
|
flex: 1 1 0;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: 100%;
|
||||||
|
aspect-ratio: unset;
|
||||||
|
padding-top: var(--theme-spacing-xs);
|
||||||
|
padding-bottom: var(--theme-spacing-xs);
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--theme-radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-button-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
z-index: 10;
|
||||||
|
opacity: 0.6;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-button-overlay:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-button-overlay button {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-image {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: center;
|
||||||
|
border-radius: var(--theme-radius-md);
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import styles from './image-column.module.css';
|
||||||
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
|
import { ItemImage } from '/@/renderer/components/item-image/item-image';
|
||||||
|
import { PlayButton } from '/@/renderer/features/shared/components/play-button';
|
||||||
|
import {
|
||||||
|
LONG_PRESS_PLAY_BEHAVIOR,
|
||||||
|
PlayTooltip,
|
||||||
|
} from '/@/renderer/features/shared/components/play-button-group';
|
||||||
|
import { usePlayButtonBehavior } from '/@/renderer/store';
|
||||||
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
import { Play } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
export const ImageColumn = ({
|
||||||
|
controls,
|
||||||
|
internalState,
|
||||||
|
rowIndex = 0,
|
||||||
|
song,
|
||||||
|
}: ItemDetailListCellProps) => {
|
||||||
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
|
const handlePlay = (playType: Play) => {
|
||||||
|
if (!song || !controls?.onDoubleClick) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
controls.onDoubleClick({
|
||||||
|
event: null,
|
||||||
|
index: rowIndex,
|
||||||
|
internalState,
|
||||||
|
item: song,
|
||||||
|
itemType: LibraryItem.SONG,
|
||||||
|
meta: { playType, singleSongOnly: true },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.imageContainer}
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
>
|
||||||
|
<ItemImage
|
||||||
|
className={styles.compactImage}
|
||||||
|
containerClassName={styles.compactContainer}
|
||||||
|
explicitStatus={song.explicitStatus}
|
||||||
|
id={song.imageId}
|
||||||
|
itemType={LibraryItem.SONG}
|
||||||
|
serverId={song._serverId}
|
||||||
|
type="table"
|
||||||
|
/>
|
||||||
|
{isHovered && (
|
||||||
|
<div className={clsx(styles.playButtonOverlay)}>
|
||||||
|
<PlayTooltip disabled={false} type={playButtonBehavior}>
|
||||||
|
<PlayButton
|
||||||
|
fill
|
||||||
|
onClick={() => handlePlay(playButtonBehavior)}
|
||||||
|
onLongPress={() =>
|
||||||
|
handlePlay(LONG_PRESS_PLAY_BEHAVIOR[playButtonBehavior])
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PlayTooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import React, { type ReactNode } from 'react';
|
||||||
|
|
||||||
|
import type { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
|
import { ActionsColumn } from './actions-column';
|
||||||
|
import { AlbumArtistColumn } from './album-artist-column';
|
||||||
|
import { AlbumColumn } from './album-column';
|
||||||
|
import { ArtistColumn } from './artist-column';
|
||||||
|
import { BitDepthColumn } from './bit-depth-column';
|
||||||
|
import { BitRateColumn } from './bit-rate-column';
|
||||||
|
import { BpmColumn } from './bpm-column';
|
||||||
|
import { ChannelsColumn } from './channels-column';
|
||||||
|
import { CodecColumn } from './codec-column';
|
||||||
|
import { CommentColumn } from './comment-column';
|
||||||
|
import { ComposerColumn } from './composer-column';
|
||||||
|
import { DateAddedColumn } from './date-added-column';
|
||||||
|
import { DefaultColumn } from './default-column';
|
||||||
|
import { DiscNumberColumn } from './disc-number-column';
|
||||||
|
import { DurationColumn } from './duration-column';
|
||||||
|
import { FavoriteColumn } from './favorite-column';
|
||||||
|
import { GenreBadgeColumn } from './genre-badge-column';
|
||||||
|
import { GenreColumn } from './genre-column';
|
||||||
|
import { ImageColumn } from './image-column';
|
||||||
|
import { LastPlayedColumn } from './last-played-column';
|
||||||
|
import { PathColumn } from './path-column';
|
||||||
|
import { PlayCountColumn } from './play-count-column';
|
||||||
|
import { RatingColumn } from './rating-column';
|
||||||
|
import { ReleaseDateColumn } from './release-date-column';
|
||||||
|
import { RowIndexColumn } from './row-index-column';
|
||||||
|
import { SampleRateColumn } from './sample-rate-column';
|
||||||
|
import { SizeColumn } from './size-column';
|
||||||
|
import { TitleArtistColumn } from './title-artist-column';
|
||||||
|
import { TitleColumn } from './title-column';
|
||||||
|
import { TitleCombinedColumn } from './title-combined-column';
|
||||||
|
import { TrackNumberColumn } from './track-number-column';
|
||||||
|
import { YearColumn } from './year-column';
|
||||||
|
|
||||||
|
import { TableColumn } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
type CellComponent = (props: ItemDetailListCellProps) => ReactNode;
|
||||||
|
|
||||||
|
const COLUMN_MAP: Partial<Record<TableColumn, CellComponent>> = {
|
||||||
|
[TableColumn.ACTIONS]: ActionsColumn,
|
||||||
|
[TableColumn.ALBUM]: AlbumColumn,
|
||||||
|
[TableColumn.ALBUM_ARTIST]: AlbumArtistColumn,
|
||||||
|
[TableColumn.ARTIST]: ArtistColumn,
|
||||||
|
[TableColumn.BIT_DEPTH]: BitDepthColumn,
|
||||||
|
[TableColumn.BIT_RATE]: BitRateColumn,
|
||||||
|
[TableColumn.BPM]: BpmColumn,
|
||||||
|
[TableColumn.CHANNELS]: ChannelsColumn,
|
||||||
|
[TableColumn.CODEC]: CodecColumn,
|
||||||
|
[TableColumn.COMMENT]: CommentColumn,
|
||||||
|
[TableColumn.COMPOSER]: ComposerColumn,
|
||||||
|
[TableColumn.DATE_ADDED]: DateAddedColumn,
|
||||||
|
[TableColumn.DISC_NUMBER]: DiscNumberColumn,
|
||||||
|
[TableColumn.DURATION]: DurationColumn,
|
||||||
|
[TableColumn.GENRE]: GenreColumn,
|
||||||
|
[TableColumn.GENRE_BADGE]: GenreBadgeColumn,
|
||||||
|
[TableColumn.IMAGE]: ImageColumn,
|
||||||
|
[TableColumn.LAST_PLAYED]: LastPlayedColumn,
|
||||||
|
[TableColumn.PATH]: PathColumn,
|
||||||
|
[TableColumn.PLAY_COUNT]: PlayCountColumn,
|
||||||
|
[TableColumn.RELEASE_DATE]: ReleaseDateColumn,
|
||||||
|
[TableColumn.ROW_INDEX]: RowIndexColumn,
|
||||||
|
[TableColumn.SAMPLE_RATE]: SampleRateColumn,
|
||||||
|
[TableColumn.SIZE]: SizeColumn,
|
||||||
|
[TableColumn.TITLE]: TitleColumn,
|
||||||
|
[TableColumn.TITLE_ARTIST]: TitleArtistColumn,
|
||||||
|
[TableColumn.TITLE_COMBINED]: TitleCombinedColumn,
|
||||||
|
[TableColumn.TRACK_NUMBER]: TrackNumberColumn,
|
||||||
|
[TableColumn.USER_FAVORITE]: FavoriteColumn,
|
||||||
|
[TableColumn.USER_RATING]: RatingColumn,
|
||||||
|
[TableColumn.YEAR]: YearColumn,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DetailListCellComponentProps = ItemDetailListCellProps & { columnId?: string };
|
||||||
|
|
||||||
|
export function getDetailListCellComponent(
|
||||||
|
columnId: string | TableColumn,
|
||||||
|
): (props: DetailListCellComponentProps) => ReactNode {
|
||||||
|
const Component = COLUMN_MAP[columnId as TableColumn];
|
||||||
|
if (Component) {
|
||||||
|
return Component as (props: DetailListCellComponentProps) => ReactNode;
|
||||||
|
}
|
||||||
|
return (props: DetailListCellComponentProps) =>
|
||||||
|
React.createElement(DefaultColumn, {
|
||||||
|
columnId: props.columnId ?? (columnId as string),
|
||||||
|
song: props.song,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
|
export {
|
||||||
|
ActionsColumn,
|
||||||
|
AlbumArtistColumn,
|
||||||
|
AlbumColumn,
|
||||||
|
ArtistColumn,
|
||||||
|
BitDepthColumn,
|
||||||
|
BitRateColumn,
|
||||||
|
BpmColumn,
|
||||||
|
ChannelsColumn,
|
||||||
|
CodecColumn,
|
||||||
|
CommentColumn,
|
||||||
|
ComposerColumn,
|
||||||
|
DateAddedColumn,
|
||||||
|
DefaultColumn,
|
||||||
|
DiscNumberColumn,
|
||||||
|
DurationColumn,
|
||||||
|
FavoriteColumn,
|
||||||
|
GenreBadgeColumn,
|
||||||
|
GenreColumn,
|
||||||
|
ImageColumn,
|
||||||
|
LastPlayedColumn,
|
||||||
|
PathColumn,
|
||||||
|
PlayCountColumn,
|
||||||
|
RatingColumn,
|
||||||
|
ReleaseDateColumn,
|
||||||
|
RowIndexColumn,
|
||||||
|
SampleRateColumn,
|
||||||
|
SizeColumn,
|
||||||
|
TitleArtistColumn,
|
||||||
|
TitleColumn,
|
||||||
|
TitleCombinedColumn,
|
||||||
|
TrackNumberColumn,
|
||||||
|
YearColumn,
|
||||||
|
};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
|
import { formatDateRelative } from '/@/renderer/utils/format';
|
||||||
|
|
||||||
|
export const LastPlayedColumn = ({ song }: ItemDetailListCellProps) =>
|
||||||
|
song.lastPlayedAt ? formatDateRelative(song.lastPlayedAt) : <> </>;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
|
export const PathColumn = ({ song }: ItemDetailListCellProps) => song.path ?? <> </>;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
|
export const PlayCountColumn = ({ song }: ItemDetailListCellProps) =>
|
||||||
|
song.playCount ? String(song.playCount) : <> </>;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
|
import { useIsMutatingRating } from '/@/renderer/features/shared/mutations/set-rating-mutation';
|
||||||
|
import { Rating } from '/@/shared/components/rating/rating';
|
||||||
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
export const RatingColumn = ({ controls, internalState, song }: ItemDetailListCellProps) => {
|
||||||
|
const isMutatingRating = useIsMutatingRating();
|
||||||
|
const value = song.userRating ?? 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Rating
|
||||||
|
onChange={(rating) => {
|
||||||
|
const index = internalState?.findItemIndex(song.id) ?? -1;
|
||||||
|
controls?.onRating?.({
|
||||||
|
event: null,
|
||||||
|
index,
|
||||||
|
internalState: internalState ?? undefined,
|
||||||
|
item: song,
|
||||||
|
itemType: LibraryItem.SONG,
|
||||||
|
rating,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
readOnly={isMutatingRating}
|
||||||
|
size="xs"
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
|
import { formatDateAbsoluteUTC } from '/@/renderer/utils/format';
|
||||||
|
|
||||||
|
export const ReleaseDateColumn = ({ song }: ItemDetailListCellProps) =>
|
||||||
|
song.releaseDate ? formatDateAbsoluteUTC(song.releaseDate) : <> </>;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.icon-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import styles from './row-index-column.module.css';
|
||||||
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
|
import { useIsCurrentSong } from '/@/renderer/features/player/hooks/use-is-current-song';
|
||||||
|
import { usePlayerStatus } from '/@/renderer/store';
|
||||||
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
|
import { PlayerStatus } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
export const RowIndexColumn = ({ rowIndex, song }: ItemDetailListCellProps) => {
|
||||||
|
const status = usePlayerStatus();
|
||||||
|
const { isActive } = useIsCurrentSong(song);
|
||||||
|
const isPlaying = isActive && status === PlayerStatus.PLAYING;
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
return (
|
||||||
|
<div className={styles.iconWrapper}>
|
||||||
|
<Icon fill="primary" icon={isPlaying ? 'mediaPlay' : 'mediaPause'} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{String((rowIndex ?? 0) + 1)}</>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
|
export const SampleRateColumn = ({ song }: ItemDetailListCellProps) =>
|
||||||
|
song.sampleRate ? `${song.sampleRate} Hz` : <> </>;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user