Merge branch 'development' into related-similar-songs

This commit is contained in:
Jeff 2024-03-04 05:04:54 -08:00 committed by GitHub
commit 132b0e173f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
69 changed files with 891 additions and 565 deletions

3
.dockerignore Normal file
View file

@ -0,0 +1,3 @@
node_modules
Dockerfile
docker-compose.*

View file

@ -16,7 +16,7 @@ import webpackPaths from './webpack.paths';
// When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's
// at the dev webpack config is not accidentally run in a production environment // at the dev webpack config is not accidentally run in a production environment
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
checkNodeEnv('development'); checkNodeEnv('development');
} }
const port = process.env.PORT || 4343; const port = process.env.PORT || 4343;
@ -28,171 +28,174 @@ const requiredByDLLConfig = module.parent!.filename.includes('webpack.config.ren
* Warn if the DLL is not built * Warn if the DLL is not built
*/ */
if (!requiredByDLLConfig && !(fs.existsSync(webpackPaths.dllPath) && fs.existsSync(manifest))) { if (!requiredByDLLConfig && !(fs.existsSync(webpackPaths.dllPath) && fs.existsSync(manifest))) {
console.log( console.log(
chalk.black.bgYellow.bold( chalk.black.bgYellow.bold(
'The DLL files are missing. Sit back while we build them for you with "npm run build-dll"', 'The DLL files are missing. Sit back while we build them for you with "npm run build-dll"',
), ),
); );
execSync('npm run postinstall'); execSync('npm run postinstall');
} }
const configuration: webpack.Configuration = { const configuration: webpack.Configuration = {
devtool: 'inline-source-map', devtool: 'inline-source-map',
mode: 'development', mode: 'development',
target: ['web', 'electron-renderer'], target: ['web', 'electron-renderer'],
entry: [ entry: [
`webpack-dev-server/client?http://localhost:${port}/dist`, `webpack-dev-server/client?http://localhost:${port}/dist`,
'webpack/hot/only-dev-server', 'webpack/hot/only-dev-server',
path.join(webpackPaths.srcRendererPath, 'index.tsx'), path.join(webpackPaths.srcRendererPath, 'index.tsx'),
],
output: {
path: webpackPaths.distRendererPath,
publicPath: '/',
filename: 'renderer.dev.js',
library: {
type: 'umd',
},
},
module: {
rules: [
{
test: /\.s?css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]',
exportLocalsConvention: 'camelCaseOnly',
},
sourceMap: true,
importLoaders: 1,
},
},
'sass-loader',
],
include: /\.module\.s?(c|a)ss$/,
},
{
test: /\.s?css$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
exclude: /\.module\.s?(c|a)ss$/,
},
// Fonts
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
// Images
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
], ],
},
plugins: [
...(requiredByDLLConfig
? []
: [
new webpack.DllReferencePlugin({
context: webpackPaths.dllPath,
manifest: require(manifest),
sourceType: 'var',
}),
]),
new webpack.NoEmitOnErrorsPlugin(), output: {
path: webpackPaths.distRendererPath,
/** publicPath: '/',
* Create global constants which can be configured at compile time. filename: 'renderer.dev.js',
* library: {
* Useful for allowing different behaviour between development builds and type: 'umd',
* release builds },
*
* NODE_ENV should be production so that modules do not perform certain
* development checks
*
* By default, use 'development' as NODE_ENV. This can be overriden with
* 'staging', for example, by changing the ENV variables in the npm scripts
*/
new webpack.EnvironmentPlugin({
NODE_ENV: 'development',
}),
new webpack.LoaderOptionsPlugin({
debug: true,
}),
new ReactRefreshWebpackPlugin(),
new HtmlWebpackPlugin({
filename: path.join('index.html'),
template: path.join(webpackPaths.srcRendererPath, 'index.ejs'),
minify: {
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true,
},
isBrowser: false,
env: process.env.NODE_ENV,
isDevelopment: process.env.NODE_ENV !== 'production',
nodeModules: webpackPaths.appNodeModulesPath,
}),
],
node: {
__dirname: false,
__filename: false,
},
devServer: {
port,
compress: true,
hot: true,
headers: { 'Access-Control-Allow-Origin': '*' },
static: {
publicPath: '/',
}, },
historyApiFallback: {
verbose: true,
},
setupMiddlewares(middlewares) {
console.log('Starting preload.js builder...');
const preloadProcess = spawn('npm', ['run', 'start:preload'], {
shell: true,
stdio: 'inherit',
})
.on('close', (code: number) => process.exit(code!))
.on('error', (spawnError) => console.error(spawnError));
console.log('Starting remote.js builder...'); module: {
const remoteProcess = spawn('npm', ['run', 'start:remote'], { rules: [
shell: true, {
stdio: 'inherit', test: /\.s?css$/,
}) use: [
.on('close', (code: number) => process.exit(code!)) 'style-loader',
.on('error', (spawnError) => console.error(spawnError)); {
loader: 'css-loader',
console.log('Starting Main Process...'); options: {
spawn('npm', ['run', 'start:main'], { modules: {
shell: true, localIdentName: '[name]__[local]--[hash:base64:5]',
stdio: 'inherit', exportLocalsConvention: 'camelCaseOnly',
}) },
.on('close', (code: number) => { sourceMap: true,
preloadProcess.kill(); importLoaders: 1,
remoteProcess.kill(); },
process.exit(code!); },
}) 'sass-loader',
.on('error', (spawnError) => console.error(spawnError)); ],
return middlewares; include: /\.module\.s?(c|a)ss$/,
},
{
test: /\.s?css$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
exclude: /\.module\.s?(c|a)ss$/,
},
// Fonts
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
// Images
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
],
},
plugins: [
...(requiredByDLLConfig
? []
: [
new webpack.DllReferencePlugin({
context: webpackPaths.dllPath,
manifest: require(manifest),
sourceType: 'var',
}),
]),
new webpack.NoEmitOnErrorsPlugin(),
/**
* Create global constants which can be configured at compile time.
*
* Useful for allowing different behaviour between development builds and
* release builds
*
* NODE_ENV should be production so that modules do not perform certain
* development checks
*
* By default, use 'development' as NODE_ENV. This can be overriden with
* 'staging', for example, by changing the ENV variables in the npm scripts
*/
new webpack.EnvironmentPlugin({
NODE_ENV: 'development',
}),
new webpack.LoaderOptionsPlugin({
debug: true,
}),
new ReactRefreshWebpackPlugin(),
new HtmlWebpackPlugin({
filename: path.join('index.html'),
template: path.join(webpackPaths.srcRendererPath, 'index.ejs'),
minify: {
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true,
},
isBrowser: false,
env: process.env.NODE_ENV,
isDevelopment: process.env.NODE_ENV !== 'production',
nodeModules: webpackPaths.appNodeModulesPath,
templateParameters: {
web: false,
},
}),
],
node: {
__dirname: false,
__filename: false,
},
devServer: {
port,
compress: true,
hot: true,
headers: { 'Access-Control-Allow-Origin': '*' },
static: {
publicPath: '/',
},
historyApiFallback: {
verbose: true,
},
setupMiddlewares(middlewares) {
console.log('Starting preload.js builder...');
const preloadProcess = spawn('npm', ['run', 'start:preload'], {
shell: true,
stdio: 'inherit',
})
.on('close', (code: number) => process.exit(code!))
.on('error', (spawnError) => console.error(spawnError));
console.log('Starting remote.js builder...');
const remoteProcess = spawn('npm', ['run', 'start:remote'], {
shell: true,
stdio: 'inherit',
})
.on('close', (code: number) => process.exit(code!))
.on('error', (spawnError) => console.error(spawnError));
console.log('Starting Main Process...');
spawn('npm', ['run', 'start:main'], {
shell: true,
stdio: 'inherit',
})
.on('close', (code: number) => {
preloadProcess.kill();
remoteProcess.kill();
process.exit(code!);
})
.on('error', (spawnError) => console.error(spawnError));
return middlewares;
},
}, },
},
}; };
export default merge(baseConfig, configuration); export default merge(baseConfig, configuration);

View file

@ -21,114 +21,117 @@ checkNodeEnv('production');
deleteSourceMaps(); deleteSourceMaps();
const devtoolsConfig = const devtoolsConfig =
process.env.DEBUG_PROD === 'true' process.env.DEBUG_PROD === 'true'
? { ? {
devtool: 'source-map', devtool: 'source-map',
} }
: {}; : {};
const configuration: webpack.Configuration = { const configuration: webpack.Configuration = {
...devtoolsConfig, ...devtoolsConfig,
mode: 'production', mode: 'production',
target: ['web', 'electron-renderer'], target: ['web', 'electron-renderer'],
entry: [path.join(webpackPaths.srcRendererPath, 'index.tsx')], entry: [path.join(webpackPaths.srcRendererPath, 'index.tsx')],
output: { output: {
path: webpackPaths.distRendererPath, path: webpackPaths.distRendererPath,
publicPath: './', publicPath: './',
filename: 'renderer.js', filename: 'renderer.js',
library: { library: {
type: 'umd', type: 'umd',
},
}, },
},
module: { module: {
rules: [ rules: [
{ {
test: /\.s?(a|c)ss$/, test: /\.s?(a|c)ss$/,
use: [ use: [
MiniCssExtractPlugin.loader, MiniCssExtractPlugin.loader,
{ {
loader: 'css-loader', loader: 'css-loader',
options: { options: {
modules: { modules: {
localIdentName: '[name]__[local]--[hash:base64:5]', localIdentName: '[name]__[local]--[hash:base64:5]',
exportLocalsConvention: 'camelCaseOnly', exportLocalsConvention: 'camelCaseOnly',
}, },
sourceMap: true, sourceMap: true,
importLoaders: 1, importLoaders: 1,
},
},
'sass-loader',
],
include: /\.module\.s?(c|a)ss$/,
},
{
test: /\.s?(a|c)ss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
exclude: /\.module\.s?(c|a)ss$/,
},
// Fonts
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
// Images
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
}, },
},
'sass-loader',
], ],
include: /\.module\.s?(c|a)ss$/, },
},
{ optimization: {
test: /\.s?(a|c)ss$/, minimize: true,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], minimizer: [
exclude: /\.module\.s?(c|a)ss$/, new TerserPlugin({
}, parallel: true,
// Fonts }),
{ new CssMinimizerPlugin(),
test: /\.(woff|woff2|eot|ttf|otf)$/i, ],
type: 'asset/resource', },
},
// Images plugins: [
{ /**
test: /\.(png|svg|jpg|jpeg|gif)$/i, * Create global constants which can be configured at compile time.
type: 'asset/resource', *
}, * Useful for allowing different behaviour between development builds and
* release builds
*
* NODE_ENV should be production so that modules do not perform certain
* development checks
*/
new webpack.EnvironmentPlugin({
NODE_ENV: 'production',
DEBUG_PROD: false,
}),
new MiniCssExtractPlugin({
filename: 'style.css',
}),
new BundleAnalyzerPlugin({
analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled',
}),
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.join(webpackPaths.srcRendererPath, 'index.ejs'),
minify: {
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true,
},
isBrowser: false,
isDevelopment: process.env.NODE_ENV !== 'production',
templateParameters: {
web: false,
},
}),
], ],
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true,
}),
new CssMinimizerPlugin(),
],
},
plugins: [
/**
* Create global constants which can be configured at compile time.
*
* Useful for allowing different behaviour between development builds and
* release builds
*
* NODE_ENV should be production so that modules do not perform certain
* development checks
*/
new webpack.EnvironmentPlugin({
NODE_ENV: 'production',
DEBUG_PROD: false,
}),
new MiniCssExtractPlugin({
filename: 'style.css',
}),
new BundleAnalyzerPlugin({
analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled',
}),
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.join(webpackPaths.srcRendererPath, 'index.ejs'),
minify: {
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true,
},
isBrowser: false,
isDevelopment: process.env.NODE_ENV !== 'production',
}),
],
}; };
export default merge(baseConfig, configuration); export default merge(baseConfig, configuration);

View file

@ -116,6 +116,9 @@ const configuration: webpack.Configuration = {
env: process.env.NODE_ENV, env: process.env.NODE_ENV,
isDevelopment: process.env.NODE_ENV !== 'production', isDevelopment: process.env.NODE_ENV !== 'production',
nodeModules: webpackPaths.appNodeModulesPath, nodeModules: webpackPaths.appNodeModulesPath,
templateParameters: {
web: false, // with hot reload, we don't have NGINX injecting variables
},
}), }),
], ],

View file

@ -128,6 +128,9 @@ const configuration: webpack.Configuration = {
}, },
isBrowser: false, isBrowser: false,
isDevelopment: process.env.NODE_ENV !== 'production', isDevelopment: process.env.NODE_ENV !== 'production',
templateParameters: {
web: true,
},
}), }),
], ],
}; };

View file

@ -1,16 +1,20 @@
# --- Builder stage # --- Builder stage
FROM node:18-alpine as builder FROM node:18-alpine as builder
WORKDIR /app WORKDIR /app
COPY . /app
#Copy package.json first to cache node_modules
COPY package.json package-lock.json .
# Scripts include electron-specific dependencies, which we don't need # Scripts include electron-specific dependencies, which we don't need
RUN npm install --legacy-peer-deps --ignore-scripts RUN npm install --legacy-peer-deps --ignore-scripts
#Copy code and build with cached modules
COPY . .
RUN npm run build:web RUN npm run build:web
# --- Production stage # --- Production stage
FROM nginx:alpine-slim FROM nginx:alpine-slim
COPY --chown=nginx:nginx --from=builder /app/release/app/dist/web /usr/share/nginx/html COPY --chown=nginx:nginx --from=builder /app/release/app/dist/web /usr/share/nginx/html
COPY ./settings.js.template /etc/nginx/templates/settings.js.template
COPY ng.conf.template /etc/nginx/templates/default.conf.template COPY ng.conf.template /etc/nginx/templates/default.conf.template
ENV PUBLIC_PATH="/" ENV PUBLIC_PATH="/"

View file

@ -81,6 +81,8 @@ docker run --name feishin -p 9180:9180 feishin
3. _Optional_ - If you want to host Feishin on a subpath (not `/`), then pass in the following environment variable: `PUBLIC_PATH=PATH`. For example, to host on `/feishin`, pass in `PUBLIC_PATH=/feishin`. 3. _Optional_ - If you want to host Feishin on a subpath (not `/`), then pass in the following environment variable: `PUBLIC_PATH=PATH`. For example, to host on `/feishin`, pass in `PUBLIC_PATH=/feishin`.
4. _Optional_ - To hard code the server url, pass the following environment variables: `SERVER_NAME`, `SERVER_TYPE` (one of `jellyfin` or `navidrome`), `SERVER_URL`. To prevent users from changing these settings, pass `SERVER_LOCK=true`. This can only be set if all three of the previous values are set.
## FAQ ## FAQ
### MPV is either not working or is rapidly switching between pause/play states ### MPV is either not working or is rapidly switching between pause/play states

