Merge branch 'development' into navidrome-version
This commit is contained in:
commit
cc6cad1d70
32 changed files with 531 additions and 345 deletions
3
.dockerignore
Normal file
3
.dockerignore
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.*
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -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="/"
|
||||||
|
|
|
@ -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
13
docker-compose.yaml
Normal 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
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
12
package-lock.json
generated
12
package-lock.json
generated
|
@ -12532,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": {
|
||||||
|
@ -30639,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": {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
1
settings.js.template
Normal file
1
settings.js.template
Normal 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};
|
|
@ -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",
|
||||||
|
@ -567,6 +568,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 +595,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 +630,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)",
|
||||||
|
|
|
@ -323,13 +323,16 @@ ipcMain.on('player-set-queue', async (_event, data: PlayerData, pause?: boolean)
|
||||||
await getMpvInstance()?.load(data.queue.next.streamUrl, 'append');
|
await getMpvInstance()?.load(data.queue.next.streamUrl, 'append');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pause) {
|
||||||
|
await getMpvInstance()?.pause();
|
||||||
|
} else if (pause === false) {
|
||||||
|
// Only force play if pause is explicitly false
|
||||||
|
await getMpvInstance()?.play();
|
||||||
|
}
|
||||||
} catch (err: NodeMpvError | any) {
|
} catch (err: NodeMpvError | any) {
|
||||||
mpvLog({ action: `Failed to set play queue` }, err);
|
mpvLog({ action: `Failed to set play queue` }, err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pause) {
|
|
||||||
getMpvInstance()?.pause();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Replaces the queue in position 1 to the given data
|
// Replaces the queue in position 1 to the given data
|
||||||
|
|
|
@ -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,19 +350,21 @@ 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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.on('closed', () => {
|
mainWindow.on('closed', () => {
|
||||||
|
ipcMain.removeHandler('window-clear-cache');
|
||||||
mainWindow = null;
|
mainWindow = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -552,6 +554,7 @@ app.on('window-all-closed', () => {
|
||||||
// Respect the OSX convention of having the application in memory even
|
// Respect the OSX convention of having the application in memory even
|
||||||
// after all windows have been closed
|
// after all windows have been closed
|
||||||
if (isMacOS()) {
|
if (isMacOS()) {
|
||||||
|
ipcMain.removeHandler('window-clear-cache');
|
||||||
mainWindow = null;
|
mainWindow = null;
|
||||||
} else {
|
} else {
|
||||||
app.quit();
|
app.quit();
|
||||||
|
@ -606,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);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 = [];
|
||||||
|
|
|
@ -89,8 +89,7 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref<any>) => {
|
||||||
|
|
||||||
if (playbackType === PlaybackType.LOCAL) {
|
if (playbackType === PlaybackType.LOCAL) {
|
||||||
mpvPlayer!.volume(volume);
|
mpvPlayer!.volume(volume);
|
||||||
mpvPlayer!.setQueue(playerData);
|
mpvPlayer!.setQueue(playerData, false);
|
||||||
mpvPlayer!.play();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
play();
|
play();
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -42,11 +42,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;
|
||||||
|
@ -82,8 +82,6 @@ export const FullScreenPlayerQueue = () => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
console.log('opacity', opacity);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GridContainer
|
<GridContainer
|
||||||
className="full-screen-player-queue-container"
|
className="full-screen-player-queue-container"
|
||||||
|
|
|
@ -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) })}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -175,8 +175,7 @@ export const useHandlePlayQueueAdd = () => {
|
||||||
|
|
||||||
if (playType === Play.NOW || !hadSong) {
|
if (playType === Play.NOW || !hadSong) {
|
||||||
mpvPlayer!.pause();
|
mpvPlayer!.pause();
|
||||||
mpvPlayer!.setQueue(playerData);
|
mpvPlayer!.setQueue(playerData, false);
|
||||||
mpvPlayer!.play();
|
|
||||||
} else {
|
} else {
|
||||||
mpvPlayer!.setQueueNext(playerData);
|
mpvPlayer!.setQueueNext(playerData);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import isElectron from 'is-electron';
|
||||||
import { nanoid } from 'nanoid/non-secure';
|
import { nanoid } from 'nanoid/non-secure';
|
||||||
import { AuthenticationResponse, ServerType } from '/@/renderer/api/types';
|
import { AuthenticationResponse, ServerType } from '/@/renderer/api/types';
|
||||||
import { useAuthStoreActions } from '/@/renderer/store';
|
import { useAuthStoreActions } from '/@/renderer/store';
|
||||||
|
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';
|
||||||
|
|
||||||
|
@ -32,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) => {
|
||||||
|
@ -61,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) {
|
||||||
|
@ -75,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,
|
||||||
|
@ -116,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',
|
||||||
|
@ -128,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',
|
||||||
|
|
|
@ -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} />;
|
||||||
|
|
|
@ -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} />;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
4
src/renderer/preload.d.ts
vendored
4
src/renderer/preload.d.ts
vendored
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -54,6 +54,37 @@ export enum Platform {
|
||||||
WINDOWS = 'windows',
|
WINDOWS = 'windows',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ServerType {
|
||||||
|
JELLYFIN = 'jellyfin',
|
||||||
|
NAVIDROME = 'navidrome',
|
||||||
|
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 = {
|
||||||
|
credential: string;
|
||||||
|
features?: Record<string, number[]>;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
ndCredential?: string;
|
||||||
|
savePassword?: boolean;
|
||||||
|
type: ServerType;
|
||||||
|
url: string;
|
||||||
|
userId: string | null;
|
||||||
|
username: string;
|
||||||
|
version?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export enum PlayerStatus {
|
export enum PlayerStatus {
|
||||||
PAUSED = 'paused',
|
PAUSED = 'paused',
|
||||||
PLAYING = 'playing',
|
PLAYING = 'playing',
|
||||||
|
@ -124,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',
|
||||||
|
|
Reference in a new issue