add global music folder selector

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