Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f02307ff2a | |||
| 2647c36326 | |||
| 16a9d6e702 | |||
| 0a4d789f08 | |||
| 7f5742119b | |||
| 04d8e013e1 | |||
| 022b83ab32 | |||
| 551d705ee1 | |||
| 83f73c7fa9 | |||
| cc8cb4f4f1 | |||
| 496eab7d09 | |||
| 5197c967c2 | |||
| 74b615dba7 | |||
| b67ee797cb | |||
| 4228084810 | |||
| b514c7972d | |||
| 83d9042a47 | |||
| a28c403093 | |||
| 2927fa5ff7 | |||
| f39a7f8d6f |
@@ -155,17 +155,6 @@ jobs:
|
|||||||
pnpm run publish:win:alpha
|
pnpm run publish:win:alpha
|
||||||
on_retry_command: pnpm cache delete
|
on_retry_command: pnpm cache delete
|
||||||
|
|
||||||
- name: Build and Publish to R2 (Windows ARM64)
|
|
||||||
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-arm64:alpha
|
|
||||||
on_retry_command: pnpm cache delete
|
|
||||||
|
|
||||||
- name: Build and Publish to R2 (macOS)
|
- name: Build and Publish to R2 (macOS)
|
||||||
if: matrix.os == 'macos-latest'
|
if: matrix.os == 'macos-latest'
|
||||||
uses: nick-invision/retry@v2.8.2
|
uses: nick-invision/retry@v2.8.2
|
||||||
|
|||||||
@@ -155,19 +155,6 @@ jobs:
|
|||||||
pnpm run publish:win:beta
|
pnpm run publish:win:beta
|
||||||
on_retry_command: pnpm cache delete
|
on_retry_command: pnpm cache delete
|
||||||
|
|
||||||
- name: Build and Publish releases (Windows ARM64)
|
|
||||||
if: matrix.os == 'windows-latest'
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
uses: nick-invision/retry@v2.8.2
|
|
||||||
with:
|
|
||||||
timeout_minutes: 30
|
|
||||||
max_attempts: 3
|
|
||||||
retry_on: error
|
|
||||||
command: |
|
|
||||||
pnpm run publish:win-arm64:beta
|
|
||||||
on_retry_command: pnpm cache delete
|
|
||||||
|
|
||||||
- name: Build and Publish releases (macOS)
|
- name: Build and Publish releases (macOS)
|
||||||
if: matrix.os == 'macos-latest'
|
if: matrix.os == 'macos-latest'
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -50,16 +50,6 @@ jobs:
|
|||||||
command: |
|
command: |
|
||||||
pnpm run package:win:pr
|
pnpm run package:win:pr
|
||||||
|
|
||||||
- name: Build for Windows (ARM64)
|
|
||||||
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 package:win-arm64:pr
|
|
||||||
|
|
||||||
- name: Build for Linux
|
- name: Build for Linux
|
||||||
if: ${{ matrix.os == 'ubuntu-latest' }}
|
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||||
uses: nick-invision/retry@v2.8.2
|
uses: nick-invision/retry@v2.8.2
|
||||||
|
|||||||
@@ -33,15 +33,3 @@ jobs:
|
|||||||
command: |
|
command: |
|
||||||
pnpm run publish:win
|
pnpm run publish:win
|
||||||
on_retry_command: pnpm cache delete
|
on_retry_command: pnpm cache delete
|
||||||
|
|
||||||
- name: Build and Publish releases (ARM64)
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
uses: nick-invision/retry@v2.8.2
|
|
||||||
with:
|
|
||||||
timeout_minutes: 30
|
|
||||||
max_attempts: 3
|
|
||||||
retry_on: error
|
|
||||||
command: |
|
|
||||||
pnpm run publish:win-arm64
|
|
||||||
on_retry_command: pnpm cache delete
|
|
||||||
|
|||||||
@@ -35,19 +35,6 @@ jobs:
|
|||||||
pnpm run publish:win
|
pnpm run publish:win
|
||||||
on_retry_command: pnpm cache delete
|
on_retry_command: pnpm cache delete
|
||||||
|
|
||||||
- name: Build and Publish releases (Windows ARM64)
|
|
||||||
if: matrix.os == 'windows-latest'
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
uses: nick-invision/retry@v2.8.2
|
|
||||||
with:
|
|
||||||
timeout_minutes: 30
|
|
||||||
max_attempts: 3
|
|
||||||
retry_on: error
|
|
||||||
command: |
|
|
||||||
pnpm run publish:win-arm64
|
|
||||||
on_retry_command: pnpm cache delete
|
|
||||||
|
|
||||||
- name: Build and Publish releases (macOS)
|
- name: Build and Publish releases (macOS)
|
||||||
if: matrix.os == 'macos-latest'
|
if: matrix.os == 'macos-latest'
|
||||||
env:
|
env:
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 645 B After Width: | Height: | Size: 820 B |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.1 KiB |
@@ -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,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:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"version": "1.4.2",
|
"version": "1.5.0",
|
||||||
"description": "A modern self-hosted music player.",
|
"description": "A modern self-hosted music player.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"subsonic",
|
"subsonic",
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -191,6 +192,14 @@
|
|||||||
},
|
},
|
||||||
"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ó"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -289,8 +298,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 +325,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 +550,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",
|
||||||
@@ -862,7 +871,14 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"column": {
|
"column": {
|
||||||
@@ -924,7 +940,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})",
|
||||||
@@ -1013,7 +1030,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",
|
||||||
|
|||||||
@@ -98,7 +98,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)",
|
||||||
@@ -284,7 +283,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 +379,13 @@
|
|||||||
"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í)"
|
||||||
},
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"editPlaylist": "upravit $t(entity.playlist, {\"count\": 1})",
|
"editPlaylist": "upravit $t(entity.playlist, {\"count\": 1})",
|
||||||
@@ -899,7 +904,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 +942,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": {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -667,7 +667,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",
|
||||||
@@ -713,6 +722,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",
|
||||||
@@ -749,7 +760,8 @@
|
|||||||
"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_optionAlpha": "alpha (nightly)",
|
||||||
"releaseChannel_optionBeta": "beta",
|
"releaseChannel_optionBeta": "beta",
|
||||||
"releaseChannel_optionLatest": "latest",
|
"releaseChannel_optionLatest": "latest",
|
||||||
|
|||||||
@@ -95,7 +95,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",
|
||||||
@@ -283,7 +282,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",
|
||||||
@@ -380,7 +379,13 @@
|
|||||||
"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)"
|
||||||
},
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"editPlaylist": "editar $t(entity.playlist, {\"count\": 1})",
|
"editPlaylist": "editar $t(entity.playlist, {\"count\": 1})",
|
||||||
@@ -792,7 +797,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 +835,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": {
|
||||||
|
|||||||
@@ -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)",
|
||||||
|
|||||||
@@ -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,9 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"remotePortWarning": "redémarrer le serveur pour appliquer le nouveau port",
|
"remotePortWarning": "redémarrer le serveur pour appliquer le nouveau port",
|
||||||
@@ -270,7 +272,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",
|
||||||
@@ -425,7 +427,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})"
|
||||||
@@ -487,7 +489,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 +718,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 +1023,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 +1067,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": "インスタントミックスを優先する",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -176,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",
|
||||||
@@ -428,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",
|
||||||
@@ -577,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",
|
||||||
@@ -611,6 +615,11 @@
|
|||||||
"collections": {
|
"collections": {
|
||||||
"overrideExisting": "nadpisz istniejące",
|
"overrideExisting": "nadpisz istniejące",
|
||||||
"saveAsCollection": "zapisz jako kolekcję"
|
"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": {
|
||||||
@@ -684,7 +693,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)",
|
||||||
@@ -728,7 +736,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",
|
||||||
@@ -997,7 +1005,10 @@
|
|||||||
"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_description": "ukryj playlisty w pasku bocznym pasujące do wyrażenia regularnego",
|
||||||
"sidebarPlaylistListFilterRegex_placeholder": "np. ^Miks codzienny.^",
|
"sidebarPlaylistListFilterRegex_placeholder": "np. ^Miks codzienny.^",
|
||||||
"sidebarPlaylistListFilterRegex": "filtr playlist regex"
|
"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)"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"config": {
|
"config": {
|
||||||
|
|||||||
@@ -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}})",
|
||||||
|
|||||||
@@ -1,5 +1,423 @@
|
|||||||
{
|
{
|
||||||
"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": "не вдалося створити спільний доступ (чи ввімкнено спільний доступ?)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"player": {
|
||||||
|
"skip": "пропустити"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,7 +229,6 @@
|
|||||||
"audioPlayer_description": "选择用于播放的音频播放器",
|
"audioPlayer_description": "选择用于播放的音频播放器",
|
||||||
"globalMediaHotkeys": "全局媒体快捷键",
|
"globalMediaHotkeys": "全局媒体快捷键",
|
||||||
"gaplessAudio_description": "调整 mpv 无缝音频设置",
|
"gaplessAudio_description": "调整 mpv 无缝音频设置",
|
||||||
"disableAutomaticUpdates": "禁用自动更新",
|
|
||||||
"followLyric_description": "滚动歌词到当前播放位置",
|
"followLyric_description": "滚动歌词到当前播放位置",
|
||||||
"audioExclusiveMode": "音频独占模式",
|
"audioExclusiveMode": "音频独占模式",
|
||||||
"font": "字体",
|
"font": "字体",
|
||||||
|
|||||||
@@ -296,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})",
|
||||||
@@ -314,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": "管理伺服器",
|
||||||
@@ -342,7 +347,17 @@
|
|||||||
"title": "電台"
|
"title": "電台"
|
||||||
},
|
},
|
||||||
"windowBar": {
|
"windowBar": {
|
||||||
"paused": "(暫停) "
|
"paused": "(暫停) ",
|
||||||
|
"privateMode": "(私人模式)"
|
||||||
|
},
|
||||||
|
"collections": {
|
||||||
|
"overrideExisting": "複寫現有的",
|
||||||
|
"saveAsCollection": "儲存為收藏"
|
||||||
|
},
|
||||||
|
"releasenotes": {
|
||||||
|
"commitsSinceStable": "提交自 {{stable}}",
|
||||||
|
"noNewCommits": "在此區間內沒有新的提交",
|
||||||
|
"noStableReleaseToCompare": "沒有穩定的發行可供比較"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
@@ -398,7 +413,7 @@
|
|||||||
"accentColor": "強調色",
|
"accentColor": "強調色",
|
||||||
"accentColor_description": "設定應用程式的強調色",
|
"accentColor_description": "設定應用程式的強調色",
|
||||||
"applicationHotkeys": "應用程式快捷鍵",
|
"applicationHotkeys": "應用程式快捷鍵",
|
||||||
"applicationHotkeys_description": "設定應用程式快捷鍵。切換勾選框來設為全局快捷鍵(僅桌面端)",
|
"applicationHotkeys_description": "設定應用程式快捷鍵。切換勾選框來設為全域快捷鍵(僅桌面端)",
|
||||||
"audioDevice": "音訊設備",
|
"audioDevice": "音訊設備",
|
||||||
"audioDevice_description": "選擇用於播放的音訊設備(僅 web 播放器)",
|
"audioDevice_description": "選擇用於播放的音訊設備(僅 web 播放器)",
|
||||||
"audioExclusiveMode": "音訊獨占模式",
|
"audioExclusiveMode": "音訊獨占模式",
|
||||||
@@ -409,7 +424,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}})",
|
||||||
@@ -431,10 +445,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": "暫停",
|
||||||
@@ -558,7 +572,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",
|
||||||
@@ -618,7 +632,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": "歌曲名稱",
|
||||||
@@ -630,8 +644,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": "經過匿名處理的使用情況資料將傳送給開發者,以協助改進應用程式",
|
||||||
@@ -715,7 +729,24 @@
|
|||||||
"pathReplace_description": "替換您伺服器的預設檔案路徑",
|
"pathReplace_description": "替換您伺服器的預設檔案路徑",
|
||||||
"pathReplace_optionRemovePrefix": "移除前綴",
|
"pathReplace_optionRemovePrefix": "移除前綴",
|
||||||
"pathReplace_optionAddPrefix": "增加前綴",
|
"pathReplace_optionAddPrefix": "增加前綴",
|
||||||
"sidebarPlaylistSorting": "側邊欄播放清單排序"
|
"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 (每日建構版)"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"config": {
|
"config": {
|
||||||
@@ -748,7 +779,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})",
|
||||||
@@ -783,7 +815,9 @@
|
|||||||
"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": "表格",
|
||||||
@@ -926,7 +960,8 @@
|
|||||||
"title": "標題",
|
"title": "標題",
|
||||||
"toYear": "從年份",
|
"toYear": "從年份",
|
||||||
"trackNumber": "曲目",
|
"trackNumber": "曲目",
|
||||||
"explicitStatus": "$t(common.explicitStatus)"
|
"explicitStatus": "$t(common.explicitStatus)",
|
||||||
|
"sortName": "排序名稱"
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"addServer": {
|
"addServer": {
|
||||||
@@ -1168,6 +1203,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,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,
|
||||||
@@ -21,6 +23,7 @@ import log from 'electron-log/main';
|
|||||||
import { AppImageUpdater, autoUpdater, MacUpdater, NsisUpdater } 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';
|
||||||
@@ -54,27 +57,17 @@ const ALPHA_UPDATER_CONFIG: {
|
|||||||
|
|
||||||
type UpdaterInstance = AppImageUpdater | MacUpdater | NsisUpdater | typeof autoUpdater;
|
type UpdaterInstance = AppImageUpdater | MacUpdater | NsisUpdater | typeof autoUpdater;
|
||||||
|
|
||||||
class AlphaAppUpdater {
|
|
||||||
constructor() {
|
|
||||||
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;
|
|
||||||
updater.checkForUpdatesAndNotify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AppUpdater {
|
class AppUpdater {
|
||||||
constructor() {
|
constructor() {
|
||||||
const effectiveChannel = store.get('release_channel') as string;
|
const effectiveChannel = store.get('release_channel') as string;
|
||||||
console.log('Effective update channel:', effectiveChannel);
|
console.log('Effective update channel:', effectiveChannel);
|
||||||
if (effectiveChannel === 'alpha') {
|
if (effectiveChannel === 'alpha') {
|
||||||
return new AlphaAppUpdater();
|
checkAllChannelsAndGetBest().then(({ updater: updaterInstance }) => {
|
||||||
|
updaterInstance.autoInstallOnAppQuit = true;
|
||||||
|
updaterInstance.autoRunAppAfterInstall = true;
|
||||||
|
updaterInstance.checkForUpdatesAndNotify();
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
configureAndGetUpdater();
|
configureAndGetUpdater();
|
||||||
@@ -82,6 +75,71 @@ class AppUpdater {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
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 {
|
||||||
|
configureAutoUpdaterForChannel('latest');
|
||||||
|
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 {
|
function configureAndGetUpdater(): UpdaterInstance {
|
||||||
const isBetaVersion = packageJson.version.includes('-beta');
|
const isBetaVersion = packageJson.version.includes('-beta');
|
||||||
const isAlphaVersion = packageJson.version.includes('-alpha');
|
const isAlphaVersion = packageJson.version.includes('-alpha');
|
||||||
@@ -122,17 +180,37 @@ function configureAndGetUpdater(): UpdaterInstance {
|
|||||||
|
|
||||||
if (effectiveChannel === 'beta') {
|
if (effectiveChannel === 'beta') {
|
||||||
autoUpdater.channel = 'beta';
|
autoUpdater.channel = 'beta';
|
||||||
|
autoUpdater.allowDowngrade = true;
|
||||||
autoUpdater.allowPrerelease = true;
|
autoUpdater.allowPrerelease = true;
|
||||||
autoUpdater.disableDifferentialDownload = true;
|
autoUpdater.disableDifferentialDownload = true;
|
||||||
} else {
|
} else {
|
||||||
autoUpdater.channel = 'latest';
|
autoUpdater.channel = 'latest';
|
||||||
autoUpdater.allowDowngrade = true;
|
|
||||||
autoUpdater.allowPrerelease = false;
|
autoUpdater.allowPrerelease = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return autoUpdater;
|
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 {
|
function createAlphaUpdaterInstance(): AppImageUpdater | MacUpdater | NsisUpdater {
|
||||||
if (isMacOS()) {
|
if (isMacOS()) {
|
||||||
return new MacUpdater(ALPHA_UPDATER_CONFIG);
|
return new MacUpdater(ALPHA_UPDATER_CONFIG);
|
||||||
@@ -440,8 +518,19 @@ async function createWindow(first = true): Promise<void> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Checking for updates');
|
console.log('Checking for updates');
|
||||||
const updater = configureAndGetUpdater();
|
const effectiveChannel = store.get('release_channel') as string;
|
||||||
const result = await updater.checkForUpdates();
|
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;
|
const updateAvailable = result?.isUpdateAvailable ?? false;
|
||||||
console.log('Update available:', updateAvailable);
|
console.log('Update available:', updateAvailable);
|
||||||
if (updateAvailable && store.get('disable_auto_updates') !== true) {
|
if (updateAvailable && store.get('disable_auto_updates') !== true) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -244,8 +244,6 @@ 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;
|
const singleSongOnly = meta?.singleSongOnly === true;
|
||||||
|
|
||||||
// For single-song actions (e.g. image play button), or NEXT/LAST/..., only add the clicked song
|
|
||||||
// For row double-click with NOW/SHUFFLE, add a range of songs around the clicked song
|
|
||||||
let songsToAdd: Song[];
|
let songsToAdd: Song[];
|
||||||
if (
|
if (
|
||||||
singleSongOnly ||
|
singleSongOnly ||
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
|
|
||||||
.tracks-table-header {
|
.tracks-table-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -80,12 +81,14 @@
|
|||||||
.track-header-cell {
|
.track-header-cell {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 60%;
|
min-height: 60%;
|
||||||
padding-right: var(--theme-spacing-sm);
|
padding-right: var(--theme-spacing-sm);
|
||||||
padding-left: var(--theme-spacing-sm);
|
padding-left: var(--theme-spacing-sm);
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-header-cell-no-h-padding {
|
.track-header-cell-no-h-padding {
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ interface ItemDetailListProps {
|
|||||||
internalState?: ItemListStateActions;
|
internalState?: ItemListStateActions;
|
||||||
itemCount?: number;
|
itemCount?: number;
|
||||||
items?: unknown[];
|
items?: unknown[];
|
||||||
|
listKey?: ItemListKey;
|
||||||
onColumnReordered?: (
|
onColumnReordered?: (
|
||||||
columnIdFrom: TableColumn,
|
columnIdFrom: TableColumn,
|
||||||
columnIdTo: TableColumn,
|
columnIdTo: TableColumn,
|
||||||
@@ -92,8 +93,15 @@ interface ItemDetailListProps {
|
|||||||
onColumnResized?: (columnId: TableColumn, width: number) => void;
|
onColumnResized?: (columnId: TableColumn, width: number) => void;
|
||||||
onRangeChanged?: (range: { startIndex: number; stopIndex: number }) => Promise<void> | void;
|
onRangeChanged?: (range: { startIndex: number; stopIndex: number }) => Promise<void> | void;
|
||||||
onScrollEnd?: (rowIndex: number) => void;
|
onScrollEnd?: (rowIndex: number) => void;
|
||||||
|
onSongRowDoubleClick?: (params: {
|
||||||
|
index: number;
|
||||||
|
internalState: ItemListStateActions;
|
||||||
|
item: Song;
|
||||||
|
}) => void;
|
||||||
|
overrideControls?: Partial<ItemControls>;
|
||||||
rowHeight?: number;
|
rowHeight?: number;
|
||||||
scrollOffset?: number;
|
scrollOffset?: number;
|
||||||
|
songsByAlbumId?: Record<string, Song[]>;
|
||||||
tableId?: string;
|
tableId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +117,13 @@ interface RowData {
|
|||||||
getItem?: (index: number) => unknown;
|
getItem?: (index: number) => unknown;
|
||||||
internalState: ItemListStateActions;
|
internalState: ItemListStateActions;
|
||||||
isMutatingFavorite: boolean;
|
isMutatingFavorite: boolean;
|
||||||
|
onSongRowDoubleClick?: (params: {
|
||||||
|
index: number;
|
||||||
|
internalState: ItemListStateActions;
|
||||||
|
item: Song;
|
||||||
|
}) => void;
|
||||||
registerSongs: (albumId: string, songs: Song[]) => void;
|
registerSongs: (albumId: string, songs: Song[]) => void;
|
||||||
|
songsByAlbumId?: Record<string, Song[]>;
|
||||||
trackColumns: ItemTableListColumnConfig[];
|
trackColumns: ItemTableListColumnConfig[];
|
||||||
trackTableSize: 'compact' | 'default' | 'large';
|
trackTableSize: 'compact' | 'default' | 'large';
|
||||||
}
|
}
|
||||||
@@ -126,6 +140,11 @@ interface TrackRowProps {
|
|||||||
internalState: ItemListStateActions;
|
internalState: ItemListStateActions;
|
||||||
isMutatingFavorite: boolean;
|
isMutatingFavorite: boolean;
|
||||||
isSongsLoading?: boolean;
|
isSongsLoading?: boolean;
|
||||||
|
onSongRowDoubleClick?: (params: {
|
||||||
|
index: number;
|
||||||
|
internalState: ItemListStateActions;
|
||||||
|
item: Song;
|
||||||
|
}) => void;
|
||||||
rowIndex: number;
|
rowIndex: number;
|
||||||
size: 'compact' | 'default' | 'large';
|
size: 'compact' | 'default' | 'large';
|
||||||
song: Song;
|
song: Song;
|
||||||
@@ -147,6 +166,7 @@ const TrackRow = memo(
|
|||||||
internalState,
|
internalState,
|
||||||
isMutatingFavorite,
|
isMutatingFavorite,
|
||||||
isSongsLoading,
|
isSongsLoading,
|
||||||
|
onSongRowDoubleClick,
|
||||||
rowIndex,
|
rowIndex,
|
||||||
size,
|
size,
|
||||||
song,
|
song,
|
||||||
@@ -167,11 +187,37 @@ const TrackRow = memo(
|
|||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
if (onSongRowDoubleClick) {
|
||||||
|
onSongRowDoubleClick({
|
||||||
|
index: internalState.findItemIndex(song.id),
|
||||||
|
internalState,
|
||||||
|
item: song,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (controls?.onDoubleClick) {
|
||||||
|
controls.onDoubleClick({
|
||||||
|
event: e,
|
||||||
|
index: internalState.findItemIndex(song.id),
|
||||||
|
internalState,
|
||||||
|
item: song,
|
||||||
|
itemType: LibraryItem.SONG,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isSongsLoading || albumSongs.length === 0) return;
|
if (isSongsLoading || albumSongs.length === 0) return;
|
||||||
internalState.setSelected([song]);
|
internalState.setSelected([song]);
|
||||||
playerContext.addToQueueByData(albumSongs, Play.NOW, song.id);
|
playerContext.addToQueueByData(albumSongs, Play.NOW, song.id);
|
||||||
},
|
},
|
||||||
[albumSongs, internalState, isSongsLoading, playerContext, song],
|
[
|
||||||
|
albumSongs,
|
||||||
|
controls,
|
||||||
|
internalState,
|
||||||
|
isSongsLoading,
|
||||||
|
onSongRowDoubleClick,
|
||||||
|
playerContext,
|
||||||
|
song,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleRowClick = useCallback(
|
const handleRowClick = useCallback(
|
||||||
@@ -610,7 +656,9 @@ const RowContent = memo(
|
|||||||
index,
|
index,
|
||||||
internalState,
|
internalState,
|
||||||
isMutatingFavorite,
|
isMutatingFavorite,
|
||||||
|
onSongRowDoubleClick,
|
||||||
registerSongs,
|
registerSongs,
|
||||||
|
songsByAlbumId,
|
||||||
trackColumns,
|
trackColumns,
|
||||||
trackTableSize,
|
trackTableSize,
|
||||||
}: RowContentProps) => {
|
}: RowContentProps) => {
|
||||||
@@ -622,8 +670,10 @@ const RowContent = memo(
|
|||||||
return (data?.[index] as Album | undefined) || undefined;
|
return (data?.[index] as Album | undefined) || undefined;
|
||||||
}, [data, getItem, index]);
|
}, [data, getItem, index]);
|
||||||
|
|
||||||
|
const useClientSideSongs = Boolean(songsByAlbumId);
|
||||||
|
|
||||||
const songListQuery = useMemo(() => {
|
const songListQuery = useMemo(() => {
|
||||||
if (!item?.id || !item?._serverId) return null;
|
if (useClientSideSongs || !item?.id || !item?._serverId) return null;
|
||||||
return {
|
return {
|
||||||
query: {
|
query: {
|
||||||
albumIds: [item.id],
|
albumIds: [item.id],
|
||||||
@@ -634,7 +684,7 @@ const RowContent = memo(
|
|||||||
},
|
},
|
||||||
serverId: item?._serverId || '',
|
serverId: item?._serverId || '',
|
||||||
};
|
};
|
||||||
}, [item]);
|
}, [item, useClientSideSongs]);
|
||||||
|
|
||||||
const { data: songListData, isLoading: isSongsQueryLoading } = useQuery({
|
const { data: songListData, isLoading: isSongsQueryLoading } = useQuery({
|
||||||
enabled: !!songListQuery,
|
enabled: !!songListQuery,
|
||||||
@@ -646,8 +696,17 @@ const RowContent = memo(
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const songItems = songListData?.items;
|
const songItemsFromQuery = songListData?.items;
|
||||||
const isSongsLoading = !!item && isSongsQueryLoading && !songItems?.length;
|
const songItemsFromClient = useMemo(() => {
|
||||||
|
const rowSongs = (item as { _playlistSongs?: Song[] })?._playlistSongs;
|
||||||
|
if (rowSongs?.length) return rowSongs;
|
||||||
|
if (!songsByAlbumId || !item?.id) return undefined;
|
||||||
|
return songsByAlbumId[item.id];
|
||||||
|
}, [item, songsByAlbumId]);
|
||||||
|
|
||||||
|
const songItems = useClientSideSongs ? songItemsFromClient : songItemsFromQuery;
|
||||||
|
const isSongsLoading =
|
||||||
|
!useClientSideSongs && !!item && isSongsQueryLoading && !songItemsFromQuery?.length;
|
||||||
|
|
||||||
const songs = useMemo(() => {
|
const songs = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
@@ -705,6 +764,7 @@ const RowContent = memo(
|
|||||||
isMutatingFavorite={isMutatingFavorite}
|
isMutatingFavorite={isMutatingFavorite}
|
||||||
isSongsLoading={isSongsLoading}
|
isSongsLoading={isSongsLoading}
|
||||||
key={song.id}
|
key={song.id}
|
||||||
|
onSongRowDoubleClick={onSongRowDoubleClick}
|
||||||
rowIndex={rowIndex}
|
rowIndex={rowIndex}
|
||||||
size={trackTableSize}
|
size={trackTableSize}
|
||||||
song={song as Song}
|
song={song as Song}
|
||||||
@@ -729,6 +789,7 @@ const RowContent = memo(
|
|||||||
prev.isMutatingFavorite === next.isMutatingFavorite &&
|
prev.isMutatingFavorite === next.isMutatingFavorite &&
|
||||||
prev.controls === next.controls &&
|
prev.controls === next.controls &&
|
||||||
prev.registerSongs === next.registerSongs &&
|
prev.registerSongs === next.registerSongs &&
|
||||||
|
prev.songsByAlbumId === next.songsByAlbumId &&
|
||||||
prev.trackColumns === next.trackColumns &&
|
prev.trackColumns === next.trackColumns &&
|
||||||
prev.trackTableSize === next.trackTableSize,
|
prev.trackTableSize === next.trackTableSize,
|
||||||
);
|
);
|
||||||
@@ -1113,10 +1174,14 @@ export const ItemDetailList = ({
|
|||||||
getItem,
|
getItem,
|
||||||
itemCount: externalItemCount,
|
itemCount: externalItemCount,
|
||||||
items,
|
items,
|
||||||
|
listKey = ItemListKey.ALBUM,
|
||||||
onColumnReordered,
|
onColumnReordered,
|
||||||
onColumnResized,
|
onColumnResized,
|
||||||
onRangeChanged,
|
onRangeChanged,
|
||||||
onScrollEnd,
|
onScrollEnd,
|
||||||
|
onSongRowDoubleClick,
|
||||||
|
overrideControls,
|
||||||
|
songsByAlbumId,
|
||||||
tableId = DEFAULT_DETAIL_TABLE_ID,
|
tableId = DEFAULT_DETAIL_TABLE_ID,
|
||||||
}: ItemDetailListProps) => {
|
}: ItemDetailListProps) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -1127,6 +1192,7 @@ export const ItemDetailList = ({
|
|||||||
const controls = useDefaultItemListControls({
|
const controls = useDefaultItemListControls({
|
||||||
onColumnReordered,
|
onColumnReordered,
|
||||||
onColumnResized,
|
onColumnResized,
|
||||||
|
overrides: overrideControls,
|
||||||
});
|
});
|
||||||
const isMutatingCreateFavorite = useIsMutatingCreateFavorite();
|
const isMutatingCreateFavorite = useIsMutatingCreateFavorite();
|
||||||
const isMutatingDeleteFavorite = useIsMutatingDeleteFavorite();
|
const isMutatingDeleteFavorite = useIsMutatingDeleteFavorite();
|
||||||
@@ -1172,7 +1238,7 @@ export const ItemDetailList = ({
|
|||||||
|
|
||||||
const internalState = useItemListState(getDataFn, extractRowIdSong);
|
const internalState = useItemListState(getDataFn, extractRowIdSong);
|
||||||
|
|
||||||
const tableConfig = useSettingsStore((state) => state.lists[ItemListKey.ALBUM]?.detail);
|
const tableConfig = useSettingsStore((state) => state.lists[listKey]?.detail);
|
||||||
const trackColumns = useMemo((): ItemTableListColumnConfig[] => {
|
const trackColumns = useMemo((): ItemTableListColumnConfig[] => {
|
||||||
const raw = tableConfig?.columns;
|
const raw = tableConfig?.columns;
|
||||||
if (raw && raw.length > 0) {
|
if (raw && raw.length > 0) {
|
||||||
@@ -1263,8 +1329,10 @@ export const ItemDetailList = ({
|
|||||||
getItem,
|
getItem,
|
||||||
internalState,
|
internalState,
|
||||||
isMutatingFavorite,
|
isMutatingFavorite,
|
||||||
|
onSongRowDoubleClick,
|
||||||
queryClient,
|
queryClient,
|
||||||
registerSongs,
|
registerSongs,
|
||||||
|
songsByAlbumId,
|
||||||
trackColumns,
|
trackColumns,
|
||||||
trackTableSize,
|
trackTableSize,
|
||||||
}),
|
}),
|
||||||
@@ -1279,8 +1347,10 @@ export const ItemDetailList = ({
|
|||||||
getItem,
|
getItem,
|
||||||
internalState,
|
internalState,
|
||||||
isMutatingFavorite,
|
isMutatingFavorite,
|
||||||
|
onSongRowDoubleClick,
|
||||||
queryClient,
|
queryClient,
|
||||||
registerSongs,
|
registerSongs,
|
||||||
|
songsByAlbumId,
|
||||||
trackColumns,
|
trackColumns,
|
||||||
trackTableSize,
|
trackTableSize,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { TableColumn } from '/@/shared/types/types';
|
|||||||
|
|
||||||
const FIXED_TRACK_COLUMN_WIDTHS: Partial<Record<TableColumn, number>> = {
|
const FIXED_TRACK_COLUMN_WIDTHS: Partial<Record<TableColumn, number>> = {
|
||||||
[TableColumn.ACTIONS]: 32,
|
[TableColumn.ACTIONS]: 32,
|
||||||
[TableColumn.BIT_DEPTH]: 80,
|
[TableColumn.BIT_DEPTH]: 88,
|
||||||
[TableColumn.BIT_RATE]: 80,
|
[TableColumn.BIT_RATE]: 88,
|
||||||
[TableColumn.BPM]: 56,
|
[TableColumn.BPM]: 56,
|
||||||
[TableColumn.CHANNELS]: 80,
|
[TableColumn.CHANNELS]: 80,
|
||||||
[TableColumn.CODEC]: 80,
|
[TableColumn.CODEC]: 80,
|
||||||
@@ -11,8 +11,8 @@ const FIXED_TRACK_COLUMN_WIDTHS: Partial<Record<TableColumn, number>> = {
|
|||||||
[TableColumn.DISC_NUMBER]: 36,
|
[TableColumn.DISC_NUMBER]: 36,
|
||||||
[TableColumn.DURATION]: 72,
|
[TableColumn.DURATION]: 72,
|
||||||
[TableColumn.RELEASE_DATE]: 128,
|
[TableColumn.RELEASE_DATE]: 128,
|
||||||
[TableColumn.SAMPLE_RATE]: 90,
|
[TableColumn.SAMPLE_RATE]: 112,
|
||||||
[TableColumn.TRACK_NUMBER]: 56,
|
[TableColumn.TRACK_NUMBER]: 64,
|
||||||
[TableColumn.USER_FAVORITE]: 32,
|
[TableColumn.USER_FAVORITE]: 32,
|
||||||
[TableColumn.USER_RATING]: 64,
|
[TableColumn.USER_RATING]: 64,
|
||||||
[TableColumn.YEAR]: 56,
|
[TableColumn.YEAR]: 56,
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
import { createContext, useContext } from 'react';
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
export type ListDisplayMode = LibraryItem.ALBUM | LibraryItem.SONG;
|
||||||
|
|
||||||
interface ListContextProps {
|
interface ListContextProps {
|
||||||
customFilters?: Record<string, unknown>;
|
customFilters?: Record<string, unknown>;
|
||||||
|
displayMode?: ListDisplayMode;
|
||||||
id?: string;
|
id?: string;
|
||||||
isSidebarOpen?: boolean;
|
isSidebarOpen?: boolean;
|
||||||
isSmartPlaylist?: boolean;
|
isSmartPlaylist?: boolean;
|
||||||
itemCount?: number;
|
itemCount?: number;
|
||||||
listData?: unknown[];
|
listData?: unknown[];
|
||||||
|
listKey?: ItemListKey;
|
||||||
mode?: 'edit' | 'view';
|
mode?: 'edit' | 'view';
|
||||||
pageKey: ItemListKey | string;
|
pageKey: ItemListKey | string;
|
||||||
|
setDisplayMode?: (displayMode: ListDisplayMode) => void;
|
||||||
setIsSidebarOpen?: (isSidebarOpen: boolean) => void;
|
setIsSidebarOpen?: (isSidebarOpen: boolean) => void;
|
||||||
setItemCount?: (itemCount: number) => void;
|
setItemCount?: (itemCount: number) => void;
|
||||||
setListData?: (items: unknown[]) => void;
|
setListData?: (items: unknown[]) => void;
|
||||||
|
|||||||
@@ -345,8 +345,7 @@ export const AddToPlaylistAction = ({ items, itemType }: AddToPlaylistActionProp
|
|||||||
|
|
||||||
openContextModal({
|
openContextModal({
|
||||||
innerProps: {
|
innerProps: {
|
||||||
itemIds: items,
|
...modalProps,
|
||||||
resourceType: itemType,
|
|
||||||
},
|
},
|
||||||
modalKey: 'addToPlaylist',
|
modalKey: 'addToPlaylist',
|
||||||
size: 'lg',
|
size: 'lg',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { DiscordRpcHook } from '/@/renderer/features/discord-rpc/use-discord-rpc
|
|||||||
import { MainPlayerListenerHook } from '/@/renderer/features/player/audio-player/hooks/use-main-player-listener';
|
import { MainPlayerListenerHook } from '/@/renderer/features/player/audio-player/hooks/use-main-player-listener';
|
||||||
import { MpvPlayer } from '/@/renderer/features/player/audio-player/mpv-player';
|
import { MpvPlayer } from '/@/renderer/features/player/audio-player/mpv-player';
|
||||||
import { WebPlayer } from '/@/renderer/features/player/audio-player/web-player';
|
import { WebPlayer } from '/@/renderer/features/player/audio-player/web-player';
|
||||||
|
import { SleepTimerHook } from '/@/renderer/features/player/components/sleep-timer-button';
|
||||||
import { AutoDJHook } from '/@/renderer/features/player/hooks/use-auto-dj';
|
import { AutoDJHook } from '/@/renderer/features/player/hooks/use-auto-dj';
|
||||||
import { MediaSessionHook } from '/@/renderer/features/player/hooks/use-media-session';
|
import { MediaSessionHook } from '/@/renderer/features/player/hooks/use-media-session';
|
||||||
import { MPRISHook } from '/@/renderer/features/player/hooks/use-mpris';
|
import { MPRISHook } from '/@/renderer/features/player/hooks/use-mpris';
|
||||||
@@ -48,6 +49,7 @@ export const AudioPlayers = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<SleepTimerHook />
|
||||||
<ScrobbleHook />
|
<ScrobbleHook />
|
||||||
<PowerSaveBlockerHook />
|
<PowerSaveBlockerHook />
|
||||||
<DiscordRpcHook />
|
<DiscordRpcHook />
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { PopoverPlayQueue } from '/@/renderer/features/now-playing/components/popover-play-queue';
|
import { PopoverPlayQueue } from '/@/renderer/features/now-playing/components/popover-play-queue';
|
||||||
import { PlayerConfig } from '/@/renderer/features/player/components/player-config';
|
import { PlayerConfig } from '/@/renderer/features/player/components/player-config';
|
||||||
import { CustomPlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider';
|
import { CustomPlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider';
|
||||||
|
import { SleepTimerButton } from '/@/renderer/features/player/components/sleep-timer-button';
|
||||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||||
import { useSetRating } from '/@/renderer/features/shared/hooks/use-set-rating';
|
import { useSetRating } from '/@/renderer/features/shared/hooks/use-set-rating';
|
||||||
import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
|
import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
|
||||||
@@ -72,6 +73,7 @@ export const RightControls = () => {
|
|||||||
<AutoDJButton />
|
<AutoDJButton />
|
||||||
</Group>
|
</Group>
|
||||||
<Group align="center" gap="xs" wrap="nowrap">
|
<Group align="center" gap="xs" wrap="nowrap">
|
||||||
|
<SleepTimerButton />
|
||||||
<PlayerConfig />
|
<PlayerConfig />
|
||||||
<LyricsButton />
|
<LyricsButton />
|
||||||
<FavoriteButton />
|
<FavoriteButton />
|
||||||
|
|||||||
@@ -0,0 +1,344 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
|
||||||
|
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||||
|
import { usePlayerStatus, usePlayerStoreBase } from '/@/renderer/store/player.store';
|
||||||
|
import {
|
||||||
|
useSleepTimerActions,
|
||||||
|
useSleepTimerActive,
|
||||||
|
useSleepTimerMode,
|
||||||
|
useSleepTimerRemaining,
|
||||||
|
useSleepTimerStore,
|
||||||
|
} from '/@/renderer/store/sleep-timer.store';
|
||||||
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
|
import { Button } from '/@/shared/components/button/button';
|
||||||
|
import { Flex } from '/@/shared/components/flex/flex';
|
||||||
|
import { Group } from '/@/shared/components/group/group';
|
||||||
|
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||||
|
import { Popover } from '/@/shared/components/popover/popover';
|
||||||
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
|
import { Text } from '/@/shared/components/text/text';
|
||||||
|
import { PlayerStatus } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
const PRESET_OPTIONS = [
|
||||||
|
{ minutes: 0, mode: 'endOfSong' as const },
|
||||||
|
{ minutes: 5, mode: 'timed' as const },
|
||||||
|
{ minutes: 10, mode: 'timed' as const },
|
||||||
|
{ minutes: 15, mode: 'timed' as const },
|
||||||
|
{ minutes: 30, mode: 'timed' as const },
|
||||||
|
{ minutes: 45, mode: 'timed' as const },
|
||||||
|
{ minutes: 60, mode: 'timed' as const },
|
||||||
|
{ minutes: 120, mode: 'timed' as const },
|
||||||
|
];
|
||||||
|
|
||||||
|
function formatRemaining(totalSeconds: number): string {
|
||||||
|
const h = Math.floor(totalSeconds / 3600);
|
||||||
|
const m = Math.floor((totalSeconds % 3600) / 60);
|
||||||
|
const s = Math.floor(totalSeconds % 60);
|
||||||
|
|
||||||
|
if (h > 0) {
|
||||||
|
return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
return `${m}:${String(s).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useSleepTimer = () => {
|
||||||
|
const active = useSleepTimerActive();
|
||||||
|
const mode = useSleepTimerMode();
|
||||||
|
const { cancelTimer, setRemaining } = useSleepTimerActions();
|
||||||
|
const { mediaPause } = usePlayer();
|
||||||
|
|
||||||
|
const mediaPauseRef = useRef(mediaPause);
|
||||||
|
mediaPauseRef.current = mediaPause;
|
||||||
|
|
||||||
|
const handleOnCurrentSongChange = useCallback(() => {
|
||||||
|
if (!active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel and pause on song change in end-of-song mode
|
||||||
|
if (mode === 'endOfSong') {
|
||||||
|
cancelTimer();
|
||||||
|
mediaPauseRef.current();
|
||||||
|
}
|
||||||
|
}, [active, mode, cancelTimer, mediaPauseRef]);
|
||||||
|
|
||||||
|
const status = usePlayerStatus();
|
||||||
|
|
||||||
|
const handleOnPlayerProgress = useCallback(() => {
|
||||||
|
if (!active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status !== PlayerStatus.PLAYING) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count down in timed mode
|
||||||
|
if (mode === 'timed') {
|
||||||
|
const remaining = useSleepTimerStore.getState().remaining;
|
||||||
|
|
||||||
|
if (remaining <= 0) {
|
||||||
|
cancelTimer();
|
||||||
|
mediaPauseRef.current();
|
||||||
|
} else {
|
||||||
|
setRemaining(Math.max(0, remaining - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [active, cancelTimer, mode, setRemaining, status]);
|
||||||
|
|
||||||
|
usePlayerEvents(
|
||||||
|
{
|
||||||
|
onCurrentSongChange: handleOnCurrentSongChange,
|
||||||
|
onPlayerProgress: handleOnPlayerProgress,
|
||||||
|
},
|
||||||
|
[handleOnCurrentSongChange, handleOnPlayerProgress],
|
||||||
|
);
|
||||||
|
|
||||||
|
// End-of-song mode: subscribe to player index changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!active || mode !== 'endOfSong') return;
|
||||||
|
|
||||||
|
const initialIndex = usePlayerStoreBase.getState().player.index;
|
||||||
|
|
||||||
|
const unsub = usePlayerStoreBase.subscribe(
|
||||||
|
(state) => state.player.index,
|
||||||
|
(index) => {
|
||||||
|
if (index !== initialIndex) {
|
||||||
|
cancelTimer();
|
||||||
|
mediaPauseRef.current();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => unsub();
|
||||||
|
}, [active, mode, cancelTimer]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SleepTimerHookInner = () => {
|
||||||
|
useSleepTimer();
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SleepTimerHook = () => {
|
||||||
|
const active = useSleepTimerActive();
|
||||||
|
|
||||||
|
if (!active) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return React.createElement(SleepTimerHookInner);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SleepTimerButton = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const active = useSleepTimerActive();
|
||||||
|
const mode = useSleepTimerMode();
|
||||||
|
const remaining = useSleepTimerRemaining();
|
||||||
|
const { cancelTimer, startEndOfSongTimer, startTimedTimer } = useSleepTimerActions();
|
||||||
|
const { mediaPause } = usePlayer();
|
||||||
|
|
||||||
|
const [showCustom, setShowCustom] = useState(false);
|
||||||
|
const [customHours, setCustomHours] = useState<number>(0);
|
||||||
|
const [customMinutes, setCustomMinutes] = useState<number>(20);
|
||||||
|
const [customSeconds, setCustomSeconds] = useState<number>(0);
|
||||||
|
const [opened, setOpened] = useState(false);
|
||||||
|
|
||||||
|
const mediaPauseRef = useRef(mediaPause);
|
||||||
|
mediaPauseRef.current = mediaPause;
|
||||||
|
|
||||||
|
const handlePreset = useCallback(
|
||||||
|
(option: (typeof PRESET_OPTIONS)[number]) => {
|
||||||
|
if (option.mode === 'endOfSong') {
|
||||||
|
startEndOfSongTimer();
|
||||||
|
} else {
|
||||||
|
startTimedTimer(option.minutes * 60);
|
||||||
|
}
|
||||||
|
setShowCustom(false);
|
||||||
|
setOpened(false);
|
||||||
|
},
|
||||||
|
[startEndOfSongTimer, startTimedTimer],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCustomStart = useCallback(() => {
|
||||||
|
const totalSeconds = customHours * 3600 + customMinutes * 60 + customSeconds;
|
||||||
|
if (totalSeconds > 0) {
|
||||||
|
startTimedTimer(totalSeconds);
|
||||||
|
setShowCustom(false);
|
||||||
|
setOpened(false);
|
||||||
|
}
|
||||||
|
}, [customHours, customMinutes, customSeconds, startTimedTimer]);
|
||||||
|
|
||||||
|
const handleCancel = useCallback(() => {
|
||||||
|
cancelTimer();
|
||||||
|
setShowCustom(false);
|
||||||
|
}, [cancelTimer]);
|
||||||
|
|
||||||
|
const getPresetLabel = (option: (typeof PRESET_OPTIONS)[number]) => {
|
||||||
|
if (option.mode === 'endOfSong') {
|
||||||
|
return t('player.sleepTimer_endOfSong', { postProcess: 'sentenceCase' });
|
||||||
|
}
|
||||||
|
if (option.minutes >= 60) {
|
||||||
|
return t('player.sleepTimer_hours', {
|
||||||
|
count: option.minutes / 60,
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return t('player.sleepTimer_minutes', {
|
||||||
|
count: option.minutes,
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover onChange={setOpened} opened={opened} position="top" width={260}>
|
||||||
|
<Popover.Target>
|
||||||
|
<ActionIcon
|
||||||
|
icon={active ? 'sleepTimer' : 'sleepTimerOff'}
|
||||||
|
iconProps={{
|
||||||
|
color: active ? 'primary' : undefined,
|
||||||
|
size: 'lg',
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setOpened((prev) => !prev);
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
tooltip={{
|
||||||
|
label: t('player.sleepTimer', { postProcess: 'titleCase' }),
|
||||||
|
openDelay: 0,
|
||||||
|
}}
|
||||||
|
variant="subtle"
|
||||||
|
/>
|
||||||
|
</Popover.Target>
|
||||||
|
<Popover.Dropdown>
|
||||||
|
<Stack gap="xs" p="xs">
|
||||||
|
<Text fw="600" size="sm" ta="center">
|
||||||
|
{t('player.sleepTimer', { postProcess: 'titleCase' })}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{active && (
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
direction="column"
|
||||||
|
gap={4}
|
||||||
|
mb="xs"
|
||||||
|
style={{
|
||||||
|
background: 'var(--theme-colors-surface)',
|
||||||
|
borderRadius: 'var(--theme-radius-md)',
|
||||||
|
padding: 'var(--theme-spacing-sm) var(--theme-spacing-md)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{mode === 'endOfSong' ? (
|
||||||
|
<Text c="primary" size="sm">
|
||||||
|
{t('player.sleepTimer_endOfSong', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text c="primary" fw="600" size="lg">
|
||||||
|
{formatRemaining(remaining)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleCancel();
|
||||||
|
}}
|
||||||
|
size="compact-xs"
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
|
{t('player.sleepTimer_cancel', { postProcess: 'titleCase' })}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{PRESET_OPTIONS.map((option, index) => (
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
justify="flex-start"
|
||||||
|
key={index}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handlePreset(option);
|
||||||
|
}}
|
||||||
|
size="xs"
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
|
{getPresetLabel(option)}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{!showCustom ? (
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
justify="flex-start"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowCustom(true);
|
||||||
|
}}
|
||||||
|
size="xs"
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
|
{t('player.sleepTimer_custom', { postProcess: 'sentenceCase' })}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Group gap={4} wrap="nowrap">
|
||||||
|
<NumberInput
|
||||||
|
max={23}
|
||||||
|
min={0}
|
||||||
|
onChange={(val) => setCustomHours(Number(val) || 0)}
|
||||||
|
placeholder="hr"
|
||||||
|
size="xs"
|
||||||
|
value={customHours}
|
||||||
|
/>
|
||||||
|
<Text>:</Text>
|
||||||
|
<NumberInput
|
||||||
|
max={59}
|
||||||
|
min={0}
|
||||||
|
onChange={(val) => setCustomMinutes(Number(val) || 0)}
|
||||||
|
placeholder="min"
|
||||||
|
size="xs"
|
||||||
|
value={customMinutes}
|
||||||
|
/>
|
||||||
|
<Text>:</Text>
|
||||||
|
<NumberInput
|
||||||
|
max={59}
|
||||||
|
min={0}
|
||||||
|
onChange={(val) => setCustomSeconds(Number(val) || 0)}
|
||||||
|
placeholder="sec"
|
||||||
|
size="xs"
|
||||||
|
value={customSeconds}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Group gap="xs" grow>
|
||||||
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleCustomStart();
|
||||||
|
}}
|
||||||
|
size="xs"
|
||||||
|
variant="filled"
|
||||||
|
>
|
||||||
|
{t('player.sleepTimer_setCustom', { postProcess: 'titleCase' })}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowCustom(false);
|
||||||
|
}}
|
||||||
|
size="xs"
|
||||||
|
variant="default"
|
||||||
|
>
|
||||||
|
{t('common.cancel', { postProcess: 'titleCase' })}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Popover.Dropdown>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
import { useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows';
|
||||||
|
import { useItemListColumnReorder } from '/@/renderer/components/item-list/helpers/use-item-list-column-reorder';
|
||||||
|
import { useItemListColumnResize } from '/@/renderer/components/item-list/helpers/use-item-list-column-resize';
|
||||||
|
import { useItemListScrollPersist } from '/@/renderer/components/item-list/helpers/use-item-list-scroll-persist';
|
||||||
|
import { ItemDetailList } from '/@/renderer/components/item-list/item-detail-list/item-detail-list';
|
||||||
|
import { ItemGridList } from '/@/renderer/components/item-list/item-grid-list/item-grid-list';
|
||||||
|
import { ItemListWithPagination } from '/@/renderer/components/item-list/item-list-pagination/item-list-pagination';
|
||||||
|
import { useItemListPagination } from '/@/renderer/components/item-list/item-list-pagination/use-item-list-pagination';
|
||||||
|
import { ItemTableList } from '/@/renderer/components/item-list/item-table-list/item-table-list';
|
||||||
|
import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
||||||
|
import { DefaultItemControlProps, ItemControls } from '/@/renderer/components/item-list/types';
|
||||||
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
|
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||||
|
import { usePlaylistSongListFilters } from '/@/renderer/features/playlists/hooks/use-playlist-song-list-filters';
|
||||||
|
import { type PlaylistAlbumRow, playlistSongsToAlbums } from '/@/renderer/features/playlists/utils';
|
||||||
|
import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter';
|
||||||
|
import { searchLibraryItems } from '/@/renderer/features/shared/utils';
|
||||||
|
import { useGeneralSettings, useListSettings } from '/@/renderer/store';
|
||||||
|
import { sortSongList } from '/@/shared/api/utils';
|
||||||
|
import {
|
||||||
|
LibraryItem,
|
||||||
|
PlaylistSongListResponse,
|
||||||
|
SongListSort,
|
||||||
|
SortOrder,
|
||||||
|
} from '/@/shared/types/domain-types';
|
||||||
|
import { ItemListKey, ListDisplayType, ListPaginationType, Play } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
export const PlaylistDetailAlbumView = ({ data }: { data: PlaylistSongListResponse }) => {
|
||||||
|
const player = usePlayer();
|
||||||
|
const { setItemCount, setListData } = useListContext();
|
||||||
|
const { detail, display, grid, itemsPerPage, pagination, table } = useListSettings(
|
||||||
|
ItemListKey.PLAYLIST_ALBUM,
|
||||||
|
);
|
||||||
|
const { enableGridMultiSelect } = useGeneralSettings();
|
||||||
|
const { currentPage, onChange: onPageChange } = useItemListPagination();
|
||||||
|
const { searchTerm } = useSearchTermFilter();
|
||||||
|
const { query } = usePlaylistSongListFilters();
|
||||||
|
|
||||||
|
const sortedAlbums = useMemo(() => {
|
||||||
|
let songs = data?.items ?? [];
|
||||||
|
if (searchTerm?.trim()) {
|
||||||
|
songs = searchLibraryItems(songs, searchTerm, LibraryItem.SONG);
|
||||||
|
}
|
||||||
|
const sortedSongs = sortSongList(
|
||||||
|
songs,
|
||||||
|
(query.sortBy as SongListSort) ?? SongListSort.ID,
|
||||||
|
(query.sortOrder as SortOrder) ?? SortOrder.ASC,
|
||||||
|
);
|
||||||
|
return playlistSongsToAlbums(sortedSongs);
|
||||||
|
}, [data?.items, searchTerm, query.sortBy, query.sortOrder]);
|
||||||
|
|
||||||
|
const isPaginated = pagination === ListPaginationType.PAGINATED;
|
||||||
|
const totalAlbumCount = sortedAlbums.length;
|
||||||
|
const albumPageCount = Math.max(1, Math.ceil(totalAlbumCount / itemsPerPage));
|
||||||
|
const paginatedAlbums = useMemo(() => {
|
||||||
|
if (!isPaginated) return sortedAlbums;
|
||||||
|
const start = currentPage * itemsPerPage;
|
||||||
|
return sortedAlbums.slice(start, start + itemsPerPage);
|
||||||
|
}, [isPaginated, currentPage, itemsPerPage, sortedAlbums]);
|
||||||
|
const albumsToRender = isPaginated ? paginatedAlbums : sortedAlbums;
|
||||||
|
|
||||||
|
const playlistSongs = useMemo(() => data?.items ?? [], [data?.items]);
|
||||||
|
|
||||||
|
const albumControlOverrides = useMemo<Partial<ItemControls>>(() => {
|
||||||
|
return {
|
||||||
|
onPlay: ({
|
||||||
|
item,
|
||||||
|
itemType,
|
||||||
|
playType,
|
||||||
|
}: DefaultItemControlProps & { playType: Play }) => {
|
||||||
|
if (!item) return;
|
||||||
|
const rowSongs = (item as PlaylistAlbumRow)._playlistSongs;
|
||||||
|
if (itemType === LibraryItem.ALBUM && rowSongs?.length) {
|
||||||
|
player.addToQueueByData(rowSongs, playType);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
player.addToQueueByFetch(item._serverId, [item.id], itemType, playType);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, [player]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setItemCount?.(totalAlbumCount);
|
||||||
|
}, [setItemCount, totalAlbumCount]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setListData?.(data?.items ?? []);
|
||||||
|
}, [data?.items, setListData]);
|
||||||
|
|
||||||
|
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({ enabled: true });
|
||||||
|
const { handleColumnReordered } = useItemListColumnReorder({
|
||||||
|
itemListKey: ItemListKey.PLAYLIST_ALBUM,
|
||||||
|
});
|
||||||
|
const { handleColumnResized } = useItemListColumnResize({
|
||||||
|
itemListKey: ItemListKey.PLAYLIST_ALBUM,
|
||||||
|
});
|
||||||
|
const { handleColumnReordered: handleDetailColumnReordered } = useItemListColumnReorder({
|
||||||
|
itemListKey: ItemListKey.PLAYLIST_ALBUM,
|
||||||
|
tableKey: 'detail',
|
||||||
|
});
|
||||||
|
const { handleColumnResized: handleDetailColumnResized } = useItemListColumnResize({
|
||||||
|
itemListKey: ItemListKey.PLAYLIST_ALBUM,
|
||||||
|
tableKey: 'detail',
|
||||||
|
});
|
||||||
|
const rows = useGridRows(LibraryItem.ALBUM, ItemListKey.PLAYLIST_ALBUM, grid.size);
|
||||||
|
|
||||||
|
const renderAlbumList = () => {
|
||||||
|
switch (display) {
|
||||||
|
case ListDisplayType.DETAIL:
|
||||||
|
return (
|
||||||
|
<ItemDetailList
|
||||||
|
enableHeader={detail?.enableHeader}
|
||||||
|
items={albumsToRender}
|
||||||
|
listKey={ItemListKey.PLAYLIST_ALBUM}
|
||||||
|
onColumnReordered={handleDetailColumnReordered}
|
||||||
|
onColumnResized={handleDetailColumnResized}
|
||||||
|
onScrollEnd={handleOnScrollEnd}
|
||||||
|
onSongRowDoubleClick={({ internalState, item }) => {
|
||||||
|
if (playlistSongs.length === 0) return;
|
||||||
|
internalState?.setSelected([item]);
|
||||||
|
player.addToQueueByData(playlistSongs, Play.NOW, item.id);
|
||||||
|
}}
|
||||||
|
overrideControls={albumControlOverrides}
|
||||||
|
scrollOffset={scrollOffset ?? 0}
|
||||||
|
songsByAlbumId={{}}
|
||||||
|
tableId="album-detail"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case ListDisplayType.GRID:
|
||||||
|
return (
|
||||||
|
<ItemGridList
|
||||||
|
data={albumsToRender}
|
||||||
|
enableExpansion
|
||||||
|
enableMultiSelect={enableGridMultiSelect}
|
||||||
|
gap={grid.itemGap}
|
||||||
|
initialTop={{
|
||||||
|
to: scrollOffset ?? 0,
|
||||||
|
type: 'offset',
|
||||||
|
}}
|
||||||
|
itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined}
|
||||||
|
itemType={LibraryItem.ALBUM}
|
||||||
|
onScrollEnd={handleOnScrollEnd}
|
||||||
|
overrideControls={albumControlOverrides}
|
||||||
|
rows={rows}
|
||||||
|
size={grid.size}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case ListDisplayType.TABLE:
|
||||||
|
return (
|
||||||
|
<ItemTableList
|
||||||
|
autoFitColumns={table.autoFitColumns}
|
||||||
|
CellComponent={ItemTableListColumn}
|
||||||
|
columns={table.columns}
|
||||||
|
data={albumsToRender}
|
||||||
|
enableAlternateRowColors={table.enableAlternateRowColors}
|
||||||
|
enableHeader={table.enableHeader}
|
||||||
|
enableHorizontalBorders={table.enableHorizontalBorders}
|
||||||
|
enableRowHoverHighlight={table.enableRowHoverHighlight}
|
||||||
|
enableSelection
|
||||||
|
enableVerticalBorders={table.enableVerticalBorders}
|
||||||
|
initialTop={{
|
||||||
|
to: scrollOffset ?? 0,
|
||||||
|
type: 'offset',
|
||||||
|
}}
|
||||||
|
itemType={LibraryItem.ALBUM}
|
||||||
|
onColumnReordered={handleColumnReordered}
|
||||||
|
onColumnResized={handleColumnResized}
|
||||||
|
onScrollEnd={handleOnScrollEnd}
|
||||||
|
overrideControls={albumControlOverrides}
|
||||||
|
size={table.size}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isPaginated) {
|
||||||
|
return (
|
||||||
|
<ItemListWithPagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
itemsPerPage={itemsPerPage}
|
||||||
|
onChange={onPageChange}
|
||||||
|
pageCount={albumPageCount}
|
||||||
|
totalItemCount={totalAlbumCount}
|
||||||
|
>
|
||||||
|
{renderAlbumList()}
|
||||||
|
</ItemListWithPagination>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderAlbumList();
|
||||||
|
};
|
||||||
@@ -2,14 +2,27 @@ import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
|
|||||||
import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
|
|
||||||
|
import { useItemListPagination } from '/@/renderer/components/item-list/item-list-pagination/use-item-list-pagination';
|
||||||
import { ItemListHandle } from '/@/renderer/components/item-list/types';
|
import { ItemListHandle } from '/@/renderer/components/item-list/types';
|
||||||
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 { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
||||||
|
import { PlaylistDetailAlbumView } from '/@/renderer/features/playlists/components/playlist-detail-album-view';
|
||||||
|
import { usePlaylistTrackList } from '/@/renderer/features/playlists/hooks/use-playlist-track-list';
|
||||||
import { useCurrentServer, useListSettings } from '/@/renderer/store';
|
import { useCurrentServer, useListSettings } from '/@/renderer/store';
|
||||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||||
import { PlaylistSongListQuery, PlaylistSongListResponse } from '/@/shared/types/domain-types';
|
import {
|
||||||
import { ItemListKey, ListDisplayType, TableColumn } from '/@/shared/types/types';
|
LibraryItem,
|
||||||
|
PlaylistSongListQuery,
|
||||||
|
PlaylistSongListResponse,
|
||||||
|
Song,
|
||||||
|
} from '/@/shared/types/domain-types';
|
||||||
|
import {
|
||||||
|
ItemListKey,
|
||||||
|
ListDisplayType,
|
||||||
|
ListPaginationType,
|
||||||
|
TableColumn,
|
||||||
|
} from '/@/shared/types/types';
|
||||||
|
|
||||||
const PlaylistDetailSongListTable = lazy(() =>
|
const PlaylistDetailSongListTable = lazy(() =>
|
||||||
import('/@/renderer/features/playlists/components/playlist-detail-song-list-table').then(
|
import('/@/renderer/features/playlists/components/playlist-detail-song-list-table').then(
|
||||||
@@ -38,7 +51,6 @@ const PlaylistDetailSongListGrid = lazy(() =>
|
|||||||
export const PlaylistDetailSongListContent = () => {
|
export const PlaylistDetailSongListContent = () => {
|
||||||
const { playlistId } = useParams() as { playlistId: string };
|
const { playlistId } = useParams() as { playlistId: string };
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const { setItemCount } = useListContext();
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const playlistSongsQuery = useSuspenseQuery(
|
const playlistSongsQuery = useSuspenseQuery(
|
||||||
@@ -50,18 +62,12 @@ export const PlaylistDetailSongListContent = () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
playlistSongsQuery.data?.totalRecordCount !== undefined &&
|
|
||||||
playlistSongsQuery.data.totalRecordCount !== null
|
|
||||||
) {
|
|
||||||
setItemCount?.(playlistSongsQuery.data.totalRecordCount);
|
|
||||||
}
|
|
||||||
}, [playlistSongsQuery.data?.totalRecordCount, setItemCount]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleRefresh = async (payload: { key: string }) => {
|
const handleRefresh = async (payload: { key: string }) => {
|
||||||
if (payload.key !== ItemListKey.PLAYLIST_SONG) {
|
if (
|
||||||
|
payload.key !== ItemListKey.PLAYLIST_SONG &&
|
||||||
|
payload.key !== ItemListKey.PLAYLIST_ALBUM
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +87,7 @@ export const PlaylistDetailSongListContent = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
eventEmitter.off('ITEM_LIST_REFRESH', handleRefresh);
|
eventEmitter.off('ITEM_LIST_REFRESH', handleRefresh);
|
||||||
};
|
};
|
||||||
}, [playlistId, queryClient, server.id]);
|
}, [playlistId, queryClient, server?.id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<Spinner container />}>
|
<Suspense fallback={<Spinner container />}>
|
||||||
@@ -92,13 +98,36 @@ export const PlaylistDetailSongListContent = () => {
|
|||||||
|
|
||||||
export type OverridePlaylistSongListQuery = Omit<Partial<PlaylistSongListQuery>, 'id'>;
|
export type OverridePlaylistSongListQuery = Omit<Partial<PlaylistSongListQuery>, 'id'>;
|
||||||
|
|
||||||
export const PlaylistDetailSongListView = ({ data }: { data: PlaylistSongListResponse }) => {
|
interface PlaylistDetailSongListViewProps {
|
||||||
|
data: PlaylistSongListResponse;
|
||||||
|
/** When provided, table/grid use this instead of computing from data (avoids duplicate filter/sort). */
|
||||||
|
items?: Song[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PlaylistDetailSongListView = ({ data, items }: PlaylistDetailSongListViewProps) => {
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const { display, table } = useListSettings(ItemListKey.PLAYLIST_SONG);
|
const { display, itemsPerPage, pagination, table } = useListSettings(ItemListKey.PLAYLIST_SONG);
|
||||||
|
const { currentPage, onChange: onPageChange } = useItemListPagination();
|
||||||
|
const isPaginated = pagination === ListPaginationType.PAGINATED;
|
||||||
|
|
||||||
|
const paginationProps = isPaginated
|
||||||
|
? {
|
||||||
|
currentPage,
|
||||||
|
itemsPerPage,
|
||||||
|
onPageChange,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
switch (display) {
|
switch (display) {
|
||||||
case ListDisplayType.GRID: {
|
case ListDisplayType.GRID: {
|
||||||
return <PlaylistDetailSongListGrid data={data} serverId={server.id} />;
|
return (
|
||||||
|
<PlaylistDetailSongListGrid
|
||||||
|
data={data}
|
||||||
|
items={items}
|
||||||
|
serverId={server.id}
|
||||||
|
{...paginationProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
case ListDisplayType.TABLE: {
|
case ListDisplayType.TABLE: {
|
||||||
return (
|
return (
|
||||||
@@ -111,8 +140,10 @@ export const PlaylistDetailSongListView = ({ data }: { data: PlaylistSongListRes
|
|||||||
enableHorizontalBorders={table.enableHorizontalBorders}
|
enableHorizontalBorders={table.enableHorizontalBorders}
|
||||||
enableRowHoverHighlight={table.enableRowHoverHighlight}
|
enableRowHoverHighlight={table.enableRowHoverHighlight}
|
||||||
enableVerticalBorders={table.enableVerticalBorders}
|
enableVerticalBorders={table.enableVerticalBorders}
|
||||||
|
items={items}
|
||||||
serverId={server.id}
|
serverId={server.id}
|
||||||
size={table.size}
|
size={table.size}
|
||||||
|
{...paginationProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -252,19 +283,33 @@ export const PlaylistDetailSongListEdit = ({ data }: { data: PlaylistSongListRes
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const PlaylistDetailSongList = ({ data }: { data: PlaylistSongListResponse }) => {
|
/** Track view: view mode uses centralized list derivation; edit mode uses local reorder state. */
|
||||||
|
const PlaylistDetailTrackView = ({ data }: { data: PlaylistSongListResponse }) => {
|
||||||
const { isSmartPlaylist, mode } = useListContext();
|
const { isSmartPlaylist, mode } = useListContext();
|
||||||
|
|
||||||
if (isSmartPlaylist) {
|
if (isSmartPlaylist) {
|
||||||
return <PlaylistDetailSongListView data={data} />;
|
return <PlaylistDetailTrackViewContent data={data} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (mode) {
|
if (mode === 'edit') {
|
||||||
case 'edit':
|
return <PlaylistDetailSongListEdit data={data} />;
|
||||||
return <PlaylistDetailSongListEdit data={data} />;
|
|
||||||
case 'view':
|
|
||||||
return <PlaylistDetailSongListView data={data} />;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return <PlaylistDetailTrackViewContent data={data} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Uses usePlaylistTrackList once and passes derived items to the list view. */
|
||||||
|
const PlaylistDetailTrackViewContent = ({ data }: { data: PlaylistSongListResponse }) => {
|
||||||
|
const { sortedAndFilteredSongs } = usePlaylistTrackList(data);
|
||||||
|
return <PlaylistDetailSongListView data={data} items={sortedAndFilteredSongs} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PlaylistDetailSongList = ({ data }: { data: PlaylistSongListResponse }) => {
|
||||||
|
const { displayMode } = useListContext();
|
||||||
|
|
||||||
|
if (displayMode === LibraryItem.ALBUM) {
|
||||||
|
return <PlaylistDetailAlbumView data={data} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <PlaylistDetailTrackView data={data} />;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useEffect } from 'react';
|
|||||||
import { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows';
|
import { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows';
|
||||||
import { useItemListScrollPersist } from '/@/renderer/components/item-list/helpers/use-item-list-scroll-persist';
|
import { useItemListScrollPersist } from '/@/renderer/components/item-list/helpers/use-item-list-scroll-persist';
|
||||||
import { ItemGridList } from '/@/renderer/components/item-list/item-grid-list/item-grid-list';
|
import { ItemGridList } from '/@/renderer/components/item-list/item-grid-list/item-grid-list';
|
||||||
|
import { ItemListWithPagination } from '/@/renderer/components/item-list/item-list-pagination/item-list-pagination';
|
||||||
import { ItemListGridComponentProps } from '/@/renderer/components/item-list/types';
|
import { ItemListGridComponentProps } from '/@/renderer/components/item-list/types';
|
||||||
import { useListContext } from '/@/renderer/context/list-context';
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
import { usePlaylistSongListFilters } from '/@/renderer/features/playlists/hooks/use-playlist-song-list-filters';
|
import { usePlaylistSongListFilters } from '/@/renderer/features/playlists/hooks/use-playlist-song-list-filters';
|
||||||
@@ -15,39 +16,52 @@ import {
|
|||||||
LibraryItem,
|
LibraryItem,
|
||||||
PlaylistSongListQuery,
|
PlaylistSongListQuery,
|
||||||
PlaylistSongListResponse,
|
PlaylistSongListResponse,
|
||||||
|
Song,
|
||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
interface PlaylistDetailSongListGridProps
|
interface PlaylistDetailSongListGridProps
|
||||||
extends Omit<ItemListGridComponentProps<PlaylistSongListQuery>, 'query'> {
|
extends Omit<ItemListGridComponentProps<PlaylistSongListQuery>, 'query'> {
|
||||||
|
currentPage?: number;
|
||||||
data: PlaylistSongListResponse;
|
data: PlaylistSongListResponse;
|
||||||
|
items?: Song[];
|
||||||
|
itemsPerPage?: number;
|
||||||
|
onPageChange?: (page: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlaylistDetailSongListGrid = forwardRef<any, PlaylistDetailSongListGridProps>(
|
export const PlaylistDetailSongListGrid = forwardRef<any, PlaylistDetailSongListGridProps>(
|
||||||
({ data, saveScrollOffset = true }) => {
|
({
|
||||||
|
currentPage,
|
||||||
|
data,
|
||||||
|
items: itemsProp,
|
||||||
|
itemsPerPage,
|
||||||
|
onPageChange,
|
||||||
|
saveScrollOffset = true,
|
||||||
|
}) => {
|
||||||
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({
|
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({
|
||||||
enabled: saveScrollOffset,
|
enabled: saveScrollOffset,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { searchTerm } = useSearchTermFilter();
|
const { searchTerm } = useSearchTermFilter();
|
||||||
const { query } = usePlaylistSongListFilters();
|
const { query } = usePlaylistSongListFilters();
|
||||||
const { setListData } = useListContext();
|
|
||||||
|
|
||||||
const songData = useMemo(() => {
|
|
||||||
let items = data?.items || [];
|
|
||||||
|
|
||||||
|
const songDataFromData = useMemo(() => {
|
||||||
|
let list = data?.items || [];
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
items = searchLibraryItems(items, searchTerm, LibraryItem.SONG);
|
list = searchLibraryItems(list, searchTerm, LibraryItem.SONG);
|
||||||
|
return list;
|
||||||
}
|
}
|
||||||
|
return sortSongList(list, query.sortBy, query.sortOrder);
|
||||||
return sortSongList(items, query.sortBy, query.sortOrder);
|
|
||||||
}, [data?.items, searchTerm, query.sortBy, query.sortOrder]);
|
}, [data?.items, searchTerm, query.sortBy, query.sortOrder]);
|
||||||
|
|
||||||
|
const { setListData } = useListContext();
|
||||||
|
const songData = itemsProp ?? songDataFromData;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (setListData) {
|
if (itemsProp == null && setListData) {
|
||||||
setListData(songData);
|
setListData(songDataFromData);
|
||||||
}
|
}
|
||||||
}, [songData, setListData]);
|
}, [itemsProp, songDataFromData, setListData]);
|
||||||
|
|
||||||
const gridProps = useListSettings(ItemListKey.PLAYLIST_SONG).grid;
|
const gridProps = useListSettings(ItemListKey.PLAYLIST_SONG).grid;
|
||||||
|
|
||||||
@@ -58,9 +72,22 @@ export const PlaylistDetailSongListGrid = forwardRef<any, PlaylistDetailSongList
|
|||||||
);
|
);
|
||||||
const { enableGridMultiSelect } = useGeneralSettings();
|
const { enableGridMultiSelect } = useGeneralSettings();
|
||||||
|
|
||||||
return (
|
const isPaginated =
|
||||||
|
typeof currentPage === 'number' &&
|
||||||
|
typeof itemsPerPage === 'number' &&
|
||||||
|
typeof onPageChange === 'function';
|
||||||
|
const totalCount = songData.length;
|
||||||
|
const pageCount = Math.max(1, Math.ceil(totalCount / (itemsPerPage ?? 1)));
|
||||||
|
const paginatedData = useMemo(() => {
|
||||||
|
if (!isPaginated || currentPage == null || itemsPerPage == null) return songData;
|
||||||
|
const start = currentPage * itemsPerPage;
|
||||||
|
return songData.slice(start, start + itemsPerPage);
|
||||||
|
}, [currentPage, isPaginated, itemsPerPage, songData]);
|
||||||
|
const dataToRender = isPaginated ? paginatedData : songData;
|
||||||
|
|
||||||
|
const grid = (
|
||||||
<ItemGridList
|
<ItemGridList
|
||||||
data={songData}
|
data={dataToRender}
|
||||||
enableMultiSelect={enableGridMultiSelect}
|
enableMultiSelect={enableGridMultiSelect}
|
||||||
gap={gridProps.itemGap}
|
gap={gridProps.itemGap}
|
||||||
initialTop={{
|
initialTop={{
|
||||||
@@ -74,5 +101,21 @@ export const PlaylistDetailSongListGrid = forwardRef<any, PlaylistDetailSongList
|
|||||||
size={gridProps.size}
|
size={gridProps.size}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isPaginated && itemsPerPage != null) {
|
||||||
|
return (
|
||||||
|
<ItemListWithPagination
|
||||||
|
currentPage={currentPage!}
|
||||||
|
itemsPerPage={itemsPerPage}
|
||||||
|
onChange={onPageChange!}
|
||||||
|
pageCount={pageCount}
|
||||||
|
totalItemCount={totalCount}
|
||||||
|
>
|
||||||
|
{grid}
|
||||||
|
</ItemListWithPagination>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return grid;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,19 +5,27 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
|
|
||||||
import i18n from '/@/i18n/i18n';
|
import i18n from '/@/i18n/i18n';
|
||||||
import { PLAYLIST_SONG_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns';
|
import {
|
||||||
|
ALBUM_TABLE_COLUMNS,
|
||||||
|
PLAYLIST_SONG_TABLE_COLUMNS,
|
||||||
|
SONG_TABLE_COLUMNS,
|
||||||
|
} from '/@/renderer/components/item-list/item-table-list/default-columns';
|
||||||
import { useListContext } from '/@/renderer/context/list-context';
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
|
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
|
||||||
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
||||||
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
||||||
import { ListDisplayTypeToggleButton } from '/@/renderer/features/shared/components/list-display-type-toggle-button';
|
import { ListDisplayTypeToggleButton } from '/@/renderer/features/shared/components/list-display-type-toggle-button';
|
||||||
import { ListRefreshButton } from '/@/renderer/features/shared/components/list-refresh-button';
|
import { ListRefreshButton } from '/@/renderer/features/shared/components/list-refresh-button';
|
||||||
import { ListSearchInput } from '/@/renderer/features/shared/components/list-search-input';
|
|
||||||
import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown';
|
import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown';
|
||||||
import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
|
import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
|
||||||
import { MoreButton } from '/@/renderer/features/shared/components/more-button';
|
import { MoreButton } from '/@/renderer/features/shared/components/more-button';
|
||||||
import { useContainerQuery } from '/@/renderer/hooks';
|
import { useContainerQuery } from '/@/renderer/hooks';
|
||||||
import { useCurrentServerId } from '/@/renderer/store';
|
import {
|
||||||
|
PlaylistTarget,
|
||||||
|
useCurrentServerId,
|
||||||
|
usePlaylistTarget,
|
||||||
|
useSettingsStoreActions,
|
||||||
|
} from '/@/renderer/store';
|
||||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { Button } from '/@/shared/components/button/button';
|
import { Button } from '/@/shared/components/button/button';
|
||||||
import { Divider } from '/@/shared/components/divider/divider';
|
import { Divider } from '/@/shared/components/divider/divider';
|
||||||
@@ -37,8 +45,10 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||||||
isSmartPlaylist,
|
isSmartPlaylist,
|
||||||
}: PlaylistDetailSongListHeaderFiltersProps) => {
|
}: PlaylistDetailSongListHeaderFiltersProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { mode, setMode } = useListContext();
|
const { listKey: listKeyFromContext, mode, setMode } = useListContext();
|
||||||
const { playlistId } = useParams() as { playlistId: string };
|
const { playlistId } = useParams() as { playlistId: string };
|
||||||
|
const playlistTarget = usePlaylistTarget();
|
||||||
|
const { setPlaylistBehavior } = useSettingsStoreActions();
|
||||||
const serverId = useCurrentServerId();
|
const serverId = useCurrentServerId();
|
||||||
|
|
||||||
const detailQuery = useQuery(playlistsQueries.detail({ query: { id: playlistId }, serverId }));
|
const detailQuery = useQuery(playlistsQueries.detail({ query: { id: playlistId }, serverId }));
|
||||||
@@ -55,9 +65,25 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const listKey =
|
||||||
|
listKeyFromContext ??
|
||||||
|
(playlistTarget === PlaylistTarget.ALBUM
|
||||||
|
? ItemListKey.PLAYLIST_ALBUM
|
||||||
|
: ItemListKey.PLAYLIST_SONG);
|
||||||
|
const isAlbumMode = listKey === ItemListKey.PLAYLIST_ALBUM;
|
||||||
|
const toggleChoice = isAlbumMode
|
||||||
|
? t('entity.album', { count: 2, postProcess: 'titleCase' })
|
||||||
|
: t('entity.track', { count: 2, postProcess: 'titleCase' });
|
||||||
|
|
||||||
|
const handleToggleDisplayMode = useCallback(() => {
|
||||||
|
setPlaylistBehavior(
|
||||||
|
playlistTarget === PlaylistTarget.ALBUM ? PlaylistTarget.TRACK : PlaylistTarget.ALBUM,
|
||||||
|
);
|
||||||
|
}, [playlistTarget, setPlaylistBehavior]);
|
||||||
|
|
||||||
const { ref: containerRef, ...breakpoints } = useContainerQuery();
|
const { ref: containerRef, ...breakpoints } = useContainerQuery();
|
||||||
|
|
||||||
const isViewEditMode = !isSmartPlaylist && breakpoints.isSm;
|
const isViewEditMode = !isSmartPlaylist && (breakpoints.isSm || isAlbumMode);
|
||||||
const isEditMode = mode === 'edit';
|
const isEditMode = mode === 'edit';
|
||||||
|
|
||||||
const [collapsed, setCollapsed] = useLocalStorage<boolean>({
|
const [collapsed, setCollapsed] = useLocalStorage<boolean>({
|
||||||
@@ -68,6 +94,14 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||||||
return (
|
return (
|
||||||
<Flex justify="space-between" ref={containerRef}>
|
<Flex justify="space-between" ref={containerRef}>
|
||||||
<Group gap="sm" w="100%">
|
<Group gap="sm" w="100%">
|
||||||
|
<Button
|
||||||
|
leftSection={<Icon icon="arrowLeftRight" />}
|
||||||
|
onClick={handleToggleDisplayMode}
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
|
{toggleChoice}
|
||||||
|
</Button>
|
||||||
|
<Divider orientation="vertical" />
|
||||||
<ListSortByDropdown
|
<ListSortByDropdown
|
||||||
defaultSortByValue={SongListSort.ID}
|
defaultSortByValue={SongListSort.ID}
|
||||||
disabled={isEditMode}
|
disabled={isEditMode}
|
||||||
@@ -80,8 +114,7 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||||||
disabled={isEditMode}
|
disabled={isEditMode}
|
||||||
listKey={ItemListKey.PLAYLIST_SONG}
|
listKey={ItemListKey.PLAYLIST_SONG}
|
||||||
/>
|
/>
|
||||||
{!collapsed && <ListSearchInput />}
|
<ListRefreshButton disabled={isEditMode} listKey={listKey} />
|
||||||
<ListRefreshButton disabled={isEditMode} listKey={ItemListKey.PLAYLIST_SONG} />
|
|
||||||
<MoreButton onClick={handleMore} />
|
<MoreButton onClick={handleMore} />
|
||||||
</Group>
|
</Group>
|
||||||
<Group gap="sm" wrap="nowrap">
|
<Group gap="sm" wrap="nowrap">
|
||||||
@@ -109,11 +142,25 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||||||
variant="subtle"
|
variant="subtle"
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<ListDisplayTypeToggleButton listKey={ItemListKey.PLAYLIST_SONG} />
|
<ListDisplayTypeToggleButton enableDetail={isAlbumMode} listKey={listKey} />
|
||||||
<ListConfigMenu
|
{isAlbumMode ? (
|
||||||
listKey={ItemListKey.PLAYLIST_SONG}
|
<ListConfigMenu
|
||||||
tableColumnsData={PLAYLIST_SONG_TABLE_COLUMNS}
|
detailConfig={{
|
||||||
/>
|
optionsConfig: {
|
||||||
|
autoFitColumns: { hidden: true },
|
||||||
|
},
|
||||||
|
tableColumnsData: SONG_TABLE_COLUMNS,
|
||||||
|
tableKey: 'detail',
|
||||||
|
}}
|
||||||
|
listKey={listKey}
|
||||||
|
tableColumnsData={ALBUM_TABLE_COLUMNS}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ListConfigMenu
|
||||||
|
listKey={listKey}
|
||||||
|
tableColumnsData={PLAYLIST_SONG_TABLE_COLUMNS}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export const PlaylistDetailSongListHeader = ({
|
|||||||
</PageHeader>
|
</PageHeader>
|
||||||
) : (
|
) : (
|
||||||
<LibraryHeader
|
<LibraryHeader
|
||||||
|
compact
|
||||||
imageUrl={imageUrl}
|
imageUrl={imageUrl}
|
||||||
item={{
|
item={{
|
||||||
imageId: detailQuery?.data?.imageId,
|
imageId: detailQuery?.data?.imageId,
|
||||||
@@ -101,6 +102,7 @@ export const PlaylistDetailSongListHeader = ({
|
|||||||
type: LibraryItem.PLAYLIST,
|
type: LibraryItem.PLAYLIST,
|
||||||
}}
|
}}
|
||||||
title={detailQuery?.data?.name || ''}
|
title={detailQuery?.data?.name || ''}
|
||||||
|
topRight={<ListSearchInput />}
|
||||||
>
|
>
|
||||||
<LibraryHeaderMenu
|
<LibraryHeaderMenu
|
||||||
onPlay={(type) => handlePlay(type)}
|
onPlay={(type) => handlePlay(type)}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useEffect } from 'react';
|
|||||||
import { useItemListColumnReorder } from '/@/renderer/components/item-list/helpers/use-item-list-column-reorder';
|
import { useItemListColumnReorder } from '/@/renderer/components/item-list/helpers/use-item-list-column-reorder';
|
||||||
import { useItemListColumnResize } from '/@/renderer/components/item-list/helpers/use-item-list-column-resize';
|
import { useItemListColumnResize } from '/@/renderer/components/item-list/helpers/use-item-list-column-resize';
|
||||||
import { useItemListScrollPersist } from '/@/renderer/components/item-list/helpers/use-item-list-scroll-persist';
|
import { useItemListScrollPersist } from '/@/renderer/components/item-list/helpers/use-item-list-scroll-persist';
|
||||||
|
import { ItemListWithPagination } from '/@/renderer/components/item-list/item-list-pagination/item-list-pagination';
|
||||||
import { ItemTableList } from '/@/renderer/components/item-list/item-table-list/item-table-list';
|
import { ItemTableList } from '/@/renderer/components/item-list/item-table-list/item-table-list';
|
||||||
import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
||||||
import { ItemControls, ItemListTableComponentProps } from '/@/renderer/components/item-list/types';
|
import { ItemControls, ItemListTableComponentProps } from '/@/renderer/components/item-list/types';
|
||||||
@@ -24,7 +25,11 @@ import { ItemListKey, Play } from '/@/shared/types/types';
|
|||||||
|
|
||||||
interface PlaylistDetailSongListTableProps
|
interface PlaylistDetailSongListTableProps
|
||||||
extends Omit<ItemListTableComponentProps<PlaylistSongListQuery>, 'query'> {
|
extends Omit<ItemListTableComponentProps<PlaylistSongListQuery>, 'query'> {
|
||||||
|
currentPage?: number;
|
||||||
data: PlaylistSongListResponse;
|
data: PlaylistSongListResponse;
|
||||||
|
items?: Song[];
|
||||||
|
itemsPerPage?: number;
|
||||||
|
onPageChange?: (page: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongListTableProps>(
|
export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongListTableProps>(
|
||||||
@@ -32,6 +37,7 @@ export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongLis
|
|||||||
{
|
{
|
||||||
autoFitColumns = false,
|
autoFitColumns = false,
|
||||||
columns,
|
columns,
|
||||||
|
currentPage,
|
||||||
data,
|
data,
|
||||||
enableAlternateRowColors = false,
|
enableAlternateRowColors = false,
|
||||||
enableHeader = true,
|
enableHeader = true,
|
||||||
@@ -39,6 +45,9 @@ export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongLis
|
|||||||
enableRowHoverHighlight = true,
|
enableRowHoverHighlight = true,
|
||||||
enableSelection = true,
|
enableSelection = true,
|
||||||
enableVerticalBorders = false,
|
enableVerticalBorders = false,
|
||||||
|
items: itemsProp,
|
||||||
|
itemsPerPage,
|
||||||
|
onPageChange,
|
||||||
saveScrollOffset = true,
|
saveScrollOffset = true,
|
||||||
size = 'default',
|
size = 'default',
|
||||||
},
|
},
|
||||||
@@ -58,23 +67,24 @@ export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongLis
|
|||||||
|
|
||||||
const { searchTerm } = useSearchTermFilter();
|
const { searchTerm } = useSearchTermFilter();
|
||||||
const { query } = usePlaylistSongListFilters();
|
const { query } = usePlaylistSongListFilters();
|
||||||
const { setListData } = useListContext();
|
|
||||||
|
|
||||||
const songData = useMemo(() => {
|
|
||||||
let items = data?.items || [];
|
|
||||||
|
|
||||||
|
const songDataFromData = useMemo(() => {
|
||||||
|
let list = data?.items || [];
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
items = searchLibraryItems(items, searchTerm, LibraryItem.SONG);
|
list = searchLibraryItems(list, searchTerm, LibraryItem.SONG);
|
||||||
|
return list;
|
||||||
}
|
}
|
||||||
|
return sortSongList(list, query.sortBy, query.sortOrder);
|
||||||
return sortSongList(items, query.sortBy, query.sortOrder);
|
|
||||||
}, [data?.items, searchTerm, query.sortBy, query.sortOrder]);
|
}, [data?.items, searchTerm, query.sortBy, query.sortOrder]);
|
||||||
|
|
||||||
|
const { setListData } = useListContext();
|
||||||
|
const songData = itemsProp ?? songDataFromData;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (setListData) {
|
if (itemsProp == null && setListData) {
|
||||||
setListData(songData);
|
setListData(songDataFromData);
|
||||||
}
|
}
|
||||||
}, [songData, setListData]);
|
}, [itemsProp, songDataFromData, setListData]);
|
||||||
|
|
||||||
const player = usePlayer();
|
const player = usePlayer();
|
||||||
|
|
||||||
@@ -107,13 +117,26 @@ export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongLis
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
const isPaginated =
|
||||||
|
typeof currentPage === 'number' &&
|
||||||
|
typeof itemsPerPage === 'number' &&
|
||||||
|
typeof onPageChange === 'function';
|
||||||
|
const totalCount = songData.length;
|
||||||
|
const pageCount = Math.max(1, Math.ceil(totalCount / (itemsPerPage ?? 1)));
|
||||||
|
const paginatedData = useMemo(() => {
|
||||||
|
if (!isPaginated || currentPage == null || itemsPerPage == null) return songData;
|
||||||
|
const start = currentPage * itemsPerPage;
|
||||||
|
return songData.slice(start, start + itemsPerPage);
|
||||||
|
}, [isPaginated, currentPage, itemsPerPage, songData]);
|
||||||
|
const dataToRender = isPaginated ? paginatedData : songData;
|
||||||
|
|
||||||
|
const table = (
|
||||||
<ItemTableList
|
<ItemTableList
|
||||||
activeRowId={currentSong?.id}
|
activeRowId={currentSong?.id}
|
||||||
autoFitColumns={autoFitColumns}
|
autoFitColumns={autoFitColumns}
|
||||||
CellComponent={ItemTableListColumn}
|
CellComponent={ItemTableListColumn}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={songData}
|
data={dataToRender}
|
||||||
enableAlternateRowColors={enableAlternateRowColors}
|
enableAlternateRowColors={enableAlternateRowColors}
|
||||||
enableExpansion={false}
|
enableExpansion={false}
|
||||||
enableHeader={enableHeader}
|
enableHeader={enableHeader}
|
||||||
@@ -135,6 +158,22 @@ export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongLis
|
|||||||
size={size}
|
size={size}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isPaginated && itemsPerPage != null) {
|
||||||
|
return (
|
||||||
|
<ItemListWithPagination
|
||||||
|
currentPage={currentPage!}
|
||||||
|
itemsPerPage={itemsPerPage}
|
||||||
|
onChange={onPageChange!}
|
||||||
|
pageCount={pageCount}
|
||||||
|
totalItemCount={totalCount}
|
||||||
|
>
|
||||||
|
{table}
|
||||||
|
</ItemListWithPagination>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return table;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
|
import { usePlaylistSongListFilters } from '/@/renderer/features/playlists/hooks/use-playlist-song-list-filters';
|
||||||
|
import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter';
|
||||||
|
import { searchLibraryItems } from '/@/renderer/features/shared/utils';
|
||||||
|
import { sortSongList } from '/@/shared/api/utils';
|
||||||
|
import { LibraryItem, PlaylistSongListResponse, Song } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
export function usePlaylistTrackList(data: PlaylistSongListResponse | undefined): {
|
||||||
|
sortedAndFilteredSongs: Song[];
|
||||||
|
totalCount: number;
|
||||||
|
} {
|
||||||
|
const { setItemCount, setListData } = useListContext();
|
||||||
|
const { searchTerm } = useSearchTermFilter();
|
||||||
|
const { query } = usePlaylistSongListFilters();
|
||||||
|
|
||||||
|
const sortedAndFilteredSongs = useMemo(() => {
|
||||||
|
const raw = data?.items ?? [];
|
||||||
|
|
||||||
|
if (searchTerm) {
|
||||||
|
return searchLibraryItems(raw, searchTerm, LibraryItem.SONG);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortSongList(raw, query.sortBy, query.sortOrder);
|
||||||
|
}, [data?.items, searchTerm, query.sortBy, query.sortOrder]);
|
||||||
|
|
||||||
|
const totalCount = sortedAndFilteredSongs.length;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setListData?.(sortedAndFilteredSongs);
|
||||||
|
setItemCount?.(totalCount);
|
||||||
|
}, [sortedAndFilteredSongs, totalCount, setListData, setItemCount]);
|
||||||
|
|
||||||
|
return { sortedAndFilteredSongs, totalCount };
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ import { AnimatedPage } from '/@/renderer/features/shared/components/animated-pa
|
|||||||
import { JsonPreview } from '/@/renderer/features/shared/components/json-preview';
|
import { JsonPreview } from '/@/renderer/features/shared/components/json-preview';
|
||||||
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
|
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { useCurrentServer } from '/@/renderer/store';
|
import { PlaylistTarget, useCurrentServer, usePlaylistTarget } from '/@/renderer/store';
|
||||||
import { Button } from '/@/shared/components/button/button';
|
import { Button } from '/@/shared/components/button/button';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
@@ -29,7 +29,7 @@ import { Spinner } from '/@/shared/components/spinner/spinner';
|
|||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { toast } from '/@/shared/components/toast/toast';
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
import { ServerType, SongListSort } from '/@/shared/types/domain-types';
|
import { LibraryItem, ServerType, SongListSort } from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
interface PlaylistQueryEditorProps {
|
interface PlaylistQueryEditorProps {
|
||||||
@@ -154,14 +154,17 @@ const PlaylistQueryEditor = ({
|
|||||||
}, [detailQuery?.data?.rules?.order, detailQuery?.data?.rules?.sort]);
|
}, [detailQuery?.data?.rules?.order, detailQuery?.data?.rules?.sort]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="query-editor-container">
|
<div
|
||||||
<Stack gap={0} h="100%" mah="30dvh" p="md" w="100%">
|
className="query-editor-container"
|
||||||
<Group justify="space-between" pb="md" wrap="nowrap">
|
style={{ borderTop: '1px solid var(--theme-colors-border)' }}
|
||||||
|
>
|
||||||
|
<Stack gap={0} h="100%" mah="30dvh" p="sm" w="100%">
|
||||||
|
<Group justify="space-between" wrap="nowrap">
|
||||||
<Group gap="sm" wrap="nowrap">
|
<Group gap="sm" wrap="nowrap">
|
||||||
<Button
|
<Button
|
||||||
leftSection={
|
leftSection={
|
||||||
<Icon
|
<Icon
|
||||||
icon={isQueryBuilderExpanded ? 'arrowUpS' : 'arrowDownS'}
|
icon={isQueryBuilderExpanded ? 'arrowDownS' : 'arrowUpS'}
|
||||||
size="lg"
|
size="lg"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -396,6 +399,12 @@ const PlaylistDetailSongListRoute = () => {
|
|||||||
setIsQueryBuilderExpanded(true);
|
setIsQueryBuilderExpanded(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const playlistTarget = usePlaylistTarget();
|
||||||
|
const displayMode: LibraryItem.ALBUM | LibraryItem.SONG =
|
||||||
|
playlistTarget === PlaylistTarget.ALBUM ? LibraryItem.ALBUM : LibraryItem.SONG;
|
||||||
|
const listKey =
|
||||||
|
displayMode === LibraryItem.ALBUM ? ItemListKey.PLAYLIST_ALBUM : ItemListKey.PLAYLIST_SONG;
|
||||||
|
|
||||||
const [itemCount, setItemCount] = useState<number | undefined>(undefined);
|
const [itemCount, setItemCount] = useState<number | undefined>(undefined);
|
||||||
const [listData, setListData] = useState<unknown[]>([]);
|
const [listData, setListData] = useState<unknown[]>([]);
|
||||||
const [mode, setMode] = useState<'edit' | 'view'>('view');
|
const [mode, setMode] = useState<'edit' | 'view'>('view');
|
||||||
@@ -403,17 +412,19 @@ const PlaylistDetailSongListRoute = () => {
|
|||||||
const providerValue = useMemo(() => {
|
const providerValue = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
customFilters: undefined,
|
customFilters: undefined,
|
||||||
|
displayMode,
|
||||||
id: playlistId,
|
id: playlistId,
|
||||||
isSmartPlaylist,
|
isSmartPlaylist,
|
||||||
itemCount,
|
itemCount,
|
||||||
listData,
|
listData,
|
||||||
|
listKey,
|
||||||
mode,
|
mode,
|
||||||
pageKey: ItemListKey.PLAYLIST_SONG,
|
pageKey: listKey,
|
||||||
setItemCount,
|
setItemCount,
|
||||||
setListData,
|
setListData,
|
||||||
setMode,
|
setMode,
|
||||||
};
|
};
|
||||||
}, [playlistId, isSmartPlaylist, itemCount, listData, mode]);
|
}, [playlistId, isSmartPlaylist, displayMode, listKey, itemCount, listData, mode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedPage key={`playlist-detail-songList-${playlistId}`}>
|
<AnimatedPage key={`playlist-detail-songList-${playlistId}`}>
|
||||||
@@ -429,6 +440,10 @@ const PlaylistDetailSongListRoute = () => {
|
|||||||
onDelete={() => openDeletePlaylistModal()}
|
onDelete={() => openDeletePlaylistModal()}
|
||||||
onToggleQueryBuilder={handleToggleShowQueryBuilder}
|
onToggleQueryBuilder={handleToggleShowQueryBuilder}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Suspense fallback={<Spinner container />}>
|
||||||
|
<PlaylistDetailSongListContent />
|
||||||
|
</Suspense>
|
||||||
{(isSmartPlaylist || showQueryBuilder) && (
|
{(isSmartPlaylist || showQueryBuilder) && (
|
||||||
<PlaylistQueryEditor
|
<PlaylistQueryEditor
|
||||||
createPlaylistMutation={createPlaylistMutation}
|
createPlaylistMutation={createPlaylistMutation}
|
||||||
@@ -441,9 +456,6 @@ const PlaylistDetailSongListRoute = () => {
|
|||||||
queryBuilderRef={queryBuilderRef}
|
queryBuilderRef={queryBuilderRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Suspense fallback={<Spinner container />}>
|
|
||||||
<PlaylistDetailSongListContent />
|
|
||||||
</Suspense>
|
|
||||||
</ListContext.Provider>
|
</ListContext.Provider>
|
||||||
</AnimatedPage>
|
</AnimatedPage>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,75 @@
|
|||||||
import { nanoid } from 'nanoid/non-secure';
|
import { nanoid } from 'nanoid/non-secure';
|
||||||
|
|
||||||
import { NDSongQueryFields } from '/@/shared/api/navidrome/navidrome-types';
|
import { NDSongQueryFields } from '/@/shared/api/navidrome/navidrome-types';
|
||||||
|
import { Album, LibraryItem, Song } from '/@/shared/types/domain-types';
|
||||||
import { QueryBuilderGroup } from '/@/shared/types/types';
|
import { QueryBuilderGroup } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
export type PlaylistAlbumRow = Album & { _playlistSongs?: Song[] };
|
||||||
|
|
||||||
|
export function playlistSongsToAlbums(songs: Song[]): PlaylistAlbumRow[] {
|
||||||
|
if (songs.length === 0) return [];
|
||||||
|
|
||||||
|
const rows: PlaylistAlbumRow[] = [];
|
||||||
|
let group: Song[] = [songs[0]];
|
||||||
|
let prevAlbumId = songs[0].albumId;
|
||||||
|
|
||||||
|
const pushRow = (song: Song, groupSongs: Song[]) => {
|
||||||
|
rows.push({
|
||||||
|
_itemType: LibraryItem.ALBUM,
|
||||||
|
_playlistSongs: groupSongs,
|
||||||
|
_serverId: song._serverId,
|
||||||
|
_serverType: song._serverType,
|
||||||
|
albumArtistName: song.albumArtistName,
|
||||||
|
albumArtists: song.albumArtists,
|
||||||
|
artists: song.artists,
|
||||||
|
comment: song.comment,
|
||||||
|
createdAt: song.createdAt,
|
||||||
|
duration: null,
|
||||||
|
explicitStatus: song.explicitStatus,
|
||||||
|
genres: song.genres,
|
||||||
|
id: song.albumId,
|
||||||
|
imageId: song.imageId,
|
||||||
|
imageUrl: song.imageUrl,
|
||||||
|
isCompilation: song.compilation,
|
||||||
|
lastPlayedAt: song.lastPlayedAt,
|
||||||
|
mbzId: null,
|
||||||
|
mbzReleaseGroupId: null,
|
||||||
|
name: song.album ?? '',
|
||||||
|
originalDate: null,
|
||||||
|
originalYear: null,
|
||||||
|
participants: song.participants,
|
||||||
|
playCount: null,
|
||||||
|
recordLabels: [],
|
||||||
|
releaseDate: song.releaseDate,
|
||||||
|
releaseType: null,
|
||||||
|
releaseTypes: [],
|
||||||
|
releaseYear: song.releaseYear,
|
||||||
|
size: null,
|
||||||
|
songCount: null,
|
||||||
|
sortName: song.album ?? '',
|
||||||
|
tags: song.tags,
|
||||||
|
updatedAt: song.updatedAt,
|
||||||
|
userFavorite: false,
|
||||||
|
userRating: null,
|
||||||
|
version: null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 1; i < songs.length; i++) {
|
||||||
|
const song = songs[i];
|
||||||
|
if (song.albumId === prevAlbumId) {
|
||||||
|
group.push(song);
|
||||||
|
} else {
|
||||||
|
pushRow(group[0], group);
|
||||||
|
group = [song];
|
||||||
|
prevAlbumId = song.albumId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pushRow(group[0], group);
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
export const parseQueryBuilderChildren = (groups: QueryBuilderGroup[], data: any[]) => {
|
export const parseQueryBuilderChildren = (groups: QueryBuilderGroup[], data: any[]) => {
|
||||||
if (groups.length === 0) {
|
if (groups.length === 0) {
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import { Switch } from '/@/shared/components/switch/switch';
|
|||||||
export const AnalyticsSettings = memo(() => {
|
export const AnalyticsSettings = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleToggleAnalytics = (disable: boolean) => {
|
const handleSetSendAnalytics = (send: boolean) => {
|
||||||
if (disable) {
|
if (send) {
|
||||||
localStorage.setItem('umami.disabled', '1');
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem('umami.disabled');
|
localStorage.removeItem('umami.disabled');
|
||||||
|
} else {
|
||||||
|
localStorage.setItem('umami.disabled', '1');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -22,12 +22,13 @@ export const AnalyticsSettings = memo(() => {
|
|||||||
{
|
{
|
||||||
control: (
|
control: (
|
||||||
<Switch
|
<Switch
|
||||||
defaultChecked={localStorage.getItem('umami.disabled') === '1'}
|
aria-label={t('setting.analyticsEnable', { postProcess: 'sentenceCase' })}
|
||||||
onChange={(e) => handleToggleAnalytics(e.currentTarget.checked)}
|
defaultChecked={localStorage.getItem('umami.disabled') !== '1'}
|
||||||
|
onChange={(e) => handleSetSendAnalytics(e.currentTarget.checked)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
description: t('setting.analyticsDisable_description', { postProcess: 'sentenceCase' }),
|
description: t('setting.analyticsEnable_description', { postProcess: 'sentenceCase' }),
|
||||||
title: t('setting.analyticsDisable', { postProcess: 'sentenceCase' }),
|
title: t('setting.analyticsEnable', { postProcess: 'sentenceCase' }),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -109,9 +109,7 @@ export const ThemeSettings = memo(() => {
|
|||||||
localSettings.themeSet(
|
localSettings.themeSet(
|
||||||
e.currentTarget.checked
|
e.currentTarget.checked
|
||||||
? 'system'
|
? 'system'
|
||||||
: settings.theme === AppTheme.DEFAULT_DARK
|
: (getAppTheme(settings.theme).mode ?? 'dark'),
|
||||||
? 'dark'
|
|
||||||
: 'light',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -138,7 +136,7 @@ export const ThemeSettings = memo(() => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const colorScheme = theme === AppTheme.DEFAULT_DARK ? 'dark' : 'light';
|
const colorScheme = getAppTheme(theme).mode ?? 'dark';
|
||||||
|
|
||||||
setColorScheme(colorScheme);
|
setColorScheme(colorScheme);
|
||||||
|
|
||||||
|
|||||||
@@ -71,26 +71,27 @@ export const UpdateSettings = memo(() => {
|
|||||||
{
|
{
|
||||||
control: (
|
control: (
|
||||||
<Switch
|
<Switch
|
||||||
aria-label="Disable automatic updates"
|
aria-label={t('setting.automaticUpdates', { postProcess: 'sentenceCase' })}
|
||||||
defaultChecked={settings.disableAutoUpdate}
|
defaultChecked={!settings.disableAutoUpdate}
|
||||||
disabled={disableAutoUpdates()}
|
disabled={disableAutoUpdates()}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (!e) return;
|
if (!e) return;
|
||||||
localSettings?.set('disable_auto_updates', e.currentTarget.checked);
|
const enabled = e.currentTarget.checked;
|
||||||
|
localSettings?.set('disable_auto_updates', !enabled);
|
||||||
setSettings({
|
setSettings({
|
||||||
window: {
|
window: {
|
||||||
disableAutoUpdate: e.currentTarget.checked,
|
disableAutoUpdate: !enabled,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
description: t('setting.disableAutomaticUpdates', {
|
description: t('setting.automaticUpdates', {
|
||||||
context: 'description',
|
context: 'description',
|
||||||
postProcess: 'sentenceCase',
|
postProcess: 'sentenceCase',
|
||||||
}),
|
}),
|
||||||
isHidden: disableAutoUpdates(),
|
isHidden: disableAutoUpdates(),
|
||||||
title: t('setting.disableAutomaticUpdates', { postProcess: 'sentenceCase' }),
|
title: t('setting.automaticUpdates', { postProcess: 'sentenceCase' }),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
.top-right {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--theme-spacing-lg);
|
||||||
|
right: var(--theme-spacing-md);
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
.library-header {
|
.library-header {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -56,6 +63,52 @@
|
|||||||
height: 250px;
|
height: 250px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.compact {
|
||||||
|
min-height: unset;
|
||||||
|
padding: var(--theme-spacing-md) var(--theme-spacing-xs);
|
||||||
|
|
||||||
|
:global(.item-image-placeholder) {
|
||||||
|
width: 250px !important;
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: 250px !important;
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (min-width: $mantine-breakpoint-sm) {
|
||||||
|
grid-template-columns: 200px minmax(0, 1fr);
|
||||||
|
min-height: unset;
|
||||||
|
padding: var(--theme-spacing-md) var(--theme-spacing-sm);
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: 200px !important;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.item-image-placeholder) {
|
||||||
|
width: 200px !important;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (min-width: $mantine-breakpoint-lg) {
|
||||||
|
grid-template-columns: 200px minmax(0, 1fr);
|
||||||
|
padding: var(--theme-spacing-md) var(--theme-spacing-md);
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: 200px !important;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.item-image-placeholder) {
|
||||||
|
width: 200px !important;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-section {
|
.image-section {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { Play } from '/@/shared/types/types';
|
|||||||
|
|
||||||
interface LibraryHeaderProps {
|
interface LibraryHeaderProps {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
compact?: boolean;
|
||||||
containerClassName?: string;
|
containerClassName?: string;
|
||||||
imagePlaceholderUrl?: null | string;
|
imagePlaceholderUrl?: null | string;
|
||||||
imageUrl?: null | string;
|
imageUrl?: null | string;
|
||||||
@@ -45,11 +46,20 @@ interface LibraryHeaderProps {
|
|||||||
};
|
};
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
|
topRight?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LibraryHeader = forwardRef(
|
export const LibraryHeader = forwardRef(
|
||||||
(
|
(
|
||||||
{ children, containerClassName, imageUrl, item, title }: LibraryHeaderProps,
|
{
|
||||||
|
children,
|
||||||
|
compact,
|
||||||
|
containerClassName,
|
||||||
|
imageUrl,
|
||||||
|
item,
|
||||||
|
title,
|
||||||
|
topRight,
|
||||||
|
}: LibraryHeaderProps,
|
||||||
ref: Ref<HTMLDivElement>,
|
ref: Ref<HTMLDivElement>,
|
||||||
) => {
|
) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -125,7 +135,15 @@ export const LibraryHeader = forwardRef(
|
|||||||
}, [item.explicitStatus, item.imageId, item.type]);
|
}, [item.explicitStatus, item.imageId, item.type]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(styles.libraryHeader, containerClassName)} ref={ref}>
|
<div
|
||||||
|
className={clsx(
|
||||||
|
styles.libraryHeader,
|
||||||
|
containerClassName,
|
||||||
|
compact && styles.compact,
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
{topRight && <div className={styles.topRight}>{topRight}</div>}
|
||||||
<div
|
<div
|
||||||
className={styles.imageSection}
|
className={styles.imageSection}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -224,6 +224,11 @@ export const CLIENT_SIDE_ALBUM_FILTERS = [
|
|||||||
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
|
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
|
||||||
value: AlbumListSort.ALBUM_ARTIST,
|
value: AlbumListSort.ALBUM_ARTIST,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.id', { postProcess: 'titleCase' }),
|
||||||
|
value: AlbumListSort.ID,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
defaultOrder: SortOrder.DESC,
|
defaultOrder: SortOrder.DESC,
|
||||||
name: i18n.t('filter.duration', { postProcess: 'titleCase' }),
|
name: i18n.t('filter.duration', { postProcess: 'titleCase' }),
|
||||||
@@ -295,6 +300,11 @@ const ALBUM_LIST_FILTERS: Partial<
|
|||||||
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
|
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
|
||||||
value: AlbumListSort.ALBUM_ARTIST,
|
value: AlbumListSort.ALBUM_ARTIST,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.id', { postProcess: 'titleCase' }),
|
||||||
|
value: AlbumListSort.ID,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
defaultOrder: SortOrder.DESC,
|
defaultOrder: SortOrder.DESC,
|
||||||
name: i18n.t('filter.communityRating', { postProcess: 'titleCase' }),
|
name: i18n.t('filter.communityRating', { postProcess: 'titleCase' }),
|
||||||
@@ -337,6 +347,11 @@ const ALBUM_LIST_FILTERS: Partial<
|
|||||||
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
|
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
|
||||||
value: AlbumListSort.ALBUM_ARTIST,
|
value: AlbumListSort.ALBUM_ARTIST,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.id', { postProcess: 'titleCase' }),
|
||||||
|
value: AlbumListSort.ID,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
defaultOrder: SortOrder.ASC,
|
defaultOrder: SortOrder.ASC,
|
||||||
name: i18n.t('filter.artist', { postProcess: 'titleCase' }),
|
name: i18n.t('filter.artist', { postProcess: 'titleCase' }),
|
||||||
@@ -399,6 +414,11 @@ const ALBUM_LIST_FILTERS: Partial<
|
|||||||
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
|
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
|
||||||
value: AlbumListSort.ALBUM_ARTIST,
|
value: AlbumListSort.ALBUM_ARTIST,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.id', { postProcess: 'titleCase' }),
|
||||||
|
value: AlbumListSort.ID,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
defaultOrder: SortOrder.DESC,
|
defaultOrder: SortOrder.DESC,
|
||||||
name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }),
|
name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }),
|
||||||
|
|||||||
@@ -152,6 +152,8 @@ const DiscordLinkTypeSchema = z.enum(['last_fm', 'musicbrainz', 'musicbrainz_las
|
|||||||
|
|
||||||
const GenreTargetSchema = z.enum(['album', 'track']);
|
const GenreTargetSchema = z.enum(['album', 'track']);
|
||||||
|
|
||||||
|
const PlaylistTargetSchema = z.enum(['album', 'track']);
|
||||||
|
|
||||||
const SideQueueTypeSchema = z.enum(['sideDrawerQueue', 'sideQueue']);
|
const SideQueueTypeSchema = z.enum(['sideDrawerQueue', 'sideQueue']);
|
||||||
|
|
||||||
const SidebarPanelTypeSchema = z.enum(['queue', 'lyrics', 'visualizer']);
|
const SidebarPanelTypeSchema = z.enum(['queue', 'lyrics', 'visualizer']);
|
||||||
@@ -458,6 +460,7 @@ export const GeneralSettingsSchema = z.object({
|
|||||||
playButtonBehavior: z.nativeEnum(Play),
|
playButtonBehavior: z.nativeEnum(Play),
|
||||||
playerbarOpenDrawer: z.boolean(),
|
playerbarOpenDrawer: z.boolean(),
|
||||||
playerbarSlider: PlayerbarSliderSchema,
|
playerbarSlider: PlayerbarSliderSchema,
|
||||||
|
playlistTarget: PlaylistTargetSchema,
|
||||||
resume: z.boolean(),
|
resume: z.boolean(),
|
||||||
showLyricsInSidebar: z.boolean(),
|
showLyricsInSidebar: z.boolean(),
|
||||||
showRatings: z.boolean(),
|
showRatings: z.boolean(),
|
||||||
@@ -775,6 +778,11 @@ export enum PlayerbarSliderType {
|
|||||||
WAVEFORM = 'waveform',
|
WAVEFORM = 'waveform',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum PlaylistTarget {
|
||||||
|
ALBUM = 'album',
|
||||||
|
TRACK = 'track',
|
||||||
|
}
|
||||||
|
|
||||||
export enum SidebarItem {
|
export enum SidebarItem {
|
||||||
ALBUMS = 'Albums',
|
ALBUMS = 'Albums',
|
||||||
ARTISTS = 'Artists',
|
ARTISTS = 'Artists',
|
||||||
@@ -829,6 +837,7 @@ export interface SettingsSlice extends z.infer<typeof SettingsStateSchema> {
|
|||||||
setHomeItems: (item: SortableItem<HomeItem>[]) => void;
|
setHomeItems: (item: SortableItem<HomeItem>[]) => void;
|
||||||
setList: (type: ItemListKey, data: DeepPartial<ItemListSettings>) => void;
|
setList: (type: ItemListKey, data: DeepPartial<ItemListSettings>) => void;
|
||||||
setPlaybackFilters: (filters: PlayerFilter[]) => void;
|
setPlaybackFilters: (filters: PlayerFilter[]) => void;
|
||||||
|
setPlaylistBehavior: (target: PlaylistTarget) => void;
|
||||||
setSettings: (data: DeepPartial<SettingsState>) => void;
|
setSettings: (data: DeepPartial<SettingsState>) => void;
|
||||||
setSidebarItems: (items: SidebarItemType[]) => void;
|
setSidebarItems: (items: SidebarItemType[]) => void;
|
||||||
setTable: (type: ItemListKey, data: DataTableProps) => void;
|
setTable: (type: ItemListKey, data: DataTableProps) => void;
|
||||||
@@ -988,7 +997,7 @@ const initialState: SettingsState = {
|
|||||||
showServerImage: false,
|
showServerImage: false,
|
||||||
},
|
},
|
||||||
font: {
|
font: {
|
||||||
builtIn: 'Poppins',
|
builtIn: 'Inter',
|
||||||
custom: null,
|
custom: null,
|
||||||
system: null,
|
system: null,
|
||||||
type: FontType.BUILT_IN,
|
type: FontType.BUILT_IN,
|
||||||
@@ -1039,6 +1048,7 @@ const initialState: SettingsState = {
|
|||||||
barWidth: 2,
|
barWidth: 2,
|
||||||
type: PlayerbarSliderType.SLIDER,
|
type: PlayerbarSliderType.SLIDER,
|
||||||
},
|
},
|
||||||
|
playlistTarget: PlaylistTarget.TRACK,
|
||||||
resume: true,
|
resume: true,
|
||||||
showLyricsInSidebar: true,
|
showLyricsInSidebar: true,
|
||||||
showRatings: true,
|
showRatings: true,
|
||||||
@@ -1175,6 +1185,83 @@ const initialState: SettingsState = {
|
|||||||
size: 'default',
|
size: 'default',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[ItemListKey.PLAYLIST_ALBUM]: {
|
||||||
|
detail: {
|
||||||
|
columns: pickTableColumns({
|
||||||
|
autoSizeColumns: [],
|
||||||
|
columns: SONG_TABLE_COLUMNS,
|
||||||
|
columnWidths: {
|
||||||
|
[TableColumn.ACTIONS]: 60,
|
||||||
|
[TableColumn.DURATION]: 100,
|
||||||
|
[TableColumn.TITLE]: 400,
|
||||||
|
[TableColumn.TRACK_NUMBER]: 50,
|
||||||
|
[TableColumn.USER_FAVORITE]: 60,
|
||||||
|
},
|
||||||
|
enabledColumns: [
|
||||||
|
TableColumn.TRACK_NUMBER,
|
||||||
|
TableColumn.TITLE,
|
||||||
|
TableColumn.DURATION,
|
||||||
|
TableColumn.USER_FAVORITE,
|
||||||
|
TableColumn.ACTIONS,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
enableAlternateRowColors: false,
|
||||||
|
enableHeader: true,
|
||||||
|
enableHorizontalBorders: false,
|
||||||
|
enableRowHoverHighlight: true,
|
||||||
|
enableVerticalBorders: false,
|
||||||
|
size: 'compact',
|
||||||
|
},
|
||||||
|
display: ListDisplayType.GRID,
|
||||||
|
grid: {
|
||||||
|
itemGap: 'sm',
|
||||||
|
itemsPerRow: 6,
|
||||||
|
itemsPerRowEnabled: false,
|
||||||
|
rows: pickGridRows({
|
||||||
|
alignLeftColumns: [
|
||||||
|
TableColumn.TITLE,
|
||||||
|
TableColumn.ALBUM_ARTIST,
|
||||||
|
TableColumn.YEAR,
|
||||||
|
],
|
||||||
|
columns: ALBUM_TABLE_COLUMNS,
|
||||||
|
enabledColumns: [TableColumn.TITLE, TableColumn.ALBUM_ARTIST, TableColumn.YEAR],
|
||||||
|
pickColumns: [
|
||||||
|
TableColumn.TITLE,
|
||||||
|
TableColumn.DURATION,
|
||||||
|
TableColumn.ALBUM_ARTIST,
|
||||||
|
TableColumn.BIT_RATE,
|
||||||
|
TableColumn.BPM,
|
||||||
|
TableColumn.DATE_ADDED,
|
||||||
|
TableColumn.GENRE,
|
||||||
|
TableColumn.PLAY_COUNT,
|
||||||
|
TableColumn.SONG_COUNT,
|
||||||
|
TableColumn.RELEASE_DATE,
|
||||||
|
TableColumn.LAST_PLAYED,
|
||||||
|
TableColumn.YEAR,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
size: 'default',
|
||||||
|
},
|
||||||
|
itemsPerPage: 100,
|
||||||
|
pagination: ListPaginationType.INFINITE,
|
||||||
|
table: {
|
||||||
|
autoFitColumns: true,
|
||||||
|
columns: ALBUM_TABLE_COLUMNS.map((column) => ({
|
||||||
|
align: column.align,
|
||||||
|
autoSize: column.autoSize,
|
||||||
|
id: column.value,
|
||||||
|
isEnabled: column.isEnabled,
|
||||||
|
pinned: column.pinned,
|
||||||
|
width: column.width,
|
||||||
|
})),
|
||||||
|
enableAlternateRowColors: false,
|
||||||
|
enableHeader: true,
|
||||||
|
enableHorizontalBorders: false,
|
||||||
|
enableRowHoverHighlight: true,
|
||||||
|
enableVerticalBorders: false,
|
||||||
|
size: 'default',
|
||||||
|
},
|
||||||
|
},
|
||||||
[LibraryItem.ALBUM]: {
|
[LibraryItem.ALBUM]: {
|
||||||
detail: {
|
detail: {
|
||||||
columns: pickTableColumns({
|
columns: pickTableColumns({
|
||||||
@@ -1808,6 +1895,11 @@ export const useSettingsStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
state.playback.filters = filters;
|
state.playback.filters = filters;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
setPlaylistBehavior: (target: PlaylistTarget) => {
|
||||||
|
set((state) => {
|
||||||
|
state.general.playlistTarget = target;
|
||||||
|
});
|
||||||
|
},
|
||||||
setSettings: (data) => {
|
setSettings: (data) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
deepMergeIntoState(state, data);
|
deepMergeIntoState(state, data);
|
||||||
@@ -2218,6 +2310,9 @@ export const usePlayerbarSlider = () =>
|
|||||||
|
|
||||||
export const useGenreTarget = () => useSettingsStore((store) => store.general.genreTarget, shallow);
|
export const useGenreTarget = () => useSettingsStore((store) => store.general.genreTarget, shallow);
|
||||||
|
|
||||||
|
export const usePlaylistTarget = () =>
|
||||||
|
useSettingsStore((store) => store.general.playlistTarget, shallow);
|
||||||
|
|
||||||
export const useLanguage = () => useSettingsStore((state) => state.general.language, shallow);
|
export const useLanguage = () => useSettingsStore((state) => state.general.language, shallow);
|
||||||
|
|
||||||
export const useAccent = () => useSettingsStore((state) => state.general.accent, shallow);
|
export const useAccent = () => useSettingsStore((state) => state.general.accent, shallow);
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
import { createWithEqualityFn } from 'zustand/traditional';
|
||||||
|
|
||||||
|
export type SleepTimerMode = 'endOfSong' | 'timed';
|
||||||
|
|
||||||
|
interface SleepTimerActions {
|
||||||
|
cancelTimer: () => void;
|
||||||
|
setRemaining: (remaining: number) => void;
|
||||||
|
startEndOfSongTimer: () => void;
|
||||||
|
startTimedTimer: (durationSeconds: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SleepTimerState {
|
||||||
|
/** Whether the timer is currently active */
|
||||||
|
active: boolean;
|
||||||
|
/** The mode of the timer */
|
||||||
|
mode: SleepTimerMode;
|
||||||
|
/** Remaining seconds (only ticks while playing) */
|
||||||
|
remaining: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSleepTimerStore = createWithEqualityFn<SleepTimerActions & SleepTimerState>()(
|
||||||
|
(set) => ({
|
||||||
|
active: false,
|
||||||
|
cancelTimer: () => {
|
||||||
|
set({
|
||||||
|
active: false,
|
||||||
|
mode: 'timed',
|
||||||
|
remaining: 0,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
mode: 'timed',
|
||||||
|
remaining: 0,
|
||||||
|
|
||||||
|
setRemaining: (remaining: number) => {
|
||||||
|
set({ remaining });
|
||||||
|
},
|
||||||
|
|
||||||
|
startEndOfSongTimer: () => {
|
||||||
|
set({
|
||||||
|
active: true,
|
||||||
|
mode: 'endOfSong',
|
||||||
|
remaining: 0,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
startTimedTimer: (durationSeconds: number) => {
|
||||||
|
set({
|
||||||
|
active: true,
|
||||||
|
mode: 'timed',
|
||||||
|
remaining: durationSeconds,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Selectors
|
||||||
|
export const useSleepTimerActive = () => useSleepTimerStore((s) => s.active);
|
||||||
|
export const useSleepTimerMode = () => useSleepTimerStore((s) => s.mode);
|
||||||
|
export const useSleepTimerRemaining = () => useSleepTimerStore((s) => s.remaining);
|
||||||
|
export const useSleepTimerActions = () =>
|
||||||
|
useSleepTimerStore(
|
||||||
|
useShallow((s) => ({
|
||||||
|
cancelTimer: s.cancelTimer,
|
||||||
|
setRemaining: s.setRemaining,
|
||||||
|
startEndOfSongTimer: s.startEndOfSongTimer,
|
||||||
|
startTimedTimer: s.startTimedTimer,
|
||||||
|
})),
|
||||||
|
);
|
||||||
@@ -151,7 +151,7 @@ export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: Sor
|
|||||||
results = orderBy(
|
results = orderBy(
|
||||||
results,
|
results,
|
||||||
[(v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
[(v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||||
[order, 'asc', 'asc'],
|
[order, order, order],
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: Sor
|
|||||||
results = orderBy(
|
results = orderBy(
|
||||||
results,
|
results,
|
||||||
[(v) => v.albumArtists[0]?.name.toLowerCase(), 'discNumber', 'trackNumber'],
|
[(v) => v.albumArtists[0]?.name.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||||
[order, order, 'asc', 'asc'],
|
[order, order, order, order],
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -167,32 +167,54 @@ export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: Sor
|
|||||||
results = orderBy(
|
results = orderBy(
|
||||||
results,
|
results,
|
||||||
[(v) => v.artistName?.toLowerCase(), 'discNumber', 'trackNumber'],
|
[(v) => v.artistName?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||||
[order, order, 'asc', 'asc'],
|
[order, order, order, order],
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SongListSort.BPM:
|
case SongListSort.BPM:
|
||||||
results = orderBy(results, ['bpm'], [order]);
|
results = orderBy(
|
||||||
|
results,
|
||||||
|
['bpm', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||||
|
[order, order, order, order],
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SongListSort.CHANNELS:
|
case SongListSort.CHANNELS:
|
||||||
results = orderBy(results, ['channels'], [order]);
|
results = orderBy(
|
||||||
|
results,
|
||||||
|
['channels', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||||
|
[order, order, order, order],
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SongListSort.COMMENT:
|
case SongListSort.COMMENT:
|
||||||
results = orderBy(
|
results = orderBy(
|
||||||
results,
|
results,
|
||||||
['comment', 'discNumber', 'trackNumber'],
|
['comment', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||||
[order, order, 'asc', 'asc'],
|
[order, order, order, order],
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SongListSort.DURATION:
|
case SongListSort.DURATION:
|
||||||
results = orderBy(results, ['duration'], [order]);
|
results = orderBy(
|
||||||
|
results,
|
||||||
|
['duration', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||||
|
[order, order, order, order],
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SongListSort.FAVORITED:
|
case SongListSort.FAVORITED:
|
||||||
results = orderBy(results, ['userFavorite', (v) => v.name.toLowerCase()], [order]);
|
results = orderBy(
|
||||||
|
results,
|
||||||
|
[
|
||||||
|
'userFavorite',
|
||||||
|
(v) => v.name.toLowerCase(),
|
||||||
|
(v) => v.album?.toLowerCase(),
|
||||||
|
'discNumber',
|
||||||
|
'trackNumber',
|
||||||
|
],
|
||||||
|
[order, order, order, order, order],
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SongListSort.GENRE:
|
case SongListSort.GENRE:
|
||||||
@@ -204,7 +226,7 @@ export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: Sor
|
|||||||
'discNumber',
|
'discNumber',
|
||||||
'trackNumber',
|
'trackNumber',
|
||||||
],
|
],
|
||||||
[order, order, 'asc', 'asc'],
|
[order, order, order, order],
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -217,11 +239,19 @@ export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: Sor
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case SongListSort.NAME:
|
case SongListSort.NAME:
|
||||||
results = orderBy(results, [(v) => v.name.toLowerCase()], [order]);
|
results = orderBy(
|
||||||
|
results,
|
||||||
|
[(v) => v.name.toLowerCase(), (v) => v.album?.toLowerCase()],
|
||||||
|
[order, order],
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SongListSort.PLAY_COUNT:
|
case SongListSort.PLAY_COUNT:
|
||||||
results = orderBy(results, ['playCount'], [order]);
|
results = orderBy(
|
||||||
|
results,
|
||||||
|
['playCount', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||||
|
[order, order, order, order],
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SongListSort.RANDOM:
|
case SongListSort.RANDOM:
|
||||||
@@ -229,19 +259,51 @@ export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: Sor
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case SongListSort.RATING:
|
case SongListSort.RATING:
|
||||||
results = orderBy(results, ['userRating', (v) => v.name.toLowerCase()], [order]);
|
results = orderBy(
|
||||||
|
results,
|
||||||
|
[
|
||||||
|
'userRating',
|
||||||
|
(v) => v.name.toLowerCase(),
|
||||||
|
(v) => v.album?.toLowerCase(),
|
||||||
|
'discNumber',
|
||||||
|
'trackNumber',
|
||||||
|
],
|
||||||
|
[order, order, order, order, order],
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SongListSort.RECENTLY_ADDED:
|
case SongListSort.RECENTLY_ADDED:
|
||||||
results = orderBy(results, ['createdAt'], [order]);
|
results = orderBy(
|
||||||
|
results,
|
||||||
|
[
|
||||||
|
(v) => {
|
||||||
|
const x = v.createdAt;
|
||||||
|
if (x == null) return null;
|
||||||
|
const d = new Date(x);
|
||||||
|
return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
||||||
|
},
|
||||||
|
(v) => v.album?.toLowerCase(),
|
||||||
|
'discNumber',
|
||||||
|
'trackNumber',
|
||||||
|
],
|
||||||
|
[order, order, order, order],
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SongListSort.RECENTLY_PLAYED:
|
case SongListSort.RECENTLY_PLAYED:
|
||||||
results = orderBy(results, ['lastPlayedAt'], [order]);
|
results = orderBy(
|
||||||
|
results,
|
||||||
|
['lastPlayedAt', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||||
|
[order, order, order, order],
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SongListSort.RELEASE_DATE:
|
case SongListSort.RELEASE_DATE:
|
||||||
results = orderBy(results, ['releaseDate'], [order]);
|
results = orderBy(
|
||||||
|
results,
|
||||||
|
['releaseDate', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||||
|
[order, order, order, order],
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SongListSort.SORT_NAME:
|
case SongListSort.SORT_NAME:
|
||||||
@@ -252,7 +314,7 @@ export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: Sor
|
|||||||
results = orderBy(
|
results = orderBy(
|
||||||
results,
|
results,
|
||||||
['releaseYear', (v) => v.album?.toLowerCase(), 'discNumber', 'track'],
|
['releaseYear', (v) => v.album?.toLowerCase(), 'discNumber', 'track'],
|
||||||
[order, 'asc', 'asc', 'asc'],
|
[order, order, order, order],
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -404,6 +466,9 @@ export const sortAlbumList = (albums: Album[], sortBy: AlbumListSort, sortOrder:
|
|||||||
case AlbumListSort.FAVORITED:
|
case AlbumListSort.FAVORITED:
|
||||||
results = orderBy(results, ['starred'], [order]);
|
results = orderBy(results, ['starred'], [order]);
|
||||||
break;
|
break;
|
||||||
|
case AlbumListSort.ID:
|
||||||
|
results = sortOrder === SortOrder.DESC ? [...results].reverse() : results;
|
||||||
|
break;
|
||||||
case AlbumListSort.NAME:
|
case AlbumListSort.NAME:
|
||||||
results = orderBy(results, [(v) => v.name.toLowerCase()], [order]);
|
results = orderBy(results, [(v) => v.name.toLowerCase()], [order]);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -105,6 +105,8 @@ import {
|
|||||||
LuStepForward,
|
LuStepForward,
|
||||||
LuSun,
|
LuSun,
|
||||||
LuTable,
|
LuTable,
|
||||||
|
LuTimer,
|
||||||
|
LuTimerOff,
|
||||||
LuTriangleAlert,
|
LuTriangleAlert,
|
||||||
LuUpload,
|
LuUpload,
|
||||||
LuUser,
|
LuUser,
|
||||||
@@ -237,6 +239,8 @@ export const AppIcon = {
|
|||||||
share: LuShare2,
|
share: LuShare2,
|
||||||
signIn: LuLogIn,
|
signIn: LuLogIn,
|
||||||
signOut: LuLogOut,
|
signOut: LuLogOut,
|
||||||
|
sleepTimer: LuTimer,
|
||||||
|
sleepTimerOff: LuTimerOff,
|
||||||
sort: LuArrowUpDown,
|
sort: LuArrowUpDown,
|
||||||
sortAsc: LuArrowUpNarrowWide,
|
sortAsc: LuArrowUpNarrowWide,
|
||||||
sortDesc: LuArrowDownWideNarrow,
|
sortDesc: LuArrowDownWideNarrow,
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ export interface ImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 's
|
|||||||
|
|
||||||
interface ImageContainerProps extends HTMLAttributes<HTMLDivElement> {
|
interface ImageContainerProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
enableAnimation?: boolean;
|
|
||||||
isExplicit?: boolean;
|
isExplicit?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +104,6 @@ export function BaseImage({
|
|||||||
return (
|
return (
|
||||||
<ImageContainer
|
<ImageContainer
|
||||||
className={clsx(containerClassName, containerPropsClassName)}
|
className={clsx(containerClassName, containerPropsClassName)}
|
||||||
enableAnimation={enableAnimation}
|
|
||||||
isExplicit={isExplicit}
|
isExplicit={isExplicit}
|
||||||
{...restContainerProps}
|
{...restContainerProps}
|
||||||
>
|
>
|
||||||
@@ -182,7 +180,6 @@ function ImageWithDebounce({
|
|||||||
return (
|
return (
|
||||||
<ImageContainer
|
<ImageContainer
|
||||||
className={clsx(containerClassName, containerPropsClassName)}
|
className={clsx(containerClassName, containerPropsClassName)}
|
||||||
enableAnimation={enableAnimation}
|
|
||||||
isExplicit={isExplicit}
|
isExplicit={isExplicit}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...restContainerProps}
|
{...restContainerProps}
|
||||||
@@ -216,7 +213,6 @@ function ImageWithDebounce({
|
|||||||
return (
|
return (
|
||||||
<ImageContainer
|
<ImageContainer
|
||||||
className={clsx(containerClassName, containerPropsClassName)}
|
className={clsx(containerClassName, containerPropsClassName)}
|
||||||
enableAnimation={enableAnimation}
|
|
||||||
isExplicit={isExplicit}
|
isExplicit={isExplicit}
|
||||||
{...restContainerProps}
|
{...restContainerProps}
|
||||||
>
|
>
|
||||||
@@ -284,7 +280,6 @@ function ImageWithViewport({
|
|||||||
return (
|
return (
|
||||||
<ImageContainer
|
<ImageContainer
|
||||||
className={clsx(containerClassName, containerPropsClassName)}
|
className={clsx(containerClassName, containerPropsClassName)}
|
||||||
enableAnimation={enableAnimation}
|
|
||||||
isExplicit={isExplicit}
|
isExplicit={isExplicit}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...restContainerProps}
|
{...restContainerProps}
|
||||||
|
|||||||
@@ -466,6 +466,7 @@ export enum AlbumListSort {
|
|||||||
DURATION = 'duration',
|
DURATION = 'duration',
|
||||||
EXPLICIT_STATUS = 'explicitStatus',
|
EXPLICIT_STATUS = 'explicitStatus',
|
||||||
FAVORITED = 'favorited',
|
FAVORITED = 'favorited',
|
||||||
|
ID = 'id',
|
||||||
NAME = 'name',
|
NAME = 'name',
|
||||||
PLAY_COUNT = 'playCount',
|
PLAY_COUNT = 'playCount',
|
||||||
RANDOM = 'random',
|
RANDOM = 'random',
|
||||||
@@ -521,6 +522,7 @@ export const albumListSortMap: AlbumListSortMap = {
|
|||||||
duration: undefined,
|
duration: undefined,
|
||||||
explicitStatus: undefined,
|
explicitStatus: undefined,
|
||||||
favorited: undefined,
|
favorited: undefined,
|
||||||
|
id: undefined,
|
||||||
name: JFAlbumListSort.NAME,
|
name: JFAlbumListSort.NAME,
|
||||||
playCount: JFAlbumListSort.PLAY_COUNT,
|
playCount: JFAlbumListSort.PLAY_COUNT,
|
||||||
random: JFAlbumListSort.RANDOM,
|
random: JFAlbumListSort.RANDOM,
|
||||||
@@ -540,6 +542,7 @@ export const albumListSortMap: AlbumListSortMap = {
|
|||||||
duration: NDAlbumListSort.DURATION,
|
duration: NDAlbumListSort.DURATION,
|
||||||
explicitStatus: NDAlbumListSort.EXPLICIT_STATUS,
|
explicitStatus: NDAlbumListSort.EXPLICIT_STATUS,
|
||||||
favorited: NDAlbumListSort.STARRED,
|
favorited: NDAlbumListSort.STARRED,
|
||||||
|
id: undefined,
|
||||||
name: NDAlbumListSort.NAME,
|
name: NDAlbumListSort.NAME,
|
||||||
playCount: NDAlbumListSort.PLAY_COUNT,
|
playCount: NDAlbumListSort.PLAY_COUNT,
|
||||||
random: NDAlbumListSort.RANDOM,
|
random: NDAlbumListSort.RANDOM,
|
||||||
@@ -560,6 +563,7 @@ export const albumListSortMap: AlbumListSortMap = {
|
|||||||
duration: undefined,
|
duration: undefined,
|
||||||
explicitStatus: undefined,
|
explicitStatus: undefined,
|
||||||
favorited: undefined,
|
favorited: undefined,
|
||||||
|
id: undefined,
|
||||||
name: undefined,
|
name: undefined,
|
||||||
playCount: undefined,
|
playCount: undefined,
|
||||||
random: undefined,
|
random: undefined,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export enum ItemListKey {
|
|||||||
GENRE_ALBUM = 'genreAlbum',
|
GENRE_ALBUM = 'genreAlbum',
|
||||||
GENRE_SONG = 'genreSong',
|
GENRE_SONG = 'genreSong',
|
||||||
PLAYLIST = LibraryItem.PLAYLIST,
|
PLAYLIST = LibraryItem.PLAYLIST,
|
||||||
|
PLAYLIST_ALBUM = 'playlistAlbum',
|
||||||
PLAYLIST_SONG = LibraryItem.PLAYLIST_SONG,
|
PLAYLIST_SONG = LibraryItem.PLAYLIST_SONG,
|
||||||
QUEUE_SONG = LibraryItem.QUEUE_SONG,
|
QUEUE_SONG = LibraryItem.QUEUE_SONG,
|
||||||
RADIO = 'radio',
|
RADIO = 'radio',
|
||||||
|
|||||||