mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
Merge branch 'master' into add-music-stack-theme
This commit is contained in:
commit
821ffa1317
59
ui/package-lock.json
generated
59
ui/package-lock.json
generated
@ -117,7 +117,6 @@
|
|||||||
"node_modules/@babel/core": {
|
"node_modules/@babel/core": {
|
||||||
"version": "7.28.6",
|
"version": "7.28.6",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.28.6",
|
"@babel/code-frame": "^7.28.6",
|
||||||
"@babel/generator": "^7.28.6",
|
"@babel/generator": "^7.28.6",
|
||||||
@ -1530,7 +1529,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
@ -1552,7 +1550,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@ -2296,7 +2293,6 @@
|
|||||||
"node_modules/@jsonforms/core": {
|
"node_modules/@jsonforms/core": {
|
||||||
"version": "2.5.2",
|
"version": "2.5.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/json-schema": "^7.0.3",
|
"@types/json-schema": "^7.0.3",
|
||||||
"ajv": "^6.10.2",
|
"ajv": "^6.10.2",
|
||||||
@ -2340,7 +2336,6 @@
|
|||||||
"node_modules/@jsonforms/react": {
|
"node_modules/@jsonforms/react": {
|
||||||
"version": "2.5.2",
|
"version": "2.5.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
"object-hash": "^2.0.0"
|
"object-hash": "^2.0.0"
|
||||||
@ -2353,7 +2348,6 @@
|
|||||||
"node_modules/@material-ui/core": {
|
"node_modules/@material-ui/core": {
|
||||||
"version": "4.12.4",
|
"version": "4.12.4",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.4.4",
|
"@babel/runtime": "^7.4.4",
|
||||||
"@material-ui/styles": "^4.11.5",
|
"@material-ui/styles": "^4.11.5",
|
||||||
@ -2396,7 +2390,6 @@
|
|||||||
"node_modules/@material-ui/icons": {
|
"node_modules/@material-ui/icons": {
|
||||||
"version": "4.11.3",
|
"version": "4.11.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.4.4"
|
"@babel/runtime": "^7.4.4"
|
||||||
},
|
},
|
||||||
@ -2794,7 +2787,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/pluginutils/node_modules/picomatch": {
|
"node_modules/@rollup/pluginutils/node_modules/picomatch": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@ -3013,7 +3008,6 @@
|
|||||||
"node_modules/@types/hoist-non-react-statics": {
|
"node_modules/@types/hoist-non-react-statics": {
|
||||||
"version": "3.3.7",
|
"version": "3.3.7",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hoist-non-react-statics": "^3.3.0"
|
"hoist-non-react-statics": "^3.3.0"
|
||||||
},
|
},
|
||||||
@ -3054,7 +3048,6 @@
|
|||||||
"version": "24.10.9",
|
"version": "24.10.9",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
@ -3070,7 +3063,6 @@
|
|||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "17.0.90",
|
"version": "17.0.90",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"@types/scheduler": "^0.16",
|
"@types/scheduler": "^0.16",
|
||||||
@ -3206,7 +3198,6 @@
|
|||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "6.21.0",
|
"@typescript-eslint/scope-manager": "6.21.0",
|
||||||
"@typescript-eslint/types": "6.21.0",
|
"@typescript-eslint/types": "6.21.0",
|
||||||
@ -3504,7 +3495,6 @@
|
|||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.15.0",
|
"version": "8.15.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@ -4076,7 +4066,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@ -4390,7 +4379,6 @@
|
|||||||
"node_modules/connected-react-router": {
|
"node_modules/connected-react-router": {
|
||||||
"version": "6.9.3",
|
"version": "6.9.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lodash.isequalwith": "^4.4.0",
|
"lodash.isequalwith": "^4.4.0",
|
||||||
"prop-types": "^15.7.2"
|
"prop-types": "^15.7.2"
|
||||||
@ -5142,7 +5130,6 @@
|
|||||||
"version": "8.57.1",
|
"version": "8.57.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.6.1",
|
"@eslint-community/regexpp": "^4.6.1",
|
||||||
@ -5640,7 +5627,6 @@
|
|||||||
"node_modules/final-form": {
|
"node_modules/final-form": {
|
||||||
"version": "4.20.10",
|
"version": "4.20.10",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.10.0"
|
"@babel/runtime": "^7.10.0"
|
||||||
},
|
},
|
||||||
@ -5655,7 +5641,6 @@
|
|||||||
"node_modules/final-form-arrays": {
|
"node_modules/final-form-arrays": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"final-form": "^4.20.8"
|
"final-form": "^4.20.8"
|
||||||
}
|
}
|
||||||
@ -6004,7 +5989,6 @@
|
|||||||
"version": "20.3.3",
|
"version": "20.3.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": ">=20.0.0",
|
"@types/node": ">=20.0.0",
|
||||||
"@types/whatwg-mimetype": "^3.0.2",
|
"@types/whatwg-mimetype": "^3.0.2",
|
||||||
@ -6100,7 +6084,6 @@
|
|||||||
"node_modules/history": {
|
"node_modules/history": {
|
||||||
"version": "4.10.1",
|
"version": "4.10.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.1.2",
|
"@babel/runtime": "^7.1.2",
|
||||||
"loose-envify": "^1.2.0",
|
"loose-envify": "^1.2.0",
|
||||||
@ -7433,7 +7416,6 @@
|
|||||||
"node_modules/moment": {
|
"node_modules/moment": {
|
||||||
"version": "2.30.1",
|
"version": "2.30.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
@ -7932,7 +7914,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
@ -8039,7 +8023,6 @@
|
|||||||
"node_modules/prop-types": {
|
"node_modules/prop-types": {
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.4.0",
|
"loose-envify": "^1.4.0",
|
||||||
"object-assign": "^4.1.1",
|
"object-assign": "^4.1.1",
|
||||||
@ -8115,7 +8098,6 @@
|
|||||||
"node_modules/ra-core": {
|
"node_modules/ra-core": {
|
||||||
"version": "3.19.12",
|
"version": "3.19.12",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"classnames": "~2.3.1",
|
"classnames": "~2.3.1",
|
||||||
"date-fns": "^1.29.0",
|
"date-fns": "^1.29.0",
|
||||||
@ -8469,7 +8451,6 @@
|
|||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"object-assign": "^4.1.1"
|
"object-assign": "^4.1.1"
|
||||||
@ -8543,7 +8524,6 @@
|
|||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"object-assign": "^4.1.1",
|
"object-assign": "^4.1.1",
|
||||||
@ -8608,7 +8588,6 @@
|
|||||||
"node_modules/react-final-form": {
|
"node_modules/react-final-form": {
|
||||||
"version": "6.5.9",
|
"version": "6.5.9",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.15.4"
|
"@babel/runtime": "^7.15.4"
|
||||||
},
|
},
|
||||||
@ -8624,7 +8603,6 @@
|
|||||||
"node_modules/react-final-form-arrays": {
|
"node_modules/react-final-form-arrays": {
|
||||||
"version": "3.1.4",
|
"version": "3.1.4",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.19.4"
|
"@babel/runtime": "^7.19.4"
|
||||||
},
|
},
|
||||||
@ -8711,7 +8689,6 @@
|
|||||||
"node_modules/react-redux": {
|
"node_modules/react-redux": {
|
||||||
"version": "7.2.9",
|
"version": "7.2.9",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.15.4",
|
"@babel/runtime": "^7.15.4",
|
||||||
"@types/react-redux": "^7.1.20",
|
"@types/react-redux": "^7.1.20",
|
||||||
@ -8743,7 +8720,6 @@
|
|||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "5.3.4",
|
"version": "5.3.4",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.13",
|
"@babel/runtime": "^7.12.13",
|
||||||
"history": "^4.9.0",
|
"history": "^4.9.0",
|
||||||
@ -8762,7 +8738,6 @@
|
|||||||
"node_modules/react-router-dom": {
|
"node_modules/react-router-dom": {
|
||||||
"version": "5.3.4",
|
"version": "5.3.4",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.13",
|
"@babel/runtime": "^7.12.13",
|
||||||
"history": "^4.9.0",
|
"history": "^4.9.0",
|
||||||
@ -8916,7 +8891,6 @@
|
|||||||
"node_modules/redux": {
|
"node_modules/redux": {
|
||||||
"version": "4.2.1",
|
"version": "4.2.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.9.2"
|
"@babel/runtime": "^7.9.2"
|
||||||
}
|
}
|
||||||
@ -8924,7 +8898,6 @@
|
|||||||
"node_modules/redux-saga": {
|
"node_modules/redux-saga": {
|
||||||
"version": "1.4.2",
|
"version": "1.4.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@redux-saga/core": "^1.4.2"
|
"@redux-saga/core": "^1.4.2"
|
||||||
}
|
}
|
||||||
@ -9133,7 +9106,6 @@
|
|||||||
"version": "4.55.2",
|
"version": "4.55.2",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.8"
|
"@types/estree": "1.0.8"
|
||||||
},
|
},
|
||||||
@ -9925,10 +9897,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinyglobby/node_modules/picomatch": {
|
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@ -10116,7 +10089,6 @@
|
|||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@ -10319,7 +10291,6 @@
|
|||||||
"version": "7.3.1",
|
"version": "7.3.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@ -10435,10 +10406,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite/node_modules/picomatch": {
|
"node_modules/vite/node_modules/picomatch": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@ -10450,7 +10422,6 @@
|
|||||||
"version": "4.0.17",
|
"version": "4.0.17",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/expect": "4.0.17",
|
"@vitest/expect": "4.0.17",
|
||||||
"@vitest/mocker": "4.0.17",
|
"@vitest/mocker": "4.0.17",
|
||||||
@ -10524,7 +10495,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vitest/node_modules/picomatch": {
|
"node_modules/vitest/node_modules/picomatch": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -10904,7 +10877,6 @@
|
|||||||
"node_modules/workbox-build/node_modules/ajv": {
|
"node_modules/workbox-build/node_modules/ajv": {
|
||||||
"version": "8.18.0",
|
"version": "8.18.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fast-uri": "^3.0.1",
|
"fast-uri": "^3.0.1",
|
||||||
@ -10978,7 +10950,6 @@
|
|||||||
"node_modules/workbox-build/node_modules/rollup": {
|
"node_modules/workbox-build/node_modules/rollup": {
|
||||||
"version": "2.79.2",
|
"version": "2.79.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"rollup": "dist/bin/rollup"
|
"rollup": "dist/bin/rollup"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import {
|
|||||||
PlayButton,
|
PlayButton,
|
||||||
ArtistLinkField,
|
ArtistLinkField,
|
||||||
OverflowTooltip,
|
OverflowTooltip,
|
||||||
|
useImageUrl,
|
||||||
} from '../common'
|
} from '../common'
|
||||||
import { COVER_ART_SIZE, DraggableTypes } from '../consts'
|
import { COVER_ART_SIZE, DraggableTypes } from '../consts'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
@ -105,7 +106,7 @@ const useCoverStyles = makeStyles({
|
|||||||
transition: 'opacity 0.3s ease-in-out',
|
transition: 'opacity 0.3s ease-in-out',
|
||||||
},
|
},
|
||||||
coverLoading: {
|
coverLoading: {
|
||||||
opacity: 0.5,
|
opacity: 0,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -125,8 +126,6 @@ const Cover = withContentRect('bounds')(({
|
|||||||
// Force height to be the same as the width determined by the GridList
|
// Force height to be the same as the width determined by the GridList
|
||||||
// noinspection JSSuspiciousNameCombination
|
// noinspection JSSuspiciousNameCombination
|
||||||
const classes = useCoverStyles({ height: contentRect.bounds.width })
|
const classes = useCoverStyles({ height: contentRect.bounds.width })
|
||||||
const [imageLoading, setImageLoading] = React.useState(true)
|
|
||||||
const [imageError, setImageError] = React.useState(false)
|
|
||||||
const [, dragAlbumRef] = useDrag(
|
const [, dragAlbumRef] = useDrag(
|
||||||
() => ({
|
() => ({
|
||||||
type: DraggableTypes.ALBUM,
|
type: DraggableTypes.ALBUM,
|
||||||
@ -136,32 +135,16 @@ const Cover = withContentRect('bounds')(({
|
|||||||
[record],
|
[record],
|
||||||
)
|
)
|
||||||
|
|
||||||
// Reset image state when record changes
|
const url = subsonic.getCoverArtUrl(record, COVER_ART_SIZE, true)
|
||||||
React.useEffect(() => {
|
const { imgUrl, loading: imageLoading } = useImageUrl(url)
|
||||||
setImageLoading(true)
|
|
||||||
setImageError(false)
|
|
||||||
}, [record.id])
|
|
||||||
|
|
||||||
const handleImageLoad = React.useCallback(() => {
|
|
||||||
setImageLoading(false)
|
|
||||||
setImageError(false)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleImageError = React.useCallback(() => {
|
|
||||||
setImageLoading(false)
|
|
||||||
setImageError(true)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={measureRef} className={classes.coverContainer}>
|
<div ref={measureRef} className={classes.coverContainer}>
|
||||||
<div ref={dragAlbumRef}>
|
<div ref={dragAlbumRef}>
|
||||||
<img
|
<img
|
||||||
key={record.id} // Force re-render when record changes
|
src={imgUrl || undefined}
|
||||||
src={subsonic.getCoverArtUrl(record, COVER_ART_SIZE, true)}
|
|
||||||
alt={record.name}
|
alt={record.name}
|
||||||
className={`${classes.cover} ${imageLoading ? classes.coverLoading : ''}`}
|
className={`${classes.cover} ${imageLoading ? classes.coverLoading : ''}`}
|
||||||
onLoad={handleImageLoad}
|
|
||||||
onError={handleImageError}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -175,12 +175,12 @@ const AlbumListTitle = ({ albumListType }) => {
|
|||||||
return <Title subTitle={title} args={{ smart_count: 2 }} />
|
return <Title subTitle={title} args={{ smart_count: 2 }} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const AlbumListPagination = (props) => {
|
const AlbumListPagination = ({ albumListType, ...rest }) => {
|
||||||
const { loading } = useListContext()
|
const { loading } = useListContext()
|
||||||
if (loading) {
|
if (loading && albumListType === 'random') {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return <Pagination {...props} />
|
return <Pagination {...rest} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const randomStartingSeed = Math.random().toString()
|
const randomStartingSeed = Math.random().toString()
|
||||||
@ -243,7 +243,12 @@ const AlbumList = (props) => {
|
|||||||
actions={<AlbumListActions />}
|
actions={<AlbumListActions />}
|
||||||
filters={<AlbumFilter />}
|
filters={<AlbumFilter />}
|
||||||
perPage={perPage}
|
perPage={perPage}
|
||||||
pagination={<AlbumListPagination rowsPerPageOptions={perPageOptions} />}
|
pagination={
|
||||||
|
<AlbumListPagination
|
||||||
|
rowsPerPageOptions={perPageOptions}
|
||||||
|
albumListType={albumListType}
|
||||||
|
/>
|
||||||
|
}
|
||||||
title={<AlbumListTitle albumListType={albumListType} />}
|
title={<AlbumListTitle albumListType={albumListType} />}
|
||||||
>
|
>
|
||||||
{albumView.grid ? (
|
{albumView.grid ? (
|
||||||
|
|||||||
@ -4,12 +4,16 @@ import { makeStyles } from '@material-ui/core/styles'
|
|||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { COVER_ART_SIZE } from '../consts'
|
import { COVER_ART_SIZE } from '../consts'
|
||||||
import subsonic from '../subsonic'
|
import subsonic from '../subsonic'
|
||||||
|
import { useImageUrl } from './useImageUrl'
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
avatar: {
|
avatar: {
|
||||||
width: '55px',
|
width: '55px',
|
||||||
height: '55px',
|
height: '55px',
|
||||||
},
|
},
|
||||||
|
avatarEmpty: {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
},
|
||||||
square: {
|
square: {
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
},
|
},
|
||||||
@ -22,15 +26,26 @@ export const CoverArtAvatar = ({
|
|||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const recordContext = useRecordContext()
|
const recordContext = useRecordContext()
|
||||||
const record = recordProp || recordContext
|
const record = recordProp || recordContext
|
||||||
if (!record) return null
|
|
||||||
const square = variant !== 'circular'
|
const square = variant !== 'circular'
|
||||||
|
const url = record
|
||||||
|
? subsonic.getCoverArtUrl(record, COVER_ART_SIZE, square)
|
||||||
|
: null
|
||||||
|
const { imgUrl } = useImageUrl(url)
|
||||||
|
if (!record) return null
|
||||||
return (
|
return (
|
||||||
<Avatar
|
<Avatar
|
||||||
src={subsonic.getCoverArtUrl(record, COVER_ART_SIZE, square)}
|
src={imgUrl || undefined}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
className={clsx(classes.avatar, square && classes.square)}
|
className={clsx(
|
||||||
|
classes.avatar,
|
||||||
|
square && classes.square,
|
||||||
|
!imgUrl && classes.avatarEmpty,
|
||||||
|
)}
|
||||||
alt={record.name}
|
alt={record.name}
|
||||||
/>
|
>
|
||||||
|
{/* Empty child prevents default person icon while loading */}
|
||||||
|
{!imgUrl && <span />}
|
||||||
|
</Avatar>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -46,3 +46,4 @@ export * from './useSearchRefocus'
|
|||||||
export * from './ImageUploadOverlay'
|
export * from './ImageUploadOverlay'
|
||||||
export * from './CoverArtAvatar'
|
export * from './CoverArtAvatar'
|
||||||
export * from './useImageLoadingState'
|
export * from './useImageLoadingState'
|
||||||
|
export * from './useImageUrl'
|
||||||
|
|||||||
144
ui/src/common/useImageUrl.js
Normal file
144
ui/src/common/useImageUrl.js
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import { useEffect, useState, useRef } from 'react'
|
||||||
|
|
||||||
|
// Persists across component mount/unmount cycles so that
|
||||||
|
// React Admin refreshes (which remount list items) don't re-fetch images.
|
||||||
|
const cache = new Map()
|
||||||
|
const MAX_CACHE_SIZE = 300
|
||||||
|
|
||||||
|
// Limit concurrent fetches to leave browser connections free for API requests.
|
||||||
|
// Browsers allow ~6 connections per origin on HTTP/1.1; reserving 2 for API
|
||||||
|
// calls prevents image fetches from blocking pagination/data requests.
|
||||||
|
const MAX_CONCURRENT = 4
|
||||||
|
let activeFetches = 0
|
||||||
|
const pendingQueue = []
|
||||||
|
|
||||||
|
const processQueue = () => {
|
||||||
|
while (pendingQueue.length > 0 && activeFetches < MAX_CONCURRENT) {
|
||||||
|
const next = pendingQueue.shift()
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evicts oldest unused entries (Map iterates in insertion order).
|
||||||
|
const evictIfNeeded = () => {
|
||||||
|
if (cache.size <= MAX_CACHE_SIZE) return
|
||||||
|
for (const [key, entry] of cache) {
|
||||||
|
if (cache.size <= MAX_CACHE_SIZE) break
|
||||||
|
if (entry.refCount === 0) {
|
||||||
|
if (entry.blobUrl) URL.revokeObjectURL(entry.blobUrl)
|
||||||
|
cache.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads an image via fetch() with AbortController so that in-flight requests
|
||||||
|
* are canceled on unmount (e.g., during pagination). Uses a module-level cache
|
||||||
|
* so remounting returns the cached blob URL instantly.
|
||||||
|
*/
|
||||||
|
export const useImageUrl = (url) => {
|
||||||
|
const cached = url ? cache.get(url) : null
|
||||||
|
const [imgUrl, setImgUrl] = useState(cached?.blobUrl || null)
|
||||||
|
const [loading, setLoading] = useState(!!url && !cached)
|
||||||
|
const [error, setError] = useState(cached?.error || false)
|
||||||
|
const abortedRef = useRef(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
abortedRef.current = false
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
setImgUrl(null)
|
||||||
|
setLoading(false)
|
||||||
|
setError(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-check: another component's effect may have populated the cache
|
||||||
|
// between this component's render and effect execution.
|
||||||
|
const entry = cache.get(url)
|
||||||
|
if (entry) {
|
||||||
|
entry.refCount++
|
||||||
|
setImgUrl(entry.blobUrl)
|
||||||
|
setLoading(false)
|
||||||
|
setError(entry.error || false)
|
||||||
|
return () => {
|
||||||
|
entry.refCount--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController()
|
||||||
|
let queued = true
|
||||||
|
setImgUrl(null)
|
||||||
|
setLoading(true)
|
||||||
|
setError(false)
|
||||||
|
|
||||||
|
const doFetch = () => {
|
||||||
|
queued = false
|
||||||
|
activeFetches++
|
||||||
|
fetch(url, { signal: controller.signal })
|
||||||
|
.then((res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`HTTP ${res.status}`)
|
||||||
|
}
|
||||||
|
return res.blob()
|
||||||
|
})
|
||||||
|
.then((blob) => {
|
||||||
|
activeFetches--
|
||||||
|
processQueue()
|
||||||
|
// Guard against late resolution after abort
|
||||||
|
if (abortedRef.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const objectUrl = URL.createObjectURL(blob)
|
||||||
|
// Handle concurrent fetches: if another component already cached
|
||||||
|
// this URL, use its entry and discard our blob.
|
||||||
|
const existing = cache.get(url)
|
||||||
|
if (existing && existing.blobUrl) {
|
||||||
|
existing.refCount++
|
||||||
|
URL.revokeObjectURL(objectUrl)
|
||||||
|
setImgUrl(existing.blobUrl)
|
||||||
|
} else {
|
||||||
|
cache.set(url, { blobUrl: objectUrl, refCount: 1 })
|
||||||
|
evictIfNeeded()
|
||||||
|
setImgUrl(objectUrl)
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
activeFetches--
|
||||||
|
processQueue()
|
||||||
|
if (err.name === 'AbortError') {
|
||||||
|
return // Expected on unmount or URL change
|
||||||
|
}
|
||||||
|
// Cache the error so repeated mounts don't re-fetch broken URLs
|
||||||
|
cache.set(url, { blobUrl: null, error: true, refCount: 0 })
|
||||||
|
setError(true)
|
||||||
|
setLoading(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeFetches < MAX_CONCURRENT) {
|
||||||
|
queued = false
|
||||||
|
doFetch()
|
||||||
|
} else {
|
||||||
|
pendingQueue.push(doFetch)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
abortedRef.current = true
|
||||||
|
if (queued) {
|
||||||
|
// Remove from queue if not yet started
|
||||||
|
const idx = pendingQueue.indexOf(doFetch)
|
||||||
|
if (idx !== -1) pendingQueue.splice(idx, 1)
|
||||||
|
} else {
|
||||||
|
controller.abort()
|
||||||
|
}
|
||||||
|
const entry = cache.get(url)
|
||||||
|
if (entry) {
|
||||||
|
entry.refCount--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [url])
|
||||||
|
|
||||||
|
return { imgUrl, loading, error }
|
||||||
|
}
|
||||||
234
ui/src/common/useImageUrl.test.js
Normal file
234
ui/src/common/useImageUrl.test.js
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
import { renderHook, act } from '@testing-library/react-hooks'
|
||||||
|
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
|
|
||||||
|
// Helper to flush all pending promises
|
||||||
|
const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0))
|
||||||
|
|
||||||
|
// We need a fresh module for each test to reset the module-level cache
|
||||||
|
let useImageUrl
|
||||||
|
|
||||||
|
describe('useImageUrl', () => {
|
||||||
|
let abortSpy
|
||||||
|
let OriginalAbortController
|
||||||
|
let originalCreateObjectURL
|
||||||
|
let originalRevokeObjectURL
|
||||||
|
let originalFetch
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Reset module to clear the cache
|
||||||
|
vi.resetModules()
|
||||||
|
const mod = await import('./useImageUrl')
|
||||||
|
useImageUrl = mod.useImageUrl
|
||||||
|
|
||||||
|
abortSpy = vi.fn()
|
||||||
|
OriginalAbortController = global.AbortController
|
||||||
|
originalCreateObjectURL = global.URL.createObjectURL
|
||||||
|
originalRevokeObjectURL = global.URL.revokeObjectURL
|
||||||
|
originalFetch = global.fetch
|
||||||
|
|
||||||
|
global.AbortController = function () {
|
||||||
|
this.signal = 'mock-signal'
|
||||||
|
this.abort = abortSpy
|
||||||
|
}
|
||||||
|
global.URL.createObjectURL = vi.fn(() => 'blob:mock-url')
|
||||||
|
global.URL.revokeObjectURL = vi.fn()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
global.AbortController = OriginalAbortController
|
||||||
|
global.URL.createObjectURL = originalCreateObjectURL
|
||||||
|
global.URL.revokeObjectURL = originalRevokeObjectURL
|
||||||
|
global.fetch = originalFetch
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return null values when url is null', () => {
|
||||||
|
const { result } = renderHook(() => useImageUrl(null))
|
||||||
|
|
||||||
|
expect(result.current.loading).toBe(false)
|
||||||
|
expect(result.current.imgUrl).toBeNull()
|
||||||
|
expect(result.current.error).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return loading state initially', () => {
|
||||||
|
global.fetch = vi.fn(() => new Promise(() => {}))
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useImageUrl('http://example.com/img.jpg'),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.current.loading).toBe(true)
|
||||||
|
expect(result.current.imgUrl).toBeNull()
|
||||||
|
expect(result.current.error).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fetch image and return blob URL on success', async () => {
|
||||||
|
const mockBlob = new Blob(['image-data'], { type: 'image/png' })
|
||||||
|
global.fetch = vi.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
blob: () => Promise.resolve(mockBlob),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useImageUrl('http://example.com/img.jpg'),
|
||||||
|
)
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await flushPromises()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.loading).toBe(false)
|
||||||
|
expect(result.current.imgUrl).toBe('blob:mock-url')
|
||||||
|
expect(result.current.error).toBe(false)
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith('http://example.com/img.jpg', {
|
||||||
|
signal: 'mock-signal',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set error on HTTP failure', async () => {
|
||||||
|
global.fetch = vi.fn(() => Promise.resolve({ ok: false, status: 404 }))
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useImageUrl('http://example.com/missing.jpg'),
|
||||||
|
)
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await flushPromises()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.loading).toBe(false)
|
||||||
|
expect(result.current.imgUrl).toBeNull()
|
||||||
|
expect(result.current.error).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should abort fetch on unmount', async () => {
|
||||||
|
global.fetch = vi.fn(() => new Promise(() => {}))
|
||||||
|
|
||||||
|
const { unmount } = renderHook(() =>
|
||||||
|
useImageUrl('http://example.com/img.jpg'),
|
||||||
|
)
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await flushPromises()
|
||||||
|
})
|
||||||
|
|
||||||
|
unmount()
|
||||||
|
|
||||||
|
expect(abortSpy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should abort previous fetch when URL changes', async () => {
|
||||||
|
const abortSpies = []
|
||||||
|
global.AbortController = function () {
|
||||||
|
const spy = vi.fn()
|
||||||
|
abortSpies.push(spy)
|
||||||
|
this.signal = `signal-${abortSpies.length}`
|
||||||
|
this.abort = spy
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockBlob = new Blob(['data'], { type: 'image/png' })
|
||||||
|
global.fetch = vi.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
blob: () => Promise.resolve(mockBlob),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const { rerender } = renderHook(({ url }) => useImageUrl(url), {
|
||||||
|
initialProps: { url: 'http://example.com/img1.jpg' },
|
||||||
|
})
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await flushPromises()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Change URL - should abort the first controller
|
||||||
|
rerender({ url: 'http://example.com/img2.jpg' })
|
||||||
|
|
||||||
|
expect(abortSpies[0]).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not set error on AbortError', async () => {
|
||||||
|
const abortError = new DOMException('Aborted', 'AbortError')
|
||||||
|
global.fetch = vi.fn(() => Promise.reject(abortError))
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useImageUrl('http://example.com/img.jpg'),
|
||||||
|
)
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await flushPromises()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.error).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use cached blob URL on remount without re-fetching', async () => {
|
||||||
|
const mockBlob = new Blob(['data'], { type: 'image/png' })
|
||||||
|
global.fetch = vi.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
blob: () => Promise.resolve(mockBlob),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// First mount — fetches and caches
|
||||||
|
const { unmount } = renderHook(() =>
|
||||||
|
useImageUrl('http://example.com/img.jpg'),
|
||||||
|
)
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await flushPromises()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
// Unmount (simulates React Admin refresh)
|
||||||
|
unmount()
|
||||||
|
|
||||||
|
// Remount with same URL — should use cache
|
||||||
|
const { result: result2 } = renderHook(() =>
|
||||||
|
useImageUrl('http://example.com/img.jpg'),
|
||||||
|
)
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await flushPromises()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Should NOT have fetched again
|
||||||
|
expect(global.fetch).toHaveBeenCalledTimes(1)
|
||||||
|
expect(result2.current.imgUrl).toBe('blob:mock-url')
|
||||||
|
expect(result2.current.loading).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should cache errors and not re-fetch broken URLs', async () => {
|
||||||
|
global.fetch = vi.fn(() => Promise.resolve({ ok: false, status: 404 }))
|
||||||
|
|
||||||
|
// First mount — fetch fails and error is cached
|
||||||
|
const { unmount } = renderHook(() =>
|
||||||
|
useImageUrl('http://example.com/broken.jpg'),
|
||||||
|
)
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await flushPromises()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledTimes(1)
|
||||||
|
unmount()
|
||||||
|
|
||||||
|
// Remount with same URL — should use cached error, not re-fetch
|
||||||
|
const { result: result2 } = renderHook(() =>
|
||||||
|
useImageUrl('http://example.com/broken.jpg'),
|
||||||
|
)
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await flushPromises()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledTimes(1)
|
||||||
|
expect(result2.current.error).toBe(true)
|
||||||
|
expect(result2.current.imgUrl).toBeNull()
|
||||||
|
expect(result2.current.loading).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -14,8 +14,12 @@ import {
|
|||||||
UrlField,
|
UrlField,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
} from 'react-admin'
|
} from 'react-admin'
|
||||||
import { List } from '../common'
|
import {
|
||||||
import { ToggleFieldsMenu, useSelectedFields } from '../common'
|
List,
|
||||||
|
useImageUrl,
|
||||||
|
ToggleFieldsMenu,
|
||||||
|
useSelectedFields,
|
||||||
|
} from '../common'
|
||||||
import subsonic from '../subsonic'
|
import subsonic from '../subsonic'
|
||||||
import { StreamField } from './StreamField'
|
import { StreamField } from './StreamField'
|
||||||
import { setTrack } from '../actions'
|
import { setTrack } from '../actions'
|
||||||
@ -78,10 +82,12 @@ const RadioListActions = ({
|
|||||||
const avatarStyle = { width: 40, height: 40 }
|
const avatarStyle = { width: 40, height: 40 }
|
||||||
|
|
||||||
const CoverArtField = ({ record }) => {
|
const CoverArtField = ({ record }) => {
|
||||||
if (!record) return null
|
const directUrl = record?.uploadedImage
|
||||||
const src = record.uploadedImage
|
|
||||||
? subsonic.getCoverArtUrl(record, 40, true)
|
? subsonic.getCoverArtUrl(record, 40, true)
|
||||||
: RADIO_PLACEHOLDER_IMAGE
|
: null
|
||||||
|
const { imgUrl } = useImageUrl(directUrl)
|
||||||
|
if (!record) return null
|
||||||
|
const src = imgUrl || RADIO_PLACEHOLDER_IMAGE
|
||||||
return (
|
return (
|
||||||
<Avatar src={src} variant="rounded" style={avatarStyle} alt={record.name} />
|
<Avatar src={src} variant="rounded" style={avatarStyle} alt={record.name} />
|
||||||
)
|
)
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import GruvboxDarkTheme from './gruvboxDark'
|
|||||||
import CatppuccinMacchiatoTheme from './catppuccinMacchiato'
|
import CatppuccinMacchiatoTheme from './catppuccinMacchiato'
|
||||||
import DraculaTheme from './dracula'
|
import DraculaTheme from './dracula'
|
||||||
import NuclearTheme from './nuclear'
|
import NuclearTheme from './nuclear'
|
||||||
|
import NutballTheme from './nutball'
|
||||||
import AmusicTheme from './amusic'
|
import AmusicTheme from './amusic'
|
||||||
import SquiddiesGlassTheme from './SquiddiesGlass'
|
import SquiddiesGlassTheme from './SquiddiesGlass'
|
||||||
import NautilineTheme from './nautiline'
|
import NautilineTheme from './nautiline'
|
||||||
@ -37,6 +38,7 @@ export default {
|
|||||||
NautilineTheme,
|
NautilineTheme,
|
||||||
NordTheme,
|
NordTheme,
|
||||||
NuclearTheme,
|
NuclearTheme,
|
||||||
|
NutballTheme,
|
||||||
SpotifyTheme,
|
SpotifyTheme,
|
||||||
SquiddiesGlassTheme,
|
SquiddiesGlassTheme,
|
||||||
}
|
}
|
||||||
|
|||||||
380
ui/src/themes/nutball.css.js
Normal file
380
ui/src/themes/nutball.css.js
Normal file
@ -0,0 +1,380 @@
|
|||||||
|
const stylesheet = `
|
||||||
|
html {
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
body::-webkit-scrollbar, body::-webkit-scrollbar-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main .music-player-panel {
|
||||||
|
background-color: white!important;
|
||||||
|
box-shadow: none;
|
||||||
|
font-family: monospace;
|
||||||
|
color: black;
|
||||||
|
border-top: 1px solid black;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main .music-player-panel .panel-content div.img-content {
|
||||||
|
animation: none;
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main .music-player-panel .panel-content .progress-bar-content {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: calc(50% - 150px);
|
||||||
|
margin-left: 10px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
section.audio-main {
|
||||||
|
position: absolute;
|
||||||
|
width: calc(100% - 131px)!important;
|
||||||
|
bottom: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
span.audio-title {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
span.audio-title .songTitle {
|
||||||
|
color: black!important;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main .music-player-panel .panel-content .player-content {
|
||||||
|
flex: 1;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
div.player-content > span:first-child {
|
||||||
|
flex: 1!important;
|
||||||
|
justify-content: flex-start!important;
|
||||||
|
}
|
||||||
|
div.player-content > span:first-child svg {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main .music-player-panel .panel-content .player-content > .group {
|
||||||
|
flex: 0;
|
||||||
|
}
|
||||||
|
.play-sounds svg, .loop-btn svg, .audio-lists-btn svg, .destroy-btn {
|
||||||
|
margin-left: 0!important;
|
||||||
|
}
|
||||||
|
.play-sounds svg, .loop-btn svg, .audio-lists-btn svg, .destroy-btn svg {
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main .music-player-panel .panel-content .player-content .audio-lists-btn {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main .music-player-panel .panel-content .player-content .audio-lists-btn .audio-lists-icon svg {
|
||||||
|
height: .75em;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main .music-player-panel .panel-content .progress-bar-content .audio-main .current-time, .react-jinke-music-player-main .music-player-panel .panel-content .progress-bar-content .audio-main .duration {
|
||||||
|
flex-basis: 0;
|
||||||
|
}
|
||||||
|
.progress-bar > div:nth-child(2) > div:nth-child(4) {
|
||||||
|
transform: translateX(-50%) translateY(5%) !important;
|
||||||
|
}
|
||||||
|
.progress-load-bar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.sound-operation > div:nth-child(4) {
|
||||||
|
transform: translateX(-50%) translateY(5%) !important;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main .music-player-panel .panel-content .player-content .play-sounds .sound-operation {
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
.rc-slider {
|
||||||
|
border-radius: 0px;
|
||||||
|
border: 1px solid black;
|
||||||
|
padding: 3px 0!important;
|
||||||
|
}
|
||||||
|
.rc-slider .rc-slider-handle {
|
||||||
|
box-shadow: none!important;
|
||||||
|
border-radius: 0px;
|
||||||
|
background-color: black!important;
|
||||||
|
border: hidden!important;
|
||||||
|
}
|
||||||
|
.rc-slider[style*="left: 0%"] {
|
||||||
|
transform: translateX(0) !important;
|
||||||
|
}
|
||||||
|
.rc-slider .rc-slider-track {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main .rc-slider-rail, .react-jinke-music-player-main.light-theme .rc-slider-rail {
|
||||||
|
background-color: white!important;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main .music-player-panel .panel-content .player-content .play-sounds .sounds-icon {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.lyric-btn {
|
||||||
|
display: none!important;
|
||||||
|
}
|
||||||
|
button[data-testid="save-queue-button"] {
|
||||||
|
display: none!important;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main .music-player-panel .panel-content .player-content .audio-lists-btn {
|
||||||
|
box-shadow: 0 0 0 0;
|
||||||
|
margin: 0;
|
||||||
|
margin-left: -8px;
|
||||||
|
margin-right: -5px;
|
||||||
|
}
|
||||||
|
.audio-lists-btn:hover span,
|
||||||
|
.audio-lists-btn:hover svg {
|
||||||
|
color: #a8fe40!important;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main.light-theme .audio-lists-btn {
|
||||||
|
background-color: white!important;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main .music-player-panel .panel-content .player-content .audio-lists-btn .audio-lists-num {
|
||||||
|
color: grey;
|
||||||
|
margin-left: 5px;
|
||||||
|
font-size: .7rem;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main .music-player-panel .panel-content .player-content .hide-panel {
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main .music-player-panel .panel-content .player-content .hide-panel svg {
|
||||||
|
stroke-width: 15px;
|
||||||
|
stroke: #fff;
|
||||||
|
height: .8em;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 810px) {
|
||||||
|
.react-jinke-music-player-main .music-player-panel .panel-content .player-content .play-sounds .sounds-icon {
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main .music-player-panel .panel-content .player-content .loop-btn {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
.play-sounds svg, .loop-btn svg, .audio-lists-btn svg, .destroy-btn {
|
||||||
|
margin-left: -3px!important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.panel-content li {
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player .music-player-controller,
|
||||||
|
.react-jinke-music-player-main.light-theme .music-player-controller {
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player .music-player-controller:hover,
|
||||||
|
.react-jinke-music-player .music-player-controller:has(+ .destroy-btn:hover) {
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player .music-player-controller .controller-title,
|
||||||
|
.react-jinke-music-player .music-player-controller .music-player-controller-setting {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player .music-player-controller.music-player-playing:before {
|
||||||
|
animation: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
@media screen and (max-width:767px) {
|
||||||
|
.react-jinke-music-player .music-player .destroy-btn {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main .destroy-btn svg {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main svg {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main svg, .react-jinke-music-player-main.light-theme svg {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main svg:active, .react-jinke-music-player-main svg:hover,
|
||||||
|
.react-jinke-music-player-main.light-theme svg:active, .react-jinke-music-player-main.light-theme svg:hover {
|
||||||
|
color: #a8fe40;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main .play-mode-title {
|
||||||
|
font-family: monospace;
|
||||||
|
background-color: white;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-mobile,
|
||||||
|
.react-jinke-music-player-main.light-theme .react-jinke-music-player-mobile {
|
||||||
|
font-family: monospace;
|
||||||
|
background-color: rgba(255, 255, 255, .9);
|
||||||
|
color: black!important;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 50px;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-mobile:before {
|
||||||
|
content: " ";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
text-align: center;
|
||||||
|
width: 90%;
|
||||||
|
height: 700px;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid black;
|
||||||
|
z-index: -1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-mobile-header {
|
||||||
|
align-items: start;
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
justify-content: start;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-mobile-header-title {
|
||||||
|
text-align: left;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-mobile-header-right {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-mobile > .group {
|
||||||
|
flex: 0;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-mobile-cover,
|
||||||
|
.react-jinke-music-player-main.light-theme .react-jinke-music-player-mobile-cover {
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: none;
|
||||||
|
animation: none;
|
||||||
|
border: 1px solid black;
|
||||||
|
margin: 0 auto 4rem auto;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-mobile-cover .cover {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-mobile-progress .current-time {
|
||||||
|
/* margin-right: 17px; */
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-mobile-progress .current-time, .react-jinke-music-player-mobile-progress .duration {
|
||||||
|
color: black!important;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-mobile-progress .rc-slider {
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-mobile-progress .rc-slider-handle {
|
||||||
|
border: 2px solid black;
|
||||||
|
margin-top: -4px;
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-mobile-toggle {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 2rem 0;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-mobile-operation .items .item svg {
|
||||||
|
color: black!important;
|
||||||
|
font-size: 2rem;
|
||||||
|
width: 2rem;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-mobile-operation .items .item svg:hover,
|
||||||
|
.react-jinke-music-player-mobile-operation .items .item button:hover svg {
|
||||||
|
color: #a8fe40!important;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-mobile-operation .items .item .MuiIconButton-root:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.0);
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-mobile-operation .MuiButtonBase-root.Mui-disabled {
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-mobile-operation .items li:nth-child(5) svg {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-mobile-operation .items li:nth-child(5) svg g path:nth-child(2) {
|
||||||
|
stroke-width: .4px;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-mobile-operation .items li:nth-child(2),
|
||||||
|
.react-jinke-music-player-mobile-operation .items li:nth-child(3) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-mobile-play-model-tip {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.audio-lists-panel {
|
||||||
|
overflow-y: scroll;
|
||||||
|
scrollbar-width: none;
|
||||||
|
border-radius: .625rem;
|
||||||
|
bottom: 6.25rem;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main.light-theme .audio-lists-panel {
|
||||||
|
font-family: monospace;
|
||||||
|
box-shadow: none;
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main.light-theme .audio-lists-panel-header {
|
||||||
|
text-shadow: none;
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
}
|
||||||
|
.audio-lists-panel-header-line {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
.audio-lists-panel-header-close-btn:hover svg {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
.audio-lists-panel-content .audio-item,
|
||||||
|
.react-jinke-music-player-main.light-theme .audio-item {
|
||||||
|
border-radius: 0px;
|
||||||
|
margin: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
box-shadow: none;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main.light-theme .audio-lists-panel .audio-lists-panel-content .audio-item:nth-child(2n+1) {
|
||||||
|
background-color: white!important;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main.light-theme .audio-lists-panel .audio-lists-panel-content .audio-item:nth-child(2n+1):hover {
|
||||||
|
background-color: #fafafa!important;
|
||||||
|
}
|
||||||
|
.audio-lists-panel-content .audio-item .player-singer {
|
||||||
|
width: unset;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
.audio-lists-panel-content .audio-item .player-delete:hover svg {
|
||||||
|
color: #a8fe40!important;
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main .audio-lists-panel-content .audio-item:active .group:not([class=".player-delete"]) svg,
|
||||||
|
.react-jinke-music-player-main .audio-lists-panel-content .audio-item:hover .group:not([class=".player-delete"]) svg,
|
||||||
|
.react-jinke-music-player-main.light-theme .audio-item:active svg,
|
||||||
|
.react-jinke-music-player-main.light-theme .audio-item:hover svg {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.audio-lists-panel-content .audio-item .player-delete {
|
||||||
|
justify-content: center;
|
||||||
|
width: 25px;
|
||||||
|
}
|
||||||
|
.audio-lists-panel-content .audio-item .player-delete svg {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
.react-jinke-music-player-main.light-theme .audio-lists-panel .audio-item.playing, .react-jinke-music-player-main.light-theme .audio-lists-panel .audio-item.playing svg {
|
||||||
|
color: #a8fe40!important;
|
||||||
|
}
|
||||||
|
.audio-lists-panel-content .audio-item .player-name,
|
||||||
|
.audio-lists-panel-content .audio-item .player-singer,
|
||||||
|
.react-jinke-music-player-main.light-theme .audio-item.playing .player-singer,
|
||||||
|
.react-jinke-music-player-main.light-theme .audio-lists-panel .audio-item.playing, .react-jinke-music-player-main.light-theme .audio-lists-panel .audio-item.playing .player-delete svg {
|
||||||
|
color: black!important;
|
||||||
|
}
|
||||||
|
.audio-lists-panel-mobile {
|
||||||
|
height: 750px !important;
|
||||||
|
top: calc(100vh / 2 - 375px) !important;
|
||||||
|
width: 91% !important;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.audio-lists-panel-mobile .audio-lists-panel-content {
|
||||||
|
height: auto!important;
|
||||||
|
}
|
||||||
|
.audio-lists-panel-content {
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
@keyframes fromOut {
|
||||||
|
0% {
|
||||||
|
transform:scale(1) translateZ(0)
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform:scale(1) translate3d(0,150%,0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
export default stylesheet
|
||||||
684
ui/src/themes/nutball.js
Normal file
684
ui/src/themes/nutball.js
Normal file
@ -0,0 +1,684 @@
|
|||||||
|
import stylesheet from './nutball.css.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
themeName: 'Nutball',
|
||||||
|
palette: {
|
||||||
|
primary: {
|
||||||
|
main: '#80ea00',
|
||||||
|
light: '#fff',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
main: '#80ea00',
|
||||||
|
contrastText: '#fff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typography: {
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
h6: {
|
||||||
|
fontSize: '1rem',
|
||||||
|
},
|
||||||
|
h4: {
|
||||||
|
fontSize: '1.2rem',
|
||||||
|
},
|
||||||
|
h1: {
|
||||||
|
fontSize: '1.4rem',
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
overrides: {
|
||||||
|
MuiAppBar: {
|
||||||
|
root: {
|
||||||
|
borderBottom: '1px solid black',
|
||||||
|
},
|
||||||
|
colorSecondary: {
|
||||||
|
color: 'black',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiPaper: {
|
||||||
|
elevation1: {
|
||||||
|
boxShadow: 'none',
|
||||||
|
},
|
||||||
|
elevation4: {
|
||||||
|
boxShadow: 'none',
|
||||||
|
},
|
||||||
|
elevation6: {
|
||||||
|
boxShadow: 'none',
|
||||||
|
},
|
||||||
|
elevation8: {
|
||||||
|
boxShadow: 'none',
|
||||||
|
border: '1px solid black',
|
||||||
|
},
|
||||||
|
elevation16: {
|
||||||
|
boxShadow: 'none',
|
||||||
|
borderRight: '1px solid grey!important',
|
||||||
|
},
|
||||||
|
elevation24: {
|
||||||
|
boxShadow: 'none',
|
||||||
|
border: '1px solid black',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiButton: {
|
||||||
|
root: {
|
||||||
|
color: '#80ea00',
|
||||||
|
border: '1px solid rgba(0, 0, 0, 0.23)',
|
||||||
|
transition: 'none',
|
||||||
|
'&[aria-label="Grid"]': {
|
||||||
|
width: '50%',
|
||||||
|
marginLeft: '15px',
|
||||||
|
marginRight: '2px',
|
||||||
|
marginBottom: '10px',
|
||||||
|
'& .MuiButton-label': {
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'&[aria-label="Table"]': {
|
||||||
|
width: '50%',
|
||||||
|
marginRight: '15px',
|
||||||
|
marginLeft: '2px',
|
||||||
|
marginBottom: '10px',
|
||||||
|
'& .MuiButton-label': {
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
textPrimary: {
|
||||||
|
color: 'rgba(0,0,0,.57)',
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: 'black',
|
||||||
|
backgroundColor: '#eaeaea',
|
||||||
|
},
|
||||||
|
'&[aria-label="Grid"]': {
|
||||||
|
color: 'black',
|
||||||
|
borderColor: 'black!important',
|
||||||
|
},
|
||||||
|
'&[aria-label="Table"]': {
|
||||||
|
color: 'black',
|
||||||
|
borderColor: 'black!important',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
textSecondary: {
|
||||||
|
color: 'rgba(0,0,0,.57)',
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: 'black',
|
||||||
|
backgroundColor: '#eaeaea',
|
||||||
|
},
|
||||||
|
'&[aria-label="Grid"]': {
|
||||||
|
color: 'grey',
|
||||||
|
borderColor: 'grey!important',
|
||||||
|
},
|
||||||
|
'&[aria-label="Table"]': {
|
||||||
|
color: 'grey',
|
||||||
|
borderColor: 'grey!important',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
'& svg': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
'& span': {
|
||||||
|
paddingLeft: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contained: {
|
||||||
|
boxShadow: 'none',
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiButtonGroup: {
|
||||||
|
groupedTextHorizontal: {
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
margin: '0 .5rem',
|
||||||
|
'& button': {
|
||||||
|
width: '25%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
groupedTextPrimary: {
|
||||||
|
'&:not(:last-child)': {
|
||||||
|
border: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiIconButton: {
|
||||||
|
root: {
|
||||||
|
'&[aria-label="Settings"]': {
|
||||||
|
padding: '12px!important',
|
||||||
|
marginRight: '-9px!important',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiSwitch: {
|
||||||
|
thumb: {
|
||||||
|
color: '#eaeaea',
|
||||||
|
boxShadow: 'none',
|
||||||
|
borderRadius: '0',
|
||||||
|
},
|
||||||
|
track: {
|
||||||
|
borderRadius: '0',
|
||||||
|
},
|
||||||
|
switchBase: {
|
||||||
|
color: '#eaeaea',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiCheckbox: {
|
||||||
|
root: {
|
||||||
|
'& svg': {
|
||||||
|
width: '.8em',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PrivateSwitchBase: {
|
||||||
|
root: {
|
||||||
|
padding: '8px 8px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RaButton: {
|
||||||
|
button: {
|
||||||
|
marginRight: '10px',
|
||||||
|
lineHeight: 'normal',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiMenu: {
|
||||||
|
list: {
|
||||||
|
'& p': {
|
||||||
|
fontSize: '.85rem',
|
||||||
|
},
|
||||||
|
'& p:first-of-type': {
|
||||||
|
margin: '6px 1rem',
|
||||||
|
},
|
||||||
|
'& li:has(span.MuiCheckbox-root)': {
|
||||||
|
marginLeft: '-10px',
|
||||||
|
},
|
||||||
|
'& span.MuiCheckbox-root .MuiSvgIcon-root': {
|
||||||
|
width: '.75em',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiMenuItem: {
|
||||||
|
root: {
|
||||||
|
fontSize: '.85rem',
|
||||||
|
minHeight: 'inherit',
|
||||||
|
'&[aria-label="Clear value"]:before': {
|
||||||
|
display: 'block',
|
||||||
|
content: "'(any)'",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiListItem: {
|
||||||
|
button: {
|
||||||
|
'& span.MuiCheckbox-root': {
|
||||||
|
padding: '0px 8px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiTooltip: {
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: 'rgb(117 117 117)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiCircularProgress: {
|
||||||
|
root: {
|
||||||
|
color: '#80ea00!important',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiAvatar: {
|
||||||
|
img: {
|
||||||
|
borderRadius: '5px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiFab: {
|
||||||
|
root: {
|
||||||
|
boxShadow: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiTableHead: {
|
||||||
|
root: {
|
||||||
|
boxShadow: 'none!important',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiTableCell: {
|
||||||
|
root: {
|
||||||
|
borderBottom: 'none',
|
||||||
|
},
|
||||||
|
sizeSmall: {
|
||||||
|
'&:last-child': {
|
||||||
|
textAlign: 'right',
|
||||||
|
},
|
||||||
|
'&:last-child:is(th)': {
|
||||||
|
paddingRight: '45px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiTablePagination: {
|
||||||
|
root: {
|
||||||
|
fontSize: '.6rem',
|
||||||
|
},
|
||||||
|
caption: {
|
||||||
|
fontSize: '.6rem',
|
||||||
|
},
|
||||||
|
menuItem: {
|
||||||
|
fontSize: '.6rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiTabs: {
|
||||||
|
root: {
|
||||||
|
marginBottom: '1rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiToolbar: {
|
||||||
|
gutters: {
|
||||||
|
'@media (min-width: 600px)': {
|
||||||
|
paddingLeft: '16px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RaListToolbar: {
|
||||||
|
toolbar: {
|
||||||
|
alignItems: 'start',
|
||||||
|
'& form:has(> div:nth-child(3))': {
|
||||||
|
paddingBottom: '.6rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
paddingRight: '0!important',
|
||||||
|
marginTop: '-8px',
|
||||||
|
textWrap: 'nowrap',
|
||||||
|
'@media (max-width: 599.95px)': {
|
||||||
|
marginTop: '3px',
|
||||||
|
},
|
||||||
|
'& .MuiButton-text': {
|
||||||
|
height: '2.5rem',
|
||||||
|
padding: '7px 10px',
|
||||||
|
marginRight: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RaTopToolbar: {
|
||||||
|
root: {
|
||||||
|
'& div:first-of-type > div:first-of-type': {
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
rowGap: '10px',
|
||||||
|
},
|
||||||
|
'& div:first-of-type > div:first-of-type button': {
|
||||||
|
height: '2rem',
|
||||||
|
},
|
||||||
|
'& div:first-of-type > div:first-of-type .MuiIconButton-root': {
|
||||||
|
padding: '0',
|
||||||
|
marginRight: '2rem',
|
||||||
|
},
|
||||||
|
'& div:first-of-type > div:nth-of-type(2)': {
|
||||||
|
height: '2rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RaToolbar: {
|
||||||
|
toolbar: {
|
||||||
|
backgroundColor: 'white',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RaFilterButton: {
|
||||||
|
root: {
|
||||||
|
textWrap: 'nowrap',
|
||||||
|
'& button': {
|
||||||
|
'@media (max-width: 599.95px)': {
|
||||||
|
padding: '12px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"&[resource*='song']": {
|
||||||
|
marginLeft: '10px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RaDeleteWithUndoButton: {
|
||||||
|
deleteButton: {
|
||||||
|
color: 'rgba(0,0,0,.57)',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.04)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RaAutocompleteSuggestionList: {
|
||||||
|
suggestionsContainer: {
|
||||||
|
borderRadius: '4px',
|
||||||
|
outline: '1px solid black',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RaEmpty: {
|
||||||
|
message: {
|
||||||
|
marginTop: '3rem',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RaAutocompleteArrayInput: {
|
||||||
|
chipContainerOutlined: {
|
||||||
|
'&:empty': {
|
||||||
|
margin: '0',
|
||||||
|
},
|
||||||
|
margin: '10px 0',
|
||||||
|
},
|
||||||
|
chip: {
|
||||||
|
margin: '4px 4px 4px 0!important',
|
||||||
|
},
|
||||||
|
inputInput: {
|
||||||
|
flexGrow: '0',
|
||||||
|
'& #genre_id': {
|
||||||
|
flexGrow: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RaLayout: {
|
||||||
|
content: {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RaDatagrid: {
|
||||||
|
headerCell: {
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NDAlbumShow: {
|
||||||
|
albumActions: {
|
||||||
|
padding: '0',
|
||||||
|
alignItems: 'center',
|
||||||
|
margin: '1rem 0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiCardContent: {
|
||||||
|
root: {
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '.8rem',
|
||||||
|
'& #now-playing-title': {
|
||||||
|
fontSize: '.8rem',
|
||||||
|
},
|
||||||
|
'&:last-child': {
|
||||||
|
paddingBottom: '16px',
|
||||||
|
},
|
||||||
|
'&[class*="makeStyles-usernameWrap-"]': {
|
||||||
|
paddingBottom: '16px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiDialogContent: {
|
||||||
|
root: {
|
||||||
|
'& .MuiTableCell-sizeSmall:last-child': {
|
||||||
|
textAlign: 'left',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiGridList: {
|
||||||
|
root: {
|
||||||
|
'&:empty': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
backgroundColor: 'white',
|
||||||
|
borderRadius: '4px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiGridListTile: {
|
||||||
|
root: {
|
||||||
|
'@media (max-width: 599.95px)': {
|
||||||
|
padding: '7px!important',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tile: {
|
||||||
|
'& img': {
|
||||||
|
borderRadius: '5px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NDAlbumGridView: {
|
||||||
|
root: {
|
||||||
|
'&:has(.MuiGridList-root:empty)': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
albumContainer: {
|
||||||
|
border: '1px solid white',
|
||||||
|
borderRadius: '5px',
|
||||||
|
'& a:hover img': {
|
||||||
|
outline: '1px solid black',
|
||||||
|
},
|
||||||
|
'& a:hover > div:nth-of-type(2)': {
|
||||||
|
border: 'none',
|
||||||
|
outline: '1px solid black',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
albumLink: {
|
||||||
|
paddingRight: '6px',
|
||||||
|
},
|
||||||
|
albumSubtitle: {
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
},
|
||||||
|
tileBar: {
|
||||||
|
transition: 'all 50ms ease-out',
|
||||||
|
},
|
||||||
|
tileBarMobile: {
|
||||||
|
transition: 'all 50ms ease-out',
|
||||||
|
borderLeft: '1px solid black',
|
||||||
|
borderRight: '1px solid black',
|
||||||
|
borderBottom: '1px solid black',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiGridListTileBar: {
|
||||||
|
root: {
|
||||||
|
height: '30px!important',
|
||||||
|
background: 'white!important',
|
||||||
|
borderTop: '1px solid black',
|
||||||
|
borderBottom: '1px solid black',
|
||||||
|
borderRadius: '0 0 5px 5px',
|
||||||
|
},
|
||||||
|
titleWrap: {
|
||||||
|
marginLeft: '0px',
|
||||||
|
},
|
||||||
|
titlePositionBottom: {
|
||||||
|
bottom: '0',
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
'& button': {
|
||||||
|
color: 'black!important',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actionIcon: {
|
||||||
|
'& button': {
|
||||||
|
color: 'black!important',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RaFilter: {
|
||||||
|
form: {
|
||||||
|
width: '100%',
|
||||||
|
'& div.filter-field:first-child': {
|
||||||
|
flex: '1 100%',
|
||||||
|
'& [class*="RaSearchInput-input-"]': {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiInputAdornment: {
|
||||||
|
positionEnd: {
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RaFilterFormInput: {
|
||||||
|
body: {
|
||||||
|
'& label': {
|
||||||
|
transform: 'translate(14px, -6px) scale(0.75)!important',
|
||||||
|
backgroundColor: '#fafafa',
|
||||||
|
padding: '0 5px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hideButton: {
|
||||||
|
order: '1',
|
||||||
|
marginLeft: '2px',
|
||||||
|
top: '-7px',
|
||||||
|
padding: '8px',
|
||||||
|
},
|
||||||
|
spacer: {
|
||||||
|
order: '2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RaPaginationActions: {
|
||||||
|
actions: {
|
||||||
|
'& button': {
|
||||||
|
border: 'none',
|
||||||
|
fontSize: '.6rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NDAlbumDetails: {
|
||||||
|
cover: {
|
||||||
|
borderRadius: '5px',
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
padding: '0',
|
||||||
|
marginLeft: '1rem',
|
||||||
|
},
|
||||||
|
externalLinks: {
|
||||||
|
marginTop: '5px',
|
||||||
|
},
|
||||||
|
notes: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
root: {
|
||||||
|
'& p': {
|
||||||
|
fontSize: '.7rem',
|
||||||
|
backgroundColor: '#e0e0e0',
|
||||||
|
borderRadius: '10px',
|
||||||
|
width: 'fit-content',
|
||||||
|
padding: '2px 7px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NDPlaylistDetails: {
|
||||||
|
cover: {
|
||||||
|
borderRadius: '5px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NDDesktopArtistDetails: {
|
||||||
|
cover: {
|
||||||
|
borderRadius: '0px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NDMobileArtistDetails: {
|
||||||
|
bgContainer: {
|
||||||
|
background:
|
||||||
|
'linear-gradient(to bottom, rgb(255 255 255 / 51%), rgb(250 250 250))!important',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NDArtistShow: {
|
||||||
|
actionsContainer: {
|
||||||
|
'& button': {
|
||||||
|
padding: '4px 5px',
|
||||||
|
fontSize: '0.8125rem',
|
||||||
|
height: '2rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NDAudioPlayer: {
|
||||||
|
audioTitle: {
|
||||||
|
color: 'black',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NDLogin: {
|
||||||
|
main: {
|
||||||
|
background: 'white',
|
||||||
|
'& .MuiFormLabel-root': {
|
||||||
|
color: '#000',
|
||||||
|
},
|
||||||
|
'& .MuiFormLabel-root.Mui-error': {
|
||||||
|
color: '#000',
|
||||||
|
},
|
||||||
|
'& .MuiInput-underline:before': {
|
||||||
|
borderBottom: 'none',
|
||||||
|
},
|
||||||
|
'& .MuiInput-underline:after': {
|
||||||
|
borderBottom: 'none',
|
||||||
|
},
|
||||||
|
'& .MuiFormHelperText-root.Mui-error': {
|
||||||
|
color: '#000',
|
||||||
|
paddingLeft: '10px',
|
||||||
|
},
|
||||||
|
'& .MuiInput-underline:hover:not(.Mui-disabled):before': {
|
||||||
|
borderBottom: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
minWidth: 300,
|
||||||
|
marginTop: '6em',
|
||||||
|
backgroundColor: '#ffffffe6',
|
||||||
|
border: '1px solid black',
|
||||||
|
},
|
||||||
|
avatar: {
|
||||||
|
marginTop: '1rem',
|
||||||
|
'& img': {
|
||||||
|
filter: 'invert(1)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icon: {},
|
||||||
|
input: {
|
||||||
|
'& .MuiInput-root': {
|
||||||
|
border: '1px solid black',
|
||||||
|
borderRadius: '4px',
|
||||||
|
padding: '10px',
|
||||||
|
},
|
||||||
|
'& .MuiInputLabel-root': {
|
||||||
|
padding: '10px',
|
||||||
|
},
|
||||||
|
'& .MuiInputLabel-shrink': {
|
||||||
|
transform: 'translate(0, -5.5px) scale(0.75)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
marginTop: '2rem',
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
boxShadow: 'none',
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow: 'none',
|
||||||
|
backgroundColor: 'rgb(117, 177, 44)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
systemNameLink: {
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
marginBottom: '1rem',
|
||||||
|
color: 'black',
|
||||||
|
'&:before': {
|
||||||
|
content: "'Welcome to '",
|
||||||
|
},
|
||||||
|
'&:after': {
|
||||||
|
content: "' *~*!'",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiCssBaseline: {
|
||||||
|
'@global': {
|
||||||
|
'*::-webkit-scrollbar': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiBackdrop: {
|
||||||
|
root: {
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.5)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RaLoading: {
|
||||||
|
message: {
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
player: {
|
||||||
|
theme: 'light',
|
||||||
|
stylesheet,
|
||||||
|
},
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user