mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ab0eba23e | |||
| 08fc307516 | |||
| e5adc0caa9 | |||
| 7f0bdf20fc | |||
| deb89ef87d | |||
| 9751e22db4 | |||
| 6128470a47 | |||
| ea79885ef5 | |||
| 84fd6e482d | |||
| 904f05ff61 | |||
| e78ec7688a | |||
| f43de7f23c | |||
| 07532ca55a | |||
| 040a805f5f | |||
| 33af5e625b | |||
| bdedcb883d | |||
| e842a75722 | |||
| d28054cc7f | |||
| 41f8c34f6e | |||
| 4b4df28641 | |||
| 8b141d652c | |||
| 92ed8e20c9 | |||
| 746ab8c2d9 | |||
| 69341f4492 | |||
| 56130d8503 | |||
| 4407c8d424 | |||
| 58bb8e716e | |||
| 0becfd4b59 |
+1
-1
@@ -8,7 +8,7 @@ arrowParens: always
|
||||
proseWrap: never
|
||||
htmlWhitespaceSensitivity: strict
|
||||
endOfLine: lf
|
||||
singleAttributePerLine: true
|
||||
singleAttributePerLine: false
|
||||
bracketSpacing: true
|
||||
plugins:
|
||||
- prettier-plugin-packagejson
|
||||
|
||||
@@ -129,6 +129,7 @@ Feishin supports any music server that implements a [Navidrome](https://www.navi
|
||||
- [LMS](https://github.com/epoupon/lms)
|
||||
- [Nextcloud Music](https://apps.nextcloud.com/apps/music)
|
||||
- [Supysonic](https://github.com/spl0k/supysonic)
|
||||
- [Qm-Music](https://github.com/chenqimiao/qm-music)
|
||||
- More (?)
|
||||
|
||||
### I have the issue "The SUID sandbox helper binary was found, but is not configured correctly" on Linux
|
||||
|
||||
+1
-27
@@ -35,39 +35,13 @@ mac:
|
||||
notarize: false
|
||||
dmg:
|
||||
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:
|
||||
target:
|
||||
- AppImage
|
||||
- tar.xz
|
||||
category: AudioVideo;Audio;Player
|
||||
icon: assets/icons/icon.png
|
||||
artifactName: ${productName}-${os}-${arch}.${ext}
|
||||
npmRebuild: false
|
||||
publish:
|
||||
provider: github
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "feishin",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"description": "A modern self-hosted music player.",
|
||||
"keywords": [
|
||||
"subsonic",
|
||||
|
||||
Generated
+47
-41
@@ -766,6 +766,10 @@ packages:
|
||||
resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==}
|
||||
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':
|
||||
resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -778,8 +782,8 @@ packages:
|
||||
resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@eslint/plugin-kit@0.3.1':
|
||||
resolution: {integrity: sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==}
|
||||
'@eslint/plugin-kit@0.3.4':
|
||||
resolution: {integrity: sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@floating-ui/core@1.7.0':
|
||||
@@ -834,26 +838,24 @@ packages:
|
||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.8':
|
||||
resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
'@jridgewell/gen-mapping@0.3.12':
|
||||
resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==}
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2':
|
||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@jridgewell/set-array@1.2.1':
|
||||
resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@jridgewell/source-map@0.3.6':
|
||||
resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==}
|
||||
'@jridgewell/source-map@0.3.10':
|
||||
resolution: {integrity: sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.0':
|
||||
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.25':
|
||||
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
||||
'@jridgewell/sourcemap-codec@1.5.4':
|
||||
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':
|
||||
resolution: {integrity: sha512-qnEovoOp5Np2JDGonIDL6Ayihw0RhnRh6vxPuHo4RDn1UOzwEo4AeIfpL6UGIrsceWrCMiVPgwRjbHu4vYFc3g==}
|
||||
@@ -2370,8 +2372,8 @@ packages:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
form-data@4.0.2:
|
||||
resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
|
||||
form-data@4.0.4:
|
||||
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
format-duration@2.0.0:
|
||||
@@ -4128,8 +4130,8 @@ packages:
|
||||
resolution: {integrity: sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
socks@2.8.5:
|
||||
resolution: {integrity: sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==}
|
||||
socks@2.8.6:
|
||||
resolution: {integrity: sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==}
|
||||
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
|
||||
|
||||
sort-keys@5.1.0:
|
||||
@@ -4786,8 +4788,8 @@ snapshots:
|
||||
|
||||
'@ampproject/remapping@2.3.0':
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.8
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
'@jridgewell/gen-mapping': 0.3.12
|
||||
'@jridgewell/trace-mapping': 0.3.29
|
||||
|
||||
'@atlaskit/pragmatic-drag-and-drop-auto-scroll@2.1.1':
|
||||
dependencies:
|
||||
@@ -4843,8 +4845,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/parser': 7.27.2
|
||||
'@babel/types': 7.27.1
|
||||
'@jridgewell/gen-mapping': 0.3.8
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
'@jridgewell/gen-mapping': 0.3.12
|
||||
'@jridgewell/trace-mapping': 0.3.29
|
||||
jsesc: 3.1.0
|
||||
|
||||
'@babel/helper-compilation-targets@7.27.2':
|
||||
@@ -5206,6 +5208,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/json-schema': 7.0.15
|
||||
|
||||
'@eslint/core@0.15.1':
|
||||
dependencies:
|
||||
'@types/json-schema': 7.0.15
|
||||
|
||||
'@eslint/eslintrc@3.3.1':
|
||||
dependencies:
|
||||
ajv: 6.12.6
|
||||
@@ -5224,9 +5230,9 @@ snapshots:
|
||||
|
||||
'@eslint/object-schema@2.1.6': {}
|
||||
|
||||
'@eslint/plugin-kit@0.3.1':
|
||||
'@eslint/plugin-kit@0.3.4':
|
||||
dependencies:
|
||||
'@eslint/core': 0.14.0
|
||||
'@eslint/core': 0.15.1
|
||||
levn: 0.4.1
|
||||
|
||||
'@floating-ui/core@1.7.0':
|
||||
@@ -5282,28 +5288,27 @@ snapshots:
|
||||
wrap-ansi: 8.1.0
|
||||
wrap-ansi-cjs: wrap-ansi@7.0.0
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.8':
|
||||
'@jridgewell/gen-mapping@0.3.12':
|
||||
dependencies:
|
||||
'@jridgewell/set-array': 1.2.1
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
'@jridgewell/sourcemap-codec': 1.5.4
|
||||
'@jridgewell/trace-mapping': 0.3.29
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2': {}
|
||||
|
||||
'@jridgewell/set-array@1.2.1': {}
|
||||
|
||||
'@jridgewell/source-map@0.3.6':
|
||||
'@jridgewell/source-map@0.3.10':
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.8
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
'@jridgewell/gen-mapping': 0.3.12
|
||||
'@jridgewell/trace-mapping': 0.3.29
|
||||
optional: true
|
||||
|
||||
'@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:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
'@jridgewell/sourcemap-codec': 1.5.4
|
||||
|
||||
'@keyv/serialize@1.0.3':
|
||||
dependencies:
|
||||
@@ -6062,7 +6067,7 @@ snapshots:
|
||||
axios@1.9.0:
|
||||
dependencies:
|
||||
follow-redirects: 1.15.9
|
||||
form-data: 4.0.2
|
||||
form-data: 4.0.4
|
||||
proxy-from-env: 1.1.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
@@ -6708,7 +6713,7 @@ snapshots:
|
||||
builder-util: 26.0.11
|
||||
builder-util-runtime: 9.3.1
|
||||
chalk: 4.1.2
|
||||
form-data: 4.0.2
|
||||
form-data: 4.0.4
|
||||
fs-extra: 10.1.0
|
||||
lazy-val: 1.0.5
|
||||
mime: 2.6.0
|
||||
@@ -7009,7 +7014,7 @@ snapshots:
|
||||
'@eslint/core': 0.14.0
|
||||
'@eslint/eslintrc': 3.3.1
|
||||
'@eslint/js': 9.27.0
|
||||
'@eslint/plugin-kit': 0.3.1
|
||||
'@eslint/plugin-kit': 0.3.4
|
||||
'@humanfs/node': 0.16.6
|
||||
'@humanwhocodes/module-importer': 1.0.1
|
||||
'@humanwhocodes/retry': 0.4.3
|
||||
@@ -7171,11 +7176,12 @@ snapshots:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
form-data@4.0.2:
|
||||
form-data@4.0.4:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
es-set-tostringtag: 2.1.0
|
||||
hasown: 2.0.2
|
||||
mime-types: 2.1.35
|
||||
|
||||
format-duration@2.0.0: {}
|
||||
@@ -8940,11 +8946,11 @@ snapshots:
|
||||
dependencies:
|
||||
agent-base: 6.0.2
|
||||
debug: 4.4.1
|
||||
socks: 2.8.5
|
||||
socks: 2.8.6
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
socks@2.8.5:
|
||||
socks@2.8.6:
|
||||
dependencies:
|
||||
ip-address: 9.0.5
|
||||
smart-buffer: 4.2.0
|
||||
@@ -9244,7 +9250,7 @@ snapshots:
|
||||
|
||||
terser@5.39.2:
|
||||
dependencies:
|
||||
'@jridgewell/source-map': 0.3.6
|
||||
'@jridgewell/source-map': 0.3.10
|
||||
acorn: 8.15.0
|
||||
commander: 2.20.3
|
||||
source-map-support: 0.5.21
|
||||
|
||||
@@ -271,7 +271,9 @@
|
||||
"discordPausedStatus": "zobrazit rich presence při pozastavení",
|
||||
"discordPausedStatus_description": "pokud je povoleno, bude při pozastavení přehrávače zobrazen stav",
|
||||
"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": {
|
||||
"editPlaylist": "upravit $t(entity.playlist_one)",
|
||||
@@ -393,7 +395,9 @@
|
||||
"additionalParticipants": "další přispívající",
|
||||
"tags": "štítky",
|
||||
"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": {
|
||||
"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.",
|
||||
"networkError": "vyskytla se chyba sítě",
|
||||
"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": {
|
||||
"mostPlayed": "nejvíce přehráváno",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"ascending": "ascending",
|
||||
"backward": "backward",
|
||||
"biography": "biography",
|
||||
"bitDepth": "bit depth",
|
||||
"bitrate": "bitrate",
|
||||
"bpm": "bpm",
|
||||
"cancel": "cancel",
|
||||
@@ -99,6 +100,7 @@
|
||||
"resetToDefault": "reset to default",
|
||||
"restartRequired": "restart required",
|
||||
"right": "right",
|
||||
"sampleRate": "sample rate",
|
||||
"save": "save",
|
||||
"saveAndReplace": "save and replace",
|
||||
"saveAs": "save as",
|
||||
|
||||
@@ -271,7 +271,9 @@
|
||||
"discordPausedStatus": "Mostrar estado de actividad cuando esté en pausa",
|
||||
"discordPausedStatus_description": "Cuando está activado, el estado mostrará cuando el reproductor esté en pausa",
|
||||
"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": {
|
||||
"editPlaylist": "editar $t(entity.playlist_one)",
|
||||
@@ -393,7 +395,9 @@
|
||||
"additionalParticipants": "Participantes adicionales",
|
||||
"tags": "Etiquetas",
|
||||
"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": {
|
||||
"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.",
|
||||
"networkError": "Ocurrió un error de red",
|
||||
"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": {
|
||||
"mostPlayed": "más reproducido",
|
||||
|
||||
@@ -155,7 +155,9 @@
|
||||
"additionalParticipants": "participants additionnels",
|
||||
"tags": "tags",
|
||||
"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": {
|
||||
"remotePortWarning": "redémarrer le serveur pour appliquer le nouveau port",
|
||||
@@ -180,7 +182,8 @@
|
||||
"openError": "impossible d'ouvrir le fichier",
|
||||
"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)\".",
|
||||
"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": {
|
||||
"mostPlayed": "plus joués",
|
||||
@@ -616,7 +619,9 @@
|
||||
"discordPausedStatus_description": "quand activé, le status s'affichera lorsque le lecteur est en pause",
|
||||
"discordPausedStatus": "afficher le status d'activité en pause",
|
||||
"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": {
|
||||
"deletePlaylist": {
|
||||
|
||||
@@ -104,13 +104,14 @@
|
||||
"year": "år",
|
||||
"yes": "ja",
|
||||
"descending": "synkende",
|
||||
"dismiss": "avkreft",
|
||||
"dismiss": "lukk",
|
||||
"delete": "slett",
|
||||
"description": "beskrivelse",
|
||||
"manage": "håndtere",
|
||||
"maximize": "maksimer",
|
||||
"right": "høyre",
|
||||
"sortOrder": "rekkefølge"
|
||||
"sortOrder": "rekkefølge",
|
||||
"tags": "tagger"
|
||||
},
|
||||
"entity": {
|
||||
"smartPlaylist": "smart $t(entity.playlist_one)",
|
||||
@@ -233,7 +234,7 @@
|
||||
"addServer": {
|
||||
"ignoreCors": "ignorer cors ($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_url": "lenke",
|
||||
"input_username": "brukernavn",
|
||||
@@ -269,6 +270,10 @@
|
||||
"updateServer": {
|
||||
"success": "vellykket oppdatering av serveren",
|
||||
"title": "oppdater server"
|
||||
},
|
||||
"queryEditor": {
|
||||
"input_optionMatchAll": "match alle",
|
||||
"input_optionMatchAny": "matche hvilken som helst"
|
||||
}
|
||||
},
|
||||
"page": {
|
||||
@@ -338,7 +343,7 @@
|
||||
"lyricGap": "sangtekstavstand",
|
||||
"dynamicImageBlur": "bilduskarphetstørrelse",
|
||||
"lyricAlignment": "sangtekstjustering",
|
||||
"lyricOffset": "sangtekstjustering (ms)",
|
||||
"lyricOffset": "sangtekstforskyvning (ms)",
|
||||
"lyricSize": "sangtekststørrelse",
|
||||
"opacity": "absorpsjon",
|
||||
"showLyricMatch": "vis sangteksttreff",
|
||||
@@ -405,7 +410,8 @@
|
||||
"search": "$t(common.search)",
|
||||
"settings": "$t(common.setting_other)",
|
||||
"shared": "delt $t(entity.playlist_other)",
|
||||
"artists": "$t(entity.artist_other)"
|
||||
"artists": "$t(entity.artist_other)",
|
||||
"myLibrary": "mitt bibliotek"
|
||||
},
|
||||
"setting": {
|
||||
"generalTab": "generelt",
|
||||
@@ -416,6 +422,9 @@
|
||||
},
|
||||
"playlistList": {
|
||||
"title": "$t(entity.playlist_other)"
|
||||
},
|
||||
"playlist": {
|
||||
"reorder": "omorganisering kun mulig ved sortering på id"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
@@ -439,6 +448,68 @@
|
||||
"queue_moveToTop": "flytt valgte til bunnen",
|
||||
"playbackFetchNoResults": "ingen sanger funnet",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,9 @@
|
||||
"additionalParticipants": "其他参与者",
|
||||
"tags": "标签",
|
||||
"viewReleaseNotes": "查看发行说明",
|
||||
"newVersion": "已安装新版本 ({{version}})"
|
||||
"newVersion": "已安装新版本 ({{version}})",
|
||||
"bitDepth": "位深度",
|
||||
"sampleRate": "采样率"
|
||||
},
|
||||
"entity": {
|
||||
"albumArtist_other": "专辑艺术家",
|
||||
@@ -407,7 +409,9 @@
|
||||
"discordPausedStatus": "暂停时显示rich presence",
|
||||
"discordPausedStatus_description": "启用后将在播放器暂停时显示状态",
|
||||
"preservePitch": "保持音高",
|
||||
"preservePitch_description": "在调整播放速度时保持音高"
|
||||
"preservePitch_description": "在调整播放速度时保持音高",
|
||||
"notify": "启用歌曲通知",
|
||||
"notify_description": "更改当前歌曲时显示通知"
|
||||
},
|
||||
"error": {
|
||||
"remotePortWarning": "重启服务器使新端口生效",
|
||||
@@ -432,7 +436,8 @@
|
||||
"badAlbum": "您看到此页面是因为这首歌不是专辑的一部分。如果您的音乐文件夹顶层有一首歌曲,您很可能会遇到此问题。jellyfin 仅对位于文件夹中的曲目进行分组。",
|
||||
"networkError": "发生网络错误",
|
||||
"openError": "无法打开文件",
|
||||
"badValue": "无效的选项 \"{{value}}\". 此值不再存在"
|
||||
"badValue": "无效的选项 \"{{value}}\". 此值不再存在",
|
||||
"notificationDenied": "通知权限被拒绝。此设置无效"
|
||||
},
|
||||
"filter": {
|
||||
"mostPlayed": "最多播放过",
|
||||
|
||||
+1
-4
@@ -22,10 +22,7 @@ export const App = () => {
|
||||
const { mode, theme } = useAppTheme(isDark ? AppTheme.DEFAULT_DARK : AppTheme.DEFAULT_LIGHT);
|
||||
|
||||
return (
|
||||
<MantineProvider
|
||||
defaultColorScheme={mode}
|
||||
theme={theme}
|
||||
>
|
||||
<MantineProvider defaultColorScheme={mode} theme={theme}>
|
||||
<Shell />
|
||||
</MantineProvider>
|
||||
);
|
||||
|
||||
@@ -18,17 +18,7 @@ export const ThemeButton = () => {
|
||||
}}
|
||||
variant="default"
|
||||
>
|
||||
{isDark ? (
|
||||
<Icon
|
||||
icon="themeLight"
|
||||
size={30}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
icon="themeDark"
|
||||
size={30}
|
||||
/>
|
||||
)}
|
||||
{isDark ? <Icon icon="themeLight" size={30} /> : <Icon icon="themeDark" size={30} />}
|
||||
</ActionIcon>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -32,17 +32,9 @@ export const RemoteContainer = () => {
|
||||
const debouncedSetRating = debounce(setRating, 400);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
gap="md"
|
||||
h="100dvh"
|
||||
w="100%"
|
||||
>
|
||||
<Stack gap="md" h="100dvh" w="100%">
|
||||
{showImage && (
|
||||
<Flex
|
||||
align="center"
|
||||
justify="center"
|
||||
w="100%"
|
||||
>
|
||||
<Flex align="center" justify="center" w="100%">
|
||||
<PlayerImage src={song?.imageUrl} />
|
||||
</Flex>
|
||||
)}
|
||||
@@ -87,10 +79,7 @@ export const RemoteContainer = () => {
|
||||
</Group>
|
||||
</Stack>
|
||||
)}
|
||||
<Group
|
||||
gap={0}
|
||||
grow
|
||||
>
|
||||
<Group gap={0} grow>
|
||||
<ActionIcon
|
||||
disabled={!id}
|
||||
icon="favorite"
|
||||
@@ -109,10 +98,7 @@ export const RemoteContainer = () => {
|
||||
/>
|
||||
{(song?.serverType === 'navidrome' || song?.serverType === 'subsonic') && (
|
||||
<div style={{ margin: 'auto' }}>
|
||||
<Tooltip
|
||||
label="Double click to clear"
|
||||
openDelay={1000}
|
||||
>
|
||||
<Tooltip label="Double click to clear" openDelay={1000}>
|
||||
<Rating
|
||||
onChange={debouncedSetRating}
|
||||
onDoubleClick={() => debouncedSetRating(0)}
|
||||
@@ -123,10 +109,7 @@ export const RemoteContainer = () => {
|
||||
</div>
|
||||
)}
|
||||
</Group>
|
||||
<Group
|
||||
gap="xs"
|
||||
grow
|
||||
>
|
||||
<Group gap="xs" grow>
|
||||
<ActionIcon
|
||||
disabled={!id}
|
||||
icon="mediaPrevious"
|
||||
@@ -174,10 +157,7 @@ export const RemoteContainer = () => {
|
||||
variant="default"
|
||||
/>
|
||||
</Group>
|
||||
<Group
|
||||
gap="xs"
|
||||
grow
|
||||
>
|
||||
<Group gap="xs" grow>
|
||||
<ActionIcon
|
||||
icon="mediaShuffle"
|
||||
iconProps={{
|
||||
@@ -232,10 +212,7 @@ export const RemoteContainer = () => {
|
||||
max={100}
|
||||
onChangeEnd={(e) => send({ event: 'volume', volume: e })}
|
||||
rightLabel={
|
||||
<Text
|
||||
fw={600}
|
||||
size="xs"
|
||||
>
|
||||
<Text fw={600} size="xs">
|
||||
{volume ?? 0}
|
||||
</Text>
|
||||
}
|
||||
|
||||
@@ -13,16 +13,9 @@ export const Shell = () => {
|
||||
const connected = useConnected();
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
h="100vh"
|
||||
padding="md"
|
||||
w="100vw"
|
||||
>
|
||||
<AppShell h="100vh" padding="md" w="100vw">
|
||||
<AppShell.Header style={{ background: 'var(--theme-colors-surface)' }}>
|
||||
<Grid
|
||||
px="md"
|
||||
py="sm"
|
||||
>
|
||||
<Grid px="md" py="sm">
|
||||
<Grid.Col span={4}>
|
||||
<Flex
|
||||
align="center"
|
||||
@@ -33,20 +26,11 @@ export const Shell = () => {
|
||||
justifySelf: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
fit="contain"
|
||||
height={32}
|
||||
src="/favicon.ico"
|
||||
width={32}
|
||||
/>
|
||||
<Image fit="contain" height={32} src="/favicon.ico" width={32} />
|
||||
</Flex>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={8}>
|
||||
<Group
|
||||
gap="sm"
|
||||
justify="flex-end"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Group gap="sm" justify="flex-end" wrap="nowrap">
|
||||
<ReconnectButton />
|
||||
<ImageButton />
|
||||
<ThemeButton />
|
||||
@@ -58,10 +42,7 @@ export const Shell = () => {
|
||||
{connected ? (
|
||||
<RemoteContainer />
|
||||
) : (
|
||||
<Center
|
||||
h="100vh"
|
||||
w="100vw"
|
||||
>
|
||||
<Center h="100vh" w="100vw">
|
||||
<Spinner />
|
||||
</Center>
|
||||
)}
|
||||
|
||||
@@ -61,10 +61,7 @@ export const WrappedSlider = ({ leftLabel, rightLabel, value, ...props }: Wrappe
|
||||
const [seek, setSeek] = useState(0);
|
||||
|
||||
return (
|
||||
<Group
|
||||
align="center"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Group align="center" wrap="nowrap">
|
||||
{leftLabel && <Text size="sm">{leftLabel}</Text>}
|
||||
<PlayerbarSlider
|
||||
{...props}
|
||||
|
||||
@@ -349,7 +349,14 @@ axiosClient.interceptors.response.use(
|
||||
.catch((newError: any) => {
|
||||
if (newError !== TIMEOUT_ERROR) {
|
||||
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
|
||||
@@ -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);
|
||||
|
||||
@@ -251,6 +251,9 @@ axiosClient.interceptors.response.use(
|
||||
message: data['subsonic-response'].error.message,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,10 @@ const ALBUM_LIST_SORT_MAPPING: Record<AlbumListSort, AlbumListSortType | undefin
|
||||
[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 = {
|
||||
addToPlaylist: async ({ apiClientProps, body, query }) => {
|
||||
const res = await ssApiClient(apiClientProps).updatePlaylist({
|
||||
@@ -90,7 +94,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
};
|
||||
}
|
||||
|
||||
await ssApiClient({ server: null, url: cleanServerUrl }).authenticate({
|
||||
const resp = await ssApiClient({ server: null, url: cleanServerUrl }).authenticate({
|
||||
query: {
|
||||
c: 'Feishin',
|
||||
f: 'json',
|
||||
@@ -99,6 +103,10 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
},
|
||||
});
|
||||
|
||||
if (resp.status !== 200) {
|
||||
throw new Error('Failed to log in');
|
||||
}
|
||||
|
||||
return {
|
||||
credential,
|
||||
userId: null,
|
||||
@@ -269,7 +277,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
albumOffset: query.startIndex,
|
||||
artistCount: 0,
|
||||
artistOffset: 0,
|
||||
query: query.searchTerm || '""',
|
||||
query: query.searchTerm || '',
|
||||
songCount: 0,
|
||||
songOffset: 0,
|
||||
},
|
||||
@@ -418,11 +426,11 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
while (fetchNextPage) {
|
||||
const res = await ssApiClient(apiClientProps).search3({
|
||||
query: {
|
||||
albumCount: 500,
|
||||
albumCount: MAX_SUBSONIC_ITEMS,
|
||||
albumOffset: startIndex,
|
||||
artistCount: 0,
|
||||
artistOffset: 0,
|
||||
query: query.searchTerm || '""',
|
||||
query: query.searchTerm || '',
|
||||
songCount: 0,
|
||||
songOffset: 0,
|
||||
},
|
||||
@@ -437,8 +445,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
totalRecordCount += albumCount;
|
||||
startIndex += albumCount;
|
||||
|
||||
// The max limit size for Subsonic is 500
|
||||
fetchNextPage = albumCount === 500;
|
||||
fetchNextPage = albumCount === MAX_SUBSONIC_ITEMS;
|
||||
}
|
||||
|
||||
return totalRecordCount;
|
||||
@@ -522,7 +529,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
genre: query.genres?.length ? query.genres[0] : undefined,
|
||||
musicFolderId: query.musicFolderId,
|
||||
offset: startIndex,
|
||||
size: 500,
|
||||
size: MAX_SUBSONIC_ITEMS,
|
||||
toYear,
|
||||
type,
|
||||
},
|
||||
@@ -546,8 +553,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
totalRecordCount += albumCount;
|
||||
startIndex += albumCount;
|
||||
|
||||
// The max limit size for Subsonic is 500
|
||||
fetchNextPage = albumCount === 500;
|
||||
fetchNextPage = albumCount === MAX_SUBSONIC_ITEMS;
|
||||
}
|
||||
|
||||
return totalRecordCount;
|
||||
@@ -904,7 +910,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
albumOffset: 0,
|
||||
artistCount: 0,
|
||||
artistOffset: 0,
|
||||
query: query.searchTerm || '""',
|
||||
query: query.searchTerm || '',
|
||||
songCount: query.limit,
|
||||
songOffset: query.startIndex,
|
||||
},
|
||||
@@ -1046,7 +1052,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
albumOffset: 0,
|
||||
artistCount: 0,
|
||||
artistOffset: 0,
|
||||
query: query.searchTerm || '""',
|
||||
query: query.searchTerm || '',
|
||||
songCount: query.limit,
|
||||
songOffset: query.startIndex,
|
||||
},
|
||||
@@ -1086,8 +1092,8 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
albumOffset: 0,
|
||||
artistCount: 0,
|
||||
artistOffset: 0,
|
||||
query: query.searchTerm || '""',
|
||||
songCount: 500,
|
||||
query: query.searchTerm || '',
|
||||
songCount: MAX_SUBSONIC_ITEMS,
|
||||
songOffset: startIndex,
|
||||
},
|
||||
});
|
||||
@@ -1101,8 +1107,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
totalRecordCount += songCount;
|
||||
startIndex += songCount;
|
||||
|
||||
// The max limit size for Subsonic is 500
|
||||
fetchNextPage = songCount === 500;
|
||||
fetchNextPage = songCount === MAX_SUBSONIC_ITEMS;
|
||||
}
|
||||
|
||||
return totalRecordCount;
|
||||
@@ -1110,6 +1115,10 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
|
||||
if (query.genreIds) {
|
||||
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) {
|
||||
const res = await ssApiClient(apiClientProps).getSongsByGenre({
|
||||
query: {
|
||||
@@ -1128,17 +1137,17 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
|
||||
if (numberOfResults !== 1) {
|
||||
fetchNextSection = false;
|
||||
startIndex = sectionIndex === 0 ? 0 : sectionIndex - 5000;
|
||||
startIndex = sectionIndex === 0 ? 0 : sectionIndex - SUBSONIC_FAST_BATCH_SIZE;
|
||||
break;
|
||||
} else {
|
||||
sectionIndex += 5000;
|
||||
sectionIndex += SUBSONIC_FAST_BATCH_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
while (fetchNextPage) {
|
||||
const res = await ssApiClient(apiClientProps).getSongsByGenre({
|
||||
query: {
|
||||
count: 500,
|
||||
count: MAX_SUBSONIC_ITEMS,
|
||||
genre: query.genreIds[0],
|
||||
musicFolderId: query.musicFolderId,
|
||||
offset: startIndex,
|
||||
@@ -1154,7 +1163,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
totalRecordCount = startIndex + numberOfResults;
|
||||
startIndex += numberOfResults;
|
||||
|
||||
fetchNextPage = numberOfResults === 500;
|
||||
fetchNextPage = numberOfResults === MAX_SUBSONIC_ITEMS;
|
||||
}
|
||||
|
||||
return totalRecordCount;
|
||||
@@ -1176,6 +1185,9 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
|
||||
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) {
|
||||
const res = await ssApiClient(apiClientProps).search3({
|
||||
query: {
|
||||
@@ -1183,7 +1195,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
albumOffset: 0,
|
||||
artistCount: 0,
|
||||
artistOffset: 0,
|
||||
query: query.searchTerm || '""',
|
||||
query: query.searchTerm || '',
|
||||
songCount: 1,
|
||||
songOffset: sectionIndex,
|
||||
},
|
||||
@@ -1195,13 +1207,12 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
|
||||
const numberOfResults = (res.body.searchResult3?.song || []).length || 0;
|
||||
|
||||
// Check each batch of 5000 songs to check for data
|
||||
sectionIndex += 5000;
|
||||
fetchNextSection = numberOfResults === 1;
|
||||
|
||||
if (!fetchNextSection) {
|
||||
// fetchNextBlock will be false on the next loop so we need to subtract 5000 * 2
|
||||
startIndex = sectionIndex - 10000;
|
||||
if (numberOfResults !== 1) {
|
||||
fetchNextSection = false;
|
||||
startIndex = sectionIndex === 0 ? 0 : sectionIndex - SUBSONIC_FAST_BATCH_SIZE;
|
||||
break;
|
||||
} else {
|
||||
sectionIndex += SUBSONIC_FAST_BATCH_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1212,8 +1223,8 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
albumOffset: 0,
|
||||
artistCount: 0,
|
||||
artistOffset: 0,
|
||||
query: query.searchTerm || '""',
|
||||
songCount: 500,
|
||||
query: query.searchTerm || '',
|
||||
songCount: MAX_SUBSONIC_ITEMS,
|
||||
songOffset: startIndex,
|
||||
},
|
||||
});
|
||||
@@ -1227,8 +1238,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
totalRecordCount = startIndex + numberOfResults;
|
||||
startIndex += numberOfResults;
|
||||
|
||||
// The max limit size for Subsonic is 500
|
||||
fetchNextPage = numberOfResults === 500;
|
||||
fetchNextPage = numberOfResults === MAX_SUBSONIC_ITEMS;
|
||||
}
|
||||
|
||||
return totalRecordCount;
|
||||
|
||||
@@ -190,15 +190,8 @@ export const App = () => {
|
||||
}, [language]);
|
||||
|
||||
return (
|
||||
<MantineProvider
|
||||
defaultColorScheme={mode as 'dark' | 'light'}
|
||||
theme={theme}
|
||||
>
|
||||
<Notifications
|
||||
containerWidth="300px"
|
||||
position="bottom-center"
|
||||
zIndex={5}
|
||||
/>
|
||||
<MantineProvider defaultColorScheme={mode as 'dark' | 'light'} theme={theme}>
|
||||
<Notifications containerWidth="300px" position="bottom-center" zIndex={50000} />
|
||||
<PlayQueueHandlerContext.Provider value={providerValue}>
|
||||
<ContextMenuProvider>
|
||||
<WebAudioContext.Provider value={webAudioProvider}>
|
||||
|
||||
@@ -47,10 +47,7 @@ export const CardControls = ({
|
||||
return (
|
||||
<div className={styles.gridCardControlsContainer}>
|
||||
<div className={styles.bottomControls}>
|
||||
<button
|
||||
className={styles.playButton}
|
||||
onClick={handlePlay}
|
||||
>
|
||||
<button className={styles.playButton} onClick={handlePlay}>
|
||||
<Icon icon="mediaPlay" />
|
||||
</button>
|
||||
<Group gap="xs">
|
||||
|
||||
@@ -55,14 +55,8 @@ export const PosterCard = ({
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<Link
|
||||
className={styles.imageContainer}
|
||||
to={path}
|
||||
>
|
||||
<Image
|
||||
className={styles.image}
|
||||
src={data?.imageUrl}
|
||||
/>
|
||||
<Link className={styles.imageContainer} to={path}>
|
||||
<Image className={styles.image} src={data?.imageUrl} />
|
||||
<GridCardControls
|
||||
handleFavorite={controls.handleFavorite}
|
||||
handlePlayQueueAdd={controls.handlePlayQueueAdd}
|
||||
@@ -72,30 +66,21 @@ export const PosterCard = ({
|
||||
/>
|
||||
</Link>
|
||||
<div className={styles.detailContainer}>
|
||||
<CardRows
|
||||
data={data}
|
||||
rows={controls.cardRows}
|
||||
/>
|
||||
<CardRows data={data} rows={controls.cardRows} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.container}
|
||||
key={`placeholder-${uniqueId}-${data.id}`}
|
||||
>
|
||||
<div className={styles.container} key={`placeholder-${uniqueId}-${data.id}`}>
|
||||
<div className={styles.imageContainer}>
|
||||
<Skeleton className={styles.image} />
|
||||
</div>
|
||||
<div className={styles.detailContainer}>
|
||||
<Stack gap="xs">
|
||||
{(controls?.cardRows || []).map((row, index) => (
|
||||
<Skeleton
|
||||
height={14}
|
||||
key={`${index}-${row.arrayProperty}`}
|
||||
/>
|
||||
<Skeleton height={14} key={`${index}-${row.arrayProperty}`} />
|
||||
))}
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
@@ -35,14 +35,8 @@ export const ContextMenuButton = forwardRef(
|
||||
onClick={props.onClick}
|
||||
ref={ref}
|
||||
>
|
||||
<Group
|
||||
justify="space-between"
|
||||
w="100%"
|
||||
>
|
||||
<Group
|
||||
className={styles.left}
|
||||
gap="md"
|
||||
>
|
||||
<Group justify="space-between" w="100%">
|
||||
<Group className={styles.left} gap="md">
|
||||
{leftIcon}
|
||||
{children}
|
||||
</Group>
|
||||
|
||||
@@ -77,11 +77,7 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
|
||||
className={styles.wrapper}
|
||||
to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: currentItem?.id || '' })}
|
||||
>
|
||||
<AnimatePresence
|
||||
custom={direction}
|
||||
initial={false}
|
||||
mode="popLayout"
|
||||
>
|
||||
<AnimatePresence custom={direction} initial={false} mode="popLayout">
|
||||
{data && (
|
||||
<motion.div
|
||||
animate="animate"
|
||||
@@ -101,10 +97,7 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.infoColumn}>
|
||||
<Stack
|
||||
gap="md"
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Stack gap="md" style={{ width: '100%' }}>
|
||||
<div className={styles.titleWrapper}>
|
||||
<TextTitle
|
||||
fw={900}
|
||||
@@ -117,10 +110,7 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
|
||||
</div>
|
||||
<div className={styles.titleWrapper}>
|
||||
{currentItem?.albumArtists.slice(0, 1).map((artist) => (
|
||||
<Text
|
||||
fw={600}
|
||||
key={`carousel-artist-${artist.id}`}
|
||||
>
|
||||
<Text fw={600} key={`carousel-artist-${artist.id}`}>
|
||||
{artist.name}
|
||||
</Text>
|
||||
))}
|
||||
|
||||
@@ -60,10 +60,7 @@ const Title = ({ handleNext, handlePrev, label, pagination }: TitleProps) => {
|
||||
{isValidElement(label) ? (
|
||||
label
|
||||
) : (
|
||||
<TextTitle
|
||||
order={3}
|
||||
weight={700}
|
||||
>
|
||||
<TextTitle order={3} weight={700}>
|
||||
{label}
|
||||
</TextTitle>
|
||||
)}
|
||||
@@ -280,11 +277,7 @@ export const SwiperGridCarousel = ({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className="grid-carousel"
|
||||
gap="md"
|
||||
ref={containerRef as any}
|
||||
>
|
||||
<Stack className="grid-carousel" gap="md" ref={containerRef as any}>
|
||||
{title ? (
|
||||
<Title
|
||||
{...title}
|
||||
|
||||
@@ -91,11 +91,7 @@ export const NativeScrollArea = forwardRef(
|
||||
{...pageHeaderProps}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={styles.scrollArea}
|
||||
ref={mergedRef}
|
||||
{...props}
|
||||
>
|
||||
<div className={styles.scrollArea} ref={mergedRef} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -99,10 +99,7 @@ export const QueryBuilder = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
gap="sm"
|
||||
ml={`${level * 10}px`}
|
||||
>
|
||||
<Stack gap="sm" ml={`${level * 10}px`}>
|
||||
<Group gap="sm">
|
||||
<Select
|
||||
data={FILTER_GROUP_OPTIONS_DATA}
|
||||
@@ -112,12 +109,7 @@ export const QueryBuilder = ({
|
||||
value={data.type}
|
||||
width="20%"
|
||||
/>
|
||||
<ActionIcon
|
||||
icon="add"
|
||||
onClick={handleAddRule}
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
/>
|
||||
<ActionIcon icon="add" onClick={handleAddRule} size="sm" variant="subtle" />
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<ActionIcon
|
||||
@@ -150,24 +142,14 @@ export const QueryBuilder = ({
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Item
|
||||
isDanger
|
||||
leftSection={
|
||||
<Icon
|
||||
color="error"
|
||||
icon="refresh"
|
||||
/>
|
||||
}
|
||||
leftSection={<Icon color="error" icon="refresh" />}
|
||||
onClick={onResetFilters}
|
||||
>
|
||||
Reset to default
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
isDanger
|
||||
leftSection={
|
||||
<Icon
|
||||
color="error"
|
||||
icon="delete"
|
||||
/>
|
||||
}
|
||||
leftSection={<Icon color="error" icon="delete" />}
|
||||
onClick={onClearFilters}
|
||||
>
|
||||
Clear filters
|
||||
|
||||
@@ -48,13 +48,7 @@ const QueryValueInput = ({ data, onChange, type, ...props }: any) => {
|
||||
/>
|
||||
);
|
||||
case 'date':
|
||||
return (
|
||||
<TextInput
|
||||
onChange={onChange}
|
||||
size="sm"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
return <TextInput onChange={onChange} size="sm" {...props} />;
|
||||
case 'dateRange':
|
||||
return (
|
||||
<>
|
||||
@@ -92,21 +86,9 @@ const QueryValueInput = ({ data, onChange, type, ...props }: any) => {
|
||||
/>
|
||||
);
|
||||
case 'playlist':
|
||||
return (
|
||||
<Select
|
||||
data={data}
|
||||
onChange={onChange}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
return <Select data={data} onChange={onChange} {...props} />;
|
||||
case 'string':
|
||||
return (
|
||||
<TextInput
|
||||
onChange={onChange}
|
||||
size="sm"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
return <TextInput onChange={onChange} size="sm" {...props} />;
|
||||
|
||||
default:
|
||||
return <></>;
|
||||
@@ -188,10 +170,7 @@ export const QueryBuilderOption = ({
|
||||
const ml = (level + 1) * 10;
|
||||
|
||||
return (
|
||||
<Group
|
||||
gap="sm"
|
||||
ml={ml}
|
||||
>
|
||||
<Group gap="sm" ml={ml}>
|
||||
<Select
|
||||
data={filters}
|
||||
maxWidth={170}
|
||||
|
||||
@@ -81,10 +81,7 @@ export const DefaultCard = ({
|
||||
data?.userFavorite && styles.isFavorite,
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
className={styles.image}
|
||||
src={data?.imageUrl}
|
||||
/>
|
||||
<Image className={styles.image} src={data?.imageUrl} />
|
||||
<GridCardControls
|
||||
handleFavorite={controls.handleFavorite}
|
||||
handlePlayQueueAdd={controls.handlePlayQueueAdd}
|
||||
@@ -95,10 +92,7 @@ export const DefaultCard = ({
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.detailContainer}>
|
||||
<CardRows
|
||||
data={data}
|
||||
rows={controls.cardRows}
|
||||
/>
|
||||
<CardRows data={data} rows={controls.cardRows} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -86,10 +86,7 @@ export const GridCardControls = ({
|
||||
onClick={handlePlay}
|
||||
variant="filled"
|
||||
>
|
||||
<Icon
|
||||
icon="mediaPlay"
|
||||
size="xl"
|
||||
/>
|
||||
<Icon icon="mediaPlay" size="xl" />
|
||||
</Button>
|
||||
<div className={styles.bottomControls}>
|
||||
{itemType !== LibraryItem.PLAYLIST && (
|
||||
|
||||
@@ -73,17 +73,11 @@ export const PosterCard = ({
|
||||
margin: controls.itemGap,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={styles.linkContainer}
|
||||
onClick={() => navigate(path)}
|
||||
>
|
||||
<div className={styles.linkContainer} onClick={() => navigate(path)}>
|
||||
<div
|
||||
className={`${styles.imageContainer} ${data?.userFavorite ? styles.isFavorite : ''}`}
|
||||
>
|
||||
<Image
|
||||
className={styles.image}
|
||||
src={data?.imageUrl}
|
||||
/>
|
||||
<Image className={styles.image} src={data?.imageUrl} />
|
||||
<GridCardControls
|
||||
handleFavorite={controls.handleFavorite}
|
||||
handlePlayQueueAdd={controls.handlePlayQueueAdd}
|
||||
@@ -95,10 +89,7 @@ export const PosterCard = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.detailContainer}>
|
||||
<CardRows
|
||||
data={data}
|
||||
rows={controls.cardRows}
|
||||
/>
|
||||
<CardRows data={data} rows={controls.cardRows} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -15,21 +15,14 @@ export const AlbumArtistCell = ({ data, value }: ICellRendererParams) => {
|
||||
if (value === undefined) {
|
||||
return (
|
||||
<CellContainer position="left">
|
||||
<Skeleton
|
||||
height="1rem"
|
||||
width="80%"
|
||||
/>
|
||||
<Skeleton height="1rem" width="80%" />
|
||||
</CellContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CellContainer position="left">
|
||||
<Text
|
||||
isMuted
|
||||
overflow="hidden"
|
||||
size="md"
|
||||
>
|
||||
<Text isMuted overflow="hidden" size="md">
|
||||
{value?.map((item: AlbumArtist | Artist, index: number) => (
|
||||
<React.Fragment key={`row-${item.id}-${data.uniqueId}`}>
|
||||
{index > 0 && <Separator />}
|
||||
@@ -47,11 +40,7 @@ export const AlbumArtistCell = ({ data, value }: ICellRendererParams) => {
|
||||
{item.name || '—'}
|
||||
</Text>
|
||||
) : (
|
||||
<Text
|
||||
isMuted
|
||||
overflow="hidden"
|
||||
size="md"
|
||||
>
|
||||
<Text isMuted overflow="hidden" size="md">
|
||||
{item.name || '—'}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -15,21 +15,14 @@ export const ArtistCell = ({ data, value }: ICellRendererParams) => {
|
||||
if (value === undefined) {
|
||||
return (
|
||||
<CellContainer position="left">
|
||||
<Skeleton
|
||||
height="1rem"
|
||||
width="80%"
|
||||
/>
|
||||
<Skeleton height="1rem" width="80%" />
|
||||
</CellContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CellContainer position="left">
|
||||
<Text
|
||||
isMuted
|
||||
overflow="hidden"
|
||||
size="md"
|
||||
>
|
||||
<Text isMuted overflow="hidden" size="md">
|
||||
{value?.map((item: AlbumArtist | Artist, index: number) => (
|
||||
<React.Fragment key={`row-${item.id}-${data.uniqueId}`}>
|
||||
{index > 0 && <Separator />}
|
||||
@@ -47,11 +40,7 @@ export const ArtistCell = ({ data, value }: ICellRendererParams) => {
|
||||
{item.name || '—'}
|
||||
</Text>
|
||||
) : (
|
||||
<Text
|
||||
isMuted
|
||||
overflow="hidden"
|
||||
size="md"
|
||||
>
|
||||
<Text isMuted overflow="hidden" size="md">
|
||||
{item.name || '—'}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -41,11 +41,7 @@ export const CombinedTitleCell = ({
|
||||
>
|
||||
<Skeleton className={styles.image} />
|
||||
</div>
|
||||
<Skeleton
|
||||
className={styles.skeletonMetadata}
|
||||
height="1rem"
|
||||
width="80%"
|
||||
/>
|
||||
<Skeleton className={styles.skeletonMetadata} height="1rem" width="80%" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -62,11 +58,7 @@ export const CombinedTitleCell = ({
|
||||
width: `${(node.rowHeight || 40) - 10}px`,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
alt="cover"
|
||||
className={styles.image}
|
||||
src={value.imageUrl}
|
||||
/>
|
||||
<Image alt="cover" className={styles.image} src={value.imageUrl} />
|
||||
|
||||
<ListCoverControls
|
||||
className={styles.playButton}
|
||||
@@ -77,18 +69,10 @@ export const CombinedTitleCell = ({
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.metadataWrapper}>
|
||||
<Text
|
||||
className="current-song-child"
|
||||
overflow="hidden"
|
||||
size="md"
|
||||
>
|
||||
<Text className="current-song-child" overflow="hidden" size="md">
|
||||
{value.name}
|
||||
</Text>
|
||||
<Text
|
||||
isMuted
|
||||
overflow="hidden"
|
||||
size="md"
|
||||
>
|
||||
<Text isMuted overflow="hidden" size="md">
|
||||
{artists?.length ? (
|
||||
artists.map((artist: AlbumArtist | Artist, index: number) => (
|
||||
<React.Fragment key={`queue-${rowIndex}-artist-${artist.id}`}>
|
||||
|
||||
@@ -25,10 +25,7 @@ export const FullWidthDiscCell = ({ api, data, node }: ICellRendererParams) => {
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Group
|
||||
justify="space-between"
|
||||
w="100%"
|
||||
>
|
||||
<Group justify="space-between" w="100%">
|
||||
<Button
|
||||
leftSection={isSelected ? <Icon icon="squareCheck" /> : <Icon icon="square" />}
|
||||
onClick={handleToggleDiscNodes}
|
||||
|
||||
@@ -23,10 +23,7 @@ export const GenericCell = ({ value, valueFormatted }: ICellRendererParams, opti
|
||||
if (value === undefined) {
|
||||
return (
|
||||
<CellContainer position={position || 'left'}>
|
||||
<Skeleton
|
||||
height="1rem"
|
||||
width="80%"
|
||||
/>
|
||||
<Skeleton height="1rem" width="80%" />
|
||||
</CellContainer>
|
||||
);
|
||||
}
|
||||
@@ -45,12 +42,7 @@ export const GenericCell = ({ value, valueFormatted }: ICellRendererParams, opti
|
||||
{isLink ? displayedValue.value : displayedValue}
|
||||
</Text>
|
||||
) : (
|
||||
<Text
|
||||
isMuted={!primary}
|
||||
isNoSelect={false}
|
||||
overflow="hidden"
|
||||
size="md"
|
||||
>
|
||||
<Text isMuted={!primary} isNoSelect={false} overflow="hidden" size="md">
|
||||
{displayedValue}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -13,11 +13,7 @@ export const GenreCell = ({ data, value }: ICellRendererParams) => {
|
||||
const genrePath = useGenreRoute();
|
||||
return (
|
||||
<CellContainer position="left">
|
||||
<Text
|
||||
isMuted
|
||||
overflow="hidden"
|
||||
size="md"
|
||||
>
|
||||
<Text isMuted overflow="hidden" size="md">
|
||||
{value?.map((item: AlbumArtist | Artist, index: number) => (
|
||||
<React.Fragment key={`row-${item.id}-${data.uniqueId}`}>
|
||||
{index > 0 && <Separator />}
|
||||
|
||||
@@ -19,20 +19,14 @@ export const NoteCell = ({ value }: ICellRendererParams) => {
|
||||
if (value === undefined) {
|
||||
return (
|
||||
<CellContainer position="left">
|
||||
<Skeleton
|
||||
height="1rem"
|
||||
width="80%"
|
||||
/>
|
||||
<Skeleton height="1rem" width="80%" />
|
||||
</CellContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CellContainer position="left">
|
||||
<Text
|
||||
isMuted
|
||||
overflow="hidden"
|
||||
>
|
||||
<Text isMuted overflow="hidden">
|
||||
{formattedValue}
|
||||
</Text>
|
||||
</CellContainer>
|
||||
|
||||
@@ -26,11 +26,7 @@ export const RatingCell = ({ node, value }: ICellRendererParams) => {
|
||||
|
||||
return (
|
||||
<CellContainer position="center">
|
||||
<Rating
|
||||
onChange={handleUpdateRating}
|
||||
size="xs"
|
||||
value={value?.userRating}
|
||||
/>
|
||||
<Rating onChange={handleUpdateRating} size="xs" value={value?.userRating} />
|
||||
</CellContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -144,15 +144,9 @@ export const RowIndexCell = ({ eGridCell, value }: ICellRendererParams) => {
|
||||
return (
|
||||
<CellContainer position="right">
|
||||
{isPlaying && isCurrentSong ? (
|
||||
<Icon
|
||||
fill="primary"
|
||||
icon="mediaPlay"
|
||||
/>
|
||||
<Icon fill="primary" icon="mediaPlay" />
|
||||
) : isCurrentSong ? (
|
||||
<Icon
|
||||
fill="primary"
|
||||
icon="mediaPause"
|
||||
/>
|
||||
<Icon fill="primary" icon="mediaPause" />
|
||||
) : (
|
||||
<Text
|
||||
className="current-song-child current-song-index"
|
||||
|
||||
@@ -8,21 +8,14 @@ export const TitleCell = ({ value }: ICellRendererParams) => {
|
||||
if (value === undefined) {
|
||||
return (
|
||||
<CellContainer position="left">
|
||||
<Skeleton
|
||||
height="1rem"
|
||||
width="80%"
|
||||
/>
|
||||
<Skeleton height="1rem" width="80%" />
|
||||
</CellContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CellContainer position="left">
|
||||
<Text
|
||||
className="current-song-child"
|
||||
overflow="hidden"
|
||||
size="md"
|
||||
>
|
||||
<Text className="current-song-child" overflow="hidden" size="md">
|
||||
{value}
|
||||
</Text>
|
||||
</CellContainer>
|
||||
|
||||
@@ -7,10 +7,5 @@ export interface ICustomHeaderParams extends IHeaderParams {
|
||||
}
|
||||
|
||||
export const DurationHeader = () => {
|
||||
return (
|
||||
<Icon
|
||||
icon="duration"
|
||||
size="sm"
|
||||
/>
|
||||
);
|
||||
return <Icon icon="duration" size="sm" />;
|
||||
};
|
||||
|
||||
@@ -16,36 +16,11 @@ type Options = {
|
||||
type Presets = 'actions' | 'duration' | 'rowIndex' | 'userFavorite' | 'userRating';
|
||||
|
||||
const headerPresets = {
|
||||
actions: (
|
||||
<Icon
|
||||
icon="ellipsisHorizontal"
|
||||
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"
|
||||
/>
|
||||
),
|
||||
actions: <Icon icon="ellipsisHorizontal" 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 = (
|
||||
|
||||
@@ -635,15 +635,8 @@ export const VirtualTable = forwardRef(
|
||||
onNewColumnsLoaded={handleNewColumnsLoaded}
|
||||
/>
|
||||
{paginationProps && (
|
||||
<AnimatePresence
|
||||
initial={false}
|
||||
mode="wait"
|
||||
presenceAffectsLayout
|
||||
>
|
||||
<TablePagination
|
||||
{...paginationProps}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
<AnimatePresence initial={false} mode="wait" presenceAffectsLayout>
|
||||
<TablePagination {...paginationProps} tableRef={tableRef} />
|
||||
</AnimatePresence>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -76,10 +76,7 @@ export const TablePagination = ({
|
||||
ref={containerQuery.ref}
|
||||
style={{ borderTop: '1px solid var(--theme-generic-border-color)' }}
|
||||
>
|
||||
<Text
|
||||
isMuted
|
||||
size="md"
|
||||
>
|
||||
<Text isMuted size="md">
|
||||
{containerQuery.isMd ? (
|
||||
<>
|
||||
Showing <b>{currentPageStartIndex}</b> - <b>{currentPageStopIndex}</b> of{' '}
|
||||
@@ -97,11 +94,7 @@ export const TablePagination = ({
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
<Group
|
||||
gap="sm"
|
||||
ref={containerQuery.ref}
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Group gap="sm" ref={containerQuery.ref} wrap="nowrap">
|
||||
<Popover
|
||||
onClose={() => handlers.close()}
|
||||
opened={isGoToPageOpen}
|
||||
@@ -127,10 +120,7 @@ export const TablePagination = ({
|
||||
min={1}
|
||||
width={70}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="filled"
|
||||
>
|
||||
<Button type="submit" variant="filled">
|
||||
Go
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
@@ -13,15 +13,8 @@ interface ActionRequiredContainerProps {
|
||||
export const ActionRequiredContainer = ({ children, title }: ActionRequiredContainerProps) => (
|
||||
<Stack style={{ cursor: 'default', maxWidth: '700px' }}>
|
||||
<Group>
|
||||
<Icon
|
||||
fill="warn"
|
||||
icon="warn"
|
||||
size="lg"
|
||||
/>
|
||||
<Text
|
||||
size="xl"
|
||||
style={{ textTransform: 'uppercase' }}
|
||||
>
|
||||
<Icon fill="warn" icon="warn" size="lg" />
|
||||
<Text size="xl" style={{ textTransform: 'uppercase' }}>
|
||||
{title}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
@@ -21,18 +21,11 @@ export const ErrorFallback = ({ resetErrorBoundary }: FallbackProps) => {
|
||||
<Center style={{ height: '100vh' }}>
|
||||
<Stack style={{ maxWidth: '50%' }}>
|
||||
<Group gap="xs">
|
||||
<Icon
|
||||
fill="error"
|
||||
icon="error"
|
||||
size="lg"
|
||||
/>
|
||||
<Icon fill="error" icon="error" size="lg" />
|
||||
<Text size="lg">{t('error.genericError')}</Text>
|
||||
</Group>
|
||||
<Text>{error?.message}</Text>
|
||||
<Button
|
||||
onClick={resetErrorBoundary}
|
||||
variant="filled"
|
||||
>
|
||||
<Button onClick={resetErrorBoundary} variant="filled">
|
||||
{t('common.reload')}
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
@@ -43,18 +43,11 @@ export const MpvRequired = () => {
|
||||
<Text>Set your MPV executable location below and restart the application.</Text>
|
||||
<Text>
|
||||
MPV is available at the following:{' '}
|
||||
<a
|
||||
href="https://mpv.io/installation/"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<a href="https://mpv.io/installation/" rel="noreferrer" target="_blank">
|
||||
https://mpv.io/
|
||||
</a>
|
||||
</Text>
|
||||
<FileInput
|
||||
disabled={disabled}
|
||||
onChange={handleSetMpvPath}
|
||||
/>
|
||||
<FileInput disabled={disabled} onChange={handleSetMpvPath} />
|
||||
<Text>{t('setting.disable_mpv', { context: 'description' })}</Text>
|
||||
<Checkbox
|
||||
label={t('setting.disableMpv')}
|
||||
|
||||
@@ -42,19 +42,12 @@ const RouteErrorBoundary = () => {
|
||||
px={10}
|
||||
variant="subtle"
|
||||
/>
|
||||
<Icon
|
||||
fill="error"
|
||||
icon="error"
|
||||
size="lg"
|
||||
/>
|
||||
<Icon fill="error" icon="error" size="lg" />
|
||||
<Text size="lg">{t('error.genericError')}</Text>
|
||||
</Group>
|
||||
<Divider my={5} />
|
||||
<Text size="sm">{error?.message}</Text>
|
||||
<Group
|
||||
gap="sm"
|
||||
grow
|
||||
>
|
||||
<Group gap="sm" grow>
|
||||
<Button
|
||||
leftSection={<Icon icon="home" />}
|
||||
onClick={handleHome}
|
||||
@@ -81,11 +74,7 @@ const RouteErrorBoundary = () => {
|
||||
</DropdownMenu>
|
||||
</Group>
|
||||
<Group grow>
|
||||
<Button
|
||||
onClick={handleReload}
|
||||
size="md"
|
||||
variant="filled"
|
||||
>
|
||||
<Button onClick={handleReload} size="md" variant="filled">
|
||||
{t('common.reload')}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
@@ -132,10 +132,7 @@ function ServerSelector() {
|
||||
}}
|
||||
variant={server.id === currentServer?.id ? 'filled' : 'default'}
|
||||
>
|
||||
<Group
|
||||
justify="space-between"
|
||||
w="100%"
|
||||
>
|
||||
<Group justify="space-between" w="100%">
|
||||
<Group>
|
||||
<img
|
||||
src={logo}
|
||||
@@ -144,10 +141,7 @@ function ServerSelector() {
|
||||
width: 'var(--theme-font-size-2xl)',
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
fw={600}
|
||||
size="lg"
|
||||
>
|
||||
<Text fw={600} size="lg">
|
||||
{server.name}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
@@ -49,10 +49,7 @@ const ActionRequiredRoute = () => {
|
||||
<AnimatedPage>
|
||||
<PageHeader />
|
||||
<Center style={{ height: '100%', width: '100vw' }}>
|
||||
<Stack
|
||||
gap="xl"
|
||||
style={{ maxWidth: '50%' }}
|
||||
>
|
||||
<Stack gap="xl" style={{ maxWidth: '50%' }}>
|
||||
<Group wrap="nowrap">
|
||||
{displayedCheck && (
|
||||
<ActionRequiredContainer title={displayedCheck.title}>
|
||||
@@ -64,10 +61,7 @@ const ActionRequiredRoute = () => {
|
||||
{canReturnHome && <Navigate to={AppRoute.HOME} />}
|
||||
{/* This should be displayed if a credential is required */}
|
||||
{isCredentialRequired && (
|
||||
<Group
|
||||
justify="center"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Group justify="center" wrap="nowrap">
|
||||
<Button
|
||||
fullWidth
|
||||
leftSection={<Icon icon="edit" />}
|
||||
|
||||
@@ -18,24 +18,14 @@ const InvalidRoute = () => {
|
||||
<AnimatedPage>
|
||||
<Center style={{ height: '100%', width: '100%' }}>
|
||||
<Stack>
|
||||
<Group
|
||||
justify="center"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Icon
|
||||
color="warn"
|
||||
icon="error"
|
||||
/>
|
||||
<Group justify="center" wrap="nowrap">
|
||||
<Icon color="warn" icon="error" />
|
||||
<Text size="xl">
|
||||
{t('error.apiRouteError', { postProcess: 'sentenceCase' })}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text>{location.pathname}</Text>
|
||||
<ActionIcon
|
||||
icon="arrowLeftS"
|
||||
onClick={() => navigate(-1)}
|
||||
variant="filled"
|
||||
/>
|
||||
<ActionIcon icon="arrowLeftS" onClick={() => navigate(-1)} variant="filled" />
|
||||
</Stack>
|
||||
</Center>
|
||||
</AnimatedPage>
|
||||
|
||||
@@ -319,17 +319,11 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
|
||||
const mbzId = detailQuery?.data?.mbzId;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.contentContainer}
|
||||
ref={cq.ref}
|
||||
>
|
||||
<div className={styles.contentContainer} ref={cq.ref}>
|
||||
<LibraryBackgroundOverlay backgroundColor={background} />
|
||||
<div className={styles.detailContainer}>
|
||||
<section>
|
||||
<Group
|
||||
gap="sm"
|
||||
justify="space-between"
|
||||
>
|
||||
<Group gap="sm" justify="space-between">
|
||||
<Group>
|
||||
<PlayButton onClick={() => handlePlay(playButtonBehavior)} />
|
||||
<Group gap="xs">
|
||||
@@ -485,11 +479,7 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
|
||||
suppressRowDrag
|
||||
/>
|
||||
</div>
|
||||
<Stack
|
||||
gap="lg"
|
||||
mt="3rem"
|
||||
ref={cq.ref}
|
||||
>
|
||||
<Stack gap="lg" mt="3rem" ref={cq.ref}>
|
||||
{cq.height || cq.width ? (
|
||||
<>
|
||||
{carousels
|
||||
|
||||
@@ -33,15 +33,9 @@ export const AlbumListContent = ({ gridRef, itemCount, tableRef }: AlbumListCont
|
||||
return (
|
||||
<Suspense fallback={<Spinner container />}>
|
||||
{display === ListDisplayType.CARD || display === ListDisplayType.GRID ? (
|
||||
<AlbumListGridView
|
||||
gridRef={gridRef}
|
||||
itemCount={itemCount}
|
||||
/>
|
||||
<AlbumListGridView gridRef={gridRef} itemCount={itemCount} />
|
||||
) : (
|
||||
<AlbumListTableView
|
||||
itemCount={itemCount}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
<AlbumListTableView itemCount={itemCount} tableRef={tableRef} />
|
||||
)}
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -448,11 +448,7 @@ export const AlbumListHeaderFilters = ({
|
||||
|
||||
return (
|
||||
<Flex justify="space-between">
|
||||
<Group
|
||||
gap="sm"
|
||||
ref={cq.ref}
|
||||
w="100%"
|
||||
>
|
||||
<Group gap="sm" ref={cq.ref} w="100%">
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button variant="subtle">{sortByLabel}</Button>
|
||||
@@ -471,10 +467,7 @@ export const AlbumListHeaderFilters = ({
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
<Divider orientation="vertical" />
|
||||
<OrderToggleButton
|
||||
onToggle={handleToggleSortOrder}
|
||||
sortOrder={filter.sortOrder}
|
||||
/>
|
||||
<OrderToggleButton onToggle={handleToggleSortOrder} sortOrder={filter.sortOrder} />
|
||||
{server?.type === ServerType.JELLYFIN && (
|
||||
<>
|
||||
<Divider orientation="vertical" />
|
||||
@@ -497,10 +490,7 @@ export const AlbumListHeaderFilters = ({
|
||||
</DropdownMenu>
|
||||
</>
|
||||
)}
|
||||
<FilterButton
|
||||
isActive={!!isFilterApplied}
|
||||
onClick={handleOpenFiltersModal}
|
||||
/>
|
||||
<FilterButton isActive={!!isFilterApplied} onClick={handleOpenFiltersModal} />
|
||||
<RefreshButton onClick={handleRefresh} />
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
@@ -535,10 +525,7 @@ export const AlbumListHeaderFilters = ({
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
</Group>
|
||||
<Group
|
||||
gap="sm"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<ListConfigMenu
|
||||
autoFitColumns={table.autoFit}
|
||||
disabledViewTypes={[ListDisplayType.LIST]}
|
||||
|
||||
@@ -61,15 +61,9 @@ export const AlbumListHeader = ({
|
||||
}, [filter, genreId, refresh, tableRef]);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
gap={0}
|
||||
ref={cq.ref}
|
||||
>
|
||||
<Stack gap={0} ref={cq.ref}>
|
||||
<PageHeader backgroundColor="var(--theme-colors-background)">
|
||||
<Flex
|
||||
justify="space-between"
|
||||
w="100%"
|
||||
>
|
||||
<Flex justify="space-between" w="100%">
|
||||
<LibraryHeaderBar>
|
||||
<LibraryHeaderBar.PlayButton
|
||||
onClick={() => handlePlay?.({ playType: playButtonBehavior })}
|
||||
@@ -85,10 +79,7 @@ export const AlbumListHeader = ({
|
||||
</LibraryHeaderBar.Badge>
|
||||
</LibraryHeaderBar>
|
||||
<Group>
|
||||
<SearchInput
|
||||
defaultValue={filter.searchTerm}
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
<SearchInput defaultValue={filter.searchTerm} onChange={handleSearch} />
|
||||
</Group>
|
||||
</Flex>
|
||||
</PageHeader>
|
||||
|
||||
@@ -227,16 +227,9 @@ export const JellyfinAlbumFilters = ({
|
||||
return (
|
||||
<Stack p="0.8rem">
|
||||
{yesNoFilter.map((filter) => (
|
||||
<Group
|
||||
justify="space-between"
|
||||
key={`nd-filter-${filter.label}`}
|
||||
>
|
||||
<Group justify="space-between" key={`nd-filter-${filter.label}`}>
|
||||
<Text>{filter.label}</Text>
|
||||
<YesNoSelect
|
||||
onChange={filter.onChange}
|
||||
size="xs"
|
||||
value={filter.value}
|
||||
/>
|
||||
<YesNoSelect onChange={filter.onChange} size="xs" value={filter.value} />
|
||||
</Group>
|
||||
))}
|
||||
<Divider my="0.5rem" />
|
||||
|
||||
@@ -248,28 +248,15 @@ export const NavidromeAlbumFilters = ({
|
||||
return (
|
||||
<Stack p="0.8rem">
|
||||
{yesNoUndefinedFilters.map((filter) => (
|
||||
<Group
|
||||
justify="space-between"
|
||||
key={`nd-filter-${filter.label}`}
|
||||
>
|
||||
<Group justify="space-between" key={`nd-filter-${filter.label}`}>
|
||||
<Text>{filter.label}</Text>
|
||||
<YesNoSelect
|
||||
onChange={filter.onChange}
|
||||
size="xs"
|
||||
value={filter.value}
|
||||
/>
|
||||
<YesNoSelect onChange={filter.onChange} size="xs" value={filter.value} />
|
||||
</Group>
|
||||
))}
|
||||
{toggleFilters.map((filter) => (
|
||||
<Group
|
||||
justify="space-between"
|
||||
key={`nd-filter-${filter.label}`}
|
||||
>
|
||||
<Group justify="space-between" key={`nd-filter-${filter.label}`}>
|
||||
<Text>{filter.label}</Text>
|
||||
<Switch
|
||||
checked={filter?.value || false}
|
||||
onChange={filter.onChange}
|
||||
/>
|
||||
<Switch checked={filter?.value || false} onChange={filter.onChange} />
|
||||
</Group>
|
||||
))}
|
||||
<Divider my="0.5rem" />
|
||||
@@ -307,10 +294,7 @@ export const NavidromeAlbumFilters = ({
|
||||
{tagsQuery.data?.enumTags?.length &&
|
||||
tagsQuery.data.enumTags.length > 0 &&
|
||||
tagsQuery.data.enumTags.map((tag) => (
|
||||
<Group
|
||||
grow
|
||||
key={tag.name}
|
||||
>
|
||||
<Group grow key={tag.name}>
|
||||
<SelectWithInvalidData
|
||||
clearable
|
||||
data={tag.options}
|
||||
|
||||
@@ -148,15 +148,9 @@ export const SubsonicAlbumFilters = ({
|
||||
return (
|
||||
<Stack p="0.8rem">
|
||||
{toggleFilters.map((filter) => (
|
||||
<Group
|
||||
justify="space-between"
|
||||
key={`nd-filter-${filter.label}`}
|
||||
>
|
||||
<Group justify="space-between" key={`nd-filter-${filter.label}`}>
|
||||
<Text>{filter.label}</Text>
|
||||
<Switch
|
||||
checked={filter?.value || false}
|
||||
onChange={filter.onChange}
|
||||
/>
|
||||
<Switch checked={filter?.value || false} onChange={filter.onChange} />
|
||||
</Group>
|
||||
))}
|
||||
<Divider my="0.5rem" />
|
||||
|
||||
@@ -70,10 +70,7 @@ const AlbumDetailRoute = () => {
|
||||
}}
|
||||
ref={headerRef}
|
||||
/>
|
||||
<AlbumDetailContent
|
||||
background={background}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
<AlbumDetailContent background={background} tableRef={tableRef} />
|
||||
</NativeScrollArea>
|
||||
</AnimatedPage>
|
||||
);
|
||||
|
||||
@@ -144,11 +144,7 @@ const AlbumListRoute = () => {
|
||||
tableRef={tableRef}
|
||||
title={title}
|
||||
/>
|
||||
<AlbumListContent
|
||||
gridRef={gridRef}
|
||||
itemCount={itemCount}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
<AlbumListContent gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
|
||||
</ListContext.Provider>
|
||||
</AnimatedPage>
|
||||
);
|
||||
|
||||
@@ -174,10 +174,7 @@ const DummyAlbumDetailRoute = () => {
|
||||
</Stack>
|
||||
<div className={styles.detailContainer}>
|
||||
<section>
|
||||
<Group
|
||||
gap="sm"
|
||||
justify="space-between"
|
||||
>
|
||||
<Group gap="sm" justify="space-between">
|
||||
<Group>
|
||||
<PlayButton onClick={() => handlePlay()} />
|
||||
<ActionIcon
|
||||
@@ -231,11 +228,7 @@ const DummyAlbumDetailRoute = () => {
|
||||
<section>
|
||||
<Center>
|
||||
<Group mr={5}>
|
||||
<Icon
|
||||
fill="error"
|
||||
icon="error"
|
||||
size={30}
|
||||
/>
|
||||
<Icon fill="error" icon="error" size={30} />
|
||||
</Group>
|
||||
<h2>{t('error.badAlbum', { postProcess: 'sentenceCase' })}</h2>
|
||||
</Center>
|
||||
|
||||
@@ -202,10 +202,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||
order: itemOrder.recentAlbums,
|
||||
title: (
|
||||
<Group align="flex-end">
|
||||
<TextTitle
|
||||
fw={700}
|
||||
order={2}
|
||||
>
|
||||
<TextTitle fw={700} order={2}>
|
||||
{t('page.albumArtistDetail.recentReleases', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
@@ -232,10 +229,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||
loading: compilationAlbumsQuery?.isLoading || compilationAlbumsQuery.isFetching,
|
||||
order: itemOrder.compilations,
|
||||
title: (
|
||||
<TextTitle
|
||||
fw={700}
|
||||
order={2}
|
||||
>
|
||||
<TextTitle fw={700} order={2}>
|
||||
{t('page.albumArtistDetail.appearsOn', { postProcess: 'sentenceCase' })}
|
||||
</TextTitle>
|
||||
),
|
||||
@@ -247,10 +241,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||
itemType: LibraryItem.ALBUM_ARTIST,
|
||||
order: itemOrder.similarArtists,
|
||||
title: (
|
||||
<TextTitle
|
||||
fw={700}
|
||||
order={2}
|
||||
>
|
||||
<TextTitle fw={700} order={2}>
|
||||
{t('page.albumArtistDetail.relatedArtists', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
@@ -355,19 +346,10 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||
detailQuery?.isLoading ||
|
||||
(server?.type === ServerType.NAVIDROME && enabledItem.topSongs && topSongsQuery?.isLoading);
|
||||
|
||||
if (isLoading)
|
||||
return (
|
||||
<div
|
||||
className={styles.contentContainer}
|
||||
ref={cq.ref}
|
||||
/>
|
||||
);
|
||||
if (isLoading) return <div className={styles.contentContainer} ref={cq.ref} />;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.contentContainer}
|
||||
ref={cq.ref}
|
||||
>
|
||||
<div className={styles.contentContainer} ref={cq.ref}>
|
||||
<LibraryBackgroundOverlay backgroundColor={background} />
|
||||
<div className={styles.detailContainer}>
|
||||
<Group gap="md">
|
||||
@@ -481,15 +463,9 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||
) : null}
|
||||
<Grid gutter="xl">
|
||||
{biography ? (
|
||||
<Grid.Col
|
||||
order={itemOrder.biography}
|
||||
span={12}
|
||||
>
|
||||
<Grid.Col order={itemOrder.biography} span={12}>
|
||||
<section style={{ maxWidth: '1280px' }}>
|
||||
<TextTitle
|
||||
fw={700}
|
||||
order={2}
|
||||
>
|
||||
<TextTitle fw={700} order={2}>
|
||||
{t('page.albumArtistDetail.about', {
|
||||
artist: detailQuery?.data?.name,
|
||||
})}
|
||||
@@ -499,23 +475,11 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||
</Grid.Col>
|
||||
) : null}
|
||||
{showTopSongs ? (
|
||||
<Grid.Col
|
||||
order={itemOrder.topSongs}
|
||||
span={12}
|
||||
>
|
||||
<Grid.Col order={itemOrder.topSongs} span={12}>
|
||||
<section>
|
||||
<Group
|
||||
justify="space-between"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Group
|
||||
align="flex-end"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<TextTitle
|
||||
fw={700}
|
||||
order={2}
|
||||
>
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Group align="flex-end" wrap="nowrap">
|
||||
<TextTitle fw={700} order={2}>
|
||||
{t('page.albumArtistDetail.topSongs', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
|
||||
@@ -42,15 +42,9 @@ export const AlbumArtistListContent = ({
|
||||
return (
|
||||
<Suspense fallback={<Spinner container />}>
|
||||
{isGrid ? (
|
||||
<AlbumArtistListGridView
|
||||
gridRef={gridRef}
|
||||
itemCount={itemCount}
|
||||
/>
|
||||
<AlbumArtistListGridView gridRef={gridRef} itemCount={itemCount} />
|
||||
) : (
|
||||
<AlbumArtistListTableView
|
||||
itemCount={itemCount}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
<AlbumArtistListTableView itemCount={itemCount} tableRef={tableRef} />
|
||||
)}
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -372,11 +372,7 @@ export const AlbumArtistListHeaderFilters = ({
|
||||
|
||||
return (
|
||||
<Flex justify="space-between">
|
||||
<Group
|
||||
gap="sm"
|
||||
ref={cq.ref}
|
||||
w="100%"
|
||||
>
|
||||
<Group gap="sm" ref={cq.ref} w="100%">
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button variant="subtle">{sortByLabel}</Button>
|
||||
@@ -395,10 +391,7 @@ export const AlbumArtistListHeaderFilters = ({
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
<Divider orientation="vertical" />
|
||||
<OrderToggleButton
|
||||
onToggle={handleToggleSortOrder}
|
||||
sortOrder={filter.sortOrder}
|
||||
/>
|
||||
<OrderToggleButton onToggle={handleToggleSortOrder} sortOrder={filter.sortOrder} />
|
||||
{server?.type === ServerType.JELLYFIN && (
|
||||
<>
|
||||
<DropdownMenu position="bottom-start">
|
||||
@@ -437,10 +430,7 @@ export const AlbumArtistListHeaderFilters = ({
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
</Group>
|
||||
<Group
|
||||
gap="sm"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<ListConfigMenu
|
||||
autoFitColumns={table.autoFit}
|
||||
disabledViewTypes={[ListDisplayType.LIST]}
|
||||
|
||||
@@ -46,15 +46,9 @@ export const AlbumArtistListHeader = ({
|
||||
}, 500);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
gap={0}
|
||||
ref={cq.ref}
|
||||
>
|
||||
<Stack gap={0} ref={cq.ref}>
|
||||
<PageHeader>
|
||||
<Flex
|
||||
justify="space-between"
|
||||
w="100%"
|
||||
>
|
||||
<Flex justify="space-between" w="100%">
|
||||
<LibraryHeaderBar>
|
||||
<LibraryHeaderBar.Title>
|
||||
{t('page.albumArtistList.title', { postProcess: 'titleCase' })}
|
||||
@@ -66,18 +60,12 @@ export const AlbumArtistListHeader = ({
|
||||
</LibraryHeaderBar.Badge>
|
||||
</LibraryHeaderBar>
|
||||
<Group>
|
||||
<SearchInput
|
||||
defaultValue={filter.searchTerm}
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
<SearchInput defaultValue={filter.searchTerm} onChange={handleSearch} />
|
||||
</Group>
|
||||
</Flex>
|
||||
</PageHeader>
|
||||
<FilterBar>
|
||||
<AlbumArtistListHeaderFilters
|
||||
gridRef={gridRef}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
<AlbumArtistListHeaderFilters gridRef={gridRef} tableRef={tableRef} />
|
||||
</FilterBar>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -34,15 +34,9 @@ export const ArtistListContent = ({ gridRef, itemCount, tableRef }: ArtistListCo
|
||||
return (
|
||||
<Suspense fallback={<Spinner container />}>
|
||||
{isGrid ? (
|
||||
<ArtistListGridView
|
||||
gridRef={gridRef}
|
||||
itemCount={itemCount}
|
||||
/>
|
||||
<ArtistListGridView gridRef={gridRef} itemCount={itemCount} />
|
||||
) : (
|
||||
<ArtistListTableView
|
||||
itemCount={itemCount}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
<ArtistListTableView itemCount={itemCount} tableRef={tableRef} />
|
||||
)}
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -388,11 +388,7 @@ export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderF
|
||||
|
||||
return (
|
||||
<Flex justify="space-between">
|
||||
<Group
|
||||
gap="sm"
|
||||
ref={cq.ref}
|
||||
w="100%"
|
||||
>
|
||||
<Group gap="sm" ref={cq.ref} w="100%">
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button variant="subtle">{sortByLabel}</Button>
|
||||
@@ -411,19 +407,13 @@ export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderF
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
<Divider orientation="vertical" />
|
||||
<OrderToggleButton
|
||||
onToggle={handleToggleSortOrder}
|
||||
sortOrder={filter.sortOrder}
|
||||
/>
|
||||
<OrderToggleButton onToggle={handleToggleSortOrder} sortOrder={filter.sortOrder} />
|
||||
{server?.type === ServerType.JELLYFIN && (
|
||||
<>
|
||||
<Divider orientation="vertical" />
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<ActionIcon
|
||||
icon="folder"
|
||||
variant="subtle"
|
||||
/>
|
||||
<ActionIcon icon="folder" variant="subtle" />
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
{musicFoldersQuery.data?.items.map((folder) => (
|
||||
@@ -442,11 +432,7 @@ export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderF
|
||||
)}
|
||||
{roles.data?.length && (
|
||||
<>
|
||||
<Select
|
||||
data={roles.data}
|
||||
onChange={handleSetRole}
|
||||
value={filter.role}
|
||||
/>
|
||||
<Select data={roles.data} onChange={handleSetRole} value={filter.role} />
|
||||
</>
|
||||
)}
|
||||
<RefreshButton onClick={handleRefresh} />
|
||||
@@ -466,10 +452,7 @@ export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderF
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
</Group>
|
||||
<Group
|
||||
gap="xs"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<ListConfigMenu
|
||||
autoFitColumns={table.autoFit}
|
||||
displayType={display}
|
||||
|
||||
@@ -42,15 +42,9 @@ export const ArtistListHeader = ({ gridRef, itemCount, tableRef }: ArtistListHea
|
||||
}, 500);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
gap={0}
|
||||
ref={cq.ref}
|
||||
>
|
||||
<Stack gap={0} ref={cq.ref}>
|
||||
<PageHeader>
|
||||
<Flex
|
||||
justify="space-between"
|
||||
w="100%"
|
||||
>
|
||||
<Flex justify="space-between" w="100%">
|
||||
<LibraryHeaderBar>
|
||||
<LibraryHeaderBar.Title>
|
||||
{t('entity.artist_other', { postProcess: 'titleCase' })}
|
||||
@@ -62,18 +56,12 @@ export const ArtistListHeader = ({ gridRef, itemCount, tableRef }: ArtistListHea
|
||||
</LibraryHeaderBar.Badge>
|
||||
</LibraryHeaderBar>
|
||||
<Group>
|
||||
<SearchInput
|
||||
defaultValue={filter.searchTerm}
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
<SearchInput defaultValue={filter.searchTerm} onChange={handleSearch} />
|
||||
</Group>
|
||||
</Flex>
|
||||
</PageHeader>
|
||||
<FilterBar>
|
||||
<ArtistListHeaderFilters
|
||||
gridRef={gridRef}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
<ArtistListHeaderFilters gridRef={gridRef} tableRef={tableRef} />
|
||||
</FilterBar>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -41,16 +41,8 @@ const ArtistListRoute = () => {
|
||||
return (
|
||||
<AnimatedPage>
|
||||
<ListContext.Provider value={providerValue}>
|
||||
<ArtistListHeader
|
||||
gridRef={gridRef}
|
||||
itemCount={itemCount}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
<ArtistListContent
|
||||
gridRef={gridRef}
|
||||
itemCount={itemCount}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
<ArtistListHeader gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
|
||||
<ArtistListContent gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
|
||||
</ListContext.Provider>
|
||||
</AnimatedPage>
|
||||
);
|
||||
|
||||
@@ -533,10 +533,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
||||
|
||||
openModal({
|
||||
children: (
|
||||
<ConfirmModal
|
||||
loading={removeFromPlaylistMutation.isLoading}
|
||||
onConfirm={confirm}
|
||||
>
|
||||
<ConfirmModal loading={removeFromPlaylistMutation.isLoading} onConfirm={confirm}>
|
||||
{t('common.areYouSure', { postProcess: 'sentenceCase' })}
|
||||
</ConfirmModal>
|
||||
),
|
||||
@@ -922,26 +919,15 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
||||
<Portal>
|
||||
<AnimatePresence>
|
||||
{opened && (
|
||||
<ContextMenu
|
||||
minWidth={125}
|
||||
ref={mergedRef}
|
||||
xPos={ctx.xPos}
|
||||
yPos={ctx.yPos}
|
||||
>
|
||||
<ContextMenu minWidth={125} ref={mergedRef} xPos={ctx.xPos} yPos={ctx.yPos}>
|
||||
<Stack gap={0}>
|
||||
<Stack
|
||||
gap={0}
|
||||
onClick={closeContextMenu}
|
||||
>
|
||||
<Stack gap={0} onClick={closeContextMenu}>
|
||||
{ctx.menuItems?.map((item) => {
|
||||
return (
|
||||
!contextMenuItems[item.id].disabled && (
|
||||
<Fragment key={`context-menu-${item.id}`}>
|
||||
{item.children ? (
|
||||
<HoverCard
|
||||
offset={0}
|
||||
position="right"
|
||||
>
|
||||
<HoverCard offset={0} position="right">
|
||||
<HoverCard.Target>
|
||||
<ContextMenuButton
|
||||
leftIcon={
|
||||
|
||||
@@ -26,10 +26,8 @@ export const useDiscordRpc = () => {
|
||||
) => {
|
||||
if (
|
||||
!current[0] || // No track
|
||||
(current[0] &&
|
||||
current[2] === 'paused' && // Track paused
|
||||
(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)
|
||||
current[1] === 0 || // Start of track
|
||||
(current[2] === 'paused' && !discordSettings.showPaused) // Track paused with show paused setting disabled
|
||||
)
|
||||
return discordRpc?.clearActivity();
|
||||
|
||||
@@ -38,11 +36,13 @@ export const useDiscordRpc = () => {
|
||||
const trackChanged = lastUniqueId !== song.uniqueId;
|
||||
|
||||
/*
|
||||
1. If we jump more then 1.2 seconds from last state, update status to match
|
||||
2. If the current song id is completely different, update status
|
||||
3. If the player state changed, update status
|
||||
1. If the song has just started, update status
|
||||
2. If we jump more then 1.2 seconds from last state, update status to match
|
||||
3. If the current song id is completely different, update status
|
||||
4. If the player state changed, update status
|
||||
*/
|
||||
if (
|
||||
previous[1] === 0 ||
|
||||
Math.abs((current[1] as number) - (previous[1] as number)) > 1.2 ||
|
||||
trackChanged ||
|
||||
current[2] !== previous[2]
|
||||
|
||||
@@ -33,15 +33,9 @@ export const GenreListContent = ({ gridRef, itemCount, tableRef }: AlbumListCont
|
||||
return (
|
||||
<Suspense fallback={<Spinner container />}>
|
||||
{display === ListDisplayType.CARD || display === ListDisplayType.GRID ? (
|
||||
<GenreListGridView
|
||||
gridRef={gridRef}
|
||||
itemCount={itemCount}
|
||||
/>
|
||||
<GenreListGridView gridRef={gridRef} itemCount={itemCount} />
|
||||
) : (
|
||||
<GenreListTableView
|
||||
itemCount={itemCount}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
<GenreListTableView itemCount={itemCount} tableRef={tableRef} />
|
||||
)}
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -254,11 +254,7 @@ export const GenreListHeaderFilters = ({
|
||||
|
||||
return (
|
||||
<Flex justify="space-between">
|
||||
<Group
|
||||
gap="sm"
|
||||
ref={cq.ref}
|
||||
w="100%"
|
||||
>
|
||||
<Group gap="sm" ref={cq.ref} w="100%">
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button variant="subtle">{sortByLabel}</Button>
|
||||
@@ -277,10 +273,7 @@ export const GenreListHeaderFilters = ({
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
<Divider orientation="vertical" />
|
||||
<OrderToggleButton
|
||||
onToggle={handleToggleSortOrder}
|
||||
sortOrder={filter.sortOrder}
|
||||
/>
|
||||
<OrderToggleButton onToggle={handleToggleSortOrder} sortOrder={filter.sortOrder} />
|
||||
{server?.type === ServerType.JELLYFIN && (
|
||||
<>
|
||||
<Divider orientation="vertical" />
|
||||
@@ -340,10 +333,7 @@ export const GenreListHeaderFilters = ({
|
||||
</Button>
|
||||
</DropdownMenu>
|
||||
</Group>
|
||||
<Group
|
||||
gap="sm"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<ListConfigMenu
|
||||
autoFitColumns={table.autoFit}
|
||||
disabledViewTypes={[ListDisplayType.LIST]}
|
||||
|
||||
@@ -40,15 +40,9 @@ export const GenreListHeader = ({ gridRef, itemCount, tableRef }: GenreListHeade
|
||||
}, 500);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
gap={0}
|
||||
ref={cq.ref}
|
||||
>
|
||||
<Stack gap={0} ref={cq.ref}>
|
||||
<PageHeader>
|
||||
<Flex
|
||||
justify="space-between"
|
||||
w="100%"
|
||||
>
|
||||
<Flex justify="space-between" w="100%">
|
||||
<LibraryHeaderBar>
|
||||
<LibraryHeaderBar.Title>
|
||||
{t('page.genreList.title', { postProcess: 'titleCase' })}
|
||||
@@ -60,10 +54,7 @@ export const GenreListHeader = ({ gridRef, itemCount, tableRef }: GenreListHeade
|
||||
</LibraryHeaderBar.Badge>
|
||||
</LibraryHeaderBar>
|
||||
<Group>
|
||||
<SearchInput
|
||||
defaultValue={filter.searchTerm}
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
<SearchInput defaultValue={filter.searchTerm} onChange={handleSearch} />
|
||||
</Group>
|
||||
</Flex>
|
||||
</PageHeader>
|
||||
|
||||
@@ -42,16 +42,8 @@ const GenreListRoute = () => {
|
||||
return (
|
||||
<AnimatedPage>
|
||||
<ListContext.Provider value={providerValue}>
|
||||
<GenreListHeader
|
||||
gridRef={gridRef}
|
||||
itemCount={itemCount}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
<GenreListContent
|
||||
gridRef={gridRef}
|
||||
itemCount={itemCount}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
<GenreListHeader gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
|
||||
<GenreListContent gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
|
||||
</ListContext.Provider>
|
||||
</AnimatedPage>
|
||||
);
|
||||
|
||||
@@ -81,10 +81,7 @@ const formatArtists = (artists: null | RelatedArtist[] | undefined) =>
|
||||
{artist.name || '—'}
|
||||
</Text>
|
||||
) : (
|
||||
<Text
|
||||
overflow="visible"
|
||||
size="md"
|
||||
>
|
||||
<Text overflow="visible" size="md">
|
||||
{artist.name || '-'}
|
||||
</Text>
|
||||
)}
|
||||
@@ -119,17 +116,7 @@ const FormatGenre = (item: Album | AlbumArtist | Playlist | Song) => {
|
||||
};
|
||||
|
||||
const BoolField = (key: boolean) =>
|
||||
key ? (
|
||||
<Icon
|
||||
color="success"
|
||||
icon="check"
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
color="error"
|
||||
icon="x"
|
||||
/>
|
||||
);
|
||||
key ? <Icon color="success" icon="check" /> : <Icon color="error" icon="x" />;
|
||||
|
||||
const AlbumPropertyMapping: ItemDetailRow<Album>[] = [
|
||||
{ key: 'name', label: 'common.title' },
|
||||
@@ -287,6 +274,8 @@ const SongPropertyMapping: ItemDetailRow<Song>[] = [
|
||||
{ label: 'filter.isCompilation', render: (song) => BoolField(song.compilation || false) },
|
||||
{ key: 'container', label: 'common.codec' },
|
||||
{ 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: 'size', label: 'common.size', render: (song) => formatSizeString(song.size) },
|
||||
{
|
||||
@@ -409,12 +398,7 @@ export const ItemDetailsModal = ({ item }: ItemDetailsModalProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
highlightOnHover
|
||||
variant="vertical"
|
||||
withRowBorders={false}
|
||||
withTableBorder
|
||||
>
|
||||
<Table highlightOnHover variant="vertical" withRowBorders={false} withTableBorder>
|
||||
<Table.Tbody>{body}</Table.Tbody>
|
||||
</Table>
|
||||
);
|
||||
|
||||
@@ -22,10 +22,7 @@ export const SongPath = ({ path }: SongPathProps) => {
|
||||
|
||||
return (
|
||||
<Group>
|
||||
<CopyButton
|
||||
timeout={2000}
|
||||
value={path}
|
||||
>
|
||||
<CopyButton timeout={2000} value={path}>
|
||||
{({ copied, copy }) => (
|
||||
<Tooltip
|
||||
label={t(
|
||||
@@ -36,10 +33,7 @@ export const SongPath = ({ path }: SongPathProps) => {
|
||||
)}
|
||||
withinPortal
|
||||
>
|
||||
<ActionIcon
|
||||
onClick={copy}
|
||||
variant="transparent"
|
||||
>
|
||||
<ActionIcon onClick={copy} variant="transparent">
|
||||
{copied ? <Icon icon="check" /> : <Icon icon="clipboardCopy" />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
@@ -38,33 +38,15 @@ const SearchResult = ({ data, onClick }: SearchResultProps) => {
|
||||
source === LyricSource.GENIUS ? id.replace(/^((http[s]?|ftp):\/)?\/?([^:/\s]+)/g, '') : id;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={styles.searchItem}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Group
|
||||
justify="space-between"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Stack
|
||||
gap={0}
|
||||
maw="65%"
|
||||
>
|
||||
<Text
|
||||
fw={600}
|
||||
size="md"
|
||||
>
|
||||
<button className={styles.searchItem} onClick={onClick}>
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Stack gap={0} maw="65%">
|
||||
<Text fw={600} size="md">
|
||||
{name}
|
||||
</Text>
|
||||
<Text isMuted>{artist}</Text>
|
||||
<Group
|
||||
gap="sm"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Text
|
||||
isMuted
|
||||
size="sm"
|
||||
>
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<Text isMuted size="sm">
|
||||
{[source, cleanId].join(' — ')}
|
||||
</Text>
|
||||
</Group>
|
||||
@@ -167,11 +149,7 @@ export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearch
|
||||
export const openLyricSearchModal = ({ artist, name, onSearchOverride }: LyricSearchFormProps) => {
|
||||
openModal({
|
||||
children: (
|
||||
<LyricsSearchForm
|
||||
artist={artist}
|
||||
name={name}
|
||||
onSearchOverride={onSearchOverride}
|
||||
/>
|
||||
<LyricsSearchForm artist={artist} name={name} onSearchOverride={onSearchOverride} />
|
||||
),
|
||||
size: 'lg',
|
||||
title: i18n.t('form.lyricSearch.title', { postProcess: 'titleCase' }) as string,
|
||||
|
||||
@@ -151,10 +151,7 @@ export const Lyrics = () => {
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||
<div className={styles.lyricsContainer}>
|
||||
{isLoadingLyrics ? (
|
||||
<Spinner
|
||||
container
|
||||
size={25}
|
||||
/>
|
||||
<Spinner container size={25} />
|
||||
) : (
|
||||
<AnimatePresence mode="sync">
|
||||
{hasNoLyrics ? (
|
||||
|
||||
@@ -29,10 +29,7 @@ export const UnsynchronizedLyrics = ({
|
||||
}, [translatedLyrics]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.container}
|
||||
style={{ gap: `${settings.gapUnsync}px` }}
|
||||
>
|
||||
<div className={styles.container} style={{ gap: `${settings.gapUnsync}px` }}>
|
||||
{settings.showProvider && source && (
|
||||
<LyricLine
|
||||
alignment={settings.alignment}
|
||||
|
||||
@@ -11,30 +11,17 @@ export const DrawerPlayQueue = () => {
|
||||
const queueRef = useRef<null | { grid: AgGridReactType<Song> }>(null);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
direction="column"
|
||||
h="100%"
|
||||
>
|
||||
<Flex direction="column" h="100%">
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'var(--theme-colors-background)',
|
||||
borderRadius: '10px',
|
||||
}}
|
||||
>
|
||||
<PlayQueueListControls
|
||||
tableRef={queueRef}
|
||||
type="sideQueue"
|
||||
/>
|
||||
<PlayQueueListControls tableRef={queueRef} type="sideQueue" />
|
||||
</div>
|
||||
<Flex
|
||||
bg="var(--theme-colors-background)"
|
||||
h="100%"
|
||||
mb="0.6rem"
|
||||
>
|
||||
<PlayQueue
|
||||
ref={queueRef}
|
||||
type="sideQueue"
|
||||
/>
|
||||
<Flex bg="var(--theme-colors-background)" h="100%" mb="0.6rem">
|
||||
<PlayQueue ref={queueRef} type="sideQueue" />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -174,10 +174,7 @@ export const PlayQueueListControls = ({ tableRef, type }: PlayQueueListOptionsPr
|
||||
/>
|
||||
</Group>
|
||||
<Group>
|
||||
<Popover
|
||||
position="top-end"
|
||||
transitionProps={{ transition: 'fade' }}
|
||||
>
|
||||
<Popover position="top-end" transitionProps={{ transition: 'fade' }}>
|
||||
<Popover.Target>
|
||||
<ActionIcon
|
||||
icon="settings"
|
||||
|
||||
@@ -18,19 +18,10 @@ export const SidebarPlayQueue = () => {
|
||||
const isWeb = windowBarStyle === Platform.WEB;
|
||||
return (
|
||||
<VirtualGridContainer>
|
||||
<Box
|
||||
display={!isWeb ? 'flex' : undefined}
|
||||
h="65px"
|
||||
>
|
||||
<PlayQueueListControls
|
||||
tableRef={queueRef}
|
||||
type="sideQueue"
|
||||
/>
|
||||
<Box display={!isWeb ? 'flex' : undefined} h="65px">
|
||||
<PlayQueueListControls tableRef={queueRef} type="sideQueue" />
|
||||
</Box>
|
||||
<PlayQueue
|
||||
ref={queueRef}
|
||||
type="sideQueue"
|
||||
/>
|
||||
<PlayQueue ref={queueRef} type="sideQueue" />
|
||||
</VirtualGridContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,14 +16,8 @@ const NowPlayingRoute = () => {
|
||||
<AnimatedPage>
|
||||
<VirtualGridContainer>
|
||||
<NowPlayingHeader />
|
||||
<PlayQueueListControls
|
||||
tableRef={queueRef}
|
||||
type="nowPlaying"
|
||||
/>
|
||||
<PlayQueue
|
||||
ref={queueRef}
|
||||
type="nowPlaying"
|
||||
/>
|
||||
<PlayQueueListControls tableRef={queueRef} type="nowPlaying" />
|
||||
<PlayQueue ref={queueRef} type="nowPlaying" />
|
||||
</VirtualGridContainer>
|
||||
</AnimatedPage>
|
||||
);
|
||||
|
||||
@@ -115,13 +115,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||
<div className={styles.controlsContainer}>
|
||||
<div className={styles.buttonsContainer}>
|
||||
<PlayerButton
|
||||
icon={
|
||||
<Icon
|
||||
fill="default"
|
||||
icon="mediaStop"
|
||||
size={buttonSize - 2}
|
||||
/>
|
||||
}
|
||||
icon={<Icon fill="default" icon="mediaStop" size={buttonSize - 2} />}
|
||||
onClick={handleStop}
|
||||
tooltip={{
|
||||
label: t('player.stop', { postProcess: 'sentenceCase' }),
|
||||
@@ -152,13 +146,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||
variant="tertiary"
|
||||
/>
|
||||
<PlayerButton
|
||||
icon={
|
||||
<Icon
|
||||
fill="default"
|
||||
icon="mediaPrevious"
|
||||
size={buttonSize}
|
||||
/>
|
||||
}
|
||||
icon={<Icon fill="default" icon="mediaPrevious" size={buttonSize} />}
|
||||
onClick={handlePrevTrack}
|
||||
tooltip={{
|
||||
label: t('player.previous', { postProcess: 'sentenceCase' }),
|
||||
@@ -169,11 +157,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||
{skip?.enabled && (
|
||||
<PlayerButton
|
||||
icon={
|
||||
<Icon
|
||||
fill="default"
|
||||
icon="mediaStepBackward"
|
||||
size={buttonSize}
|
||||
/>
|
||||
<Icon fill="default" icon="mediaStepBackward" size={buttonSize} />
|
||||
}
|
||||
onClick={() => handleSkipBackward(skip?.skipBackwardSeconds)}
|
||||
tooltip={{
|
||||
@@ -194,13 +178,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||
/>
|
||||
{skip?.enabled && (
|
||||
<PlayerButton
|
||||
icon={
|
||||
<Icon
|
||||
fill="default"
|
||||
icon="mediaStepForward"
|
||||
size={buttonSize}
|
||||
/>
|
||||
}
|
||||
icon={<Icon fill="default" icon="mediaStepForward" size={buttonSize} />}
|
||||
onClick={() => handleSkipForward(skip?.skipForwardSeconds)}
|
||||
tooltip={{
|
||||
label: t('player.skip', {
|
||||
@@ -214,13 +192,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||
/>
|
||||
)}
|
||||
<PlayerButton
|
||||
icon={
|
||||
<Icon
|
||||
fill="default"
|
||||
icon="mediaNext"
|
||||
size={buttonSize}
|
||||
/>
|
||||
}
|
||||
icon={<Icon fill="default" icon="mediaNext" size={buttonSize} />}
|
||||
onClick={handleNextTrack}
|
||||
tooltip={{
|
||||
label: t('player.next', { postProcess: 'sentenceCase' }),
|
||||
@@ -231,11 +203,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||
<PlayerButton
|
||||
icon={
|
||||
repeat === PlayerRepeat.ONE ? (
|
||||
<Icon
|
||||
fill="primary"
|
||||
icon="mediaRepeatOne"
|
||||
size={buttonSize}
|
||||
/>
|
||||
<Icon fill="primary" icon="mediaRepeatOne" size={buttonSize} />
|
||||
) : (
|
||||
<Icon
|
||||
fill={repeat === PlayerRepeat.NONE ? 'default' : 'primary'}
|
||||
@@ -268,13 +236,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||
variant="tertiary"
|
||||
/>
|
||||
<PlayerButton
|
||||
icon={
|
||||
<Icon
|
||||
fill="default"
|
||||
icon="mediaRandom"
|
||||
size={buttonSize}
|
||||
/>
|
||||
}
|
||||
icon={<Icon fill="default" icon="mediaRandom" size={buttonSize} />}
|
||||
onClick={() =>
|
||||
openShuffleAllModal({
|
||||
handlePlayQueueAdd,
|
||||
@@ -291,12 +253,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||
</div>
|
||||
<div className={styles.sliderContainer}>
|
||||
<div className={styles.sliderValueWrapper}>
|
||||
<Text
|
||||
fw={600}
|
||||
isMuted
|
||||
isNoSelect
|
||||
size="xs"
|
||||
>
|
||||
<Text fw={600} isMuted isNoSelect size="xs">
|
||||
{formattedTime}
|
||||
</Text>
|
||||
</div>
|
||||
@@ -324,12 +281,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.sliderValueWrapper}>
|
||||
<Text
|
||||
fw={600}
|
||||
isMuted
|
||||
isNoSelect
|
||||
size="xs"
|
||||
>
|
||||
<Text fw={600} isMuted isNoSelect size="xs">
|
||||
{duration}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
@@ -68,11 +68,7 @@ const ImageWithPlaceholder = ({
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
color="muted"
|
||||
icon="itemAlbum"
|
||||
size="25%"
|
||||
/>
|
||||
<Icon color="muted" icon="itemAlbum" size="25%" />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
@@ -167,14 +163,8 @@ export const FullScreenPlayerImage = () => {
|
||||
justify="flex-start"
|
||||
p="1rem"
|
||||
>
|
||||
<div
|
||||
className={styles.imageContainer}
|
||||
ref={mainImageRef}
|
||||
>
|
||||
<AnimatePresence
|
||||
initial={false}
|
||||
mode="sync"
|
||||
>
|
||||
<div className={styles.imageContainer} ref={mainImageRef}>
|
||||
<AnimatePresence initial={false} mode="sync">
|
||||
{imageState.current === 0 && (
|
||||
<ImageWithPlaceholder
|
||||
animate="open"
|
||||
@@ -206,18 +196,8 @@ export const FullScreenPlayerImage = () => {
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<Stack
|
||||
className={styles.metadataContainer}
|
||||
gap="md"
|
||||
maw="100%"
|
||||
>
|
||||
<Text
|
||||
fw={900}
|
||||
lh="1.2"
|
||||
overflow="hidden"
|
||||
size="4xl"
|
||||
w="100%"
|
||||
>
|
||||
<Stack className={styles.metadataContainer} gap="md" maw="100%">
|
||||
<Text fw={900} lh="1.2" overflow="hidden" size="4xl" w="100%">
|
||||
{currentSong?.name}
|
||||
</Text>
|
||||
<Text
|
||||
@@ -257,10 +237,7 @@ export const FullScreenPlayerImage = () => {
|
||||
</Fragment>
|
||||
))}
|
||||
</Text>
|
||||
<Group
|
||||
justify="center"
|
||||
mt="sm"
|
||||
>
|
||||
<Group justify="center" mt="sm">
|
||||
{currentSong?.container && (
|
||||
<Badge variant="transparent">{currentSong?.container}</Badge>
|
||||
)}
|
||||
|
||||
@@ -76,10 +76,7 @@ export const FullScreenPlayerQueue = () => {
|
||||
justify="center"
|
||||
>
|
||||
{headerItems.map((item) => (
|
||||
<div
|
||||
className={styles.headerItemWrapper}
|
||||
key={`tab-${item.label}`}
|
||||
>
|
||||
<div className={styles.headerItemWrapper} key={`tab-${item.label}`}>
|
||||
<Button
|
||||
flex={1}
|
||||
fw="600"
|
||||
|
||||
@@ -238,10 +238,7 @@ const Controls = ({ isPageHovered }: ControlsProps) => {
|
||||
})}
|
||||
</Option.Label>
|
||||
<Option.Control>
|
||||
<Group
|
||||
w="100%"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Group w="100%" wrap="nowrap">
|
||||
<Slider
|
||||
defaultValue={lyricConfig.fontSize}
|
||||
label={(e) =>
|
||||
@@ -278,10 +275,7 @@ const Controls = ({ isPageHovered }: ControlsProps) => {
|
||||
})}
|
||||
</Option.Label>
|
||||
<Option.Control>
|
||||
<Group
|
||||
w="100%"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Group w="100%" wrap="nowrap">
|
||||
<Slider
|
||||
defaultValue={lyricConfig.gap}
|
||||
label={(e) => `Synchronized: ${e}px`}
|
||||
|
||||
@@ -4,10 +4,5 @@ import { useCurrentSong } from '/@/renderer/store';
|
||||
export const FullScreenSimilarSongs = () => {
|
||||
const currentSong = useCurrentSong();
|
||||
|
||||
return currentSong?.id ? (
|
||||
<SimilarSongsList
|
||||
fullScreen
|
||||
song={currentSong}
|
||||
/>
|
||||
) : null;
|
||||
return currentSong?.id ? <SimilarSongsList fullScreen song={currentSong} /> : null;
|
||||
};
|
||||
|
||||
@@ -69,10 +69,7 @@ export const LeftControls = () => {
|
||||
return (
|
||||
<div className={styles.leftControlsContainer}>
|
||||
<LayoutGroup>
|
||||
<AnimatePresence
|
||||
initial={false}
|
||||
mode="popLayout"
|
||||
>
|
||||
<AnimatePresence initial={false} mode="popLayout">
|
||||
{!hideImage && (
|
||||
<div className={styles.imageWrapper}>
|
||||
<motion.div
|
||||
@@ -123,19 +120,9 @@ export const LeftControls = () => {
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<motion.div
|
||||
className={styles.metadataStack}
|
||||
layout="position"
|
||||
>
|
||||
<div
|
||||
className={styles.lineItem}
|
||||
onClick={stopPropagation}
|
||||
>
|
||||
<Group
|
||||
align="center"
|
||||
gap="xs"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<motion.div className={styles.metadataStack} layout="position">
|
||||
<div className={styles.lineItem} onClick={stopPropagation}>
|
||||
<Group align="center" gap="xs" wrap="nowrap">
|
||||
<Text
|
||||
component={Link}
|
||||
fw={500}
|
||||
|
||||
@@ -193,13 +193,7 @@ export const RightControls = () => {
|
||||
}, [addToFavoritesMutation, removeFromFavoritesMutation, updateRatingMutation]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
align="flex-end"
|
||||
direction="column"
|
||||
h="100%"
|
||||
px="1rem"
|
||||
py="0.5rem"
|
||||
>
|
||||
<Flex align="flex-end" direction="column" h="100%" px="1rem" py="0.5rem">
|
||||
<Group h="calc(100% / 3)">
|
||||
{showRating && (
|
||||
<Rating
|
||||
@@ -209,18 +203,8 @@ export const RightControls = () => {
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
<Group
|
||||
align="center"
|
||||
gap="xs"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<DropdownMenu
|
||||
arrowOffset={12}
|
||||
offset={0}
|
||||
position="top-end"
|
||||
width={425}
|
||||
withArrow
|
||||
>
|
||||
<Group align="center" gap="xs" wrap="nowrap">
|
||||
<DropdownMenu arrowOffset={12} offset={0} position="top-end" width={425} withArrow>
|
||||
<DropdownMenu.Target>
|
||||
<ActionIcon
|
||||
icon="mediaSpeed"
|
||||
|
||||
@@ -33,10 +33,5 @@ export const Visualizer = () => {
|
||||
return () => {};
|
||||
}, [accent, canvasRef, motion, webAudio]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.container}
|
||||
ref={canvasRef}
|
||||
/>
|
||||
);
|
||||
return <div className={styles.container} ref={canvasRef} />;
|
||||
};
|
||||
|
||||
@@ -34,6 +34,8 @@ Progress Events (Jellyfin only):
|
||||
- Sends the 'progress' scrobble event on an interval
|
||||
*/
|
||||
|
||||
type PlayerEvent = [PlayerStatus, number];
|
||||
|
||||
type SongEvent = [QueueSong | undefined, number, 1 | 2];
|
||||
|
||||
const checkScrobbleConditions = (args: {
|
||||
@@ -86,21 +88,35 @@ export const useScrobble = () => {
|
||||
);
|
||||
|
||||
const progressIntervalId = useRef<null | ReturnType<typeof setInterval>>(null);
|
||||
const songChangeTimeoutId = useRef<null | ReturnType<typeof setTimeout>>(null);
|
||||
const songChangeTimeoutId = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||
const notifyTimeoutId = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||
|
||||
const handleScrobbleFromSongChange = useCallback(
|
||||
(current: SongEvent, previous: SongEvent) => {
|
||||
if (scrobbleSettings?.notify && current[0]) {
|
||||
if (scrobbleSettings?.notify && current[0]?.id) {
|
||||
clearTimeout(notifyTimeoutId.current);
|
||||
const currentSong = current[0];
|
||||
|
||||
const artists =
|
||||
currentSong.artists?.length > 0
|
||||
? currentSong.artists.map((artist) => artist.name).join(', ')
|
||||
: currentSong.artistName;
|
||||
// Set a delay so that quickly (within a second) switching songs doesn't trigger multiple
|
||||
// notifications
|
||||
notifyTimeoutId.current = setTimeout(() => {
|
||||
// Only trigger if the song changed, or the player changed. This should be the case
|
||||
// anyways, but who knows
|
||||
if (
|
||||
currentSong.uniqueId !== previous[0]?.uniqueId ||
|
||||
current[2] !== previous[2]
|
||||
) {
|
||||
const artists =
|
||||
currentSong.artists?.length > 0
|
||||
? currentSong.artists.map((artist) => artist.name).join(', ')
|
||||
: currentSong.artistName;
|
||||
|
||||
new Notification(`Now playing ${currentSong.name}`, {
|
||||
body: `by ${artists} on ${currentSong.album}`,
|
||||
icon: currentSong.imageUrl || undefined,
|
||||
});
|
||||
new Notification(`Now playing ${currentSong.name}`, {
|
||||
body: `by ${artists} on ${currentSong.album}`,
|
||||
icon: currentSong.imageUrl || undefined,
|
||||
});
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
if (!isScrobbleEnabled) return;
|
||||
@@ -110,7 +126,6 @@ export const useScrobble = () => {
|
||||
progressIntervalId.current = null;
|
||||
}
|
||||
|
||||
// const currentSong = current[0] as QueueSong | undefined;
|
||||
const previousSong = previous[0];
|
||||
const previousSongTimeSec = previous[1];
|
||||
|
||||
@@ -146,7 +161,7 @@ export const useScrobble = () => {
|
||||
setIsCurrentSongScrobbled(false);
|
||||
|
||||
// Use a timeout to prevent spamming the server with scrobble events when switching through songs quickly
|
||||
clearTimeout(songChangeTimeoutId.current as ReturnType<typeof setTimeout>);
|
||||
clearTimeout(songChangeTimeoutId.current);
|
||||
songChangeTimeoutId.current = setTimeout(() => {
|
||||
const currentSong = current[0];
|
||||
// Get the current status from the state, not variable. This is because
|
||||
@@ -193,10 +208,7 @@ export const useScrobble = () => {
|
||||
);
|
||||
|
||||
const handleScrobbleFromStatusChange = useCallback(
|
||||
(
|
||||
current: (number | PlayerStatus | undefined)[],
|
||||
previous: (number | PlayerStatus | undefined)[],
|
||||
) => {
|
||||
(current: PlayerEvent, previous: PlayerEvent) => {
|
||||
if (!isScrobbleEnabled) return;
|
||||
|
||||
const currentSong = usePlayerStore.getState().current.song;
|
||||
@@ -208,8 +220,8 @@ export const useScrobble = () => {
|
||||
? usePlayerStore.getState().current.time * 1e7
|
||||
: undefined;
|
||||
|
||||
const currentStatus = current[0] as PlayerStatus;
|
||||
const currentTimeSec = current[1] as number;
|
||||
const currentStatus = current[0];
|
||||
const currentTimeSec = current[1];
|
||||
|
||||
// Whenever the player is restarted, send a 'start' scrobble
|
||||
if (currentStatus === PlayerStatus.PLAYING) {
|
||||
@@ -249,12 +261,12 @@ export const useScrobble = () => {
|
||||
});
|
||||
|
||||
if (progressIntervalId.current) {
|
||||
clearInterval(progressIntervalId.current as ReturnType<typeof setInterval>);
|
||||
clearInterval(progressIntervalId.current);
|
||||
progressIntervalId.current = null;
|
||||
}
|
||||
} else {
|
||||
const isLastTrackInQueue = usePlayerStore.getState().actions.checkIsLastTrack();
|
||||
const previousTimeSec = previous[1] as number;
|
||||
const previousTimeSec = previous[1];
|
||||
|
||||
// If not already scrobbled, send a 'submission' scrobble if conditions are met
|
||||
const shouldSubmitScrobble = checkScrobbleConditions({
|
||||
@@ -358,17 +370,17 @@ export const useScrobble = () => {
|
||||
// multiple times in a row and playback goes normally (no next/previous)
|
||||
equalityFn: (a, b) =>
|
||||
// compute whether the song changed
|
||||
(a[0] as QueueSong)?.uniqueId === (b[0] as QueueSong)?.uniqueId &&
|
||||
a[0]?.uniqueId === b[0]?.uniqueId &&
|
||||
// compute whether the same player: relevant for repeat one and repeat all (one track)
|
||||
a[2] === b[2],
|
||||
},
|
||||
);
|
||||
|
||||
const unsubStatusChange = usePlayerStore.subscribe(
|
||||
(state) => [state.current.status, state.current.time],
|
||||
(state): PlayerEvent => [state.current.status, state.current.time],
|
||||
handleScrobbleFromStatusChange,
|
||||
{
|
||||
equalityFn: (a, b) => (a[0] as PlayerStatus) === (b[0] as PlayerStatus),
|
||||
equalityFn: (a, b) => a[0] === b[0],
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -155,10 +155,7 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
|
||||
)}
|
||||
|
||||
<Group justify="flex-end">
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
variant="subtle"
|
||||
>
|
||||
<Button onClick={onCancel} variant="subtle">
|
||||
{t('common.cancel', { postProcess: 'titleCase' })}
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -331,11 +331,7 @@ export const PlaylistDetailSongListContent = ({ songs, tableRef }: PlaylistDetai
|
||||
/>
|
||||
</VirtualGridAutoSizerContainer>
|
||||
{isPaginationEnabled && (
|
||||
<AnimatePresence
|
||||
initial={false}
|
||||
mode="wait"
|
||||
presenceAffectsLayout
|
||||
>
|
||||
<AnimatePresence initial={false} mode="wait" presenceAffectsLayout>
|
||||
{page.display === ListDisplayType.TABLE_PAGINATED && (
|
||||
<TablePagination
|
||||
pageKey={playlistId}
|
||||
|
||||
+2
-9
@@ -469,11 +469,7 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
||||
|
||||
return (
|
||||
<Flex justify="space-between">
|
||||
<Group
|
||||
gap="sm"
|
||||
ref={cq.ref}
|
||||
w="100%"
|
||||
>
|
||||
<Group gap="sm" ref={cq.ref} w="100%">
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
@@ -555,10 +551,7 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
||||
{server?.type === ServerType.NAVIDROME && !isSmartPlaylist && (
|
||||
<>
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Item
|
||||
isDanger
|
||||
onClick={handleToggleShowQueryBuilder}
|
||||
>
|
||||
<DropdownMenu.Item isDanger onClick={handleToggleShowQueryBuilder}>
|
||||
{t('action.toggleSmartPlaylistEditor', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
|
||||
@@ -33,15 +33,9 @@ export const PlaylistListContent = ({ gridRef, itemCount, tableRef }: PlaylistLi
|
||||
return (
|
||||
<Suspense fallback={<Spinner container />}>
|
||||
{display === ListDisplayType.CARD || display === ListDisplayType.GRID ? (
|
||||
<PlaylistListGridView
|
||||
gridRef={gridRef}
|
||||
itemCount={itemCount}
|
||||
/>
|
||||
<PlaylistListGridView gridRef={gridRef} itemCount={itemCount} />
|
||||
) : (
|
||||
<PlaylistListTableView
|
||||
itemCount={itemCount}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
<PlaylistListTableView itemCount={itemCount} tableRef={tableRef} />
|
||||
)}
|
||||
<div />
|
||||
</Suspense>
|
||||
|
||||
@@ -355,11 +355,7 @@ export const PlaylistListHeaderFilters = ({
|
||||
|
||||
return (
|
||||
<Flex justify="space-between">
|
||||
<Group
|
||||
gap="sm"
|
||||
ref={cq.ref}
|
||||
w="100%"
|
||||
>
|
||||
<Group gap="sm" ref={cq.ref} w="100%">
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button variant="subtle">{sortByLabel}</Button>
|
||||
@@ -378,10 +374,7 @@ export const PlaylistListHeaderFilters = ({
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
<Divider orientation="vertical" />
|
||||
<OrderToggleButton
|
||||
onToggle={handleToggleSortOrder}
|
||||
sortOrder={filter.sortOrder}
|
||||
/>
|
||||
<OrderToggleButton onToggle={handleToggleSortOrder} sortOrder={filter.sortOrder} />
|
||||
<RefreshButton onClick={handleRefresh} />
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
@@ -397,14 +390,8 @@ export const PlaylistListHeaderFilters = ({
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
</Group>
|
||||
<Group
|
||||
gap="xs"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Button
|
||||
onClick={handleCreatePlaylistModal}
|
||||
variant="subtle"
|
||||
>
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Button onClick={handleCreatePlaylistModal} variant="subtle">
|
||||
{t('action.createPlaylist', { postProcess: 'sentenceCase' })}
|
||||
</Button>
|
||||
<ListConfigMenu
|
||||
|
||||
@@ -44,16 +44,9 @@ export const PlaylistListHeader = ({ gridRef, itemCount, tableRef }: PlaylistLis
|
||||
}, 500);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
gap={0}
|
||||
ref={cq.ref}
|
||||
>
|
||||
<Stack gap={0} ref={cq.ref}>
|
||||
<PageHeader>
|
||||
<Flex
|
||||
align="center"
|
||||
justify="space-between"
|
||||
w="100%"
|
||||
>
|
||||
<Flex align="center" justify="space-between" w="100%">
|
||||
<LibraryHeaderBar>
|
||||
<LibraryHeaderBar.Title>
|
||||
{t('page.playlistList.title', { postProcess: 'titleCase' })}
|
||||
@@ -67,18 +60,12 @@ export const PlaylistListHeader = ({ gridRef, itemCount, tableRef }: PlaylistLis
|
||||
</Badge>
|
||||
</LibraryHeaderBar>
|
||||
<Group>
|
||||
<SearchInput
|
||||
defaultValue={filter.searchTerm}
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
<SearchInput defaultValue={filter.searchTerm} onChange={handleSearch} />
|
||||
</Group>
|
||||
</Flex>
|
||||
</PageHeader>
|
||||
<FilterBar>
|
||||
<PlaylistListHeaderFilters
|
||||
gridRef={gridRef}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
<PlaylistListHeaderFilters gridRef={gridRef} tableRef={tableRef} />
|
||||
</FilterBar>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user