Compare commits

..

1 Commits

Author SHA1 Message Date
jeffvli 95f395bd87 add jukebox endpoint / controller 2025-12-16 18:07:09 -08:00
372 changed files with 4519 additions and 35046 deletions
-15
View File
@@ -4,24 +4,9 @@ on:
pull_request: pull_request:
branches: branches:
- development - development
paths:
- 'src/**'
jobs: jobs:
wait-for-lint:
runs-on: ubuntu-latest
steps:
- name: Wait for Test workflow to complete
uses: lewagon/wait-on-check-action@v1.4.1
with:
ref: ${{ github.event.pull_request.head.sha }}
check-name: 'lint'
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 10
allowed-conclusions: success
publish: publish:
needs: wait-for-lint
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
+1 -8
View File
@@ -18,15 +18,8 @@ FROM nginx:alpine-slim
COPY --chown=nginx:nginx --from=builder /app/out/web /usr/share/nginx/html COPY --chown=nginx:nginx --from=builder /app/out/web /usr/share/nginx/html
COPY ./settings.js.template /etc/nginx/templates/settings.js.template COPY ./settings.js.template /etc/nginx/templates/settings.js.template
COPY ./ng.conf.template /etc/nginx/templates/default.conf.template COPY ng.conf.template /etc/nginx/templates/default.conf.template
COPY ./docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
ENV PUBLIC_PATH="/" ENV PUBLIC_PATH="/"
ENV LEGACY_AUTHENTICATION="false"
ENV ANALYTICS_DISABLED="false"
EXPOSE 9180 EXPOSE 9180
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]
+9 -13
View File
@@ -62,21 +62,18 @@ For media keys to work, you will be prompted to allow Feishin to be a Trusted Ac
We provide a small install script to download the latest `.AppImage`, make it executable, and also download the icons required by Desktop Environments. Finally, it generates a `.desktop` file to add Feishin to your Application Launcher. We provide a small install script to download the latest `.AppImage`, make it executable, and also download the icons required by Desktop Environments. Finally, it generates a `.desktop` file to add Feishin to your Application Launcher.
Simply run the installer like this: Simply run the installer like this:
```sh ```sh
dir=/your/application/directory dir=/your/application/directory
curl 'https://raw.githubusercontent.com/jeffvli/feishin/refs/heads/development/install-feishin-appimage' | sh -s -- "$dir" curl 'https://raw.githubusercontent.com/jeffvli/feishin/refs/heads/development/install-feishin-appimage' | sh -s -- "$dir"
``` ```
The script also has an option to add launch arguments to run Feishin in native Wayland mode. Note that this is experimental in Electron and therefore not officially supported. If you want to use it, run this instead: The script also has an option to add launch arguments to run Feishin in native Wayland mode. Note that this is experimental in Electron and therefore not officially supported. If you want to use it, run this instead:
```sh ```sh
dir=/your/application/directory dir=/your/application/directory
curl 'https://raw.githubusercontent.com/jeffvli/feishin/refs/heads/development/install-feishin-appimage' | sh -s -- "$dir" wayland-native curl 'https://raw.githubusercontent.com/jeffvli/feishin/refs/heads/development/install-feishin-appimage' | sh -s -- "$dir" wayland-native
``` ```
It also provides a simple uninstall routine, removing the downloaded files: It also provides a simple uninstall routine, removing the downloaded files:
```sh ```sh
dir=/your/application/directory dir=/your/application/directory
curl 'https://raw.githubusercontent.com/jeffvli/feishin/refs/heads/development/install-feishin-appimage' | sh -s -- "$dir" remove curl 'https://raw.githubusercontent.com/jeffvli/feishin/refs/heads/development/install-feishin-appimage' | sh -s -- "$dir" remove
@@ -108,17 +105,18 @@ services:
feishin: feishin:
container_name: feishin container_name: feishin
image: 'ghcr.io/jeffvli/feishin:latest' image: 'ghcr.io/jeffvli/feishin:latest'
restart: unless-stopped
environment: environment:
- SERVER_NAME=jellyfin # pre-defined server name - SERVER_NAME=jellyfin # pre defined server name
- SERVER_LOCK=true # When true AND name/type/url are set, only username/password can be toggled - SERVER_LOCK=true # When true AND name/type/url are set, only username/password can be toggled
- SERVER_TYPE=jellyfin # the allowed types are: jellyfin, navidrome, subsonic. These values are case insensitive - SERVER_TYPE=jellyfin # navidrome also works
- SERVER_URL= # http://address:port or https://address:port - SERVER_URL= # http://address:port
- LEGACY_AUTHENTICATION=false # When SERVER_LOCK is true, sets the legacy (plaintext) authentication flag for Subsonic/OpenSubsonic servers - PUID=1000
- ANALYTICS_DISABLED=true # Set to true to disable Umami analytics tracking - PGID=1000
- UMASK=002
- TZ=America/Los_Angeles
ports: ports:
- 9180:9180 - 9180:9180
# Alternatively, to restrict to only localhost, - 127.0.0.1:9180:8190 restart: unless-stopped
``` ```
### Configuration ### Configuration
@@ -132,9 +130,7 @@ services:
3. _Optional_ - If you want to host Feishin on a subpath (not `/`), then pass in the following environment variable: `PUBLIC_PATH=PATH`. For example, to host on `/feishin`, pass in `PUBLIC_PATH=/feishin`. 3. _Optional_ - If you want to host Feishin on a subpath (not `/`), then pass in the following environment variable: `PUBLIC_PATH=PATH`. For example, to host on `/feishin`, pass in `PUBLIC_PATH=/feishin`.
4. _Optional_ - To hard code the server url, pass the following environment variables: `SERVER_NAME`, `SERVER_TYPE` (one of `jellyfin` or `navidrome` or `subsonic`), `SERVER_URL`. To prevent users from changing these settings, pass `SERVER_LOCK=true`. This can only be set if all three of the previous values are set. When `SERVER_LOCK=true`, you can also set `LEGACY_AUTHENTICATION=true` or `LEGACY_AUTHENTICATION=false` to configure the legacy authentication flag for the server (only applicable for Subsonic/OpenSubsonic servers). 4. _Optional_ - To hard code the server url, pass the following environment variables: `SERVER_NAME`, `SERVER_TYPE` (one of `jellyfin` or `navidrome` or `subsonic`), `SERVER_URL`. To prevent users from changing these settings, pass `SERVER_LOCK=true`. This can only be set if all three of the previous values are set.
5. _Optional_ - To disable Umami analytics tracking in the Docker/web version, set the environment variable `ANALYTICS_DISABLED=true`. When enabled, the analytics script will not be loaded and all tracking will be disabled.
## FAQ ## FAQ
Binary file not shown.
Binary file not shown.
+6 -8
View File
@@ -1,15 +1,13 @@
version: '3.5'
services: services:
feishin: feishin:
container_name: feishin container_name: feishin
image: ghcr.io/jeffvli/feishin:latest image: ghcr.io/jeffvli/feishin:latest
restart: unless-stopped restart: unless-stopped
environment:
- SERVER_NAME=jellyfin # pre-defined server name
- SERVER_LOCK=true # When true AND name/type/url are set, only username/password can be toggled
- SERVER_TYPE=jellyfin # the allowed types are: jellyfin, navidrome, subsonic. These values are case insensitive
- SERVER_URL= # http://address:port or https://address:port
- LEGACY_AUTHENTICATION=false # When SERVER_LOCK is true, sets the legacyauth flag for server authentication (true or false)
- ANALYTICS_DISABLED=false # When true, disables analytics
ports: ports:
- 9180:9180 - 9180:9180
# Alternatively, to restrict to only localhost, - 127.0.0.1:9180:8190 environment:
- SERVER_NAME=jellyfin # pre defined server name
- SERVER_LOCK=true # When true AND name/type/url are set, only username/password can be toggled
- SERVER_TYPE=jellyfin # navidrome also works
- SERVER_URL= # http://address:port
-7
View File
@@ -1,7 +0,0 @@
#!/bin/sh
# Set default values for environment variables if not already set
export LEGACY_AUTHENTICATION=${LEGACY_AUTHENTICATION:-false}
export ANALYTICS_DISABLED=${ANALYTICS_DISABLED:-false}
# Execute the original nginx command
exec "$@"
+1 -1
View File
@@ -1,7 +1,7 @@
appId: org.jeffvli.feishin appId: org.jeffvli.feishin
productName: Feishin productName: Feishin
artifactName: ${productName}-${version}-${os}-${arch}.${ext} artifactName: ${productName}-${version}-${os}-${arch}.${ext}
electronVersion: 39.2.7 electronVersion: 38.5.0
directories: directories:
buildResources: assets buildResources: assets
files: files:
+1 -1
View File
@@ -1,7 +1,7 @@
appId: org.jeffvli.feishin appId: org.jeffvli.feishin
productName: Feishin productName: Feishin
artifactName: ${productName}-${version}-${os}-${arch}.${ext} artifactName: ${productName}-${version}-${os}-${arch}.${ext}
electronVersion: 39.2.7 electronVersion: 38.5.0
directories: directories:
buildResources: assets buildResources: assets
files: files:
-18321
View File
File diff suppressed because it is too large Load Diff
+3 -6
View File
@@ -1,6 +1,6 @@
{ {
"name": "feishin", "name": "feishin",
"version": "1.2.0", "version": "0.22.0",
"description": "A modern self-hosted music player.", "description": "A modern self-hosted music player.",
"keywords": [ "keywords": [
"subsonic", "subsonic",
@@ -82,8 +82,6 @@
"@xhayper/discord-rpc": "^1.3.0", "@xhayper/discord-rpc": "^1.3.0",
"audiomotion-analyzer": "^4.5.1", "audiomotion-analyzer": "^4.5.1",
"axios": "^1.13.2", "axios": "^1.13.2",
"butterchurn": "^3.0.0-beta.5",
"butterchurn-presets": "^3.0.0-beta.4",
"cheerio": "^1.1.2", "cheerio": "^1.1.2",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
@@ -112,7 +110,7 @@
"nuqs": "^2.7.1", "nuqs": "^2.7.1",
"overlayscrollbars": "^2.11.1", "overlayscrollbars": "^2.11.1",
"overlayscrollbars-react": "^0.5.6", "overlayscrollbars-react": "^0.5.6",
"qs": "^6.14.1", "qs": "^6.14.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-call": "^1.8.1", "react-call": "^1.8.1",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
@@ -123,7 +121,6 @@
"react-loading-skeleton": "^3.5.0", "react-loading-skeleton": "^3.5.0",
"react-player": "^2.16.0", "react-player": "^2.16.0",
"react-router": "^7.9.6", "react-router": "^7.9.6",
"react-split-pane": "^3.0.4",
"react-virtualized-auto-sizer": "^1.0.26", "react-virtualized-auto-sizer": "^1.0.26",
"react-window": "1.8.11", "react-window": "1.8.11",
"react-window-v2": "npm:react-window@^2.2.3", "react-window-v2": "npm:react-window@^2.2.3",
@@ -150,7 +147,7 @@
"@vitejs/plugin-react": "^5.1.1", "@vitejs/plugin-react": "^5.1.1",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
"electron": "^39.2.7", "electron": "^38.5.0",
"electron-builder": "^26.0.12", "electron-builder": "^26.0.12",
"electron-devtools-installer": "^4.0.0", "electron-devtools-installer": "^4.0.0",
"electron-vite": "^4.0.1", "electron-vite": "^4.0.1",
+44 -114
View File
@@ -19,10 +19,10 @@ importers:
version: 1.1.0 version: 1.1.0
'@electron-toolkit/preload': '@electron-toolkit/preload':
specifier: ^3.0.1 specifier: ^3.0.1
version: 3.0.2(electron@39.2.7) version: 3.0.2(electron@38.5.0)
'@electron-toolkit/utils': '@electron-toolkit/utils':
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.0(electron@39.2.7) version: 4.0.0(electron@38.5.0)
'@mantine/colors-generator': '@mantine/colors-generator':
specifier: ^8.3.8 specifier: ^8.3.8
version: 8.3.8(chroma-js@3.1.2) version: 8.3.8(chroma-js@3.1.2)
@@ -71,12 +71,6 @@ importers:
axios: axios:
specifier: ^1.13.2 specifier: ^1.13.2
version: 1.13.2 version: 1.13.2
butterchurn:
specifier: ^3.0.0-beta.5
version: 3.0.0-beta.5
butterchurn-presets:
specifier: ^3.0.0-beta.4
version: 3.0.0-beta.4
cheerio: cheerio:
specifier: ^1.1.2 specifier: ^1.1.2
version: 1.1.2 version: 1.1.2
@@ -162,8 +156,8 @@ importers:
specifier: ^0.5.6 specifier: ^0.5.6
version: 0.5.6(overlayscrollbars@2.11.3)(react@19.1.0) version: 0.5.6(overlayscrollbars@2.11.3)(react@19.1.0)
qs: qs:
specifier: ^6.14.1 specifier: ^6.14.0
version: 6.14.1 version: 6.14.0
react: react:
specifier: ^19.1.0 specifier: ^19.1.0
version: 19.1.0 version: 19.1.0
@@ -194,9 +188,6 @@ importers:
react-router: react-router:
specifier: ^7.9.6 specifier: ^7.9.6
version: 7.9.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) version: 7.9.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react-split-pane:
specifier: ^3.0.4
version: 3.0.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react-virtualized-auto-sizer: react-virtualized-auto-sizer:
specifier: ^1.0.26 specifier: ^1.0.26
version: 1.0.26(react-dom@19.1.0(react@19.1.0))(react@19.1.0) version: 1.0.26(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -271,8 +262,8 @@ importers:
specifier: ^10.1.0 specifier: ^10.1.0
version: 10.1.0 version: 10.1.0
electron: electron:
specifier: ^39.2.7 specifier: ^38.5.0
version: 39.2.7 version: 38.5.0
electron-builder: electron-builder:
specifier: ^26.0.12 specifier: ^26.0.12
version: 26.0.12(electron-builder-squirrel-windows@26.0.12) version: 26.0.12(electron-builder-squirrel-windows@26.0.12)
@@ -361,9 +352,6 @@ packages:
peerDependencies: peerDependencies:
ajv: '>=8' ajv: '>=8'
'@assemblyscript/loader@0.17.14':
resolution: {integrity: sha512-+PVTOfla/0XMLRTQLJFPg4u40XcdTfon6GGea70hBGi8Pd7ZymIXyVUR+vK8wt5Jb4MVKTKPIz43Myyebw5mZA==}
'@atlaskit/pragmatic-drag-and-drop-auto-scroll@2.1.2': '@atlaskit/pragmatic-drag-and-drop-auto-scroll@2.1.2':
resolution: {integrity: sha512-6BgAUxSNbQFiG3uqNxf53cDQADn5mSeh/JsQzCHo46GPQnVWIJk77zWC8yZ++0Mfg1ECy02zNrbniF7SgHAhXQ==} resolution: {integrity: sha512-6BgAUxSNbQFiG3uqNxf53cDQADn5mSeh/JsQzCHo46GPQnVWIJk77zWC8yZ++0Mfg1ECy02zNrbniF7SgHAhXQ==}
@@ -892,8 +880,8 @@ packages:
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@bufbuild/protobuf@2.10.2': '@bufbuild/protobuf@2.10.1':
resolution: {integrity: sha512-uFsRXwIGyu+r6AMdz+XijIIZJYpoWeYzILt5yZ2d3mCjQrWUTVpVD9WL/jZAbvp+Ed04rOhrsk7FiTcEDseB5A==} resolution: {integrity: sha512-ckS3+vyJb5qGpEYv/s1OebUHDi/xSNtfgw1wqKZo7MR9F2z+qXr0q5XagafAG/9O0QPVIUfST0smluYSTpYFkg==}
'@cacheable/memoize@2.0.3': '@cacheable/memoize@2.0.3':
resolution: {integrity: sha512-hl9wfQgpiydhQEIv7fkjEzTGE+tcosCXLKFDO707wYJ/78FVOlowb36djex5GdbSyeHnG62pomYLMuV/OT8Pbw==} resolution: {integrity: sha512-hl9wfQgpiydhQEIv7fkjEzTGE+tcosCXLKFDO707wYJ/78FVOlowb36djex5GdbSyeHnG62pomYLMuV/OT8Pbw==}
@@ -2291,8 +2279,8 @@ packages:
resolution: {integrity: sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ==} resolution: {integrity: sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ==}
hasBin: true hasBin: true
baseline-browser-mapping@2.9.11: baseline-browser-mapping@2.8.32:
resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} resolution: {integrity: sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==}
hasBin: true hasBin: true
bind-event-listener@3.0.0: bind-event-listener@3.0.0:
@@ -2344,8 +2332,8 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true hasBin: true
browserslist@4.28.1: browserslist@4.28.0:
resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true hasBin: true
@@ -2371,12 +2359,6 @@ packages:
builder-util@26.0.11: builder-util@26.0.11:
resolution: {integrity: sha512-xNjXfsldUEe153h1DraD0XvDOpqGR0L5eKFkdReB7eFW5HqysDZFfly4rckda6y9dF39N3pkPlOblcfHKGw+uA==} resolution: {integrity: sha512-xNjXfsldUEe153h1DraD0XvDOpqGR0L5eKFkdReB7eFW5HqysDZFfly4rckda6y9dF39N3pkPlOblcfHKGw+uA==}
butterchurn-presets@3.0.0-beta.4:
resolution: {integrity: sha512-TbQLUPvGOYMZAtWKoCmBtludh9aQZ6NaMGQU4lvPeadBPy3Du3yNmwBjlTMLP5c5mRWElxQPjTL1PtR7FZK3OQ==}
butterchurn@3.0.0-beta.5:
resolution: {integrity: sha512-BStK4OAbBb9Pvt8PuQlS4WVmYBwU1KuDMRHF1V89QjoIFauAqq7tpV4EpYXj7K563r5daLrMX+2y5DBhZZ9Xig==}
cac@6.7.14: cac@6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -2419,8 +2401,8 @@ packages:
caniuse-lite@1.0.30001751: caniuse-lite@1.0.30001751:
resolution: {integrity: sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==} resolution: {integrity: sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==}
caniuse-lite@1.0.30001762: caniuse-lite@1.0.30001757:
resolution: {integrity: sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==} resolution: {integrity: sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==}
chalk@4.1.2: chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
@@ -2797,12 +2779,6 @@ packages:
eastasianwidth@0.2.0: eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
ecma-proposal-math-extensions@0.0.2:
resolution: {integrity: sha512-80BnDp2Fn7RxXlEr5HHZblniY4aQ97MOAicdWWpSo0vkQiISSE9wLR4SqxKsu4gCtXFBIPPzy8JMhay4NWRg/Q==}
eel-wasm@0.0.16:
resolution: {integrity: sha512-1tkId7I7E1Vs4fXTRsH83Sjn2S/AbzrVQKLBRGys6NLc3eVH4NBffJsdEeLHOWWUgQpVXBEP3CV/srUZNIuBnw==}
ejs@3.1.10: ejs@3.1.10:
resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -2844,8 +2820,8 @@ packages:
electron-to-chromium@1.5.242: electron-to-chromium@1.5.242:
resolution: {integrity: sha512-msZ7SYGFpXkm/iUizlMrm/FPNeYo8uSltQccLVFO3fV4RN2JWGdG7Aatztxtw3uDWp3DkupfkrosLjUnhY+iOw==} resolution: {integrity: sha512-msZ7SYGFpXkm/iUizlMrm/FPNeYo8uSltQccLVFO3fV4RN2JWGdG7Aatztxtw3uDWp3DkupfkrosLjUnhY+iOw==}
electron-to-chromium@1.5.267: electron-to-chromium@1.5.262:
resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} resolution: {integrity: sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==}
electron-updater@6.6.2: electron-updater@6.6.2:
resolution: {integrity: sha512-Cr4GDOkbAUqRHP5/oeOmH/L2Bn6+FQPxVLZtPbcmKZC63a1F3uu5EefYOssgZXG3u/zBlubbJ5PJdITdMVggbw==} resolution: {integrity: sha512-Cr4GDOkbAUqRHP5/oeOmH/L2Bn6+FQPxVLZtPbcmKZC63a1F3uu5EefYOssgZXG3u/zBlubbJ5PJdITdMVggbw==}
@@ -2865,8 +2841,8 @@ packages:
resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==}
engines: {node: '>=8.0.0'} engines: {node: '>=8.0.0'}
electron@39.2.7: electron@38.5.0:
resolution: {integrity: sha512-KU0uFS6LSTh4aOIC3miolcbizOFP7N1M46VTYVfqIgFiuA2ilfNaOHLDS9tCMvwwHRowAsvqBrh9NgMXcTOHCQ==} resolution: {integrity: sha512-dbC7V+eZweerYMJfxQldzHOg37a1VdNMCKxrJxlkp3cA30gOXtXSg4ZYs07L5+QwI19WOy1uyvtEUgbw1RRsCQ==}
engines: {node: '>= 12.20.55'} engines: {node: '>= 12.20.55'}
hasBin: true hasBin: true
@@ -3202,10 +3178,6 @@ packages:
resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==}
engines: {node: '>=14.14'} engines: {node: '>=14.14'}
fs-extra@11.3.3:
resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==}
engines: {node: '>=14.14'}
fs-extra@7.0.1: fs-extra@7.0.1:
resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
engines: {node: '>=6 <7 || >=8'} engines: {node: '>=6 <7 || >=8'}
@@ -4503,8 +4475,8 @@ packages:
resolution: {integrity: sha512-7gJ6mxcQb9vUBOtbKm5mDevbe2uRcOEVp1g4gb/Q+oLntB3HY8eBhOYRxFI2mlDFlY1e4DOSCptzxarXRvzxCA==} resolution: {integrity: sha512-7gJ6mxcQb9vUBOtbKm5mDevbe2uRcOEVp1g4gb/Q+oLntB3HY8eBhOYRxFI2mlDFlY1e4DOSCptzxarXRvzxCA==}
engines: {node: '>=20'} engines: {node: '>=20'}
qs@6.14.1: qs@6.14.0:
resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
engines: {node: '>=0.6'} engines: {node: '>=0.6'}
queue-microtask@1.2.3: queue-microtask@1.2.3:
@@ -4649,13 +4621,6 @@ packages:
react-dom: react-dom:
optional: true optional: true
react-split-pane@3.0.4:
resolution: {integrity: sha512-+QNayN8lsYhT87z0bH5yAuUocoqHlc3AQnw/+pGXMH2kG2+mSfNAR4fHhEdmweHLFjIyX811hh9sgCkiHXCYag==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
react-style-singleton@2.2.3: react-style-singleton@2.2.3:
resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -5513,12 +5478,6 @@ packages:
peerDependencies: peerDependencies:
browserslist: '>= 4.21.0' browserslist: '>= 4.21.0'
update-browserslist-db@1.2.3:
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
uri-js@4.4.1: uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
@@ -5690,7 +5649,6 @@ packages:
whatwg-encoding@3.1.1: whatwg-encoding@3.1.1:
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
whatwg-mimetype@4.0.0: whatwg-mimetype@4.0.0:
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
@@ -5908,8 +5866,6 @@ snapshots:
jsonpointer: 5.0.1 jsonpointer: 5.0.1
leven: 3.1.0 leven: 3.1.0
'@assemblyscript/loader@0.17.14': {}
'@atlaskit/pragmatic-drag-and-drop-auto-scroll@2.1.2': '@atlaskit/pragmatic-drag-and-drop-auto-scroll@2.1.2':
dependencies: dependencies:
'@atlaskit/pragmatic-drag-and-drop': 1.7.7 '@atlaskit/pragmatic-drag-and-drop': 1.7.7
@@ -6626,7 +6582,7 @@ snapshots:
'@babel/helper-string-parser': 7.27.1 '@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5 '@babel/helper-validator-identifier': 7.28.5
'@bufbuild/protobuf@2.10.2': '@bufbuild/protobuf@2.10.1':
optional: true optional: true
'@cacheable/memoize@2.0.3': '@cacheable/memoize@2.0.3':
@@ -6702,17 +6658,17 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@electron-toolkit/preload@3.0.2(electron@39.2.7)': '@electron-toolkit/preload@3.0.2(electron@38.5.0)':
dependencies: dependencies:
electron: 39.2.7 electron: 38.5.0
'@electron-toolkit/tsconfig@2.0.0(@types/node@24.10.1)': '@electron-toolkit/tsconfig@2.0.0(@types/node@24.10.1)':
dependencies: dependencies:
'@types/node': 24.10.1 '@types/node': 24.10.1
'@electron-toolkit/utils@4.0.0(electron@39.2.7)': '@electron-toolkit/utils@4.0.0(electron@38.5.0)':
dependencies: dependencies:
electron: 39.2.7 electron: 38.5.0
'@electron/asar@3.2.18': '@electron/asar@3.2.18':
dependencies: dependencies:
@@ -6817,7 +6773,7 @@ snapshots:
dependencies: dependencies:
cross-dirname: 0.1.0 cross-dirname: 0.1.0
debug: 4.4.3 debug: 4.4.3
fs-extra: 11.3.3 fs-extra: 11.3.2
minimist: 1.2.8 minimist: 1.2.8
postject: 1.0.0-alpha.6 postject: 1.0.0-alpha.6
transitivePeerDependencies: transitivePeerDependencies:
@@ -7621,7 +7577,7 @@ snapshots:
'@types/electron-localshortcut@3.1.3': '@types/electron-localshortcut@3.1.3':
dependencies: dependencies:
electron: 39.2.7 electron: 38.5.0
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -8053,7 +8009,7 @@ snapshots:
baseline-browser-mapping@2.8.20: {} baseline-browser-mapping@2.8.20: {}
baseline-browser-mapping@2.9.11: {} baseline-browser-mapping@2.8.32: {}
bind-event-listener@3.0.0: {} bind-event-listener@3.0.0: {}
@@ -8124,13 +8080,13 @@ snapshots:
node-releases: 2.0.26 node-releases: 2.0.26
update-browserslist-db: 1.1.4(browserslist@4.27.0) update-browserslist-db: 1.1.4(browserslist@4.27.0)
browserslist@4.28.1: browserslist@4.28.0:
dependencies: dependencies:
baseline-browser-mapping: 2.9.11 baseline-browser-mapping: 2.8.32
caniuse-lite: 1.0.30001762 caniuse-lite: 1.0.30001757
electron-to-chromium: 1.5.267 electron-to-chromium: 1.5.262
node-releases: 2.0.27 node-releases: 2.0.27
update-browserslist-db: 1.2.3(browserslist@4.28.1) update-browserslist-db: 1.1.4(browserslist@4.28.0)
buffer-builder@0.2.0: buffer-builder@0.2.0:
optional: true optional: true
@@ -8178,16 +8134,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
butterchurn-presets@3.0.0-beta.4:
dependencies:
'@babel/runtime': 7.28.4
butterchurn@3.0.0-beta.5:
dependencies:
'@assemblyscript/loader': 0.17.14
ecma-proposal-math-extensions: 0.0.2
eel-wasm: 0.0.16
cac@6.7.14: {} cac@6.7.14: {}
cacache@16.1.3: cacache@16.1.3:
@@ -8257,7 +8203,7 @@ snapshots:
caniuse-lite@1.0.30001751: {} caniuse-lite@1.0.30001751: {}
caniuse-lite@1.0.30001762: {} caniuse-lite@1.0.30001757: {}
chalk@4.1.2: chalk@4.1.2:
dependencies: dependencies:
@@ -8413,7 +8359,7 @@ snapshots:
core-js-compat@3.47.0: core-js-compat@3.47.0:
dependencies: dependencies:
browserslist: 4.28.1 browserslist: 4.28.0
core-util-is@1.0.2: core-util-is@1.0.2:
optional: true optional: true
@@ -8658,10 +8604,6 @@ snapshots:
eastasianwidth@0.2.0: {} eastasianwidth@0.2.0: {}
ecma-proposal-math-extensions@0.0.2: {}
eel-wasm@0.0.16: {}
ejs@3.1.10: ejs@3.1.10:
dependencies: dependencies:
jake: 10.9.2 jake: 10.9.2
@@ -8739,7 +8681,7 @@ snapshots:
electron-to-chromium@1.5.242: {} electron-to-chromium@1.5.242: {}
electron-to-chromium@1.5.267: {} electron-to-chromium@1.5.262: {}
electron-updater@6.6.2: electron-updater@6.6.2:
dependencies: dependencies:
@@ -8778,7 +8720,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
electron@39.2.7: electron@38.5.0:
dependencies: dependencies:
'@electron/get': 2.0.3 '@electron/get': 2.0.3
'@types/node': 22.15.32 '@types/node': 22.15.32
@@ -9239,13 +9181,6 @@ snapshots:
jsonfile: 6.2.0 jsonfile: 6.2.0
universalify: 2.0.1 universalify: 2.0.1
fs-extra@11.3.3:
dependencies:
graceful-fs: 4.2.11
jsonfile: 6.2.0
universalify: 2.0.1
optional: true
fs-extra@7.0.1: fs-extra@7.0.1:
dependencies: dependencies:
graceful-fs: 4.2.11 graceful-fs: 4.2.11
@@ -9591,7 +9526,7 @@ snapshots:
i18next@24.2.3(typescript@5.8.3): i18next@24.2.3(typescript@5.8.3):
dependencies: dependencies:
'@babel/runtime': 7.28.4 '@babel/runtime': 7.27.1
optionalDependencies: optionalDependencies:
typescript: 5.8.3 typescript: 5.8.3
@@ -10520,7 +10455,7 @@ snapshots:
dependencies: dependencies:
hookified: 1.13.0 hookified: 1.13.0
qs@6.14.1: qs@6.14.0:
dependencies: dependencies:
side-channel: 1.1.0 side-channel: 1.1.0
@@ -10653,11 +10588,6 @@ snapshots:
optionalDependencies: optionalDependencies:
react-dom: 19.1.0(react@19.1.0) react-dom: 19.1.0(react@19.1.0)
react-split-pane@3.0.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
react-style-singleton@2.2.3(@types/react@19.2.5)(react@19.1.0): react-style-singleton@2.2.3(@types/react@19.2.5)(react@19.1.0):
dependencies: dependencies:
get-nonce: 1.0.1 get-nonce: 1.0.1
@@ -10668,7 +10598,7 @@ snapshots:
react-textarea-autosize@8.5.9(@types/react@19.2.5)(react@19.1.0): react-textarea-autosize@8.5.9(@types/react@19.2.5)(react@19.1.0):
dependencies: dependencies:
'@babel/runtime': 7.28.4 '@babel/runtime': 7.27.1
react: 19.1.0 react: 19.1.0
use-composed-ref: 1.4.0(@types/react@19.2.5)(react@19.1.0) use-composed-ref: 1.4.0(@types/react@19.2.5)(react@19.1.0)
use-latest: 1.3.0(@types/react@19.2.5)(react@19.1.0) use-latest: 1.3.0(@types/react@19.2.5)(react@19.1.0)
@@ -10677,7 +10607,7 @@ snapshots:
react-transition-group@4.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0): react-transition-group@4.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies: dependencies:
'@babel/runtime': 7.28.4 '@babel/runtime': 7.27.1
dom-helpers: 5.2.1 dom-helpers: 5.2.1
loose-envify: 1.4.0 loose-envify: 1.4.0
prop-types: 15.8.1 prop-types: 15.8.1
@@ -10972,7 +10902,7 @@ snapshots:
sass-embedded@1.89.0: sass-embedded@1.89.0:
dependencies: dependencies:
'@bufbuild/protobuf': 2.10.2 '@bufbuild/protobuf': 2.10.1
buffer-builder: 0.2.0 buffer-builder: 0.2.0
colorjs.io: 0.5.2 colorjs.io: 0.5.2
immutable: 5.1.4 immutable: 5.1.4
@@ -11629,9 +11559,9 @@ snapshots:
escalade: 3.2.0 escalade: 3.2.0
picocolors: 1.1.1 picocolors: 1.1.1
update-browserslist-db@1.2.3(browserslist@4.28.1): update-browserslist-db@1.1.4(browserslist@4.28.0):
dependencies: dependencies:
browserslist: 4.28.1 browserslist: 4.28.0
escalade: 3.2.0 escalade: 3.2.0
picocolors: 1.1.1 picocolors: 1.1.1
+1 -1
View File
@@ -1 +1 @@
"use strict";window.SERVER_URL="${SERVER_URL}";window.SERVER_NAME="${SERVER_NAME}";window.SERVER_TYPE="${SERVER_TYPE}";window.SERVER_LOCK=${SERVER_LOCK};window.LEGACY_AUTHENTICATION=${LEGACY_AUTHENTICATION};window.ANALYTICS_DISABLED="${ANALYTICS_DISABLED}"; "use strict";window.SERVER_URL="${SERVER_URL}";window.SERVER_NAME="${SERVER_NAME}";window.SERVER_TYPE="${SERVER_TYPE}";window.SERVER_LOCK=${SERVER_LOCK};
+15 -214
View File
@@ -14,8 +14,7 @@
"tracks": "$t(entity.track_other)", "tracks": "$t(entity.track_other)",
"nowPlaying": "ara sona", "nowPlaying": "ara sona",
"shared": "$t(entity.playlist_other) compartida", "shared": "$t(entity.playlist_other) compartida",
"favorites": "$t(entity.favorite_other)", "favorites": "$t(entity.favorite_other)"
"radio": "$t(entity.radioStation_other)"
}, },
"albumArtistDetail": { "albumArtistDetail": {
"relatedArtists": "$t(entity.artist_other) similars", "relatedArtists": "$t(entity.artist_other) similars",
@@ -26,9 +25,7 @@
"viewDiscography": "Mosta la discografia", "viewDiscography": "Mosta la discografia",
"topSongs": "millors cançons", "topSongs": "millors cançons",
"topSongsFrom": "les millors cançons de {{title}}", "topSongsFrom": "les millors cançons de {{title}}",
"viewAll": "mostra-ho tot", "viewAll": "mostra-ho tot"
"groupingTypeAll": "tots els tipus de llançaments",
"groupingTypePrimary": "tipus principals de llançament"
}, },
"albumArtistList": { "albumArtistList": {
"title": "$t(entity.albumArtist_other)" "title": "$t(entity.albumArtist_other)"
@@ -164,8 +161,7 @@
"transcoding": "transcodificació", "transcoding": "transcodificació",
"discord": "discord", "discord": "discord",
"logger": "registres", "logger": "registres",
"playerFilters": "filtres de reproducció", "playerFilters": "filtres de reproducció"
"lyricsDisplay": "mostra la lletra"
}, },
"globalSearch": { "globalSearch": {
"commands": { "commands": {
@@ -188,9 +184,6 @@
}, },
"folderList": { "folderList": {
"title": "$t(entity.folder_other)" "title": "$t(entity.folder_other)"
},
"radioList": {
"title": "emissores de ràdio"
} }
}, },
"common": { "common": {
@@ -308,9 +301,7 @@
"sort": "ordre", "sort": "ordre",
"gridRows": "files de la quadrícula", "gridRows": "files de la quadrícula",
"tableColumns": "columnes de la taula", "tableColumns": "columnes de la taula",
"itemsMore": "{{count}} més", "itemsMore": "{{count}} més"
"countSelected": "{{count}} seleccionats",
"retry": "reintenta"
}, },
"entity": { "entity": {
"album_one": "àlbum", "album_one": "àlbum",
@@ -364,13 +355,7 @@
"song_other": "cançons", "song_other": "cançons",
"favorite_one": "preferit", "favorite_one": "preferit",
"favorite_many": "preferits", "favorite_many": "preferits",
"favorite_other": "preferits", "favorite_other": "preferits"
"radioStation_one": "emissora de ràdio",
"radioStation_many": "emissores de ràdio",
"radioStation_other": "emissores de ràdio",
"radioStationWithCount_one": "{{count}} emissora de ràdio",
"radioStationWithCount_many": "{{count}} emissores de ràdio",
"radioStationWithCount_other": "{{count}} emissores de ràdio"
}, },
"form": { "form": {
"addToPlaylist": { "addToPlaylist": {
@@ -460,21 +445,6 @@
"input_played_optionAll": "totes les pistes", "input_played_optionAll": "totes les pistes",
"input_played_optionUnplayed": "només les pistes sense reproduir", "input_played_optionUnplayed": "només les pistes sense reproduir",
"input_played_optionPlayed": "només les pistes reproduïdes" "input_played_optionPlayed": "només les pistes reproduïdes"
},
"createRadioStation": {
"success": "emissora de ràdio creada amb èxit",
"title": "crea una emissora de ràdio",
"input_homepageUrl": "URL de la pàgina d'inici",
"input_name": "nom",
"input_streamUrl": "URL de transmissió"
},
"saveQueue": {
"success": "cua de reproducció desada al servidor"
},
"lyricsExport": {
"export": "exporta la lletra",
"input_synced": "exporta la lletra sincronitzada",
"input_offset": "$t(setting.lyricOffset)"
} }
}, },
"action": { "action": {
@@ -509,13 +479,7 @@
"shuffle": "mescla", "shuffle": "mescla",
"shuffleAll": "mescla-ho tot", "shuffleAll": "mescla-ho tot",
"shuffleSelected": "mescla els seleccionats", "shuffleSelected": "mescla els seleccionats",
"viewMore": "mostra'n més", "viewMore": "mostra'n més"
"createRadioStation": "crea $t(entity.radioStation_one)",
"deleteRadioStation": "elimina $t(entity.radioStation_one)",
"addOrRemoveFromSelection": "afegeix o elimina de la selecció",
"selectRangeOfItems": "selecciona un interval d'elements",
"selectAll": "selecciona-ho tot",
"openApplicationDirectory": "obre el directori de l'aplicació"
}, },
"setting": { "setting": {
"language_description": "estableix l'idioma de l'aplicació ($t(common.restartRequired))", "language_description": "estableix l'idioma de l'aplicació ($t(common.restartRequired))",
@@ -679,6 +643,8 @@
"playbackStyle_optionNormal": "normal", "playbackStyle_optionNormal": "normal",
"playButtonBehavior": "comportament del botó de reproducció", "playButtonBehavior": "comportament del botó de reproducció",
"playButtonBehavior_description": "estableix el comportament predeterminat del botó de reproducció quan s'afegeixen cançons a la cua", "playButtonBehavior_description": "estableix el comportament predeterminat del botó de reproducció quan s'afegeixen cançons a la cua",
"playerAlbumArtResolution": "resolució de la caràtula de l'àlbum al reproductor",
"playerAlbumArtResolution_description": "la resolució de la previsualització gran de la caràtula al reproductor. si és més alta, serà més nítida, però es carregarà més lent. el valor predeterminat 0 vol dir automàtic",
"playerbarOpenDrawer": "activa el reproductor en pantalla completa", "playerbarOpenDrawer": "activa el reproductor en pantalla completa",
"playerbarOpenDrawer_description": "permet fer clic a la barra de reproducció per obrir el reproductor de pantalla completa", "playerbarOpenDrawer_description": "permet fer clic a la barra de reproducció per obrir el reproductor de pantalla completa",
"remotePassword": "contrasenya del servidor de control remot", "remotePassword": "contrasenya del servidor de control remot",
@@ -821,22 +787,7 @@
"queryBuilderCustomFields_inputLabel": "discogràfica", "queryBuilderCustomFields_inputLabel": "discogràfica",
"queryBuilderCustomFields_inputTag": "etiqueta", "queryBuilderCustomFields_inputTag": "etiqueta",
"queryBuilderCustomFields": "camps personalitzats", "queryBuilderCustomFields": "camps personalitzats",
"queryBuilderCustomFields_description": "afegeix camps personalitzats pel constructor de consultes", "queryBuilderCustomFields_description": "afegeix camps personalitzats pel constructor de consultes"
"useThemeAccentColor": "fes servir el color d'accent del tema",
"useThemeAccentColor_description": "fes servir el color primari definit pel tema seleccionat en comptes del color d'accent personalitzat",
"artistRadioCount_description": "estableix el número de cançons per cercar per la ràdio d'artista i pista",
"artistRadioCount": "recompte de ràdios d'artista o pista",
"imageResolution": "resolució d'imatge",
"imageResolution_description": "la resolució per les imatges que s'utilitzen a l'aplicació. un valor de 0 equival a la resolució nativa de la imatge",
"imageResolution_optionTable": "taula",
"imageResolution_optionItemCard": "targeta d'element",
"imageResolution_optionSidebar": "tauler lateral",
"imageResolution_optionHeader": "encapçalament",
"imageResolution_optionFullScreenPlayer": "reproductor de pantalla completa",
"showRatings_description": "controla si es mostren les estrelles de valoració a la interfície",
"showRatings": "mostra la valoració d'estrelles",
"combinedLyricsAndVisualizer_description": "combina la lletra i el visualitzador en un sol tauler",
"combinedLyricsAndVisualizer": "combina la lletra i el visualitzador al tauler lateral del reproductor"
}, },
"table": { "table": {
"column": { "column": {
@@ -1002,8 +953,8 @@
"repeat_all": "repetició", "repeat_all": "repetició",
"shuffle": "reprodueix (mesclat)", "shuffle": "reprodueix (mesclat)",
"shuffle_off": "reproducció aleatòria desactivada", "shuffle_off": "reproducció aleatòria desactivada",
"addLast": "al final", "addLast": "afegeix al final",
"addNext": "a continuació", "addNext": "afegeix a continuació",
"favorite": "marcar com a preferida", "favorite": "marcar com a preferida",
"mute": "silencia", "mute": "silencia",
"next": "següent", "next": "següent",
@@ -1019,17 +970,12 @@
"toggleFullscreenPlayer": "activa el reproductor de pantalla completa", "toggleFullscreenPlayer": "activa el reproductor de pantalla completa",
"unfavorite": "elimina de preferits", "unfavorite": "elimina de preferits",
"pause": "pausa", "pause": "pausa",
"addLastShuffled": "al final (mesclat)", "addLastShuffled": "afegeix al final (mesclat)",
"addNextShuffled": "a continuació (mesclat)", "addNextShuffled": "afegeix a continuació (mesclat)",
"holdToShuffle": "mantén premut per mesclar", "holdToShuffle": "mantén premut per mesclar",
"queueType": "tipus de cua", "queueType": "tipus de cua",
"queueType_default": "predeterminat", "queueType_default": "predeterminat",
"queueType_priority": "prioritat", "queueType_priority": "prioritat"
"lyrics": "lletra",
"restoreQueueFromServer": "restaura la cua del servidor",
"saveQueueToServer": "desa la cua al servidor",
"artistRadio": "ràdio de l'artista",
"trackRadio": "ràdio de la pista"
}, },
"error": { "error": {
"credentialsRequired": "credencials requerides", "credentialsRequired": "credencials requerides",
@@ -1055,12 +1001,7 @@
"notificationDenied": "s'han negat els permisos per enviar notificacions. aquesta opció no té cap efecte", "notificationDenied": "s'han negat els permisos per enviar notificacions. aquesta opció no té cap efecte",
"playbackError": "hi ha hagut un error en intentar reproduir el mitjà", "playbackError": "hi ha hagut un error en intentar reproduir el mitjà",
"remoteDisableError": "hi ha hagut un error en intentar $t(common.disable) el servidor remot", "remoteDisableError": "hi ha hagut un error en intentar $t(common.disable) el servidor remot",
"endpointNotImplementedError": "el punt final {{endpoint}} no està implementat per {{serverType}}", "endpointNotImplementedError": "el punt final {{endpoint}} no està implementat per {{serverType}}"
"multipleServerSaveQueueError": "la cua de reproducció té una o més cançons que no són del servidor actual, cosa que no és compatible",
"saveQueueFailed": "error en desar la cua",
"settingsSyncError": "hi ha discrepàncies entre la configuració del renderitzador i el procés principal. reinicieu l'aplicació per aplicar els canvis",
"noNetwork": "servidor no disponible",
"noNetworkDescription": "no s'ha pogut connectar amb el servidor"
}, },
"releaseType": { "releaseType": {
"primary": { "primary": {
@@ -1114,145 +1055,5 @@
"queryBuilder": { "queryBuilder": {
"standardTags": "etiquetes estàndard", "standardTags": "etiquetes estàndard",
"customTags": "etiquetes personalitzades" "customTags": "etiquetes personalitzades"
},
"datetime": {
"minuteShort": "min",
"secondShort": "s",
"hourShort": "h",
"dayShort": "d"
},
"visualizer": {
"visualizerType": "tipus de visualitzador",
"cyclePresets": "opcions preconfigurades",
"cycleTime": "duració d'un cicle (segons)",
"includeAllPresets": "inclou totes les opcions predeterminades",
"ignoredPresets": "ignora les opcions predeterminades",
"selectedPresets": "opcions predeterminades seleccionades",
"randomizeNextPreset": "tria la següent opcions predeterminada a l'atzar",
"blendTime": "duració de la mescla",
"presets": "opcions predeterminades",
"selectPreset": "selecciona una opció predeterminada",
"applyPreset": "aplica l'opció predeterminada",
"saveAsPreset": "desa com a opció predeterminada",
"updatePreset": "actualitza l'opció predeterminada",
"copyConfiguration": "copia la configuració",
"pasteConfiguration": "enganxa la configuració",
"pasteConfigurationPlaceholder": "enganxa la configuració JSON aquí...",
"pasteFromClipboard": "enganxa des del portaretalls",
"applyConfiguration": "aplica la configuració",
"configCopied": "configuració copiada al portaretalls",
"configCopyFailed": "error en copiar la configuració",
"configPasted": "configuració aplicada correctament",
"configPasteFailed": "Error en aplicar la configuració. Reviseu-ne el format.",
"configPasteReadFailed": "Error en llegir del portaretalls",
"presetName": "Nom de l'opció predeterminada",
"presetNamePlaceholder": "Escriviu el nom de l'opció predeterminada",
"general": "General",
"mode": "Mode",
"mode1To8": "Mode 1 - 8",
"mode10": "Mode 10",
"barSpace": "Espai entre barres",
"lineWidth": "Amplitud de línia",
"fillAlpha": "Omplir alfa",
"channelLayout": "Disseny del canal",
"maxFPS": "FPS màxims",
"opacity": "Opacitat",
"customGradients": "Degradats personalitzats",
"addCustomGradient": "Afegeix un degradat personalitzat",
"gradientName": "Nom del degradat",
"gradientNamePlaceholder": "Nom del degradat",
"vertical": "Vertical",
"horizontal": "Horitzontal",
"colorStops": "Parades de color",
"addColor": "Afegeix el color",
"position": "Posició",
"level": "Nivell",
"remove": "Elimina",
"custom": "Personalitzat",
"builtIn": "Integrat",
"colors": "Colors",
"colorMode": "Mode de color",
"gradient": "Degradat",
"gradientLeft": "Esquerra del degradat",
"gradientRight": "Dreta del degradat",
"fft": "FFT",
"fftSize": "Mida del FFT",
"smoothing": "Suavitzador",
"frequencyRangeAndScaling": "Escala i rang de freqüència",
"minimumFrequency": "Freqüència mínima",
"maximumFrequency": "Freqüència màxima",
"frequencyScale": "Escala de freqüència",
"sensitivity": "Sensibilitat",
"weightingFilter": "Filtre de pes",
"minimumDecibels": "Decibels mínims",
"maximumDecibels": "Decibels màxims",
"linearAmplitude": "Amplitud lineal",
"linearBoost": "Augment lineal",
"peakBehavior": "Comportament del pic",
"showPeaks": "Mostra els pics",
"fadePeaks": "Pics de fosa",
"peakLine": "Línea del pic",
"gravity": "Gravetat",
"peakFadeTime": "Temps de fosa del pic (ms)",
"peakHoldTime": "Temps d'espera del pic (ms)",
"radialSpectrum": "Espectre radial",
"radial": "Radial",
"radialInvert": "Invertir el radial",
"spinSpeed": "Velocitat de gir",
"radius": "Radi",
"reflexMirror": "Mirall del reflex",
"reflexFit": "Ajustament del reflex",
"reflexRatio": "Proporció del reflex",
"reflexAlpha": "Alfa del reflex",
"reflexBrightness": "Brillantor del reflex",
"mirror": "Mirall",
"miscellaneousSettings": "Configuració miscel·lànea",
"alphaBars": "Barres alfa",
"ansiBands": "Bandes ANSI",
"ledBars": "Barres LED",
"trueLeds": "LEDs reals",
"lumiBars": "Barres Lumi",
"outlineBars": "Barres de vora",
"roundBars": "Barres arrodonides",
"lowResolution": "Baixa resolució",
"splitGradient": "Degradat dividit",
"showFPS": "Mostra els FPS",
"showScaleX": "Mostra l'escala X",
"noteLabels": "Etiquetes de nota",
"showScaleY": "Mostra l'escala Y",
"options": {
"colorMode": {
"gradient": "Degradat",
"barIndex": "Índex de barra",
"barLevel": "Nivell de barra"
},
"gradient": {
"classic": "Classic",
"prism": "Prisme",
"rainbow": "Arc de Sant Martí",
"steelblue": "Blau d'acer",
"orangered": "Vermell ataronjat"
},
"channelLayout": {
"single": "Únic",
"dualCombined": "Dual-Combinat",
"dualHorizontal": "Dual-Horitzontal",
"dualVertical": "Dual-Vertical"
},
"frequencyScale": {
"bark": "Bark",
"linear": "Lineal",
"log": "Registre",
"mel": "Mel"
},
"weightingFilter": {
"none": "Cap",
"a": "A",
"b": "B",
"c": "C",
"d": "D",
"z": "Z"
}
}
} }
} }
+12 -215
View File
@@ -39,9 +39,7 @@
"holdToShuffle": "podržte pro zamíchání", "holdToShuffle": "podržte pro zamíchání",
"lyrics": "texty", "lyrics": "texty",
"restoreQueueFromServer": "obnovit frontu ze serveru", "restoreQueueFromServer": "obnovit frontu ze serveru",
"saveQueueToServer": "uložit frontu na server", "saveQueueToServer": "uložit frontu na server"
"artistRadio": "rádio umělce",
"trackRadio": "rádio skladby"
}, },
"setting": { "setting": {
"crossfadeStyle_description": "vyberte způsob prolnutí u přehrávače zvuku", "crossfadeStyle_description": "vyberte způsob prolnutí u přehrávače zvuku",
@@ -209,6 +207,8 @@
"passwordStore": "ukládání hesel / tajných klíčů", "passwordStore": "ukládání hesel / tajných klíčů",
"mpvExtraParameters_help": "jeden na řádek", "mpvExtraParameters_help": "jeden na řádek",
"homeConfiguration": "nastavení domovské stránky", "homeConfiguration": "nastavení domovské stránky",
"playerAlbumArtResolution_description": "rozlišení náhledu obalu alba ve velkém přehrávači. větší hodnota znamená kvalitnější obrázek, ale může se déle načítat. výchozí hodnota je 0, což znamená automatické rozlišení",
"playerAlbumArtResolution": "rozlišení obalu alba v přehrávači",
"externalLinks_description": "zapne zobrazování externích odkazů (Last.fm, MusicBrainz) na stránce umělce/alba", "externalLinks_description": "zapne zobrazování externích odkazů (Last.fm, MusicBrainz) na stránce umělce/alba",
"clearCacheSuccess": "mezipaměť úspěšně vymazána", "clearCacheSuccess": "mezipaměť úspěšně vymazána",
"externalLinks": "zobrazit externí odkazy", "externalLinks": "zobrazit externí odkazy",
@@ -349,33 +349,7 @@
"logLevel_optionInfo": "informace", "logLevel_optionInfo": "informace",
"logLevel_optionWarn": "varování", "logLevel_optionWarn": "varování",
"useThemeAccentColor": "použít barvu motivu", "useThemeAccentColor": "použít barvu motivu",
"useThemeAccentColor_description": "použít primární barvu definovanou ve zvoleném motivu namísto vlastní barvy rozhraní", "useThemeAccentColor_description": "použít primární barvu definovanou ve zvoleném motivu namísto vlastní barvy rozhraní"
"artistRadioCount_description": "nastaví počet skladeb, které načíst pro rádio umělce a rádio skladby",
"artistRadioCount": "počet skladeb pro rádio umělce/skladby",
"imageResolution": "rozlišení obrázků",
"imageResolution_description": "rozlišení obrázků používaných napříč aplikací. nastavení hodnoty 0 použije nativní rozlišení obrázku",
"imageResolution_optionTable": "tabulka",
"imageResolution_optionItemCard": "karta položky",
"imageResolution_optionSidebar": "postranní lišta",
"imageResolution_optionHeader": "záhlaví",
"imageResolution_optionFullScreenPlayer": "přehrávač na celé obrazovce",
"combinedLyricsAndVisualizer_description": "spojit texty a vizualizér do jednoho panelu",
"combinedLyricsAndVisualizer": "spojit texty a vizualizér v postranní liště přehrávače",
"showRatings_description": "ovládá, zda se funkce hodnocení pomocí hvězdiček objeví v rozhraní",
"showRatings": "zobrazit hodnocení pomocí hvězdiček",
"artistReleaseTypeConfiguration": "nastavení typu vydání umělce",
"artistReleaseTypeConfiguration_description": "nastavit, jaké typy vydání a v jakém pořadí jsou zobrazeny na stránce umělce alba",
"mpvExtraParameters": "extra parametry mpv",
"mpvExtraParameters_description": "další argumenty, které předat přehrávači mpv",
"hotkey_listNavigateToPage": "navigace na stránku položky v seznamu",
"hotkey_listPlayDefault": "přehrání v seznamu",
"hotkey_listPlayLast": "přehrání poslední položky v seznamu",
"hotkey_listPlayNext": "přehrání další položky v seznamu",
"hotkey_listPlayNow": "okamžité přehrání v seznamu",
"pathReplace": "nahrazení cesty k souborům",
"pathReplace_description": "nahradit výchozí cestu k souborům vašeho serveru",
"pathReplace_optionRemovePrefix": "odstranit předponu",
"pathReplace_optionAddPrefix": "přidat předponu"
}, },
"action": { "action": {
"editPlaylist": "upravit $t(entity.playlist_one)", "editPlaylist": "upravit $t(entity.playlist_one)",
@@ -411,11 +385,7 @@
"holdToMoveToTop": "podržte pro přesunutí nahoru", "holdToMoveToTop": "podržte pro přesunutí nahoru",
"holdToMoveToBottom": "podržte pro přesunutí dolů", "holdToMoveToBottom": "podržte pro přesunutí dolů",
"createRadioStation": "vytvořit $t(entity.radioStation_one)", "createRadioStation": "vytvořit $t(entity.radioStation_one)",
"deleteRadioStation": "odstranit $t(entity.radioStation_one)", "deleteRadioStation": "odstranit $t(entity.radioStation_one)"
"openApplicationDirectory": "otevřít adresář aplikace",
"addOrRemoveFromSelection": "přidat nebo odebrat z výběru",
"selectRangeOfItems": "vyberte rozsah položek",
"selectAll": "vybrat vše"
}, },
"common": { "common": {
"backward": "zpátky", "backward": "zpátky",
@@ -532,11 +502,7 @@
"tableColumns": "sloupce tabulky", "tableColumns": "sloupce tabulky",
"itemsMore": "{{count}} dalších", "itemsMore": "{{count}} dalších",
"noFilters": "nejsou nastaveny žádné filtry", "noFilters": "nejsou nastaveny žádné filtry",
"view": "zobrazit", "view": "zobrazit"
"countSelected": "vybráno {{count}}",
"retry": "zkusit znovu",
"mood": "nálada",
"example": "příklad"
}, },
"table": { "table": {
"config": { "config": {
@@ -668,10 +634,7 @@
"badValue": "neplatná možnost „{{value}}“. tato možnost již neexistuje", "badValue": "neplatná možnost „{{value}}“. tato možnost již neexistuje",
"notificationDenied": "oprávnění k posílání oznámení byla zamítnuta. toto nastavení nemá žádný vliv", "notificationDenied": "oprávnění k posílání oznámení byla zamítnuta. toto nastavení nemá žádný vliv",
"multipleServerSaveQueueError": "fronta přehrávání má jednu nebo více skladeb, které nejsou z aktuálního serveru. tato funkce není podporována", "multipleServerSaveQueueError": "fronta přehrávání má jednu nebo více skladeb, které nejsou z aktuálního serveru. tato funkce není podporována",
"saveQueueFailed": "nepodařilo se uložit frontu", "saveQueueFailed": "nepodařilo se uložit frontu"
"settingsSyncError": "byly zjištěny nesrovnalosti mezi nastavením v rendereru a hlavním procesem. restartujte aplikaci, aby se změny projevily",
"noNetwork": "server je nedostupný",
"noNetworkDescription": "k tomuto serveru se nepodařilo připojit"
}, },
"filter": { "filter": {
"mostPlayed": "nejvíce přehráváno", "mostPlayed": "nejvíce přehráváno",
@@ -841,8 +804,7 @@
"transcoding": "překódování", "transcoding": "překódování",
"discord": "discord", "discord": "discord",
"playerFilters": "filtry přehrávače", "playerFilters": "filtry přehrávače",
"logger": "protokol", "logger": "protokol"
"lyricsDisplay": "zobrazení textů"
}, },
"albumArtistList": { "albumArtistList": {
"title": "$t(entity.albumArtist_other)" "title": "$t(entity.albumArtist_other)"
@@ -882,9 +844,7 @@
"topSongsFrom": "nejlepší skladby od umělce {{title}}", "topSongsFrom": "nejlepší skladby od umělce {{title}}",
"relatedArtists": "podobní $t(entity.artist_other)", "relatedArtists": "podobní $t(entity.artist_other)",
"viewAllTracks": "zobrazit všechny $t(entity.track_other)", "viewAllTracks": "zobrazit všechny $t(entity.track_other)",
"viewAll": "zobrazit vše", "viewAll": "zobrazit vše"
"groupingTypeAll": "všechny typy vydání",
"groupingTypePrimary": "primární typy vydání"
}, },
"itemDetail": { "itemDetail": {
"copiedPath": "cesta úspěšně zkopírována", "copiedPath": "cesta úspěšně zkopírována",
@@ -939,10 +899,7 @@
"ignoreCors": "ignorovat CORS $t(common.restartRequired)", "ignoreCors": "ignorovat CORS $t(common.restartRequired)",
"error_savePassword": "při ukládání hesla se vyskytla chyba", "error_savePassword": "při ukládání hesla se vyskytla chyba",
"input_preferInstantMix": "preferovat instantní mix", "input_preferInstantMix": "preferovat instantní mix",
"input_preferInstantMixDescription": "pro získání podobných skladeb použít pouze instantní mix. užitečné, pokud máte doplňky, které upravují toto chování", "input_preferInstantMixDescription": "pro získání podobných skladeb použít pouze instantní mix. užitečné, pokud máte doplňky, které upravují toto chování"
"input_preferRemoteUrl": "preferovat veřejnou adresu url",
"input_remoteUrl": "veřejná adresa url",
"input_remoteUrlPlaceholder": "volitelné: veřejná adresa url pro externí funkce"
}, },
"addToPlaylist": { "addToPlaylist": {
"success": "přidáno $t(entity.trackWithCount, {\"count\": {{message}} }) do $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })", "success": "přidáno $t(entity.trackWithCount, {\"count\": {{message}} }) do $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
@@ -1013,11 +970,6 @@
"input_homepageUrl": "adresa domovské stránky", "input_homepageUrl": "adresa domovské stránky",
"input_name": "název", "input_name": "název",
"input_streamUrl": "adresa streamu" "input_streamUrl": "adresa streamu"
},
"lyricsExport": {
"export": "exportovat texty",
"input_synced": "exportovat synchronizované texty",
"input_offset": "$t(setting.lyricOffset)"
} }
}, },
"entity": { "entity": {
@@ -1048,9 +1000,9 @@
"albumWithCount_one": "{{count}} album", "albumWithCount_one": "{{count}} album",
"albumWithCount_few": "{{count}} alba", "albumWithCount_few": "{{count}} alba",
"albumWithCount_other": "{{count}} alb", "albumWithCount_other": "{{count}} alb",
"favorite_one": "oblíbený", "favorite_one": "oblíbená",
"favorite_few": "oblíbené", "favorite_few": "oblíbené",
"favorite_other": "oblíbené", "favorite_other": "oblíbených",
"artistWithCount_one": "{{count}} umělec", "artistWithCount_one": "{{count}} umělec",
"artistWithCount_few": "{{count}} umělci", "artistWithCount_few": "{{count}} umělci",
"artistWithCount_other": "{{count}} umělců", "artistWithCount_other": "{{count}} umělců",
@@ -1132,160 +1084,5 @@
"notInPlaylist": "není v", "notInPlaylist": "není v",
"notInTheLast": "není v posledním", "notInTheLast": "není v posledním",
"startsWith": "začíná na" "startsWith": "začíná na"
},
"datetime": {
"minuteShort": "min.",
"secondShort": "s",
"hourShort": "h.",
"dayShort": "d."
},
"visualizer": {
"visualizerType": "Typ vizualizéru",
"cyclePresets": "Cyklicky procházet předvolby",
"cycleTime": "Čas cyklování (sekundy)",
"includeAllPresets": "Zahrnout všechny předvolby",
"ignoredPresets": "Ignorované předvolby",
"selectedPresets": "Vybrané předvolby",
"randomizeNextPreset": "Náhodně vybrat další předvolbu",
"blendTime": "Čas prolnutí",
"presets": "Předvolby",
"selectPreset": "Vybrat předvolbu",
"applyPreset": "Použít předvolbu",
"saveAsPreset": "Uložit jako předvolbu",
"updatePreset": "Aktualizovat předvolbu",
"copyConfiguration": "Kopírovat konfiguraci",
"pasteConfiguration": "Vložit konfiguraci",
"pasteConfigurationPlaceholder": "Sem vložte konfiguraci JSON…",
"pasteFromClipboard": "Vložit ze schránky",
"applyConfiguration": "Použít konfiguraci",
"configCopied": "Konfigurace zkopírována do schránky",
"configCopyFailed": "Nepodařilo se zkopírovat konfiguraci",
"configPasted": "Konfigurace úspěšně použita",
"configPasteFailed": "Nepodařilo se použít konfiguraci. Zkontrolujte prosím formát.",
"configPasteReadFailed": "Nepodařilo se přečíst schránku",
"presetName": "Název předvolby",
"presetNamePlaceholder": "Zadejte název předvolby",
"general": "Obecné",
"mode": "Režim",
"mode1To8": "Režim 18",
"mode10": "Režim 10",
"barSpace": "Mezera mezi sloupci",
"lineWidth": "Šířka linky",
"fillAlpha": "Vyplnit alfu",
"channelLayout": "Rozložení kanálů",
"maxFPS": "Max. počet snímků za sekundu",
"opacity": "Neprůhlednost",
"customGradients": "Vlastní přechody",
"addCustomGradient": "Přidat vlastní přechod",
"gradientName": "Název přechodu",
"gradientNamePlaceholder": "Název přechodu",
"vertical": "Vertikální",
"horizontal": "Horizontální",
"colorStops": "Ukončení barev",
"addColor": "Přidat barvu",
"position": "Pozice",
"level": "Úroveň",
"remove": "Odstranit",
"custom": "Vlastní",
"builtIn": "Vestavěné",
"colors": "Barvy",
"colorMode": "Režim barev",
"gradient": "Přechod",
"gradientLeft": "Přechod zleva",
"gradientRight": "Přechod zprava",
"fft": "FFT",
"fftSize": "Velikost FFT",
"smoothing": "Vyhlazování",
"frequencyRangeAndScaling": "Rozsah a škálování frekvencí",
"minimumFrequency": "Minimální frekvence",
"maximumFrequency": "Maximální frekvence",
"frequencyScale": "Škála frekvence",
"sensitivity": "Citlivost",
"weightingFilter": "Filtr váhy",
"minimumDecibels": "Minimální decibely",
"maximumDecibels": "Maximální decibely",
"linearAmplitude": "Lineární amplituda",
"linearBoost": "Lineární zesílení",
"peakBehavior": "Chování ve špičce",
"showPeaks": "Zobrazit špičky",
"fadePeaks": "Prolnout špičky",
"peakLine": "Linka špiček",
"gravity": "Gravitace",
"peakFadeTime": "Čas pádu ze špičky (ms)",
"peakHoldTime": "Čas udržení na špičce (ms)",
"radialSpectrum": "Kruhové spektrum",
"radial": "Kruhové",
"radialInvert": "Kruhové invertované",
"spinSpeed": "Rychlost rotace",
"radius": "Poloměr",
"reflexMirror": "Reflexní zrcadlení",
"reflexFit": "Reflexní vyplnění",
"reflexRatio": "Reflexní poměr",
"reflexAlpha": "Reflexní alfa",
"reflexBrightness": "Reflexní jas",
"mirror": "Zrcadlení",
"miscellaneousSettings": "Různá nastavení",
"alphaBars": "Alfa sloupce",
"ansiBands": "ANSI sloupce",
"ledBars": "LED sloupce",
"trueLeds": "Pravé LED",
"lumiBars": "Lumi sloupce",
"outlineBars": "Obrysové sloupce",
"roundBars": "Zaoblené sloupce",
"lowResolution": "Nízké rozlišení",
"splitGradient": "Přechod rozdělení",
"showFPS": "Zobrazit FPS",
"showScaleX": "Zobrazit osu X",
"noteLabels": "Štítky not",
"showScaleY": "Zobrazit osu Y",
"options": {
"colorMode": {
"gradient": "Přechod",
"barIndex": "Index sloupce",
"barLevel": "Úroveň sloupce"
},
"gradient": {
"classic": "Klasický",
"prism": "Prism",
"rainbow": "Duha",
"steelblue": "Ocelově modrá",
"orangered": "Oranžová"
},
"channelLayout": {
"single": "Jeden",
"dualCombined": "Duální kombinované",
"dualHorizontal": "Duální horizontální",
"dualVertical": "Duální vertikální"
},
"frequencyScale": {
"bark": "Barkova stupnice",
"linear": "Lineární stupnice",
"log": "Logaritmická stupnice",
"mel": "Melová stupnice",
"none": "Žádný"
},
"weightingFilter": {
"none": "Žádný",
"a": "A",
"b": "B",
"c": "C",
"d": "D",
"z": "Z"
},
"mode": {
"0": "[0] Diskrétní frekvence",
"1": "[1] 1/24 oktávy / 240 pásem",
"2": "[2] 1/12 oktávy / 120 pásem",
"3": "[3] 1/8 oktávy / 80 pásem",
"4": "[4] 1/6 oktávy / 60 pásem",
"5": "[5] 1/4 oktávy / 40 pásem",
"6": "[6] 1/3 oktávy / 30 pásem",
"7": "[7] Polovina oktávy / 20 pásem",
"8": "[8] Celá oktáva / 10 pásem",
"10": "[10] Linka / Graf oblasti"
}
},
"pasteGradient": "Vložit přechod",
"pasteGradientPlaceholder": "Sem vložte JSON přechodu…"
} }
} }
+47 -121
View File
@@ -1,7 +1,7 @@
{ {
"action": { "action": {
"editPlaylist": "$t(entity.playlist_one) bearbeiten", "editPlaylist": "$t(entity.playlist_one) bearbeiten",
"clearQueue": "Wiedergabeliste leeren", "clearQueue": "Warteschlange leeren",
"addToFavorites": "Zu $t(entity.favorite_other) hinzufügen", "addToFavorites": "Zu $t(entity.favorite_other) hinzufügen",
"addToPlaylist": "Zu $t(entity.playlist_one) hinzufügen", "addToPlaylist": "Zu $t(entity.playlist_one) hinzufügen",
"createPlaylist": "$t(entity.playlist_one) erstellen", "createPlaylist": "$t(entity.playlist_one) erstellen",
@@ -13,7 +13,7 @@
"removeFromPlaylist": "Aus $t(entity.playlist_one) entfernen", "removeFromPlaylist": "Aus $t(entity.playlist_one) entfernen",
"viewPlaylists": "$t(entity.playlist_other) anzeigen", "viewPlaylists": "$t(entity.playlist_other) anzeigen",
"refresh": "$t(common.refresh)", "refresh": "$t(common.refresh)",
"removeFromQueue": "Aus Wiedergabeliste entfernen", "removeFromQueue": "Aus Warteschlange entfernen",
"setRating": "Bewerten", "setRating": "Bewerten",
"toggleSmartPlaylistEditor": "Editor für $t(entity.smartPlaylist) ein-/ausblenden", "toggleSmartPlaylistEditor": "Editor für $t(entity.smartPlaylist) ein-/ausblenden",
"removeFromFavorites": "Aus $t(entity.favorite_other) entfernen", "removeFromFavorites": "Aus $t(entity.favorite_other) entfernen",
@@ -29,11 +29,7 @@
"shuffleSelected": "Ausgewählte zufällig wiedergeben", "shuffleSelected": "Ausgewählte zufällig wiedergeben",
"viewMore": "Mehr zeigen", "viewMore": "Mehr zeigen",
"moveUp": "Nach oben bewegen", "moveUp": "Nach oben bewegen",
"moveDown": "Nach unten bewegen", "moveDown": "Nach unten bewegen"
"createRadioStation": "$t(entity.radioStation_one) erstellen",
"deleteRadioStation": "$t(entity.radioStation_one) löschen",
"selectAll": "alle auswählen",
"openApplicationDirectory": "Anwendungsverzeichnis öffnen"
}, },
"common": { "common": {
"backward": "zurück", "backward": "zurück",
@@ -122,11 +118,11 @@
"close": "schließen", "close": "schließen",
"share": "Teilen", "share": "Teilen",
"translation": "Übersetzung", "translation": "Übersetzung",
"trackGain": "Track Gain", "trackGain": "Track-Pegelverstärkung",
"trackPeak": "Track Peak", "trackPeak": "Track-Spitzenpegel",
"codec": "Codec", "codec": "Codec",
"albumPeak": "Album-Spitzenpegel", "albumPeak": "Album-Spitzenpegel",
"albumGain": "Album Gain", "albumGain": "Album-Pegelverstärkung",
"tags": "tags", "tags": "tags",
"viewReleaseNotes": "Veröffentlichungsnotizen anzeigen", "viewReleaseNotes": "Veröffentlichungsnotizen anzeigen",
"newVersion": "eine neue Version wurde installiert ({{version}})", "newVersion": "eine neue Version wurde installiert ({{version}})",
@@ -149,9 +145,7 @@
"recordLabel": "Plattenlabel", "recordLabel": "Plattenlabel",
"slower": "langsamer", "slower": "langsamer",
"releaseType": "Veröffentlichungsformat", "releaseType": "Veröffentlichungsformat",
"view": "Betrachten", "view": "Betrachten"
"countSelected": "{{count}} ausgewählt",
"mood": "Stimmung"
}, },
"error": { "error": {
"remotePortWarning": "Starten Sie den Server neu, um den neuen Port anzuwenden", "remotePortWarning": "Starten Sie den Server neu, um den neuen Port anzuwenden",
@@ -173,15 +167,11 @@
"audioDeviceFetchError": "Beim Versuch, Audiogeräte abzurufen, ist ein Fehler aufgetreten", "audioDeviceFetchError": "Beim Versuch, Audiogeräte abzurufen, ist ein Fehler aufgetreten",
"invalidServer": "Ungültiger Server", "invalidServer": "Ungültiger Server",
"loginRateError": "Zu viele Anmeldeversuche, bitte versuche es in einigen Sekunden erneut", "loginRateError": "Zu viele Anmeldeversuche, bitte versuche es in einigen Sekunden erneut",
"badAlbum": "sie sehen diese Seite, weil dieses Lied nicht Teil eines Albums ist. Dieses Problem tritt meist auf, wenn sich ein Lied im Überordner befindet. Jellyfin gruppiert Tracks nur, wenn diese sich innerhalb eines Ordners befinden", "badAlbum": "sie sehen diese Seite, weil dieses Lied nicht Teil eines Albums ist. Wahrscheinlich sehen Sie dieses Problem, wenn Sie einen Song in Ihrem Musikordner auf oberster Ebene haben. Jellyfin gruppiert nur Songs, wenn sie sich in einem Ordner befinden",
"networkError": "ein Netzwerkfehler ist aufgetreten", "networkError": "ein Netzwerkfehler ist aufgetreten",
"openError": "datei kann nicht geöffnet werden", "openError": "datei kann nicht geöffnet werden",
"badValue": "ungültige option \"{{value}}\". Dieser Wert existiert nicht mehr", "badValue": "ungültige option \"{{value}}\". Dieser Wert existiert nicht mehr",
"notificationDenied": "Berechtigungen über Benachrichtigungen wurden verweigert. Diese Einstellung hat keinen Effekt", "notificationDenied": "Berechtigungen über Benachrichtigungen wurden verweigert. Diese Einstellung hat keinen Effekt"
"saveQueueFailed": "Wiedergabeliste konnte nicht gespeichert werden",
"multipleServerSaveQueueError": "die Wiedergabeliste enthält einen oder mehrere Titel, die nicht vom aktuellen Server stammen. dies wird nicht unterstützt",
"noNetwork": "Server nicht verfügbar",
"noNetworkDescription": "Verbindung zum Server konnte nicht hergestellt werden"
}, },
"filter": { "filter": {
"mostPlayed": "Meistgespielt", "mostPlayed": "Meistgespielt",
@@ -294,7 +284,7 @@
"setExpiration": "Ablaufdatum setzen", "setExpiration": "Ablaufdatum setzen",
"expireInvalid": "Ablaufdatum muss in der Zukunft liegen", "expireInvalid": "Ablaufdatum muss in der Zukunft liegen",
"allowDownloading": "Herunterladen zulassen", "allowDownloading": "Herunterladen zulassen",
"success": "Link in die Zwischenablage kopiert (oder hier klicken, um zu öffnen)", "success": "Link in die Zwischenablage kopiert (oder hier klicken um zu öffnen)",
"createFailed": "fehler beim Teilen (Ist Teilen aktiviert?)" "createFailed": "fehler beim Teilen (Ist Teilen aktiviert?)"
}, },
"privateMode": { "privateMode": {
@@ -303,7 +293,7 @@
"title": "Privatmodus" "title": "Privatmodus"
}, },
"largeFetchConfirmation": { "largeFetchConfirmation": {
"title": "Elemente der Wiedergabeliste hinzufügen", "title": "Elemente der Warteschlange hinzufügen",
"description": "Diese Aktion fügt alle Elemente in der aktuell gefilterten Ansicht hinzu" "description": "Diese Aktion fügt alle Elemente in der aktuell gefilterten Ansicht hinzu"
}, },
"shuffleAll": { "shuffleAll": {
@@ -313,22 +303,9 @@
"input_minYear": "ab Jahr", "input_minYear": "ab Jahr",
"input_maxYear": "bis Jahr", "input_maxYear": "bis Jahr",
"input_played_optionAll": "alle Tracks", "input_played_optionAll": "alle Tracks",
"input_played_optionUnplayed": "nur nicht gespielte Tracks", "input_played_optionUnplayed": "nur ungespielte Tracks",
"input_played_optionPlayed": "nur gespielte Tracks", "input_played_optionPlayed": "nur gespielte Tracks",
"input_played": "Wiedergabefilter" "input_played": "Wiedergabefilter"
},
"saveQueue": {
"success": "Wiedergabeliste auf Server gespeichert"
},
"createRadioStation": {
"success": "Radiosender erfolgreich erstellt",
"title": "Radiosender erstellen",
"input_homepageUrl": "Homepage URL",
"input_name": "Name",
"input_streamUrl": "Stream URL"
},
"lyricsExport": {
"input_offset": "$t(setting.lyricOffset)"
} }
}, },
"entity": { "entity": {
@@ -366,11 +343,7 @@
"play_one": "{{count}} Wiedergabe", "play_one": "{{count}} Wiedergabe",
"play_other": "{{count}} Wiedergaben", "play_other": "{{count}} Wiedergaben",
"song_one": "Lied", "song_one": "Lied",
"song_other": "Lieder", "song_other": "Lieder"
"radioStation_one": "Radiosender",
"radioStation_other": "Radiosender",
"radioStationWithCount_one": "{{count}} Radiosender",
"radioStationWithCount_other": "{{count}} Radiosender"
}, },
"table": { "table": {
"config": { "config": {
@@ -386,17 +359,7 @@
"displayType": "Anzeigestil", "displayType": "Anzeigestil",
"autoFitColumns": "automatisch Spalten einpassen", "autoFitColumns": "automatisch Spalten einpassen",
"size_default": "Standard", "size_default": "Standard",
"followCurrentSong": "aktuellem Titel folgen", "followCurrentSong": "aktuellem Titel folgen"
"advancedSettings": "erweiterte Einstellungen",
"autosize": "automatische Größe",
"alignLeft": "linksbündig",
"alignCenter": "mittig",
"alignRight": "rechtsbündig",
"size_compact": "kompakt",
"size_large": "groß",
"pagination": "Seitenzahlen",
"pagination_itemsPerPage": "Elemente pro Seite",
"pagination_infinite": "unendlich"
}, },
"label": { "label": {
"dateAdded": "Hinzugefügt am", "dateAdded": "Hinzugefügt am",
@@ -424,14 +387,7 @@
"title": "$t(common.title)", "title": "$t(common.title)",
"year": "$t(common.year)", "year": "$t(common.year)",
"discNumber": "disk-Nummer", "discNumber": "disk-Nummer",
"playCount": "Wiedergaben", "playCount": "Wiedergaben"
"albumCount": "$t(entity.album_other)",
"bitDepth": "$t(common.bitDepth)",
"codec": "$t(common.codec)",
"image": "Bild",
"sampleRate": "$t(common.sampleRate)",
"songCount": "$t(entity.track_other)",
"genreBadge": "$t(entity.genre_one) (Abzeichen)"
} }
}, },
"column": { "column": {
@@ -457,11 +413,7 @@
"genre": "$t(entity.genre_one)", "genre": "$t(entity.genre_one)",
"songCount": "$t(entity.track_other)", "songCount": "$t(entity.track_other)",
"trackNumber": "titel", "trackNumber": "titel",
"size": "$t(common.size)", "size": "$t(common.size)"
"bitDepth": "$t(common.bitDepth)",
"codec": "$t(common.codec)",
"sampleRate": "$t(common.sampleRate)",
"owner": "Besitzer"
} }
}, },
"page": { "page": {
@@ -570,8 +522,7 @@
"albumArtists": "$t(entity.albumArtist_other)", "albumArtists": "$t(entity.albumArtist_other)",
"shared": "$t(entity.playlist_other) geteilt", "shared": "$t(entity.playlist_other) geteilt",
"myLibrary": "meine bibliothek", "myLibrary": "meine bibliothek",
"favorites": "$t(entity.favorite_other)", "favorites": "$t(entity.favorite_other)"
"radio": "$t(entity.radioStation_other)"
}, },
"setting": { "setting": {
"playbackTab": "Wiedergabe", "playbackTab": "Wiedergabe",
@@ -587,7 +538,7 @@
"application": "App", "application": "App",
"queryBuilder": "Abfrage-Editor", "queryBuilder": "Abfrage-Editor",
"theme": "Erscheinungsbild", "theme": "Erscheinungsbild",
"controls": "Steuerelemente", "controls": "Steuerung",
"sidebar": "Seitenleiste", "sidebar": "Seitenleiste",
"scrobble": "Scrobbeln", "scrobble": "Scrobbeln",
"audio": "Audio", "audio": "Audio",
@@ -627,9 +578,7 @@
"topSongsFrom": "Toplieder von {{title}}", "topSongsFrom": "Toplieder von {{title}}",
"viewAll": "Alles ansehen", "viewAll": "Alles ansehen",
"topSongs": "Toplieder", "topSongs": "Toplieder",
"relatedArtists": "ähnliche $t(entity.artist_other)", "relatedArtists": "ähnliche $t(entity.artist_other)"
"groupingTypeAll": "alle Veröffentlichungsformate",
"groupingTypePrimary": "primäre Veröffentlichungsformate"
}, },
"manageServers": { "manageServers": {
"title": "Servers verwalten", "title": "Servers verwalten",
@@ -652,17 +601,14 @@
}, },
"playlist": { "playlist": {
"reorder": "Neuanordnung nur bei Sortierung nach ID möglich" "reorder": "Neuanordnung nur bei Sortierung nach ID möglich"
},
"radioList": {
"title": "Radiosender"
} }
}, },
"player": { "player": {
"next": "nächster", "next": "nächster",
"addNext": "als Nächstes", "addNext": "Als Nächstes spielen",
"play": "Abspielen", "play": "Abspielen",
"muted": "stummgeschaltet", "muted": "stummgeschaltet",
"addLast": "als Letztes", "addLast": "Als Letztes spielen",
"mute": "Stumm", "mute": "Stumm",
"playRandom": "Zufällige Wiedergabe", "playRandom": "Zufällige Wiedergabe",
"previous": "Vorheriger", "previous": "Vorheriger",
@@ -671,7 +617,7 @@
"playbackFetchInProgress": "lieder werden geladen…", "playbackFetchInProgress": "lieder werden geladen…",
"playbackSpeed": "Wiedergabegeschwindigkeit", "playbackSpeed": "Wiedergabegeschwindigkeit",
"playbackFetchCancel": "Das dauert eine Weile. Schließen Sie die Benachrichtigung, um den Vorgang abzubrechen", "playbackFetchCancel": "Das dauert eine Weile. Schließen Sie die Benachrichtigung, um den Vorgang abzubrechen",
"queue_clear": "Wiedergabeliste bereinigen", "queue_clear": "Bereinige Warteschlange",
"repeat_all": "Alle wiederholen", "repeat_all": "Alle wiederholen",
"repeat": "Wiederholen", "repeat": "Wiederholen",
"queue_remove": "Ausgewählte entfernen", "queue_remove": "Ausgewählte entfernen",
@@ -688,15 +634,13 @@
"skip_forward": "vorspulen", "skip_forward": "vorspulen",
"skip": "Überspringen", "skip": "Überspringen",
"playSimilarSongs": "Ähnliche Lieder abspielen", "playSimilarSongs": "Ähnliche Lieder abspielen",
"viewQueue": "Wiedergabeliste anzeigen", "viewQueue": "Warteschlange anzeigen",
"addLastShuffled": "als Letztes (zufällige Wiedergabe)", "addLastShuffled": "Als Letztes spielen (zufällige Wiedergabe)",
"addNextShuffled": "als Nächstes (zufällige Wiedergabe)", "addNextShuffled": "Als Nächstes spielen (zufällige Wiedergabe)",
"queueType_default": "Standard", "queueType_default": "Standard",
"queueType_priority": "Priorität", "queueType_priority": "Priorität",
"holdToShuffle": "Halten für Zufallswiedergabe", "holdToShuffle": "Halten für Zufallswiedergabe",
"queueType": "Wiedergabelistentyp", "queueType": "Warteschlangentyp"
"restoreQueueFromServer": "Wiedergabeliste von Server wiederherstellen",
"saveQueueToServer": "Wiedergabeliste auf Server speichern"
}, },
"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)",
@@ -772,7 +716,7 @@
"themeLight_description": "Legt das Erscheinungsbild für den hellen Modus fest", "themeLight_description": "Legt das Erscheinungsbild für den hellen Modus fest",
"hotkey_toggleFullScreenPlayer": "Vollbildmodus umschalten", "hotkey_toggleFullScreenPlayer": "Vollbildmodus umschalten",
"hotkey_localSearch": "Suche auf Seite", "hotkey_localSearch": "Suche auf Seite",
"hotkey_toggleQueue": "Wiedergabeliste umschalten", "hotkey_toggleQueue": "Warteschlange umschalten",
"remotePassword_description": "Legt das Passwort für den Fernsteuerungsserver fest. Diese Anmeldeinformationen werden standardmäßig unsicher übertragen, daher sollten Sie ein Passwort verwenden, das Ihnen egal ist", "remotePassword_description": "Legt das Passwort für den Fernsteuerungsserver fest. Diese Anmeldeinformationen werden standardmäßig unsicher übertragen, daher sollten Sie ein Passwort verwenden, das Ihnen egal ist",
"hotkey_rate5": "Bewertung 5 Sterne", "hotkey_rate5": "Bewertung 5 Sterne",
"hotkey_playbackPrevious": "Vorheriger Track", "hotkey_playbackPrevious": "Vorheriger Track",
@@ -783,18 +727,18 @@
"playbackStyle_description": "Wählen Sie den Wiedergabestil aus, der für den Audioplayer verwendet werden soll", "playbackStyle_description": "Wählen Sie den Wiedergabestil aus, der für den Audioplayer verwendet werden soll",
"mpvExecutablePath": "Pfad der ausführbaren MPV-Datei", "mpvExecutablePath": "Pfad der ausführbaren MPV-Datei",
"hotkey_rate2": "Bewertung 2 Sterne", "hotkey_rate2": "Bewertung 2 Sterne",
"playButtonBehavior_description": "legt das Standardverhalten des Wiedergabe-Buttons fest, wenn Lieder zur Wiedergabeliste hinzugefügt werden", "playButtonBehavior_description": "Legt das Standardverhalten des Wiedergabe-Buttons fest, wenn Songs zur Warteschlange hinzugefügt werden",
"minimumScrobblePercentage_description": "die Mindestdauer in Prozent, welche das Lied gespielt werden muss, bevor dieses gescrobbelt wird", "minimumScrobblePercentage_description": "die Mindestdauer in Prozent, welche das Lied gespielt werden muss, bevor dieses gescrobbelt wird",
"hotkey_rate4": "Bewertung 4 Sterne", "hotkey_rate4": "Bewertung 4 Sterne",
"showSkipButton_description": "Ein- oder Ausblenden der Überspringen-Schaltflächen in der Player-Leiste", "showSkipButton_description": "Ein- oder Ausblenden der Überspringen-Schaltflächen in der Player-Leiste",
"savePlayQueue": "Wiedergabeliste speichern", "savePlayQueue": "Wiedergabe-Warteschlange speichern",
"minimumScrobbleSeconds_description": "die Mindestdauer in Sekunden, welche das Lied gespielt werden muss, bevor dieses gescrobbelt wird", "minimumScrobbleSeconds_description": "die Mindestdauer in Sekunden, welche das Lied gespielt werden muss, bevor dieses gescrobbelt wird",
"skipPlaylistPage_description": "Gehe beim Navigieren zu einer Wiedergabeliste zu deren Titelseite und nicht zur Standardseite", "skipPlaylistPage_description": "Gehe beim Navigieren zu einer Wiedergabeliste zu deren Titelseite und nicht zur Standardseite",
"fontType_description": "Die integrierte Schriftart wählt eine der von feishin bereitgestellten Schriftarten aus. Mit der Systemschriftart können Sie jede von Ihrem Betriebssystem bereitgestellte Schriftart auswählen. Benutzerdefiniert erlaubt es eine eigene Schriftart bereitzustellen", "fontType_description": "Die integrierte Schriftart wählt eine der von feishin bereitgestellten Schriftarten aus. Mit der Systemschriftart können Sie jede von Ihrem Betriebssystem bereitgestellte Schriftart auswählen. Benutzerdefiniert erlaubt es eine eigene Schriftart bereitzustellen",
"playButtonBehavior": "Verhalten der Wiedergabetaste", "playButtonBehavior": "Verhalten der Wiedergabetaste",
"volumeWheelStep": "Lautstärkeänderung mit Mausrad", "volumeWheelStep": "Lautstärkeänderung mit Mausrad",
"sidebarPlaylistList_description": "Ein- oder Ausblenden der Playlisten-Liste in der Seitenleiste", "sidebarPlaylistList_description": "Ein- oder Ausblenden der Playlisten-Liste in der Seitenleiste",
"sidePlayQueueStyle_description": "legt den Stil der Wiedergabeliste in der Seitenleiste fest", "sidePlayQueueStyle_description": "Legt den Stil der Wiedergabewarteliste in der Seitenleiste fest",
"replayGainMode": "{{ReplayGain}} Modus", "replayGainMode": "{{ReplayGain}} Modus",
"playbackStyle_optionNormal": "Normal", "playbackStyle_optionNormal": "Normal",
"windowBarStyle": "Fensterleistenstil", "windowBarStyle": "Fensterleistenstil",
@@ -825,7 +769,7 @@
"gaplessAudio_optionWeak": "schwach (empfohlen)", "gaplessAudio_optionWeak": "schwach (empfohlen)",
"minimumScrobbleSeconds": "Minimum Scrobble-Dauer (Sekunden)", "minimumScrobbleSeconds": "Minimum Scrobble-Dauer (Sekunden)",
"hotkey_playbackStop": "Stoppen", "hotkey_playbackStop": "Stoppen",
"savePlayQueue_description": "speichert die Wiedergabeliste beim Schließen der Anwendung, und stellt diese wieder her, wenn die Anwendung geöffnet wird", "savePlayQueue_description": "Speichert Wiedergabewarteschlange, wenn die Anwendung geschlossen wird, und stellt sie wieder her, wenn die Anwendung geöffnet wird",
"useSystemTheme": "Nach Erscheinungsbild des Systems richten", "useSystemTheme": "Nach Erscheinungsbild des Systems richten",
"enableRemote_description": "Aktiviert den Server für die Fernsteuerung, damit andere Geräte die Anwendung steuern können", "enableRemote_description": "Aktiviert den Server für die Fernsteuerung, damit andere Geräte die Anwendung steuern können",
"fontType_optionSystem": "System Schriftart", "fontType_optionSystem": "System Schriftart",
@@ -851,33 +795,33 @@
"clearCache": "Browser-Zwischenspeicher löschen", "clearCache": "Browser-Zwischenspeicher löschen",
"clearQueryCache": "feishins Zwischenspeicher leeren", "clearQueryCache": "feishins Zwischenspeicher leeren",
"clearCache_description": "Hartes Zurücksetzen. Neben feishins Zwischenspeicher wird auch der des Browsers gelöscht (Bilder und andere Daten). Zugangsinformationen und Einstellungen werden behalten", "clearCache_description": "Hartes Zurücksetzen. Neben feishins Zwischenspeicher wird auch der des Browsers gelöscht (Bilder und andere Daten). Zugangsinformationen und Einstellungen werden behalten",
"sidePlayQueueStyle": "Stil der Wiedergabeliste in der Seitenleiste", "sidePlayQueueStyle": "Wiedergabelistenstil in der Seitenleiste",
"zoom_description": "Setzt den Zoom (in %) für das Programm", "zoom_description": "Setzt den Zoom (in %) für das Programm",
"zoom": "Zoom", "zoom": "Zoom",
"albumBackground": "Album Hintergrund", "albumBackground": "Album Hintergrund",
"customCss": "Benutzerdefiniertes CSS", "customCss": "Benutzerdefiniert css",
"homeConfiguration": "Startseite Konfiguration", "homeConfiguration": "Startseite Konfiguration",
"lastfmApiKey": "{{lastfm}} API-Schlüssel", "lastfmApiKey": "{{lastfm}} API-Schlüssel",
"lastfmApiKey_description": "Der API-Schlüssel für {{lastfm}}. wird für Albumcover benötigt", "lastfmApiKey_description": "Der API-Schlüssel für {{lastfm}}. wird für benötigt",
"discordListening": "Status als hört zu anzeigen", "discordListening": "Status als hört zu anzeigen",
"discordListening_description": "Status als hört zu statt als spielt anzeigen", "discordListening_description": "Status als hört zu statt als spielt anzeigen",
"lastfm": "zeige last.fm links", "lastfm": "zeige last.fm links",
"lastfm_description": "zeige links zu Last.fm auf dem Künstler/Album-Seiten", "lastfm_description": "zeige links zu Last.fm auf dem Künstler/Album-Seiten",
"musicbrainz": "Zeig MusicBrainz links", "musicbrainz": "Zeig MusicBrainz links",
"customCssEnable": "benutzerdefiniertes CSS aktivieren", "customCssEnable": "aktiviere Benutzerdefinierte css",
"albumBackground_description": "fügt ein Hintergrundbild für die Albumseiten hinzu, welche das Albumcover zeigen", "albumBackground_description": "fügt ein Hintergrundbild für die Albumseiten hinzu, welche das Albumcover zeigen",
"albumBackgroundBlur": "Größe der Album-Bildunschärfe", "albumBackgroundBlur": "Größe der Album-Bildunschärfe",
"albumBackgroundBlur_description": "passt die Stärke der Unschärfe an, welche auf das Hintergrundbild des Albums angewandt wird", "albumBackgroundBlur_description": "passt die Stärke der Unschärfe an, welche auf das Hintergrundbild des Albums angewandt wird",
"clearCacheSuccess": "Cache erfolgreich geleert", "clearCacheSuccess": "Cache erfolgreich geleert",
"contextMenu": "Kontextmenü-Einstellungen (Rechtsklick)", "contextMenu": "Kontextmenü-Einstellungen (Rechtsklick)",
"customCssEnable_description": "erlaubt das Hinzufügen von benutzerdefiniertem CSS", "customCssEnable_description": "ermöglicht das Schreiben benutzerdefinierten CSS",
"artistBackground": "Künstler Hintergrundbild", "artistBackground": "Künstler Hintergrundbild",
"artistBackground_description": "fügt ein Hintergrundbild für die Künstlerseite hinzu", "artistBackground_description": "fügt ein Hintergrundbild für die Künstlerseite hinzu",
"artistConfiguration": "künstler Albumseite Konfiguration", "artistConfiguration": "künstler Albumseite Konfiguration",
"buttonSize": "spielerleisten-Knopfgröße", "buttonSize": "spielerleisten-Knopfgröße",
"buttonSize_description": "die Größe der Spieler-Knöpfe", "buttonSize_description": "die Größe der Spieler-Knöpfe",
"hotkey_togglePreviousSongFavorite": "wähle $t(common.previousSong) als Favorit aus", "hotkey_togglePreviousSongFavorite": "wähle $t(common.previousSong) als Favorit aus",
"replayGainFallback": "{{ReplayGain}} Alternative", "replayGainFallback": "{{ReplayGain}} Rückgriff",
"replayGainClipping": "{{ReplayGain}} Clipping", "replayGainClipping": "{{ReplayGain}} Clipping",
"exportImportSettings_control_description": "Einstellungen mit JSON exportieren und importieren", "exportImportSettings_control_description": "Einstellungen mit JSON exportieren und importieren",
"exportImportSettings_control_exportText": "Einstellungen exportieren", "exportImportSettings_control_exportText": "Einstellungen exportieren",
@@ -889,7 +833,9 @@
"exportImportSettings_notValidJSON": "Die Datei ist kein gültiges JSON", "exportImportSettings_notValidJSON": "Die Datei ist kein gültiges JSON",
"exportImportSettings_offendingKeyError": "\"{{offendingKey}}\" ist falsch - {{reason}}", "exportImportSettings_offendingKeyError": "\"{{offendingKey}}\" ist falsch - {{reason}}",
"language": "Sprache", "language": "Sprache",
"playerAlbumArtResolution": "Auflösung des Albumcovers",
"imageAspectRatio": "Original Seitenverhältnis des Albumcovers verwenden", "imageAspectRatio": "Original Seitenverhältnis des Albumcovers verwenden",
"playerAlbumArtResolution_description": "die Auflösung des Albumcovers im großen Player. Eine höhere Auflösung sorgt für ein schärferes Bild, kann jedoch das Laden verlangsamen. Standardwert: 0 (automatische Berechnung)",
"analyticsDisable": "Keine nutzungsbasierte Analyse", "analyticsDisable": "Keine nutzungsbasierte Analyse",
"analyticsDisable_description": "Anonymisierte Nutzungsdaten werden an den Entwickler geschickt, um die Anwendung zu verbessern", "analyticsDisable_description": "Anonymisierte Nutzungsdaten werden an den Entwickler geschickt, um die Anwendung zu verbessern",
"logLevel_optionDebug": "Debug", "logLevel_optionDebug": "Debug",
@@ -898,11 +844,11 @@
"logLevel_optionError": "Fehler", "logLevel_optionError": "Fehler",
"logLevel_optionInfo": "Info", "logLevel_optionInfo": "Info",
"logLevel_optionWarn": "Warnung", "logLevel_optionWarn": "Warnung",
"autoDJ_description": "füge automatisch ähnliche Lieder der Wiedergabeliste hinzu", "autoDJ_description": "Füge automatisch ähnliche Lieder der Warteschlange hinzu",
"autoDJ": "Auto DJ", "autoDJ": "Auto DJ",
"autoDJ_itemCount": "Anzahl", "autoDJ_itemCount": "Anzahl",
"autoDJ_itemCount_description": "die Anzahl der Lieder, die bei aktiviertem Auto DJ zur Wiedergabeliste hinzugefügt werden sollen", "autoDJ_itemCount_description": "Die Anzahl der Lieder, die bei aktiviertem Auto DJ zur Warteschlange hinzugefügt werden sollen",
"autoDJ_timing_description": "die Anzahl der Lieder, die sich noch in der Wiedergabeliste befinden, bevor Auto DJ ausgelöst wird", "autoDJ_timing_description": "Die Anzahl der Lieder, die sich noch in der Warteschlange befinden, bevor Auto DJ ausgelöst wird",
"autoDJ_timing": "Timing", "autoDJ_timing": "Timing",
"discordDisplayType": "{{discord}} Presence Darstellungsart", "discordDisplayType": "{{discord}} Presence Darstellungsart",
"discordLinkType_mbz_lastfm": "{{musicbrainz}} mit {{lastfm}} als Ersatz", "discordLinkType_mbz_lastfm": "{{musicbrainz}} mit {{lastfm}} als Ersatz",
@@ -921,11 +867,11 @@
"neteaseTranslation": "NetEase Übersetzungen aktivieren", "neteaseTranslation": "NetEase Übersetzungen aktivieren",
"notify": "Benachrichtigungen aktivieren", "notify": "Benachrichtigungen aktivieren",
"notify_description": "Zeigt Benachrichtigungen beim Titelwechsel", "notify_description": "Zeigt Benachrichtigungen beim Titelwechsel",
"playerFilters": "Lieder der Wiedergabeliste filtern", "playerFilters": "Lieder in der Warteschlange filtern",
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)", "playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
"volumeWidth_description": "Die Breite des Lautstärkereglers", "volumeWidth_description": "Die Breite des Lautstärkereglers",
"volumeWidth": "Lautstärkereglerbreite", "volumeWidth": "Lautstärkereglerbreite",
"webAudio_description": "Web-Audio verwenden. Dies ermöglicht erweiterte Funktionen wie ReplayGain. Deaktiviere die Option, falls bei der Wiedergabe Probleme auftreten", "webAudio_description": "Web-Audio verwenden. Dies ermöglicht erweiterte Funktionen wie ReplayGain. Deaktiviere dies, wenn bei der Wiedergabe Probleme auftreten",
"webAudio": "Web-Audio verwenden", "webAudio": "Web-Audio verwenden",
"trayEnabled": "Info-Symbol anzeigen", "trayEnabled": "Info-Symbol anzeigen",
"transcode": "Transkodierung aktivieren", "transcode": "Transkodierung aktivieren",
@@ -943,7 +889,7 @@
"artistConfiguration_description": "Legt fest, welche Elemente auf der Albumkünstlerseite angezeigt werden und in welcher Reihenfolge", "artistConfiguration_description": "Legt fest, welche Elemente auf der Albumkünstlerseite angezeigt werden und in welcher Reihenfolge",
"contextMenu_description": "Legt die Einträge fest, die im Rechtsklick-Menü angezeigt werden sollen. Abgewählte Einträge werden ausgeblendet", "contextMenu_description": "Legt die Einträge fest, die im Rechtsklick-Menü angezeigt werden sollen. Abgewählte Einträge werden ausgeblendet",
"crossfadeStyle": "Art der Überblende", "crossfadeStyle": "Art der Überblende",
"customCss_description": "Benutzerdefinierter CSS-Inhalt. Hinweis: Inhalte und Remote-URLs sind nicht zulässige Eigenschaften. Unten siehst du eine Vorschau deines Inhalts. Aufgrund von Bereinigung werden womöglich zusätzliche, nicht von dir definierte Felder angezeigt", "customCss_description": "Benutzerdefinierter CSS-Inhalt. Hinweis: content und entfernte URLs sind unzulässig. Siehe Vorschau unten. Aufgrund von Bereinigung werden womöglich nicht gesetzte Felder angezeigt",
"customCssNotice": "Warnung: Obwohl eine gewisse Bereinigung erfolgt (url() und content: sind nicht zulässig), kann die Verwendung von benutzerdefiniertem CSS dennoch Risiken mit sich bringen, da dadurch die Benutzeroberfläche verändert wird", "customCssNotice": "Warnung: Obwohl eine gewisse Bereinigung erfolgt (url() und content: sind nicht zulässig), kann die Verwendung von benutzerdefiniertem CSS dennoch Risiken mit sich bringen, da dadurch die Benutzeroberfläche verändert wird",
"releaseChannel_optionBeta": "Beta", "releaseChannel_optionBeta": "Beta",
"releaseChannel_optionLatest": "Stabil", "releaseChannel_optionLatest": "Stabil",
@@ -957,25 +903,11 @@
"exportImportSettings_destructiveWarning": "Das Importieren von Einstellungen ist irreversibel. Bitte lies die Hinweise oben sorgfältig durch, bevor du auf \"Importieren\" klickst!", "exportImportSettings_destructiveWarning": "Das Importieren von Einstellungen ist irreversibel. Bitte lies die Hinweise oben sorgfältig durch, bevor du auf \"Importieren\" klickst!",
"followCurrentSong": "aktuellem Titel folgen", "followCurrentSong": "aktuellem Titel folgen",
"followCurrentSong_description": "die Wiedergabeliste scrollt automatisch zum aktuellen Titel", "followCurrentSong_description": "die Wiedergabeliste scrollt automatisch zum aktuellen Titel",
"playerFilters_description": "verhindert, dass Titel anhand der folgenden Kriterien zur Wiedergabeliste hinzugefügt werden", "playerFilters_description": "verhindert, dass Titel anhand der folgenden Kriterien zur Warteschlange hinzugefügt werden",
"preferLocalLyrics_description": "lokale Songtexte gegenüber externen Quellen bevorzugen (sofern verfügbar)", "preferLocalLyrics_description": "lokale Songtexte gegenüber externen Quellen bevorzugen (sofern verfügbar)",
"preferLocalLyrics": "Priorisiere lokale Songtexte", "preferLocalLyrics": "Priorisiere lokale Songtexte",
"showLyricsInSidebar_description": "ein Bereich, in dem Songtexte angezeigt werden, wird der Wiedergabeliste hinzugefügt", "showLyricsInSidebar_description": "ein Bereich, in dem Songtexte angezeigt werden, wird der Wiedergabeliste hinzugefügt",
"showLyricsInSidebar": "zeige Songtexte in der Player-Seitenleiste", "showLyricsInSidebar": "zeige Songtexte in der Player-Seitenleiste"
"homeFeature_description": "steuert, ob das große Featured-Karussell auf der Startseite angezeigt wird",
"homeFeature": "Feature-Karussell",
"playerbarWaveformAlign_optionTop": "Oben",
"playerbarWaveformAlign_optionCenter": "Mitte",
"playerbarWaveformAlign_optionBottom": "Unten",
"translationApiKey_description": "API-Schlüssel für Übersetzungen (nur globale Service-Endpunkte)",
"translationApiKey": "API-Schlüssel für Übersetzungen",
"translationApiProvider_description": "API-Anbieter für Übersetzungen",
"translationApiProvider": "API-Anbieter für Übersetzungen",
"hotkey_navigateHome": "zurück zur Startseite",
"translationTargetLanguage_description": "die gewünschte Sprache der Übersetzung",
"translationTargetLanguage": "Zielsprache der Übersetzung",
"queryBuilderCustomFields": "benutzerdefiniertes Feld",
"queryBuilderCustomFields_inputTag": "Tag"
}, },
"dragDropZone": { "dragDropZone": {
"error_oneFileOnly": "Bitte wähle nur 1 Datei", "error_oneFileOnly": "Bitte wähle nur 1 Datei",
@@ -1029,11 +961,5 @@
"soundtrack": "Soundtrack", "soundtrack": "Soundtrack",
"spokenWord": "Gesprochenes Wort" "spokenWord": "Gesprochenes Wort"
} }
},
"datetime": {
"minuteShort": "Min",
"secondShort": "Sek",
"hourShort": "Std",
"dayShort": "Tag"
} }
} }
+6 -201
View File
@@ -2,14 +2,11 @@
"action": { "action": {
"addToFavorites": "add to $t(entity.favorite_other)", "addToFavorites": "add to $t(entity.favorite_other)",
"addToPlaylist": "add to $t(entity.playlist_one)", "addToPlaylist": "add to $t(entity.playlist_one)",
"addOrRemoveFromSelection": "add or remove from selection",
"selectRangeOfItems": "select a range of items",
"clearQueue": "clear queue", "clearQueue": "clear queue",
"createPlaylist": "create $t(entity.playlist_one)", "createPlaylist": "create $t(entity.playlist_one)",
"createRadioStation": "create $t(entity.radioStation_one)", "createRadioStation": "create $t(entity.radioStation_one)",
"deletePlaylist": "delete $t(entity.playlist_one)", "deletePlaylist": "delete $t(entity.playlist_one)",
"deleteRadioStation": "delete $t(entity.radioStation_one)", "deleteRadioStation": "delete $t(entity.radioStation_one)",
"selectAll": "select all",
"deselectAll": "deselect all", "deselectAll": "deselect all",
"downloadStarted": "started download of {{count}} items", "downloadStarted": "started download of {{count}} items",
"editPlaylist": "edit $t(entity.playlist_one)", "editPlaylist": "edit $t(entity.playlist_one)",
@@ -40,7 +37,6 @@
} }
}, },
"common": { "common": {
"countSelected": "{{count}} selected",
"explicitStatus": "explicit status", "explicitStatus": "explicit status",
"action_one": "action", "action_one": "action",
"action_other": "actions", "action_other": "actions",
@@ -83,7 +79,6 @@
"edit": "edit", "edit": "edit",
"enable": "enable", "enable": "enable",
"expand": "expand", "expand": "expand",
"example": "example",
"externalLinks": "external links", "externalLinks": "external links",
"faster": "faster", "faster": "faster",
"favorite": "favorite", "favorite": "favorite",
@@ -103,7 +98,6 @@
"minimize": "minimize", "minimize": "minimize",
"modified": "modified", "modified": "modified",
"mbid": "MusicBrainz ID", "mbid": "MusicBrainz ID",
"mood": "mood",
"name": "name", "name": "name",
"no": "no", "no": "no",
"none": "none", "none": "none",
@@ -121,7 +115,6 @@
"quit": "quit", "quit": "quit",
"random": "random", "random": "random",
"rating": "rating", "rating": "rating",
"retry": "retry",
"recordLabel": "record label", "recordLabel": "record label",
"releaseType": "release type", "releaseType": "release type",
"refresh": "refresh", "refresh": "refresh",
@@ -215,8 +208,6 @@
"mpvRequired": "MPV required", "mpvRequired": "MPV required",
"multipleServerSaveQueueError": "the play queue has one or more songs which are not from the current server. this is not supported", "multipleServerSaveQueueError": "the play queue has one or more songs which are not from the current server. this is not supported",
"networkError": "a network error occurred", "networkError": "a network error occurred",
"noNetwork": "server unavailable",
"noNetworkDescription": "couldn't connect to this server",
"notificationDenied": "permissions for notifications were denied. this setting has no effect", "notificationDenied": "permissions for notifications were denied. this setting has no effect",
"openError": "could not open file", "openError": "could not open file",
"playbackError": "an error occurred when trying to play the media", "playbackError": "an error occurred when trying to play the media",
@@ -277,10 +268,10 @@
"explicitStatus": "$t(common.explicitStatus)" "explicitStatus": "$t(common.explicitStatus)"
}, },
"datetime": { "datetime": {
"minuteShort": "m", "minuteShort": "min",
"secondShort": "s", "secondShort": "sec",
"hourShort": "h", "hourShort": "hr",
"dayShort": "d" "dayShort": "day"
}, },
"filterOperator": { "filterOperator": {
"after": "is after", "after": "is after",
@@ -313,9 +304,6 @@
"input_password": "password", "input_password": "password",
"input_preferInstantMix": "prefer instant mix", "input_preferInstantMix": "prefer instant mix",
"input_preferInstantMixDescription": "only use instant mix to get similar songs. useful if you have plugins that modify this behavior", "input_preferInstantMixDescription": "only use instant mix to get similar songs. useful if you have plugins that modify this behavior",
"input_preferRemoteUrl": "prefer public url",
"input_remoteUrl": "public url",
"input_remoteUrlPlaceholder": "optional: public url for external features",
"input_savePassword": "save password", "input_savePassword": "save password",
"input_url": "url", "input_url": "url",
"input_username": "username", "input_username": "username",
@@ -360,11 +348,6 @@
"success": "$t(entity.playlist_one) updated successfully", "success": "$t(entity.playlist_one) updated successfully",
"title": "edit $t(entity.playlist_one)" "title": "edit $t(entity.playlist_one)"
}, },
"lyricsExport": {
"export": "export lyrics",
"input_synced": "export synced lyrics",
"input_offset": "$t(setting.lyricOffset)"
},
"lyricSearch": { "lyricSearch": {
"input_artist": "$t(entity.artist_one)", "input_artist": "$t(entity.artist_one)",
"input_name": "$t(common.name)", "input_name": "$t(common.name)",
@@ -415,8 +398,6 @@
"albumArtistDetail": { "albumArtistDetail": {
"about": "About {{artist}}", "about": "About {{artist}}",
"appearsOn": "appears on", "appearsOn": "appears on",
"groupingTypeAll": "all release types",
"groupingTypePrimary": "primary release types",
"recentReleases": "recent releases", "recentReleases": "recent releases",
"viewDiscography": "view discography", "viewDiscography": "view discography",
"relatedArtists": "related $t(entity.artist_other)", "relatedArtists": "related $t(entity.artist_other)",
@@ -576,7 +557,6 @@
"scrobble": "scrobble", "scrobble": "scrobble",
"audio": "audio", "audio": "audio",
"lyrics": "lyrics", "lyrics": "lyrics",
"lyricsDisplay": "lyrics display",
"transcoding": "transcoding", "transcoding": "transcoding",
"discord": "discord", "discord": "discord",
"logger": "logger", "logger": "logger",
@@ -610,7 +590,6 @@
"addNext": "next", "addNext": "next",
"addLastShuffled": "last (shuffled)", "addLastShuffled": "last (shuffled)",
"addNextShuffled": "next (shuffled)", "addNextShuffled": "next (shuffled)",
"artistRadio": "artist radio",
"holdToShuffle": "hold to shuffle", "holdToShuffle": "hold to shuffle",
"favorite": "favorite", "favorite": "favorite",
"lyrics": "lyrics", "lyrics": "lyrics",
@@ -646,7 +625,6 @@
"skip_forward": "skip forwards", "skip_forward": "skip forwards",
"stop": "stop", "stop": "stop",
"toggleFullscreenPlayer": "toggle fullscreen player", "toggleFullscreenPlayer": "toggle fullscreen player",
"trackRadio": "track radio",
"unfavorite": "unfavorite", "unfavorite": "unfavorite",
"pause": "pause", "pause": "pause",
"viewQueue": "view queue" "viewQueue": "view queue"
@@ -703,8 +681,6 @@
"artistBackgroundBlur_description": "adjusts the amount of blur applied to the artist background image", "artistBackgroundBlur_description": "adjusts the amount of blur applied to the artist background image",
"artistConfiguration": "album artist page configuration", "artistConfiguration": "album artist page configuration",
"artistConfiguration_description": "configure what items are shown, and in what order, on the album artist page", "artistConfiguration_description": "configure what items are shown, and in what order, on the album artist page",
"artistReleaseTypeConfiguration": "artist release type configuration",
"artistReleaseTypeConfiguration_description": "configure what release types are shown, and in what order, on the album artist page",
"audioDevice_description": "select the audio device to use for playback (web player only)", "audioDevice_description": "select the audio device to use for playback (web player only)",
"audioDevice": "audio device", "audioDevice": "audio device",
"audioExclusiveMode_description": "enable exclusive output mode. In this mode, the system is usually locked out, and only mpv will be able to output audio", "audioExclusiveMode_description": "enable exclusive output mode. In this mode, the system is usually locked out, and only mpv will be able to output audio",
@@ -803,11 +779,6 @@
"hotkey_favoritePreviousSong": "favorite $t(common.previousSong)", "hotkey_favoritePreviousSong": "favorite $t(common.previousSong)",
"hotkey_globalSearch": "global search", "hotkey_globalSearch": "global search",
"hotkey_localSearch": "in-page search", "hotkey_localSearch": "in-page search",
"hotkey_listNavigateToPage": "list navigate to item page",
"hotkey_listPlayDefault": "list play",
"hotkey_listPlayLast": "list play last",
"hotkey_listPlayNext": "list play next",
"hotkey_listPlayNow": "list play now",
"hotkey_navigateHome": "navigate to home", "hotkey_navigateHome": "navigate to home",
"hotkey_playbackNext": "next track", "hotkey_playbackNext": "next track",
"hotkey_playbackPause": "pause", "hotkey_playbackPause": "pause",
@@ -864,8 +835,6 @@
"minimumScrobbleSeconds": "minimum scrobble (seconds)", "minimumScrobbleSeconds": "minimum scrobble (seconds)",
"mpvExecutablePath_description": "sets the path to the mpv executable. if left empty, the default path will be used", "mpvExecutablePath_description": "sets the path to the mpv executable. if left empty, the default path will be used",
"mpvExecutablePath": "mpv executable path", "mpvExecutablePath": "mpv executable path",
"mpvExtraParameters": "mpv extra parameters",
"mpvExtraParameters_description": "extra arguments to pass to mpv",
"mpvExtraParameters_help": "one per line", "mpvExtraParameters_help": "one per line",
"musicbrainz_description": "show links to MusicBrainz on artist/album pages, where MusicBrainz ID exists", "musicbrainz_description": "show links to MusicBrainz on artist/album pages, where MusicBrainz ID exists",
"musicbrainz": "show MusicBrainz links", "musicbrainz": "show MusicBrainz links",
@@ -873,10 +842,6 @@
"neteaseTranslation": "Enable NetEase translations", "neteaseTranslation": "Enable NetEase translations",
"notify": "enable song notifications", "notify": "enable song notifications",
"notify_description": "show notifications when changing the current song", "notify_description": "show notifications when changing the current song",
"pathReplace": "file path replacement",
"pathReplace_description": "replace your server's default filepath",
"pathReplace_optionRemovePrefix": "remove prefix",
"pathReplace_optionAddPrefix": "add prefix",
"passwordStore_description": "what password/secret store to use. change this if you are having issues storing passwords", "passwordStore_description": "what password/secret store to use. change this if you are having issues storing passwords",
"passwordStore": "passwords/secret store", "passwordStore": "passwords/secret store",
"playerFilters": "Filter songs from the queue", "playerFilters": "Filter songs from the queue",
@@ -891,15 +856,8 @@
"playButtonBehavior_optionPlay": "$t(player.play)", "playButtonBehavior_optionPlay": "$t(player.play)",
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)", "playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
"playButtonBehavior": "play button behavior", "playButtonBehavior": "play button behavior",
"artistRadioCount_description": "sets the number of songs to fetch for artist radio and track radio", "playerAlbumArtResolution_description": "the resolution for the large player's album art preview. larger makes it look more crisp, but may slow loading down. defaults to 0, meaning auto",
"artistRadioCount": "artist/track radio count", "playerAlbumArtResolution": "player album art resolution",
"imageResolution": "image resolution",
"imageResolution_description": "the resolution for the images used around the app. using a value of 0 will default to the native image resolution",
"imageResolution_optionTable": "table",
"imageResolution_optionItemCard": "item card",
"imageResolution_optionSidebar": "sidebar",
"imageResolution_optionHeader": "header",
"imageResolution_optionFullScreenPlayer": "fullscreen player",
"playerbarOpenDrawer_description": "allows clicking of the playerbar to open the full screen player", "playerbarOpenDrawer_description": "allows clicking of the playerbar to open the full screen player",
"playerbarOpenDrawer": "playerbar fullscreen toggle", "playerbarOpenDrawer": "playerbar fullscreen toggle",
"playerbarSlider": "playerbar slider", "playerbarSlider": "playerbar slider",
@@ -917,12 +875,8 @@
"preferLocalLyrics": "prefer local lyrics", "preferLocalLyrics": "prefer local lyrics",
"showLyricsInSidebar_description": "a panel will be added to the attached play queue that displays the lyrics", "showLyricsInSidebar_description": "a panel will be added to the attached play queue that displays the lyrics",
"showLyricsInSidebar": "show lyrics in player sidebar", "showLyricsInSidebar": "show lyrics in player sidebar",
"showRatings_description": "controls if the star ratings feature shows up in the interface",
"showRatings": "show star ratings",
"showVisualizerInSidebar_description": "a panel will be added to the player sidebar that displays the visualizer", "showVisualizerInSidebar_description": "a panel will be added to the player sidebar that displays the visualizer",
"showVisualizerInSidebar": "show visualizer in player sidebar", "showVisualizerInSidebar": "show visualizer in player sidebar",
"combinedLyricsAndVisualizer_description": "combine lyrics and visualizer into the same panel",
"combinedLyricsAndVisualizer": "combine lyrics and visualizer in player sidebar",
"preservePitch_description": "preserves pitch when modifying playback speed", "preservePitch_description": "preserves pitch when modifying playback speed",
"preservePitch": "preserve pitch", "preservePitch": "preserve pitch",
"audioFadeOnStatusChange": "audio fade on status change", "audioFadeOnStatusChange": "audio fade on status change",
@@ -1120,154 +1074,5 @@
"error_oneFileOnly": "Please only select 1 file", "error_oneFileOnly": "Please only select 1 file",
"error_readingFile": "there has been an issue reading the file: {{errorMessage}}", "error_readingFile": "there has been an issue reading the file: {{errorMessage}}",
"mainText": "drop a file here" "mainText": "drop a file here"
},
"visualizer": {
"visualizerType": "Visualizer Type",
"cyclePresets": "Cycle Presets",
"cycleTime": "Cycle Time (seconds)",
"includeAllPresets": "Include All Presets",
"ignoredPresets": "Ignored Presets",
"selectedPresets": "Selected Presets",
"randomizeNextPreset": "Randomize Next Preset",
"blendTime": "Blend Time",
"presets": "Presets",
"selectPreset": "Select Preset",
"applyPreset": "Apply Preset",
"saveAsPreset": "Save as Preset",
"updatePreset": "Update Preset",
"copyConfiguration": "Copy Configuration",
"pasteConfiguration": "Paste Configuration",
"pasteConfigurationPlaceholder": "Paste JSON configuration here...",
"pasteFromClipboard": "Paste from Clipboard",
"applyConfiguration": "Apply Configuration",
"configCopied": "Configuration copied to clipboard",
"configCopyFailed": "Failed to copy configuration",
"configPasted": "Configuration applied successfully",
"configPasteFailed": "Failed to apply configuration. Please check the format.",
"configPasteReadFailed": "Failed to read from clipboard",
"presetName": "Preset Name",
"presetNamePlaceholder": "Enter preset name",
"general": "General",
"mode": "Mode",
"mode1To8": "Mode 1 - 8",
"mode10": "Mode 10",
"barSpace": "Bar Space",
"lineWidth": "Line Width",
"fillAlpha": "Fill Alpha",
"channelLayout": "Channel Layout",
"maxFPS": "Max FPS",
"opacity": "Opacity",
"customGradients": "Custom Gradients",
"addCustomGradient": "Add Custom Gradient",
"gradientName": "Gradient Name",
"gradientNamePlaceholder": "Gradient Name",
"vertical": "Vertical",
"horizontal": "Horizontal",
"colorStops": "Color Stops",
"addColor": "Add Color",
"position": "Position",
"level": "Level",
"remove": "Remove",
"pasteGradient": "Paste Gradient",
"pasteGradientPlaceholder": "Paste gradient JSON here...",
"custom": "Custom",
"builtIn": "Built-in",
"colors": "Colors",
"colorMode": "Color Mode",
"gradient": "Gradient",
"gradientLeft": "Gradient Left",
"gradientRight": "Gradient Right",
"fft": "FFT",
"fftSize": "FFT Size",
"smoothing": "Smoothing",
"frequencyRangeAndScaling": "Frequency range and scaling",
"minimumFrequency": "Minimum Frequency",
"maximumFrequency": "Maximum Frequency",
"frequencyScale": "Frequency Scale",
"sensitivity": "Sensitivity",
"weightingFilter": "Weighting Filter",
"minimumDecibels": "Minimum Decibels",
"maximumDecibels": "Maximum Decibels",
"linearAmplitude": "Linear Amplitude",
"linearBoost": "Linear Boost",
"peakBehavior": "Peak Behavior",
"showPeaks": "Show Peaks",
"fadePeaks": "Fade Peaks",
"peakLine": "Peak Line",
"gravity": "Gravity",
"peakFadeTime": "Peak Fade Time (ms)",
"peakHoldTime": "Peak Hold Time (ms)",
"radialSpectrum": "Radial Spectrum",
"radial": "Radial",
"radialInvert": "Radial Invert",
"spinSpeed": "Spin Speed",
"radius": "Radius",
"reflexMirror": "Reflex Mirror",
"reflexFit": "Reflex Fit",
"reflexRatio": "Reflex Ratio",
"reflexAlpha": "Reflex Alpha",
"reflexBrightness": "Reflex Brightness",
"mirror": "Mirror",
"miscellaneousSettings": "Miscellaneous Settings",
"alphaBars": "Alpha Bars",
"ansiBands": "ANSI Bands",
"ledBars": "LED Bars",
"trueLeds": "True LEDs",
"lumiBars": "Lumi Bars",
"outlineBars": "Outline Bars",
"roundBars": "Round Bars",
"lowResolution": "Low Resolution",
"splitGradient": "Split Gradient",
"showFPS": "Show FPS",
"showScaleX": "Show Scale X",
"noteLabels": "Note Labels",
"showScaleY": "Show Scale Y",
"options": {
"mode": {
"0": "[0] Discrete Frequencies",
"1": "[1] 1/24th octave / 240 bands",
"2": "[2] 1/12th octave / 120 bands",
"3": "[3] 1/8th octave / 80 bands",
"4": "[4] 1/6th octave / 60 bands",
"5": "[5] 1/4th octave / 40 bands",
"6": "[6] 1/3rd octave / 30 bands",
"7": "[7] Half octave / 20 bands",
"8": "[8] Full octave / 10 bands",
"10": "[10] Line / Area graph"
},
"colorMode": {
"gradient": "Gradient",
"barIndex": "Bar-Index",
"barLevel": "Bar-Level"
},
"gradient": {
"classic": "Classic",
"prism": "Prism",
"rainbow": "Rainbow",
"steelblue": "Steelblue",
"orangered": "Orangered"
},
"channelLayout": {
"single": "Single",
"dualCombined": "Dual-Combined",
"dualHorizontal": "Dual-Horizontal",
"dualVertical": "Dual-Vertical"
},
"frequencyScale": {
"none": "None",
"bark": "Bark Scale",
"linear": "Linear Scale",
"log": "Log Scale",
"mel": "Mel Scale"
},
"weightingFilter": {
"none": "None",
"a": "A",
"b": "B",
"c": "C",
"d": "D",
"z": "Z"
}
}
} }
} }
+10 -213
View File
@@ -39,9 +39,7 @@
"holdToShuffle": "Mantener para mezclar", "holdToShuffle": "Mantener para mezclar",
"lyrics": "Letras", "lyrics": "Letras",
"restoreQueueFromServer": "Restaurar cola del servidor", "restoreQueueFromServer": "Restaurar cola del servidor",
"saveQueueToServer": "Guardar cola en el servidor", "saveQueueToServer": "Guardar cola en el servidor"
"artistRadio": "Radio de artista",
"trackRadio": "Radio de pista"
}, },
"setting": { "setting": {
"crossfadeStyle_description": "selecciona el estilo de crossfade a usar por el reproductor de audio", "crossfadeStyle_description": "selecciona el estilo de crossfade a usar por el reproductor de audio",
@@ -208,6 +206,8 @@
"startMinimized_description": "inicia la aplicación en la bandeja del sistema", "startMinimized_description": "inicia la aplicación en la bandeja del sistema",
"startMinimized": "iniciar minimizado", "startMinimized": "iniciar minimizado",
"passwordStore": "contraseñas/almacenamiento secreto", "passwordStore": "contraseñas/almacenamiento secreto",
"playerAlbumArtResolution_description": "la resolución para la vista previa de la carátula del álbum del reproductor grande. más grande hace que parezca más nítido, pero puede ralentizar la carga. El valor predeterminado es 0, lo que significa automático",
"playerAlbumArtResolution": "resolución de la carátula del álbum del reproductor",
"homeConfiguration": "Configuración de la página de inicio", "homeConfiguration": "Configuración de la página de inicio",
"mpvExtraParameters_help": "Uno por línea", "mpvExtraParameters_help": "Uno por línea",
"externalLinks_description": "Permite mostrar enlaces externos (Last.fm, MusicBrainz) en las páginas del artista/álbum", "externalLinks_description": "Permite mostrar enlaces externos (Last.fm, MusicBrainz) en las páginas del artista/álbum",
@@ -349,33 +349,7 @@
"logLevel_optionInfo": "Información", "logLevel_optionInfo": "Información",
"logLevel_optionWarn": "Advertencia", "logLevel_optionWarn": "Advertencia",
"useThemeAccentColor": "Usar color de acentuación de tema", "useThemeAccentColor": "Usar color de acentuación de tema",
"useThemeAccentColor_description": "Usa el color principal definido en el tema seleccionado en lugar del color de acentuación personalizado", "useThemeAccentColor_description": "Usa el color principal definido en el tema seleccionado en lugar del color de acentuación personalizado"
"artistRadioCount_description": "Establece el número de canciones a buscar para la radio de artista y de pista",
"artistRadioCount": "Recuento de radio de artista/pista",
"imageResolution": "Resolución de imagen",
"imageResolution_description": "La resolución de las imágenes usadas en la aplicación. Usar un valor de 0 lo dejará de forma predeterminada a la resolución nativa de la imagen",
"imageResolution_optionTable": "Tabla",
"imageResolution_optionItemCard": "Tarjeta de elemento",
"imageResolution_optionSidebar": "Barra lateral",
"imageResolution_optionHeader": "Cabecera",
"imageResolution_optionFullScreenPlayer": "Reproductor a pantalla completa",
"showRatings_description": "Controla si la característica de calificación de estrellas aparece en la interfaz",
"showRatings": "Mostrar calificación de estrellas",
"combinedLyricsAndVisualizer_description": "Combina letras y visualizador en el mismo panel",
"combinedLyricsAndVisualizer": "Combinar letras y visualizador en la barra lateral del reproductor",
"artistReleaseTypeConfiguration": "Configuración de tipo de lanzamiento de artista",
"artistReleaseTypeConfiguration_description": "Configura qué tipos de lanzamiento son mostrados, y en qué orden, en la página del artista del álbum",
"mpvExtraParameters": "Parámetros adicionales de MPV",
"mpvExtraParameters_description": "Argumentos adicionales a pasar a MPV",
"hotkey_listPlayDefault": "Reproducir lista",
"hotkey_listPlayLast": "Reproducir lista al final",
"hotkey_listPlayNext": "Reproducir lista a continuación",
"hotkey_listPlayNow": "Reproducir lista ahora",
"hotkey_listNavigateToPage": "Navegar por la lista hasta la página del elemento",
"pathReplace_description": "Reemplaza la ruta de archivo predeterminada de tu servidor",
"pathReplace": "Reemplazo de la ruta de archivo",
"pathReplace_optionRemovePrefix": "Eliminar prefijo",
"pathReplace_optionAddPrefix": "Añadir prefijo"
}, },
"action": { "action": {
"editPlaylist": "editar $t(entity.playlist_one)", "editPlaylist": "editar $t(entity.playlist_one)",
@@ -411,11 +385,7 @@
"moveUp": "Desplazar hacia arriba", "moveUp": "Desplazar hacia arriba",
"moveDown": "Desplazar hacia abajo", "moveDown": "Desplazar hacia abajo",
"createRadioStation": "Crear $t(entity.radioStation_one)", "createRadioStation": "Crear $t(entity.radioStation_one)",
"deleteRadioStation": "Borrar $t(entity.radioStation_one)", "deleteRadioStation": "Borrar $t(entity.radioStation_one)"
"openApplicationDirectory": "Abrir directorio de la aplicación",
"addOrRemoveFromSelection": "Añadir o quitar de la selección",
"selectRangeOfItems": "Seleccionar un intervalo de elementos",
"selectAll": "Seleccionar todo"
}, },
"common": { "common": {
"backward": "hacia atrás", "backward": "hacia atrás",
@@ -532,11 +502,7 @@
"tableColumns": "Columnas de la tabla", "tableColumns": "Columnas de la tabla",
"itemsMore": "{{count}} más", "itemsMore": "{{count}} más",
"noFilters": "Ningún filtro configurado", "noFilters": "Ningún filtro configurado",
"view": "Vista", "view": "Vista"
"countSelected": "{{count}} seleccionados",
"retry": "Reintentar",
"mood": "Estado de ánimo",
"example": "Ejemplo"
}, },
"error": { "error": {
"remotePortWarning": "reiniciar el servidor para aplicar el nuevo puerto", "remotePortWarning": "reiniciar el servidor para aplicar el nuevo puerto",
@@ -564,10 +530,7 @@
"badValue": "Opción inválida \"{{value}}\". Este valor ya no existe", "badValue": "Opción inválida \"{{value}}\". Este valor ya no existe",
"notificationDenied": "Se denegaron los permisos para notificaciones. Esta configuración no tiene efecto", "notificationDenied": "Se denegaron los permisos para notificaciones. Esta configuración no tiene efecto",
"saveQueueFailed": "Error al guardar la cola", "saveQueueFailed": "Error al guardar la cola",
"multipleServerSaveQueueError": "La cola de reproducción tiene una o más canciones que no son del servidor actual. Esto no está soportado", "multipleServerSaveQueueError": "La cola de reproducción tiene una o más canciones que no son del servidor actual. Esto no está soportado"
"settingsSyncError": "Se encontraron discrepancias entre las opciones del renderizador y el proceso principal. Reinicia la aplicación para aplicar los cambios",
"noNetwork": "Servidor no disponible",
"noNetworkDescription": "No se pudo conectar a este servidor"
}, },
"filter": { "filter": {
"mostPlayed": "más reproducido", "mostPlayed": "más reproducido",
@@ -737,8 +700,7 @@
"discord": "Discord", "discord": "Discord",
"sidebar": "Barra lateral", "sidebar": "Barra lateral",
"playerFilters": "Filtros del reproductor", "playerFilters": "Filtros del reproductor",
"logger": "Registrador", "logger": "Registrador"
"lyricsDisplay": "Mostrar letras"
}, },
"albumArtistList": { "albumArtistList": {
"title": "$t(entity.albumArtist_other)" "title": "$t(entity.albumArtist_other)"
@@ -778,9 +740,7 @@
"recentReleases": "Lanzamientos recientes", "recentReleases": "Lanzamientos recientes",
"viewDiscography": "Ver discografía", "viewDiscography": "Ver discografía",
"about": "Sobre {{artist}}", "about": "Sobre {{artist}}",
"appearsOn": "Aparece en", "appearsOn": "Aparece en"
"groupingTypeAll": "Todos los tipos de lanzamiento",
"groupingTypePrimary": "Tipos de lanzamiento principales"
}, },
"itemDetail": { "itemDetail": {
"copiedPath": "Ruta copiada correctamente", "copiedPath": "Ruta copiada correctamente",
@@ -835,10 +795,7 @@
"ignoreCors": "ignorar cors ($t(common.restartRequired))", "ignoreCors": "ignorar cors ($t(common.restartRequired))",
"error_savePassword": "un error ocurrió cuando se intentó guardar la contraseña", "error_savePassword": "un error ocurrió cuando se intentó guardar la contraseña",
"input_preferInstantMix": "Preferir mix instantáneo", "input_preferInstantMix": "Preferir mix instantáneo",
"input_preferInstantMixDescription": "Usa solo el mix instantáneo para obtener canciones similares. Útil si tienes complementos que modifican este comportamiento", "input_preferInstantMixDescription": "Usa solo el mix instantáneo para obtener canciones similares. Útil si tienes complementos que modifican este comportamiento"
"input_remoteUrl": "URL pública",
"input_preferRemoteUrl": "Preferir URL pública",
"input_remoteUrlPlaceholder": "Opcional: URL pública para características externas"
}, },
"addToPlaylist": { "addToPlaylist": {
"success": "añadido $t(entity.trackWithCount, {\"count\": {{message}} }) a $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })", "success": "añadido $t(entity.trackWithCount, {\"count\": {{message}} }) a $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
@@ -909,11 +866,6 @@
"input_homepageUrl": "URL de la página de inicio", "input_homepageUrl": "URL de la página de inicio",
"input_name": "Nombre", "input_name": "Nombre",
"input_streamUrl": "URL de la transmisión" "input_streamUrl": "URL de la transmisión"
},
"lyricsExport": {
"export": "Exportar letras",
"input_synced": "Exportar letras sincronizadas",
"input_offset": "$t(setting.lyricOffset)"
} }
}, },
"table": { "table": {
@@ -1132,160 +1084,5 @@
"notInTheLast": "no está en el último", "notInTheLast": "no está en el último",
"startsWith": "empieza con", "startsWith": "empieza con",
"matchesRegex": "coincide con expresión regular" "matchesRegex": "coincide con expresión regular"
},
"datetime": {
"minuteShort": "m",
"secondShort": "s",
"hourShort": "h",
"dayShort": "d"
},
"visualizer": {
"visualizerType": "Tipo de visualizador",
"copyConfiguration": "Copiar configuración",
"pasteConfiguration": "Pegar configuración",
"pasteConfigurationPlaceholder": "Pegar configuración de JSON aquí...",
"pasteFromClipboard": "Pegar desde el portapapeles",
"applyConfiguration": "Aplicar configuración",
"configCopied": "Configuración copiada al portapapeles",
"configCopyFailed": "Error al copiar la configuración",
"configPasted": "Configuración aplicada con éxito",
"configPasteFailed": "Error al aplicar la configuración. Por favor revisa el formato.",
"configPasteReadFailed": "Error al leer del portapapeles",
"general": "General",
"mode": "Modo",
"mode1To8": "Modo 1 - 8",
"mode10": "Modo 10",
"barSpace": "Espacio de barra",
"lineWidth": "Ancho de línea",
"maxFPS": "FPS máximos",
"opacity": "Opacidad",
"channelLayout": "Diseño del canal",
"fillAlpha": "Rellenar alfa",
"customGradients": "Degradados personalizados",
"addCustomGradient": "Añadir degradado personalizado",
"gradientName": "Nombre del degradado",
"gradientNamePlaceholder": "Nombre del degradado",
"vertical": "Vertical",
"horizontal": "Horizontal",
"addColor": "Añadir color",
"colorStops": "Paradas de color",
"position": "Posición",
"level": "Nivel",
"remove": "Eliminar",
"custom": "Personalizado",
"builtIn": "Integrado",
"colors": "Colores",
"colorMode": "Modo de color",
"gradient": "Degradado",
"gradientLeft": "Izquierda del degradado",
"gradientRight": "Derecha del degradado",
"fft": "FFT",
"fftSize": "Tamaño del FFT",
"smoothing": "Suavizado",
"frequencyRangeAndScaling": "Rango de frecuencia y escala",
"minimumFrequency": "Frecuencia mínima",
"maximumFrequency": "Frecuencia máxima",
"frequencyScale": "Escala de frecuencia",
"sensitivity": "Sensibilidad",
"weightingFilter": "Filtro de ponderación",
"minimumDecibels": "Decibelios mínimos",
"maximumDecibels": "Decibelios máximos",
"linearAmplitude": "Amplitud lineal",
"linearBoost": "Aumento lineal",
"peakBehavior": "Comportamiento del pico",
"showPeaks": "Mostrar picos",
"fadePeaks": "Picos desvanecidos",
"peakLine": "Línea del pico",
"gravity": "Gravedad",
"peakFadeTime": "Tiempo de desvanecimiento del pico (ms)",
"peakHoldTime": "Tiempo de espera del pico (ms)",
"radialSpectrum": "Espectro radial",
"radial": "Radial",
"radialInvert": "Invertir radial",
"spinSpeed": "Velocidad de giro",
"radius": "Radio",
"reflexMirror": "Espejo del reflejo",
"reflexFit": "Ajuste del reflejo",
"reflexRatio": "Proporción del reflejo",
"reflexAlpha": "Alfa del reflejo",
"reflexBrightness": "Brillo del reflejo",
"mirror": "Espejo",
"miscellaneousSettings": "Miscelánea",
"alphaBars": "Barras alfa",
"ansiBands": "Bandas ANSI",
"ledBars": "Barras LED",
"trueLeds": "True LED",
"options": {
"colorMode": {
"gradient": "Degradado",
"barLevel": "Nivel de barra",
"barIndex": "Índice de barra"
},
"gradient": {
"classic": "Clásico",
"prism": "Prisma",
"rainbow": "Arcoíris",
"steelblue": "Azul acero",
"orangered": "Naranja rojizo"
},
"channelLayout": {
"single": "Sencillo",
"dualCombined": "Doble combinado",
"dualHorizontal": "Doble horizontal",
"dualVertical": "Doble vertical"
},
"frequencyScale": {
"linear": "Escala lineal",
"none": "Ninguna",
"log": "Escala de registro",
"bark": "Escala Bark",
"mel": "Escala Mel"
},
"weightingFilter": {
"none": "Ninguno",
"a": "A",
"b": "B",
"c": "C",
"d": "D",
"z": "Z"
},
"mode": {
"0": "[0] Frecuencias discretas",
"1": "[1] 1/24ª octava / 240 bandas",
"2": "[1] 1/12ª octava / 120 bandas",
"3": "[3] 1/8ª octava / 80 bandas",
"4": "[4] 1/6ª octava / 60 bandas",
"5": "[5] 1/4ª octava / 40 bandas",
"6": "[6] 1/3ª octava / 30 bandas",
"7": "[7] Media octava / 20 bandas",
"8": "[8] Octava completa / 10 bandas",
"10": "[10] Línea / Gráfico de área"
}
},
"showFPS": "Mostrar FPS",
"showScaleX": "Mostrar escala X",
"showScaleY": "Mostrar escala Y",
"cyclePresets": "Ajustes preestablecidos del ciclo",
"cycleTime": "Tiempo del ciclo (segundos)",
"includeAllPresets": "Incluir todos los ajustes preestablecidos",
"ignoredPresets": "Ajustes preestablecidos ignorados",
"selectedPresets": "Ajustes preestablecidos seleccionados",
"randomizeNextPreset": "Aleatorizar el siguiente ajuste preestablecido",
"blendTime": "Tiempo de mezcla",
"presets": "Ajustes preestablecidos",
"selectPreset": "Seleccionar ajuste preestablecido",
"applyPreset": "Aplicar ajuste preestablecido",
"saveAsPreset": "Guardar como ajuste preestablecido",
"updatePreset": "Actualizar ajuste preestablecido",
"presetName": "Nombre del ajuste preestablecido",
"presetNamePlaceholder": "Introduce el nombre del ajuste preestablecido",
"pasteGradient": "Pegar degradado",
"pasteGradientPlaceholder": "Pegar JSON del degradado aquí...",
"outlineBars": "Barras de contorno",
"roundBars": "Barras redondeadas",
"lowResolution": "Baja resolución",
"splitGradient": "Dividir degradado",
"noteLabels": "Etiquetas de notas",
"lumiBars": "Barras luminiscentes"
} }
} }
+16 -146
View File
@@ -21,11 +21,7 @@
"createPlaylist": "sortu $t(entity.playlist_one)", "createPlaylist": "sortu $t(entity.playlist_one)",
"deletePlaylist": "ezabatu $t(entity.playlist_one)", "deletePlaylist": "ezabatu $t(entity.playlist_one)",
"addToFavorites": "gehitu $t(entity.favorite_other)-(e)ra", "addToFavorites": "gehitu $t(entity.favorite_other)-(e)ra",
"addToPlaylist": "gehitu $t(entity.playlist_one)-(e)ra", "addToPlaylist": "gehitu $t(entity.playlist_one)-(e)ra"
"createRadioStation": "sortu $t(entity.radioStation_one)",
"deleteRadioStation": "ezabatu $t(entity.radioStation_one)",
"viewMore": "ikusi gehiago",
"shuffle": "nahastu"
}, },
"common": { "common": {
"add": "gehitu", "add": "gehitu",
@@ -128,16 +124,7 @@
"clean": "garbia", "clean": "garbia",
"private": "pribatua", "private": "pribatua",
"public": "publikoa", "public": "publikoa",
"releaseType": "argitalpen mota", "releaseType": "argitalpen mota"
"countSelected": "{{count}} hautatuta",
"view": "ikuspegia",
"externalLinks": "kanpoko estekak",
"faster": "azkarrago",
"noFilters": "ez dago iragazkirik konfiguratuta",
"retry": "saiatu berriro",
"slower": "motelago",
"itemsMore": "{{count}} gehiago",
"sort": "ordenatu"
}, },
"player": { "player": {
"repeat": "errepikatu", "repeat": "errepikatu",
@@ -164,19 +151,13 @@
"queue_remove": "kendu hautatutakoak", "queue_remove": "kendu hautatutakoak",
"repeat_all": "errepikatu dena", "repeat_all": "errepikatu dena",
"repeat_off": "errepikapena desgaituta", "repeat_off": "errepikapena desgaituta",
"shuffle": "erreproduzitu (ausaz)", "shuffle": "erreproduzitu ausaz",
"shuffle_off": "auza desgaituta", "shuffle_off": "auza desgaituta",
"skip_back": "saltatu atzeraka", "skip_back": "saltatu atzeraka",
"skip_forward": "saltatu aurreraka", "skip_forward": "saltatu aurreraka",
"toggleFullscreenPlayer": "txandakatu pantaila osoko erreproduzitzailea", "toggleFullscreenPlayer": "txandakatu pantaila osoko erreproduzitzailea",
"viewQueue": "ikusi ilara", "viewQueue": "ikusi ilara",
"playbackFetchCancel": "honek denbora pixka bat behar du... itxi jakinarazpena bertan behera uzteko", "playbackFetchCancel": "honek denbora pixka bat behar du... itxi jakinarazpena bertan behera uzteko"
"lyrics": "letrak",
"queueType": "ilara mota",
"queueType_default": "lehenetsia",
"queueType_priority": "lehentasuna",
"restoreQueueFromServer": "berrezarri ilara zerbitzaritik",
"saveQueueToServer": "gorde ilara zerbitzarira"
}, },
"table": { "table": {
"config": { "config": {
@@ -190,8 +171,7 @@
"size": "$t(common.size)", "size": "$t(common.size)",
"tableColumns": "taula zutabeak", "tableColumns": "taula zutabeak",
"itemSize": "elementuaren tamaina (px)", "itemSize": "elementuaren tamaina (px)",
"followCurrentSong": "jarraitu uneko abestia", "followCurrentSong": "jarraitu uneko abestia"
"size_default": "lehenetsia"
}, },
"label": { "label": {
"actions": "$t(common.action_other)", "actions": "$t(common.action_other)",
@@ -219,11 +199,7 @@
"playCount": "erreprodukzio kopurua", "playCount": "erreprodukzio kopurua",
"lastPlayed": "azken aldiz entzunda", "lastPlayed": "azken aldiz entzunda",
"discNumber": "disko zenbakia", "discNumber": "disko zenbakia",
"dateAdded": "gehitze data", "dateAdded": "gehitze data"
"albumCount": "$t(entity.album_other)",
"image": "irudia",
"bitDepth": "$t(common.bitDepth)",
"sampleRate": "$t(common.sampleRate)"
} }
}, },
"column": { "column": {
@@ -250,10 +226,7 @@
"releaseDate": "argitalpen data", "releaseDate": "argitalpen data",
"lastPlayed": "azken aldiz entzundakoa", "lastPlayed": "azken aldiz entzundakoa",
"dateAdded": "gehitutako data", "dateAdded": "gehitutako data",
"albumArtist": "albumeko artista", "albumArtist": "albumeko artista"
"owner": "jabea",
"bitDepth": "$t(common.bitDepth)",
"sampleRate": "$t(common.sampleRate)"
} }
}, },
"entity": { "entity": {
@@ -317,10 +290,7 @@
"badAlbum": "Orrialde hau ikusten ari zara abesti hau album batekoa ez delako. Ziurrenik arazo hau ikusten ari zara zure musika karpetaren goiko mailan abesti bat baduzu. Jellyfinek abestiak karpeta batean badaude taldekatzen ditu bakarrik", "badAlbum": "Orrialde hau ikusten ari zara abesti hau album batekoa ez delako. Ziurrenik arazo hau ikusten ari zara zure musika karpetaren goiko mailan abesti bat baduzu. Jellyfinek abestiak karpeta batean badaude taldekatzen ditu bakarrik",
"loginRateError": "Saioa hasteko saiakera gehiegi egin dira, saiatu berriro segundo batzuk barru", "loginRateError": "Saioa hasteko saiakera gehiegi egin dira, saiatu berriro segundo batzuk barru",
"notificationDenied": "jakinarazpenetarako baimenak ukatu dira. Ezarpen honek ez du eraginik", "notificationDenied": "jakinarazpenetarako baimenak ukatu dira. Ezarpen honek ez du eraginik",
"systemFontError": "errore bat gertatu da sistemaren letra-tipoak lortzen saiatzean", "systemFontError": "errore bat gertatu da sistemaren letra-tipoak lortzen saiatzean"
"noNetwork": "zerbitzaria ez dago erabilgarri",
"noNetworkDescription": "ezin izan da zerbitzari honetara konektatu",
"saveQueueFailed": "huts egin du ilara gordetzean"
}, },
"filter": { "filter": {
"disc": "diskoa", "disc": "diskoa",
@@ -549,6 +519,7 @@
"playbackStyle_description": "aukeratu audio erreproduzitzailearentzat erabiliko den erreprodukzio estiloa", "playbackStyle_description": "aukeratu audio erreproduzitzailearentzat erabiliko den erreprodukzio estiloa",
"playButtonBehavior": "erreprodukzio botoiaren portaera", "playButtonBehavior": "erreprodukzio botoiaren portaera",
"playButtonBehavior_description": "ezartzen du erreprodukzio botoiaren portaera lehenetsia abestiak ilaran gehitzean", "playButtonBehavior_description": "ezartzen du erreprodukzio botoiaren portaera lehenetsia abestiak ilaran gehitzean",
"playerAlbumArtResolution": "erreproduzitzailearen albumaren arte-azalaren erresoluzioa",
"gaplessAudio": "hutsune gabeko audioa", "gaplessAudio": "hutsune gabeko audioa",
"gaplessAudio_description": "ezartzen du hutsunik gabeko audio ezarpena mpv-rako", "gaplessAudio_description": "ezartzen du hutsunik gabeko audio ezarpena mpv-rako",
"passwordStore": "pasahitzak/biltegi sekretua", "passwordStore": "pasahitzak/biltegi sekretua",
@@ -566,17 +537,7 @@
"exportImportSettings_control_importText": "inportatu ezarpenak", "exportImportSettings_control_importText": "inportatu ezarpenak",
"exportImportSettings_control_title": "inportatu / esportatu ezarpenak", "exportImportSettings_control_title": "inportatu / esportatu ezarpenak",
"exportImportSettings_importBtn": "inportatu ezarpenak", "exportImportSettings_importBtn": "inportatu ezarpenak",
"exportImportSettings_importModalTitle": "inportatu feishin ezarpenak", "exportImportSettings_importModalTitle": "inportatu feishin ezarpenak"
"autoDJ_itemCount": "elementu kopurua",
"language": "hizkuntza",
"queryBuilderCustomFields_inputTag": "etiketa",
"logLevel_optionError": "errore bat",
"logLevel_optionInfo": "informazioa",
"imageResolution_optionTable": "taula",
"imageResolution_optionSidebar": "alboko barra",
"replayGainClipping": "{{ReplayGain}} mozketa",
"replayGainFallback": "{{ReplayGain}} ordezko aukera",
"trayEnabled": "erakutsi erretilua"
}, },
"form": { "form": {
"addServer": { "addServer": {
@@ -631,8 +592,7 @@
"editPlaylist": { "editPlaylist": {
"success": "$t(entity.playlist_one) behar bezala eguneratu da", "success": "$t(entity.playlist_one) behar bezala eguneratu da",
"title": "$t(entity.playlist_one) editatu", "title": "$t(entity.playlist_one) editatu",
"publicJellyfinNote": "Arrazoiren batengatik, Jellyfin ez du erakusten erreprodukzio-zerrendak publikoak diren edo ez. Hau publiko izaten jarraitzea nahi baduzu, hautatu sarrera hau", "publicJellyfinNote": "Arrazoiren batengatik, Jellyfin ez du erakusten erreprodukzio-zerrendak publikoak diren edo ez. Hau publiko izaten jarraitzea nahi baduzu, hautatu sarrera hau"
"editNote": "ez da gomendatzen eskuzko edizioak egitea erreprodukzio-zerrenda handietarako. ziur zaude onartzen duzula lehendik dagoen erreprodukzio-zerrenda gainidazteagatik datuak galtzeko arriskua?"
}, },
"queryEditor": { "queryEditor": {
"title": "kontsulta editorea", "title": "kontsulta editorea",
@@ -647,21 +607,6 @@
"title": "modu pribatua", "title": "modu pribatua",
"enabled": "modu pribatua gaituta, erreprodukzio egoera kanpoko integrazioetatik ezkutatuta dago orain", "enabled": "modu pribatua gaituta, erreprodukzio egoera kanpoko integrazioetatik ezkutatuta dago orain",
"disabled": "modu pribatua desgaituta, erreprodukzio egoera ikusgai dago orain gaitutako kanpoko integrazioentzat" "disabled": "modu pribatua desgaituta, erreprodukzio egoera ikusgai dago orain gaitutako kanpoko integrazioentzat"
},
"largeFetchConfirmation": {
"title": "gehitu elementuak ilaran"
},
"createRadioStation": {
"input_homepageUrl": "hasierako orriaren URLa",
"input_name": "izena"
},
"lyricsExport": {
"export": "esportatu letrak",
"input_synced": "esportatu sinkronizatutako letrak",
"input_offset": "$t(setting.lyricOffset)"
},
"shuffleAll": {
"input_genre": "$t(entity.genre_one)"
} }
}, },
"page": { "page": {
@@ -724,8 +669,7 @@
"shareItem": "partekatu elementua", "shareItem": "partekatu elementua",
"goToAlbum": "joan $t(entity.album_one)-(e)ra", "goToAlbum": "joan $t(entity.album_one)-(e)ra",
"goToAlbumArtist": "joan albumera", "goToAlbumArtist": "joan albumera",
"showDetails": "informazioa lortu", "showDetails": "informazioa lortu"
"moveItems": "$t(action.moveItems)"
}, },
"fullscreenPlayer": { "fullscreenPlayer": {
"config": { "config": {
@@ -769,8 +713,7 @@
"newlyAdded": "azken aldian gehitutako argitalpenak", "newlyAdded": "azken aldian gehitutako argitalpenak",
"recentlyPlayed": "azken aldian entzundakoak", "recentlyPlayed": "azken aldian entzundakoak",
"recentlyReleased": "azken aldian argitaratutak", "recentlyReleased": "azken aldian argitaratutak",
"explore": "arakatu zure liburutegitik", "explore": "arakatu zure liburutegitik"
"genres": "$t(entity.genre_other)"
}, },
"playlistList": { "playlistList": {
"title": "$t(entity.playlist_other)" "title": "$t(entity.playlist_other)"
@@ -780,18 +723,7 @@
"generalTab": "orokorra", "generalTab": "orokorra",
"playbackTab": "erreprodukzioa", "playbackTab": "erreprodukzioa",
"windowTab": "leihoa", "windowTab": "leihoa",
"hotkeysTab": "laster-teklak", "hotkeysTab": "laster-teklak"
"cache": "katxea",
"application": "aplikazioa",
"theme": "gaia",
"sidebar": "alboko barra",
"exportImport": "inportatu/esportatu",
"scrobble": "scrobble",
"audio": "audioa",
"lyrics": "letrak",
"discord": "discord",
"playerFilters": "erreproduzitzailearen iragazkiak",
"updates": "eguneraketa"
}, },
"sidebar": { "sidebar": {
"albumArtists": "$t(entity.albumArtist_other)", "albumArtists": "$t(entity.albumArtist_other)",
@@ -806,9 +738,7 @@
"tracks": "$t(entity.track_other)", "tracks": "$t(entity.track_other)",
"myLibrary": "nire liburutegia", "myLibrary": "nire liburutegia",
"nowPlaying": "orain erreproduzitzen", "nowPlaying": "orain erreproduzitzen",
"shared": "partekatutako $t(entity.playlist_other)", "shared": "partekatutako $t(entity.playlist_other)"
"favorites": "$t(entity.favorite_other)",
"radio": "$t(entity.radioStation_other)"
}, },
"trackList": { "trackList": {
"title": "$t(entity.track_other)", "title": "$t(entity.track_other)",
@@ -833,19 +763,12 @@
}, },
"playlist": { "playlist": {
"reorder": "berrantolaketa IDaren arabera ordenatzean bakarrik gaituta dago" "reorder": "berrantolaketa IDaren arabera ordenatzean bakarrik gaituta dago"
},
"folderList": {
"title": "$t(entity.folder_other)"
},
"favorites": {
"title": "$t(entity.favorite_other)"
} }
}, },
"releaseType": { "releaseType": {
"primary": { "primary": {
"album": "$t(entity.album_one)", "album": "$t(entity.album_one)",
"other": "bestelakoa", "other": "bestelakoa"
"ep": "ep"
}, },
"secondary": { "secondary": {
"compilation": "konpilazioa", "compilation": "konpilazioa",
@@ -853,58 +776,5 @@
"interview": "elkarrizketa", "interview": "elkarrizketa",
"remix": "nahasketa" "remix": "nahasketa"
} }
},
"datetime": {
"minuteShort": "m",
"secondShort": "s",
"hourShort": "h",
"dayShort": "d"
},
"queryBuilder": {
"customTags": "etiketa pertsonalizatutak"
},
"filterOperator": {
"is": "da"
},
"visualizer": {
"general": "Orokorra",
"mode": "Modua",
"vertical": "Bertikala",
"horizontal": "Horizontala",
"position": "Posizioa",
"level": "Maila",
"remove": "Kendu",
"custom": "Pertsonalizatua",
"builtIn": "Barneratua",
"colors": "Koloreak",
"gradient": "Gradientea",
"fft": "FFT",
"sensitivity": "Sentikortasuna",
"smoothing": "Leuntzea",
"gravity": "Grabitatea",
"radial": "Erradiala",
"radius": "Erradioa",
"mirror": "Ispilua",
"options": {
"colorMode": {
"gradient": "Gradientea",
"barIndex": "Barra-indizea",
"barLevel": "Barra-maila"
},
"gradient": {
"classic": "Klasikoa",
"prism": "Prisma",
"rainbow": "Ostadarra"
},
"weightingFilter": {
"none": "Bat ere ez",
"a": "A",
"b": "B",
"c": "C",
"d": "D",
"z": "Z"
}
},
"opacity": "Opakotasuna"
} }
} }
+2
View File
@@ -474,6 +474,7 @@
"replayGainClipping": "{{ReplayGain}} leikkaus", "replayGainClipping": "{{ReplayGain}} leikkaus",
"replayGainClipping_description": "Estää {{ReplayGain}}n aiheuttaman leikkauksen laskemalla vahvistusta automaatisesti", "replayGainClipping_description": "Estää {{ReplayGain}}n aiheuttaman leikkauksen laskemalla vahvistusta automaatisesti",
"replayGainFallback": "{{ReplayGain}} palautus", "replayGainFallback": "{{ReplayGain}} palautus",
"playerAlbumArtResolution_description": "suurien kansikuvien resoluutio soittimen esikatselussa. suurempi tekee niistä terävempiä, mutta voi hidastaa latausta. oletuksena on 0, joka tarkoittaa automaattista",
"replayGainMode_optionAlbum": "$t(entity.album_one)", "replayGainMode_optionAlbum": "$t(entity.album_one)",
"replayGainPreamp": "{{ReplayGain}} esivahvistus (dB)", "replayGainPreamp": "{{ReplayGain}} esivahvistus (dB)",
"scrobble_description": "skrobblaa toistot mediapalvelimellesi", "scrobble_description": "skrobblaa toistot mediapalvelimellesi",
@@ -489,6 +490,7 @@
"sidebarConfiguration": "sivupalkin asetukset", "sidebarConfiguration": "sivupalkin asetukset",
"sidebarConfiguration_description": "valitse kohteet ja niiden järjestys sivupalkissa", "sidebarConfiguration_description": "valitse kohteet ja niiden järjestys sivupalkissa",
"volumeWidth_description": "äänenvoimakkuuden säätimen leveys", "volumeWidth_description": "äänenvoimakkuuden säätimen leveys",
"playerAlbumArtResolution": "soittimen kansikuvien resoluutio",
"playerbarOpenDrawer": "toistipalkin kokoruudun kytkin", "playerbarOpenDrawer": "toistipalkin kokoruudun kytkin",
"playerbarOpenDrawer_description": "sallii toistopalkin klikkaamisen avaamaan kokonäytön soittimen", "playerbarOpenDrawer_description": "sallii toistopalkin klikkaamisen avaamaan kokonäytön soittimen",
"replayGainFallback_description": "asetettava vahvistus desibelinä (dB), jos tiedostolla ei ole {{ReplayGain}} tageja", "replayGainFallback_description": "asetettava vahvistus desibelinä (dB), jos tiedostolla ei ole {{ReplayGain}} tageja",
+11 -10
View File
@@ -14,7 +14,7 @@
"shuffle": "lecture (mélangé)", "shuffle": "lecture (mélangé)",
"playbackFetchNoResults": "aucun titre trouvé", "playbackFetchNoResults": "aucun titre trouvé",
"playbackFetchInProgress": "chargement des titres…", "playbackFetchInProgress": "chargement des titres…",
"addNext": "prochain", "addNext": "ajouter ensuite",
"playbackSpeed": "vitesse de lecture", "playbackSpeed": "vitesse de lecture",
"playbackFetchCancel": "cela prend du temps… fermez la notification pour annuler", "playbackFetchCancel": "cela prend du temps… fermez la notification pour annuler",
"play": "lecture", "play": "lecture",
@@ -24,15 +24,15 @@
"queue_moveToTop": "déplacer la sélection vers le bas", "queue_moveToTop": "déplacer la sélection vers le bas",
"queue_moveToBottom": "déplacer la sélection vers le haut", "queue_moveToBottom": "déplacer la sélection vers le haut",
"shuffle_off": "aléatoire désactivée", "shuffle_off": "aléatoire désactivée",
"addLast": "dernier", "addLast": "ajouter en dernier",
"mute": "muet", "mute": "muet",
"skip_forward": "avancer", "skip_forward": "avancer",
"pause": "pause", "pause": "pause",
"unfavorite": "retirer des favoris", "unfavorite": "retirer des favoris",
"playSimilarSongs": "jouer des titres similaires", "playSimilarSongs": "jouer des titres similaires",
"viewQueue": "voir la file d'attente", "viewQueue": "voir la file d'attente",
"addLastShuffled": "dernier (mélangé)", "addLastShuffled": "ajouter en dernier (mélangé)",
"addNextShuffled": "prochain (mélangé)", "addNextShuffled": "ajouter ensuite (mélangé)",
"queueType": "type de file d'attente", "queueType": "type de file d'attente",
"queueType_default": "défaut", "queueType_default": "défaut",
"queueType_priority": "priorité", "queueType_priority": "priorité",
@@ -223,8 +223,7 @@
"badValue": "option {{value}} invalide. Cette valeur n'existe plus", "badValue": "option {{value}} invalide. Cette valeur n'existe plus",
"notificationDenied": "les autorisations pour les notifications ont été refusées. ce paramètre n'a aucun effet", "notificationDenied": "les autorisations pour les notifications ont été refusées. ce paramètre n'a aucun effet",
"multipleServerSaveQueueError": "la file d'attente de lecture contient un ou plusieurs morceaux qui ne proviennent pas du serveur actuel. Ceci n'est pas prise en charge", "multipleServerSaveQueueError": "la file d'attente de lecture contient un ou plusieurs morceaux qui ne proviennent pas du serveur actuel. Ceci n'est pas prise en charge",
"saveQueueFailed": "échec de l'enregistrement de la file d'attente", "saveQueueFailed": "échec de l'enregistrement de la file d'attente"
"settingsSyncError": "des incohérences ont été détectées entre les paramètres du moteur de rendu et ceux du processus principal. redémarrez l'application pour appliquer les modifications"
}, },
"filter": { "filter": {
"mostPlayed": "plus joués", "mostPlayed": "plus joués",
@@ -634,7 +633,9 @@
"imageAspectRatio_description": "si cette option est activée, les pochettes d'album seront affichées en utilisant leur rapport hauteur/largeur natif. pour les pochettes qui n'ont pas un rapport 1:1 (carré), l'espace restant sera vide", "imageAspectRatio_description": "si cette option est activée, les pochettes d'album seront affichées en utilisant leur rapport hauteur/largeur natif. pour les pochettes qui n'ont pas un rapport 1:1 (carré), l'espace restant sera vide",
"mpvExtraParameters_help": "un par ligne", "mpvExtraParameters_help": "un par ligne",
"passwordStore_description": "quel mot de passe utiliser. changez cela si vous rencontrez des problèmes pour stocker les mots de passe", "passwordStore_description": "quel mot de passe utiliser. changez cela si vous rencontrez des problèmes pour stocker les mots de passe",
"playerAlbumArtResolution": "résolution de la pochette d'album du lecteur",
"passwordStore": "mots de passe", "passwordStore": "mots de passe",
"playerAlbumArtResolution_description": "résolution pour l'aperçu de la pochette d'album agrandie du lecteur. plus grand le rend plus net, mais peut ralentir le chargement. la valeur par défaut est 0 (automatique)",
"homeConfiguration_description": "configurer quels éléments sont affichés sur la page d'accueil, et dans quel ordre", "homeConfiguration_description": "configurer quels éléments sont affichés sur la page d'accueil, et dans quel ordre",
"startMinimized": "démarrer l'application en mode réduit", "startMinimized": "démarrer l'application en mode réduit",
"transcode_description": "permet le transcodage vers différents formats", "transcode_description": "permet le transcodage vers différents formats",
@@ -924,11 +925,11 @@
"song_many": "titres", "song_many": "titres",
"song_other": "titres", "song_other": "titres",
"radioStation_one": "station radio", "radioStation_one": "station radio",
"radioStation_many": "stations radio", "radioStation_many": "stations radios",
"radioStation_other": "stations radio", "radioStation_other": "",
"radioStationWithCount_one": "{{count}} station radio", "radioStationWithCount_one": "{{count}} station radio",
"radioStationWithCount_many": "{{count}} stations radio", "radioStationWithCount_many": "{{count}} stations radios",
"radioStationWithCount_other": "{{count}} stations radio" "radioStationWithCount_other": ""
}, },
"table": { "table": {
"config": { "config": {
+13 -50
View File
@@ -31,13 +31,7 @@
"moveUp": "ugrás fel", "moveUp": "ugrás fel",
"moveDown": "ugrás le", "moveDown": "ugrás le",
"holdToMoveToTop": "hosszan nyomva felülre mozgat", "holdToMoveToTop": "hosszan nyomva felülre mozgat",
"holdToMoveToBottom": "hosszan nyomva lejjebb mozgat", "holdToMoveToBottom": "hosszan nyomva lejjebb mozgat"
"selectAll": "összes kijelölése",
"deleteRadioStation": "$t(entity.radioStation_one) törlése",
"createRadioStation": "$t(entity.radioStation_one) létrehozása",
"openApplicationDirectory": "app könyvtár megnyitása",
"addOrRemoveFromSelection": "hozzáadás vagy eltávolítás a kiválasztásból",
"selectRangeOfItems": "válaszd ki a tartományt"
}, },
"common": { "common": {
"collapse": "összecsukás", "collapse": "összecsukás",
@@ -151,9 +145,7 @@
"tableColumns": "táblázat oszlopok", "tableColumns": "táblázat oszlopok",
"itemsMore": "{{count}} még több", "itemsMore": "{{count}} még több",
"view": "nézet", "view": "nézet",
"noFilters": "nincs konfigurált szűrő", "noFilters": "nincs konfigurált szűrő"
"countSelected": "{{count}} kiválasztott",
"retry": "újra"
}, },
"entity": { "entity": {
"albumArtist_one": "Zenész", "albumArtist_one": "Zenész",
@@ -192,9 +184,7 @@
"trackWithCount_one": "{{count}} sáv", "trackWithCount_one": "{{count}} sáv",
"trackWithCount_other": "{{count}} sávok", "trackWithCount_other": "{{count}} sávok",
"radioStation_one": "rádió állomás", "radioStation_one": "rádió állomás",
"radioStation_other": "rádió állomások", "radioStation_other": "rádió állomások"
"radioStationWithCount_one": "{{count}} rádióállomás",
"radioStationWithCount_other": "{{count}} rádióállomások"
}, },
"error": { "error": {
"apiRouteError": "a kérést nem sikerült célba juttatni", "apiRouteError": "a kérést nem sikerült célba juttatni",
@@ -220,12 +210,7 @@
"serverRequired": "szerver szükséges", "serverRequired": "szerver szükséges",
"serverNotSelectedError": "nincs szerver kiválasztva", "serverNotSelectedError": "nincs szerver kiválasztva",
"notificationDenied": "Az értesítések engedélyezése megtagadva. Ez a beállítás hatástalan", "notificationDenied": "Az értesítések engedélyezése megtagadva. Ez a beállítás hatástalan",
"badValue": "érvénytelen opció \"{{value}}\". ez az érték már nem létezik", "badValue": "érvénytelen opció \"{{value}}\". ez az érték már nem létezik"
"noNetwork": "Szerver nem elérhető",
"noNetworkDescription": "Nem tudok csatlakozni a szerverhez",
"saveQueueFailed": "műsorlista mentése sikertelen",
"settingsSyncError": "Eltéréseket találtam a leképző és a fő folyamat beállításai között. Indítsd újra az alkalmazást",
"multipleServerSaveQueueError": "a műsorlistában egy vagy több olyan dal található, amely nem az aktuális szerverről származik. Ez nem támogatott"
}, },
"filter": { "filter": {
"albumCount": "$t(entity.album_other) darab", "albumCount": "$t(entity.album_other) darab",
@@ -360,16 +345,6 @@
"input_played": "csak szűrt zenék", "input_played": "csak szűrt zenék",
"input_played_optionUnplayed": "Csak a még nem lejátszottak", "input_played_optionUnplayed": "Csak a még nem lejátszottak",
"input_played_optionPlayed": "Csak a játszottak számok" "input_played_optionPlayed": "Csak a játszottak számok"
},
"createRadioStation": {
"success": "rádió állomás sikeresen létrehozva",
"title": "rádió állomás létrehozása",
"input_homepageUrl": "oldal url",
"input_name": "név",
"input_streamUrl": "stream url"
},
"saveQueue": {
"success": "mentett lejátszási műsorlista a szerverre"
} }
}, },
"dragDropZone": { "dragDropZone": {
@@ -550,8 +525,7 @@
"settings": "$t(common.setting_other)", "settings": "$t(common.setting_other)",
"shared": "megosztott $t(entity.playlist_other)", "shared": "megosztott $t(entity.playlist_other)",
"tracks": "$t(entity.track_other)", "tracks": "$t(entity.track_other)",
"favorites": "$t(entity.favorite_other)", "favorites": "$t(entity.favorite_other)"
"radio": "$t(entity.radioStation_other)"
}, },
"trackList": { "trackList": {
"artistTracks": "dalok tőle {{artist}}", "artistTracks": "dalok tőle {{artist}}",
@@ -563,14 +537,11 @@
}, },
"folderList": { "folderList": {
"title": "$t(entity.folder_other)" "title": "$t(entity.folder_other)"
},
"radioList": {
"title": "rádió állomások"
} }
}, },
"player": { "player": {
"addLast": "utolsónak", "addLast": "utoljára hozzáadva",
"addNext": "következő", "addNext": "következő hozzáadása",
"favorite": "kedvenc", "favorite": "kedvenc",
"mute": "némítás", "mute": "némítás",
"muted": "némítva", "muted": "némítva",
@@ -600,15 +571,13 @@
"pause": "szünet", "pause": "szünet",
"viewQueue": "műsorlista megtekintése", "viewQueue": "műsorlista megtekintése",
"shuffle_off": "kevert lejátszás ki", "shuffle_off": "kevert lejátszás ki",
"addLastShuffled": "végére (keverve)", "addLastShuffled": "Hozzáadás a végére (keverve)",
"addNextShuffled": "következő (keverve)", "addNextShuffled": "Hozzáadás következőnek (keverve)",
"queueType": "lekérdezés típus", "queueType": "lekérdezés típus",
"queueType_default": "alapértelmezett", "queueType_default": "alapértelmezett",
"queueType_priority": "prioritás", "queueType_priority": "prioritás",
"holdToShuffle": "tartsd lenyomva a keveréshez", "holdToShuffle": "tartsd lenyomva a keveréshez",
"lyrics": "dalszöveg", "lyrics": "dalszöveg"
"saveQueueToServer": "műsorlista mentése a szerverre",
"restoreQueueFromServer": "műsorlista visszaállítása a szerverről"
}, },
"releaseType": { "releaseType": {
"primary": { "primary": {
@@ -797,6 +766,7 @@
"playButtonBehavior_optionPlay": "$t(player.play)", "playButtonBehavior_optionPlay": "$t(player.play)",
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)", "playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
"playButtonBehavior": "lejátszás gomb viselkedése", "playButtonBehavior": "lejátszás gomb viselkedése",
"playerAlbumArtResolution_description": "A nagy lejátszó albumborító-előnézetének felbontása. A nagyobb érték élesebb képet ad, de lassíthatja a betöltést. Alapértelmezés: 0, ami az automatikus módot jelenti",
"minimumScrobblePercentage_description": "a szám lejátszásának minimális százaléka, amelynek el kell hangzania, mielőtt Scrobble-nak számít", "minimumScrobblePercentage_description": "a szám lejátszásának minimális százaléka, amelynek el kell hangzania, mielőtt Scrobble-nak számít",
"minimumScrobblePercentage": "Minimális Scrobble arány (százalék)", "minimumScrobblePercentage": "Minimális Scrobble arány (százalék)",
"minimumScrobbleSeconds": "Minimum Scrobble arány (mp)", "minimumScrobbleSeconds": "Minimum Scrobble arány (mp)",
@@ -810,6 +780,7 @@
"notify": "bekapcsolja a dal értesítéseket", "notify": "bekapcsolja a dal értesítéseket",
"notify_description": "értesítések megjelenítése az aktuális dal megváltoztatásakor", "notify_description": "értesítések megjelenítése az aktuális dal megváltoztatásakor",
"playbackStyle_description": "válaszd ki az lejátszóhoz használni kívánt lejátszási stílust", "playbackStyle_description": "válaszd ki az lejátszóhoz használni kívánt lejátszási stílust",
"playerAlbumArtResolution": "lejátszó albumborító felbontás",
"playerbarOpenDrawer_description": "lehetővé teszi a lejátszósávra kattintással a teljes képernyős lejátszó megnyitását", "playerbarOpenDrawer_description": "lehetővé teszi a lejátszósávra kattintással a teljes képernyős lejátszó megnyitását",
"playerbarOpenDrawer": "lejátszósáv teljes képernyőre váltás", "playerbarOpenDrawer": "lejátszósáv teljes képernyőre váltás",
"preferLocalLyrics_description": "ha elérhető, akkor a távoli dalszövegek helyett a helyi dalszövegeket részesítse előnyben", "preferLocalLyrics_description": "ha elérhető, akkor a távoli dalszövegek helyett a helyi dalszövegeket részesítse előnyben",
@@ -937,9 +908,7 @@
"playerFilters_description": "a következő kritériumok alapján kihagyja a dalokat a műsorlistából", "playerFilters_description": "a következő kritériumok alapján kihagyja a dalokat a műsorlistából",
"playerbarSlider_description": "a hullámforma nem ajánlott lassú vagy korlátozott internetkapcsolat esetén", "playerbarSlider_description": "a hullámforma nem ajánlott lassú vagy korlátozott internetkapcsolat esetén",
"audioFadeOnStatusChange": "audio behúzás állapotváltozáskor", "audioFadeOnStatusChange": "audio behúzás állapotváltozáskor",
"audioFadeOnStatusChange_description": "lehetővé teszi a lehúzást és a behúzást, amikor a lejátszás/szünet állapot megváltozik", "audioFadeOnStatusChange_description": "lehetővé teszi a lehúzást és a behúzást, amikor a lejátszás/szünet állapot megváltozik"
"useThemeAccentColor": "használd a téma kiemelő színét",
"useThemeAccentColor_description": "a kiválasztott témában meghatározott alapszínt használja az egyéni kiemelő szín helyett"
}, },
"table": { "table": {
"config": { "config": {
@@ -1069,11 +1038,5 @@
"matchesRegex": "illeszkedik a regexre", "matchesRegex": "illeszkedik a regexre",
"is": "van", "is": "van",
"isNot": "nincs" "isNot": "nincs"
},
"datetime": {
"minuteShort": "perc",
"secondShort": "mp",
"hourShort": "óra",
"dayShort": "nap"
} }
} }
+2
View File
@@ -579,6 +579,8 @@
"playButtonBehavior_optionAddLast": "$t(player.addLast)", "playButtonBehavior_optionAddLast": "$t(player.addLast)",
"playButtonBehavior_optionAddNext": "$t(player.addNext)", "playButtonBehavior_optionAddNext": "$t(player.addNext)",
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)", "playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
"playerAlbumArtResolution": "resolusi sampul album pemutar",
"playerAlbumArtResolution_description": "resolusi untuk pratinjau sampul album pemutar besar. semakin besar akan membuatnya lebih tajam, tetapi dapat memperlambat pemuatan. Nilai default adalah 0, yang berarti otomatis",
"playerbarOpenDrawer": "Buka pemutar ke layar penuh", "playerbarOpenDrawer": "Buka pemutar ke layar penuh",
"playerbarOpenDrawer_description": "Izinkan mengklik bilah pemutar untuk membuka pemutar di layar penuh", "playerbarOpenDrawer_description": "Izinkan mengklik bilah pemutar untuk membuka pemutar di layar penuh",
"remotePassword": "kata sandi kontrol jarak jauh server", "remotePassword": "kata sandi kontrol jarak jauh server",
+2
View File
@@ -355,6 +355,8 @@
"passwordStore": "Archivio di password/segreti", "passwordStore": "Archivio di password/segreti",
"passwordStore_description": "specifica quale archivio di password e segreti utilizzare. modificalo in caso di problemi nel salvataggio delle credenziali", "passwordStore_description": "specifica quale archivio di password e segreti utilizzare. modificalo in caso di problemi nel salvataggio delle credenziali",
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)", "playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
"playerAlbumArtResolution": "risoluzione della copertina nel lettore",
"playerAlbumArtResolution_description": "la risoluzione dellanteprima della copertina nel lettore in formato grande. valori più alti la rendono più nitida, ma possono rallentare il caricamento. Il valore predefinito è 0, che indica la modalità automatica",
"sidePlayQueueStyle_optionAttached": "fissata", "sidePlayQueueStyle_optionAttached": "fissata",
"sidePlayQueueStyle_optionDetached": "sganciata", "sidePlayQueueStyle_optionDetached": "sganciata",
"startMinimized": "avvia minimizzato", "startMinimized": "avvia minimizzato",
+22 -151
View File
@@ -11,10 +11,10 @@
"skip_back": "前へスキップ", "skip_back": "前へスキップ",
"favorite": "お気に入り", "favorite": "お気に入り",
"next": "次へ", "next": "次へ",
"shuffle": "再生 (シャッフル)", "shuffle": "シャッフル",
"playbackFetchNoResults": "曲が見つかりません", "playbackFetchNoResults": "曲が見つかりません",
"playbackFetchInProgress": "曲を読み込み中…", "playbackFetchInProgress": "曲を読み込み中…",
"addNext": "次", "addNext": "次へ追加",
"playbackSpeed": "再生速度", "playbackSpeed": "再生速度",
"playbackFetchCancel": "処理に時間がかかります… 通知を閉じるとキャンセルします", "playbackFetchCancel": "処理に時間がかかります… 通知を閉じるとキャンセルします",
"play": "再生", "play": "再生",
@@ -25,18 +25,12 @@
"queue_moveToTop": "選択項目を一番下に移動", "queue_moveToTop": "選択項目を一番下に移動",
"queue_moveToBottom": "選択項目を先頭に移動", "queue_moveToBottom": "選択項目を先頭に移動",
"shuffle_off": "シャッフル無効", "shuffle_off": "シャッフル無効",
"addLast": "最後", "addLast": "最後へ追加",
"mute": "ミュート", "mute": "ミュート",
"skip_forward": "次へスキップ", "skip_forward": "次へスキップ",
"pause": "一時停止", "pause": "一時停止",
"playSimilarSongs": "似たような曲を再生する", "playSimilarSongs": "似たような曲を再生する",
"viewQueue": "キューを表示する", "viewQueue": "キューを表示する"
"lyrics": "歌詞",
"queueType": "キュータイプ",
"queueType_default": "デフォルト",
"queueType_priority": "優先度",
"restoreQueueFromServer": "サーバーからキューを復元",
"saveQueueToServer": "サーバーにキューを保存"
}, },
"setting": { "setting": {
"crossfadeStyle_description": "オーディオプレーヤーが使用するクロスフェードのスタイルを選択します", "crossfadeStyle_description": "オーディオプレーヤーが使用するクロスフェードのスタイルを選択します",
@@ -209,13 +203,15 @@
"volumeWidth_description": "音量スライダーの幅", "volumeWidth_description": "音量スライダーの幅",
"volumeWidth": "音量スライダーの幅", "volumeWidth": "音量スライダーの幅",
"webAudio_description": "Web Audio を使用します。これにより、リプレイゲインなどの高度な機能が有効になります。それ以外の場合は無効にしてください", "webAudio_description": "Web Audio を使用します。これにより、リプレイゲインなどの高度な機能が有効になります。それ以外の場合は無効にしてください",
"playerAlbumArtResolution_description": "大画面プレーヤーのアルバムアートプレビューの解像度。解像度が高いほど鮮明になりますが、読み込みが遅くなる可能性があります。デフォルトは 0 (自動設定) です",
"mpvExtraParameters_help": "1 行に 1 つずつ", "mpvExtraParameters_help": "1 行に 1 つずつ",
"musicbrainz_description": "MusicBrainz ID が存在するアーティスト/アルバムページに MusicBrainz へのリンクを表示します", "musicbrainz_description": "MusicBrainz ID が存在するアーティスト/アルバムページに MusicBrainz へのリンクを表示します",
"musicbrainz": "MusicBrainz リンクを表示する", "musicbrainz": "MusicBrainz リンクを表示する",
"neteaseTranslation_description": "有効にすると、利用可能な場合は NetEase から翻訳された歌詞を取得して表示します", "neteaseTranslation_description": "有効にすると、利用可能な場合は NetEase から翻訳された歌詞を取得して表示します",
"neteaseTranslation": "NetEase 翻訳歌詞を有効にする", "neteaseTranslation": "NetEase 翻訳歌詞を有効にする",
"passwordStore_description": "使用するパスワード / シークレットストア。パスワードの保存に問題がある場合はこれを変更してください", "passwordStore_description": "使用するパスワード/シークレットストア。パスワードの保存に問題がある場合はこれを変更してください",
"passwordStore": "パスワード / シークレットストア", "passwordStore": "パスワード/シークレットストア",
"playerAlbumArtResolution": "プレーヤーのアルバムアートの解像度",
"playerbarOpenDrawer_description": "プレーヤーバーをクリックすると全画面プレーヤーが開きます", "playerbarOpenDrawer_description": "プレーヤーバーをクリックすると全画面プレーヤーが開きます",
"preferLocalLyrics_description": "利用可能な場合は、リモート歌詞よりもローカル歌詞を優先します", "preferLocalLyrics_description": "利用可能な場合は、リモート歌詞よりもローカル歌詞を優先します",
"preferLocalLyrics": "ローカル歌詞を優先する", "preferLocalLyrics": "ローカル歌詞を優先する",
@@ -302,33 +298,7 @@
"exportImportSettings_notValidJSON": "渡されたファイルは有効な JSON ではありません", "exportImportSettings_notValidJSON": "渡されたファイルは有効な JSON ではありません",
"exportImportSettings_importSuccess": "設定が正常にインポートされました!", "exportImportSettings_importSuccess": "設定が正常にインポートされました!",
"exportImportSettings_importModalTitle": "Feishin 設定をインポート", "exportImportSettings_importModalTitle": "Feishin 設定をインポート",
"exportImportSettings_importBtn": "設定をインポート", "exportImportSettings_importBtn": "設定をインポート"
"autoDJ_description": "類似の曲を自動でキューに追加します",
"autoDJ": "自動 DJ",
"autoDJ_itemCount_description": "自動 DJ が有効なときにキューに追加しようとした曲数",
"autoDJ_itemCount": "曲数",
"autoDJ_timing": "タイミング",
"autoDJ_timing_description": "自動 DJ が作動するまでのキューに残っている曲数",
"analyticsDisable": "使用状況に基づく分析のオプトアウト",
"analyticsDisable_description": "匿名化された利用データは、アプリケーションの改善のために開発者に送信されます",
"useThemeAccentColor": "テーマのアクセントカラーを使用",
"useThemeAccentColor_description": "カスタムアクセントカラーの代わりに、選択したテーマで定義されたプライマリカラーを使用します",
"artistReleaseTypeConfiguration": "アーティストリリースタイプの設定",
"artistReleaseTypeConfiguration_description": "アルバムアーティストページでどのリリースタイプをどのような順序で表示するかを設定します",
"followCurrentSong": "現在の曲をフォロー",
"followCurrentSong_description": "再生キューを現在再生中の曲まで自動的にスクロールします",
"logLevel": "ログレベル",
"logLevel_description": "表示するログの最小レベルを設定します。debug はすべてのログを表示し、error はエラーのみを表示します",
"logLevel_optionDebug": "debug",
"logLevel_optionError": "error",
"logLevel_optionInfo": "info",
"logLevel_optionWarn": "warn",
"playerFilters": "キューから曲をフィルタリング",
"playerFilters_description": "以下の基準に基づいて曲をキューに追加しないようにします",
"artistRadioCount": "アーティスト / トラックのラジオカウント",
"artistRadioCount_description": "アーティストラジオとトラックラジオで取得する曲数を設定します",
"imageResolution": "画像の解像度",
"imageResolution_description": "アプリ内で使用される画像の解像度。値を 0 に設定すると、デフォルトでネイティブ画像解像度が適用されます"
}, },
"action": { "action": {
"editPlaylist": "$t(entity.playlist_one) を編集", "editPlaylist": "$t(entity.playlist_one) を編集",
@@ -358,15 +328,7 @@
"shuffle": "シャッフル", "shuffle": "シャッフル",
"shuffleAll": "すべてシャッフル", "shuffleAll": "すべてシャッフル",
"shuffleSelected": "選択した曲をシャッフル", "shuffleSelected": "選択した曲をシャッフル",
"viewMore": "さらに表示", "viewMore": "さらに表示"
"createRadioStation": "$t(entity.radioStation_one) を作成",
"deleteRadioStation": "$t(entity.radioStation_one) を削除",
"selectAll": "すべて選択",
"moveUp": "上に移動",
"moveDown": "下に移動",
"holdToMoveToTop": "押し続けると一番上に移動します",
"holdToMoveToBottom": "押し続けると一番下に移動します",
"openApplicationDirectory": "アプリケーションディレクトリを開く"
}, },
"common": { "common": {
"backward": "戻る", "backward": "戻る",
@@ -469,14 +431,7 @@
"doNotShowAgain": "再度表示しない", "doNotShowAgain": "再度表示しない",
"externalLinks": "外部リンク", "externalLinks": "外部リンク",
"sort": "分類", "sort": "分類",
"gridRows": "グリッド行", "gridRows": "グリッド行"
"countSelected": "{{count}} 個選択されました",
"view": "表示",
"noFilters": "フィルターが設定されていません",
"retry": "再試行",
"itemsMore": "{{count}} 個以上",
"faster": "より速く",
"slower": "より遅く"
}, },
"table": { "table": {
"config": { "config": {
@@ -523,12 +478,7 @@
"year": "$t(common.year)", "year": "$t(common.year)",
"albumArtist": "$t(entity.albumArtist_one)", "albumArtist": "$t(entity.albumArtist_one)",
"codec": "$t(common.codec)", "codec": "$t(common.codec)",
"songCount": "$t(entity.track_other)", "songCount": "$t(entity.track_other)"
"albumCount": "$t(entity.album_other)",
"bitDepth": "$t(common.bitDepth)",
"genreBadge": "$t(entity.genre_one) (バッジ)",
"image": "画像",
"sampleRate": "$t(common.sampleRate)"
} }
}, },
"column": { "column": {
@@ -555,9 +505,7 @@
"discNumber": "ディスク", "discNumber": "ディスク",
"channels": "$t(common.channel_other)", "channels": "$t(common.channel_other)",
"size": "$t(common.size)", "size": "$t(common.size)",
"codec": "$t(common.codec)", "codec": "$t(common.codec)"
"bitDepth": "$t(common.bitDepth)",
"sampleRate": "$t(common.sampleRate)"
} }
}, },
"error": { "error": {
@@ -584,12 +532,7 @@
"networkError": "ネットワークエラーが発生しました", "networkError": "ネットワークエラーが発生しました",
"notificationDenied": "通知の許可が拒否されました。この設定は効果がありません", "notificationDenied": "通知の許可が拒否されました。この設定は効果がありません",
"openError": "ファイルを開けませんでした", "openError": "ファイルを開けませんでした",
"badValue": "無効なオプション「{{value}}」。この値は存在しません", "badValue": "無効なオプション「{{value}}」。この値は存在しません"
"multipleServerSaveQueueError": "再生キューに現在のサーバーに存在しない曲が 1 曲以上あります。これはサポートされていません",
"noNetwork": "サーバーが利用できません",
"noNetworkDescription": "このサーバーに接続できませんでした",
"saveQueueFailed": "キューを保存できませんでした",
"settingsSyncError": "レンダラーとメインプロセスの設定に矛盾が見つかりました。変更を適用するにはアプリケーションを再起動してください"
}, },
"filter": { "filter": {
"mostPlayed": "最も多く再生", "mostPlayed": "最も多く再生",
@@ -650,9 +593,7 @@
"artists": "$t(entity.artist_other)", "artists": "$t(entity.artist_other)",
"albumArtists": "$t(entity.albumArtist_other)", "albumArtists": "$t(entity.albumArtist_other)",
"myLibrary": "マイライブラリ", "myLibrary": "マイライブラリ",
"shared": "$t(entity.playlist_other) を共有", "shared": "$t(entity.playlist_other) を共有"
"radio": "$t(entity.radioStation_other)",
"favorites": "$t(entity.favorite_other)"
}, },
"fullscreenPlayer": { "fullscreenPlayer": {
"config": { "config": {
@@ -689,11 +630,7 @@
"goBack": "戻る", "goBack": "戻る",
"goForward": "進む", "goForward": "進む",
"privateModeOff": "プライベートモードをオフにする", "privateModeOff": "プライベートモードをオフにする",
"privateModeOn": "プライベートモードをオンにする", "privateModeOn": "プライベートモードをオンにする"
"selectMusicFolder": "音楽フォルダを選択",
"noMusicFolder": "音楽フォルダを選択",
"commandPalette": "コマンドパレットを開く",
"multipleMusicFolders": "{{count}} 個の音楽フォルダが選択されました"
}, },
"contextMenu": { "contextMenu": {
"addToPlaylist": "$t(action.addToPlaylist)", "addToPlaylist": "$t(action.addToPlaylist)",
@@ -719,9 +656,7 @@
"goToAlbum": "$t(entity.album_one) に移動", "goToAlbum": "$t(entity.album_one) に移動",
"goToAlbumArtist": "$t(entity.albumArtist_one) に移動", "goToAlbumArtist": "$t(entity.albumArtist_one) に移動",
"showDetails": "情報を取得する", "showDetails": "情報を取得する",
"playShuffled": "$t(player.shuffle)", "playShuffled": "$t(player.shuffle)"
"moveItems": "$t(action.moveItems)",
"goTo": "移動"
}, },
"home": { "home": {
"mostPlayed": "最も多く再生", "mostPlayed": "最も多く再生",
@@ -729,8 +664,7 @@
"title": "$t(common.home)", "title": "$t(common.home)",
"explore": "ライブラリから検索", "explore": "ライブラリから検索",
"recentlyPlayed": "最近の再生", "recentlyPlayed": "最近の再生",
"recentlyReleased": "最近のリリース", "recentlyReleased": "最近のリリース"
"genres": "$t(entity.genre_other)"
}, },
"albumDetail": { "albumDetail": {
"moreFromArtist": "$t(entity.artist_one) の他の項目", "moreFromArtist": "$t(entity.artist_one) の他の項目",
@@ -742,24 +676,7 @@
"generalTab": "一般", "generalTab": "一般",
"hotkeysTab": "ホットキー", "hotkeysTab": "ホットキー",
"windowTab": "ウィンドウ", "windowTab": "ウィンドウ",
"advanced": "高度", "advanced": "高度"
"analytics": "分析",
"updates": "更新",
"cache": "キャッシュ",
"application": "アプリケーション",
"queryBuilder": "クエリビルダー",
"theme": "テーマ",
"controls": "コントロール",
"sidebar": "サイドバー",
"remote": "リモート",
"exportImport": "インポート / エクスポート",
"scrobble": "Scrobble",
"audio": "オーディオ",
"lyrics": "歌詞",
"lyricsDisplay": "歌詞表示",
"transcoding": "トランスコーディング",
"discord": "Discord",
"logger": "ロガー"
}, },
"albumArtistList": { "albumArtistList": {
"title": "$t(entity.albumArtist_other)" "title": "$t(entity.albumArtist_other)"
@@ -799,9 +716,7 @@
"topSongsFrom": "{{title}} からの人気曲", "topSongsFrom": "{{title}} からの人気曲",
"viewAll": "すべて表示", "viewAll": "すべて表示",
"viewAllTracks": "$t(entity.track_other) をすべて表示", "viewAllTracks": "$t(entity.track_other) をすべて表示",
"relatedArtists": "関連の $t(entity.artist_other)", "relatedArtists": "関連の $t(entity.artist_other)"
"groupingTypeAll": "すべてのリリースタイプ",
"groupingTypePrimary": "主なリリースタイプ"
}, },
"manageServers": { "manageServers": {
"title": "サーバーの管理", "title": "サーバーの管理",
@@ -818,15 +733,6 @@
}, },
"playlist": { "playlist": {
"reorder": "ID によるソート時のみ並べ替えが可能です" "reorder": "ID によるソート時のみ並べ替えが可能です"
},
"radioList": {
"title": "ラジオ局"
},
"favorites": {
"title": "$t(entity.favorite_other)"
},
"folderList": {
"title": "$t(entity.folder_other)"
} }
}, },
"form": { "form": {
@@ -883,8 +789,7 @@
"editPlaylist": { "editPlaylist": {
"title": "$t(entity.playlist_one) を編集", "title": "$t(entity.playlist_one) を編集",
"publicJellyfinNote": "Jellyfin では、何らかの理由でプレイリストが公開されているかどうかが表示されません。公開されたままにしたい場合は、以下の項目を選択してください", "publicJellyfinNote": "Jellyfin では、何らかの理由でプレイリストが公開されているかどうかが表示されません。公開されたままにしたい場合は、以下の項目を選択してください",
"success": "$t(entity.playlist_one) が正常に更新されました", "success": "$t(entity.playlist_one) が正常に更新されました"
"editNote": "大規模なプレイリストの場合、手動編集は推奨されません。既存のプレイリストを上書きすることでデータ損失が発生するリスクを許容しますか?"
}, },
"shareItem": { "shareItem": {
"allowDownloading": "ダウンロードを許可", "allowDownloading": "ダウンロードを許可",
@@ -902,29 +807,6 @@
"largeFetchConfirmation": { "largeFetchConfirmation": {
"title": "キューにアイテムを追加する", "title": "キューにアイテムを追加する",
"description": "このアクションは、現在のフィルターされたビュー内のすべてのアイテムを追加します" "description": "このアクションは、現在のフィルターされたビュー内のすべてのアイテムを追加します"
},
"createRadioStation": {
"success": "ラジオ局が正常に作成されました",
"title": "ラジオ局を作成",
"input_homepageUrl": "ホームページ URL",
"input_name": "名前",
"input_streamUrl": "Stream URL"
},
"lyricsExport": {
"export": "歌詞をエクスポート",
"input_synced": "同期歌詞をエクスポート",
"input_offset": "$t(setting.lyricOffset)"
},
"shuffleAll": {
"title": "ランダムに再生",
"input_genre": "$t(entity.genre_one)",
"input_limit": "曲が多すぎます",
"input_minYear": "年から",
"input_maxYear": "年まで",
"input_played_optionAll": "すべてのトラック",
"input_played_optionUnplayed": "未再生のトラックのみ",
"input_played_optionPlayed": "再生されたトラックのみ",
"input_played": "再生フィルター"
} }
}, },
"entity": { "entity": {
@@ -945,8 +827,7 @@
"genreWithCount_other": "{{count}} 個のジャンル", "genreWithCount_other": "{{count}} 個のジャンル",
"trackWithCount_other": "{{count}} 個のトラック", "trackWithCount_other": "{{count}} 個のトラック",
"play_other": "{{count}} 回再生", "play_other": "{{count}} 回再生",
"song_other": "曲", "song_other": "曲"
"radioStation_other": "ラジオ局"
}, },
"dragDropZone": { "dragDropZone": {
"error_oneFileOnly": "1 つのファイルのみ選択してください", "error_oneFileOnly": "1 つのファイルのみ選択してください",
@@ -969,15 +850,5 @@
"demo": "デモ", "demo": "デモ",
"soundtrack": "サウンドトラック" "soundtrack": "サウンドトラック"
} }
},
"datetime": {
"minuteShort": "分",
"secondShort": "秒",
"hourShort": "時間",
"dayShort": "日"
},
"queryBuilder": {
"standardTags": "標準タグ",
"customTags": "カスタムタグ"
} }
} }
+3 -32
View File
@@ -21,23 +21,7 @@
}, },
"viewPlaylists": "$t(entity.playlist_other) 보기", "viewPlaylists": "$t(entity.playlist_other) 보기",
"setRating": "평점 지정", "setRating": "평점 지정",
"toggleSmartPlaylistEditor": "$t(entity.smartPlaylist) 편집기 펼치기", "toggleSmartPlaylistEditor": "$t(entity.smartPlaylist) 편집기 펼치기"
"addOrRemoveFromSelection": "선택항목에서 추가 또는 제거",
"selectRangeOfItems": "항목의 범위 선택",
"createRadioStation": "$t(entity.radioStation_one) 생성",
"deleteRadioStation": "$t(entity.radioStation_one) 삭제",
"selectAll": "전부 선택",
"downloadStarted": "{{count}}개 항목 다운로드 시작했습니다",
"moveUp": "위로 옮기기",
"moveDown": "아래로 옮기기",
"holdToMoveToTop": "맨 위로 옮기기 위해 끌기",
"holdToMoveToBottom": "맨 아래로 옮기기 위해 끌기",
"moveItems": "항목 옮기기",
"shuffle": "섞기",
"shuffleAll": "모두 섞기",
"shuffleSelected": "선택항목 섞기",
"viewMore": "더 보기",
"openApplicationDirectory": "앱 디렉토리 열기"
}, },
"common": { "common": {
"translation": "번역", "translation": "번역",
@@ -138,18 +122,7 @@
"recordLabel": "레이블", "recordLabel": "레이블",
"releaseType": "발매형태", "releaseType": "발매형태",
"explicit": "성인컨텐츠", "explicit": "성인컨텐츠",
"clean": "클린", "clean": "클린"
"countSelected": "{{count}}개 선택됨",
"doNotShowAgain": "다시 보지 않기",
"view": "보기",
"externalLinks": "외부 링크",
"faster": "빠르게",
"noFilters": "필터 미설정",
"slower": "천천히",
"sort": "정렬",
"gridRows": "행 그리드",
"tableColumns": "테이블 열",
"itemsMore": "{{count}}개 더"
}, },
"entity": { "entity": {
"albumWithCount_other": "{{count}} 앨범", "albumWithCount_other": "{{count}} 앨범",
@@ -169,9 +142,7 @@
"play_other": "{{count}} 재생", "play_other": "{{count}} 재생",
"playlistWithCount_other": "{{count}} 재생목록", "playlistWithCount_other": "{{count}} 재생목록",
"smartPlaylist": "스마트 $t(entity.playlist_one)", "smartPlaylist": "스마트 $t(entity.playlist_one)",
"track_other": "트랙", "track_other": "트랙"
"radioStation_other": "라디오 방송국",
"radioStationWithCount_other": "{{count}}개 라디오 방송국"
}, },
"error": { "error": {
"systemFontError": "시스템 폰트를 가져오는데 실패하였습니다", "systemFontError": "시스템 폰트를 가져오는데 실패하였습니다",
+3 -20
View File
@@ -27,17 +27,7 @@
"shuffle": "shuffle", "shuffle": "shuffle",
"shuffleAll": "shuffle alles", "shuffleAll": "shuffle alles",
"shuffleSelected": "shuffle geselecteerde", "shuffleSelected": "shuffle geselecteerde",
"viewMore": "bekijk meer", "viewMore": "bekijk meer"
"addOrRemoveFromSelection": "toevoegen of verwijderen van selectie",
"selectRangeOfItems": "selecteer een reeks van nummers",
"createRadioStation": "maak $t(entity.radioStation_one)",
"deleteRadioStation": "verwijder $t(entity.radioStation_one)",
"selectAll": "selecteer alles",
"moveUp": "beweeg naar boven",
"moveDown": "beweeg naar beneden",
"holdToMoveToTop": "ingedrukt houden om naar boven te verplaatsen",
"holdToMoveToBottom": "ingedrukt houden om naar beneden te verplaatsen",
"openApplicationDirectory": "applicatiefolder openen"
}, },
"common": { "common": {
"backward": "achteruit", "backward": "achteruit",
@@ -149,10 +139,7 @@
"clean": "schoon", "clean": "schoon",
"gridRows": "rasterrijen", "gridRows": "rasterrijen",
"tableColumns": "tabelkolommen", "tableColumns": "tabelkolommen",
"itemsMore": "{{count}} meer", "itemsMore": "{{count}} meer"
"countSelected": "{{count}} geselecteerd",
"view": "bekijken",
"noFilters": "geen filters ingesteld"
}, },
"filter": { "filter": {
"rating": "rating", "rating": "rating",
@@ -440,11 +427,7 @@
"song_one": "lied", "song_one": "lied",
"song_other": "liedjes", "song_other": "liedjes",
"play_one": "{{count}} keer afgespeeld", "play_one": "{{count}} keer afgespeeld",
"play_other": "{{count}} keren afgespeeld", "play_other": "{{count}} keren afgespeeld"
"radioStation_one": "radiostation",
"radioStation_other": "radiostations",
"radioStationWithCount_one": "{{count}} radiostation",
"radioStationWithCount_other": "{{count}} radiostations"
}, },
"table": { "table": {
"column": { "column": {
+10 -203
View File
@@ -33,11 +33,7 @@
"holdToMoveToTop": "przytrzymaj aby, przesunąć na górę", "holdToMoveToTop": "przytrzymaj aby, przesunąć na górę",
"holdToMoveToBottom": "przytrzymaj aby, przesunąć na dół", "holdToMoveToBottom": "przytrzymaj aby, przesunąć na dół",
"createRadioStation": "utwórz $t(entity.radioStation_one)", "createRadioStation": "utwórz $t(entity.radioStation_one)",
"deleteRadioStation": "usuń $t(entity.radioStation_one)", "deleteRadioStation": "usuń $t(entity.radioStation_one)"
"addOrRemoveFromSelection": "dodaj lub usuń z wyboru",
"selectRangeOfItems": "wybierz zakres elementów",
"selectAll": "wybierz wszystkie",
"openApplicationDirectory": "otwórz katalog aplikacji"
}, },
"common": { "common": {
"increase": "zwiększ", "increase": "zwiększ",
@@ -154,10 +150,7 @@
"tableColumns": "tabela kolumn", "tableColumns": "tabela kolumn",
"itemsMore": "{{count}} więcej", "itemsMore": "{{count}} więcej",
"noFilters": "nie skonfigurowano filtrów", "noFilters": "nie skonfigurowano filtrów",
"view": "wyświetl", "view": "wyświetl"
"countSelected": "wybrano {{count}}",
"retry": "spróbuj ponownie",
"mood": "nastrój"
}, },
"entity": { "entity": {
"genre_one": "gatunek", "genre_one": "gatunek",
@@ -245,10 +238,7 @@
"badValue": "niewłaściwa opcja \"{{value}}\". ta wartość już nie istnieje", "badValue": "niewłaściwa opcja \"{{value}}\". ta wartość już nie istnieje",
"notificationDenied": "odmówiono uprawnień dla powiadomień. to ustawienie nie będzie miało efektu", "notificationDenied": "odmówiono uprawnień dla powiadomień. to ustawienie nie będzie miało efektu",
"multipleServerSaveQueueError": "kolejka odtwarzania ma jedną lub więcej piosenek które nie pochodzą z aktualnego serwera. to nie jest wspierane", "multipleServerSaveQueueError": "kolejka odtwarzania ma jedną lub więcej piosenek które nie pochodzą z aktualnego serwera. to nie jest wspierane",
"saveQueueFailed": "nie udało się zapisać kolejki", "saveQueueFailed": "nie udało się zapisać kolejki"
"settingsSyncError": "zostały znalezione różnice pomiędzy ustawieniami w rendererze a głównym procesem. uruchom aplikację ponownie aby, zastosować zmiany",
"noNetwork": "serwer niedostępny",
"noNetworkDescription": "nie udało się połączyć z tym serwerem"
}, },
"filter": { "filter": {
"mostPlayed": "najczęściej odtwarzane", "mostPlayed": "najczęściej odtwarzane",
@@ -322,10 +312,7 @@
"ignoreCors": "zignoruj cors ($t(common.restartRequired))", "ignoreCors": "zignoruj cors ($t(common.restartRequired))",
"error_savePassword": "wystąpił błąd podczas próby zapisania hasła", "error_savePassword": "wystąpił błąd podczas próby zapisania hasła",
"input_preferInstantMix": "preferuj natychmiastowy mix", "input_preferInstantMix": "preferuj natychmiastowy mix",
"input_preferInstantMixDescription": "używaj tylko natychmiastowego mixu, by otrzymać podobne piosenki. przydatne gdy masz wtyczki które zmieniają to zachowanie", "input_preferInstantMixDescription": "używaj tylko natychmiastowego mixu, by otrzymać podobne piosenki. przydatne gdy masz wtyczki które zmieniają to zachowanie"
"input_preferRemoteUrl": "preferuj publiczny url",
"input_remoteUrl": "publiczny url",
"input_remoteUrlPlaceholder": "opcjonalne: publiczny url dla funkcji zewnętrznych"
}, },
"addToPlaylist": { "addToPlaylist": {
"success": "dodano $t(entity.trackWithCount, {\"count\": {{message}} }) do $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })", "success": "dodano $t(entity.trackWithCount, {\"count\": {{message}} }) do $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
@@ -396,11 +383,6 @@
"input_homepageUrl": "url strony głównej", "input_homepageUrl": "url strony głównej",
"input_name": "nazwa", "input_name": "nazwa",
"input_streamUrl": "url strumienia" "input_streamUrl": "url strumienia"
},
"lyricsExport": {
"export": "eksportuj tekst",
"input_synced": "eksportuj zsynchronizowany tekst",
"input_offset": "$t(setting.lyricOffset)"
} }
}, },
"page": { "page": {
@@ -539,8 +521,7 @@
"transcoding": "transkodowanie", "transcoding": "transkodowanie",
"discord": "discord", "discord": "discord",
"playerFilters": "filtry odtwarzacza", "playerFilters": "filtry odtwarzacza",
"logger": "logger", "logger": "logger"
"lyricsDisplay": "wyświetlanie tekstu"
}, },
"trackList": { "trackList": {
"title": "$t(entity.track_other)", "title": "$t(entity.track_other)",
@@ -567,9 +548,7 @@
"viewDiscography": "przeglądaj dyskografię", "viewDiscography": "przeglądaj dyskografię",
"relatedArtists": "powiązane z $t(entity.artist_other)", "relatedArtists": "powiązane z $t(entity.artist_other)",
"appearsOn": "pojawia się na", "appearsOn": "pojawia się na",
"viewAllTracks": "zobacz wszystko $t(entity.track_other)", "viewAllTracks": "zobacz wszystko $t(entity.track_other)"
"groupingTypeAll": "wszystkie typy wydań",
"groupingTypePrimary": "główne typy wydań"
}, },
"itemDetail": { "itemDetail": {
"copyPath": "kopiuj ścieżkę do schowka", "copyPath": "kopiuj ścieżkę do schowka",
@@ -637,9 +616,7 @@
"holdToShuffle": "przytrzymaj aby odtwarzać losowo", "holdToShuffle": "przytrzymaj aby odtwarzać losowo",
"lyrics": "tekst", "lyrics": "tekst",
"restoreQueueFromServer": "przywróć kolejkę z serwera", "restoreQueueFromServer": "przywróć kolejkę z serwera",
"saveQueueToServer": "zapisz kolejkę na serwerze", "saveQueueToServer": "zapisz kolejkę na serwerze"
"artistRadio": "radio wykonawcy",
"trackRadio": "radio utworu"
}, },
"setting": { "setting": {
"crossfadeStyle_description": "wybierz styl przenikania, który ma być używany do odtwarzania dźwięku", "crossfadeStyle_description": "wybierz styl przenikania, który ma być używany do odtwarzania dźwięku",
@@ -802,10 +779,12 @@
"clearQueryCache_description": "\"miękkie wyczyszczenie\" feishin. spowoduje to odświeżenie playlist, metadanych utworów i zresetowanie zapisanych tekstów. ustawienia, dane uwierzytelniające serwera i obrazy w pamięci podręcznej zostaną zachowane", "clearQueryCache_description": "\"miękkie wyczyszczenie\" feishin. spowoduje to odświeżenie playlist, metadanych utworów i zresetowanie zapisanych tekstów. ustawienia, dane uwierzytelniające serwera i obrazy w pamięci podręcznej zostaną zachowane",
"buttonSize_description": "rozmiar przycisków paska odtwarzacza", "buttonSize_description": "rozmiar przycisków paska odtwarzacza",
"clearCache": "wyczyść pamięć podręczną przeglądarki", "clearCache": "wyczyść pamięć podręczną przeglądarki",
"playerAlbumArtResolution": "rozdzielczość okładki albumu odtwarzacza",
"externalLinks": "pokaż zewnętrzne linki", "externalLinks": "pokaż zewnętrzne linki",
"mpvExtraParameters_help": "po jednym na linię", "mpvExtraParameters_help": "po jednym na linię",
"passwordStore": "hasła", "passwordStore": "hasła",
"passwordStore_description": "jakie hasło ma być używane. zmień to, jeśli masz problemy z przechowywaniem haseł", "passwordStore_description": "jakie hasło ma być używane. zmień to, jeśli masz problemy z przechowywaniem haseł",
"playerAlbumArtResolution_description": "rozdzielczość podglądu okładki albumu w dużym odtwarzaczu. większa sprawia, że wygląda bardziej wyraziście, ale może spowolnić ładowanie. domyślnie 0, czyli auto",
"startMinimized": "uruchom zminimalizowany", "startMinimized": "uruchom zminimalizowany",
"startMinimized_description": "uruchom aplikację w zasobniku systemowym", "startMinimized_description": "uruchom aplikację w zasobniku systemowym",
"clearCacheSuccess": "pamięć podręczna została wyczyszczona pomyślnie", "clearCacheSuccess": "pamięć podręczna została wyczyszczona pomyślnie",
@@ -947,24 +926,7 @@
"logLevel_optionInfo": "info", "logLevel_optionInfo": "info",
"logLevel_optionWarn": "ostrzeżenia", "logLevel_optionWarn": "ostrzeżenia",
"useThemeAccentColor": "używaj koloru akcentu motywu", "useThemeAccentColor": "używaj koloru akcentu motywu",
"useThemeAccentColor_description": "używaj głównego koloru ustawionego w wybranym motywie zamiast niestandardowego koloru akcentu", "useThemeAccentColor_description": "używaj głównego koloru ustawionego w wybranym motywie zamiast niestandardowego koloru akcentu"
"artistRadioCount_description": "ustawia liczbę piosenek do załadowania dla radia wykonawcy i radia utworu",
"artistRadioCount": "liczba radio wykonawców/utworów",
"imageResolution": "rozdzielczość obrazu",
"imageResolution_description": "rozdzielczość dla obrazów używanych w programie. użycie wartości 0 ustawi rozdzielczość na natywną",
"imageResolution_optionTable": "tabela",
"imageResolution_optionItemCard": "karta elementu",
"imageResolution_optionSidebar": "­pasek boczny",
"imageResolution_optionHeader": "nagłówek",
"imageResolution_optionFullScreenPlayer": "odtwarzacz pełnoekranowy",
"combinedLyricsAndVisualizer_description": "połącz tekst i wizualizacje w tym samym panelu",
"combinedLyricsAndVisualizer": "połącz tekst i wizualizacje w pasku bocznym odtwarzacza",
"artistReleaseTypeConfiguration": "konfiguracja typu wydań wykonawcy",
"artistReleaseTypeConfiguration_description": "skonfiguruj jakie typy wydań są pokazywane i w jakiej kolejności na stronie albumów wykonawcy",
"showRatings_description": "kontroluje czy funkcja oceniania gwiazdkami jest pokazywana w interfejsie",
"showRatings": "pokaż ocenianie gwiazdkami",
"mpvExtraParameters": "dodatkowe parametry mpv",
"mpvExtraParameters_description": "dodatkowe argumenty do przekazania mpv"
}, },
"table": { "table": {
"config": { "config": {
@@ -1122,160 +1084,5 @@
"notInPlaylist": "nie jest w", "notInPlaylist": "nie jest w",
"notInTheLast": "nie jest w ostatnim", "notInTheLast": "nie jest w ostatnim",
"startsWith": "zaczyna się od" "startsWith": "zaczyna się od"
},
"datetime": {
"minuteShort": "min",
"secondShort": "sek",
"hourShort": "godz",
"dayShort": "dzień"
},
"visualizer": {
"visualizerType": "Typ Wizualizacji",
"cycleTime": "Czas cyklu (w sekundach)",
"copyConfiguration": "Kopiuj Konfigurację",
"pasteConfiguration": "Wklej Konfigurację",
"pasteConfigurationPlaceholder": "Wklej konfigurację JSON tutaj...",
"pasteFromClipboard": "Wklej z schowka",
"applyConfiguration": "Zastosuj Konfigurację",
"configCopied": "Konfiguracja skopiowana do schowka",
"configCopyFailed": "Nie udało się skopiować konfiguracji",
"configPasted": "Konfiguracja zastosowana pomyślnie",
"configPasteFailed": "Nie udało się zastosować konfiguracji. Sprawdź jej format.",
"configPasteReadFailed": "Nie udało się odczytać z schowka",
"cyclePresets": "Cykl Ustawień",
"includeAllPresets": "Uwzględnij wszystkie Ustawienia",
"ignoredPresets": "Ignorowane Ustawienia",
"selectedPresets": "Wybrane Ustawienia",
"randomizeNextPreset": "Losuj Następne Ustawienie",
"blendTime": "Czas Mieszania",
"presets": "Ustawienia",
"selectPreset": "Wybierz Ustawienie",
"applyPreset": "Zastosuj Ustawienie",
"saveAsPreset": "Zapisz jako Ustawienie",
"updatePreset": "Uaktualnij Ustawienie",
"presetName": "Nazwa Ustawienia",
"presetNamePlaceholder": "Wpisz nazwę ustawienia",
"general": "Ogólne",
"mode": "Tryb",
"mode1To8": "Tryb 1 - 8",
"mode10": "Tryb 10",
"barSpace": "Odstęp Pasków",
"lineWidth": "Szerokość Linii",
"fillAlpha": "Wypełnij Alpha",
"channelLayout": "Układ Kanałów",
"maxFPS": "Maks FPS",
"opacity": "Nieprzezroczystość",
"customGradients": "­Niestandardowe Gradienty",
"addCustomGradient": "Dodaj Niestandardowy Gradient",
"gradientName": "Nazwa Gradientu",
"gradientNamePlaceholder": "Nazwa Gradientu",
"vertical": "Pionowy",
"horizontal": "Poziomy",
"colorStops": "Kroki Kolorów",
"addColor": "Dodaj Kolor",
"position": "Pozycja",
"level": "Poziom",
"remove": "Usuń",
"custom": "Niestandardowy",
"builtIn": "Wbudowany",
"colors": "Kolory",
"colorMode": "Tryb Koloru",
"gradient": "Gradient",
"gradientLeft": "Lewa Gradientu",
"gradientRight": "Prawa Gradientu",
"fft": "FFT",
"fftSize": "Rozmiar FFT",
"smoothing": "Wygładzanie",
"frequencyRangeAndScaling": "Zakres częstotliwości i skalowanie",
"minimumFrequency": "Minimalna Częstotliwość",
"maximumFrequency": "Maksymalna Częstotliwość",
"frequencyScale": "Skala Częstotliwości",
"sensitivity": "Czułość",
"weightingFilter": "Filtr Wagi",
"minimumDecibels": "Minimum Decybeli",
"maximumDecibels": "Maksimum Decybeli",
"linearAmplitude": "Amplituda Linearna",
"linearBoost": "Podbicie Linearne",
"peakBehavior": "Zachowanie Szczytów",
"showPeaks": "Pokaż Szczyty",
"fadePeaks": "Zanikaj Sczyty",
"peakLine": "Linia Szczytów",
"gravity": "Grawitacja",
"peakFadeTime": "Czas Zanikania Szczytów (ms)",
"peakHoldTime": "Czas Utrzymywania Szczytu (ms)",
"radialSpectrum": "Spektrum Promieniowe",
"radial": "Promieniowe",
"radialInvert": "Odwrócenie Promieniowe",
"spinSpeed": "Prędkość Obrotu",
"radius": "Promień",
"reflexMirror": "Lustro refleksyjne",
"reflexFit": "Dopasowanie Odbić",
"reflexRatio": "Współczynnik Odbić",
"reflexAlpha": "Alpha Odbić",
"reflexBrightness": "Jasność Odbić",
"mirror": "Odbij lustrzanie",
"miscellaneousSettings": "Różne Ustawienia",
"alphaBars": "Alpha Pasków",
"ledBars": "Paski LED",
"trueLeds": "Prawdziwe LEDy",
"lumiBars": "Paski Lumi",
"outlineBars": "Obwódki Pasków",
"roundBars": "Zaokrąglone Paski",
"lowResolution": "­Niska Rozdzielczość",
"splitGradient": "Rozdziel Gradient",
"showFPS": "Pokaż FPS",
"showScaleX": "Pokaż Skalę X",
"noteLabels": "Etykiety Nut",
"showScaleY": "Pokaż Skalę Y",
"options": {
"colorMode": {
"gradient": "Gradient",
"barIndex": "Indeks-Paska",
"barLevel": "Poziom-Paska"
},
"gradient": {
"classic": "Klasyczny",
"prism": "Pryzmat",
"rainbow": "Tęcza",
"steelblue": "Stalowoniebieski",
"orangered": "Pomarańczowo-czerwony"
},
"channelLayout": {
"single": "Pojedynczy",
"dualCombined": "Podwójne-Połączone",
"dualHorizontal": "Podwójne-Poziome",
"dualVertical": "Podwójne-Pionowe"
},
"frequencyScale": {
"linear": "Skala linearna",
"none": "Żadna",
"bark": "Skala bark",
"log": "Skala log",
"mel": "Skala Mel"
},
"weightingFilter": {
"none": "Żadne",
"a": "A",
"b": "B",
"c": "C",
"d": "D",
"z": "Z"
},
"mode": {
"0": "[0] Dyskretne częstotliwości",
"1": "[1] 1/24 oktawy / 240 pasm",
"2": "[2] 1/12 oktawy / 120 pasm",
"3": "[3] 1/8 oktawy / 80 pasm",
"4": "[4] 1/6 oktawy / 60 pasm",
"5": "[5] 1/4 oktawy / 40 pasm",
"6": "[6] 1/3 oktawy / 30 pasm",
"7": "[7] Pół oktawy / 20 pasm",
"8": "[8] Pełna oktawa / 10 pasm",
"10": "[10] Linia / Wykres miejscowy"
}
},
"pasteGradient": "Wklej Gradient",
"pasteGradientPlaceholder": "Wklej tutaj JSON gradientu...",
"ansiBands": "Paski ANSI"
} }
} }
+2 -2
View File
@@ -356,6 +356,8 @@
"playButtonBehavior_optionAddNext": "$t(player.addNext)", "playButtonBehavior_optionAddNext": "$t(player.addNext)",
"playButtonBehavior_optionPlay": "$t(player.play)", "playButtonBehavior_optionPlay": "$t(player.play)",
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)", "playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
"playerAlbumArtResolution": "resolução da capa do álbum no reprodutor",
"playerAlbumArtResolution_description": "a resolução da pré-visualização da capa do álbum no reprodutor grande. Resoluções maiores deixam a imagem mais nítida, mas podem diminuir a velocidade de carregamento. O padrão é 0, ou seja, automático",
"playerbarOpenDrawer": "alternar tela cheia na barra do reprodutor", "playerbarOpenDrawer": "alternar tela cheia na barra do reprodutor",
"playerbarOpenDrawer_description": "permite clicar na barra do reprodutor para abrir o reprodutor em tela cheia", "playerbarOpenDrawer_description": "permite clicar na barra do reprodutor para abrir o reprodutor em tela cheia",
"remotePassword": "Senha do servidor de controle remoto", "remotePassword": "Senha do servidor de controle remoto",
@@ -381,8 +383,6 @@
"savePlayQueue_description": "Salvar a fila de reprodução ao fechar a aplicação e restaurá-la ao abrir a aplicação", "savePlayQueue_description": "Salvar a fila de reprodução ao fechar a aplicação e restaurá-la ao abrir a aplicação",
"scrobble": "Scrobblar", "scrobble": "Scrobblar",
"scrobble_description": "Scrobblar reproduções para o seu servidor de mídia", "scrobble_description": "Scrobblar reproduções para o seu servidor de mídia",
"showRatings": "exibir avaliações por estrelas",
"showRatings_description": "exibir ou ocultar as avaliações por estrelas",
"showSkipButton": "Exibir botões de pular", "showSkipButton": "Exibir botões de pular",
"showSkipButton_description": "Exibir ou ocultar os botões de pular na barra do reprodutor", "showSkipButton_description": "Exibir ou ocultar os botões de pular na barra do reprodutor",
"showSkipButtons": "Exibir botões de pular", "showSkipButtons": "Exibir botões de pular",
+2
View File
@@ -670,6 +670,7 @@
"playButtonBehavior": "поведение кнопки воспроизведения", "playButtonBehavior": "поведение кнопки воспроизведения",
"playButtonBehavior_optionAddNext": "$t(player.addNext)", "playButtonBehavior_optionAddNext": "$t(player.addNext)",
"playButtonBehavior_optionPlay": "$t(player.play)", "playButtonBehavior_optionPlay": "$t(player.play)",
"playerAlbumArtResolution_description": "разрешение большой версии обложки альбома в проигрывателе. при большем разрешении она выглядит более четкой, но может замедлить загрузку. по умолчанию равно 0 - устанавливает разрешение автоматически",
"playerbarOpenDrawer": "полноэкранный переключатель по панели проигрывателя", "playerbarOpenDrawer": "полноэкранный переключатель по панели проигрывателя",
"playerbarOpenDrawer_description": "позволяет перейти в полноэкранный режим воспроизведения нажатием на панель проигрывателя", "playerbarOpenDrawer_description": "позволяет перейти в полноэкранный режим воспроизведения нажатием на панель проигрывателя",
"remotePort": "порт сервера удалённого управления", "remotePort": "порт сервера удалённого управления",
@@ -710,6 +711,7 @@
"imageAspectRatio_description": "если эта опция включена, обложки будут отображаться в соответствии с их собственным соотношением сторон. для обложек не 1:1 оставшееся пространство будет пустым", "imageAspectRatio_description": "если эта опция включена, обложки будут отображаться в соответствии с их собственным соотношением сторон. для обложек не 1:1 оставшееся пространство будет пустым",
"minimumScrobblePercentage": "минимальное время для скробблинга (в процентах)", "minimumScrobblePercentage": "минимальное время для скробблинга (в процентах)",
"playbackStyle": "стиль воспроизведения", "playbackStyle": "стиль воспроизведения",
"playerAlbumArtResolution": "разрешение обложки альбома",
"remotePassword_description": "задает пароль для сервера удалённого управления. По умолчанию эти учетные данные передаются небезопасным способом, поэтому следует использовать уникальный пароль, который вам неважен", "remotePassword_description": "задает пароль для сервера удалённого управления. По умолчанию эти учетные данные передаются небезопасным способом, поэтому следует использовать уникальный пароль, который вам неважен",
"replayGainClipping_description": "Предотвращение клиппинга, вызванного {{ReplayGain}}, путём автоматического снижения усиления", "replayGainClipping_description": "Предотвращение клиппинга, вызванного {{ReplayGain}}, путём автоматического снижения усиления",
"replayGainFallback_description": "усиление в db для применения, если у файла нет тегов {{ReplayGain}}", "replayGainFallback_description": "усиление в db для применения, если у файла нет тегов {{ReplayGain}}",
+2
View File
@@ -654,6 +654,8 @@
"playButtonBehavior_optionAddNext": "$t(player.addNext)", "playButtonBehavior_optionAddNext": "$t(player.addNext)",
"playButtonBehavior_optionPlay": "$t(player.play)", "playButtonBehavior_optionPlay": "$t(player.play)",
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)", "playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
"playerAlbumArtResolution": "rozlíšenie obrázka albumu",
"playerAlbumArtResolution_description": "rozlíšenie zobrazenia náhľadu veľkých obrázkov albumov. pri väčšom rozlíšení budú krajšie, ale môže sa spomaliť ich načítavanie. predvolené je 0, čo znamená automatické",
"playerbarOpenDrawer": "zobrazenie na celú obrazovku panelom prehrávača", "playerbarOpenDrawer": "zobrazenie na celú obrazovku panelom prehrávača",
"playerbarOpenDrawer_description": "umožní kliknutím na panel prehrávača prepnúť zobrazenie prehrávača na celú obrazovku", "playerbarOpenDrawer_description": "umožní kliknutím na panel prehrávača prepnúť zobrazenie prehrávača na celú obrazovku",
"remotePassword": "heslo servera vzdialeného ovládania", "remotePassword": "heslo servera vzdialeného ovládania",
+9 -80
View File
@@ -33,11 +33,7 @@
"musicbrainz": "Öppna i MusicBrainz" "musicbrainz": "Öppna i MusicBrainz"
}, },
"createRadioStation": "skapa $t(entity.radioStation_one)", "createRadioStation": "skapa $t(entity.radioStation_one)",
"deleteRadioStation": "ta bort $t(entity.radioStation_one)", "deleteRadioStation": "ta bort $t(entity.radioStation_one)"
"addOrRemoveFromSelection": "lägg till eller ta bort från markerade",
"selectRangeOfItems": "välj en mängd objekt",
"selectAll": "markera alla",
"openApplicationDirectory": "öppna applikationskatalog"
}, },
"common": { "common": {
"backward": "bakåt", "backward": "bakåt",
@@ -147,8 +143,7 @@
"clean": "städad", "clean": "städad",
"gridRows": "rutnätsrader", "gridRows": "rutnätsrader",
"tableColumns": "tabellkolumner", "tableColumns": "tabellkolumner",
"itemsMore": "{{count}} fler", "itemsMore": "{{count}} fler"
"countSelected": "{{count}} markerade"
}, },
"error": { "error": {
"remotePortWarning": "starta om servern för att tillämpa den nya porten", "remotePortWarning": "starta om servern för att tillämpa den nya porten",
@@ -171,12 +166,7 @@
"invalidServer": "ogiltig server", "invalidServer": "ogiltig server",
"loginRateError": "för många inloggningsförsök, försök igen om några sekunder", "loginRateError": "för många inloggningsförsök, försök igen om några sekunder",
"badAlbum": "du ser denna sidan eftersom denna låten inte är en del av ett album. du ser troligtvis detta problemet för att du har en låt på toppnivån i din musikmapp. Jellyfin grupperar bara låtar om de finns i en mapp", "badAlbum": "du ser denna sidan eftersom denna låten inte är en del av ett album. du ser troligtvis detta problemet för att du har en låt på toppnivån i din musikmapp. Jellyfin grupperar bara låtar om de finns i en mapp",
"badValue": "felaktigt alternativ \"{{value}}\". detta värde existerar inte längre", "badValue": "felaktigt alternativ \"{{value}}\". detta värde existerar inte längre"
"multipleServerSaveQueueError": "spelningskön har en eller flera låtar som inte är från den nuvarande valda servern. detta är inte stöttat",
"networkError": "en nätverksfel uppstod",
"notificationDenied": "åtkomst till notifieringarna var nekad. inställningen har ingen verkan",
"openError": "kunde inte öppna filen",
"settingsSyncError": "diskrepans hittades mellan inställningarna för renderingsprocessen och huvudprocessen. starta om applikationen för att ändringarna ska tillämpas"
}, },
"filter": { "filter": {
"mostPlayed": "mest spelade", "mostPlayed": "mest spelade",
@@ -219,9 +209,7 @@
"album": "$t(entity.album_one)", "album": "$t(entity.album_one)",
"trackNumber": "spår", "trackNumber": "spår",
"songCount": "sångräkning", "songCount": "sångräkning",
"criticRating": "kritikerbetyg", "criticRating": "kritikerbetyg"
"albumCount": "$t(entity.album_other) antal",
"explicitStatus": "$t(common.explicitStatus)"
}, },
"form": { "form": {
"deletePlaylist": { "deletePlaylist": {
@@ -248,17 +236,13 @@
"input_savePassword": "spara lösenord", "input_savePassword": "spara lösenord",
"ignoreSsl": "ignorera ssl ($t(common.restartRequired))", "ignoreSsl": "ignorera ssl ($t(common.restartRequired))",
"ignoreCors": "ignorera cors ($t(common.restartRequired))", "ignoreCors": "ignorera cors ($t(common.restartRequired))",
"error_savePassword": "ett fel uppstod när lösenordet skulle sparas", "error_savePassword": "ett fel uppstod när lösenordet skulle sparas"
"input_preferInstantMix": "föredra instant mixning",
"input_preferInstantMixDescription": "använd bara instant mixning för att få liknande låtar. användbar om du har plugin för att förändra detta beteendet"
}, },
"addToPlaylist": { "addToPlaylist": {
"success": "lade till $t(entity.trackWithCount, {\"count\": {{message}} }) till $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })", "success": "tillade {{message}} $t(entity.track_other) til {{numOfPlaylists}} $t(entity.playlist_other)",
"title": "lägg till i $t(entity.playlist_one)", "title": "lägg till i $t(entity.playlist_one)",
"input_skipDuplicates": "hoppa över dubbletter", "input_skipDuplicates": "hoppa över dubbletter",
"input_playlists": "$t(entity.playlist_other)", "input_playlists": "$t(entity.playlist_other)"
"create": "skapa $t(entity.playlist_one) {{playlist}}",
"searchOrCreate": "sök $t(entity.playlist_other) eller skriv för att skapa en ny"
}, },
"updateServer": { "updateServer": {
"title": "uppdatera server", "title": "uppdatera server",
@@ -274,19 +258,7 @@
"title": "sångtext sök" "title": "sångtext sök"
}, },
"editPlaylist": { "editPlaylist": {
"title": "redigera $t(entity.playlist_one)", "title": "redigera $t(entity.playlist_one)"
"publicJellyfinNote": "Jellyfin visar av någon anledning inte om en spellista är publik eller inte. Om du önskar att denna ska förbli publik, så får du ha följande indata markerade"
},
"largeFetchConfirmation": {
"title": "lägg till objekt till kön",
"description": "Åtgärden kommer att lägga till alla objekt till den nuvarande filtrerade vyn"
},
"createRadioStation": {
"success": "radiostation skapades",
"title": "skapa radiostation",
"input_homepageUrl": "hemside-URL",
"input_name": "namn",
"input_streamUrl": "stream url"
} }
}, },
"page": { "page": {
@@ -334,17 +306,7 @@
"addFavorite": "$t(action.addToFavorites)", "addFavorite": "$t(action.addToFavorites)",
"play": "$t(player.play)", "play": "$t(player.play)",
"numberSelected": "{{count}} vald", "numberSelected": "{{count}} vald",
"removeFromQueue": "$t(action.removeFromQueue)", "removeFromQueue": "$t(action.removeFromQueue)"
"download": "ladda ner",
"moveItems": "$t(action.moveItems)",
"moveToNext": "$t(action.moveToNext)",
"playSimilarSongs": "$t(player.playSimilarSongs)",
"playShuffled": "$t(player.shuffle)",
"shareItem": "dela objekt",
"goTo": "gå till",
"goToAlbum": "gå till $t(entity.album_one)",
"goToAlbumArtist": "gå till $t(entity.albumArtist_one)",
"showDetails": "hämta information"
}, },
"albumDetail": { "albumDetail": {
"moreFromArtist": "mer från $t(entity.artist_one)", "moreFromArtist": "mer från $t(entity.artist_one)",
@@ -378,12 +340,6 @@
"searchFor": "sök efter {{query}}" "searchFor": "sök efter {{query}}"
}, },
"title": "kommandon" "title": "kommandon"
},
"manageServers": {
"url": "URL",
"username": "användarnamn",
"editServerDetailsTooltip": "redigera serverinställningar",
"removeServer": "ta bort server"
} }
}, },
"entity": { "entity": {
@@ -449,32 +405,5 @@
"queue_moveToBottom": "flytta markerad till toppen", "queue_moveToBottom": "flytta markerad till toppen",
"addLast": "lägg till sist", "addLast": "lägg till sist",
"mute": "muta" "mute": "muta"
},
"datetime": {
"minuteShort": "min",
"secondShort": "sek",
"hourShort": "h",
"dayShort": "dag"
},
"filterOperator": {
"after": "är efter",
"afterDate": "är efter (datum)",
"before": "är före",
"beforeDate": "är före (datum)",
"contains": "innehåller",
"endsWith": "slutar med",
"inPlaylist": "är inom",
"inTheLast": "är i den sista",
"inTheRange": "är i spannet",
"inTheRangeDate": "är i spannet (datum)",
"is": "är",
"isNot": "är inte",
"isGreaterThan": "är större än",
"isLessThan": "är mindre än",
"matchesRegex": "matchar regex",
"notContains": "innehåller inte",
"notInPlaylist": "är inte inom",
"notInTheLast": "är inte inom den sista",
"startsWith": "startar med"
} }
} }
+2
View File
@@ -550,6 +550,8 @@
"playButtonBehavior_description": "வரிசையில் பாடல்களைச் சேர்க்கும்போது ப்ளே பொத்தானின் இயல்புநிலை நடத்தை அமைக்கிறது", "playButtonBehavior_description": "வரிசையில் பாடல்களைச் சேர்க்கும்போது ப்ளே பொத்தானின் இயல்புநிலை நடத்தை அமைக்கிறது",
"playButtonBehavior_optionAddLast": "$t(player.addLast)", "playButtonBehavior_optionAddLast": "$t(player.addLast)",
"playButtonBehavior_optionAddNext": "$t(player.addNext)", "playButtonBehavior_optionAddNext": "$t(player.addNext)",
"playerAlbumArtResolution": "பிளேயர் ஆல்பம் கலைத் தீர்மானம்",
"playerAlbumArtResolution_description": "பெரிய வீரரின் ஆல்பம் கலை முன்னோட்டத்திற்கான தீர்மானம். பெரியது இது மிகவும் மிருதுவானதாக தோற்றமளிக்கிறது, ஆனால் மெதுவாக ஏற்றுவதை மெதுவாகக் கொண்டிருக்கலாம். இயல்புநிலை 0 க்கு, அதாவது ஆட்டோ",
"playerbarOpenDrawer": "பிளேயர்பார் முழுத்திரை மாற்று", "playerbarOpenDrawer": "பிளேயர்பார் முழுத்திரை மாற்று",
"playerbarOpenDrawer_description": "முழு திரை பிளேயரைத் திறக்க பிளேயர்பாரைக் சொடுக்கு செய்ய அனுமதிக்கிறது", "playerbarOpenDrawer_description": "முழு திரை பிளேயரைத் திறக்க பிளேயர்பாரைக் சொடுக்கு செய்ய அனுமதிக்கிறது",
"remotePassword": "ரிமோட் கண்ட்ரோல் சர்வர் கடவுச்சொல்", "remotePassword": "ரிமோட் கண்ட்ரோல் சர்வர் கடவுச்சொல்",
+4 -11
View File
@@ -21,15 +21,7 @@
"goToPage": "sayfaya git", "goToPage": "sayfaya git",
"moveToNext": "sonrakine geç", "moveToNext": "sonrakine geç",
"refresh": "$t(common.refresh)", "refresh": "$t(common.refresh)",
"toggleSmartPlaylistEditor": "$t(entity.smartPlaylist) düzenleyiciye geç", "toggleSmartPlaylistEditor": "$t(entity.smartPlaylist) düzenleyiciye geç"
"addOrRemoveFromSelection": "seçime ekle veya seçimi kaldır",
"selectRangeOfItems": "bir dizi öğe seçin",
"createRadioStation": "$t(entity.radioStation_one) oluştur",
"deleteRadioStation": "$t(entity.radioStation_one) istasyonunu sil",
"selectAll": "tümünü seç",
"downloadStarted": "{{count}} öğenin indirilmesine başlandı",
"moveUp": "yukarı kaydır",
"moveDown": "aşağı kaydır"
}, },
"common": { "common": {
"action_one": "eylem", "action_one": "eylem",
@@ -128,8 +120,7 @@
"trackGain": "parça kazancı", "trackGain": "parça kazancı",
"trackPeak": "parça zirvesi", "trackPeak": "parça zirvesi",
"private": "gizli", "private": "gizli",
"clean": "temiz", "clean": "temiz"
"countSelected": "{{count}} adet seçildi"
}, },
"entity": { "entity": {
"album_one": "albüm", "album_one": "albüm",
@@ -613,6 +604,8 @@
"playButtonBehavior_optionAddNext": "$t(player.addNext)", "playButtonBehavior_optionAddNext": "$t(player.addNext)",
"playButtonBehavior_optionPlay": "$t(player.play)", "playButtonBehavior_optionPlay": "$t(player.play)",
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)", "playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
"playerAlbumArtResolution": "oynatıcı albüm resmi çözünürlüğü",
"playerAlbumArtResolution_description": "büyük oynatıcının albüm resmi önizlemesi için çözünürlük. daha büyük değerler daha net görünmesini sağlar, ancak yüklemeyi yavaşlatabilir. varsayılan değer 0, otomatik olarak çalışır",
"playerbarOpenDrawer": "oynatma çubuğu tam ekran geçişi", "playerbarOpenDrawer": "oynatma çubuğu tam ekran geçişi",
"playerbarOpenDrawer_description": "tam ekran oynatıcıyı açmak için oynatma çubuğuna tıklamaya izin verir", "playerbarOpenDrawer_description": "tam ekran oynatıcıyı açmak için oynatma çubuğuna tıklamaya izin verir",
"remotePassword": "uzaktan kontrol sunucusu şifresi", "remotePassword": "uzaktan kontrol sunucusu şifresi",
+26 -266
View File
@@ -31,13 +31,7 @@
"shuffle": "随机播放", "shuffle": "随机播放",
"shuffleAll": "随机播放全部", "shuffleAll": "随机播放全部",
"shuffleSelected": "随机播放选定的内容", "shuffleSelected": "随机播放选定的内容",
"viewMore": "查看更多", "viewMore": "查看更多"
"addOrRemoveFromSelection": "在所选内容中添加或移除",
"selectRangeOfItems": "批量选择",
"selectAll": "全选",
"createRadioStation": "创建$t(entity.radioStation_one)",
"deleteRadioStation": "删除$t(entity.radioStation_one)",
"openApplicationDirectory": "打开应用程序目录"
}, },
"common": { "common": {
"increase": "增高", "increase": "增高",
@@ -148,9 +142,7 @@
"sort": "排序", "sort": "排序",
"gridRows": "网格行", "gridRows": "网格行",
"tableColumns": "表格列", "tableColumns": "表格列",
"itemsMore": "{{count}} 更多", "itemsMore": "{{count}} 更多"
"countSelected": "已选择{{count}}项",
"retry": "重试"
}, },
"entity": { "entity": {
"albumArtist_other": "专辑艺术家", "albumArtist_other": "专辑艺术家",
@@ -184,10 +176,10 @@
"skip_back": "向后跳过", "skip_back": "向后跳过",
"favorite": "收藏", "favorite": "收藏",
"next": "下一首", "next": "下一首",
"shuffle": "播放(随机)", "shuffle": "随机播放",
"playbackFetchNoResults": "未找到歌曲", "playbackFetchNoResults": "未找到歌曲",
"playbackFetchInProgress": "正在加载歌曲…", "playbackFetchInProgress": "正在加载歌曲…",
"addNext": "下一首播放", "addNext": "添加为播放列表下一首",
"playbackFetchCancel": "请稍等…关闭通知以取消操作", "playbackFetchCancel": "请稍等…关闭通知以取消操作",
"play": "播放", "play": "播放",
"repeat_off": "循环关闭", "repeat_off": "循环关闭",
@@ -197,18 +189,13 @@
"queue_moveToTop": "将所选项移至底部", "queue_moveToTop": "将所选项移至底部",
"queue_moveToBottom": "将所选项移至顶部", "queue_moveToBottom": "将所选项移至顶部",
"shuffle_off": "禁用随机播放", "shuffle_off": "禁用随机播放",
"addLast": "上一曲", "addLast": "添加至播放列表末尾",
"mute": "静音", "mute": "静音",
"skip_forward": "向前跳过", "skip_forward": "向前跳过",
"playbackSpeed": "播放速度", "playbackSpeed": "播放速度",
"pause": "暂停", "pause": "暂停",
"playSimilarSongs": "播放类似的歌曲", "playSimilarSongs": "播放类似的歌曲",
"viewQueue": "查看播放队列", "viewQueue": "查看播放队列"
"saveQueueToServer": "将播放队列保存到服务器",
"restoreQueueFromServer": "从服务器恢复播放队列",
"queueType_default": "默认",
"lyrics": "歌词",
"queueType": "队列类型"
}, },
"setting": { "setting": {
"crossfadeStyle_description": "选择用于音频播放器的淡入淡出风格", "crossfadeStyle_description": "选择用于音频播放器的淡入淡出风格",
@@ -380,6 +367,8 @@
"startMinimized_description": "在系统托盘中启动应用程序", "startMinimized_description": "在系统托盘中启动应用程序",
"passwordStore_description": "使用什么密码/密钥存储。如果您在存储密码时遇到问题,请更改此设置", "passwordStore_description": "使用什么密码/密钥存储。如果您在存储密码时遇到问题,请更改此设置",
"clearCacheSuccess": "缓存清除成功", "clearCacheSuccess": "缓存清除成功",
"playerAlbumArtResolution": "播放器专辑封面分辨率",
"playerAlbumArtResolution_description": "大型播放器专辑封面预览的分辨率。较大使其看起来更清晰,但可能会减慢加载速度。默认为0,表示自动",
"homeConfiguration": "主页配置", "homeConfiguration": "主页配置",
"homeConfiguration_description": "配置主页上显示的项目以及显示顺序", "homeConfiguration_description": "配置主页上显示的项目以及显示顺序",
"passwordStore": "密码/密钥存储", "passwordStore": "密码/密钥存储",
@@ -468,56 +457,7 @@
"exportImportSettings_notValidJSON": "传递的文件不是有效的 JSON 文件", "exportImportSettings_notValidJSON": "传递的文件不是有效的 JSON 文件",
"exportImportSettings_offendingKeyError": "\"{{offendingKey}}\" 不正确 - {{reason}}", "exportImportSettings_offendingKeyError": "\"{{offendingKey}}\" 不正确 - {{reason}}",
"enableAutoTranslation_description": "歌词加载时自动启用翻译", "enableAutoTranslation_description": "歌词加载时自动启用翻译",
"enableAutoTranslation": "启用自动翻译", "enableAutoTranslation": "启用自动翻译"
"imageResolution_description": "程序中使用的图片分辨率,设置为0时使用原始图片",
"artistReleaseTypeConfiguration_description": "配置专辑艺术家页面上显示的发行类型及顺序",
"logLevel_description": "设置显示的最低日志级别。debug显示所有日志,error仅显示错误日志",
"showLyricsInSidebar_description": "在播放列表的附加面板中增加歌词显示页面",
"playerbarSlider_description": "不建议在网络速度较慢或按流量计费情况下使用波形图",
"showVisualizerInSidebar_description": "在播放侧边栏中增加可视化效果",
"analyticsDisable_description": "发送匿名使用数据帮助开发者改进应用程序",
"showRatings_description": "控制是否在界面上显示星级评分",
"followCurrentSong_description": "自动滚动播放列表至当前播放的歌曲",
"audioFadeOnStatusChange_description": "启用音乐淡入和淡出效果",
"combinedLyricsAndVisualizer_description": "将歌词和可视化界面合并到同一面板中",
"queryBuilderCustomFields_description": "在查询构建器添加自定义字段",
"combinedLyricsAndVisualizer": "在播放器侧边栏合并歌词和可视化界面",
"autoDJ_description": "自动添加相似歌曲到队列中",
"notify_description": "歌曲变更时显示通知",
"mpvExtraParameters_description": "向mpv传递额外参数",
"audioFadeOnStatusChange": "音频改变时淡入淡出",
"showVisualizerInSidebar": "在播放器侧边栏显示可视化效果",
"showLyricsInSidebar": "在播放器侧边栏显示歌词",
"analyticsDisable": "退出使用情况的分析",
"artistReleaseTypeConfiguration": "艺术家发行类型设置",
"useThemeAccentColor": "使用主题强调色",
"mpvExtraParameters": "mpv额外参数",
"showRatings": "显示星级评分",
"followCurrentSong": "跟随当前歌曲",
"logLevel": "日志等级",
"playerbarWaveformAlign_optionTop": "顶部对齐",
"playerbarWaveformAlign_optionCenter": "居中对齐",
"playerbarWaveformAlign_optionBottom": "底部对齐",
"queryBuilderCustomFields_inputLabel": "厂牌",
"queryBuilderCustomFields_inputTag": "标签",
"logLevel_optionDebug": "Debug",
"logLevel_optionError": "Error",
"logLevel_optionInfo": "Info",
"logLevel_optionWarn": "Warn",
"imageResolution_optionSidebar": "侧边栏",
"imageResolution_optionHeader": "页首",
"language": "语言",
"notify": "启用歌曲通知",
"imageResolution": "图像分辨率",
"imageResolution_optionTable": "表格",
"imageResolution_optionFullScreenPlayer": "全屏播放器",
"playerbarSlider": "播放进度条",
"playerbarSliderType_optionSlider": "滑块",
"playerbarSliderType_optionWaveform": "波形",
"playerbarWaveformAlign": "波形对齐方式",
"playerbarWaveformBarWidth": "波形宽度",
"playerbarWaveformGap": "波形间距",
"transcode": "启用转码功能"
}, },
"error": { "error": {
"remotePortWarning": "重启服务器使新端口生效", "remotePortWarning": "重启服务器使新端口生效",
@@ -543,12 +483,7 @@
"networkError": "发生网络错误", "networkError": "发生网络错误",
"openError": "无法打开文件", "openError": "无法打开文件",
"badValue": "无效的选项 \"{{value}}\". 此值不再存在", "badValue": "无效的选项 \"{{value}}\". 此值不再存在",
"notificationDenied": "通知权限被拒绝。此设置无效", "notificationDenied": "通知权限被拒绝。此设置无效"
"multipleServerSaveQueueError": "不支持此操作(播放列表中包含来自其他服务器的歌曲)",
"noNetwork": "服务器不可用",
"noNetworkDescription": "无法连接到该服务器",
"saveQueueFailed": "播放列表保存失败",
"settingsSyncError": "渲染器设置与主进程中存在差异,请重启程序以应用更改"
}, },
"filter": { "filter": {
"mostPlayed": "最多播放过", "mostPlayed": "最多播放过",
@@ -571,8 +506,8 @@
"songCount": "歌曲数量", "songCount": "歌曲数量",
"random": "随机", "random": "随机",
"lastPlayed": "上次播放过", "lastPlayed": "上次播放过",
"toYear": "截止年份", "toYear": "年份",
"fromYear": "起始年份", "fromYear": "年份",
"criticRating": "评论家评分", "criticRating": "评论家评分",
"trackNumber": "曲目", "trackNumber": "曲目",
"bpm": "bpm", "bpm": "bpm",
@@ -609,9 +544,7 @@
"artists": "$t(entity.artist_other)", "artists": "$t(entity.artist_other)",
"albumArtists": "$t(entity.albumArtist_other)", "albumArtists": "$t(entity.albumArtist_other)",
"shared": "共享$t(entity.playlist_other)", "shared": "共享$t(entity.playlist_other)",
"myLibrary": "我的媒体库", "myLibrary": "我的媒体库"
"favorites": "$t(entity.favorite_other)",
"radio": "$t(entity.radioStation_other)"
}, },
"fullscreenPlayer": { "fullscreenPlayer": {
"config": { "config": {
@@ -648,10 +581,7 @@
"settings": "$t(common.setting_other)", "settings": "$t(common.setting_other)",
"quit": "$t(common.quit)", "quit": "$t(common.quit)",
"privateModeOff": "关闭私人模式", "privateModeOff": "关闭私人模式",
"privateModeOn": "开启私人模式", "privateModeOn": "开启私人模式"
"multipleMusicFolders": "已选择{{count}}个媒体库",
"noMusicFolder": "未选择任何音乐库",
"selectMusicFolder": "选择媒体库"
}, },
"home": { "home": {
"mostPlayed": "最多播放", "mostPlayed": "最多播放",
@@ -659,8 +589,7 @@
"explore": "从库中搜索", "explore": "从库中搜索",
"recentlyPlayed": "最近播放", "recentlyPlayed": "最近播放",
"title": "$t(common.home)", "title": "$t(common.home)",
"recentlyReleased": "最近发布", "recentlyReleased": "最近发布"
"genres": "$t(entity.genre_other)"
}, },
"albumDetail": { "albumDetail": {
"moreFromArtist": "更多该$t(entity.artist_one)作品", "moreFromArtist": "更多该$t(entity.artist_one)作品",
@@ -672,24 +601,7 @@
"generalTab": "通用", "generalTab": "通用",
"hotkeysTab": "快捷键", "hotkeysTab": "快捷键",
"windowTab": "窗口", "windowTab": "窗口",
"advanced": "高级", "advanced": "高级"
"updates": "更新",
"cache": "缓存",
"analytics": "分析",
"application": "应用",
"theme": "主题",
"controls": "控制",
"sidebar": "侧边栏",
"remote": "远程服务",
"exportImport": "导入/导出",
"scrobble": "播放记录",
"audio": "音频",
"lyrics": "歌词",
"transcoding": "转码",
"discord": "Discord",
"logger": "日志记录器",
"queryBuilder": "查询构建器",
"lyricsDisplay": "歌词显示"
}, },
"globalSearch": { "globalSearch": {
"commands": { "commands": {
@@ -723,9 +635,7 @@
"playShuffled": "$t(player.shuffle)", "playShuffled": "$t(player.shuffle)",
"moveToNext": "$t(action.moveToNext)", "moveToNext": "$t(action.moveToNext)",
"goToAlbum": "转到 $t(entity.album_one)", "goToAlbum": "转到 $t(entity.album_one)",
"goToAlbumArtist": "转到 $t(entity.albumArtist_one)", "goToAlbumArtist": "转到 $t(entity.albumArtist_one)"
"moveItems": "$t(action.moveItems)",
"goTo": "前往"
}, },
"trackList": { "trackList": {
"title": "$t(entity.track_other)", "title": "$t(entity.track_other)",
@@ -757,8 +667,7 @@
"viewAllTracks": "查看所有$t(entity.track_other)", "viewAllTracks": "查看所有$t(entity.track_other)",
"about": "关于{{artist}}", "about": "关于{{artist}}",
"appearsOn": "出现在", "appearsOn": "出现在",
"viewAll": "查看全部", "viewAll": "查看全部"
"groupingTypeAll": "所有发行类型"
}, },
"itemDetail": { "itemDetail": {
"copyPath": "将路径复制到剪贴板", "copyPath": "将路径复制到剪贴板",
@@ -775,12 +684,6 @@
"username": "用户名", "username": "用户名",
"editServerDetailsTooltip": "编辑服务器详细信息", "editServerDetailsTooltip": "编辑服务器详细信息",
"removeServer": "移除服务器" "removeServer": "移除服务器"
},
"favorites": {
"title": "$t(entity.favorite_other)"
},
"folderList": {
"title": "$t(entity.folder_other)"
} }
}, },
"form": { "form": {
@@ -802,10 +705,7 @@
"error_savePassword": "保存密码时出现错误", "error_savePassword": "保存密码时出现错误",
"input_url": "url", "input_url": "url",
"input_preferInstantMixDescription": "仅使用即时混音来获取类似的歌曲。如果您有修改此行为的插件,则很有用", "input_preferInstantMixDescription": "仅使用即时混音来获取类似的歌曲。如果您有修改此行为的插件,则很有用",
"input_preferInstantMix": "首选即时混音", "input_preferInstantMix": "首选即时混音"
"input_preferRemoteUrl": "首选公共 URL",
"input_remoteUrl": "公共 URL",
"input_remoteUrlPlaceholder": "可选:对外功能的公共 URL"
}, },
"addToPlaylist": { "addToPlaylist": {
"success": "添加$t(entity.trackWithCount, {\"count\": {{message}} })到$t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })", "success": "添加$t(entity.trackWithCount, {\"count\": {{message}} })到$t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
@@ -830,17 +730,12 @@
"queryEditor": { "queryEditor": {
"input_optionMatchAll": "匹配全部", "input_optionMatchAll": "匹配全部",
"input_optionMatchAny": "匹配任何", "input_optionMatchAny": "匹配任何",
"title": "查询编辑器", "title": "查询编辑器"
"resetToDefault": "恢复默认值",
"clearFilters": "清除筛选",
"addRuleGroup": "添加规则组",
"removeRuleGroup": "移除规则组"
}, },
"editPlaylist": { "editPlaylist": {
"title": "编辑$t(entity.playlist_one)", "title": "编辑$t(entity.playlist_one)",
"publicJellyfinNote": "Jellyfin 出于某种原因不会显示播放列表是否公开。如果您希望保持公开,请选择以下输入", "publicJellyfinNote": "Jellyfin 出于某种原因不会显示播放列表是否公开。如果您希望保持公开,请选择以下输入",
"success": "$t(entity.playlist_one)更新成功", "success": "$t(entity.playlist_one)更新成功"
"editNote": "不建议对大型播放列表进行手动编辑,你确定接受新播放列表覆盖已有播放列表可能导致的数据丢失风险吗?"
}, },
"lyricSearch": { "lyricSearch": {
"title": "搜索歌词", "title": "搜索歌词",
@@ -859,32 +754,6 @@
"enabled": "启用私人模式,播放状态现在对外部集成隐藏", "enabled": "启用私人模式,播放状态现在对外部集成隐藏",
"disabled": "私人模式已禁用,播放状态现在对启用的外部集成可见", "disabled": "私人模式已禁用,播放状态现在对启用的外部集成可见",
"title": "私人模式" "title": "私人模式"
},
"largeFetchConfirmation": {
"title": "将项目加入到播放列表",
"description": "此操作将添加当前筛选视图中的所有项目"
},
"createRadioStation": {
"input_homepageUrl": "首页地址",
"input_name": "名称",
"input_streamUrl": "串流地址"
},
"lyricsExport": {
"export": "导出歌词",
"input_synced": "导出同步歌词",
"input_offset": "$t(setting.lyricOffset)"
},
"saveQueue": {
"success": "播放列表已保存至服务器"
},
"shuffleAll": {
"title": "随机播放",
"input_genre": "$t(entity.genre_one)",
"input_played_optionAll": "所有曲目",
"input_maxYear": "截止年份",
"input_minYear": "起始年份",
"input_played_optionUnplayed": "仅未播放的曲目",
"input_played_optionPlayed": "仅已播放的曲目"
} }
}, },
"table": { "table": {
@@ -897,28 +766,7 @@
"size": "$t(common.size)", "size": "$t(common.size)",
"itemGap": "项目间隙(px", "itemGap": "项目间隙(px",
"itemSize": "项目大小 (px)", "itemSize": "项目大小 (px)",
"followCurrentSong": "关注当前播放的歌曲", "followCurrentSong": "关注当前播放的歌曲"
"rowHoverHighlight": "鼠标悬停时高亮",
"pagination_itemsPerPage": "每页项目条数",
"itemsPerRow": "每行项目条数",
"pinToRight": "固定到右侧",
"size_default": "默认",
"size_compact": "紧凑",
"size_large": "松散",
"pagination": "分页",
"pagination_infinite": "无限滚动",
"pagination_paginate": "分页式",
"moveUp": "上移",
"moveDown": "下移",
"pinToLeft": "固定在左侧",
"alignLeft": "左对齐",
"alignCenter": "居中对齐",
"alignRight": "右对齐",
"alternateRowColors": "隔行填色",
"advancedSettings": "高级设置",
"autosize": "自动调整大小",
"horizontalBorders": "行边框",
"verticalBorders": "列边框"
}, },
"view": { "view": {
"table": "表格", "table": "表格",
@@ -953,12 +801,7 @@
"albumArtist": "$t(entity.albumArtist_one)", "albumArtist": "$t(entity.albumArtist_one)",
"titleCombined": "$t(common.title)(合并)", "titleCombined": "$t(common.title)(合并)",
"codec": "$t(common.codec)", "codec": "$t(common.codec)",
"songCount": "$t(entity.track_other)", "songCount": "$t(entity.track_other)"
"albumCount": "$t(entity.album_other)",
"image": "图片",
"bitDepth": "$t(common.bitDepth)",
"sampleRate": "$t(common.sampleRate)",
"genreBadge": "$t(entity.genre_one)(徽章)"
} }
}, },
"column": { "column": {
@@ -985,10 +828,7 @@
"channels": "$t(common.channel_other)", "channels": "$t(common.channel_other)",
"discNumber": "碟片", "discNumber": "碟片",
"size": "$t(common.size)", "size": "$t(common.size)",
"codec": "$t(common.codec)", "codec": "$t(common.codec)"
"owner": "所有者",
"bitDepth": "$t(common.bitDepth)",
"sampleRate": "$t(common.sampleRate)"
} }
}, },
"dragDropZone": { "dragDropZone": {
@@ -999,21 +839,7 @@
"releaseType": { "releaseType": {
"primary": { "primary": {
"album": "$t(entity.album_one)", "album": "$t(entity.album_one)",
"broadcast": "播送", "broadcast": "播送"
"ep": "迷你专辑(EP",
"single": "单曲",
"other": "其他"
},
"secondary": {
"audiobook": "有声读物",
"compilation": "合辑",
"demo": "样本唱片(Demo",
"interview": "访谈",
"live": "现场表演(Live",
"mixtape": "混音专辑",
"remix": "再混音(Remix",
"soundtrack": "原声带",
"audioDrama": "广播剧"
} }
}, },
"filterOperator": { "filterOperator": {
@@ -1021,72 +847,6 @@
"afterDate": "晚于(日期)", "afterDate": "晚于(日期)",
"before": "之前", "before": "之前",
"beforeDate": "早于(日期)", "beforeDate": "早于(日期)",
"contains": "包含", "contains": "包含"
"endsWith": "以…结尾",
"inPlaylist": "在…中",
"inTheRange": "在范围内",
"inTheLast": "在最后",
"is": "是",
"isNot": "不是",
"isGreaterThan": "大于",
"isLessThan": "小于",
"matchesRegex": "匹配正则表达式",
"notContains": "不包含",
"startsWith": "以…开头",
"inTheRangeDate": "在(日期)范围内",
"notInPlaylist": "不在…中"
},
"datetime": {
"minuteShort": "分",
"secondShort": "秒",
"hourShort": "小时",
"dayShort": "天"
},
"visualizer": {
"configPasteFailed": "应用配置失败,请检查配置格式。",
"configPasteReadFailed": "读取剪贴板失败",
"configCopyFailed": "复制设置失败",
"configCopied": "已复制设置到剪贴板",
"pasteConfigurationPlaceholder": "将JSON配置粘贴到此处…",
"addCustomGradient": "添加自定义渐变",
"presetNamePlaceholder": "输入预设名称",
"configPasted": "成功应用配置",
"pasteFromClipboard": "从剪贴板粘贴",
"saveAsPreset": "保存为预设",
"customGradients": "自定义渐变",
"showFPS": "显示帧率(FPS",
"presets": "预设",
"general": "普通",
"mode": "模式",
"visualizerType": "可视化器效果类型",
"selectPreset": "选择预设",
"applyPreset": "应用预设",
"updatePreset": "更新预设",
"copyConfiguration": "复制配置",
"pasteConfiguration": "粘贴配置",
"applyConfiguration": "应用配置",
"presetName": "预设名称",
"mode1To8": "模式 1 - 8",
"mode10": "模式 10",
"fillAlpha": "填充透明度",
"lineWidth": "线宽",
"maxFPS": "最大帧率(FPS",
"opacity": "不透明度",
"gradientName": "渐变名称",
"gradientNamePlaceholder": "渐变名称",
"vertical": "垂直",
"horizontal": "水平",
"addColor": "添加颜色",
"position": "位置",
"cycleTime": "循环时间(秒)",
"channelLayout": "声道布局",
"remove": "移除",
"pasteGradientPlaceholder": "在此处粘贴颜色渐变的配置JSON…",
"pasteGradient": "粘贴颜色渐变配置",
"custom": "自定义",
"builtIn": "内置",
"colors": "颜色",
"gradient": "渐变",
"miscellaneousSettings": "杂项设置"
} }
} }
+24 -134
View File
@@ -106,9 +106,7 @@
"explicitStatus": "Explicit狀態", "explicitStatus": "Explicit狀態",
"explicit": "Explicit", "explicit": "Explicit",
"gridRows": "網格行", "gridRows": "網格行",
"noFilters": "未設定任何過濾器", "noFilters": "未設定任何過濾器"
"countSelected": "{{count}}個已選取",
"retry": "重試"
}, },
"error": { "error": {
"endpointNotImplementedError": "{{serverType}} 尚未實現端點 {{endpoint}}", "endpointNotImplementedError": "{{serverType}} 尚未實現端點 {{endpoint}}",
@@ -136,10 +134,7 @@
"notificationDenied": "通知權限被拒絕。此設定無效", "notificationDenied": "通知權限被拒絕。此設定無效",
"openError": "無法開啟檔案", "openError": "無法開啟檔案",
"multipleServerSaveQueueError": "播放佇列中包含不是來自目前伺服器的歌曲,此操作不受支援", "multipleServerSaveQueueError": "播放佇列中包含不是來自目前伺服器的歌曲,此操作不受支援",
"saveQueueFailed": "儲存播放佇列失敗", "saveQueueFailed": "儲存播放佇列失敗"
"settingsSyncError": "偵測到渲染器與主程序之間的設定不一致,請重新啟動應用程式以套用變更",
"noNetwork": "伺服器無法連線",
"noNetworkDescription": "無法連接到此伺服器"
}, },
"page": { "page": {
"contextMenu": { "contextMenu": {
@@ -179,7 +174,7 @@
} }
}, },
"home": { "home": {
"explore": "從媒體庫中搜尋", "explore": "從資料庫中搜尋",
"recentlyPlayed": "最近播放", "recentlyPlayed": "最近播放",
"title": "$t(common.home)", "title": "$t(common.home)",
"mostPlayed": "最多播放", "mostPlayed": "最多播放",
@@ -253,8 +248,7 @@
"discord": "Discord", "discord": "Discord",
"queryBuilder": "查詢建構器", "queryBuilder": "查詢建構器",
"playerFilters": "播放過濾器", "playerFilters": "播放過濾器",
"logger": "日誌記錄器", "logger": "日誌記錄器"
"lyricsDisplay": "歌詞顯示"
}, },
"albumArtistList": { "albumArtistList": {
"title": "$t(entity.albumArtist_other)" "title": "$t(entity.albumArtist_other)"
@@ -286,7 +280,7 @@
"home": "$t(common.home)", "home": "$t(common.home)",
"nowPlaying": "正在播放", "nowPlaying": "正在播放",
"playlists": "$t(entity.playlist_other)", "playlists": "$t(entity.playlist_other)",
"myLibrary": "我的媒體庫", "myLibrary": "我的資料庫",
"shared": "已分享 $t(entity.playlist_other)", "shared": "已分享 $t(entity.playlist_other)",
"favorites": "$t(entity.favorite_other)", "favorites": "$t(entity.favorite_other)",
"radio": "$t(entity.radioStation_other)" "radio": "$t(entity.radioStation_other)"
@@ -305,9 +299,7 @@
"topSongs": "熱門歌曲", "topSongs": "熱門歌曲",
"topSongsFrom": "{{title}} 的熱門歌曲", "topSongsFrom": "{{title}} 的熱門歌曲",
"viewAll": "檢視所有", "viewAll": "檢視所有",
"viewAllTracks": "檢視所有$t(entity.track_other)", "viewAllTracks": "檢視所有$t(entity.track_other)"
"groupingTypeAll": "所有發佈類型",
"groupingTypePrimary": "主要發佈類型"
}, },
"manageServers": { "manageServers": {
"title": "管理伺服器", "title": "管理伺服器",
@@ -375,9 +367,7 @@
"holdToShuffle": "按住以隨機", "holdToShuffle": "按住以隨機",
"lyrics": "歌詞", "lyrics": "歌詞",
"restoreQueueFromServer": "從伺服器還原播放佇列", "restoreQueueFromServer": "從伺服器還原播放佇列",
"saveQueueToServer": "將播放佇列儲存至伺服器", "saveQueueToServer": "將播放佇列儲存至伺服器"
"artistRadio": "藝人電台",
"trackRadio": "曲目電台"
}, },
"setting": { "setting": {
"audioPlayer_description": "選擇用於播放的音訊播放器", "audioPlayer_description": "選擇用於播放的音訊播放器",
@@ -413,7 +403,7 @@
"discordUpdateInterval_description": "更新間隔秒數(至少 15 秒)", "discordUpdateInterval_description": "更新間隔秒數(至少 15 秒)",
"enableRemote": "啟用遠端控制伺服器", "enableRemote": "啟用遠端控制伺服器",
"enableRemote_description": "啟用遠端控制伺服器,以允許其他設備控制此應用程式", "enableRemote_description": "啟用遠端控制伺服器,以允許其他設備控制此應用程式",
"exitToTray": "關閉時到將視窗最小化", "exitToTray": "退出時最小化到系統匣",
"followLyric": "跟隨目前歌詞", "followLyric": "跟隨目前歌詞",
"font_description": "設定應用程式使用的字體", "font_description": "設定應用程式使用的字體",
"fontType": "字體類型", "fontType": "字體類型",
@@ -458,7 +448,7 @@
"lyricOffset": "歌詞偏移(毫秒)", "lyricOffset": "歌詞偏移(毫秒)",
"lyricOffset_description": "將歌詞偏移指定的毫秒數", "lyricOffset_description": "將歌詞偏移指定的毫秒數",
"lyricFetchProvider_description": "選擇歌詞來源。 來源順序即為搜尋的順序", "lyricFetchProvider_description": "選擇歌詞來源。 來源順序即為搜尋的順序",
"minimizeToTray": "最小化到系統匣", "minimizeToTray": "最小化到匣",
"minimizeToTray_description": "將應用程式最小化到系統匣", "minimizeToTray_description": "將應用程式最小化到系統匣",
"minimumScrobbleSeconds": "最小紀錄時間(秒)", "minimumScrobbleSeconds": "最小紀錄時間(秒)",
"minimumScrobbleSeconds_description": "歌曲被記錄為已播放(scrobble)所需的最小播放時間", "minimumScrobbleSeconds_description": "歌曲被記錄為已播放(scrobble)所需的最小播放時間",
@@ -544,8 +534,8 @@
"albumBackground_description": "為包含專輯封面的專輯頁面新增背景圖片", "albumBackground_description": "為包含專輯封面的專輯頁面新增背景圖片",
"albumBackgroundBlur": "專輯背景圖片模糊大小", "albumBackgroundBlur": "專輯背景圖片模糊大小",
"albumBackgroundBlur_description": "調整應用於專輯背景圖片的模糊量", "albumBackgroundBlur_description": "調整應用於專輯背景圖片的模糊量",
"artistConfiguration": "專輯藝頁面設定", "artistConfiguration": "專輯藝術家頁面設定",
"artistConfiguration_description": "設定專輯藝頁面顯示的項目及序", "artistConfiguration_description": "設定專輯藝術家頁面顯示的項目及其顯示順序",
"clearCacheSuccess": "成功清除快取", "clearCacheSuccess": "成功清除快取",
"contextMenu": "右鍵選單配置", "contextMenu": "右鍵選單配置",
"contextMenu_description": "允許您隱藏在右鍵選單項目時顯示的項目。未選取的項目將被隱藏", "contextMenu_description": "允許您隱藏在右鍵選單項目時顯示的項目。未選取的項目將被隱藏",
@@ -561,7 +551,7 @@
"discordServeImage": "從伺服器提供{{discord}}圖片", "discordServeImage": "從伺服器提供{{discord}}圖片",
"discordServeImage_description": "從伺服器本身分享 {{discord}} Rich Presence的封面圖片,僅支援 Jellyfin 與 Navidrome。{{discord}} 會透過機器人擷取圖片,因此您的伺服器必須能從公開網路連線", "discordServeImage_description": "從伺服器本身分享 {{discord}} Rich Presence的封面圖片,僅支援 Jellyfin 與 Navidrome。{{discord}} 會透過機器人擷取圖片,因此您的伺服器必須能從公開網路連線",
"externalLinks": "顯示外部連結", "externalLinks": "顯示外部連結",
"externalLinks_description": "在藝/專輯頁面顯示外部連結(Last.fm, MusicBrainz)", "externalLinks_description": "在藝術家/專輯頁面顯示外部連結(Last.fm, MusicBrainz)",
"preferLocalLyrics": "偏好本地歌詞", "preferLocalLyrics": "偏好本地歌詞",
"preferLocalLyrics_description": "優先選擇本地歌詞,而不是遠端歌詞(如果可用)", "preferLocalLyrics_description": "優先選擇本地歌詞,而不是遠端歌詞(如果可用)",
"homeConfiguration": "首頁配置", "homeConfiguration": "首頁配置",
@@ -571,20 +561,22 @@
"imageAspectRatio": "使用原生封面照長寬比", "imageAspectRatio": "使用原生封面照長寬比",
"imageAspectRatio_description": "如果啟用,封面照將使用其原始長寬比顯示。對於非 1:1 的封面,剩餘空間將為空", "imageAspectRatio_description": "如果啟用,封面照將使用其原始長寬比顯示。對於非 1:1 的封面,剩餘空間將為空",
"lastfm": "顯示 last.fm 連結", "lastfm": "顯示 last.fm 連結",
"lastfm_description": "在藝/專輯頁面顯示 Last.fm 連結", "lastfm_description": "在藝術家/專輯頁面顯示 Last.fm 連結",
"lastfmApiKey": "{{lastfm}} API金鑰", "lastfmApiKey": "{{lastfm}} API金鑰",
"lastfmApiKey_description": "{{lastfm}}的API金鑰。用於封面照", "lastfmApiKey_description": "{{lastfm}}的API金鑰。用於封面照",
"mpvExtraParameters_help": "一行一個", "mpvExtraParameters_help": "一行一個",
"musicbrainz": "顯示 MusicBrainz 連結", "musicbrainz": "顯示 MusicBrainz 連結",
"musicbrainz_description": "在存在 MusicBrainz ID 的藝/專輯頁面上顯示 MusicBrainz 的連結", "musicbrainz_description": "在存在 MusicBrainz ID 的藝術家/專輯頁面上顯示 MusicBrainz 的鏈接",
"neteaseTranslation": "啟用網易翻譯", "neteaseTranslation": "啟用網易翻譯",
"neteaseTranslation_description": "啟用後,將從網易取得並顯示翻譯的歌詞(如果有)", "neteaseTranslation_description": "啟用後,將從網易取得並顯示翻譯的歌詞(如果有)",
"passwordStore": "密碼/secret儲存", "passwordStore": "密碼/secret儲存",
"passwordStore_description": "使用什麼密碼/secret儲存。如果您在儲存密碼時遇到問題,請變更此項目", "passwordStore_description": "使用什麼密碼/secret儲存。如果您在儲存密碼時遇到問題,請變更此項目",
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)", "playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
"playerAlbumArtResolution": "播放器專輯封面解析度",
"playerAlbumArtResolution_description": "大型播放器專輯封面預覽的解析度。較大的解析度使其看起來更清晰,但可能會減慢載入速度。預設為 0,表示自動",
"playerbarOpenDrawer": "播放器列全螢幕切換", "playerbarOpenDrawer": "播放器列全螢幕切換",
"playerbarOpenDrawer_description": "允許點擊播放器列以開啟全螢幕播放器", "playerbarOpenDrawer_description": "允許點擊播放器列以開啟全螢幕播放器",
"startMinimized": "啟動時最小化", "startMinimized": "最小化啟動",
"startMinimized_description": "在系統匣中啟動應用程式", "startMinimized_description": "在系統匣中啟動應用程式",
"transcode_description": "啟用轉碼到不同格式", "transcode_description": "啟用轉碼到不同格式",
"transcodeBitrate": "要轉碼的比特率", "transcodeBitrate": "要轉碼的比特率",
@@ -685,20 +677,7 @@
"logLevel_optionInfo": "Info", "logLevel_optionInfo": "Info",
"logLevel_optionWarn": "Warn", "logLevel_optionWarn": "Warn",
"useThemeAccentColor": "使用主題強調色", "useThemeAccentColor": "使用主題強調色",
"useThemeAccentColor_description": "使用所選主題中定義的主要顏色,而非自訂的強調色", "useThemeAccentColor_description": "使用所選主題中定義的主要顏色,而非自訂的強調色"
"artistRadioCount_description": "設定為藝人電台與曲目電台擷取的歌曲數量",
"imageResolution": "圖片解析度",
"imageResolution_description": "應用程式中所使用圖片的解析度。設定為 0 時,將使用圖片的原始解析度",
"imageResolution_optionTable": "表格",
"imageResolution_optionItemCard": "項目卡片",
"imageResolution_optionSidebar": "側邊欄",
"imageResolution_optionHeader": "頁首",
"imageResolution_optionFullScreenPlayer": "全螢幕播放器",
"combinedLyricsAndVisualizer_description": "將歌詞與視覺化效果整合至同一個面板",
"combinedLyricsAndVisualizer": "在播放器側邊欄整合歌詞與視覺化效果",
"artistRadioCount": "藝人/歌曲電台數量",
"showRatings_description": "控制星級評分功能是否顯示於介面中",
"showRatings": "顯示星級評分"
}, },
"table": { "table": {
"config": { "config": {
@@ -776,7 +755,7 @@
}, },
"column": { "column": {
"album": "專輯", "album": "專輯",
"albumArtist": "專輯藝", "albumArtist": "專輯藝術家",
"albumCount": "$t(entity.album_other)", "albumCount": "$t(entity.album_other)",
"artist": "$t(entity.artist_one)", "artist": "$t(entity.artist_one)",
"biography": "簡介", "biography": "簡介",
@@ -838,17 +817,14 @@
"holdToMoveToTop": "按住以移動至頂部", "holdToMoveToTop": "按住以移動至頂部",
"holdToMoveToBottom": "按住以移動至底部", "holdToMoveToBottom": "按住以移動至底部",
"createRadioStation": "創建 $t(entity.radioStation_one)", "createRadioStation": "創建 $t(entity.radioStation_one)",
"deleteRadioStation": "刪除 $t(entity.radioStation_one)", "deleteRadioStation": "刪除 $t(entity.radioStation_one)"
"openApplicationDirectory": "開啟應用程式目錄",
"addOrRemoveFromSelection": "新增或移除選取項目",
"selectAll": "全選"
}, },
"entity": { "entity": {
"album_other": "專輯", "album_other": "專輯",
"albumArtist_other": "專輯藝", "albumArtist_other": "專輯藝術家",
"albumArtistCount_other": "{{count}} 位專輯藝", "albumArtistCount_other": "{{count}} 位專輯藝術家",
"artist_other": "藝", "artist_other": "藝術家",
"artistWithCount_other": "{{count}} 位藝", "artistWithCount_other": "{{count}} 位藝術家",
"favorite_other": "收藏", "favorite_other": "收藏",
"folder_other": "資料夾", "folder_other": "資料夾",
"folderWithCount_other": "{{count}} 個資料夾", "folderWithCount_other": "{{count}} 個資料夾",
@@ -1008,11 +984,6 @@
}, },
"saveQueue": { "saveQueue": {
"success": "已將播放佇列儲存至伺服器" "success": "已將播放佇列儲存至伺服器"
},
"lyricsExport": {
"export": "匯出歌詞",
"input_synced": "匯出同步歌詞",
"input_offset": "$t(setting.lyricOffset)"
} }
}, },
"releaseType": { "releaseType": {
@@ -1064,86 +1035,5 @@
"notContains": "不包含", "notContains": "不包含",
"notInPlaylist": "不在…之中", "notInPlaylist": "不在…之中",
"startsWith": "以…開頭" "startsWith": "以…開頭"
},
"datetime": {
"minuteShort": "分",
"secondShort": "秒",
"hourShort": "小時",
"dayShort": "天"
},
"visualizer": {
"visualizerType": "視覺化效果類型",
"cyclePresets": "循環切換預設",
"cycleTime": "循環時間 (秒)",
"includeAllPresets": "包含所有預設",
"ignoredPresets": "忽略的預設",
"selectedPresets": "已選取的預設",
"randomizeNextPreset": "隨機切換下一個預設",
"blendTime": "過渡時間",
"presets": "預設",
"selectPreset": "選擇預設",
"applyPreset": "套用預設",
"saveAsPreset": "儲存為預設",
"updatePreset": "更新預設",
"copyConfiguration": "複製設定",
"pasteConfiguration": "貼上設定",
"pasteConfigurationPlaceholder": "在此處貼上JSON設定...",
"pasteFromClipboard": "從剪貼簿貼上",
"applyConfiguration": "套用設定",
"configCopied": "設定已複製至剪貼簿",
"configCopyFailed": "無法複製設定",
"configPasted": "設定套用成功",
"configPasteFailed": "無法套用設定,請檢查格式。",
"configPasteReadFailed": "無法從剪貼簿讀取內容",
"presetName": "預設名稱",
"presetNamePlaceholder": "輸入預設名稱",
"general": "一般",
"mode": "模式",
"mode1To8": "模式 1 - 8",
"mode10": "模式 10",
"barSpace": "柱間距",
"lineWidth": "線條寬度",
"fillAlpha": "填充透明度",
"channelLayout": "聲道佈局",
"maxFPS": "最大幀率",
"opacity": "不透明度",
"customGradients": "自定義漸層",
"addCustomGradient": "新增自定義漸層",
"gradientName": "漸層名稱",
"gradientNamePlaceholder": "漸層名稱",
"vertical": "垂直",
"horizontal": "水平",
"colorStops": "顏色分界點",
"addColor": "新增顏色",
"position": "位置",
"remove": "移除",
"custom": "自訂",
"builtIn": "內建",
"colors": "顏色",
"colorMode": "顏色模式",
"gradient": "漸層",
"gradientLeft": "左側漸層",
"gradientRight": "右側漸層",
"fft": "FFT",
"fftSize": "FFT 取樣大小",
"smoothing": "平滑度",
"frequencyRangeAndScaling": "頻率範圍與縮放",
"minimumFrequency": "最低頻率",
"maximumFrequency": "最高頻率",
"frequencyScale": "頻率量表",
"sensitivity": "靈敏度",
"weightingFilter": "權重濾波器",
"minimumDecibels": "最小分貝",
"maximumDecibels": "最大分貝",
"linearAmplitude": "線性振幅",
"linearBoost": "線性增益",
"peakBehavior": "峰值行為",
"showPeaks": "顯示峰值",
"fadePeaks": "峰值淡出",
"peakLine": "峰值線條",
"gravity": "重力",
"peakFadeTime": "峰值淡出時間 (毫秒)",
"peakHoldTime": "峰值停留時間 (毫秒)",
"radialSpectrum": "圓形頻譜"
} }
} }
-3
View File
@@ -657,9 +657,6 @@ if (mprisPlayer) {
} }
currentState.volume = volume; currentState.volume = volume;
broadcast({ data: volume, event: 'volume' }); broadcast({ data: volume, event: 'volume' });
getMainWindow()?.webContents.send('request-volume', {
volume,
});
}); });
} }
+1 -31
View File
@@ -3,40 +3,10 @@ import type { TitleTheme } from '/@/shared/types/types';
import { dialog, ipcMain, nativeTheme, OpenDialogOptions, safeStorage } from 'electron'; import { dialog, ipcMain, nativeTheme, OpenDialogOptions, safeStorage } from 'electron';
import Store from 'electron-store'; import Store from 'electron-store';
const getFrame = () => { export const store = new Store({
const isWindows = process.platform === 'win32';
const isMacOS = process.platform === 'darwin';
if (isWindows) {
return 'windows';
}
if (isMacOS) {
return 'macOS';
}
return 'linux';
};
export const store = new Store<any>({
beforeEachMigration: (_store, context) => { beforeEachMigration: (_store, context) => {
console.log(`settings migrate from ${context.fromVersion}${context.toVersion}`); console.log(`settings migrate from ${context.fromVersion}${context.toVersion}`);
}, },
defaults: {
disable_auto_updates: false,
enableNeteaseTranslation: false,
global_media_hotkeys: true,
lyrics: ['NetEase', 'lrclib.net'],
mediaSession: false,
playbackType: 'web',
should_prompt_accessibility: true,
shown_accessibility_warning: false,
window_enable_tray: true,
window_exit_to_tray: false,
window_minimize_to_tray: false,
window_start_minimized: false,
window_window_bar_style: getFrame(),
},
migrations: { migrations: {
'>=0.21.2': (store) => { '>=0.21.2': (store) => {
store.set('window_bar_style', 'linux'); store.set('window_bar_style', 'linux');
+42 -42
View File
@@ -141,48 +141,48 @@ ipcMain.on('update-shuffle', (_event, shuffle: boolean) => {
mprisPlayer.shuffle = shuffle; mprisPlayer.shuffle = shuffle;
}); });
ipcMain.on( ipcMain.on('update-song', (_event, song: QueueSong | undefined) => {
'update-song', try {
(_event, song: QueueSong | undefined, imageUrl: null | string | undefined) => { if (!song?.id) {
try { mprisPlayer.metadata = {};
if (!song?.id) { return;
mprisPlayer.metadata = {};
return;
}
mprisPlayer.metadata = {
'mpris:artUrl': imageUrl || null,
'mpris:length': song.duration ? Math.round((song.duration || 0) * 1e3) : null,
'mpris:trackid': song.id
? mprisPlayer.objectPath(`track/${song.id?.replace('-', '')}`)
: '',
'xesam:album': song.album || null,
'xesam:albumArtist': song.albumArtists?.length
? song.albumArtists.map((artist) => artist.name)
: null,
'xesam:artist': song.artists?.length
? song.artists.map((artist) => artist.name)
: null,
'xesam:audioBpm': song.bpm,
// Comment is a `list of strings` type
'xesam:comment': song.comment ? [song.comment] : null,
'xesam:contentCreated': song.releaseDate,
'xesam:discNumber': song.discNumber ? song.discNumber : null,
'xesam:genre': song.genres?.length
? song.genres.map((genre: any) => genre.name)
: null,
'xesam:lastUsed': song.lastPlayedAt,
'xesam:title': song.name || null,
'xesam:trackNumber': song.trackNumber ? song.trackNumber : null,
'xesam:useCount':
song.playCount !== null && song.playCount !== undefined ? song.playCount : null,
// User ratings are only on Navidrome/Subsonic and are on a scale of 1-5
'xesam:userRating': song.userRating ? song.userRating / 5 : null,
};
} catch (err) {
console.error(err);
} }
},
); const upsizedImageUrl = song.imageUrl
? song.imageUrl
?.replace(/&size=\d+/, '&size=300')
.replace(/\?width=\d+/, '?width=300')
.replace(/&height=\d+/, '&height=300')
: null;
mprisPlayer.metadata = {
'mpris:artUrl': upsizedImageUrl,
'mpris:length': song.duration ? Math.round((song.duration || 0) * 1e3) : null,
'mpris:trackid': song.id
? mprisPlayer.objectPath(`track/${song.id?.replace('-', '')}`)
: '',
'xesam:album': song.album || null,
'xesam:albumArtist': song.albumArtists?.length
? song.albumArtists.map((artist) => artist.name)
: null,
'xesam:artist': song.artists?.length ? song.artists.map((artist) => artist.name) : null,
'xesam:audioBpm': song.bpm,
// Comment is a `list of strings` type
'xesam:comment': song.comment ? [song.comment] : null,
'xesam:contentCreated': song.releaseDate,
'xesam:discNumber': song.discNumber ? song.discNumber : null,
'xesam:genre': song.genres?.length ? song.genres.map((genre: any) => genre.name) : null,
'xesam:lastUsed': song.lastPlayedAt,
'xesam:title': song.name || null,
'xesam:trackNumber': song.trackNumber ? song.trackNumber : null,
'xesam:useCount':
song.playCount !== null && song.playCount !== undefined ? song.playCount : null,
// User ratings are only on Navidrome/Subsonic and are on a scale of 1-5
'xesam:userRating': song.userRating ? song.userRating / 5 : null,
};
} catch (err) {
console.error(err);
}
});
export { mprisPlayer }; export { mprisPlayer };
+1 -5
View File
@@ -30,7 +30,6 @@ import MenuBuilder from './menu';
import { import {
autoUpdaterLogInterface, autoUpdaterLogInterface,
createLog, createLog,
disableAutoUpdates,
hotkeyToElectronAccelerator, hotkeyToElectronAccelerator,
isLinux, isLinux,
isMacOS, isMacOS,
@@ -457,7 +456,7 @@ async function createWindow(first = true): Promise<void> {
return { action: 'deny' }; return { action: 'deny' };
}); });
if (!disableAutoUpdates() && store.get('disable_auto_updates') !== true) { if (store.get('disable_auto_updates') !== true) {
new AppUpdater(); new AppUpdater();
} }
@@ -497,9 +496,6 @@ if (shouldDisableMediaFeatures) {
// https://github.com/electron/electron/issues/46538#issuecomment-2808806722 // https://github.com/electron/electron/issues/46538#issuecomment-2808806722
app.commandLine.appendSwitch('gtk-version', '3'); app.commandLine.appendSwitch('gtk-version', '3');
// Enable garbage collection API
app.commandLine.appendSwitch('js-flags', '--expose-gc');
// Must duplicate with the one in renderer process settings.store.ts // Must duplicate with the one in renderer process settings.store.ts
enum BindingActions { enum BindingActions {
GLOBAL_SEARCH = 'globalSearch', GLOBAL_SEARCH = 'globalSearch',
-4
View File
@@ -18,10 +18,6 @@ if (process.env.NODE_ENV === 'development') {
}; };
} }
export const disableAutoUpdates = () => {
return process.env['DISABLE_AUTO_UPDATES'];
};
export const isMacOS = () => { export const isMacOS = () => {
return process.platform === 'darwin'; return process.platform === 'darwin';
}; };
-1
View File
@@ -6,7 +6,6 @@ declare global {
interface Window { interface Window {
api: PreloadApi; api: PreloadApi;
electron: ElectronAPI; electron: ElectronAPI;
LEGACY_AUTHENTICATION?: boolean;
queryLocalFonts?: () => Promise<Font[]>; queryLocalFonts?: () => Promise<Font[]>;
SERVER_LOCK?: boolean; SERVER_LOCK?: boolean;
SERVER_NAME?: string; SERVER_NAME?: string;
-4
View File
@@ -78,10 +78,6 @@ export const toServerType = (value?: string): null | string => {
const SERVER_TYPE = toServerType(process.env.SERVER_TYPE); const SERVER_TYPE = toServerType(process.env.SERVER_TYPE);
const env = { const env = {
LEGACY_AUTHENTICATION:
SERVER_TYPE !== null
? process.env.LEGACY_AUTHENTICATION?.toLocaleLowerCase() === 'true'
: false,
SERVER_LOCK: SERVER_LOCK:
SERVER_TYPE !== null ? process.env.SERVER_LOCK?.toLocaleLowerCase() === 'true' : false, SERVER_TYPE !== null ? process.env.SERVER_LOCK?.toLocaleLowerCase() === 'true' : false,
SERVER_NAME: process.env.SERVER_NAME ?? '', SERVER_NAME: process.env.SERVER_NAME ?? '',
+2 -7
View File
@@ -27,8 +27,8 @@ const updateShuffle = (shuffle: boolean) => {
ipcRenderer.send('update-shuffle', shuffle); ipcRenderer.send('update-shuffle', shuffle);
}; };
const updateSong = (song: QueueSong | undefined, imageUrl?: null | string) => { const updateSong = (song: QueueSong | undefined) => {
ipcRenderer.send('update-song', song, imageUrl); ipcRenderer.send('update-song', song);
}; };
const requestSeek = (cb: (event: IpcRendererEvent, data: { offset: number }) => void) => { const requestSeek = (cb: (event: IpcRendererEvent, data: { offset: number }) => void) => {
@@ -51,16 +51,11 @@ const requestToggleShuffle = (
ipcRenderer.on('mpris-request-toggle-shuffle', cb); ipcRenderer.on('mpris-request-toggle-shuffle', cb);
}; };
const requestVolume = (cb: (event: IpcRendererEvent, data: { volume: number }) => void) => {
ipcRenderer.on('request-volume', cb);
};
export const mpris = { export const mpris = {
requestPosition, requestPosition,
requestSeek, requestSeek,
requestToggleRepeat, requestToggleRepeat,
requestToggleShuffle, requestToggleShuffle,
requestVolume,
updatePosition, updatePosition,
updateRepeat, updateRepeat,
updateSeek, updateSeek,
+2 -22
View File
@@ -1,6 +1,6 @@
import { ipcRenderer, IpcRendererEvent, webFrame } from 'electron'; import { ipcRenderer, IpcRendererEvent } from 'electron';
import { disableAutoUpdates, isLinux, isMacOS, isWindows } from '../main/utils'; import { isLinux, isMacOS, isWindows } from '../main/utils';
const openItem = async (path: string) => { const openItem = async (path: string) => {
return ipcRenderer.invoke('open-item', path); return ipcRenderer.invoke('open-item', path);
@@ -39,28 +39,8 @@ const download = (url: string) => {
ipcRenderer.send('download-url', url); ipcRenderer.send('download-url', url);
}; };
const forceGarbageCollection = (): boolean => {
try {
if (typeof global.gc === 'function') {
global.gc();
webFrame.clearCache();
return true;
}
if (typeof window.gc === 'function') {
window.gc();
webFrame.clearCache();
return true;
}
return false;
} catch {
return false;
}
};
export const utils = { export const utils = {
disableAutoUpdates,
download, download,
forceGarbageCollection,
isLinux, isLinux,
isMacOS, isMacOS,
isWindows, isWindows,
+95 -178
View File
@@ -3,7 +3,7 @@ import { JellyfinController } from '/@/renderer/api/jellyfin/jellyfin-controller
import { NavidromeController } from '/@/renderer/api/navidrome/navidrome-controller'; import { NavidromeController } from '/@/renderer/api/navidrome/navidrome-controller';
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller'; import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
import { mergeMusicFolderId } from '/@/renderer/api/utils-music-folder'; import { mergeMusicFolderId } from '/@/renderer/api/utils-music-folder';
import { getServerById, useAuthStore, useSettingsStore } from '/@/renderer/store'; import { getServerById, useAuthStore } from '/@/renderer/store';
import { toast } from '/@/shared/components/toast/toast'; import { toast } from '/@/shared/components/toast/toast';
import { import {
AuthenticationResponse, AuthenticationResponse,
@@ -60,22 +60,6 @@ const apiController = <K extends keyof ControllerEndpoint>(
return controllerFn; return controllerFn;
}; };
const getPathReplaceSettings = () => {
const { pathReplace, pathReplaceWith } = useSettingsStore.getState().general;
return { pathReplace, pathReplaceWith };
};
const addContext = <T extends { apiClientProps: any; context?: any }>(args: T): T => {
const pathSettings = getPathReplaceSettings();
return {
...args,
context: {
...(args.context || {}),
...pathSettings,
},
};
};
export interface GeneralController extends Omit<Required<ControllerEndpoint>, 'authenticate'> { export interface GeneralController extends Omit<Required<ControllerEndpoint>, 'authenticate'> {
authenticate: ( authenticate: (
url: string, url: string,
@@ -97,7 +81,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'addToPlaylist', 'addToPlaylist',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
authenticate(url, body, type) { authenticate(url, body, type) {
return apiController('authenticate', type)(url, body); return apiController('authenticate', type)(url, body);
@@ -114,7 +98,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'createFavorite', 'createFavorite',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
createInternetRadioStation(args) { createInternetRadioStation(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -128,7 +112,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'createInternetRadioStation', 'createInternetRadioStation',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
createPlaylist(args) { createPlaylist(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -142,7 +126,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'createPlaylist', 'createPlaylist',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
deleteFavorite(args) { deleteFavorite(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -156,7 +140,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'deleteFavorite', 'deleteFavorite',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
deleteInternetRadioStation(args) { deleteInternetRadioStation(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -170,7 +154,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'deleteInternetRadioStation', 'deleteInternetRadioStation',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
deletePlaylist(args) { deletePlaylist(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -184,7 +168,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'deletePlaylist', 'deletePlaylist',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
getAlbumArtistDetail(args) { getAlbumArtistDetail(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -198,7 +182,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getAlbumArtistDetail', 'getAlbumArtistDetail',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
getAlbumArtistList(args) { getAlbumArtistList(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -212,13 +196,11 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getAlbumArtistList', 'getAlbumArtistList',
server.type, server.type,
)?.( )?.({
addContext({ ...args,
...args, apiClientProps: { ...args.apiClientProps, server },
apiClientProps: { ...args.apiClientProps, server }, query: mergeMusicFolderId(args.query, server),
query: mergeMusicFolderId(args.query, server), });
}),
);
}, },
getAlbumArtistListCount(args) { getAlbumArtistListCount(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -232,13 +214,11 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getAlbumArtistListCount', 'getAlbumArtistListCount',
server.type, server.type,
)?.( )?.({
addContext({ ...args,
...args, apiClientProps: { ...args.apiClientProps, server },
apiClientProps: { ...args.apiClientProps, server }, query: mergeMusicFolderId(args.query, server),
query: mergeMusicFolderId(args.query, server), });
}),
);
}, },
getAlbumDetail(args) { getAlbumDetail(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -252,7 +232,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getAlbumDetail', 'getAlbumDetail',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
getAlbumInfo(args) { getAlbumInfo(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -266,7 +246,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getAlbumInfo', 'getAlbumInfo',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
getAlbumList(args) { getAlbumList(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -280,13 +260,11 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getAlbumList', 'getAlbumList',
server.type, server.type,
)?.( )?.({
addContext({ ...args,
...args, apiClientProps: { ...args.apiClientProps, server },
apiClientProps: { ...args.apiClientProps, server }, query: mergeMusicFolderId(args.query, server),
query: mergeMusicFolderId(args.query, server), });
}),
);
}, },
getAlbumListCount(args) { getAlbumListCount(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -300,13 +278,11 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getAlbumListCount', 'getAlbumListCount',
server.type, server.type,
)?.( )?.({
addContext({ ...args,
...args, apiClientProps: { ...args.apiClientProps, server },
apiClientProps: { ...args.apiClientProps, server }, query: mergeMusicFolderId(args.query, server),
query: mergeMusicFolderId(args.query, server), });
}),
);
}, },
getArtistList(args) { getArtistList(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -320,13 +296,11 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getArtistList', 'getArtistList',
server.type, server.type,
)?.( )?.({
addContext({ ...args,
...args, apiClientProps: { ...args.apiClientProps, server },
apiClientProps: { ...args.apiClientProps, server }, query: mergeMusicFolderId(args.query, server),
query: mergeMusicFolderId(args.query, server), });
}),
);
}, },
getArtistListCount(args) { getArtistListCount(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -340,27 +314,11 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getArtistListCount', 'getArtistListCount',
server.type, server.type,
)?.( )?.({
addContext({ ...args,
...args, apiClientProps: { ...args.apiClientProps, server },
apiClientProps: { ...args.apiClientProps, server }, query: mergeMusicFolderId(args.query, server),
query: mergeMusicFolderId(args.query, server), });
}),
);
},
getArtistRadio(args) {
const server = getServerById(args.apiClientProps.serverId);
if (!server) {
throw new Error(
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getArtistRadio`,
);
}
return apiController(
'getArtistRadio',
server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
}, },
getDownloadUrl(args) { getDownloadUrl(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -374,7 +332,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getDownloadUrl', 'getDownloadUrl',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
getFolder(args) { getFolder(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -388,13 +346,11 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getFolder', 'getFolder',
server.type, server.type,
)?.( )?.({
addContext({ ...args,
...args, apiClientProps: { ...args.apiClientProps, server },
apiClientProps: { ...args.apiClientProps, server }, query: mergeMusicFolderId(args.query, server),
query: mergeMusicFolderId(args.query, server), });
}),
);
}, },
getGenreList(args) { getGenreList(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -408,32 +364,11 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getGenreList', 'getGenreList',
server.type, server.type,
)?.( )?.({
addContext({ ...args,
...args, apiClientProps: { ...args.apiClientProps, server },
apiClientProps: { ...args.apiClientProps, server }, query: mergeMusicFolderId(args.query, server),
query: mergeMusicFolderId(args.query, server), });
}),
);
},
getImageUrl(args) {
const server = getServerById(args.apiClientProps.serverId);
if (!server) {
return null;
}
return (
apiController(
'getImageUrl',
server.type,
)?.(
addContext({
...args,
apiClientProps: { ...args.apiClientProps, server },
}),
) || null
);
}, },
getInternetRadioStations(args) { getInternetRadioStations(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -446,7 +381,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getInternetRadioStations', 'getInternetRadioStations',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
getLyrics(args) { getLyrics(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -460,7 +395,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getLyrics', 'getLyrics',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
getMusicFolderList(args) { getMusicFolderList(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -474,7 +409,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getMusicFolderList', 'getMusicFolderList',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
getPlaylistDetail(args) { getPlaylistDetail(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -488,7 +423,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getPlaylistDetail', 'getPlaylistDetail',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
getPlaylistList(args) { getPlaylistList(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -502,7 +437,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getPlaylistList', 'getPlaylistList',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
getPlaylistListCount(args) { getPlaylistListCount(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -516,7 +451,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getPlaylistListCount', 'getPlaylistListCount',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
getPlaylistSongList(args) { getPlaylistSongList(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -530,7 +465,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getPlaylistSongList', 'getPlaylistSongList',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
getPlayQueue(args) { getPlayQueue(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -544,7 +479,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getPlayQueue', 'getPlayQueue',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
getRandomSongList(args) { getRandomSongList(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -558,13 +493,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getRandomSongList', 'getRandomSongList',
server.type, server.type,
)?.( )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
addContext({
...args,
apiClientProps: { ...args.apiClientProps, server },
query: mergeMusicFolderId(args.query, server),
}),
);
}, },
getRoles(args) { getRoles(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -578,7 +507,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getRoles', 'getRoles',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
getServerInfo(args) { getServerInfo(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -592,7 +521,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getServerInfo', 'getServerInfo',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
getSimilarSongs(args) { getSimilarSongs(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -606,13 +535,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getSimilarSongs', 'getSimilarSongs',
server.type, server.type,
)?.( )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
addContext({
...args,
apiClientProps: { ...args.apiClientProps, server },
query: mergeMusicFolderId(args.query, server),
}),
);
}, },
getSongDetail(args) { getSongDetail(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -626,7 +549,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getSongDetail', 'getSongDetail',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
getSongList(args) { getSongList(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -640,13 +563,11 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getSongList', 'getSongList',
server.type, server.type,
)?.( )?.({
addContext({ ...args,
...args, apiClientProps: { ...args.apiClientProps, server },
apiClientProps: { ...args.apiClientProps, server }, query: mergeMusicFolderId(args.query, server),
query: mergeMusicFolderId(args.query, server), });
}),
);
}, },
getSongListCount(args) { getSongListCount(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -660,13 +581,11 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getSongListCount', 'getSongListCount',
server.type, server.type,
)?.( )?.({
addContext({ ...args,
...args, apiClientProps: { ...args.apiClientProps, server },
apiClientProps: { ...args.apiClientProps, server }, query: mergeMusicFolderId(args.query, server),
query: mergeMusicFolderId(args.query, server), });
}),
);
}, },
getStreamUrl(args) { getStreamUrl(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -678,7 +597,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getStreamUrl', 'getStreamUrl',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
getStructuredLyrics(args) { getStructuredLyrics(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -692,7 +611,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getStructuredLyrics', 'getStructuredLyrics',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
getTagList(args) { getTagList(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -706,7 +625,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getTagList', 'getTagList',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
getTopSongs(args) { getTopSongs(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -720,7 +639,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getTopSongs', 'getTopSongs',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
getUserInfo(args) { getUserInfo(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -734,7 +653,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getUserInfo', 'getUserInfo',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
getUserList(args) { getUserList(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -748,7 +667,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'getUserList', 'getUserList',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
movePlaylistItem(args) { movePlaylistItem(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -762,7 +681,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'movePlaylistItem', 'movePlaylistItem',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
removeFromPlaylist(args) { removeFromPlaylist(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -776,7 +695,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'removeFromPlaylist', 'removeFromPlaylist',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
replacePlaylist(args) { replacePlaylist(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -790,7 +709,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'replacePlaylist', 'replacePlaylist',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
savePlayQueue(args) { savePlayQueue(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -804,7 +723,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'savePlayQueue', 'savePlayQueue',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
scrobble(args) { scrobble(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -818,7 +737,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'scrobble', 'scrobble',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
search(args) { search(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -832,13 +751,11 @@ export const controller: GeneralController = {
return apiController( return apiController(
'search', 'search',
server.type, server.type,
)?.( )?.({
addContext({ ...args,
...args, apiClientProps: { ...args.apiClientProps, server },
apiClientProps: { ...args.apiClientProps, server }, query: mergeMusicFolderId(args.query, server),
query: mergeMusicFolderId(args.query, server), });
}),
);
}, },
setRating(args) { setRating(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -852,7 +769,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'setRating', 'setRating',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
shareItem(args) { shareItem(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -866,7 +783,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'shareItem', 'shareItem',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
updateInternetRadioStation(args) { updateInternetRadioStation(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -880,7 +797,7 @@ export const controller: GeneralController = {
return apiController( return apiController(
'updateInternetRadioStation', 'updateInternetRadioStation',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
updatePlaylist(args) { updatePlaylist(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -894,6 +811,6 @@ export const controller: GeneralController = {
return apiController( return apiController(
'updatePlaylist', 'updatePlaylist',
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
}, },
}; };
+1 -3
View File
@@ -9,7 +9,6 @@ import packageJson from '../../../../package.json';
import i18n from '/@/i18n/i18n'; import i18n from '/@/i18n/i18n';
import { authenticationFailure } from '/@/renderer/api/utils'; import { authenticationFailure } from '/@/renderer/api/utils';
import { useAuthStore } from '/@/renderer/store'; import { useAuthStore } from '/@/renderer/store';
import { getServerUrl } from '/@/renderer/utils/normalize-server-url';
import { jfType } from '/@/shared/api/jellyfin/jellyfin-types'; import { jfType } from '/@/shared/api/jellyfin/jellyfin-types';
import { getClientType } from '/@/shared/api/utils'; import { getClientType } from '/@/shared/api/utils';
import { ServerListItemWithCredential } from '/@/shared/types/domain-types'; import { ServerListItemWithCredential } from '/@/shared/types/domain-types';
@@ -409,8 +408,7 @@ export const jfApiClient = (args: {
const { params, path: api } = parsePath(path); const { params, path: api } = parsePath(path);
if (server) { if (server) {
const serverUrl = getServerUrl(server); baseUrl = `${server?.url}`;
baseUrl = serverUrl;
token = server?.credential; token = server?.credential;
} else { } else {
baseUrl = url; baseUrl = url;
+34 -140
View File
@@ -6,7 +6,6 @@ import { z } from 'zod';
import { jfApiClient } from '/@/renderer/api/jellyfin/jellyfin-api'; import { jfApiClient } from '/@/renderer/api/jellyfin/jellyfin-api';
import { useRadioStore } from '/@/renderer/features/radio/store/radio-store'; import { useRadioStore } from '/@/renderer/features/radio/store/radio-store';
import { getServerUrl } from '/@/renderer/utils/normalize-server-url';
import { jfNormalize } from '/@/shared/api/jellyfin/jellyfin-normalize'; import { jfNormalize } from '/@/shared/api/jellyfin/jellyfin-normalize';
import { JFSongListSort, JFSortOrder, jfType } from '/@/shared/api/jellyfin/jellyfin-types'; import { JFSongListSort, JFSortOrder, jfType } from '/@/shared/api/jellyfin/jellyfin-types';
import { getFeatures, hasFeature, sortSongList, VersionInfo } from '/@/shared/api/utils'; import { getFeatures, hasFeature, sortSongList, VersionInfo } from '/@/shared/api/utils';
@@ -218,25 +217,24 @@ export const JellyfinController: InternalControllerEndpoint = {
throw new Error('No userId found'); throw new Error('No userId found');
} }
const [res, similarArtistsRes] = await Promise.all([ const res = await jfApiClient(apiClientProps).getAlbumArtistDetail({
jfApiClient(apiClientProps).getAlbumArtistDetail({ params: {
params: { id: query.id,
id: query.id, userId: apiClientProps.server?.userId,
userId: apiClientProps.server?.userId, },
}, query: {
query: { Fields: 'Genres, Overview',
Fields: 'Genres, Overview', },
}, });
}),
jfApiClient(apiClientProps).getSimilarArtistList({ const similarArtistsRes = await jfApiClient(apiClientProps).getSimilarArtistList({
params: { params: {
id: query.id, id: query.id,
}, },
query: { query: {
Limit: 10, Limit: 10,
}, },
}), });
]);
if (res.status !== 200 || similarArtistsRes.status !== 200) { if (res.status !== 200 || similarArtistsRes.status !== 200) {
throw new Error('Failed to get album artist detail'); throw new Error('Failed to get album artist detail');
@@ -362,7 +360,7 @@ export const JellyfinController: InternalControllerEndpoint = {
}, },
query: { query: {
...artistQuery, ...artistQuery,
Fields: 'People, Tags, Studios', Fields: 'People, Tags',
GenreIds: query.genreIds ? query.genreIds.join(',') : undefined, GenreIds: query.genreIds ? query.genreIds.join(',') : undefined,
IncludeItemTypes: 'MusicAlbum', IncludeItemTypes: 'MusicAlbum',
IsFavorite: query.favorite, IsFavorite: query.favorite,
@@ -428,38 +426,10 @@ export const JellyfinController: InternalControllerEndpoint = {
apiClientProps, apiClientProps,
query: { ...query, limit: 1, startIndex: 0 }, query: { ...query, limit: 1, startIndex: 0 },
}).then((result) => result!.totalRecordCount!), }).then((result) => result!.totalRecordCount!),
getArtistRadio: async (args) => {
const { apiClientProps, query } = args;
// For Jellyfin, use instant mix for artist radio
const res = await jfApiClient(apiClientProps).getInstantMix({
params: {
itemId: query.artistId,
},
query: {
Fields: 'Genres, DateCreated, MediaSources, ParentId',
Limit: query.count,
UserId: apiClientProps.server?.userId || undefined,
},
});
if (res.status !== 200) {
throw new Error('Failed to get artist radio songs');
}
return res.body.Items.map((song) =>
jfNormalize.song(
song,
apiClientProps.server,
args.context?.pathReplace,
args.context?.pathReplaceWith,
),
);
},
getDownloadUrl: (args) => { getDownloadUrl: (args) => {
const { apiClientProps, query } = args; const { apiClientProps, query } = args;
return `${apiClientProps.server?.url}/items/${query.id}/download?apiKey=${apiClientProps.server?.credential}`; return `${apiClientProps.server?.url}/items/${query.id}/download?api_key=${apiClientProps.server?.credential}`;
}, },
getFolder: async ({ apiClientProps, query }) => { getFolder: async ({ apiClientProps, query }) => {
const userId = apiClientProps.server?.userId; const userId = apiClientProps.server?.userId;
@@ -700,23 +670,6 @@ export const JellyfinController: InternalControllerEndpoint = {
totalRecordCount: res.body?.TotalRecordCount || 0, totalRecordCount: res.body?.TotalRecordCount || 0,
}; };
}, },
getImageUrl: ({ apiClientProps: { server }, baseUrl, query }) => {
const { id, size } = query;
const imageSize = size;
const url = baseUrl || getServerUrl(server);
if (!url) {
return null;
}
// For Jellyfin, we construct the URL pattern
// The server will return a 404 or placeholder if no image exists
const imageUrl = `${url}/Items/${id}/Images/Primary?quality=96${imageSize ? `&width=${imageSize}` : ''}`;
// For songs, we might want to fall back to album art, but we don't have albumId here
// The caller can handle this if needed
return imageUrl;
},
getInternetRadioStations: async (args) => { getInternetRadioStations: async (args) => {
const { apiClientProps } = args; const { apiClientProps } = args;
@@ -865,14 +818,7 @@ export const JellyfinController: InternalControllerEndpoint = {
} }
return { return {
items: res.body.Items.map((item) => items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server)),
jfNormalize.song(
item,
apiClientProps.server,
args.context?.pathReplace,
args.context?.pathReplaceWith,
),
),
startIndex: 0, startIndex: 0,
totalRecordCount: res.body.TotalRecordCount, totalRecordCount: res.body.TotalRecordCount,
}; };
@@ -925,14 +871,7 @@ export const JellyfinController: InternalControllerEndpoint = {
} }
return { return {
items: res.body.Items.map((item) => items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server)),
jfNormalize.song(
item,
apiClientProps.server,
args.context?.pathReplace,
args.context?.pathReplaceWith,
),
),
startIndex: 0, startIndex: 0,
totalRecordCount: res.body.Items.length || 0, totalRecordCount: res.body.Items.length || 0,
}; };
@@ -947,12 +886,7 @@ export const JellyfinController: InternalControllerEndpoint = {
throw new Error('Failed to get server info'); throw new Error('Failed to get server info');
} }
const defaultFeatures = {}; const features = getFeatures(VERSION_INFO, res.body.Version);
const features = {
...defaultFeatures,
...getFeatures(VERSION_INFO, res.body.Version),
};
return { return {
features, features,
@@ -982,14 +916,7 @@ export const JellyfinController: InternalControllerEndpoint = {
if (res.status === 200 && res.body.Items.length) { if (res.status === 200 && res.body.Items.length) {
const results = res.body.Items.reduce<Song[]>((acc, song) => { const results = res.body.Items.reduce<Song[]>((acc, song) => {
if (song.Id !== query.songId) { if (song.Id !== query.songId) {
acc.push( acc.push(jfNormalize.song(song, apiClientProps.server));
jfNormalize.song(
song,
apiClientProps.server,
args.context?.pathReplace,
args.context?.pathReplaceWith,
),
);
} }
return acc; return acc;
@@ -1018,14 +945,7 @@ export const JellyfinController: InternalControllerEndpoint = {
return mix.body.Items.reduce<Song[]>((acc, song) => { return mix.body.Items.reduce<Song[]>((acc, song) => {
if (song.Id !== query.songId) { if (song.Id !== query.songId) {
acc.push( acc.push(jfNormalize.song(song, apiClientProps.server));
jfNormalize.song(
song,
apiClientProps.server,
args.context?.pathReplace,
args.context?.pathReplaceWith,
),
);
} }
return acc; return acc;
@@ -1045,12 +965,7 @@ export const JellyfinController: InternalControllerEndpoint = {
throw new Error('Failed to get song detail'); throw new Error('Failed to get song detail');
} }
return jfNormalize.song( return jfNormalize.song(res.body, apiClientProps.server);
res.body,
apiClientProps.server,
args.context?.pathReplace,
args.context?.pathReplaceWith,
);
}, },
getSongList: async (args) => { getSongList: async (args) => {
const { apiClientProps, query } = args; const { apiClientProps, query } = args;
@@ -1163,12 +1078,7 @@ export const JellyfinController: InternalControllerEndpoint = {
return { return {
items: items.map((item) => items: items.map((item) =>
jfNormalize.song( jfNormalize.song(item, apiClientProps.server, query.imageSize),
item,
apiClientProps.server,
args.context?.pathReplace,
args.context?.pathReplaceWith,
),
), ),
startIndex: query.startIndex, startIndex: query.startIndex,
totalRecordCount, totalRecordCount,
@@ -1183,7 +1093,7 @@ export const JellyfinController: InternalControllerEndpoint = {
const { bitrate, format, id, transcode } = query; const { bitrate, format, id, transcode } = query;
const deviceId = ''; const deviceId = '';
let url = `${server?.url}/Items/${id}/Download?apiKey=${server?.credential}&playSessionId=${deviceId}`; let url = `${server?.url}/Items/${id}/Download?api_key=${server?.credential}&playSessionId=${deviceId}`;
if (transcode) { if (transcode) {
// Some format appears to be required. Fall back to trusty MP3 if not specified // Some format appears to be required. Fall back to trusty MP3 if not specified
@@ -1268,14 +1178,7 @@ export const JellyfinController: InternalControllerEndpoint = {
} }
return { return {
items: res.body.Items.map((item) => items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server)),
jfNormalize.song(
item,
apiClientProps.server,
args.context?.pathReplace,
args.context?.pathReplaceWith,
),
),
startIndex: 0, startIndex: 0,
totalRecordCount: res.body.TotalRecordCount, totalRecordCount: res.body.TotalRecordCount,
}; };
@@ -1299,6 +1202,9 @@ export const JellyfinController: InternalControllerEndpoint = {
name: res.body.Name, name: res.body.Name,
}; };
}, },
jukeboxControl: async () => {
throw new Error('Not implemented');
},
movePlaylistItem: async (args) => { movePlaylistItem: async (args) => {
const { apiClientProps, query } = args; const { apiClientProps, query } = args;
@@ -1360,12 +1266,7 @@ export const JellyfinController: InternalControllerEndpoint = {
} }
const existingSongs = existingSongsRes.body.Items.map((item) => const existingSongs = existingSongsRes.body.Items.map((item) =>
jfNormalize.song( jfNormalize.song(item, apiClientProps.server),
item,
apiClientProps.server,
args.context?.pathReplace,
args.context?.pathReplaceWith,
),
); );
// 2. Get playlist detail to get the name // 2. Get playlist detail to get the name
@@ -1605,14 +1506,7 @@ export const JellyfinController: InternalControllerEndpoint = {
jfNormalize.albumArtist(item, apiClientProps.server), jfNormalize.albumArtist(item, apiClientProps.server),
), ),
albums: albums.map((item) => jfNormalize.album(item, apiClientProps.server)), albums: albums.map((item) => jfNormalize.album(item, apiClientProps.server)),
songs: songs.map((item) => songs: songs.map((item) => jfNormalize.song(item, apiClientProps.server)),
jfNormalize.song(
item,
apiClientProps.server,
args.context?.pathReplace,
args.context?.pathReplaceWith,
),
),
}; };
}, },
updateInternetRadioStation: async (args) => { updateInternetRadioStation: async (args) => {
+1 -3
View File
@@ -8,7 +8,6 @@ import qs from 'qs';
import i18n from '/@/i18n/i18n'; import i18n from '/@/i18n/i18n';
import { authenticationFailure } from '/@/renderer/api/utils'; import { authenticationFailure } from '/@/renderer/api/utils';
import { useAuthStore } from '/@/renderer/store'; import { useAuthStore } from '/@/renderer/store';
import { getServerUrl } from '/@/renderer/utils/normalize-server-url';
import { ndType } from '/@/shared/api/navidrome/navidrome-types'; import { ndType } from '/@/shared/api/navidrome/navidrome-types';
import { resultWithHeaders } from '/@/shared/api/utils'; import { resultWithHeaders } from '/@/shared/api/utils';
import { toast } from '/@/shared/components/toast/toast'; import { toast } from '/@/shared/components/toast/toast';
@@ -412,8 +411,7 @@ export const ndApiClient = (args: {
const { params, path: api } = parsePath(path); const { params, path: api } = parsePath(path);
if (server) { if (server) {
const serverUrl = getServerUrl(server); baseUrl = `${server?.url}/api`;
baseUrl = serverUrl ? `${serverUrl}/api` : undefined;
token = server?.ndCredential; token = server?.ndCredential;
} else { } else {
baseUrl = url; baseUrl = url;
@@ -184,19 +184,18 @@ export const NavidromeController: InternalControllerEndpoint = {
getAlbumArtistDetail: async (args) => { getAlbumArtistDetail: async (args) => {
const { apiClientProps, query } = args; const { apiClientProps, query } = args;
const [res, artistInfoRes] = await Promise.all([ const res = await ndApiClient(apiClientProps).getAlbumArtistDetail({
ndApiClient(apiClientProps).getAlbumArtistDetail({ params: {
params: { id: query.id,
id: query.id, },
}, });
}),
ssApiClient(apiClientProps).getArtistInfo({ const artistInfoRes = await ssApiClient(apiClientProps).getArtistInfo({
query: { query: {
count: 10, count: 10,
id: query.id, id: query.id,
}, },
}), });
]);
if (res.status !== 200) { if (res.status !== 200) {
throw new Error('Failed to get album artist detail'); throw new Error('Failed to get album artist detail');
@@ -268,7 +267,7 @@ export const NavidromeController: InternalControllerEndpoint = {
query: { ...query, limit: 1, startIndex: 0 }, query: { ...query, limit: 1, startIndex: 0 },
}).then((result) => result!.totalRecordCount!), }).then((result) => result!.totalRecordCount!),
getAlbumDetail: async (args) => { getAlbumDetail: async (args) => {
const { apiClientProps, context, query } = args; const { apiClientProps, query } = args;
const albumRes = await ndApiClient(apiClientProps).getAlbumDetail({ const albumRes = await ndApiClient(apiClientProps).getAlbumDetail({
params: { params: {
@@ -294,8 +293,6 @@ export const NavidromeController: InternalControllerEndpoint = {
return ndNormalize.album( return ndNormalize.album(
{ ...albumRes.body.data, songs: songsData.body.data }, { ...albumRes.body.data, songs: songsData.body.data },
apiClientProps.server, apiClientProps.server,
context?.pathReplace,
context?.pathReplaceWith,
); );
}, },
getAlbumInfo: async (args) => { getAlbumInfo: async (args) => {
@@ -319,23 +316,19 @@ export const NavidromeController: InternalControllerEndpoint = {
}; };
}, },
getAlbumList: async (args) => { getAlbumList: async (args) => {
const { apiClientProps, context, query } = args; const { apiClientProps, query } = args;
const genres = hasFeature(apiClientProps.server, ServerFeature.BFR) const genres = hasFeature(apiClientProps.server, ServerFeature.BFR)
? query.genreIds ? query.genreIds
: query.genreIds?.[0]; : query.genreIds?.[0];
const artistIds = hasFeature(apiClientProps.server, ServerFeature.BFR)
? query.artistIds
: query.artistIds?.[0];
const res = await ndApiClient(apiClientProps).getAlbumList({ const res = await ndApiClient(apiClientProps).getAlbumList({
query: { query: {
_end: query.startIndex + (query.limit || 0), _end: query.startIndex + (query.limit || 0),
_order: sortOrderMap.navidrome[query.sortOrder], _order: sortOrderMap.navidrome[query.sortOrder],
_sort: albumListSortMap.navidrome[query.sortBy], _sort: albumListSortMap.navidrome[query.sortBy],
_start: query.startIndex, _start: query.startIndex,
artist_id: artistIds, artist_id: query.artistIds?.[0],
compilation: query.compilation, compilation: query.compilation,
genre_id: genres, genre_id: genres,
has_rating: query.hasRating, has_rating: query.hasRating,
@@ -354,14 +347,7 @@ export const NavidromeController: InternalControllerEndpoint = {
} }
return { return {
items: res.body.data.map((album) => items: res.body.data.map((album) => ndNormalize.album(album, apiClientProps.server)),
ndNormalize.album(
album,
apiClientProps.server,
context?.pathReplace,
context?.pathReplaceWith,
),
),
startIndex: query?.startIndex || 0, startIndex: query?.startIndex || 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0), totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
}; };
@@ -415,32 +401,6 @@ export const NavidromeController: InternalControllerEndpoint = {
apiClientProps, apiClientProps,
query: { ...query, limit: 1, startIndex: 0 }, query: { ...query, limit: 1, startIndex: 0 },
}).then((result) => result!.totalRecordCount!), }).then((result) => result!.totalRecordCount!),
getArtistRadio: async (args) => {
const { apiClientProps, query } = args;
// Use getSimilarSongs2 API for artist radio
const res = await ssApiClient({
...apiClientProps,
silent: true,
}).getSimilarSongs2({
query: {
count: query.count,
id: query.artistId,
},
});
if (res.status !== 200) {
throw new Error('Failed to get artist radio songs');
}
if (!res.body.similarSongs2?.song) {
return [];
}
return res.body.similarSongs2.song.map((song) =>
ssNormalize.song(song, apiClientProps.server),
);
},
getDownloadUrl: SubsonicController.getDownloadUrl, getDownloadUrl: SubsonicController.getDownloadUrl,
getFolder: SubsonicController.getFolder, getFolder: SubsonicController.getFolder,
getGenreList: async (args) => { getGenreList: async (args) => {
@@ -501,7 +461,6 @@ export const NavidromeController: InternalControllerEndpoint = {
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0), totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
}; };
}, },
getImageUrl: SubsonicController.getImageUrl,
getInternetRadioStations: SubsonicController.getInternetRadioStations, getInternetRadioStations: SubsonicController.getInternetRadioStations,
getLyrics: SubsonicController.getLyrics, getLyrics: SubsonicController.getLyrics,
getMusicFolderList: SubsonicController.getMusicFolderList, getMusicFolderList: SubsonicController.getMusicFolderList,
@@ -569,14 +528,7 @@ export const NavidromeController: InternalControllerEndpoint = {
} }
return { return {
items: res.body.data.map((item) => items: res.body.data.map((item) => ndNormalize.song(item, apiClientProps.server)),
ndNormalize.song(
item,
apiClientProps.server,
args.context?.pathReplace,
args.context?.pathReplaceWith,
),
),
startIndex: 0, startIndex: 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0), totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
}; };
@@ -593,14 +545,7 @@ export const NavidromeController: InternalControllerEndpoint = {
const { changedBy, current, items, position, updatedAt } = res.body.data; const { changedBy, current, items, position, updatedAt } = res.body.data;
const entries = items.map((song) => const entries = items.map((song) => ndNormalize.song(song, apiClientProps.server));
ndNormalize.song(
song,
apiClientProps.server,
args.context?.pathReplace,
args.context?.pathReplaceWith,
),
);
return { return {
changed: updatedAt, changed: updatedAt,
@@ -637,6 +582,7 @@ export const NavidromeController: InternalControllerEndpoint = {
const features = { const features = {
...subsonicArgs.features, ...subsonicArgs.features,
...navidromeFeatures, ...navidromeFeatures,
jukebox: [1],
publicPlaylist: [1], publicPlaylist: [1],
[ServerFeature.MUSIC_FOLDER_MULTISELECT]: [1], [ServerFeature.MUSIC_FOLDER_MULTISELECT]: [1],
}; };
@@ -691,12 +637,7 @@ export const NavidromeController: InternalControllerEndpoint = {
throw new Error('Failed to get song detail'); throw new Error('Failed to get song detail');
} }
return ndNormalize.song( return ndNormalize.song(res.body.data, apiClientProps.server);
res.body.data,
apiClientProps.server,
args.context?.pathReplace,
args.context?.pathReplaceWith,
);
}, },
getSongList: async (args) => { getSongList: async (args) => {
const { apiClientProps, query } = args; const { apiClientProps, query } = args;
@@ -725,12 +666,7 @@ export const NavidromeController: InternalControllerEndpoint = {
return { return {
items: res.body.data.map((song) => items: res.body.data.map((song) =>
ndNormalize.song( ndNormalize.song(song, apiClientProps.server, query.imageSize),
song,
apiClientProps.server,
args.context?.pathReplace,
args.context?.pathReplaceWith,
),
), ),
startIndex: query?.startIndex || 0, startIndex: query?.startIndex || 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0), totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
@@ -826,6 +762,7 @@ export const NavidromeController: InternalControllerEndpoint = {
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0), totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
}; };
}, },
jukeboxControl: SubsonicController.jukeboxControl,
movePlaylistItem: async (args) => { movePlaylistItem: async (args) => {
const { apiClientProps, query } = args; const { apiClientProps, query } = args;
@@ -882,12 +819,7 @@ export const NavidromeController: InternalControllerEndpoint = {
} }
const existingSongs = existingSongsRes.body.data.map((item) => const existingSongs = existingSongsRes.body.data.map((item) =>
ndNormalize.song( ndNormalize.song(item, apiClientProps.server),
item,
apiClientProps.server,
args.context?.pathReplace,
args.context?.pathReplaceWith,
),
); );
// 2. Get playlist detail to get the name // 2. Get playlist detail to get the name
+7 -25
View File
@@ -4,9 +4,9 @@ import type {
AlbumDetailQuery, AlbumDetailQuery,
AlbumListQuery, AlbumListQuery,
ArtistListQuery, ArtistListQuery,
ArtistRadioQuery,
FolderQuery, FolderQuery,
GenreListQuery, GenreListQuery,
JukeboxControlQuery,
LyricSearchQuery, LyricSearchQuery,
LyricsQuery, LyricsQuery,
PlaylistDetailQuery, PlaylistDetailQuery,
@@ -263,6 +263,12 @@ export const queryKeys: Record<
}, },
root: (serverId: string) => [serverId, 'genres'] as const, root: (serverId: string) => [serverId, 'genres'] as const,
}, },
jukebox: {
control: (serverId: string, query?: JukeboxControlQuery) => {
if (query) return [serverId, 'jukebox', 'control', query] as const;
return [serverId, 'jukebox', 'control'] as const;
},
},
musicFolders: { musicFolders: {
list: (serverId: string) => [serverId, 'musicFolders', 'list'] as const, list: (serverId: string) => [serverId, 'musicFolders', 'list'] as const,
}, },
@@ -341,10 +347,6 @@ export const queryKeys: Record<
root: (serverId: string) => [serverId] as const, root: (serverId: string) => [serverId] as const,
}, },
songs: { songs: {
artistRadio: (serverId: string, query?: ArtistRadioQuery) => {
if (query) return [serverId, 'songs', 'artistRadio', query] as const;
return [serverId, 'songs', 'artistRadio'] as const;
},
count: (serverId: string, query?: SongListQuery) => { count: (serverId: string, query?: SongListQuery) => {
const { filter, pagination } = splitPaginatedQuery(query); const { filter, pagination } = splitPaginatedQuery(query);
if (query && pagination) { if (query && pagination) {
@@ -364,18 +366,6 @@ export const queryKeys: Record<
return [serverId, 'songs', 'detail'] as const; return [serverId, 'songs', 'detail'] as const;
}, },
infiniteList: (serverId: string, query?: SongListQuery) => {
const { filter, pagination } = splitPaginatedQuery(query);
if (query && pagination) {
return [serverId, 'songs', 'infiniteList', filter, pagination] as const;
}
if (query) {
return [serverId, 'songs', 'infiniteList', filter] as const;
}
return [serverId, 'songs', 'infiniteList'] as const;
},
list: (serverId: string, query?: SongListQuery) => { list: (serverId: string, query?: SongListQuery) => {
const { filter, pagination } = splitPaginatedQuery(query); const { filter, pagination } = splitPaginatedQuery(query);
if (query && pagination) { if (query && pagination) {
@@ -403,15 +393,7 @@ export const queryKeys: Record<
if (query) return [serverId, 'songs', 'randomSongList', query] as const; if (query) return [serverId, 'songs', 'randomSongList', query] as const;
return [serverId, 'songs', 'randomSongList'] as const; return [serverId, 'songs', 'randomSongList'] as const;
}, },
remoteLyrics: (serverId: string, query?: LyricsQuery) => {
if (query) return [serverId, 'song', 'lyrics', 'remote', query] as const;
return [serverId, 'song', 'lyrics', 'remote'] as const;
},
root: (serverId: string) => [serverId, 'songs'] as const, root: (serverId: string) => [serverId, 'songs'] as const,
serverLyrics: (serverId: string, query?: LyricsQuery) => {
if (query) return [serverId, 'song', 'lyrics', 'server', query] as const;
return [serverId, 'song', 'lyrics', 'server'] as const;
},
similar: (serverId: string, query?: SimilarSongsQuery) => { similar: (serverId: string, query?: SimilarSongsQuery) => {
if (query) return [serverId, 'song', 'similar', query] as const; if (query) return [serverId, 'song', 'similar', query] as const;
return [serverId, 'song', 'similar'] as const; return [serverId, 'song', 'similar'] as const;
+9 -11
View File
@@ -5,7 +5,6 @@ import qs from 'qs';
import { z } from 'zod'; import { z } from 'zod';
import i18n from '/@/i18n/i18n'; import i18n from '/@/i18n/i18n';
import { getServerUrl } from '/@/renderer/utils/normalize-server-url';
import { ssType } from '/@/shared/api/subsonic/subsonic-types'; import { ssType } from '/@/shared/api/subsonic/subsonic-types';
import { hasFeature } from '/@/shared/api/utils'; import { hasFeature } from '/@/shared/api/utils';
import { toast } from '/@/shared/components/toast/toast'; import { toast } from '/@/shared/components/toast/toast';
@@ -202,14 +201,6 @@ export const contract = c.router({
200: ssType._response.similarSongs, 200: ssType._response.similarSongs,
}, },
}, },
getSimilarSongs2: {
method: 'GET',
path: 'getSimilarSongs2',
query: ssType._parameters.similarSongs2,
responses: {
200: ssType._response.similarSongs2,
},
},
getSong: { getSong: {
method: 'GET', method: 'GET',
path: 'getSong.view', path: 'getSong.view',
@@ -258,6 +249,14 @@ export const contract = c.router({
200: ssType._response.user, 200: ssType._response.user,
}, },
}, },
jukeboxControl: {
method: 'GET',
path: 'jukeboxControl.view',
query: ssType._parameters.jukeboxControl,
responses: {
200: ssType._response.jukeboxPlaylist,
},
},
ping: { ping: {
method: 'GET', method: 'GET',
path: 'ping.view', path: 'ping.view',
@@ -399,8 +398,7 @@ export const ssApiClient = (args: {
const { params, path: api } = parsePath(path); const { params, path: api } = parsePath(path);
if (server) { if (server) {
const serverUrl = getServerUrl(server); baseUrl = `${server.url}/rest`;
baseUrl = serverUrl ? `${serverUrl}/rest` : undefined;
const token = server.credential; const token = server.credential;
const params = token.split(/&?\w=/gm); const params = token.split(/&?\w=/gm);
+86 -263
View File
@@ -9,7 +9,6 @@ import { z } from 'zod';
import { contract, ssApiClient } from '/@/renderer/api/subsonic/subsonic-api'; import { contract, ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
import { randomString } from '/@/renderer/utils'; import { randomString } from '/@/renderer/utils';
import { getServerUrl } from '/@/renderer/utils/normalize-server-url';
import { ssNormalize } from '/@/shared/api/subsonic/subsonic-normalize'; import { ssNormalize } from '/@/shared/api/subsonic/subsonic-normalize';
import { import {
AlbumListSortType, AlbumListSortType,
@@ -156,10 +155,7 @@ export const SubsonicController: InternalControllerEndpoint = {
const res = await ssApiClient(apiClientProps).createFavorite({ const res = await ssApiClient(apiClientProps).createFavorite({
query: { query: {
albumId: query.type === LibraryItem.ALBUM ? query.id : undefined, albumId: query.type === LibraryItem.ALBUM ? query.id : undefined,
artistId: artistId: query.type === LibraryItem.ALBUM_ARTIST ? query.id : undefined,
query.type === LibraryItem.ALBUM_ARTIST || query.type === LibraryItem.ARTIST
? query.id
: undefined,
id: query.type === LibraryItem.SONG ? query.id : undefined, id: query.type === LibraryItem.SONG ? query.id : undefined,
}, },
}); });
@@ -209,10 +205,7 @@ export const SubsonicController: InternalControllerEndpoint = {
const res = await ssApiClient(apiClientProps).removeFavorite({ const res = await ssApiClient(apiClientProps).removeFavorite({
query: { query: {
albumId: query.type === LibraryItem.ALBUM ? query.id : undefined, albumId: query.type === LibraryItem.ALBUM ? query.id : undefined,
artistId: artistId: query.type === LibraryItem.ALBUM_ARTIST ? query.id : undefined,
query.type === LibraryItem.ALBUM_ARTIST || query.type === LibraryItem.ARTIST
? query.id
: undefined,
id: query.type === LibraryItem.SONG ? query.id : undefined, id: query.type === LibraryItem.SONG ? query.id : undefined,
}, },
}); });
@@ -256,18 +249,17 @@ export const SubsonicController: InternalControllerEndpoint = {
getAlbumArtistDetail: async (args) => { getAlbumArtistDetail: async (args) => {
const { apiClientProps, query } = args; const { apiClientProps, query } = args;
const [artistInfoRes, res] = await Promise.all([ const artistInfoRes = await ssApiClient(apiClientProps).getArtistInfo({
ssApiClient(apiClientProps).getArtistInfo({ query: {
query: { id: query.id,
id: query.id, },
}, });
}),
ssApiClient(apiClientProps).getArtist({ const res = await ssApiClient(apiClientProps).getArtist({
query: { query: {
id: query.id, id: query.id,
}, },
}), });
]);
if (res.status !== 200) { if (res.status !== 200) {
throw new Error('Failed to get album artist detail'); throw new Error('Failed to get album artist detail');
@@ -281,18 +273,11 @@ export const SubsonicController: InternalControllerEndpoint = {
} }
return { return {
...ssNormalize.albumArtist(artist, apiClientProps.server), ...ssNormalize.albumArtist(artist, apiClientProps.server, 300),
albums: artist.album?.map((album) => albums: artist.album?.map((album) => ssNormalize.album(album, apiClientProps.server)),
ssNormalize.album(
album,
apiClientProps.server,
args.context?.pathReplace,
args.context?.pathReplaceWith,
),
),
similarArtists: similarArtists:
artistInfo?.similarArtist?.map((artist) => artistInfo?.similarArtist?.map((artist) =>
ssNormalize.albumArtist(artist, apiClientProps.server), ssNormalize.albumArtist(artist, apiClientProps.server, 300),
) || null, ) || null,
}; };
}, },
@@ -312,7 +297,7 @@ export const SubsonicController: InternalControllerEndpoint = {
const artists = (res.body.artists?.index || []).flatMap((index) => index.artist); const artists = (res.body.artists?.index || []).flatMap((index) => index.artist);
let results = artists.map((artist) => let results = artists.map((artist) =>
ssNormalize.albumArtist(artist, apiClientProps.server), ssNormalize.albumArtist(artist, apiClientProps.server, 300),
); );
if (query.searchTerm) { if (query.searchTerm) {
@@ -323,18 +308,19 @@ export const SubsonicController: InternalControllerEndpoint = {
results = searchResults; results = searchResults;
} }
return sortAndPaginate(results, { if (query.sortBy) {
limit: query.limit, results = sortAlbumArtistList(results, query.sortBy, query.sortOrder);
sortBy: query.sortBy, }
sortFn: query.sortBy ? sortAlbumArtistList : undefined,
sortOrder: query.sortOrder, return {
items: results,
startIndex: query.startIndex, startIndex: query.startIndex,
}); totalRecordCount: artists.length,
};
}, },
getAlbumArtistListCount: (args) => getAlbumArtistListCount: (args) =>
SubsonicController.getAlbumArtistList({ SubsonicController.getAlbumArtistList({
...args, ...args,
context: args.context,
query: { ...args.query, startIndex: 0 }, query: { ...args.query, startIndex: 0 },
}).then((res) => res!.totalRecordCount!), }).then((res) => res!.totalRecordCount!),
getAlbumDetail: async (args) => { getAlbumDetail: async (args) => {
@@ -350,12 +336,7 @@ export const SubsonicController: InternalControllerEndpoint = {
throw new Error('Failed to get album detail'); throw new Error('Failed to get album detail');
} }
return ssNormalize.album( return ssNormalize.album(res.body.album, apiClientProps.server);
res.body.album,
apiClientProps.server,
args.context?.pathReplace,
args.context?.pathReplaceWith,
);
}, },
getAlbumList: async (args) => { getAlbumList: async (args) => {
const { apiClientProps, query } = args; const { apiClientProps, query } = args;
@@ -367,7 +348,6 @@ export const SubsonicController: InternalControllerEndpoint = {
albumOffset: query.startIndex, albumOffset: query.startIndex,
artistCount: 0, artistCount: 0,
artistOffset: 0, artistOffset: 0,
musicFolderId: getLibraryId(query.musicFolderId),
query: query.searchTerm || '', query: query.searchTerm || '',
songCount: 0, songCount: 0,
songOffset: 0, songOffset: 0,
@@ -380,12 +360,7 @@ export const SubsonicController: InternalControllerEndpoint = {
const results = const results =
res.body.searchResult3?.album?.map((album) => res.body.searchResult3?.album?.map((album) =>
ssNormalize.album( ssNormalize.album(album, apiClientProps.server),
album,
apiClientProps.server,
args.context?.pathReplace,
args.context?.pathReplaceWith,
),
) || []; ) || [];
return { return {
@@ -420,14 +395,7 @@ export const SubsonicController: InternalControllerEndpoint = {
return artist.body.artist.album ?? []; return artist.body.artist.album ?? [];
}); });
const items = albums.map((album) => const items = albums.map((album) => ssNormalize.album(album, apiClientProps.server));
ssNormalize.album(
album,
apiClientProps.server,
args.context?.pathReplace,
args.context?.pathReplaceWith,
),
);
return { return {
items: sortAlbumList(items, query.sortBy, query.sortOrder), items: sortAlbumList(items, query.sortBy, query.sortOrder),
@@ -449,12 +417,7 @@ export const SubsonicController: InternalControllerEndpoint = {
const allResults = const allResults =
res.body.starred?.album?.map((album) => res.body.starred?.album?.map((album) =>
ssNormalize.album( ssNormalize.album(album, apiClientProps.server),
album,
apiClientProps.server,
args.context?.pathReplace,
args.context?.pathReplaceWith,
),
) || []; ) || [];
return sortAndPaginate(allResults, { return sortAndPaginate(allResults, {
@@ -519,12 +482,7 @@ export const SubsonicController: InternalControllerEndpoint = {
return { return {
items: items:
res.body.albumList2.album?.map((album) => res.body.albumList2.album?.map((album) =>
ssNormalize.album( ssNormalize.album(album, apiClientProps.server, 300),
album,
apiClientProps.server,
args.context?.pathReplace,
args.context?.pathReplaceWith,
),
) || [], ) || [],
startIndex: query.startIndex, startIndex: query.startIndex,
totalRecordCount: null, totalRecordCount: null,
@@ -545,7 +503,6 @@ export const SubsonicController: InternalControllerEndpoint = {
albumOffset: startIndex, albumOffset: startIndex,
artistCount: 0, artistCount: 0,
artistOffset: 0, artistOffset: 0,
musicFolderId: getLibraryId(query.musicFolderId),
query: query.searchTerm || '', query: query.searchTerm || '',
songCount: 0, songCount: 0,
songOffset: 0, songOffset: 0,
@@ -695,7 +652,7 @@ export const SubsonicController: InternalControllerEndpoint = {
} }
let results = artists.map((artist) => let results = artists.map((artist) =>
ssNormalize.albumArtist(artist, apiClientProps.server), ssNormalize.albumArtist(artist, apiClientProps.server, 300),
); );
if (query.searchTerm) { if (query.searchTerm) {
@@ -717,36 +674,8 @@ export const SubsonicController: InternalControllerEndpoint = {
getArtistListCount: async (args) => getArtistListCount: async (args) =>
SubsonicController.getArtistList({ SubsonicController.getArtistList({
...args, ...args,
context: args.context,
query: { ...args.query, startIndex: 0 }, query: { ...args.query, startIndex: 0 },
}).then((res) => res!.totalRecordCount!), }).then((res) => res!.totalRecordCount!),
getArtistRadio: async (args) => {
const { apiClientProps, context, query } = args;
const res = await ssApiClient(apiClientProps).getSimilarSongs2({
query: {
count: query.count,
id: query.artistId,
},
});
if (res.status !== 200) {
throw new Error('Failed to get artist radio songs');
}
if (!res.body.similarSongs2?.song) {
return [];
}
return res.body.similarSongs2.song.map((song) =>
ssNormalize.song(
song,
apiClientProps.server,
context?.pathReplace,
context?.pathReplaceWith,
),
);
},
getDownloadUrl: (args) => { getDownloadUrl: (args) => {
const { apiClientProps, query } = args; const { apiClientProps, query } = args;
@@ -758,7 +687,7 @@ export const SubsonicController: InternalControllerEndpoint = {
'&c=Feishin' '&c=Feishin'
); );
}, },
getFolder: async ({ apiClientProps, context, query }) => { getFolder: async ({ apiClientProps, query }) => {
const sortOrder = (query.sortOrder?.toLowerCase() ?? 'asc') as 'asc' | 'desc'; const sortOrder = (query.sortOrder?.toLowerCase() ?? 'asc') as 'asc' | 'desc';
const isRootFolderId = /^\d+$/.test(query.id); const isRootFolderId = /^\d+$/.test(query.id);
@@ -790,14 +719,7 @@ export const SubsonicController: InternalControllerEndpoint = {
}); });
} }
let folders = items.map((item) => let folders = items.map((item) => ssNormalize.folder(item, apiClientProps.server));
ssNormalize.folder(
item,
apiClientProps.server,
context?.pathReplace,
context?.pathReplaceWith,
),
);
folders = orderBy(folders, [(v) => v.name.toLowerCase()], [sortOrder]); folders = orderBy(folders, [(v) => v.name.toLowerCase()], [sortOrder]);
@@ -825,12 +747,7 @@ export const SubsonicController: InternalControllerEndpoint = {
throw new Error('Failed to get folder'); throw new Error('Failed to get folder');
} }
const folder = ssNormalize.folder( const folder = ssNormalize.folder(directoryRes.body.directory, apiClientProps.server);
directoryRes.body.directory,
apiClientProps.server,
context?.pathReplace,
context?.pathReplaceWith,
);
let filteredFolders = folder.children?.folders || []; let filteredFolders = folder.children?.folders || [];
let filteredSongs = folder.children?.songs || []; let filteredSongs = folder.children?.songs || [];
@@ -904,29 +821,6 @@ export const SubsonicController: InternalControllerEndpoint = {
startIndex: query.startIndex, startIndex: query.startIndex,
}); });
}, },
getImageUrl: ({ apiClientProps: { server }, baseUrl, query }) => {
const { id, size } = query;
const imageSize = size;
const url = baseUrl || getServerUrl(server);
if (!url || !server?.credential) {
return null;
}
// Check for default placeholder image ID
if (id.match('2a96cbd8b46e442fc41c2b86b821562f')) {
return null;
}
return (
`${url}/rest/getCoverArt.view` +
`?id=${id}` +
`&${server.credential}` +
'&v=1.13.0' +
'&c=Feishin' +
(imageSize ? `&size=${imageSize}` : '')
);
},
getInternetRadioStations: async (args) => { getInternetRadioStations: async (args) => {
const { apiClientProps } = args; const { apiClientProps } = args;
@@ -958,7 +852,6 @@ export const SubsonicController: InternalControllerEndpoint = {
totalRecordCount: res.body.musicFolders.musicFolder.length, totalRecordCount: res.body.musicFolders.musicFolder.length,
}; };
}, },
getPlaylistDetail: async (args) => { getPlaylistDetail: async (args) => {
const { apiClientProps, query } = args; const { apiClientProps, query } = args;
@@ -1044,7 +937,7 @@ export const SubsonicController: InternalControllerEndpoint = {
return results.length; return results.length;
}, },
getPlaylistSongList: async ({ apiClientProps, context, query }) => { getPlaylistSongList: async ({ apiClientProps, query }) => {
const res = await ssApiClient(apiClientProps).getPlaylist({ const res = await ssApiClient(apiClientProps).getPlaylist({
query: { query: {
id: query.id, id: query.id,
@@ -1056,14 +949,8 @@ export const SubsonicController: InternalControllerEndpoint = {
} }
const items = const items =
res.body.playlist.entry?.map((song) => res.body.playlist.entry?.map((song) => ssNormalize.song(song, apiClientProps.server)) ||
ssNormalize.song( [];
song,
apiClientProps.server,
context?.pathReplace,
context?.pathReplaceWith,
),
) || [];
return { return {
items, items,
@@ -1071,7 +958,7 @@ export const SubsonicController: InternalControllerEndpoint = {
totalRecordCount: items.length, totalRecordCount: items.length,
}; };
}, },
getPlayQueue: async ({ apiClientProps, context }) => { getPlayQueue: async ({ apiClientProps }) => {
if (hasFeature(apiClientProps.server, ServerFeature.SERVER_PLAY_QUEUE)) { if (hasFeature(apiClientProps.server, ServerFeature.SERVER_PLAY_QUEUE)) {
const res = await ssApiClient(apiClientProps).getPlayQueueByIndex(); const res = await ssApiClient(apiClientProps).getPlayQueueByIndex();
@@ -1086,15 +973,7 @@ export const SubsonicController: InternalControllerEndpoint = {
changed, changed,
changedBy, changedBy,
currentIndex: currentIndex ?? 0, currentIndex: currentIndex ?? 0,
entry: entry: entry?.map((song) => ssNormalize.song(song, apiClientProps.server)) || [],
entry?.map((song) =>
ssNormalize.song(
song,
apiClientProps.server,
context?.pathReplace,
context?.pathReplaceWith,
),
) || [],
positionMs: position ?? 0, positionMs: position ?? 0,
username, username,
}; };
@@ -1111,22 +990,14 @@ export const SubsonicController: InternalControllerEndpoint = {
changed, changed,
changedBy, changedBy,
currentIndex: current ? entry.findIndex((item) => item.id === current) : 0, currentIndex: current ? entry.findIndex((item) => item.id === current) : 0,
entry: entry: entry?.map((song) => ssNormalize.song(song, apiClientProps.server)) || [],
entry?.map((song) =>
ssNormalize.song(
song,
apiClientProps.server,
context?.pathReplace,
context?.pathReplaceWith,
),
) || [],
positionMs: position ?? 0, positionMs: position ?? 0,
username, username,
}; };
} }
}, },
getRandomSongList: async (args) => { getRandomSongList: async (args) => {
const { apiClientProps, context, query } = args; const { apiClientProps, query } = args;
const res = await ssApiClient(apiClientProps).getRandomSongList({ const res = await ssApiClient(apiClientProps).getRandomSongList({
query: { query: {
@@ -1144,12 +1015,7 @@ export const SubsonicController: InternalControllerEndpoint = {
const results = res.body.randomSongs?.song || []; const results = res.body.randomSongs?.song || [];
const normalizedResults = results.map((song) => const normalizedResults = results.map((song) =>
ssNormalize.song( ssNormalize.song(song, apiClientProps.server),
song,
apiClientProps.server,
context?.pathReplace,
context?.pathReplaceWith,
),
); );
return { return {
@@ -1193,7 +1059,9 @@ export const SubsonicController: InternalControllerEndpoint = {
throw new Error('Failed to ping server'); throw new Error('Failed to ping server');
} }
const features: ServerFeatures = {}; const features: ServerFeatures = {
jukebox: [1],
};
if (!ping.body.openSubsonic || !ping.body.serverVersion) { if (!ping.body.openSubsonic || !ping.body.serverVersion) {
return { features, version: ping.body.version }; return { features, version: ping.body.version };
@@ -1227,7 +1095,7 @@ export const SubsonicController: InternalControllerEndpoint = {
return { features, id: apiClientProps.server?.id, version: ping.body.serverVersion }; return { features, id: apiClientProps.server?.id, version: ping.body.serverVersion };
}, },
getSimilarSongs: async (args) => { getSimilarSongs: async (args) => {
const { apiClientProps, context, query } = args; const { apiClientProps, query } = args;
const res = await ssApiClient(apiClientProps).getSimilarSongs({ const res = await ssApiClient(apiClientProps).getSimilarSongs({
query: { query: {
@@ -1246,21 +1114,14 @@ export const SubsonicController: InternalControllerEndpoint = {
return res.body.similarSongs.song.reduce<Song[]>((acc, song) => { return res.body.similarSongs.song.reduce<Song[]>((acc, song) => {
if (song.id !== query.songId) { if (song.id !== query.songId) {
acc.push( acc.push(ssNormalize.song(song, apiClientProps.server));
ssNormalize.song(
song,
apiClientProps.server,
context?.pathReplace,
context?.pathReplaceWith,
),
);
} }
return acc; return acc;
}, []); }, []);
}, },
getSongDetail: async (args) => { getSongDetail: async (args) => {
const { apiClientProps, context, query } = args; const { apiClientProps, query } = args;
const res = await ssApiClient(apiClientProps).getSong({ const res = await ssApiClient(apiClientProps).getSong({
query: { query: {
@@ -1272,14 +1133,9 @@ export const SubsonicController: InternalControllerEndpoint = {
throw new Error('Failed to get song detail'); throw new Error('Failed to get song detail');
} }
return ssNormalize.song( return ssNormalize.song(res.body.song, apiClientProps.server);
res.body.song,
apiClientProps.server,
context?.pathReplace,
context?.pathReplaceWith,
);
}, },
getSongList: async ({ apiClientProps, context, query }) => { getSongList: async ({ apiClientProps, query }) => {
const fromAlbumPromises: Promise<ServerInferResponses<typeof contract.getAlbum>>[] = []; const fromAlbumPromises: Promise<ServerInferResponses<typeof contract.getAlbum>>[] = [];
const artistDetailPromises: Promise<ServerInferResponses<typeof contract.getArtist>>[] = []; const artistDetailPromises: Promise<ServerInferResponses<typeof contract.getArtist>>[] = [];
@@ -1290,7 +1146,6 @@ export const SubsonicController: InternalControllerEndpoint = {
albumOffset: 0, albumOffset: 0,
artistCount: 0, artistCount: 0,
artistOffset: 0, artistOffset: 0,
musicFolderId: getLibraryId(query.musicFolderId),
query: query.searchTerm || '', query: query.searchTerm || '',
songCount: query.limit, songCount: query.limit,
songOffset: query.startIndex, songOffset: query.startIndex,
@@ -1304,12 +1159,7 @@ export const SubsonicController: InternalControllerEndpoint = {
return { return {
items: items:
res.body.searchResult3?.song?.map((song) => res.body.searchResult3?.song?.map((song) =>
ssNormalize.song( ssNormalize.song(song, apiClientProps.server),
song,
apiClientProps.server,
context?.pathReplace,
context?.pathReplaceWith,
),
) || [], ) || [],
startIndex: query.startIndex, startIndex: query.startIndex,
totalRecordCount: null, totalRecordCount: null,
@@ -1333,15 +1183,7 @@ export const SubsonicController: InternalControllerEndpoint = {
const results = res.body.songsByGenre?.song || []; const results = res.body.songsByGenre?.song || [];
return { return {
items: items: results.map((song) => ssNormalize.song(song, apiClientProps.server)) || [],
results.map((song) =>
ssNormalize.song(
song,
apiClientProps.server,
context?.pathReplace,
context?.pathReplaceWith,
),
) || [],
startIndex: 0, startIndex: 0,
totalRecordCount: null, totalRecordCount: null,
}; };
@@ -1360,12 +1202,7 @@ export const SubsonicController: InternalControllerEndpoint = {
const allResults = const allResults =
(res.body.starred?.song || []).map((song) => (res.body.starred?.song || []).map((song) =>
ssNormalize.song( ssNormalize.song(song, apiClientProps.server),
song,
apiClientProps.server,
context?.pathReplace,
context?.pathReplaceWith,
),
) || []; ) || [];
return sortAndPaginate(allResults, { return sortAndPaginate(allResults, {
@@ -1441,15 +1278,7 @@ export const SubsonicController: InternalControllerEndpoint = {
} }
return { return {
items: items: results.map((song) => ssNormalize.song(song, apiClientProps.server)) || [],
results.map((song) =>
ssNormalize.song(
song,
apiClientProps.server,
context?.pathReplace,
context?.pathReplaceWith,
),
) || [],
startIndex: 0, startIndex: 0,
totalRecordCount: results.length, totalRecordCount: results.length,
}; };
@@ -1461,7 +1290,6 @@ export const SubsonicController: InternalControllerEndpoint = {
albumOffset: 0, albumOffset: 0,
artistCount: 0, artistCount: 0,
artistOffset: 0, artistOffset: 0,
musicFolderId: getLibraryId(query.musicFolderId),
query: query.searchTerm || '', query: query.searchTerm || '',
songCount: query.limit, songCount: query.limit,
songOffset: query.startIndex, songOffset: query.startIndex,
@@ -1475,12 +1303,7 @@ export const SubsonicController: InternalControllerEndpoint = {
return { return {
items: items:
res.body.searchResult3?.song?.map((song) => res.body.searchResult3?.song?.map((song) =>
ssNormalize.song( ssNormalize.song(song, apiClientProps.server),
song,
apiClientProps.server,
context?.pathReplace,
context?.pathReplaceWith,
),
) || [], ) || [],
startIndex: 0, startIndex: 0,
totalRecordCount: null, totalRecordCount: null,
@@ -1507,7 +1330,6 @@ export const SubsonicController: InternalControllerEndpoint = {
albumOffset: 0, albumOffset: 0,
artistCount: 0, artistCount: 0,
artistOffset: 0, artistOffset: 0,
musicFolderId: getLibraryId(query.musicFolderId),
query: query.searchTerm || '', query: query.searchTerm || '',
songCount: MAX_SUBSONIC_ITEMS, songCount: MAX_SUBSONIC_ITEMS,
songOffset: startIndex, songOffset: startIndex,
@@ -1611,7 +1433,6 @@ export const SubsonicController: InternalControllerEndpoint = {
albumOffset: 0, albumOffset: 0,
artistCount: 0, artistCount: 0,
artistOffset: 0, artistOffset: 0,
musicFolderId: getLibraryId(query.musicFolderId),
query: query.searchTerm || '', query: query.searchTerm || '',
songCount: 1, songCount: 1,
songOffset: sectionIndex, songOffset: sectionIndex,
@@ -1640,7 +1461,6 @@ export const SubsonicController: InternalControllerEndpoint = {
albumOffset: 0, albumOffset: 0,
artistCount: 0, artistCount: 0,
artistOffset: 0, artistOffset: 0,
musicFolderId: getLibraryId(query.musicFolderId),
query: query.searchTerm || '', query: query.searchTerm || '',
songCount: MAX_SUBSONIC_ITEMS, songCount: MAX_SUBSONIC_ITEMS,
songOffset: startIndex, songOffset: startIndex,
@@ -1719,7 +1539,7 @@ export const SubsonicController: InternalControllerEndpoint = {
}); });
}, },
getTopSongs: async (args) => { getTopSongs: async (args) => {
const { apiClientProps, context, query } = args; const { apiClientProps, query } = args;
const res = await ssApiClient(apiClientProps).getTopSongsList({ const res = await ssApiClient(apiClientProps).getTopSongsList({
query: { query: {
@@ -1735,12 +1555,7 @@ export const SubsonicController: InternalControllerEndpoint = {
return { return {
items: items:
res.body.topSongs?.song?.map((song) => res.body.topSongs?.song?.map((song) =>
ssNormalize.song( ssNormalize.song(song, apiClientProps.server),
song,
apiClientProps.server,
context?.pathReplace,
context?.pathReplaceWith,
),
) || [], ) || [],
startIndex: 0, startIndex: 0,
totalRecordCount: res.body.topSongs?.song?.length || 0, totalRecordCount: res.body.topSongs?.song?.length || 0,
@@ -1765,6 +1580,30 @@ export const SubsonicController: InternalControllerEndpoint = {
name: res.body.user.username, name: res.body.user.username,
}; };
}, },
jukeboxControl: async (args) => {
const { apiClientProps, query } = args;
const res = await ssApiClient(apiClientProps).jukeboxControl({
query: query,
});
if (res.status !== 200) {
throw new Error('Failed to control jukebox');
}
const jukeboxPlaylist = res.body.jukeboxPlaylist;
return {
currentIndex: jukeboxPlaylist.currentIndex,
gain: jukeboxPlaylist.gain,
playing: jukeboxPlaylist.playing,
position: jukeboxPlaylist.position ?? 0,
songs:
jukeboxPlaylist.entry?.map((song) =>
ssNormalize.song(song, apiClientProps.server),
) || [],
};
},
removeFromPlaylist: async ({ apiClientProps, query }) => { removeFromPlaylist: async ({ apiClientProps, query }) => {
const res = await ssApiClient(apiClientProps).updatePlaylist({ const res = await ssApiClient(apiClientProps).updatePlaylist({
query: { query: {
@@ -1780,7 +1619,7 @@ export const SubsonicController: InternalControllerEndpoint = {
return null; return null;
}, },
replacePlaylist: async (args) => { replacePlaylist: async (args) => {
const { apiClientProps, body, context, query } = args; const { apiClientProps, body, query } = args;
// 1. Fetch existing songs from the playlist // 1. Fetch existing songs from the playlist
const existingSongsRes = await ssApiClient(apiClientProps).getPlaylist({ const existingSongsRes = await ssApiClient(apiClientProps).getPlaylist({
@@ -1795,12 +1634,7 @@ export const SubsonicController: InternalControllerEndpoint = {
const existingSongs = const existingSongs =
existingSongsRes.body.playlist.entry?.map((song) => existingSongsRes.body.playlist.entry?.map((song) =>
ssNormalize.song( ssNormalize.song(song, apiClientProps.server),
song,
apiClientProps.server,
context?.pathReplace,
context?.pathReplaceWith,
),
) || []; ) || [];
// 2. Get playlist detail to get the name // 2. Get playlist detail to get the name
@@ -1912,7 +1746,7 @@ export const SubsonicController: InternalControllerEndpoint = {
return null; return null;
}, },
search: async (args) => { search: async (args) => {
const { apiClientProps, context, query } = args; const { apiClientProps, query } = args;
const res = await ssApiClient(apiClientProps).search3({ const res = await ssApiClient(apiClientProps).search3({
query: { query: {
@@ -1920,7 +1754,6 @@ export const SubsonicController: InternalControllerEndpoint = {
albumOffset: query.albumStartIndex, albumOffset: query.albumStartIndex,
artistCount: query.albumArtistLimit, artistCount: query.albumArtistLimit,
artistOffset: query.albumArtistStartIndex, artistOffset: query.albumArtistStartIndex,
musicFolderId: getLibraryId(query.musicFolderId),
query: query.query, query: query.query,
songCount: query.songLimit, songCount: query.songLimit,
songOffset: query.songStartIndex, songOffset: query.songStartIndex,
@@ -1936,20 +1769,10 @@ export const SubsonicController: InternalControllerEndpoint = {
ssNormalize.albumArtist(artist, apiClientProps.server), ssNormalize.albumArtist(artist, apiClientProps.server),
), ),
albums: (res.body.searchResult3?.album || []).map((album) => albums: (res.body.searchResult3?.album || []).map((album) =>
ssNormalize.album( ssNormalize.album(album, apiClientProps.server),
album,
apiClientProps.server,
args.context?.pathReplace,
args.context?.pathReplaceWith,
),
), ),
songs: (res.body.searchResult3?.song || []).map((song) => songs: (res.body.searchResult3?.song || []).map((song) =>
ssNormalize.song( ssNormalize.song(song, apiClientProps.server),
song,
apiClientProps.server,
context?.pathReplace,
context?.pathReplaceWith,
),
), ),
}; };
}, },
+7 -12
View File
@@ -14,7 +14,7 @@ import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-co
import { useSyncSettingsToMain } from '/@/renderer/hooks/use-sync-settings-to-main'; import { useSyncSettingsToMain } from '/@/renderer/hooks/use-sync-settings-to-main';
import { ReleaseNotesModal } from './release-notes-modal'; import { ReleaseNotesModal } from './release-notes-modal';
import { AppRouter } from '/@/renderer/router/app-router'; import { AppRouter } from '/@/renderer/router/app-router';
import { useCssSettings, useHotkeySettings, useLanguage } from '/@/renderer/store'; import { useCssSettings, useHotkeySettings, useSettingsStore } from '/@/renderer/store';
import { useAppTheme } from '/@/renderer/themes/use-app-theme'; import { useAppTheme } from '/@/renderer/themes/use-app-theme';
import { sanitizeCss } from '/@/renderer/utils/sanitize'; import { sanitizeCss } from '/@/renderer/utils/sanitize';
import { WebAudio } from '/@/shared/types/types'; import { WebAudio } from '/@/shared/types/types';
@@ -26,7 +26,7 @@ const ipc = isElectron() ? window.api.ipc : null;
export const App = () => { export const App = () => {
const { mode, theme } = useAppTheme(); const { mode, theme } = useAppTheme();
const language = useLanguage(); const language = useSettingsStore((store) => store.general.language);
const { content, enabled } = useCssSettings(); const { content, enabled } = useCssSettings();
const { bindings } = useHotkeySettings(); const { bindings } = useHotkeySettings();
@@ -72,21 +72,16 @@ export const App = () => {
} }
}, [language]); }, [language]);
const notificationStyles = useMemo(
() => ({
root: {
marginBottom: 90,
},
}),
[],
);
return ( return (
<MantineProvider forceColorScheme={mode} theme={theme}> <MantineProvider forceColorScheme={mode} theme={theme}>
<Notifications <Notifications
containerWidth="300px" containerWidth="300px"
position="bottom-center" position="bottom-center"
styles={notificationStyles} styles={{
root: {
marginBottom: 90,
},
}}
zIndex={50000} zIndex={50000}
/> />
<WebAudioContext.Provider value={webAudioProvider}> <WebAudioContext.Provider value={webAudioProvider}>
@@ -177,7 +177,6 @@
} }
.artist { .artist {
width: 100%;
color: white; color: white;
text-shadow: 0 0 8px rgb(0 0 0 / 50%); text-shadow: 0 0 8px rgb(0 0 0 / 50%);
} }
@@ -1,12 +1,11 @@
import type { MouseEvent } from 'react'; import type { MouseEvent } from 'react';
import { AnimatePresence, motion } from 'motion/react'; import { AnimatePresence, motion } from 'motion/react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { generatePath, Link } from 'react-router'; import { generatePath, Link } from 'react-router';
import styles from './feature-carousel.module.css'; import styles from './feature-carousel.module.css';
import { ItemImage, useItemImageUrl } from '/@/renderer/components/item-image/item-image';
import { usePlayer } from '/@/renderer/features/player/context/player-context'; import { usePlayer } from '/@/renderer/features/player/context/player-context';
import { BackgroundOverlay } from '/@/renderer/features/shared/components/library-background-overlay'; import { BackgroundOverlay } from '/@/renderer/features/shared/components/library-background-overlay';
import { PlayButtonGroup } from '/@/renderer/features/shared/components/play-button-group'; import { PlayButtonGroup } from '/@/renderer/features/shared/components/play-button-group';
@@ -16,6 +15,7 @@ import { useCurrentServer } from '/@/renderer/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Badge } from '/@/shared/components/badge/badge'; import { Badge } from '/@/shared/components/badge/badge';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { Image } from '/@/shared/components/image/image';
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 { Album, LibraryItem } from '/@/shared/types/domain-types'; import { Album, LibraryItem } from '/@/shared/types/domain-types';
@@ -78,15 +78,9 @@ interface CarouselItemProps {
} }
const CarouselItem = ({ album }: CarouselItemProps) => { const CarouselItem = ({ album }: CarouselItemProps) => {
const imageUrl = useItemImageUrl({
id: album.imageId || undefined,
itemType: LibraryItem.ALBUM,
type: 'itemCard',
});
const { background: backgroundColor } = useFastAverageColor({ const { background: backgroundColor } = useFastAverageColor({
algorithm: 'dominant', algorithm: 'dominant',
src: imageUrl || null, src: album.imageUrl || null,
srcLoaded: true, srcLoaded: true,
}); });
@@ -116,13 +110,10 @@ const CarouselItem = ({ album }: CarouselItemProps) => {
</div> </div>
<div className={styles.imageSection}> <div className={styles.imageSection}>
<ItemImage <Image
className={styles.albumImage} className={styles.albumImage}
containerClassName={styles.albumImageContainer} containerClassName={styles.albumImageContainer}
id={album.imageId} src={album.imageUrl || undefined}
itemType={LibraryItem.ALBUM}
src={imageUrl}
type="itemCard"
/> />
<div className={styles.playButtonOverlay}> <div className={styles.playButtonOverlay}>
<PlayButtonGroup onPlay={handlePlay} /> <PlayButtonGroup onPlay={handlePlay} />
@@ -132,13 +123,7 @@ const CarouselItem = ({ album }: CarouselItemProps) => {
<div className={styles.metadataSection}> <div className={styles.metadataSection}>
<Stack gap="sm"> <Stack gap="sm">
{album.albumArtists?.[0] && ( {album.albumArtists?.[0] && (
<Text <Text className={styles.artist} fw={500} size="md">
className={styles.artist}
fw={500}
lineClamp={1}
size="md"
ta="center"
>
{album.albumArtists[0].name} {album.albumArtists[0].name}
</Text> </Text>
)} )}
@@ -216,70 +201,28 @@ export const FeatureCarousel = ({ data, onNearEnd }: FeatureCarouselProps) => {
} }
}, [data, startIndex, itemsPerRow, onNearEnd]); }, [data, startIndex, itemsPerRow, onNearEnd]);
const handleNext = useCallback( const handleNext = (e?: MouseEvent<HTMLButtonElement>) => {
(e?: MouseEvent<HTMLButtonElement>) => { e?.preventDefault();
e?.preventDefault(); e?.stopPropagation();
e?.stopPropagation(); if (!data) return;
if (!data) return; directionRef.current = { isNext: true };
directionRef.current = { isNext: true }; setStartIndex((prev) => (prev + itemsPerRow) % data.length);
setStartIndex((prev) => (prev + itemsPerRow) % data.length); };
},
[data, itemsPerRow],
);
const handlePrevious = useCallback( const handlePrevious = (e?: MouseEvent<HTMLButtonElement>) => {
(e?: MouseEvent<HTMLButtonElement>) => { e?.preventDefault();
e?.preventDefault(); e?.stopPropagation();
e?.stopPropagation(); if (!data) return;
if (!data) return; directionRef.current = { isNext: false };
directionRef.current = { isNext: false }; setStartIndex((prev) => (prev - itemsPerRow + data.length) % data.length);
setStartIndex((prev) => (prev - itemsPerRow + data.length) % data.length); };
},
[data, itemsPerRow],
);
const canNavigate = data && data.length > itemsPerRow;
const wheelCooldownRef = useRef(0);
const wheelThreshold = 10;
const wheelCooldownMs = 250;
const handleWheel = useCallback(
(event: React.WheelEvent<HTMLDivElement>) => {
if (!canNavigate || !data) {
return;
}
if (!event.shiftKey) {
return;
}
const now = Date.now();
const elapsed = now - wheelCooldownRef.current;
const horizontalDelta = Math.abs(event.deltaY);
if (horizontalDelta < wheelThreshold || elapsed < wheelCooldownMs) {
return;
}
if (event.deltaY > 0) {
wheelCooldownRef.current = now;
handleNext();
} else if (event.deltaY < 0) {
wheelCooldownRef.current = now;
handlePrevious();
}
},
[canNavigate, data, handleNext, handlePrevious, wheelCooldownMs, wheelThreshold],
);
if (!data || data.length === 0) { if (!data || data.length === 0) {
return null; return null;
} }
return ( return (
<div className={styles.carouselContainer} onWheel={handleWheel} ref={containerRef}> <div className={styles.carouselContainer} ref={containerRef}>
<AnimatePresence initial={false} mode="popLayout"> <AnimatePresence initial={false} mode="popLayout">
<motion.div <motion.div
animate="animate" animate="animate"
@@ -18,7 +18,6 @@ interface Card {
interface GridCarouselProps { interface GridCarouselProps {
cards: Card[]; cards: Card[];
enableRefresh?: boolean;
hasNextPage?: boolean; hasNextPage?: boolean;
loadNextPage?: () => void; loadNextPage?: () => void;
onNextPage: (page: number) => void; onNextPage: (page: number) => void;
@@ -47,7 +46,6 @@ const pageVariants: Variants = {
function BaseGridCarousel(props: GridCarouselProps) { function BaseGridCarousel(props: GridCarouselProps) {
const { const {
cards, cards,
enableRefresh = false,
hasNextPage, hasNextPage,
loadNextPage, loadNextPage,
onNextPage, onNextPage,
@@ -157,65 +155,45 @@ function BaseGridCarousel(props: GridCarouselProps) {
{cq.isCalculated && ( {cq.isCalculated && (
<> <>
<div className={styles.navigation}> <div className={styles.navigation}>
{typeof title === 'string' ? ( <Group gap="xs" justify="space-between" w="100%">
<Group gap="xs" justify="space-between" w="100%"> <Group gap="xs">
<Group gap="xs"> {typeof title === 'string' ? (
<TextTitle fw={700} isNoSelect order={3}> <TextTitle fw={700} isNoSelect order={3}>
{title} {title}
</TextTitle> </TextTitle>
{enableRefresh && onRefresh && ( ) : (
<ActionIcon title
icon="refresh" )}
iconProps={{ size: 'xs' }} {onRefresh && (
onClick={onRefresh}
size="xs"
tooltip={{ label: 'Refresh' }}
variant="transparent"
/>
)}
</Group>
<Group gap="xs" justify="end">
<ActionIcon <ActionIcon
disabled={isPrevDisabled} icon="refresh"
icon="arrowLeftS" iconProps={{ size: 'md' }}
iconProps={{ size: 'lg' }} onClick={onRefresh}
onClick={handlePrevPage}
size="xs" size="xs"
variant="subtle" tooltip={{ label: 'Refresh' }}
variant="transparent"
/> />
<ActionIcon )}
disabled={isNextDisabled}
icon="arrowRightS"
iconProps={{ size: 'lg' }}
onClick={handleNextPage}
size="xs"
variant="subtle"
/>
</Group>
</Group> </Group>
) : ( <Group gap="xs" justify="end">
<div className={styles.customTitleContainer}> <ActionIcon
<div className={styles.customTitleContent}>{title}</div> disabled={isPrevDisabled}
<Group gap="xs" justify="end"> icon="arrowLeftS"
<ActionIcon iconProps={{ size: 'lg' }}
disabled={isPrevDisabled} onClick={handlePrevPage}
icon="arrowLeftS" size="xs"
iconProps={{ size: 'lg' }} variant="subtle"
onClick={handlePrevPage} />
size="xs" <ActionIcon
variant="subtle" disabled={isNextDisabled}
/> icon="arrowRightS"
<ActionIcon iconProps={{ size: 'lg' }}
disabled={isNextDisabled} onClick={handleNextPage}
icon="arrowRightS" size="xs"
iconProps={{ size: 'lg' }} variant="subtle"
onClick={handleNextPage} />
size="xs" </Group>
variant="subtle" </Group>
/>
</Group>
</div>
)}
</div> </div>
<AnimatePresence custom={currentPage} initial={false} mode="wait"> <AnimatePresence custom={currentPage} initial={false} mode="wait">
<motion.div <motion.div
@@ -14,19 +14,6 @@
justify-content: space-between; justify-content: space-between;
} }
.custom-title-container {
display: flex;
gap: var(--theme-spacing-sm);
align-items: center;
justify-content: space-between;
width: 100%;
}
.custom-title-content {
flex: 1;
min-width: 0;
}
.grid { .grid {
display: grid; display: grid;
grid-template-columns: repeat(var(--cards-to-show, 2), minmax(0, 1fr)); grid-template-columns: repeat(var(--cards-to-show, 2), minmax(0, 1fr));
@@ -32,7 +32,6 @@ interface ItemCardControlsProps {
internalState?: ItemListStateActions; internalState?: ItemListStateActions;
item: Album | AlbumArtist | Artist | Playlist | Song | undefined; item: Album | AlbumArtist | Artist | Playlist | Song | undefined;
itemType: LibraryItem; itemType: LibraryItem;
showRating: boolean;
type?: 'compact' | 'default' | 'poster'; type?: 'compact' | 'default' | 'poster';
} }
@@ -73,29 +72,6 @@ const createPlayHandler =
return; return;
} }
const isSongItem =
itemType === LibraryItem.SONG ||
itemType === LibraryItem.PLAYLIST_SONG ||
(item as { _itemType: LibraryItem })._itemType === LibraryItem.SONG;
if (isSongItem && controls?.onDoubleClick && internalState) {
const rowId = internalState.extractRowId(item);
if (rowId) {
const index = internalState.findItemIndex(rowId);
return controls.onDoubleClick({
event: null,
index,
internalState,
item,
itemType,
meta: {
playType,
},
});
}
}
controls?.onPlay?.({ controls?.onPlay?.({
event: e, event: e,
internalState, internalState,
@@ -204,7 +180,6 @@ export const ItemCardControls = ({
internalState, internalState,
item, item,
itemType, itemType,
showRating,
type = 'default', type = 'default',
}: ItemCardControlsProps) => { }: ItemCardControlsProps) => {
const playNowHandler = useMemo( const playNowHandler = useMemo(
@@ -292,7 +267,6 @@ export const ItemCardControls = ({
<FavoriteButton isFavorite={isFavorite} onClick={favoriteHandler} /> <FavoriteButton isFavorite={isFavorite} onClick={favoriteHandler} />
)} )}
{controls?.onRating && {controls?.onRating &&
showRating &&
(item?._serverType === ServerType.NAVIDROME || (item?._serverType === ServerType.NAVIDROME ||
item?._serverType === ServerType.SUBSONIC) && ( item?._serverType === ServerType.SUBSONIC) && (
<RatingButton <RatingButton
@@ -3,7 +3,6 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
height: 100%;
padding: var(--theme-spacing-md); padding: var(--theme-spacing-md);
overflow: hidden; overflow: hidden;
user-select: none; user-select: none;
@@ -178,10 +177,10 @@
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 0; left: 0;
gap: 0;
width: 100%; width: 100%;
padding: var(--theme-spacing-xs); padding: var(--theme-spacing-xs);
background-color: alpha(var(--theme-colors-background), 0.5); text-shadow: 0 1px 2px rgb(0 0 0 / 80%);
background-color: rgb(0 0 0 / 50%);
backdrop-filter: blur(2px); backdrop-filter: blur(2px);
transform: translateY(0); transform: translateY(0);
transition: transition:
+121 -276
View File
@@ -1,13 +1,12 @@
import clsx from 'clsx'; import clsx from 'clsx';
import formatDuration from 'format-duration';
import { AnimatePresence } from 'motion/react'; import { AnimatePresence } from 'motion/react';
import { Fragment, memo, ReactNode, useCallback, useMemo, useState } from 'react'; import { Fragment, memo, ReactNode, useState } from 'react';
import { generatePath, Link } from 'react-router'; import { generatePath, Link } from 'react-router';
import styles from './item-card.module.css'; import styles from './item-card.module.css';
import i18n from '/@/i18n/i18n';
import { ItemCardControls } from '/@/renderer/components/item-card/item-card-controls'; import { ItemCardControls } from '/@/renderer/components/item-card/item-card-controls';
import { ItemImage } from '/@/renderer/components/item-image/item-image';
import { getDraggedItems } from '/@/renderer/components/item-list/helpers/get-dragged-items'; import { getDraggedItems } from '/@/renderer/components/item-list/helpers/get-dragged-items';
import { getTitlePath } from '/@/renderer/components/item-list/helpers/get-title-path'; import { getTitlePath } from '/@/renderer/components/item-list/helpers/get-title-path';
import { import {
@@ -16,20 +15,10 @@ import {
useItemSelectionState, useItemSelectionState,
} from '/@/renderer/components/item-list/helpers/item-list-state'; } from '/@/renderer/components/item-list/helpers/item-list-state';
import { ItemControls } from '/@/renderer/components/item-list/types'; import { ItemControls } from '/@/renderer/components/item-list/types';
import { JoinedArtists } from '/@/renderer/features/albums/components/joined-artists';
import { useDragDrop } from '/@/renderer/hooks/use-drag-drop'; import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { useShowRatings } from '/@/renderer/store'; import { formatDateAbsolute, formatDateRelative, formatRating } from '/@/renderer/utils/format';
import { import { Image } from '/@/shared/components/image/image';
formatDateAbsolute,
formatDateAbsoluteUTC,
formatDateRelative,
formatDurationString,
formatRating,
} from '/@/renderer/utils/format';
import { SEPARATOR_STRING } from '/@/shared/api/utils';
import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon';
import { Separator } from '/@/shared/components/separator/separator'; import { Separator } from '/@/shared/components/separator/separator';
import { Skeleton } from '/@/shared/components/skeleton/skeleton'; import { Skeleton } from '/@/shared/components/skeleton/skeleton';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
@@ -78,14 +67,13 @@ export const ItemCard = ({
type = 'poster', type = 'poster',
withControls, withControls,
}: ItemCardProps) => { }: ItemCardProps) => {
const showRatings = useShowRatings();
const imageUrl = getImageUrl(data); const imageUrl = getImageUrl(data);
const rows = providedRows || []; const rows = providedRows || [];
switch (type) { switch (type) {
case 'compact': case 'compact':
return ( return (
<MemoizedCompactItemCard <CompactItemCard
controls={controls} controls={controls}
data={data} data={data}
enableDrag={enableDrag} enableDrag={enableDrag}
@@ -96,13 +84,12 @@ export const ItemCard = ({
isRound={isRound} isRound={isRound}
itemType={itemType} itemType={itemType}
rows={rows} rows={rows}
showRating={showRatings}
withControls={withControls} withControls={withControls}
/> />
); );
case 'poster': case 'poster':
return ( return (
<MemoizedPosterItemCard <PosterItemCard
controls={controls} controls={controls}
data={data} data={data}
enableDrag={enableDrag} enableDrag={enableDrag}
@@ -113,14 +100,13 @@ export const ItemCard = ({
isRound={isRound} isRound={isRound}
itemType={itemType} itemType={itemType}
rows={rows} rows={rows}
showRating={showRatings}
withControls={withControls} withControls={withControls}
/> />
); );
case 'default': case 'default':
default: default:
return ( return (
<MemoizedDefaultItemCard <DefaultItemCard
controls={controls} controls={controls}
data={data} data={data}
enableDrag={enableDrag} enableDrag={enableDrag}
@@ -131,7 +117,6 @@ export const ItemCard = ({
isRound={isRound} isRound={isRound}
itemType={itemType} itemType={itemType}
rows={rows} rows={rows}
showRating={showRatings}
withControls={withControls} withControls={withControls}
/> />
); );
@@ -145,20 +130,18 @@ export interface ItemCardDerivativeProps extends Omit<ItemCardProps, 'type'> {
imageUrl: string | undefined; imageUrl: string | undefined;
internalState?: ItemListStateActions; internalState?: ItemListStateActions;
rows: DataRow[]; rows: DataRow[];
showRating: boolean;
} }
const CompactItemCard = ({ const CompactItemCard = ({
controls, controls,
data, data,
enableDrag,
enableExpansion, enableExpansion,
enableNavigation, enableNavigation,
imageUrl,
internalState, internalState,
isRound, isRound,
itemType, itemType,
rows, rows,
showRating,
withControls, withControls,
}: ItemCardDerivativeProps) => { }: ItemCardDerivativeProps) => {
const [showControls, setShowControls] = useState(false); const [showControls, setShowControls] = useState(false);
@@ -168,71 +151,6 @@ const CompactItemCard = ({
: undefined; : undefined;
const isSelected = useItemSelectionState(internalState, itemRowId || undefined); const isSelected = useItemSelectionState(internalState, itemRowId || undefined);
const getId = useCallback(() => {
if (!data) {
return [];
}
const draggedItems = getDraggedItems(data, internalState);
return draggedItems.map((item) => item.id);
}, [data, internalState]);
const getItem = useCallback(() => {
if (!data) {
return [];
}
const draggedItems = getDraggedItems(data, internalState);
return draggedItems;
}, [data, internalState]);
const onDragStart = useCallback(() => {
if (!data) {
return;
}
const draggedItems = getDraggedItems(data, internalState);
if (internalState) {
internalState.setDragging(draggedItems);
}
}, [data, internalState]);
const onDrop = useCallback(() => {
if (internalState) {
internalState.setDragging([]);
}
}, [internalState]);
const dragOperation = useMemo(
() =>
itemType === LibraryItem.QUEUE_SONG
? [DragOperation.REORDER, DragOperation.ADD]
: [DragOperation.ADD],
[itemType],
);
const drag = useMemo(
() => ({
getId,
getItem,
itemType,
onDragStart,
onDrop,
operation: dragOperation,
target: DragTarget.ALBUM,
}),
[getId, getItem, itemType, onDragStart, onDrop, dragOperation],
);
const { isDragging: isDraggingLocal, ref } = useDragDrop<HTMLDivElement>({
drag,
isEnabled: !!enableDrag && !!data,
});
const itemId = data && internalState ? data.id : undefined;
const isDraggingState = useItemDraggingState(internalState, itemId);
const isDragging = isDraggingState || isDraggingLocal;
const handleClick = useDoubleClick({ const handleClick = useDoubleClick({
onDoubleClick: (e: React.MouseEvent<HTMLDivElement>) => { onDoubleClick: (e: React.MouseEvent<HTMLDivElement>) => {
if (!data || !controls || !internalState) { if (!data || !controls || !internalState) {
@@ -321,7 +239,7 @@ const CompactItemCard = ({
typeof (data as { userRating: null | number }).userRating === 'number' typeof (data as { userRating: null | number }).userRating === 'number'
? (data as { userRating: null | number }).userRating ? (data as { userRating: null | number }).userRating
: null; : null;
const hasRating = showRating && userRating !== null && userRating > 0; const hasRating = userRating !== null && userRating > 0;
const imageContainerClassName = clsx(styles.imageContainer, { const imageContainerClassName = clsx(styles.imageContainer, {
[styles.isRound]: isRound, [styles.isRound]: isRound,
@@ -329,26 +247,21 @@ const CompactItemCard = ({
const imageContainerContent = ( const imageContainerContent = (
<> <>
<ItemImage <Image
className={clsx(styles.image, { className={clsx(styles.image, {
[styles.isRound]: isRound, [styles.isRound]: isRound,
})} })}
id={data?.imageId} src={imageUrl}
itemType={itemType}
src={(data as Album | AlbumArtist | Playlist | Song)?.imageUrl}
type="itemCard"
/> />
{isFavorite && <div className={styles.favoriteBadge} />} {isFavorite && <div className={styles.favoriteBadge} />}
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>} {hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
<AnimatePresence> <AnimatePresence>
{withControls && showControls && data && ( {withControls && showControls && (
<ItemCardControls <ItemCardControls
controls={controls} controls={controls}
enableExpansion={enableExpansion} enableExpansion={enableExpansion}
internalState={internalState}
item={data} item={data}
itemType={itemType} itemType={itemType}
showRating={showRating}
type="compact" type="compact"
/> />
)} )}
@@ -375,10 +288,8 @@ const CompactItemCard = ({
return ( return (
<div <div
className={clsx(styles.container, styles.compact, { className={clsx(styles.container, styles.compact, {
[styles.dragging]: isDragging,
[styles.selected]: isSelected, [styles.selected]: isSelected,
})} })}
ref={ref}
> >
{enableNavigation && navigationPath && !internalState ? ( {enableNavigation && navigationPath && !internalState ? (
<Link <Link
@@ -440,11 +351,11 @@ const DefaultItemCard = ({
data, data,
enableExpansion, enableExpansion,
enableNavigation, enableNavigation,
imageUrl,
internalState, internalState,
isRound, isRound,
itemType, itemType,
rows, rows,
showRating,
withControls, withControls,
}: ItemCardDerivativeProps) => { }: ItemCardDerivativeProps) => {
const [showControls, setShowControls] = useState(false); const [showControls, setShowControls] = useState(false);
@@ -546,16 +457,13 @@ const DefaultItemCard = ({
typeof (data as { userRating: null | number }).userRating === 'number' typeof (data as { userRating: null | number }).userRating === 'number'
? (data as { userRating: null | number }).userRating ? (data as { userRating: null | number }).userRating
: null; : null;
const hasRating = showRating && userRating !== null && userRating > 0; const hasRating = userRating !== null && userRating > 0;
const imageContainerContent = ( const imageContainerContent = (
<> <>
<ItemImage <Image
className={clsx(styles.image, { [styles.isRound]: isRound })} className={clsx(styles.image, { [styles.isRound]: isRound })}
id={data?.imageId} src={imageUrl}
itemType={itemType}
src={(data as Album | AlbumArtist | Playlist | Song)?.imageUrl}
type="itemCard"
/> />
{isFavorite && <div className={styles.favoriteBadge} />} {isFavorite && <div className={styles.favoriteBadge} />}
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>} {hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
@@ -566,7 +474,6 @@ const DefaultItemCard = ({
enableExpansion={enableExpansion} enableExpansion={enableExpansion}
item={data} item={data}
itemType={itemType} itemType={itemType}
showRating={showRating}
type="default" type="default"
/> />
)} )}
@@ -656,11 +563,11 @@ const PosterItemCard = ({
enableDrag, enableDrag,
enableExpansion, enableExpansion,
enableNavigation, enableNavigation,
imageUrl,
internalState, internalState,
isRound, isRound,
itemType, itemType,
rows, rows,
showRating,
withControls, withControls,
}: ItemCardDerivativeProps) => { }: ItemCardDerivativeProps) => {
const [showControls, setShowControls] = useState(false); const [showControls, setShowControls] = useState(false);
@@ -670,64 +577,46 @@ const PosterItemCard = ({
: undefined; : undefined;
const isSelected = useItemSelectionState(internalState, itemRowId || undefined); const isSelected = useItemSelectionState(internalState, itemRowId || undefined);
const getId = useCallback(() => {
if (!data) {
return [];
}
const draggedItems = getDraggedItems(data, internalState);
return draggedItems.map((item) => item.id);
}, [data, internalState]);
const getItem = useCallback(() => {
if (!data) {
return [];
}
const draggedItems = getDraggedItems(data, internalState);
return draggedItems;
}, [data, internalState]);
const onDragStart = useCallback(() => {
if (!data) {
return;
}
const draggedItems = getDraggedItems(data, internalState);
if (internalState) {
internalState.setDragging(draggedItems);
}
}, [data, internalState]);
const onDrop = useCallback(() => {
if (internalState) {
internalState.setDragging([]);
}
}, [internalState]);
const dragOperation = useMemo(
() =>
itemType === LibraryItem.QUEUE_SONG
? [DragOperation.REORDER, DragOperation.ADD]
: [DragOperation.ADD],
[itemType],
);
const drag = useMemo(
() => ({
getId,
getItem,
itemType,
onDragStart,
onDrop,
operation: dragOperation,
target: DragTarget.ALBUM,
}),
[getId, getItem, itemType, onDragStart, onDrop, dragOperation],
);
const { isDragging: isDraggingLocal, ref } = useDragDrop<HTMLDivElement>({ const { isDragging: isDraggingLocal, ref } = useDragDrop<HTMLDivElement>({
drag, drag: {
getId: () => {
if (!data) {
return [];
}
const draggedItems = getDraggedItems(data, internalState);
return draggedItems.map((item) => item.id);
},
getItem: () => {
if (!data) {
return [];
}
const draggedItems = getDraggedItems(data, internalState);
return draggedItems;
},
itemType,
onDragStart: () => {
if (!data) {
return;
}
const draggedItems = getDraggedItems(data, internalState);
if (internalState) {
internalState.setDragging(draggedItems);
}
},
onDrop: () => {
if (internalState) {
internalState.setDragging([]);
}
},
operation:
itemType === LibraryItem.QUEUE_SONG
? [DragOperation.REORDER, DragOperation.ADD]
: [DragOperation.ADD],
target: DragTarget.ALBUM,
},
isEnabled: !!enableDrag && !!data, isEnabled: !!enableDrag && !!data,
}); });
@@ -827,16 +716,13 @@ const PosterItemCard = ({
typeof (data as { userRating: null | number }).userRating === 'number' typeof (data as { userRating: null | number }).userRating === 'number'
? (data as { userRating: null | number }).userRating ? (data as { userRating: null | number }).userRating
: null; : null;
const hasRating = showRating && userRating !== null && userRating > 0; const hasRating = userRating !== null && userRating > 0;
const imageContainerContent = ( const imageContainerContent = (
<> <>
<ItemImage <Image
className={clsx(styles.image, { [styles.isRound]: isRound })} className={clsx(styles.image, { [styles.isRound]: isRound })}
id={(data as { imageId: string })?.imageId} src={imageUrl}
itemType={itemType}
src={(data as { imageUrl: string })?.imageUrl}
type="itemCard"
/> />
{isFavorite && <div className={styles.favoriteBadge} />} {isFavorite && <div className={styles.favoriteBadge} />}
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>} {hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
@@ -848,7 +734,6 @@ const PosterItemCard = ({
internalState={internalState} internalState={internalState}
item={data} item={data}
itemType={itemType} itemType={itemType}
showRating={showRating}
type="poster" type="poster"
/> />
)} )}
@@ -936,16 +821,7 @@ const PosterItemCard = ({
); );
}; };
const MemoizedPosterItemCard = memo(PosterItemCard); export const getDataRows = (): DataRow[] => {
MemoizedPosterItemCard.displayName = 'MemoizedPosterItemCard';
const MemoizedCompactItemCard = memo(CompactItemCard);
MemoizedCompactItemCard.displayName = 'MemoizedCompactItemCard';
const MemoizedDefaultItemCard = memo(DefaultItemCard);
MemoizedDefaultItemCard.displayName = 'MemoizedDefaultItemCard';
export const getDataRows = (type?: 'compact' | 'default' | 'poster'): DataRow[] => {
return [ return [
{ {
format: (data) => { format: (data) => {
@@ -1003,18 +879,21 @@ export const getDataRows = (type?: 'compact' | 'default' | 'poster'): DataRow[]
{ {
format: (data) => { format: (data) => {
if ('albumArtists' in data && Array.isArray(data.albumArtists)) { if ('albumArtists' in data && Array.isArray(data.albumArtists)) {
return ( return (data as Album | Song).albumArtists.map((artist, index) => (
<JoinedArtists <Fragment key={artist.id}>
artistName={data.albumArtistName} <Link
artists={data.albumArtists} state={{ item: artist }}
linkProps={{ fw: 400, isMuted: true }} to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
rootTextProps={{ albumArtistId: artist.id,
fw: 400, })}
isMuted: type === 'compact' ? false : true, >
size: 'sm', {artist.name}
}} </Link>
/> {index < (data as Album | Song).albumArtists.length - 1 && (
); <Separator />
)}
</Fragment>
));
} }
return ''; return '';
}, },
@@ -1046,7 +925,7 @@ export const getDataRows = (type?: 'compact' | 'default' | 'poster'): DataRow[]
{ {
format: (data) => { format: (data) => {
if ('duration' in data && data.duration !== null) { if ('duration' in data && data.duration !== null) {
return formatDurationString(data.duration); return formatDuration(data.duration * 1000);
} }
return ''; return '';
}, },
@@ -1055,17 +934,7 @@ export const getDataRows = (type?: 'compact' | 'default' | 'poster'): DataRow[]
{ {
format: (data) => { format: (data) => {
if ('releaseYear' in data && data.releaseYear !== null) { if ('releaseYear' in data && data.releaseYear !== null) {
const releaseYear = data.releaseYear; return String(data.releaseYear);
const originalYear =
'originalYear' in data && data.originalYear !== null
? data.originalYear
: null;
if (originalYear !== null && originalYear !== releaseYear) {
return `${originalYear}${SEPARATOR_STRING}${releaseYear}`;
}
return String(releaseYear);
} }
return ''; return '';
}, },
@@ -1074,15 +943,7 @@ export const getDataRows = (type?: 'compact' | 'default' | 'poster'): DataRow[]
{ {
format: (data) => { format: (data) => {
if ('releaseDate' in data && data.releaseDate) { if ('releaseDate' in data && data.releaseDate) {
if ( return data.releaseDate;
'originalDate' in data &&
data.originalDate &&
data.originalDate !== data.releaseDate
) {
return `${formatDateAbsoluteUTC(data.originalDate)}${SEPARATOR_STRING}${formatDateAbsoluteUTC(data.releaseDate)}`;
}
return `${formatDateAbsoluteUTC(data.releaseDate)}`;
} }
return ''; return '';
}, },
@@ -1100,12 +961,7 @@ export const getDataRows = (type?: 'compact' | 'default' | 'poster'): DataRow[]
{ {
format: (data) => { format: (data) => {
if ('lastPlayedAt' in data && data.lastPlayedAt) { if ('lastPlayedAt' in data && data.lastPlayedAt) {
return ( return formatDateRelative(data.lastPlayedAt);
<Group align="center" gap="xs">
<Icon icon="lastPlayed" size="sm" />
{formatDateRelative(data.lastPlayedAt)}
</Group>
);
} }
return ''; return '';
}, },
@@ -1114,7 +970,7 @@ export const getDataRows = (type?: 'compact' | 'default' | 'poster'): DataRow[]
{ {
format: (data) => { format: (data) => {
if ('playCount' in data && data.playCount !== null) { if ('playCount' in data && data.playCount !== null) {
return i18n.t('entity.play', { count: data.playCount }); return String(data.playCount);
} }
return ''; return '';
}, },
@@ -1163,7 +1019,7 @@ export const getDataRows = (type?: 'compact' | 'default' | 'poster'): DataRow[]
{ {
format: (data) => { format: (data) => {
if ('songCount' in data && data.songCount !== null) { if ('songCount' in data && data.songCount !== null) {
return i18n.t('entity.trackWithCount', { count: data.songCount }); return String(data.songCount);
} }
return ''; return '';
}, },
@@ -1172,7 +1028,7 @@ export const getDataRows = (type?: 'compact' | 'default' | 'poster'): DataRow[]
{ {
format: (data) => { format: (data) => {
if ('albumCount' in data && data.albumCount !== null) { if ('albumCount' in data && data.albumCount !== null) {
return i18n.t('entity.albumWithCount', { count: data.albumCount }); return String(data.albumCount);
} }
return ''; return '';
}, },
@@ -1227,67 +1083,56 @@ const getItemNavigationPath = (
return getTitlePath(effectiveItemType, data.id); return getTitlePath(effectiveItemType, data.id);
}; };
const ItemCardRow = memo( const ItemCardRow = ({
({ data,
data, index,
index, row,
row, type,
type, }: {
}: { data: Album | AlbumArtist | Artist | Playlist | Song | undefined;
data: Album | AlbumArtist | Artist | Playlist | Song | undefined; index: number;
index: number; row: DataRow;
row: DataRow; type?: 'compact' | 'default' | 'poster';
type?: 'compact' | 'default' | 'poster'; }) => {
}) => { const alignmentClass =
const alignmentClass = row.align === 'center'
row.align === 'center' ? styles['align-center']
? styles['align-center'] : row.align === 'end'
: row.align === 'end' ? styles['align-end']
? styles['align-end'] : styles['align-start'];
: styles['align-start'];
// All rows except the first one (index 0) should be muted // All rows except the first one (index 0) should be muted
const isMuted = index > 0 || row.isMuted; const isMuted = index > 0 || row.isMuted;
const formattedContent = useMemo(() => {
if (!data) {
return null;
}
return row.format(data);
}, [data, row]);
if (!data) {
return (
<div
className={clsx(styles.row, alignmentClass, {
[styles.compact]: type === 'compact',
[styles.default]: type === 'default',
[styles.muted]: isMuted,
[styles.poster]: type === 'poster',
})}
>
&nbsp;
</div>
);
}
if (!data) {
return ( return (
<Text <div
className={clsx(styles.row, alignmentClass, { className={clsx(styles.row, alignmentClass, {
[styles.bold]: index === 0,
[styles.compact]: type === 'compact', [styles.compact]: type === 'compact',
[styles.default]: type === 'default', [styles.default]: type === 'default',
[styles.muted]: isMuted, [styles.muted]: isMuted,
[styles.poster]: type === 'poster', [styles.poster]: type === 'poster',
})} })}
size={index > 0 ? 'sm' : 'md'}
> >
{formattedContent} &nbsp;
</Text> </div>
); );
}, }
);
ItemCardRow.displayName = 'ItemCardRow'; return (
<Text
className={clsx(styles.row, alignmentClass, {
[styles.bold]: index === 0,
[styles.compact]: type === 'compact',
[styles.default]: type === 'default',
[styles.muted]: isMuted,
[styles.poster]: type === 'poster',
})}
size={index > 0 ? 'sm' : 'md'}
>
{row.format(data)}
</Text>
);
};
export const MemoizedItemCard = memo(ItemCard); export const MemoizedItemCard = memo(ItemCard);
@@ -1,146 +1,146 @@
// import { AnimatePresence } from 'motion/react'; import { AnimatePresence } from 'motion/react';
// import { MouseEvent, useMemo, useState } from 'react'; import { MouseEvent, useMemo, useState } from 'react';
// import { Link } from 'react-router'; import { Link } from 'react-router';
// import styles from './item-detail.module.css'; import styles from './item-detail.module.css';
// import { ItemCardControls } from '/@/renderer/components/item-card/item-card-controls'; import { ItemCardControls } from '/@/renderer/components/item-card/item-card-controls';
// import { useFastAverageColor } from '/@/renderer/hooks'; import { useFastAverageColor } from '/@/renderer/hooks';
// import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
// import { Badge } from '/@/shared/components/badge/badge'; import { Badge } from '/@/shared/components/badge/badge';
// import { Divider } from '/@/shared/components/divider/divider'; import { Divider } from '/@/shared/components/divider/divider';
// import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
// import { Image } from '/@/shared/components/image/image'; import { Image } from '/@/shared/components/image/image';
// import { Rating } from '/@/shared/components/rating/rating'; import { Rating } from '/@/shared/components/rating/rating';
// import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
// import { import {
// Album, Album,
// AlbumArtist, AlbumArtist,
// Artist, Artist,
// LibraryItem, LibraryItem,
// Playlist, Playlist,
// Song, Song,
// } from '/@/shared/types/domain-types'; } from '/@/shared/types/domain-types';
// import { stringToColor } from '/@/shared/utils/string-to-color'; import { stringToColor } from '/@/shared/utils/string-to-color';
// interface ItemDetailProps { interface ItemDetailProps {
// data: Album | AlbumArtist | Artist | Playlist | Song | undefined; data: Album | AlbumArtist | Artist | Playlist | Song | undefined;
// itemHeight: number; itemHeight: number;
// itemType: LibraryItem; itemType: LibraryItem;
// onClick?: (e: MouseEvent<HTMLDivElement>, item: unknown, itemType: LibraryItem) => void; onClick?: (e: MouseEvent<HTMLDivElement>, item: unknown, itemType: LibraryItem) => void;
// withControls?: boolean; withControls?: boolean;
// } }
// export const ItemDetail = ({ data, itemType, onClick, withControls }: ItemDetailProps) => { export const ItemDetail = ({ data, itemType, onClick, withControls }: ItemDetailProps) => {
// const imageUrl = getImageUrl(data); const imageUrl = getImageUrl(data);
// const [showControls, setShowControls] = useState(false); const [showControls, setShowControls] = useState(false);
// const { background } = useFastAverageColor({ const { background } = useFastAverageColor({
// algorithm: 'simple', algorithm: 'simple',
// src: imageUrl, src: imageUrl,
// srcLoaded: false, srcLoaded: false,
// }); });
// // const tags = [...(data?.genres ?? [])]; // const tags = [...(data?.genres ?? [])];
// const tags = useMemo(() => { const tags = useMemo(() => {
// if (!data) { if (!data) {
// return []; return [];
// } }
// const items: { const items: {
// color?: string; color?: string;
// id: string; id: string;
// isLight?: boolean; isLight?: boolean;
// itemType: LibraryItem; itemType: LibraryItem;
// name: string; name: string;
// }[] = []; }[] = [];
// if ('albumArtists' in data && Array.isArray(data.albumArtists)) { if ('albumArtists' in data && Array.isArray(data.albumArtists)) {
// data.albumArtists?.forEach((tag: { id: string; name: string }) => { data.albumArtists?.forEach((tag: { id: string; name: string }) => {
// items.push({ id: tag.id, itemType: LibraryItem.ALBUM_ARTIST, name: tag.name }); items.push({ id: tag.id, itemType: LibraryItem.ALBUM_ARTIST, name: tag.name });
// }); });
// } }
// if ('genres' in data && Array.isArray(data.genres)) { if ('genres' in data && Array.isArray(data.genres)) {
// data.genres?.forEach((tag: { id: string; itemType: LibraryItem; name: string }) => { data.genres?.forEach((tag: { id: string; itemType: LibraryItem; name: string }) => {
// const { color, isLight } = stringToColor(tag.name); const { color, isLight } = stringToColor(tag.name);
// items.push({ ...tag, color, isLight }); items.push({ ...tag, color, isLight });
// }); });
// } }
// // if ('tags' in data && typeof data.tags === 'object') { // if ('tags' in data && typeof data.tags === 'object') {
// // console.log('data.tags :>> ', data.tags); // console.log('data.tags :>> ', data.tags);
// // Object.entries(data.tags).forEach(([key, value]) => { // Object.entries(data.tags).forEach(([key, value]) => {
// // items.push({ id: key, itemType: LibraryItem.TAG, name: value }); // items.push({ id: key, itemType: LibraryItem.TAG, name: value });
// // }); // });
// // } // }
// return items; return items;
// }, [data]); }, [data]);
// return ( return (
// <div <div
// className={styles.container} className={styles.container}
// onClick={(e) => onClick?.(e, data, itemType)} onClick={(e) => onClick?.(e, data, itemType)}
// style={{ backgroundColor: background }} style={{ backgroundColor: background }}
// > >
// <div <div
// className={styles.imageContainer} className={styles.imageContainer}
// onMouseEnter={() => withControls && setShowControls(true)} onMouseEnter={() => withControls && setShowControls(true)}
// onMouseLeave={() => withControls && setShowControls(false)} onMouseLeave={() => withControls && setShowControls(false)}
// > >
// <Image alt={data?.name} src={imageUrl} /> <Image alt={data?.name} src={imageUrl} />
// <AnimatePresence> <AnimatePresence>
// {withControls && showControls && <ItemCardControls type="compact" />} {withControls && showControls && <ItemCardControls type="compact" />}
// </AnimatePresence> </AnimatePresence>
// </div> </div>
// <div className={styles.metadataContainer}> <div className={styles.metadataContainer}>
// <div className={styles.header}> <div className={styles.header}>
// <Text className={styles.title} component={Link} isLink size="lg" weight={500}> <Text className={styles.title} component={Link} isLink size="lg" weight={500}>
// {data?.name} {data?.name}
// </Text> </Text>
// <Group> <Group>
// {data && 'userRating' in data && ( {data && 'userRating' in data && (
// <Rating size="xs" value={data?.userRating ?? 0} /> <Rating size="xs" value={data?.userRating ?? 0} />
// )} )}
// {data && 'userFavorite' in data && ( {data && 'userFavorite' in data && (
// <ActionIcon <ActionIcon
// icon="favorite" icon="favorite"
// iconProps={{ iconProps={{
// fill: data?.userFavorite ? 'primary' : 'default', fill: data?.userFavorite ? 'primary' : 'default',
// }} }}
// size="xs" size="xs"
// /> />
// )} )}
// </Group> </Group>
// </div> </div>
// <Divider /> <Divider />
// <div className={styles.content}> <div className={styles.content}>
// <Group className={styles.tags} gap="xs"> <Group className={styles.tags} gap="xs">
// {tags.map((tag) => ( {tags.map((tag) => (
// <Badge <Badge
// key={tag.id} key={tag.id}
// style={{ style={{
// backgroundColor: tag.color, backgroundColor: tag.color,
// color: tag.isLight ? 'black' : 'white', color: tag.isLight ? 'black' : 'white',
// }} }}
// > >
// {tag.name} {tag.name}
// </Badge> </Badge>
// ))} ))}
// </Group> </Group>
// </div> </div>
// </div> </div>
// </div> </div>
// ); );
// }; };
// const getImageUrl = (data: Album | AlbumArtist | Artist | Playlist | Song | undefined) => { const getImageUrl = (data: Album | AlbumArtist | Artist | Playlist | Song | undefined) => {
// if (data && 'imageUrl' in data) { if (data && 'imageUrl' in data) {
// return data.imageUrl || undefined; return data.imageUrl || undefined;
// } }
// return undefined; return undefined;
// }; };
@@ -1,139 +0,0 @@
import { memo, useMemo } from 'react';
import z from 'zod';
import { api } from '/@/renderer/api';
import {
GeneralSettingsSchema,
getServerById,
useAuthStore,
useCurrentServerId,
useImageRes,
useSettingsStore,
} from '/@/renderer/store';
import { BaseImage, ImageProps } from '/@/shared/components/image/image';
import { LibraryItem } from '/@/shared/types/domain-types';
const getUnloaderIcon = (itemType: LibraryItem) => {
switch (itemType) {
case LibraryItem.ALBUM:
return 'emptyAlbumImage';
case LibraryItem.ALBUM_ARTIST:
return 'emptyArtistImage';
case LibraryItem.ARTIST:
return 'emptyArtistImage';
case LibraryItem.GENRE:
return 'emptyGenreImage';
case LibraryItem.PLAYLIST:
return 'emptyPlaylistImage';
case LibraryItem.SONG:
return 'emptySongImage';
default:
return 'emptyImage';
}
};
const BaseItemImage = (
props: Omit<ImageProps, 'id' | 'src'> & {
id?: null | string;
itemType: LibraryItem;
src?: null | string;
type?: keyof z.infer<typeof GeneralSettingsSchema>['imageRes'];
},
) => {
const { src, ...rest } = props;
const imageUrl = useItemImageUrl({
id: props.id,
imageUrl: src,
itemType: props.itemType,
type: props.type,
});
return (
<BaseImage
src={imageUrl}
unloaderIcon={getUnloaderIcon(props.itemType)}
{...rest}
id={props.id || undefined}
/>
);
};
export const ItemImage = memo(BaseItemImage);
interface UseItemImageUrlProps {
id?: null | string;
imageUrl?: null | string;
itemType: LibraryItem;
serverId?: string;
size?: number;
type?: keyof z.infer<typeof GeneralSettingsSchema>['imageRes'];
useRemoteUrl?: boolean;
}
export const useItemImageUrl = (args: UseItemImageUrlProps) => {
const { id, imageUrl, itemType, size, type, useRemoteUrl } = args;
const serverId = useCurrentServerId();
const imageRes = useImageRes();
const sizeByType: number | undefined = type ? imageRes[type] : undefined;
return useMemo(() => {
if (imageUrl) {
return imageUrl;
}
if (!id) {
return undefined;
}
const targetServerId = args.serverId || serverId;
let baseUrl: string | undefined;
if (useRemoteUrl) {
const server = getServerById(targetServerId);
baseUrl = server?.remoteUrl || server?.url;
}
return (
api.controller.getImageUrl({
apiClientProps: { serverId: targetServerId },
baseUrl,
query: { id, itemType, size: size ?? sizeByType },
}) || undefined
);
}, [args.serverId, id, imageUrl, itemType, serverId, size, sizeByType, useRemoteUrl]);
};
export function getItemImageUrl(args: UseItemImageUrlProps) {
const { id, imageUrl, itemType, size, type, useRemoteUrl } = args;
const authStore = useAuthStore.getState();
const currentServerId = authStore.currentServer?.id;
const serverId = (args.serverId || currentServerId) as string;
const imageRes = useSettingsStore.getState().general.imageRes;
const sizeByType: number | undefined = type ? imageRes[type] : undefined;
if (imageUrl) {
return imageUrl;
}
if (!id) {
return undefined;
}
let baseUrl: string | undefined;
if (useRemoteUrl) {
const server = getServerById(serverId);
baseUrl = server?.remoteUrl || server?.url;
}
return (
api.controller.getImageUrl({
apiClientProps: { serverId },
baseUrl,
query: { id, itemType, size: size ?? sizeByType },
}) || undefined
);
}
@@ -191,7 +191,7 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
onColumnResized?.(columnId, width); onColumnResized?.(columnId, width);
}, },
onDoubleClick: ({ internalState, item, itemType, meta }: DefaultItemControlProps) => { onDoubleClick: ({ internalState, item, itemType }: DefaultItemControlProps) => {
if (!item || !internalState) { if (!item || !internalState) {
return; return;
} }
@@ -212,7 +212,7 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
} }
} }
if (itemType === LibraryItem.SONG || itemType === LibraryItem.PLAYLIST_SONG) { if (itemType === LibraryItem.SONG) {
const data = internalState.getData(); const data = internalState.getData();
const validSongs = data.filter((d): d is Song => { const validSongs = data.filter((d): d is Song => {
if (!d || typeof d !== 'object') { if (!d || typeof d !== 'object') {
@@ -235,31 +235,17 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
return; return;
} }
const playType = (meta?.playType as Play) || Play.NOW; const songsBefore = 100;
const songsAfter = 100;
// For NEXT, LAST, NEXT_SHUFFLE, and LAST_SHUFFLE, only add the clicked song const startIndex = Math.max(0, clickedIndex - songsBefore);
// For NOW and SHUFFLE, add a range of songs around the clicked song const endIndex = Math.min(validSongs.length, clickedIndex + songsAfter + 1);
let songsToAdd: Song[]; const songsToAdd = validSongs.slice(startIndex, endIndex);
if (
playType === Play.NEXT ||
playType === Play.LAST ||
playType === Play.NEXT_SHUFFLE ||
playType === Play.LAST_SHUFFLE
) {
songsToAdd = [item as Song];
} else {
const songsBefore = 50;
const songsAfter = 50;
const startIndex = Math.max(0, clickedIndex - songsBefore);
const endIndex = Math.min(validSongs.length, clickedIndex + songsAfter + 1);
songsToAdd = validSongs.slice(startIndex, endIndex);
}
if (songsToAdd.length === 0) { if (songsToAdd.length === 0) {
return; return;
} }
player.addToQueueByData(songsToAdd, playType, item.id); player.addToQueueByData(songsToAdd, Play.NOW, item.id);
return; return;
} }
@@ -13,7 +13,7 @@ import { eventEmitter } from '/@/renderer/events/event-emitter';
import { UserFavoriteEventPayload, UserRatingEventPayload } from '/@/renderer/events/events'; import { UserFavoriteEventPayload, UserRatingEventPayload } from '/@/renderer/events/events';
import { LibraryItem } from '/@/shared/types/domain-types'; import { LibraryItem } from '/@/shared/types/domain-types';
export const getListQueryKeyName = (itemType: LibraryItem): string => { const getQueryKeyName = (itemType: LibraryItem): string => {
switch (itemType) { switch (itemType) {
case LibraryItem.ALBUM: case LibraryItem.ALBUM:
return 'albums'; return 'albums';
@@ -115,7 +115,7 @@ export const useItemListInfiniteLoader = ({
return result; return result;
}, },
queryKey: queryKeys[getListQueryKeyName(itemType)].list(serverId, queryParams), queryKey: queryKeys[getQueryKeyName(itemType)].list(serverId, queryParams),
}); });
const endIndex = startIndex + itemsPerPage; const endIndex = startIndex + itemsPerPage;
@@ -186,9 +186,10 @@ export const useItemListInfiniteLoader = ({
lastFetchedPageRef.current = -1; lastFetchedPageRef.current = -1;
currentVisibleRangeRef.current = null; currentVisibleRangeRef.current = null;
// Invalidate and wait for count query to refetch // Invalidate and wait for count query to refetch (this will suspend via useSuspenseQuery)
await queryClient.ensureQueryData({ await queryClient.refetchQueries({
queryKey: countQueryKey, queryKey: countQueryKey,
type: 'active',
}); });
// Fetch the first page after count is refetched // Fetch the first page after count is refetched
@@ -6,11 +6,8 @@ import { LibraryItem } from '/@/shared/types/domain-types';
import { TableColumn } from '/@/shared/types/types'; import { TableColumn } from '/@/shared/types/types';
import { ItemListKey } from '/@/shared/types/types'; import { ItemListKey } from '/@/shared/types/types';
const getDefaultRowsForItemType = ( const getDefaultRowsForItemType = (itemType: LibraryItem): DataRow[] => {
itemType: LibraryItem, const allRows = getDataRows();
type?: 'compact' | 'default' | 'poster',
): DataRow[] => {
const allRows = getDataRows(type);
const rowMap = new Map(allRows.map((row) => [row.id, row])); const rowMap = new Map(allRows.map((row) => [row.id, row]));
switch (itemType) { switch (itemType) {
@@ -83,22 +80,16 @@ const getRowIdFromTableColumn = (tableColumn: TableColumn): null | string => {
return columnToRowIdMap[tableColumn] || null; return columnToRowIdMap[tableColumn] || null;
}; };
export const useGridRows = ( export const useGridRows = (itemType: LibraryItem, listKey?: ItemListKey) => {
itemType: LibraryItem,
listKey?: ItemListKey,
size?: 'compact' | 'default' | 'large',
) => {
const gridRowsConfig = useSettingsStore((state) => const gridRowsConfig = useSettingsStore((state) =>
listKey ? state.lists[listKey]?.grid?.rows : undefined, listKey ? state.lists[listKey]?.grid?.rows : undefined,
); );
const type: 'compact' | 'default' | 'poster' = size === 'compact' ? 'compact' : 'poster';
return useMemo(() => { return useMemo(() => {
const allRows = getDataRows(type); const allRows = getDataRows();
if (!listKey || !gridRowsConfig || gridRowsConfig.length === 0) { if (!listKey || !gridRowsConfig || gridRowsConfig.length === 0) {
const defaultRows = getDefaultRowsForItemType(itemType, type); const defaultRows = getDefaultRowsForItemType(itemType);
return defaultRows.length > 0 ? defaultRows : allRows; return defaultRows.length > 0 ? defaultRows : allRows;
} }
@@ -119,5 +110,5 @@ export const useGridRows = (
.filter((row): row is NonNullable<typeof row> => row !== null && row !== undefined); .filter((row): row is NonNullable<typeof row> => row !== null && row !== undefined);
return configuredRows.length > 0 ? configuredRows : allRows; return configuredRows.length > 0 ? configuredRows : allRows;
}, [itemType, listKey, gridRowsConfig, type]); }, [itemType, listKey, gridRowsConfig]);
}; };
@@ -1,26 +0,0 @@
import { useIsFetching } from '@tanstack/react-query';
import { queryKeys } from '/@/renderer/api/query-keys';
import { getListQueryKeyName } from '/@/renderer/components/item-list/helpers/item-list-infinite-loader';
import { useCurrentServerId } from '/@/renderer/store';
import { LibraryItem } from '/@/shared/types/domain-types';
export const useIsFetchingItemListCount = ({ itemType }: { itemType: LibraryItem }) => {
const serverId = useCurrentServerId();
const isFetching = useIsFetching({
queryKey: queryKeys[getListQueryKeyName(itemType)].count(serverId),
});
return isFetching > 0;
};
export const useIsFetchingItemList = ({ itemType }: { itemType: LibraryItem }) => {
const serverId = useCurrentServerId();
const isFetching = useIsFetching({
queryKey: queryKeys[getListQueryKeyName(itemType)].list(serverId),
});
return isFetching > 0;
};
@@ -1,123 +0,0 @@
import { useNavigate } from 'react-router';
import { getTitlePath } from '/@/renderer/components/item-list/helpers/get-title-path';
import {
ItemListStateActions,
ItemListStateItemWithRequiredProperties,
} from '/@/renderer/components/item-list/helpers/item-list-state';
import { ItemControls } from '/@/renderer/components/item-list/types';
import { useHotkeySettings, usePlayButtonBehavior } from '/@/renderer/store';
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
import { LibraryItem } from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types';
export const useListHotkeys = ({
controls,
focused,
internalState,
itemType,
}: {
controls: ItemControls;
focused: boolean;
internalState: ItemListStateActions;
itemType: LibraryItem;
}) => {
const { bindings } = useHotkeySettings();
const playButtonBehavior = usePlayButtonBehavior();
const navigate = useNavigate();
// Helper to check if item has required properties
const hasRequiredStateItemProperties = (
item: unknown,
): item is ItemListStateItemWithRequiredProperties => {
return (
typeof item === 'object' &&
item !== null &&
'id' in item &&
typeof (item as any).id === 'string' &&
'_serverId' in item &&
typeof (item as any)._serverId === 'string' &&
'_itemType' in item &&
typeof (item as any)._itemType === 'string'
);
};
useHotkeys([
[
'mod+a',
() => {
if (focused) {
if (internalState.isAllSelected()) {
internalState.deselectAll();
} else {
internalState.selectAll();
}
}
},
],
[
bindings.listPlayDefault.hotkey,
() => {
if (!focused) return;
const selected = internalState.getSelected();
const validSelected = selected.filter(hasRequiredStateItemProperties);
if (validSelected.length === 0) return;
const item = validSelected[0];
const playType = playButtonBehavior;
controls.onPlay?.({ item, itemType, playType } as any);
},
],
[
bindings.listPlayNow.hotkey,
() => {
if (!focused) return;
const selected = internalState.getSelected();
const validSelected = selected.filter(hasRequiredStateItemProperties);
if (validSelected.length === 0) return;
const item = validSelected[0];
controls.onPlay?.({ item, itemType, playType: Play.NOW } as any);
},
],
[
bindings.listPlayNext.hotkey,
() => {
if (!focused) return;
const selected = internalState.getSelected();
const validSelected = selected.filter(hasRequiredStateItemProperties);
if (validSelected.length === 0) return;
const item = validSelected[0];
controls.onPlay?.({ item, itemType, playType: Play.NEXT } as any);
},
],
[
bindings.listPlayLast.hotkey,
() => {
if (!focused) return;
const selected = internalState.getSelected();
const validSelected = selected.filter(hasRequiredStateItemProperties);
if (validSelected.length === 0) return;
const item = validSelected[0];
controls.onPlay?.({ item, itemType, playType: Play.LAST } as any);
},
],
[
bindings.listNavigateToPage.hotkey,
() => {
if (!focused) return;
const selected = internalState.getSelected();
const validSelected = selected.filter(hasRequiredStateItemProperties);
if (validSelected.length === 0) return;
const item = validSelected[0];
const path = getTitlePath(itemType, item.id);
if (path) {
navigate(path, { state: { item } });
}
},
],
]);
};
@@ -1,5 +1,4 @@
.item-grid-container { .item-grid-container {
position: relative;
display: flex; display: flex;
flex-direction: column !important; flex-direction: column !important;
width: 100%; width: 100%;
@@ -41,11 +41,11 @@ import {
useItemListState, useItemListState,
useItemListStateSubscription, useItemListStateSubscription,
} from '/@/renderer/components/item-list/helpers/item-list-state'; } from '/@/renderer/components/item-list/helpers/item-list-state';
import { useListHotkeys } from '/@/renderer/components/item-list/helpers/use-list-hotkeys';
import { ItemControls, ItemListHandle } from '/@/renderer/components/item-list/types'; import { ItemControls, ItemListHandle } from '/@/renderer/components/item-list/types';
import { animationProps } from '/@/shared/components/animations/animation-props'; import { animationProps } from '/@/shared/components/animations/animation-props';
import { useElementSize } from '/@/shared/hooks/use-element-size'; import { useElementSize } from '/@/shared/hooks/use-element-size';
import { useFocusWithin } from '/@/shared/hooks/use-focus-within'; import { useFocusWithin } from '/@/shared/hooks/use-focus-within';
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
import { useMergedRef } from '/@/shared/hooks/use-merged-ref'; import { useMergedRef } from '/@/shared/hooks/use-merged-ref';
import { LibraryItem } from '/@/shared/types/domain-types'; import { LibraryItem } from '/@/shared/types/domain-types';
@@ -68,7 +68,6 @@ interface VirtualizedGridListProps {
outerRef: RefObject<any>; outerRef: RefObject<any>;
ref: RefObject<FixedSizeList<GridItemProps> | null>; ref: RefObject<FixedSizeList<GridItemProps> | null>;
rows?: ItemCardProps['rows']; rows?: ItemCardProps['rows'];
size?: 'compact' | 'default' | 'large';
tableMetaRef: RefObject<null | { tableMetaRef: RefObject<null | {
columnCount: number; columnCount: number;
itemHeight: number; itemHeight: number;
@@ -96,7 +95,6 @@ const VirtualizedGridList = React.memo(
outerRef, outerRef,
ref, ref,
rows, rows,
size,
tableMetaRef, tableMetaRef,
width, width,
}: VirtualizedGridListProps) => { }: VirtualizedGridListProps) => {
@@ -115,7 +113,6 @@ const VirtualizedGridList = React.memo(
internalState, internalState,
itemType, itemType,
rows, rows,
size,
tableMeta, tableMeta,
}; };
}, [ }, [
@@ -129,7 +126,6 @@ const VirtualizedGridList = React.memo(
gap, gap,
internalState, internalState,
itemType, itemType,
size,
]); ]);
const handleOnScroll = useCallback( const handleOnScroll = useCallback(
@@ -219,11 +215,7 @@ const VirtualizedGridList = React.memo(
VirtualizedGridList.displayName = 'VirtualizedGridList'; VirtualizedGridList.displayName = 'VirtualizedGridList';
const createThrottledSetTableMeta = ( const createThrottledSetTableMeta = (itemsPerRow?: number, rowsCount?: number) => {
itemsPerRow?: number,
rowsCount?: number,
size?: 'compact' | 'default' | 'large',
) => {
return throttle((width: number, dataLength: number, setTableMeta: (meta: any) => void) => { return throttle((width: number, dataLength: number, setTableMeta: (meta: any) => void) => {
const isSm = width >= 600; const isSm = width >= 600;
const isMd = width >= 768; const isMd = width >= 768;
@@ -236,11 +228,11 @@ const createThrottledSetTableMeta = (
let dynamicItemsPerRow = 2; let dynamicItemsPerRow = 2;
if (is4xl) { if (is4xl) {
dynamicItemsPerRow = 10; dynamicItemsPerRow = 12;
} else if (is3xl) { } else if (is3xl) {
dynamicItemsPerRow = 8; dynamicItemsPerRow = 10;
} else if (is2xl) { } else if (is2xl) {
dynamicItemsPerRow = 7; dynamicItemsPerRow = 8;
} else if (isXl) { } else if (isXl) {
dynamicItemsPerRow = 6; dynamicItemsPerRow = 6;
} else if (isLg) { } else if (isLg) {
@@ -253,22 +245,10 @@ const createThrottledSetTableMeta = (
dynamicItemsPerRow = 2; dynamicItemsPerRow = 2;
} }
if (size === 'large') {
dynamicItemsPerRow = Math.round(dynamicItemsPerRow * 0.75);
if (dynamicItemsPerRow < 1) {
dynamicItemsPerRow = 1;
}
}
const setItemsPerRow = itemsPerRow || dynamicItemsPerRow; const setItemsPerRow = itemsPerRow || dynamicItemsPerRow;
const widthPerItem = Number(width) / setItemsPerRow; const widthPerItem = Number(width) / setItemsPerRow;
// For compact size, don't include text lines in height calculation const itemHeight = widthPerItem + (rowsCount || getDataRowsCount()) * 26;
// CompactItemCard has a different layout that doesn't need the extra space
const itemHeight =
size === 'compact'
? widthPerItem
: widthPerItem + (rowsCount || getDataRowsCount()) * 26;
if (widthPerItem === 0) { if (widthPerItem === 0) {
return; return;
@@ -293,7 +273,6 @@ export interface GridItemProps {
internalState: ItemListStateActions; internalState: ItemListStateActions;
itemType: LibraryItem; itemType: LibraryItem;
rows?: ItemCardProps['rows']; rows?: ItemCardProps['rows'];
size?: 'compact' | 'default' | 'large';
tableMeta: null | { tableMeta: null | {
columnCount: number; columnCount: number;
itemHeight: number; itemHeight: number;
@@ -307,7 +286,6 @@ export interface ItemGridListProps {
enableDrag?: boolean; enableDrag?: boolean;
enableExpansion?: boolean; enableExpansion?: boolean;
enableSelection?: boolean; enableSelection?: boolean;
enableSelectionDialog?: boolean;
gap?: 'lg' | 'md' | 'sm' | 'xl' | 'xs'; gap?: 'lg' | 'md' | 'sm' | 'xl' | 'xs';
getRowId?: ((item: unknown) => string) | string; getRowId?: ((item: unknown) => string) | string;
initialTop?: { initialTop?: {
@@ -322,7 +300,6 @@ export interface ItemGridListProps {
overrideControls?: Partial<ItemControls>; overrideControls?: Partial<ItemControls>;
ref?: Ref<ItemListHandle>; ref?: Ref<ItemListHandle>;
rows?: ItemCardProps['rows']; rows?: ItemCardProps['rows'];
size?: 'compact' | 'default' | 'large';
} }
const BaseItemGridList = ({ const BaseItemGridList = ({
@@ -342,7 +319,6 @@ const BaseItemGridList = ({
overrideControls, overrideControls,
ref, ref,
rows, rows,
size = 'default',
}: ItemGridListProps) => { }: ItemGridListProps) => {
const rootRef = useRef(null); const rootRef = useRef(null);
const outerRef = useRef(null); const outerRef = useRef(null);
@@ -433,8 +409,8 @@ const BaseItemGridList = ({
}, [osInstance]); }, [osInstance]);
const throttledSetTableMeta = useMemo(() => { const throttledSetTableMeta = useMemo(() => {
return createThrottledSetTableMeta(itemsPerRow, rows?.length, size); return createThrottledSetTableMeta(itemsPerRow, rows?.length);
}, [itemsPerRow, rows?.length, size]); }, [itemsPerRow, rows?.length]);
useLayoutEffect(() => { useLayoutEffect(() => {
const { current: container } = containerRef; const { current: container } = containerRef;
@@ -714,12 +690,20 @@ const BaseItemGridList = ({
useImperativeHandle(ref, () => imperativeHandle, [imperativeHandle]); useImperativeHandle(ref, () => imperativeHandle, [imperativeHandle]);
useListHotkeys({ useHotkeys([
controls, [
focused, 'mod+a',
internalState, () => {
itemType, if (focused) {
}); if (internalState.isAllSelected()) {
internalState.deselectAll();
} else {
internalState.selectAll();
}
}
},
],
]);
return ( return (
<motion.div <motion.div
@@ -753,23 +737,19 @@ const BaseItemGridList = ({
outerRef={outerRef} outerRef={outerRef}
ref={listRef} ref={listRef}
rows={rows} rows={rows}
size={size}
tableMetaRef={tableMetaRef} tableMetaRef={tableMetaRef}
width={width} width={width}
/> />
)} )}
</AutoSizer> </AutoSizer>
<AnimatePresence presenceAffectsLayout> <ExpandedContainer internalState={internalState} itemType={itemType} />
<ExpandedContainer internalState={internalState} itemType={itemType} />
{/* {enableSelectionDialog && <SelectionDialog internalState={internalState} />} */}
</AnimatePresence>
</motion.div> </motion.div>
); );
}; };
const ListComponent = memo((props: ListChildComponentProps<GridItemProps>) => { const ListComponent = memo((props: ListChildComponentProps<GridItemProps>) => {
const { index, style } = props; const { index, style } = props;
const { columns, controls, data, enableDrag, gap, itemType, rows, size } = props.data; const { columns, controls, data, enableDrag, gap, itemType, rows } = props.data;
const items: ReactNode[] = []; const items: ReactNode[] = [];
const itemCount = data.length; const itemCount = data.length;
@@ -800,7 +780,6 @@ const ListComponent = memo((props: ListChildComponentProps<GridItemProps>) => {
internalState={props.data.internalState} internalState={props.data.internalState}
itemType={itemType} itemType={itemType}
rows={rows} rows={rows}
type={size === 'compact' ? 'compact' : 'poster'}
withControls withControls
/> />
</div>, </div>,
@@ -1,5 +1,6 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { memo } from 'react'; import { Fragment, memo, useMemo } from 'react';
import { generatePath, Link } from 'react-router';
import styles from './album-artists-column.module.css'; import styles from './album-artists-column.module.css';
@@ -9,16 +10,24 @@ import {
ItemTableListInnerColumn, ItemTableListInnerColumn,
TableColumnContainer, TableColumnContainer,
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
import { JoinedArtists } from '/@/renderer/features/albums/components/joined-artists'; import { AppRoute } from '/@/renderer/router/routes';
import { Album, RelatedAlbumArtist, Song } from '/@/shared/types/domain-types'; import { Text } from '/@/shared/components/text/text';
import { RelatedAlbumArtist } from '/@/shared/types/domain-types';
const AlbumArtistsColumn = (props: ItemTableListInnerColumn) => { const AlbumArtistsColumn = (props: ItemTableListInnerColumn) => {
const row: RelatedAlbumArtist[] | undefined = ( const row: RelatedAlbumArtist[] | undefined = (
props.data as (RelatedAlbumArtist[] | undefined)[] props.data as (RelatedAlbumArtist[] | undefined)[]
)[props.rowIndex]?.[props.columns[props.columnIndex].id]; )[props.rowIndex]?.[props.columns[props.columnIndex].id];
const item = props.data[props.rowIndex] as Album | Song | undefined; const albumArtists = useMemo(() => {
const albumArtistString = item && 'albumArtistName' in item ? item.albumArtistName : ''; if (!row) return [];
return row.map((albumArtist) => {
const path = generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
albumArtistId: albumArtist.id,
});
return { ...albumArtist, path };
});
}, [row]);
if (Array.isArray(row)) { if (Array.isArray(row)) {
return ( return (
@@ -29,20 +38,21 @@ const AlbumArtistsColumn = (props: ItemTableListInnerColumn) => {
[styles.large]: props.size === 'large', [styles.large]: props.size === 'large',
})} })}
> >
<JoinedArtists {albumArtists.map((albumArtist, index) => (
artistName={albumArtistString} <Fragment key={albumArtist.id}>
artists={row} <Text
linkProps={{ fw: 400, isMuted: true }} component={Link}
rootTextProps={{ isLink
className: clsx(styles.artistsContainer, { isMuted
[styles.compact]: props.size === 'compact', isNoSelect
[styles.large]: props.size === 'large', state={{ item: albumArtist }}
}), to={albumArtist.path}
fw: 400, >
isMuted: true, {albumArtist.name}
size: 'sm', </Text>
}} {index < albumArtists.length - 1 && ', '}
/> </Fragment>
))}
</div> </div>
</TableColumnContainer> </TableColumnContainer>
); );
@@ -10,12 +10,11 @@ import {
ItemTableListInnerColumn, ItemTableListInnerColumn,
TableColumnContainer, TableColumnContainer,
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
import { JoinedArtists } from '/@/renderer/features/albums/components/joined-artists';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { LibraryItem, RelatedAlbumArtist, Song } from '/@/shared/types/domain-types'; import { RelatedAlbumArtist } from '/@/shared/types/domain-types';
const AlbumArtistsColumn = (props: ItemTableListInnerColumn) => { const ArtistsColumn = (props: ItemTableListInnerColumn) => {
const row: RelatedAlbumArtist[] | undefined = ( const row: RelatedAlbumArtist[] | undefined = (
props.data as (RelatedAlbumArtist[] | undefined)[] props.data as (RelatedAlbumArtist[] | undefined)[]
)[props.rowIndex]?.[props.columns[props.columnIndex].id]; )[props.rowIndex]?.[props.columns[props.columnIndex].id];
@@ -66,47 +65,6 @@ const AlbumArtistsColumn = (props: ItemTableListInnerColumn) => {
return <ColumnSkeletonVariable {...props} />; return <ColumnSkeletonVariable {...props} />;
}; };
const SongArtistsColumn = (props: ItemTableListInnerColumn) => { export const ArtistsColumnMemo = memo(ArtistsColumn);
const row: Song | undefined = (props.data as (Song | undefined)[])[props.rowIndex];
if (row) {
return (
<TableColumnContainer {...props}>
<div
className={clsx(styles.artistsContainer, {
[styles.compact]: props.size === 'compact',
[styles.large]: props.size === 'large',
})}
>
<JoinedArtists
artistName={row.artistName}
artists={row.artists}
linkProps={{ fw: 400, isMuted: true }}
rootTextProps={{ fw: 400, isMuted: true, size: 'sm' }}
/>
</div>
</TableColumnContainer>
);
}
if (row === null) {
return <ColumnNullFallback {...props} />;
}
return <ColumnSkeletonVariable {...props} />;
};
const BaseArtistsColumn = (props: ItemTableListInnerColumn) => {
const { itemType } = props;
switch (itemType) {
case LibraryItem.ALBUM:
return <AlbumArtistsColumn {...props} />;
default:
return <SongArtistsColumn {...props} />;
}
};
const ArtistsColumnMemo = memo(BaseArtistsColumn);
export { ArtistsColumnMemo as ArtistsColumn }; export { ArtistsColumnMemo as ArtistsColumn };
@@ -10,11 +10,9 @@ import {
formatDateRelative, formatDateRelative,
formatHrDateTime, formatHrDateTime,
} from '/@/renderer/utils/format'; } from '/@/renderer/utils/format';
import { SEPARATOR_STRING } from '/@/shared/api/utils';
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 { Tooltip } from '/@/shared/components/tooltip/tooltip'; import { Tooltip } from '/@/shared/components/tooltip/tooltip';
import { TableColumn } from '/@/shared/types/types';
const getDateTooltipLabel = (utcString: string) => { const getDateTooltipLabel = (utcString: string) => {
return ( return (
@@ -56,47 +54,6 @@ export const AbsoluteDateColumn = (props: ItemTableListInnerColumn) => {
props.columns[props.columnIndex].id props.columns[props.columnIndex].id
]; ];
if (props.type === TableColumn.RELEASE_DATE) {
const item = (props.data as (any | undefined)[])[props.rowIndex];
if (item && 'releaseDate' in item && item.releaseDate) {
const releaseDate = item.releaseDate;
const originalDate =
'originalDate' in item && item.originalDate && item.originalDate !== releaseDate
? item.originalDate
: null;
if (originalDate) {
const formattedOriginalDate = formatDateAbsoluteUTC(originalDate);
const formattedReleaseDate = formatDateAbsoluteUTC(releaseDate);
const displayText = `${formattedOriginalDate}${SEPARATOR_STRING}${formattedReleaseDate}`;
return (
<TableColumnTextContainer {...props}>
<Tooltip label={getDateTooltipLabel(releaseDate)} multiline={false}>
<span>{displayText}</span>
</Tooltip>
</TableColumnTextContainer>
);
}
if (typeof releaseDate === 'string' && releaseDate) {
return (
<TableColumnTextContainer {...props}>
<Tooltip label={getDateTooltipLabel(releaseDate)} multiline={false}>
<span>{formatDateAbsoluteUTC(releaseDate)}</span>
</Tooltip>
</TableColumnTextContainer>
);
}
}
if (row === null) {
return <ColumnNullFallback {...props} />;
}
return <ColumnSkeletonFixed {...props} />;
}
if (typeof row === 'string' && row) { if (typeof row === 'string' && row) {
return ( return (
<TableColumnTextContainer {...props}> <TableColumnTextContainer {...props}>
@@ -4,15 +4,6 @@
border-radius: 0; border-radius: 0;
} }
.image-container {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.compact-image-container .skeleton { .compact-image-container .skeleton {
border-radius: 0; border-radius: 0;
} }
@@ -33,9 +24,11 @@
} }
.image-container-with-aspect-ratio { .image-container-with-aspect-ratio {
width: auto; display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%; height: 100%;
aspect-ratio: 1 / 1;
border-radius: var(--theme-radius-md); border-radius: var(--theme-radius-md);
} }
@@ -47,10 +40,10 @@
object-position: center; object-position: center;
} }
.skeleton-with-aspect-ratio { .image-container {
width: auto; position: relative;
width: 100%;
height: 100%; height: 100%;
aspect-ratio: 1 / 1;
} }
.play-button-overlay { .play-button-overlay {
@@ -3,7 +3,6 @@ import { useState } from 'react';
import styles from './image-column.module.css'; import styles from './image-column.module.css';
import { ItemImage } from '/@/renderer/components/item-image/item-image';
import { import {
ItemTableListInnerColumn, ItemTableListInnerColumn,
TableColumnContainer, TableColumnContainer,
@@ -15,14 +14,17 @@ import {
} from '/@/renderer/features/shared/components/play-button-group'; } from '/@/renderer/features/shared/components/play-button-group';
import { usePlayButtonBehavior } from '/@/renderer/store'; import { usePlayButtonBehavior } from '/@/renderer/store';
import { Icon } from '/@/shared/components/icon/icon'; import { Icon } from '/@/shared/components/icon/icon';
import { Image } from '/@/shared/components/image/image';
import { Skeleton } from '/@/shared/components/skeleton/skeleton'; import { Skeleton } from '/@/shared/components/skeleton/skeleton';
import { Folder, LibraryItem } from '/@/shared/types/domain-types'; import { Folder, LibraryItem } from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types'; import { Play } from '/@/shared/types/types';
export const ImageColumn = (props: ItemTableListInnerColumn) => { export const ImageColumn = (props: ItemTableListInnerColumn) => {
const row: string | undefined = (props.data as (any | undefined)[])[props.rowIndex]?.id; const row: string | undefined = (props.data as (any | undefined)[])[props.rowIndex]?.[
const item = props.data[props.rowIndex] as any; props.columns[props.columnIndex].id
];
const playButtonBehavior = usePlayButtonBehavior(); const playButtonBehavior = usePlayButtonBehavior();
const item = props.data[props.rowIndex] as any;
const internalState = (props as any).internalState; const internalState = (props as any).internalState;
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
@@ -78,15 +80,12 @@ export const ImageColumn = (props: ItemTableListInnerColumn) => {
onMouseEnter={() => setIsHovered(true)} onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(false)}
> >
<ItemImage <Image
containerClassName={clsx({ containerClassName={clsx({
[styles.imageContainerWithAspectRatio]: [styles.imageContainerWithAspectRatio]:
props.size === 'default' || props.size === 'large', props.size === 'default' || props.size === 'large',
})} })}
id={item?.imageId} src={row}
itemType={item?._itemType}
src={item?.imageUrl}
type="table"
/> />
{isHovered && ( {isHovered && (
<div <div
@@ -126,8 +125,6 @@ export const ImageColumn = (props: ItemTableListInnerColumn) => {
<div <div
className={clsx(styles.imageContainer, { className={clsx(styles.imageContainer, {
[styles.compactImageContainer]: props.size === 'compact', [styles.compactImageContainer]: props.size === 'compact',
[styles.skeletonWithAspectRatio]:
props.size === 'default' || props.size === 'large',
})} })}
> >
<Skeleton containerClassName={styles.skeleton} /> <Skeleton containerClassName={styles.skeleton} />
@@ -16,7 +16,3 @@
.name-container.large { .name-container.large {
-webkit-line-clamp: 3; -webkit-line-clamp: 3;
} }
.active {
color: var(--theme-colors-primary);
}
@@ -83,6 +83,8 @@ function QueueSongTitleColumn(props: ItemTableListInnerColumn) {
const path = getTitlePath(props.itemType, (props.data[props.rowIndex] as any).id as string); const path = getTitlePath(props.itemType, (props.data[props.rowIndex] as any).id as string);
const item = props.data[props.rowIndex] as any; const item = props.data[props.rowIndex] as any;
const textStyles = isActive ? { color: 'var(--theme-colors-primary)' } : {};
const titleLinkProps = path const titleLinkProps = path
? { ? {
component: Link, component: Link,
@@ -96,29 +98,15 @@ function QueueSongTitleColumn(props: ItemTableListInnerColumn) {
<TableColumnContainer {...props}> <TableColumnContainer {...props}>
<Text <Text
className={clsx({ className={clsx({
[styles.active]: isActive,
[styles.compact]: props.size === 'compact', [styles.compact]: props.size === 'compact',
[styles.large]: props.size === 'large', [styles.large]: props.size === 'large',
[styles.nameContainer]: true, [styles.nameContainer]: true,
})} })}
isNoSelect isNoSelect
{...titleLinkProps} {...titleLinkProps}
style={textStyles}
> >
{row} {row}
{song?.trackSubtitle && props.itemType !== LibraryItem.QUEUE_SONG && (
<Text
className={clsx({
[styles.active]: isActive,
})}
component="span"
isMuted
size="sm"
>
{' ('}
{song.trackSubtitle}
{')'}
</Text>
)}
</Text> </Text>
</TableColumnContainer> </TableColumnContainer>
); );
@@ -70,7 +70,3 @@
width: 24px; width: 24px;
height: 24px; height: 24px;
} }
.active {
color: var(--theme-colors-primary);
}
@@ -1,10 +1,9 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { CSSProperties, useState } from 'react'; import { CSSProperties, useMemo, useState } from 'react';
import { Link } from 'react-router'; import { generatePath, Link } from 'react-router';
import styles from './title-combined-column.module.css'; import styles from './title-combined-column.module.css';
import { ItemImage } from '/@/renderer/components/item-image/item-image';
import { getTitlePath } from '/@/renderer/components/item-list/helpers/get-title-path'; import { getTitlePath } from '/@/renderer/components/item-list/helpers/get-title-path';
import { import {
ColumnNullFallback, ColumnNullFallback,
@@ -12,20 +11,21 @@ import {
ItemTableListInnerColumn, ItemTableListInnerColumn,
TableColumnContainer, TableColumnContainer,
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
import { JoinedArtists } from '/@/renderer/features/albums/components/joined-artists';
import { PlayButton } from '/@/renderer/features/shared/components/play-button'; import { PlayButton } from '/@/renderer/features/shared/components/play-button';
import { import {
LONG_PRESS_PLAY_BEHAVIOR, LONG_PRESS_PLAY_BEHAVIOR,
PlayTooltip, PlayTooltip,
} from '/@/renderer/features/shared/components/play-button-group'; } from '/@/renderer/features/shared/components/play-button-group';
import { AppRoute } from '/@/renderer/router/routes';
import { usePlayButtonBehavior } from '/@/renderer/store'; import { usePlayButtonBehavior } from '/@/renderer/store';
import { Icon } from '/@/shared/components/icon/icon'; import { Icon } from '/@/shared/components/icon/icon';
import { Image } from '/@/shared/components/image/image';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { Folder, LibraryItem, QueueSong } from '/@/shared/types/domain-types'; import { Folder, LibraryItem, QueueSong, RelatedAlbumArtist } from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types'; import { Play } from '/@/shared/types/types';
export const DefaultTitleCombinedColumn = (props: ItemTableListInnerColumn) => { export const DefaultTitleCombinedColumn = (props: ItemTableListInnerColumn) => {
const row: object | undefined = (props.data as (any | undefined)[])[props.rowIndex]?.id; const row: object | undefined = (props.data as (any | undefined)[])[props.rowIndex];
const item = props.data[props.rowIndex] as any; const item = props.data[props.rowIndex] as any;
const internalState = (props as any).internalState; const internalState = (props as any).internalState;
const playButtonBehavior = usePlayButtonBehavior(); const playButtonBehavior = usePlayButtonBehavior();
@@ -73,7 +73,19 @@ export const DefaultTitleCombinedColumn = (props: ItemTableListInnerColumn) => {
}); });
}; };
if (item && 'name' in item && 'imageUrl' in item && 'artists' in item) { const artists = useMemo(() => {
if (row && 'artists' in row && Array.isArray(row.artists)) {
return (row.artists as RelatedAlbumArtist[]).map((artist) => {
const path = generatePath(AppRoute.LIBRARY_ARTISTS_DETAIL, {
artistId: artist.id,
});
return { ...artist, path };
});
}
return [];
}, [row]);
if (row && 'name' in row && 'imageUrl' in row && 'artists' in row) {
const rowHeight = props.getRowHeight(props.rowIndex, props); const rowHeight = props.getRowHeight(props.rowIndex, props);
const path = getTitlePath(props.itemType, (props.data[props.rowIndex] as any).id as string); const path = getTitlePath(props.itemType, (props.data[props.rowIndex] as any).id as string);
@@ -98,13 +110,7 @@ export const DefaultTitleCombinedColumn = (props: ItemTableListInnerColumn) => {
onMouseEnter={() => setIsHovered(true)} onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(false)}
> >
<ItemImage <Image containerClassName={styles.image} src={row.imageUrl as string} />
containerClassName={styles.image}
id={item?.imageId}
itemType={item?._itemType}
src={item?.imageUrl}
type="table"
/>
{isHovered && ( {isHovered && (
<div <div
className={clsx(styles.playButtonOverlay, { className={clsx(styles.playButtonOverlay, {
@@ -132,15 +138,25 @@ export const DefaultTitleCombinedColumn = (props: ItemTableListInnerColumn) => {
})} })}
> >
<Text className={styles.title} isNoSelect size="md" {...titleLinkProps}> <Text className={styles.title} isNoSelect size="md" {...titleLinkProps}>
{item.name as string} {row.name as string}
</Text> </Text>
<div className={styles.artists}> <div className={styles.artists}>
<JoinedArtists {artists.map((artist, index) => (
artistName={item.albumArtist} <span key={artist.id}>
artists={item.albumArtists} <Text
linkProps={{ fw: 400, isMuted: true }} component={Link}
rootTextProps={{ fw: 400, isMuted: true, size: 'sm' }} isLink
/> isMuted
isNoSelect
size="sm"
state={{ item: artist }}
to={artist.path}
>
{artist.name}
</Text>
{index < artists.length - 1 && ', '}
</span>
))}
</div> </div>
</div> </div>
</TableColumnContainer> </TableColumnContainer>
@@ -208,11 +224,24 @@ export const QueueSongTitleCombinedColumn = (props: ItemTableListInnerColumn) =>
}); });
}; };
const artists = useMemo(() => {
if (row && 'artists' in row && Array.isArray(row.artists)) {
return (row.artists as RelatedAlbumArtist[]).map((artist) => {
const path = generatePath(AppRoute.LIBRARY_ARTISTS_DETAIL, {
artistId: artist.id,
});
return { ...artist, path };
});
}
return [];
}, [row]);
if (row && 'name' in row && 'imageUrl' in row && 'artists' in row) { if (row && 'name' in row && 'imageUrl' in row && 'artists' in row) {
const rowHeight = props.getRowHeight(props.rowIndex, props); const rowHeight = props.getRowHeight(props.rowIndex, props);
const path = getTitlePath(props.itemType, (props.data[props.rowIndex] as any).id as string); const path = getTitlePath(props.itemType, (props.data[props.rowIndex] as any).id as string);
const item = props.data[props.rowIndex] as any; const item = props.data[props.rowIndex] as any;
const textStyles = isActive ? { color: 'var(--theme-colors-primary)' } : {};
const titleLinkProps = path const titleLinkProps = path
? { ? {
@@ -234,13 +263,7 @@ export const QueueSongTitleCombinedColumn = (props: ItemTableListInnerColumn) =>
onMouseEnter={() => setIsHovered(true)} onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(false)}
> >
<ItemImage <Image containerClassName={styles.image} src={row.imageUrl as string} />
containerClassName={styles.image}
id={item?.imageId}
itemType={item?._itemType}
src={item?.imageUrl}
type="table"
/>
{isHovered && ( {isHovered && (
<div <div
className={clsx(styles.playButtonOverlay, { className={clsx(styles.playButtonOverlay, {
@@ -264,42 +287,35 @@ export const QueueSongTitleCombinedColumn = (props: ItemTableListInnerColumn) =>
</div> </div>
<div <div
className={clsx(styles.textContainer, { className={clsx(styles.textContainer, {
[styles.active]: isActive,
[styles.compact]: props.size === 'compact', [styles.compact]: props.size === 'compact',
})} })}
> >
<Text <Text
className={clsx({ className={styles.title}
[styles.active]: isActive,
[styles.title]: true,
})}
isNoSelect isNoSelect
size="md" size="md"
{...titleLinkProps} {...titleLinkProps}
style={textStyles}
> >
{row.name as string} {row.name as string}
{song?.trackSubtitle && props.itemType !== LibraryItem.QUEUE_SONG && (
<Text
className={clsx({
[styles.active]: isActive,
})}
component="span"
isMuted
size="sm"
>
{' ('}
{song.trackSubtitle}
{')'}
</Text>
)}
</Text> </Text>
<div className={styles.artists}> <div className={styles.artists}>
<JoinedArtists {artists.map((artist, index) => (
artistName={item.artistName} <span key={artist.id}>
artists={item.artists} <Text
linkProps={{ fw: 400, isMuted: true }} component={Link}
rootTextProps={{ fw: 400, isMuted: true, size: 'sm' }} isLink
/> isMuted
isNoSelect
size="sm"
state={{ item: artist }}
to={artist.path}
>
{artist.name}
</Text>
{index < artists.length - 1 && ', '}
</span>
))}
</div> </div>
</div> </div>
</TableColumnContainer> </TableColumnContainer>
@@ -1,41 +0,0 @@
import {
ColumnNullFallback,
ColumnSkeletonFixed,
ItemTableListInnerColumn,
TableColumnTextContainer,
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
import { SEPARATOR_STRING } from '/@/shared/api/utils';
export const YearColumn = (props: ItemTableListInnerColumn) => {
const item = (props.data as (any | undefined)[])[props.rowIndex];
if (item && 'releaseYear' in item && item.releaseYear !== null) {
const releaseYear = item.releaseYear;
const originalYear =
'originalYear' in item && item.originalYear !== null ? item.originalYear : null;
if (originalYear !== null && originalYear !== releaseYear) {
return (
<TableColumnTextContainer {...props}>
{originalYear}
{SEPARATOR_STRING}
{releaseYear}
</TableColumnTextContainer>
);
}
if (typeof releaseYear === 'number') {
return <TableColumnTextContainer {...props}>{releaseYear}</TableColumnTextContainer>;
}
}
const row: number | undefined = (props.data as (any | undefined)[])[props.rowIndex]?.[
props.columns[props.columnIndex].id
];
if (row === null) {
return <ColumnNullFallback {...props} />;
}
return <ColumnSkeletonFixed {...props} />;
};
@@ -18,34 +18,34 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
autoSize: false, autoSize: false,
isEnabled: true, isEnabled: true,
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
pinned: null, pinned: 'left',
value: TableColumn.ROW_INDEX, value: TableColumn.ROW_INDEX,
width: 60, width: 80,
}, },
{ {
align: 'center', align: 'center',
autoSize: false, autoSize: false,
isEnabled: false, isEnabled: true,
label: i18n.t('table.config.label.image', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.image', { postProcess: 'titleCase' }),
pinned: null, pinned: 'left',
value: TableColumn.IMAGE, value: TableColumn.IMAGE,
width: 70, width: 70,
}, },
{ {
align: 'start', align: 'start',
autoSize: false, autoSize: false,
isEnabled: false, isEnabled: true,
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
pinned: null, pinned: 'left',
value: TableColumn.TITLE, value: TableColumn.TITLE,
width: 300, width: 300,
}, },
{ {
align: 'start', align: 'start',
autoSize: false, autoSize: false,
isEnabled: true, isEnabled: false,
label: i18n.t('table.config.label.titleCombined', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.titleCombined', { postProcess: 'titleCase' }),
pinned: null, pinned: 'left',
value: TableColumn.TITLE_COMBINED, value: TableColumn.TITLE_COMBINED,
width: 300, width: 300,
}, },
@@ -61,7 +61,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
{ {
align: 'start', align: 'start',
autoSize: false, autoSize: false,
isEnabled: true, isEnabled: false,
label: i18n.t('table.config.label.album', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.album', { postProcess: 'titleCase' }),
pinned: null, pinned: null,
value: TableColumn.ALBUM, value: TableColumn.ALBUM,
@@ -70,7 +70,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
{ {
align: 'start', align: 'start',
autoSize: true, autoSize: true,
isEnabled: false, isEnabled: true,
label: i18n.t('table.config.label.albumArtist', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.albumArtist', { postProcess: 'titleCase' }),
pinned: null, pinned: null,
value: TableColumn.ALBUM_ARTIST, value: TableColumn.ALBUM_ARTIST,
@@ -110,16 +110,16 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
label: i18n.t('table.config.label.year', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.year', { postProcess: 'titleCase' }),
pinned: null, pinned: null,
value: TableColumn.YEAR, value: TableColumn.YEAR,
width: 200, width: 100,
}, },
{ {
align: 'center', align: 'center',
autoSize: false, autoSize: false,
isEnabled: false, isEnabled: true,
label: i18n.t('table.config.label.releaseDate', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.releaseDate', { postProcess: 'titleCase' }),
pinned: null, pinned: null,
value: TableColumn.RELEASE_DATE, value: TableColumn.RELEASE_DATE,
width: 240, width: 120,
}, },
{ {
align: 'center', align: 'center',
@@ -178,7 +178,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
{ {
align: 'center', align: 'center',
autoSize: false, autoSize: false,
isEnabled: false, isEnabled: true,
label: i18n.t('table.config.label.lastPlayed', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.lastPlayed', { postProcess: 'titleCase' }),
pinned: null, pinned: null,
value: TableColumn.LAST_PLAYED, value: TableColumn.LAST_PLAYED,
@@ -214,7 +214,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
{ {
align: 'center', align: 'center',
autoSize: false, autoSize: false,
isEnabled: false, isEnabled: true,
label: i18n.t('table.config.label.dateAdded', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.dateAdded', { postProcess: 'titleCase' }),
pinned: null, pinned: null,
value: TableColumn.DATE_ADDED, value: TableColumn.DATE_ADDED,
@@ -232,7 +232,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
{ {
align: 'center', align: 'center',
autoSize: false, autoSize: false,
isEnabled: false, isEnabled: true,
label: i18n.t('table.config.label.playCount', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.playCount', { postProcess: 'titleCase' }),
pinned: null, pinned: null,
value: TableColumn.PLAY_COUNT, value: TableColumn.PLAY_COUNT,
@@ -252,7 +252,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
autoSize: false, autoSize: false,
isEnabled: true, isEnabled: true,
label: i18n.t('table.config.label.favorite', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.favorite', { postProcess: 'titleCase' }),
pinned: null, pinned: 'right',
value: TableColumn.USER_FAVORITE, value: TableColumn.USER_FAVORITE,
width: 60, width: 60,
}, },
@@ -268,9 +268,9 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
{ {
align: 'center', align: 'center',
autoSize: false, autoSize: false,
isEnabled: false, isEnabled: true,
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
pinned: null, pinned: 'right',
value: TableColumn.ACTIONS, value: TableColumn.ACTIONS,
width: 60, width: 60,
}, },
@@ -284,34 +284,34 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
autoSize: false, autoSize: false,
isEnabled: true, isEnabled: true,
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
pinned: null, pinned: 'left',
value: TableColumn.ROW_INDEX, value: TableColumn.ROW_INDEX,
width: 60, width: 80,
}, },
{ {
align: 'center', align: 'center',
autoSize: false, autoSize: false,
isEnabled: false, isEnabled: true,
label: i18n.t('table.config.label.image', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.image', { postProcess: 'titleCase' }),
pinned: null, pinned: 'left',
value: TableColumn.IMAGE, value: TableColumn.IMAGE,
width: 70, width: 70,
}, },
{ {
align: 'start', align: 'start',
autoSize: false, autoSize: false,
isEnabled: false, isEnabled: true,
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
pinned: null, pinned: 'left',
value: TableColumn.TITLE, value: TableColumn.TITLE,
width: 300, width: 300,
}, },
{ {
align: 'start', align: 'start',
autoSize: false, autoSize: false,
isEnabled: true, isEnabled: false,
label: i18n.t('table.config.label.titleCombined', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.titleCombined', { postProcess: 'titleCase' }),
pinned: null, pinned: 'left',
value: TableColumn.TITLE_COMBINED, value: TableColumn.TITLE_COMBINED,
width: 300, width: 300,
}, },
@@ -327,7 +327,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
{ {
align: 'start', align: 'start',
autoSize: true, autoSize: true,
isEnabled: false, isEnabled: true,
label: i18n.t('table.config.label.albumArtist', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.albumArtist', { postProcess: 'titleCase' }),
pinned: null, pinned: null,
value: TableColumn.ALBUM_ARTIST, value: TableColumn.ALBUM_ARTIST,
@@ -376,21 +376,21 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
label: i18n.t('table.config.label.year', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.year', { postProcess: 'titleCase' }),
pinned: null, pinned: null,
value: TableColumn.YEAR, value: TableColumn.YEAR,
width: 200, width: 100,
}, },
{ {
align: 'center', align: 'center',
autoSize: false, autoSize: false,
isEnabled: false, isEnabled: true,
label: i18n.t('table.config.label.releaseDate', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.releaseDate', { postProcess: 'titleCase' }),
pinned: null, pinned: null,
value: TableColumn.RELEASE_DATE, value: TableColumn.RELEASE_DATE,
width: 240, width: 120,
}, },
{ {
align: 'center', align: 'center',
autoSize: false, autoSize: false,
isEnabled: false, isEnabled: true,
label: i18n.t('table.config.label.lastPlayed', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.lastPlayed', { postProcess: 'titleCase' }),
pinned: null, pinned: null,
value: TableColumn.LAST_PLAYED, value: TableColumn.LAST_PLAYED,
@@ -399,7 +399,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
{ {
align: 'center', align: 'center',
autoSize: false, autoSize: false,
isEnabled: false, isEnabled: true,
label: i18n.t('table.config.label.dateAdded', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.dateAdded', { postProcess: 'titleCase' }),
pinned: null, pinned: null,
value: TableColumn.DATE_ADDED, value: TableColumn.DATE_ADDED,
@@ -408,7 +408,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
{ {
align: 'center', align: 'center',
autoSize: false, autoSize: false,
isEnabled: false, isEnabled: true,
label: i18n.t('table.config.label.playCount', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.playCount', { postProcess: 'titleCase' }),
pinned: null, pinned: null,
value: TableColumn.PLAY_COUNT, value: TableColumn.PLAY_COUNT,
@@ -419,7 +419,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
autoSize: false, autoSize: false,
isEnabled: true, isEnabled: true,
label: i18n.t('table.config.label.favorite', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.favorite', { postProcess: 'titleCase' }),
pinned: null, pinned: 'right',
value: TableColumn.USER_FAVORITE, value: TableColumn.USER_FAVORITE,
width: 60, width: 60,
}, },
@@ -435,9 +435,9 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
{ {
align: 'center', align: 'center',
autoSize: false, autoSize: false,
isEnabled: false, isEnabled: true,
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
pinned: null, pinned: 'right',
value: TableColumn.ACTIONS, value: TableColumn.ACTIONS,
width: 60, width: 60,
}, },
@@ -451,7 +451,7 @@ export const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
pinned: null, pinned: null,
value: TableColumn.ROW_INDEX, value: TableColumn.ROW_INDEX,
width: 60, width: 80,
}, },
{ {
align: 'center', align: 'center',
@@ -539,7 +539,7 @@ export const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [
autoSize: false, autoSize: false,
isEnabled: true, isEnabled: true,
label: i18n.t('table.config.label.favorite', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.favorite', { postProcess: 'titleCase' }),
pinned: null, pinned: 'right',
value: TableColumn.USER_FAVORITE, value: TableColumn.USER_FAVORITE,
width: 60, width: 60,
}, },
@@ -555,9 +555,9 @@ export const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [
{ {
align: 'center', align: 'center',
autoSize: false, autoSize: false,
isEnabled: false, isEnabled: true,
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
pinned: null, pinned: 'right',
value: TableColumn.ACTIONS, value: TableColumn.ACTIONS,
width: 60, width: 60,
}, },
@@ -571,7 +571,7 @@ export const PLAYLIST_TABLE_COLUMNS: DefaultTableColumn[] = [
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
pinned: null, pinned: null,
value: TableColumn.ROW_INDEX, value: TableColumn.ROW_INDEX,
width: 60, width: 80,
}, },
{ {
align: 'center', align: 'center',
@@ -630,9 +630,9 @@ export const PLAYLIST_TABLE_COLUMNS: DefaultTableColumn[] = [
{ {
align: 'center', align: 'center',
autoSize: false, autoSize: false,
isEnabled: false, isEnabled: true,
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
pinned: null, pinned: 'right',
value: TableColumn.ACTIONS, value: TableColumn.ACTIONS,
width: 60, width: 60,
}, },
@@ -646,7 +646,7 @@ export const GENRE_TABLE_COLUMNS: DefaultTableColumn[] = [
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
pinned: null, pinned: null,
value: TableColumn.ROW_INDEX, value: TableColumn.ROW_INDEX,
width: 60, width: 80,
}, },
{ {
align: 'start', align: 'start',
@@ -678,9 +678,9 @@ export const GENRE_TABLE_COLUMNS: DefaultTableColumn[] = [
{ {
align: 'center', align: 'center',
autoSize: false, autoSize: false,
isEnabled: false, isEnabled: true,
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
pinned: null, pinned: 'right',
value: TableColumn.ACTIONS, value: TableColumn.ACTIONS,
width: 60, width: 60,
}, },
@@ -728,15 +728,6 @@ export const pickTableColumns = (options: {
const enabledSet = new Set(enabledColumns); const enabledSet = new Set(enabledColumns);
const remaining = columns.filter((col) => !enabledSet.has(col.value)); const remaining = columns.filter((col) => !enabledSet.has(col.value));
columnsToProcess = [...columnsToProcess, ...remaining]; columnsToProcess = [...columnsToProcess, ...remaining];
} else {
// When pickColumns is provided, include pickColumns that aren't in enabledColumns
// so they can be added as disabled entries
const enabledSet = new Set(enabledColumns);
const pickColumnsNotEnabled = pickColumns
.filter((col) => !enabledSet.has(col))
.map((col) => columnMap.get(col))
.filter((col): col is DefaultTableColumn => col !== undefined);
columnsToProcess = [...columnsToProcess, ...pickColumnsNotEnabled];
} }
} else { } else {
columnsToProcess = columns; columnsToProcess = columns;
@@ -47,7 +47,6 @@ import { SizeColumn } from '/@/renderer/components/item-list/item-table-list/col
import { TextColumn } from '/@/renderer/components/item-list/item-table-list/columns/text-column'; import { TextColumn } from '/@/renderer/components/item-list/item-table-list/columns/text-column';
import { TitleColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-column'; import { TitleColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-column';
import { TitleCombinedColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-combined-column'; import { TitleCombinedColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-combined-column';
import { YearColumn } from '/@/renderer/components/item-list/item-table-list/columns/year-column';
import { TableItemProps } from '/@/renderer/components/item-list/item-table-list/item-table-list'; import { TableItemProps } from '/@/renderer/components/item-list/item-table-list/item-table-list';
import { ItemControls, ItemListItem } from '/@/renderer/components/item-list/types'; import { ItemControls, ItemListItem } from '/@/renderer/components/item-list/types';
import { eventEmitter } from '/@/renderer/events/event-emitter'; import { eventEmitter } from '/@/renderer/events/event-emitter';
@@ -488,6 +487,7 @@ export const ItemTableListColumn = (props: ItemTableListColumn) => {
case TableColumn.DISC_NUMBER: case TableColumn.DISC_NUMBER:
case TableColumn.SAMPLE_RATE: case TableColumn.SAMPLE_RATE:
case TableColumn.TRACK_NUMBER: case TableColumn.TRACK_NUMBER:
case TableColumn.YEAR:
return <NumericColumn {...props} {...dragProps} controls={controls} type={type} />; return <NumericColumn {...props} {...dragProps} controls={controls} type={type} />;
case TableColumn.DATE_ADDED: case TableColumn.DATE_ADDED:
@@ -548,9 +548,6 @@ export const ItemTableListColumn = (props: ItemTableListColumn) => {
case TableColumn.USER_RATING: case TableColumn.USER_RATING:
return <RatingColumn {...props} {...dragProps} controls={controls} type={type} />; return <RatingColumn {...props} {...dragProps} controls={controls} type={type} />;
case TableColumn.YEAR:
return <YearColumn {...props} {...dragProps} controls={controls} type={type} />;
default: default:
return <DefaultColumn {...props} {...dragProps} controls={controls} type={type} />; return <DefaultColumn {...props} {...dragProps} controls={controls} type={type} />;
} }
@@ -614,52 +611,33 @@ export const TableColumnTextContainer = (
: props.rowIndex === props.data.length - 1); : props.rowIndex === props.data.length - 1);
useEffect(() => { useEffect(() => {
if (!isDataRow || !containerRef.current || !props.enableRowHoverHighlight) return; if (!isDataRow || !containerRef.current) return;
const container = containerRef.current; const container = containerRef.current;
const rowIndex = props.rowIndex; const rowIndex = props.rowIndex;
const rowSelector = `[data-row-index="${props.tableId}-${rowIndex}"]`;
let rafId: null | number = null;
let cachedCells: NodeListOf<Element> | null = null;
const getCells = () => {
if (!cachedCells) {
cachedCells = document.querySelectorAll(rowSelector);
}
return cachedCells;
};
const handleMouseEnter = () => { const handleMouseEnter = () => {
if (rafId !== null) { // Find all cells in the same row and add hover class
cancelAnimationFrame(rafId); const allCells = document.querySelectorAll(
} `[data-row-index="${props.tableId}-${rowIndex}"]`,
rafId = requestAnimationFrame(() => { );
const cells = getCells(); allCells.forEach((cell) => cell.classList.add(styles.rowHovered));
cells.forEach((cell) => cell.classList.add(styles.rowHovered));
});
}; };
const handleMouseLeave = () => { const handleMouseLeave = () => {
if (rafId !== null) { // Remove hover class from all cells in the same row
cancelAnimationFrame(rafId); const allCells = document.querySelectorAll(
} `[data-row-index="${props.tableId}-${rowIndex}"]`,
rafId = requestAnimationFrame(() => { );
const cells = getCells(); allCells.forEach((cell) => cell.classList.remove(styles.rowHovered));
cells.forEach((cell) => cell.classList.remove(styles.rowHovered));
cachedCells = null;
});
}; };
container.addEventListener('mouseenter', handleMouseEnter); container.addEventListener('mouseenter', handleMouseEnter);
container.addEventListener('mouseleave', handleMouseLeave); container.addEventListener('mouseleave', handleMouseLeave);
return () => { return () => {
if (rafId !== null) {
cancelAnimationFrame(rafId);
}
container.removeEventListener('mouseenter', handleMouseEnter); container.removeEventListener('mouseenter', handleMouseEnter);
container.removeEventListener('mouseleave', handleMouseLeave); container.removeEventListener('mouseleave', handleMouseLeave);
cachedCells = null;
}; };
}, [isDataRow, props.rowIndex, props.enableRowHoverHighlight, props.tableId]); }, [isDataRow, props.rowIndex, props.enableRowHoverHighlight, props.tableId]);
@@ -669,64 +647,44 @@ export const TableColumnTextContainer = (
const rowIndex = props.rowIndex; const rowIndex = props.rowIndex;
const draggedOverState = props.isDraggedOver; const draggedOverState = props.isDraggedOver;
const rowSelector = `[data-row-index="${props.tableId}-${rowIndex}"]`;
let rafId: null | number = null;
let cachedCells: NodeListOf<Element> | null = null;
const getCells = () => { if (draggedOverState) {
if (!cachedCells) { // Find all cells in the same row and add dragged over class
cachedCells = document.querySelectorAll(rowSelector); const allCells = document.querySelectorAll(
} `[data-row-index="${props.tableId}-${rowIndex}"]`,
return cachedCells; );
}; allCells.forEach((cell, index) => {
if (draggedOverState === 'top') {
if (rafId !== null) { cell.classList.add(styles.draggedOverTop);
cancelAnimationFrame(rafId);
}
rafId = requestAnimationFrame(() => {
const cells = getCells();
if (draggedOverState) {
cells.forEach((cell, index) => {
if (draggedOverState === 'top') {
cell.classList.add(styles.draggedOverTop);
cell.classList.remove(styles.draggedOverBottom);
// Mark first cell so border can span full width
if (index === 0) {
cell.classList.add(styles.draggedOverFirstCell);
} else {
cell.classList.remove(styles.draggedOverFirstCell);
}
} else if (draggedOverState === 'bottom') {
cell.classList.add(styles.draggedOverBottom);
cell.classList.remove(styles.draggedOverTop);
// Mark first cell so border can span full width
if (index === 0) {
cell.classList.add(styles.draggedOverFirstCell);
} else {
cell.classList.remove(styles.draggedOverFirstCell);
}
}
});
} else {
// Remove dragged over classes from all cells in the same row
cells.forEach((cell) => {
cell.classList.remove(styles.draggedOverTop);
cell.classList.remove(styles.draggedOverBottom); cell.classList.remove(styles.draggedOverBottom);
cell.classList.remove(styles.draggedOverFirstCell); // Mark first cell so border can span full width
}); if (index === 0) {
// Clear cache when state is cleared cell.classList.add(styles.draggedOverFirstCell);
cachedCells = null; } else {
} cell.classList.remove(styles.draggedOverFirstCell);
}); }
} else if (draggedOverState === 'bottom') {
return () => { cell.classList.add(styles.draggedOverBottom);
if (rafId !== null) { cell.classList.remove(styles.draggedOverTop);
cancelAnimationFrame(rafId); // Mark first cell so border can span full width
} if (index === 0) {
cachedCells = null; cell.classList.add(styles.draggedOverFirstCell);
}; } else {
cell.classList.remove(styles.draggedOverFirstCell);
}
}
});
} else {
// Remove dragged over classes from all cells in the same row
const allCells = document.querySelectorAll(
`[data-row-index="${props.tableId}-${rowIndex}"]`,
);
allCells.forEach((cell) => {
cell.classList.remove(styles.draggedOverTop);
cell.classList.remove(styles.draggedOverBottom);
cell.classList.remove(styles.draggedOverFirstCell);
});
}
}, [isDataRow, props.rowIndex, props.isDraggedOver, props.tableId]); }, [isDataRow, props.rowIndex, props.isDraggedOver, props.tableId]);
const handleClick = useDoubleClick({ const handleClick = useDoubleClick({
@@ -866,52 +824,33 @@ export const TableColumnContainer = (
: props.rowIndex === props.data.length - 1); : props.rowIndex === props.data.length - 1);
useEffect(() => { useEffect(() => {
if (!isDataRow || !containerRef.current || !props.enableRowHoverHighlight) return; if (!isDataRow || !containerRef.current) return;
const container = containerRef.current; const container = containerRef.current;
const rowIndex = props.rowIndex; const rowIndex = props.rowIndex;
const rowSelector = `[data-row-index="${props.tableId}-${rowIndex}"]`;
let rafId: null | number = null;
let cachedCells: NodeListOf<Element> | null = null;
const getCells = () => {
if (!cachedCells) {
cachedCells = document.querySelectorAll(rowSelector);
}
return cachedCells;
};
const handleMouseEnter = () => { const handleMouseEnter = () => {
if (rafId !== null) { // Find all cells in the same row and add hover class
cancelAnimationFrame(rafId); const allCells = document.querySelectorAll(
} `[data-row-index="${props.tableId}-${rowIndex}"]`,
rafId = requestAnimationFrame(() => { );
const cells = getCells(); allCells.forEach((cell) => cell.classList.add(styles.rowHovered));
cells.forEach((cell) => cell.classList.add(styles.rowHovered));
});
}; };
const handleMouseLeave = () => { const handleMouseLeave = () => {
if (rafId !== null) { // Remove hover class from all cells in the same row
cancelAnimationFrame(rafId); const allCells = document.querySelectorAll(
} `[data-row-index="${props.tableId}-${rowIndex}"]`,
rafId = requestAnimationFrame(() => { );
const cells = getCells(); allCells.forEach((cell) => cell.classList.remove(styles.rowHovered));
cells.forEach((cell) => cell.classList.remove(styles.rowHovered));
cachedCells = null;
});
}; };
container.addEventListener('mouseenter', handleMouseEnter); container.addEventListener('mouseenter', handleMouseEnter);
container.addEventListener('mouseleave', handleMouseLeave); container.addEventListener('mouseleave', handleMouseLeave);
return () => { return () => {
if (rafId !== null) {
cancelAnimationFrame(rafId);
}
container.removeEventListener('mouseenter', handleMouseEnter); container.removeEventListener('mouseenter', handleMouseEnter);
container.removeEventListener('mouseleave', handleMouseLeave); container.removeEventListener('mouseleave', handleMouseLeave);
cachedCells = null;
}; };
}, [isDataRow, props.rowIndex, props.enableRowHoverHighlight, props.tableId]); }, [isDataRow, props.rowIndex, props.enableRowHoverHighlight, props.tableId]);
@@ -921,64 +860,44 @@ export const TableColumnContainer = (
const rowIndex = props.rowIndex; const rowIndex = props.rowIndex;
const draggedOverState = props.isDraggedOver; const draggedOverState = props.isDraggedOver;
const rowSelector = `[data-row-index="${props.tableId}-${rowIndex}"]`;
let rafId: null | number = null;
let cachedCells: NodeListOf<Element> | null = null;
const getCells = () => { if (draggedOverState) {
if (!cachedCells) { // Find all cells in the same row and add dragged over class
cachedCells = document.querySelectorAll(rowSelector); const allCells = document.querySelectorAll(
} `[data-row-index="${props.tableId}-${rowIndex}"]`,
return cachedCells; );
}; allCells.forEach((cell, index) => {
if (draggedOverState === 'top') {
if (rafId !== null) { cell.classList.add(styles.draggedOverTop);
cancelAnimationFrame(rafId);
}
rafId = requestAnimationFrame(() => {
const cells = getCells();
if (draggedOverState) {
cells.forEach((cell, index) => {
if (draggedOverState === 'top') {
cell.classList.add(styles.draggedOverTop);
cell.classList.remove(styles.draggedOverBottom);
// Mark first cell so border can span full width
if (index === 0) {
cell.classList.add(styles.draggedOverFirstCell);
} else {
cell.classList.remove(styles.draggedOverFirstCell);
}
} else if (draggedOverState === 'bottom') {
cell.classList.add(styles.draggedOverBottom);
cell.classList.remove(styles.draggedOverTop);
// Mark first cell so border can span full width
if (index === 0) {
cell.classList.add(styles.draggedOverFirstCell);
} else {
cell.classList.remove(styles.draggedOverFirstCell);
}
}
});
} else {
// Remove dragged over classes from all cells in the same row
cells.forEach((cell) => {
cell.classList.remove(styles.draggedOverTop);
cell.classList.remove(styles.draggedOverBottom); cell.classList.remove(styles.draggedOverBottom);
cell.classList.remove(styles.draggedOverFirstCell); // Mark first cell so border can span full width
}); if (index === 0) {
// Clear cache when state is cleared cell.classList.add(styles.draggedOverFirstCell);
cachedCells = null; } else {
} cell.classList.remove(styles.draggedOverFirstCell);
}); }
} else if (draggedOverState === 'bottom') {
return () => { cell.classList.add(styles.draggedOverBottom);
if (rafId !== null) { cell.classList.remove(styles.draggedOverTop);
cancelAnimationFrame(rafId); // Mark first cell so border can span full width
} if (index === 0) {
cachedCells = null; cell.classList.add(styles.draggedOverFirstCell);
}; } else {
cell.classList.remove(styles.draggedOverFirstCell);
}
}
});
} else {
// Remove dragged over classes from all cells in the same row
const allCells = document.querySelectorAll(
`[data-row-index="${props.tableId}-${rowIndex}"]`,
);
allCells.forEach((cell) => {
cell.classList.remove(styles.draggedOverTop);
cell.classList.remove(styles.draggedOverBottom);
cell.classList.remove(styles.draggedOverFirstCell);
});
}
}, [isDataRow, props.rowIndex, props.isDraggedOver, props.tableId]); }, [isDataRow, props.rowIndex, props.isDraggedOver, props.tableId]);
const handleClick = useDoubleClick({ const handleClick = useDoubleClick({
@@ -1,5 +1,4 @@
.item-table-list-container { .item-table-list-container {
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
@@ -186,24 +185,12 @@
z-index: 1; z-index: 1;
height: 8px; height: 8px;
pointer-events: none; pointer-events: none;
background: linear-gradient(
@mixin dark { to bottom,
background: linear-gradient( darken(var(--theme-colors-background), 10%) 0%,
to bottom, darken(var(--theme-colors-background), 5%) 50%,
color-mix(in srgb, var(--theme-colors-background) 85%, black) 0%, transparent 100%
color-mix(in srgb, var(--theme-colors-background) 92%, black) 50%, );
transparent 100%
);
}
@mixin light {
background: linear-gradient(
to bottom,
color-mix(in srgb, var(--theme-colors-background) 92%, black) 0%,
color-mix(in srgb, var(--theme-colors-background) 96%, black) 50%,
transparent 100%
);
}
} }
.item-table-left-scroll-shadow { .item-table-left-scroll-shadow {
@@ -218,8 +205,8 @@
@mixin dark { @mixin dark {
background: linear-gradient( background: linear-gradient(
to right, to right,
color-mix(in srgb, var(--theme-colors-background) 85%, black) 0%, darken(var(--theme-colors-background), 10%) 0%,
color-mix(in srgb, var(--theme-colors-background) 92%, black) 50%, darken(var(--theme-colors-background), 5%) 50%,
transparent 100% transparent 100%
); );
} }
@@ -227,8 +214,8 @@
@mixin light { @mixin light {
background: linear-gradient( background: linear-gradient(
to right, to right,
color-mix(in srgb, var(--theme-colors-background) 92%, black) 0%, darken(var(--theme-colors-background), 5%) 0%,
color-mix(in srgb, var(--theme-colors-background) 96%, black) 50%, darken(var(--theme-colors-background), 3%) 50%,
transparent 100% transparent 100%
); );
} }
@@ -246,8 +233,8 @@
@mixin dark { @mixin dark {
background: linear-gradient( background: linear-gradient(
to left, to left,
color-mix(in srgb, var(--theme-colors-background) 85%, black) 0%, darken(var(--theme-colors-background), 10%) 0%,
color-mix(in srgb, var(--theme-colors-background) 92%, black) 50%, darken(var(--theme-colors-background), 5%) 50%,
transparent 100% transparent 100%
); );
} }
@@ -255,8 +242,8 @@
@mixin light { @mixin light {
background: linear-gradient( background: linear-gradient(
to left, to left,
color-mix(in srgb, var(--theme-colors-background) 92%, black) 0%, darken(var(--theme-colors-background), 5%) 0%,
color-mix(in srgb, var(--theme-colors-background) 96%, black) 50%, darken(var(--theme-colors-background), 3%) 50%,
transparent 100% transparent 100%
); );
} }
@@ -274,8 +261,8 @@
@mixin dark { @mixin dark {
background: linear-gradient( background: linear-gradient(
to bottom, to bottom,
color-mix(in srgb, var(--theme-colors-background) 85%, black) 0%, darken(var(--theme-colors-background), 10%) 0%,
color-mix(in srgb, var(--theme-colors-background) 92%, black) 50%, darken(var(--theme-colors-background), 5%) 50%,
transparent 100% transparent 100%
); );
} }
@@ -283,8 +270,8 @@
@mixin light { @mixin light {
background: linear-gradient( background: linear-gradient(
to bottom, to bottom,
color-mix(in srgb, var(--theme-colors-background) 92%, black) 0%, darken(var(--theme-colors-background), 5%) 0%,
color-mix(in srgb, var(--theme-colors-background) 96%, black) 50%, darken(var(--theme-colors-background), 3%) 50%,
transparent 100% transparent 100%
); );
} }
@@ -2,7 +2,6 @@
import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element'; import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element';
import clsx from 'clsx'; import clsx from 'clsx';
import throttle from 'lodash/throttle';
import { AnimatePresence, motion } from 'motion/react'; import { AnimatePresence, motion } from 'motion/react';
import { useOverlayScrollbars } from 'overlayscrollbars-react'; import { useOverlayScrollbars } from 'overlayscrollbars-react';
import React, { import React, {
@@ -33,7 +32,6 @@ import {
useItemListStateSubscription, useItemListStateSubscription,
} from '/@/renderer/components/item-list/helpers/item-list-state'; } from '/@/renderer/components/item-list/helpers/item-list-state';
import { parseTableColumns } from '/@/renderer/components/item-list/helpers/parse-table-columns'; import { parseTableColumns } from '/@/renderer/components/item-list/helpers/parse-table-columns';
import { useListHotkeys } from '/@/renderer/components/item-list/helpers/use-list-hotkeys';
import { useStickyTableGroupRows } from '/@/renderer/components/item-list/item-table-list/hooks/use-sticky-table-group-rows'; import { useStickyTableGroupRows } from '/@/renderer/components/item-list/item-table-list/hooks/use-sticky-table-group-rows';
import { useStickyTableHeader } from '/@/renderer/components/item-list/item-table-list/hooks/use-sticky-table-header'; import { useStickyTableHeader } from '/@/renderer/components/item-list/item-table-list/hooks/use-sticky-table-header';
import { import {
@@ -44,6 +42,7 @@ import {
import { PlayerContext, usePlayer } from '/@/renderer/features/player/context/player-context'; import { PlayerContext, usePlayer } from '/@/renderer/features/player/context/player-context';
import { animationProps } from '/@/shared/components/animations/animation-props'; import { animationProps } from '/@/shared/components/animations/animation-props';
import { useFocusWithin } from '/@/shared/hooks/use-focus-within'; import { useFocusWithin } from '/@/shared/hooks/use-focus-within';
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
import { useMergedRef } from '/@/shared/hooks/use-merged-ref'; import { useMergedRef } from '/@/shared/hooks/use-merged-ref';
import { LibraryItem } from '/@/shared/types/domain-types'; import { LibraryItem } from '/@/shared/types/domain-types';
import { TableColumn } from '/@/shared/types/types'; import { TableColumn } from '/@/shared/types/types';
@@ -58,8 +57,8 @@ const hasRequiredItemProperties = (item: unknown): item is { id: string; serverI
item !== null && item !== null &&
'id' in item && 'id' in item &&
typeof (item as any).id === 'string' && typeof (item as any).id === 'string' &&
'_serverId' in item && 'serverId' in item &&
typeof (item as any)._serverId === 'string' typeof (item as any).serverId === 'string'
); );
}; };
@@ -77,7 +76,9 @@ const hasRequiredStateItemProperties = (
'_serverId' in item && '_serverId' in item &&
typeof (item as any)._serverId === 'string' && typeof (item as any)._serverId === 'string' &&
'_itemType' in item && '_itemType' in item &&
typeof (item as any)._itemType === 'string' typeof (item as any)._itemType === 'string' &&
'rowId' in item &&
typeof (item as any).rowId === 'string'
); );
}; };
@@ -94,7 +95,6 @@ interface VirtualizedTableGridProps {
cellPadding: 'lg' | 'md' | 'sm' | 'xl' | 'xs'; cellPadding: 'lg' | 'md' | 'sm' | 'xl' | 'xs';
controls: ItemControls; controls: ItemControls;
data: unknown[]; data: unknown[];
dataWithGroups: (null | unknown)[];
enableAlternateRowColors: boolean; enableAlternateRowColors: boolean;
enableColumnReorder: boolean; enableColumnReorder: boolean;
enableColumnResize: boolean; enableColumnResize: boolean;
@@ -103,7 +103,6 @@ interface VirtualizedTableGridProps {
enableHeader: boolean; enableHeader: boolean;
enableHorizontalBorders: boolean; enableHorizontalBorders: boolean;
enableRowHoverHighlight: boolean; enableRowHoverHighlight: boolean;
enableScrollShadow: boolean;
enableSelection: boolean; enableSelection: boolean;
enableVerticalBorders: boolean; enableVerticalBorders: boolean;
getRowHeight: (index: number, cellProps: TableItemProps) => number; getRowHeight: (index: number, cellProps: TableItemProps) => number;
@@ -137,8 +136,7 @@ const VirtualizedTableGrid = ({
CellComponent, CellComponent,
cellPadding, cellPadding,
controls, controls,
// data, data,
dataWithGroups,
enableAlternateRowColors, enableAlternateRowColors,
enableColumnReorder, enableColumnReorder,
enableColumnResize, enableColumnResize,
@@ -147,7 +145,6 @@ const VirtualizedTableGrid = ({
enableHeader, enableHeader,
enableHorizontalBorders, enableHorizontalBorders,
enableRowHoverHighlight, enableRowHoverHighlight,
enableScrollShadow,
enableSelection, enableSelection,
enableVerticalBorders, enableVerticalBorders,
getRowHeight, getRowHeight,
@@ -190,6 +187,53 @@ const VirtualizedTableGrid = ({
); );
}, [pinnedRightColumnCount, pinnedLeftColumnCount, totalColumnCount, columnWidth]); }, [pinnedRightColumnCount, pinnedLeftColumnCount, totalColumnCount, columnWidth]);
// Create data array with group headers inserted as null values
// Groups are defined by itemCount, so we calculate indexes based on cumulative item counts
const dataWithGroups = useMemo(() => {
const result: (null | unknown)[] = enableHeader ? [null] : [];
if (!groups || groups.length === 0) {
// No groups, just add all data
result.push(...data);
return result;
}
// Calculate group header indexes based on itemCounts
const groupIndexes: number[] = [];
let cumulativeDataIndex = 0;
const headerOffset = enableHeader ? 1 : 0;
groups.forEach((group, groupIndex) => {
// Group header appears before its items
// Index = header offset + cumulative data index + number of previous group headers
const groupHeaderIndex = headerOffset + cumulativeDataIndex + groupIndex;
groupIndexes.push(groupHeaderIndex);
cumulativeDataIndex += group.itemCount;
});
let dataIndex = 0;
const startIndex = enableHeader ? 1 : 0;
let groupHeaderCount = 0;
// Iterate through the expanded row space (data + group headers)
for (
let rowIndex = startIndex;
rowIndex < startIndex + data.length + groupIndexes.length;
rowIndex++
) {
// Check if this row should have a group header
const expectedGroupIndex = groupIndexes[groupHeaderCount];
if (expectedGroupIndex !== undefined && rowIndex === expectedGroupIndex) {
result.push(null); // Group header row
groupHeaderCount++;
} else if (dataIndex < data.length) {
result.push(data[dataIndex]);
dataIndex++;
}
}
return result;
}, [data, enableHeader, groups]);
const adjustedRowIndexMap = useMemo(() => { const adjustedRowIndexMap = useMemo(() => {
const map = new Map<number, number>(); const map = new Map<number, number>();
@@ -233,94 +277,71 @@ const VirtualizedTableGrid = ({
return map; return map;
}, [dataWithGroups, enableHeader, groups]); }, [dataWithGroups, enableHeader, groups]);
const stableConfigProps = useMemo( const itemProps: TableItemProps = useMemo(
() => ({ () => ({
activeRowId,
adjustedRowIndexMap,
calculatedColumnWidths,
cellPadding, cellPadding,
columns: parsedColumns, columns: parsedColumns,
controls, controls,
data: dataWithGroups,
enableAlternateRowColors,
enableColumnReorder,
enableColumnResize,
enableDrag,
enableExpansion,
enableHeader, enableHeader,
enableHorizontalBorders,
enableRowHoverHighlight,
enableSelection,
enableVerticalBorders,
getRowHeight, getRowHeight,
groups,
internalState, internalState,
itemType, itemType,
pinnedLeftColumnCount,
pinnedLeftColumnWidths,
pinnedRightColumnCount,
pinnedRightColumnWidths,
playerContext, playerContext,
size, size,
startRowIndex,
tableId, tableId,
}), }),
[ [
activeRowId,
adjustedRowIndexMap,
calculatedColumnWidths,
cellPadding, cellPadding,
parsedColumns, parsedColumns,
controls, controls,
dataWithGroups,
enableAlternateRowColors,
enableColumnReorder,
enableColumnResize,
enableDrag,
enableExpansion,
enableHeader, enableHeader,
enableHorizontalBorders,
enableRowHoverHighlight,
enableSelection,
enableVerticalBorders,
getRowHeight, getRowHeight,
groups,
internalState, internalState,
itemType, itemType,
pinnedLeftColumnCount,
pinnedLeftColumnWidths,
pinnedRightColumnCount,
pinnedRightColumnWidths,
playerContext, playerContext,
size, size,
startRowIndex,
tableId, tableId,
], ],
); );
const dynamicDataProps = useMemo(
() => ({
activeRowId,
adjustedRowIndexMap,
calculatedColumnWidths,
data: dataWithGroups,
pinnedLeftColumnCount,
pinnedLeftColumnWidths,
pinnedRightColumnCount,
pinnedRightColumnWidths,
startRowIndex,
}),
[
activeRowId,
adjustedRowIndexMap,
calculatedColumnWidths,
dataWithGroups,
pinnedLeftColumnCount,
pinnedLeftColumnWidths,
pinnedRightColumnCount,
pinnedRightColumnWidths,
startRowIndex,
],
);
const featureFlags = useMemo(
() => ({
enableAlternateRowColors,
enableColumnReorder,
enableColumnResize,
enableDrag,
enableExpansion,
enableHorizontalBorders,
enableRowHoverHighlight,
enableSelection,
enableVerticalBorders,
groups,
}),
[
enableAlternateRowColors,
enableColumnReorder,
enableColumnResize,
enableDrag,
enableExpansion,
enableHorizontalBorders,
enableRowHoverHighlight,
enableSelection,
enableVerticalBorders,
groups,
],
);
const itemProps: TableItemProps = useMemo(
() => ({
...stableConfigProps,
...dynamicDataProps,
...featureFlags,
}),
[stableConfigProps, dynamicDataProps, featureFlags],
);
const PinnedRowCell = useCallback( const PinnedRowCell = useCallback(
(cellProps: CellComponentProps & TableItemProps) => { (cellProps: CellComponentProps & TableItemProps) => {
return ( return (
@@ -431,7 +452,7 @@ const VirtualizedTableGrid = ({
/> />
</div> </div>
)} )}
{enableHeader && enableScrollShadow && showTopShadow && ( {enableHeader && showTopShadow && (
<div className={styles.itemTableTopScrollShadow} /> <div className={styles.itemTableTopScrollShadow} />
)} )}
{!!pinnedLeftColumnCount && ( {!!pinnedLeftColumnCount && (
@@ -491,7 +512,7 @@ const VirtualizedTableGrid = ({
/> />
</div> </div>
)} )}
{enableHeader && enableScrollShadow && showTopShadow && ( {enableHeader && showTopShadow && (
<div className={styles.itemTableTopScrollShadow} /> <div className={styles.itemTableTopScrollShadow} />
)} )}
<div className={styles.itemTableGridContainer} ref={mergedRowRef}> <div className={styles.itemTableGridContainer} ref={mergedRowRef}>
@@ -509,10 +530,10 @@ const VirtualizedTableGrid = ({
return getRowHeight(index + pinnedRowCount, cellProps); return getRowHeight(index + pinnedRowCount, cellProps);
}} }}
/> />
{pinnedLeftColumnCount > 0 && enableScrollShadow && showLeftShadow && ( {pinnedLeftColumnCount > 0 && showLeftShadow && (
<div className={styles.itemTableLeftScrollShadow} /> <div className={styles.itemTableLeftScrollShadow} />
)} )}
{pinnedRightColumnCount > 0 && enableScrollShadow && showRightShadow && ( {pinnedRightColumnCount > 0 && showRightShadow && (
<div className={styles.itemTableRightScrollShadow} /> <div className={styles.itemTableRightScrollShadow} />
)} )}
</div> </div>
@@ -562,7 +583,7 @@ const VirtualizedTableGrid = ({
/> />
</div> </div>
)} )}
{enableHeader && enableScrollShadow && showTopShadow && ( {enableHeader && showTopShadow && (
<div className={styles.itemTableTopScrollShadow} /> <div className={styles.itemTableTopScrollShadow} />
)} )}
<div <div
@@ -593,54 +614,6 @@ const VirtualizedTableGrid = ({
VirtualizedTableGrid.displayName = 'VirtualizedTableGrid'; VirtualizedTableGrid.displayName = 'VirtualizedTableGrid';
const MemoizedVirtualizedTableGrid = memo(VirtualizedTableGrid, (prevProps, nextProps) => {
return (
prevProps.activeRowId === nextProps.activeRowId &&
prevProps.calculatedColumnWidths === nextProps.calculatedColumnWidths &&
prevProps.cellPadding === nextProps.cellPadding &&
prevProps.controls === nextProps.controls &&
prevProps.data === nextProps.data &&
prevProps.dataWithGroups === nextProps.dataWithGroups &&
prevProps.enableAlternateRowColors === nextProps.enableAlternateRowColors &&
prevProps.enableColumnReorder === nextProps.enableColumnReorder &&
prevProps.enableColumnResize === nextProps.enableColumnResize &&
prevProps.enableDrag === nextProps.enableDrag &&
prevProps.enableExpansion === nextProps.enableExpansion &&
prevProps.enableHeader === nextProps.enableHeader &&
prevProps.enableHorizontalBorders === nextProps.enableHorizontalBorders &&
prevProps.enableRowHoverHighlight === nextProps.enableRowHoverHighlight &&
prevProps.enableScrollShadow === nextProps.enableScrollShadow &&
prevProps.enableSelection === nextProps.enableSelection &&
prevProps.enableVerticalBorders === nextProps.enableVerticalBorders &&
prevProps.getRowHeight === nextProps.getRowHeight &&
prevProps.groups === nextProps.groups &&
prevProps.headerHeight === nextProps.headerHeight &&
prevProps.internalState === nextProps.internalState &&
prevProps.itemType === nextProps.itemType &&
prevProps.mergedRowRef === nextProps.mergedRowRef &&
prevProps.onRangeChanged === nextProps.onRangeChanged &&
prevProps.parsedColumns === nextProps.parsedColumns &&
prevProps.pinnedLeftColumnCount === nextProps.pinnedLeftColumnCount &&
prevProps.pinnedLeftColumnRef === nextProps.pinnedLeftColumnRef &&
prevProps.pinnedRightColumnCount === nextProps.pinnedRightColumnCount &&
prevProps.pinnedRightColumnRef === nextProps.pinnedRightColumnRef &&
prevProps.pinnedRowCount === nextProps.pinnedRowCount &&
prevProps.pinnedRowRef === nextProps.pinnedRowRef &&
prevProps.playerContext === nextProps.playerContext &&
prevProps.showLeftShadow === nextProps.showLeftShadow &&
prevProps.showRightShadow === nextProps.showRightShadow &&
prevProps.showTopShadow === nextProps.showTopShadow &&
prevProps.size === nextProps.size &&
prevProps.startRowIndex === nextProps.startRowIndex &&
prevProps.tableId === nextProps.tableId &&
prevProps.totalColumnCount === nextProps.totalColumnCount &&
prevProps.totalRowCount === nextProps.totalRowCount &&
prevProps.CellComponent === nextProps.CellComponent
);
});
MemoizedVirtualizedTableGrid.displayName = 'MemoizedVirtualizedTableGrid';
export interface TableGroupHeader { export interface TableGroupHeader {
itemCount: number; itemCount: number;
render: (props: { render: (props: {
@@ -698,9 +671,7 @@ interface ItemTableListProps {
enableHeader?: boolean; enableHeader?: boolean;
enableHorizontalBorders?: boolean; enableHorizontalBorders?: boolean;
enableRowHoverHighlight?: boolean; enableRowHoverHighlight?: boolean;
enableScrollShadow?: boolean;
enableSelection?: boolean; enableSelection?: boolean;
enableSelectionDialog?: boolean;
enableStickyGroupRows?: boolean; enableStickyGroupRows?: boolean;
enableStickyHeader?: boolean; enableStickyHeader?: boolean;
enableVerticalBorders?: boolean; enableVerticalBorders?: boolean;
@@ -741,7 +712,6 @@ const BaseItemTableList = ({
enableHeader = true, enableHeader = true,
enableHorizontalBorders = false, enableHorizontalBorders = false,
enableRowHoverHighlight = true, enableRowHoverHighlight = true,
enableScrollShadow = true,
enableSelection = true, enableSelection = true,
enableStickyGroupRows = false, enableStickyGroupRows = false,
enableStickyHeader = false, enableStickyHeader = false,
@@ -769,53 +739,6 @@ const BaseItemTableList = ({
const [centerContainerWidth, setCenterContainerWidth] = useState(0); const [centerContainerWidth, setCenterContainerWidth] = useState(0);
const [totalContainerWidth, setTotalContainerWidth] = useState(0); const [totalContainerWidth, setTotalContainerWidth] = useState(0);
// Compute dataWithGroups once to avoid duplicate computation
// This is used by both VirtualizedTableGrid and getDataFn
const dataWithGroups = useMemo(() => {
const result: (null | unknown)[] = enableHeader ? [null] : [];
if (!groups || groups.length === 0) {
// No groups, just add all data
result.push(...data);
return result;
}
// Calculate group header indexes based on itemCounts
const groupIndexes: number[] = [];
let cumulativeDataIndex = 0;
const headerOffset = enableHeader ? 1 : 0;
groups.forEach((group, groupIndex) => {
// Group header appears before its items
// Index = header offset + cumulative data index + number of previous group headers
const groupHeaderIndex = headerOffset + cumulativeDataIndex + groupIndex;
groupIndexes.push(groupHeaderIndex);
cumulativeDataIndex += group.itemCount;
});
let dataIndex = 0;
const startIndex = enableHeader ? 1 : 0;
let groupHeaderCount = 0;
// Iterate through the expanded row space (data + group headers)
for (
let rowIndex = startIndex;
rowIndex < startIndex + data.length + groupIndexes.length;
rowIndex++
) {
// Check if this row should have a group header
const expectedGroupIndex = groupIndexes[groupHeaderCount];
if (expectedGroupIndex !== undefined && rowIndex === expectedGroupIndex) {
result.push(null); // Group header row
groupHeaderCount++;
} else if (dataIndex < data.length) {
result.push(data[dataIndex]);
dataIndex++;
}
}
return result;
}, [data, enableHeader, groups]);
// Compute distributed widths: unpinned columns with autoWidth will share any remaining space // Compute distributed widths: unpinned columns with autoWidth will share any remaining space
// When autoSizeColumns is true, all column widths are treated as proportions and scaled to fit the container // When autoSizeColumns is true, all column widths are treated as proportions and scaled to fit the container
const calculatedColumnWidths = useMemo(() => { const calculatedColumnWidths = useMemo(() => {
@@ -945,20 +868,11 @@ const BaseItemTableList = ({
const stickyHeader = stickyHeaderRef.current; const stickyHeader = stickyHeaderRef.current;
const container = containerRef.current; const container = containerRef.current;
let isMounted = true;
const updatePosition = () => { const updatePosition = () => {
// Guard against updates after unmount const containerRect = container.getBoundingClientRect();
if (!isMounted || !stickyHeader || !container) { stickyHeader.style.left = `${containerRect.left}px`;
return; stickyHeader.style.width = `${containerRect.width}px`;
}
try {
const containerRect = container.getBoundingClientRect();
stickyHeader.style.left = `${containerRect.left}px`;
stickyHeader.style.width = `${containerRect.width}px`;
} catch {
// Silently handle errors if elements are no longer in DOM
}
}; };
updatePosition(); updatePosition();
@@ -967,7 +881,6 @@ const BaseItemTableList = ({
window.addEventListener('scroll', updatePosition, true); window.addEventListener('scroll', updatePosition, true);
return () => { return () => {
isMounted = false;
window.removeEventListener('resize', updatePosition); window.removeEventListener('resize', updatePosition);
window.removeEventListener('scroll', updatePosition, true); window.removeEventListener('scroll', updatePosition, true);
}; };
@@ -983,22 +896,13 @@ const BaseItemTableList = ({
updateWidth(); updateWidth();
let debounceTimeout: NodeJS.Timeout | null = null;
const resizeObserver = new ResizeObserver(() => { const resizeObserver = new ResizeObserver(() => {
if (debounceTimeout) { updateWidth();
clearTimeout(debounceTimeout);
}
debounceTimeout = setTimeout(() => {
updateWidth();
}, 100);
}); });
resizeObserver.observe(el); resizeObserver.observe(el);
return () => { return () => {
if (debounceTimeout) {
clearTimeout(debounceTimeout);
}
resizeObserver.disconnect(); resizeObserver.disconnect();
}; };
}, []); }, []);
@@ -1014,22 +918,13 @@ const BaseItemTableList = ({
updateWidth(); updateWidth();
let debounceTimeout: NodeJS.Timeout | null = null;
const resizeObserver = new ResizeObserver(() => { const resizeObserver = new ResizeObserver(() => {
if (debounceTimeout) { updateWidth();
clearTimeout(debounceTimeout);
}
debounceTimeout = setTimeout(() => {
updateWidth();
}, 100);
}); });
resizeObserver.observe(el); resizeObserver.observe(el);
return () => { return () => {
if (debounceTimeout) {
clearTimeout(debounceTimeout);
}
resizeObserver.disconnect(); resizeObserver.disconnect();
}; };
}, [autoFitColumns]); }, [autoFitColumns]);
@@ -1472,6 +1367,7 @@ const BaseItemTableList = ({
const scrollTop = (e.currentTarget as HTMLDivElement).scrollTop; const scrollTop = (e.currentTarget as HTMLDivElement).scrollTop;
const scrollLeft = (e.currentTarget as HTMLDivElement).scrollLeft; const scrollLeft = (e.currentTarget as HTMLDivElement).scrollLeft;
// Prevent recursive scroll events
const isScrolling = { const isScrolling = {
header: false, header: false,
pinnedLeft: false, pinnedLeft: false,
@@ -1595,14 +1491,8 @@ const BaseItemTableList = ({
} }
// Add resize observer to maintain height sync // Add resize observer to maintain height sync
let heightSyncDebounceTimeout: NodeJS.Timeout | null = null;
const resizeObserver = new ResizeObserver(() => { const resizeObserver = new ResizeObserver(() => {
if (heightSyncDebounceTimeout) { syncHeights();
clearTimeout(heightSyncDebounceTimeout);
}
heightSyncDebounceTimeout = setTimeout(() => {
syncHeights();
}, 100);
}); });
resizeObserver.observe(row); resizeObserver.observe(row);
@@ -1637,9 +1527,6 @@ const BaseItemTableList = ({
pinnedRight.removeEventListener('wheel', setActiveElementFromWheel); pinnedRight.removeEventListener('wheel', setActiveElementFromWheel);
pinnedRight.removeEventListener('scroll', syncScroll); pinnedRight.removeEventListener('scroll', syncScroll);
} }
if (heightSyncDebounceTimeout) {
clearTimeout(heightSyncDebounceTimeout);
}
resizeObserver.disconnect(); resizeObserver.disconnect();
}; };
} }
@@ -1660,20 +1547,19 @@ const BaseItemTableList = ({
return () => clearTimeout(timeout); return () => clearTimeout(timeout);
} }
const checkScrollPosition = throttle(() => { const checkScrollPosition = () => {
const scrollLeft = row.scrollLeft; const scrollLeft = row.scrollLeft;
const maxScrollLeft = row.scrollWidth - row.clientWidth; const maxScrollLeft = row.scrollWidth - row.clientWidth;
setShowLeftShadow(pinnedLeftColumnCount > 0 && scrollLeft > 0); setShowLeftShadow(pinnedLeftColumnCount > 0 && scrollLeft > 0);
setShowRightShadow(pinnedRightColumnCount > 0 && scrollLeft < maxScrollLeft); setShowRightShadow(pinnedRightColumnCount > 0 && scrollLeft < maxScrollLeft);
}, 50); };
checkScrollPosition(); checkScrollPosition();
row.addEventListener('scroll', checkScrollPosition, { passive: true }); row.addEventListener('scroll', checkScrollPosition);
return () => { return () => {
checkScrollPosition.cancel();
row.removeEventListener('scroll', checkScrollPosition); row.removeEventListener('scroll', checkScrollPosition);
}; };
}, [pinnedLeftColumnCount, pinnedRightColumnCount]); }, [pinnedLeftColumnCount, pinnedRightColumnCount]);
@@ -1694,17 +1580,16 @@ const BaseItemTableList = ({
// When right-pinned columns exist, use right pinned column's scroll position // When right-pinned columns exist, use right pinned column's scroll position
const scrollElement = pinnedRightColumnCount > 0 && pinnedRight ? pinnedRight : row; const scrollElement = pinnedRightColumnCount > 0 && pinnedRight ? pinnedRight : row;
const checkScrollPosition = throttle(() => { const checkScrollPosition = () => {
const currentScrollTop = scrollElement.scrollTop; const currentScrollTop = scrollElement.scrollTop;
setShowTopShadow(currentScrollTop > 0); setShowTopShadow(currentScrollTop > 0);
}, 50); };
checkScrollPosition(); checkScrollPosition();
scrollElement.addEventListener('scroll', checkScrollPosition, { passive: true }); scrollElement.addEventListener('scroll', checkScrollPosition);
return () => { return () => {
checkScrollPosition.cancel();
scrollElement.removeEventListener('scroll', checkScrollPosition); scrollElement.removeEventListener('scroll', checkScrollPosition);
}; };
}, [enableHeader, pinnedRightColumnCount]); }, [enableHeader, pinnedRightColumnCount]);
@@ -1769,20 +1654,11 @@ const BaseItemTableList = ({
const stickyGroupRow = stickyGroupRowRef.current; const stickyGroupRow = stickyGroupRowRef.current;
const container = containerRef.current; const container = containerRef.current;
let isMounted = true;
const updatePosition = () => { const updatePosition = () => {
// Guard against updates after unmount const containerRect = container.getBoundingClientRect();
if (!isMounted || !stickyGroupRow || !container) { stickyGroupRow.style.left = `${containerRect.left}px`;
return; stickyGroupRow.style.width = `${containerRect.width}px`;
}
try {
const containerRect = container.getBoundingClientRect();
stickyGroupRow.style.left = `${containerRect.left}px`;
stickyGroupRow.style.width = `${containerRect.width}px`;
} catch {
// Silently handle errors if elements are no longer in DOM
}
}; };
updatePosition(); updatePosition();
@@ -1791,15 +1667,52 @@ const BaseItemTableList = ({
window.addEventListener('scroll', updatePosition, true); window.addEventListener('scroll', updatePosition, true);
return () => { return () => {
isMounted = false;
window.removeEventListener('resize', updatePosition); window.removeEventListener('resize', updatePosition);
window.removeEventListener('scroll', updatePosition, true); window.removeEventListener('scroll', updatePosition, true);
}; };
}, [shouldRenderStickyGroupRow]); }, [shouldRenderStickyGroupRow]);
const getDataFn = useCallback(() => { const getDataFn = useCallback(() => {
return dataWithGroups; const result: (null | unknown)[] = enableHeader ? [null] : [];
}, [dataWithGroups]);
if (!groups || groups.length === 0) {
// No groups, just add all data
result.push(...data);
return result;
}
// Calculate group header indexes based on itemCounts
const groupIndexes: number[] = [];
let cumulativeDataIndex = 0;
const headerOffset = enableHeader ? 1 : 0;
groups.forEach((group, groupIndex) => {
// Index = header offset + cumulative data index + number of previous group headers
const groupHeaderIndex = headerOffset + cumulativeDataIndex + groupIndex;
groupIndexes.push(groupHeaderIndex);
cumulativeDataIndex += group.itemCount;
});
let dataIndex = 0;
const startIndex = enableHeader ? 1 : 0;
let groupHeaderCount = 0;
for (
let rowIndex = startIndex;
rowIndex < startIndex + data.length + groupIndexes.length;
rowIndex++
) {
const expectedGroupIndex = groupIndexes[groupHeaderCount];
if (expectedGroupIndex !== undefined && rowIndex === expectedGroupIndex) {
result.push(null);
groupHeaderCount++;
} else if (dataIndex < data.length) {
result.push(data[dataIndex]);
dataIndex++;
}
}
return result;
}, [data, enableHeader, groups]);
const extractRowId = useMemo(() => createExtractRowId(getRowId), [getRowId]); const extractRowId = useMemo(() => createExtractRowId(getRowId), [getRowId]);
@@ -1808,9 +1721,7 @@ const BaseItemTableList = ({
// Helper function to get ItemListStateItemWithRequiredProperties (rowId is separate, not part of item) // Helper function to get ItemListStateItemWithRequiredProperties (rowId is separate, not part of item)
const getStateItem = useCallback( const getStateItem = useCallback(
(item: any): ItemListStateItemWithRequiredProperties | null => { (item: any): ItemListStateItemWithRequiredProperties | null => {
if (!hasRequiredItemProperties(item)) { if (!hasRequiredItemProperties(item)) return null;
return null;
}
if ( if (
typeof item === 'object' && typeof item === 'object' &&
item !== null && item !== null &&
@@ -1838,7 +1749,7 @@ const BaseItemTableList = ({
if (validSelected.length > 0) { if (validSelected.length > 0) {
const lastSelected = validSelected[validSelected.length - 1]; const lastSelected = validSelected[validSelected.length - 1];
currentIndex = data.findIndex( currentIndex = data.findIndex(
(d) => extractRowId(d) === extractRowId(lastSelected), (d) => hasRequiredItemProperties(d) && d.id === lastSelected.id,
); );
} }
@@ -1853,111 +1764,93 @@ const BaseItemTableList = ({
const newItem: any = data[newIndex]; const newItem: any = data[newIndex];
if (!newItem) return; if (!newItem) return;
const newItemListItem = getStateItem(newItem); // Handle Shift + Arrow for incremental range selection (matches shift+click behavior)
if (newItemListItem && extractRowId(newItemListItem)) { if (e.shiftKey) {
internalState.setSelected([newItemListItem]); const selectedItems = internalState.getSelected();
} const validSelectedItems = selectedItems.filter(hasRequiredStateItemProperties);
const lastSelectedItem = validSelectedItems[validSelectedItems.length - 1];
// Check if we need to scroll by determining if the item is at the edge of the viewport if (lastSelectedItem) {
const gridIndex = enableHeader ? newIndex + 1 : newIndex; // Find the indices of the last selected item and new item
const lastRowId = lastSelectedItem.rowId;
const lastIndex = data.findIndex((d) => {
const rowId = extractRowId(d);
return rowId === lastRowId;
});
const mainContainer = rowRef.current?.childNodes[0] as HTMLDivElement | undefined; if (lastIndex !== -1 && newIndex !== -1) {
const pinnedRightContainer = pinnedRightColumnRef.current?.childNodes[0] as // Create range selection from last selected to new position
| HTMLDivElement const startIndex = Math.min(lastIndex, newIndex);
| undefined; const stopIndex = Math.max(lastIndex, newIndex);
// Use right pinned column scroll position if right-pinned columns exist const rangeItems: ItemListStateItemWithRequiredProperties[] = [];
const scrollContainer = for (let i = startIndex; i <= stopIndex; i++) {
pinnedRightColumnCount > 0 && pinnedRightContainer const rangeItem = data[i];
? pinnedRightContainer const stateItem = getStateItem(rangeItem);
: mainContainer; if (stateItem && extractRowId(stateItem)) {
rangeItems.push(stateItem);
}
}
if (scrollContainer) { // Add range items to selection (matching shift+click behavior)
const viewportTop = scrollContainer.scrollTop; const currentSelected = internalState.getSelected();
const viewportHeight = scrollContainer.clientHeight; const validSelected = currentSelected.filter(
const viewportBottom = viewportTop + viewportHeight; hasRequiredStateItemProperties,
);
const newSelected: ItemListStateItemWithRequiredProperties[] = [
...validSelected,
];
rangeItems.forEach((rangeItem) => {
const rangeRowId = extractRowId(rangeItem);
if (
rangeRowId &&
!newSelected.some(
(selected) => extractRowId(selected) === rangeRowId,
)
) {
newSelected.push(rangeItem);
}
});
const rowTop = calculateScrollTopForIndex(gridIndex); // Ensure the last item in selection is the item at newIndex for incremental extension
const adjustedIndex = enableHeader ? Math.max(0, newIndex - 1) : newIndex; const newItemListItem = getStateItem(newItem);
const mockCellProps: TableItemProps = { if (newItemListItem && extractRowId(newItemListItem)) {
cellPadding, const newItemRowId = extractRowId(newItemListItem);
columns: parsedColumns, // Remove the new item from its current position if it exists
controls: {} as ItemControls, const filteredSelected = newSelected.filter(
data: enableHeader ? [null, ...data] : data, (item) => extractRowId(item) !== newItemRowId,
enableAlternateRowColors, );
enableExpansion, // Add it at the end so it becomes the last selected item
enableHeader, filteredSelected.push(newItemListItem);
enableHorizontalBorders, internalState.setSelected(filteredSelected);
enableRowHoverHighlight, }
enableSelection, }
enableVerticalBorders,
getRowHeight: () => DEFAULT_ROW_HEIGHT,
internalState: {} as ItemListStateActions,
itemType,
playerContext,
size,
tableId,
};
let calculatedRowHeight: number;
if (typeof rowHeight === 'number') {
calculatedRowHeight = rowHeight;
} else if (typeof rowHeight === 'function') {
calculatedRowHeight = rowHeight(adjustedIndex, mockCellProps);
} else { } else {
calculatedRowHeight = DEFAULT_ROW_HEIGHT; // No previous selection, just select the new item
const newItemListItem = getStateItem(newItem);
if (newItemListItem && extractRowId(newItemListItem)) {
internalState.setSelected([newItemListItem]);
}
} }
} else {
const rowBottom = rowTop + calculatedRowHeight; // Without Shift: select only the new item
const newItemListItem = getStateItem(newItem);
// Check if row is fully visible within viewport if (newItemListItem && extractRowId(newItemListItem)) {
const isFullyVisible = rowTop >= viewportTop && rowBottom <= viewportBottom; internalState.setSelected([newItemListItem]);
// Check if row is at the edge (top or bottom of viewport)
const isAtTopEdge = rowTop < viewportTop;
const isAtBottomEdge = rowBottom >= viewportBottom;
// Only scroll if the item is not fully visible or at the edge
if (!isFullyVisible || isAtTopEdge || isAtBottomEdge) {
// Determine alignment based on direction
const align: 'bottom' | 'top' =
e.key === 'ArrowDown' && isAtBottomEdge
? 'bottom'
: e.key === 'ArrowUp' && isAtTopEdge
? 'top'
: isAtBottomEdge
? 'bottom'
: isAtTopEdge
? 'top'
: 'top';
scrollToTableIndex(gridIndex, { align });
} }
} }
const offset = calculateScrollTopForIndex(newIndex);
scrollToTableOffset(offset);
}, },
[ [
data, data,
enableSelection, enableSelection,
internalState, internalState,
calculateScrollTopForIndex, calculateScrollTopForIndex,
scrollToTableIndex, scrollToTableOffset,
extractRowId, extractRowId,
getStateItem, getStateItem,
pinnedRightColumnCount,
enableHeader,
cellPadding,
parsedColumns,
enableAlternateRowColors,
enableExpansion,
enableHorizontalBorders,
enableRowHoverHighlight,
enableVerticalBorders,
itemType,
playerContext,
size,
tableId,
DEFAULT_ROW_HEIGHT,
rowHeight,
], ],
); );
@@ -2350,12 +2243,20 @@ const BaseItemTableList = ({
stickyGroupTop, stickyGroupTop,
]); ]);
useListHotkeys({ useHotkeys([
controls, [
focused, 'mod+a',
internalState, () => {
itemType, if (focused) {
}); if (internalState.isAllSelected()) {
internalState.deselectAll();
} else {
internalState.selectAll();
}
}
},
],
]);
return ( return (
<motion.div <motion.div
@@ -2375,14 +2276,13 @@ const BaseItemTableList = ({
> >
{StickyHeader} {StickyHeader}
{StickyGroupRow} {StickyGroupRow}
<MemoizedVirtualizedTableGrid <VirtualizedTableGrid
activeRowId={activeRowId} activeRowId={activeRowId}
calculatedColumnWidths={calculatedColumnWidths} calculatedColumnWidths={calculatedColumnWidths}
CellComponent={CellComponent} CellComponent={CellComponent}
cellPadding={cellPadding} cellPadding={cellPadding}
controls={controls} controls={controls}
data={data} data={data}
dataWithGroups={dataWithGroups}
enableAlternateRowColors={enableAlternateRowColors} enableAlternateRowColors={enableAlternateRowColors}
enableColumnReorder={!!onColumnReordered} enableColumnReorder={!!onColumnReordered}
enableColumnResize={!!onColumnResized} enableColumnResize={!!onColumnResized}
@@ -2391,7 +2291,6 @@ const BaseItemTableList = ({
enableHeader={enableHeader} enableHeader={enableHeader}
enableHorizontalBorders={enableHorizontalBorders} enableHorizontalBorders={enableHorizontalBorders}
enableRowHoverHighlight={enableRowHoverHighlight} enableRowHoverHighlight={enableRowHoverHighlight}
enableScrollShadow={enableScrollShadow}
enableSelection={enableSelection} enableSelection={enableSelection}
enableVerticalBorders={enableVerticalBorders} enableVerticalBorders={enableVerticalBorders}
getRowHeight={getRowHeight} getRowHeight={getRowHeight}
@@ -2419,7 +2318,6 @@ const BaseItemTableList = ({
totalRowCount={totalRowCount} totalRowCount={totalRowCount}
/> />
<ExpandedContainer internalState={internalState} itemType={itemType} /> <ExpandedContainer internalState={internalState} itemType={itemType} />
{/* {enableSelectionDialog && <SelectionDialog internalState={internalState} />} */}
</motion.div> </motion.div>
); );
}; };
@@ -1,22 +0,0 @@
.selection-indicator {
position: absolute;
bottom: 0;
left: 50%;
z-index: 100;
min-width: 320px;
padding: var(--theme-spacing-sm) var(--theme-spacing-md);
color: var(--theme-colors-surface-foreground);
background: color-mix(in srgb, var(--theme-colors-surface) 85%, transparent);
border: 1px solid color-mix(in srgb, var(--theme-colors-border) 50%, transparent);
border-radius: var(--theme-radius-md);
box-shadow:
2px 2px 10px 2px rgb(0 0 0 / 40%),
0 0 0 1px rgb(255 255 255 / 5%);
backdrop-filter: blur(12px) saturate(180%);
transform: translateX(-50%);
}
.info-icon {
display: flex;
cursor: pointer;
}
@@ -1,135 +0,0 @@
import { AnimatePresence, motion } from 'motion/react';
import { useTranslation } from 'react-i18next';
import styles from './selection-dialog.module.css';
import i18n from '/@/i18n/i18n';
import {
ItemListStateActions,
useItemListStateSubscription,
} from '/@/renderer/components/item-list/helpers/item-list-state';
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { animationProps } from '/@/shared/components/animations/animation-props';
import { Group } from '/@/shared/components/group/group';
import { HoverCard } from '/@/shared/components/hover-card/hover-card';
import { Icon } from '/@/shared/components/icon/icon';
import { Kbd } from '/@/shared/components/kbd/kbd';
import { Table } from '/@/shared/components/table/table';
import { Text } from '/@/shared/components/text/text';
const controls = [
{
control1: <Kbd>CTRL</Kbd>,
control2: <Kbd>A</Kbd>,
label: i18n.t('action.selectAll', { postProcess: 'sentenceCase' }),
},
{
control1: <Kbd>CTRL</Kbd>,
control2: <Icon fill="default" icon="mouseLeftClick" />,
label: i18n.t('action.addOrRemoveFromSelection', { postProcess: 'sentenceCase' }),
},
{
control1: <Kbd>SHIFT</Kbd>,
control2: <Icon fill="default" icon="mouseLeftClick" />,
label: i18n.t('action.selectRangeOfItems', { postProcess: 'sentenceCase' }),
},
];
export const SelectionDialog = ({ internalState }: { internalState: ItemListStateActions }) => {
const { t } = useTranslation();
const isListExpanded = useItemListStateSubscription(internalState, (state) =>
state ? state.expanded.size > 0 : false,
);
const selectedCount = useItemListStateSubscription(internalState, (state) =>
state ? state.selected.size : 0,
);
const handleClearSelection = () => {
internalState.clearSelected();
};
const handleOpenMoreActions = (event: React.MouseEvent<unknown>) => {
event.preventDefault();
event.stopPropagation();
const selectedItems = internalState.getSelected();
if (selectedItems.length === 0) {
return;
}
ContextMenuController.call({
cmd: { items: selectedItems as any[], type: (selectedItems[0] as any)._itemType },
event,
});
};
const isOpen = selectedCount > 0;
return (
<AnimatePresence initial={false} mode="sync">
{isOpen && (
<motion.div
{...animationProps.fadeIn}
className={styles.selectionIndicator}
style={{ bottom: isListExpanded ? '320px' : '1rem' }}
>
<Group gap="xl" justify="space-between">
<Group gap="sm">
<HoverCard offset={20} position="top">
<HoverCard.Target>
<span className={styles.infoIcon}>
<Icon icon="keyboard" />
</span>
</HoverCard.Target>
<HoverCard.Dropdown>
<Table>
<Table.Tbody>
{controls.map((control) => (
<Table.Tr key={control.label}>
<Table.Td ta="start">
{control.control1}
</Table.Td>
<Table.Td>+</Table.Td>
<Table.Td ta="center">
{control.control2}
</Table.Td>
<Table.Td>
<Text size="xs">{control.label}</Text>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</HoverCard.Dropdown>
</HoverCard>
<Text fw={500} isNoSelect size="sm">
{t('common.countSelected', { count: selectedCount })}
</Text>
</Group>
<Group gap="xs">
<ActionIcon
icon="x"
iconProps={{ size: 'xl' }}
onClick={handleClearSelection}
size="xs"
variant="subtle"
/>
<ActionIcon
icon="ellipsisHorizontal"
iconProps={{ size: 'xl' }}
onClick={handleOpenMoreActions}
size="xs"
variant="subtle"
/>
</Group>
</Group>
</motion.div>
)}
</AnimatePresence>
);
};
@@ -66,7 +66,6 @@ export interface ItemListComponentProps<TQuery> {
export interface ItemListGridComponentProps<TQuery> extends ItemListComponentProps<TQuery> { export interface ItemListGridComponentProps<TQuery> extends ItemListComponentProps<TQuery> {
gap?: 'lg' | 'md' | 'sm' | 'xl' | 'xs'; gap?: 'lg' | 'md' | 'sm' | 'xl' | 'xs';
itemsPerRow?: number; itemsPerRow?: number;
size?: 'compact' | 'default' | 'large';
} }
export interface ItemListHandle { export interface ItemListHandle {
@@ -6,7 +6,6 @@ import styles from './native-scroll-area.module.css';
import { PageHeader, PageHeaderProps } from '/@/renderer/components/page-header/page-header'; import { PageHeader, PageHeaderProps } from '/@/renderer/components/page-header/page-header';
import { useWindowSettings } from '/@/renderer/store/settings.store'; import { useWindowSettings } from '/@/renderer/store/settings.store';
import { useMergedRef } from '/@/shared/hooks/use-merged-ref'; import { useMergedRef } from '/@/shared/hooks/use-merged-ref';
import { useThrottledCallback } from '/@/shared/hooks/use-throttled-callback';
import { Platform } from '/@/shared/types/types'; import { Platform } from '/@/shared/types/types';
interface NativeScrollAreaProps { interface NativeScrollAreaProps {
@@ -27,31 +26,35 @@ const BaseNativeScrollArea = forwardRef(
const { windowBarStyle } = useWindowSettings(); const { windowBarStyle } = useWindowSettings();
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
const scrollHandler = useThrottledCallback((e: Event) => { const scrollHandlerRef = useRef<null | number>(null);
if (noHeader || !pageHeaderProps) {
return;
}
const scrollElement = e?.target as HTMLDivElement;
if (!scrollElement || !containerRef.current) {
return;
}
const offset = pageHeaderProps.offset || 0;
const scrollTop = scrollElement.scrollTop;
if (scrollTop > offset) {
containerRef.current.setAttribute('data-scrolled', 'true');
} else {
containerRef.current.setAttribute('data-scrolled', 'false');
}
}, 100);
const [initialize] = useOverlayScrollbars({ const [initialize] = useOverlayScrollbars({
defer: false, defer: false,
events: { events: {
scroll: (_instance, e) => { scroll: (_instance, e) => {
scrollHandler(e); if (scrollHandlerRef.current) {
cancelAnimationFrame(scrollHandlerRef.current);
}
scrollHandlerRef.current = requestAnimationFrame(() => {
if (noHeader || !pageHeaderProps) {
return;
}
const scrollElement = e?.target as HTMLDivElement;
if (!scrollElement || !containerRef.current) {
return;
}
const offset = pageHeaderProps.offset || 0;
const scrollTop = scrollElement.scrollTop;
if (scrollTop > offset) {
containerRef.current.setAttribute('data-scrolled', 'true');
} else {
containerRef.current.setAttribute('data-scrolled', 'false');
}
});
}, },
}, },
options: { options: {
@@ -37,18 +37,6 @@
input { input {
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
} }
[role='button'] {
-webkit-app-region: no-drag;
}
a {
-webkit-app-region: no-drag;
}
[style*='cursor: pointer'] {
-webkit-app-region: no-drag;
}
} }
.header.pad-right { .header.pad-right {
@@ -1,4 +1,3 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { QueryBuilderOption } from '/@/renderer/components/query-builder/query-builder-option'; import { QueryBuilderOption } from '/@/renderer/components/query-builder/query-builder-option';
@@ -108,17 +107,15 @@ export const QueryBuilder = ({
onChangeType({ groupIndex, level, value }); onChangeType({ groupIndex, level, value });
}; };
const boxStyle = useMemo(
() => ({
border: '1px solid var(--theme-colors-border)',
borderRadius: 'var(--theme-radius-md)',
marginLeft: level > 0 ? '20px' : '0px',
}),
[level],
);
return ( return (
<Box p="md" style={boxStyle}> <Box
p="md"
style={{
border: '1px solid var(--theme-colors-border)',
borderRadius: 'var(--theme-radius-md)',
marginLeft: level > 0 ? '20px' : '0px',
}}
>
<Stack gap="sm"> <Stack gap="sm">
<Group gap="sm" justify="space-between" wrap="nowrap"> <Group gap="sm" justify="space-between" wrap="nowrap">
<Group gap="sm" wrap="nowrap"> <Group gap="sm" wrap="nowrap">
@@ -67,13 +67,16 @@ export const MultiSelectWithInvalidData = ({ data, defaultValue, ...props }: Mul
return [data, []]; return [data, []];
}, [data, defaultValue]); }, [data, defaultValue]);
const error = useMemo( return (
() => <MultiSelect
missing.length data={fullData}
? t('error.badValue', { postProcess: 'sentenceCase', value: missing }) defaultValue={defaultValue}
: undefined, error={
[missing, t], missing.length
? t('error.badValue', { postProcess: 'sentenceCase', value: missing })
: undefined
}
{...props}
/>
); );
return <MultiSelect data={fullData} defaultValue={defaultValue} error={error} {...props} />;
}; };
-13
View File
@@ -1,18 +1,11 @@
import { LibraryItem, Song } from '/@/shared/types/domain-types'; import { LibraryItem, Song } from '/@/shared/types/domain-types';
export type AutoDJQueueAddedEventPayload = {
songCount: number;
};
export type EventMap = { export type EventMap = {
AUTODJ_QUEUE_ADDED: AutoDJQueueAddedEventPayload;
ITEM_LIST_REFRESH: ItemListRefreshEventPayload; ITEM_LIST_REFRESH: ItemListRefreshEventPayload;
ITEM_LIST_UPDATE_ITEM: ItemListUpdateItemEventPayload; ITEM_LIST_UPDATE_ITEM: ItemListUpdateItemEventPayload;
MEDIA_NEXT: MediaNextEventPayload; MEDIA_NEXT: MediaNextEventPayload;
MEDIA_PREV: MediaPrevEventPayload; MEDIA_PREV: MediaPrevEventPayload;
MPV_RELOAD: MpvReloadEventPayload;
PLAYER_PLAY: PlayerPlayEventPayload; PLAYER_PLAY: PlayerPlayEventPayload;
PLAYER_REPEATED: PlayerRepeatedEventPayload;
PLAYLIST_MOVE_DOWN: PlaylistMoveEventPayload; PLAYLIST_MOVE_DOWN: PlaylistMoveEventPayload;
PLAYLIST_MOVE_TO_BOTTOM: PlaylistMoveEventPayload; PLAYLIST_MOVE_TO_BOTTOM: PlaylistMoveEventPayload;
PLAYLIST_MOVE_TO_TOP: PlaylistMoveEventPayload; PLAYLIST_MOVE_TO_TOP: PlaylistMoveEventPayload;
@@ -43,17 +36,11 @@ export type MediaPrevEventPayload = {
prevIndex: number; prevIndex: number;
}; };
export type MpvReloadEventPayload = Record<string, never>;
export type PlayerPlayEventPayload = { export type PlayerPlayEventPayload = {
id: string; id: string;
index: number; index: number;
}; };
export type PlayerRepeatedEventPayload = {
index: number;
};
export type PlaylistMoveEventPayload = { export type PlaylistMoveEventPayload = {
playlistId: string; playlistId: string;
sourceIds: string[]; sourceIds: string[];
@@ -30,7 +30,7 @@ export const ServerRequired = () => {
const isServerLock = Boolean(window.SERVER_LOCK) || false; const isServerLock = Boolean(window.SERVER_LOCK) || false;
if (Object.keys(serverList).length > 0) { if (Object.keys(serverList).length > 1) {
return ( return (
<ScrollArea> <ScrollArea>
<Stack miw="300px"> <Stack miw="300px">
@@ -23,8 +23,8 @@ const NoNetworkRoute = () => {
return ( return (
<AnimatedPage> <AnimatedPage>
<PageHeader /> <PageHeader />
<Center style={{ height: '100%' }}> <Center style={{ height: '100%', width: '100vw' }}>
<Stack align="center" gap="xl" style={{ maxWidth: '50%', textAlign: 'center' }}> <Stack gap="xl" style={{ maxWidth: '50%', textAlign: 'center' }}>
<Icon icon="wifiOff" size="4rem" /> <Icon icon="wifiOff" size="4rem" />
<Stack gap="md"> <Stack gap="md">
<Text size="xl" weight={600}> <Text size="xl" weight={600}>
@@ -27,11 +27,7 @@
.metadata-column { .metadata-column {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex-wrap: wrap;
grid-area: metadata; grid-area: metadata;
gap: var(--theme-spacing-xl);
align-items: center;
text-align: center;
} }
.songs-column { .songs-column {
@@ -57,47 +53,6 @@
} }
} }
.external-links-group {
justify-content: center;
}
.metadata-pill-group {
align-items: center;
}
.pill-group-wrapper {
display: flex;
width: 100%;
& > div {
justify-content: center;
}
}
@container (min-width: $mantine-breakpoint-sm) {
.metadata-column {
flex-direction: row;
justify-content: flex-start;
text-align: left;
}
.external-links-group {
justify-content: flex-start;
}
.metadata-pill-group {
align-items: flex-start;
}
.pill-group-wrapper {
justify-content: flex-start;
& > div {
justify-content: flex-start;
}
}
}
@container (min-width: $mantine-breakpoint-lg) { @container (min-width: $mantine-breakpoint-lg) {
.content-layout { .content-layout {
grid-template-areas: 'songs metadata'; grid-template-areas: 'songs metadata';
@@ -21,13 +21,19 @@ import { searchLibraryItems } from '/@/renderer/features/shared/utils';
import { useContainerQuery } from '/@/renderer/hooks'; import { useContainerQuery } from '/@/renderer/hooks';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer, usePlayerSong } from '/@/renderer/store'; import { useCurrentServer, usePlayerSong } from '/@/renderer/store';
import { useExternalLinks, useSettingsStore } from '/@/renderer/store/settings.store'; import { useGeneralSettings, useSettingsStore } from '/@/renderer/store/settings.store';
import { sentenceCase, titleCase } from '/@/renderer/utils'; import {
formatDateAbsoluteUTC,
formatDurationString,
formatSizeString,
titleCase,
} from '/@/renderer/utils';
import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify'; import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify';
import { normalizeReleaseTypes } from '/@/renderer/utils/normalize-release-types'; import { normalizeReleaseTypes } from '/@/renderer/utils/normalize-release-types';
import { sortSongList } from '/@/shared/api/utils'; import { sortSongList } from '/@/shared/api/utils';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Checkbox } from '/@/shared/components/checkbox/checkbox'; import { Checkbox } from '/@/shared/components/checkbox/checkbox';
import { Flex } from '/@/shared/components/flex/flex';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon'; import { Icon } from '/@/shared/components/icon/icon';
import { Pill, PillLink } from '/@/shared/components/pill/pill'; import { Pill, PillLink } from '/@/shared/components/pill/pill';
@@ -36,7 +42,6 @@ import { Spoiler } from '/@/shared/components/spoiler/spoiler';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { TextInput } from '/@/shared/components/text-input/text-input'; import { TextInput } from '/@/shared/components/text-input/text-input';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { useDebouncedValue } from '/@/shared/hooks/use-debounced-value';
import { useHotkeys } from '/@/shared/hooks/use-hotkeys'; import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
import { import {
Album, Album,
@@ -49,83 +54,79 @@ import {
} from '/@/shared/types/domain-types'; } from '/@/shared/types/domain-types';
import { ItemListKey, ListDisplayType, Play } from '/@/shared/types/types'; import { ItemListKey, ListDisplayType, Play } from '/@/shared/types/types';
const MetadataPillGroup = ({
items,
title,
}: {
items: undefined | { id: string; value: ReactNode | string | undefined }[];
title: string;
}) => {
if (!items || items.length === 0) return null;
return (
<Stack align="center" className={styles.metadataPillGroup} gap="xs">
<Text fw={600} isNoSelect size="sm" tt="uppercase">
{title}
</Text>
<div className={styles['pill-group-wrapper']}>
<Pill.Group>
{items.map((tag, index) => (
<Pill key={`item-${tag.id}-${index}`} size="md">
{tag.value}
</Pill>
))}
</Pill.Group>
</div>
</Stack>
);
};
interface AlbumMetadataTagsProps { interface AlbumMetadataTagsProps {
album: Album | undefined; album: Album | undefined;
} }
const MOOD_TAG = 'mood';
const RELEASE_COUNTRY_TAG = 'releasecountry';
const RELEASE_STATUS_TAG = 'releasestatus';
const AlbumMetadataTags = ({ album }: AlbumMetadataTagsProps) => { const AlbumMetadataTags = ({ album }: AlbumMetadataTagsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const defaultTagItems = useMemo(() => { const metadataItems = useMemo(() => {
if (!album) return []; if (!album) return [];
const originalDifferentFromRelease =
album.originalDate && album.originalDate !== album.releaseDate;
const releasePrefix = originalDifferentFromRelease
? t('page.albumDetail.released', { postProcess: 'sentenceCase' })
: '♫';
const releaseTypes = normalizeReleaseTypes(album.releaseTypes ?? [], t).map((type) => ({ const releaseTypes = normalizeReleaseTypes(album.releaseTypes ?? [], t).map((type) => ({
id: type, id: type,
value: titleCase(type), value: titleCase(type),
})); }));
const releaseCountries =
album.tags?.[RELEASE_COUNTRY_TAG]?.map((country) => ({
id: country,
value: country,
})) || [];
const releaseStatuses =
album.tags?.[RELEASE_STATUS_TAG]?.map((status) => ({
id: status,
value: status,
})) || [];
const recordLabels =
album.recordLabels?.map((label) => ({
id: label,
value: label,
})) || [];
const items: Array<{ id: string; value: ReactNode | string | undefined }> = []; const items: Array<{ id: string; value: ReactNode | string | undefined }> = [];
if (originalDifferentFromRelease && album.originalDate) {
items.push({
id: 'originalDate',
value: `${formatDateAbsoluteUTC(album.originalDate)}`,
});
}
items.push(...releaseTypes);
items.push( items.push(
...releaseTypes,
{ {
id: 'isCompilation', id: 'releaseDate',
value: album?.isCompilation value: album.releaseDate
? t('filter.isCompilation', { postProcess: 'sentenceCase' }) ? `${releasePrefix} ${formatDateAbsoluteUTC(album.releaseDate)}`
: undefined, : undefined,
}, },
...releaseCountries, {
...releaseStatuses, id: 'releaseYear',
...recordLabels, value: album.releaseYear?.toString(),
},
{
id: 'songCount',
value: album.songCount
? t('entity.trackWithCount', {
count: album.songCount,
})
: undefined,
},
{
id: 'duration',
value: album.duration ? (
<Flex align="center" gap="xs">
<Icon icon="duration" size="md" /> {formatDurationString(album.duration)}
</Flex>
) : undefined,
},
{
id: 'size',
value: album.size ? formatSizeString(album.size) : undefined,
},
{
id: 'playCount',
value:
typeof album.playCount === 'number'
? t('entity.play', {
count: album.playCount,
})
: undefined,
},
{ {
id: 'explicitStatus', id: 'explicitStatus',
value: value:
@@ -135,32 +136,43 @@ const AlbumMetadataTags = ({ album }: AlbumMetadataTagsProps) => {
? t('common.clean', { postProcess: 'sentenceCase' }) ? t('common.clean', { postProcess: 'sentenceCase' })
: undefined, : undefined,
}, },
{
id: 'isCompilation',
value: album?.isCompilation
? t('filter.isCompilation', { postProcess: 'sentenceCase' })
: undefined,
},
{
id: 'recordLabels',
value:
album.recordLabels && album.recordLabels.length > 0
? album.recordLabels.join(', ')
: undefined,
},
{
id: 'version',
value: album.version || undefined,
},
); );
return items.filter((item) => item.value); return items.filter((item) => item.value);
}, [album, t]); }, [album, t]);
const moodTagItems = useMemo(() => { if (metadataItems.length === 0) return null;
if (!album) return [];
return album.tags?.[MOOD_TAG]?.map((tag) => ({
id: tag,
value: tag,
}));
}, [album]);
return ( return (
<> <Stack gap="xs">
<MetadataPillGroup <Text fw={600} isNoSelect size="sm" tt="uppercase">
items={defaultTagItems} {t('common.tags', { postProcess: 'sentenceCase' })}
title={t('common.tags', { postProcess: 'sentenceCase' })} </Text>
/> <Pill.Group>
{metadataItems.map((item, index) => (
<MetadataPillGroup <Pill key={`item-${item.id}-${index}`} size="md">
items={moodTagItems} {item.value}
title={t('common.mood', { postProcess: 'sentenceCase' })} </Pill>
/> ))}
</> </Pill.Group>
</Stack>
); );
}; };
@@ -197,38 +209,38 @@ const AlbumMetadataGenres = ({ genres }: AlbumMetadataGenresProps) => {
); );
}; };
// interface AlbumMetadataArtistsProps { interface AlbumMetadataArtistsProps {
// artists?: Array<{ id: string; name: string }>; artists?: Array<{ id: string; name: string }>;
// } }
// const AlbumMetadataArtists = ({ artists }: AlbumMetadataArtistsProps) => { const AlbumMetadataArtists = ({ artists }: AlbumMetadataArtistsProps) => {
// const { t } = useTranslation(); const { t } = useTranslation();
// if (!artists || artists.length === 0) return null; if (!artists || artists.length === 0) return null;
// return ( return (
// <Stack gap="xs"> <Stack gap="xs">
// <Text fw={600} isNoSelect size="sm" tt="uppercase"> <Text fw={600} isNoSelect size="sm" tt="uppercase">
// {t('entity.albumArtist', { {t('entity.albumArtist', {
// count: artists.length, count: artists.length,
// })} })}
// </Text> </Text>
// <Pill.Group> <Pill.Group>
// {artists.map((artist) => ( {artists.map((artist) => (
// <PillLink <PillLink
// key={`artist-${artist.id}`} key={`artist-${artist.id}`}
// size="md" size="md"
// to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, { to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
// albumArtistId: artist.id, albumArtistId: artist.id,
// })} })}
// > >
// {artist.name} {artist.name}
// </PillLink> </PillLink>
// ))} ))}
// </Pill.Group> </Pill.Group>
// </Stack> </Stack>
// ); );
// }; };
interface AlbumMetadataExternalLinksProps { interface AlbumMetadataExternalLinksProps {
albumArtist?: string; albumArtist?: string;
@@ -258,7 +270,7 @@ const AlbumMetadataExternalLinks = ({
postProcess: 'sentenceCase', postProcess: 'sentenceCase',
})} })}
</Text> </Text>
<Group className={styles.externalLinksGroup} gap="sm"> <Group gap="sm">
{lastFM && ( {lastFM && (
<ActionIcon <ActionIcon
component="a" component="a"
@@ -312,65 +324,44 @@ export const AlbumDetailContent = () => {
); );
const { ref, ...cq } = useContainerQuery(); const { ref, ...cq } = useContainerQuery();
const { externalLinks, lastFM, musicBrainz } = useExternalLinks(); const { externalLinks, lastFM, musicBrainz } = useGeneralSettings();
const genreCarousels = useMemo(() => { const carousels = [
const genreLimit = 2; {
const selectedGenres = detailQuery?.data?.genres?.slice(0, genreLimit); excludeIds: detailQuery?.data?.id ? [detailQuery.data.id] : undefined,
isHidden: !detailQuery?.data?.albumArtists?.[0]?.id,
if (!selectedGenres || selectedGenres.length === 0) return []; query: {
artistIds: detailQuery?.data?.albumArtists.length
return selectedGenres ? [detailQuery?.data?.albumArtists[0].id]
.map((genre) => { : undefined,
const uniqueId = `moreFromGenre-${genre.id}`;
return {
enableRefresh: true,
excludeIds: detailQuery?.data?.id ? [detailQuery.data.id] : undefined,
isHidden: !genre,
query: {
genreIds: [genre.id],
},
rowCount: 1,
sortBy: AlbumListSort.RANDOM,
sortOrder: SortOrder.ASC,
title: sentenceCase(
t('page.albumDetail.moreFromGeneric', {
item: genre.name,
}),
),
uniqueId,
};
})
.filter((carousel) => !carousel.isHidden);
}, [detailQuery.data, t]);
const carousels = useMemo(() => {
const moreFromArtistUniqueId = 'moreFromArtist';
return [
{
enableRefresh: false,
excludeIds: detailQuery?.data?.id ? [detailQuery.data.id] : undefined,
isHidden: !detailQuery?.data?.albumArtists?.[0]?.id,
query: {
artistIds: detailQuery?.data?.albumArtists.length
? [detailQuery?.data?.albumArtists[0].id]
: undefined,
},
rowCount: 1,
sortBy: AlbumListSort.YEAR,
sortOrder: SortOrder.DESC,
title: t('page.albumDetail.moreFromArtist', { postProcess: 'sentenceCase' }),
uniqueId: moreFromArtistUniqueId,
}, },
...genreCarousels, rowCount: 1,
]; sortBy: AlbumListSort.YEAR,
}, [detailQuery.data, genreCarousels, t]); sortOrder: SortOrder.DESC,
title: t('page.albumDetail.moreFromArtist', { postProcess: 'sentenceCase' }),
uniqueId: 'moreFromArtist',
},
{
excludeIds: detailQuery?.data?.id ? [detailQuery.data.id] : undefined,
isHidden: !detailQuery?.data?.genres?.[0],
query: {
genreIds: detailQuery?.data?.genres.length
? [detailQuery?.data?.genres[0].id]
: undefined,
},
rowCount: 1,
sortBy: AlbumListSort.RANDOM,
sortOrder: SortOrder.ASC,
title: `${t('page.albumDetail.moreFromGeneric', {
item: '',
postProcess: 'sentenceCase',
})} ${detailQuery?.data?.genres?.[0]?.name}`,
uniqueId: 'relatedGenres',
},
];
const comment = detailQuery?.data?.comment; const comment = detailQuery?.data?.comment;
const releaseYear = detailQuery?.data?.releaseYear;
const labels = detailQuery?.data?.recordLabels;
const mbzId = detailQuery?.data?.mbzId; const mbzId = detailQuery?.data?.mbzId;
return ( return (
@@ -378,7 +369,9 @@ export const AlbumDetailContent = () => {
<div className={styles.detailContainer}> <div className={styles.detailContainer}>
{comment && ( {comment && (
<Spoiler maxHeight={75}> <Spoiler maxHeight={75}>
<Text pb="md">{replaceURLWithHTMLLinks(comment)}</Text> <Text
dangerouslySetInnerHTML={{ __html: replaceURLWithHTMLLinks(comment) }}
/>
</Spoiler> </Spoiler>
)} )}
<div className={styles.contentLayout}> <div className={styles.contentLayout}>
@@ -388,28 +381,22 @@ export const AlbumDetailContent = () => {
)} )}
</div> </div>
<div className={styles.metadataColumn}> <div className={styles.metadataColumn}>
{/* <AlbumMetadataArtists artists={detailQuery?.data?.albumArtists} /> */} <Stack gap="2xl">
<AlbumMetadataGenres genres={detailQuery?.data?.genres} /> <AlbumMetadataArtists artists={detailQuery?.data?.albumArtists} />
<AlbumMetadataTags album={detailQuery?.data} /> <AlbumMetadataGenres genres={detailQuery?.data?.genres} />
<AlbumMetadataExternalLinks <AlbumMetadataTags album={detailQuery?.data} />
albumArtist={detailQuery?.data?.albumArtistName} <AlbumMetadataExternalLinks
albumName={detailQuery?.data?.name} albumArtist={detailQuery?.data?.albumArtist}
externalLinks={externalLinks} albumName={detailQuery?.data?.name}
lastFM={lastFM} externalLinks={externalLinks}
mbzId={mbzId || undefined} lastFM={lastFM}
musicBrainz={musicBrainz} mbzId={mbzId || undefined}
/> musicBrainz={musicBrainz}
/>
</Stack>
</div> </div>
</div> </div>
{labels && (
<Stack gap="xs">
{labels.map((label) => (
<Text isMuted key={`label-${label}`} size="sm">
{releaseYear ? ` ${releaseYear}` : ''} {label}
</Text>
))}
</Stack>
)}
<Stack gap="lg" mt="3rem"> <Stack gap="lg" mt="3rem">
{cq.height || cq.width ? ( {cq.height || cq.width ? (
<Suspense fallback={<Spinner container />}> <Suspense fallback={<Spinner container />}>
@@ -417,7 +404,6 @@ export const AlbumDetailContent = () => {
.filter((c) => !c.isHidden) .filter((c) => !c.isHidden)
.map((carousel) => ( .map((carousel) => (
<AlbumInfiniteCarousel <AlbumInfiniteCarousel
enableRefresh={carousel.enableRefresh}
excludeIds={carousel.excludeIds} excludeIds={carousel.excludeIds}
key={`carousel-${carousel.uniqueId}`} key={`carousel-${carousel.uniqueId}`}
query={carousel.query} query={carousel.query}
@@ -442,7 +428,6 @@ interface AlbumDetailSongsTableProps {
const AlbumDetailSongsTable = ({ songs }: AlbumDetailSongsTableProps) => { const AlbumDetailSongsTable = ({ songs }: AlbumDetailSongsTableProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [debouncedSearchTerm] = useDebouncedValue(searchTerm, 300);
const tableConfig = useSettingsStore((state) => state.lists[ItemListKey.ALBUM_DETAIL]?.table); const tableConfig = useSettingsStore((state) => state.lists[ItemListKey.ALBUM_DETAIL]?.table);
const currentSong = usePlayerSong(); const currentSong = usePlayerSong();
@@ -456,11 +441,11 @@ const AlbumDetailSongsTable = ({ songs }: AlbumDetailSongsTableProps) => {
const filteredSongs = useMemo(() => { const filteredSongs = useMemo(() => {
return sortSongList( return sortSongList(
searchLibraryItems(songs, debouncedSearchTerm, LibraryItem.SONG), searchLibraryItems(songs, searchTerm, LibraryItem.SONG),
sortBy, sortBy,
sortOrder, sortOrder,
); );
}, [songs, debouncedSearchTerm, sortBy, sortOrder]); }, [songs, searchTerm, sortBy, sortOrder]);
const { handleColumnReordered } = useItemListColumnReorder({ const { handleColumnReordered } = useItemListColumnReorder({
itemListKey: ItemListKey.ALBUM_DETAIL, itemListKey: ItemListKey.ALBUM_DETAIL,
@@ -508,7 +493,7 @@ const AlbumDetailSongsTable = ({ songs }: AlbumDetailSongsTableProps) => {
const groups = useMemo(() => { const groups = useMemo(() => {
// Remove groups when filtering // Remove groups when filtering
if (debouncedSearchTerm.trim()) { if (searchTerm.trim()) {
return undefined; return undefined;
} }
@@ -594,7 +579,7 @@ const AlbumDetailSongsTable = ({ songs }: AlbumDetailSongsTableProps) => {
}, },
rowHeight: 40, rowHeight: 40,
})); }));
}, [debouncedSearchTerm, sortBy, discGroups, t]); }, [searchTerm, sortBy, discGroups, t]);
const player = usePlayer(); const player = usePlayer();
@@ -692,7 +677,6 @@ const AlbumDetailSongsTable = ({ songs }: AlbumDetailSongsTableProps) => {
enableHorizontalBorders={tableConfig.enableHorizontalBorders} enableHorizontalBorders={tableConfig.enableHorizontalBorders}
enableRowHoverHighlight={tableConfig.enableRowHoverHighlight} enableRowHoverHighlight={tableConfig.enableRowHoverHighlight}
enableSelection enableSelection
enableSelectionDialog={false}
enableStickyGroupRows enableStickyGroupRows
enableStickyHeader enableStickyHeader
enableVerticalBorders={tableConfig.enableVerticalBorders} enableVerticalBorders={tableConfig.enableVerticalBorders}
@@ -1,12 +1,10 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { forwardRef, Fragment, useMemo } from 'react'; import { forwardRef } from 'react';
import { useTranslation } from 'react-i18next'; import { generatePath, Link, useParams } from 'react-router';
import { Link, useParams } from 'react-router';
import styles from './album-detail-header.module.css'; import styles from './album-detail-header.module.css';
import { albumQueries } from '/@/renderer/features/albums/api/album-api'; import { albumQueries } from '/@/renderer/features/albums/api/album-api';
import { JoinedArtists } from '/@/renderer/features/albums/components/joined-artists';
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller'; import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
import { usePlayer } from '/@/renderer/features/player/context/player-context'; import { usePlayer } from '/@/renderer/features/player/context/player-context';
import { import {
@@ -14,12 +12,9 @@ import {
LibraryHeaderMenu, LibraryHeaderMenu,
} from '/@/renderer/features/shared/components/library-header'; } from '/@/renderer/features/shared/components/library-header';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer, useShowRatings } from '/@/renderer/store'; import { useCurrentServer } from '/@/renderer/store';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { formatDateAbsoluteUTC, formatDurationString } from '/@/renderer/utils';
import { normalizeReleaseTypes } from '/@/renderer/utils/normalize-release-types';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { Separator } from '/@/shared/components/separator/separator';
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 { LibraryItem, ServerType } from '/@/shared/types/domain-types'; import { LibraryItem, ServerType } from '/@/shared/types/domain-types';
@@ -27,17 +22,14 @@ import { Play } from '/@/shared/types/types';
export const AlbumDetailHeader = forwardRef<HTMLDivElement>((_props, ref) => { export const AlbumDetailHeader = forwardRef<HTMLDivElement>((_props, ref) => {
const { albumId } = useParams() as { albumId: string }; const { albumId } = useParams() as { albumId: string };
const { t } = useTranslation();
const server = useCurrentServer(); const server = useCurrentServer();
const showRatings = useShowRatings();
const detailQuery = useQuery( const detailQuery = useQuery(
albumQueries.detail({ query: { id: albumId }, serverId: server?.id }), albumQueries.detail({ query: { id: albumId }, serverId: server?.id }),
); );
const showRating = const showRating =
showRatings && detailQuery?.data?._serverType === ServerType.NAVIDROME ||
(detailQuery?.data?._serverType === ServerType.NAVIDROME || detailQuery?.data?._serverType === ServerType.SUBSONIC;
detailQuery?.data?._serverType === ServerType.SUBSONIC);
const { addToQueueByFetch, setFavorite, setRating } = usePlayer(); const { addToQueueByFetch, setFavorite, setRating } = usePlayer();
const playButtonBehavior = usePlayButtonBehavior(); const playButtonBehavior = usePlayButtonBehavior();
@@ -87,160 +79,44 @@ export const AlbumDetailHeader = forwardRef<HTMLDivElement>((_props, ref) => {
}); });
}; };
const firstAlbumArtist = detailQuery?.data?.albumArtists?.[0];
const releaseYear = detailQuery?.data?.releaseYear; const releaseYear = detailQuery?.data?.releaseYear;
const releaseDate = detailQuery?.data?.releaseDate;
const metadataItems = useMemo(() => {
const items: Array<{ id: string; value: React.ReactNode | string | undefined }> = [];
const album = detailQuery?.data;
if (!album) return [];
const originalDifferentFromRelease =
album?.originalDate && album?.originalDate !== album?.releaseDate;
const originalYearDifferentFromRelease = album?.originalYear !== album?.releaseYear;
const playCount = album?.playCount;
const releasePrefix = originalDifferentFromRelease
? t('page.albumDetail.released', { postProcess: 'sentenceCase' })
: '♫';
const releaseYearPrefix = originalYearDifferentFromRelease
? t('page.albumDetail.released', { postProcess: 'sentenceCase' })
: '♫';
if (album.originalDate) {
if (originalDifferentFromRelease) {
items.push({
id: 'originalDate',
value: `${formatDateAbsoluteUTC(album.originalDate)}`,
});
}
if (releaseDate) {
items.push({
id: 'releaseDate',
value: `${releasePrefix} ${formatDateAbsoluteUTC(releaseDate)}`,
});
}
} else if (album.originalYear) {
if (originalYearDifferentFromRelease) {
items.push({
id: 'originalYear',
value: `${album.originalYear}`,
});
}
if (releaseDate) {
items.push({
id: 'releaseDate',
value: `${releaseYearPrefix} ${formatDateAbsoluteUTC(releaseDate)}`,
});
} else if (releaseYear) {
items.push({
id: 'releaseYear',
value: `${releaseYearPrefix} ${releaseYear}`,
});
}
}
items.push(
...[
{
id: 'songCount',
value: t('entity.trackWithCount', { count: detailQuery?.data?.songCount || 0 }),
},
{
id: 'duration',
value: formatDurationString(detailQuery?.data?.duration || 0),
},
{
id: 'explicitStatus',
value: detailQuery?.data?.explicitStatus,
},
{
id: 'playCount',
value: playCount ? t('entity.play', { count: playCount }) : undefined,
},
],
);
return items.filter((item) => !!item.value);
}, [detailQuery?.data, releaseDate, releaseYear, t]);
const headerItem = useMemo(() => {
const album = detailQuery?.data;
if (!album) return null;
const releaseTypes = album.releaseType
? normalizeReleaseTypes([album.releaseType], t)
: null;
const releaseTypeText = releaseTypes?.length ? releaseTypes[0] : null;
if (releaseTypeText) {
return (
<Group gap="sm">
<Text
component={Link}
fw={600}
isLink
size="md"
to={AppRoute.LIBRARY_ALBUMS}
tt="uppercase"
>
{releaseTypeText}
</Text>
{album.version && (
<>
<Text fw={600} isMuted>
<Separator />
</Text>
<Text>{album.version}</Text>
</>
)}
</Group>
);
}
return null;
}, [detailQuery?.data, t]);
return ( return (
<Stack ref={ref}> <Stack ref={ref}>
<LibraryHeader <LibraryHeader
item={{ imageUrl={detailQuery?.data?.imageUrl}
children: headerItem, item={{ route: AppRoute.LIBRARY_ALBUMS, type: LibraryItem.ALBUM }}
imageId: detailQuery?.data?.imageId,
imageUrl: detailQuery?.data?.imageUrl,
route: AppRoute.LIBRARY_ALBUMS,
type: LibraryItem.ALBUM,
}}
title={detailQuery?.data?.name || ''} title={detailQuery?.data?.name || ''}
> >
<Stack gap="md" w="100%"> <Stack gap="md" w="100%">
<Group className={styles.metadataGroup} gap="xs"> {(firstAlbumArtist || releaseYear) && (
{metadataItems.map((item, index) => ( <Group className={styles.metadataGroup}>
<Fragment key={item.id}> {firstAlbumArtist && (
{index > 0 && ( <Text
<Text fw={400} isMuted isNoSelect> component={Link}
fw={600}
</Text> isLink
)} isNoSelect
<Text fw={400}>{item.value}</Text> to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
</Fragment> albumArtistId: firstAlbumArtist.id,
))} })}
</Group> >
<Group className={styles.metadataGroup}> {firstAlbumArtist.name}
<JoinedArtists </Text>
artistName={detailQuery?.data?.albumArtistName || ''} )}
artists={detailQuery?.data?.albumArtists || []} {firstAlbumArtist && releaseYear && (
/> <Text fw={600} isNoSelect>
</Group>
</Text>
)}
{releaseYear && (
<Text fw={600} isMuted isNoSelect>
{releaseYear}
</Text>
)}
</Group>
)}
<LibraryHeaderMenu <LibraryHeaderMenu
favorite={detailQuery?.data?.userFavorite} favorite={detailQuery?.data?.userFavorite}
onFavorite={handleFavorite} onFavorite={handleFavorite}
@@ -19,7 +19,6 @@ import {
import { ItemListKey } from '/@/shared/types/types'; import { ItemListKey } from '/@/shared/types/types';
interface AlbumCarouselProps { interface AlbumCarouselProps {
enableRefresh?: boolean;
excludeIds?: string[]; excludeIds?: string[];
query?: Partial<Omit<AlbumListQuery, 'startIndex'>>; query?: Partial<Omit<AlbumListQuery, 'startIndex'>>;
rowCount?: number; rowCount?: number;
@@ -29,15 +28,7 @@ interface AlbumCarouselProps {
} }
const BaseAlbumInfiniteCarousel = (props: AlbumCarouselProps) => { const BaseAlbumInfiniteCarousel = (props: AlbumCarouselProps) => {
const { const { excludeIds, query: additionalQuery, rowCount = 1, sortBy, sortOrder, title } = props;
enableRefresh,
excludeIds,
query: additionalQuery,
rowCount = 1,
sortBy,
sortOrder,
title,
} = props;
const rows = useGridRows(LibraryItem.ALBUM, ItemListKey.ALBUM); const rows = useGridRows(LibraryItem.ALBUM, ItemListKey.ALBUM);
const { const {
data: albums, data: albums,
@@ -90,7 +81,6 @@ const BaseAlbumInfiniteCarousel = (props: AlbumCarouselProps) => {
return ( return (
<GridCarousel <GridCarousel
cards={cards} cards={cards}
enableRefresh={enableRefresh}
hasNextPage={hasNextPage} hasNextPage={hasNextPage}
loadNextPage={fetchNextPage} loadNextPage={fetchNextPage}
onNextPage={handleNextPage} onNextPage={handleNextPage}
@@ -115,7 +115,6 @@ export const AlbumListView = ({
itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined} itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined}
query={mergedQuery} query={mergedQuery}
serverId={server.id} serverId={server.id}
size={grid.size}
/> />
); );
} }
@@ -127,7 +126,6 @@ export const AlbumListView = ({
itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined} itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined}
query={mergedQuery} query={mergedQuery}
serverId={server.id} serverId={server.id}
size={grid.size}
/> />
); );
} }
@@ -2,7 +2,6 @@ import { useSuspenseQuery } from '@tanstack/react-query';
import { Suspense, useMemo } from 'react'; import { Suspense, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useIsFetchingItemListCount } from '/@/renderer/components/item-list/helpers/use-is-fetching-item-list';
import { PageHeader } from '/@/renderer/components/page-header/page-header'; import { PageHeader } from '/@/renderer/components/page-header/page-header';
import { useListContext } from '/@/renderer/context/list-context'; import { useListContext } from '/@/renderer/context/list-context';
import { AlbumListHeaderFilters } from '/@/renderer/features/albums/components/album-list-header-filters'; import { AlbumListHeaderFilters } from '/@/renderer/features/albums/components/album-list-header-filters';
@@ -23,13 +22,17 @@ interface AlbumListHeaderProps {
} }
export const AlbumListHeader = ({ title }: AlbumListHeaderProps) => { export const AlbumListHeader = ({ title }: AlbumListHeaderProps) => {
const { itemCount } = useListContext();
return ( return (
<Stack gap={0}> <Stack gap={0}>
<PageHeader> <PageHeader>
<LibraryHeaderBar ignoreMaxWidth> <LibraryHeaderBar ignoreMaxWidth>
<PlayButton /> <PlayButton />
<PageTitle title={title} /> <PageTitle title={title} />
<AlbumListHeaderBadge /> <LibraryHeaderBar.Badge isLoading={!itemCount}>
{itemCount}
</LibraryHeaderBar.Badge>
</LibraryHeaderBar> </LibraryHeaderBar>
<Group> <Group>
<ListSearchInput /> <ListSearchInput />
@@ -42,16 +45,6 @@ export const AlbumListHeader = ({ title }: AlbumListHeaderProps) => {
); );
}; };
const AlbumListHeaderBadge = () => {
const { itemCount } = useListContext();
const isFetching = useIsFetchingItemListCount({
itemType: LibraryItem.ALBUM,
});
return <LibraryHeaderBar.Badge isLoading={isFetching}>{itemCount}</LibraryHeaderBar.Badge>;
};
const PageTitle = ({ title }: { title?: string }) => { const PageTitle = ({ title }: { title?: string }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { pageKey } = useListContext(); const { pageKey } = useListContext();

Some files were not shown because too many files have changed in this diff Show More