13
docker-compose.yaml Normal file
View file

@ -0,0 +1,13 @@
version: '3.5'
services:
feishin:
container_name: feishin
image: jeffvli/feishin
restart: unless-stopped
ports:
- 9180:9180
environment:
- SERVER_NAME=jellyfin # pre defined server name
- SERVER_LOCK=true # When true AND name/type/url are set, only username/password can be toggled
- SERVER_TYPE=jellyfin # navidrome also works
- SERVER_URL= # http://address:port

View file

@ -16,4 +16,12 @@ server {
alias /usr/share/nginx/html/; alias /usr/share/nginx/html/;
try_files $uri $uri/ /index.html =404; try_files $uri $uri/ /index.html =404;
} }
location ${PUBLIC_PATH}settings.js {
alias /etc/nginx/conf.d/settings.js;
}
location ${PUBLIC_PATH}/settings.js {
alias /etc/nginx/conf.d/settings.js;
}
} }

214
package-lock.json generated
View file

@ -66,6 +66,7 @@
"react-virtualized-auto-sizer": "^1.0.17", "react-virtualized-auto-sizer": "^1.0.17",
"react-window": "^1.8.9", "react-window": "^1.8.9",
"react-window-infinite-loader": "^1.0.9", "react-window-infinite-loader": "^1.0.9",
"semver": "^7.5.4",
"styled-components": "^6.0.8", "styled-components": "^6.0.8",
"swiper": "^9.3.1", "swiper": "^9.3.1",
"zod": "^3.22.3", "zod": "^3.22.3",
@ -5023,7 +5024,7 @@
"version": "15.7.4", "version": "15.7.4",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
"integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==", "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==",
"devOptional": true "dev": true
}, },
"node_modules/@types/qs": { "node_modules/@types/qs": {
"version": "6.9.7", "version": "6.9.7",
@ -5041,7 +5042,7 @@
"version": "18.0.26", "version": "18.0.26",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz",
"integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==", "integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==",
"devOptional": true, "dev": true,
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"@types/scheduler": "*", "@types/scheduler": "*",
@ -5113,7 +5114,7 @@
"version": "0.16.2", "version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
"devOptional": true "dev": true
}, },
"node_modules/@types/semver": { "node_modules/@types/semver": {
"version": "7.3.13", "version": "7.3.13",
@ -6445,30 +6446,6 @@
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
} }
}, },
"node_modules/babel-plugin-styled-components": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.7.tgz",
"integrity": "sha512-i7YhvPgVqRKfoQ66toiZ06jPNA3p6ierpfUuEWxNF+fV27Uv5gxBkf8KZLHUCc1nFA9j6+80pYoIpqCeyW3/bA==",
"optional": true,
"peer": true,
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.16.0",
"@babel/helper-module-imports": "^7.16.0",
"babel-plugin-syntax-jsx": "^6.18.0",
"lodash": "^4.17.11",
"picomatch": "^2.3.0"
},
"peerDependencies": {
"styled-components": ">= 2"
}
},
"node_modules/babel-plugin-syntax-jsx": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
"integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==",
"optional": true,
"peer": true
},
"node_modules/babel-preset-current-node-syntax": { "node_modules/babel-preset-current-node-syntax": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz",
@ -12555,9 +12532,9 @@
} }
}, },
"node_modules/ip": { "node_modules/ip": {
"version": "1.1.5", "version": "1.1.9",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz",
"integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==",
"dev": true "dev": true
}, },
"node_modules/ipaddr.js": { "node_modules/ipaddr.js": {
@ -21959,8 +21936,7 @@
"@babel/plugin-proposal-private-property-in-object": { "@babel/plugin-proposal-private-property-in-object": {
"version": "7.21.0-placeholder-for-preset-env.2", "version": "7.21.0-placeholder-for-preset-env.2",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
"integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w=="
"requires": {}
}, },
"@babel/plugin-syntax-async-generators": { "@babel/plugin-syntax-async-generators": {
"version": "7.8.4", "version": "7.8.4",
@ -22834,8 +22810,7 @@
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.3.1.tgz", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.3.1.tgz",
"integrity": "sha512-xrvsmVUtefWMWQsGgFffqWSK03pZ1vfDki4IVIIUxxDKnGBzqNgv0A7SB1oXtVNEkcVO8xi1ZrTL29HhSu5kGA==", "integrity": "sha512-xrvsmVUtefWMWQsGgFffqWSK03pZ1vfDki4IVIIUxxDKnGBzqNgv0A7SB1oXtVNEkcVO8xi1ZrTL29HhSu5kGA==",
"dev": true, "dev": true
"requires": {}
}, },
"@csstools/css-tokenizer": { "@csstools/css-tokenizer": {
"version": "2.2.0", "version": "2.2.0",
@ -22847,15 +22822,13 @@
"version": "2.1.4", "version": "2.1.4",
"resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.4.tgz", "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.4.tgz",
"integrity": "sha512-V/OUXYX91tAC1CDsiY+HotIcJR+vPtzrX8pCplCpT++i8ThZZsq5F5dzZh/bDM3WUOjrvC1ljed1oSJxMfjqhw==", "integrity": "sha512-V/OUXYX91tAC1CDsiY+HotIcJR+vPtzrX8pCplCpT++i8ThZZsq5F5dzZh/bDM3WUOjrvC1ljed1oSJxMfjqhw==",
"dev": true, "dev": true
"requires": {}
}, },
"@csstools/selector-specificity": { "@csstools/selector-specificity": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.0.0.tgz", "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.0.0.tgz",
"integrity": "sha512-hBI9tfBtuPIi885ZsZ32IMEU/5nlZH/KOVYJCOh7gyMxaVLGmLedYqFN6Ui1LXkI8JlC8IsuC0rF0btcRZKd5g==", "integrity": "sha512-hBI9tfBtuPIi885ZsZ32IMEU/5nlZH/KOVYJCOh7gyMxaVLGmLedYqFN6Ui1LXkI8JlC8IsuC0rF0btcRZKd5g==",
"dev": true, "dev": true
"requires": {}
}, },
"@develar/schema-utils": { "@develar/schema-utils": {
"version": "2.6.5", "version": "2.6.5",
@ -23440,8 +23413,7 @@
"@emotion/use-insertion-effect-with-fallbacks": { "@emotion/use-insertion-effect-with-fallbacks": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz",
"integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==", "integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A=="
"requires": {}
}, },
"@emotion/utils": { "@emotion/utils": {
"version": "1.2.0", "version": "1.2.0",
@ -23994,8 +23966,7 @@
"@mantine/hooks": { "@mantine/hooks": {
"version": "6.0.17", "version": "6.0.17",
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-6.0.17.tgz", "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-6.0.17.tgz",
"integrity": "sha512-7vf2w1NlzKlUynSuyI2DAIKoEOYKYC8k+tlSsk3BRdbzhbJAiWxcYzJy5seg5dFW1WIpKAZ0wiVdHXf/WRlRgg==", "integrity": "sha512-7vf2w1NlzKlUynSuyI2DAIKoEOYKYC8k+tlSsk3BRdbzhbJAiWxcYzJy5seg5dFW1WIpKAZ0wiVdHXf/WRlRgg=="
"requires": {}
}, },
"@mantine/modals": { "@mantine/modals": {
"version": "6.0.17", "version": "6.0.17",
@ -24038,8 +24009,7 @@
"@mantine/utils": { "@mantine/utils": {
"version": "6.0.17", "version": "6.0.17",
"resolved": "https://registry.npmjs.org/@mantine/utils/-/utils-6.0.17.tgz", "resolved": "https://registry.npmjs.org/@mantine/utils/-/utils-6.0.17.tgz",
"integrity": "sha512-U6SWV/asYE6NhiHx4ltmVZdQR3HwGVqJxVulhOylMcV1tX/P1LMQUCbGV2Oe4O9jbX4/YW5B/CBb4BbEhENQFQ==", "integrity": "sha512-U6SWV/asYE6NhiHx4ltmVZdQR3HwGVqJxVulhOylMcV1tX/P1LMQUCbGV2Oe4O9jbX4/YW5B/CBb4BbEhENQFQ=="
"requires": {}
}, },
"@mdn/browser-compat-data": { "@mdn/browser-compat-data": {
"version": "5.5.10", "version": "5.5.10",
@ -24596,8 +24566,7 @@
"@ts-rest/core": { "@ts-rest/core": {
"version": "3.23.0", "version": "3.23.0",
"resolved": "https://registry.npmjs.org/@ts-rest/core/-/core-3.23.0.tgz", "resolved": "https://registry.npmjs.org/@ts-rest/core/-/core-3.23.0.tgz",
"integrity": "sha512-2vJwa682m9yS/xQPvPxZBluJfIZwNkt2HY9ER3UtGnu8Dijw+8iymSyIyjRLpFFWUyRnVp9IqrEi/d84bkNFIw==", "integrity": "sha512-2vJwa682m9yS/xQPvPxZBluJfIZwNkt2HY9ER3UtGnu8Dijw+8iymSyIyjRLpFFWUyRnVp9IqrEi/d84bkNFIw=="
"requires": {}
}, },
"@tsconfig/node10": { "@tsconfig/node10": {
"version": "1.0.8", "version": "1.0.8",
@ -24965,7 +24934,7 @@
"version": "15.7.4", "version": "15.7.4",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
"integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==", "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==",
"devOptional": true "dev": true
}, },
"@types/qs": { "@types/qs": {
"version": "6.9.7", "version": "6.9.7",
@ -24983,7 +24952,7 @@
"version": "18.0.26", "version": "18.0.26",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz",
"integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==", "integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==",
"devOptional": true, "dev": true,
"requires": { "requires": {
"@types/prop-types": "*", "@types/prop-types": "*",
"@types/scheduler": "*", "@types/scheduler": "*",
@ -25055,7 +25024,7 @@
"version": "0.16.2", "version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
"devOptional": true "dev": true
}, },
"@types/semver": { "@types/semver": {
"version": "7.3.13", "version": "7.3.13",
@ -25434,8 +25403,7 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.1.1.tgz",
"integrity": "sha512-1FBc1f9G4P/AxMqIgfZgeOTuRnwZMten8E7zap5zgpPInnCrP8D4Q81+4CWIch8i/Nf7nXjP0v6CjjbHOrXhKg==", "integrity": "sha512-1FBc1f9G4P/AxMqIgfZgeOTuRnwZMten8E7zap5zgpPInnCrP8D4Q81+4CWIch8i/Nf7nXjP0v6CjjbHOrXhKg==",
"dev": true, "dev": true
"requires": {}
}, },
"@webpack-cli/info": { "@webpack-cli/info": {
"version": "1.4.1", "version": "1.4.1",
@ -25450,8 +25418,7 @@
"version": "1.6.1", "version": "1.6.1",
"resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.6.1.tgz", "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.6.1.tgz",
"integrity": "sha512-gNGTiTrjEVQ0OcVnzsRSqTxaBSr+dmTfm+qJsCDluky8uhdLWep7Gcr62QsAKHTMxjCS/8nEITsmFAhfIx+QSw==", "integrity": "sha512-gNGTiTrjEVQ0OcVnzsRSqTxaBSr+dmTfm+qJsCDluky8uhdLWep7Gcr62QsAKHTMxjCS/8nEITsmFAhfIx+QSw==",
"dev": true, "dev": true
"requires": {}
}, },
"@xhayper/discord-rpc": { "@xhayper/discord-rpc": {
"version": "1.0.24", "version": "1.0.24",
@ -25465,8 +25432,7 @@
"ws": { "ws": {
"version": "8.14.2", "version": "8.14.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
"integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g=="
"requires": {}
} }
} }
}, },
@ -25544,15 +25510,13 @@
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz",
"integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==",
"dev": true, "dev": true
"requires": {}
}, },
"acorn-jsx": { "acorn-jsx": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true, "dev": true
"requires": {}
}, },
"acorn-walk": { "acorn-walk": {
"version": "7.2.0", "version": "7.2.0",
@ -25638,8 +25602,7 @@
"version": "3.5.2", "version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true, "dev": true
"requires": {}
}, },
"ansi-escapes": { "ansi-escapes": {
"version": "4.3.2", "version": "4.3.2",
@ -26082,27 +26045,6 @@
"@babel/helper-define-polyfill-provider": "^0.4.2" "@babel/helper-define-polyfill-provider": "^0.4.2"
} }
}, },
"babel-plugin-styled-components": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.7.tgz",
"integrity": "sha512-i7YhvPgVqRKfoQ66toiZ06jPNA3p6ierpfUuEWxNF+fV27Uv5gxBkf8KZLHUCc1nFA9j6+80pYoIpqCeyW3/bA==",
"optional": true,
"peer": true,
"requires": {
"@babel/helper-annotate-as-pure": "^7.16.0",
"@babel/helper-module-imports": "^7.16.0",
"babel-plugin-syntax-jsx": "^6.18.0",
"lodash": "^4.17.11",
"picomatch": "^2.3.0"
}
},
"babel-plugin-syntax-jsx": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
"integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==",
"optional": true,
"peer": true
},
"babel-preset-current-node-syntax": { "babel-preset-current-node-syntax": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz",
@ -26361,8 +26303,7 @@
"version": "0.0.3", "version": "0.0.3",
"resolved": "https://registry.npmjs.org/browserslist-config-erb/-/browserslist-config-erb-0.0.3.tgz", "resolved": "https://registry.npmjs.org/browserslist-config-erb/-/browserslist-config-erb-0.0.3.tgz",
"integrity": "sha512-y47DryCY92lxkKyRVMlaZvXAolIY7U33q9e4CS0MdWeJkoAht7OzsrkfdZFCBOP3H5q1EVUxS0L7VVsKM6gZCQ==", "integrity": "sha512-y47DryCY92lxkKyRVMlaZvXAolIY7U33q9e4CS0MdWeJkoAht7OzsrkfdZFCBOP3H5q1EVUxS0L7VVsKM6gZCQ==",
"dev": true, "dev": true
"requires": {}
}, },
"bs-logger": { "bs-logger": {
"version": "0.2.6", "version": "0.2.6",
@ -27202,8 +27143,7 @@
"version": "6.2.2", "version": "6.2.2",
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.2.2.tgz", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.2.2.tgz",
"integrity": "sha512-Ufadglr88ZLsrvS11gjeu/40Lw74D9Am/Jpr3LlYm5Q4ZP5KdlUhG+6u2EjyXeZcxmZ2h1ebCKngDjolpeLHpg==", "integrity": "sha512-Ufadglr88ZLsrvS11gjeu/40Lw74D9Am/Jpr3LlYm5Q4ZP5KdlUhG+6u2EjyXeZcxmZ2h1ebCKngDjolpeLHpg==",
"dev": true, "dev": true
"requires": {}
}, },
"css-functions-list": { "css-functions-list": {
"version": "3.2.0", "version": "3.2.0",
@ -27399,8 +27339,7 @@
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz",
"integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==",
"dev": true, "dev": true
"requires": {}
}, },
"csso": { "csso": {
"version": "4.2.0", "version": "4.2.0",
@ -28672,8 +28611,7 @@
"version": "8.8.0", "version": "8.8.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz",
"integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==", "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==",
"dev": true, "dev": true
"requires": {}
}, },
"eslint-import-resolver-node": { "eslint-import-resolver-node": {
"version": "0.3.7", "version": "0.3.7",
@ -28904,8 +28842,7 @@
"version": "6.1.1", "version": "6.1.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz",
"integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==", "integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==",
"dev": true, "dev": true
"requires": {}
}, },
"eslint-plugin-react": { "eslint-plugin-react": {
"version": "7.33.0", "version": "7.33.0",
@ -28962,8 +28899,7 @@
"version": "4.6.0", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz",
"integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==",
"dev": true, "dev": true
"requires": {}
}, },
"eslint-plugin-sort-keys-fix": { "eslint-plugin-sort-keys-fix": {
"version": "1.1.2", "version": "1.1.2",
@ -30558,8 +30494,7 @@
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
"integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
"dev": true, "dev": true
"requires": {}
}, },
"idb-keyval": { "idb-keyval": {
"version": "6.2.1", "version": "6.2.1",
@ -30704,9 +30639,9 @@
} }
}, },
"ip": { "ip": {
"version": "1.1.5", "version": "1.1.9",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz",
"integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==",
"dev": true "dev": true
}, },
"ipaddr.js": { "ipaddr.js": {
@ -31390,8 +31325,7 @@
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz",
"integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==",
"dev": true, "dev": true
"requires": {}
}, },
"jest-regex-util": { "jest-regex-util": {
"version": "27.5.1", "version": "27.5.1",
@ -32921,8 +32855,7 @@
"overlayscrollbars-react": { "overlayscrollbars-react": {
"version": "0.5.1", "version": "0.5.1",
"resolved": "https://registry.npmjs.org/overlayscrollbars-react/-/overlayscrollbars-react-0.5.1.tgz", "resolved": "https://registry.npmjs.org/overlayscrollbars-react/-/overlayscrollbars-react-0.5.1.tgz",
"integrity": "sha512-0xw9J1CT/cQ+ELYy3hudG6nY1H5dgJ1DdVW3d8aZwqx6wyHNZV4nsBQXUxoHmPo3dmlJ5MvOLzpKWA4X6nL4QA==", "integrity": "sha512-0xw9J1CT/cQ+ELYy3hudG6nY1H5dgJ1DdVW3d8aZwqx6wyHNZV4nsBQXUxoHmPo3dmlJ5MvOLzpKWA4X6nL4QA=="
"requires": {}
}, },
"p-cancelable": { "p-cancelable": {
"version": "2.1.1", "version": "2.1.1",
@ -33303,29 +33236,25 @@
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.1.tgz", "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.1.tgz",
"integrity": "sha512-5JscyFmvkUxz/5/+TB3QTTT9Gi9jHkcn8dcmmuN68JQcv3aQg4y88yEHHhwFB52l/NkaJ43O0dbksGMAo49nfQ==", "integrity": "sha512-5JscyFmvkUxz/5/+TB3QTTT9Gi9jHkcn8dcmmuN68JQcv3aQg4y88yEHHhwFB52l/NkaJ43O0dbksGMAo49nfQ==",
"dev": true, "dev": true
"requires": {}
}, },
"postcss-discard-duplicates": { "postcss-discard-duplicates": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz",
"integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==",
"dev": true, "dev": true
"requires": {}
}, },
"postcss-discard-empty": { "postcss-discard-empty": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz",
"integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==",
"dev": true, "dev": true
"requires": {}
}, },
"postcss-discard-overridden": { "postcss-discard-overridden": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz",
"integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==",
"dev": true, "dev": true
"requires": {}
}, },
"postcss-media-query-parser": { "postcss-media-query-parser": {
"version": "0.2.3", "version": "0.2.3",
@ -33399,8 +33328,7 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
"integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==",
"dev": true, "dev": true
"requires": {}
}, },
"postcss-modules-local-by-default": { "postcss-modules-local-by-default": {
"version": "4.0.0", "version": "4.0.0",
@ -33435,8 +33363,7 @@
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz",
"integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==",
"dev": true, "dev": true
"requires": {}
}, },
"postcss-normalize-display-values": { "postcss-normalize-display-values": {
"version": "5.1.0", "version": "5.1.0",
@ -33551,15 +33478,13 @@
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz",
"integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==",
"dev": true, "dev": true
"requires": {}
}, },
"postcss-scss": { "postcss-scss": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.6.tgz", "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.6.tgz",
"integrity": "sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ==", "integrity": "sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ==",
"dev": true, "dev": true
"requires": {}
}, },
"postcss-selector-parser": { "postcss-selector-parser": {
"version": "6.0.13", "version": "6.0.13",
@ -33604,8 +33529,7 @@
"version": "0.36.2", "version": "0.36.2",
"resolved": "https://registry.npmjs.org/postcss-syntax/-/postcss-syntax-0.36.2.tgz", "resolved": "https://registry.npmjs.org/postcss-syntax/-/postcss-syntax-0.36.2.tgz",
"integrity": "sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==", "integrity": "sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==",
"dev": true, "dev": true
"requires": {}
}, },
"postcss-unique-selectors": { "postcss-unique-selectors": {
"version": "5.1.1", "version": "5.1.1",
@ -33950,8 +33874,7 @@
"react-icons": { "react-icons": {
"version": "4.10.1", "version": "4.10.1",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.10.1.tgz", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.10.1.tgz",
"integrity": "sha512-/ngzDP/77tlCfqthiiGNZeYFACw85fUjZtLbedmJ5DTlNDIwETxhwBzdOJ21zj4iJdvc0J3y7yOsX3PpxAJzrw==", "integrity": "sha512-/ngzDP/77tlCfqthiiGNZeYFACw85fUjZtLbedmJ5DTlNDIwETxhwBzdOJ21zj4iJdvc0J3y7yOsX3PpxAJzrw=="
"requires": {}
}, },
"react-is": { "react-is": {
"version": "17.0.2", "version": "17.0.2",
@ -33988,8 +33911,7 @@
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/react-refresh-typescript/-/react-refresh-typescript-2.0.4.tgz", "resolved": "https://registry.npmjs.org/react-refresh-typescript/-/react-refresh-typescript-2.0.4.tgz",
"integrity": "sha512-ySsBExEFik5Jjf7NoXtFbzUk2rYWM4gF5gg+wRTNmp9p7B2uMpAAa339FHWqmB8EAr0e6mzzskAXxc0Jd04fBw==", "integrity": "sha512-ySsBExEFik5Jjf7NoXtFbzUk2rYWM4gF5gg+wRTNmp9p7B2uMpAAa339FHWqmB8EAr0e6mzzskAXxc0Jd04fBw==",
"dev": true, "dev": true
"requires": {}
}, },
"react-remove-scroll": { "react-remove-scroll": {
"version": "2.5.5", "version": "2.5.5",
@ -34042,8 +33964,7 @@
"react-simple-img": { "react-simple-img": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/react-simple-img/-/react-simple-img-3.0.0.tgz", "resolved": "https://registry.npmjs.org/react-simple-img/-/react-simple-img-3.0.0.tgz",
"integrity": "sha512-I0sG/GgY9c+04BgWf1YRlipWBQxR3oG2s/bagU8EO7zals3/Vkfk1PJMeYh/wHfjxJtUmal+y7HWEBm4MzXVsQ==", "integrity": "sha512-I0sG/GgY9c+04BgWf1YRlipWBQxR3oG2s/bagU8EO7zals3/Vkfk1PJMeYh/wHfjxJtUmal+y7HWEBm4MzXVsQ=="
"requires": {}
}, },
"react-style-singleton": { "react-style-singleton": {
"version": "2.2.1", "version": "2.2.1",
@ -34098,8 +34019,7 @@
"react-virtualized-auto-sizer": { "react-virtualized-auto-sizer": {
"version": "1.0.20", "version": "1.0.20",
"resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.20.tgz", "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.20.tgz",
"integrity": "sha512-OdIyHwj4S4wyhbKHOKM1wLSj/UDXm839Z3Cvfg2a9j+He6yDa6i5p0qQvEiCnyQlGO/HyfSnigQwuxvYalaAXA==", "integrity": "sha512-OdIyHwj4S4wyhbKHOKM1wLSj/UDXm839Z3Cvfg2a9j+He6yDa6i5p0qQvEiCnyQlGO/HyfSnigQwuxvYalaAXA=="
"requires": {}
}, },
"react-window": { "react-window": {
"version": "1.8.9", "version": "1.8.9",
@ -34120,8 +34040,7 @@
"react-window-infinite-loader": { "react-window-infinite-loader": {
"version": "1.0.9", "version": "1.0.9",
"resolved": "https://registry.npmjs.org/react-window-infinite-loader/-/react-window-infinite-loader-1.0.9.tgz", "resolved": "https://registry.npmjs.org/react-window-infinite-loader/-/react-window-infinite-loader-1.0.9.tgz",
"integrity": "sha512-5Hg89IdU4Vrp0RT8kZYKeTIxWZYhNkVXeI1HbKo01Vm/Z7qztDvXljwx16sMzsa9yapRJQW3ODZfMUw38SOWHw==", "integrity": "sha512-5Hg89IdU4Vrp0RT8kZYKeTIxWZYhNkVXeI1HbKo01Vm/Z7qztDvXljwx16sMzsa9yapRJQW3ODZfMUw38SOWHw=="
"requires": {}
}, },
"read-config-file": { "read-config-file": {
"version": "6.3.2", "version": "6.3.2",
@ -35268,8 +35187,7 @@
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz",
"integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==", "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==",
"dev": true, "dev": true
"requires": {}
}, },
"style-search": { "style-search": {
"version": "0.1.0", "version": "0.1.0",
@ -35478,8 +35396,7 @@
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/postcss-sorting/-/postcss-sorting-8.0.2.tgz", "resolved": "https://registry.npmjs.org/postcss-sorting/-/postcss-sorting-8.0.2.tgz",
"integrity": "sha512-M9dkSrmU00t/jK7rF6BZSZauA5MAaBW4i5EnJXspMwt4iqTh/L9j6fgMnbElEOfyRyfLfVbIHj/R52zHzAPe1Q==", "integrity": "sha512-M9dkSrmU00t/jK7rF6BZSZauA5MAaBW4i5EnJXspMwt4iqTh/L9j6fgMnbElEOfyRyfLfVbIHj/R52zHzAPe1Q==",
"dev": true, "dev": true
"requires": {}
}, },
"stylelint-order": { "stylelint-order": {
"version": "6.0.3", "version": "6.0.3",
@ -35497,8 +35414,7 @@
"version": "13.0.0", "version": "13.0.0",
"resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-13.0.0.tgz", "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-13.0.0.tgz",
"integrity": "sha512-EH+yRj6h3GAe/fRiyaoO2F9l9Tgg50AOFhaszyfov9v6ayXJ1IkSHwTxd7lB48FmOeSGDPLjatjO11fJpmarkQ==", "integrity": "sha512-EH+yRj6h3GAe/fRiyaoO2F9l9Tgg50AOFhaszyfov9v6ayXJ1IkSHwTxd7lB48FmOeSGDPLjatjO11fJpmarkQ==",
"dev": true, "dev": true
"requires": {}
}, },
"stylelint-config-standard": { "stylelint-config-standard": {
"version": "34.0.0", "version": "34.0.0",
@ -35534,8 +35450,7 @@
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-7.0.0.tgz", "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-7.0.0.tgz",
"integrity": "sha512-yGn84Bf/q41J4luis1AZ95gj0EQwRX8lWmGmBwkwBNSkpGSpl66XcPTulxGa/Z91aPoNGuIGBmFkcM1MejMo9Q==", "integrity": "sha512-yGn84Bf/q41J4luis1AZ95gj0EQwRX8lWmGmBwkwBNSkpGSpl66XcPTulxGa/Z91aPoNGuIGBmFkcM1MejMo9Q==",
"dev": true, "dev": true
"requires": {}
} }
} }
}, },
@ -35552,8 +35467,7 @@
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-7.0.0.tgz", "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-7.0.0.tgz",
"integrity": "sha512-yGn84Bf/q41J4luis1AZ95gj0EQwRX8lWmGmBwkwBNSkpGSpl66XcPTulxGa/Z91aPoNGuIGBmFkcM1MejMo9Q==", "integrity": "sha512-yGn84Bf/q41J4luis1AZ95gj0EQwRX8lWmGmBwkwBNSkpGSpl66XcPTulxGa/Z91aPoNGuIGBmFkcM1MejMo9Q==",
"dev": true, "dev": true
"requires": {}
} }
} }
} }
@ -36272,8 +36186,7 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/typescript-plugin-styled-components/-/typescript-plugin-styled-components-3.0.0.tgz", "resolved": "https://registry.npmjs.org/typescript-plugin-styled-components/-/typescript-plugin-styled-components-3.0.0.tgz",
"integrity": "sha512-QWlhTl6NqsFxtJyxn7pJjm3RhgzXSByUftZ3AoQClrMMpa4yAaHuJKTN1gFpH3Ti+Rwm56fNUfG9pXSBU+WW3A==", "integrity": "sha512-QWlhTl6NqsFxtJyxn7pJjm3RhgzXSByUftZ3AoQClrMMpa4yAaHuJKTN1gFpH3Ti+Rwm56fNUfG9pXSBU+WW3A==",
"dev": true, "dev": true
"requires": {}
}, },
"unbox-primitive": { "unbox-primitive": {
"version": "1.0.2", "version": "1.0.2",
@ -36429,14 +36342,12 @@
"use-composed-ref": { "use-composed-ref": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.3.0.tgz", "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.3.0.tgz",
"integrity": "sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==", "integrity": "sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ=="
"requires": {}
}, },
"use-isomorphic-layout-effect": { "use-isomorphic-layout-effect": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
"integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA=="
"requires": {}
}, },
"use-latest": { "use-latest": {
"version": "1.2.1", "version": "1.2.1",
@ -36458,8 +36369,7 @@
"use-sync-external-store": { "use-sync-external-store": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA=="
"requires": {}
}, },
"utf8-byte-length": { "utf8-byte-length": {
"version": "1.0.4", "version": "1.0.4",
@ -36987,8 +36897,7 @@
"version": "8.5.0", "version": "8.5.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz",
"integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==",
"dev": true, "dev": true
"requires": {}
} }
} }
}, },
@ -37137,8 +37046,7 @@
"version": "7.5.7", "version": "7.5.7",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz",
"integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==", "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==",
"dev": true, "dev": true
"requires": {}
}, },
"xml-name-validator": { "xml-name-validator": {
"version": "3.0.0", "version": "3.0.0",

View file

@ -9,7 +9,7 @@
"build:remote": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.remote.prod.ts", "build:remote": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.remote.prod.ts",
"build:renderer": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.prod.ts", "build:renderer": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.prod.ts",
"build:web": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.web.prod.ts", "build:web": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.web.prod.ts",
"build:docker": "npm run build:web && docker build -t jeffvli/feishin .", "build:docker": "docker build -t jeffvli/feishin .",
"rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app", "rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app",
"lint": "concurrently \"npm run lint:code\" \"npm run lint:styles\"", "lint": "concurrently \"npm run lint:code\" \"npm run lint:styles\"",
"lint:code": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx --fix", "lint:code": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx --fix",
@ -345,6 +345,7 @@
"react-virtualized-auto-sizer": "^1.0.17", "react-virtualized-auto-sizer": "^1.0.17",
"react-window": "^1.8.9", "react-window": "^1.8.9",
"react-window-infinite-loader": "^1.0.9", "react-window-infinite-loader": "^1.0.9",
"semver": "^7.5.4",
"styled-components": "^6.0.8", "styled-components": "^6.0.8",
"swiper": "^9.3.1", "swiper": "^9.3.1",
"zod": "^3.22.3", "zod": "^3.22.3",

View file

@ -2293,8 +2293,7 @@
"ws": { "ws": {
"version": "8.13.0", "version": "8.13.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA=="
"requires": {}
}, },
"xml2js": { "xml2js": {
"version": "0.4.23", "version": "0.4.23",

1
settings.js.template Normal file
View file

@ -0,0 +1 @@
"use strict";window.SERVER_URL="${SERVER_URL}";window.SERVER_NAME="${SERVER_NAME}";window.SERVER_TYPE="${SERVER_TYPE}";window.SERVER_LOCK=${SERVER_LOCK};

View file

@ -38,6 +38,7 @@
"channel_other": "channels", "channel_other": "channels",
"clear": "clear", "clear": "clear",
"close": "close", "close": "close",
"codec": "codec",
"collapse": "collapse", "collapse": "collapse",
"comingSoon": "coming soon…", "comingSoon": "coming soon…",
"configure": "configure", "configure": "configure",
@ -363,6 +364,7 @@
"playlists": "$t(entity.playlist_other)", "playlists": "$t(entity.playlist_other)",
"search": "$t(common.search)", "search": "$t(common.search)",
"settings": "$t(common.setting_other)", "settings": "$t(common.setting_other)",
"shared": "shared $t(entity.playlist_other)",
"tracks": "$t(entity.track_other)" "tracks": "$t(entity.track_other)"
}, },
"trackList": { "trackList": {
@ -567,6 +569,8 @@
"skipDuration_description": "sets the duration to skip when using the skip buttons on the player bar", "skipDuration_description": "sets the duration to skip when using the skip buttons on the player bar",
"skipPlaylistPage": "skip playlist page", "skipPlaylistPage": "skip playlist page",
"skipPlaylistPage_description": "when navigating to a playlist, go to the playlist song list page instead of the default page", "skipPlaylistPage_description": "when navigating to a playlist, go to the playlist song list page instead of the default page",
"startMinimized": "start minimized",
"startMinimized_description": "start the application in system tray",
"theme": "theme", "theme": "theme",
"theme_description": "sets the theme to use for the application", "theme_description": "sets the theme to use for the application",
"themeDark": "theme (dark)", "themeDark": "theme (dark)",
@ -592,6 +596,7 @@
"bitrate": "bitrate", "bitrate": "bitrate",
"bpm": "bpm", "bpm": "bpm",
"channels": "$t(common.channel_other)", "channels": "$t(common.channel_other)",
"codec": "$t(common.codec)",
"comment": "comment", "comment": "comment",
"dateAdded": "date added", "dateAdded": "date added",
"discNumber": "disc", "discNumber": "disc",
@ -626,6 +631,7 @@
"bitrate": "$t(common.bitrate)", "bitrate": "$t(common.bitrate)",
"bpm": "$t(common.bpm)", "bpm": "$t(common.bpm)",
"channels": "$t(common.channel_other)", "channels": "$t(common.channel_other)",
"codec": "$t(common.codec)",
"dateAdded": "date added", "dateAdded": "date added",
"discNumber": "disc number", "discNumber": "disc number",
"duration": "$t(common.duration)", "duration": "$t(common.duration)",

View file

@ -206,7 +206,7 @@ const createTray = () => {
tray.setContextMenu(contextMenu); tray.setContextMenu(contextMenu);
}; };
const createWindow = async () => { const createWindow = async (first = true) => {
if (isDevelopment) { if (isDevelopment) {
await installExtensions(); await installExtensions();
} }
@ -350,13 +350,14 @@ const createWindow = async () => {
mainWindow.loadURL(resolveHtmlPath('index.html')); mainWindow.loadURL(resolveHtmlPath('index.html'));
const startWindowMinimized = store.get('window_start_minimized', false) as boolean;
mainWindow.on('ready-to-show', () => { mainWindow.on('ready-to-show', () => {
if (!mainWindow) { if (!mainWindow) {
throw new Error('"mainWindow" is not defined'); throw new Error('"mainWindow" is not defined');
} }
if (process.env.START_MINIMIZED) {
mainWindow.minimize(); if (!first || !startWindowMinimized) {
} else {
mainWindow.show(); mainWindow.show();
createWinThumbarButtons(); createWinThumbarButtons();
} }
@ -608,7 +609,11 @@ if (!singleInstance) {
app.on('activate', () => { app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the // On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open. // dock icon is clicked and there are no other windows open.
if (mainWindow === null) createWindow(); if (mainWindow === null) createWindow(false);
else if (!mainWindow.isVisible()) {
mainWindow.show();
createWinThumbarButtons();
}
}); });
}) })
.catch(console.log); .catch(console.log);

View file

@ -1,6 +1,6 @@
import { IpcRendererEvent, ipcRenderer, webFrame } from 'electron'; import { IpcRendererEvent, ipcRenderer, webFrame } from 'electron';
import Store from 'electron-store'; import Store from 'electron-store';
import type { TitleTheme } from '/@/renderer/types'; import { toServerType, type TitleTheme } from '/@/renderer/types';
const store = new Store(); const store = new Store();
@ -56,9 +56,20 @@ const themeSet = (theme: TitleTheme): void => {
ipcRenderer.send('theme-set', theme); ipcRenderer.send('theme-set', theme);
}; };
const SERVER_TYPE = toServerType(process.env.SERVER_TYPE);
const env = {
SERVER_LOCK:
SERVER_TYPE !== null ? process.env.SERVER_LOCK?.toLocaleLowerCase() === 'true' : false,
SERVER_NAME: process.env.SERVER_NAME ?? '',
SERVER_TYPE,
SERVER_URL: process.env.SERVER_URL ?? 'http://',
};
export const localSettings = { export const localSettings = {
disableMediaKeys, disableMediaKeys,
enableMediaKeys, enableMediaKeys,
env,
fontError, fontError,
get, get,
passwordGet, passwordGet,

View file

@ -54,8 +54,8 @@ import type {
StructuredLyric, StructuredLyric,
SimilarSongsArgs, SimilarSongsArgs,
Song, Song,
ServerType,
} from '/@/renderer/api/types'; } from '/@/renderer/api/types';
import { ServerType } from '/@/renderer/types';
import { DeletePlaylistResponse, RandomSongListArgs } from './types'; import { DeletePlaylistResponse, RandomSongListArgs } from './types';
import { ndController } from '/@/renderer/api/navidrome/navidrome-controller'; import { ndController } from '/@/renderer/api/navidrome/navidrome-controller';
import { ssController } from '/@/renderer/api/subsonic/subsonic-controller'; import { ssController } from '/@/renderer/api/subsonic/subsonic-controller';
@ -177,7 +177,7 @@ const endpoints: ApiController = {
getPlaylistList: ndController.getPlaylistList, getPlaylistList: ndController.getPlaylistList,
getPlaylistSongList: ndController.getPlaylistSongList, getPlaylistSongList: ndController.getPlaylistSongList,
getRandomSongList: ssController.getRandomSongList, getRandomSongList: ssController.getRandomSongList,
getServerInfo: ssController.getServerInfo, getServerInfo: ndController.getServerInfo,
getSimilarSongs: ssController.getSimilarSongs, getSimilarSongs: ssController.getSimilarSongs,
getSongDetail: ndController.getSongDetail, getSongDetail: ndController.getSongDetail,
getSongList: ndController.getSongList, getSongList: ndController.getSongList,

View file

@ -0,0 +1,6 @@
export enum ServerFeature {
SMART_PLAYLISTS = 'smartPlaylists',
SONG_LYRICS = 'songLyrics',
}
export type ServerFeatures = Record<Partial<ServerFeature>, boolean>;

View file

@ -3,7 +3,7 @@ import { jfType } from '/@/renderer/api/jellyfin/jellyfin-types';
import { initClient, initContract } from '@ts-rest/core'; import { initClient, initContract } from '@ts-rest/core';
import axios, { AxiosError, AxiosResponse, isAxiosError, Method } from 'axios'; import axios, { AxiosError, AxiosResponse, isAxiosError, Method } from 'axios';
import qs from 'qs'; import qs from 'qs';
import { ServerListItem } from '/@/renderer/types'; import { ServerListItem } from '/@/renderer/api/types';
import omitBy from 'lodash/omitBy'; import omitBy from 'lodash/omitBy';
import { z } from 'zod'; import { z } from 'zod';
import { authenticationFailure } from '/@/renderer/api/utils'; import { authenticationFailure } from '/@/renderer/api/utils';

View file

@ -61,6 +61,7 @@ import packageJson from '../../../../package.json';
import { z } from 'zod'; import { z } from 'zod';
import { JFSongListSort, JFSortOrder } from '/@/renderer/api/jellyfin.types'; import { JFSongListSort, JFSortOrder } from '/@/renderer/api/jellyfin.types';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { ServerFeatures } from '/@/renderer/api/features.types';
const formatCommaDelimitedString = (value: string[]) => { const formatCommaDelimitedString = (value: string[]) => {
return value.join(','); return value.join(',');
@ -959,7 +960,16 @@ const getServerInfo = async (args: ServerInfoArgs): Promise<ServerInfo> => {
throw new Error('Failed to get server info'); throw new Error('Failed to get server info');
} }
return { id: apiClientProps.server?.id, version: res.body.Version }; const features: ServerFeatures = {
smartPlaylists: false,
songLyrics: true,
};
return {
features,
id: apiClientProps.server?.id,
version: res.body.Version,
};
}; };
const getSimilarSongs = async (args: SimilarSongsArgs): Promise<Song[]> => { const getSimilarSongs = async (args: SimilarSongsArgs): Promise<Song[]> => {

View file

@ -10,8 +10,9 @@ import {
Playlist, Playlist,
MusicFolder, MusicFolder,
Genre, Genre,
ServerListItem,
ServerType,
} from '/@/renderer/api/types'; } from '/@/renderer/api/types';
import { ServerListItem, ServerType } from '/@/renderer/types';
const getStreamUrl = (args: { const getStreamUrl = (args: {
container?: string; container?: string;

View file

@ -675,6 +675,10 @@ const similarSongs = pagination.extend({
Items: z.array(song), Items: z.array(song),
}); });
export enum JellyfinExtensions {
SONG_LYRICS = 'songLyrics',
}
export const jfType = { export const jfType = {
_enum: { _enum: {
albumArtistList: albumArtistListSort, albumArtistList: albumArtistListSort,

View file

@ -7,7 +7,7 @@ import qs from 'qs';
import { ndType } from './navidrome-types'; import { ndType } from './navidrome-types';
import { authenticationFailure, resultWithHeaders } from '/@/renderer/api/utils'; import { authenticationFailure, resultWithHeaders } from '/@/renderer/api/utils';
import { useAuthStore } from '/@/renderer/store'; import { useAuthStore } from '/@/renderer/store';
import { ServerListItem } from '/@/renderer/types'; import { ServerListItem } from '/@/renderer/api/types';
import { toast } from '/@/renderer/components'; import { toast } from '/@/renderer/components';
import i18n from '/@/i18n/i18n'; import i18n from '/@/i18n/i18n';

View file

@ -1,3 +1,9 @@
import { ndApiClient } from '/@/renderer/api/navidrome/navidrome-api';
import { ndNormalize } from '/@/renderer/api/navidrome/navidrome-normalize';
import { NavidromeExtensions, ndType } from '/@/renderer/api/navidrome/navidrome-types';
import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
import semverCoerce from 'semver/functions/coerce';
import semverGte from 'semver/functions/gte';
import { import {
AlbumArtistDetailArgs, AlbumArtistDetailArgs,
AlbumArtistDetailResponse, AlbumArtistDetailResponse,
@ -39,11 +45,11 @@ import {
RemoveFromPlaylistResponse, RemoveFromPlaylistResponse,
RemoveFromPlaylistArgs, RemoveFromPlaylistArgs,
genreListSortMap, genreListSortMap,
ServerInfo,
ServerInfoArgs,
} from '../types'; } from '../types';
import { ndApiClient } from '/@/renderer/api/navidrome/navidrome-api'; import { hasFeature } from '/@/renderer/api/utils';
import { ndNormalize } from '/@/renderer/api/navidrome/navidrome-normalize'; import { ServerFeature, ServerFeatures } from '/@/renderer/api/features.types';
import { ndType } from '/@/renderer/api/navidrome/navidrome-types';
import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
const authenticate = async ( const authenticate = async (
url: string, url: string,
@ -355,6 +361,16 @@ const deletePlaylist = async (args: DeletePlaylistArgs): Promise<DeletePlaylistR
const getPlaylistList = async (args: PlaylistListArgs): Promise<PlaylistListResponse> => { const getPlaylistList = async (args: PlaylistListArgs): Promise<PlaylistListResponse> => {
const { query, apiClientProps } = args; const { query, apiClientProps } = args;
const customQuery = query._custom?.navidrome;
// Smart playlists only became available in 0.48.0. Do not filter for previous versions
if (
customQuery &&
customQuery.smart !== undefined &&
!hasFeature(apiClientProps.server, ServerFeature.SMART_PLAYLISTS)
) {
customQuery.smart = undefined;
}
const res = await ndApiClient(apiClientProps).getPlaylistList({ const res = await ndApiClient(apiClientProps).getPlaylistList({
query: { query: {
@ -363,7 +379,7 @@ const getPlaylistList = async (args: PlaylistListArgs): Promise<PlaylistListResp
_sort: query.sortBy ? playlistListSortMap.navidrome[query.sortBy] : undefined, _sort: query.sortBy ? playlistListSortMap.navidrome[query.sortBy] : undefined,
_start: query.startIndex, _start: query.startIndex,
q: query.searchTerm, q: query.searchTerm,
...query._custom?.navidrome, ...customQuery,
}, },
}); });
@ -465,6 +481,70 @@ const removeFromPlaylist = async (
return null; return null;
}; };
const VERSION_INFO: Array<[string, Record<string, number[]>]> = [
['0.48.0', { [ServerFeature.SMART_PLAYLISTS]: [1] }],
];
const getFeatures = (version: string): Record<string, number[]> => {
const cleanVersion = semverCoerce(version);
const features: Record<string, number[]> = {};
let matched = cleanVersion === null;
for (const [version, supportedFeatures] of VERSION_INFO) {
if (!matched) {
matched = semverGte(cleanVersion!, version);
}
if (matched) {
for (const [feature, feat] of Object.entries(supportedFeatures)) {
if (feature in features) {
features[feature].push(...feat);
} else {
features[feature] = feat;
}
}
}
}
return features;
};
const getServerInfo = async (args: ServerInfoArgs): Promise<ServerInfo> => {
const { apiClientProps } = args;
// Navidrome will always populate serverVersion
const ping = await ssApiClient(apiClientProps).ping();
if (ping.status !== 200) {
throw new Error('Failed to ping server');
}
const navidromeFeatures: Record<string, number[]> = getFeatures(ping.body.serverVersion!);
if (ping.body.openSubsonic) {
const res = await ssApiClient(apiClientProps).getServerInfo();
if (res.status !== 200) {
throw new Error('Failed to get server extensions');
}
for (const extension of res.body.openSubsonicExtensions) {
navidromeFeatures[extension.name] = extension.versions;
}
}
const features: ServerFeatures = {
smartPlaylists: false,
songLyrics: true,
};
if (navidromeFeatures[NavidromeExtensions.SMART_PLAYLISTS]) {
features[ServerFeature.SMART_PLAYLISTS] = true;
}
return { features, id: apiClientProps.server?.id, version: ping.body.serverVersion! };
};
export const ndController = { export const ndController = {
addToPlaylist, addToPlaylist,
authenticate, authenticate,
@ -478,6 +558,7 @@ export const ndController = {
getPlaylistDetail, getPlaylistDetail,
getPlaylistList, getPlaylistList,
getPlaylistSongList, getPlaylistSongList,
getServerInfo,
getSongDetail, getSongDetail,
getSongList, getSongList,
getUserList, getUserList,

View file

@ -7,8 +7,9 @@ import {
User, User,
AlbumArtist, AlbumArtist,
Genre, Genre,
ServerListItem,
ServerType,
} from '/@/renderer/api/types'; } from '/@/renderer/api/types';
import { ServerListItem, ServerType } from '/@/renderer/types';
import z from 'zod'; import z from 'zod';
import { ndType } from './navidrome-types'; import { ndType } from './navidrome-types';
import { ssType } from '/@/renderer/api/subsonic/subsonic-types'; import { ssType } from '/@/renderer/api/subsonic/subsonic-types';

View file

@ -343,6 +343,10 @@ const removeFromPlaylistParameters = z.object({
id: z.array(z.string()), id: z.array(z.string()),
}); });
export enum NavidromeExtensions {
SMART_PLAYLISTS = 'smartPlaylists',
}
export const ndType = { export const ndType = {
_enum: { _enum: {
albumArtistList: ndAlbumArtistListSort, albumArtistList: ndAlbumArtistListSort,

View file

@ -2,7 +2,7 @@ import md5 from 'md5';
import { z } from 'zod'; import { z } from 'zod';
import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api'; import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
import { ssNormalize } from '/@/renderer/api/subsonic/subsonic-normalize'; import { ssNormalize } from '/@/renderer/api/subsonic/subsonic-normalize';
import { ssType } from '/@/renderer/api/subsonic/subsonic-types'; import { SubsonicExtensions, ssType } from '/@/renderer/api/subsonic/subsonic-types';
import { import {
ArtistInfoArgs, ArtistInfoArgs,
AuthenticationResponse, AuthenticationResponse,
@ -29,6 +29,7 @@ import {
Song, Song,
} from '/@/renderer/api/types'; } from '/@/renderer/api/types';
import { randomString } from '/@/renderer/utils'; import { randomString } from '/@/renderer/utils';
import { ServerFeatures } from '/@/renderer/api/features.types';
const authenticate = async ( const authenticate = async (
url: string, url: string,
@ -383,8 +384,13 @@ const getServerInfo = async (args: ServerInfoArgs): Promise<ServerInfo> => {
throw new Error('Failed to ping server'); throw new Error('Failed to ping server');
} }
const features: ServerFeatures = {
smartPlaylists: false,
songLyrics: false,
};
if (!ping.body.openSubsonic || !ping.body.serverVersion) { if (!ping.body.openSubsonic || !ping.body.serverVersion) {
return { version: ping.body.version }; return { features, version: ping.body.version };
} }
const res = await ssApiClient(apiClientProps).getServerInfo(); const res = await ssApiClient(apiClientProps).getServerInfo();
@ -393,9 +399,13 @@ const getServerInfo = async (args: ServerInfoArgs): Promise<ServerInfo> => {
throw new Error('Failed to get server extensions'); throw new Error('Failed to get server extensions');
} }
const features: Record<string, number[]> = {}; const subsonicFeatures: Record<string, number[]> = {};
for (const extension of res.body.openSubsonicExtensions) { for (const extension of res.body.openSubsonicExtensions) {
features[extension.name] = extension.versions; subsonicFeatures[extension.name] = extension.versions;
}
if (subsonicFeatures[SubsonicExtensions.SONG_LYRICS]) {
features.songLyrics = true;
} }
return { features, id: apiClientProps.server?.id, version: ping.body.serverVersion }; return { features, id: apiClientProps.server?.id, version: ping.body.serverVersion };

View file

@ -1,8 +1,14 @@
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { z } from 'zod'; import { z } from 'zod';
import { ssType } from '/@/renderer/api/subsonic/subsonic-types'; import { ssType } from '/@/renderer/api/subsonic/subsonic-types';
import { QueueSong, LibraryItem, AlbumArtist, Album } from '/@/renderer/api/types'; import {
import { ServerListItem, ServerType } from '/@/renderer/types'; QueueSong,
LibraryItem,
AlbumArtist,
Album,
ServerListItem,
ServerType,
} from '/@/renderer/api/types';
const getCoverArtUrl = (args: { const getCoverArtUrl = (args: {
baseUrl: string | undefined; baseUrl: string | undefined;

View file

@ -260,6 +260,13 @@ const similarSongs = z.object({
.optional(), .optional(),
}); });
export enum SubsonicExtensions {
FORM_POST = 'formPost',
SONG_LYRICS = 'songLyrics',
TRANSCODE_OFFSET = 'transcodeOffset',
}
export const ssType = { export const ssType = {
_parameters: { _parameters: {
albumList: albumListParameters, albumList: albumListParameters,

View file

@ -20,6 +20,7 @@ import {
NDUserListSort, NDUserListSort,
NDGenreListSort, NDGenreListSort,
} from './navidrome.types'; } from './navidrome.types';
import { ServerFeatures } from '/@/renderer/api/features.types';
export enum LibraryItem { export enum LibraryItem {
ALBUM = 'album', ALBUM = 'album',
@ -57,13 +58,16 @@ export type User = {
export type ServerListItem = { export type ServerListItem = {
credential: string; credential: string;
features?: ServerFeatures;
id: string; id: string;
name: string; name: string;
ndCredential?: string; ndCredential?: string;
savePassword?: boolean;
type: ServerType; type: ServerType;
url: string; url: string;
userId: string | null; userId: string | null;
username: string; username: string;
version?: string;
}; };
export enum ServerType { export enum ServerType {
@ -1141,14 +1145,8 @@ export type FontData = {
export type ServerInfoArgs = BaseEndpointArgs; export type ServerInfoArgs = BaseEndpointArgs;
export enum SubsonicExtensions {
FORM_POST = 'formPost',
SONG_LYRICS = 'songLyrics',
TRANSCODE_OFFSET = 'transcodeOffset',
}
export type ServerInfo = { export type ServerInfo = {
features?: Record<string, number[]>; features: ServerFeatures;
id?: string; id?: string;
version: string; version: string;
}; };

View file

@ -2,7 +2,8 @@ import { AxiosHeaders } from 'axios';
import { z } from 'zod'; import { z } from 'zod';
import { toast } from '/@/renderer/components'; import { toast } from '/@/renderer/components';
import { useAuthStore } from '/@/renderer/store'; import { useAuthStore } from '/@/renderer/store';
import { ServerListItem } from '/@/renderer/types'; import { ServerListItem } from '/@/renderer/api/types';
import { ServerFeature } from '/@/renderer/api/features.types';
// Since ts-rest client returns a strict response type, we need to add the headers to the body object // Since ts-rest client returns a strict response type, we need to add the headers to the body object
export const resultWithHeaders = <ItemType extends z.ZodTypeAny>(itemSchema: ItemType) => { export const resultWithHeaders = <ItemType extends z.ZodTypeAny>(itemSchema: ItemType) => {
@ -38,3 +39,11 @@ export const authenticationFailure = (currentServer: ServerListItem | null) => {
useAuthStore.getState().actions.setCurrentServer(null); useAuthStore.getState().actions.setCurrentServer(null);
} }
}; };
export const hasFeature = (server: ServerListItem | null, feature: ServerFeature): boolean => {
if (!server || !server.features) {
return false;
}
return server.features[feature];
};

View file

@ -4,6 +4,7 @@ import {
ReactNode, ReactNode,
useCallback, useCallback,
useEffect, useEffect,
useLayoutEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
@ -114,9 +115,11 @@ export const SwiperGridCarousel = ({
isLoading, isLoading,
uniqueId, uniqueId,
}: SwiperGridCarouselProps) => { }: SwiperGridCarouselProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const swiperRef = useRef<SwiperCore | any>(null); const swiperRef = useRef<SwiperCore | any>(null);
const playButtonBehavior = usePlayButtonBehavior(); const playButtonBehavior = usePlayButtonBehavior();
const handlePlayQueueAdd = usePlayQueueAdd(); const handlePlayQueueAdd = usePlayQueueAdd();
const [slideCount, setSlideCount] = useState(4);
useEffect(() => { useEffect(() => {
swiperRef.current?.slideTo(0, 0); swiperRef.current?.slideTo(0, 0);
@ -191,23 +194,24 @@ export const SwiperGridCarousel = ({
const handleNext = useCallback(() => { const handleNext = useCallback(() => {
const activeIndex = swiperRef?.current?.activeIndex || 0; const activeIndex = swiperRef?.current?.activeIndex || 0;
const slidesPerView = Math.round(Number(swiperProps?.slidesPerView || 4)); const slidesPerView = Math.round(Number(swiperProps?.slidesPerView || slideCount));
swiperRef?.current?.slideTo(activeIndex + slidesPerView); swiperRef?.current?.slideTo(activeIndex + slidesPerView);
}, [swiperProps?.slidesPerView]); }, [slideCount, swiperProps?.slidesPerView]);
const handlePrev = useCallback(() => { const handlePrev = useCallback(() => {
const activeIndex = swiperRef?.current?.activeIndex || 0; const activeIndex = swiperRef?.current?.activeIndex || 0;
const slidesPerView = Math.round(Number(swiperProps?.slidesPerView || 4)); const slidesPerView = Math.round(Number(swiperProps?.slidesPerView || slideCount));
swiperRef?.current?.slideTo(activeIndex - slidesPerView); swiperRef?.current?.slideTo(activeIndex - slidesPerView);
}, [swiperProps?.slidesPerView]); }, [slideCount, swiperProps?.slidesPerView]);
const handleOnSlideChange = useCallback((e: SwiperCore) => { const handleOnSlideChange = useCallback((e: SwiperCore) => {
const { slides, isEnd, isBeginning, params } = e; const { slides, isEnd, isBeginning, params } = e;
if (isEnd || isBeginning) return; if (isEnd || isBeginning) return;
const slideCount = (params.slidesPerView as number | undefined) || 4;
setPagination({ setPagination({
hasNextPage: (params?.slidesPerView || 4) < slides.length, hasNextPage: slideCount < slides.length,
hasPreviousPage: (params?.slidesPerView || 4) < slides.length, hasPreviousPage: slideCount < slides.length,
}); });
}, []); }, []);
@ -215,82 +219,106 @@ export const SwiperGridCarousel = ({
const { slides, isEnd, isBeginning, params } = e; const { slides, isEnd, isBeginning, params } = e;
if (isEnd || isBeginning) return; if (isEnd || isBeginning) return;
const slideCount = (params.slidesPerView as number | undefined) || 4;
setPagination({ setPagination({
hasNextPage: (params.slidesPerView || 4) < slides.length, hasNextPage: slideCount < slides.length,
hasPreviousPage: (params.slidesPerView || 4) < slides.length, hasPreviousPage: slideCount < slides.length,
}); });
}, []); }, []);
const handleOnReachEnd = useCallback((e: SwiperCore) => { const handleOnReachEnd = useCallback((e: SwiperCore) => {
const { slides, params } = e; const { slides, params } = e;
const slideCount = (params.slidesPerView as number | undefined) || 4;
setPagination({ setPagination({
hasNextPage: false, hasNextPage: false,
hasPreviousPage: (params.slidesPerView || 4) < slides.length, hasPreviousPage: slideCount < slides.length,
}); });
}, []); }, []);
const handleOnReachBeginning = useCallback((e: SwiperCore) => { const handleOnReachBeginning = useCallback((e: SwiperCore) => {
const { slides, params } = e; const { slides, params } = e;
const slideCount = (params.slidesPerView as number | undefined) || 4;
setPagination({ setPagination({
hasNextPage: (params.slidesPerView || 4) < slides.length, hasNextPage: slideCount < slides.length,
hasPreviousPage: false, hasPreviousPage: false,
}); });
}, []); }, []);
const handleOnResize = useCallback((e: SwiperCore) => { useLayoutEffect(() => {
if (!e) return; const handleResize = () => {
const { width } = e; // Use the container div ref and not swiper width, as this value is more accurate
const slidesPerView = getSlidesPerView(width); const width = containerRef.current?.clientWidth;
if (!e.params) return; const { activeIndex, params, slides } =
e.params.slidesPerView = slidesPerView; (swiperRef.current as SwiperCore | undefined) ?? {};
}, []);
const throttledOnResize = throttle(handleOnResize, 200); if (width) {
const slidesPerView = getSlidesPerView(width);
setSlideCount(slidesPerView);
}
if (activeIndex !== undefined && slides && params?.slidesPerView) {
const slideCount = (params.slidesPerView as number | undefined) || 4;
setPagination({
hasNextPage: activeIndex + slideCount < slides.length,
hasPreviousPage: activeIndex > 0,
});
}
};
handleResize();
const throttledResize = throttle(handleResize, 200);
window.addEventListener('resize', throttledResize);
return () => {
window.removeEventListener('resize', throttledResize);
};
}, []);
return ( return (
<CarouselContainer <CarouselContainer
className="grid-carousel" className="grid-carousel"
spacing="md" spacing="md"
> >
{title ? ( <div ref={containerRef}>
<Title {title ? (
{...title} <Title
handleNext={handleNext} {...title}
handlePrev={handlePrev} handleNext={handleNext}
pagination={pagination} handlePrev={handlePrev}
/> pagination={pagination}
) : null} />
<Swiper ) : null}
ref={swiperRef} <Swiper
resizeObserver ref={swiperRef}
modules={[Virtual]} resizeObserver
slidesPerView={4} modules={[Virtual]}
spaceBetween={20} slidesPerView={slideCount}
style={{ height: '100%', width: '100%' }} spaceBetween={20}
onBeforeInit={(swiper) => { style={{ height: '100%', width: '100%' }}
swiperRef.current = swiper; onBeforeInit={(swiper) => {
}} swiperRef.current = swiper;
onBeforeResize={handleOnResize} }}
onReachBeginning={handleOnReachBeginning} onReachBeginning={handleOnReachBeginning}
onReachEnd={handleOnReachEnd} onReachEnd={handleOnReachEnd}
onResize={throttledOnResize} onSlideChange={handleOnSlideChange}
onSlideChange={handleOnSlideChange} onZoomChange={handleOnZoomChange}
onZoomChange={handleOnZoomChange} {...swiperProps}
{...swiperProps} >
> {slides.map((slideContent, index) => {
{slides.map((slideContent, index) => { return (
return ( <SwiperSlide
<SwiperSlide key={`${uniqueId}-${slideContent?.props?.data?.id}-${index}`}
key={`${uniqueId}-${slideContent?.props?.data?.id}-${index}`} virtualIndex={index}
virtualIndex={index} >
> {slideContent}
{slideContent} </SwiperSlide>
</SwiperSlide> );
); })}
})} </Swiper>
</Swiper> </div>
</CarouselContainer> </CarouselContainer>
); );
}; };

View file

@ -10,7 +10,7 @@ import styled from 'styled-components';
import type { AlbumArtist, Artist } from '/@/renderer/api/types'; import type { AlbumArtist, Artist } from '/@/renderer/api/types';
import { Text } from '/@/renderer/components/text'; import { Text } from '/@/renderer/components/text';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { ServerType } from '/@/renderer/types'; import { ServerType } from '/@/renderer/api/types';
import { Skeleton } from '/@/renderer/components/skeleton'; import { Skeleton } from '/@/renderer/components/skeleton';
const CellContainer = styled(motion.div)<{ height: number }>` const CellContainer = styled(motion.div)<{ height: number }>`

View file

@ -11,9 +11,9 @@ import {
LibraryItem, LibraryItem,
AnyLibraryItems, AnyLibraryItems,
RatingResponse, RatingResponse,
ServerType,
} from '/@/renderer/api/types'; } from '/@/renderer/api/types';
import { useSetAlbumListItemDataById, useSetQueueRating, getServerById } from '/@/renderer/store'; import { useSetAlbumListItemDataById, useSetQueueRating, getServerById } from '/@/renderer/store';
import { ServerType } from '/@/renderer/types';
export const useUpdateRating = () => { export const useUpdateRating = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();

View file

@ -16,12 +16,12 @@ import orderBy from 'lodash/orderBy';
import { generatePath, useNavigate } from 'react-router'; import { generatePath, useNavigate } from 'react-router';
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import { QueryPagination, queryKeys } from '/@/renderer/api/query-keys'; import { QueryPagination, queryKeys } from '/@/renderer/api/query-keys';
import { BasePaginatedResponse, LibraryItem } from '/@/renderer/api/types'; import { BasePaginatedResponse, LibraryItem, ServerListItem } from '/@/renderer/api/types';
import { getColumnDefs, VirtualTableProps } from '/@/renderer/components/virtual-table'; import { getColumnDefs, VirtualTableProps } from '/@/renderer/components/virtual-table';
import { SetContextMenuItems, useHandleTableContextMenu } from '/@/renderer/features/context-menu'; import { SetContextMenuItems, useHandleTableContextMenu } from '/@/renderer/features/context-menu';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { useListStoreActions } from '/@/renderer/store'; import { useListStoreActions } from '/@/renderer/store';
import { ListDisplayType, ServerListItem, TablePagination } from '/@/renderer/types'; import { ListDisplayType, TablePagination } from '/@/renderer/types';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { ListKey, useListStoreByKey } from '../../../store/list.store'; import { ListKey, useListStoreByKey } from '../../../store/list.store';

View file

@ -158,6 +158,14 @@ const tableColumns: { [key: string]: ColDef } = {
params.data ? params.data.channels : undefined, params.data ? params.data.channels : undefined,
width: 100, width: 100,
}, },
codec: {
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
colId: TableColumn.CODEC,
headerName: i18n.t('table.column.codec'),
valueGetter: (params: ValueGetterParams) =>
params.data ? params.data.container : undefined,
width: 60,
},
comment: { comment: {
cellRenderer: NoteCell, cellRenderer: NoteCell,
colId: TableColumn.COMMENT, colId: TableColumn.COMMENT,

View file

@ -60,6 +60,10 @@ export const SONG_TABLE_COLUMNS = [
label: i18n.t('table.config.label.bitrate', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.bitrate', { postProcess: 'titleCase' }),
value: TableColumn.BIT_RATE, value: TableColumn.BIT_RATE,
}, },
{
label: i18n.t('table.config.label.codec', { postProcess: 'titleCase' }),
value: TableColumn.CODEC,
},
{ {
label: i18n.t('table.config.label.lastPlayed', { postProcess: 'titleCase' }), label: i18n.t('table.config.label.lastPlayed', { postProcess: 'titleCase' }),
value: TableColumn.LAST_PLAYED, value: TableColumn.LAST_PLAYED,

View file

@ -15,7 +15,7 @@ import {
RiSettings3Fill, RiSettings3Fill,
} from 'react-icons/ri'; } from 'react-icons/ri';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { AlbumListSort, LibraryItem, SortOrder } from '/@/renderer/api/types'; import { AlbumListSort, LibraryItem, ServerType, SortOrder } from '/@/renderer/api/types';
import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components'; import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { ALBUM_TABLE_COLUMNS } from '/@/renderer/components/virtual-table'; import { ALBUM_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
@ -31,7 +31,7 @@ import {
useListStoreActions, useListStoreActions,
useListStoreByKey, useListStoreByKey,
} from '/@/renderer/store'; } from '/@/renderer/store';
import { ListDisplayType, Play, ServerType, TableColumn } from '/@/renderer/types'; import { ListDisplayType, Play, TableColumn } from '/@/renderer/types';
import i18n from '/@/i18n/i18n'; import i18n from '/@/i18n/i18n';
const FILTERS = { const FILTERS = {

View file

@ -9,7 +9,7 @@ import { RiFolder2Line, RiMoreFill, RiRefreshLine, RiSettings3Fill } from 'react
import { useListContext } from '../../../context/list-context'; import { useListContext } from '../../../context/list-context';
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { AlbumArtistListSort, LibraryItem, SortOrder } from '/@/renderer/api/types'; import { AlbumArtistListSort, LibraryItem, ServerType, SortOrder } from '/@/renderer/api/types';
import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components'; import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { ALBUMARTIST_TABLE_COLUMNS } from '/@/renderer/components/virtual-table'; import { ALBUMARTIST_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
@ -21,7 +21,7 @@ import {
useListStoreActions, useListStoreActions,
useListStoreByKey, useListStoreByKey,
} from '/@/renderer/store'; } from '/@/renderer/store';
import { ListDisplayType, ServerType, TableColumn } from '/@/renderer/types'; import { ListDisplayType, TableColumn } from '/@/renderer/types';
import i18n from '/@/i18n/i18n'; import i18n from '/@/i18n/i18n';
const FILTERS = { const FILTERS = {

View file

@ -522,7 +522,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
const handleUpdateRating = useCallback( const handleUpdateRating = useCallback(
(rating: number) => { (rating: number) => {
if (!ctx.dataNodes || !ctx.data) return; if (!ctx.dataNodes && !ctx.data) return;
let uniqueServerIds: string[] = []; let uniqueServerIds: string[] = [];
let items: AnyLibraryItems = []; let items: AnyLibraryItems = [];

View file

@ -8,7 +8,8 @@ import {
usePlayerStore, usePlayerStore,
} from '/@/renderer/store'; } from '/@/renderer/store';
import { SetActivity } from '@xhayper/discord-rpc'; import { SetActivity } from '@xhayper/discord-rpc';
import { PlayerStatus, ServerType } from '/@/renderer/types'; import { PlayerStatus } from '/@/renderer/types';
import { ServerType } from '/@/renderer/api/types';
const discordRpc = isElectron() ? window.electron.discordRpc : null; const discordRpc = isElectron() ? window.electron.discordRpc : null;
@ -40,7 +41,7 @@ export const useDiscordRpc = () => {
largeImageText: currentSong?.album || 'Unknown album', largeImageText: currentSong?.album || 'Unknown album',
smallImageKey: undefined, smallImageKey: undefined,
smallImageText: currentStatus, smallImageText: currentStatus,
state: artists && `By ${artists}` || "Unknown artist", state: (artists && `By ${artists}`) || 'Unknown artist',
}; };
if (currentStatus === PlayerStatus.PLAYING) { if (currentStatus === PlayerStatus.PLAYING) {

View file

@ -5,7 +5,7 @@ import { useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { RiFolder2Fill, RiMoreFill, RiRefreshLine, RiSettings3Fill } from 'react-icons/ri'; import { RiFolder2Fill, RiMoreFill, RiRefreshLine, RiSettings3Fill } from 'react-icons/ri';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { GenreListSort, LibraryItem, SortOrder } from '/@/renderer/api/types'; import { GenreListSort, LibraryItem, ServerType, SortOrder } from '/@/renderer/api/types';
import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components'; import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { GENRE_TABLE_COLUMNS } from '/@/renderer/components/virtual-table'; import { GENRE_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
@ -19,7 +19,7 @@ import {
useListStoreActions, useListStoreActions,
useListStoreByKey, useListStoreByKey,
} from '/@/renderer/store'; } from '/@/renderer/store';
import { ListDisplayType, ServerType, TableColumn } from '/@/renderer/types'; import { ListDisplayType, TableColumn } from '/@/renderer/types';
import i18n from '/@/i18n/i18n'; import i18n from '/@/i18n/i18n';
const FILTERS = { const FILTERS = {

View file

@ -6,15 +6,16 @@ import {
InternetProviderLyricResponse, InternetProviderLyricResponse,
FullLyricsMetadata, FullLyricsMetadata,
LyricGetQuery, LyricGetQuery,
SubsonicExtensions,
StructuredLyric, StructuredLyric,
ServerType,
} from '/@/renderer/api/types'; } from '/@/renderer/api/types';
import { QueryHookArgs } from '/@/renderer/lib/react-query'; import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById, useLyricsSettings } from '/@/renderer/store'; import { getServerById, useLyricsSettings } from '/@/renderer/store';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { ServerType } from '/@/renderer/types';
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { hasFeature } from '/@/renderer/api/utils';
import { ServerFeature } from '/@/renderer/api/features.types';
const lyricsIpc = isElectron() ? window.electron.lyrics : null; const lyricsIpc = isElectron() ? window.electron.lyrics : null;
@ -112,7 +113,7 @@ export const useSongLyricsBySong = (
source: server?.name ?? 'music server', source: server?.name ?? 'music server',
}; };
} }
} else if (server.features && SubsonicExtensions.SONG_LYRICS in server.features) { } else if (hasFeature(server, ServerFeature.SONG_LYRICS)) {
const subsonicLyrics = await api.controller const subsonicLyrics = await api.controller
.getStructuredLyrics({ .getStructuredLyrics({
apiClientProps: { server, signal }, apiClientProps: { server, signal },

View file

@ -134,7 +134,7 @@ export const FullScreenPlayerImage = () => {
const albumArtRes = useSettingsStore((store) => store.general.albumArtRes); const albumArtRes = useSettingsStore((store) => store.general.albumArtRes);
const { queue } = usePlayerData(); const { queue } = usePlayerData();
const { opacity, useImageAspectRatio } = useFullScreenPlayerStore(); const { useImageAspectRatio } = useFullScreenPlayerStore();
const currentSong = queue.current; const currentSong = queue.current;
const { color: background } = useFastAverageColor({ const { color: background } = useFastAverageColor({
algorithm: 'dominant', algorithm: 'dominant',
@ -250,7 +250,6 @@ export const FullScreenPlayerImage = () => {
<MetadataContainer <MetadataContainer
className="full-screen-player-image-metadata" className="full-screen-player-image-metadata"
maw="100%" maw="100%"
opacity={opacity}
spacing="xs" spacing="xs"
> >
<TextTitle <TextTitle
@ -278,7 +277,6 @@ export const FullScreenPlayerImage = () => {
to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
albumId: currentSong?.albumId || '', albumId: currentSong?.albumId || '',
})} })}
transform="uppercase"
w="100%" w="100%"
weight={600} weight={600}
> >
@ -292,7 +290,6 @@ export const FullScreenPlayerImage = () => {
style={{ style={{
textShadow: 'var(--fullscreen-player-text-shadow)', textShadow: 'var(--fullscreen-player-text-shadow)',
}} }}
transform="uppercase"
> >
{index > 0 && ( {index > 0 && (
<Text <Text
@ -313,7 +310,6 @@ export const FullScreenPlayerImage = () => {
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, { to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
albumArtistId: artist.id, albumArtistId: artist.id,
})} })}
transform="uppercase"
weight={600} weight={600}
> >
{artist.name} {artist.name}

View file

@ -43,11 +43,11 @@ const HeaderItemWrapper = styled.div`
z-index: 2; z-index: 2;
`; `;
interface TransparendGridContainerProps { interface TransparentGridContainerProps {
opacity: number; opacity: number;
} }
const GridContainer = styled.div<TransparendGridContainerProps>` const GridContainer = styled.div<TransparentGridContainerProps>`
display: grid; display: grid;
grid-template-rows: auto minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr);
grid-template-columns: 1fr; grid-template-columns: 1fr;

View file

@ -153,7 +153,7 @@ const Controls = () => {
defaultValue={opacity} defaultValue={opacity}
label={(e) => `${e} %`} label={(e) => `${e} %`}
max={100} max={100}
min={1} min={0}
w="100%" w="100%"
onChangeEnd={(e) => setStore({ opacity: Number(e) })} onChangeEnd={(e) => setStore({ opacity: Number(e) })}
/> />

View file

@ -15,11 +15,12 @@ import {
ServerType, ServerType,
GenreListSort, GenreListSort,
SortOrder, SortOrder,
ServerListItem,
} from '/@/renderer/api/types'; } from '/@/renderer/api/types';
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import { useAuthStore } from '/@/renderer/store'; import { useAuthStore } from '/@/renderer/store';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { Play, PlayQueueAddOptions, ServerListItem } from '/@/renderer/types'; import { Play, PlayQueueAddOptions } from '/@/renderer/types';
import i18n from '/@/i18n/i18n'; import i18n from '/@/i18n/i18n';
interface ShuffleAllSlice extends RandomSongListQuery { interface ShuffleAllSlice extends RandomSongListQuery {

View file

@ -8,8 +8,9 @@ import {
SongListResponse, SongListResponse,
SongListSort, SongListSort,
SortOrder, SortOrder,
ServerListItem,
ServerType,
} from '/@/renderer/api/types'; } from '/@/renderer/api/types';
import { ServerListItem, ServerType } from '/@/renderer/types';
export const getPlaylistSongsById = async (args: { export const getPlaylistSongsById = async (args: {
id: string; id: string;

View file

@ -11,6 +11,8 @@ import { useCreatePlaylist } from '/@/renderer/features/playlists/mutations/crea
import { convertQueryGroupToNDQuery } from '/@/renderer/features/playlists/utils'; import { convertQueryGroupToNDQuery } from '/@/renderer/features/playlists/utils';
import { useCurrentServer } from '/@/renderer/store'; import { useCurrentServer } from '/@/renderer/store';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { hasFeature } from '/@/renderer/api/utils';
import { ServerFeature } from '/@/renderer/api/features.types';
interface CreatePlaylistFormProps { interface CreatePlaylistFormProps {
onCancel: () => void; onCancel: () => void;
@ -120,12 +122,13 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
})} })}
/> />
)} )}
{server?.type === ServerType.NAVIDROME && ( {server?.type === ServerType.NAVIDROME &&
<Switch hasFeature(server, ServerFeature.SMART_PLAYLISTS) && (
label="Is smart playlist?" <Switch
onChange={(e) => setIsSmartPlaylist(e.currentTarget.checked)} label="Is smart playlist?"
/> onChange={(e) => setIsSmartPlaylist(e.currentTarget.checked)}
)} />
)}
</Group> </Group>
{server?.type === ServerType.NAVIDROME && isSmartPlaylist && ( {server?.type === ServerType.NAVIDROME && isSmartPlaylist && (
<Stack pt="1rem"> <Stack pt="1rem">

View file

@ -17,7 +17,13 @@ import {
} from 'react-icons/ri'; } from 'react-icons/ri';
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { LibraryItem, PlaylistSongListQuery, SongListSort, SortOrder } from '/@/renderer/api/types'; import {
LibraryItem,
PlaylistSongListQuery,
ServerType,
SongListSort,
SortOrder,
} from '/@/renderer/api/types';
import { import {
DropdownMenu, DropdownMenu,
Button, Button,
@ -39,7 +45,7 @@ import {
useSetPlaylistStore, useSetPlaylistStore,
useSetPlaylistTablePagination, useSetPlaylistTablePagination,
} from '/@/renderer/store'; } from '/@/renderer/store';
import { ListDisplayType, ServerType, Play, TableColumn } from '/@/renderer/types'; import { ListDisplayType, Play, TableColumn } from '/@/renderer/types';
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query'; import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
import { useParams, useNavigate } from 'react-router'; import { useParams, useNavigate } from 'react-router';
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table'; import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';

View file

@ -9,11 +9,11 @@ import { PlaylistListHeaderFilters } from '/@/renderer/features/playlists/compon
import { LibraryHeaderBar } from '/@/renderer/features/shared'; import { LibraryHeaderBar } from '/@/renderer/features/shared';
import { useContainerQuery } from '/@/renderer/hooks'; import { useContainerQuery } from '/@/renderer/hooks';
import { PlaylistListFilter, useCurrentServer, useListStoreActions } from '/@/renderer/store'; import { PlaylistListFilter, useCurrentServer, useListStoreActions } from '/@/renderer/store';
import { ListDisplayType, ServerType } from '/@/renderer/types'; import { ListDisplayType } from '/@/renderer/types';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { RiFileAddFill } from 'react-icons/ri'; import { RiFileAddFill } from 'react-icons/ri';
import { LibraryItem } from '/@/renderer/api/types'; import { LibraryItem, ServerType } from '/@/renderer/api/types';
import { useListFilterRefresh } from '../../../hooks/use-list-filter-refresh'; import { useListFilterRefresh } from '../../../hooks/use-list-filter-refresh';
import { useListContext } from '/@/renderer/context/list-context'; import { useListContext } from '/@/renderer/context/list-context';
import { useListStoreByKey } from '../../../store/list.store'; import { useListStoreByKey } from '../../../store/list.store';

View file

@ -4,12 +4,11 @@ import { nanoid } from 'nanoid/non-secure';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { generatePath, useNavigate } from 'react-router'; import { generatePath, useNavigate } from 'react-router';
import { createSearchParams } from 'react-router-dom'; import { createSearchParams } from 'react-router-dom';
import { LibraryItem } from '/@/renderer/api/types'; import { LibraryItem, ServerType } from '/@/renderer/api/types';
import { CreatePlaylistForm } from '/@/renderer/features/playlists'; import { CreatePlaylistForm } from '/@/renderer/features/playlists';
import { Command, CommandPalettePages } from '/@/renderer/features/search/components/command'; import { Command, CommandPalettePages } from '/@/renderer/features/search/components/command';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer } from '/@/renderer/store'; import { useCurrentServer } from '/@/renderer/store';
import { ServerType } from '/@/renderer/types';
interface HomeCommandsProps { interface HomeCommandsProps {
handleClose: () => void; handleClose: () => void;

View file

@ -3,7 +3,7 @@ import { openModal } from '@mantine/modals';
import { Command, CommandPalettePages } from '/@/renderer/features/search/components/command'; import { Command, CommandPalettePages } from '/@/renderer/features/search/components/command';
import { ServerList } from '/@/renderer/features/servers'; import { ServerList } from '/@/renderer/features/servers';
import { useAuthStoreActions, useServerList } from '/@/renderer/store'; import { useAuthStoreActions, useServerList } from '/@/renderer/store';
import { ServerListItem } from '/@/renderer/types'; import { ServerListItem } from '/@/renderer/api/types';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';

View file

@ -6,9 +6,9 @@ import { useFocusTrap } from '@mantine/hooks';
import { closeAllModals } from '@mantine/modals'; import { closeAllModals } from '@mantine/modals';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { nanoid } from 'nanoid/non-secure'; import { nanoid } from 'nanoid/non-secure';
import { AuthenticationResponse } from '/@/renderer/api/types'; import { AuthenticationResponse, ServerType } from '/@/renderer/api/types';
import { useAuthStoreActions } from '/@/renderer/store'; import { useAuthStoreActions } from '/@/renderer/store';
import { ServerType } from '/@/renderer/types'; import { ServerType, toServerType } from '/@/renderer/types';
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -33,15 +33,27 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
const form = useForm({ const form = useForm({
initialValues: { initialValues: {
legacyAuth: false, legacyAuth: false,
name: '', name: (localSettings ? localSettings.env.SERVER_NAME : window.SERVER_NAME) ?? '',
password: '', password: '',
savePassword: false, savePassword: false,
type: ServerType.JELLYFIN, type:
url: 'http://', (localSettings
? localSettings.env.SERVER_TYPE
: toServerType(window.SERVER_TYPE)) ?? ServerType.JELLYFIN,
url: (localSettings ? localSettings.env.SERVER_URL : window.SERVER_URL) ?? 'https://',
username: '', username: '',
}, },
}); });
// server lock for web is only true if lock is true *and* all other properties are set
const serverLock =
(localSettings
? !!localSettings.env.SERVER_LOCK
: !!window.SERVER_LOCK &&
window.SERVER_TYPE &&
window.SERVER_NAME &&
window.SERVER_URL) || false;
const isSubmitDisabled = !form.values.name || !form.values.url || !form.values.username; const isSubmitDisabled = !form.values.name || !form.values.url || !form.values.username;
const handleSubmit = form.onSubmit(async (values) => { const handleSubmit = form.onSubmit(async (values) => {
@ -62,7 +74,7 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
password: values.password, password: values.password,
username: values.username, username: values.username,
}, },
values.type, values.type as ServerType,
); );
if (!data) { if (!data) {
@ -76,7 +88,7 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
id: nanoid(), id: nanoid(),
name: values.name, name: values.name,
ndCredential: data.ndCredential, ndCredential: data.ndCredential,
type: values.type, type: values.type as ServerType,
url: values.url.replace(/\/$/, ''), url: values.url.replace(/\/$/, ''),
userId: data.userId, userId: data.userId,
username: data.username, username: data.username,
@ -117,11 +129,13 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
> >
<SegmentedControl <SegmentedControl
data={SERVER_TYPES} data={SERVER_TYPES}
disabled={serverLock}
{...form.getInputProps('type')} {...form.getInputProps('type')}
/> />
<Group grow> <Group grow>
<TextInput <TextInput
data-autofocus data-autofocus
disabled={serverLock}
label={t('form.addServer.input', { label={t('form.addServer.input', {
context: 'name', context: 'name',
postProcess: 'titleCase', postProcess: 'titleCase',
@ -129,6 +143,7 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
{...form.getInputProps('name')} {...form.getInputProps('name')}
/> />
<TextInput <TextInput
disabled={serverLock}
label={t('form.addServer.input', { label={t('form.addServer.input', {
context: 'url', context: 'url',
postProcess: 'titleCase', postProcess: 'titleCase',

View file

@ -7,9 +7,8 @@ import { closeAllModals } from '@mantine/modals';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { RiInformationLine } from 'react-icons/ri'; import { RiInformationLine } from 'react-icons/ri';
import { AuthenticationResponse } from '/@/renderer/api/types'; import { AuthenticationResponse, ServerListItem, ServerType } from '/@/renderer/api/types';
import { useAuthStoreActions } from '/@/renderer/store'; import { useAuthStoreActions } from '/@/renderer/store';
import { ServerListItem, ServerType } from '/@/renderer/types';
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import i18n from '/@/i18n/i18n'; import i18n from '/@/i18n/i18n';
import { queryClient } from '/@/renderer/lib/react-query'; import { queryClient } from '/@/renderer/lib/react-query';

View file

@ -7,7 +7,7 @@ import { RiDeleteBin2Line, RiEdit2Fill } from 'react-icons/ri';
import { EditServerForm } from '/@/renderer/features/servers/components/edit-server-form'; import { EditServerForm } from '/@/renderer/features/servers/components/edit-server-form';
import { ServerSection } from '/@/renderer/features/servers/components/server-section'; import { ServerSection } from '/@/renderer/features/servers/components/server-section';
import { useAuthStoreActions } from '/@/renderer/store'; import { useAuthStoreActions } from '/@/renderer/store';
import { ServerListItem as ServerItem } from '/@/renderer/types'; import { ServerListItem as ServerItem } from '/@/renderer/api/types';
const localSettings = isElectron() ? window.electron.localSettings : null; const localSettings = isElectron() ? window.electron.localSettings : null;

View file

@ -131,6 +131,31 @@ export const WindowSettings = () => {
isHidden: !isElectron(), isHidden: !isElectron(),
title: t('setting.exitToTray', { postProcess: 'sentenceCase' }), title: t('setting.exitToTray', { postProcess: 'sentenceCase' }),
}, },
{
control: (
<Switch
aria-label="Toggle start in tray"
defaultChecked={settings.startMinimized}
disabled={!isElectron()}
onChange={(e) => {
if (!e) return;
localSettings?.set('window_start_minimized', e.currentTarget.checked);
setSettings({
window: {
...settings,
startMinimized: e.currentTarget.checked,
},
});
}}
/>
),
description: t('setting.startMinimized', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !isElectron(),
title: t('setting.startMinimized', { postProcess: 'sentenceCase' }),
},
]; ];
return <SettingsSection options={windowOptions} />; return <SettingsSection options={windowOptions} />;

View file

@ -38,6 +38,9 @@ export const SidebarIcon = ({ active, route, size }: SidebarIconProps) => {
case AppRoute.LIBRARY_ALBUMS: case AppRoute.LIBRARY_ALBUMS:
if (active) return <RiAlbumFill size={size} />; if (active) return <RiAlbumFill size={size} />;
return <RiAlbumLine size={size} />; return <RiAlbumLine size={size} />;
case AppRoute.LIBRARY_ALBUM_ARTISTS:
if (active) return <RiUserVoiceFill size={size} />;
return <RiUserVoiceLine size={size} />;
case AppRoute.LIBRARY_ARTISTS: case AppRoute.LIBRARY_ARTISTS:
if (active) return <RiUserVoiceFill size={size} />; if (active) return <RiUserVoiceFill size={size} />;
return <RiUserVoiceLine size={size} />; return <RiUserVoiceLine size={size} />;

View file

@ -1,11 +1,11 @@
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { Flex, Group } from '@mantine/core'; import { Box, Flex, Group } from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks'; import { useDebouncedValue } from '@mantine/hooks';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { RiAddBoxFill, RiAddCircleFill, RiPlayFill } from 'react-icons/ri'; import { RiAddBoxFill, RiAddCircleFill, RiPlayFill } from 'react-icons/ri';
import { generatePath } from 'react-router'; import { generatePath } from 'react-router';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { LibraryItem } from '/@/renderer/api/types'; import { LibraryItem, Playlist } from '/@/renderer/api/types';
import { Button, Text } from '/@/renderer/components'; import { Button, Text } from '/@/renderer/components';
import { usePlayQueueAdd } from '/@/renderer/features/player'; import { usePlayQueueAdd } from '/@/renderer/features/player';
import { usePlaylistList } from '/@/renderer/features/playlists'; import { usePlaylistList } from '/@/renderer/features/playlists';
@ -14,7 +14,7 @@ import { Play } from '/@/renderer/types';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import { FixedSizeList, ListChildComponentProps } from 'react-window'; import { FixedSizeList, ListChildComponentProps } from 'react-window';
import { useHideScrollbar } from '/@/renderer/hooks'; import { useHideScrollbar } from '/@/renderer/hooks';
import { useGeneralSettings } from '/@/renderer/store'; import { useCurrentServer, useGeneralSettings } from '/@/renderer/store';
interface SidebarPlaylistListProps { interface SidebarPlaylistListProps {
data: ReturnType<typeof usePlaylistList>['data']; data: ReturnType<typeof usePlaylistList>['data'];
@ -22,6 +22,20 @@ interface SidebarPlaylistListProps {
const PlaylistRow = ({ index, data, style }: ListChildComponentProps) => { const PlaylistRow = ({ index, data, style }: ListChildComponentProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
if (data?.items[index] === null) {
return (
<div style={{ margin: '0.5rem 0', padding: '0 1.5rem', ...style }}>
<Box
fw="600"
sx={{ fontSize: '1.2rem' }}
>
{t('page.sidebar.shared', { postProcess: 'titleCase' })}
</Box>
</div>
);
}
const path = data?.items[index].id const path = data?.items[index].id
? data.defaultFullPlaylist ? data.defaultFullPlaylist
? generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId: data.items[index].id }) ? generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId: data.items[index].id })
@ -125,6 +139,7 @@ export const SidebarPlaylistList = ({ data }: SidebarPlaylistListProps) => {
const { isScrollbarHidden, hideScrollbarElementProps } = useHideScrollbar(0); const { isScrollbarHidden, hideScrollbarElementProps } = useHideScrollbar(0);
const handlePlayQueueAdd = usePlayQueueAdd(); const handlePlayQueueAdd = usePlayQueueAdd();
const { defaultFullPlaylist } = useGeneralSettings(); const { defaultFullPlaylist } = useGeneralSettings();
const { type, username } = useCurrentServer() || {};
const [rect, setRect] = useState({ const [rect, setRect] = useState({
height: 0, height: 0,
@ -147,12 +162,30 @@ export const SidebarPlaylistList = ({ data }: SidebarPlaylistListProps) => {
); );
const memoizedItemData = useMemo(() => { const memoizedItemData = useMemo(() => {
return { const base = { defaultFullPlaylist, handlePlay: handlePlayPlaylist };
defaultFullPlaylist,
handlePlay: handlePlayPlaylist, if (!type || !username || !data?.items) {
items: data?.items, return { ...base, items: data?.items };
}; }
}, [data?.items, defaultFullPlaylist, handlePlayPlaylist]);
const owned: Array<Playlist | null> = [];
const shared: Playlist[] = [];
for (const playlist of data.items) {
if (playlist.owner !== username) {
shared.push(playlist);
} else {
owned.push(playlist);
}
}
if (shared.length > 0) {
// Use `null` as a separator between owned and shared playlists
owned.push(null);
}
return { ...base, items: owned.concat(shared) };
}, [data?.items, defaultFullPlaylist, handlePlayPlaylist, type, username]);
return ( return (
<Flex <Flex
@ -168,7 +201,7 @@ export const SidebarPlaylistList = ({ data }: SidebarPlaylistListProps) => {
: 'overlay-scrollbar' : 'overlay-scrollbar'
} }
height={debounced.height} height={debounced.height}
itemCount={data?.items?.length || 0} itemCount={memoizedItemData?.items?.length || 0}
itemData={memoizedItemData} itemData={memoizedItemData}
itemSize={25} itemSize={25}
overscanCount={20} overscanCount={20}

View file

@ -15,7 +15,7 @@ import {
} from 'react-icons/ri'; } from 'react-icons/ri';
import { useListStoreByKey } from '../../../store/list.store'; import { useListStoreByKey } from '../../../store/list.store';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { LibraryItem, SongListSort, SortOrder } from '/@/renderer/api/types'; import { LibraryItem, ServerType, SongListSort, SortOrder } from '/@/renderer/api/types';
import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components'; import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table'; import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
@ -27,7 +27,7 @@ import { useContainerQuery } from '/@/renderer/hooks';
import { useListFilterRefresh } from '/@/renderer/hooks/use-list-filter-refresh'; import { useListFilterRefresh } from '/@/renderer/hooks/use-list-filter-refresh';
import { queryClient } from '/@/renderer/lib/react-query'; import { queryClient } from '/@/renderer/lib/react-query';
import { SongListFilter, useCurrentServer, useListStoreActions } from '/@/renderer/store'; import { SongListFilter, useCurrentServer, useListStoreActions } from '/@/renderer/store';
import { ListDisplayType, Play, ServerType, TableColumn } from '/@/renderer/types'; import { ListDisplayType, Play, TableColumn } from '/@/renderer/types';
import i18n from '/@/i18n/i18n'; import i18n from '/@/i18n/i18n';
const FILTERS = { const FILTERS = {

View file

@ -29,7 +29,7 @@ import {
useSidebarStore, useSidebarStore,
useAppStoreActions, useAppStoreActions,
} from '/@/renderer/store'; } from '/@/renderer/store';
import { ServerListItem, ServerType } from '/@/renderer/types'; import { ServerListItem, ServerType } from '/@/renderer/api/types';
import packageJson from '../../../../../package.json'; import packageJson from '../../../../../package.json';
const browser = isElectron() ? window.electron.browser : null; const browser = isElectron() ? window.electron.browser : null;

View file

@ -0,0 +1,54 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCurrentServer } from '/@/renderer/store';
import { AuthState, ServerListItem, ServerType } from '/@/renderer/types';
import { api } from '/@/renderer/api';
import { SongListSort, SortOrder } from '/@/renderer/api/types';
import { debounce } from 'lodash';
export const useServerAuthenticated = () => {
const priorServerId = useRef<string>();
const server = useCurrentServer();
const [ready, setReady] = useState(
server?.type === ServerType.NAVIDROME ? AuthState.LOADING : AuthState.VALID,
);
const authenticateNavidrome = useCallback(async (server: ServerListItem) => {
// This trick works because navidrome-api.ts will internally check for authentication
// failures and try to log in again (where available). So, all that's necessary is
// making one request first
try {
await api.controller.getSongList({
apiClientProps: { server },
query: {
limit: 1,
sortBy: SongListSort.NAME,
sortOrder: SortOrder.ASC,
startIndex: 0,
},
});
setReady(AuthState.VALID);
} catch (error) {
setReady(AuthState.INVALID);
}
}, []);
const debouncedAuth = debounce((server: ServerListItem) => {
authenticateNavidrome(server).catch(console.error);
}, 300);
useEffect(() => {
if (priorServerId.current !== server?.id) {
priorServerId.current = server?.id || '';
if (server?.type === ServerType.NAVIDROME) {
setReady(AuthState.LOADING);
debouncedAuth(server);
} else {
setReady(AuthState.VALID);
}
}
}, [debouncedAuth, server]);
return ready;
};

View file

@ -3,6 +3,7 @@ import { useAuthStoreActions, useCurrentServer } from '/@/renderer/store';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { controller } from '/@/renderer/api/controller'; import { controller } from '/@/renderer/api/controller';
import isEqual from 'lodash/isEqual';
export const useServerVersion = () => { export const useServerVersion = () => {
const { updateServer } = useAuthStoreActions(); const { updateServer } = useAuthStoreActions();
@ -22,14 +23,18 @@ export const useServerVersion = () => {
}); });
useEffect(() => { useEffect(() => {
if (server && server.id === serverInfo.data?.id) { if (!server?.id) {
const { version, features } = serverInfo.data; return;
if (version !== server.version) { }
if (server?.id === serverInfo.data?.id) {
const { version, features } = serverInfo.data || {};
if (version !== server?.version || !isEqual(features, server?.features)) {
updateServer(server.id, { updateServer(server.id, {
features, features,
version, version,
}); });
} }
} }
}, [server, serverInfo.data, updateServer]); }, [serverInfo?.data, server?.features, server?.id, server?.version, updateServer]);
}; };

View file

@ -6,6 +6,9 @@
<meta http-equiv="Content-Security-Policy" /> <meta http-equiv="Content-Security-Policy" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Feishin</title> <title>Feishin</title>
<% if (web) { %>
<script src="settings.js"></script>
<% } %>
</head> </head>
<body> <body>

View file

@ -13,6 +13,10 @@ import { Browser } from '/@/main/preload/browser';
declare global { declare global {
interface Window { interface Window {
SERVER_LOCK?: boolean;
SERVER_NAME?: string;
SERVER_TYPE?: string;
SERVER_URL?: string;
electron: { electron: {
browser: Browser; browser: Browser;
discordRpc: DiscordRpc; discordRpc: DiscordRpc;

View file

@ -3,7 +3,9 @@ import isElectron from 'is-electron';
import { Navigate, Outlet } from 'react-router-dom'; import { Navigate, Outlet } from 'react-router-dom';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer, useSetPlayerFallback } from '/@/renderer/store'; import { useCurrentServer, useSetPlayerFallback } from '/@/renderer/store';
import { toast } from '/@/renderer/components'; import { Spinner, toast } from '/@/renderer/components';
import { useServerAuthenticated } from '/@/renderer/hooks/use-server-authenticated';
import { AuthState } from '/@/renderer/types';
const ipc = isElectron() ? window.electron.ipc : null; const ipc = isElectron() ? window.electron.ipc : null;
const utils = isElectron() ? window.electron.utils : null; const utils = isElectron() ? window.electron.utils : null;
@ -12,6 +14,7 @@ const mpvPlayerListener = isElectron() ? window.electron.mpvPlayerListener : nul
export const AppOutlet = () => { export const AppOutlet = () => {
const currentServer = useCurrentServer(); const currentServer = useCurrentServer();
const setFallback = useSetPlayerFallback(); const setFallback = useSetPlayerFallback();
const authState = useServerAuthenticated();
const isActionsRequired = useMemo(() => { const isActionsRequired = useMemo(() => {
const isServerRequired = !currentServer; const isServerRequired = !currentServer;
@ -37,7 +40,11 @@ export const AppOutlet = () => {
}; };
}, [setFallback]); }, [setFallback]);
if (isActionsRequired) { if (authState === AuthState.LOADING) {
return <Spinner container />;
}
if (isActionsRequired || authState === AuthState.INVALID) {
return ( return (
<Navigate <Navigate
replace replace

View file

@ -6,7 +6,7 @@ import { immer } from 'zustand/middleware/immer';
import { useAlbumArtistListDataStore } from '/@/renderer/store/album-artist-list-data.store'; import { useAlbumArtistListDataStore } from '/@/renderer/store/album-artist-list-data.store';
import { useAlbumListDataStore } from '/@/renderer/store/album-list-data.store'; import { useAlbumListDataStore } from '/@/renderer/store/album-list-data.store';
import { useListStore } from '/@/renderer/store/list.store'; import { useListStore } from '/@/renderer/store/list.store';
import { ServerListItem } from '/@/renderer/types'; import { ServerListItem } from '/@/renderer/api/types';
export interface AuthState { export interface AuthState {
currentServer: ServerListItem | null; currentServer: ServerListItem | null;

View file

@ -267,6 +267,7 @@ export interface SettingsState {
disableAutoUpdate: boolean; disableAutoUpdate: boolean;
exitToTray: boolean; exitToTray: boolean;
minimizeToTray: boolean; minimizeToTray: boolean;
startMinimized: boolean;
windowBarStyle: Platform; windowBarStyle: Platform;
}; };
} }
@ -575,6 +576,7 @@ const initialState: SettingsState = {
disableAutoUpdate: false, disableAutoUpdate: false,
exitToTray: false, exitToTray: false,
minimizeToTray: false, minimizeToTray: false,
startMinimized: false,
windowBarStyle: platformDefaultWindowBarStyle, windowBarStyle: platformDefaultWindowBarStyle,
}, },
}; };

View file

@ -60,6 +60,17 @@ export enum ServerType {
SUBSONIC = 'subsonic', SUBSONIC = 'subsonic',
} }
export const toServerType = (value?: string): ServerType | null => {
switch (value?.toLowerCase()) {
case ServerType.JELLYFIN:
return ServerType.JELLYFIN;
case ServerType.NAVIDROME:
return ServerType.NAVIDROME;
default:
return null;
}
};
export type ServerListItem = { export type ServerListItem = {
credential: string; credential: string;
features?: Record<string, number[]>; features?: Record<string, number[]>;
@ -144,6 +155,7 @@ export enum TableColumn {
BIT_RATE = 'bitRate', BIT_RATE = 'bitRate',
BPM = 'bpm', BPM = 'bpm',
CHANNELS = 'channels', CHANNELS = 'channels',
CODEC = 'codec',
COMMENT = 'comment', COMMENT = 'comment',
DATE_ADDED = 'dateAdded', DATE_ADDED = 'dateAdded',
DISC_NUMBER = 'discNumber', DISC_NUMBER = 'discNumber',
@ -213,3 +225,9 @@ export enum FontType {
} }
export type TitleTheme = 'dark' | 'light' | 'system'; export type TitleTheme = 'dark' | 'light' | 'system';
export enum AuthState {
INVALID = 'invalid',
LOADING = 'loading',
VALID = 'valid',
}