Compare commits

...

60 Commits

Author SHA1 Message Date
jeffvli 5ab0eba23e update to 0.19.0 2025-07-30 19:53:07 -07:00
jeffvli 08fc307516 attempt to catch network errors to prevent credential invalidation 2025-07-30 19:51:46 -07:00
Hosted Weblate e5adc0caa9 Translated using Weblate (Norwegian Bokmål)
Currently translated at 53.4% (365 of 683 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 53.4% (365 of 683 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: klodrik <klodrik@zoominn.no>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/nb_NO/
Translation: feishin/Translation
2025-07-31 04:11:35 +02:00
Hosted Weblate 7f0bdf20fc Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (685 of 685 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (683 of 683 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: 無情天 <kofzhanganguo@126.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hans/
Translation: feishin/Translation
2025-07-31 04:11:34 +02:00
Hosted Weblate deb89ef87d Translated using Weblate (French)
Currently translated at 100.0% (685 of 685 strings)

Translated using Weblate (French)

Currently translated at 100.0% (683 of 683 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: KosmoMoustache <kosmomoustache@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fr/
Translation: feishin/Translation
2025-07-31 04:11:34 +02:00
Hosted Weblate 9751e22db4 Translated using Weblate (Spanish)
Currently translated at 100.0% (685 of 685 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (683 of 683 strings)

Co-authored-by: Fordas <fordas15@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/
Translation: feishin/Translation
2025-07-31 04:11:33 +02:00
Hosted Weblate 6128470a47 Translated using Weblate (Czech)
Currently translated at 100.0% (685 of 685 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (683 of 683 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Fjuro <git@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/
Translation: feishin/Translation
2025-07-31 04:11:32 +02:00
Jeff ea79885ef5 Merge pull request #1030 from jeffvli/dependabot/npm_and_yarn/npm_and_yarn-373e2693b3
Bump @eslint/plugin-kit from 0.3.3 to 0.3.4 in the npm_and_yarn group across 1 directory
2025-07-30 21:11:24 -05:00
dependabot[bot] 84fd6e482d Bump @eslint/plugin-kit in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [@eslint/plugin-kit](https://github.com/eslint/rewrite/tree/HEAD/packages/plugin-kit).


Updates `@eslint/plugin-kit` from 0.3.3 to 0.3.4
- [Release notes](https://github.com/eslint/rewrite/releases)
- [Changelog](https://github.com/eslint/rewrite/blob/main/packages/plugin-kit/CHANGELOG.md)
- [Commits](https://github.com/eslint/rewrite/commits/plugin-kit-v0.3.4/packages/plugin-kit)

---
updated-dependencies:
- dependency-name: "@eslint/plugin-kit"
  dependency-version: 0.3.4
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-30 02:24:51 +00:00
Jeff 904f05ff61 Merge pull request #1021 from jeffvli/dependabot/npm_and_yarn/npm_and_yarn-e04d5d616f
Bump form-data from 4.0.2 to 4.0.4 in the npm_and_yarn group across 1 directory
2025-07-29 21:23:23 -05:00
Jeff e78ec7688a Merge pull request #1024 from Der-Penz/development
Support tab navigation on ActionIcons in command palette
2025-07-29 21:22:47 -05:00
jeffvli f43de7f23c remove version from linux artifactName (#1020) 2025-07-29 19:18:27 -07:00
Jeff 07532ca55a Merge pull request #1018 from Lyall-A/fix-discord-disappearing
fix discord status clearing when song loops
2025-07-29 21:15:37 -05:00
Jeff 040a805f5f Merge pull request #1023 from chenqimiao/development
Add qm-music to an OpenSubsonic compatible server list
2025-07-29 21:04:39 -05:00
DerPenz 33af5e625b support tab navigation on ActionIcons in command palette 2025-07-26 14:01:05 +02:00
Qimiao Chen bdedcb883d introduce qm-music in readme 2025-07-25 18:22:34 +08:00
dependabot[bot] e842a75722 Bump form-data in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [form-data](https://github.com/form-data/form-data).


Updates `form-data` from 4.0.2 to 4.0.4
- [Release notes](https://github.com/form-data/form-data/releases)
- [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md)
- [Commits](https://github.com/form-data/form-data/compare/v4.0.2...v4.0.4)

---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 4.0.4
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-25 03:38:33 +00:00
dependabot[bot] d28054cc7f Bump @eslint/plugin-kit in the npm_and_yarn group across 1 directory (#1014)
Bumps the npm_and_yarn group with 1 update in the / directory: [@eslint/plugin-kit](https://github.com/eslint/rewrite/tree/HEAD/packages/plugin-kit).


Updates `@eslint/plugin-kit` from 0.3.1 to 0.3.3
- [Release notes](https://github.com/eslint/rewrite/releases)
- [Changelog](https://github.com/eslint/rewrite/blob/main/packages/plugin-kit/CHANGELOG.md)
- [Commits](https://github.com/eslint/rewrite/commits/plugin-kit-v0.3.3/packages/plugin-kit)

---
updated-dependencies:
- dependency-name: "@eslint/plugin-kit"
  dependency-version: 0.3.3
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-25 03:37:07 +00:00
Lyall 41f8c34f6e fix discord status clearing when song loops 2025-07-22 10:23:02 +01:00
Kendall Garner 4b4df28641 add bit depth, sample rate 2025-07-13 07:12:13 -07:00
jeffvli 8b141d652c disable single attribute per line 2025-07-12 11:17:54 -07:00
Kendall Garner 92ed8e20c9 fix custom path 2025-07-12 07:21:34 -07:00
Kendall Garner 746ab8c2d9 large notification z-index 2025-07-11 19:21:52 -07:00
Kendall Garner 69341f4492 More typechecks on scrobble, use timeout on notification (#1004) 2025-07-10 13:53:40 +00:00
Kendall Garner 56130d8503 fix subsonic login error: use status instead 2025-07-08 16:42:30 -07:00
Kendall Garner 4407c8d424 convert query="" to query= for subsonic 2025-07-08 14:42:49 -07:00
Kendall Garner 58bb8e716e undo that change, remove magic numbers 2025-07-08 11:26:07 -07:00
Kendall Garner 0becfd4b59 subsonic limit to 500 2025-07-08 08:30:31 -07:00
jeffvli c94029012f update to v0.18.0 2025-07-08 00:48:01 -07:00
jeffvli 2d9176cd21 fix click propagation on right controls 2025-07-08 00:46:50 -07:00
jeffvli e28dad3f84 add code to language select label 2025-07-08 00:11:37 -07:00
jeffvli 60d3eec8f7 add sl to i18n config 2025-07-08 00:11:20 -07:00
Hosted Weblate 62f9d064d9 Translated using Weblate (Slovenian)
Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 74.8% (509 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 9.1% (62 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 9.1% (62 of 680 strings)

Translated using Weblate (Slovenian)

Currently translated at 9.1% (62 of 680 strings)

Added translation using Weblate (Slovenian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Martin Stojanoski <martin.stojanoski2000@gmail.com>
Co-authored-by: mytja <mamnju21@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/sl/
Translation: feishin/Translation
2025-07-08 08:39:51 +02:00
Hosted Weblate 196b9be65b Translated using Weblate (French)
Currently translated at 100.0% (680 of 680 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: KosmoMoustache <kosmomoustache@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fr/
Translation: feishin/Translation
2025-07-08 08:39:50 +02:00
Hosted Weblate 587ce68018 Translated using Weblate (Italian)
Currently translated at 100.0% (680 of 680 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (680 of 680 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (680 of 680 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (680 of 680 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (680 of 680 strings)

Co-authored-by: Daivy <reale805@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/it/
Translation: feishin/Translation
2025-07-08 08:39:49 +02:00
Hosted Weblate 1ec6176b77 Translated using Weblate (Portuguese (Brazil))
Currently translated at 61.6% (419 of 680 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Renan <renan1211@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/pt_BR/
Translation: feishin/Translation
2025-07-08 08:39:49 +02:00
jeffvli a5f28e49eb fix click propagation on PlayButton 2025-07-07 23:20:10 -07:00
Gemini Wen 0b7d4bfb6a macOS: change window close behavior, like other macOS App (#999) 2025-07-07 23:00:21 -07:00
jeffvli 2492456b93 fix search on filtered list pages 2025-07-07 21:28:34 -07:00
jeffvli 1c22c9506e remove stale lock comments 2025-07-07 21:14:18 -07:00
Kendall Garner e00aeb2b67 enable notify, simplify use-scrobble with types, remove unused check 2025-07-07 19:25:25 -07:00
Kendall Garner b219c900ca fix readme logo rendering 2025-07-06 21:37:00 -07:00
Kendall Garner 5eacb4e3cb ...lodash random uses inclusive on both ends 2025-07-06 21:26:30 -07:00
Kendall Garner a86d44a29e fix(queue): random start index when play shuffled center control is enabled, play mode is now, no start specified 2025-07-06 21:24:45 -07:00
Jeff b7a0b7f997 handle undefined options in GenericCell (#998) 2025-07-06 03:33:11 -07:00
Lyall cd2d531c54 Automatically reconnect to Discord RPC at interval (#996)
* initialize before setActivity
2025-07-06 00:34:26 -07:00
ENDzZ 19c8980784 Translation Display Normalization (#982) 2025-07-05 16:41:42 -07:00
Lyall a2e5f86eac fix navidrome filter labels (#995) 2025-07-05 16:31:35 -07:00
dependabot[bot] d8c93cadce Bump brace-expansion in the npm_and_yarn group across 1 directory (#955)
Bumps the npm_and_yarn group with 1 update in the / directory: [brace-expansion](https://github.com/juliangruber/brace-expansion).


Updates `brace-expansion` from 1.1.11 to 1.1.12
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/1.1.11...v1.1.12)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.12
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-05 00:47:48 -07:00
Kendall Garner 35f87c8552 Fix ContextMenu star menu clicking (#987)
Resolves #985.
Currently, attempting to click on one of the `set rating` buttons is a no-op.
This is because it is considered "outside" the `ContextMenu`, which immediately closes it.
Pass in the same merged ref into the body of the `DropDown` component so that it is also treated as "inside".
2025-07-05 00:33:37 -07:00
Kendall Garner 4f7b0983ec port over ND stalebot (#991) 2025-07-02 21:55:02 -07:00
Kendall Garner 055d9ac5c1 use initial index for shuffling when availabe 2025-07-02 20:48:45 -07:00
Kendall Garner 039d008223 use fr plural setting translation from navidrome 2025-07-02 20:10:49 -07:00
Kendall Garner 2b8db9cfc1 fix table albumCount translation 2025-07-02 19:52:13 -07:00
Kendall Garner caa9448200 don't set sink on closed context 2025-07-02 19:19:51 -07:00
Kendall Garner 176a95a946 Compilation support for Jellyfin artist albums, misc other album filter fixes
- Jellyfin will use `ContributingArtistsId` (compilation), `AlbumArtistIds` (compilation is false), or `ArtistIds` (unspecified; all)
- Jellyfin can filter by compilation _only_ on the artist discography page
- Navidrome album filter fix for `defaultValue` display and prevent showing `tagQuery` 0 when querying
- Subsonic can filter by one or more artists in the album page. Sort is also applied on these items
- Bump genre/tag cache/stale time to 2/1 minutes
- Fix various cases where the album filter would display as active when it wasn't
2025-07-02 07:44:57 -07:00
Kendall Garner 6f5dd4881a join path with library path 2025-07-01 21:51:09 -07:00
Kendall Garner ce6aaa709f bugfix: handle table update when column is missing 2025-07-01 19:03:54 -07:00
Kendall Garner 217a4d65fd Merge branch 'development' of github.com:jeffvli/feishin into development 2025-07-01 17:34:32 -07:00
Kendall Garner b88671161a actually actually fix album list count for subsonic artists 2025-06-30 07:19:34 -07:00
196 changed files with 2196 additions and 2199 deletions
+47
View File
@@ -0,0 +1,47 @@
name: 'Close stale issues and PRs'
on:
workflow_dispatch:
schedule:
- cron: '30 1 * * *'
permissions:
contents: read
jobs:
stale:
permissions:
issues: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v5
with:
process-only: 'issues, prs'
issue-inactive-days: 120
pr-inactive-days: 120
log-output: true
add-issue-labels: 'frozen-due-to-age'
add-pr-labels: 'frozen-due-to-age'
- uses: actions/stale@v9
with:
operations-per-run: 999
days-before-issue-stale: 180
days-before-pr-stale: 180
days-before-issue-close: 30
days-before-pr-close: 30
stale-issue-message: >
This issue has been automatically marked as stale because it has not had recent activity. The resources of the Feishin team are limited, and so we are asking for your help.
If this is a **bug** and you can still reproduce this error on the <code>development</code> branch, please reply with all of the information you have about it in order to keep the issue open.
This issue will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.
stale-pr-message: >
This PR has been automatically marked as stale because it has not had recent activity. The resources of the Feishin team are limited, and so we are asking for your help.
This PR will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.
stale-issue-label: 'stale'
exempt-issue-labels: 'enhancement,keep,security'
stale-pr-label: 'stale'
exempt-pr-labels: 'keep,security'
+1 -1
View File
@@ -8,7 +8,7 @@ arrowParens: always
proseWrap: never proseWrap: never
htmlWhitespaceSensitivity: strict htmlWhitespaceSensitivity: strict
endOfLine: lf endOfLine: lf
singleAttributePerLine: true singleAttributePerLine: false
bracketSpacing: true bracketSpacing: true
plugins: plugins:
- prettier-plugin-packagejson - prettier-plugin-packagejson
+2 -1
View File
@@ -1,4 +1,4 @@
<img src="assets/icons/icon.png" alt="logo" title="feishin" align="right" height="60px" /> <img src="assets/icons/icon.png" alt="logo" title="feishin" align="right" height="60px" width="60px" />
# Feishin # Feishin
@@ -129,6 +129,7 @@ Feishin supports any music server that implements a [Navidrome](https://www.navi
- [LMS](https://github.com/epoupon/lms) - [LMS](https://github.com/epoupon/lms)
- [Nextcloud Music](https://apps.nextcloud.com/apps/music) - [Nextcloud Music](https://apps.nextcloud.com/apps/music)
- [Supysonic](https://github.com/spl0k/supysonic) - [Supysonic](https://github.com/spl0k/supysonic)
- [Qm-Music](https://github.com/chenqimiao/qm-music)
- More (?) - More (?)
### I have the issue "The SUID sandbox helper binary was found, but is not configured correctly" on Linux ### I have the issue "The SUID sandbox helper binary was found, but is not configured correctly" on Linux
+1 -27
View File
@@ -35,39 +35,13 @@ mac:
notarize: false notarize: false
dmg: dmg:
contents: [{ x: 130, y: 220 }, { x: 410, y: 220, type: link, path: /Applications }] contents: [{ x: 130, y: 220 }, { x: 410, y: 220, type: link, path: /Applications }]
deb:
depends:
- libgssapi_krb5.so.2
- libavahi-common.so.3
- libavahi-client.so.3
- libkrb5.so.3
- libkrb5support.so.0
- libkeyutils.so.1
- libcups.so.2
rpm:
depends:
- libgssapi_krb5.so.2
- libavahi-common.so.3
- libavahi-client.so.3
- libkrb5.so.3
- libkrb5support.so.0
- libkeyutils.so.1
- libcups.so.2
freebsd:
depends:
- libgssapi_krb5.so.2
- libavahi-common.so.3
- libavahi-client.so.3
- libkrb5.so.3
- libkrb5support.so.0
- libkeyutils.so.1
- libcups.so.2
linux: linux:
target: target:
- AppImage - AppImage
- tar.xz - tar.xz
category: AudioVideo;Audio;Player category: AudioVideo;Audio;Player
icon: assets/icons/icon.png icon: assets/icons/icon.png
artifactName: ${productName}-${os}-${arch}.${ext}
npmRebuild: false npmRebuild: false
publish: publish:
provider: github provider: github
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "feishin", "name": "feishin",
"version": "0.17.0", "version": "0.19.0",
"description": "A modern self-hosted music player.", "description": "A modern self-hosted music player.",
"keywords": [ "keywords": [
"subsonic", "subsonic",
+69 -56
View File
@@ -766,6 +766,10 @@ packages:
resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==} resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/core@0.15.1':
resolution: {integrity: sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/eslintrc@3.3.1': '@eslint/eslintrc@3.3.1':
resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -778,8 +782,8 @@ packages:
resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/plugin-kit@0.3.1': '@eslint/plugin-kit@0.3.4':
resolution: {integrity: sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==} resolution: {integrity: sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@floating-ui/core@1.7.0': '@floating-ui/core@1.7.0':
@@ -834,26 +838,24 @@ packages:
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'} engines: {node: '>=12'}
'@jridgewell/gen-mapping@0.3.8': '@jridgewell/gen-mapping@0.3.12':
resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==}
engines: {node: '>=6.0.0'}
'@jridgewell/resolve-uri@3.1.2': '@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
'@jridgewell/set-array@1.2.1': '@jridgewell/source-map@0.3.10':
resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} resolution: {integrity: sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==}
engines: {node: '>=6.0.0'}
'@jridgewell/source-map@0.3.6':
resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==}
'@jridgewell/sourcemap-codec@1.5.0': '@jridgewell/sourcemap-codec@1.5.0':
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
'@jridgewell/trace-mapping@0.3.25': '@jridgewell/sourcemap-codec@1.5.4':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==}
'@jridgewell/trace-mapping@0.3.29':
resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==}
'@keyv/serialize@1.0.3': '@keyv/serialize@1.0.3':
resolution: {integrity: sha512-qnEovoOp5Np2JDGonIDL6Ayihw0RhnRh6vxPuHo4RDn1UOzwEo4AeIfpL6UGIrsceWrCMiVPgwRjbHu4vYFc3g==} resolution: {integrity: sha512-qnEovoOp5Np2JDGonIDL6Ayihw0RhnRh6vxPuHo4RDn1UOzwEo4AeIfpL6UGIrsceWrCMiVPgwRjbHu4vYFc3g==}
@@ -1383,6 +1385,11 @@ packages:
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
hasBin: true hasBin: true
acorn@8.15.0:
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
engines: {node: '>=0.4.0'}
hasBin: true
agent-base@6.0.2: agent-base@6.0.2:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'} engines: {node: '>= 6.0.0'}
@@ -1566,11 +1573,11 @@ packages:
resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
brace-expansion@1.1.11: brace-expansion@1.1.12:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
brace-expansion@2.0.1: brace-expansion@2.0.2:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
braces@3.0.3: braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
@@ -2365,8 +2372,8 @@ packages:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'} engines: {node: '>=14'}
form-data@4.0.2: form-data@4.0.4:
resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
format-duration@2.0.0: format-duration@2.0.0:
@@ -4123,8 +4130,8 @@ packages:
resolution: {integrity: sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==} resolution: {integrity: sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
socks@2.8.4: socks@2.8.6:
resolution: {integrity: sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==} resolution: {integrity: sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==}
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
sort-keys@5.1.0: sort-keys@5.1.0:
@@ -4781,8 +4788,8 @@ snapshots:
'@ampproject/remapping@2.3.0': '@ampproject/remapping@2.3.0':
dependencies: dependencies:
'@jridgewell/gen-mapping': 0.3.8 '@jridgewell/gen-mapping': 0.3.12
'@jridgewell/trace-mapping': 0.3.25 '@jridgewell/trace-mapping': 0.3.29
'@atlaskit/pragmatic-drag-and-drop-auto-scroll@2.1.1': '@atlaskit/pragmatic-drag-and-drop-auto-scroll@2.1.1':
dependencies: dependencies:
@@ -4838,8 +4845,8 @@ snapshots:
dependencies: dependencies:
'@babel/parser': 7.27.2 '@babel/parser': 7.27.2
'@babel/types': 7.27.1 '@babel/types': 7.27.1
'@jridgewell/gen-mapping': 0.3.8 '@jridgewell/gen-mapping': 0.3.12
'@jridgewell/trace-mapping': 0.3.25 '@jridgewell/trace-mapping': 0.3.29
jsesc: 3.1.0 jsesc: 3.1.0
'@babel/helper-compilation-targets@7.27.2': '@babel/helper-compilation-targets@7.27.2':
@@ -5201,6 +5208,10 @@ snapshots:
dependencies: dependencies:
'@types/json-schema': 7.0.15 '@types/json-schema': 7.0.15
'@eslint/core@0.15.1':
dependencies:
'@types/json-schema': 7.0.15
'@eslint/eslintrc@3.3.1': '@eslint/eslintrc@3.3.1':
dependencies: dependencies:
ajv: 6.12.6 ajv: 6.12.6
@@ -5219,9 +5230,9 @@ snapshots:
'@eslint/object-schema@2.1.6': {} '@eslint/object-schema@2.1.6': {}
'@eslint/plugin-kit@0.3.1': '@eslint/plugin-kit@0.3.4':
dependencies: dependencies:
'@eslint/core': 0.14.0 '@eslint/core': 0.15.1
levn: 0.4.1 levn: 0.4.1
'@floating-ui/core@1.7.0': '@floating-ui/core@1.7.0':
@@ -5277,28 +5288,27 @@ snapshots:
wrap-ansi: 8.1.0 wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0 wrap-ansi-cjs: wrap-ansi@7.0.0
'@jridgewell/gen-mapping@0.3.8': '@jridgewell/gen-mapping@0.3.12':
dependencies: dependencies:
'@jridgewell/set-array': 1.2.1 '@jridgewell/sourcemap-codec': 1.5.4
'@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping': 0.3.29
'@jridgewell/trace-mapping': 0.3.25
'@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/set-array@1.2.1': {} '@jridgewell/source-map@0.3.10':
'@jridgewell/source-map@0.3.6':
dependencies: dependencies:
'@jridgewell/gen-mapping': 0.3.8 '@jridgewell/gen-mapping': 0.3.12
'@jridgewell/trace-mapping': 0.3.25 '@jridgewell/trace-mapping': 0.3.29
optional: true optional: true
'@jridgewell/sourcemap-codec@1.5.0': {} '@jridgewell/sourcemap-codec@1.5.0': {}
'@jridgewell/trace-mapping@0.3.25': '@jridgewell/sourcemap-codec@1.5.4': {}
'@jridgewell/trace-mapping@0.3.29':
dependencies: dependencies:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/sourcemap-codec': 1.5.4
'@keyv/serialize@1.0.3': '@keyv/serialize@1.0.3':
dependencies: dependencies:
@@ -5859,12 +5869,14 @@ snapshots:
nan: 2.22.2 nan: 2.22.2
optional: true optional: true
acorn-jsx@5.3.2(acorn@8.14.1): acorn-jsx@5.3.2(acorn@8.15.0):
dependencies: dependencies:
acorn: 8.14.1 acorn: 8.15.0
acorn@8.14.1: {} acorn@8.14.1: {}
acorn@8.15.0: {}
agent-base@6.0.2: agent-base@6.0.2:
dependencies: dependencies:
debug: 4.4.1 debug: 4.4.1
@@ -6055,7 +6067,7 @@ snapshots:
axios@1.9.0: axios@1.9.0:
dependencies: dependencies:
follow-redirects: 1.15.9 follow-redirects: 1.15.9
form-data: 4.0.2 form-data: 4.0.4
proxy-from-env: 1.1.0 proxy-from-env: 1.1.0
transitivePeerDependencies: transitivePeerDependencies:
- debug - debug
@@ -6095,12 +6107,12 @@ snapshots:
boolean@3.2.0: boolean@3.2.0:
optional: true optional: true
brace-expansion@1.1.11: brace-expansion@1.1.12:
dependencies: dependencies:
balanced-match: 1.0.2 balanced-match: 1.0.2
concat-map: 0.0.1 concat-map: 0.0.1
brace-expansion@2.0.1: brace-expansion@2.0.2:
dependencies: dependencies:
balanced-match: 1.0.2 balanced-match: 1.0.2
@@ -6701,7 +6713,7 @@ snapshots:
builder-util: 26.0.11 builder-util: 26.0.11
builder-util-runtime: 9.3.1 builder-util-runtime: 9.3.1
chalk: 4.1.2 chalk: 4.1.2
form-data: 4.0.2 form-data: 4.0.4
fs-extra: 10.1.0 fs-extra: 10.1.0
lazy-val: 1.0.5 lazy-val: 1.0.5
mime: 2.6.0 mime: 2.6.0
@@ -7002,7 +7014,7 @@ snapshots:
'@eslint/core': 0.14.0 '@eslint/core': 0.14.0
'@eslint/eslintrc': 3.3.1 '@eslint/eslintrc': 3.3.1
'@eslint/js': 9.27.0 '@eslint/js': 9.27.0
'@eslint/plugin-kit': 0.3.1 '@eslint/plugin-kit': 0.3.4
'@humanfs/node': 0.16.6 '@humanfs/node': 0.16.6
'@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.4.3 '@humanwhocodes/retry': 0.4.3
@@ -7035,8 +7047,8 @@ snapshots:
espree@10.3.0: espree@10.3.0:
dependencies: dependencies:
acorn: 8.14.1 acorn: 8.15.0
acorn-jsx: 5.3.2(acorn@8.14.1) acorn-jsx: 5.3.2(acorn@8.15.0)
eslint-visitor-keys: 4.2.0 eslint-visitor-keys: 4.2.0
esquery@1.6.0: esquery@1.6.0:
@@ -7164,11 +7176,12 @@ snapshots:
cross-spawn: 7.0.6 cross-spawn: 7.0.6
signal-exit: 4.1.0 signal-exit: 4.1.0
form-data@4.0.2: form-data@4.0.4:
dependencies: dependencies:
asynckit: 0.4.0 asynckit: 0.4.0
combined-stream: 1.0.8 combined-stream: 1.0.8
es-set-tostringtag: 2.1.0 es-set-tostringtag: 2.1.0
hasown: 2.0.2
mime-types: 2.1.35 mime-types: 2.1.35
format-duration@2.0.0: {} format-duration@2.0.0: {}
@@ -7988,19 +8001,19 @@ snapshots:
minimatch@10.0.1: minimatch@10.0.1:
dependencies: dependencies:
brace-expansion: 2.0.1 brace-expansion: 2.0.2
minimatch@3.1.2: minimatch@3.1.2:
dependencies: dependencies:
brace-expansion: 1.1.11 brace-expansion: 1.1.12
minimatch@5.1.6: minimatch@5.1.6:
dependencies: dependencies:
brace-expansion: 2.0.1 brace-expansion: 2.0.2
minimatch@9.0.5: minimatch@9.0.5:
dependencies: dependencies:
brace-expansion: 2.0.1 brace-expansion: 2.0.2
minimist@1.2.8: {} minimist@1.2.8: {}
@@ -8933,11 +8946,11 @@ snapshots:
dependencies: dependencies:
agent-base: 6.0.2 agent-base: 6.0.2
debug: 4.4.1 debug: 4.4.1
socks: 2.8.4 socks: 2.8.6
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
socks@2.8.4: socks@2.8.6:
dependencies: dependencies:
ip-address: 9.0.5 ip-address: 9.0.5
smart-buffer: 4.2.0 smart-buffer: 4.2.0
@@ -9237,8 +9250,8 @@ snapshots:
terser@5.39.2: terser@5.39.2:
dependencies: dependencies:
'@jridgewell/source-map': 0.3.6 '@jridgewell/source-map': 0.3.10
acorn: 8.14.1 acorn: 8.15.0
commander: 2.20.3 commander: 2.20.3
source-map-support: 0.5.21 source-map-support: 0.5.21
optional: true optional: true
+6
View File
@@ -19,6 +19,7 @@ import nl from './locales/nl.json';
import pl from './locales/pl.json'; import pl from './locales/pl.json';
import ptBr from './locales/pt-BR.json'; import ptBr from './locales/pt-BR.json';
import ru from './locales/ru.json'; import ru from './locales/ru.json';
import sl from './locales/sl.json';
import sr from './locales/sr.json'; import sr from './locales/sr.json';
import sv from './locales/sv.json'; import sv from './locales/sv.json';
import ta from './locales/ta.json'; import ta from './locales/ta.json';
@@ -43,6 +44,7 @@ const resources = {
pl: { translation: pl }, pl: { translation: pl },
'pt-BR': { translation: ptBr }, 'pt-BR': { translation: ptBr },
ru: { translation: ru }, ru: { translation: ru },
sl: { translation: sl },
sr: { translation: sr }, sr: { translation: sr },
sv: { translation: sv }, sv: { translation: sv },
ta: { translation: ta }, ta: { translation: ta },
@@ -119,6 +121,10 @@ export const languages = [
label: 'Русский', label: 'Русский',
value: 'ru', value: 'ru',
}, },
{
label: 'Slovenščina',
value: 'sl',
},
{ {
label: 'Srpski', label: 'Srpski',
value: 'sr', value: 'sr',
+8 -3
View File
@@ -271,7 +271,9 @@
"discordPausedStatus": "zobrazit rich presence při pozastavení", "discordPausedStatus": "zobrazit rich presence při pozastavení",
"discordPausedStatus_description": "pokud je povoleno, bude při pozastavení přehrávače zobrazen stav", "discordPausedStatus_description": "pokud je povoleno, bude při pozastavení přehrávače zobrazen stav",
"preservePitch": "zachovat výšku", "preservePitch": "zachovat výšku",
"preservePitch_description": "zachová výšku při úpravě rychlosti přehrávání" "preservePitch_description": "zachová výšku při úpravě rychlosti přehrávání",
"notify": "povolit oznámení o skladbách",
"notify_description": "zobrazit oznámení při změně aktuální skladby"
}, },
"action": { "action": {
"editPlaylist": "upravit $t(entity.playlist_one)", "editPlaylist": "upravit $t(entity.playlist_one)",
@@ -393,7 +395,9 @@
"additionalParticipants": "další přispívající", "additionalParticipants": "další přispívající",
"tags": "štítky", "tags": "štítky",
"viewReleaseNotes": "zobrazit seznam změn", "viewReleaseNotes": "zobrazit seznam změn",
"newVersion": "byla nainstalována nová verze ({{version}})" "newVersion": "byla nainstalována nová verze ({{version}})",
"bitDepth": "bitová hloubka",
"sampleRate": "vzorkovací frekvence"
}, },
"table": { "table": {
"config": { "config": {
@@ -495,7 +499,8 @@
"badAlbum": "tuto stránku vidíte, protože tato skladba není součástí alba. tento problém může nastat, pokud máte skladbu na nejvyšší úrovni vaší složky s hudbou. jellyfin seskupuje skladby pouze, pokud se nacházejí ve složce.", "badAlbum": "tuto stránku vidíte, protože tato skladba není součástí alba. tento problém může nastat, pokud máte skladbu na nejvyšší úrovni vaší složky s hudbou. jellyfin seskupuje skladby pouze, pokud se nacházejí ve složce.",
"networkError": "vyskytla se chyba sítě", "networkError": "vyskytla se chyba sítě",
"openError": "nepodařilo se otevřít soubor", "openError": "nepodařilo se otevřít soubor",
"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"
}, },
"filter": { "filter": {
"mostPlayed": "nejvíce přehráváno", "mostPlayed": "nejvíce přehráváno",
+5
View File
@@ -36,6 +36,7 @@
"ascending": "ascending", "ascending": "ascending",
"backward": "backward", "backward": "backward",
"biography": "biography", "biography": "biography",
"bitDepth": "bit depth",
"bitrate": "bitrate", "bitrate": "bitrate",
"bpm": "bpm", "bpm": "bpm",
"cancel": "cancel", "cancel": "cancel",
@@ -99,6 +100,7 @@
"resetToDefault": "reset to default", "resetToDefault": "reset to default",
"restartRequired": "restart required", "restartRequired": "restart required",
"right": "right", "right": "right",
"sampleRate": "sample rate",
"save": "save", "save": "save",
"saveAndReplace": "save and replace", "saveAndReplace": "save and replace",
"saveAs": "save as", "saveAs": "save as",
@@ -171,6 +173,7 @@
"loginRateError": "too many login attempts, please try again in a few seconds", "loginRateError": "too many login attempts, please try again in a few seconds",
"mpvRequired": "MPV required", "mpvRequired": "MPV required",
"networkError": "a network error occurred", "networkError": "a network error occurred",
"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",
"remoteDisableError": "an error occurred when trying to $t(common.disable) the remote server", "remoteDisableError": "an error occurred when trying to $t(common.disable) the remote server",
@@ -605,6 +608,8 @@
"lyricFetchProvider_description": "select the providers to fetch lyrics from. the order of the providers is the order in which they will be queried", "lyricFetchProvider_description": "select the providers to fetch lyrics from. the order of the providers is the order in which they will be queried",
"lyricOffset": "lyric offset (ms)", "lyricOffset": "lyric offset (ms)",
"lyricOffset_description": "offset the lyric by the specified amount of milliseconds", "lyricOffset_description": "offset the lyric by the specified amount of milliseconds",
"notify": "enable song notifications",
"notify_description": "show notifications when changing the current song",
"minimizeToTray": "minimize to tray", "minimizeToTray": "minimize to tray",
"minimizeToTray_description": "minimize the application to the system tray", "minimizeToTray_description": "minimize the application to the system tray",
"minimumScrobblePercentage": "minimum scrobble duration (percentage)", "minimumScrobblePercentage": "minimum scrobble duration (percentage)",
+8 -3
View File
@@ -271,7 +271,9 @@
"discordPausedStatus": "Mostrar estado de actividad cuando esté en pausa", "discordPausedStatus": "Mostrar estado de actividad cuando esté en pausa",
"discordPausedStatus_description": "Cuando está activado, el estado mostrará cuando el reproductor esté en pausa", "discordPausedStatus_description": "Cuando está activado, el estado mostrará cuando el reproductor esté en pausa",
"preservePitch": "Mantener el tono", "preservePitch": "Mantener el tono",
"preservePitch_description": "Mantiene el tono cuando se modifica la velocidad de reproducción" "preservePitch_description": "Mantiene el tono cuando se modifica la velocidad de reproducción",
"notify": "Activar notificaciones de canciones",
"notify_description": "Muestra notificaciones cuando se cambia la canción actual"
}, },
"action": { "action": {
"editPlaylist": "editar $t(entity.playlist_one)", "editPlaylist": "editar $t(entity.playlist_one)",
@@ -393,7 +395,9 @@
"additionalParticipants": "Participantes adicionales", "additionalParticipants": "Participantes adicionales",
"tags": "Etiquetas", "tags": "Etiquetas",
"newVersion": "Una nueva versión ha sido instalada ({{version}})", "newVersion": "Una nueva versión ha sido instalada ({{version}})",
"viewReleaseNotes": "Ver notas de lanzamiento" "viewReleaseNotes": "Ver notas de lanzamiento",
"bitDepth": "Profundidad de bit",
"sampleRate": "Frecuencia de muestreo"
}, },
"error": { "error": {
"remotePortWarning": "reiniciar el servidor para aplicar el nuevo puerto", "remotePortWarning": "reiniciar el servidor para aplicar el nuevo puerto",
@@ -418,7 +422,8 @@
"badAlbum": "Estás viendo esta página porque esta canción no forma parte de un álbum. Este problema puede ocurrir si tienes una canción en el nivel superior de tu carpeta de música. Jellyfin solo agrupa pistas si están en una carpeta.", "badAlbum": "Estás viendo esta página porque esta canción no forma parte de un álbum. Este problema puede ocurrir si tienes una canción en el nivel superior de tu carpeta de música. Jellyfin solo agrupa pistas si están en una carpeta.",
"networkError": "Ocurrió un error de red", "networkError": "Ocurrió un error de red",
"openError": "No se pudo abrir el archivo", "openError": "No se pudo abrir el archivo",
"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"
}, },
"filter": { "filter": {
"mostPlayed": "más reproducido", "mostPlayed": "más reproducido",
+13 -5
View File
@@ -100,6 +100,9 @@
"cancel": "annuler", "cancel": "annuler",
"forceRestartRequired": "redémarrer pour appliquer les changements… fermer la notification pour redémarrer", "forceRestartRequired": "redémarrer pour appliquer les changements… fermer la notification pour redémarrer",
"setting": "paramètre", "setting": "paramètre",
"setting_one": "paramètre",
"setting_many": "",
"setting_other": "paramètres",
"version": "version", "version": "version",
"title": "titre", "title": "titre",
"filter_one": "filtre", "filter_one": "filtre",
@@ -152,7 +155,9 @@
"additionalParticipants": "participants additionnels", "additionalParticipants": "participants additionnels",
"tags": "tags", "tags": "tags",
"newVersion": "une nouvelle version vient d'être installé ({{version}})", "newVersion": "une nouvelle version vient d'être installé ({{version}})",
"viewReleaseNotes": "voir la note de version" "viewReleaseNotes": "voir la note de version",
"sampleRate": "taux d'échantillonnage",
"bitDepth": "bit par échantillon"
}, },
"error": { "error": {
"remotePortWarning": "redémarrer le serveur pour appliquer le nouveau port", "remotePortWarning": "redémarrer le serveur pour appliquer le nouveau port",
@@ -177,7 +182,8 @@
"openError": "impossible d'ouvrir le fichier", "openError": "impossible d'ouvrir le fichier",
"networkError": "une erreur de réseau est survenue", "networkError": "une erreur de réseau est survenue",
"badAlbum": "vous voyez cette page parce que cette chanson ne fait pas parti d'un album. vous rencontrez probablement cette erreur si vous avez une chanson qui n'est pas dans votre répertoire de musique. jellyfin gère les chansons uniquement si elles sont dans un sous-dossier, qui est lui-même dans un dossier \"Musique(s)\".", "badAlbum": "vous voyez cette page parce que cette chanson ne fait pas parti d'un album. vous rencontrez probablement cette erreur si vous avez une chanson qui n'est pas dans votre répertoire de musique. jellyfin gère les chansons uniquement si elles sont dans un sous-dossier, qui est lui-même dans un dossier \"Musique(s)\".",
"badValue": "option {{value}} invalide. Cette valeur n'existe plus" "badValue": "option {{value}} invalide. Cette valeur n'existe plus",
"notificationDenied": "les autorisations pour les notifications ont été refusées. ce paramètre n'a aucun effet"
}, },
"filter": { "filter": {
"mostPlayed": "plus joués", "mostPlayed": "plus joués",
@@ -398,7 +404,7 @@
"discordIdleStatus_description": "quand activé, mettre à jour le status pendant que le lecteur est inactif", "discordIdleStatus_description": "quand activé, mettre à jour le status pendant que le lecteur est inactif",
"showSkipButtons": "affiche les boutons suivants et précédents", "showSkipButtons": "affiche les boutons suivants et précédents",
"minimumScrobblePercentage": "durée minimal du scobble (pourcentage)", "minimumScrobblePercentage": "durée minimal du scobble (pourcentage)",
"lyricFetch": "récupère les paroles depuis internet", "lyricFetch": "récupérer les paroles depuis internet",
"scrobble": "scrobble", "scrobble": "scrobble",
"enableRemote_description": "activer le serveur de contrôle à distance, qui permet à d'autres appareils de contrôler l'application", "enableRemote_description": "activer le serveur de contrôle à distance, qui permet à d'autres appareils de contrôler l'application",
"fontType_optionSystem": "police système", "fontType_optionSystem": "police système",
@@ -576,7 +582,7 @@
"artistConfiguration": "page de configuration de l'artiste de l'album", "artistConfiguration": "page de configuration de l'artiste de l'album",
"artistConfiguration_description": "configurer les éléments et l'ordre à afficher, sur la page de l'artiste de l'album", "artistConfiguration_description": "configurer les éléments et l'ordre à afficher, sur la page de l'artiste de l'album",
"doubleClickBehavior": "mettre en file d'attente toutes les pistes recherchées lors d'un double clic", "doubleClickBehavior": "mettre en file d'attente toutes les pistes recherchées lors d'un double clic",
"contextMenu": "configuration du menu contexte (clic droit)", "contextMenu": "configuration du menu contextuel (clic droit)",
"contextMenu_description": "permet de masquer les éléments qui s'affichent dans le menu lorsque vous cliquez avec le bouton droit de la souris sur un élément. les éléments qui ne sont pas cochés seront masqués", "contextMenu_description": "permet de masquer les éléments qui s'affichent dans le menu lorsque vous cliquez avec le bouton droit de la souris sur un élément. les éléments qui ne sont pas cochés seront masqués",
"albumBackground": "image d'arrière-plan de l'album", "albumBackground": "image d'arrière-plan de l'album",
"albumBackground_description": "ajoute une image d'arrière-plan pour les pages de l'album contenant les illustrations de l'album", "albumBackground_description": "ajoute une image d'arrière-plan pour les pages de l'album contenant les illustrations de l'album",
@@ -613,7 +619,9 @@
"discordPausedStatus_description": "quand activé, le status s'affichera lorsque le lecteur est en pause", "discordPausedStatus_description": "quand activé, le status s'affichera lorsque le lecteur est en pause",
"discordPausedStatus": "afficher le status d'activité en pause", "discordPausedStatus": "afficher le status d'activité en pause",
"preservePitch": "préserver la hauteur", "preservePitch": "préserver la hauteur",
"preservePitch_description": "préserver la hauteur lors du changement de la vitesse de lecture" "preservePitch_description": "préserver la hauteur lors du changement de la vitesse de lecture",
"notify": "activer les notifications des chansons",
"notify_description": "affiche une notification lors du changement de chanson"
}, },
"form": { "form": {
"deletePlaylist": { "deletePlaylist": {
+203 -25
View File
@@ -16,7 +16,12 @@
"toggleSmartPlaylistEditor": "attiva/disattiva editor $t(entity.smartPlaylist)", "toggleSmartPlaylistEditor": "attiva/disattiva editor $t(entity.smartPlaylist)",
"removeFromFavorites": "rimuovi da $t(entity.favorite_other)", "removeFromFavorites": "rimuovi da $t(entity.favorite_other)",
"moveToTop": "sposta in cima", "moveToTop": "sposta in cima",
"moveToBottom": "sposta in fondo" "moveToBottom": "sposta in fondo",
"moveToNext": "passa al successivo",
"openIn": {
"lastfm": "Apri in Last.fm",
"musicbrainz": "Apri in MusicBrainz"
}
}, },
"common": { "common": {
"backward": "indietro", "backward": "indietro",
@@ -99,7 +104,22 @@
"yes": "si", "yes": "si",
"random": "casuale", "random": "casuale",
"size": "dimensione", "size": "dimensione",
"note": "nota" "note": "nota",
"additionalParticipants": "partecipanti aggiuntivi",
"newVersion": "è stata installata una nuova versione ({{version}})",
"viewReleaseNotes": "mostra le note di rilascio",
"albumGain": "guadagno (gain) dell'album",
"albumPeak": "picco di volume dell'album",
"close": "chiudi",
"codec": "codec",
"mbid": "MusicBrainz ID",
"preview": "anteprima",
"reload": "ricarica",
"share": "condividi",
"tags": "tags",
"trackGain": "normalizzazione (gain) del brano",
"trackPeak": "picco di volume del brano",
"translation": "traduzione"
}, },
"player": { "player": {
"repeat_all": "ripeti coda", "repeat_all": "ripeti coda",
@@ -113,7 +133,7 @@
"skip_back": "salta indietro", "skip_back": "salta indietro",
"favorite": "preferito", "favorite": "preferito",
"next": "successivo", "next": "successivo",
"shuffle": "mescola", "shuffle": "riproduzione casuale",
"playbackFetchNoResults": "nessuna canzone trovata", "playbackFetchNoResults": "nessuna canzone trovata",
"playbackFetchInProgress": "caricamento canzoni…", "playbackFetchInProgress": "caricamento canzoni…",
"addNext": "aggiungi successivo", "addNext": "aggiungi successivo",
@@ -130,7 +150,9 @@
"shuffle_off": "non mescolare", "shuffle_off": "non mescolare",
"addLast": "aggiungi in coda", "addLast": "aggiungi in coda",
"mute": "silenzia", "mute": "silenzia",
"skip_forward": "salta avanti" "skip_forward": "salta avanti",
"playSimilarSongs": "riproduci brani simili",
"viewQueue": "visualizza coda"
}, },
"setting": { "setting": {
"crossfadeStyle_description": "seleziona lo stile dissolvenza da usare per il player audio", "crossfadeStyle_description": "seleziona lo stile dissolvenza da usare per il player audio",
@@ -150,7 +172,7 @@
"skipDuration_description": "imposta la durata da saltare quando vengono usati i pulsanti di salto nella barra del player", "skipDuration_description": "imposta la durata da saltare quando vengono usati i pulsanti di salto nella barra del player",
"enableRemote_description": "abilita il controllo remoto del server per permettere ad altri dispositivi di controllare l'applicazione", "enableRemote_description": "abilita il controllo remoto del server per permettere ad altri dispositivi di controllare l'applicazione",
"fontType_optionSystem": "font di sistema", "fontType_optionSystem": "font di sistema",
"mpvExecutablePath_description": "imposta il percorso dell'eseguibile di mpv", "mpvExecutablePath_description": "imposta il percorso dell'eseguibile mpv. se lasciato vuoto, verrà utilizzato il percorso predefinito",
"hotkey_favoriteCurrentSong": "$t(common.currentSong) preferita", "hotkey_favoriteCurrentSong": "$t(common.currentSong) preferita",
"crossfadeStyle": "stile dissolvenza", "crossfadeStyle": "stile dissolvenza",
"sidebarConfiguration": "configurazione barra laterale", "sidebarConfiguration": "configurazione barra laterale",
@@ -268,7 +290,7 @@
"replayGainMode_description": "aggiusta il volume secondo i valori {{ReplayGain}} salvati nei metadati del file", "replayGainMode_description": "aggiusta il volume secondo i valori {{ReplayGain}} salvati nei metadati del file",
"showSkipButtons": "mostra pulsanti per saltare", "showSkipButtons": "mostra pulsanti per saltare",
"sampleRate": "frequenza di campionamento", "sampleRate": "frequenza di campionamento",
"sampleRate_description": "seleziona la frequenza di campionamento di output da usare se la frequenza di campionamento selezionata è diversa da quella della del media attuale", "sampleRate_description": "seleziona la frequenza di campionamento di output da utilizzare se quella selezionata è diversa da quella del file sorgente in riproduzione. Un valore inferiore a 8000 utilizzerà la frequenza predefinita",
"hotkey_togglePreviousSongFavorite": "imposta/rimuovi $t(common.previousSong) favorito", "hotkey_togglePreviousSongFavorite": "imposta/rimuovi $t(common.previousSong) favorito",
"hotkey_unfavoritePreviousSong": "rimuovi $t(common.previousSong) dai preferiti", "hotkey_unfavoritePreviousSong": "rimuovi $t(common.previousSong) dai preferiti",
"showSkipButton_description": "mostra o nascondi i pulsanti per saltare nella barra del player", "showSkipButton_description": "mostra o nascondi i pulsanti per saltare nella barra del player",
@@ -293,7 +315,85 @@
"clearQueryCache": "pulisci cache di feishin", "clearQueryCache": "pulisci cache di feishin",
"buttonSize_description": "Dimensione bottoni nella barra di riproduzione", "buttonSize_description": "Dimensione bottoni nella barra di riproduzione",
"clearCache": "pulisci la cache del browser", "clearCache": "pulisci la cache del browser",
"clearQueryCache_description": "\"leggera\" pulizia di feishin. verranno aggiornate le playlist, metadata delle tracce e i testi salvati. impostazioni, credenziali del server e le immagini salvate saranno mantenute" "clearQueryCache_description": "\"leggera\" pulizia di feishin. verranno aggiornate le playlist, metadata delle tracce e i testi salvati. impostazioni, credenziali del server e le immagini salvate saranno mantenute",
"albumBackground": "immagine di sfondo dell'album",
"albumBackground_description": "aggiunge un'immagine di sfondo per le pagine degli album contenenti l'album art",
"albumBackgroundBlur": "intensità sfocatura immagine di sfondo dell'album",
"albumBackgroundBlur_description": "regola la quantità di sfocatura applicata all'immagine di sfondo dell'album",
"artistConfiguration": "configurazione della pagina artista dellalbum",
"artistConfiguration_description": "configurare quali elementi vengono visualizzati, e in quale ordine, nella pagina dell'artista dell'album",
"buttonSize": "dimensione del bottone nella barra di riproduzione",
"clearCacheSuccess": "cache pulita correttamente",
"contextMenu": "configurazione menu contestuale (clic destro)",
"contextMenu_description": "consente di nascondere gli elementi che vengono visualizzati nel menu quando si fa clic destro su un elemento. gli oggetti non selezionati saranno nascosti",
"customCssEnable": "abilita css personalizzato",
"customCssEnable_description": "consente di scrivere css personalizzati.",
"customCssNotice": "Attenzione: sebbene ci sia una certa sanitizzazione (vengono bloccati url() e content:), luso di CSS personalizzati può comunque comportare dei rischi modificando linterfaccia.",
"customCss": "css personalizzato",
"customCss_description": "contenuto CSS personalizzato. Nota: le proprietà content e gli URL remoti non sono consentiti. Di seguito è mostrata unanteprima del tuo contenuto. Sono presenti anche altri campi non impostati da te a causa della sanitizzazione.",
"discordPausedStatus": "mostra rich presence di Discord quando la riproduzione è in pausa",
"discordPausedStatus_description": "quando abilitato, verrà mostrato lo stato del lettore in standby/pausa (nessun brano in riproduzione)",
"discordListening": "mostra stato come in ascolto",
"discordListening_description": "mostra lo stato come in ascolto invece che in riproduzione",
"discordServeImage": "recupera le immagini di {{discord}} dal server",
"discordServeImage_description": "condividi la copertina per la rich presence di {{discord}} direttamente dal server, disponibile solo per Jellyfin e Navidrome",
"doubleClickBehavior": "aggiungi alla coda tutte le tracce cercate, con un doppio clic",
"doubleClickBehavior_description": "se attivato, tutte le tracce corrispondenti alla ricerca verranno aggiunte alla coda. altrimenti, verrà aggiunta alla coda solo la traccia selezionata",
"externalLinks": "mostra link esterni",
"externalLinks_description": "consente di visualizzare link esterni (Last.fm, MusicBrainz) sulle pagine di artista/album",
"preferLocalLyrics": "utilizza i testi locali",
"preferLocalLyrics_description": "usa i testi locali anziché quelli online, quando disponibili",
"genreBehavior": "comportamento predefinito della pagina genere",
"genreBehavior_description": "determina se cliccando su un genere si apre di default la lista dei brani o degli album",
"homeConfiguration": "configurazione della home page",
"homeConfiguration_description": "configura quali elementi vengono mostrati e in quale ordine nella home page",
"homeFeature": "carosello in evidenza nella home page",
"homeFeature_description": "controlla se mostrare il grande carosello in evidenza nella pagina principale",
"imageAspectRatio": "usa dimensioni originali(aspect ratio) della copertina",
"imageAspectRatio_description": "se abilitato, la copertina verrà mostrata utilizzando le dimesioni originali. per le immagini con rapporto diverso da 1:1, lo spazio residuo resterà vuoto",
"lastfm": "mostra links last.fm",
"lastfm_description": "mostra i link per last.fm sulle pagine di artista/album",
"lastfmApiKey": "{{lastfm}} chiave API",
"lastfmApiKey_description": "chiave API per {{lastfm}}. necessaria per visualizzare le copertine",
"mpvExtraParameters_help": "uno per linea",
"musicbrainz": "mostra links musicbrainz",
"musicbrainz_description": "mostra link a musicbrainz sulle pagine degli artisti/album, se è disponibile un mbid",
"neteaseTranslation": "Abilita traduzioni di NetEase",
"neteaseTranslation_description": "Se abilitato, recupera e mostra i testi tradotti da NetEase, se disponibili.",
"passwordStore": "Archivio di password/segreti",
"passwordStore_description": "specifica quale archivio di password e segreti utilizzare. modificalo in caso di problemi nel salvataggio delle credenziali.",
"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_optionDetached": "sganciata",
"startMinimized": "avvia minimizzato",
"startMinimized_description": "avvia l'app nella barra di sistema",
"transcodeNote": "ha effetto dopo 1 brano (web) - 2 brani (mpv)",
"transcode": "abilita la transcodifica",
"transcode_description": "abilita la transcodifica in formati diversi",
"playerbarOpenDrawer": "attiva/disattiva schermo intero",
"playerbarOpenDrawer_description": "consente di cliccare sulla barra del lettore per aprire il lettore a schermo intero",
"replayGainClipping": "clipping di {{ReplayGain}}",
"replayGainFallback": "metodo alternativo di {{ReplayGain}}",
"transcodeBitrate": "bitrate per la transcodifica",
"transcodeBitrate_description": "seleziona il bitrate per la transcodifica. 0 significa lasciare che sia il server a scegliere",
"transcodeFormat": "formato per la transcodifica",
"transcodeFormat_description": "seleziona il formato per la transcodifica. se vuoto viene decisco dal server",
"translationApiProvider": "translation api provider",
"translationApiProvider_description": "api provider for translation",
"translationApiKey": "chiave api translation",
"translationApiKey_description": "chiave api per la traduzione (supporta solo endpoint di servizio globali)",
"translationTargetLanguage": "lingua di destinazione della traduzione",
"translationTargetLanguage_description": "lingua di destinazione per la traduzione",
"trayEnabled": "Mostra icona app nella barra di sistema",
"trayEnabled_description": "mostra/nascondi icona app nella barra si sistema. se disabilitato, disattiva anche minimizza/chiudi nella barra di sistema",
"volumeWidth": "larghezza della barra del volume",
"webAudio": "use audio web",
"webAudio_description": "usa audio web. abilita funzionalità avanzate come ReplayGain. disabilita se riscontri problemi",
"preservePitch": "mantieni tono (pitch)",
"preservePitch_description": "mantiene il tono (pitch) durante la modifica della velocità di riproduzione",
"volumeWidth_description": "larghezza del cursore del volume"
}, },
"error": { "error": {
"remotePortWarning": "riavvia il server per applicare la nuova porta", "remotePortWarning": "riavvia il server per applicare la nuova porta",
@@ -314,7 +414,11 @@
"mpvRequired": "MPV richiesto", "mpvRequired": "MPV richiesto",
"audioDeviceFetchError": "si è verificato un errore nel provare ad ottenre i device audio", "audioDeviceFetchError": "si è verificato un errore nel provare ad ottenre i device audio",
"invalidServer": "server non valido", "invalidServer": "server non valido",
"loginRateError": "troppi tentativi di accesso, per favore riprova tra qualche secondo" "loginRateError": "troppi tentativi di accesso, per favore riprova tra qualche secondo",
"badAlbum": "stai visualizzando questa pagina perché questa canzone non fa parte di un album. probabilmente vedi questo messaggio perché hai una canzone posizionata direttamente nella cartella principale della tua libreria musicale. jellyfin raggruppa le tracce solo se si trovano allinterno di una cartella.",
"badValue": "opzione non valida \"{{value}}\". valore inesistente",
"networkError": "si è verificato un errore di rete",
"openError": "impossibile aprire il file"
}, },
"filter": { "filter": {
"mostPlayed": "più riprodotti", "mostPlayed": "più riprodotti",
@@ -372,7 +476,9 @@
"settings": "$t(common.setting_other)", "settings": "$t(common.setting_other)",
"home": "$t(common.home)", "home": "$t(common.home)",
"artists": "$t(entity.artist_other)", "artists": "$t(entity.artist_other)",
"albumArtists": "$t(entity.albumArtist_other)" "albumArtists": "$t(entity.albumArtist_other)",
"myLibrary": "la mia libreria",
"shared": "condivisa $t(entity.playlist_other)"
}, },
"fullscreenPlayer": { "fullscreenPlayer": {
"config": { "config": {
@@ -386,11 +492,16 @@
"unsynchronized": "non sinncronizzato", "unsynchronized": "non sinncronizzato",
"lyricAlignment": "allineamento testo", "lyricAlignment": "allineamento testo",
"useImageAspectRatio": "usa le proporzioni dell'immagine", "useImageAspectRatio": "usa le proporzioni dell'immagine",
"lyricGap": "gap testo" "lyricGap": "gap testo",
"dynamicImageBlur": "intensità sfocatura immagine",
"dynamicIsImage": "abilita immagine di sfondo",
"lyricOffset": "ritardo testi (ms)"
}, },
"upNext": "successivamente", "upNext": "successivamente",
"lyrics": "testi", "lyrics": "testi",
"related": "correlati" "related": "correlati",
"visualizer": "visualizzatore audio",
"noLyrics": "nessun testo trovato"
}, },
"appMenu": { "appMenu": {
"selectServer": "seleziona server", "selectServer": "seleziona server",
@@ -420,7 +531,13 @@
"addFavorite": "$t(action.addToFavorites)", "addFavorite": "$t(action.addToFavorites)",
"play": "$t(player.play)", "play": "$t(player.play)",
"numberSelected": "{{count}} selezionati", "numberSelected": "{{count}} selezionati",
"removeFromQueue": "$t(action.removeFromQueue)" "removeFromQueue": "$t(action.removeFromQueue)",
"download": "download",
"moveToNext": "$t(action.moveToNext)",
"playSimilarSongs": "$t(player.playSimilarSongs)",
"playShuffled": "$t(player.shuffle)",
"shareItem": "condividi elemento",
"showDetails": "mostra info"
}, },
"home": { "home": {
"mostPlayed": "più riprodotti", "mostPlayed": "più riprodotti",
@@ -431,22 +548,28 @@
}, },
"albumDetail": { "albumDetail": {
"moreFromArtist": "di più da questo $t(entity.artist_one)", "moreFromArtist": "di più da questo $t(entity.artist_one)",
"moreFromGeneric": "di più da {{item}}" "moreFromGeneric": "di più da {{item}}",
"released": "rilasciato"
}, },
"setting": { "setting": {
"playbackTab": "riproduzione", "playbackTab": "riproduzione",
"generalTab": "generale", "generalTab": "generale",
"hotkeysTab": "tasti a scelta rapida", "hotkeysTab": "tasti a scelta rapida",
"windowTab": "finestra" "windowTab": "finestra",
"advanced": "avanzate"
}, },
"albumArtistList": { "albumArtistList": {
"title": "$t(entity.albumArtist_other)" "title": "$t(entity.albumArtist_other)"
}, },
"genreList": { "genreList": {
"title": "$t(entity.genre_other)" "title": "$t(entity.genre_other)",
"showAlbums": "mostra $t(entity.genre_one) $t(entity.album_other)",
"showTracks": "mostra $t(entity.genre_one) $t(entity.track_other)"
}, },
"trackList": { "trackList": {
"title": "$t(entity.track_other)" "title": "$t(entity.track_other)",
"artistTracks": "tracce di {{artist}}",
"genreTracks": "\"{{genre}}\" $t(entity.track_other)"
}, },
"globalSearch": { "globalSearch": {
"commands": { "commands": {
@@ -460,7 +583,36 @@
"title": "$t(entity.playlist_other)" "title": "$t(entity.playlist_other)"
}, },
"albumList": { "albumList": {
"title": "$t(entity.album_other)" "title": "$t(entity.album_other)",
"artistAlbums": "albums di {{artist}}",
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)"
},
"albumArtistDetail": {
"about": "Info {{artist}}",
"appearsOn": "compare su",
"recentReleases": "uscite recenti",
"viewDiscography": "mostra discografia",
"relatedArtists": "correlati $t(entity.artist_other)",
"topSongs": "brani migliori",
"topSongsFrom": "brani migliori da {{title}}",
"viewAll": "mostra tutto",
"viewAllTracks": "mostra tutto $t(entity.track_other)"
},
"manageServers": {
"title": "gestisci servers",
"serverDetails": "dettagli server",
"url": "URL",
"username": "nome utente",
"editServerDetailsTooltip": "modifica dettagli server",
"removeServer": "rimuovi server"
},
"itemDetail": {
"copyPath": "copia percorso negli appunti",
"copiedPath": "percorso copiato con successo",
"openFile": "mostra traccia nel gestore file"
},
"playlist": {
"reorder": "riordino abilitato solo quando si ordina per id"
} }
}, },
"form": { "form": {
@@ -491,7 +643,7 @@
"error_savePassword": "si è verificato un errore quando si è provato a salvare la password" "error_savePassword": "si è verificato un errore quando si è provato a salvare la password"
}, },
"addToPlaylist": { "addToPlaylist": {
"success": "aggiunto {{message}} $t(entity.track_other) a {{numOfPlaylists}} $t(entity.playlist_other)", "success": "aggiunto $t(entity.trackWithCount, {\"count\": {{message}} }) a $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
"title": "aggiungi a $t(entity.playlist_one)", "title": "aggiungi a $t(entity.playlist_one)",
"input_skipDuplicates": "salta duplicati", "input_skipDuplicates": "salta duplicati",
"input_playlists": "$t(entity.playlist_other)" "input_playlists": "$t(entity.playlist_other)"
@@ -502,7 +654,8 @@
}, },
"queryEditor": { "queryEditor": {
"input_optionMatchAll": "soddisfa tutti", "input_optionMatchAll": "soddisfa tutti",
"input_optionMatchAny": "soddisfa qualsiasi" "input_optionMatchAny": "soddisfa qualsiasi",
"title": "editor di query"
}, },
"lyricSearch": { "lyricSearch": {
"input_name": "$t(common.name)", "input_name": "$t(common.name)",
@@ -510,7 +663,17 @@
"title": "cerca testi" "title": "cerca testi"
}, },
"editPlaylist": { "editPlaylist": {
"title": "modifica $t(entity.playlist_one)" "title": "modifica $t(entity.playlist_one)",
"publicJellyfinNote": "Jellyfin non mostra se una playlist è pubblica o meno. Se vuoi che rimanga pubblica, assicurati di selezionare lopzione seguente",
"success": "$t(entity.playlist_one) aggiornato con successo"
},
"shareItem": {
"allowDownloading": "consentire il download",
"description": "descrizione",
"setExpiration": "imposta scadenza",
"success": "link di condivisione copiato negli appunti (o clicca qui per aprirlo)",
"expireInvalid": "la scadenza deve essere nel futuro",
"createFailed": "condivisione fallita (è abilitata la condivisione?)"
} }
}, },
"table": { "table": {
@@ -520,11 +683,17 @@
"gap": "$t(common.gap)", "gap": "$t(common.gap)",
"tableColumns": "tabella colonne", "tableColumns": "tabella colonne",
"autoFitColumns": "adatta colonne automaticamente", "autoFitColumns": "adatta colonne automaticamente",
"size": "$t(common.size)" "size": "$t(common.size)",
"followCurrentSong": "segui il brano corrente",
"itemGap": "spaziatura tra gli elementi (px)",
"itemSize": "dimensione dellelemento (px)"
}, },
"view": { "view": {
"table": "tabella", "table": "tabella",
"card": "Scheda" "card": "Scheda",
"grid": "griglia",
"list": "lista",
"poster": "poster"
}, },
"label": { "label": {
"releaseDate": "data rilascio", "releaseDate": "data rilascio",
@@ -552,7 +721,9 @@
"discNumber": "numero disco", "discNumber": "numero disco",
"favorite": "$t(common.favorite)", "favorite": "$t(common.favorite)",
"year": "$t(common.year)", "year": "$t(common.year)",
"albumArtist": "$t(entity.albumArtist_one)" "albumArtist": "$t(entity.albumArtist_one)",
"codec": "$t(common.codec)",
"songCount": "$t(entity.track_other)"
} }
}, },
"column": { "column": {
@@ -578,7 +749,8 @@
"path": "percorso", "path": "percorso",
"discNumber": "disco", "discNumber": "disco",
"channels": "$t(common.channel_other)", "channels": "$t(common.channel_other)",
"size": "$t(common.size)" "size": "$t(common.size)",
"codec": "$t(common.codec)"
} }
}, },
"entity": { "entity": {
@@ -627,6 +799,12 @@
"genreWithCount_other": "{{count}} generi", "genreWithCount_other": "{{count}} generi",
"trackWithCount_one": "{{count}} traccia", "trackWithCount_one": "{{count}} traccia",
"trackWithCount_many": "{{count}} tracce", "trackWithCount_many": "{{count}} tracce",
"trackWithCount_other": "{{count}} tracce" "trackWithCount_other": "{{count}} tracce",
"play_one": "{{count}} riproduzione",
"play_many": "{{count}} riproduzioni",
"play_other": "{{count}} riproduzioni",
"song_one": "traccia",
"song_many": "tracce",
"song_other": "tracce"
} }
} }
+77 -6
View File
@@ -104,13 +104,14 @@
"year": "år", "year": "år",
"yes": "ja", "yes": "ja",
"descending": "synkende", "descending": "synkende",
"dismiss": "avkreft", "dismiss": "lukk",
"delete": "slett", "delete": "slett",
"description": "beskrivelse", "description": "beskrivelse",
"manage": "håndtere", "manage": "håndtere",
"maximize": "maksimer", "maximize": "maksimer",
"right": "høyre", "right": "høyre",
"sortOrder": "rekkefølge" "sortOrder": "rekkefølge",
"tags": "tagger"
}, },
"entity": { "entity": {
"smartPlaylist": "smart $t(entity.playlist_one)", "smartPlaylist": "smart $t(entity.playlist_one)",
@@ -233,7 +234,7 @@
"addServer": { "addServer": {
"ignoreCors": "ignorer cors ($t(common.restartRequired))", "ignoreCors": "ignorer cors ($t(common.restartRequired))",
"ignoreSsl": "ignorer ssl ($t(common.restartRequired))", "ignoreSsl": "ignorer ssl ($t(common.restartRequired))",
"error_savePassword": "en problem oppstod ved lagring av passord", "error_savePassword": "et problem oppstod ved lagring av passord",
"input_savePassword": "lagre passord", "input_savePassword": "lagre passord",
"input_url": "lenke", "input_url": "lenke",
"input_username": "brukernavn", "input_username": "brukernavn",
@@ -269,6 +270,10 @@
"updateServer": { "updateServer": {
"success": "vellykket oppdatering av serveren", "success": "vellykket oppdatering av serveren",
"title": "oppdater server" "title": "oppdater server"
},
"queryEditor": {
"input_optionMatchAll": "match alle",
"input_optionMatchAny": "matche hvilken som helst"
} }
}, },
"page": { "page": {
@@ -338,7 +343,7 @@
"lyricGap": "sangtekstavstand", "lyricGap": "sangtekstavstand",
"dynamicImageBlur": "bilduskarphetstørrelse", "dynamicImageBlur": "bilduskarphetstørrelse",
"lyricAlignment": "sangtekstjustering", "lyricAlignment": "sangtekstjustering",
"lyricOffset": "sangtekstjustering (ms)", "lyricOffset": "sangtekstforskyvning (ms)",
"lyricSize": "sangtekststørrelse", "lyricSize": "sangtekststørrelse",
"opacity": "absorpsjon", "opacity": "absorpsjon",
"showLyricMatch": "vis sangteksttreff", "showLyricMatch": "vis sangteksttreff",
@@ -405,7 +410,8 @@
"search": "$t(common.search)", "search": "$t(common.search)",
"settings": "$t(common.setting_other)", "settings": "$t(common.setting_other)",
"shared": "delt $t(entity.playlist_other)", "shared": "delt $t(entity.playlist_other)",
"artists": "$t(entity.artist_other)" "artists": "$t(entity.artist_other)",
"myLibrary": "mitt bibliotek"
}, },
"setting": { "setting": {
"generalTab": "generelt", "generalTab": "generelt",
@@ -416,6 +422,9 @@
}, },
"playlistList": { "playlistList": {
"title": "$t(entity.playlist_other)" "title": "$t(entity.playlist_other)"
},
"playlist": {
"reorder": "omorganisering kun mulig ved sortering på id"
} }
}, },
"player": { "player": {
@@ -439,6 +448,68 @@
"queue_moveToTop": "flytt valgte til bunnen", "queue_moveToTop": "flytt valgte til bunnen",
"playbackFetchNoResults": "ingen sanger funnet", "playbackFetchNoResults": "ingen sanger funnet",
"playbackSpeed": "avspillingshastighet", "playbackSpeed": "avspillingshastighet",
"playSimilarSongs": "spill lignende sanger" "playSimilarSongs": "spill lignende sanger",
"skip": "hopp over",
"shuffle": "spill i tilfeldig rekkefølge",
"shuffle_off": "tilfeldig rekkefølge skrudd av",
"skip_back": "hopp bakover",
"skip_forward": "hopp fremover",
"stop": "stopp",
"toggleFullscreenPlayer": "bytt til fullskjermspiller",
"pause": "sett på pause",
"viewQueue": "se kø",
"unfavorite": "fjern fra favoritter"
},
"setting": {
"accentColor": "aksentfarge",
"accentColor_description": "setter aksentfarge i applikasjonen",
"albumBackground": "album bakgrunnsbilde",
"albumBackgroundBlur": "album bakgrunnsbilde uskarphetsstørrelse",
"albumBackgroundBlur_description": "justerer grad av uskarphet lagt til på album bakgrunnsbilde",
"audioDevice": "lydenhet",
"zoom": "zoomprosent",
"zoom_description": "angir zoomprosent for applikasjonen"
},
"table": {
"config": {
"label": {
"playCount": "antall avspillinger",
"releaseDate": "utgivelsesdato",
"trackNumber": "spornummer",
"rowIndex": "radindeks",
"dateAdded": "dato lagt til",
"discNumber": "skivenummer",
"lastPlayed": "sist avspilt"
},
"view": {
"table": "tabell",
"card": "kort",
"grid": "rutenett",
"list": "liste",
"poster": "plakat"
},
"general": {
"autoFitColumns": "automatisk kolonnetilpasning",
"displayType": "visningstype",
"followCurrentSong": "følg gjeldende sang"
}
},
"column": {
"releaseYear": "år",
"comment": "kommentar",
"biography": "biografi",
"album": "album",
"albumArtist": "albumartist",
"dateAdded": "dato lagt til",
"discNumber": "skive",
"favorite": "favoritt",
"lastPlayed": "sist avspilt",
"path": "sti",
"playCount": "avspillinger",
"rating": "vurdering",
"releaseDate": "utgivelsesdato",
"title": "tittel",
"trackNumber": "spor"
}
} }
} }
+8 -3
View File
@@ -93,7 +93,9 @@
"albumPeak": "pico do álbum", "albumPeak": "pico do álbum",
"trackGain": "ganho da faixa", "trackGain": "ganho da faixa",
"additionalParticipants": "participantes adicionais", "additionalParticipants": "participantes adicionais",
"tags": "tags" "tags": "tags",
"newVersion": "uma nova versão foi instalada ({{version}})",
"viewReleaseNotes": "ver notas de lançamento"
}, },
"action": { "action": {
"goToPage": "vá para página", "goToPage": "vá para página",
@@ -216,7 +218,9 @@
"crossfadeDuration_description": "define a duração do efeito crossfade", "crossfadeDuration_description": "define a duração do efeito crossfade",
"customCssNotice": "Aviso: apesar de existir alguma higienização (url() e content: não são permitidas), o uso de CSS personalizado ainda pode representar riscos ao alterar a interface.", "customCssNotice": "Aviso: apesar de existir alguma higienização (url() e content: não são permitidas), o uso de CSS personalizado ainda pode representar riscos ao alterar a interface.",
"crossfadeStyle": "estilo do crossfade", "crossfadeStyle": "estilo do crossfade",
"crossfadeStyle_description": "seleciona qual estilo de crossfade usado no player de áudio" "crossfadeStyle_description": "seleciona qual estilo de crossfade usado no player de áudio",
"disableAutomaticUpdates": "desabilitar atualizações automáticas",
"disableLibraryUpdateOnStartup": "desabilitar a verificação de novas versões na inicialização"
}, },
"table": { "table": {
"config": { "config": {
@@ -273,7 +277,8 @@
"nowPlaying": "tocando agora", "nowPlaying": "tocando agora",
"playlists": "$t(entity.playlist_other)", "playlists": "$t(entity.playlist_other)",
"search": "$t(common.search)", "search": "$t(common.search)",
"settings": "$t(common.setting_other)" "settings": "$t(common.setting_other)",
"myLibrary": "minha biblioteca"
}, },
"playlistList": { "playlistList": {
"title": "$t(entity.playlist_other)" "title": "$t(entity.playlist_other)"
+647
View File
@@ -0,0 +1,647 @@
{
"action": {
"addToFavorites": "dodaj na $t(entity.favorite_other)",
"addToPlaylist": "dodaj na $t(entity.playlist_one)",
"clearQueue": "počisti čakalno vrsto",
"createPlaylist": "ustvari $t(entity.playlist_one)",
"deletePlaylist": "izbriši $t(entity.playlist_one)",
"deselectAll": "odizberi vse",
"editPlaylist": "uredi $t(entity.playlist_one)",
"goToPage": "pojdi na stran",
"moveToNext": "pojdi na naslednjo",
"moveToBottom": "pojdi na dno",
"moveToTop": "pojdi na vrh",
"refresh": "$t(common.refresh)",
"removeFromFavorites": "odstrani iz $t(entity.favorite_other)",
"removeFromPlaylist": "odstrani iz seznama predvajanja",
"removeFromQueue": "odstrani iz čakalne vrste",
"setRating": "nastavi oceno",
"toggleSmartPlaylistEditor": "preklopi urejevalnik $t(entity.smartPlaylist)",
"viewPlaylists": "poglej $t(entity.playlist_other)",
"openIn": {
"lastfm": "Odpri v Last.fm",
"musicbrainz": "Odpri v MusicBrainz"
}
},
"common": {
"action_one": "dejanje",
"action_two": "dejanji",
"action_few": "dejanja",
"action_other": "dejanj",
"add": "dodaj",
"additionalParticipants": "dodatni udeleženci",
"newVersion": "nova verzija je bila nameščena ({{version}})",
"viewReleaseNotes": "poglej zapiske o različici",
"albumGain": "ojačitev albuma",
"albumPeak": "vrh albuma",
"areYouSure": "ali si prepričan?",
"ascending": "naraščajoče",
"backward": "nazaj",
"biography": "biografija",
"bitrate": "bitna hitrost",
"bpm": "unm",
"cancel": "prekliči",
"center": "center",
"channel_one": "kanal",
"channel_two": "kanala",
"channel_few": "kanali",
"channel_other": "kanalov",
"clear": "počisti",
"close": "zapri",
"codec": "kodek",
"collapse": "strni",
"comingSoon": "prihaja kmalu …",
"configure": "prilagodi",
"confirm": "potrdi",
"create": "ustvari",
"currentSong": "trenutna $t(entity.track_one)",
"decrease": "zmanjšaj",
"delete": "izbriši",
"descending": "padajoče",
"description": "opis",
"disable": "onemogoči",
"disc": "disk",
"dismiss": "spreglej",
"duration": "trajanje",
"edit": "uredi",
"enable": "omogoči",
"expand": "razširi",
"favorite": "najljubša",
"filter_one": "filter",
"filter_two": "filtra",
"filter_few": "filtri",
"filter_other": "filtrov",
"filters": "filtri",
"forceRestartRequired": "znova zaženi, da potrdiš spremembe ... zapri obvestilo, da znova zaženeš",
"forward": "naprej",
"gap": "reža",
"home": "domov",
"increase": "povišaj",
"limit": "omeji",
"manage": "upravljaj",
"maximize": "maksimiziraj",
"menu": "meni",
"minimize": "pomanjšaj",
"modified": "spremenjeno",
"mbid": "MusicBrainz identifikator (ID)",
"left": "levo",
"no": "ne",
"none": "noben",
"noResultsFromQuery": "poizvedba ni vrnila rezultatov",
"note": "opomba",
"ok": "ok",
"owner": "lastnik",
"path": "pot",
"playerMustBePaused": "predvajalnik mora biti ustavljen",
"preview": "predogled",
"previousSong": "prejšnja $t(entity.track_one)",
"quit": "izhod",
"random": "naključno",
"rating": "ocena",
"refresh": "osveži",
"reload": "ponovno naloži",
"reset": "ponastavi",
"resetToDefault": "ponastavi na privzeto",
"restartRequired": "zahtevan je ponovni zagon",
"right": "desno",
"save": "shrani",
"saveAndReplace": "shrani in zamenjaj",
"saveAs": "shrani kot",
"search": "išči",
"setting": "nastavitev",
"share": "deli",
"size": "velikost",
"sortOrder": "vrstni red",
"tags": "oznake",
"title": "naslov",
"trackNumber": "skladba",
"trackGain": "glasnost skladbe",
"trackPeak": "vrhunec skladbe",
"translation": "prevod",
"unknown": "neznan",
"version": "verzija",
"year": "leto",
"yes": "da",
"name": "ime"
},
"entity": {
"album_one": "album",
"album_two": "albuma",
"album_few": "albumi",
"album_other": "albumov",
"albumArtist_one": "izvajalec albuma",
"albumArtist_two": "izvajalec albumov",
"albumArtist_few": "izvajalec albumov",
"albumArtist_other": "izvajalec albumov",
"albumArtistCount_one": "{{count}} izvajalec albuma",
"albumArtistCount_two": "{{count}} izvajalca albuma",
"albumArtistCount_few": "{{count}} izvajalci albuma",
"albumArtistCount_other": "{{count}} izvajalcev albuma",
"albumWithCount_one": "{{count}} album",
"albumWithCount_two": "{{count}} albuma",
"albumWithCount_few": "{{count}} albumi",
"albumWithCount_other": "{{count}} albumov",
"artist_one": "izvajalec",
"artist_two": "izvajalca",
"artist_few": "izvajalci",
"artist_other": "izvajalcev",
"artistWithCount_one": "{{count}} izvajalec",
"artistWithCount_two": "{{count}} izvajalca",
"artistWithCount_few": "{{count}} izvajalci",
"artistWithCount_other": "{{count}} izvajalcev",
"favorite_one": "priljubljen",
"favorite_two": "priljubljena",
"favorite_few": "priljubljeni",
"favorite_other": "priljubljenih",
"folder_one": "mapa",
"folder_two": "mapi",
"folder_few": "mape",
"folder_other": "map",
"folderWithCount_one": "{{count}} mapa",
"folderWithCount_two": "{{count}} mapi",
"folderWithCount_few": "{{count}} mape",
"folderWithCount_other": "{{count}} map",
"genre_one": "zvrst",
"genre_two": "zvrsti",
"genre_few": "zvrsti",
"genre_other": "zvrsti",
"genreWithCount_one": "{{count}} zvrst",
"genreWithCount_two": "{{count}} zvrsti",
"genreWithCount_few": "{{count}} zvrsti",
"genreWithCount_other": "{{count}} zvrsti",
"playlist_one": "seznam predvajanja",
"playlist_two": "seznama predvajanja",
"playlist_few": "seznami predvajanja",
"playlist_other": "seznamov predvajanja",
"play_one": "{{count}} predvajanje",
"play_two": "{{count}} predvajanji",
"play_few": "{{count}} predvajanja",
"play_other": "{{count}} predvajanj",
"playlistWithCount_one": "{{count}} seznam predvajanja",
"playlistWithCount_two": "{{count}} seznama predvajanja",
"playlistWithCount_few": "{{count}} seznami predvajanja",
"playlistWithCount_other": "{{count}} seznamov predvajanja",
"smartPlaylist": "pametni $t(entity.playlist_one)",
"track_one": "skladba",
"track_two": "skladbi",
"track_few": "skladbe",
"track_other": "skladb",
"song_one": "pesem",
"song_two": "pesmi",
"song_few": "pesmi",
"song_other": "pesmi",
"trackWithCount_one": "{{count}} skladba",
"trackWithCount_two": "{{count}} skladbi",
"trackWithCount_few": "{{count}} skladbe",
"trackWithCount_other": "{{count}} skladb"
},
"error": {
"apiRouteError": "preusmeritev zahteve ni bila mogoča",
"audioDeviceFetchError": "napaka pri poskusu pridobivanja avdio naprav",
"authenticationFailed": "napaka pri avtentikaciji",
"badAlbum": "ta stran je prikazana ker skladba ne pripada nobenemu albumu. skladba se verjetno nahaja na vrhu datotečne strukture direktorija z glasbo. jellyfin razporedi skladbe v skupine samo v primeru, ko se nahajajo v direktoriju.",
"badValue": "neveljavna možnost \"{{value}}\". ta vrednost ne obstaja več",
"credentialsRequired": "zahtevana prijava",
"endpointNotImplementedError": "{{serverType}} ne implementira končne točke {{endpoint}}",
"genericError": "prišlo je do napake",
"invalidServer": "neveljaven strežnik",
"localFontAccessDenied": "dostop do lokalnih pisav je bil zavrnjen",
"loginRateError": "preveč poskusov prijave, prosimo, poskusite čez nekaj sekund",
"mpvRequired": "obvezen MPV",
"networkError": "prišlo je do mrežne napake",
"openError": "datoteke ni mogoče odpreti",
"playbackError": "prišlo je do napake pri poskusu predvajanja skladbe",
"remoteDisableError": "oddaljenega strežnika ni bilo mogoče $t(common.disable)ti",
"remoteEnableError": "oddaljenega strežnika ni bilo mogoče $t(common.enable)ti",
"remotePortError": "pri nastavljanju vrat oddaljenega strežnika je prišlo do napake",
"remotePortWarning": "ponovno zaženite strežnik da aplicirate spremembo strežniških vrat",
"serverNotSelectedError": "izbran ni bil noben strežnik",
"serverRequired": "strežnik zahtevan",
"sessionExpiredError": "vaša seja se je iztekla",
"systemFontError": "napaka pri pridobivanju sistemskih pisav"
},
"filter": {
"album": "$t(entity.album_one)",
"albumArtist": "$t(entity.albumArtist_one)",
"albumCount": "število $t(entity.album_other)",
"artist": "$t(entity.artist_one)",
"biography": "biografija",
"bitrate": "bitna hitrost",
"bpm": "bpm",
"channels": "$t(common.channel_other)",
"comment": "komentar",
"communityRating": "ocena skupnosti",
"criticRating": "ocena kritikov",
"dateAdded": "dodano",
"disc": "disk",
"duration": "trajanje",
"favorited": "priljubljeno",
"fromYear": "od leta",
"genre": "$t(entity.genre_one)",
"id": "identifikator",
"isCompilation": "je kompilacija",
"isFavorited": "je dodan med priljubljene",
"isPublic": "je javno",
"isRated": "je ocenjen",
"isRecentlyPlayed": "je bil nedavno predvajan",
"lastPlayed": "zadnje predvajano",
"mostPlayed": "najpogosteje predvajano",
"name": "ime",
"note": "opomba",
"owner": "$t(common.owner)",
"path": "pot",
"playCount": "število predvajanj",
"random": "naključno",
"rating": "ocena",
"recentlyAdded": "nedavno dodano",
"recentlyPlayed": "nedavno predvajano",
"recentlyUpdated": "nedavno posodobljeno",
"releaseDate": "datum izida",
"releaseYear": "leto izida",
"search": "išči",
"songCount": "število pesmi",
"title": "naslov",
"toYear": "do leta",
"trackNumber": "skladba"
},
"form": {
"addServer": {
"error_savePassword": "pri shranjevanju gesla je prišlo do napake",
"ignoreCors": "ignoriraj cors $t(common.restartRequired)",
"ignoreSsl": "ignoriraj ssl $t(common.restartRequired)",
"input_legacyAuthentication": "omogoči legacy avtentikacijo",
"input_name": "ime strežnika",
"input_password": "geslo",
"input_savePassword": "shrani geslo",
"input_url": "url",
"input_username": "uporabniško ime",
"success": "dodajanje strežnika uspešno",
"title": "dodaj strežnik"
},
"addToPlaylist": {
"input_playlists": "$t(entity.playlist_other)",
"input_skipDuplicates": "preskoči duplikate",
"success": "$t(entity.trackWithCount, {\"count\": {{message}} }) dodan v $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
"title": "dodaj v $t(entity.playlist_one)"
},
"createPlaylist": {
"input_description": "$t(common.description)",
"input_name": "$t(common.name)",
"input_owner": "$t(common.owner)",
"input_public": "javno",
"success": "$t(entity.playlist_one) je bil uspešno ustvarjen",
"title": "ustvari $t(entity.playlist_one)"
},
"deletePlaylist": {
"input_confirm": "vpišite ime $t(entity.playlist_one) za potrditev",
"success": "$t(entity.playlist_one) uspešno izbrisan",
"title": "izbriši $t(entity.playlist_one)"
},
"editPlaylist": {
"publicJellyfinNote": "Jellyfin ne poda informacij o tem, ali gre za javni ali zasebni seznam predvajanja. Če želite, da seznam predvajanja ostane javen, izberite naslednji vnos",
"success": "$t(entity.playlist_one) uspešno posodobljen",
"title": "uredi $t(entity.playlist_one)"
},
"lyricSearch": {
"input_artist": "$t(entity.artist_one)",
"input_name": "$t(common.name)",
"title": "iskanje po besedilu"
},
"queryEditor": {
"title": "urejevalnik poizvedb",
"input_optionMatchAll": "ujemanje vseh",
"input_optionMatchAny": "ujemanje z najmanj enim"
},
"shareItem": {
"allowDownloading": "dovoli prenašanje",
"description": "opis",
"setExpiration": "nastavi datum poteka veljavnosti",
"success": "deli povezavo v odložišču (ali klikni tukaj za odpiranje)",
"expireInvalid": "datum poteka veljavnosti mora biti v prihodnosti",
"createFailed": "deljenje ni uspelo (je deljenje omogočeno?)"
},
"updateServer": {
"success": "strežnik uspešno posodobljen",
"title": "posodobi strežnik"
}
},
"page": {
"albumArtistDetail": {
"about": "O izvajalcu",
"appearsOn": "se pojavi na",
"recentReleases": "zadnje izdaje",
"viewDiscography": "poglej diskografijo",
"relatedArtists": "sorodni $t(entity.artist_other)",
"topSongs": "najboljše skladbe",
"topSongsFrom": "najboljše skladbe iz {{title}}",
"viewAll": "poglej vse",
"viewAllTracks": "poglej vse $t(entity.track_other)"
},
"albumArtistList": {
"title": "$t(entity.albumArtist_other)"
},
"albumDetail": {
"moreFromArtist": "več od $t(entity.artist_one)",
"moreFromGeneric": "več iz {{item}}",
"released": "izdano"
},
"albumList": {
"artistAlbums": "albumi izvajalca {{artist}}",
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)",
"title": "$t(entity.album_other)"
},
"appMenu": {
"collapseSidebar": "skrij stransko vrstico",
"expandSidebar": "razširi stransko vrstico",
"goBack": "nazaj",
"goForward": "naprej",
"manageServers": "urejanje strežnikov",
"openBrowserDevtools": "odpri orodja za razvijalce brskalnika",
"quit": "$t(common.quit)",
"selectServer": "izberi strežnik",
"settings": "$t(common.setting_other)",
"version": "verzija {{version}}"
},
"manageServers": {
"title": "urejanje strežnikov",
"serverDetails": "podrobosti o strežniku",
"url": "URL",
"username": "uporabniško ime",
"editServerDetailsTooltip": "urejanje podrobnosti strežnika",
"removeServer": "odstrani strežnik"
},
"contextMenu": {
"addFavorite": "$t(action.addToFavorites)",
"addLast": "$t(player.addLast)",
"addNext": "$t(player.addNext)",
"addToFavorites": "$t(action.addToFavorites)",
"addToPlaylist": "$t(action.addToPlaylist)",
"createPlaylist": "$t(action.createPlaylist)",
"deletePlaylist": "$t(action.deletePlaylist)",
"deselectAll": "$t(action.deselectAll)",
"download": "prenesi",
"moveToNext": "$t(action.moveToNext)",
"moveToBottom": "$t(action.moveToBottom)",
"moveToTop": "$t(action.moveToTop)",
"numberSelected": "{{count}} izbranih",
"play": "$t(player.play)",
"playSimilarSongs": "$t(player.playSimilarSongs)",
"removeFromFavorites": "$t(action.removeFromFavorites)",
"removeFromPlaylist": "$t(action.removeFromPlaylist)",
"removeFromQueue": "$t(action.removeFromQueue)",
"setRating": "$t(action.setRating)",
"playShuffled": "$t(player.shuffle)",
"shareItem": "deli",
"showDetails": "pridobi informacije"
},
"fullscreenPlayer": {
"config": {
"dynamicBackground": "dinamično ozadje",
"dynamicImageBlur": "velikost zameglitve slike",
"dynamicIsImage": "omogoči sliko v ozadju",
"followCurrentLyric": "sledi besedilu",
"lyricAlignment": "poravnava besedila",
"lyricOffset": "zamik besedila (ms)",
"lyricGap": "razmik besedila",
"lyricSize": "velikost besedila",
"opacity": "prosojnost",
"showLyricMatch": "prikaži ujemanje besedila",
"showLyricProvider": "pokaži ponudnika besedila",
"synchronized": "sinhronizirano",
"unsynchronized": "nesinhronizirano",
"useImageAspectRatio": "uporabi razmerje stranic slike"
},
"lyrics": "besedilo",
"related": "sorodno",
"upNext": "sledi",
"visualizer": "vizualizator",
"noLyrics": "ni bilo najdenih besedil"
},
"genreList": {
"showAlbums": "prikaži $t(entity.genre_one) $t(entity.album_other)",
"showTracks": "prikaži $t(entity.genre_one) $t(entity.track_other)",
"title": "$t(entity.genre_other)"
},
"globalSearch": {
"commands": {
"goToPage": "pojdi na stran",
"searchFor": "išči {{query}}",
"serverCommands": "strežniški ukazi"
},
"title": "ukazi"
},
"home": {
"explore": "razišči knjižnico",
"mostPlayed": "najpogosteje predvajano",
"newlyAdded": "zadnje dodane izdaje",
"recentlyPlayed": "nedavno predvajano",
"title": "$t(common.home)"
},
"itemDetail": {
"copyPath": "kopiraj v odložišče",
"copiedPath": "kopiranje poti uspešno",
"openFile": "prikaži skladbo v upravitelju datotek"
},
"playlist": {
"reorder": "preurejanje je omogočeno samo pri razvrščanju po identifikatorju"
},
"playlistList": {
"title": "$t(entity.playlist_other)"
},
"setting": {
"advanced": "napredno",
"generalTab": "splošno",
"hotkeysTab": "blžnjice",
"playbackTab": "predvajanje",
"windowTab": "okno"
},
"sidebar": {
"albumArtists": "$t(entity.albumArtist_other)",
"albums": "$t(entity.album_other)",
"artists": "$t(entity.artist_other)",
"folders": "$t(entity.folder_other)",
"genres": "$t(entity.genre_other)",
"home": "$t(common.home)",
"myLibrary": "moja knjižnica",
"nowPlaying": "trenutno se predvaja",
"playlists": "$t(entity.playlist_other)",
"search": "$t(common.search)",
"settings": "$t(common.setting_other)",
"shared": "deljen $t(entity.playlist_other)",
"tracks": "$t(entity.track_other)"
},
"trackList": {
"artistTracks": "skladbe po {{artist}}",
"genreTracks": "\"{{genre}}\" $t(entity.track_other)",
"title": "$t(entity.track_other)"
}
},
"player": {
"addLast": "dodaj zadnje",
"addNext": "dodaj naslednje",
"favorite": "dodaj med priljubljene",
"mute": "utišaj",
"muted": "utišano",
"next": "naslednje",
"play": "predvajaj",
"playbackFetchCancel": "akcija traja dlje časa... zaprite obvestilo za preklic",
"playbackFetchInProgress": "nalaganje pesmi…",
"playbackFetchNoResults": "nobena pesem ni bila najdena",
"playbackSpeed": "hitrost predvajanja",
"playRandom": "predvajaj naključno",
"playSimilarSongs": "predvajaj sorodne pesmi",
"previous": "prejšnje",
"queue_clear": "počisti čakalno vrsto",
"queue_moveToBottom": "premakni izbrano na vrh",
"queue_moveToTop": "premakni izbrano na dno",
"queue_remove": "odstrani izbrano",
"repeat": "ponovi",
"repeat_all": "ponovi vse",
"repeat_off": "ne ponavljaj",
"shuffle": "predvajaj v naključnem vrstnem redu",
"shuffle_off": "prevajanje v naključnem vrstnem redu izključeno",
"skip": "preskoči",
"skip_back": "preskoči nazaj",
"skip_forward": "preskoči naprej",
"stop": "ustavi",
"toggleFullscreenPlayer": "preklopi predvajalnik v celozaslonski način",
"unfavorite": "odstrani iz priljubljenih",
"pause": "premor",
"viewQueue": "poglej čakalno vrsto"
},
"setting": {
"accentColor": "barva poudarka",
"accentColor_description": "nastavi barva poudarka aplikacije",
"albumBackground": "slika ozadja albuma",
"albumBackground_description": "doda sliko ozadja za strani albuma",
"albumBackgroundBlur": "velikost zameglitve slike ozadja albuma",
"albumBackgroundBlur_description": "spremeni moč zameglitve slike ozadja albuma",
"applicationHotkeys": "bližnjične tipke aplikacije",
"applicationHotkeys_description": "konfigurira bližnjične tipke aplikacije. obkljukajte da nastavite globalne bližnjico na tipkovnici (samo na namizju)",
"artistConfiguration": "konfiguracija strani izvajalca albuma",
"artistConfiguration_description": "konfiguriranje vsebine in vrstnega reda prikaza na strani izvajalca albuma",
"audioDevice": "avdio naprava",
"audioDevice_description": "izberite avdio napravo za predvajanje (samo v spletnem predvajalniku)",
"audioExclusiveMode": "avdio način",
"audioExclusiveMode_description": "omogoči način ekskluzivnega predvajanja. V tem načinu je sistem običajno zaklenjen in samo mpv lahko oddaja zvok",
"audioPlayer": "avdio predvajalnik",
"audioPlayer_description": "izberite avdio predvajalnik za predvajanje",
"buttonSize": "velikost gumbov vrstice predvajalnika",
"buttonSize_description": "velikost gumbov v vrstici predvajalnika",
"clearCache": "izbriši začasni pomnilnik",
"clearCache_description": "poleg brisanja feishinovega začasnega pomnilnika bo izbrisan tudi začasni pomnilnik brskalnika. nastavitve in prijavni podatki strežnikov se ohranijo",
"clearQueryCache": "počisti feishinov začasni pomnilnik",
"clearQueryCache_description": "osveži sezname predvajanja, metapodatke in ponastavi shranjena besedila. nastavitve, prijavni podatki za strežnike in slike se ohranijo",
"clearCacheSuccess": "začasni pomnilnik uspešno izbrisan",
"contextMenu": "konfiguracija kontekstnega menija (desni klik)",
"contextMenu_description": "omogoči skrivanje vrstic v meniju, prikazanem ob desnem kliku. odznačeni predmeti bodo skriti",
"crossfadeDuration": "trajanje prehoda",
"crossfadeDuration_description": "nastavi čas trajanja prehoda med pesmimi",
"crossfadeStyle": "tip prehoda",
"crossfadeStyle_description": "izbira tipa efekta prehoda",
"customCssEnable": "omogoči css po meri",
"customCssEnable_description": "omogoča urejanje css-ja po meri.",
"customCssNotice": "Opozorilo: kljub določenim varnostnim ukrepom (prepoved url() in content:) lahko uporaba CSS po meri s spreminjanjem vmesnika še vedno predstavlja tveganje.",
"customCss": "css po meri",
"customCss_description": "vsebina css po meri. Opomba: vsebina in oddaljeni url-ji so prepovedane lastnosti. Spodaj je prikazan predogled vaše vsebine. Dodatna polja, ki jih niste nastavili, so prisotna zaradi prečiščevanja.",
"customFontPath": "pot za pisavo po meri",
"customFontPath_description": "nastavi pot do pisave po meri",
"disableAutomaticUpdates": "onemogoči samodejne posodobitve",
"disableLibraryUpdateOnStartup": "onemogoči prevejranje novih verzij ob zagonu",
"discordApplicationId": "{{discord}} identifikator aplikacije",
"discordApplicationId_description": "identifikator aplikacije za {{discord}} bogato prezenco (privzeto {{defaultId}})",
"discordPausedStatus": "prikaži bogato prezenco med ustavljenim predvajanjem",
"discordPausedStatus_description": "ko je nastavitev omogočena, se bo status prikazal tudi ko je predvajanje začasno zaustavljeno",
"discordIdleStatus": "prikaže stanje mirovanja v bogati prezenci",
"discordIdleStatus_description": "ko je nastavitev omogočena, se bo status posodabljal ko predvajalnik miruje",
"discordListening": "prikaži status poslušanja",
"discordListening_description": "prikaži status poslušanja namesto predvajanja",
"discordRichPresence": "{{discord}} bogata prezenca",
"discordRichPresence_description": "omogoči prikaz statusa predvajanja v {{discord}} bogati prezenci. Oznake slike so: {{icon}}, {{playing}} in {{paused}}",
"discordServeImage": "pošiljaj {{discord}} u slike iz strežnika",
"discordServeImage_description": "deli naslovne slike za {{discord}} bogato prisotnost iz samega strežnika, na voljo samo za jellyfin in navidrome",
"discordUpdateInterval": "interval posodabljanja {{discord}} bogate prezence",
"discordUpdateInterval_description": "čas v sekundah med posameznimi posodobitvami (najmanj 15 sekund)",
"doubleClickBehavior": "dvojni klik doda vse iskane skladbe v čakalno vrsto",
"doubleClickBehavior_description": "če je nastavitev vklopljena se bodo v čakalno vrsto dodale vse skladbe, ki ustrezajo iskanju. v nasprotnem primeru se v čakalno vrsto doda samo izbrana skladba",
"enableRemote": "omogoči oddaljeno upravljanje strežnika",
"enableRemote_description": "omogoči oddaljeno nadzorovanje strežnika in s tem dovoli drugim napravam da upravljajo aplikacijo",
"externalLinks": "prikaži zunanje povezave",
"externalLinks_description": "omogoči prikaz zunanjih povezav (Last.fm, MusicBrainz) na straneh albumov,izvajalcev",
"exitToTray": "minimiziraj",
"exitToTray_description": "ob izhodu se aplikacija minimizira v opravilno vrstico",
"floatingQueueArea": "prikaži območje plavajoče čakalne vrste",
"floatingQueueArea_description": "na desni strani zaslona prikažite ikono za ogled čakalne vrste predvajanja",
"followLyric": "sledenje besedilu",
"followLyric_description": "pomaknite besedilo pesmi do trenutnega položaja predvajanja",
"preferLocalLyrics": "prioritiziraj lokalna besedila",
"preferLocalLyrics_description": "prioritiziraj lokalna besedila pred oddaljenimi, kadar so na voljo",
"font": "pisava",
"font_description": "nastavi pisavo, ki jo bo aplikacija uporabljala",
"fontType": "tip pisave",
"fontType_description": "vgrajena pisava izbere eno od pisav, ki jih ponuja Feishin. sistemska pisava vam omogoča, da izberete katero koli pisavo, ki jo ponuja vaš operacijski sistem. po meri lahko izberete svojo pisavo",
"fontType_optionBuiltIn": "vgrajena pisava",
"fontType_optionCustom": "pisava po meri",
"fontType_optionSystem": "sistemska pisava",
"gaplessAudio": "neprekinjen avdio",
"gaplessAudio_description": "nastavi neprekinjen avdio za mpv",
"gaplessAudio_optionWeak": "šibko (priporočeno)",
"genreBehavior": "privzeto vedenje strani z zvrstmi",
"genreBehavior_description": "določa, ali se ob kliku na zvrst privzeto odpre seznam skladb ali albumov",
"globalMediaHotkeys": "globalne bližnjične tipke za vsebino",
"globalMediaHotkeys_description": "omogočite ali onemogočite uporabo bližnjic za sistemske medije za nadzor predvajanja",
"homeConfiguration": "konfiguracija domače strani",
"homeConfiguration_description": "konfigurirajte, kateri elementi so prikazani na domači strani in v kakšnem vrstnem redu",
"homeFeature": "tekoči trak na domači strani",
"homeFeature_description": "nadzoruje, ali naj se na domači strani prikaže velik tekoči trak",
"hotkey_browserBack": "nazaj (brskalnik)",
"hotkey_browserForward": "naprej (brskalnik)",
"hotkey_favoriteCurrentSong": "dodaj $t(common.currentSong) med priljubljene",
"hotkey_favoritePreviousSong": "dodaj $t(common.previousSong) med priljubljene",
"hotkey_globalSearch": "globalno iskanje",
"hotkey_localSearch": "iskanje na strani",
"hotkey_playbackNext": "naslednja skladba",
"hotkey_playbackPause": "pavza",
"hotkey_playbackPlay": "predvajaj",
"hotkey_playbackPlayPause": "predvajaj / pavza",
"hotkey_playbackPrevious": "prejšnja skladba",
"hotkey_playbackStop": "ustavi",
"hotkey_rate0": "počisti oceno",
"hotkey_rate1": "oceni z 1 zvezdico",
"hotkey_rate2": "oceni z 2 zvezdicama",
"hotkey_rate3": "oceni s 3 zvezdicami",
"hotkey_rate4": "oceni s 4 zvezdicami",
"hotkey_rate5": "oceni s 5 zvezdicami",
"hotkey_skipBackward": "preskoči nazaj",
"hotkey_skipForward": "preskoči naprej",
"hotkey_toggleCurrentSongFavorite": "dodaj/odstrani $t(common.currentSong) iz seznama priljubljenih",
"hotkey_toggleFullScreenPlayer": "preklopi predvajalnik na celozaslonski način",
"hotkey_togglePreviousSongFavorite": "dodaj/odstrani $t(common.previousSong) iz seznama priljubljenih",
"hotkey_toggleQueue": "preklopi čakalno vrsto",
"hotkey_toggleRepeat": "preklopi ponovitve",
"hotkey_toggleShuffle": "preklopi naključni vrstni red predvajanja",
"hotkey_unfavoriteCurrentSong": "odstrani $t(common.currentSong) iz seznama priljubljenih",
"hotkey_unfavoritePreviousSong": "odstrani $t(common.previousSong) iz seznama priljubljenih",
"hotkey_volumeDown": "znižaj glasnost",
"hotkey_volumeMute": "utišaj",
"hotkey_volumeUp": "povišaj glasnost",
"hotkey_zoomIn": "povečaj",
"hotkey_zoomOut": "pomanjšaj",
"imageAspectRatio": "uporabi razmerje stranic izvorne naslovnice",
"imageAspectRatio_description": "če je omogočeno, bo naslovnica prikazana z izvornim razmerjem stranic. za slike, ki niso 1:1, bo preostali prostor prazen",
"language": "jezik",
"language_description": "nastavi jezik aplikacije ($t(common.restartRequired))",
"lastfm": "prikaži last.fm povezave",
"lastfm_description": "prikaži povezave do last.fm na straneh izvajalcev/albumov",
"lastfmApiKey": "API ključ {{lastfm}}",
"lastfmApiKey_description": "API ključ za {{lastfm}}. potreben za naslovnico albuma",
"lyricFetch": "pridobi besedila iz interneta",
"lyricFetch_description": "pridobivanje besedil iz različnih internetnih virov",
"lyricFetchProvider": "ponudniki za pridobivanje besedil",
"lyricFetchProvider_description": "izberite ponudnike, od katerih želite pridobiti besedila. vrstni red ponudnikov je vrstni red, v katerem bodo poizvedovani",
"lyricOffset": "zamik besedila (ms)",
"lyricOffset_description": "zamakni besedilo za določeno število milisekund",
"minimizeToTray": "minimiziraj v sistemsko vrstico",
"minimizeToTray_description": "minimizirajte aplikacijo v sistemsko vrstico"
}
}
+8 -3
View File
@@ -113,7 +113,9 @@
"additionalParticipants": "其他参与者", "additionalParticipants": "其他参与者",
"tags": "标签", "tags": "标签",
"viewReleaseNotes": "查看发行说明", "viewReleaseNotes": "查看发行说明",
"newVersion": "已安装新版本 ({{version}})" "newVersion": "已安装新版本 ({{version}})",
"bitDepth": "位深度",
"sampleRate": "采样率"
}, },
"entity": { "entity": {
"albumArtist_other": "专辑艺术家", "albumArtist_other": "专辑艺术家",
@@ -407,7 +409,9 @@
"discordPausedStatus": "暂停时显示rich presence", "discordPausedStatus": "暂停时显示rich presence",
"discordPausedStatus_description": "启用后将在播放器暂停时显示状态", "discordPausedStatus_description": "启用后将在播放器暂停时显示状态",
"preservePitch": "保持音高", "preservePitch": "保持音高",
"preservePitch_description": "在调整播放速度时保持音高" "preservePitch_description": "在调整播放速度时保持音高",
"notify": "启用歌曲通知",
"notify_description": "更改当前歌曲时显示通知"
}, },
"error": { "error": {
"remotePortWarning": "重启服务器使新端口生效", "remotePortWarning": "重启服务器使新端口生效",
@@ -432,7 +436,8 @@
"badAlbum": "您看到此页面是因为这首歌不是专辑的一部分。如果您的音乐文件夹顶层有一首歌曲,您很可能会遇到此问题。jellyfin 仅对位于文件夹中的曲目进行分组。", "badAlbum": "您看到此页面是因为这首歌不是专辑的一部分。如果您的音乐文件夹顶层有一首歌曲,您很可能会遇到此问题。jellyfin 仅对位于文件夹中的曲目进行分组。",
"networkError": "发生网络错误", "networkError": "发生网络错误",
"openError": "无法打开文件", "openError": "无法打开文件",
"badValue": "无效的选项 \"{{value}}\". 此值不再存在" "badValue": "无效的选项 \"{{value}}\". 此值不再存在",
"notificationDenied": "通知权限被拒绝。此设置无效"
}, },
"filter": { "filter": {
"mostPlayed": "最多播放过", "mostPlayed": "最多播放过",
+13 -4
View File
@@ -5,16 +5,20 @@ const FEISHIN_DISCORD_APPLICATION_ID = '1165957668758900787';
let client: Client | null = null; let client: Client | null = null;
const createClient = (clientId?: string) => { const createClient = async (clientId?: string) => {
client = new Client({ client = new Client({
clientId: clientId || FEISHIN_DISCORD_APPLICATION_ID, clientId: clientId || FEISHIN_DISCORD_APPLICATION_ID,
}); });
client.login(); await client.login();
return client; return client;
}; };
const isConnected = () => {
return client?.isConnected;
};
const setActivity = (activity: SetActivity) => { const setActivity = (activity: SetActivity) => {
if (client) { if (client) {
client.user?.setActivity({ client.user?.setActivity({
@@ -35,8 +39,12 @@ const quit = () => {
} }
}; };
ipcMain.handle('discord-rpc-initialize', (_event, clientId?: string) => { ipcMain.handle('discord-rpc-initialize', async (_event, clientId?: string) => {
createClient(clientId); await createClient(clientId);
});
ipcMain.handle('discord-rpc-is-connected', () => {
return isConnected();
}); });
ipcMain.handle('discord-rpc-set-activity', (_event, activity: SetActivity) => { ipcMain.handle('discord-rpc-set-activity', (_event, activity: SetActivity) => {
@@ -58,6 +66,7 @@ ipcMain.handle('discord-rpc-quit', () => {
export const discordRpc = { export const discordRpc = {
clearActivity, clearActivity,
createClient, createClient,
isConnected,
quit, quit,
setActivity, setActivity,
}; };
+8 -6
View File
@@ -421,9 +421,6 @@ async function createWindow(first = true): Promise<void> {
store.set('fullscreen', mainWindow?.isFullScreen()); store.set('fullscreen', mainWindow?.isFullScreen());
if (!exitFromTray && store.get('window_exit_to_tray')) { if (!exitFromTray && store.get('window_exit_to_tray')) {
if (isMacOS() && !forceQuit) {
exitFromTray = true;
}
event.preventDefault(); event.preventDefault();
mainWindow?.hide(); mainWindow?.hide();
} }
@@ -432,8 +429,6 @@ async function createWindow(first = true): Promise<void> {
event.preventDefault(); event.preventDefault();
saved = true; saved = true;
getMainWindow()?.webContents.send('renderer-save-queue');
ipcMain.once('player-save-queue', async (_event, data: Record<string, any>) => { ipcMain.once('player-save-queue', async (_event, data: Record<string, any>) => {
const queueLocation = join(app.getPath('userData'), 'queue'); const queueLocation = join(app.getPath('userData'), 'queue');
const serialized = JSON.stringify(data); const serialized = JSON.stringify(data);
@@ -457,12 +452,19 @@ async function createWindow(first = true): Promise<void> {
} catch (error) { } catch (error) {
console.error('error saving queue state: ', error); console.error('error saving queue state: ', error);
} finally { } finally {
mainWindow?.close(); if (!isMacOS()) {
mainWindow?.close();
}
if (forceQuit) { if (forceQuit) {
app.exit(); app.exit();
} }
} }
}); });
getMainWindow()?.webContents.send('renderer-save-queue');
} else {
if (forceQuit) {
app.exit();
}
} }
}); });
+6
View File
@@ -6,6 +6,11 @@ const initialize = (clientId: string) => {
return client; return client;
}; };
const isConnected = () => {
const isConnected = ipcRenderer.invoke('discord-rpc-is-connected');
return isConnected;
};
const clearActivity = () => { const clearActivity = () => {
ipcRenderer.invoke('discord-rpc-clear-activity'); ipcRenderer.invoke('discord-rpc-clear-activity');
}; };
@@ -21,6 +26,7 @@ const quit = () => {
export const discordRpc = { export const discordRpc = {
clearActivity, clearActivity,
initialize, initialize,
isConnected,
quit, quit,
setActivity, setActivity,
}; };
+1 -4
View File
@@ -22,10 +22,7 @@ export const App = () => {
const { mode, theme } = useAppTheme(isDark ? AppTheme.DEFAULT_DARK : AppTheme.DEFAULT_LIGHT); const { mode, theme } = useAppTheme(isDark ? AppTheme.DEFAULT_DARK : AppTheme.DEFAULT_LIGHT);
return ( return (
<MantineProvider <MantineProvider defaultColorScheme={mode} theme={theme}>
defaultColorScheme={mode}
theme={theme}
>
<Shell /> <Shell />
</MantineProvider> </MantineProvider>
); );
+1 -11
View File
@@ -18,17 +18,7 @@ export const ThemeButton = () => {
}} }}
variant="default" variant="default"
> >
{isDark ? ( {isDark ? <Icon icon="themeLight" size={30} /> : <Icon icon="themeDark" size={30} />}
<Icon
icon="themeLight"
size={30}
/>
) : (
<Icon
icon="themeDark"
size={30}
/>
)}
</ActionIcon> </ActionIcon>
); );
}; };
+7 -30
View File
@@ -32,17 +32,9 @@ export const RemoteContainer = () => {
const debouncedSetRating = debounce(setRating, 400); const debouncedSetRating = debounce(setRating, 400);
return ( return (
<Stack <Stack gap="md" h="100dvh" w="100%">
gap="md"
h="100dvh"
w="100%"
>
{showImage && ( {showImage && (
<Flex <Flex align="center" justify="center" w="100%">
align="center"
justify="center"
w="100%"
>
<PlayerImage src={song?.imageUrl} /> <PlayerImage src={song?.imageUrl} />
</Flex> </Flex>
)} )}
@@ -87,10 +79,7 @@ export const RemoteContainer = () => {
</Group> </Group>
</Stack> </Stack>
)} )}
<Group <Group gap={0} grow>
gap={0}
grow
>
<ActionIcon <ActionIcon
disabled={!id} disabled={!id}
icon="favorite" icon="favorite"
@@ -109,10 +98,7 @@ export const RemoteContainer = () => {
/> />
{(song?.serverType === 'navidrome' || song?.serverType === 'subsonic') && ( {(song?.serverType === 'navidrome' || song?.serverType === 'subsonic') && (
<div style={{ margin: 'auto' }}> <div style={{ margin: 'auto' }}>
<Tooltip <Tooltip label="Double click to clear" openDelay={1000}>
label="Double click to clear"
openDelay={1000}
>
<Rating <Rating
onChange={debouncedSetRating} onChange={debouncedSetRating}
onDoubleClick={() => debouncedSetRating(0)} onDoubleClick={() => debouncedSetRating(0)}
@@ -123,10 +109,7 @@ export const RemoteContainer = () => {
</div> </div>
)} )}
</Group> </Group>
<Group <Group gap="xs" grow>
gap="xs"
grow
>
<ActionIcon <ActionIcon
disabled={!id} disabled={!id}
icon="mediaPrevious" icon="mediaPrevious"
@@ -174,10 +157,7 @@ export const RemoteContainer = () => {
variant="default" variant="default"
/> />
</Group> </Group>
<Group <Group gap="xs" grow>
gap="xs"
grow
>
<ActionIcon <ActionIcon
icon="mediaShuffle" icon="mediaShuffle"
iconProps={{ iconProps={{
@@ -232,10 +212,7 @@ export const RemoteContainer = () => {
max={100} max={100}
onChangeEnd={(e) => send({ event: 'volume', volume: e })} onChangeEnd={(e) => send({ event: 'volume', volume: e })}
rightLabel={ rightLabel={
<Text <Text fw={600} size="xs">
fw={600}
size="xs"
>
{volume ?? 0} {volume ?? 0}
</Text> </Text>
} }
+5 -24
View File
@@ -13,16 +13,9 @@ export const Shell = () => {
const connected = useConnected(); const connected = useConnected();
return ( return (
<AppShell <AppShell h="100vh" padding="md" w="100vw">
h="100vh"
padding="md"
w="100vw"
>
<AppShell.Header style={{ background: 'var(--theme-colors-surface)' }}> <AppShell.Header style={{ background: 'var(--theme-colors-surface)' }}>
<Grid <Grid px="md" py="sm">
px="md"
py="sm"
>
<Grid.Col span={4}> <Grid.Col span={4}>
<Flex <Flex
align="center" align="center"
@@ -33,20 +26,11 @@ export const Shell = () => {
justifySelf: 'flex-start', justifySelf: 'flex-start',
}} }}
> >
<Image <Image fit="contain" height={32} src="/favicon.ico" width={32} />
fit="contain"
height={32}
src="/favicon.ico"
width={32}
/>
</Flex> </Flex>
</Grid.Col> </Grid.Col>
<Grid.Col span={8}> <Grid.Col span={8}>
<Group <Group gap="sm" justify="flex-end" wrap="nowrap">
gap="sm"
justify="flex-end"
wrap="nowrap"
>
<ReconnectButton /> <ReconnectButton />
<ImageButton /> <ImageButton />
<ThemeButton /> <ThemeButton />
@@ -58,10 +42,7 @@ export const Shell = () => {
{connected ? ( {connected ? (
<RemoteContainer /> <RemoteContainer />
) : ( ) : (
<Center <Center h="100vh" w="100vw">
h="100vh"
w="100vw"
>
<Spinner /> <Spinner />
</Center> </Center>
)} )}
+1 -4
View File
@@ -61,10 +61,7 @@ export const WrappedSlider = ({ leftLabel, rightLabel, value, ...props }: Wrappe
const [seek, setSeek] = useState(0); const [seek, setSeek] = useState(0);
return ( return (
<Group <Group align="center" wrap="nowrap">
align="center"
wrap="nowrap"
>
{leftLabel && <Text size="sm">{leftLabel}</Text>} {leftLabel && <Text size="sm">{leftLabel}</Text>}
<PlayerbarSlider <PlayerbarSlider
{...props} {...props}
@@ -290,19 +290,32 @@ export const JellyfinController: ControllerEndpoint = {
const yearsFilter = yearsGroup.length ? yearsGroup.join(',') : undefined; const yearsFilter = yearsGroup.length ? yearsGroup.join(',') : undefined;
let artistQuery:
| Omit<z.infer<typeof jfType._parameters.albumList>, 'IncludeItemTypes'>
| undefined;
if (query.artistIds) {
// Based mostly off of observation, this is the behavior I've seen:
// ContributingArtistIds is the _closest_ to where the album is a compilation and the artist is involved
// AlbumArtistIds is where the artist is an album artist
// ArtistIds is all credits
if (query.compilation) {
artistQuery = {
ContributingArtistIds: formatCommaDelimitedString(query.artistIds),
};
} else if (query.compilation === false) {
artistQuery = { AlbumArtistIds: formatCommaDelimitedString(query.artistIds) };
} else {
artistQuery = { ArtistIds: formatCommaDelimitedString(query.artistIds) };
}
}
const res = await jfApiClient(apiClientProps).getAlbumList({ const res = await jfApiClient(apiClientProps).getAlbumList({
params: { params: {
userId: apiClientProps.server?.userId, userId: apiClientProps.server?.userId,
}, },
query: { query: {
...(!query.compilation && ...artistQuery,
query.artistIds && {
AlbumArtistIds: formatCommaDelimitedString(query.artistIds),
}),
...(query.compilation &&
query.artistIds && {
ContributingArtistIds: query.artistIds[0],
}),
Fields: 'People, Tags', Fields: 'People, Tags',
GenreIds: query.genres ? query.genres.join(',') : undefined, GenreIds: query.genres ? query.genres.join(',') : undefined,
IncludeItemTypes: 'MusicAlbum', IncludeItemTypes: 'MusicAlbum',
+13 -2
View File
@@ -349,7 +349,14 @@ axiosClient.interceptors.response.use(
.catch((newError: any) => { .catch((newError: any) => {
if (newError !== TIMEOUT_ERROR) { if (newError !== TIMEOUT_ERROR) {
console.error('Error when trying to reauthenticate: ', newError); console.error('Error when trying to reauthenticate: ', newError);
limitedFail(currentServer);
if (isAxiosError(newError) && newError.code === 'ERR_NETWORK') {
console.log(
'Network error during reauthentication - preserving credentials',
);
} else {
limitedFail(currentServer);
}
} }
// make sure to pass the error so axios will error later on // make sure to pass the error so axios will error later on
@@ -360,7 +367,11 @@ axiosClient.interceptors.response.use(
}); });
} }
limitedFail(currentServer); if (isAxiosError(error) && error.code === 'ERR_NETWORK') {
console.log('Network error during authentication - preserving credentials');
} else {
limitedFail(currentServer);
}
} }
return Promise.reject(error); return Promise.reject(error);
@@ -251,6 +251,9 @@ axiosClient.interceptors.response.use(
message: data['subsonic-response'].error.message, message: data['subsonic-response'].error.message,
title: i18n.t('error.genericError', { postProcess: 'sentenceCase' }) as string, title: i18n.t('error.genericError', { postProcess: 'sentenceCase' }) as string,
}); });
// Since we do status === 200, override this value with the error code
response.status = data['subsonic-response'].error.code;
} }
} }
@@ -1,12 +1,19 @@
import type { ServerInferResponses } from '@ts-rest/core';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import filter from 'lodash/filter'; import filter from 'lodash/filter';
import orderBy from 'lodash/orderBy'; import orderBy from 'lodash/orderBy';
import md5 from 'md5'; import md5 from 'md5';
import { z } from 'zod';
import { 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 { ssNormalize } from '/@/shared/api/subsonic/subsonic-normalize'; import { ssNormalize } from '/@/shared/api/subsonic/subsonic-normalize';
import { AlbumListSortType, SubsonicExtensions } from '/@/shared/api/subsonic/subsonic-types'; import {
AlbumListSortType,
ssType,
SubsonicExtensions,
} from '/@/shared/api/subsonic/subsonic-types';
import { import {
AlbumListSort, AlbumListSort,
ControllerEndpoint, ControllerEndpoint,
@@ -39,6 +46,10 @@ const ALBUM_LIST_SORT_MAPPING: Record<AlbumListSort, AlbumListSortType | undefin
[AlbumListSort.YEAR]: AlbumListSortType.BY_YEAR, [AlbumListSort.YEAR]: AlbumListSortType.BY_YEAR,
}; };
const MAX_SUBSONIC_ITEMS = 500;
// A trick to skip ahead 10x
const SUBSONIC_FAST_BATCH_SIZE = MAX_SUBSONIC_ITEMS * 10;
export const SubsonicController: ControllerEndpoint = { export const SubsonicController: ControllerEndpoint = {
addToPlaylist: async ({ apiClientProps, body, query }) => { addToPlaylist: async ({ apiClientProps, body, query }) => {
const res = await ssApiClient(apiClientProps).updatePlaylist({ const res = await ssApiClient(apiClientProps).updatePlaylist({
@@ -83,7 +94,7 @@ export const SubsonicController: ControllerEndpoint = {
}; };
} }
await ssApiClient({ server: null, url: cleanServerUrl }).authenticate({ const resp = await ssApiClient({ server: null, url: cleanServerUrl }).authenticate({
query: { query: {
c: 'Feishin', c: 'Feishin',
f: 'json', f: 'json',
@@ -92,6 +103,10 @@ export const SubsonicController: ControllerEndpoint = {
}, },
}); });
if (resp.status !== 200) {
throw new Error('Failed to log in');
}
return { return {
credential, credential,
userId: null, userId: null,
@@ -262,7 +277,7 @@ export const SubsonicController: ControllerEndpoint = {
albumOffset: query.startIndex, albumOffset: query.startIndex,
artistCount: 0, artistCount: 0,
artistOffset: 0, artistOffset: 0,
query: query.searchTerm || '""', query: query.searchTerm || '',
songCount: 0, songCount: 0,
songOffset: 0, songOffset: 0,
}, },
@@ -287,7 +302,7 @@ export const SubsonicController: ControllerEndpoint = {
let type = ALBUM_LIST_SORT_MAPPING[query.sortBy] ?? AlbumListSortType.ALPHABETICAL_BY_NAME; let type = ALBUM_LIST_SORT_MAPPING[query.sortBy] ?? AlbumListSortType.ALPHABETICAL_BY_NAME;
if (query.artistIds) { if (query.artistIds) {
const promises: any[] = []; const promises: Promise<ServerInferResponses<typeof contract.getArtist>>[] = [];
for (const artistId of query.artistIds) { for (const artistId of query.artistIds) {
promises.push( promises.push(
@@ -309,8 +324,10 @@ export const SubsonicController: ControllerEndpoint = {
return artist.body.artist.album ?? []; return artist.body.artist.album ?? [];
}); });
const items = albums.map((album) => ssNormalize.album(album, apiClientProps.server));
return { return {
items: albums.map((album) => ssNormalize.album(album, apiClientProps.server)), items: sortAlbumList(items, query.sortBy, query.sortOrder),
startIndex: 0, startIndex: 0,
totalRecordCount: albums.length, totalRecordCount: albums.length,
}; };
@@ -409,11 +426,11 @@ export const SubsonicController: ControllerEndpoint = {
while (fetchNextPage) { while (fetchNextPage) {
const res = await ssApiClient(apiClientProps).search3({ const res = await ssApiClient(apiClientProps).search3({
query: { query: {
albumCount: 500, albumCount: MAX_SUBSONIC_ITEMS,
albumOffset: startIndex, albumOffset: startIndex,
artistCount: 0, artistCount: 0,
artistOffset: 0, artistOffset: 0,
query: query.searchTerm || '""', query: query.searchTerm || '',
songCount: 0, songCount: 0,
songOffset: 0, songOffset: 0,
}, },
@@ -428,13 +445,39 @@ export const SubsonicController: ControllerEndpoint = {
totalRecordCount += albumCount; totalRecordCount += albumCount;
startIndex += albumCount; startIndex += albumCount;
// The max limit size for Subsonic is 500 fetchNextPage = albumCount === MAX_SUBSONIC_ITEMS;
fetchNextPage = albumCount === 500;
} }
return totalRecordCount; return totalRecordCount;
} }
if (query.artistIds) {
const promises: Promise<ServerInferResponses<typeof contract.getArtist>>[] = [];
for (const artistId of query.artistIds) {
promises.push(
ssApiClient(apiClientProps).getArtist({
query: {
id: artistId,
},
}),
);
}
const artistResult = await Promise.all(promises);
const albums = artistResult.reduce((total: number, artist) => {
if (artist.status !== 200) {
return 0;
}
const length = artist.body.artist.album?.length ?? 0;
return length + total;
}, 0);
return albums;
}
if (query.favorite) { if (query.favorite) {
const res = await ssApiClient(apiClientProps).getStarred({ const res = await ssApiClient(apiClientProps).getStarred({
query: { query: {
@@ -486,7 +529,7 @@ export const SubsonicController: ControllerEndpoint = {
genre: query.genres?.length ? query.genres[0] : undefined, genre: query.genres?.length ? query.genres[0] : undefined,
musicFolderId: query.musicFolderId, musicFolderId: query.musicFolderId,
offset: startIndex, offset: startIndex,
size: 500, size: MAX_SUBSONIC_ITEMS,
toYear, toYear,
type, type,
}, },
@@ -510,8 +553,7 @@ export const SubsonicController: ControllerEndpoint = {
totalRecordCount += albumCount; totalRecordCount += albumCount;
startIndex += albumCount; startIndex += albumCount;
// The max limit size for Subsonic is 500 fetchNextPage = albumCount === MAX_SUBSONIC_ITEMS;
fetchNextPage = albumCount === 500;
} }
return totalRecordCount; return totalRecordCount;
@@ -858,9 +900,8 @@ export const SubsonicController: ControllerEndpoint = {
return ssNormalize.song(res.body.song, apiClientProps.server); return ssNormalize.song(res.body.song, apiClientProps.server);
}, },
getSongList: async ({ apiClientProps, query }) => { getSongList: async ({ apiClientProps, query }) => {
const fromAlbumPromises: any[] = []; const fromAlbumPromises: Promise<ServerInferResponses<typeof contract.getAlbum>>[] = [];
const artistDetailPromises: any[] = []; const artistDetailPromises: Promise<ServerInferResponses<typeof contract.getArtist>>[] = [];
let results: any[] = [];
if (query.searchTerm) { if (query.searchTerm) {
const res = await ssApiClient(apiClientProps).search3({ const res = await ssApiClient(apiClientProps).search3({
@@ -869,7 +910,7 @@ export const SubsonicController: ControllerEndpoint = {
albumOffset: 0, albumOffset: 0,
artistCount: 0, artistCount: 0,
artistOffset: 0, artistOffset: 0,
query: query.searchTerm || '""', query: query.searchTerm || '',
songCount: query.limit, songCount: query.limit,
songOffset: query.startIndex, songOffset: query.startIndex,
}, },
@@ -984,6 +1025,8 @@ export const SubsonicController: ControllerEndpoint = {
} }
} }
let results: z.infer<typeof ssType._response.song>[] = [];
if (fromAlbumPromises) { if (fromAlbumPromises) {
const albumsResult = await Promise.all(fromAlbumPromises); const albumsResult = await Promise.all(fromAlbumPromises);
@@ -1009,7 +1052,7 @@ export const SubsonicController: ControllerEndpoint = {
albumOffset: 0, albumOffset: 0,
artistCount: 0, artistCount: 0,
artistOffset: 0, artistOffset: 0,
query: query.searchTerm || '""', query: query.searchTerm || '',
songCount: query.limit, songCount: query.limit,
songOffset: query.startIndex, songOffset: query.startIndex,
}, },
@@ -1049,8 +1092,8 @@ export const SubsonicController: ControllerEndpoint = {
albumOffset: 0, albumOffset: 0,
artistCount: 0, artistCount: 0,
artistOffset: 0, artistOffset: 0,
query: query.searchTerm || '""', query: query.searchTerm || '',
songCount: 500, songCount: MAX_SUBSONIC_ITEMS,
songOffset: startIndex, songOffset: startIndex,
}, },
}); });
@@ -1064,8 +1107,7 @@ export const SubsonicController: ControllerEndpoint = {
totalRecordCount += songCount; totalRecordCount += songCount;
startIndex += songCount; startIndex += songCount;
// The max limit size for Subsonic is 500 fetchNextPage = songCount === MAX_SUBSONIC_ITEMS;
fetchNextPage = songCount === 500;
} }
return totalRecordCount; return totalRecordCount;
@@ -1073,6 +1115,10 @@ export const SubsonicController: ControllerEndpoint = {
if (query.genreIds) { if (query.genreIds) {
let totalRecordCount = 0; let totalRecordCount = 0;
// Rather than just do `getSongsByGenre` by groups of 500, instead
// jump the offset 10x, and then backtrack on the last chunk. This improves
// performance for extremely large libraries
while (fetchNextSection) { while (fetchNextSection) {
const res = await ssApiClient(apiClientProps).getSongsByGenre({ const res = await ssApiClient(apiClientProps).getSongsByGenre({
query: { query: {
@@ -1091,17 +1137,17 @@ export const SubsonicController: ControllerEndpoint = {
if (numberOfResults !== 1) { if (numberOfResults !== 1) {
fetchNextSection = false; fetchNextSection = false;
startIndex = sectionIndex === 0 ? 0 : sectionIndex - 5000; startIndex = sectionIndex === 0 ? 0 : sectionIndex - SUBSONIC_FAST_BATCH_SIZE;
break; break;
} else { } else {
sectionIndex += 5000; sectionIndex += SUBSONIC_FAST_BATCH_SIZE;
} }
} }
while (fetchNextPage) { while (fetchNextPage) {
const res = await ssApiClient(apiClientProps).getSongsByGenre({ const res = await ssApiClient(apiClientProps).getSongsByGenre({
query: { query: {
count: 500, count: MAX_SUBSONIC_ITEMS,
genre: query.genreIds[0], genre: query.genreIds[0],
musicFolderId: query.musicFolderId, musicFolderId: query.musicFolderId,
offset: startIndex, offset: startIndex,
@@ -1117,7 +1163,7 @@ export const SubsonicController: ControllerEndpoint = {
totalRecordCount = startIndex + numberOfResults; totalRecordCount = startIndex + numberOfResults;
startIndex += numberOfResults; startIndex += numberOfResults;
fetchNextPage = numberOfResults === 500; fetchNextPage = numberOfResults === MAX_SUBSONIC_ITEMS;
} }
return totalRecordCount; return totalRecordCount;
@@ -1139,6 +1185,9 @@ export const SubsonicController: ControllerEndpoint = {
let totalRecordCount = 0; let totalRecordCount = 0;
// Rather than just do `search3` by groups of 500, instead
// jump the offset 10x, and then backtrack on the last chunk. This improves
// performance for extremely large libraries
while (fetchNextSection) { while (fetchNextSection) {
const res = await ssApiClient(apiClientProps).search3({ const res = await ssApiClient(apiClientProps).search3({
query: { query: {
@@ -1146,7 +1195,7 @@ export const SubsonicController: ControllerEndpoint = {
albumOffset: 0, albumOffset: 0,
artistCount: 0, artistCount: 0,
artistOffset: 0, artistOffset: 0,
query: query.searchTerm || '""', query: query.searchTerm || '',
songCount: 1, songCount: 1,
songOffset: sectionIndex, songOffset: sectionIndex,
}, },
@@ -1158,13 +1207,12 @@ export const SubsonicController: ControllerEndpoint = {
const numberOfResults = (res.body.searchResult3?.song || []).length || 0; const numberOfResults = (res.body.searchResult3?.song || []).length || 0;
// Check each batch of 5000 songs to check for data if (numberOfResults !== 1) {
sectionIndex += 5000; fetchNextSection = false;
fetchNextSection = numberOfResults === 1; startIndex = sectionIndex === 0 ? 0 : sectionIndex - SUBSONIC_FAST_BATCH_SIZE;
break;
if (!fetchNextSection) { } else {
// fetchNextBlock will be false on the next loop so we need to subtract 5000 * 2 sectionIndex += SUBSONIC_FAST_BATCH_SIZE;
startIndex = sectionIndex - 10000;
} }
} }
@@ -1175,8 +1223,8 @@ export const SubsonicController: ControllerEndpoint = {
albumOffset: 0, albumOffset: 0,
artistCount: 0, artistCount: 0,
artistOffset: 0, artistOffset: 0,
query: query.searchTerm || '""', query: query.searchTerm || '',
songCount: 500, songCount: MAX_SUBSONIC_ITEMS,
songOffset: startIndex, songOffset: startIndex,
}, },
}); });
@@ -1190,8 +1238,7 @@ export const SubsonicController: ControllerEndpoint = {
totalRecordCount = startIndex + numberOfResults; totalRecordCount = startIndex + numberOfResults;
startIndex += numberOfResults; startIndex += numberOfResults;
// The max limit size for Subsonic is 500 fetchNextPage = numberOfResults === MAX_SUBSONIC_ITEMS;
fetchNextPage = numberOfResults === 500;
} }
return totalRecordCount; return totalRecordCount;
+2 -9
View File
@@ -190,15 +190,8 @@ export const App = () => {
}, [language]); }, [language]);
return ( return (
<MantineProvider <MantineProvider defaultColorScheme={mode as 'dark' | 'light'} theme={theme}>
defaultColorScheme={mode as 'dark' | 'light'} <Notifications containerWidth="300px" position="bottom-center" zIndex={50000} />
theme={theme}
>
<Notifications
containerWidth="300px"
position="bottom-center"
zIndex={5}
/>
<PlayQueueHandlerContext.Provider value={providerValue}> <PlayQueueHandlerContext.Provider value={providerValue}>
<ContextMenuProvider> <ContextMenuProvider>
<WebAudioContext.Provider value={webAudioProvider}> <WebAudioContext.Provider value={webAudioProvider}>
@@ -322,10 +322,8 @@ export const AudioPlayer = forwardRef<AudioPlayerRef, AudioPlayerProps>((props,
if (isElectron() && webAudio && 'setSinkId' in webAudio.context && audioDeviceId) { if (isElectron() && webAudio && 'setSinkId' in webAudio.context && audioDeviceId) {
const setSink = async () => { const setSink = async () => {
try { try {
if (audioDeviceId !== 'default') { if (webAudio.context.state !== 'closed') {
await (webAudio.context as any).setSinkId(audioDeviceId); await (webAudio.context as any).setSinkId(audioDeviceId);
} else {
await (webAudio.context as any).setSinkId('');
} }
} catch (error) { } catch (error) {
toast.error({ message: `Error setting sink: ${(error as Error).message}` }); toast.error({ message: `Error setting sink: ${(error as Error).message}` });
@@ -47,10 +47,7 @@ export const CardControls = ({
return ( return (
<div className={styles.gridCardControlsContainer}> <div className={styles.gridCardControlsContainer}>
<div className={styles.bottomControls}> <div className={styles.bottomControls}>
<button <button className={styles.playButton} onClick={handlePlay}>
className={styles.playButton}
onClick={handlePlay}
>
<Icon icon="mediaPlay" /> <Icon icon="mediaPlay" />
</button> </button>
<Group gap="xs"> <Group gap="xs">
+5 -20
View File
@@ -55,14 +55,8 @@ export const PosterCard = ({
onMouseEnter={() => setIsHovered(true)} onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(false)}
> >
<Link <Link className={styles.imageContainer} to={path}>
className={styles.imageContainer} <Image className={styles.image} src={data?.imageUrl} />
to={path}
>
<Image
className={styles.image}
src={data?.imageUrl}
/>
<GridCardControls <GridCardControls
handleFavorite={controls.handleFavorite} handleFavorite={controls.handleFavorite}
handlePlayQueueAdd={controls.handlePlayQueueAdd} handlePlayQueueAdd={controls.handlePlayQueueAdd}
@@ -72,30 +66,21 @@ export const PosterCard = ({
/> />
</Link> </Link>
<div className={styles.detailContainer}> <div className={styles.detailContainer}>
<CardRows <CardRows data={data} rows={controls.cardRows} />
data={data}
rows={controls.cardRows}
/>
</div> </div>
</div> </div>
); );
} }
return ( return (
<div <div className={styles.container} key={`placeholder-${uniqueId}-${data.id}`}>
className={styles.container}
key={`placeholder-${uniqueId}-${data.id}`}
>
<div className={styles.imageContainer}> <div className={styles.imageContainer}>
<Skeleton className={styles.image} /> <Skeleton className={styles.image} />
</div> </div>
<div className={styles.detailContainer}> <div className={styles.detailContainer}>
<Stack gap="xs"> <Stack gap="xs">
{(controls?.cardRows || []).map((row, index) => ( {(controls?.cardRows || []).map((row, index) => (
<Skeleton <Skeleton height={14} key={`${index}-${row.arrayProperty}`} />
height={14}
key={`${index}-${row.arrayProperty}`}
/>
))} ))}
</Stack> </Stack>
</div> </div>
@@ -35,14 +35,8 @@ export const ContextMenuButton = forwardRef(
onClick={props.onClick} onClick={props.onClick}
ref={ref} ref={ref}
> >
<Group <Group justify="space-between" w="100%">
justify="space-between" <Group className={styles.left} gap="md">
w="100%"
>
<Group
className={styles.left}
gap="md"
>
{leftIcon} {leftIcon}
{children} {children}
</Group> </Group>
@@ -77,11 +77,7 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
className={styles.wrapper} className={styles.wrapper}
to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: currentItem?.id || '' })} to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: currentItem?.id || '' })}
> >
<AnimatePresence <AnimatePresence custom={direction} initial={false} mode="popLayout">
custom={direction}
initial={false}
mode="popLayout"
>
{data && ( {data && (
<motion.div <motion.div
animate="animate" animate="animate"
@@ -101,10 +97,7 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
/> />
</div> </div>
<div className={styles.infoColumn}> <div className={styles.infoColumn}>
<Stack <Stack gap="md" style={{ width: '100%' }}>
gap="md"
style={{ width: '100%' }}
>
<div className={styles.titleWrapper}> <div className={styles.titleWrapper}>
<TextTitle <TextTitle
fw={900} fw={900}
@@ -117,10 +110,7 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
</div> </div>
<div className={styles.titleWrapper}> <div className={styles.titleWrapper}>
{currentItem?.albumArtists.slice(0, 1).map((artist) => ( {currentItem?.albumArtists.slice(0, 1).map((artist) => (
<Text <Text fw={600} key={`carousel-artist-${artist.id}`}>
fw={600}
key={`carousel-artist-${artist.id}`}
>
{artist.name} {artist.name}
</Text> </Text>
))} ))}
@@ -60,10 +60,7 @@ const Title = ({ handleNext, handlePrev, label, pagination }: TitleProps) => {
{isValidElement(label) ? ( {isValidElement(label) ? (
label label
) : ( ) : (
<TextTitle <TextTitle order={3} weight={700}>
order={3}
weight={700}
>
{label} {label}
</TextTitle> </TextTitle>
)} )}
@@ -280,11 +277,7 @@ export const SwiperGridCarousel = ({
}, []); }, []);
return ( return (
<Stack <Stack className="grid-carousel" gap="md" ref={containerRef as any}>
className="grid-carousel"
gap="md"
ref={containerRef as any}
>
{title ? ( {title ? (
<Title <Title
{...title} {...title}
@@ -91,11 +91,7 @@ export const NativeScrollArea = forwardRef(
{...pageHeaderProps} {...pageHeaderProps}
/> />
)} )}
<div <div className={styles.scrollArea} ref={mergedRef} {...props}>
className={styles.scrollArea}
ref={mergedRef}
{...props}
>
{children} {children}
</div> </div>
</> </>
@@ -99,10 +99,7 @@ export const QueryBuilder = ({
}; };
return ( return (
<Stack <Stack gap="sm" ml={`${level * 10}px`}>
gap="sm"
ml={`${level * 10}px`}
>
<Group gap="sm"> <Group gap="sm">
<Select <Select
data={FILTER_GROUP_OPTIONS_DATA} data={FILTER_GROUP_OPTIONS_DATA}
@@ -112,12 +109,7 @@ export const QueryBuilder = ({
value={data.type} value={data.type}
width="20%" width="20%"
/> />
<ActionIcon <ActionIcon icon="add" onClick={handleAddRule} size="sm" variant="subtle" />
icon="add"
onClick={handleAddRule}
size="sm"
variant="subtle"
/>
<DropdownMenu position="bottom-start"> <DropdownMenu position="bottom-start">
<DropdownMenu.Target> <DropdownMenu.Target>
<ActionIcon <ActionIcon
@@ -150,24 +142,14 @@ export const QueryBuilder = ({
<DropdownMenu.Divider /> <DropdownMenu.Divider />
<DropdownMenu.Item <DropdownMenu.Item
isDanger isDanger
leftSection={ leftSection={<Icon color="error" icon="refresh" />}
<Icon
color="error"
icon="refresh"
/>
}
onClick={onResetFilters} onClick={onResetFilters}
> >
Reset to default Reset to default
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Item <DropdownMenu.Item
isDanger isDanger
leftSection={ leftSection={<Icon color="error" icon="delete" />}
<Icon
color="error"
icon="delete"
/>
}
onClick={onClearFilters} onClick={onClearFilters}
> >
Clear filters Clear filters
@@ -48,13 +48,7 @@ const QueryValueInput = ({ data, onChange, type, ...props }: any) => {
/> />
); );
case 'date': case 'date':
return ( return <TextInput onChange={onChange} size="sm" {...props} />;
<TextInput
onChange={onChange}
size="sm"
{...props}
/>
);
case 'dateRange': case 'dateRange':
return ( return (
<> <>
@@ -92,21 +86,9 @@ const QueryValueInput = ({ data, onChange, type, ...props }: any) => {
/> />
); );
case 'playlist': case 'playlist':
return ( return <Select data={data} onChange={onChange} {...props} />;
<Select
data={data}
onChange={onChange}
{...props}
/>
);
case 'string': case 'string':
return ( return <TextInput onChange={onChange} size="sm" {...props} />;
<TextInput
onChange={onChange}
size="sm"
{...props}
/>
);
default: default:
return <></>; return <></>;
@@ -188,10 +170,7 @@ export const QueryBuilderOption = ({
const ml = (level + 1) * 10; const ml = (level + 1) * 10;
return ( return (
<Group <Group gap="sm" ml={ml}>
gap="sm"
ml={ml}
>
<Select <Select
data={filters} data={filters}
maxWidth={170} maxWidth={170}
@@ -81,10 +81,7 @@ export const DefaultCard = ({
data?.userFavorite && styles.isFavorite, data?.userFavorite && styles.isFavorite,
)} )}
> >
<Image <Image className={styles.image} src={data?.imageUrl} />
className={styles.image}
src={data?.imageUrl}
/>
<GridCardControls <GridCardControls
handleFavorite={controls.handleFavorite} handleFavorite={controls.handleFavorite}
handlePlayQueueAdd={controls.handlePlayQueueAdd} handlePlayQueueAdd={controls.handlePlayQueueAdd}
@@ -95,10 +92,7 @@ export const DefaultCard = ({
/> />
</div> </div>
<div className={styles.detailContainer}> <div className={styles.detailContainer}>
<CardRows <CardRows data={data} rows={controls.cardRows} />
data={data}
rows={controls.cardRows}
/>
</div> </div>
</div> </div>
</div> </div>
@@ -86,10 +86,7 @@ export const GridCardControls = ({
onClick={handlePlay} onClick={handlePlay}
variant="filled" variant="filled"
> >
<Icon <Icon icon="mediaPlay" size="xl" />
icon="mediaPlay"
size="xl"
/>
</Button> </Button>
<div className={styles.bottomControls}> <div className={styles.bottomControls}>
{itemType !== LibraryItem.PLAYLIST && ( {itemType !== LibraryItem.PLAYLIST && (
@@ -73,17 +73,11 @@ export const PosterCard = ({
margin: controls.itemGap, margin: controls.itemGap,
}} }}
> >
<div <div className={styles.linkContainer} onClick={() => navigate(path)}>
className={styles.linkContainer}
onClick={() => navigate(path)}
>
<div <div
className={`${styles.imageContainer} ${data?.userFavorite ? styles.isFavorite : ''}`} className={`${styles.imageContainer} ${data?.userFavorite ? styles.isFavorite : ''}`}
> >
<Image <Image className={styles.image} src={data?.imageUrl} />
className={styles.image}
src={data?.imageUrl}
/>
<GridCardControls <GridCardControls
handleFavorite={controls.handleFavorite} handleFavorite={controls.handleFavorite}
handlePlayQueueAdd={controls.handlePlayQueueAdd} handlePlayQueueAdd={controls.handlePlayQueueAdd}
@@ -95,10 +89,7 @@ export const PosterCard = ({
</div> </div>
</div> </div>
<div className={styles.detailContainer}> <div className={styles.detailContainer}>
<CardRows <CardRows data={data} rows={controls.cardRows} />
data={data}
rows={controls.cardRows}
/>
</div> </div>
</div> </div>
); );
@@ -15,21 +15,14 @@ export const AlbumArtistCell = ({ data, value }: ICellRendererParams) => {
if (value === undefined) { if (value === undefined) {
return ( return (
<CellContainer position="left"> <CellContainer position="left">
<Skeleton <Skeleton height="1rem" width="80%" />
height="1rem"
width="80%"
/>
</CellContainer> </CellContainer>
); );
} }
return ( return (
<CellContainer position="left"> <CellContainer position="left">
<Text <Text isMuted overflow="hidden" size="md">
isMuted
overflow="hidden"
size="md"
>
{value?.map((item: AlbumArtist | Artist, index: number) => ( {value?.map((item: AlbumArtist | Artist, index: number) => (
<React.Fragment key={`row-${item.id}-${data.uniqueId}`}> <React.Fragment key={`row-${item.id}-${data.uniqueId}`}>
{index > 0 && <Separator />} {index > 0 && <Separator />}
@@ -47,11 +40,7 @@ export const AlbumArtistCell = ({ data, value }: ICellRendererParams) => {
{item.name || '—'} {item.name || '—'}
</Text> </Text>
) : ( ) : (
<Text <Text isMuted overflow="hidden" size="md">
isMuted
overflow="hidden"
size="md"
>
{item.name || '—'} {item.name || '—'}
</Text> </Text>
)} )}
@@ -15,21 +15,14 @@ export const ArtistCell = ({ data, value }: ICellRendererParams) => {
if (value === undefined) { if (value === undefined) {
return ( return (
<CellContainer position="left"> <CellContainer position="left">
<Skeleton <Skeleton height="1rem" width="80%" />
height="1rem"
width="80%"
/>
</CellContainer> </CellContainer>
); );
} }
return ( return (
<CellContainer position="left"> <CellContainer position="left">
<Text <Text isMuted overflow="hidden" size="md">
isMuted
overflow="hidden"
size="md"
>
{value?.map((item: AlbumArtist | Artist, index: number) => ( {value?.map((item: AlbumArtist | Artist, index: number) => (
<React.Fragment key={`row-${item.id}-${data.uniqueId}`}> <React.Fragment key={`row-${item.id}-${data.uniqueId}`}>
{index > 0 && <Separator />} {index > 0 && <Separator />}
@@ -47,11 +40,7 @@ export const ArtistCell = ({ data, value }: ICellRendererParams) => {
{item.name || '—'} {item.name || '—'}
</Text> </Text>
) : ( ) : (
<Text <Text isMuted overflow="hidden" size="md">
isMuted
overflow="hidden"
size="md"
>
{item.name || '—'} {item.name || '—'}
</Text> </Text>
)} )}
@@ -41,11 +41,7 @@ export const CombinedTitleCell = ({
> >
<Skeleton className={styles.image} /> <Skeleton className={styles.image} />
</div> </div>
<Skeleton <Skeleton className={styles.skeletonMetadata} height="1rem" width="80%" />
className={styles.skeletonMetadata}
height="1rem"
width="80%"
/>
</div> </div>
); );
} }
@@ -62,11 +58,7 @@ export const CombinedTitleCell = ({
width: `${(node.rowHeight || 40) - 10}px`, width: `${(node.rowHeight || 40) - 10}px`,
}} }}
> >
<Image <Image alt="cover" className={styles.image} src={value.imageUrl} />
alt="cover"
className={styles.image}
src={value.imageUrl}
/>
<ListCoverControls <ListCoverControls
className={styles.playButton} className={styles.playButton}
@@ -77,18 +69,10 @@ export const CombinedTitleCell = ({
/> />
</div> </div>
<div className={styles.metadataWrapper}> <div className={styles.metadataWrapper}>
<Text <Text className="current-song-child" overflow="hidden" size="md">
className="current-song-child"
overflow="hidden"
size="md"
>
{value.name} {value.name}
</Text> </Text>
<Text <Text isMuted overflow="hidden" size="md">
isMuted
overflow="hidden"
size="md"
>
{artists?.length ? ( {artists?.length ? (
artists.map((artist: AlbumArtist | Artist, index: number) => ( artists.map((artist: AlbumArtist | Artist, index: number) => (
<React.Fragment key={`queue-${rowIndex}-artist-${artist.id}`}> <React.Fragment key={`queue-${rowIndex}-artist-${artist.id}`}>
@@ -25,10 +25,7 @@ export const FullWidthDiscCell = ({ api, data, node }: ICellRendererParams) => {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<Group <Group justify="space-between" w="100%">
justify="space-between"
w="100%"
>
<Button <Button
leftSection={isSelected ? <Icon icon="squareCheck" /> : <Icon icon="square" />} leftSection={isSelected ? <Icon icon="squareCheck" /> : <Icon icon="square" />}
onClick={handleToggleDiscNodes} onClick={handleToggleDiscNodes}
@@ -16,19 +16,14 @@ type Options = {
primary?: boolean; primary?: boolean;
}; };
export const GenericCell = ( export const GenericCell = ({ value, valueFormatted }: ICellRendererParams, options?: Options) => {
{ value, valueFormatted }: ICellRendererParams, const { isLink, position, primary } = options || {};
{ isLink, position, primary }: Options,
) => {
const displayedValue = valueFormatted || value; const displayedValue = valueFormatted || value;
if (value === undefined) { if (value === undefined) {
return ( return (
<CellContainer position={position || 'left'}> <CellContainer position={position || 'left'}>
<Skeleton <Skeleton height="1rem" width="80%" />
height="1rem"
width="80%"
/>
</CellContainer> </CellContainer>
); );
} }
@@ -47,12 +42,7 @@ export const GenericCell = (
{isLink ? displayedValue.value : displayedValue} {isLink ? displayedValue.value : displayedValue}
</Text> </Text>
) : ( ) : (
<Text <Text isMuted={!primary} isNoSelect={false} overflow="hidden" size="md">
isMuted={!primary}
isNoSelect={false}
overflow="hidden"
size="md"
>
{displayedValue} {displayedValue}
</Text> </Text>
)} )}
@@ -13,11 +13,7 @@ export const GenreCell = ({ data, value }: ICellRendererParams) => {
const genrePath = useGenreRoute(); const genrePath = useGenreRoute();
return ( return (
<CellContainer position="left"> <CellContainer position="left">
<Text <Text isMuted overflow="hidden" size="md">
isMuted
overflow="hidden"
size="md"
>
{value?.map((item: AlbumArtist | Artist, index: number) => ( {value?.map((item: AlbumArtist | Artist, index: number) => (
<React.Fragment key={`row-${item.id}-${data.uniqueId}`}> <React.Fragment key={`row-${item.id}-${data.uniqueId}`}>
{index > 0 && <Separator />} {index > 0 && <Separator />}
@@ -19,20 +19,14 @@ export const NoteCell = ({ value }: ICellRendererParams) => {
if (value === undefined) { if (value === undefined) {
return ( return (
<CellContainer position="left"> <CellContainer position="left">
<Skeleton <Skeleton height="1rem" width="80%" />
height="1rem"
width="80%"
/>
</CellContainer> </CellContainer>
); );
} }
return ( return (
<CellContainer position="left"> <CellContainer position="left">
<Text <Text isMuted overflow="hidden">
isMuted
overflow="hidden"
>
{formattedValue} {formattedValue}
</Text> </Text>
</CellContainer> </CellContainer>
@@ -26,11 +26,7 @@ export const RatingCell = ({ node, value }: ICellRendererParams) => {
return ( return (
<CellContainer position="center"> <CellContainer position="center">
<Rating <Rating onChange={handleUpdateRating} size="xs" value={value?.userRating} />
onChange={handleUpdateRating}
size="xs"
value={value?.userRating}
/>
</CellContainer> </CellContainer>
); );
}; };
@@ -144,15 +144,9 @@ export const RowIndexCell = ({ eGridCell, value }: ICellRendererParams) => {
return ( return (
<CellContainer position="right"> <CellContainer position="right">
{isPlaying && isCurrentSong ? ( {isPlaying && isCurrentSong ? (
<Icon <Icon fill="primary" icon="mediaPlay" />
fill="primary"
icon="mediaPlay"
/>
) : isCurrentSong ? ( ) : isCurrentSong ? (
<Icon <Icon fill="primary" icon="mediaPause" />
fill="primary"
icon="mediaPause"
/>
) : ( ) : (
<Text <Text
className="current-song-child current-song-index" className="current-song-child current-song-index"
@@ -8,21 +8,14 @@ export const TitleCell = ({ value }: ICellRendererParams) => {
if (value === undefined) { if (value === undefined) {
return ( return (
<CellContainer position="left"> <CellContainer position="left">
<Skeleton <Skeleton height="1rem" width="80%" />
height="1rem"
width="80%"
/>
</CellContainer> </CellContainer>
); );
} }
return ( return (
<CellContainer position="left"> <CellContainer position="left">
<Text <Text className="current-song-child" overflow="hidden" size="md">
className="current-song-child"
overflow="hidden"
size="md"
>
{value} {value}
</Text> </Text>
</CellContainer> </CellContainer>
@@ -7,10 +7,5 @@ export interface ICustomHeaderParams extends IHeaderParams {
} }
export const DurationHeader = () => { export const DurationHeader = () => {
return ( return <Icon icon="duration" size="sm" />;
<Icon
icon="duration"
size="sm"
/>
);
}; };
@@ -16,36 +16,11 @@ type Options = {
type Presets = 'actions' | 'duration' | 'rowIndex' | 'userFavorite' | 'userRating'; type Presets = 'actions' | 'duration' | 'rowIndex' | 'userFavorite' | 'userRating';
const headerPresets = { const headerPresets = {
actions: ( actions: <Icon icon="ellipsisHorizontal" size="sm" />,
<Icon duration: <Icon icon="duration" size="sm" />,
icon="ellipsisHorizontal" rowIndex: <Icon icon="hash" size="sm" />,
size="sm" userFavorite: <Icon icon="favorite" size="sm" />,
/> userRating: <Icon icon="star" size="sm" />,
),
duration: (
<Icon
icon="duration"
size="sm"
/>
),
rowIndex: (
<Icon
icon="hash"
size="sm"
/>
),
userFavorite: (
<Icon
icon="favorite"
size="sm"
/>
),
userRating: (
<Icon
icon="star"
size="sm"
/>
),
}; };
export const GenericTableHeader = ( export const GenericTableHeader = (
@@ -236,7 +236,7 @@ const tableColumns: { [key: string]: ColDef } = {
width: 130, width: 130,
}, },
path: { path: {
cellRenderer: GenericCell, cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'left' }),
colId: TableColumn.PATH, colId: TableColumn.PATH,
headerName: i18n.t('table.column.path'), headerName: i18n.t('table.column.path'),
valueGetter: (params: ValueGetterParams) => (params.data ? params.data.path : undefined), valueGetter: (params: ValueGetterParams) => (params.data ? params.data.path : undefined),
@@ -635,15 +635,8 @@ export const VirtualTable = forwardRef(
onNewColumnsLoaded={handleNewColumnsLoaded} onNewColumnsLoaded={handleNewColumnsLoaded}
/> />
{paginationProps && ( {paginationProps && (
<AnimatePresence <AnimatePresence initial={false} mode="wait" presenceAffectsLayout>
initial={false} <TablePagination {...paginationProps} tableRef={tableRef} />
mode="wait"
presenceAffectsLayout
>
<TablePagination
{...paginationProps}
tableRef={tableRef}
/>
</AnimatePresence> </AnimatePresence>
)} )}
</div> </div>
@@ -215,7 +215,7 @@ export const ALBUMARTIST_TABLE_COLUMNS = [
value: TableColumn.PLAY_COUNT, value: TableColumn.PLAY_COUNT,
}, },
{ {
label: i18n.t('table.config.label.albumCount', { postProcess: 'titleCase' }), label: i18n.t('filter.albumCount', { postProcess: 'titleCase' }),
value: TableColumn.ALBUM_COUNT, value: TableColumn.ALBUM_COUNT,
}, },
{ {
@@ -76,10 +76,7 @@ export const TablePagination = ({
ref={containerQuery.ref} ref={containerQuery.ref}
style={{ borderTop: '1px solid var(--theme-generic-border-color)' }} style={{ borderTop: '1px solid var(--theme-generic-border-color)' }}
> >
<Text <Text isMuted size="md">
isMuted
size="md"
>
{containerQuery.isMd ? ( {containerQuery.isMd ? (
<> <>
Showing <b>{currentPageStartIndex}</b> - <b>{currentPageStopIndex}</b> of{' '} Showing <b>{currentPageStartIndex}</b> - <b>{currentPageStopIndex}</b> of{' '}
@@ -97,11 +94,7 @@ export const TablePagination = ({
</> </>
)} )}
</Text> </Text>
<Group <Group gap="sm" ref={containerQuery.ref} wrap="nowrap">
gap="sm"
ref={containerQuery.ref}
wrap="nowrap"
>
<Popover <Popover
onClose={() => handlers.close()} onClose={() => handlers.close()}
opened={isGoToPageOpen} opened={isGoToPageOpen}
@@ -127,10 +120,7 @@ export const TablePagination = ({
min={1} min={1}
width={70} width={70}
/> />
<Button <Button type="submit" variant="filled">
type="submit"
variant="filled"
>
Go Go
</Button> </Button>
</Group> </Group>
@@ -13,15 +13,8 @@ interface ActionRequiredContainerProps {
export const ActionRequiredContainer = ({ children, title }: ActionRequiredContainerProps) => ( export const ActionRequiredContainer = ({ children, title }: ActionRequiredContainerProps) => (
<Stack style={{ cursor: 'default', maxWidth: '700px' }}> <Stack style={{ cursor: 'default', maxWidth: '700px' }}>
<Group> <Group>
<Icon <Icon fill="warn" icon="warn" size="lg" />
fill="warn" <Text size="xl" style={{ textTransform: 'uppercase' }}>
icon="warn"
size="lg"
/>
<Text
size="xl"
style={{ textTransform: 'uppercase' }}
>
{title} {title}
</Text> </Text>
</Group> </Group>
@@ -21,18 +21,11 @@ export const ErrorFallback = ({ resetErrorBoundary }: FallbackProps) => {
<Center style={{ height: '100vh' }}> <Center style={{ height: '100vh' }}>
<Stack style={{ maxWidth: '50%' }}> <Stack style={{ maxWidth: '50%' }}>
<Group gap="xs"> <Group gap="xs">
<Icon <Icon fill="error" icon="error" size="lg" />
fill="error"
icon="error"
size="lg"
/>
<Text size="lg">{t('error.genericError')}</Text> <Text size="lg">{t('error.genericError')}</Text>
</Group> </Group>
<Text>{error?.message}</Text> <Text>{error?.message}</Text>
<Button <Button onClick={resetErrorBoundary} variant="filled">
onClick={resetErrorBoundary}
variant="filled"
>
{t('common.reload')} {t('common.reload')}
</Button> </Button>
</Stack> </Stack>
@@ -43,18 +43,11 @@ export const MpvRequired = () => {
<Text>Set your MPV executable location below and restart the application.</Text> <Text>Set your MPV executable location below and restart the application.</Text>
<Text> <Text>
MPV is available at the following:{' '} MPV is available at the following:{' '}
<a <a href="https://mpv.io/installation/" rel="noreferrer" target="_blank">
href="https://mpv.io/installation/"
rel="noreferrer"
target="_blank"
>
https://mpv.io/ https://mpv.io/
</a> </a>
</Text> </Text>
<FileInput <FileInput disabled={disabled} onChange={handleSetMpvPath} />
disabled={disabled}
onChange={handleSetMpvPath}
/>
<Text>{t('setting.disable_mpv', { context: 'description' })}</Text> <Text>{t('setting.disable_mpv', { context: 'description' })}</Text>
<Checkbox <Checkbox
label={t('setting.disableMpv')} label={t('setting.disableMpv')}
@@ -42,19 +42,12 @@ const RouteErrorBoundary = () => {
px={10} px={10}
variant="subtle" variant="subtle"
/> />
<Icon <Icon fill="error" icon="error" size="lg" />
fill="error"
icon="error"
size="lg"
/>
<Text size="lg">{t('error.genericError')}</Text> <Text size="lg">{t('error.genericError')}</Text>
</Group> </Group>
<Divider my={5} /> <Divider my={5} />
<Text size="sm">{error?.message}</Text> <Text size="sm">{error?.message}</Text>
<Group <Group gap="sm" grow>
gap="sm"
grow
>
<Button <Button
leftSection={<Icon icon="home" />} leftSection={<Icon icon="home" />}
onClick={handleHome} onClick={handleHome}
@@ -81,11 +74,7 @@ const RouteErrorBoundary = () => {
</DropdownMenu> </DropdownMenu>
</Group> </Group>
<Group grow> <Group grow>
<Button <Button onClick={handleReload} size="md" variant="filled">
onClick={handleReload}
size="md"
variant="filled"
>
{t('common.reload')} {t('common.reload')}
</Button> </Button>
</Group> </Group>
@@ -132,10 +132,7 @@ function ServerSelector() {
}} }}
variant={server.id === currentServer?.id ? 'filled' : 'default'} variant={server.id === currentServer?.id ? 'filled' : 'default'}
> >
<Group <Group justify="space-between" w="100%">
justify="space-between"
w="100%"
>
<Group> <Group>
<img <img
src={logo} src={logo}
@@ -144,10 +141,7 @@ function ServerSelector() {
width: 'var(--theme-font-size-2xl)', width: 'var(--theme-font-size-2xl)',
}} }}
/> />
<Text <Text fw={600} size="lg">
fw={600}
size="lg"
>
{server.name} {server.name}
</Text> </Text>
</Group> </Group>
@@ -49,10 +49,7 @@ const ActionRequiredRoute = () => {
<AnimatedPage> <AnimatedPage>
<PageHeader /> <PageHeader />
<Center style={{ height: '100%', width: '100vw' }}> <Center style={{ height: '100%', width: '100vw' }}>
<Stack <Stack gap="xl" style={{ maxWidth: '50%' }}>
gap="xl"
style={{ maxWidth: '50%' }}
>
<Group wrap="nowrap"> <Group wrap="nowrap">
{displayedCheck && ( {displayedCheck && (
<ActionRequiredContainer title={displayedCheck.title}> <ActionRequiredContainer title={displayedCheck.title}>
@@ -64,10 +61,7 @@ const ActionRequiredRoute = () => {
{canReturnHome && <Navigate to={AppRoute.HOME} />} {canReturnHome && <Navigate to={AppRoute.HOME} />}
{/* This should be displayed if a credential is required */} {/* This should be displayed if a credential is required */}
{isCredentialRequired && ( {isCredentialRequired && (
<Group <Group justify="center" wrap="nowrap">
justify="center"
wrap="nowrap"
>
<Button <Button
fullWidth fullWidth
leftSection={<Icon icon="edit" />} leftSection={<Icon icon="edit" />}
@@ -18,24 +18,14 @@ const InvalidRoute = () => {
<AnimatedPage> <AnimatedPage>
<Center style={{ height: '100%', width: '100%' }}> <Center style={{ height: '100%', width: '100%' }}>
<Stack> <Stack>
<Group <Group justify="center" wrap="nowrap">
justify="center" <Icon color="warn" icon="error" />
wrap="nowrap"
>
<Icon
color="warn"
icon="error"
/>
<Text size="xl"> <Text size="xl">
{t('error.apiRouteError', { postProcess: 'sentenceCase' })} {t('error.apiRouteError', { postProcess: 'sentenceCase' })}
</Text> </Text>
</Group> </Group>
<Text>{location.pathname}</Text> <Text>{location.pathname}</Text>
<ActionIcon <ActionIcon icon="arrowLeftS" onClick={() => navigate(-1)} variant="filled" />
icon="arrowLeftS"
onClick={() => navigate(-1)}
variant="filled"
/>
</Stack> </Stack>
</Center> </Center>
</AnimatedPage> </AnimatedPage>
@@ -319,17 +319,11 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
const mbzId = detailQuery?.data?.mbzId; const mbzId = detailQuery?.data?.mbzId;
return ( return (
<div <div className={styles.contentContainer} ref={cq.ref}>
className={styles.contentContainer}
ref={cq.ref}
>
<LibraryBackgroundOverlay backgroundColor={background} /> <LibraryBackgroundOverlay backgroundColor={background} />
<div className={styles.detailContainer}> <div className={styles.detailContainer}>
<section> <section>
<Group <Group gap="sm" justify="space-between">
gap="sm"
justify="space-between"
>
<Group> <Group>
<PlayButton onClick={() => handlePlay(playButtonBehavior)} /> <PlayButton onClick={() => handlePlay(playButtonBehavior)} />
<Group gap="xs"> <Group gap="xs">
@@ -485,11 +479,7 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
suppressRowDrag suppressRowDrag
/> />
</div> </div>
<Stack <Stack gap="lg" mt="3rem" ref={cq.ref}>
gap="lg"
mt="3rem"
ref={cq.ref}
>
{cq.height || cq.width ? ( {cq.height || cq.width ? (
<> <>
{carousels {carousels
@@ -33,15 +33,9 @@ export const AlbumListContent = ({ gridRef, itemCount, tableRef }: AlbumListCont
return ( return (
<Suspense fallback={<Spinner container />}> <Suspense fallback={<Spinner container />}>
{display === ListDisplayType.CARD || display === ListDisplayType.GRID ? ( {display === ListDisplayType.CARD || display === ListDisplayType.GRID ? (
<AlbumListGridView <AlbumListGridView gridRef={gridRef} itemCount={itemCount} />
gridRef={gridRef}
itemCount={itemCount}
/>
) : ( ) : (
<AlbumListTableView <AlbumListTableView itemCount={itemCount} tableRef={tableRef} />
itemCount={itemCount}
tableRef={tableRef}
/>
)} )}
</Suspense> </Suspense>
); );
@@ -405,31 +405,35 @@ export const AlbumListHeaderFilters = ({
const isFilterApplied = useMemo(() => { const isFilterApplied = useMemo(() => {
const isNavidromeFilterApplied = const isNavidromeFilterApplied =
server?.type === ServerType.NAVIDROME && server?.type === ServerType.NAVIDROME &&
filter?._custom?.navidrome && ((filter?._custom?.navidrome &&
Object.values(filter?._custom?.navidrome).some((value) => value !== undefined); Object.values(filter?._custom?.navidrome).some((value) => value !== undefined)) ||
// Compilation is always valid
filter.compilation !== undefined);
const isJellyfinFilterApplied = const isJellyfinFilterApplied =
server?.type === ServerType.JELLYFIN && server?.type === ServerType.JELLYFIN &&
filter?._custom?.jellyfin && ((filter?._custom?.jellyfin &&
Object.values(filter?._custom?.jellyfin).some((value) => value !== undefined); Object.values(filter?._custom?.jellyfin).some((value) => value !== undefined)) ||
// Compilation filter is only valid when on the artist page
(filter.compilation !== undefined && customFilters?.artistIds));
const isSubsonicFilterApplied = const isSubsonicFilterApplied =
server?.type === ServerType.SUBSONIC && (filter.maxYear || filter.minYear); server?.type === ServerType.SUBSONIC && (filter.maxYear || filter.minYear);
const isCompilationFilterApplied =
server?.type === ServerType.NAVIDROME && filter.compilation !== undefined;
return ( return (
isNavidromeFilterApplied || isNavidromeFilterApplied ||
isJellyfinFilterApplied || isJellyfinFilterApplied ||
isSubsonicFilterApplied || isSubsonicFilterApplied ||
filter.genres?.length || filter.genres?.length ||
filter.favorite !== undefined || filter.favorite !== undefined ||
isCompilationFilterApplied // If we are on the artist page, the artist id filter should not be active
(filter.artistIds?.length && !(customFilters?.artistIds as any | undefined)?.length)
); );
}, [ }, [
customFilters?.artistIds,
filter?._custom?.jellyfin, filter?._custom?.jellyfin,
filter?._custom?.navidrome, filter?._custom?.navidrome,
filter.artistIds?.length,
filter.compilation, filter.compilation,
filter.favorite, filter.favorite,
filter.genres?.length, filter.genres?.length,
@@ -444,11 +448,7 @@ export const AlbumListHeaderFilters = ({
return ( return (
<Flex justify="space-between"> <Flex justify="space-between">
<Group <Group gap="sm" ref={cq.ref} w="100%">
gap="sm"
ref={cq.ref}
w="100%"
>
<DropdownMenu position="bottom-start"> <DropdownMenu position="bottom-start">
<DropdownMenu.Target> <DropdownMenu.Target>
<Button variant="subtle">{sortByLabel}</Button> <Button variant="subtle">{sortByLabel}</Button>
@@ -467,10 +467,7 @@ export const AlbumListHeaderFilters = ({
</DropdownMenu.Dropdown> </DropdownMenu.Dropdown>
</DropdownMenu> </DropdownMenu>
<Divider orientation="vertical" /> <Divider orientation="vertical" />
<OrderToggleButton <OrderToggleButton onToggle={handleToggleSortOrder} sortOrder={filter.sortOrder} />
onToggle={handleToggleSortOrder}
sortOrder={filter.sortOrder}
/>
{server?.type === ServerType.JELLYFIN && ( {server?.type === ServerType.JELLYFIN && (
<> <>
<Divider orientation="vertical" /> <Divider orientation="vertical" />
@@ -493,10 +490,7 @@ export const AlbumListHeaderFilters = ({
</DropdownMenu> </DropdownMenu>
</> </>
)} )}
<FilterButton <FilterButton isActive={!!isFilterApplied} onClick={handleOpenFiltersModal} />
isActive={!!isFilterApplied}
onClick={handleOpenFiltersModal}
/>
<RefreshButton onClick={handleRefresh} /> <RefreshButton onClick={handleRefresh} />
<DropdownMenu position="bottom-start"> <DropdownMenu position="bottom-start">
<DropdownMenu.Target> <DropdownMenu.Target>
@@ -531,10 +525,7 @@ export const AlbumListHeaderFilters = ({
</DropdownMenu.Dropdown> </DropdownMenu.Dropdown>
</DropdownMenu> </DropdownMenu>
</Group> </Group>
<Group <Group gap="sm" wrap="nowrap">
gap="sm"
wrap="nowrap"
>
<ListConfigMenu <ListConfigMenu
autoFitColumns={table.autoFit} autoFitColumns={table.autoFit}
disabledViewTypes={[ListDisplayType.LIST]} disabledViewTypes={[ListDisplayType.LIST]}
@@ -61,15 +61,9 @@ export const AlbumListHeader = ({
}, [filter, genreId, refresh, tableRef]); }, [filter, genreId, refresh, tableRef]);
return ( return (
<Stack <Stack gap={0} ref={cq.ref}>
gap={0}
ref={cq.ref}
>
<PageHeader backgroundColor="var(--theme-colors-background)"> <PageHeader backgroundColor="var(--theme-colors-background)">
<Flex <Flex justify="space-between" w="100%">
justify="space-between"
w="100%"
>
<LibraryHeaderBar> <LibraryHeaderBar>
<LibraryHeaderBar.PlayButton <LibraryHeaderBar.PlayButton
onClick={() => handlePlay?.({ playType: playButtonBehavior })} onClick={() => handlePlay?.({ playType: playButtonBehavior })}
@@ -85,10 +79,7 @@ export const AlbumListHeader = ({
</LibraryHeaderBar.Badge> </LibraryHeaderBar.Badge>
</LibraryHeaderBar> </LibraryHeaderBar>
<Group> <Group>
<SearchInput <SearchInput defaultValue={filter.searchTerm} onChange={handleSearch} />
defaultValue={filter.searchTerm}
onChange={handleSearch}
/>
</Group> </Group>
</Flex> </Flex>
</PageHeader> </PageHeader>
@@ -1,5 +1,5 @@
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { useMemo, useState } from 'react'; import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data'; import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
@@ -43,6 +43,10 @@ export const JellyfinAlbumFilters = ({
// TODO - eventually replace with /items/filters endpoint to fetch genres and tags specific to the selected library // TODO - eventually replace with /items/filters endpoint to fetch genres and tags specific to the selected library
const genreListQuery = useGenreList({ const genreListQuery = useGenreList({
options: {
cacheTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1,
},
query: { query: {
musicFolderId: filter?.musicFolderId, musicFolderId: filter?.musicFolderId,
sortBy: GenreListSort.NAME, sortBy: GenreListSort.NAME,
@@ -61,6 +65,10 @@ export const JellyfinAlbumFilters = ({
}, [genreListQuery.data]); }, [genreListQuery.data]);
const tagsQuery = useTagList({ const tagsQuery = useTagList({
options: {
cacheTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1,
},
query: { query: {
folder: filter?.musicFolderId, folder: filter?.musicFolderId,
type: LibraryItem.ALBUM, type: LibraryItem.ALBUM,
@@ -72,24 +80,55 @@ export const JellyfinAlbumFilters = ({
return filter?._custom?.jellyfin?.Tags?.split('|'); return filter?._custom?.jellyfin?.Tags?.split('|');
}, [filter?._custom?.jellyfin?.Tags]); }, [filter?._custom?.jellyfin?.Tags]);
const yesNoFilter = [ const yesNoFilter = useMemo(() => {
{ const filters = [
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }), {
onChange: (favorite?: boolean) => { label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
const updatedFilters = setFilter({ onChange: (favorite?: boolean) => {
customFilters, const updatedFilters = setFilter({
data: { customFilters,
_custom: filter?._custom, data: {
favorite, _custom: filter?._custom,
}, favorite,
itemType: LibraryItem.ALBUM, },
key: pageKey, itemType: LibraryItem.ALBUM,
}) as AlbumListFilter; key: pageKey,
onFilterChange(updatedFilters); }) as AlbumListFilter;
onFilterChange(updatedFilters);
},
value: filter?.favorite,
}, },
value: filter?.favorite, ];
},
]; if (customFilters?.artistIds) {
filters.push({
label: t('filter.isCompilation', { postProcess: 'sentenceCase' }),
onChange: (compilation?: boolean) => {
const updatedFilters = setFilter({
customFilters,
data: {
_custom: filter._custom,
compilation,
},
itemType: LibraryItem.ALBUM,
key: pageKey,
}) as AlbumListFilter;
onFilterChange(updatedFilters);
},
value: filter.compilation,
});
}
return filters;
}, [
customFilters,
filter._custom,
filter.compilation,
filter?.favorite,
onFilterChange,
pageKey,
setFilter,
t,
]);
const handleMinYearFilter = debounce((e: number | string) => { const handleMinYearFilter = debounce((e: number | string) => {
if (typeof e === 'number' && (e < 1700 || e > 2300)) return; if (typeof e === 'number' && (e < 1700 || e > 2300)) return;
@@ -132,8 +171,6 @@ export const JellyfinAlbumFilters = ({
onFilterChange(updatedFilters); onFilterChange(updatedFilters);
}, 250); }, 250);
const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState<string>('');
const albumArtistListQuery = useAlbumArtistList({ const albumArtistListQuery = useAlbumArtistList({
options: { options: {
cacheTime: 1000 * 60 * 2, cacheTime: 1000 * 60 * 2,
@@ -161,7 +198,7 @@ export const JellyfinAlbumFilters = ({
customFilters, customFilters,
data: { data: {
_custom: filter?._custom, _custom: filter?._custom,
artistIds: e || undefined, artistIds: e?.length ? e : undefined,
}, },
itemType: LibraryItem.ALBUM, itemType: LibraryItem.ALBUM,
key: pageKey, key: pageKey,
@@ -190,16 +227,9 @@ export const JellyfinAlbumFilters = ({
return ( return (
<Stack p="0.8rem"> <Stack p="0.8rem">
{yesNoFilter.map((filter) => ( {yesNoFilter.map((filter) => (
<Group <Group justify="space-between" key={`nd-filter-${filter.label}`}>
justify="space-between"
key={`nd-filter-${filter.label}`}
>
<Text>{filter.label}</Text> <Text>{filter.label}</Text>
<YesNoSelect <YesNoSelect onChange={filter.onChange} size="xs" value={filter.value} />
onChange={filter.onChange}
size="xs"
value={filter.value}
/>
</Group> </Group>
))} ))}
<Divider my="0.5rem" /> <Divider my="0.5rem" />
@@ -238,16 +268,14 @@ export const JellyfinAlbumFilters = ({
<MultiSelectWithInvalidData <MultiSelectWithInvalidData
clearable clearable
data={selectableAlbumArtists} data={selectableAlbumArtists}
defaultValue={filter?._custom?.jellyfin?.AlbumArtistIds?.split(',')} defaultValue={filter?.artistIds}
disabled={disableArtistFilter} disabled={disableArtistFilter}
label={t('entity.artist', { count: 2, postProcess: 'sentenceCase' })} label={t('entity.artist', { count: 2, postProcess: 'sentenceCase' })}
limit={300} limit={300}
onChange={handleAlbumArtistFilter} onChange={handleAlbumArtistFilter}
onSearchChange={setAlbumArtistSearchTerm}
placeholder="Type to search for an artist" placeholder="Type to search for an artist"
rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined} rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined}
searchable searchable
searchValue={albumArtistSearchTerm}
/> />
</Group> </Group>
{tagsQuery.data?.boolTags && tagsQuery.data.boolTags.length > 0 && ( {tagsQuery.data?.boolTags && tagsQuery.data.boolTags.length > 0 && (
@@ -1,5 +1,5 @@
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { ChangeEvent, useMemo, useState } from 'react'; import { ChangeEvent, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data'; import { SelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
@@ -7,6 +7,7 @@ import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-a
import { useGenreList } from '/@/renderer/features/genres'; import { useGenreList } from '/@/renderer/features/genres';
import { useTagList } from '/@/renderer/features/tag/queries/use-tag-list'; import { useTagList } from '/@/renderer/features/tag/queries/use-tag-list';
import { AlbumListFilter, useListStoreActions, useListStoreByKey } from '/@/renderer/store'; import { AlbumListFilter, useListStoreActions, useListStoreByKey } from '/@/renderer/store';
import { NDSongQueryFields } from '/@/shared/api/navidrome.types';
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 { NumberInput } from '/@/shared/components/number-input/number-input'; import { NumberInput } from '/@/shared/components/number-input/number-input';
@@ -43,6 +44,10 @@ export const NavidromeAlbumFilters = ({
const { setFilter } = useListStoreActions(); const { setFilter } = useListStoreActions();
const genreListQuery = useGenreList({ const genreListQuery = useGenreList({
options: {
cacheTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1,
},
query: { query: {
sortBy: GenreListSort.NAME, sortBy: GenreListSort.NAME,
sortOrder: SortOrder.ASC, sortOrder: SortOrder.ASC,
@@ -73,6 +78,10 @@ export const NavidromeAlbumFilters = ({
}, 250); }, 250);
const tagsQuery = useTagList({ const tagsQuery = useTagList({
options: {
cacheTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1,
},
query: { query: {
type: LibraryItem.ALBUM, type: LibraryItem.ALBUM,
}, },
@@ -177,8 +186,6 @@ export const NavidromeAlbumFilters = ({
onFilterChange(updatedFilters); onFilterChange(updatedFilters);
}, 500); }, 500);
const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState<string>('');
const albumArtistListQuery = useAlbumArtistList({ const albumArtistListQuery = useAlbumArtistList({
options: { options: {
cacheTime: 1000 * 60 * 2, cacheTime: 1000 * 60 * 2,
@@ -241,28 +248,15 @@ export const NavidromeAlbumFilters = ({
return ( return (
<Stack p="0.8rem"> <Stack p="0.8rem">
{yesNoUndefinedFilters.map((filter) => ( {yesNoUndefinedFilters.map((filter) => (
<Group <Group justify="space-between" key={`nd-filter-${filter.label}`}>
justify="space-between"
key={`nd-filter-${filter.label}`}
>
<Text>{filter.label}</Text> <Text>{filter.label}</Text>
<YesNoSelect <YesNoSelect onChange={filter.onChange} size="xs" value={filter.value} />
onChange={filter.onChange}
size="xs"
value={filter.value}
/>
</Group> </Group>
))} ))}
{toggleFilters.map((filter) => ( {toggleFilters.map((filter) => (
<Group <Group justify="space-between" key={`nd-filter-${filter.label}`}>
justify="space-between"
key={`nd-filter-${filter.label}`}
>
<Text>{filter.label}</Text> <Text>{filter.label}</Text>
<Switch <Switch checked={filter?.value || false} onChange={filter.onChange} />
checked={filter?.value || false}
onChange={filter.onChange}
/>
</Group> </Group>
))} ))}
<Divider my="0.5rem" /> <Divider my="0.5rem" />
@@ -293,25 +287,24 @@ export const NavidromeAlbumFilters = ({
label={t('entity.artist', { count: 1, postProcess: 'titleCase' })} label={t('entity.artist', { count: 1, postProcess: 'titleCase' })}
limit={300} limit={300}
onChange={handleAlbumArtistFilter} onChange={handleAlbumArtistFilter}
onSearchChange={setAlbumArtistSearchTerm}
rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined} rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined}
searchable searchable
searchValue={albumArtistSearchTerm}
/> />
</Group> </Group>
{tagsQuery.data?.enumTags?.length && {tagsQuery.data?.enumTags?.length &&
tagsQuery.data.enumTags.length > 0 &&
tagsQuery.data.enumTags.map((tag) => ( tagsQuery.data.enumTags.map((tag) => (
<Group <Group grow key={tag.name}>
grow
key={tag.name}
>
<SelectWithInvalidData <SelectWithInvalidData
clearable clearable
data={tag.options} data={tag.options}
defaultValue={ defaultValue={
filter._custom?.navidrome?.[tag.name] as string | undefined filter._custom?.navidrome?.[tag.name] as string | undefined
} }
label={tag.name} label={
NDSongQueryFields.find((i) => i.value === tag.name)?.label ||
tag.name
}
onChange={(value) => handleTagFilter(tag.name, value)} onChange={(value) => handleTagFilter(tag.name, value)}
searchable searchable
width={150} width={150}
@@ -1,17 +1,21 @@
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { ChangeEvent, useMemo } from 'react'; import { ChangeEvent, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query';
import { useGenreList } from '/@/renderer/features/genres'; import { useGenreList } from '/@/renderer/features/genres';
import { AlbumListFilter, useListStoreActions, useListStoreByKey } from '/@/renderer/store'; import { AlbumListFilter, useListStoreActions, useListStoreByKey } from '/@/renderer/store';
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 { NumberInput } from '/@/shared/components/number-input/number-input'; import { NumberInput } from '/@/shared/components/number-input/number-input';
import { Select } from '/@/shared/components/select/select'; import { Select } from '/@/shared/components/select/select';
import { SpinnerIcon } from '/@/shared/components/spinner/spinner';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { Switch } from '/@/shared/components/switch/switch'; import { Switch } from '/@/shared/components/switch/switch';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { import {
AlbumArtistListSort,
AlbumListQuery, AlbumListQuery,
GenreListSort, GenreListSort,
LibraryItem, LibraryItem,
@@ -19,12 +23,14 @@ import {
} from '/@/shared/types/domain-types'; } from '/@/shared/types/domain-types';
interface SubsonicAlbumFiltersProps { interface SubsonicAlbumFiltersProps {
disableArtistFilter?: boolean;
onFilterChange: (filters: AlbumListFilter) => void; onFilterChange: (filters: AlbumListFilter) => void;
pageKey: string; pageKey: string;
serverId?: string; serverId?: string;
} }
export const SubsonicAlbumFilters = ({ export const SubsonicAlbumFilters = ({
disableArtistFilter,
onFilterChange, onFilterChange,
pageKey, pageKey,
serverId, serverId,
@@ -32,8 +38,46 @@ export const SubsonicAlbumFilters = ({
const { t } = useTranslation(); const { t } = useTranslation();
const { filter } = useListStoreByKey<AlbumListQuery>({ key: pageKey }); const { filter } = useListStoreByKey<AlbumListQuery>({ key: pageKey });
const { setFilter } = useListStoreActions(); const { setFilter } = useListStoreActions();
const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState<string>('');
const albumArtistListQuery = useAlbumArtistList({
options: {
cacheTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1,
},
query: {
sortBy: AlbumArtistListSort.NAME,
sortOrder: SortOrder.ASC,
startIndex: 0,
},
serverId,
});
const selectableAlbumArtists = useMemo(() => {
if (!albumArtistListQuery?.data?.items) return [];
return albumArtistListQuery?.data?.items?.map((artist) => ({
label: artist.name,
value: artist.id,
}));
}, [albumArtistListQuery?.data?.items]);
const handleAlbumArtistFilter = (e: null | string[]) => {
const updatedFilters = setFilter({
data: {
artistIds: e?.length ? e : undefined,
},
itemType: LibraryItem.ALBUM,
key: pageKey,
}) as AlbumListFilter;
onFilterChange(updatedFilters);
};
const genreListQuery = useGenreList({ const genreListQuery = useGenreList({
options: {
cacheTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1,
},
query: { query: {
sortBy: GenreListSort.NAME, sortBy: GenreListSort.NAME,
sortOrder: SortOrder.ASC, sortOrder: SortOrder.ASC,
@@ -104,15 +148,9 @@ export const SubsonicAlbumFilters = ({
return ( return (
<Stack p="0.8rem"> <Stack p="0.8rem">
{toggleFilters.map((filter) => ( {toggleFilters.map((filter) => (
<Group <Group justify="space-between" key={`nd-filter-${filter.label}`}>
justify="space-between"
key={`nd-filter-${filter.label}`}
>
<Text>{filter.label}</Text> <Text>{filter.label}</Text>
<Switch <Switch checked={filter?.value || false} onChange={filter.onChange} />
checked={filter?.value || false}
onChange={filter.onChange}
/>
</Group> </Group>
))} ))}
<Divider my="0.5rem" /> <Divider my="0.5rem" />
@@ -147,6 +185,22 @@ export const SubsonicAlbumFilters = ({
searchable searchable
/> />
</Group> </Group>
<Group grow>
<MultiSelectWithInvalidData
clearable
data={selectableAlbumArtists}
defaultValue={filter?.artistIds}
disabled={disableArtistFilter}
label={t('entity.artist', { count: 2, postProcess: 'sentenceCase' })}
limit={300}
onChange={handleAlbumArtistFilter}
onSearchChange={setAlbumArtistSearchTerm}
placeholder="Type to search for an artist"
rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined}
searchable
searchValue={albumArtistSearchTerm}
/>
</Group>
</Stack> </Stack>
); );
}; };
@@ -70,10 +70,7 @@ const AlbumDetailRoute = () => {
}} }}
ref={headerRef} ref={headerRef}
/> />
<AlbumDetailContent <AlbumDetailContent background={background} tableRef={tableRef} />
background={background}
tableRef={tableRef}
/>
</NativeScrollArea> </NativeScrollArea>
</AnimatedPage> </AnimatedPage>
); );
@@ -144,11 +144,7 @@ const AlbumListRoute = () => {
tableRef={tableRef} tableRef={tableRef}
title={title} title={title}
/> />
<AlbumListContent <AlbumListContent gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
gridRef={gridRef}
itemCount={itemCount}
tableRef={tableRef}
/>
</ListContext.Provider> </ListContext.Provider>
</AnimatedPage> </AnimatedPage>
); );
@@ -174,10 +174,7 @@ const DummyAlbumDetailRoute = () => {
</Stack> </Stack>
<div className={styles.detailContainer}> <div className={styles.detailContainer}>
<section> <section>
<Group <Group gap="sm" justify="space-between">
gap="sm"
justify="space-between"
>
<Group> <Group>
<PlayButton onClick={() => handlePlay()} /> <PlayButton onClick={() => handlePlay()} />
<ActionIcon <ActionIcon
@@ -231,11 +228,7 @@ const DummyAlbumDetailRoute = () => {
<section> <section>
<Center> <Center>
<Group mr={5}> <Group mr={5}>
<Icon <Icon fill="error" icon="error" size={30} />
fill="error"
icon="error"
size={30}
/>
</Group> </Group>
<h2>{t('error.badAlbum', { postProcess: 'sentenceCase' })}</h2> <h2>{t('error.badAlbum', { postProcess: 'sentenceCase' })}</h2>
</Center> </Center>
@@ -202,10 +202,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
order: itemOrder.recentAlbums, order: itemOrder.recentAlbums,
title: ( title: (
<Group align="flex-end"> <Group align="flex-end">
<TextTitle <TextTitle fw={700} order={2}>
fw={700}
order={2}
>
{t('page.albumArtistDetail.recentReleases', { {t('page.albumArtistDetail.recentReleases', {
postProcess: 'sentenceCase', postProcess: 'sentenceCase',
})} })}
@@ -232,10 +229,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
loading: compilationAlbumsQuery?.isLoading || compilationAlbumsQuery.isFetching, loading: compilationAlbumsQuery?.isLoading || compilationAlbumsQuery.isFetching,
order: itemOrder.compilations, order: itemOrder.compilations,
title: ( title: (
<TextTitle <TextTitle fw={700} order={2}>
fw={700}
order={2}
>
{t('page.albumArtistDetail.appearsOn', { postProcess: 'sentenceCase' })} {t('page.albumArtistDetail.appearsOn', { postProcess: 'sentenceCase' })}
</TextTitle> </TextTitle>
), ),
@@ -247,10 +241,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
itemType: LibraryItem.ALBUM_ARTIST, itemType: LibraryItem.ALBUM_ARTIST,
order: itemOrder.similarArtists, order: itemOrder.similarArtists,
title: ( title: (
<TextTitle <TextTitle fw={700} order={2}>
fw={700}
order={2}
>
{t('page.albumArtistDetail.relatedArtists', { {t('page.albumArtistDetail.relatedArtists', {
postProcess: 'sentenceCase', postProcess: 'sentenceCase',
})} })}
@@ -355,19 +346,10 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
detailQuery?.isLoading || detailQuery?.isLoading ||
(server?.type === ServerType.NAVIDROME && enabledItem.topSongs && topSongsQuery?.isLoading); (server?.type === ServerType.NAVIDROME && enabledItem.topSongs && topSongsQuery?.isLoading);
if (isLoading) if (isLoading) return <div className={styles.contentContainer} ref={cq.ref} />;
return (
<div
className={styles.contentContainer}
ref={cq.ref}
/>
);
return ( return (
<div <div className={styles.contentContainer} ref={cq.ref}>
className={styles.contentContainer}
ref={cq.ref}
>
<LibraryBackgroundOverlay backgroundColor={background} /> <LibraryBackgroundOverlay backgroundColor={background} />
<div className={styles.detailContainer}> <div className={styles.detailContainer}>
<Group gap="md"> <Group gap="md">
@@ -481,15 +463,9 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
) : null} ) : null}
<Grid gutter="xl"> <Grid gutter="xl">
{biography ? ( {biography ? (
<Grid.Col <Grid.Col order={itemOrder.biography} span={12}>
order={itemOrder.biography}
span={12}
>
<section style={{ maxWidth: '1280px' }}> <section style={{ maxWidth: '1280px' }}>
<TextTitle <TextTitle fw={700} order={2}>
fw={700}
order={2}
>
{t('page.albumArtistDetail.about', { {t('page.albumArtistDetail.about', {
artist: detailQuery?.data?.name, artist: detailQuery?.data?.name,
})} })}
@@ -499,23 +475,11 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
</Grid.Col> </Grid.Col>
) : null} ) : null}
{showTopSongs ? ( {showTopSongs ? (
<Grid.Col <Grid.Col order={itemOrder.topSongs} span={12}>
order={itemOrder.topSongs}
span={12}
>
<section> <section>
<Group <Group justify="space-between" wrap="nowrap">
justify="space-between" <Group align="flex-end" wrap="nowrap">
wrap="nowrap" <TextTitle fw={700} order={2}>
>
<Group
align="flex-end"
wrap="nowrap"
>
<TextTitle
fw={700}
order={2}
>
{t('page.albumArtistDetail.topSongs', { {t('page.albumArtistDetail.topSongs', {
postProcess: 'sentenceCase', postProcess: 'sentenceCase',
})} })}
@@ -42,15 +42,9 @@ export const AlbumArtistListContent = ({
return ( return (
<Suspense fallback={<Spinner container />}> <Suspense fallback={<Spinner container />}>
{isGrid ? ( {isGrid ? (
<AlbumArtistListGridView <AlbumArtistListGridView gridRef={gridRef} itemCount={itemCount} />
gridRef={gridRef}
itemCount={itemCount}
/>
) : ( ) : (
<AlbumArtistListTableView <AlbumArtistListTableView itemCount={itemCount} tableRef={tableRef} />
itemCount={itemCount}
tableRef={tableRef}
/>
)} )}
</Suspense> </Suspense>
); );
@@ -372,11 +372,7 @@ export const AlbumArtistListHeaderFilters = ({
return ( return (
<Flex justify="space-between"> <Flex justify="space-between">
<Group <Group gap="sm" ref={cq.ref} w="100%">
gap="sm"
ref={cq.ref}
w="100%"
>
<DropdownMenu position="bottom-start"> <DropdownMenu position="bottom-start">
<DropdownMenu.Target> <DropdownMenu.Target>
<Button variant="subtle">{sortByLabel}</Button> <Button variant="subtle">{sortByLabel}</Button>
@@ -395,10 +391,7 @@ export const AlbumArtistListHeaderFilters = ({
</DropdownMenu.Dropdown> </DropdownMenu.Dropdown>
</DropdownMenu> </DropdownMenu>
<Divider orientation="vertical" /> <Divider orientation="vertical" />
<OrderToggleButton <OrderToggleButton onToggle={handleToggleSortOrder} sortOrder={filter.sortOrder} />
onToggle={handleToggleSortOrder}
sortOrder={filter.sortOrder}
/>
{server?.type === ServerType.JELLYFIN && ( {server?.type === ServerType.JELLYFIN && (
<> <>
<DropdownMenu position="bottom-start"> <DropdownMenu position="bottom-start">
@@ -437,10 +430,7 @@ export const AlbumArtistListHeaderFilters = ({
</DropdownMenu.Dropdown> </DropdownMenu.Dropdown>
</DropdownMenu> </DropdownMenu>
</Group> </Group>
<Group <Group gap="sm" wrap="nowrap">
gap="sm"
wrap="nowrap"
>
<ListConfigMenu <ListConfigMenu
autoFitColumns={table.autoFit} autoFitColumns={table.autoFit}
disabledViewTypes={[ListDisplayType.LIST]} disabledViewTypes={[ListDisplayType.LIST]}
@@ -46,15 +46,9 @@ export const AlbumArtistListHeader = ({
}, 500); }, 500);
return ( return (
<Stack <Stack gap={0} ref={cq.ref}>
gap={0}
ref={cq.ref}
>
<PageHeader> <PageHeader>
<Flex <Flex justify="space-between" w="100%">
justify="space-between"
w="100%"
>
<LibraryHeaderBar> <LibraryHeaderBar>
<LibraryHeaderBar.Title> <LibraryHeaderBar.Title>
{t('page.albumArtistList.title', { postProcess: 'titleCase' })} {t('page.albumArtistList.title', { postProcess: 'titleCase' })}
@@ -66,18 +60,12 @@ export const AlbumArtistListHeader = ({
</LibraryHeaderBar.Badge> </LibraryHeaderBar.Badge>
</LibraryHeaderBar> </LibraryHeaderBar>
<Group> <Group>
<SearchInput <SearchInput defaultValue={filter.searchTerm} onChange={handleSearch} />
defaultValue={filter.searchTerm}
onChange={handleSearch}
/>
</Group> </Group>
</Flex> </Flex>
</PageHeader> </PageHeader>
<FilterBar> <FilterBar>
<AlbumArtistListHeaderFilters <AlbumArtistListHeaderFilters gridRef={gridRef} tableRef={tableRef} />
gridRef={gridRef}
tableRef={tableRef}
/>
</FilterBar> </FilterBar>
</Stack> </Stack>
); );
@@ -34,15 +34,9 @@ export const ArtistListContent = ({ gridRef, itemCount, tableRef }: ArtistListCo
return ( return (
<Suspense fallback={<Spinner container />}> <Suspense fallback={<Spinner container />}>
{isGrid ? ( {isGrid ? (
<ArtistListGridView <ArtistListGridView gridRef={gridRef} itemCount={itemCount} />
gridRef={gridRef}
itemCount={itemCount}
/>
) : ( ) : (
<ArtistListTableView <ArtistListTableView itemCount={itemCount} tableRef={tableRef} />
itemCount={itemCount}
tableRef={tableRef}
/>
)} )}
</Suspense> </Suspense>
); );
@@ -388,11 +388,7 @@ export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderF
return ( return (
<Flex justify="space-between"> <Flex justify="space-between">
<Group <Group gap="sm" ref={cq.ref} w="100%">
gap="sm"
ref={cq.ref}
w="100%"
>
<DropdownMenu position="bottom-start"> <DropdownMenu position="bottom-start">
<DropdownMenu.Target> <DropdownMenu.Target>
<Button variant="subtle">{sortByLabel}</Button> <Button variant="subtle">{sortByLabel}</Button>
@@ -411,19 +407,13 @@ export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderF
</DropdownMenu.Dropdown> </DropdownMenu.Dropdown>
</DropdownMenu> </DropdownMenu>
<Divider orientation="vertical" /> <Divider orientation="vertical" />
<OrderToggleButton <OrderToggleButton onToggle={handleToggleSortOrder} sortOrder={filter.sortOrder} />
onToggle={handleToggleSortOrder}
sortOrder={filter.sortOrder}
/>
{server?.type === ServerType.JELLYFIN && ( {server?.type === ServerType.JELLYFIN && (
<> <>
<Divider orientation="vertical" /> <Divider orientation="vertical" />
<DropdownMenu position="bottom-start"> <DropdownMenu position="bottom-start">
<DropdownMenu.Target> <DropdownMenu.Target>
<ActionIcon <ActionIcon icon="folder" variant="subtle" />
icon="folder"
variant="subtle"
/>
</DropdownMenu.Target> </DropdownMenu.Target>
<DropdownMenu.Dropdown> <DropdownMenu.Dropdown>
{musicFoldersQuery.data?.items.map((folder) => ( {musicFoldersQuery.data?.items.map((folder) => (
@@ -442,11 +432,7 @@ export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderF
)} )}
{roles.data?.length && ( {roles.data?.length && (
<> <>
<Select <Select data={roles.data} onChange={handleSetRole} value={filter.role} />
data={roles.data}
onChange={handleSetRole}
value={filter.role}
/>
</> </>
)} )}
<RefreshButton onClick={handleRefresh} /> <RefreshButton onClick={handleRefresh} />
@@ -466,10 +452,7 @@ export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderF
</DropdownMenu.Dropdown> </DropdownMenu.Dropdown>
</DropdownMenu> </DropdownMenu>
</Group> </Group>
<Group <Group gap="xs" wrap="nowrap">
gap="xs"
wrap="nowrap"
>
<ListConfigMenu <ListConfigMenu
autoFitColumns={table.autoFit} autoFitColumns={table.autoFit}
displayType={display} displayType={display}
@@ -42,15 +42,9 @@ export const ArtistListHeader = ({ gridRef, itemCount, tableRef }: ArtistListHea
}, 500); }, 500);
return ( return (
<Stack <Stack gap={0} ref={cq.ref}>
gap={0}
ref={cq.ref}
>
<PageHeader> <PageHeader>
<Flex <Flex justify="space-between" w="100%">
justify="space-between"
w="100%"
>
<LibraryHeaderBar> <LibraryHeaderBar>
<LibraryHeaderBar.Title> <LibraryHeaderBar.Title>
{t('entity.artist_other', { postProcess: 'titleCase' })} {t('entity.artist_other', { postProcess: 'titleCase' })}
@@ -62,18 +56,12 @@ export const ArtistListHeader = ({ gridRef, itemCount, tableRef }: ArtistListHea
</LibraryHeaderBar.Badge> </LibraryHeaderBar.Badge>
</LibraryHeaderBar> </LibraryHeaderBar>
<Group> <Group>
<SearchInput <SearchInput defaultValue={filter.searchTerm} onChange={handleSearch} />
defaultValue={filter.searchTerm}
onChange={handleSearch}
/>
</Group> </Group>
</Flex> </Flex>
</PageHeader> </PageHeader>
<FilterBar> <FilterBar>
<ArtistListHeaderFilters <ArtistListHeaderFilters gridRef={gridRef} tableRef={tableRef} />
gridRef={gridRef}
tableRef={tableRef}
/>
</FilterBar> </FilterBar>
</Stack> </Stack>
); );
@@ -41,16 +41,8 @@ const ArtistListRoute = () => {
return ( return (
<AnimatedPage> <AnimatedPage>
<ListContext.Provider value={providerValue}> <ListContext.Provider value={providerValue}>
<ArtistListHeader <ArtistListHeader gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
gridRef={gridRef} <ArtistListContent gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
itemCount={itemCount}
tableRef={tableRef}
/>
<ArtistListContent
gridRef={gridRef}
itemCount={itemCount}
tableRef={tableRef}
/>
</ListContext.Provider> </ListContext.Provider>
</AnimatedPage> </AnimatedPage>
); );
@@ -533,10 +533,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
openModal({ openModal({
children: ( children: (
<ConfirmModal <ConfirmModal loading={removeFromPlaylistMutation.isLoading} onConfirm={confirm}>
loading={removeFromPlaylistMutation.isLoading}
onConfirm={confirm}
>
{t('common.areYouSure', { postProcess: 'sentenceCase' })} {t('common.areYouSure', { postProcess: 'sentenceCase' })}
</ConfirmModal> </ConfirmModal>
), ),
@@ -922,26 +919,15 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
<Portal> <Portal>
<AnimatePresence> <AnimatePresence>
{opened && ( {opened && (
<ContextMenu <ContextMenu minWidth={125} ref={mergedRef} xPos={ctx.xPos} yPos={ctx.yPos}>
minWidth={125}
ref={mergedRef}
xPos={ctx.xPos}
yPos={ctx.yPos}
>
<Stack gap={0}> <Stack gap={0}>
<Stack <Stack gap={0} onClick={closeContextMenu}>
gap={0}
onClick={closeContextMenu}
>
{ctx.menuItems?.map((item) => { {ctx.menuItems?.map((item) => {
return ( return (
!contextMenuItems[item.id].disabled && ( !contextMenuItems[item.id].disabled && (
<Fragment key={`context-menu-${item.id}`}> <Fragment key={`context-menu-${item.id}`}>
{item.children ? ( {item.children ? (
<HoverCard <HoverCard offset={0} position="right">
offset={0}
position="right"
>
<HoverCard.Target> <HoverCard.Target>
<ContextMenuButton <ContextMenuButton
leftIcon={ leftIcon={
@@ -964,7 +950,12 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
</ContextMenuButton> </ContextMenuButton>
</HoverCard.Target> </HoverCard.Target>
<HoverCard.Dropdown> <HoverCard.Dropdown>
<Stack gap={0}> <Stack
gap={0}
// Pass in this ref to the stack component as well
// so that it is treated as "inside" for clickOutsideRef
ref={mergedRef}
>
{contextMenuItems[ {contextMenuItems[
item.id item.id
].children?.map((child) => ( ].children?.map((child) => (
@@ -26,10 +26,8 @@ export const useDiscordRpc = () => {
) => { ) => {
if ( if (
!current[0] || // No track !current[0] || // No track
(current[0] && current[1] === 0 || // Start of track
current[2] === 'paused' && // Track paused (current[2] === 'paused' && !discordSettings.showPaused) // Track paused with show paused setting disabled
(discordSettings.showPaused ? current[1] === 0 : true)) || // Beginning of track (only if show paused setting enabled)
(discordSettings.showPaused ? false : current[1] === 0) // Beginning of track (only if show paused setting disabled)
) )
return discordRpc?.clearActivity(); return discordRpc?.clearActivity();
@@ -38,11 +36,13 @@ export const useDiscordRpc = () => {
const trackChanged = lastUniqueId !== song.uniqueId; const trackChanged = lastUniqueId !== song.uniqueId;
/* /*
1. If we jump more then 1.2 seconds from last state, update status to match 1. If the song has just started, update status
2. If the current song id is completely different, update status 2. If we jump more then 1.2 seconds from last state, update status to match
3. If the player state changed, update status 3. If the current song id is completely different, update status
4. If the player state changed, update status
*/ */
if ( if (
previous[1] === 0 ||
Math.abs((current[1] as number) - (previous[1] as number)) > 1.2 || Math.abs((current[1] as number) - (previous[1] as number)) > 1.2 ||
trackChanged || trackChanged ||
current[2] !== previous[2] current[2] !== previous[2]
@@ -121,6 +121,10 @@ export const useDiscordRpc = () => {
activity.largeImageKey = 'icon'; activity.largeImageKey = 'icon';
} }
// Initialize if needed
const isConnected = await discordRpc?.isConnected();
if (!isConnected) await discordRpc?.initialize(discordSettings.clientId);
discordRpc?.setActivity(activity); discordRpc?.setActivity(activity);
} }
}, },
@@ -129,6 +133,7 @@ export const useDiscordRpc = () => {
discordSettings.showServerImage, discordSettings.showServerImage,
discordSettings.showPaused, discordSettings.showPaused,
generalSettings.lastfmApiKey, generalSettings.lastfmApiKey,
discordSettings.clientId,
lastUniqueId, lastUniqueId,
], ],
); );
@@ -136,7 +141,6 @@ export const useDiscordRpc = () => {
useEffect(() => { useEffect(() => {
if (!discordSettings.enabled) return discordRpc?.quit(); if (!discordSettings.enabled) return discordRpc?.quit();
discordRpc?.initialize(discordSettings.clientId);
return () => { return () => {
discordRpc?.quit(); discordRpc?.quit();
}; };
@@ -33,15 +33,9 @@ export const GenreListContent = ({ gridRef, itemCount, tableRef }: AlbumListCont
return ( return (
<Suspense fallback={<Spinner container />}> <Suspense fallback={<Spinner container />}>
{display === ListDisplayType.CARD || display === ListDisplayType.GRID ? ( {display === ListDisplayType.CARD || display === ListDisplayType.GRID ? (
<GenreListGridView <GenreListGridView gridRef={gridRef} itemCount={itemCount} />
gridRef={gridRef}
itemCount={itemCount}
/>
) : ( ) : (
<GenreListTableView <GenreListTableView itemCount={itemCount} tableRef={tableRef} />
itemCount={itemCount}
tableRef={tableRef}
/>
)} )}
</Suspense> </Suspense>
); );
@@ -254,11 +254,7 @@ export const GenreListHeaderFilters = ({
return ( return (
<Flex justify="space-between"> <Flex justify="space-between">
<Group <Group gap="sm" ref={cq.ref} w="100%">
gap="sm"
ref={cq.ref}
w="100%"
>
<DropdownMenu position="bottom-start"> <DropdownMenu position="bottom-start">
<DropdownMenu.Target> <DropdownMenu.Target>
<Button variant="subtle">{sortByLabel}</Button> <Button variant="subtle">{sortByLabel}</Button>
@@ -277,10 +273,7 @@ export const GenreListHeaderFilters = ({
</DropdownMenu.Dropdown> </DropdownMenu.Dropdown>
</DropdownMenu> </DropdownMenu>
<Divider orientation="vertical" /> <Divider orientation="vertical" />
<OrderToggleButton <OrderToggleButton onToggle={handleToggleSortOrder} sortOrder={filter.sortOrder} />
onToggle={handleToggleSortOrder}
sortOrder={filter.sortOrder}
/>
{server?.type === ServerType.JELLYFIN && ( {server?.type === ServerType.JELLYFIN && (
<> <>
<Divider orientation="vertical" /> <Divider orientation="vertical" />
@@ -340,10 +333,7 @@ export const GenreListHeaderFilters = ({
</Button> </Button>
</DropdownMenu> </DropdownMenu>
</Group> </Group>
<Group <Group gap="sm" wrap="nowrap">
gap="sm"
wrap="nowrap"
>
<ListConfigMenu <ListConfigMenu
autoFitColumns={table.autoFit} autoFitColumns={table.autoFit}
disabledViewTypes={[ListDisplayType.LIST]} disabledViewTypes={[ListDisplayType.LIST]}
@@ -40,15 +40,9 @@ export const GenreListHeader = ({ gridRef, itemCount, tableRef }: GenreListHeade
}, 500); }, 500);
return ( return (
<Stack <Stack gap={0} ref={cq.ref}>
gap={0}
ref={cq.ref}
>
<PageHeader> <PageHeader>
<Flex <Flex justify="space-between" w="100%">
justify="space-between"
w="100%"
>
<LibraryHeaderBar> <LibraryHeaderBar>
<LibraryHeaderBar.Title> <LibraryHeaderBar.Title>
{t('page.genreList.title', { postProcess: 'titleCase' })} {t('page.genreList.title', { postProcess: 'titleCase' })}
@@ -60,10 +54,7 @@ export const GenreListHeader = ({ gridRef, itemCount, tableRef }: GenreListHeade
</LibraryHeaderBar.Badge> </LibraryHeaderBar.Badge>
</LibraryHeaderBar> </LibraryHeaderBar>
<Group> <Group>
<SearchInput <SearchInput defaultValue={filter.searchTerm} onChange={handleSearch} />
defaultValue={filter.searchTerm}
onChange={handleSearch}
/>
</Group> </Group>
</Flex> </Flex>
</PageHeader> </PageHeader>
@@ -42,16 +42,8 @@ const GenreListRoute = () => {
return ( return (
<AnimatedPage> <AnimatedPage>
<ListContext.Provider value={providerValue}> <ListContext.Provider value={providerValue}>
<GenreListHeader <GenreListHeader gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
gridRef={gridRef} <GenreListContent gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
itemCount={itemCount}
tableRef={tableRef}
/>
<GenreListContent
gridRef={gridRef}
itemCount={itemCount}
tableRef={tableRef}
/>
</ListContext.Provider> </ListContext.Provider>
</AnimatedPage> </AnimatedPage>
); );
@@ -81,10 +81,7 @@ const formatArtists = (artists: null | RelatedArtist[] | undefined) =>
{artist.name || '—'} {artist.name || '—'}
</Text> </Text>
) : ( ) : (
<Text <Text overflow="visible" size="md">
overflow="visible"
size="md"
>
{artist.name || '-'} {artist.name || '-'}
</Text> </Text>
)} )}
@@ -119,17 +116,7 @@ const FormatGenre = (item: Album | AlbumArtist | Playlist | Song) => {
}; };
const BoolField = (key: boolean) => const BoolField = (key: boolean) =>
key ? ( key ? <Icon color="success" icon="check" /> : <Icon color="error" icon="x" />;
<Icon
color="success"
icon="check"
/>
) : (
<Icon
color="error"
icon="x"
/>
);
const AlbumPropertyMapping: ItemDetailRow<Album>[] = [ const AlbumPropertyMapping: ItemDetailRow<Album>[] = [
{ key: 'name', label: 'common.title' }, { key: 'name', label: 'common.title' },
@@ -287,6 +274,8 @@ const SongPropertyMapping: ItemDetailRow<Song>[] = [
{ label: 'filter.isCompilation', render: (song) => BoolField(song.compilation || false) }, { label: 'filter.isCompilation', render: (song) => BoolField(song.compilation || false) },
{ key: 'container', label: 'common.codec' }, { key: 'container', label: 'common.codec' },
{ key: 'bitRate', label: 'common.bitrate', render: (song) => `${song.bitRate} kbps` }, { key: 'bitRate', label: 'common.bitrate', render: (song) => `${song.bitRate} kbps` },
{ key: 'sampleRate', label: 'common.sampleRate' },
{ key: 'bitDepth', label: 'common.bitDepth' },
{ key: 'channels', label: 'common.channel_other' }, { key: 'channels', label: 'common.channel_other' },
{ key: 'size', label: 'common.size', render: (song) => formatSizeString(song.size) }, { key: 'size', label: 'common.size', render: (song) => formatSizeString(song.size) },
{ {
@@ -409,12 +398,7 @@ export const ItemDetailsModal = ({ item }: ItemDetailsModalProps) => {
} }
return ( return (
<Table <Table highlightOnHover variant="vertical" withRowBorders={false} withTableBorder>
highlightOnHover
variant="vertical"
withRowBorders={false}
withTableBorder
>
<Table.Tbody>{body}</Table.Tbody> <Table.Tbody>{body}</Table.Tbody>
</Table> </Table>
); );
@@ -22,10 +22,7 @@ export const SongPath = ({ path }: SongPathProps) => {
return ( return (
<Group> <Group>
<CopyButton <CopyButton timeout={2000} value={path}>
timeout={2000}
value={path}
>
{({ copied, copy }) => ( {({ copied, copy }) => (
<Tooltip <Tooltip
label={t( label={t(
@@ -36,10 +33,7 @@ export const SongPath = ({ path }: SongPathProps) => {
)} )}
withinPortal withinPortal
> >
<ActionIcon <ActionIcon onClick={copy} variant="transparent">
onClick={copy}
variant="transparent"
>
{copied ? <Icon icon="check" /> : <Icon icon="clipboardCopy" />} {copied ? <Icon icon="check" /> : <Icon icon="clipboardCopy" />}
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
@@ -38,33 +38,15 @@ const SearchResult = ({ data, onClick }: SearchResultProps) => {
source === LyricSource.GENIUS ? id.replace(/^((http[s]?|ftp):\/)?\/?([^:/\s]+)/g, '') : id; source === LyricSource.GENIUS ? id.replace(/^((http[s]?|ftp):\/)?\/?([^:/\s]+)/g, '') : id;
return ( return (
<button <button className={styles.searchItem} onClick={onClick}>
className={styles.searchItem} <Group justify="space-between" wrap="nowrap">
onClick={onClick} <Stack gap={0} maw="65%">
> <Text fw={600} size="md">
<Group
justify="space-between"
wrap="nowrap"
>
<Stack
gap={0}
maw="65%"
>
<Text
fw={600}
size="md"
>
{name} {name}
</Text> </Text>
<Text isMuted>{artist}</Text> <Text isMuted>{artist}</Text>
<Group <Group gap="sm" wrap="nowrap">
gap="sm" <Text isMuted size="sm">
wrap="nowrap"
>
<Text
isMuted
size="sm"
>
{[source, cleanId].join(' — ')} {[source, cleanId].join(' — ')}
</Text> </Text>
</Group> </Group>
@@ -167,11 +149,7 @@ export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearch
export const openLyricSearchModal = ({ artist, name, onSearchOverride }: LyricSearchFormProps) => { export const openLyricSearchModal = ({ artist, name, onSearchOverride }: LyricSearchFormProps) => {
openModal({ openModal({
children: ( children: (
<LyricsSearchForm <LyricsSearchForm artist={artist} name={name} onSearchOverride={onSearchOverride} />
artist={artist}
name={name}
onSearchOverride={onSearchOverride}
/>
), ),
size: 'lg', size: 'lg',
title: i18n.t('form.lyricSearch.title', { postProcess: 'titleCase' }) as string, title: i18n.t('form.lyricSearch.title', { postProcess: 'titleCase' }) as string,
+1 -4
View File
@@ -151,10 +151,7 @@ export const Lyrics = () => {
<ErrorBoundary FallbackComponent={ErrorFallback}> <ErrorBoundary FallbackComponent={ErrorFallback}>
<div className={styles.lyricsContainer}> <div className={styles.lyricsContainer}>
{isLoadingLyrics ? ( {isLoadingLyrics ? (
<Spinner <Spinner container size={25} />
container
size={25}
/>
) : ( ) : (
<AnimatePresence mode="sync"> <AnimatePresence mode="sync">
{hasNoLyrics ? ( {hasNoLyrics ? (
@@ -1,6 +1,6 @@
import clsx from 'clsx'; import clsx from 'clsx';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { Fragment, useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import styles from './synchronized-lyrics.module.css'; import styles from './synchronized-lyrics.module.css';
@@ -338,25 +338,18 @@ export const SynchronizedLyrics = ({
/> />
)} )}
{lyrics.map(([time, text], idx) => ( {lyrics.map(([time, text], idx) => (
<Fragment key={idx}> <LyricLine
<LyricLine alignment={settings.alignment}
alignment={settings.alignment} className="lyric-line synchronized"
className="lyric-line synchronized" fontSize={settings.fontSize}
fontSize={settings.fontSize} id={`lyric-${idx}`}
id={`lyric-${idx}`} key={idx}
onClick={() => handleSeek(time / 1000)} onClick={() => handleSeek(time / 1000)}
text={text} text={
/> text +
{translatedLyrics && ( (translatedLyrics ? `_BREAK_${translatedLyrics.split('\n')[idx]}` : '')
<LyricLine }
alignment={settings.alignment} />
className="lyric-line synchronized translation"
fontSize={settings.fontSize * 0.8}
onClick={() => handleSeek(time / 1000)}
text={translatedLyrics.split('\n')[idx]}
/>
)}
</Fragment>
))} ))}
</div> </div>
); );
@@ -29,10 +29,7 @@ export const UnsynchronizedLyrics = ({
}, [translatedLyrics]); }, [translatedLyrics]);
return ( return (
<div <div className={styles.container} style={{ gap: `${settings.gapUnsync}px` }}>
className={styles.container}
style={{ gap: `${settings.gapUnsync}px` }}
>
{settings.showProvider && source && ( {settings.showProvider && source && (
<LyricLine <LyricLine
alignment={settings.alignment} alignment={settings.alignment}
@@ -50,23 +47,14 @@ export const UnsynchronizedLyrics = ({
/> />
)} )}
{lines.map((text, idx) => ( {lines.map((text, idx) => (
<div key={idx}> <LyricLine
<LyricLine alignment={settings.alignment}
alignment={settings.alignment} className="lyric-line unsynchronized"
className="lyric-line unsynchronized" fontSize={settings.fontSizeUnsync}
fontSize={settings.fontSizeUnsync} id={`lyric-${idx}`}
id={`lyric-${idx}`} key={idx}
text={text} text={text + (translatedLines[idx] ? `_BREAK_${translatedLines[idx]}` : '')}
/> />
{translatedLines[idx] && (
<LyricLine
alignment={settings.alignment}
className="lyric-line unsynchronized translation"
fontSize={settings.fontSizeUnsync * 0.8}
text={translatedLines[idx]}
/>
)}
</div>
))} ))}
</div> </div>
); );
@@ -11,30 +11,17 @@ export const DrawerPlayQueue = () => {
const queueRef = useRef<null | { grid: AgGridReactType<Song> }>(null); const queueRef = useRef<null | { grid: AgGridReactType<Song> }>(null);
return ( return (
<Flex <Flex direction="column" h="100%">
direction="column"
h="100%"
>
<div <div
style={{ style={{
backgroundColor: 'var(--theme-colors-background)', backgroundColor: 'var(--theme-colors-background)',
borderRadius: '10px', borderRadius: '10px',
}} }}
> >
<PlayQueueListControls <PlayQueueListControls tableRef={queueRef} type="sideQueue" />
tableRef={queueRef}
type="sideQueue"
/>
</div> </div>
<Flex <Flex bg="var(--theme-colors-background)" h="100%" mb="0.6rem">
bg="var(--theme-colors-background)" <PlayQueue ref={queueRef} type="sideQueue" />
h="100%"
mb="0.6rem"
>
<PlayQueue
ref={queueRef}
type="sideQueue"
/>
</Flex> </Flex>
</Flex> </Flex>
); );
@@ -174,10 +174,7 @@ export const PlayQueueListControls = ({ tableRef, type }: PlayQueueListOptionsPr
/> />
</Group> </Group>
<Group> <Group>
<Popover <Popover position="top-end" transitionProps={{ transition: 'fade' }}>
position="top-end"
transitionProps={{ transition: 'fade' }}
>
<Popover.Target> <Popover.Target>
<ActionIcon <ActionIcon
icon="settings" icon="settings"
@@ -18,19 +18,10 @@ export const SidebarPlayQueue = () => {
const isWeb = windowBarStyle === Platform.WEB; const isWeb = windowBarStyle === Platform.WEB;
return ( return (
<VirtualGridContainer> <VirtualGridContainer>
<Box <Box display={!isWeb ? 'flex' : undefined} h="65px">
display={!isWeb ? 'flex' : undefined} <PlayQueueListControls tableRef={queueRef} type="sideQueue" />
h="65px"
>
<PlayQueueListControls
tableRef={queueRef}
type="sideQueue"
/>
</Box> </Box>
<PlayQueue <PlayQueue ref={queueRef} type="sideQueue" />
ref={queueRef}
type="sideQueue"
/>
</VirtualGridContainer> </VirtualGridContainer>
); );
}; };
@@ -16,14 +16,8 @@ const NowPlayingRoute = () => {
<AnimatedPage> <AnimatedPage>
<VirtualGridContainer> <VirtualGridContainer>
<NowPlayingHeader /> <NowPlayingHeader />
<PlayQueueListControls <PlayQueueListControls tableRef={queueRef} type="nowPlaying" />
tableRef={queueRef} <PlayQueue ref={queueRef} type="nowPlaying" />
type="nowPlaying"
/>
<PlayQueue
ref={queueRef}
type="nowPlaying"
/>
</VirtualGridContainer> </VirtualGridContainer>
</AnimatedPage> </AnimatedPage>
); );
@@ -115,13 +115,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
<div className={styles.controlsContainer}> <div className={styles.controlsContainer}>
<div className={styles.buttonsContainer}> <div className={styles.buttonsContainer}>
<PlayerButton <PlayerButton
icon={ icon={<Icon fill="default" icon="mediaStop" size={buttonSize - 2} />}
<Icon
fill="default"
icon="mediaStop"
size={buttonSize - 2}
/>
}
onClick={handleStop} onClick={handleStop}
tooltip={{ tooltip={{
label: t('player.stop', { postProcess: 'sentenceCase' }), label: t('player.stop', { postProcess: 'sentenceCase' }),
@@ -152,13 +146,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
variant="tertiary" variant="tertiary"
/> />
<PlayerButton <PlayerButton
icon={ icon={<Icon fill="default" icon="mediaPrevious" size={buttonSize} />}
<Icon
fill="default"
icon="mediaPrevious"
size={buttonSize}
/>
}
onClick={handlePrevTrack} onClick={handlePrevTrack}
tooltip={{ tooltip={{
label: t('player.previous', { postProcess: 'sentenceCase' }), label: t('player.previous', { postProcess: 'sentenceCase' }),
@@ -169,11 +157,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
{skip?.enabled && ( {skip?.enabled && (
<PlayerButton <PlayerButton
icon={ icon={
<Icon <Icon fill="default" icon="mediaStepBackward" size={buttonSize} />
fill="default"
icon="mediaStepBackward"
size={buttonSize}
/>
} }
onClick={() => handleSkipBackward(skip?.skipBackwardSeconds)} onClick={() => handleSkipBackward(skip?.skipBackwardSeconds)}
tooltip={{ tooltip={{
@@ -194,13 +178,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
/> />
{skip?.enabled && ( {skip?.enabled && (
<PlayerButton <PlayerButton
icon={ icon={<Icon fill="default" icon="mediaStepForward" size={buttonSize} />}
<Icon
fill="default"
icon="mediaStepForward"
size={buttonSize}
/>
}
onClick={() => handleSkipForward(skip?.skipForwardSeconds)} onClick={() => handleSkipForward(skip?.skipForwardSeconds)}
tooltip={{ tooltip={{
label: t('player.skip', { label: t('player.skip', {
@@ -214,13 +192,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
/> />
)} )}
<PlayerButton <PlayerButton
icon={ icon={<Icon fill="default" icon="mediaNext" size={buttonSize} />}
<Icon
fill="default"
icon="mediaNext"
size={buttonSize}
/>
}
onClick={handleNextTrack} onClick={handleNextTrack}
tooltip={{ tooltip={{
label: t('player.next', { postProcess: 'sentenceCase' }), label: t('player.next', { postProcess: 'sentenceCase' }),
@@ -231,11 +203,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
<PlayerButton <PlayerButton
icon={ icon={
repeat === PlayerRepeat.ONE ? ( repeat === PlayerRepeat.ONE ? (
<Icon <Icon fill="primary" icon="mediaRepeatOne" size={buttonSize} />
fill="primary"
icon="mediaRepeatOne"
size={buttonSize}
/>
) : ( ) : (
<Icon <Icon
fill={repeat === PlayerRepeat.NONE ? 'default' : 'primary'} fill={repeat === PlayerRepeat.NONE ? 'default' : 'primary'}
@@ -268,13 +236,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
variant="tertiary" variant="tertiary"
/> />
<PlayerButton <PlayerButton
icon={ icon={<Icon fill="default" icon="mediaRandom" size={buttonSize} />}
<Icon
fill="default"
icon="mediaRandom"
size={buttonSize}
/>
}
onClick={() => onClick={() =>
openShuffleAllModal({ openShuffleAllModal({
handlePlayQueueAdd, handlePlayQueueAdd,
@@ -291,12 +253,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
</div> </div>
<div className={styles.sliderContainer}> <div className={styles.sliderContainer}>
<div className={styles.sliderValueWrapper}> <div className={styles.sliderValueWrapper}>
<Text <Text fw={600} isMuted isNoSelect size="xs">
fw={600}
isMuted
isNoSelect
size="xs"
>
{formattedTime} {formattedTime}
</Text> </Text>
</div> </div>
@@ -324,12 +281,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
/> />
</div> </div>
<div className={styles.sliderValueWrapper}> <div className={styles.sliderValueWrapper}>
<Text <Text fw={600} isMuted isNoSelect size="xs">
fw={600}
isMuted
isNoSelect
size="xs"
>
{duration} {duration}
</Text> </Text>
</div> </div>
@@ -68,11 +68,7 @@ const ImageWithPlaceholder = ({
width: '100%', width: '100%',
}} }}
> >
<Icon <Icon color="muted" icon="itemAlbum" size="25%" />
color="muted"
icon="itemAlbum"
size="25%"
/>
</Center> </Center>
); );
} }
@@ -167,14 +163,8 @@ export const FullScreenPlayerImage = () => {
justify="flex-start" justify="flex-start"
p="1rem" p="1rem"
> >
<div <div className={styles.imageContainer} ref={mainImageRef}>
className={styles.imageContainer} <AnimatePresence initial={false} mode="sync">
ref={mainImageRef}
>
<AnimatePresence
initial={false}
mode="sync"
>
{imageState.current === 0 && ( {imageState.current === 0 && (
<ImageWithPlaceholder <ImageWithPlaceholder
animate="open" animate="open"
@@ -206,18 +196,8 @@ export const FullScreenPlayerImage = () => {
)} )}
</AnimatePresence> </AnimatePresence>
</div> </div>
<Stack <Stack className={styles.metadataContainer} gap="md" maw="100%">
className={styles.metadataContainer} <Text fw={900} lh="1.2" overflow="hidden" size="4xl" w="100%">
gap="md"
maw="100%"
>
<Text
fw={900}
lh="1.2"
overflow="hidden"
size="4xl"
w="100%"
>
{currentSong?.name} {currentSong?.name}
</Text> </Text>
<Text <Text
@@ -257,10 +237,7 @@ export const FullScreenPlayerImage = () => {
</Fragment> </Fragment>
))} ))}
</Text> </Text>
<Group <Group justify="center" mt="sm">
justify="center"
mt="sm"
>
{currentSong?.container && ( {currentSong?.container && (
<Badge variant="transparent">{currentSong?.container}</Badge> <Badge variant="transparent">{currentSong?.container}</Badge>
)} )}
@@ -76,10 +76,7 @@ export const FullScreenPlayerQueue = () => {
justify="center" justify="center"
> >
{headerItems.map((item) => ( {headerItems.map((item) => (
<div <div className={styles.headerItemWrapper} key={`tab-${item.label}`}>
className={styles.headerItemWrapper}
key={`tab-${item.label}`}
>
<Button <Button
flex={1} flex={1}
fw="600" fw="600"

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