mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-06 20:10:12 +02:00
add global music folder selector
This commit is contained in:
+1
-1
@@ -127,7 +127,7 @@
|
||||
"semver": "^7.5.4",
|
||||
"string-to-color": "^2.2.2",
|
||||
"ws": "^8.18.2",
|
||||
"zod": "^4.1.12",
|
||||
"zod": "^3.22.3",
|
||||
"zustand": "^5.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Generated
+85
-85
@@ -61,7 +61,7 @@ importers:
|
||||
version: 5.90.11(@tanstack/react-query@5.90.9(react@19.1.0))(react@19.1.0)
|
||||
'@ts-rest/core':
|
||||
specifier: ^3.52.1
|
||||
version: 3.52.1(@types/node@24.10.1)(zod@4.1.12)
|
||||
version: 3.52.1(@types/node@24.10.1)(zod@3.25.76)
|
||||
'@types/react-window':
|
||||
specifier: ^1.8.8
|
||||
version: 1.8.8
|
||||
@@ -121,7 +121,7 @@ importers:
|
||||
version: 7.1.0
|
||||
i18next:
|
||||
specifier: ^25.6.2
|
||||
version: 25.6.2(typescript@5.9.3)
|
||||
version: 25.6.2(typescript@5.8.3)
|
||||
idb-keyval:
|
||||
specifier: ^6.2.2
|
||||
version: 6.2.2
|
||||
@@ -175,7 +175,7 @@ importers:
|
||||
version: 5.0.0(react@19.1.0)
|
||||
react-i18next:
|
||||
specifier: ^16.3.3
|
||||
version: 16.3.3(i18next@25.6.2(typescript@5.9.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.3)
|
||||
version: 16.3.3(i18next@25.6.2(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3)
|
||||
react-icons:
|
||||
specifier: ^5.5.0
|
||||
version: 5.5.0(react@19.1.0)
|
||||
@@ -210,8 +210,8 @@ importers:
|
||||
specifier: ^8.18.2
|
||||
version: 8.18.2
|
||||
zod:
|
||||
specifier: ^4.1.12
|
||||
version: 4.1.12
|
||||
specifier: ^3.22.3
|
||||
version: 3.25.76
|
||||
zustand:
|
||||
specifier: ^5.0.5
|
||||
version: 5.0.8(@types/react@19.2.5)(immer@10.2.0)(react@19.1.0)(use-sync-external-store@1.6.0(react@19.1.0))
|
||||
@@ -221,7 +221,7 @@ importers:
|
||||
version: 3.0.0(eslint@9.27.0)(prettier@3.6.2)
|
||||
'@electron-toolkit/eslint-config-ts':
|
||||
specifier: ^3.0.0
|
||||
version: 3.1.0(eslint@9.27.0)(typescript@5.9.3)
|
||||
version: 3.1.0(eslint@9.27.0)(typescript@5.8.3)
|
||||
'@electron-toolkit/tsconfig':
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0(@types/node@24.10.1)
|
||||
@@ -275,7 +275,7 @@ importers:
|
||||
version: 9.27.0
|
||||
eslint-plugin-perfectionist:
|
||||
specifier: ^4.13.0
|
||||
version: 4.13.0(eslint@9.27.0)(typescript@5.9.3)
|
||||
version: 4.13.0(eslint@9.27.0)(typescript@5.8.3)
|
||||
eslint-plugin-prettier:
|
||||
specifier: ^5.4.0
|
||||
version: 5.4.0(eslint-config-prettier@10.1.5(eslint@9.27.0))(eslint@9.27.0)(prettier@3.6.2)
|
||||
@@ -305,19 +305,19 @@ importers:
|
||||
version: 2.5.19(prettier@3.6.2)
|
||||
stylelint:
|
||||
specifier: ^16.25.0
|
||||
version: 16.25.0(typescript@5.9.3)
|
||||
version: 16.25.0(typescript@5.8.3)
|
||||
stylelint-config-css-modules:
|
||||
specifier: ^4.5.1
|
||||
version: 4.5.1(stylelint@16.25.0(typescript@5.9.3))
|
||||
version: 4.5.1(stylelint@16.25.0(typescript@5.8.3))
|
||||
stylelint-config-recess-order:
|
||||
specifier: ^7.4.0
|
||||
version: 7.4.0(stylelint-order@6.0.4(stylelint@16.25.0(typescript@5.9.3)))(stylelint@16.25.0(typescript@5.9.3))
|
||||
version: 7.4.0(stylelint-order@6.0.4(stylelint@16.25.0(typescript@5.8.3)))(stylelint@16.25.0(typescript@5.8.3))
|
||||
stylelint-config-standard:
|
||||
specifier: ^39.0.1
|
||||
version: 39.0.1(stylelint@16.25.0(typescript@5.9.3))
|
||||
version: 39.0.1(stylelint@16.25.0(typescript@5.8.3))
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
version: 5.9.3
|
||||
version: 5.8.3
|
||||
vite:
|
||||
specifier: ^7.2.2
|
||||
version: 7.2.2(@types/node@24.10.1)(sass-embedded@1.89.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(yaml@2.8.1)
|
||||
@@ -5367,8 +5367,8 @@ packages:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: '>=4.8.4 <5.9.0'
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
typescript@5.8.3:
|
||||
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
@@ -5790,8 +5790,8 @@ packages:
|
||||
peerDependencies:
|
||||
zod: ^3.25.0 || ^4.0.0
|
||||
|
||||
zod@4.1.12:
|
||||
resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==}
|
||||
zod@3.25.76:
|
||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||
|
||||
zustand@5.0.8:
|
||||
resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==}
|
||||
@@ -6608,14 +6608,14 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@types/eslint'
|
||||
|
||||
'@electron-toolkit/eslint-config-ts@3.1.0(eslint@9.27.0)(typescript@5.9.3)':
|
||||
'@electron-toolkit/eslint-config-ts@3.1.0(eslint@9.27.0)(typescript@5.8.3)':
|
||||
dependencies:
|
||||
'@eslint/js': 9.27.0
|
||||
eslint: 9.27.0
|
||||
globals: 16.1.0
|
||||
typescript-eslint: 8.32.1(eslint@9.27.0)(typescript@5.9.3)
|
||||
typescript-eslint: 8.32.1(eslint@9.27.0)(typescript@5.8.3)
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
typescript: 5.8.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -7504,10 +7504,10 @@ snapshots:
|
||||
|
||||
'@tootallnate/once@2.0.0': {}
|
||||
|
||||
'@ts-rest/core@3.52.1(@types/node@24.10.1)(zod@4.1.12)':
|
||||
'@ts-rest/core@3.52.1(@types/node@24.10.1)(zod@3.25.76)':
|
||||
optionalDependencies:
|
||||
'@types/node': 24.10.1
|
||||
zod: 4.1.12
|
||||
zod: 3.25.76
|
||||
|
||||
'@types/babel__core@7.20.5':
|
||||
dependencies:
|
||||
@@ -7625,32 +7625,32 @@ snapshots:
|
||||
'@types/node': 24.10.1
|
||||
optional: true
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0)(typescript@5.9.3))(eslint@9.27.0)(typescript@5.9.3)':
|
||||
'@typescript-eslint/eslint-plugin@8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0)(typescript@5.8.3))(eslint@9.27.0)(typescript@5.8.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.1
|
||||
'@typescript-eslint/parser': 8.32.1(eslint@9.27.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.32.1(eslint@9.27.0)(typescript@5.8.3)
|
||||
'@typescript-eslint/scope-manager': 8.32.1
|
||||
'@typescript-eslint/type-utils': 8.32.1(eslint@9.27.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.32.1(eslint@9.27.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/type-utils': 8.32.1(eslint@9.27.0)(typescript@5.8.3)
|
||||
'@typescript-eslint/utils': 8.32.1(eslint@9.27.0)(typescript@5.8.3)
|
||||
'@typescript-eslint/visitor-keys': 8.32.1
|
||||
eslint: 9.27.0
|
||||
graphemer: 1.4.0
|
||||
ignore: 7.0.4
|
||||
natural-compare: 1.4.0
|
||||
ts-api-utils: 2.1.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
ts-api-utils: 2.1.0(typescript@5.8.3)
|
||||
typescript: 5.8.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/parser@8.32.1(eslint@9.27.0)(typescript@5.9.3)':
|
||||
'@typescript-eslint/parser@8.32.1(eslint@9.27.0)(typescript@5.8.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.32.1
|
||||
'@typescript-eslint/types': 8.32.1
|
||||
'@typescript-eslint/typescript-estree': 8.32.1(typescript@5.9.3)
|
||||
'@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3)
|
||||
'@typescript-eslint/visitor-keys': 8.32.1
|
||||
debug: 4.4.3
|
||||
eslint: 9.27.0
|
||||
typescript: 5.9.3
|
||||
typescript: 5.8.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -7659,20 +7659,20 @@ snapshots:
|
||||
'@typescript-eslint/types': 8.32.1
|
||||
'@typescript-eslint/visitor-keys': 8.32.1
|
||||
|
||||
'@typescript-eslint/type-utils@8.32.1(eslint@9.27.0)(typescript@5.9.3)':
|
||||
'@typescript-eslint/type-utils@8.32.1(eslint@9.27.0)(typescript@5.8.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/typescript-estree': 8.32.1(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.32.1(eslint@9.27.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3)
|
||||
'@typescript-eslint/utils': 8.32.1(eslint@9.27.0)(typescript@5.8.3)
|
||||
debug: 4.4.3
|
||||
eslint: 9.27.0
|
||||
ts-api-utils: 2.1.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
ts-api-utils: 2.1.0(typescript@5.8.3)
|
||||
typescript: 5.8.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/types@8.32.1': {}
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.32.1(typescript@5.9.3)':
|
||||
'@typescript-eslint/typescript-estree@8.32.1(typescript@5.8.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.32.1
|
||||
'@typescript-eslint/visitor-keys': 8.32.1
|
||||
@@ -7681,19 +7681,19 @@ snapshots:
|
||||
is-glob: 4.0.3
|
||||
minimatch: 9.0.5
|
||||
semver: 7.7.2
|
||||
ts-api-utils: 2.1.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
ts-api-utils: 2.1.0(typescript@5.8.3)
|
||||
typescript: 5.8.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/utils@8.32.1(eslint@9.27.0)(typescript@5.9.3)':
|
||||
'@typescript-eslint/utils@8.32.1(eslint@9.27.0)(typescript@5.8.3)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.7.0(eslint@9.27.0)
|
||||
'@typescript-eslint/scope-manager': 8.32.1
|
||||
'@typescript-eslint/types': 8.32.1
|
||||
'@typescript-eslint/typescript-estree': 8.32.1(typescript@5.9.3)
|
||||
'@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3)
|
||||
eslint: 9.27.0
|
||||
typescript: 5.9.3
|
||||
typescript: 5.8.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -8301,7 +8301,7 @@ snapshots:
|
||||
config-file-ts@0.2.8-rc1:
|
||||
dependencies:
|
||||
glob: 10.4.5
|
||||
typescript: 5.9.3
|
||||
typescript: 5.8.3
|
||||
|
||||
convert-source-map@2.0.0: {}
|
||||
|
||||
@@ -8316,14 +8316,14 @@ snapshots:
|
||||
|
||||
core-util-is@1.0.3: {}
|
||||
|
||||
cosmiconfig@9.0.0(typescript@5.9.3):
|
||||
cosmiconfig@9.0.0(typescript@5.8.3):
|
||||
dependencies:
|
||||
env-paths: 2.2.1
|
||||
import-fresh: 3.3.1
|
||||
js-yaml: 4.1.0
|
||||
parse-json: 5.2.0
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
typescript: 5.8.3
|
||||
|
||||
crc@3.8.0:
|
||||
dependencies:
|
||||
@@ -8852,10 +8852,10 @@ snapshots:
|
||||
dependencies:
|
||||
eslint: 9.27.0
|
||||
|
||||
eslint-plugin-perfectionist@4.13.0(eslint@9.27.0)(typescript@5.9.3):
|
||||
eslint-plugin-perfectionist@4.13.0(eslint@9.27.0)(typescript@5.8.3):
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.32.1
|
||||
'@typescript-eslint/utils': 8.32.1(eslint@9.27.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.32.1(eslint@9.27.0)(typescript@5.8.3)
|
||||
eslint: 9.27.0
|
||||
natural-orderby: 5.0.0
|
||||
transitivePeerDependencies:
|
||||
@@ -8877,8 +8877,8 @@ snapshots:
|
||||
'@babel/parser': 7.28.5
|
||||
eslint: 9.27.0
|
||||
hermes-parser: 0.25.1
|
||||
zod: 4.1.12
|
||||
zod-validation-error: 4.0.2(zod@4.1.12)
|
||||
zod: 3.25.76
|
||||
zod-validation-error: 4.0.2(zod@3.25.76)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -9461,28 +9461,28 @@ snapshots:
|
||||
esbuild: 0.25.11
|
||||
fs-extra: 11.3.2
|
||||
gulp-sort: 2.0.0
|
||||
i18next: 24.2.3(typescript@5.9.3)
|
||||
i18next: 24.2.3(typescript@5.8.3)
|
||||
js-yaml: 4.1.0
|
||||
lilconfig: 3.1.3
|
||||
rsvp: 4.8.5
|
||||
sort-keys: 5.1.0
|
||||
typescript: 5.9.3
|
||||
typescript: 5.8.3
|
||||
vinyl: 3.0.0
|
||||
vinyl-fs: 4.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
i18next@24.2.3(typescript@5.9.3):
|
||||
i18next@24.2.3(typescript@5.8.3):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.1
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
typescript: 5.8.3
|
||||
|
||||
i18next@25.6.2(typescript@5.9.3):
|
||||
i18next@25.6.2(typescript@5.8.3):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
typescript: 5.8.3
|
||||
|
||||
iconv-corefoundation@1.1.7:
|
||||
dependencies:
|
||||
@@ -10431,16 +10431,16 @@ snapshots:
|
||||
|
||||
react-fast-compare@3.2.2: {}
|
||||
|
||||
react-i18next@16.3.3(i18next@25.6.2(typescript@5.9.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.3):
|
||||
react-i18next@16.3.3(i18next@25.6.2(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
html-parse-stringify: 3.0.1
|
||||
i18next: 25.6.2(typescript@5.9.3)
|
||||
i18next: 25.6.2(typescript@5.8.3)
|
||||
react: 19.1.0
|
||||
use-sync-external-store: 1.6.0(react@19.1.0)
|
||||
optionalDependencies:
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
typescript: 5.9.3
|
||||
typescript: 5.8.3
|
||||
|
||||
react-icons@5.5.0(react@19.1.0):
|
||||
dependencies:
|
||||
@@ -11139,33 +11139,33 @@ snapshots:
|
||||
|
||||
strnum@2.1.1: {}
|
||||
|
||||
stylelint-config-css-modules@4.5.1(stylelint@16.25.0(typescript@5.9.3)):
|
||||
stylelint-config-css-modules@4.5.1(stylelint@16.25.0(typescript@5.8.3)):
|
||||
dependencies:
|
||||
stylelint: 16.25.0(typescript@5.9.3)
|
||||
stylelint: 16.25.0(typescript@5.8.3)
|
||||
optionalDependencies:
|
||||
stylelint-scss: 6.12.1(stylelint@16.25.0(typescript@5.9.3))
|
||||
stylelint-scss: 6.12.1(stylelint@16.25.0(typescript@5.8.3))
|
||||
|
||||
stylelint-config-recess-order@7.4.0(stylelint-order@6.0.4(stylelint@16.25.0(typescript@5.9.3)))(stylelint@16.25.0(typescript@5.9.3)):
|
||||
stylelint-config-recess-order@7.4.0(stylelint-order@6.0.4(stylelint@16.25.0(typescript@5.8.3)))(stylelint@16.25.0(typescript@5.8.3)):
|
||||
dependencies:
|
||||
stylelint: 16.25.0(typescript@5.9.3)
|
||||
stylelint-order: 6.0.4(stylelint@16.25.0(typescript@5.9.3))
|
||||
stylelint: 16.25.0(typescript@5.8.3)
|
||||
stylelint-order: 6.0.4(stylelint@16.25.0(typescript@5.8.3))
|
||||
|
||||
stylelint-config-recommended@17.0.0(stylelint@16.25.0(typescript@5.9.3)):
|
||||
stylelint-config-recommended@17.0.0(stylelint@16.25.0(typescript@5.8.3)):
|
||||
dependencies:
|
||||
stylelint: 16.25.0(typescript@5.9.3)
|
||||
stylelint: 16.25.0(typescript@5.8.3)
|
||||
|
||||
stylelint-config-standard@39.0.1(stylelint@16.25.0(typescript@5.9.3)):
|
||||
stylelint-config-standard@39.0.1(stylelint@16.25.0(typescript@5.8.3)):
|
||||
dependencies:
|
||||
stylelint: 16.25.0(typescript@5.9.3)
|
||||
stylelint-config-recommended: 17.0.0(stylelint@16.25.0(typescript@5.9.3))
|
||||
stylelint: 16.25.0(typescript@5.8.3)
|
||||
stylelint-config-recommended: 17.0.0(stylelint@16.25.0(typescript@5.8.3))
|
||||
|
||||
stylelint-order@6.0.4(stylelint@16.25.0(typescript@5.9.3)):
|
||||
stylelint-order@6.0.4(stylelint@16.25.0(typescript@5.8.3)):
|
||||
dependencies:
|
||||
postcss: 8.5.6
|
||||
postcss-sorting: 8.0.2(postcss@8.5.6)
|
||||
stylelint: 16.25.0(typescript@5.9.3)
|
||||
stylelint: 16.25.0(typescript@5.8.3)
|
||||
|
||||
stylelint-scss@6.12.1(stylelint@16.25.0(typescript@5.9.3)):
|
||||
stylelint-scss@6.12.1(stylelint@16.25.0(typescript@5.8.3)):
|
||||
dependencies:
|
||||
css-tree: 3.1.0
|
||||
is-plain-object: 5.0.0
|
||||
@@ -11175,10 +11175,10 @@ snapshots:
|
||||
postcss-resolve-nested-selector: 0.1.6
|
||||
postcss-selector-parser: 7.1.0
|
||||
postcss-value-parser: 4.2.0
|
||||
stylelint: 16.25.0(typescript@5.9.3)
|
||||
stylelint: 16.25.0(typescript@5.8.3)
|
||||
optional: true
|
||||
|
||||
stylelint@16.25.0(typescript@5.9.3):
|
||||
stylelint@16.25.0(typescript@5.8.3):
|
||||
dependencies:
|
||||
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
|
||||
'@csstools/css-tokenizer': 3.0.4
|
||||
@@ -11187,7 +11187,7 @@ snapshots:
|
||||
'@dual-bundle/import-meta-resolve': 4.2.1
|
||||
balanced-match: 2.0.0
|
||||
colord: 2.9.3
|
||||
cosmiconfig: 9.0.0(typescript@5.9.3)
|
||||
cosmiconfig: 9.0.0(typescript@5.8.3)
|
||||
css-functions-list: 3.2.3
|
||||
css-tree: 3.1.0
|
||||
debug: 4.4.3
|
||||
@@ -11362,9 +11362,9 @@ snapshots:
|
||||
dependencies:
|
||||
utf8-byte-length: 1.0.5
|
||||
|
||||
ts-api-utils@2.1.0(typescript@5.9.3):
|
||||
ts-api-utils@2.1.0(typescript@5.8.3):
|
||||
dependencies:
|
||||
typescript: 5.9.3
|
||||
typescript: 5.8.3
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
@@ -11414,17 +11414,17 @@ snapshots:
|
||||
possible-typed-array-names: 1.1.0
|
||||
reflect.getprototypeof: 1.0.10
|
||||
|
||||
typescript-eslint@8.32.1(eslint@9.27.0)(typescript@5.9.3):
|
||||
typescript-eslint@8.32.1(eslint@9.27.0)(typescript@5.8.3):
|
||||
dependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0)(typescript@5.9.3))(eslint@9.27.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.32.1(eslint@9.27.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.32.1(eslint@9.27.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/eslint-plugin': 8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0)(typescript@5.8.3))(eslint@9.27.0)(typescript@5.8.3)
|
||||
'@typescript-eslint/parser': 8.32.1(eslint@9.27.0)(typescript@5.8.3)
|
||||
'@typescript-eslint/utils': 8.32.1(eslint@9.27.0)(typescript@5.8.3)
|
||||
eslint: 9.27.0
|
||||
typescript: 5.9.3
|
||||
typescript: 5.8.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
typescript@5.9.3: {}
|
||||
typescript@5.8.3: {}
|
||||
|
||||
unbox-primitive@1.1.0:
|
||||
dependencies:
|
||||
@@ -11883,11 +11883,11 @@ snapshots:
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
zod-validation-error@4.0.2(zod@4.1.12):
|
||||
zod-validation-error@4.0.2(zod@3.25.76):
|
||||
dependencies:
|
||||
zod: 4.1.12
|
||||
zod: 3.25.76
|
||||
|
||||
zod@4.1.12: {}
|
||||
zod@3.25.76: {}
|
||||
|
||||
zustand@5.0.8(@types/react@19.2.5)(immer@10.2.0)(react@19.1.0)(use-sync-external-store@1.6.0(react@19.1.0)):
|
||||
optionalDependencies:
|
||||
|
||||
@@ -349,6 +349,9 @@
|
||||
"openBrowserDevtools": "open browser devtools",
|
||||
"quit": "$t(common.quit)",
|
||||
"selectServer": "select server",
|
||||
"selectMusicFolder": "select music folder",
|
||||
"noMusicFolder": "no music folder selected",
|
||||
"multipleMusicFolders": "{{count}} music folders selected",
|
||||
"settings": "$t(common.setting_other)",
|
||||
"version": "version {{version}}"
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ import i18n from '/@/i18n/i18n';
|
||||
import { JellyfinController } from '/@/renderer/api/jellyfin/jellyfin-controller';
|
||||
import { NavidromeController } from '/@/renderer/api/navidrome/navidrome-controller';
|
||||
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
|
||||
import { mergeMusicFolderId } from '/@/renderer/api/utils-music-folder';
|
||||
import { getServerById, useAuthStore } from '/@/renderer/store';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import {
|
||||
@@ -167,7 +168,11 @@ export const controller: GeneralController = {
|
||||
return apiController(
|
||||
'getAlbumArtistList',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
)?.({
|
||||
...args,
|
||||
apiClientProps: { ...args.apiClientProps, server },
|
||||
query: mergeMusicFolderId(args.query, server),
|
||||
});
|
||||
},
|
||||
getAlbumArtistListCount(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
@@ -181,7 +186,11 @@ export const controller: GeneralController = {
|
||||
return apiController(
|
||||
'getAlbumArtistListCount',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
)?.({
|
||||
...args,
|
||||
apiClientProps: { ...args.apiClientProps, server },
|
||||
query: mergeMusicFolderId(args.query, server),
|
||||
});
|
||||
},
|
||||
getAlbumDetail(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
@@ -223,7 +232,11 @@ export const controller: GeneralController = {
|
||||
return apiController(
|
||||
'getAlbumList',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
)?.({
|
||||
...args,
|
||||
apiClientProps: { ...args.apiClientProps, server },
|
||||
query: mergeMusicFolderId(args.query, server),
|
||||
});
|
||||
},
|
||||
getAlbumListCount(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
@@ -237,7 +250,11 @@ export const controller: GeneralController = {
|
||||
return apiController(
|
||||
'getAlbumListCount',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
)?.({
|
||||
...args,
|
||||
apiClientProps: { ...args.apiClientProps, server },
|
||||
query: mergeMusicFolderId(args.query, server),
|
||||
});
|
||||
},
|
||||
getArtistList(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
@@ -251,7 +268,11 @@ export const controller: GeneralController = {
|
||||
return apiController(
|
||||
'getArtistList',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
)?.({
|
||||
...args,
|
||||
apiClientProps: { ...args.apiClientProps, server },
|
||||
query: mergeMusicFolderId(args.query, server),
|
||||
});
|
||||
},
|
||||
getArtistListCount(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
@@ -265,7 +286,11 @@ export const controller: GeneralController = {
|
||||
return apiController(
|
||||
'getArtistListCount',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
)?.({
|
||||
...args,
|
||||
apiClientProps: { ...args.apiClientProps, server },
|
||||
query: mergeMusicFolderId(args.query, server),
|
||||
});
|
||||
},
|
||||
getDownloadUrl(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
@@ -293,7 +318,11 @@ export const controller: GeneralController = {
|
||||
return apiController(
|
||||
'getGenreList',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
)?.({
|
||||
...args,
|
||||
apiClientProps: { ...args.apiClientProps, server },
|
||||
query: mergeMusicFolderId(args.query, server),
|
||||
});
|
||||
},
|
||||
getLyrics(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
@@ -461,7 +490,11 @@ export const controller: GeneralController = {
|
||||
return apiController(
|
||||
'getSongList',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
)?.({
|
||||
...args,
|
||||
apiClientProps: { ...args.apiClientProps, server },
|
||||
query: mergeMusicFolderId(args.query, server),
|
||||
});
|
||||
},
|
||||
getSongListCount(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
@@ -475,7 +508,11 @@ export const controller: GeneralController = {
|
||||
return apiController(
|
||||
'getSongListCount',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
)?.({
|
||||
...args,
|
||||
apiClientProps: { ...args.apiClientProps, server },
|
||||
query: mergeMusicFolderId(args.query, server),
|
||||
});
|
||||
},
|
||||
getStructuredLyrics(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
@@ -601,7 +638,11 @@ export const controller: GeneralController = {
|
||||
return apiController(
|
||||
'search',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
)?.({
|
||||
...args,
|
||||
apiClientProps: { ...args.apiClientProps, server },
|
||||
query: mergeMusicFolderId(args.query, server),
|
||||
});
|
||||
},
|
||||
setRating(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
@@ -59,6 +59,14 @@ const excludeMissing = (server?: null | ServerListItemWithCredential) => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const getLibraryId = (musicFolderId?: string | string[]): string[] | undefined => {
|
||||
if (!musicFolderId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return Array.isArray(musicFolderId) ? musicFolderId : [musicFolderId];
|
||||
};
|
||||
|
||||
const getArtistSongKey = (server: null | ServerListItemWithCredential) =>
|
||||
hasFeature(server, ServerFeature.TRACK_ALBUM_ARTIST_SEARCH) ? 'artists_id' : 'album_artist_id';
|
||||
|
||||
@@ -189,6 +197,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||
_sort: albumArtistListSortMap.navidrome[query.sortBy],
|
||||
_start: query.startIndex,
|
||||
library_id: getLibraryId(query.musicFolderId),
|
||||
name: query.searchTerm,
|
||||
...query._custom,
|
||||
role: hasFeature(apiClientProps.server, ServerFeature.BFR) ? 'albumartist' : '',
|
||||
@@ -287,6 +296,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
artist_id: query.artistIds?.[0],
|
||||
compilation: query.compilation,
|
||||
genre_id: genres,
|
||||
library_id: getLibraryId(query.musicFolderId),
|
||||
name: query.searchTerm,
|
||||
...query._custom,
|
||||
starred: query.favorite,
|
||||
@@ -318,6 +328,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||
_sort: albumArtistListSortMap.navidrome[query.sortBy],
|
||||
_start: query.startIndex,
|
||||
library_id: getLibraryId(query.musicFolderId),
|
||||
name: query.searchTerm,
|
||||
...query._custom,
|
||||
role: query.role || undefined,
|
||||
@@ -361,6 +372,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||
_sort: genreListSortMap.navidrome[query.sortBy],
|
||||
_start: query.startIndex,
|
||||
library_id: getLibraryId(query.musicFolderId),
|
||||
name: query.searchTerm,
|
||||
},
|
||||
});
|
||||
@@ -480,6 +492,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
...navidromeFeatures,
|
||||
...subsonicArgs.features,
|
||||
publicPlaylist: [1],
|
||||
[ServerFeature.MUSIC_FOLDER_MULTISELECT]: [1],
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -567,6 +580,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
album_id: query.albumIds,
|
||||
genre_id: query.genreIds,
|
||||
[getArtistSongKey(apiClientProps.server)]: query.artistIds ?? query.albumArtistIds,
|
||||
library_id: getLibraryId(query.musicFolderId),
|
||||
starred: query.favorite,
|
||||
title: query.searchTerm,
|
||||
...query._custom,
|
||||
|
||||
@@ -1388,7 +1388,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
setRating: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
const itemIds = query.item.map((item) => item.id);
|
||||
const itemIds = query.id;
|
||||
|
||||
for (const id of itemIds) {
|
||||
await ssApiClient(apiClientProps).setRating({
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ServerListItemWithCredential } from '/@/shared/types/domain-types';
|
||||
|
||||
export const mergeMusicFolderId = <T extends { musicFolderId?: string | string[] }>(
|
||||
query: T,
|
||||
server: null | ServerListItemWithCredential,
|
||||
): T => {
|
||||
if (
|
||||
!server ||
|
||||
!server.musicFolderId ||
|
||||
server.musicFolderId.length === 0 ||
|
||||
query.musicFolderId
|
||||
) {
|
||||
return query;
|
||||
}
|
||||
|
||||
// Only merge if server matches and musicFolderId is not already in query
|
||||
const musicFolderId =
|
||||
server.musicFolderId.length === 1 ? server.musicFolderId[0] : server.musicFolderId;
|
||||
|
||||
return {
|
||||
...query,
|
||||
musicFolderId,
|
||||
};
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ALBUM_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns';
|
||||
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
||||
import { ListFilters } from '/@/renderer/features/shared/components/list-filters';
|
||||
import { ListMusicFolderDropdown } from '/@/renderer/features/shared/components/list-music-folder-dropdown';
|
||||
import { ListRefreshButton } from '/@/renderer/features/shared/components/list-refresh-button';
|
||||
import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown';
|
||||
import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
|
||||
@@ -25,7 +24,6 @@ export const AlbumListHeaderFilters = () => {
|
||||
defaultSortOrder={SortOrder.ASC}
|
||||
listKey={ItemListKey.ALBUM}
|
||||
/>
|
||||
<ListMusicFolderDropdown listKey={ItemListKey.ALBUM} />
|
||||
<ListFilters itemType={LibraryItem.ALBUM} />
|
||||
<ListRefreshButton listKey={ItemListKey.ALBUM} />
|
||||
</Group>
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
useQueryState,
|
||||
} from 'nuqs';
|
||||
|
||||
import { useMusicFolderIdFilter } from '/@/renderer/features/shared/hooks/use-music-folder-id-filter';
|
||||
import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter';
|
||||
import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-filter';
|
||||
import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter';
|
||||
@@ -20,8 +19,6 @@ export const useAlbumListFilters = () => {
|
||||
|
||||
const { sortOrder } = useSortOrderFilter(null, ItemListKey.ALBUM);
|
||||
|
||||
const { musicFolderId } = useMusicFolderIdFilter(null, ItemListKey.ALBUM);
|
||||
|
||||
const { searchTerm, setSearchTerm } = useSearchTermFilter('');
|
||||
|
||||
const [genreId, setGenreId] = useQueryState(
|
||||
@@ -67,7 +64,6 @@ export const useAlbumListFilters = () => {
|
||||
[FILTER_KEYS.ALBUM.MAX_YEAR]: maxYear ?? undefined,
|
||||
[FILTER_KEYS.ALBUM.MIN_YEAR]: minYear ?? undefined,
|
||||
[FILTER_KEYS.ALBUM.RECENTLY_PLAYED]: recentlyPlayed ?? undefined,
|
||||
[FILTER_KEYS.SHARED.MUSIC_FOLDER_ID]: musicFolderId ?? undefined,
|
||||
[FILTER_KEYS.SHARED.SEARCH_TERM]: searchTerm ?? undefined,
|
||||
[FILTER_KEYS.SHARED.SORT_BY]: sortBy ?? undefined,
|
||||
[FILTER_KEYS.SHARED.SORT_ORDER]: sortOrder ?? undefined,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ALBUM_ARTIST_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns';
|
||||
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
||||
import { ListMusicFolderDropdown } from '/@/renderer/features/shared/components/list-music-folder-dropdown';
|
||||
import { ListRefreshButton } from '/@/renderer/features/shared/components/list-refresh-button';
|
||||
import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown';
|
||||
import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
|
||||
@@ -27,7 +26,6 @@ export const AlbumArtistListHeaderFilters = () => {
|
||||
defaultSortOrder={SortOrder.ASC}
|
||||
listKey={ItemListKey.ALBUM_ARTIST}
|
||||
/>
|
||||
<ListMusicFolderDropdown listKey={ItemListKey.ALBUM_ARTIST} />
|
||||
<ListRefreshButton listKey={ItemListKey.ALBUM_ARTIST} />
|
||||
</Group>
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { ALBUM_ARTIST_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns';
|
||||
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
|
||||
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
||||
import { ListMusicFolderDropdown } from '/@/renderer/features/shared/components/list-music-folder-dropdown';
|
||||
import { ListRefreshButton } from '/@/renderer/features/shared/components/list-refresh-button';
|
||||
import { ListSelectFilter } from '/@/renderer/features/shared/components/list-select-filter';
|
||||
import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown';
|
||||
@@ -36,7 +35,6 @@ export const ArtistListHeaderFilters = () => {
|
||||
defaultSortOrder={SortOrder.ASC}
|
||||
listKey={ItemListKey.ARTIST}
|
||||
/>
|
||||
<ListMusicFolderDropdown listKey={ItemListKey.ARTIST} />
|
||||
{rolesQuery.data && rolesQuery.data.length > 0 && (
|
||||
<>
|
||||
<Divider orientation="vertical" />
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useMusicFolderIdFilter } from '/@/renderer/features/shared/hooks/use-music-folder-id-filter';
|
||||
import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter';
|
||||
import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-filter';
|
||||
import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter';
|
||||
@@ -11,12 +10,9 @@ export const useAlbumArtistListFilters = () => {
|
||||
|
||||
const { sortOrder } = useSortOrderFilter(null, ItemListKey.ALBUM_ARTIST);
|
||||
|
||||
const { musicFolderId } = useMusicFolderIdFilter(null, ItemListKey.ALBUM_ARTIST);
|
||||
|
||||
const { searchTerm, setSearchTerm } = useSearchTermFilter('');
|
||||
|
||||
const query = {
|
||||
[FILTER_KEYS.SHARED.MUSIC_FOLDER_ID]: musicFolderId ?? undefined,
|
||||
[FILTER_KEYS.SHARED.SEARCH_TERM]: searchTerm ?? undefined,
|
||||
[FILTER_KEYS.SHARED.SORT_BY]: sortBy ?? undefined,
|
||||
[FILTER_KEYS.SHARED.SORT_ORDER]: sortOrder ?? undefined,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useMusicFolderIdFilter } from '/@/renderer/features/shared/hooks/use-music-folder-id-filter';
|
||||
import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter';
|
||||
import { useSelectFilter } from '/@/renderer/features/shared/hooks/use-select-filter';
|
||||
import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-filter';
|
||||
@@ -12,15 +11,12 @@ export const useArtistListFilters = () => {
|
||||
|
||||
const { sortOrder } = useSortOrderFilter(null, ItemListKey.ARTIST);
|
||||
|
||||
const { musicFolderId } = useMusicFolderIdFilter(null, ItemListKey.ARTIST);
|
||||
|
||||
const { searchTerm, setSearchTerm } = useSearchTermFilter('');
|
||||
|
||||
const { value: role } = useSelectFilter(FILTER_KEYS.ARTIST.ROLE, '', ItemListKey.ARTIST);
|
||||
|
||||
const query = {
|
||||
[FILTER_KEYS.ARTIST.ROLE]: role ?? undefined,
|
||||
[FILTER_KEYS.SHARED.MUSIC_FOLDER_ID]: musicFolderId ?? undefined,
|
||||
[FILTER_KEYS.SHARED.SEARCH_TERM]: searchTerm ?? undefined,
|
||||
[FILTER_KEYS.SHARED.SORT_BY]: sortBy ?? undefined,
|
||||
[FILTER_KEYS.SHARED.SORT_ORDER]: sortOrder ?? undefined,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { GENRE_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns';
|
||||
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
||||
import { ListFilters } from '/@/renderer/features/shared/components/list-filters';
|
||||
import { ListMusicFolderDropdown } from '/@/renderer/features/shared/components/list-music-folder-dropdown';
|
||||
import { ListRefreshButton } from '/@/renderer/features/shared/components/list-refresh-button';
|
||||
import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown';
|
||||
import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
|
||||
@@ -28,7 +27,6 @@ export const GenreListHeaderFilters = () => {
|
||||
defaultSortOrder={SortOrder.ASC}
|
||||
listKey={ItemListKey.GENRE}
|
||||
/>
|
||||
<ListMusicFolderDropdown listKey={ItemListKey.GENRE} />
|
||||
<ListFilters itemType={LibraryItem.GENRE} />
|
||||
<ListRefreshButton listKey={ItemListKey.GENRE} />
|
||||
</Group>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useMusicFolderIdFilter } from '/@/renderer/features/shared/hooks/use-music-folder-id-filter';
|
||||
import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter';
|
||||
import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-filter';
|
||||
import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter';
|
||||
@@ -11,12 +10,9 @@ export const useGenreListFilters = () => {
|
||||
|
||||
const { sortOrder } = useSortOrderFilter(null, ItemListKey.GENRE);
|
||||
|
||||
const { musicFolderId } = useMusicFolderIdFilter(null, ItemListKey.GENRE);
|
||||
|
||||
const { searchTerm, setSearchTerm } = useSearchTermFilter('');
|
||||
|
||||
const query = {
|
||||
[FILTER_KEYS.SHARED.MUSIC_FOLDER_ID]: musicFolderId ?? undefined,
|
||||
[FILTER_KEYS.SHARED.SEARCH_TERM]: searchTerm ?? undefined,
|
||||
[FILTER_KEYS.SHARED.SORT_BY]: sortBy ?? undefined,
|
||||
[FILTER_KEYS.SHARED.SORT_ORDER]: sortOrder ?? undefined,
|
||||
|
||||
@@ -3,11 +3,8 @@ import { useNavigate } from 'react-router';
|
||||
|
||||
import styles from './action-bar.module.css';
|
||||
|
||||
import { AppMenu } from '/@/renderer/features/titlebar/components/app-menu';
|
||||
import { useContainerQuery } from '/@/renderer/hooks';
|
||||
import { useCommandPalette } from '/@/renderer/store';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
|
||||
import { Grid } from '/@/shared/components/grid/grid';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
@@ -15,14 +12,18 @@ import { TextInput } from '/@/shared/components/text-input/text-input';
|
||||
|
||||
export const ActionBar = () => {
|
||||
const { t } = useTranslation();
|
||||
const { ref, ...cq } = useContainerQuery({ md: 300 });
|
||||
const navigate = useNavigate();
|
||||
const { open } = useCommandPalette();
|
||||
|
||||
return (
|
||||
<div className={styles.container} ref={ref}>
|
||||
<Grid display="flex" gutter="sm" px="1rem" w="100%">
|
||||
<Grid.Col span={6}>
|
||||
<div className={styles.container}>
|
||||
<Grid
|
||||
display="flex"
|
||||
gutter="sm"
|
||||
style={{ padding: '0 var(--theme-spacing-md)' }}
|
||||
w="100%"
|
||||
>
|
||||
<Grid.Col span={8}>
|
||||
<TextInput
|
||||
leftSection={<Icon icon="search" />}
|
||||
onClick={open}
|
||||
@@ -35,18 +36,8 @@ export const ActionBar = () => {
|
||||
readOnly
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<Grid.Col span={4}>
|
||||
<Group gap="sm" grow wrap="nowrap">
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button p="0.5rem">
|
||||
<Icon icon="menu" />
|
||||
</Button>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
<AppMenu />
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
<Button onClick={() => navigate(-1)} p="0.5rem">
|
||||
<Icon icon="arrowLeftS" />
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import { openModal } from '@mantine/modals';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
import JellyfinLogo from '/@/renderer/features/servers/assets/jellyfin.png';
|
||||
import NavidromeLogo from '/@/renderer/features/servers/assets/navidrome.png';
|
||||
import OpenSubsonicLogo from '/@/renderer/features/servers/assets/opensubsonic.png';
|
||||
import { ServerList } from '/@/renderer/features/servers/components/server-list';
|
||||
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { useAuthStoreActions, useCurrentServer, useServerList } from '/@/renderer/store';
|
||||
import { hasFeature } from '/@/shared/api/utils';
|
||||
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { ServerListItemWithCredential, ServerType } from '/@/shared/types/domain-types';
|
||||
import { ServerFeature } from '/@/shared/types/features-types';
|
||||
|
||||
export const ServerSelectorItems = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const currentServer = useCurrentServer();
|
||||
const serverList = useServerList();
|
||||
const { setCurrentServer, setMusicFolderId } = useAuthStoreActions();
|
||||
|
||||
const { data: musicFolders } = useQuery(
|
||||
currentServer
|
||||
? sharedQueries.musicFolders({ query: null, serverId: currentServer.id })
|
||||
: { enabled: false, queryKey: ['disabled'] },
|
||||
);
|
||||
|
||||
const handleSetCurrentServer = (server: ServerListItemWithCredential) => {
|
||||
navigate(AppRoute.HOME);
|
||||
setCurrentServer(server);
|
||||
setMusicFolderId(undefined);
|
||||
};
|
||||
|
||||
const supportsMultiSelect = hasFeature(currentServer, ServerFeature.MUSIC_FOLDER_MULTISELECT);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const handleToggleMusicFolder = (musicFolderId: string) => {
|
||||
if (supportsMultiSelect) {
|
||||
const currentIds = currentServer.musicFolderId || [];
|
||||
const isSelected = currentIds.includes(musicFolderId);
|
||||
|
||||
if (isSelected) {
|
||||
// Remove from selection
|
||||
const newIds = currentIds.filter((id) => id !== musicFolderId);
|
||||
setMusicFolderId(newIds.length > 0 ? newIds : undefined);
|
||||
} else {
|
||||
// Add to selection
|
||||
setMusicFolderId([...currentIds, musicFolderId]);
|
||||
}
|
||||
} else {
|
||||
const currentId = Array.isArray(currentServer.musicFolderId)
|
||||
? currentServer.musicFolderId[0]
|
||||
: currentServer.musicFolderId;
|
||||
const isSelected = currentId === musicFolderId;
|
||||
|
||||
if (isSelected) {
|
||||
setMusicFolderId(undefined);
|
||||
} else {
|
||||
setMusicFolderId([musicFolderId]);
|
||||
}
|
||||
}
|
||||
|
||||
queryClient.resetQueries();
|
||||
};
|
||||
|
||||
const handleClearMusicFolders = () => {
|
||||
setMusicFolderId(undefined);
|
||||
queryClient.resetQueries();
|
||||
};
|
||||
|
||||
if (!currentServer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedMusicFolders =
|
||||
musicFolders?.items.filter((folder) => currentServer.musicFolderId?.includes(folder.id)) ||
|
||||
[];
|
||||
|
||||
const handleManageServersModal = () => {
|
||||
openModal({
|
||||
children: <ServerList />,
|
||||
title: t('page.manageServers.title', { postProcess: 'titleCase' }),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu.Label>
|
||||
{t('page.appMenu.selectServer', { postProcess: 'titleCase' })}
|
||||
</DropdownMenu.Label>
|
||||
{Object.values(serverList).map((server) => {
|
||||
const isNavidromeExpired =
|
||||
server.type === ServerType.NAVIDROME && !server.ndCredential;
|
||||
const isJellyfinExpired = server.type === ServerType.JELLYFIN && !server.credential;
|
||||
const isSessionExpired = isNavidromeExpired || isJellyfinExpired;
|
||||
|
||||
const logo =
|
||||
server.type === ServerType.NAVIDROME
|
||||
? NavidromeLogo
|
||||
: server.type === ServerType.JELLYFIN
|
||||
? JellyfinLogo
|
||||
: OpenSubsonicLogo;
|
||||
|
||||
return (
|
||||
<DropdownMenu.Item
|
||||
isSelected={currentServer?.id === server.id}
|
||||
key={`server-${server.id}`}
|
||||
leftSection={<img src={logo} style={{ height: '1rem', width: '1rem' }} />}
|
||||
onClick={() => {
|
||||
if (!isSessionExpired) {
|
||||
handleSetCurrentServer(server);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{server.name}
|
||||
</DropdownMenu.Item>
|
||||
);
|
||||
})}
|
||||
<DropdownMenu.Item
|
||||
leftSection={<Icon icon="edit" />}
|
||||
onClick={handleManageServersModal}
|
||||
>
|
||||
{t('page.appMenu.manageServers', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
{musicFolders && musicFolders.items.length > 0 && (
|
||||
<>
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Label>
|
||||
{t('page.appMenu.selectMusicFolder', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Item
|
||||
isSelected={selectedMusicFolders.length === 0}
|
||||
leftSection={<Icon icon="minus" />}
|
||||
onClick={handleClearMusicFolders}
|
||||
>
|
||||
{t('common.none', { postProcess: 'titleCase' })}
|
||||
</DropdownMenu.Item>
|
||||
{musicFolders.items.map((folder) => {
|
||||
const isSelected = supportsMultiSelect
|
||||
? currentServer.musicFolderId?.includes(folder.id) || false
|
||||
: (Array.isArray(currentServer.musicFolderId)
|
||||
? currentServer.musicFolderId[0]
|
||||
: currentServer.musicFolderId) === folder.id;
|
||||
return (
|
||||
<DropdownMenu.Item
|
||||
isSelected={isSelected}
|
||||
key={`musicFolder-${folder.id}`}
|
||||
leftSection={<Icon icon={isSelected ? 'check' : 'folder'} />}
|
||||
onClick={() => handleToggleMusicFolder(folder.id)}
|
||||
>
|
||||
{folder.name}
|
||||
</DropdownMenu.Item>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
.button-container {
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: var(--theme-spacing-md);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button-container-no-bottom-padding {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
padding: var(--theme-spacing-sm);
|
||||
background: var(--theme-colors-surface);
|
||||
border-radius: var(--theme-radius-md);
|
||||
}
|
||||
|
||||
.logo {
|
||||
flex-shrink: 0;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
object-fit: cover;
|
||||
border-radius: var(--theme-radius-md);
|
||||
}
|
||||
|
||||
.button-stack {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.popover-target {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.scroll-area {
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.server-logo {
|
||||
width: var(--theme-font-size-md);
|
||||
height: var(--theme-font-size-md);
|
||||
}
|
||||
|
||||
.server-button {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import styles from './server-selector.module.css';
|
||||
|
||||
import JellyfinLogo from '/@/renderer/features/servers/assets/jellyfin.png';
|
||||
import NavidromeLogo from '/@/renderer/features/servers/assets/navidrome.png';
|
||||
import OpenSubsonicLogo from '/@/renderer/features/servers/assets/opensubsonic.png';
|
||||
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
|
||||
import { AppMenu } from '/@/renderer/features/titlebar/components/app-menu';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
import { hasFeature } from '/@/shared/api/utils';
|
||||
import { Box } from '/@/shared/components/box/box';
|
||||
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { ServerType } from '/@/shared/types/domain-types';
|
||||
import { ServerFeature } from '/@/shared/types/features-types';
|
||||
|
||||
interface ServerSelectorProps {
|
||||
showImage?: boolean;
|
||||
}
|
||||
|
||||
export const ServerSelector = ({ showImage = false }: ServerSelectorProps) => {
|
||||
const { t } = useTranslation();
|
||||
const currentServer = useCurrentServer();
|
||||
|
||||
const { data: musicFolders } = useQuery(
|
||||
currentServer
|
||||
? sharedQueries.musicFolders({ query: null, serverId: currentServer.id })
|
||||
: { enabled: false, queryKey: ['disabled'] },
|
||||
);
|
||||
|
||||
if (!currentServer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const supportsMultiSelect = hasFeature(currentServer, ServerFeature.MUSIC_FOLDER_MULTISELECT);
|
||||
|
||||
const selectedMusicFolders =
|
||||
musicFolders?.items.filter((folder) => currentServer.musicFolderId?.includes(folder.id)) ||
|
||||
[];
|
||||
|
||||
const musicFolderDisplayText = (() => {
|
||||
if (selectedMusicFolders.length === 0) {
|
||||
return t('page.appMenu.noMusicFolder', { postProcess: 'sentenceCase' });
|
||||
}
|
||||
|
||||
if (supportsMultiSelect && selectedMusicFolders.length > 1) {
|
||||
return t('page.appMenu.multipleMusicFolders', {
|
||||
count: selectedMusicFolders.length,
|
||||
postProcess: 'sentenceCase',
|
||||
});
|
||||
}
|
||||
|
||||
return selectedMusicFolders[0].name;
|
||||
})();
|
||||
|
||||
const logo =
|
||||
currentServer.type === ServerType.NAVIDROME
|
||||
? NavidromeLogo
|
||||
: currentServer.type === ServerType.JELLYFIN
|
||||
? JellyfinLogo
|
||||
: OpenSubsonicLogo;
|
||||
|
||||
return (
|
||||
<DropdownMenu position="right">
|
||||
<DropdownMenu.Target>
|
||||
<div className={styles.popoverTarget}>
|
||||
<Box
|
||||
className={`${styles.buttonContainer} ${
|
||||
showImage ? styles.buttonContainerNoBottomPadding : ''
|
||||
}`}
|
||||
>
|
||||
<Group className={styles.buttonGroup} gap="sm">
|
||||
<img className={styles.logo} src={logo} />
|
||||
<Stack className={styles.buttonStack} gap={2}>
|
||||
<Text fw={600} size="sm" truncate>
|
||||
{currentServer.name}
|
||||
</Text>
|
||||
<Text isMuted size="xs" truncate>
|
||||
{musicFolderDisplayText}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Icon icon="ellipsisVertical" size="sm" />
|
||||
</Group>
|
||||
</Box>
|
||||
</div>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
<AppMenu />
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
@@ -13,6 +13,8 @@
|
||||
}
|
||||
|
||||
.scroll-area {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 0 var(--theme-spacing-md) var(--theme-spacing-md) var(--theme-spacing-md);
|
||||
}
|
||||
|
||||
@@ -27,10 +29,11 @@
|
||||
}
|
||||
|
||||
.image-container {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: var(--sidebar-image-height);
|
||||
height: var(--sidebar-image-height);
|
||||
padding: var(--theme-spacing-md);
|
||||
cursor: pointer;
|
||||
animation: fade-in 0.2s ease-in-out;
|
||||
|
||||
@@ -48,7 +51,7 @@
|
||||
height: 100%;
|
||||
object-fit: var(--theme-image-fit);
|
||||
background: var(--theme-colors-foreground-muted);
|
||||
border-radius: 0;
|
||||
border-radius: var(--theme-radius-md);
|
||||
}
|
||||
|
||||
.accordion-root {
|
||||
|
||||
@@ -8,6 +8,7 @@ import styles from './sidebar.module.css';
|
||||
|
||||
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
|
||||
import { ActionBar } from '/@/renderer/features/sidebar/components/action-bar';
|
||||
import { ServerSelector } from '/@/renderer/features/sidebar/components/server-selector';
|
||||
import { SidebarIcon } from '/@/renderer/features/sidebar/components/sidebar-icon';
|
||||
import { SidebarItem } from '/@/renderer/features/sidebar/components/sidebar-item';
|
||||
import {
|
||||
@@ -106,15 +107,6 @@ export const Sidebar = () => {
|
||||
return items;
|
||||
}, [sidebarItems, translatedSidebarItemMap]);
|
||||
|
||||
const scrollAreaHeight = useMemo(() => {
|
||||
if (showImage) {
|
||||
// Subtract the height of the top bar and padding
|
||||
return `calc(100% - 65px - var(--mantine-spacing-xs) - ${sidebar.leftWidth})`;
|
||||
}
|
||||
|
||||
return '100%';
|
||||
}, [showImage, sidebar.leftWidth]);
|
||||
|
||||
const isCustomWindowBar =
|
||||
windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS;
|
||||
|
||||
@@ -125,16 +117,10 @@ export const Sidebar = () => {
|
||||
})}
|
||||
id="left-sidebar"
|
||||
>
|
||||
<Group grow id="global-search-container">
|
||||
<Group grow id="global-search-container" style={{ flexShrink: 0 }}>
|
||||
<ActionBar />
|
||||
</Group>
|
||||
<ScrollArea
|
||||
allowDragScroll
|
||||
className={styles.scrollArea}
|
||||
style={{
|
||||
height: scrollAreaHeight,
|
||||
}}
|
||||
>
|
||||
<ScrollArea allowDragScroll className={styles.scrollArea}>
|
||||
<Accordion
|
||||
classNames={{
|
||||
content: styles.accordionContent,
|
||||
@@ -177,6 +163,9 @@ export const Sidebar = () => {
|
||||
)}
|
||||
</Accordion>
|
||||
</ScrollArea>
|
||||
<div style={{ flexShrink: 0, position: 'relative', zIndex: 1 }}>
|
||||
<ServerSelector showImage={showImage} />
|
||||
</div>
|
||||
<AnimatePresence initial={false} mode="popLayout">
|
||||
{showImage && (
|
||||
<motion.div
|
||||
@@ -221,8 +210,8 @@ export const Sidebar = () => {
|
||||
style={{
|
||||
cursor: 'default',
|
||||
position: 'absolute',
|
||||
right: 5,
|
||||
top: 5,
|
||||
right: '1rem',
|
||||
top: '1rem',
|
||||
}}
|
||||
tooltip={{
|
||||
label: t('common.collapse', {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns';
|
||||
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
||||
import { ListFilters } from '/@/renderer/features/shared/components/list-filters';
|
||||
import { ListMusicFolderDropdown } from '/@/renderer/features/shared/components/list-music-folder-dropdown';
|
||||
import { ListRefreshButton } from '/@/renderer/features/shared/components/list-refresh-button';
|
||||
import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown';
|
||||
import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
|
||||
@@ -25,7 +24,6 @@ export const SongListHeaderFilters = () => {
|
||||
defaultSortOrder={SortOrder.ASC}
|
||||
listKey={ItemListKey.SONG}
|
||||
/>
|
||||
<ListMusicFolderDropdown listKey={ItemListKey.SONG} />
|
||||
<ListFilters itemType={LibraryItem.SONG} />
|
||||
<ListRefreshButton listKey={ItemListKey.SONG} />
|
||||
</Group>
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
useQueryState,
|
||||
} from 'nuqs';
|
||||
|
||||
import { useMusicFolderIdFilter } from '/@/renderer/features/shared/hooks/use-music-folder-id-filter';
|
||||
import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter';
|
||||
import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-filter';
|
||||
import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter';
|
||||
@@ -20,8 +19,6 @@ export const useSongListFilters = () => {
|
||||
|
||||
const { sortOrder } = useSortOrderFilter(null, ItemListKey.SONG);
|
||||
|
||||
const { musicFolderId } = useMusicFolderIdFilter(null, ItemListKey.SONG);
|
||||
|
||||
const { searchTerm, setSearchTerm } = useSearchTermFilter('');
|
||||
|
||||
const [albumIds, setAlbumIds] = useQueryState(
|
||||
@@ -51,7 +48,6 @@ export const useSongListFilters = () => {
|
||||
);
|
||||
|
||||
const query = {
|
||||
[FILTER_KEYS.SHARED.MUSIC_FOLDER_ID]: musicFolderId ?? undefined,
|
||||
[FILTER_KEYS.SHARED.SEARCH_TERM]: searchTerm ?? undefined,
|
||||
[FILTER_KEYS.SHARED.SORT_BY]: sortBy ?? undefined,
|
||||
[FILTER_KEYS.SHARED.SORT_ORDER]: sortOrder ?? undefined,
|
||||
|
||||
@@ -1,76 +1,77 @@
|
||||
import { closeAllModals, openModal } from '@mantine/modals';
|
||||
import isElectron from 'is-electron';
|
||||
import { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { Link } from 'react-router';
|
||||
import { Link, useNavigate } from 'react-router';
|
||||
|
||||
import packageJson from '../../../../../package.json';
|
||||
|
||||
import { EditServerForm } from '/@/renderer/features/servers/components/edit-server-form';
|
||||
import { ServerList } from '/@/renderer/features/servers/components/server-list';
|
||||
import { ServerSelectorItems } from '/@/renderer/features/sidebar/components/server-selector-items';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import {
|
||||
useAppStore,
|
||||
useAppStoreActions,
|
||||
useAuthStoreActions,
|
||||
useCurrentServer,
|
||||
useServerList,
|
||||
useSidebarStore,
|
||||
} from '/@/renderer/store';
|
||||
import { useAppStore, useAppStoreActions, useSidebarStore } from '/@/renderer/store';
|
||||
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { ServerListItemWithCredential, ServerType } from '/@/shared/types/domain-types';
|
||||
|
||||
const browser = isElectron() ? window.api.browser : null;
|
||||
const localSettings = isElectron() ? window.api.localSettings : null;
|
||||
|
||||
interface BaseMenuItem {
|
||||
id: string;
|
||||
type: 'conditional-group' | 'conditional-item' | 'custom' | 'divider' | 'item';
|
||||
}
|
||||
|
||||
interface ConditionalGroupItem extends BaseMenuItem {
|
||||
condition: boolean;
|
||||
items: MenuItem[];
|
||||
type: 'conditional-group';
|
||||
}
|
||||
|
||||
interface ConditionalItem extends BaseMenuItem {
|
||||
condition: boolean;
|
||||
item: Omit<MenuItem, 'id' | 'type'>;
|
||||
type: 'conditional-item';
|
||||
}
|
||||
|
||||
interface CustomItem extends BaseMenuItem {
|
||||
component: ReactNode;
|
||||
type: 'custom';
|
||||
}
|
||||
|
||||
interface DividerItem extends BaseMenuItem {
|
||||
type: 'divider';
|
||||
}
|
||||
|
||||
type MenuItem = ConditionalGroupItem | ConditionalItem | CustomItem | DividerItem | RegularMenuItem;
|
||||
|
||||
interface RegularMenuItem extends BaseMenuItem {
|
||||
component?: 'a' | typeof Link;
|
||||
href?: string;
|
||||
icon?: keyof typeof import('/@/shared/components/icon/icon').AppIcon;
|
||||
iconColor?:
|
||||
| 'contrast'
|
||||
| 'default'
|
||||
| 'error'
|
||||
| 'info'
|
||||
| 'inherit'
|
||||
| 'muted'
|
||||
| 'primary'
|
||||
| 'success'
|
||||
| 'warn';
|
||||
label: string;
|
||||
leftSection?: ReactNode;
|
||||
onClick?: () => void;
|
||||
rightSection?: ReactNode;
|
||||
target?: string;
|
||||
to?: string;
|
||||
type: 'item';
|
||||
}
|
||||
|
||||
export const AppMenu = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const currentServer = useCurrentServer();
|
||||
const serverList = useServerList();
|
||||
const { setCurrentServer } = useAuthStoreActions();
|
||||
const { collapsed } = useSidebarStore();
|
||||
const { privateMode } = useAppStore();
|
||||
const { setPrivateMode, setSideBar } = useAppStoreActions();
|
||||
|
||||
const handleSetCurrentServer = (server: ServerListItemWithCredential) => {
|
||||
navigate(AppRoute.HOME);
|
||||
setCurrentServer(server);
|
||||
};
|
||||
|
||||
const handleCredentialsModal = async (server: ServerListItemWithCredential) => {
|
||||
let password: null | string = null;
|
||||
|
||||
try {
|
||||
if (localSettings && server.savePassword) {
|
||||
password = await localSettings.passwordGet(server.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
openModal({
|
||||
children: server && (
|
||||
<EditServerForm
|
||||
isUpdate
|
||||
onCancel={closeAllModals}
|
||||
password={password}
|
||||
server={server}
|
||||
/>
|
||||
),
|
||||
size: 'sm',
|
||||
title: `Update session for "${server.name}"`,
|
||||
});
|
||||
};
|
||||
|
||||
const handleManageServersModal = () => {
|
||||
openModal({
|
||||
children: <ServerList />,
|
||||
title: t('page.manageServers.title', { postProcess: 'titleCase' }),
|
||||
});
|
||||
};
|
||||
|
||||
const handleBrowserDevTools = () => {
|
||||
browser?.devtools();
|
||||
};
|
||||
@@ -103,124 +104,184 @@ export const AppMenu = () => {
|
||||
browser?.quit();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu.Item
|
||||
leftSection={<Icon icon="arrowLeftS" />}
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
{t('page.appMenu.goBack', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
leftSection={<Icon icon="arrowRightS" />}
|
||||
onClick={() => navigate(1)}
|
||||
>
|
||||
{t('page.appMenu.goForward', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
{collapsed ? (
|
||||
<DropdownMenu.Item
|
||||
leftSection={<Icon icon="panelRightOpen" />}
|
||||
onClick={handleExpandSidebar}
|
||||
>
|
||||
{t('page.appMenu.expandSidebar', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
) : (
|
||||
<DropdownMenu.Item
|
||||
leftSection={<Icon icon="panelRightClose" />}
|
||||
onClick={handleCollapseSidebar}
|
||||
>
|
||||
{t('page.appMenu.collapseSidebar', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
)}
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Item
|
||||
component={Link}
|
||||
leftSection={<Icon icon="settings" />}
|
||||
to={AppRoute.SETTINGS}
|
||||
>
|
||||
{t('page.appMenu.settings', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
leftSection={<Icon icon="edit" />}
|
||||
onClick={handleManageServersModal}
|
||||
>
|
||||
{t('page.appMenu.manageServers', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
{privateMode ? (
|
||||
<DropdownMenu.Item
|
||||
leftSection={<Icon color="error" icon="lock" />}
|
||||
onClick={handlePrivateModeOff}
|
||||
>
|
||||
{t('page.appMenu.privateModeOff', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
) : (
|
||||
<DropdownMenu.Item
|
||||
leftSection={<Icon icon="lockOpen" />}
|
||||
onClick={handlePrivateModeOn}
|
||||
>
|
||||
{t('page.appMenu.privateModeOn', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
)}
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Label>
|
||||
{t('page.appMenu.selectServer', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Label>
|
||||
{Object.keys(serverList).map((serverId) => {
|
||||
const server = serverList[serverId];
|
||||
const isNavidromeExpired =
|
||||
server.type === ServerType.NAVIDROME && !server.ndCredential;
|
||||
const isJellyfinExpired = server.type === ServerType.JELLYFIN && !server.credential;
|
||||
const isSessionExpired = isNavidromeExpired || isJellyfinExpired;
|
||||
const menuConfig: MenuItem[] = [
|
||||
{
|
||||
condition: privateMode,
|
||||
id: 'private-mode',
|
||||
item: {
|
||||
icon: 'lock',
|
||||
iconColor: 'error',
|
||||
label: t('page.appMenu.privateModeOff', { postProcess: 'sentenceCase' }),
|
||||
onClick: handlePrivateModeOff,
|
||||
type: 'item',
|
||||
},
|
||||
type: 'conditional-item',
|
||||
},
|
||||
{
|
||||
condition: !privateMode,
|
||||
id: 'private-mode',
|
||||
item: {
|
||||
icon: 'lockOpen',
|
||||
label: t('page.appMenu.privateModeOn', { postProcess: 'sentenceCase' }),
|
||||
onClick: handlePrivateModeOn,
|
||||
type: 'item',
|
||||
},
|
||||
type: 'conditional-item',
|
||||
},
|
||||
{
|
||||
id: 'divider-1',
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
condition: collapsed,
|
||||
id: 'navigation-group',
|
||||
items: [
|
||||
{
|
||||
icon: 'arrowLeftS',
|
||||
id: 'go-back',
|
||||
label: t('page.appMenu.goBack', { postProcess: 'sentenceCase' }),
|
||||
onClick: () => navigate(-1),
|
||||
type: 'item',
|
||||
},
|
||||
{
|
||||
icon: 'arrowRightS',
|
||||
id: 'go-forward',
|
||||
label: t('page.appMenu.goForward', { postProcess: 'sentenceCase' }),
|
||||
onClick: () => navigate(1),
|
||||
type: 'item',
|
||||
},
|
||||
],
|
||||
type: 'conditional-group',
|
||||
},
|
||||
{
|
||||
condition: collapsed,
|
||||
id: 'sidebar-toggle',
|
||||
item: {
|
||||
icon: 'panelRightOpen',
|
||||
id: 'expand-sidebar',
|
||||
label: t('page.appMenu.expandSidebar', { postProcess: 'sentenceCase' }),
|
||||
onClick: handleExpandSidebar,
|
||||
type: 'item',
|
||||
},
|
||||
type: 'conditional-item',
|
||||
},
|
||||
{
|
||||
condition: !collapsed,
|
||||
id: 'sidebar-toggle',
|
||||
item: {
|
||||
icon: 'panelRightClose',
|
||||
id: 'collapse-sidebar',
|
||||
label: t('page.appMenu.collapseSidebar', { postProcess: 'sentenceCase' }),
|
||||
onClick: handleCollapseSidebar,
|
||||
type: 'item',
|
||||
},
|
||||
type: 'conditional-item',
|
||||
},
|
||||
{
|
||||
id: 'divider-2',
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
component: Link,
|
||||
icon: 'settings',
|
||||
id: 'settings',
|
||||
label: t('page.appMenu.settings', { postProcess: 'sentenceCase' }),
|
||||
to: AppRoute.SETTINGS,
|
||||
type: 'item',
|
||||
},
|
||||
{
|
||||
id: 'divider-3',
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
component: <ServerSelectorItems />,
|
||||
id: 'server-selector',
|
||||
type: 'custom',
|
||||
},
|
||||
{
|
||||
id: 'divider-4',
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
component: 'a',
|
||||
href: 'https://github.com/jeffvli/feishin/releases',
|
||||
icon: 'brandGitHub',
|
||||
id: 'version',
|
||||
label: t('page.appMenu.version', {
|
||||
postProcess: 'sentenceCase',
|
||||
version: packageJson.version,
|
||||
}),
|
||||
rightSection: <Icon icon="externalLink" />,
|
||||
target: '_blank',
|
||||
type: 'item',
|
||||
},
|
||||
{
|
||||
condition: isElectron(),
|
||||
id: 'devtools',
|
||||
item: {
|
||||
icon: 'appWindow',
|
||||
id: 'open-devtools',
|
||||
label: t('page.appMenu.openBrowserDevtools', { postProcess: 'sentenceCase' }),
|
||||
onClick: handleBrowserDevTools,
|
||||
type: 'item',
|
||||
},
|
||||
type: 'conditional-item',
|
||||
},
|
||||
{
|
||||
condition: isElectron(),
|
||||
id: 'quit',
|
||||
item: {
|
||||
icon: 'x',
|
||||
id: 'quit-app',
|
||||
label: t('page.appMenu.quit', { postProcess: 'sentenceCase' }),
|
||||
onClick: handleQuit,
|
||||
type: 'item',
|
||||
},
|
||||
type: 'conditional-item',
|
||||
},
|
||||
];
|
||||
|
||||
const renderMenuItem = (item: MenuItem): ReactNode => {
|
||||
switch (item.type) {
|
||||
case 'conditional-group':
|
||||
if (!item.condition) return null;
|
||||
return (
|
||||
<DropdownMenu.Item
|
||||
key={`server-${server.id}`}
|
||||
leftSection={
|
||||
isSessionExpired ? (
|
||||
<Icon fill="error" icon="lock" />
|
||||
) : (
|
||||
<Icon
|
||||
color={server.id === currentServer?.id ? 'primary' : undefined}
|
||||
icon="server"
|
||||
/>
|
||||
)
|
||||
}
|
||||
onClick={() => {
|
||||
if (!isSessionExpired) return handleSetCurrentServer(server);
|
||||
return handleCredentialsModal(server);
|
||||
}}
|
||||
>
|
||||
{server.name}
|
||||
</DropdownMenu.Item>
|
||||
<div key={item.id}>{item.items.map((subItem) => renderMenuItem(subItem))}</div>
|
||||
);
|
||||
})}
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Item
|
||||
component="a"
|
||||
href="https://github.com/jeffvli/feishin/releases"
|
||||
leftSection={<Icon icon="brandGitHub" />}
|
||||
rightSection={<Icon icon="externalLink" />}
|
||||
target="_blank"
|
||||
>
|
||||
{t('page.appMenu.version', {
|
||||
postProcess: 'sentenceCase',
|
||||
version: packageJson.version,
|
||||
})}
|
||||
</DropdownMenu.Item>
|
||||
{isElectron() && (
|
||||
<>
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Item
|
||||
leftSection={<Icon icon="appWindow" />}
|
||||
onClick={handleBrowserDevTools}
|
||||
>
|
||||
{t('page.appMenu.openBrowserDevtools', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item leftSection={<Icon icon="x" />} onClick={handleQuit}>
|
||||
{t('page.appMenu.quit', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
case 'conditional-item':
|
||||
if (!item.condition) return null;
|
||||
return renderMenuItem(item.item as MenuItem);
|
||||
|
||||
case 'custom':
|
||||
return <div key={item.id}>{item.component}</div>;
|
||||
|
||||
case 'divider':
|
||||
return <DropdownMenu.Divider key={item.id} />;
|
||||
|
||||
case 'item': {
|
||||
const leftSection =
|
||||
item.leftSection ||
|
||||
(item.icon && <Icon color={item.iconColor} icon={item.icon} />);
|
||||
|
||||
const props: any = {
|
||||
key: item.id,
|
||||
leftSection,
|
||||
...(item.rightSection && { rightSection: item.rightSection }),
|
||||
...(item.onClick && { onClick: item.onClick }),
|
||||
...(item.component && { component: item.component }),
|
||||
...(item.to && { to: item.to }),
|
||||
...(item.href && { href: item.href }),
|
||||
...(item.target && { target: item.target }),
|
||||
};
|
||||
|
||||
return <DropdownMenu.Item {...props}>{item.label}</DropdownMenu.Item>;
|
||||
}
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return <>{menuConfig.map((item) => renderMenuItem(item))}</>;
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface AuthSlice extends AuthState {
|
||||
deleteServer: (id: string) => void;
|
||||
getServer: (id: string) => null | ServerListItemWithCredential;
|
||||
setCurrentServer: (server: null | ServerListItemWithCredential) => void;
|
||||
setMusicFolderId: (musicFolderId: string[] | undefined) => void;
|
||||
updateServer: (id: string, args: Partial<ServerListItemWithCredential>) => void;
|
||||
};
|
||||
}
|
||||
@@ -64,15 +65,36 @@ export const useAuthStore = createWithEqualityFn<AuthSlice>()(
|
||||
}
|
||||
});
|
||||
},
|
||||
updateServer: (id: string, args: Partial<ServerListItem>) => {
|
||||
setMusicFolderId: (musicFolderId: string[] | undefined) => {
|
||||
set((state) => {
|
||||
if (state.currentServer) {
|
||||
state.currentServer.musicFolderId = musicFolderId;
|
||||
const serverId = state.currentServer.id;
|
||||
if (state.serverList[serverId]) {
|
||||
state.serverList[serverId].musicFolderId = musicFolderId;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
updateServer: (id: string, args: Partial<ServerListItemWithCredential>) => {
|
||||
set((state) => {
|
||||
const updatedServer = {
|
||||
...state.serverList[id],
|
||||
...args,
|
||||
};
|
||||
|
||||
if (
|
||||
state.currentServer?.id === id &&
|
||||
!('musicFolderId' in args) &&
|
||||
state.currentServer.musicFolderId !== undefined
|
||||
) {
|
||||
updatedServer.musicFolderId = state.currentServer.musicFolderId;
|
||||
}
|
||||
|
||||
state.serverList[id] = updatedServer;
|
||||
state.currentServer = updatedServer;
|
||||
if (state.currentServer?.id === id) {
|
||||
state.currentServer = updatedServer;
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -97,6 +119,7 @@ export const useCurrentServer = () =>
|
||||
return {
|
||||
features: state.currentServer?.features,
|
||||
id: state.currentServer?.id,
|
||||
musicFolderId: state.currentServer?.musicFolderId,
|
||||
name: state.currentServer?.name,
|
||||
preferInstantMix: state.currentServer?.preferInstantMix,
|
||||
savePassword: state.currentServer?.savePassword,
|
||||
|
||||
@@ -266,6 +266,7 @@ const genreListSort = {
|
||||
|
||||
const genreListParameters = paginationParameters.extend({
|
||||
_sort: z.nativeEnum(genreListSort).optional(),
|
||||
library_id: z.array(z.string()).optional(),
|
||||
name: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -308,6 +309,7 @@ const albumArtistList = z.array(albumArtist);
|
||||
const albumArtistListParameters = paginationParameters.extend({
|
||||
_sort: z.nativeEnum(NDAlbumArtistListSort).optional(),
|
||||
genre_id: z.string().optional(),
|
||||
library_id: z.array(z.string()).optional(),
|
||||
missing: z.boolean().optional(),
|
||||
name: z.string().optional(),
|
||||
role: z.string().optional(),
|
||||
@@ -374,6 +376,7 @@ const albumListParameters = paginationParameters.extend({
|
||||
genre_id: z.union([z.string(), z.string().array()]).optional(),
|
||||
has_rating: z.boolean().optional(),
|
||||
id: z.string().optional(),
|
||||
library_id: z.array(z.string()).optional(),
|
||||
name: z.string().optional(),
|
||||
recently_added: z.boolean().optional(),
|
||||
recently_played: z.boolean().optional(),
|
||||
@@ -456,6 +459,7 @@ const songListParameters = paginationParameters.extend({
|
||||
artist_id: z.array(z.string()).optional(),
|
||||
artists_id: z.array(z.string()).optional(),
|
||||
genre_id: z.array(z.string()).optional(),
|
||||
library_id: z.array(z.string()).optional(),
|
||||
path: z.string().optional(),
|
||||
starred: z.boolean().optional(),
|
||||
title: z.string().optional(),
|
||||
|
||||
@@ -89,6 +89,7 @@ export type QueueSong = Song & {
|
||||
export type ServerListItem = {
|
||||
features?: ServerFeatures;
|
||||
id: string;
|
||||
musicFolderId?: string[];
|
||||
name: string;
|
||||
preferInstantMix?: boolean;
|
||||
savePassword?: boolean;
|
||||
@@ -280,7 +281,7 @@ export interface GenreListQuery extends BaseQuery<GenreListSort> {
|
||||
navidrome?: null;
|
||||
};
|
||||
limit?: number;
|
||||
musicFolderId?: string;
|
||||
musicFolderId?: string | string[];
|
||||
searchTerm?: string;
|
||||
startIndex: number;
|
||||
}
|
||||
@@ -436,7 +437,7 @@ export interface AlbumListQuery extends AlbumListNavidromeQuery, BaseQuery<Album
|
||||
limit?: number;
|
||||
maxYear?: number;
|
||||
minYear?: number;
|
||||
musicFolderId?: string;
|
||||
musicFolderId?: string | string[];
|
||||
searchTerm?: string;
|
||||
startIndex: number;
|
||||
}
|
||||
@@ -564,7 +565,7 @@ export interface SongListQuery extends BaseQuery<SongListSort> {
|
||||
limit?: number;
|
||||
maxYear?: number;
|
||||
minYear?: number;
|
||||
musicFolderId?: string;
|
||||
musicFolderId?: string | string[];
|
||||
searchTerm?: string;
|
||||
startIndex: number;
|
||||
}
|
||||
@@ -667,7 +668,7 @@ export type AlbumArtistListCountArgs = BaseEndpointArgs & {
|
||||
export interface AlbumArtistListQuery extends BaseQuery<AlbumArtistListSort> {
|
||||
_custom?: Record<string, any>;
|
||||
limit?: number;
|
||||
musicFolderId?: string;
|
||||
musicFolderId?: string | string[];
|
||||
searchTerm?: string;
|
||||
startIndex: number;
|
||||
}
|
||||
@@ -759,7 +760,7 @@ export type ArtistListCountArgs = BaseEndpointArgs & { query: ListCountQuery<Art
|
||||
export interface ArtistListQuery extends BaseQuery<ArtistListSort> {
|
||||
_custom?: Record<string, any>;
|
||||
limit?: number;
|
||||
musicFolderId?: string;
|
||||
musicFolderId?: string | string[];
|
||||
role?: string;
|
||||
searchTerm?: string;
|
||||
startIndex: number;
|
||||
@@ -1058,7 +1059,7 @@ export type ArtistInfoArgs = BaseEndpointArgs & { query: ArtistInfoQuery };
|
||||
export type ArtistInfoQuery = {
|
||||
artistId: string;
|
||||
limit: number;
|
||||
musicFolderId?: string;
|
||||
musicFolderId?: string | string[];
|
||||
};
|
||||
|
||||
export type FullLyricsMetadata = Omit<InternetProviderLyricResponse, 'id' | 'lyrics' | 'source'> & {
|
||||
@@ -1104,7 +1105,7 @@ export type RandomSongListQuery = {
|
||||
limit?: number;
|
||||
maxYear?: number;
|
||||
minYear?: number;
|
||||
musicFolderId?: string;
|
||||
musicFolderId?: string | string[];
|
||||
played: Played;
|
||||
};
|
||||
|
||||
@@ -1127,14 +1128,14 @@ export type ScrobbleResponse = null;
|
||||
export type SearchAlbumArtistsQuery = {
|
||||
albumArtistLimit?: number;
|
||||
albumArtistStartIndex?: number;
|
||||
musicFolderId?: string;
|
||||
musicFolderId?: string | string[];
|
||||
query?: string;
|
||||
};
|
||||
|
||||
export type SearchAlbumsQuery = {
|
||||
albumLimit?: number;
|
||||
albumStartIndex?: number;
|
||||
musicFolderId?: string;
|
||||
musicFolderId?: string | string[];
|
||||
query?: string;
|
||||
};
|
||||
|
||||
@@ -1147,7 +1148,7 @@ export type SearchQuery = {
|
||||
albumArtistStartIndex?: number;
|
||||
albumLimit?: number;
|
||||
albumStartIndex?: number;
|
||||
musicFolderId?: string;
|
||||
musicFolderId?: string | string[];
|
||||
query?: string;
|
||||
songLimit?: number;
|
||||
songStartIndex?: number;
|
||||
@@ -1160,7 +1161,7 @@ export type SearchResponse = {
|
||||
};
|
||||
|
||||
export type SearchSongsQuery = {
|
||||
musicFolderId?: string;
|
||||
musicFolderId?: string | string[];
|
||||
query?: string;
|
||||
songLimit?: number;
|
||||
songStartIndex?: number;
|
||||
|
||||
@@ -4,6 +4,7 @@ export enum ServerFeature {
|
||||
BFR = 'bfr',
|
||||
LYRICS_MULTIPLE_STRUCTURED = 'lyricsMultipleStructured',
|
||||
LYRICS_SINGLE_STRUCTURED = 'lyricsSingleStructured',
|
||||
MUSIC_FOLDER_MULTISELECT = 'musicFolderMultiselect',
|
||||
PLAYLISTS_SMART = 'playlistsSmart',
|
||||
PUBLIC_PLAYLIST = 'publicPlaylist',
|
||||
SHARING_ALBUM_SONG = 'sharingAlbumSong',
|
||||
|
||||
Reference in New Issue
Block a user